diff --git a/.github/workflows/build-tarball.yml b/.github/workflows/build-tarball.yml index 9d2cca47824331..95437096d61138 100644 --- a/.github/workflows/build-tarball.yml +++ b/.github/workflows/build-tarball.yml @@ -41,15 +41,13 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Environment Information - run: npx envinfo - name: Make tarball run: | export DISTTYPE=nightly @@ -71,13 +69,13 @@ jobs: SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} SCCACHE_IDLE_TIMEOUT: '0' steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: .github/actions/install-clang sparse-checkout-cone-mode: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Set up sccache @@ -85,8 +83,6 @@ jobs: uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 with: version: v0.16.0 - - name: Environment Information - run: npx envinfo - name: Download tarball uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c869c23cac354e..fea54e9b885249 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index 93ca3425be496e..896340ba3a820c 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -20,7 +20,7 @@ jobs: run: | echo "plusOne=$((${{ github.event.pull_request.commits }} + 1))" >> $GITHUB_OUTPUT echo "minusOne=$((${{ github.event.pull_request.commits }} - 1))" >> $GITHUB_OUTPUT - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: ${{ steps.nb-of-commits.outputs.plusOne }} persist-credentials: false diff --git a/.github/workflows/commit-queue.yml b/.github/workflows/commit-queue.yml index 8b5f37807b12af..7af71226871159 100644 --- a/.github/workflows/commit-queue.yml +++ b/.github/workflows/commit-queue.yml @@ -59,7 +59,7 @@ jobs: if: needs.get_mergeable_prs.outputs.numbers != '' runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # A personal token is required because pushing with GITHUB_TOKEN will # prevent commits from running CI after they land. It needs diff --git a/.github/workflows/coverage-linux-without-intl.yml b/.github/workflows/coverage-linux-without-intl.yml index 69fdc02c655549..e299ea9ec759cf 100644 --- a/.github/workflows/coverage-linux-without-intl.yml +++ b/.github/workflows/coverage-linux-without-intl.yml @@ -49,11 +49,11 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Set up sccache @@ -61,8 +61,6 @@ jobs: uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 with: version: v0.16.0 - - name: Environment Information - run: npx envinfo - name: Install gcovr run: pip install gcovr==7.2 - name: Configure diff --git a/.github/workflows/coverage-linux.yml b/.github/workflows/coverage-linux.yml index f4b7ed42968111..0d00463a466190 100644 --- a/.github/workflows/coverage-linux.yml +++ b/.github/workflows/coverage-linux.yml @@ -49,11 +49,11 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-24.04-arm steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Set up sccache @@ -61,8 +61,6 @@ jobs: uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 with: version: v0.16.0 - - name: Environment Information - run: npx envinfo - name: Install gcovr run: pip install gcovr==7.2 - name: Configure diff --git a/.github/workflows/coverage-windows.yml b/.github/workflows/coverage-windows.yml index 7d5afebad423fe..9977969750e8e9 100644 --- a/.github/workflows/coverage-windows.yml +++ b/.github/workflows/coverage-windows.yml @@ -69,17 +69,15 @@ jobs: if: github.event.pull_request.draft == false runs-on: windows-2025 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install deps run: choco install nasm - - name: Environment Information - run: npx envinfo - name: Build run: ./vcbuild.bat clang-cl # TODO(bcoe): investigate tests that fail with coverage enabled diff --git a/.github/workflows/create-release-proposal.yml b/.github/workflows/create-release-proposal.yml index ae4c4240a25ca7..883bb48b0df240 100644 --- a/.github/workflows/create-release-proposal.yml +++ b/.github/workflows/create-release-proposal.yml @@ -33,7 +33,7 @@ jobs: RELEASE_LINE: ${{ inputs.release-line }} runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ env.STAGING_BRANCH }} persist-credentials: false diff --git a/.github/workflows/daily-wpt-fyi.yml b/.github/workflows/daily-wpt-fyi.yml index 2470ce6913b7ac..4787d98fe93c5d 100644 --- a/.github/workflows/daily-wpt-fyi.yml +++ b/.github/workflows/daily-wpt-fyi.yml @@ -39,11 +39,9 @@ jobs: runs-on: ubuntu-24.04-arm steps: - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Environment Information - run: npx envinfo # install a version and checkout - name: Get latest nightly @@ -63,7 +61,7 @@ jobs: SHORT_SHA=$(node -p 'process.version.split(/-nightly\d{8}/)[1]') echo "NIGHTLY_REF=$(gh api /repos/nodejs/node/commits/$SHORT_SHA --jq '.sha')" >> $GITHUB_ENV - name: Checkout ${{ steps.setup-node.outputs.node-version }} - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false ref: ${{ env.NIGHTLY_REF || steps.setup-node.outputs.node-version }} @@ -79,7 +77,7 @@ jobs: run: rm -rf wpt working-directory: test/fixtures - name: Checkout epochs/daily WPT - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: web-platform-tests/wpt persist-credentials: false @@ -110,7 +108,7 @@ jobs: # version-specific checkout above overwrites .github/actions/ - name: Checkout undici WPT actions if: ${{ env.WPT_REPORT != '' }} - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: sparse-checkout: | .github/actions/undici-wpt-current diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 23ffe66fb244b1..6c4b2ec9a4beb0 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -16,15 +16,13 @@ jobs: if: github.repository == 'nodejs/node' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-24.04-arm steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Use Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_VERSION }} - - name: Environment Information - run: npx envinfo - name: Build lto run: | sudo apt-get update && sudo apt-get install ninja-build -y diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index baf07c2b68ec8d..6e5dfe73264641 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -24,15 +24,13 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Use Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_VERSION }} - - name: Environment Information - run: npx envinfo - name: Build run: NODE=$(command -v node) make doc-only - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/find-inactive-collaborators.yml b/.github/workflows/find-inactive-collaborators.yml index 35a0f4b1f5c77d..e6e0c7f54f69fa 100644 --- a/.github/workflows/find-inactive-collaborators.yml +++ b/.github/workflows/find-inactive-collaborators.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/find-inactive-tsc.yml b/.github/workflows/find-inactive-tsc.yml index 8deeaccf5dfdd3..c1804b78d378cb 100644 --- a/.github/workflows/find-inactive-tsc.yml +++ b/.github/workflows/find-inactive-tsc.yml @@ -20,13 +20,13 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false - name: Clone nodejs/TSC repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 path: .tmp diff --git a/.github/workflows/license-builder.yml b/.github/workflows/license-builder.yml index 36d303f9e121ab..a95ee13c45f4de 100644 --- a/.github/workflows/license-builder.yml +++ b/.github/workflows/license-builder.yml @@ -17,7 +17,7 @@ jobs: if: github.repository == 'nodejs/node' runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - run: ./tools/license-builder.sh # Run the license builder tool diff --git a/.github/workflows/lint-release-proposal.yml b/.github/workflows/lint-release-proposal.yml index 93c67df521d289..e8ffab178458c9 100644 --- a/.github/workflows/lint-release-proposal.yml +++ b/.github/workflows/lint-release-proposal.yml @@ -23,7 +23,7 @@ jobs: contents: read pull-requests: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false fetch-depth: 2 diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index fae3018e7abbce..640f5db474d1b8 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -25,37 +25,33 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Use Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_VERSION }} - - name: Environment Information - run: npx envinfo - name: Lint addon docs run: NODE=$(command -v node) make lint-addon-docs lint-cpp: if: github.event.pull_request.draft == false runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Environment Information - run: npx envinfo - name: Lint C/C++ files run: make lint-cpp format-cpp: if: ${{ github.event.pull_request && github.event.pull_request.draft == false && github.base_ref == github.event.repository.default_branch }} runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false @@ -64,11 +60,9 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Environment Information - run: npx envinfo - name: Format C/C++ files run: | make format-cpp-build @@ -93,15 +87,13 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Use Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_VERSION }} - - name: Environment Information - run: npx envinfo - name: Lint JavaScript files run: | set +e @@ -142,7 +134,7 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -156,11 +148,9 @@ jobs: *.py sparse-checkout-cone-mode: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Environment Information - run: npx envinfo - name: Lint Python run: | make lint-py-build @@ -169,7 +159,7 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -179,11 +169,9 @@ jobs: *.yaml sparse-checkout-cone-mode: false - name: Use Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Environment Information - run: npx envinfo - name: Lint YAML run: | make lint-yaml-build || true @@ -193,7 +181,7 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -209,7 +197,7 @@ jobs: # cannot use ubuntu-24.04-arm here because the docker image is x86 only runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: mszostok/codeowners-validator@7f3f5e28c6d7b8dfae5731e54ce2272ca384592f @@ -219,7 +207,7 @@ jobs: if: ${{ github.event.pull_request }} runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 2 persist-credentials: false @@ -234,7 +222,7 @@ jobs: lint-readme: runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a6a5b030220559..4bd4411530113f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -41,7 +41,7 @@ jobs: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/stress-test.yml b/.github/workflows/stress-test.yml index e3846cf4d387ac..fbc31ac3b435a3 100644 --- a/.github/workflows/stress-test.yml +++ b/.github/workflows/stress-test.yml @@ -54,7 +54,7 @@ jobs: SCCACHE_GHA_ENABLED: 'true' SCCACHE_IDLE_TIMEOUT: '0' steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false path: node @@ -67,7 +67,7 @@ jobs: if: runner.os == 'macOS' run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} allow-prereleases: true @@ -79,8 +79,6 @@ jobs: uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 with: version: v0.16.0 - - name: Environment Information - run: npx envinfo@7.21.0 # This is needed due to https://github.com/nodejs/build/issues/3878 - name: Cleanup if: runner.os == 'macOS' diff --git a/.github/workflows/test-internet.yml b/.github/workflows/test-internet.yml index 7f8f4cc30269ff..f520e98548d7c0 100644 --- a/.github/workflows/test-internet.yml +++ b/.github/workflows/test-internet.yml @@ -46,11 +46,11 @@ jobs: if: (github.event_name == 'schedule' && github.repository == 'nodejs/node') || (github.event.pull_request && github.event.pull_request.draft == false) runs-on: ubuntu-24.04-arm steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Set up sccache @@ -58,8 +58,6 @@ jobs: uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 with: version: v0.16.0 - - name: Environment Information - run: npx envinfo - name: Build run: make build-ci -j4 V=1 CONFIG_FLAGS="--error-on-warn" - name: Test Internet diff --git a/.github/workflows/test-linux-quic.yml b/.github/workflows/test-linux-quic.yml new file mode 100644 index 00000000000000..d4bb479dd9f001 --- /dev/null +++ b/.github/workflows/test-linux-quic.yml @@ -0,0 +1,69 @@ +name: Test Linux (with QUIC) + +on: + workflow_dispatch: + pull_request: + paths: + - .github/workflows/test-linux-quic.yml + - configure.py + - node.gyp + - node.gypi + - deps/ngtcp2/** + - deps/nghttp3/** + - deps/openssl/** + - src/quic/** + - src/node_bob* + - lib/quic.js + - lib/http3.js + - lib/internal/quic/** + - lib/stream/iter.js + - lib/internal/streams/iter/** + - test/cctest/test_quic_* + - test/common/quic* + - test/common/quic/** + - test/parallel/*quic* + - test/parallel/test-stream-iter-* + types: [opened, synchronize, reopened, ready_for_review] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + PYTHON_VERSION: '3.14' + FLAKY_TESTS: keep_retrying + CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang + CXX: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang++ + SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} + SCCACHE_IDLE_TIMEOUT: '0' + +permissions: + contents: read + +jobs: + test-quic: + if: github.event.pull_request.draft == false + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + path: node + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Set up sccache + if: github.base_ref == 'main' || github.ref_name == 'main' + uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 + with: + version: v0.16.0 + - name: Build + working-directory: node + run: make build-ci -j4 V=1 CONFIG_FLAGS="--error-on-warn --experimental-quic" + - name: Test + working-directory: node + run: make test-ci -j1 V=1 TEST_CI_ARGS="-p actions --measure-flakiness 9" + - name: Ensure running tests did not cause any change in the tree + working-directory: node + run: git add -A && git diff --name-only --exit-code --staged diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 40ac519c7e1cd8..40f653fdfe3332 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -54,12 +54,12 @@ jobs: matrix: os: [ubuntu-24.04, ubuntu-24.04-arm] steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false path: node - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Set up sccache @@ -67,8 +67,6 @@ jobs: uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 with: version: v0.16.0 - - name: Environment Information - run: npx envinfo - name: Build working-directory: node run: make build-ci -j4 V=1 CONFIG_FLAGS="--error-on-warn" diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index 352282a9bbacc9..501eb2280468a7 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -50,12 +50,12 @@ jobs: SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} SCCACHE_IDLE_TIMEOUT: '0' steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false path: node - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Set up Xcode ${{ env.XCODE_VERSION }} @@ -65,8 +65,6 @@ jobs: uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 with: version: v0.16.0 - - name: Environment Information - run: npx envinfo # The `npm ci` for this step fails a lot as part of the Test step. Run it # now so that we don't have to wait 2 hours for the Build step to pass # first before that failure happens. (And if there's something about diff --git a/.github/workflows/timezone-update.yml b/.github/workflows/timezone-update.yml index 02299e66b31ffc..94961f8ae6ed52 100644 --- a/.github/workflows/timezone-update.yml +++ b/.github/workflows/timezone-update.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Checkout nodejs/node - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Checkout unicode-org/icu-data - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: icu-data persist-credentials: false diff --git a/.github/workflows/tools.yml b/.github/workflows/tools.yml index aa99096658a304..c025ba1693fd38 100644 --- a/.github/workflows/tools.yml +++ b/.github/workflows/tools.yml @@ -286,7 +286,7 @@ jobs: tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true rm temp-output steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 if: github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id with: persist-credentials: false @@ -294,7 +294,7 @@ jobs: if: | (matrix.id == 'icu' || matrix.id == 'inspector_protocol') && (github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id) - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ env.PYTHON_VERSION }} - run: ${{ matrix.run }} diff --git a/.github/workflows/update-openssl.yml b/.github/workflows/update-openssl.yml index d87f6b3a4b5912..49f0006526b0a5 100644 --- a/.github/workflows/update-openssl.yml +++ b/.github/workflows/update-openssl.yml @@ -15,7 +15,7 @@ jobs: # Cannot use ubuntu-slim here because the update script requires Docker runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Check and download new OpenSSL version diff --git a/.github/workflows/update-v8.yml b/.github/workflows/update-v8.yml index e2794cc7726382..e365cf77017422 100644 --- a/.github/workflows/update-v8.yml +++ b/.github/workflows/update-v8.yml @@ -16,7 +16,7 @@ jobs: if: github.repository == 'nodejs/node' runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Cache node modules and update-v8 diff --git a/.github/workflows/update-wpt.yml b/.github/workflows/update-wpt.yml index 05b1503f3c3c1f..118ee4593e012e 100644 --- a/.github/workflows/update-wpt.yml +++ b/.github/workflows/update-wpt.yml @@ -27,7 +27,7 @@ jobs: subsystem: ${{ fromJSON(github.event.inputs.subsystems || '["url", "urlpattern", "WebCryptoAPI"]') }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/benchmark/fs/bench-filehandle-pipetosync.js b/benchmark/fs/bench-filehandle-pipetosync.js new file mode 100644 index 00000000000000..e62ade89472341 --- /dev/null +++ b/benchmark/fs/bench-filehandle-pipetosync.js @@ -0,0 +1,103 @@ +// Benchmark: pipeToSync with sync compression transforms. +// Measures fully synchronous file-to-file pipeline (no threadpool, no promises). +'use strict'; + +const common = require('../common.js'); +const fs = require('fs'); +const { openSync, closeSync, writeSync, unlinkSync } = fs; + +const tmpdir = require('../../test/common/tmpdir'); +tmpdir.refresh(); +const srcFile = tmpdir.resolve(`.removeme-sync-bench-src-${process.pid}`); +const dstFile = tmpdir.resolve(`.removeme-sync-bench-dst-${process.pid}`); + +const bench = common.createBenchmark(main, { + compression: ['gzip', 'deflate', 'brotli', 'zstd'], + filesize: [1024 * 1024, 16 * 1024 * 1024, 64 * 1024 * 1024], + n: [5], +}, { + flags: ['--experimental-stream-iter'], +}); + +function main({ compression, filesize, n }) { + // Create the fixture file with repeating lowercase ASCII + const chunk = Buffer.alloc(Math.min(filesize, 64 * 1024), 'abcdefghij'); + const fd = openSync(srcFile, 'w'); + let remaining = filesize; + while (remaining > 0) { + const toWrite = Math.min(remaining, chunk.length); + writeSync(fd, chunk, 0, toWrite); + remaining -= toWrite; + } + closeSync(fd); + + const { pipeToSync } = require('stream/iter'); + const { + compressGzipSync, + compressDeflateSync, + compressBrotliSync, + compressZstdSync, + } = require('zlib/iter'); + const { open } = fs.promises; + + const compressFactory = { + gzip: compressGzipSync, + deflate: compressDeflateSync, + brotli: compressBrotliSync, + zstd: compressZstdSync, + }[compression]; + + // Stateless uppercase transform (sync) + const upper = (chunks) => { + if (chunks === null) return null; + const out = new Array(chunks.length); + for (let j = 0; j < chunks.length; j++) { + const src = chunks[j]; + const buf = Buffer.allocUnsafe(src.length); + for (let i = 0; i < src.length; i++) { + const b = src[i]; + buf[i] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b; + } + out[j] = buf; + } + return out; + }; + + // Use a synchronous wrapper since pipeToSync is fully sync. + // We need FileHandle for pullSync/writer, so open async then run sync. + (async () => { + const srcFh = await open(srcFile, 'r'); + const dstFh = await open(dstFile, 'w'); + + // Warm up + runSync(srcFh, dstFh, upper, compressFactory, pipeToSync); + + // Reset file positions for the benchmark + await srcFh.close(); + await dstFh.close(); + + bench.start(); + let totalBytes = 0; + for (let i = 0; i < n; i++) { + const src = await open(srcFile, 'r'); + const dst = await open(dstFile, 'w'); + totalBytes += runSync(src, dst, upper, compressFactory, pipeToSync); + await src.close(); + await dst.close(); + } + bench.end(totalBytes / (1024 * 1024)); + + cleanup(); + })(); +} + +function runSync(srcFh, dstFh, upper, compressFactory, pipeToSync) { + const w = dstFh.writer(); + pipeToSync(srcFh.pullSync(upper, compressFactory()), w); + return w.endSync(); +} + +function cleanup() { + try { unlinkSync(srcFile); } catch { /* Ignore */ } + try { unlinkSync(dstFile); } catch { /* Ignore */ } +} diff --git a/benchmark/fs/bench-filehandle-pull-vs-webstream.js b/benchmark/fs/bench-filehandle-pull-vs-webstream.js new file mode 100644 index 00000000000000..9ced02e817db4d --- /dev/null +++ b/benchmark/fs/bench-filehandle-pull-vs-webstream.js @@ -0,0 +1,201 @@ +// Compare FileHandle.createReadStream() vs readableWebStream() vs pull() +// reading a large file through two transforms: uppercase then compress. +'use strict'; + +const common = require('../common.js'); +const fs = require('fs'); +const zlib = require('zlib'); +const { Transform, Writable, pipeline } = require('stream'); + +const tmpdir = require('../../test/common/tmpdir'); +tmpdir.refresh(); +const filename = tmpdir.resolve(`.removeme-benchmark-garbage-${process.pid}`); + +const bench = common.createBenchmark(main, { + api: ['classic', 'webstream', 'pull'], + compression: ['gzip', 'deflate', 'brotli', 'zstd'], + filesize: [1024 * 1024, 16 * 1024 * 1024, 64 * 1024 * 1024], + n: [5], +}, { + flags: ['--experimental-stream-iter'], + // Classic and webstream only support gzip (native zlib / CompressionStream). + // Brotli, deflate, zstd are pull-only via stream/iter transforms. + combinationFilter({ api, compression }) { + if (api === 'classic' && compression !== 'gzip') return false; + if (api === 'webstream' && compression !== 'gzip') return false; + return true; + }, +}); + +function main({ api, compression, filesize, n }) { + // Create the fixture file with repeating lowercase ASCII + const chunk = Buffer.alloc(Math.min(filesize, 64 * 1024), 'abcdefghij'); + const fd = fs.openSync(filename, 'w'); + let remaining = filesize; + while (remaining > 0) { + const toWrite = Math.min(remaining, chunk.length); + fs.writeSync(fd, chunk, 0, toWrite); + remaining -= toWrite; + } + fs.closeSync(fd); + + if (api === 'classic') { + benchClassic(n, filesize).then(() => cleanup()); + } else if (api === 'webstream') { + benchWebStream(n, filesize).then(() => cleanup()); + } else { + benchPull(n, filesize, compression).then(() => cleanup()); + } +} + +function cleanup() { + try { fs.unlinkSync(filename); } catch { /* ignore */ } +} + +// Stateless uppercase transform (shared by all paths) +function uppercaseChunk(chunk) { + const buf = Buffer.allocUnsafe(chunk.length); + for (let i = 0; i < chunk.length; i++) { + const b = chunk[i]; + buf[i] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b; + } + return buf; +} + +// --------------------------------------------------------------------------- +// Classic streams path: createReadStream -> Transform (upper) -> createGzip +// --------------------------------------------------------------------------- +async function benchClassic(n, filesize) { + await runClassic(); + + bench.start(); + let totalBytes = 0; + for (let i = 0; i < n; i++) { + totalBytes += await runClassic(); + } + bench.end(totalBytes / (1024 * 1024)); +} + +function runClassic() { + return new Promise((resolve, reject) => { + const rs = fs.createReadStream(filename); + + const upper = new Transform({ + transform(chunk, encoding, callback) { + callback(null, uppercaseChunk(chunk)); + }, + }); + + const gz = zlib.createGzip(); + + let totalBytes = 0; + const sink = new Writable({ + write(chunk, encoding, callback) { + totalBytes += chunk.length; + callback(); + }, + }); + + pipeline(rs, upper, gz, sink, (err) => { + if (err) reject(err); + else resolve(totalBytes); + }); + }); +} + +// --------------------------------------------------------------------------- +// WebStream path: readableWebStream -> TransformStream (upper) -> CompressionStream +// --------------------------------------------------------------------------- +async function benchWebStream(n, filesize) { + await runWebStream(); + + bench.start(); + let totalBytes = 0; + for (let i = 0; i < n; i++) { + totalBytes += await runWebStream(); + } + bench.end(totalBytes / (1024 * 1024)); +} + +async function runWebStream() { + const fh = await fs.promises.open(filename, 'r'); + try { + const rs = fh.readableWebStream(); + + const upper = new TransformStream({ + transform(chunk, controller) { + const buf = new Uint8Array(chunk.length); + for (let i = 0; i < chunk.length; i++) { + const b = chunk[i]; + buf[i] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b; + } + controller.enqueue(buf); + }, + }); + + const compress = new CompressionStream('gzip'); + const output = rs.pipeThrough(upper).pipeThrough(compress); + const reader = output.getReader(); + + let totalBytes = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + totalBytes += value.byteLength; + } + return totalBytes; + } finally { + await fh.close(); + } +} + +// --------------------------------------------------------------------------- +// Pull/iter path: pull() with uppercase transform + selected compression +// --------------------------------------------------------------------------- +async function benchPull(n, filesize, compression) { + const iter = require('zlib/iter'); + + const compressFactory = { + gzip: iter.compressGzip, + deflate: iter.compressDeflate, + brotli: iter.compressBrotli, + zstd: iter.compressZstd, + }[compression]; + + // Warm up + await runPull(compressFactory); + + bench.start(); + let totalBytes = 0; + for (let i = 0; i < n; i++) { + totalBytes += await runPull(compressFactory); + } + bench.end(totalBytes / (1024 * 1024)); +} + +async function runPull(compressFactory) { + const fh = await fs.promises.open(filename, 'r'); + try { + // Stateless transform: uppercase each chunk in the batch + const upper = (chunks) => { + if (chunks === null) return null; + const out = new Array(chunks.length); + for (let j = 0; j < chunks.length; j++) { + out[j] = uppercaseChunk(chunks[j]); + } + return out; + }; + + const readable = fh.pull(upper, compressFactory()); + + let totalBytes = 0; + for await (const chunks of readable) { + for (let i = 0; i < chunks.length; i++) { + totalBytes += chunks[i].byteLength; + } + } + return totalBytes; + } finally { + await fh.close(); + } +} diff --git a/benchmark/streams/iter-creation.js b/benchmark/streams/iter-creation.js new file mode 100644 index 00000000000000..c5495fc29f42ec --- /dev/null +++ b/benchmark/streams/iter-creation.js @@ -0,0 +1,99 @@ +// Object creation overhead benchmark. +// Measures the cost of constructing stream infrastructure (no data flow). +'use strict'; + +const common = require('../common.js'); +const { Readable, Writable, Transform, PassThrough } = require('stream'); +const { ok } = require('assert'); + +const bench = common.createBenchmark(main, { + api: ['classic', 'webstream', 'iter'], + type: ['readable', 'writable', 'transform', 'pair'], + n: [1e5], +}, { + flags: ['--experimental-stream-iter'], + // Iter has no standalone Transform class; transforms are plain functions. + combinationFilter: ({ api, type }) => + !(api === 'iter' && type === 'transform'), +}); + +function main({ api, type, n }) { + switch (api) { + case 'classic': + return benchClassic(type, n); + case 'webstream': + return benchWebStream(type, n); + case 'iter': + return benchIter(type, n); + } +} + +function benchClassic(type, n) { + let tmp; + bench.start(); + switch (type) { + case 'readable': + for (let i = 0; i < n; i++) tmp = new Readable({ read() {} }); + break; + case 'writable': + for (let i = 0; i < n; i++) tmp = new Writable({ write(c, e, cb) { cb(); } }); + break; + case 'transform': + for (let i = 0; i < n; i++) tmp = new Transform({ + transform(c, e, cb) { cb(null, c); }, + }); + break; + case 'pair': + for (let i = 0; i < n; i++) tmp = new PassThrough(); + break; + } + bench.end(n); + ok(tmp !== undefined); +} + +function benchWebStream(type, n) { + let tmp; + bench.start(); + switch (type) { + case 'readable': + for (let i = 0; i < n; i++) tmp = new ReadableStream({ pull() {} }); + break; + case 'writable': + for (let i = 0; i < n; i++) tmp = new WritableStream({ write() {} }); + break; + case 'transform': + for (let i = 0; i < n; i++) tmp = new TransformStream({ + transform(c, controller) { controller.enqueue(c); }, + }); + break; + case 'pair': { + // TransformStream gives a readable+writable pair + for (let i = 0; i < n; i++) tmp = new TransformStream(); + break; + } + } + bench.end(n); + ok(tmp !== undefined); +} + +function benchIter(type, n) { + const { push, from, duplex } = require('stream/iter'); + let tmp; + + bench.start(); + switch (type) { + case 'readable': + for (let i = 0; i < n; i++) tmp = from('x'); + break; + case 'writable': + // push() creates a writer+readable pair + for (let i = 0; i < n; i++) tmp = push(); + break; + case 'pair': + // duplex() creates a bidirectional channel pair + for (let i = 0; i < n; i++) tmp = duplex(); + break; + } + bench.end(n); + ok(tmp !== undefined); +} diff --git a/benchmark/streams/iter-file-read.js b/benchmark/streams/iter-file-read.js new file mode 100644 index 00000000000000..6d8139dc6de399 --- /dev/null +++ b/benchmark/streams/iter-file-read.js @@ -0,0 +1,107 @@ +// File reading throughput benchmark. +// Reads a real file through the three stream APIs. +'use strict'; + +const common = require('../common.js'); +const fs = require('fs'); +const { Writable, pipeline } = require('stream'); +const tmpdir = require('../../test/common/tmpdir'); + +tmpdir.refresh(); +const filename = tmpdir.resolve(`.removeme-bench-file-read-${process.pid}`); + +const bench = common.createBenchmark(main, { + api: ['classic', 'webstream', 'iter'], + filesize: [1024 * 1024, 16 * 1024 * 1024, 64 * 1024 * 1024], + n: [5], +}, { + flags: ['--experimental-stream-iter'], +}); + +function main({ api, filesize, n }) { + // Create fixture file + const chunk = Buffer.alloc(Math.min(filesize, 64 * 1024), 'abcdefghij'); + const fd = fs.openSync(filename, 'w'); + let remaining = filesize; + while (remaining > 0) { + const size = Math.min(remaining, chunk.length); + fs.writeSync(fd, chunk, 0, size); + remaining -= size; + } + fs.closeSync(fd); + + const totalOps = (filesize * n) / (1024 * 1024); + + switch (api) { + case 'classic': + return benchClassic(filesize, n, totalOps); + case 'webstream': + return benchWebStream(filesize, n, totalOps); + case 'iter': + return benchIter(filesize, n, totalOps); + } +} + +function benchClassic(filesize, n, totalOps) { + function run(cb) { + const r = fs.createReadStream(filename); + const w = new Writable({ write(data, enc, cb) { cb(); } }); + pipeline(r, w, cb); + } + + // Warmup + run(() => { + let i = 0; + bench.start(); + (function next() { + if (i++ >= n) { + fs.unlinkSync(filename); + return bench.end(totalOps); + } + run(next); + })(); + }); +} + +function benchWebStream(filesize, n, totalOps) { + const fsp = require('fs/promises'); + + async function run() { + const fh = await fsp.open(filename, 'r'); + const rs = fh.readableWebStream(); + const ws = new WritableStream({ write() {} }); + await rs.pipeTo(ws); + await fh.close(); + } + + (async () => { + // Warmup + await run(); + + bench.start(); + for (let i = 0; i < n; i++) await run(); + fs.unlinkSync(filename); + bench.end(totalOps); + })(); +} + +function benchIter(filesize, n, totalOps) { + const fsp = require('fs/promises'); + const { pipeTo } = require('stream/iter'); + + async function run() { + const fh = await fsp.open(filename, 'r'); + await pipeTo(fh.pull(), { write() {} }); + await fh.close(); + } + + (async () => { + // Warmup + await run(); + + bench.start(); + for (let i = 0; i < n; i++) await run(); + fs.unlinkSync(filename); + bench.end(totalOps); + })(); +} diff --git a/benchmark/streams/iter-from-batching.js b/benchmark/streams/iter-from-batching.js new file mode 100644 index 00000000000000..0e09537b9eae11 --- /dev/null +++ b/benchmark/streams/iter-from-batching.js @@ -0,0 +1,100 @@ +// Measures batching behavior for stream/iter from() and fromSync() +// with plain synchronous Uint8Array iterables. +'use strict'; + +const common = require('../common.js'); +const { closeSync, openSync, writeSync, writevSync } = require('fs'); +const { devNull } = require('os'); + +const bench = common.createBenchmark(main, { + method: ['from-first-batch', 'from-sync-writev'], + chunks: [256, 4096, 16384], + chunkSize: [16], + n: [100, 1000], +}, { + flags: ['--experimental-stream-iter'], + combinationFilter({ method, chunks, n }) { + if (n === 1) { + return true; + } + if (method === 'from-first-batch') { + return n === 1000; + } + return n === 100 && chunks !== 16384; + }, + test: { + chunks: 256, + chunkSize: 16, + n: 1, + }, +}); + +function main({ method, chunks, chunkSize, n }) { + switch (method) { + case 'from-first-batch': + return benchFromFirstBatch(chunks, chunkSize, n); + case 'from-sync-writev': + return benchFromSyncWritev(chunks, chunkSize, n); + } +} + +function* source(chunks, chunk) { + for (let i = 0; i < chunks; i++) { + yield chunk; + } +} + +function benchFromFirstBatch(chunks, chunkSize, n) { + const { from } = require('stream/iter'); + const chunk = new Uint8Array(chunkSize); + let seen = 0; + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) { + const iterator = from(source(chunks, chunk))[Symbol.asyncIterator](); + const { value, done } = await iterator.next(); + if (done || value.length === 0) { + throw new Error('expected a batch'); + } + seen += value.length; + } + bench.end(n); + if (seen === 0) { + throw new Error('expected chunks'); + } + })(); +} + +function benchFromSyncWritev(chunks, chunkSize, n) { + const { pipeToSync } = require('stream/iter'); + const chunk = new Uint8Array(chunkSize); + const expected = chunks * chunkSize * n; + let seen = 0; + let total = 0; + const fd = openSync(devNull, 'w'); + const writer = { + writeSync(chunk) { + writeSync(fd, chunk); + seen++; + }, + writevSync(batch) { + writevSync(fd, batch); + seen += batch.length; + }, + }; + + try { + bench.start(); + for (let i = 0; i < n; i++) { + total += pipeToSync(source(chunks, chunk), writer); + } + bench.end(chunks * n); + } finally { + closeSync(fd); + } + + if (total !== expected || seen !== chunks * n) { + throw new Error('unexpected chunk count'); + } +} diff --git a/benchmark/streams/iter-throughput-broadcast.js b/benchmark/streams/iter-throughput-broadcast.js new file mode 100644 index 00000000000000..c5bbac9306777e --- /dev/null +++ b/benchmark/streams/iter-throughput-broadcast.js @@ -0,0 +1,147 @@ +// Throughput benchmark: fan-out data to N consumers simultaneously. +// Classic streams use PassThrough + pipe, Web Streams use tee() chains, +// stream/iter uses broadcast() with push() consumers. +'use strict'; + +const common = require('../common.js'); +const { PassThrough, Writable } = require('stream'); + +const bench = common.createBenchmark(main, { + api: ['classic', 'webstream', 'iter'], + consumers: [1, 2, 4], + datasize: [1024 * 1024, 16 * 1024 * 1024], + n: [5], +}, { + flags: ['--experimental-stream-iter'], +}); + +const CHUNK_SIZE = 64 * 1024; + +function main({ api, consumers, datasize, n }) { + const chunk = Buffer.alloc(CHUNK_SIZE, 'abcdefghij'); + const totalOps = (datasize * n) / (1024 * 1024); + + switch (api) { + case 'classic': + return benchClassic(chunk, consumers, datasize, n, totalOps); + case 'webstream': + return benchWebStream(chunk, consumers, datasize, n, totalOps); + case 'iter': + return benchIter(chunk, consumers, datasize, n, totalOps); + } +} + +function benchClassic(chunk, numConsumers, datasize, n, totalOps) { + function run(cb) { + const source = new PassThrough(); + const sinks = []; + let finished = 0; + + for (let c = 0; c < numConsumers; c++) { + const w = new Writable({ write(data, enc, cb) { cb(); } }); + source.pipe(w); + w.on('finish', () => { if (++finished === numConsumers) cb(); }); + sinks.push(w); + } + + let remaining = datasize; + function write() { + let ok = true; + while (ok && remaining > 0) { + const size = Math.min(remaining, chunk.length); + remaining -= size; + const buf = size === chunk.length ? chunk : chunk.subarray(0, size); + ok = source.write(buf); + } + if (remaining > 0) { + source.once('drain', write); + } else { + source.end(); + } + } + write(); + } + + let i = 0; + bench.start(); + (function next() { + if (i++ >= n) return bench.end(totalOps); + run(next); + })(); +} + +function benchWebStream(chunk, numConsumers, datasize, n, totalOps) { + async function run() { + let remaining = datasize; + const rs = new ReadableStream({ + pull(controller) { + if (remaining <= 0) { controller.close(); return; } + const size = Math.min(remaining, chunk.length); + remaining -= size; + controller.enqueue( + size === chunk.length ? chunk : chunk.subarray(0, size)); + }, + }); + + // Chain tee() calls to get numConsumers branches. + // tee() gives 2; for 4 we tee twice at each level. + const branches = []; + if (numConsumers === 1) { + branches.push(rs); + } else { + const pending = [rs]; + while (branches.length + pending.length < numConsumers) { + const stream = pending.shift(); + const [a, b] = stream.tee(); + pending.push(a, b); + } + branches.push(...pending); + } + + const ws = () => new WritableStream({ write() {} }); + await Promise.all(branches.map((b) => b.pipeTo(ws()))); + } + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) await run(); + bench.end(totalOps); + })(); +} + +function benchIter(chunk, numConsumers, datasize, n, totalOps) { + const { broadcast } = require('stream/iter'); + + // No-op consumer: drain all batches without collecting + async function drain(source) { + // eslint-disable-next-line no-unused-vars + for await (const _ of source) { /* drain */ } + } + + async function run() { + const { writer, broadcast: bc } = broadcast(); + const consumers = []; + for (let c = 0; c < numConsumers; c++) { + consumers.push(drain(bc.push())); + } + + let remaining = datasize; + while (remaining > 0) { + const size = Math.min(remaining, chunk.length); + const buf = size === chunk.length ? chunk : chunk.subarray(0, size); + if (!writer.writeSync(buf)) { + await writer.write(buf); + } + remaining -= size; + } + writer.endSync(); + + await Promise.all(consumers); + } + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) await run(); + bench.end(totalOps); + })(); +} diff --git a/benchmark/streams/iter-throughput-compression.js b/benchmark/streams/iter-throughput-compression.js new file mode 100644 index 00000000000000..0c32ddc9afca98 --- /dev/null +++ b/benchmark/streams/iter-throughput-compression.js @@ -0,0 +1,104 @@ +// Throughput benchmark: gzip compress then decompress round-trip. +// Tests real-world compression performance across stream APIs. +'use strict'; + +const common = require('../common.js'); +const { Readable, Writable, pipeline } = require('stream'); +const zlib = require('zlib'); + +const bench = common.createBenchmark(main, { + api: ['classic', 'webstream', 'iter'], + datasize: [1024 * 1024, 16 * 1024 * 1024, 64 * 1024 * 1024], + n: [5], +}, { + flags: ['--experimental-stream-iter'], +}); + +const CHUNK_SIZE = 64 * 1024; + +function main({ api, datasize, n }) { + const chunk = Buffer.alloc(CHUNK_SIZE, 'abcdefghij'); + const totalOps = (datasize * n) / (1024 * 1024); + + switch (api) { + case 'classic': + return benchClassic(chunk, datasize, n, totalOps); + case 'webstream': + return benchWebStream(chunk, datasize, n, totalOps); + case 'iter': + return benchIter(chunk, datasize, n, totalOps); + } +} + +function benchClassic(chunk, datasize, n, totalOps) { + function run(cb) { + let remaining = datasize; + const r = new Readable({ + read() { + if (remaining <= 0) { this.push(null); return; } + const size = Math.min(remaining, chunk.length); + remaining -= size; + this.push(size === chunk.length ? chunk : chunk.subarray(0, size)); + }, + }); + const w = new Writable({ write(data, enc, cb) { cb(); } }); + pipeline(r, zlib.createGzip(), zlib.createGunzip(), w, cb); + } + + let i = 0; + bench.start(); + (function next() { + if (i++ >= n) return bench.end(totalOps); + run(next); + })(); +} + +function benchWebStream(chunk, datasize, n, totalOps) { + async function run() { + let remaining = datasize; + const rs = new ReadableStream({ + pull(controller) { + if (remaining <= 0) { controller.close(); return; } + const size = Math.min(remaining, chunk.length); + remaining -= size; + controller.enqueue( + size === chunk.length ? chunk : chunk.subarray(0, size)); + }, + }); + const ws = new WritableStream({ write() {} }); + await rs + .pipeThrough(new CompressionStream('gzip')) + .pipeThrough(new DecompressionStream('gzip')) + .pipeTo(ws); + } + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) await run(); + bench.end(totalOps); + })(); +} + +function benchIter(chunk, datasize, n, totalOps) { + const { pipeTo } = require('stream/iter'); + const { compressGzip, decompressGzip } = require('zlib/iter'); + + async function run() { + let remaining = datasize; + async function* source() { + while (remaining > 0) { + const size = Math.min(remaining, chunk.length); + remaining -= size; + yield [size === chunk.length ? chunk : chunk.subarray(0, size)]; + } + } + await pipeTo(source(), compressGzip(), decompressGzip(), + { write() {}, writeSync() { return true; } }); + } + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) await run(); + bench.end(totalOps); + })(); +} diff --git a/benchmark/streams/iter-throughput-identity.js b/benchmark/streams/iter-throughput-identity.js new file mode 100644 index 00000000000000..42640cf0f9d857 --- /dev/null +++ b/benchmark/streams/iter-throughput-identity.js @@ -0,0 +1,132 @@ +// Throughput benchmark: raw data flow from source to consumer, no transforms. +// Compares Node.js classic streams, Web Streams, and stream/iter. +'use strict'; + +const common = require('../common.js'); +const { Readable, Writable, pipeline } = require('stream'); + +const bench = common.createBenchmark(main, { + api: ['classic', 'webstream', 'iter', 'iter-sync'], + datasize: [1024 * 1024, 16 * 1024 * 1024, 64 * 1024 * 1024], + n: [5], +}, { + flags: ['--experimental-stream-iter'], +}); + +const CHUNK_SIZE = 64 * 1024; + +function main({ api, datasize, n }) { + const chunk = Buffer.alloc(CHUNK_SIZE, 'abcdefghij'); + const totalOps = (datasize * n) / (1024 * 1024); // MB + + switch (api) { + case 'classic': + return benchClassic(chunk, datasize, n, totalOps); + case 'webstream': + return benchWebStream(chunk, datasize, n, totalOps); + case 'iter': + return benchIter(chunk, datasize, n, totalOps); + case 'iter-sync': + return benchIterSync(chunk, datasize, n, totalOps); + } +} + +function benchClassic(chunk, datasize, n, totalOps) { + let remaining = 0; + + function run(cb) { + remaining = datasize; + const r = new Readable({ + read() { + if (remaining <= 0) { + this.push(null); + return; + } + const size = Math.min(remaining, chunk.length); + remaining -= size; + this.push(size === chunk.length ? chunk : chunk.subarray(0, size)); + }, + }); + const w = new Writable({ + write(data, enc, cb) { cb(); }, + }); + pipeline(r, w, cb); + } + + let i = 0; + bench.start(); + (function next() { + if (i++ >= n) return bench.end(totalOps); + run(next); + })(); +} + +function benchWebStream(chunk, datasize, n, totalOps) { + async function run() { + let remaining = datasize; + const rs = new ReadableStream({ + pull(controller) { + if (remaining <= 0) { + controller.close(); + return; + } + const size = Math.min(remaining, chunk.length); + remaining -= size; + controller.enqueue( + size === chunk.length ? chunk : chunk.subarray(0, size)); + }, + }); + const ws = new WritableStream({ + write() {}, + }); + await rs.pipeTo(ws); + } + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) await run(); + bench.end(totalOps); + })(); +} + +function benchIter(chunk, datasize, n, totalOps) { + const { pipeTo } = require('stream/iter'); + + async function run() { + let remaining = datasize; + async function* source() { + while (remaining > 0) { + const size = Math.min(remaining, chunk.length); + remaining -= size; + yield [size === chunk.length ? chunk : chunk.subarray(0, size)]; + } + } + // Drain to no-op sink, matching classic/webstream behavior + await pipeTo(source(), { write() {}, writeSync() { return true; } }); + } + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) await run(); + bench.end(totalOps); + })(); +} + +function benchIterSync(chunk, datasize, n, totalOps) { + const { pipeToSync } = require('stream/iter'); + + bench.start(); + for (let i = 0; i < n; i++) { + let remaining = datasize; + function* source() { + while (remaining > 0) { + const size = Math.min(remaining, chunk.length); + remaining -= size; + yield [size === chunk.length ? chunk : chunk.subarray(0, size)]; + } + } + // Drain to no-op sink, matching other benchmarks + pipeToSync(source(), { writeSync() {} }); + } + bench.end(totalOps); +} diff --git a/benchmark/streams/iter-throughput-pipeto.js b/benchmark/streams/iter-throughput-pipeto.js new file mode 100644 index 00000000000000..819d5e22a8a272 --- /dev/null +++ b/benchmark/streams/iter-throughput-pipeto.js @@ -0,0 +1,146 @@ +// Throughput benchmark: pipe source to a no-op sink (write-only destination). +// Measures pure pipe throughput without consumer-side collection overhead. +'use strict'; + +const common = require('../common.js'); +const { Readable, Writable, pipeline } = require('stream'); + +const bench = common.createBenchmark(main, { + api: ['classic', 'webstream', 'iter', 'iter-sync-source', 'iter-sync'], + datasize: [1024 * 1024, 16 * 1024 * 1024, 64 * 1024 * 1024], + n: [5], +}, { + flags: ['--experimental-stream-iter'], +}); + +const CHUNK_SIZE = 64 * 1024; + +function main({ api, datasize, n }) { + const chunk = Buffer.alloc(CHUNK_SIZE, 'abcdefghij'); + const totalOps = (datasize * n) / (1024 * 1024); + + switch (api) { + case 'classic': + return benchClassic(chunk, datasize, n, totalOps); + case 'webstream': + return benchWebStream(chunk, datasize, n, totalOps); + case 'iter': + return benchIter(chunk, datasize, n, totalOps); + case 'iter-sync-source': + return benchIterSyncSource(chunk, datasize, n, totalOps); + case 'iter-sync': + return benchIterSync(chunk, datasize, n, totalOps); + } +} + +function benchClassic(chunk, datasize, n, totalOps) { + function run(cb) { + let remaining = datasize; + const r = new Readable({ + read() { + if (remaining <= 0) { this.push(null); return; } + const size = Math.min(remaining, chunk.length); + remaining -= size; + this.push(size === chunk.length ? chunk : chunk.subarray(0, size)); + }, + }); + const w = new Writable({ write(data, enc, cb) { cb(); } }); + pipeline(r, w, cb); + } + + let i = 0; + bench.start(); + (function next() { + if (i++ >= n) return bench.end(totalOps); + run(next); + })(); +} + +function benchWebStream(chunk, datasize, n, totalOps) { + async function run() { + let remaining = datasize; + const rs = new ReadableStream({ + pull(controller) { + if (remaining <= 0) { controller.close(); return; } + const size = Math.min(remaining, chunk.length); + remaining -= size; + controller.enqueue( + size === chunk.length ? chunk : chunk.subarray(0, size)); + }, + }); + const ws = new WritableStream({ write() {} }); + await rs.pipeTo(ws); + } + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) await run(); + bench.end(totalOps); + })(); +} + +function benchIter(chunk, datasize, n, totalOps) { + const { pipeTo } = require('stream/iter'); + + async function run() { + let remaining = datasize; + async function* source() { + while (remaining > 0) { + const size = Math.min(remaining, chunk.length); + remaining -= size; + yield [size === chunk.length ? chunk : chunk.subarray(0, size)]; + } + } + // Provide writeSync for the sync fast path in pipeTo + const writer = { write() {}, writeSync() { return true; } }; + await pipeTo(source(), writer); + } + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) await run(); + bench.end(totalOps); + })(); +} + +function benchIterSyncSource(chunk, datasize, n, totalOps) { + const { pipeTo } = require('stream/iter'); + + async function run() { + let remaining = datasize; + function* source() { + while (remaining > 0) { + const size = Math.min(remaining, chunk.length); + remaining -= size; + yield size === chunk.length ? chunk : chunk.subarray(0, size); + } + } + const writer = { write() {}, writeSync() { return true; } }; + await pipeTo(source(), writer); + } + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) await run(); + bench.end(totalOps); + })(); +} + +function benchIterSync(chunk, datasize, n, totalOps) { + const { pipeToSync } = require('stream/iter'); + + bench.start(); + for (let i = 0; i < n; i++) { + let remaining = datasize; + function* source() { + while (remaining > 0) { + const size = Math.min(remaining, chunk.length); + remaining -= size; + yield [size === chunk.length ? chunk : chunk.subarray(0, size)]; + } + } + const writer = { writeSync() {} }; + pipeToSync(source(), writer); + } + bench.end(totalOps); +} diff --git a/benchmark/streams/iter-throughput-share.js b/benchmark/streams/iter-throughput-share.js new file mode 100644 index 00000000000000..a0383172c04140 --- /dev/null +++ b/benchmark/streams/iter-throughput-share.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + consumers: [2, 8, 32], + batches: [1e4], + backpressure: ['block'], + n: [5], +}, { + flags: ['--experimental-stream-iter'], +}); + +async function main({ consumers, batches, backpressure, n }) { + const { share, array } = require('stream/iter'); + const chunk = Buffer.alloc(1024); + const totalOps = batches * consumers * n; + + async function* source() { + for (let i = 0; i < batches; i++) { + yield [chunk]; + } + } + + bench.start(); + for (let i = 0; i < n; i++) { + const shared = share(source(), { highWaterMark: 64, backpressure }); + const readers = Array.from({ length: consumers }, () => array(shared.pull())); + await Promise.all(readers); + } + bench.end(totalOps); +} diff --git a/benchmark/streams/iter-throughput-transform.js b/benchmark/streams/iter-throughput-transform.js new file mode 100644 index 00000000000000..b251aea4af2752 --- /dev/null +++ b/benchmark/streams/iter-throughput-transform.js @@ -0,0 +1,146 @@ +// Throughput benchmark: data flow through a single stateless transform. +// Uses buffer copy (allocate + memcpy) so pipeline overhead is measurable. +'use strict'; + +const common = require('../common.js'); +const { Readable, Transform, Writable, pipeline } = require('stream'); + +const bench = common.createBenchmark(main, { + api: ['classic', 'webstream', 'iter', 'iter-sync'], + datasize: [1024 * 1024, 16 * 1024 * 1024, 64 * 1024 * 1024], + n: [5], +}, { + flags: ['--experimental-stream-iter'], +}); + +const CHUNK_SIZE = 64 * 1024; + +// Buffer copy transform: allocate + memcpy. Cheap enough that pipeline +// overhead is a measurable fraction of total time, but non-trivial (new +// buffer per chunk, so it's a real transform that produces new data). +function copyBuf(buf) { + return Buffer.copyBytesFrom(buf); +} + +function main({ api, datasize, n }) { + const chunk = Buffer.alloc(CHUNK_SIZE, 'abcdefghij'); + const totalOps = (datasize * n) / (1024 * 1024); + + switch (api) { + case 'classic': + return benchClassic(chunk, datasize, n, totalOps); + case 'webstream': + return benchWebStream(chunk, datasize, n, totalOps); + case 'iter': + return benchIter(chunk, datasize, n, totalOps); + case 'iter-sync': + return benchIterSync(chunk, datasize, n, totalOps); + } +} + +function benchClassic(chunk, datasize, n, totalOps) { + function run(cb) { + let remaining = datasize; + const r = new Readable({ + read() { + if (remaining <= 0) { this.push(null); return; } + const size = Math.min(remaining, chunk.length); + remaining -= size; + this.push(size === chunk.length ? chunk : chunk.subarray(0, size)); + }, + }); + const t = new Transform({ + transform(data, enc, cb) { + cb(null, copyBuf(data)); + }, + }); + const w = new Writable({ write(data, enc, cb) { cb(); } }); + pipeline(r, t, w, cb); + } + + let i = 0; + bench.start(); + (function next() { + if (i++ >= n) return bench.end(totalOps); + run(next); + })(); +} + +function benchWebStream(chunk, datasize, n, totalOps) { + async function run() { + let remaining = datasize; + const rs = new ReadableStream({ + pull(controller) { + if (remaining <= 0) { controller.close(); return; } + const size = Math.min(remaining, chunk.length); + remaining -= size; + controller.enqueue( + size === chunk.length ? chunk : chunk.subarray(0, size)); + }, + }); + const ts = new TransformStream({ + transform(c, controller) { + controller.enqueue(copyBuf(c)); + }, + }); + const ws = new WritableStream({ write() {} }); + await rs.pipeThrough(ts).pipeTo(ws); + } + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) await run(); + bench.end(totalOps); + })(); +} + +function benchIter(chunk, datasize, n, totalOps) { + const { pipeTo } = require('stream/iter'); + + const upper = (chunks) => { + if (chunks === null) return null; + return chunks.map((c) => copyBuf(c)); + }; + + async function run() { + let remaining = datasize; + async function* source() { + while (remaining > 0) { + const size = Math.min(remaining, chunk.length); + remaining -= size; + yield [size === chunk.length ? chunk : chunk.subarray(0, size)]; + } + } + await pipeTo(source(), upper, + { write() {}, writeSync() { return true; } }); + } + + (async () => { + bench.start(); + for (let i = 0; i < n; i++) await run(); + bench.end(totalOps); + })(); +} + +function benchIterSync(chunk, datasize, n, totalOps) { + const { pipeToSync } = require('stream/iter'); + + const upper = (chunks) => { + if (chunks === null) return null; + return chunks.map((c) => copyBuf(c)); + }; + + bench.start(); + for (let i = 0; i < n; i++) { + let remaining = datasize; + function* source() { + while (remaining > 0) { + const size = Math.min(remaining, chunk.length); + remaining -= size; + yield [size === chunk.length ? chunk : chunk.subarray(0, size)]; + } + } + pipeToSync(source(), upper, { writeSync() {} }); + } + bench.end(totalOps); +} diff --git a/configure.py b/configure.py index a9910d038f01cb..fca8f2cdf8caf4 100755 --- a/configure.py +++ b/configure.py @@ -2065,6 +2065,8 @@ def without_ssl_error(option): configure_library('openssl', o) + o['variables']['openssl_version'] = get_openssl_version() + def configure_sqlite(o): o['variables']['node_use_sqlite'] = b(not options.without_sqlite) if options.without_sqlite: diff --git a/deps/nghttp2/nghttp2.gyp b/deps/nghttp2/nghttp2.gyp index 2601342f9e6f75..b83378d972e8bc 100644 --- a/deps/nghttp2/nghttp2.gyp +++ b/deps/nghttp2/nghttp2.gyp @@ -67,6 +67,17 @@ 'sources': [ '<@(nghttp2_sources)', ] + }, + { + 'target_name': 'sfparse', + 'type': 'static_library', + 'include_dirs': ['lib'], + 'sources': [ + 'lib/sfparse.c', + ], + 'direct_dependent_settings': { + 'include_dirs': [ 'lib/includes' ] + } } ] } diff --git a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h index c38f5e8dad4b26..83999a34d17b81 100644 --- a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h +++ b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h @@ -86,8 +86,9 @@ typedef ptrdiff_t nghttp3_ssize; * * :type:`nghttp3_tstamp` is a timestamp with nanosecond resolution. * ``UINT64_MAX`` is an invalid value, and it is often used to - * indicate that no value is set. This type is available since - * v1.12.0. + * indicate that no value is set. + * + * .. version-added:: 1.12.0 */ typedef uint64_t nghttp3_tstamp; @@ -96,8 +97,9 @@ typedef uint64_t nghttp3_tstamp; * * :type:`nghttp3_duration` is a period of time in nanosecond * resolution. ``UINT64_MAX`` is an invalid value, and it is often - * used to indicate that no value is set. This type is available - * since v1.12.0. + * used to indicate that no value is set. + * + * .. version-added:: 1.12.0. */ typedef uint64_t nghttp3_duration; @@ -105,7 +107,9 @@ typedef uint64_t nghttp3_duration; * @macro * * :macro:`NGHTTP3_NANOSECONDS` is a count of tick which corresponds - * to 1 nanosecond. This macro is available since v1.12.0. + * to 1 nanosecond. + * + * .. version-added:: 1.12.0 */ #define NGHTTP3_NANOSECONDS ((nghttp3_duration)1ULL) @@ -113,7 +117,9 @@ typedef uint64_t nghttp3_duration; * @macro * * :macro:`NGHTTP3_MICROSECONDS` is a count of tick which corresponds - * to 1 microsecond. This macro is available since v1.12.0. + * to 1 microsecond. + * + * .. version-added:: 1.12.0 */ #define NGHTTP3_MICROSECONDS ((nghttp3_duration)(1000ULL * NGHTTP3_NANOSECONDS)) @@ -121,7 +127,9 @@ typedef uint64_t nghttp3_duration; * @macro * * :macro:`NGHTTP3_MILLISECONDS` is a count of tick which corresponds - * to 1 millisecond. This macro is available since v1.12.0. + * to 1 millisecond. + * + * .. version-added:: 1.12.0 */ #define NGHTTP3_MILLISECONDS \ ((nghttp3_duration)(1000ULL * NGHTTP3_MICROSECONDS)) @@ -130,7 +138,9 @@ typedef uint64_t nghttp3_duration; * @macro * * :macro:`NGHTTP3_SECONDS` is a count of tick which corresponds to 1 - * second. This macro is available since v1.12.0. + * second. + * + * .. version-added:: 1.12.0 */ #define NGHTTP3_SECONDS ((nghttp3_duration)(1000ULL * NGHTTP3_MILLISECONDS)) @@ -429,42 +439,42 @@ typedef uint64_t nghttp3_duration; * :macro:`NGHTTP3_H3_MISSING_SETTINGS` is HTTP/3 application error * code ``H3_MISSING_SETTINGS``. */ -#define NGHTTP3_H3_MISSING_SETTINGS 0x010a +#define NGHTTP3_H3_MISSING_SETTINGS 0x010A /** * @macro * * :macro:`NGHTTP3_H3_REQUEST_REJECTED` is HTTP/3 application error * code ``H3_REQUEST_REJECTED``. */ -#define NGHTTP3_H3_REQUEST_REJECTED 0x010b +#define NGHTTP3_H3_REQUEST_REJECTED 0x010B /** * @macro * * :macro:`NGHTTP3_H3_REQUEST_CANCELLED` is HTTP/3 application error * code ``H3_REQUEST_CANCELLED``. */ -#define NGHTTP3_H3_REQUEST_CANCELLED 0x010c +#define NGHTTP3_H3_REQUEST_CANCELLED 0x010C /** * @macro * * :macro:`NGHTTP3_H3_REQUEST_INCOMPLETE` is HTTP/3 application error * code ``H3_REQUEST_INCOMPLETE``. */ -#define NGHTTP3_H3_REQUEST_INCOMPLETE 0x010d +#define NGHTTP3_H3_REQUEST_INCOMPLETE 0x010D /** * @macro * * :macro:`NGHTTP3_H3_MESSAGE_ERROR` is HTTP/3 application error code * ``H3_MESSAGE_ERROR``. */ -#define NGHTTP3_H3_MESSAGE_ERROR 0x010e +#define NGHTTP3_H3_MESSAGE_ERROR 0x010E /** * @macro * * :macro:`NGHTTP3_H3_CONNECT_ERROR` is HTTP/3 application error code * ``H3_CONNECT_ERROR``. */ -#define NGHTTP3_H3_CONNECT_ERROR 0x010f +#define NGHTTP3_H3_CONNECT_ERROR 0x010F /** * @macro * @@ -753,7 +763,7 @@ NGHTTP3_EXTERN void nghttp3_buf_reset(nghttp3_buf *buf); * * :macro:`NGHTTP3_NV_FLAG_NONE` indicates no flag set. */ -#define NGHTTP3_NV_FLAG_NONE 0x00u +#define NGHTTP3_NV_FLAG_NONE 0x00U /** * @macro @@ -762,7 +772,7 @@ NGHTTP3_EXTERN void nghttp3_buf_reset(nghttp3_buf *buf); * pair must not be indexed. Other implementation calls this bit as * "sensitive". */ -#define NGHTTP3_NV_FLAG_NEVER_INDEX 0x01u +#define NGHTTP3_NV_FLAG_NEVER_INDEX 0x01U /** * @macro @@ -771,7 +781,7 @@ NGHTTP3_EXTERN void nghttp3_buf_reset(nghttp3_buf *buf); * If this flag is set, the library does not make a copy of field * name. This could improve performance. */ -#define NGHTTP3_NV_FLAG_NO_COPY_NAME 0x02u +#define NGHTTP3_NV_FLAG_NO_COPY_NAME 0x02U /** * @macro @@ -780,7 +790,7 @@ NGHTTP3_EXTERN void nghttp3_buf_reset(nghttp3_buf *buf); * application. If this flag is set, the library does not make a copy * of field value. This could improve performance. */ -#define NGHTTP3_NV_FLAG_NO_COPY_VALUE 0x04u +#define NGHTTP3_NV_FLAG_NO_COPY_VALUE 0x04U /** * @macro @@ -790,7 +800,7 @@ NGHTTP3_EXTERN void nghttp3_buf_reset(nghttp3_buf *buf); * a hint, and QPACK encoder might not encode the field in various * reasons. */ -#define NGHTTP3_NV_FLAG_TRY_INDEX 0x08u +#define NGHTTP3_NV_FLAG_TRY_INDEX 0x08U /** * @struct @@ -1156,8 +1166,9 @@ typedef struct nghttp3_qpack_nv { * * :type:`nghttp3_qpack_indexing_strat` defines the QPACK dynamic * table indexing strategies for fields not defined in - * :type:`nghttp3_qpack_token`. This type is available since v1.13.0. - + * :type:`nghttp3_qpack_token`. + * + * .. version-added:: 1.13.0 */ typedef enum nghttp3_qpack_indexing_strat { /** @@ -1165,14 +1176,16 @@ typedef enum nghttp3_qpack_indexing_strat { * fields not defined in :type:`nghttp3_qpack_token`. This is the * default strategy. You can still use * :macro:`NGHTTP3_NV_FLAG_TRY_INDEX` to index a particular field. - * This enum is available since v1.13.0. + * + * .. version-added:: 1.13.0 */ NGHTTP3_QPACK_INDEXING_STRAT_NONE, /** * :enum:`NGHTTP3_QPACK_INDEXING_STRAT_EAGER` indexes all fields not * defined in :type:`nghttp3_qpack_token`. Please note that QPACK - * encoder might not index the field in various reasons. This enum - * is available since v1.13.0. + * encoder might not index the field in various reasons. + * + * .. version-added:: 1.13.0 */ NGHTTP3_QPACK_INDEXING_STRAT_EAGER } nghttp3_qpack_indexing_strat; @@ -1233,7 +1246,7 @@ NGHTTP3_EXTERN int nghttp3_qpack_encoder_new(nghttp3_qpack_encoder **pencoder, * :macro:`NGHTTP3_ERR_NOMEM` * Out of memory. * - * This function is available since v1.11.0. + * .. version-added:: 1.11.0 */ NGHTTP3_EXTERN int nghttp3_qpack_encoder_new2(nghttp3_qpack_encoder **pencoder, size_t hard_max_dtable_capacity, @@ -1329,9 +1342,9 @@ nghttp3_qpack_encoder_set_max_blocked_streams(nghttp3_qpack_encoder *encoder, * @function * * `nghttp3_qpack_encoder_set_indexing_strat` sets the dynamic table - * indexing strategy |strat| to |encoder|. This function is available - * since v1.13.0. - + * indexing strategy |strat| to |encoder|. + * + * .. version-added:: 1.13.0 */ NGHTTP3_EXTERN void nghttp3_qpack_encoder_set_indexing_strat(nghttp3_qpack_encoder *encoder, @@ -1352,12 +1365,28 @@ nghttp3_qpack_encoder_ack_everything(nghttp3_qpack_encoder *encoder); /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.16.0 + * Use `nghttp3_qpack_encoder_get_num_blocked_streams2` instead. + * * `nghttp3_qpack_encoder_get_num_blocked_streams` returns the number * of streams which are potentially blocked at decoder side. */ NGHTTP3_EXTERN size_t nghttp3_qpack_encoder_get_num_blocked_streams(nghttp3_qpack_encoder *encoder); +/** + * @function + * + * `nghttp3_qpack_encoder_get_num_blocked_streams2` returns the number + * of streams which are potentially blocked at decoder side. + * + * .. version-added:: 1.16.0 + */ +NGHTTP3_EXTERN size_t nghttp3_qpack_encoder_get_num_blocked_streams2( + const nghttp3_qpack_encoder *encoder); + /** * @struct * @@ -1401,12 +1430,26 @@ nghttp3_qpack_stream_context_del(nghttp3_qpack_stream_context *sctx); /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.16.0 + * Use `nghttp3_qpack_stream_context_get_ricnt2` instead. + * * `nghttp3_qpack_stream_context_get_ricnt` returns required insert * count. */ NGHTTP3_EXTERN uint64_t nghttp3_qpack_stream_context_get_ricnt(nghttp3_qpack_stream_context *sctx); +/** + * @function + * + * `nghttp3_qpack_stream_context_get_ricnt2` returns required insert + * count. + */ +NGHTTP3_EXTERN uint64_t nghttp3_qpack_stream_context_get_ricnt2( + const nghttp3_qpack_stream_context *sctx); + /** * @function * @@ -1495,7 +1538,7 @@ nghttp3_qpack_decoder_get_icnt(const nghttp3_qpack_decoder *decoder); * * :macro:`NGHTTP3_QPACK_DECODE_FLAG_NONE` indicates that no flag set. */ -#define NGHTTP3_QPACK_DECODE_FLAG_NONE 0x00u +#define NGHTTP3_QPACK_DECODE_FLAG_NONE 0x00U /** * @macro @@ -1503,7 +1546,7 @@ nghttp3_qpack_decoder_get_icnt(const nghttp3_qpack_decoder *decoder); * :macro:`NGHTTP3_QPACK_DECODE_FLAG_EMIT` indicates that an HTTP * field is successfully decoded. */ -#define NGHTTP3_QPACK_DECODE_FLAG_EMIT 0x01u +#define NGHTTP3_QPACK_DECODE_FLAG_EMIT 0x01U /** * @macro @@ -1511,7 +1554,7 @@ nghttp3_qpack_decoder_get_icnt(const nghttp3_qpack_decoder *decoder); * :macro:`NGHTTP3_QPACK_DECODE_FLAG_FINAL` indicates that an entire * HTTP field section has been decoded. */ -#define NGHTTP3_QPACK_DECODE_FLAG_FINAL 0x02u +#define NGHTTP3_QPACK_DECODE_FLAG_FINAL 0x02U /** * @macro @@ -1519,7 +1562,7 @@ nghttp3_qpack_decoder_get_icnt(const nghttp3_qpack_decoder *decoder); * :macro:`NGHTTP3_QPACK_DECODE_FLAG_BLOCKED` indicates that decoding * has been blocked. */ -#define NGHTTP3_QPACK_DECODE_FLAG_BLOCKED 0x04u +#define NGHTTP3_QPACK_DECODE_FLAG_BLOCKED 0x04U /** * @function @@ -1579,8 +1622,8 @@ NGHTTP3_EXTERN nghttp3_ssize nghttp3_qpack_decoder_read_request( * * The caller must ensure that `nghttp3_buf_left(dbuf) * ` >= - * `nghttp3_qpack_decoder_get_decoder_streamlen(decoder) - * `. + * `nghttp3_qpack_decoder_get_decoder_streamlen2(decoder) + * `. */ NGHTTP3_EXTERN void nghttp3_qpack_decoder_write_decoder(nghttp3_qpack_decoder *decoder, @@ -1589,12 +1632,28 @@ nghttp3_qpack_decoder_write_decoder(nghttp3_qpack_decoder *decoder, /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.16.0 + * Use `nghttp3_qpack_decoder_get_decoder_streamlen2` instead. + * * `nghttp3_qpack_decoder_get_decoder_streamlen` returns the length of * decoder stream that is currently pending. */ NGHTTP3_EXTERN size_t nghttp3_qpack_decoder_get_decoder_streamlen(nghttp3_qpack_decoder *decoder); +/** + * @function + * + * `nghttp3_qpack_decoder_get_decoder_streamlen2` returns the length + * of decoder stream that is currently pending. + * + * .. version-added:: 1.16.0 + */ +NGHTTP3_EXTERN size_t nghttp3_qpack_decoder_get_decoder_streamlen2( + const nghttp3_qpack_decoder *decoder); + /** * @function * @@ -1714,7 +1773,7 @@ NGHTTP3_EXTERN void nghttp3_set_debug_vprintf_callback( * by a server when it initiates graceful shutdown of the connection * via `nghttp3_conn_submit_shutdown_notice`. */ -#define NGHTTP3_SHUTDOWN_NOTICE_STREAM_ID ((1ull << 62) - 4) +#define NGHTTP3_SHUTDOWN_NOTICE_STREAM_ID ((1ULL << 62) - 4) /** * @macro @@ -1724,7 +1783,7 @@ NGHTTP3_EXTERN void nghttp3_set_debug_vprintf_callback( * `nghttp3_conn_submit_shutdown_notice`. Note that libnghttp3 does * not implement HTTP/3 Server Push. */ -#define NGHTTP3_SHUTDOWN_NOTICE_PUSH_ID ((1ull << 62) - 1) +#define NGHTTP3_SHUTDOWN_NOTICE_PUSH_ID ((1ULL << 62) - 1) /** * @struct @@ -1790,7 +1849,8 @@ typedef struct nghttp3_settings { * Datagrams (see :rfc:`9297`). */ uint8_t h3_datagram; - /* The following fields have been added since NGHTTP3_SETTINGS_V2. */ + /* The following fields have been added since + NGHTTP3_SETTINGS_V2. */ /** * :member:`origin_list`, if set, must contain a serialized HTTP/3 * ORIGIN frame (see :rfc:`9412`) payload. The ORIGIN frame payload @@ -1802,10 +1862,13 @@ typedef struct nghttp3_settings { * until the :type:`nghttp3_conn` to which this field was passed is * freed by `nghttp3_conn_del`. The object pointed to by this field * is copied internally, and does not need to be kept alive. Only - * server uses this field. This field is available since v1.11.0. + * server uses this field. + * + * .. version-added:: 1.11.0 */ const nghttp3_vec *origin_list; - /* The following fields have been added since NGHTTP3_SETTINGS_V3. */ + /* The following fields have been added since + NGHTTP3_SETTINGS_V3. */ /** * :member:`glitch_ratelim_burst` is the maximum number of tokens * available to "glitch" rate limiter. "glitch" is a suspicious @@ -1813,21 +1876,27 @@ typedef struct nghttp3_settings { * tokens are consumed. If no tokens are available to consume, the * connection is closed. The rate of token generation is specified * by :member:`glitch_ratelim_rate`. This feature is enabled only - * when `nghttp3_conn_read_stream2` is used. This field has been - * available since v1.12.0. + * when `nghttp3_conn_read_stream2` is used. + * + * .. version-added:: 1.12.0 */ uint64_t glitch_ratelim_burst; /** * :member:`glitch_ratelim_rate` is the number of tokens generated * per second. See :member:`glitch_ratelim_burst` for "glitch" rate - * limiter. This field has been available since v1.12.0. + * limiter. + * + * .. version-added:: 1.12.0 */ uint64_t glitch_ratelim_rate; + /* The following fields have been added since + NGHTTP3_SETTINGS_V4. */ /** * :member:`qpack_indexing_strat` defines the QPACK dynamic table * indexing strategy for those fields that are not defined in - * :type:`nghttp3_qpack_token`. This field has been available since - * v1.13.0. + * :type:`nghttp3_qpack_token`. + * + * .. version-added:: 1.13.0 */ nghttp3_qpack_indexing_strat qpack_indexing_strat; } nghttp3_settings; @@ -1839,7 +1908,9 @@ typedef struct nghttp3_settings { * @struct * * :type:`nghttp3_proto_settings` contains HTTP/3 settings that this - * library can recognize. This field is available since v1.14.0. + * library can recognize. + * + * .. version-added:: 1.14.0 */ typedef struct nghttp3_proto_settings { /** @@ -2096,8 +2167,9 @@ typedef int (*nghttp3_shutdown)(nghttp3_conn *conn, int64_t id, * * .. warning:: * - * Deprecated since v1.14.0. Use :type:`nghttp3_recv_settings2` - * instead. New settings will not be notified with this callback. + * .. version-deprecated:: 1.14.0 + * Use :type:`nghttp3_recv_settings2` instead. New settings will + * not be notified with this callback. * * :type:`nghttp3_recv_settings` is a callback function which is * invoked when SETTINGS frame is received. |settings| is a received @@ -2160,8 +2232,9 @@ typedef void (*nghttp3_rand)(uint8_t *dest, size_t destlen); * The implementation of this callback must return 0 if it succeeds. * Returning :macro:`NGHTTP3_ERR_CALLBACK_FAILURE` will return to the * caller immediately. Any values other than 0 is treated as - * :macro:`NGHTTP3_ERR_CALLBACK_FAILURE`. This callback is available - * since v1.14.0. + * :macro:`NGHTTP3_ERR_CALLBACK_FAILURE`. + * + * .. version-added:: 1.14.0 */ typedef int (*nghttp3_recv_settings2)(nghttp3_conn *conn, const nghttp3_proto_settings *settings, @@ -2262,24 +2335,27 @@ typedef struct nghttp3_callbacks { /** * .. warning:: * - * Deprecated since v1.14.0. Use :member:`recv_settings2` - * instead. + * .. version-deprecated:: 1.14.0 + * Use :member:`recv_settings2` instead. * * :member:`recv_settings` is a callback function which is invoked * when SETTINGS frame is received. */ nghttp3_recv_settings recv_settings; - /* The following fields have been added since NGHTTP3_CALLBACKS_V2. */ + /* The following fields have been added since + NGHTTP3_CALLBACKS_V2. */ /** * :member:`recv_origin` is a callback function which is invoked - * when a single origin in an ORIGIN frame is received. This field - * is available since v1.11.0. + * when a single origin in an ORIGIN frame is received. + * + * .. version-added:: 1.11.0 */ nghttp3_recv_origin recv_origin; /** * :member:`end_origin` is a callback function which is invoked when - * an ORIGIN frame has been completely processed. This field is - * available since v1.11.0. + * an ORIGIN frame has been completely processed. + * + * .. version-added:: 1.11.0 */ nghttp3_end_origin end_origin; /** @@ -2287,14 +2363,16 @@ typedef struct nghttp3_callbacks { * unpredictable data are needed. Although this field is optional * due to the backward compatibility, it is recommended to specify * this field to harden the runtime behavior against suspicious - * activities of a remote endpoint. This field is available since - * v1.11.0. + * activities of a remote endpoint. + * + * .. version-added:: 1.11.0 */ nghttp3_rand rand; /** * :member:`recv_settings2` is a callback function which is invoked - * when SETTINGS frame is received. This field is available since - * v1.14.0. + * when SETTINGS frame is received. + * + * .. version-added:: 1.14.0 */ nghttp3_recv_settings2 recv_settings2; } nghttp3_callbacks; @@ -2306,7 +2384,7 @@ typedef struct nghttp3_callbacks { * values. * * - :member:`max_field_section_size - * ` = :expr:`((1ull << 62) - 1)` + * ` = :expr:`((1ULL << 62) - 1)` * - :member:`qpack_max_dtable_capacity * ` = 0 * - :member:`qpack_encoder_max_dtable_capacity @@ -2417,8 +2495,8 @@ NGHTTP3_EXTERN int nghttp3_conn_bind_qpack_streams(nghttp3_conn *conn, * * .. warning:: * - * Deprecated since v1.12.0. Use `nghttp3_conn_read_stream2` - * instead. + * .. version-deprecated:: 1.12.0 + * Use `nghttp3_conn_read_stream2` instead. * * `nghttp3_conn_read_stream` reads data |src| of length |srclen| on * stream identified by |stream_id|. It returns the number of bytes @@ -2476,7 +2554,7 @@ NGHTTP3_EXTERN nghttp3_ssize nghttp3_conn_read_stream(nghttp3_conn *conn, * be closed. Calling nghttp3 API other than `nghttp3_conn_del` * causes undefined behavior. * - * This function is available since v1.12.0. + * .. version-added:: 1.12.0 */ NGHTTP3_EXTERN nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, @@ -2547,6 +2625,25 @@ NGHTTP3_EXTERN nghttp3_ssize nghttp3_conn_writev_stream(nghttp3_conn *conn, NGHTTP3_EXTERN int nghttp3_conn_add_write_offset(nghttp3_conn *conn, int64_t stream_id, size_t n); +/** + * @function + * + * `nghttp3_conn_is_stream_flushed` returns nonzero if all stream data + * for a stream identified by |stream_id| so far have been accepted by + * QUIC stack. This means that the cumulative number of bytes that + * `nghttp3_conn_add_write_offset` notified covers all stream data + * currently held. This does not mean more stream data cannot be + * submitted to this stream via :type:`nghttp3_read_data_callback` and + * all stream data have been acknowledged. + * + * If there is no stream identified by |stream_id|, this function + * returns nonzero. + * + * .. version-added:: 1.17.0 + */ +NGHTTP3_EXTERN int nghttp3_conn_is_stream_flushed(const nghttp3_conn *conn, + int64_t stream_id); + /** * @function * @@ -2624,6 +2721,11 @@ NGHTTP3_EXTERN int nghttp3_conn_unblock_stream(nghttp3_conn *conn, /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.16.0 + * Use `nghttp3_conn_is_stream_writable2` instead. + * * `nghttp3_conn_is_stream_writable` returns nonzero if a stream * identified by |stream_id| is writable. It is not writable if: * @@ -2641,6 +2743,28 @@ NGHTTP3_EXTERN int nghttp3_conn_unblock_stream(nghttp3_conn *conn, NGHTTP3_EXTERN int nghttp3_conn_is_stream_writable(nghttp3_conn *conn, int64_t stream_id); +/** + * @function + * + * `nghttp3_conn_is_stream_writable2` returns nonzero if a stream + * identified by |stream_id| is writable. It is not writable if: + * + * - the stream does not exist; or, + * - the stream is closed (e.g., `nghttp3_conn_close_stream` is + * called); or, + * - the stream is QUIC flow control blocked (e.g., + * `nghttp3_conn_block_stream` is called); or, + * - the stream is input data blocked (e.g., + * :macro:`NGHTTP3_ERR_WOULDBLOCK` is returned from + * :type:`nghttp3_read_data_callback`); or, + * - the stream is half-closed local (e.g., + * `nghttp3_conn_shutdown_stream_write` is called). + * + * .. version-added:: 1.16.0 + */ +NGHTTP3_EXTERN int nghttp3_conn_is_stream_writable2(const nghttp3_conn *conn, + int64_t stream_id); + /** * @function * @@ -2729,7 +2853,7 @@ NGHTTP3_EXTERN int nghttp3_conn_close_stream(nghttp3_conn *conn, * * :macro:`NGHTTP3_DATA_FLAG_NONE` indicates no flag set. */ -#define NGHTTP3_DATA_FLAG_NONE 0x00u +#define NGHTTP3_DATA_FLAG_NONE 0x00U /** * @macro @@ -2739,7 +2863,7 @@ NGHTTP3_EXTERN int nghttp3_conn_close_stream(nghttp3_conn *conn, * that sending side of stream is closed unless * :macro:`NGHTTP3_DATA_FLAG_NO_END_STREAM` is given at the same time. */ -#define NGHTTP3_DATA_FLAG_EOF 0x01u +#define NGHTTP3_DATA_FLAG_EOF 0x01U /** * @macro @@ -2751,7 +2875,7 @@ NGHTTP3_EXTERN int nghttp3_conn_close_stream(nghttp3_conn *conn, * has been called, regardless of this flag, the submitted trailer * fields are sent. */ -#define NGHTTP3_DATA_FLAG_NO_END_STREAM 0x02u +#define NGHTTP3_DATA_FLAG_NO_END_STREAM 0x02U /** * @function @@ -2964,6 +3088,29 @@ NGHTTP3_EXTERN int nghttp3_conn_set_stream_user_data(nghttp3_conn *conn, /** * @function * + * `nghttp3_conn_get_stream_user_data` returns the user data + * associated to the stream identified by |stream_id|. If no data is + * associated or the stream is not found, this function returns NULL. + * + * The user data can be associated to the stream by the following + * functions: + * + * - `nghttp3_conn_set_stream_user_data` + * - `nghttp3_conn_submit_request` + * + * .. version-added:: 1.16.0 + */ +NGHTTP3_EXTERN void *nghttp3_conn_get_stream_user_data(const nghttp3_conn *conn, + int64_t stream_id); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.16.0 + * Use `nghttp3_conn_get_frame_payload_left2` instead. + * * `nghttp3_conn_get_frame_payload_left` returns the number of bytes * left to read current frame payload for a stream denoted by * |stream_id|. If no such stream is found, or |stream_id| identifies @@ -2973,6 +3120,20 @@ NGHTTP3_EXTERN int nghttp3_conn_set_stream_user_data(nghttp3_conn *conn, NGHTTP3_EXTERN uint64_t nghttp3_conn_get_frame_payload_left(nghttp3_conn *conn, int64_t stream_id); +/** + * @function + * + * `nghttp3_conn_get_frame_payload_left2` returns the number of bytes + * left to read current frame payload for a stream denoted by + * |stream_id|. If no such stream is found, or |stream_id| identifies + * neither client bidirectional stream nor remote control stream, it + * returns 0. + * + * .. version-added:: 1.16.0 + */ +NGHTTP3_EXTERN uint64_t nghttp3_conn_get_frame_payload_left2( + const nghttp3_conn *conn, int64_t stream_id); + /** * @macrosection * @@ -3034,6 +3195,11 @@ typedef struct NGHTTP3_ALIGN(8) nghttp3_pri { /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.16.0 + * Use `nghttp3_conn_get_stream_priority2` instead. + * * `nghttp3_conn_get_stream_priority` stores stream priority of a * stream denoted by |stream_id| into |*dest|. |stream_id| must * identify client initiated bidirectional stream. Only server can @@ -3053,6 +3219,32 @@ typedef struct NGHTTP3_ALIGN(8) nghttp3_pri { NGHTTP3_EXTERN int nghttp3_conn_get_stream_priority_versioned( nghttp3_conn *conn, int pri_version, nghttp3_pri *dest, int64_t stream_id); +/** + * @function + * + * `nghttp3_conn_get_stream_priority2` stores stream priority of a + * stream denoted by |stream_id| into |*dest|. |stream_id| must + * identify client initiated bidirectional stream. Only server can + * use this function. + * + * This function must not be called if |conn| is initialized as + * client. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * :macro:`NGHTTP3_ERR_INVALID_ARGUMENT` + * |stream_id| is not a client initiated bidirectional stream ID. + * :macro:`NGHTTP3_ERR_STREAM_NOT_FOUND` + * Stream not found. + * + * .. version-added:: 1.16.0 + */ +NGHTTP3_EXTERN int +nghttp3_conn_get_stream_priority2_versioned(const nghttp3_conn *conn, + int pri_version, nghttp3_pri *dest, + int64_t stream_id); + /** * @function * @@ -3139,12 +3331,28 @@ NGHTTP3_EXTERN int nghttp3_check_header_value(const uint8_t *value, size_t len); /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.16.0 + * Use `nghttp3_conn_is_drained2` instead. + * * `nghttp3_conn_is_drained` returns nonzero if * `nghttp3_conn_shutdown` has been called, and there is no active * remote streams. This function is for server use only. */ NGHTTP3_EXTERN int nghttp3_conn_is_drained(nghttp3_conn *conn); +/** + * @function + * + * `nghttp3_conn_is_drained2` returns nonzero if + * `nghttp3_conn_shutdown` has been called, and there is no active + * remote streams. This function is for server use only. + * + * .. version-added:: 1.16.0 + */ +NGHTTP3_EXTERN int nghttp3_conn_is_drained2(const nghttp3_conn *conn); + /** * @function * @@ -3229,6 +3437,75 @@ NGHTTP3_EXTERN const nghttp3_info *nghttp3_version(int least_version); */ NGHTTP3_EXTERN int nghttp3_err_is_fatal(int liberr); +/** + * @function + * + * `nghttp3_get_uvarint` reads variable-length unsigned integer from + * the buffer pointed by |p|, and stores it in the object pointed by + * |dest| in host byte order. It returns |p| plus the number of bytes + * read from |p|. This function assumes that |p| points to the buffer + * that contains a valid variable-length unsigned integer. Use + * `nghttp3_get_uvarintlen` to get the number of bytes to successfully + * decode an integer. + * + * .. version-added:: 1.17.0 + */ +NGHTTP3_EXTERN const uint8_t *nghttp3_get_uvarint(uint64_t *dest, + const uint8_t *p); + +/** + * @function + * + * `nghttp3_get_uvarintlen` returns the required number of bytes to + * read variable-length unsigned integer starting at |p|. |p| must + * not be NULL. This function only reads the single byte from the + * buffer pointed by |p|, and determines the number of bytes to read. + * + * .. version-added:: 1.17.0 + */ +NGHTTP3_EXTERN size_t nghttp3_get_uvarintlen(const uint8_t *p); + +/** + * @function + * + * `nghttp3_get_varint` reads variable-length unsigned integer from + * the buffer pointed by |p|, and stores it in the object pointed by + * |dest| in host byte order. It returns |p| plus the number of bytes + * read from |p|. This function assumes that |p| points to the buffer + * that contains a valid variable-length unsigned integer. Use + * `nghttp3_get_uvarintlen` to get the number of bytes to successfully + * decode an integer. + * + * .. version-added:: 1.17.0 + */ +NGHTTP3_EXTERN const uint8_t *nghttp3_get_varint(int64_t *dest, + const uint8_t *p); + +/** + * @function + * + * `nghttp3_put_uvarint` writes |n| to the buffer pointed by |p| using + * variable-length unsigned integer encoding. It returns the one + * beyond of the last written position. This function assumes that + * the buffer pointed by |p| has sufficient capacity to encode |n|. + * To know the required capacity, use `nghttp3_put_uvarintlen`. |n| + * must be less than or equal to (1 << 62) - 1. + * + * .. version-added:: 1.17.0 + */ +NGHTTP3_EXTERN uint8_t *nghttp3_put_uvarint(uint8_t *p, uint64_t n); + +/** + * @function + * + * `nghttp3_put_uvarintlen` returns the required number of bytes to + * encode |n| in variable-length unsigned integer encoding. |n| must + * be less than or equal to (1 << 62) - 1. + * + * .. version-added:: 1.17.0 + */ +NGHTTP3_EXTERN size_t nghttp3_put_uvarintlen(uint64_t n); + /* * Versioned function wrappers */ @@ -3279,6 +3556,15 @@ NGHTTP3_EXTERN int nghttp3_err_is_fatal(int liberr); nghttp3_conn_get_stream_priority_versioned((CONN), NGHTTP3_PRI_VERSION, \ (DEST), (STREAM_ID)) +/* + * `nghttp3_conn_get_stream_priority2` is a wrapper around + * `nghttp3_conn_get_stream_priority2_versioned` to set the correct + * struct version. + */ +#define nghttp3_conn_get_stream_priority2(CONN, DEST, STREAM_ID) \ + nghttp3_conn_get_stream_priority2_versioned((CONN), NGHTTP3_PRI_VERSION, \ + (DEST), (STREAM_ID)) + /* * `nghttp3_pri_parse_priority` is a wrapper around * `nghttp3_pri_parse_priority_versioned` to set the correct struct diff --git a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h index 0710f8efbd33d0..6f9f04e7426b95 100644 --- a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h +++ b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h @@ -31,7 +31,7 @@ * * Version number of the nghttp3 library release. */ -#define NGHTTP3_VERSION "1.14.0" +#define NGHTTP3_VERSION "1.17.0" /** * @macro @@ -41,6 +41,6 @@ * number, 8 bits for minor and 8 bits for patch. Version 1.2.3 * becomes 0x010203. */ -#define NGHTTP3_VERSION_NUM 0x010e00 +#define NGHTTP3_VERSION_NUM 0x011100 #endif /* !defined(NGHTTP3_VERSION_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_balloc.c b/deps/ngtcp2/nghttp3/lib/nghttp3_balloc.c index 25c0fef2f7b187..436310633349a6 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_balloc.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_balloc.c @@ -31,7 +31,7 @@ void nghttp3_balloc_init(nghttp3_balloc *balloc, size_t blklen, const nghttp3_mem *mem) { - assert((blklen & 0xfu) == 0); + assert((blklen & 0xFU) == 0); balloc->mem = mem; balloc->blklen = blklen; @@ -67,7 +67,7 @@ int nghttp3_balloc_get(nghttp3_balloc *balloc, void **pbuf, size_t n) { if (nghttp3_buf_left(&balloc->buf) < n) { p = nghttp3_mem_malloc(balloc->mem, - sizeof(nghttp3_memblock_hd) + 0x8u + balloc->blklen); + sizeof(nghttp3_memblock_hd) + 0x8U + balloc->blklen); if (p == NULL) { return NGHTTP3_ERR_NOMEM; } @@ -77,15 +77,15 @@ int nghttp3_balloc_get(nghttp3_balloc *balloc, void **pbuf, size_t n) { balloc->head = hd; nghttp3_buf_wrap_init( &balloc->buf, - (uint8_t *)(((uintptr_t)p + sizeof(nghttp3_memblock_hd) + 0xfu) & - ~(uintptr_t)0xfu), + (uint8_t *)(((uintptr_t)p + sizeof(nghttp3_memblock_hd) + 0xFU) & + ~(uintptr_t)0xFU), balloc->blklen); } - assert(((uintptr_t)balloc->buf.last & 0xfu) == 0); + assert(((uintptr_t)balloc->buf.last & 0xFU) == 0); *pbuf = balloc->buf.last; - balloc->buf.last += (n + 0xfu) & ~(uintptr_t)0xfu; + balloc->buf.last += (n + 0xFU) & ~(size_t)0xFU; return 0; } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_callbacks.c b/deps/ngtcp2/nghttp3/lib/nghttp3_callbacks.c index 4e5111c4ca943a..283f479b0d3473 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_callbacks.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_callbacks.c @@ -45,7 +45,7 @@ nghttp3_callbacks_convert_to_latest(nghttp3_callbacks *dest, return src; } - memset(dest, 0, sizeof(*dest)); + *dest = (nghttp3_callbacks){0}; callbacks_copy(dest, src, callbacks_version); diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c index ccf04984981d59..d1b6355bb7e036 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c @@ -43,7 +43,7 @@ nghttp3_objalloc_def(chunk, nghttp3_chunk, oplent) * conn_remote_stream_uni returns nonzero if |stream_id| is remote * unidirectional stream ID. */ -static int conn_remote_stream_uni(nghttp3_conn *conn, int64_t stream_id) { +static int conn_remote_stream_uni(const nghttp3_conn *conn, int64_t stream_id) { if (conn->server) { return (stream_id & 0x03) == 0x02; } @@ -495,7 +495,7 @@ nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, } conn->rx.max_stream_id_bidi = - nghttp3_max_int64(conn->rx.max_stream_id_bidi, stream_id); + nghttp3_max(conn->rx.max_stream_id_bidi, stream_id); rv = nghttp3_conn_create_stream(conn, &stream, stream_id); if (rv != 0) { return rv; @@ -572,7 +572,7 @@ static nghttp3_ssize conn_read_type(nghttp3_conn *conn, nghttp3_stream *stream, nghttp3_stream_read_state *rstate = &stream->rstate; nghttp3_varint_read_state *rvint = &rstate->rvint; nghttp3_ssize nread; - int64_t stream_type; + uint64_t stream_type; assert(srclen); @@ -649,10 +649,7 @@ nghttp3_ssize nghttp3_conn_read_uni(nghttp3_conn *conn, nghttp3_stream *stream, return NGHTTP3_ERR_H3_EXCESSIVE_LOAD; } - rv = conn_delete_stream(conn, stream); - assert(0 == rv); - - return 0; + return conn_delete_stream(conn, stream); } nread = conn_read_type(conn, stream, src, srclen, fin); if (nread < 0) { @@ -724,8 +721,8 @@ static void conn_reset_rx_originlen(nghttp3_conn *conn) { conn->rx.originlen = 0; } -static int frame_fin(nghttp3_stream_read_state *rstate, size_t len) { - return (int64_t)len >= rstate->left; +static int frame_fin(const nghttp3_stream_read_state *rstate, size_t len) { + return len >= rstate->left; } nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, @@ -909,7 +906,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, } /* Read Identifier */ - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); assert(len > 0); nread = nghttp3_read_varint(rvint, p, p + len, frame_fin(rstate, len)); if (nread < 0) { @@ -918,12 +915,12 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, p += nread; nconsumed += (size_t)nread; - rstate->left -= nread; + rstate->left -= (uint64_t)nread; if (rvint->left) { rstate->state = NGHTTP3_CTRL_STREAM_STATE_SETTINGS_ID; return (nghttp3_ssize)nconsumed; } - rstate->fr.settings.iv[0].id = (uint64_t)rvint->acc; + rstate->fr.settings.iv[0].id = rvint->acc; nghttp3_varint_read_state_reset(rvint); /* Read Value */ @@ -944,12 +941,12 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, p += nread; nconsumed += (size_t)nread; - rstate->left -= nread; + rstate->left -= (uint64_t)nread; if (rvint->left) { rstate->state = NGHTTP3_CTRL_STREAM_STATE_SETTINGS_VALUE; return (nghttp3_ssize)nconsumed; } - rstate->fr.settings.iv[0].value = (uint64_t)rvint->acc; + rstate->fr.settings.iv[0].value = rvint->acc; nghttp3_varint_read_state_reset(rvint); rv = @@ -960,7 +957,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, } break; case NGHTTP3_CTRL_STREAM_STATE_SETTINGS_ID: - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); assert(len > 0); nread = nghttp3_read_varint(rvint, p, p + len, frame_fin(rstate, len)); if (nread < 0) { @@ -969,11 +966,11 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, p += nread; nconsumed += (size_t)nread; - rstate->left -= nread; + rstate->left -= (uint64_t)nread; if (rvint->left) { return (nghttp3_ssize)nconsumed; } - rstate->fr.settings.iv[0].id = (uint64_t)rvint->acc; + rstate->fr.settings.iv[0].id = rvint->acc; nghttp3_varint_read_state_reset(rvint); if (rstate->left == 0) { @@ -987,7 +984,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, } /* Fall through */ case NGHTTP3_CTRL_STREAM_STATE_SETTINGS_VALUE: - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); assert(len > 0); nread = nghttp3_read_varint(rvint, p, p + len, frame_fin(rstate, len)); if (nread < 0) { @@ -996,11 +993,11 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, p += nread; nconsumed += (size_t)nread; - rstate->left -= nread; + rstate->left -= (uint64_t)nread; if (rvint->left) { return (nghttp3_ssize)nconsumed; } - rstate->fr.settings.iv[0].value = (uint64_t)rvint->acc; + rstate->fr.settings.iv[0].value = rvint->acc; nghttp3_varint_read_state_reset(rvint); rv = nghttp3_conn_on_settings_entry_received(conn, &rstate->fr.settings); @@ -1021,7 +1018,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, nghttp3_stream_read_state_reset(rstate); break; case NGHTTP3_CTRL_STREAM_STATE_GOAWAY: - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); assert(len > 0); nread = nghttp3_read_varint(rvint, p, p + len, frame_fin(rstate, len)); if (nread < 0) { @@ -1030,26 +1027,26 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, p += nread; nconsumed += (size_t)nread; - rstate->left -= nread; + rstate->left -= (uint64_t)nread; if (rvint->left) { return (nghttp3_ssize)nconsumed; } - if (!conn->server && !nghttp3_client_stream_bidi(rvint->acc)) { + if (!conn->server && !nghttp3_client_stream_bidi((int64_t)rvint->acc)) { return NGHTTP3_ERR_H3_ID_ERROR; } - if (conn->rx.goaway_id < rvint->acc) { + if (conn->rx.goaway_id < (int64_t)rvint->acc) { return NGHTTP3_ERR_H3_ID_ERROR; } /* Receiving same GOAWAY ID is suspicious. */ - if (conn->rx.goaway_id == rvint->acc && + if (conn->rx.goaway_id == (int64_t)rvint->acc && conn_glitch_ratelim_drain(conn, 1, ts) != 0) { return NGHTTP3_ERR_H3_EXCESSIVE_LOAD; } conn->flags |= NGHTTP3_CONN_FLAG_GOAWAY_RECVED; - conn->rx.goaway_id = rvint->acc; + conn->rx.goaway_id = (int64_t)rvint->acc; nghttp3_varint_read_state_reset(rvint); if (conn->callbacks.shutdown) { @@ -1064,7 +1061,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, break; case NGHTTP3_CTRL_STREAM_STATE_MAX_PUSH_ID: /* server side only */ - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); assert(len > 0); nread = nghttp3_read_varint(rvint, p, p + len, frame_fin(rstate, len)); if (nread < 0) { @@ -1073,29 +1070,29 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, p += nread; nconsumed += (size_t)nread; - rstate->left -= nread; + rstate->left -= (uint64_t)nread; if (rvint->left) { return (nghttp3_ssize)nconsumed; } - if (conn->local.uni.max_pushes > (uint64_t)rvint->acc + 1) { + if (conn->local.uni.max_pushes > rvint->acc + 1) { return NGHTTP3_ERR_H3_FRAME_ERROR; } /* Receiving same MAX_PUSH_ID is suspicious. */ - if (conn->local.uni.max_pushes == (uint64_t)rvint->acc + 1 && + if (conn->local.uni.max_pushes == rvint->acc + 1 && conn_glitch_ratelim_drain(conn, 1, ts) != 0) { return NGHTTP3_ERR_H3_EXCESSIVE_LOAD; } - conn->local.uni.max_pushes = (uint64_t)rvint->acc + 1; + conn->local.uni.max_pushes = rvint->acc + 1; nghttp3_varint_read_state_reset(rvint); nghttp3_stream_read_state_reset(rstate); break; case NGHTTP3_CTRL_STREAM_STATE_PRIORITY_UPDATE_PRI_ELEM_ID: /* server side only */ - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); assert(len > 0); nread = nghttp3_read_varint(rvint, p, p + len, frame_fin(rstate, len)); if (nread < 0) { @@ -1104,12 +1101,12 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, p += nread; nconsumed += (size_t)nread; - rstate->left -= nread; + rstate->left -= (uint64_t)nread; if (rvint->left) { return (nghttp3_ssize)nconsumed; } - rstate->fr.priority_update.pri_elem_id = rvint->acc; + rstate->fr.priority_update.pri_elem_id = (int64_t)rvint->acc; nghttp3_varint_read_state_reset(rvint); if (rstate->left == 0) { @@ -1138,9 +1135,9 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, case NGHTTP3_CTRL_STREAM_STATE_PRIORITY_UPDATE: /* We need to buffer Priority Field Value because it might be fragmented. */ - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); assert(len > 0); - if (conn->rx.pri_fieldbuflen == 0 && rstate->left == (int64_t)len) { + if (conn->rx.pri_fieldbuflen == 0 && rstate->left == len) { /* Everything is in the input buffer. Apply same length limit we impose when buffering the field. */ if (len > sizeof(conn->rx.pri_fieldbuf)) { @@ -1160,7 +1157,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, memcpy(conn->rx.pri_fieldbuf + conn->rx.pri_fieldbuflen, p, len); conn->rx.pri_fieldbuflen += len; - if (rstate->left == (int64_t)len) { + if (rstate->left == len) { pri_field_value = conn->rx.pri_fieldbuf; pri_field_valuelen = conn->rx.pri_fieldbuflen; } @@ -1168,7 +1165,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, p += len; nconsumed += len; - rstate->left -= (int64_t)len; + rstate->left -= len; if (rstate->left) { return (nghttp3_ssize)nconsumed; @@ -1193,7 +1190,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, break; case NGHTTP3_CTRL_STREAM_STATE_ORIGIN_ORIGIN_LEN: /* client side only */ - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); assert(len > 0); @@ -1208,7 +1205,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, } nconsumed += (size_t)nread; - rstate->left -= nread; + rstate->left -= (uint64_t)nread; len -= (size_t)nread; if (conn->rx.originlen_offset < sizeof(conn->rx.originlen)) { @@ -1240,7 +1237,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, /* No need to update p because we will return very soon. */ nconsumed += len; - rstate->left -= (int64_t)len; + rstate->left -= len; conn->rx.originbuflen = len; @@ -1281,8 +1278,8 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, break; case NGHTTP3_CTRL_STREAM_STATE_ORIGIN_ASCII_ORIGIN: /* client side only */ - len = nghttp3_min_size(conn->rx.originlen - conn->rx.originbuflen, - (size_t)(end - p)); + len = nghttp3_min(conn->rx.originlen - conn->rx.originbuflen, + (size_t)(end - p)); assert(len > 0); @@ -1291,7 +1288,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, conn->rx.originbuflen += len; p += len; nconsumed += len; - rstate->left -= (int64_t)len; + rstate->left -= len; if (conn->rx.originbuflen < conn->rx.originlen) { return (nghttp3_ssize)nconsumed; @@ -1319,10 +1316,10 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, break; case NGHTTP3_CTRL_STREAM_STATE_IGN_FRAME: - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); p += len; nconsumed += len; - rstate->left -= (int64_t)len; + rstate->left -= len; if (rstate->left) { return (nghttp3_ssize)nconsumed; @@ -1339,7 +1336,6 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, } static int conn_delete_stream(nghttp3_conn *conn, nghttp3_stream *stream) { - int bidi = nghttp3_client_stream_bidi(stream->node.id); int rv; rv = conn_call_deferred_consume(conn, stream, @@ -1348,23 +1344,20 @@ static int conn_delete_stream(nghttp3_conn *conn, nghttp3_stream *stream) { return rv; } - if (bidi) { - if (stream->qpack_blocked_pe.index != NGHTTP3_PQ_BAD_INDEX) { - nghttp3_conn_qpack_blocked_streams_remove(conn, stream); + if (stream->qpack_blocked_pe.index != NGHTTP3_PQ_BAD_INDEX) { + nghttp3_conn_qpack_blocked_streams_remove(conn, stream); - rv = nghttp3_qpack_decoder_cancel_stream(&conn->qdec, stream->node.id); - if (rv != 0) { - return rv; - } + rv = nghttp3_qpack_decoder_cancel_stream(&conn->qdec, stream->node.id); + if (rv != 0) { + return rv; } + } - if (conn->callbacks.stream_close) { - rv = - conn->callbacks.stream_close(conn, stream->node.id, stream->error_code, - conn->user_data, stream->user_data); - if (rv != 0) { - return NGHTTP3_ERR_CALLBACK_FAILURE; - } + if (conn->callbacks.stream_close) { + rv = conn->callbacks.stream_close(conn, stream->node.id, stream->error_code, + conn->user_data, stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; } } @@ -1445,7 +1438,7 @@ nghttp3_ssize nghttp3_conn_read_qpack_encoder(nghttp3_conn *conn, for (; !nghttp3_pq_empty(&conn->qpack_blocked_streams);) { stream = nghttp3_struct_of(nghttp3_pq_top(&conn->qpack_blocked_streams), nghttp3_stream, qpack_blocked_pe); - if (nghttp3_qpack_stream_context_get_ricnt(&stream->qpack_sctx) > + if (nghttp3_qpack_stream_context_get_ricnt2(&stream->qpack_sctx) > nghttp3_qpack_decoder_get_icnt(&conn->qdec)) { break; } @@ -1647,13 +1640,13 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, } break; case NGHTTP3_REQ_STREAM_STATE_DATA: - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); rv = nghttp3_conn_on_data(conn, stream, p, len); if (rv != 0) { return rv; } p += len; - rstate->left -= (int64_t)len; + rstate->left -= len; if (rstate->left) { goto almost_done; @@ -1666,16 +1659,16 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, nghttp3_stream_read_state_reset(rstate); break; case NGHTTP3_REQ_STREAM_STATE_HEADERS: - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); - nread = nghttp3_conn_on_headers(conn, stream, p, len, - (int64_t)len == rstate->left); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); + nread = + nghttp3_conn_on_headers(conn, stream, p, len, len == rstate->left); if (nread < 0) { return nread; } p += nread; nconsumed += (size_t)nread; - rstate->left -= nread; + rstate->left -= (uint64_t)nread; if (stream->flags & NGHTTP3_STREAM_FLAG_QPACK_DECODE_BLOCKED) { if (p != end && nghttp3_stream_get_buffered_datalen(stream) == 0) { @@ -1748,10 +1741,10 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, break; case NGHTTP3_REQ_STREAM_STATE_IGN_FRAME: - len = (size_t)nghttp3_min_int64(rstate->left, (int64_t)(end - p)); + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); p += len; nconsumed += len; - rstate->left -= (int64_t)len; + rstate->left -= len; if (rstate->left) { goto almost_done; @@ -1975,7 +1968,7 @@ int nghttp3_conn_on_settings_entry_received(nghttp3_conn *conn, dest->qpack_blocked_streams = (size_t)ent->value; nghttp3_qpack_encoder_set_max_blocked_streams( - &conn->qenc, (size_t)nghttp3_min_uint64(100, ent->value)); + &conn->qenc, (size_t)nghttp3_min(100, ent->value)); break; case NGHTTP3_SETTINGS_ID_ENABLE_CONNECT_PROTOCOL: if (conn->server) { @@ -2054,7 +2047,7 @@ conn_on_priority_update_stream(nghttp3_conn *conn, } conn->rx.max_stream_id_bidi = - nghttp3_max_int64(conn->rx.max_stream_id_bidi, stream_id); + nghttp3_max(conn->rx.max_stream_id_bidi, stream_id); rv = nghttp3_conn_create_stream(conn, &stream, stream_id); if (rv != 0) { return rv; @@ -2106,11 +2099,11 @@ int nghttp3_conn_create_stream(nghttp3_conn *conn, nghttp3_stream **pstream, int64_t stream_id) { nghttp3_stream *stream; int rv; + static const nghttp3_stream_callbacks callbacks = { + .acked_data = conn_stream_acked_data, + }; - rv = nghttp3_stream_new(&stream, stream_id, - &(nghttp3_stream_callbacks){ - .acked_data = conn_stream_acked_data, - }, + rv = nghttp3_stream_new(&stream, stream_id, &callbacks, &conn->out_chunk_objalloc, &conn->stream_objalloc, conn->mem); if (rv != 0) { @@ -2135,14 +2128,14 @@ int nghttp3_conn_create_stream(nghttp3_conn *conn, nghttp3_stream **pstream, return 0; } -nghttp3_stream *nghttp3_conn_find_stream(nghttp3_conn *conn, +nghttp3_stream *nghttp3_conn_find_stream(const nghttp3_conn *conn, int64_t stream_id) { return nghttp3_map_find(&conn->streams, (nghttp3_map_key_type)stream_id); } int nghttp3_conn_bind_control_stream(nghttp3_conn *conn, int64_t stream_id) { nghttp3_stream *stream; - nghttp3_frame_entry frent; + nghttp3_frame *fr; int rv; assert(stream_id >= 0); @@ -2168,26 +2161,28 @@ int nghttp3_conn_bind_control_stream(nghttp3_conn *conn, int64_t stream_id) { return rv; } - frent.fr.settings.type = NGHTTP3_FRAME_SETTINGS; - frent.aux.settings.local_settings = &conn->local.settings; - - rv = nghttp3_stream_frq_add(stream, &frent); + rv = nghttp3_stream_frq_emplace(stream, &fr); if (rv != 0) { return rv; } + fr->settings = (nghttp3_frame_settings){ + .type = NGHTTP3_FRAME_SETTINGS, + .local_settings = &conn->local.settings, + }; + if (conn->local.settings.origin_list) { assert(conn->server); - frent.fr.origin = (nghttp3_frame_origin){ - .type = NGHTTP3_FRAME_ORIGIN, - .origin_list = *conn->local.settings.origin_list, - }; - - rv = nghttp3_stream_frq_add(stream, &frent); + rv = nghttp3_stream_frq_emplace(stream, &fr); if (rv != 0) { return rv; } + + fr->origin = (nghttp3_frame_origin){ + .type = NGHTTP3_FRAME_ORIGIN, + .origin_list = *conn->local.settings.origin_list, + }; } return 0; @@ -2414,35 +2409,35 @@ static int conn_submit_headers_data(nghttp3_conn *conn, nghttp3_stream *stream, const nghttp3_data_reader *dr) { int rv; nghttp3_nv *nnva; - nghttp3_frame_entry frent; + nghttp3_frame *fr; rv = nghttp3_nva_copy(&nnva, nva, nvlen, conn->mem); if (rv != 0) { return rv; } - frent.fr.headers = (nghttp3_frame_headers){ - .type = NGHTTP3_FRAME_HEADERS, - .nva = nnva, - .nvlen = nvlen, - }; - - rv = nghttp3_stream_frq_add(stream, &frent); + rv = nghttp3_stream_frq_emplace(stream, &fr); if (rv != 0) { nghttp3_nva_del(nnva, conn->mem); return rv; } - if (dr) { - frent.fr.data = (nghttp3_frame_data){ - .type = NGHTTP3_FRAME_DATA, - }; - frent.aux.data.dr = *dr; + fr->headers = (nghttp3_frame_headers){ + .type = NGHTTP3_FRAME_HEADERS, + .nva = nnva, + .nvlen = nvlen, + }; - rv = nghttp3_stream_frq_add(stream, &frent); + if (dr) { + rv = nghttp3_stream_frq_emplace(stream, &fr); if (rv != 0) { return rv; } + + fr->data = (nghttp3_frame_data){ + .type = NGHTTP3_FRAME_DATA, + .dr = *dr, + }; } if (nghttp3_stream_require_schedule(stream)) { @@ -2493,7 +2488,6 @@ int nghttp3_conn_submit_request(nghttp3_conn *conn, int64_t stream_id, assert(!conn->server); assert(conn->tx.qenc); - assert(stream_id >= 0); assert(stream_id <= (int64_t)NGHTTP3_MAX_VARINT); assert(nghttp3_client_stream_bidi(stream_id)); @@ -2515,6 +2509,7 @@ int nghttp3_conn_submit_request(nghttp3_conn *conn, int64_t stream_id, } stream->rx.hstate = NGHTTP3_HTTP_STATE_RESP_INITIAL; stream->user_data = stream_user_data; + stream->node.pri.inc = 1; nghttp3_http_record_request_method(stream, nva, nvlen); @@ -2585,51 +2580,51 @@ int nghttp3_conn_submit_trailers(nghttp3_conn *conn, int64_t stream_id, } int nghttp3_conn_submit_shutdown_notice(nghttp3_conn *conn) { - nghttp3_frame_entry frent; + nghttp3_frame *fr; int rv; assert(conn->tx.ctrl); - frent.fr.goaway = (nghttp3_frame_goaway){ + rv = nghttp3_stream_frq_emplace(conn->tx.ctrl, &fr); + if (rv != 0) { + return rv; + } + + fr->goaway = (nghttp3_frame_goaway){ .type = NGHTTP3_FRAME_GOAWAY, .id = conn->server ? NGHTTP3_SHUTDOWN_NOTICE_STREAM_ID : NGHTTP3_SHUTDOWN_NOTICE_PUSH_ID, }; - assert(frent.fr.goaway.id <= conn->tx.goaway_id); - - rv = nghttp3_stream_frq_add(conn->tx.ctrl, &frent); - if (rv != 0) { - return rv; - } + assert(fr->goaway.id <= conn->tx.goaway_id); - conn->tx.goaway_id = frent.fr.goaway.id; + conn->tx.goaway_id = fr->goaway.id; conn->flags |= NGHTTP3_CONN_FLAG_GOAWAY_QUEUED; return 0; } int nghttp3_conn_shutdown(nghttp3_conn *conn) { - nghttp3_frame_entry frent; + nghttp3_frame *fr; int rv; assert(conn->tx.ctrl); - frent.fr.goaway = (nghttp3_frame_goaway){ - .type = NGHTTP3_FRAME_GOAWAY, - .id = conn->server ? nghttp3_min_int64((1ll << 62) - 4, - conn->rx.max_stream_id_bidi + 4) - : 0, - }; - - assert(frent.fr.goaway.id <= conn->tx.goaway_id); - - rv = nghttp3_stream_frq_add(conn->tx.ctrl, &frent); + rv = nghttp3_stream_frq_emplace(conn->tx.ctrl, &fr); if (rv != 0) { return rv; } - conn->tx.goaway_id = frent.fr.goaway.id; + fr->goaway = (nghttp3_frame_goaway){ + .type = NGHTTP3_FRAME_GOAWAY, + .id = conn->server + ? nghttp3_min((1LL << 62) - 4, conn->rx.max_stream_id_bidi + 4) + : 0, + }; + + assert(fr->goaway.id <= conn->tx.goaway_id); + + conn->tx.goaway_id = fr->goaway.id; conn->flags |= NGHTTP3_CONN_FLAG_GOAWAY_QUEUED | NGHTTP3_CONN_FLAG_SHUTDOWN_COMMENCED; @@ -2695,7 +2690,12 @@ int nghttp3_conn_unblock_stream(nghttp3_conn *conn, int64_t stream_id) { } int nghttp3_conn_is_stream_writable(nghttp3_conn *conn, int64_t stream_id) { - nghttp3_stream *stream = nghttp3_conn_find_stream(conn, stream_id); + return nghttp3_conn_is_stream_writable2(conn, stream_id); +} + +int nghttp3_conn_is_stream_writable2(const nghttp3_conn *conn, + int64_t stream_id) { + const nghttp3_stream *stream = nghttp3_conn_find_stream(conn, stream_id); if (stream == NULL) { return 0; @@ -2813,9 +2813,29 @@ int nghttp3_conn_set_stream_user_data(nghttp3_conn *conn, int64_t stream_id, return 0; } +void *nghttp3_conn_get_stream_user_data(const nghttp3_conn *conn, + int64_t stream_id) { + nghttp3_stream *stream; + + assert(stream_id >= 0); + assert(stream_id <= (int64_t)NGHTTP3_MAX_VARINT); + + stream = nghttp3_conn_find_stream(conn, stream_id); + if (stream == NULL) { + return NULL; + } + + return stream->user_data; +} + uint64_t nghttp3_conn_get_frame_payload_left(nghttp3_conn *conn, int64_t stream_id) { - nghttp3_stream *stream; + return nghttp3_conn_get_frame_payload_left2(conn, stream_id); +} + +uint64_t nghttp3_conn_get_frame_payload_left2(const nghttp3_conn *conn, + int64_t stream_id) { + const nghttp3_stream *stream; int uni = 0; assert(stream_id >= 0); @@ -2837,14 +2857,22 @@ uint64_t nghttp3_conn_get_frame_payload_left(nghttp3_conn *conn, return 0; } - return (uint64_t)stream->rstate.left; + return stream->rstate.left; } int nghttp3_conn_get_stream_priority_versioned(nghttp3_conn *conn, int pri_version, nghttp3_pri *dest, int64_t stream_id) { - nghttp3_stream *stream; + return nghttp3_conn_get_stream_priority2_versioned(conn, pri_version, dest, + stream_id); +} + +int nghttp3_conn_get_stream_priority2_versioned(const nghttp3_conn *conn, + int pri_version, + nghttp3_pri *dest, + int64_t stream_id) { + const nghttp3_stream *stream; (void)pri_version; assert(conn->server); @@ -2870,7 +2898,8 @@ int nghttp3_conn_set_client_stream_priority(nghttp3_conn *conn, const uint8_t *data, size_t datalen) { nghttp3_stream *stream; - nghttp3_frame_entry frent; + nghttp3_frame *fr; + int rv; uint8_t *buf = NULL; assert(!conn->server); @@ -2895,14 +2924,20 @@ int nghttp3_conn_set_client_stream_priority(nghttp3_conn *conn, memcpy(buf, data, datalen); } - frent.fr.priority_update = (nghttp3_frame_priority_update){ + rv = nghttp3_stream_frq_emplace(conn->tx.ctrl, &fr); + if (rv != 0) { + nghttp3_mem_free(conn->mem, buf); + return rv; + } + + fr->priority_update = (nghttp3_frame_priority_update){ .type = NGHTTP3_FRAME_PRIORITY_UPDATE, .pri_elem_id = stream_id, .data = buf, .datalen = datalen, }; - return nghttp3_stream_frq_add(conn->tx.ctrl, &frent); + return 0; } int nghttp3_conn_set_server_stream_priority_versioned(nghttp3_conn *conn, @@ -2933,6 +2968,10 @@ int nghttp3_conn_set_server_stream_priority_versioned(nghttp3_conn *conn, } int nghttp3_conn_is_drained(nghttp3_conn *conn) { + return nghttp3_conn_is_drained2(conn); +} + +int nghttp3_conn_is_drained2(const nghttp3_conn *conn) { assert(conn->server); return (conn->flags & NGHTTP3_CONN_FLAG_SHUTDOWN_COMMENCED) && @@ -2940,3 +2979,25 @@ int nghttp3_conn_is_drained(nghttp3_conn *conn) { nghttp3_stream_outq_write_done(conn->tx.ctrl) && nghttp3_ringbuf_len(&conn->tx.ctrl->frq) == 0; } + +int nghttp3_conn_is_stream_flushed(const nghttp3_conn *conn, + int64_t stream_id) { + nghttp3_stream *stream = nghttp3_conn_find_stream(conn, stream_id); + const nghttp3_frame *fr; + + if (!stream) { + return 1; + } + + if (!nghttp3_stream_outq_write_done(stream)) { + return 0; + } + + if (nghttp3_ringbuf_len(&stream->frq) == 0) { + return 1; + } + + fr = nghttp3_ringbuf_get(&stream->frq, 0); + + return fr->hd.type == NGHTTP3_FRAME_DATA; +} diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h index 4f7f68f777a9be..6841b1c343a305 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h @@ -48,28 +48,28 @@ #define NGHTTP3_QPACK_ENCODER_MAX_BLOCK_STREAMS 100 /* NGHTTP3_CONN_FLAG_NONE indicates that no flag is set. */ -#define NGHTTP3_CONN_FLAG_NONE 0x0000u +#define NGHTTP3_CONN_FLAG_NONE 0x0000U /* NGHTTP3_CONN_FLAG_SETTINGS_RECVED is set when SETTINGS frame has been received. */ -#define NGHTTP3_CONN_FLAG_SETTINGS_RECVED 0x0001u +#define NGHTTP3_CONN_FLAG_SETTINGS_RECVED 0x0001U /* NGHTTP3_CONN_FLAG_CONTROL_OPENED is set when a control stream has opened. */ -#define NGHTTP3_CONN_FLAG_CONTROL_OPENED 0x0002u +#define NGHTTP3_CONN_FLAG_CONTROL_OPENED 0x0002U /* NGHTTP3_CONN_FLAG_QPACK_ENCODER_OPENED is set when a QPACK encoder stream has opened. */ -#define NGHTTP3_CONN_FLAG_QPACK_ENCODER_OPENED 0x0004u +#define NGHTTP3_CONN_FLAG_QPACK_ENCODER_OPENED 0x0004U /* NGHTTP3_CONN_FLAG_QPACK_DECODER_OPENED is set when a QPACK decoder stream has opened. */ -#define NGHTTP3_CONN_FLAG_QPACK_DECODER_OPENED 0x0008u +#define NGHTTP3_CONN_FLAG_QPACK_DECODER_OPENED 0x0008U /* NGHTTP3_CONN_FLAG_SHUTDOWN_COMMENCED is set when graceful shutdown has started. */ -#define NGHTTP3_CONN_FLAG_SHUTDOWN_COMMENCED 0x0010u +#define NGHTTP3_CONN_FLAG_SHUTDOWN_COMMENCED 0x0010U /* NGHTTP3_CONN_FLAG_GOAWAY_RECVED indicates that GOAWAY frame has received. */ -#define NGHTTP3_CONN_FLAG_GOAWAY_RECVED 0x0020u +#define NGHTTP3_CONN_FLAG_GOAWAY_RECVED 0x0020U /* NGHTTP3_CONN_FLAG_GOAWAY_QUEUED indicates that GOAWAY frame has been submitted for transmission. */ -#define NGHTTP3_CONN_FLAG_GOAWAY_QUEUED 0x0040u +#define NGHTTP3_CONN_FLAG_GOAWAY_QUEUED 0x0040U typedef struct nghttp3_chunk { nghttp3_opl_entry oplent; @@ -175,7 +175,8 @@ struct nghttp3_conn { } tx; }; -nghttp3_stream *nghttp3_conn_find_stream(nghttp3_conn *conn, int64_t stream_id); +nghttp3_stream *nghttp3_conn_find_stream(const nghttp3_conn *conn, + int64_t stream_id); int nghttp3_conn_create_stream(nghttp3_conn *conn, nghttp3_stream **pstream, int64_t stream_id); diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c index 3e14ffcd394b9b..031ac78d815f85 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c @@ -31,7 +31,7 @@ #include "nghttp3_str.h" #include "nghttp3_unreachable.h" -const uint8_t *nghttp3_get_varint(int64_t *dest, const uint8_t *p) { +const uint8_t *nghttp3_get_uvarint(uint64_t *dest, const uint8_t *p) { uint16_t n16; uint32_t n32; uint64_t n64; @@ -43,22 +43,22 @@ const uint8_t *nghttp3_get_varint(int64_t *dest, const uint8_t *p) { case 1: memcpy(&n16, p, 2); n16 = ntohs(n16); - n16 &= 0x3fff; + n16 &= 0x3FFFU; *dest = n16; return p + 2; case 2: memcpy(&n32, p, 4); n32 = ntohl(n32); - n32 &= 0x3fffffff; + n32 &= 0x3FFFFFFFU; *dest = n32; return p + 4; case 3: memcpy(&n64, p, 8); n64 = nghttp3_ntohl64(n64); - n64 &= 0x3fffffffffffffff; - *dest = (int64_t)n64; + n64 &= 0x3FFFFFFFFFFFFFFFU; + *dest = n64; return p + 8; default: @@ -66,8 +66,17 @@ const uint8_t *nghttp3_get_varint(int64_t *dest, const uint8_t *p) { } } -size_t nghttp3_get_varintlen(const uint8_t *p) { - return (size_t)(1u << (*p >> 6)); +size_t nghttp3_get_uvarintlen(const uint8_t *p) { + return (size_t)(1U << (*p >> 6)); +} + +const uint8_t *nghttp3_get_varint(int64_t *dest, const uint8_t *p) { + uint64_t n; + + p = nghttp3_get_uvarint(&n, p); + *dest = (int64_t)n; + + return p; } uint8_t *nghttp3_put_uint64be(uint8_t *p, uint64_t n) { @@ -85,7 +94,7 @@ uint8_t *nghttp3_put_uint16be(uint8_t *p, uint16_t n) { return nghttp3_cpymem(p, (const uint8_t *)&n, sizeof(n)); } -uint8_t *nghttp3_put_varint(uint8_t *p, int64_t n) { +uint8_t *nghttp3_put_uvarint(uint8_t *p, uint64_t n) { uint8_t *rv; if (n < 64) { *p++ = (uint8_t)n; @@ -93,21 +102,21 @@ uint8_t *nghttp3_put_varint(uint8_t *p, int64_t n) { } if (n < 16384) { rv = nghttp3_put_uint16be(p, (uint16_t)n); - *p |= 0x40; + *p |= 0x40U; return rv; } if (n < 1073741824) { rv = nghttp3_put_uint32be(p, (uint32_t)n); - *p |= 0x80; + *p |= 0x80U; return rv; } - assert(n < 4611686018427387904LL); - rv = nghttp3_put_uint64be(p, (uint64_t)n); - *p |= 0xc0; + assert(n < 4611686018427387904ULL); + rv = nghttp3_put_uint64be(p, n); + *p |= 0xC0U; return rv; } -size_t nghttp3_put_varintlen(int64_t n) { +size_t nghttp3_put_uvarintlen(uint64_t n) { if (n < 64) { return 1; } @@ -117,7 +126,7 @@ size_t nghttp3_put_varintlen(int64_t n) { if (n < 1073741824) { return 4; } - assert(n < 4611686018427387904LL); + assert(n < 4611686018427387904ULL); return 8; } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h index 8895fe8d3415d6..bd1c518fa6638d 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h @@ -56,7 +56,7 @@ #include -#define NGHTTP3_VARINT_MAX ((1ull << 62) - 1) +#define NGHTTP3_VARINT_MAX ((1ULL << 62) - 1) #if HAVE_DECL_BE64TOH # define nghttp3_ntohl64(N) be64toh(N) @@ -67,11 +67,11 @@ # define nghttp3_htonl64(N) (N) # else /* !defined(WORDS_BIGENDIAN) */ # if HAVE_DECL_BSWAP_64 -# define nghttp3_bswap64 bswap_64 +# define nghttp3_bswap64(N) bswap_64(N) # elif defined(WIN32) -# define nghttp3_bswap64 _byteswap_uint64 +# define nghttp3_bswap64(N) _byteswap_uint64(N) # elif defined(__APPLE__) -# define nghttp3_bswap64 OSSwapInt64 +# define nghttp3_bswap64(N) OSSwapInt64(N) # else /* !(HAVE_DECL_BSWAP_64 || defined(WIN32) || defined(__APPLE__)) */ # define nghttp3_bswap64(N) \ ((uint64_t)(ntohl((uint32_t)(N))) << 32 | ntohl((uint32_t)((N) >> 32))) @@ -82,67 +82,16 @@ #endif /* !HAVE_DECL_BE64TOH */ #ifdef WIN32 -/* Windows requires ws2_32 library for ntonl family of functions. We - define inline functions for those functions so that we don't have - dependency on that lib. */ - -# ifdef _MSC_VER -# define STIN static __inline -# else /* !defined(_MSC_VER) */ -# define STIN static inline -# endif /* !defined(_MSC_VER) */ - -STIN uint32_t htonl(uint32_t hostlong) { - uint32_t res; - unsigned char *p = (unsigned char *)&res; - *p++ = (unsigned char)(hostlong >> 24); - *p++ = (hostlong >> 16) & 0xffu; - *p++ = (hostlong >> 8) & 0xffu; - *p = hostlong & 0xffu; - return res; -} - -STIN uint16_t htons(uint16_t hostshort) { - uint16_t res; - unsigned char *p = (unsigned char *)&res; - *p++ = (unsigned char)(hostshort >> 8); - *p = hostshort & 0xffu; - return res; -} - -STIN uint32_t ntohl(uint32_t netlong) { - uint32_t res; - unsigned char *p = (unsigned char *)&netlong; - res = (uint32_t)(*p++ << 24); - res += (uint32_t)(*p++ << 16); - res += (uint32_t)(*p++ << 8); - res += *p; - return res; -} - -STIN uint16_t ntohs(uint16_t netshort) { - uint16_t res; - unsigned char *p = (unsigned char *)&netshort; - res = (uint16_t)(*p++ << 8); - res += *p; - return res; -} - +/* Windows requires ws2_32 library for ntonl family of functions. + Instead of using them, use _byteswap_* functions. This is fine + because all platforms that can run Windows these days are little + endian. */ +# define htonl(N) _byteswap_ulong(N) +# define htons(N) _byteswap_ushort(N) +# define ntohl(N) _byteswap_ulong(N) +# define ntohs(N) _byteswap_ushort(N) #endif /* defined(WIN32) */ -/* - * nghttp3_get_varint reads variable-length unsigned integer from |p|, - * and stores it in the buffer pointed by |dest| in host byte order. - * It returns |p| plus the number of bytes read from |p|. - */ -const uint8_t *nghttp3_get_varint(int64_t *dest, const uint8_t *p); - -/* - * nghttp3_get_varintlen returns the required number of bytes to read - * variable-length integer starting at |p|. - */ -size_t nghttp3_get_varintlen(const uint8_t *p); - /* * nghttp3_put_uint64be writes |n| in host byte order in |p| in * network byte order. It returns the one beyond of the last written @@ -164,27 +113,9 @@ uint8_t *nghttp3_put_uint32be(uint8_t *p, uint32_t n); */ uint8_t *nghttp3_put_uint16be(uint8_t *p, uint16_t n); -/* - * nghttp3_put_varint writes |n| in |p| using variable-length integer - * encoding. It returns the one beyond of the last written position. - */ -uint8_t *nghttp3_put_varint(uint8_t *p, int64_t n); - -/* - * nghttp3_put_varintlen returns the required number of bytes to - * encode |n|. - */ -size_t nghttp3_put_varintlen(int64_t n); - /* * nghttp3_ord_stream_id returns the ordinal number of |stream_id|. */ uint64_t nghttp3_ord_stream_id(int64_t stream_id); -/* - * NGHTTP3_PRI_INC_MASK is a bit mask to retrieve incremental bit from - * a value produced by nghttp3_pri_to_uint8. - */ -#define NGHTTP3_PRI_INC_MASK (1 << 7) - #endif /* !defined(NGHTTP3_CONV_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c index da88b54f96de65..2efba7472c251f 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c @@ -31,69 +31,70 @@ #include "nghttp3_conv.h" #include "nghttp3_str.h" -uint8_t *nghttp3_frame_write_hd(uint8_t *p, int64_t type, int64_t payloadlen) { - p = nghttp3_put_varint(p, type); - p = nghttp3_put_varint(p, payloadlen); +uint8_t *nghttp3_frame_write_hd(uint8_t *p, uint64_t type, + uint64_t payloadlen) { + p = nghttp3_put_uvarint(p, type); + p = nghttp3_put_uvarint(p, payloadlen); return p; } -size_t nghttp3_frame_write_hd_len(int64_t type, int64_t payloadlen) { - return nghttp3_put_varintlen(type) + nghttp3_put_varintlen(payloadlen); +size_t nghttp3_frame_write_hd_len(uint64_t type, uint64_t payloadlen) { + return nghttp3_put_uvarintlen(type) + nghttp3_put_uvarintlen(payloadlen); } uint8_t *nghttp3_frame_write_settings(uint8_t *p, const nghttp3_frame_settings *fr, - int64_t payloadlen) { + uint64_t payloadlen) { size_t i; p = nghttp3_frame_write_hd(p, fr->type, payloadlen); for (i = 0; i < fr->niv; ++i) { - p = nghttp3_put_varint(p, (int64_t)fr->iv[i].id); - p = nghttp3_put_varint(p, (int64_t)fr->iv[i].value); + p = nghttp3_put_uvarint(p, fr->iv[i].id); + p = nghttp3_put_uvarint(p, fr->iv[i].value); } return p; } -size_t nghttp3_frame_write_settings_len(int64_t *ppayloadlen, +size_t nghttp3_frame_write_settings_len(uint64_t *ppayloadlen, const nghttp3_frame_settings *fr) { size_t payloadlen = 0; size_t i; for (i = 0; i < fr->niv; ++i) { - payloadlen += nghttp3_put_varintlen((int64_t)fr->iv[i].id) + - nghttp3_put_varintlen((int64_t)fr->iv[i].value); + payloadlen += nghttp3_put_uvarintlen(fr->iv[i].id) + + nghttp3_put_uvarintlen(fr->iv[i].value); } - *ppayloadlen = (int64_t)payloadlen; + *ppayloadlen = payloadlen; - return nghttp3_put_varintlen(NGHTTP3_FRAME_SETTINGS) + - nghttp3_put_varintlen((int64_t)payloadlen) + payloadlen; + return nghttp3_put_uvarintlen(NGHTTP3_FRAME_SETTINGS) + + nghttp3_put_uvarintlen(payloadlen) + payloadlen; } uint8_t *nghttp3_frame_write_goaway(uint8_t *p, const nghttp3_frame_goaway *fr, - int64_t payloadlen) { + uint64_t payloadlen) { p = nghttp3_frame_write_hd(p, fr->type, payloadlen); - p = nghttp3_put_varint(p, fr->id); + p = nghttp3_put_uvarint(p, (uint64_t)fr->id); return p; } -size_t nghttp3_frame_write_goaway_len(int64_t *ppayloadlen, +size_t nghttp3_frame_write_goaway_len(uint64_t *ppayloadlen, const nghttp3_frame_goaway *fr) { - size_t payloadlen = nghttp3_put_varintlen(fr->id); + size_t payloadlen = nghttp3_put_uvarintlen((uint64_t)fr->id); - *ppayloadlen = (int64_t)payloadlen; + *ppayloadlen = payloadlen; - return nghttp3_put_varintlen(NGHTTP3_FRAME_GOAWAY) + - nghttp3_put_varintlen((int64_t)payloadlen) + payloadlen; + return nghttp3_put_uvarintlen(NGHTTP3_FRAME_GOAWAY) + + nghttp3_put_uvarintlen(payloadlen) + payloadlen; } uint8_t *nghttp3_frame_write_priority_update( - uint8_t *p, const nghttp3_frame_priority_update *fr, int64_t payloadlen) { + uint8_t *p, const nghttp3_frame_priority_update *fr, uint64_t payloadlen) { p = nghttp3_frame_write_hd(p, fr->type, payloadlen); - p = nghttp3_put_varint(p, fr->pri_elem_id); + p = nghttp3_put_uvarint(p, (uint64_t)fr->pri_elem_id); if (fr->datalen) { p = nghttp3_cpymem(p, fr->data, fr->datalen); } @@ -102,17 +103,18 @@ uint8_t *nghttp3_frame_write_priority_update( } size_t nghttp3_frame_write_priority_update_len( - int64_t *ppayloadlen, const nghttp3_frame_priority_update *fr) { - size_t payloadlen = nghttp3_put_varintlen(fr->pri_elem_id) + fr->datalen; + uint64_t *ppayloadlen, const nghttp3_frame_priority_update *fr) { + size_t payloadlen = + nghttp3_put_uvarintlen((uint64_t)fr->pri_elem_id) + fr->datalen; - *ppayloadlen = (int64_t)payloadlen; + *ppayloadlen = payloadlen; - return nghttp3_put_varintlen(fr->type) + - nghttp3_put_varintlen((int64_t)payloadlen) + payloadlen; + return nghttp3_put_uvarintlen(fr->type) + nghttp3_put_uvarintlen(payloadlen) + + payloadlen; } uint8_t *nghttp3_frame_write_origin(uint8_t *p, const nghttp3_frame_origin *fr, - int64_t payloadlen) { + uint64_t payloadlen) { p = nghttp3_frame_write_hd(p, fr->type, payloadlen); if (fr->origin_list.len) { p = nghttp3_cpymem(p, fr->origin_list.base, fr->origin_list.len); @@ -121,14 +123,14 @@ uint8_t *nghttp3_frame_write_origin(uint8_t *p, const nghttp3_frame_origin *fr, return p; } -size_t nghttp3_frame_write_origin_len(int64_t *ppayloadlen, +size_t nghttp3_frame_write_origin_len(uint64_t *ppayloadlen, const nghttp3_frame_origin *fr) { size_t payloadlen = fr->origin_list.len; - *ppayloadlen = (int64_t)payloadlen; + *ppayloadlen = payloadlen; - return nghttp3_put_varintlen(fr->type) + - nghttp3_put_varintlen((int64_t)payloadlen) + payloadlen; + return nghttp3_put_uvarintlen(fr->type) + nghttp3_put_uvarintlen(payloadlen) + + payloadlen; } int nghttp3_nva_copy(nghttp3_nv **pnva, const nghttp3_nv *nva, size_t nvlen, diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h index 2f35ff1d2b5fbd..7806cadbcf5f5a 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h @@ -34,50 +34,53 @@ #include "nghttp3_buf.h" -#define NGHTTP3_FRAME_DATA 0x00 -#define NGHTTP3_FRAME_HEADERS 0x01 -#define NGHTTP3_FRAME_CANCEL_PUSH 0x03 -#define NGHTTP3_FRAME_SETTINGS 0x04 -#define NGHTTP3_FRAME_PUSH_PROMISE 0x05 -#define NGHTTP3_FRAME_GOAWAY 0x07 -#define NGHTTP3_FRAME_MAX_PUSH_ID 0x0d +#define NGHTTP3_FRAME_DATA 0x00U +#define NGHTTP3_FRAME_HEADERS 0x01U +#define NGHTTP3_FRAME_CANCEL_PUSH 0x03U +#define NGHTTP3_FRAME_SETTINGS 0x04U +#define NGHTTP3_FRAME_PUSH_PROMISE 0x05U +#define NGHTTP3_FRAME_GOAWAY 0x07U +#define NGHTTP3_FRAME_MAX_PUSH_ID 0x0DU /* PRIORITY_UPDATE: https://datatracker.ietf.org/doc/html/rfc9218 */ -#define NGHTTP3_FRAME_PRIORITY_UPDATE 0x0f0700 -#define NGHTTP3_FRAME_PRIORITY_UPDATE_PUSH_ID 0x0f0701 +#define NGHTTP3_FRAME_PRIORITY_UPDATE 0x0F0700U +#define NGHTTP3_FRAME_PRIORITY_UPDATE_PUSH_ID 0x0F0701U /* ORIGIN: https://datatracker.ietf.org/doc/html/rfc9412 */ -#define NGHTTP3_FRAME_ORIGIN 0x0c +#define NGHTTP3_FRAME_ORIGIN 0x0CU /* Frame types that are reserved for HTTP/2, and must not be used in HTTP/3. */ -#define NGHTTP3_H2_FRAME_PRIORITY 0x02 -#define NGHTTP3_H2_FRAME_PING 0x06 -#define NGHTTP3_H2_FRAME_WINDOW_UPDATE 0x08 -#define NGHTTP3_H2_FRAME_CONTINUATION 0x9 +#define NGHTTP3_H2_FRAME_PRIORITY 0x02U +#define NGHTTP3_H2_FRAME_PING 0x06U +#define NGHTTP3_H2_FRAME_WINDOW_UPDATE 0x08U +#define NGHTTP3_H2_FRAME_CONTINUATION 0x9U typedef struct nghttp3_frame_hd { - int64_t type; + uint64_t type; } nghttp3_frame_hd; typedef struct nghttp3_frame_data { - int64_t type; + uint64_t type; + /* dr is set when sending DATA frame. It is not used on + reception. */ + nghttp3_data_reader dr; } nghttp3_frame_data; typedef struct nghttp3_frame_headers { - int64_t type; + uint64_t type; nghttp3_nv *nva; size_t nvlen; } nghttp3_frame_headers; -#define NGHTTP3_SETTINGS_ID_MAX_FIELD_SECTION_SIZE 0x06 -#define NGHTTP3_SETTINGS_ID_QPACK_MAX_TABLE_CAPACITY 0x01 -#define NGHTTP3_SETTINGS_ID_QPACK_BLOCKED_STREAMS 0x07 -#define NGHTTP3_SETTINGS_ID_ENABLE_CONNECT_PROTOCOL 0x08 -#define NGHTTP3_SETTINGS_ID_H3_DATAGRAM 0x33 +#define NGHTTP3_SETTINGS_ID_MAX_FIELD_SECTION_SIZE 0x06U +#define NGHTTP3_SETTINGS_ID_QPACK_MAX_TABLE_CAPACITY 0x01U +#define NGHTTP3_SETTINGS_ID_QPACK_BLOCKED_STREAMS 0x07U +#define NGHTTP3_SETTINGS_ID_ENABLE_CONNECT_PROTOCOL 0x08U +#define NGHTTP3_SETTINGS_ID_H3_DATAGRAM 0x33U -#define NGHTTP3_H2_SETTINGS_ID_ENABLE_PUSH 0x2 -#define NGHTTP3_H2_SETTINGS_ID_MAX_CONCURRENT_STREAMS 0x3 -#define NGHTTP3_H2_SETTINGS_ID_INITIAL_WINDOW_SIZE 0x4 -#define NGHTTP3_H2_SETTINGS_ID_MAX_FRAME_SIZE 0x5 +#define NGHTTP3_H2_SETTINGS_ID_ENABLE_PUSH 0x2U +#define NGHTTP3_H2_SETTINGS_ID_MAX_CONCURRENT_STREAMS 0x3U +#define NGHTTP3_H2_SETTINGS_ID_INITIAL_WINDOW_SIZE 0x4U +#define NGHTTP3_H2_SETTINGS_ID_MAX_FRAME_SIZE 0x5U typedef struct nghttp3_settings_entry { uint64_t id; @@ -85,18 +88,21 @@ typedef struct nghttp3_settings_entry { } nghttp3_settings_entry; typedef struct nghttp3_frame_settings { - int64_t type; + uint64_t type; size_t niv; nghttp3_settings_entry *iv; + /* local_settings is set when sending SETTINGS frame. It is not + used on reception. */ + const nghttp3_settings *local_settings; } nghttp3_frame_settings; typedef struct nghttp3_frame_goaway { - int64_t type; + uint64_t type; int64_t id; } nghttp3_frame_goaway; typedef struct nghttp3_frame_priority_update { - int64_t type; + uint64_t type; /* pri_elem_id is stream ID if type == NGHTTP3_FRAME_PRIORITY_UPDATE. It is push ID if type == NGHTTP3_FRAME_PRIORITY_UPDATE_PUSH_ID. It is undefined @@ -120,7 +126,7 @@ typedef struct nghttp3_frame_priority_update { } nghttp3_frame_priority_update; typedef struct nghttp3_frame_origin { - int64_t type; + uint64_t type; /* These fields are only used by server to send ORIGIN frame. Client never use them. */ nghttp3_vec origin_list; @@ -143,14 +149,14 @@ typedef union nghttp3_frame { * * This function returns |dest| plus the number of bytes written. */ -uint8_t *nghttp3_frame_write_hd(uint8_t *dest, int64_t type, - int64_t payloadlen); +uint8_t *nghttp3_frame_write_hd(uint8_t *dest, uint64_t type, + uint64_t payloadlen); /* * nghttp3_frame_write_hd_len returns the number of bytes required to * write a frame header consisting of |type| and |payloadlen|. */ -size_t nghttp3_frame_write_hd_len(int64_t type, int64_t payloadlen); +size_t nghttp3_frame_write_hd_len(uint64_t type, uint64_t payloadlen); /* * nghttp3_frame_write_settings writes SETTINGS frame |fr| to |dest|. @@ -161,14 +167,14 @@ size_t nghttp3_frame_write_hd_len(int64_t type, int64_t payloadlen); */ uint8_t *nghttp3_frame_write_settings(uint8_t *dest, const nghttp3_frame_settings *fr, - int64_t payloadlen); + uint64_t payloadlen); /* * nghttp3_frame_write_settings_len returns the number of bytes * required to write |fr|. This function stores the frame payload * length in |*ppayloadlen|. */ -size_t nghttp3_frame_write_settings_len(int64_t *pppayloadlen, +size_t nghttp3_frame_write_settings_len(uint64_t *pppayloadlen, const nghttp3_frame_settings *fr); /* @@ -180,14 +186,14 @@ size_t nghttp3_frame_write_settings_len(int64_t *pppayloadlen, */ uint8_t *nghttp3_frame_write_goaway(uint8_t *dest, const nghttp3_frame_goaway *fr, - int64_t payloadlen); + uint64_t payloadlen); /* * nghttp3_frame_write_goaway_len returns the number of bytes required * to write |fr|. This function stores the frame payload length in * |*ppayloadlen|. */ -size_t nghttp3_frame_write_goaway_len(int64_t *ppayloadlen, +size_t nghttp3_frame_write_goaway_len(uint64_t *ppayloadlen, const nghttp3_frame_goaway *fr); /* @@ -198,7 +204,7 @@ size_t nghttp3_frame_write_goaway_len(int64_t *ppayloadlen, * This function returns |dest| plus the number of bytes written; */ uint8_t *nghttp3_frame_write_priority_update( - uint8_t *dest, const nghttp3_frame_priority_update *fr, int64_t payloadlen); + uint8_t *dest, const nghttp3_frame_priority_update *fr, uint64_t payloadlen); /* * nghttp3_frame_write_priority_update_len returns the number of bytes @@ -206,7 +212,7 @@ uint8_t *nghttp3_frame_write_priority_update( * length in |*ppayloadlen|. */ size_t nghttp3_frame_write_priority_update_len( - int64_t *ppayloadlen, const nghttp3_frame_priority_update *fr); + uint64_t *ppayloadlen, const nghttp3_frame_priority_update *fr); /* * nghttp3_frame_write_origin writes ORIGIN frame |fr| to |dest|. @@ -217,14 +223,14 @@ size_t nghttp3_frame_write_priority_update_len( */ uint8_t *nghttp3_frame_write_origin(uint8_t *dest, const nghttp3_frame_origin *fr, - int64_t payloadlen); + uint64_t payloadlen); /* * nghttp3_frame_write_origin_len returns the number of bytes required * to write |fr|. This function stores the frame payload length in * |*ppayloadlen|. */ -size_t nghttp3_frame_write_origin_len(int64_t *ppayloadlen, +size_t nghttp3_frame_write_origin_len(uint64_t *ppayloadlen, const nghttp3_frame_origin *fr); /* diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_gaptr.c b/deps/ngtcp2/nghttp3/lib/nghttp3_gaptr.c index 68a4f8f747d682..bd557d08c34d51 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_gaptr.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_gaptr.c @@ -36,11 +36,11 @@ void nghttp3_gaptr_init(nghttp3_gaptr *gaptr, const nghttp3_mem *mem) { } static int gaptr_gap_init(nghttp3_gaptr *gaptr) { - return nghttp3_ksl_insert(&gaptr->gap, NULL, - &(nghttp3_range){ - .end = UINT64_MAX, - }, - NULL); + static const nghttp3_range end = { + .end = UINT64_MAX, + }; + + return nghttp3_ksl_insert(&gaptr->gap, NULL, &end, NULL); } void nghttp3_gaptr_free(nghttp3_gaptr *gaptr) { diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_http.c b/deps/ngtcp2/nghttp3/lib/nghttp3_http.c index a7524740ef7682..4194a404b33f97 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_http.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_http.c @@ -36,12 +36,9 @@ #include "nghttp3_macro.h" #include "nghttp3_conv.h" #include "nghttp3_unreachable.h" +#include "nghttp3_str.h" #include "sfparse/sfparse.h" -static uint8_t downcase(uint8_t c) { - return 'A' <= c && c <= 'Z' ? (uint8_t)(c - 'A' + 'a') : c; -} - /* * memieq returns 1 if the data pointed by |a| of length |n| equals to * |b| of the same length in case-insensitive manner. The data @@ -52,36 +49,49 @@ static int memieq(const void *a, const void *b, size_t n) { const uint8_t *aa = a, *bb = b; for (i = 0; i < n; ++i) { - if (aa[i] != downcase(bb[i])) { + if (aa[i] != nghttp3_downcase_byte(bb[i])) { return 0; } } return 1; } -#define lstrieq(A, B, N) ((sizeof((A)) - 1) == (N) && memieq((A), (B), (N))) +#define lstrieq(A, B, N) \ + (nghttp3_strlen_lit((A)) == (N) && memieq((A), (B), (N))) + +static int32_t parse_status_code(const uint8_t *s, size_t len) { + if (len != 3 || '1' > s[0] || s[0] > '9' || '0' > s[1] || s[1] > '9' || + '0' > s[2] || s[2] > '9') { + return -1; + } + + return (s[0] - '0') * 100 + (s[1] - '0') * 10 + (s[2] - '0'); +} static int64_t parse_uint(const uint8_t *s, size_t len) { - int64_t n = 0; + uint64_t n = 0; + uint32_t c; size_t i; + if (len == 0) { return -1; } + for (i = 0; i < len; ++i) { - if ('0' <= s[i] && s[i] <= '9') { - if (n > (int64_t)NGHTTP3_MAX_VARINT / 10) { - return -1; - } - n *= 10; - if (n > (int64_t)NGHTTP3_MAX_VARINT - (s[i] - '0')) { - return -1; - } - n += s[i] - '0'; - continue; + if ('0' > s[i] || s[i] > '9') { + return -1; } - return -1; + + c = s[i] - '0'; + + if (n > (NGHTTP3_MAX_VARINT - c) / 10) { + return -1; + } + + n = n * 10 + c; } - return n; + + return (int64_t)n; } static int check_pseudo_header(nghttp3_http_state *http, @@ -93,7 +103,7 @@ static int check_pseudo_header(nghttp3_http_state *http, return 1; } -static int expect_response_body(nghttp3_http_state *http) { +static int expect_response_body(const nghttp3_http_state *http) { return (http->flags & NGHTTP3_HTTP_FLAG_METH_HEAD) == 0 && http->status_code / 100 != 1 && http->status_code != 304 && http->status_code != 204; @@ -104,7 +114,7 @@ static int expect_response_body(nghttp3_http_state *http) { :path header field value must start with "/". This function must be called after ":method" header field was received. This function returns nonzero if path is valid.*/ -static int check_path_flags(nghttp3_http_state *http) { +static int check_path_flags(const nghttp3_http_state *http) { return (http->flags & NGHTTP3_HTTP_FLAG_SCHEME_HTTP) == 0 || ((http->flags & NGHTTP3_HTTP_FLAG_PATH_REGULAR) || ((http->flags & NGHTTP3_HTTP_FLAG_METH_OPTIONS) && @@ -274,26 +284,26 @@ static const int8_t VALID_PATH_CHARS[256] = { ['u'] = 1, ['v'] = 1, ['w'] = 1, ['x'] = 1, ['y'] = 1, ['z'] = 1, ['{'] = 1, ['|'] = 1, ['}'] = 1, ['~'] = 1, [0x80] = 1, [0x81] = 1, [0x82] = 1, [0x83] = 1, [0x84] = 1, [0x85] = 1, [0x86] = 1, [0x87] = 1, - [0x88] = 1, [0x89] = 1, [0x8a] = 1, [0x8b] = 1, [0x8c] = 1, [0x8d] = 1, - [0x8e] = 1, [0x8f] = 1, [0x90] = 1, [0x91] = 1, [0x92] = 1, [0x93] = 1, + [0x88] = 1, [0x89] = 1, [0x8A] = 1, [0x8B] = 1, [0x8C] = 1, [0x8D] = 1, + [0x8E] = 1, [0x8F] = 1, [0x90] = 1, [0x91] = 1, [0x92] = 1, [0x93] = 1, [0x94] = 1, [0x95] = 1, [0x96] = 1, [0x97] = 1, [0x98] = 1, [0x99] = 1, - [0x9a] = 1, [0x9b] = 1, [0x9c] = 1, [0x9d] = 1, [0x9e] = 1, [0x9f] = 1, - [0xa0] = 1, [0xa1] = 1, [0xa2] = 1, [0xa3] = 1, [0xa4] = 1, [0xa5] = 1, - [0xa6] = 1, [0xa7] = 1, [0xa8] = 1, [0xa9] = 1, [0xaa] = 1, [0xab] = 1, - [0xac] = 1, [0xad] = 1, [0xae] = 1, [0xaf] = 1, [0xb0] = 1, [0xb1] = 1, - [0xb2] = 1, [0xb3] = 1, [0xb4] = 1, [0xb5] = 1, [0xb6] = 1, [0xb7] = 1, - [0xb8] = 1, [0xb9] = 1, [0xba] = 1, [0xbb] = 1, [0xbc] = 1, [0xbd] = 1, - [0xbe] = 1, [0xbf] = 1, [0xc0] = 1, [0xc1] = 1, [0xc2] = 1, [0xc3] = 1, - [0xc4] = 1, [0xc5] = 1, [0xc6] = 1, [0xc7] = 1, [0xc8] = 1, [0xc9] = 1, - [0xca] = 1, [0xcb] = 1, [0xcc] = 1, [0xcd] = 1, [0xce] = 1, [0xcf] = 1, - [0xd0] = 1, [0xd1] = 1, [0xd2] = 1, [0xd3] = 1, [0xd4] = 1, [0xd5] = 1, - [0xd6] = 1, [0xd7] = 1, [0xd8] = 1, [0xd9] = 1, [0xda] = 1, [0xdb] = 1, - [0xdc] = 1, [0xdd] = 1, [0xde] = 1, [0xdf] = 1, [0xe0] = 1, [0xe1] = 1, - [0xe2] = 1, [0xe3] = 1, [0xe4] = 1, [0xe5] = 1, [0xe6] = 1, [0xe7] = 1, - [0xe8] = 1, [0xe9] = 1, [0xea] = 1, [0xeb] = 1, [0xec] = 1, [0xed] = 1, - [0xee] = 1, [0xef] = 1, [0xf0] = 1, [0xf1] = 1, [0xf2] = 1, [0xf3] = 1, - [0xf4] = 1, [0xf5] = 1, [0xf6] = 1, [0xf7] = 1, [0xf8] = 1, [0xf9] = 1, - [0xfa] = 1, [0xfb] = 1, [0xfc] = 1, [0xfd] = 1, [0xfe] = 1, [0xff] = 1, + [0x9A] = 1, [0x9B] = 1, [0x9C] = 1, [0x9D] = 1, [0x9E] = 1, [0x9F] = 1, + [0xA0] = 1, [0xA1] = 1, [0xA2] = 1, [0xA3] = 1, [0xA4] = 1, [0xA5] = 1, + [0xA6] = 1, [0xA7] = 1, [0xA8] = 1, [0xA9] = 1, [0xAA] = 1, [0xAB] = 1, + [0xAC] = 1, [0xAD] = 1, [0xAE] = 1, [0xAF] = 1, [0xB0] = 1, [0xB1] = 1, + [0xB2] = 1, [0xB3] = 1, [0xB4] = 1, [0xB5] = 1, [0xB6] = 1, [0xB7] = 1, + [0xB8] = 1, [0xB9] = 1, [0xBA] = 1, [0xBB] = 1, [0xBC] = 1, [0xBD] = 1, + [0xBE] = 1, [0xBF] = 1, [0xC0] = 1, [0xC1] = 1, [0xC2] = 1, [0xC3] = 1, + [0xC4] = 1, [0xC5] = 1, [0xC6] = 1, [0xC7] = 1, [0xC8] = 1, [0xC9] = 1, + [0xCA] = 1, [0xCB] = 1, [0xCC] = 1, [0xCD] = 1, [0xCE] = 1, [0xCF] = 1, + [0xD0] = 1, [0xD1] = 1, [0xD2] = 1, [0xD3] = 1, [0xD4] = 1, [0xD5] = 1, + [0xD6] = 1, [0xD7] = 1, [0xD8] = 1, [0xD9] = 1, [0xDA] = 1, [0xDB] = 1, + [0xDC] = 1, [0xDD] = 1, [0xDE] = 1, [0xDF] = 1, [0xE0] = 1, [0xE1] = 1, + [0xE2] = 1, [0xE3] = 1, [0xE4] = 1, [0xE5] = 1, [0xE6] = 1, [0xE7] = 1, + [0xE8] = 1, [0xE9] = 1, [0xEA] = 1, [0xEB] = 1, [0xEC] = 1, [0xED] = 1, + [0xEE] = 1, [0xEF] = 1, [0xF0] = 1, [0xF1] = 1, [0xF2] = 1, [0xF3] = 1, + [0xF4] = 1, [0xF5] = 1, [0xF6] = 1, [0xF7] = 1, [0xF8] = 1, [0xF9] = 1, + [0xFA] = 1, [0xFB] = 1, [0xFC] = 1, [0xFD] = 1, [0xFE] = 1, [0xFF] = 1, }; static int check_path(const uint8_t *value, size_t len) { @@ -307,7 +317,7 @@ static int check_path(const uint8_t *value, size_t len) { } static int http_request_on_header(nghttp3_http_state *http, - nghttp3_qpack_nv *nv, int trailers, + const nghttp3_qpack_nv *nv, int trailers, int connect_protocol) { nghttp3_pri pri; @@ -451,15 +461,14 @@ static int http_request_on_header(nghttp3_http_state *http, } static int http_response_on_header(nghttp3_http_state *http, - nghttp3_qpack_nv *nv, int trailers) { + const nghttp3_qpack_nv *nv, int trailers) { switch (nv->token) { case NGHTTP3_QPACK_TOKEN__STATUS: { - if (!check_pseudo_header(http, nv, NGHTTP3_HTTP_FLAG__STATUS) || - nv->value->len != 3) { + if (!check_pseudo_header(http, nv, NGHTTP3_HTTP_FLAG__STATUS)) { return NGHTTP3_ERR_MALFORMED_HTTP_HEADER; } - http->status_code = (int16_t)parse_uint(nv->value->base, nv->value->len); - if (http->status_code < 100 || http->status_code == 101) { + http->status_code = parse_status_code(nv->value->base, nv->value->len); + if (http->status_code == -1 || http->status_code == 101) { return NGHTTP3_ERR_MALFORMED_HTTP_HEADER; } break; @@ -526,7 +535,7 @@ static int http_response_on_header(nghttp3_http_state *http, static int http_check_nonempty_header_name(const uint8_t *name, size_t len); -int nghttp3_http_on_header(nghttp3_http_state *http, nghttp3_qpack_nv *nv, +int nghttp3_http_on_header(nghttp3_http_state *http, const nghttp3_qpack_nv *nv, int request, int trailers, int connect_protocol) { if (nv->name->len == 0) { http->flags |= NGHTTP3_HTTP_FLAG_PSEUDO_HEADER_DISALLOWED; @@ -576,10 +585,13 @@ int nghttp3_http_on_request_headers(nghttp3_http_state *http) { (NGHTTP3_HTTP_FLAG__AUTHORITY | NGHTTP3_HTTP_FLAG_HOST)) == 0) { return NGHTTP3_ERR_MALFORMED_HTTP_HEADER; } - if ((http->flags & NGHTTP3_HTTP_FLAG__PROTOCOL) && - ((http->flags & NGHTTP3_HTTP_FLAG_METH_CONNECT) == 0 || - (http->flags & NGHTTP3_HTTP_FLAG__AUTHORITY) == 0)) { - return NGHTTP3_ERR_MALFORMED_HTTP_HEADER; + if (http->flags & NGHTTP3_HTTP_FLAG__PROTOCOL) { + if ((http->flags & NGHTTP3_HTTP_FLAG_METH_CONNECT) == 0 || + (http->flags & NGHTTP3_HTTP_FLAG__AUTHORITY) == 0) { + return NGHTTP3_ERR_MALFORMED_HTTP_HEADER; + } + + http->content_length = -1; } if (!check_path_flags(http)) { return NGHTTP3_ERR_MALFORMED_HTTP_HEADER; @@ -614,7 +626,7 @@ int nghttp3_http_on_response_headers(nghttp3_http_state *http) { return 0; } -int nghttp3_http_on_remote_end_stream(nghttp3_stream *stream) { +int nghttp3_http_on_remote_end_stream(const nghttp3_stream *stream) { if ((stream->rx.http.flags & NGHTTP3_HTTP_FLAG_EXPECT_FINAL_RESPONSE) || (stream->rx.http.content_length != -1 && stream->rx.http.content_length != stream->rx.http.recv_content_length)) { @@ -689,7 +701,7 @@ int nghttp3_check_header_name(const uint8_t *name, size_t len) { --len; } for (last = name + len; name != last; ++name) { - if (!VALID_HD_NAME_CHARS[*name]) { + if (VALID_HD_NAME_CHARS[*name] != 1) { return 0; } } @@ -732,36 +744,36 @@ static const int8_t VALID_HD_VALUE_CHARS[256] = { ['s'] = 1, ['t'] = 1, ['u'] = 1, ['v'] = 1, ['w'] = 1, ['x'] = 1, ['y'] = 1, ['z'] = 1, ['{'] = 1, ['|'] = 1, ['}'] = 1, ['~'] = 1, [0x80] = 1, [0x81] = 1, [0x82] = 1, [0x83] = 1, [0x84] = 1, [0x85] = 1, - [0x86] = 1, [0x87] = 1, [0x88] = 1, [0x89] = 1, [0x8a] = 1, [0x8b] = 1, - [0x8c] = 1, [0x8d] = 1, [0x8e] = 1, [0x8f] = 1, [0x90] = 1, [0x91] = 1, + [0x86] = 1, [0x87] = 1, [0x88] = 1, [0x89] = 1, [0x8A] = 1, [0x8B] = 1, + [0x8C] = 1, [0x8D] = 1, [0x8E] = 1, [0x8F] = 1, [0x90] = 1, [0x91] = 1, [0x92] = 1, [0x93] = 1, [0x94] = 1, [0x95] = 1, [0x96] = 1, [0x97] = 1, - [0x98] = 1, [0x99] = 1, [0x9a] = 1, [0x9b] = 1, [0x9c] = 1, [0x9d] = 1, - [0x9e] = 1, [0x9f] = 1, [0xa0] = 1, [0xa1] = 1, [0xa2] = 1, [0xa3] = 1, - [0xa4] = 1, [0xa5] = 1, [0xa6] = 1, [0xa7] = 1, [0xa8] = 1, [0xa9] = 1, - [0xaa] = 1, [0xab] = 1, [0xac] = 1, [0xad] = 1, [0xae] = 1, [0xaf] = 1, - [0xb0] = 1, [0xb1] = 1, [0xb2] = 1, [0xb3] = 1, [0xb4] = 1, [0xb5] = 1, - [0xb6] = 1, [0xb7] = 1, [0xb8] = 1, [0xb9] = 1, [0xba] = 1, [0xbb] = 1, - [0xbc] = 1, [0xbd] = 1, [0xbe] = 1, [0xbf] = 1, [0xc0] = 1, [0xc1] = 1, - [0xc2] = 1, [0xc3] = 1, [0xc4] = 1, [0xc5] = 1, [0xc6] = 1, [0xc7] = 1, - [0xc8] = 1, [0xc9] = 1, [0xca] = 1, [0xcb] = 1, [0xcc] = 1, [0xcd] = 1, - [0xce] = 1, [0xcf] = 1, [0xd0] = 1, [0xd1] = 1, [0xd2] = 1, [0xd3] = 1, - [0xd4] = 1, [0xd5] = 1, [0xd6] = 1, [0xd7] = 1, [0xd8] = 1, [0xd9] = 1, - [0xda] = 1, [0xdb] = 1, [0xdc] = 1, [0xdd] = 1, [0xde] = 1, [0xdf] = 1, - [0xe0] = 1, [0xe1] = 1, [0xe2] = 1, [0xe3] = 1, [0xe4] = 1, [0xe5] = 1, - [0xe6] = 1, [0xe7] = 1, [0xe8] = 1, [0xe9] = 1, [0xea] = 1, [0xeb] = 1, - [0xec] = 1, [0xed] = 1, [0xee] = 1, [0xef] = 1, [0xf0] = 1, [0xf1] = 1, - [0xf2] = 1, [0xf3] = 1, [0xf4] = 1, [0xf5] = 1, [0xf6] = 1, [0xf7] = 1, - [0xf8] = 1, [0xf9] = 1, [0xfa] = 1, [0xfb] = 1, [0xfc] = 1, [0xfd] = 1, - [0xfe] = 1, [0xff] = 1, + [0x98] = 1, [0x99] = 1, [0x9A] = 1, [0x9B] = 1, [0x9C] = 1, [0x9D] = 1, + [0x9E] = 1, [0x9F] = 1, [0xA0] = 1, [0xA1] = 1, [0xA2] = 1, [0xA3] = 1, + [0xA4] = 1, [0xA5] = 1, [0xA6] = 1, [0xA7] = 1, [0xA8] = 1, [0xA9] = 1, + [0xAA] = 1, [0xAB] = 1, [0xAC] = 1, [0xAD] = 1, [0xAE] = 1, [0xAF] = 1, + [0xB0] = 1, [0xB1] = 1, [0xB2] = 1, [0xB3] = 1, [0xB4] = 1, [0xB5] = 1, + [0xB6] = 1, [0xB7] = 1, [0xB8] = 1, [0xB9] = 1, [0xBA] = 1, [0xBB] = 1, + [0xBC] = 1, [0xBD] = 1, [0xBE] = 1, [0xBF] = 1, [0xC0] = 1, [0xC1] = 1, + [0xC2] = 1, [0xC3] = 1, [0xC4] = 1, [0xC5] = 1, [0xC6] = 1, [0xC7] = 1, + [0xC8] = 1, [0xC9] = 1, [0xCA] = 1, [0xCB] = 1, [0xCC] = 1, [0xCD] = 1, + [0xCE] = 1, [0xCF] = 1, [0xD0] = 1, [0xD1] = 1, [0xD2] = 1, [0xD3] = 1, + [0xD4] = 1, [0xD5] = 1, [0xD6] = 1, [0xD7] = 1, [0xD8] = 1, [0xD9] = 1, + [0xDA] = 1, [0xDB] = 1, [0xDC] = 1, [0xDD] = 1, [0xDE] = 1, [0xDF] = 1, + [0xE0] = 1, [0xE1] = 1, [0xE2] = 1, [0xE3] = 1, [0xE4] = 1, [0xE5] = 1, + [0xE6] = 1, [0xE7] = 1, [0xE8] = 1, [0xE9] = 1, [0xEA] = 1, [0xEB] = 1, + [0xEC] = 1, [0xED] = 1, [0xEE] = 1, [0xEF] = 1, [0xF0] = 1, [0xF1] = 1, + [0xF2] = 1, [0xF3] = 1, [0xF4] = 1, [0xF5] = 1, [0xF6] = 1, [0xF7] = 1, + [0xF8] = 1, [0xF9] = 1, [0xFA] = 1, [0xFB] = 1, [0xFC] = 1, [0xFD] = 1, + [0xFE] = 1, [0xFF] = 1, }; #ifdef __AVX2__ static int contains_bad_header_value_char_avx2(const uint8_t *first, const uint8_t *last) { const __m256i ctll = _mm256_set1_epi8(0x00 - 1); - const __m256i ctlr = _mm256_set1_epi8(0x1f + 1); + const __m256i ctlr = _mm256_set1_epi8(0x1F + 1); const __m256i ht = _mm256_set1_epi8('\t'); - const __m256i del = _mm256_set1_epi8(0x7f); + const __m256i del = _mm256_set1_epi8(0x7F); __m256i s, x; uint32_t m; @@ -808,7 +820,7 @@ int nghttp3_check_header_value(const uint8_t *value, size_t len) { #ifdef __AVX2__ if (len >= 32) { - last32 = value + (len & ~0x1fu); + last32 = value + (len & ~(size_t)0x1FU); if (contains_bad_header_value_char_avx2(value, last32)) { return 0; } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_http.h b/deps/ngtcp2/nghttp3/lib/nghttp3_http.h index f0bfc69fbade75..2bdf3110027c15 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_http.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_http.h @@ -39,48 +39,48 @@ typedef struct nghttp3_http_state nghttp3_http_state; /* HTTP related flags to enforce HTTP semantics */ /* NGHTTP3_HTTP_FLAG_NONE indicates that no flag is set. */ -#define NGHTTP3_HTTP_FLAG_NONE 0x00u +#define NGHTTP3_HTTP_FLAG_NONE 0x00U /* header field seen so far */ -#define NGHTTP3_HTTP_FLAG__AUTHORITY 0x01u -#define NGHTTP3_HTTP_FLAG__PATH 0x02u -#define NGHTTP3_HTTP_FLAG__METHOD 0x04u -#define NGHTTP3_HTTP_FLAG__SCHEME 0x08u +#define NGHTTP3_HTTP_FLAG__AUTHORITY 0x01U +#define NGHTTP3_HTTP_FLAG__PATH 0x02U +#define NGHTTP3_HTTP_FLAG__METHOD 0x04U +#define NGHTTP3_HTTP_FLAG__SCHEME 0x08U /* host is not pseudo header, but we require either host or :authority */ -#define NGHTTP3_HTTP_FLAG_HOST 0x10u -#define NGHTTP3_HTTP_FLAG__STATUS 0x20u +#define NGHTTP3_HTTP_FLAG_HOST 0x10U +#define NGHTTP3_HTTP_FLAG__STATUS 0x20U /* required header fields for HTTP request except for CONNECT method. */ #define NGHTTP3_HTTP_FLAG_REQ_HEADERS \ (NGHTTP3_HTTP_FLAG__METHOD | NGHTTP3_HTTP_FLAG__PATH | \ NGHTTP3_HTTP_FLAG__SCHEME) -#define NGHTTP3_HTTP_FLAG_PSEUDO_HEADER_DISALLOWED 0x40u +#define NGHTTP3_HTTP_FLAG_PSEUDO_HEADER_DISALLOWED 0x40U /* HTTP method flags */ -#define NGHTTP3_HTTP_FLAG_METH_CONNECT 0x80u -#define NGHTTP3_HTTP_FLAG_METH_HEAD 0x0100u -#define NGHTTP3_HTTP_FLAG_METH_OPTIONS 0x0200u +#define NGHTTP3_HTTP_FLAG_METH_CONNECT 0x80U +#define NGHTTP3_HTTP_FLAG_METH_HEAD 0x0100U +#define NGHTTP3_HTTP_FLAG_METH_OPTIONS 0x0200U #define NGHTTP3_HTTP_FLAG_METH_ALL \ (NGHTTP3_HTTP_FLAG_METH_CONNECT | NGHTTP3_HTTP_FLAG_METH_HEAD | \ NGHTTP3_HTTP_FLAG_METH_OPTIONS) /* :path category */ /* path starts with "/" */ -#define NGHTTP3_HTTP_FLAG_PATH_REGULAR 0x0400u +#define NGHTTP3_HTTP_FLAG_PATH_REGULAR 0x0400U /* path "*" */ -#define NGHTTP3_HTTP_FLAG_PATH_ASTERISK 0x0800u +#define NGHTTP3_HTTP_FLAG_PATH_ASTERISK 0x0800U /* scheme */ /* "http" or "https" scheme */ -#define NGHTTP3_HTTP_FLAG_SCHEME_HTTP 0x1000u +#define NGHTTP3_HTTP_FLAG_SCHEME_HTTP 0x1000U /* set if final response is expected */ -#define NGHTTP3_HTTP_FLAG_EXPECT_FINAL_RESPONSE 0x2000u +#define NGHTTP3_HTTP_FLAG_EXPECT_FINAL_RESPONSE 0x2000U /* NGHTTP3_HTTP_FLAG__PROTOCOL is set when :protocol pseudo header field is seen. */ -#define NGHTTP3_HTTP_FLAG__PROTOCOL 0x4000u +#define NGHTTP3_HTTP_FLAG__PROTOCOL 0x4000U /* NGHTTP3_HTTP_FLAG_PRIORITY is set when priority header field is processed. */ -#define NGHTTP3_HTTP_FLAG_PRIORITY 0x8000u +#define NGHTTP3_HTTP_FLAG_PRIORITY 0x8000U /* NGHTTP3_HTTP_FLAG_BAD_PRIORITY is set when an error is encountered while parsing priority header field. */ -#define NGHTTP3_HTTP_FLAG_BAD_PRIORITY 0x010000u +#define NGHTTP3_HTTP_FLAG_BAD_PRIORITY 0x010000U /* * This function is called when HTTP header field |nv| received for @@ -98,7 +98,7 @@ typedef struct nghttp3_http_state nghttp3_http_state; * Invalid HTTP header field was received but it can be treated as * if it was not received because of compatibility reasons. */ -int nghttp3_http_on_header(nghttp3_http_state *http, nghttp3_qpack_nv *nv, +int nghttp3_http_on_header(nghttp3_http_state *http, const nghttp3_qpack_nv *nv, int request, int trailers, int connect_protocol); /* @@ -131,7 +131,7 @@ int nghttp3_http_on_response_headers(nghttp3_http_state *http); * NGHTTP3_ERR_MALFORMED_HTTP_MESSAGING * HTTP messaging is violated. */ -int nghttp3_http_on_remote_end_stream(nghttp3_stream *stream); +int nghttp3_http_on_remote_end_stream(const nghttp3_stream *stream); /* * This function is called when chunk of data is received. This diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c b/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c index 669b59efa7e0aa..efb4f958a85eb5 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c @@ -32,7 +32,6 @@ #include "nghttp3_macro.h" #include "nghttp3_mem.h" -#include "nghttp3_range.h" static nghttp3_ksl_blk null_blk; @@ -57,13 +56,14 @@ void nghttp3_ksl_init(nghttp3_ksl *ksl, nghttp3_ksl_compar compar, assert(keylen >= sizeof(uint64_t)); - aligned_keylen = (keylen + 0x7u) & ~0x7u; + aligned_keylen = (keylen + 0x7U) & ~(size_t)0x7U; - nghttp3_objalloc_init(&ksl->blkalloc, - (ksl_blklen(aligned_keylen) + 0xfu) & ~(uintptr_t)0xfu, - mem); + assert(aligned_keylen <= UINT16_MAX); - ksl->head = NULL; + nghttp3_objalloc_init( + &ksl->blkalloc, (ksl_blklen(aligned_keylen) + 0xFU) & ~(size_t)0xFU, mem); + + ksl->root = NULL; ksl->front = ksl->back = NULL; ksl->compar = compar; ksl->search = search; @@ -81,6 +81,7 @@ static nghttp3_ksl_blk *ksl_blk_objalloc_new(nghttp3_ksl *ksl) { } blk->keys = (uint8_t *)blk + sizeof(*blk); + blk->aligned_keylen = (uint16_t)ksl->aligned_keylen; return blk; } @@ -89,19 +90,19 @@ static void ksl_blk_objalloc_del(nghttp3_ksl *ksl, nghttp3_ksl_blk *blk) { nghttp3_objalloc_ksl_blk_release(&ksl->blkalloc, blk); } -static int ksl_head_init(nghttp3_ksl *ksl) { - nghttp3_ksl_blk *head = ksl_blk_objalloc_new(ksl); +static int ksl_root_init(nghttp3_ksl *ksl) { + nghttp3_ksl_blk *root = ksl_blk_objalloc_new(ksl); - if (!head) { + if (!root) { return NGHTTP3_ERR_NOMEM; } - head->next = head->prev = NULL; - head->n = 0; - head->leaf = 1; + root->next = root->prev = NULL; + root->n = 0; + root->leaf = 1; - ksl->head = head; - ksl->front = ksl->back = head; + ksl->root = root; + ksl->front = ksl->back = root; return 0; } @@ -124,12 +125,12 @@ static void ksl_free_blk(nghttp3_ksl *ksl, nghttp3_ksl_blk *blk) { #endif /* defined(NOMEMPOOL) */ void nghttp3_ksl_free(nghttp3_ksl *ksl) { - if (!ksl || !ksl->head) { + if (!ksl || !ksl->root) { return; } #ifdef NOMEMPOOL - ksl_free_blk(ksl, ksl->head); + ksl_free_blk(ksl, ksl->root); #endif /* defined(NOMEMPOOL) */ nghttp3_objalloc_free(&ksl->blkalloc); @@ -199,22 +200,21 @@ static int ksl_split_node(nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, size_t i) { memmove(blk->nodes + (i + 2), blk->nodes + (i + 1), (blk->n - (i + 1)) * sizeof(nghttp3_ksl_node)); - memmove(blk->keys + (i + 2) * ksl->aligned_keylen, - blk->keys + (i + 1) * ksl->aligned_keylen, - (blk->n - (i + 1)) * ksl->aligned_keylen); + memmove(blk->keys + (i + 1) * ksl->aligned_keylen, + blk->keys + i * ksl->aligned_keylen, + (blk->n - i) * ksl->aligned_keylen); blk->nodes[i + 1].blk = rblk; ++blk->n; - ksl_set_nth_key(ksl, blk, i + 1, nghttp3_ksl_nth_key(ksl, rblk, rblk->n - 1)); - ksl_set_nth_key(ksl, blk, i, nghttp3_ksl_nth_key(ksl, lblk, lblk->n - 1)); + ksl_set_nth_key(ksl, blk, i, nghttp3_ksl_blk_nth_key(lblk, lblk->n - 1)); return 0; } /* - * ksl_split_head splits a head (root) block. It increases the height - * of skip list by 1. + * ksl_split_root splits a root block. It increases the height of + * skip list by 1. * * It returns 0 if it succeeds, or one of the following negative error * codes: @@ -222,34 +222,34 @@ static int ksl_split_node(nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, size_t i) { * NGHTTP3_ERR_NOMEM * Out of memory. */ -static int ksl_split_head(nghttp3_ksl *ksl) { - nghttp3_ksl_blk *rblk = NULL, *lblk, *nhead = NULL; +static int ksl_split_root(nghttp3_ksl *ksl) { + nghttp3_ksl_blk *rblk = NULL, *lblk, *nroot = NULL; - rblk = ksl_split_blk(ksl, ksl->head); + rblk = ksl_split_blk(ksl, ksl->root); if (rblk == NULL) { return NGHTTP3_ERR_NOMEM; } - lblk = ksl->head; + lblk = ksl->root; - nhead = ksl_blk_objalloc_new(ksl); + nroot = ksl_blk_objalloc_new(ksl); - if (nhead == NULL) { + if (nroot == NULL) { ksl_blk_objalloc_del(ksl, rblk); return NGHTTP3_ERR_NOMEM; } - nhead->next = nhead->prev = NULL; - nhead->n = 2; - nhead->leaf = 0; + nroot->next = nroot->prev = NULL; + nroot->n = 2; + nroot->leaf = 0; - ksl_set_nth_key(ksl, nhead, 0, nghttp3_ksl_nth_key(ksl, lblk, lblk->n - 1)); - nhead->nodes[0].blk = lblk; + ksl_set_nth_key(ksl, nroot, 0, nghttp3_ksl_blk_nth_key(lblk, lblk->n - 1)); + nroot->nodes[0].blk = lblk; - ksl_set_nth_key(ksl, nhead, 1, nghttp3_ksl_nth_key(ksl, rblk, rblk->n - 1)); - nhead->nodes[1].blk = rblk; + ksl_set_nth_key(ksl, nroot, 1, nghttp3_ksl_blk_nth_key(rblk, rblk->n - 1)); + nroot->nodes[1].blk = rblk; - ksl->head = nhead; + ksl->root = nroot; return 0; } @@ -284,27 +284,27 @@ int nghttp3_ksl_insert(nghttp3_ksl *ksl, nghttp3_ksl_it *it, size_t i; int rv; - if (!ksl->head) { - rv = ksl_head_init(ksl); + if (!ksl->root) { + rv = ksl_root_init(ksl); if (rv != 0) { return rv; } } - if (ksl->head->n == NGHTTP3_KSL_MAX_NBLK) { - rv = ksl_split_head(ksl); + if (ksl->root->n == NGHTTP3_KSL_MAX_NBLK) { + rv = ksl_split_root(ksl); if (rv != 0) { return rv; } } - blk = ksl->head; + blk = ksl->root; for (;;) { i = ksl->search(ksl, blk, key); if (blk->leaf) { - if (i < blk->n && !ksl->compar(key, nghttp3_ksl_nth_key(ksl, blk, i))) { + if (i < blk->n && !ksl->compar(key, nghttp3_ksl_blk_nth_key(blk, i))) { if (it) { *it = nghttp3_ksl_end(ksl); } @@ -316,7 +316,7 @@ int nghttp3_ksl_insert(nghttp3_ksl *ksl, nghttp3_ksl_it *it, ++ksl->n; if (it) { - nghttp3_ksl_it_init(it, ksl, blk, i); + nghttp3_ksl_it_init(it, blk, i); } return 0; @@ -343,7 +343,7 @@ int nghttp3_ksl_insert(nghttp3_ksl *ksl, nghttp3_ksl_it *it, ++ksl->n; if (it) { - nghttp3_ksl_it_init(it, ksl, blk, blk->n - 1); + nghttp3_ksl_it_init(it, blk, blk->n - 1); } return 0; @@ -357,12 +357,8 @@ int nghttp3_ksl_insert(nghttp3_ksl *ksl, nghttp3_ksl_it *it, return rv; } - if (ksl->compar(nghttp3_ksl_nth_key(ksl, blk, i), key)) { + if (ksl->compar(nghttp3_ksl_blk_nth_key(blk, i), key)) { node = &blk->nodes[i + 1]; - - if (ksl->compar(nghttp3_ksl_nth_key(ksl, blk, i + 1), key)) { - ksl_set_nth_key(ksl, blk, i + 1, key); - } } } @@ -389,9 +385,9 @@ static void ksl_remove_node(nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, size_t i) { * ksl_merge_node merges 2 nodes which are the nodes at the index of * |i| and |i + 1|. * - * If |blk| is the head (root) block and it contains just 2 nodes - * before merging nodes, the merged block becomes head block, which - * decreases the height of |ksl| by 1. + * If |blk| is the root block and it contains just 2 nodes before + * merging nodes, the merged block becomes root block, which decreases + * the height of |ksl| by 1. * * This function returns the pointer to the merged block. */ @@ -407,7 +403,7 @@ static nghttp3_ksl_blk *ksl_merge_node(nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, lblk = lnode->blk; rblk = blk->nodes[i + 1].blk; - assert(lblk->n + rblk->n < NGHTTP3_KSL_MAX_NBLK); + assert(lblk->n + rblk->n <= NGHTTP3_KSL_MAX_NBLK); memcpy(lblk->nodes + lblk->n, rblk->nodes, rblk->n * sizeof(nghttp3_ksl_node)); @@ -426,12 +422,12 @@ static nghttp3_ksl_blk *ksl_merge_node(nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, ksl_blk_objalloc_del(ksl, rblk); - if (ksl->head == blk && blk->n == 2) { - ksl_blk_objalloc_del(ksl, ksl->head); - ksl->head = lblk; + if (ksl->root == blk && blk->n == 2) { + ksl_blk_objalloc_del(ksl, ksl->root); + ksl->root = lblk; } else { ksl_remove_node(ksl, blk, i + 1); - ksl_set_nth_key(ksl, blk, i, nghttp3_ksl_nth_key(ksl, lblk, lblk->n - 1)); + ksl_set_nth_key(ksl, blk, i, nghttp3_ksl_blk_nth_key(lblk, lblk->n - 1)); } return lblk; @@ -472,7 +468,7 @@ static void ksl_shift_left(nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, size_t i) { lblk->n += (uint32_t)n; rblk->n -= (uint32_t)n; - ksl_set_nth_key(ksl, blk, i - 1, nghttp3_ksl_nth_key(ksl, lblk, lblk->n - 1)); + ksl_set_nth_key(ksl, blk, i - 1, nghttp3_ksl_blk_nth_key(lblk, lblk->n - 1)); memmove(rblk->nodes, rblk->nodes + n, rblk->n * sizeof(nghttp3_ksl_node)); @@ -520,7 +516,7 @@ static void ksl_shift_right(nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, size_t i) { memcpy(rblk->keys, lblk->keys + lblk->n * ksl->aligned_keylen, n * ksl->aligned_keylen); - ksl_set_nth_key(ksl, blk, i, nghttp3_ksl_nth_key(ksl, lblk, lblk->n - 1)); + ksl_set_nth_key(ksl, blk, i, nghttp3_ksl_blk_nth_key(lblk, lblk->n - 1)); } /* @@ -537,9 +533,9 @@ int nghttp3_ksl_remove_hint(nghttp3_ksl *ksl, nghttp3_ksl_it *it, const nghttp3_ksl_key *key) { nghttp3_ksl_blk *blk = hint->blk; - assert(ksl->head); + assert(ksl->root); - if (blk->n <= NGHTTP3_KSL_MIN_NBLK) { + if (blk != ksl->root && blk->n == NGHTTP3_KSL_MIN_NBLK) { return nghttp3_ksl_remove(ksl, it, key); } @@ -549,9 +545,9 @@ int nghttp3_ksl_remove_hint(nghttp3_ksl *ksl, nghttp3_ksl_it *it, if (it) { if (hint->i == blk->n && blk->next) { - nghttp3_ksl_it_init(it, ksl, blk->next, 0); + nghttp3_ksl_it_init(it, blk->next, 0); } else { - nghttp3_ksl_it_init(it, ksl, blk, hint->i); + nghttp3_ksl_it_init(it, blk, hint->i); } } @@ -560,7 +556,7 @@ int nghttp3_ksl_remove_hint(nghttp3_ksl *ksl, nghttp3_ksl_it *it, int nghttp3_ksl_remove(nghttp3_ksl *ksl, nghttp3_ksl_it *it, const nghttp3_ksl_key *key) { - nghttp3_ksl_blk *blk = ksl->head; + nghttp3_ksl_blk *blk = ksl->root; nghttp3_ksl_node *node; size_t i; @@ -586,7 +582,7 @@ int nghttp3_ksl_remove(nghttp3_ksl *ksl, nghttp3_ksl_it *it, } if (blk->leaf) { - if (ksl->compar(key, nghttp3_ksl_nth_key(ksl, blk, i))) { + if (ksl->compar(key, nghttp3_ksl_blk_nth_key(blk, i))) { if (it) { *it = nghttp3_ksl_end(ksl); } @@ -599,9 +595,9 @@ int nghttp3_ksl_remove(nghttp3_ksl *ksl, nghttp3_ksl_it *it, if (it) { if (blk->n == i && blk->next) { - nghttp3_ksl_it_init(it, ksl, blk->next, 0); + nghttp3_ksl_it_init(it, blk->next, 0); } else { - nghttp3_ksl_it_init(it, ksl, blk, i); + nghttp3_ksl_it_init(it, blk, i); } } @@ -650,12 +646,12 @@ nghttp3_ksl_it nghttp3_ksl_lower_bound(const nghttp3_ksl *ksl, nghttp3_ksl_it nghttp3_ksl_lower_bound_search(const nghttp3_ksl *ksl, const nghttp3_ksl_key *key, nghttp3_ksl_search search) { - nghttp3_ksl_blk *blk = ksl->head; + nghttp3_ksl_blk *blk = ksl->root; nghttp3_ksl_it it; size_t i; if (!blk) { - nghttp3_ksl_it_init(&it, ksl, &null_blk, 0); + nghttp3_ksl_it_init(&it, &null_blk, 0); return it; } @@ -668,7 +664,7 @@ nghttp3_ksl_it nghttp3_ksl_lower_bound_search(const nghttp3_ksl *ksl, i = 0; } - nghttp3_ksl_it_init(&it, ksl, blk, i); + nghttp3_ksl_it_init(&it, blk, i); return it; } @@ -686,7 +682,7 @@ nghttp3_ksl_it nghttp3_ksl_lower_bound_search(const nghttp3_ksl *ksl, i = blk->n; } - nghttp3_ksl_it_init(&it, ksl, blk, i); + nghttp3_ksl_it_init(&it, blk, i); return it; } @@ -697,19 +693,19 @@ nghttp3_ksl_it nghttp3_ksl_lower_bound_search(const nghttp3_ksl *ksl, void nghttp3_ksl_update_key(nghttp3_ksl *ksl, const nghttp3_ksl_key *old_key, const nghttp3_ksl_key *new_key) { - nghttp3_ksl_blk *blk = ksl->head; + nghttp3_ksl_blk *blk = ksl->root; nghttp3_ksl_node *node; const nghttp3_ksl_key *node_key; size_t i; - assert(ksl->head); + assert(ksl->root); for (;;) { i = ksl->search(ksl, blk, old_key); assert(i < blk->n); node = &blk->nodes[i]; - node_key = nghttp3_ksl_nth_key(ksl, blk, i); + node_key = nghttp3_ksl_blk_nth_key(blk, i); if (blk->leaf) { assert(key_equal(ksl->compar, node_key, old_key)); @@ -730,15 +726,15 @@ void nghttp3_ksl_update_key(nghttp3_ksl *ksl, const nghttp3_ksl_key *old_key, size_t nghttp3_ksl_len(const nghttp3_ksl *ksl) { return ksl->n; } void nghttp3_ksl_clear(nghttp3_ksl *ksl) { - if (!ksl->head) { + if (!ksl->root) { return; } #ifdef NOMEMPOOL - ksl_free_blk(ksl, ksl->head); + ksl_free_blk(ksl, ksl->root); #endif /* defined(NOMEMPOOL) */ - ksl->front = ksl->back = ksl->head = NULL; + ksl->front = ksl->back = ksl->root = NULL; ksl->n = 0; nghttp3_objalloc_clear(&ksl->blkalloc); @@ -753,8 +749,7 @@ static void ksl_print(const nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, if (blk->leaf) { for (i = 0; i < blk->n; ++i) { - fprintf(stderr, " %" PRId64, - *(int64_t *)nghttp3_ksl_nth_key(ksl, blk, i)); + fprintf(stderr, " %" PRId64, *(int64_t *)nghttp3_ksl_blk_nth_key(blk, i)); } fprintf(stderr, "\n"); @@ -768,21 +763,21 @@ static void ksl_print(const nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, } void nghttp3_ksl_print(const nghttp3_ksl *ksl) { - if (!ksl->head) { + if (!ksl->root) { return; } - ksl_print(ksl, ksl->head, 0); + ksl_print(ksl, ksl->root, 0); } #endif /* !defined(WIN32) */ nghttp3_ksl_it nghttp3_ksl_begin(const nghttp3_ksl *ksl) { nghttp3_ksl_it it; - if (ksl->head) { - nghttp3_ksl_it_init(&it, ksl, ksl->front, 0); + if (ksl->root) { + nghttp3_ksl_it_init(&it, ksl->front, 0); } else { - nghttp3_ksl_it_init(&it, ksl, &null_blk, 0); + nghttp3_ksl_it_init(&it, &null_blk, 0); } return it; @@ -791,18 +786,16 @@ nghttp3_ksl_it nghttp3_ksl_begin(const nghttp3_ksl *ksl) { nghttp3_ksl_it nghttp3_ksl_end(const nghttp3_ksl *ksl) { nghttp3_ksl_it it; - if (ksl->head) { - nghttp3_ksl_it_init(&it, ksl, ksl->back, ksl->back->n); + if (ksl->root) { + nghttp3_ksl_it_init(&it, ksl->back, ksl->back->n); } else { - nghttp3_ksl_it_init(&it, ksl, &null_blk, 0); + nghttp3_ksl_it_init(&it, &null_blk, 0); } return it; } -void nghttp3_ksl_it_init(nghttp3_ksl_it *it, const nghttp3_ksl *ksl, - nghttp3_ksl_blk *blk, size_t i) { - it->ksl = ksl; +void nghttp3_ksl_it_init(nghttp3_ksl_it *it, nghttp3_ksl_blk *blk, size_t i) { it->blk = blk; it->i = i; } @@ -822,12 +815,6 @@ int nghttp3_ksl_it_begin(const nghttp3_ksl_it *it) { return it->i == 0 && it->blk->prev == NULL; } -int nghttp3_ksl_range_compar(const nghttp3_ksl_key *lhs, - const nghttp3_ksl_key *rhs) { - const nghttp3_range *a = lhs, *b = rhs; - return a->begin < b->begin; -} - nghttp3_ksl_search_def(range, nghttp3_ksl_range_compar) size_t nghttp3_ksl_range_search(const nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, @@ -835,13 +822,6 @@ size_t nghttp3_ksl_range_search(const nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, return ksl_range_search(ksl, blk, key); } -int nghttp3_ksl_range_exclusive_compar(const nghttp3_ksl_key *lhs, - const nghttp3_ksl_key *rhs) { - const nghttp3_range *a = lhs, *b = rhs; - return a->begin < b->begin && !(nghttp3_max_uint64(a->begin, b->begin) < - nghttp3_min_uint64(a->end, b->end)); -} - nghttp3_ksl_search_def(range_exclusive, nghttp3_ksl_range_exclusive_compar) size_t nghttp3_ksl_range_exclusive_search(const nghttp3_ksl *ksl, @@ -850,11 +830,6 @@ size_t nghttp3_ksl_range_exclusive_search(const nghttp3_ksl *ksl, return ksl_range_exclusive_search(ksl, blk, key); } -int nghttp3_ksl_uint64_less(const nghttp3_ksl_key *lhs, - const nghttp3_ksl_key *rhs) { - return *(uint64_t *)lhs < *(uint64_t *)rhs; -} - nghttp3_ksl_search_def(uint64_less, nghttp3_ksl_uint64_less) size_t nghttp3_ksl_uint64_less_search(const nghttp3_ksl *ksl, @@ -863,11 +838,6 @@ size_t nghttp3_ksl_uint64_less_search(const nghttp3_ksl *ksl, return ksl_uint64_less_search(ksl, blk, key); } -int nghttp3_ksl_int64_greater(const nghttp3_ksl_key *lhs, - const nghttp3_ksl_key *rhs) { - return *(int64_t *)lhs > *(int64_t *)rhs; -} - nghttp3_ksl_search_def(int64_greater, nghttp3_ksl_int64_greater) size_t nghttp3_ksl_int64_greater_search(const nghttp3_ksl *ksl, diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.h b/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.h index 0b5663d2995932..852e4d471f23d7 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.h @@ -35,14 +35,15 @@ #include #include "nghttp3_objalloc.h" +#include "nghttp3_range.h" #define NGHTTP3_KSL_DEGR 16 /* NGHTTP3_KSL_MAX_NBLK is the maximum number of nodes which a single block can contain. */ -#define NGHTTP3_KSL_MAX_NBLK (2 * NGHTTP3_KSL_DEGR - 1) +#define NGHTTP3_KSL_MAX_NBLK (2 * NGHTTP3_KSL_DEGR) /* NGHTTP3_KSL_MIN_NBLK is the minimum number of nodes which a single block other than root must contain. */ -#define NGHTTP3_KSL_MIN_NBLK (NGHTTP3_KSL_DEGR - 1) +#define NGHTTP3_KSL_MIN_NBLK NGHTTP3_KSL_DEGR /* * nghttp3_ksl_key represents key in nghttp3_ksl. @@ -76,16 +77,19 @@ struct nghttp3_ksl_blk { /* prev points to the previous block if leaf field is nonzero. */ nghttp3_ksl_blk *prev; - /* n is the number of nodes this object contains in nodes. */ - uint32_t n; - /* leaf is nonzero if this block contains leaf nodes. */ - uint32_t leaf; nghttp3_ksl_node nodes[NGHTTP3_KSL_MAX_NBLK]; /* keys is a pointer to the buffer to include NGHTTP3_KSL_MAX_NBLK keys. Because the length of key is unknown until nghttp3_ksl_init is called, the actual buffer - will be allocated after this field. */ + will be allocated after this object. */ uint8_t *keys; + /* n is the number of nodes this object contains in nodes. */ + uint32_t n; + /* aligned_keylen is the length of the single key including + alignment. */ + uint16_t aligned_keylen; + /* leaf is nonzero if this block contains leaf nodes. */ + uint8_t leaf; }; nghttp3_opl_entry oplent; @@ -137,7 +141,6 @@ typedef struct nghttp3_ksl_it nghttp3_ksl_it; * nghttp3_ksl_it is a bidirectional iterator to iterate nodes. */ struct nghttp3_ksl_it { - const nghttp3_ksl *ksl; nghttp3_ksl_blk *blk; size_t i; }; @@ -147,8 +150,8 @@ struct nghttp3_ksl_it { */ struct nghttp3_ksl { nghttp3_objalloc blkalloc; - /* head points to the root block. */ - nghttp3_ksl_blk *head; + /* root points to the root block. */ + nghttp3_ksl_blk *root; /* front points to the first leaf block. */ nghttp3_ksl_blk *front; /* back points to the last leaf block. */ @@ -278,12 +281,11 @@ size_t nghttp3_ksl_len(const nghttp3_ksl *ksl); void nghttp3_ksl_clear(nghttp3_ksl *ksl); /* - * nghttp3_ksl_nth_key returns the |n|th key under |blk|. + * nghttp3_ksl_blk_nth_key returns the |n|th key under |blk|. */ static inline const nghttp3_ksl_key * -nghttp3_ksl_nth_key(const nghttp3_ksl *ksl, const nghttp3_ksl_blk *blk, - size_t n) { - return blk->keys + n * ksl->aligned_keylen; +nghttp3_ksl_blk_nth_key(const nghttp3_ksl_blk *blk, size_t n) { + return blk->keys + n * blk->aligned_keylen; } #ifndef WIN32 @@ -298,8 +300,7 @@ void nghttp3_ksl_print(const nghttp3_ksl *ksl); /* * nghttp3_ksl_it_init initializes |it|. */ -void nghttp3_ksl_it_init(nghttp3_ksl_it *it, const nghttp3_ksl *ksl, - nghttp3_ksl_blk *blk, size_t i); +void nghttp3_ksl_it_init(nghttp3_ksl_it *it, nghttp3_ksl_blk *blk, size_t i); /* * nghttp3_ksl_it_get returns the data associated to the node which @@ -351,7 +352,7 @@ int nghttp3_ksl_it_begin(const nghttp3_ksl_it *it); */ static inline const nghttp3_ksl_key * nghttp3_ksl_it_key(const nghttp3_ksl_it *it) { - return nghttp3_ksl_nth_key(it->ksl, it->blk, it->i); + return nghttp3_ksl_blk_nth_key(it->blk, it->i); } /* @@ -360,8 +361,12 @@ nghttp3_ksl_it_key(const nghttp3_ksl_it *it) { * object, and the function returns nonzero if ((const nghttp3_range * *)lhs)->begin < ((const nghttp3_range *)rhs)->begin. */ -int nghttp3_ksl_range_compar(const nghttp3_ksl_key *lhs, - const nghttp3_ksl_key *rhs); +static inline int nghttp3_ksl_range_compar(const nghttp3_ksl_key *lhs, + const nghttp3_ksl_key *rhs) { + const nghttp3_range *a = (const nghttp3_range *)lhs, + *b = (const nghttp3_range *)rhs; + return a->begin < b->begin; +} /* * nghttp3_ksl_range_search is an implementation of nghttp3_ksl_search @@ -377,8 +382,14 @@ size_t nghttp3_ksl_range_search(const nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, * *)lhs)->begin < ((const nghttp3_range *)rhs)->begin, and the 2 * ranges do not intersect. */ -int nghttp3_ksl_range_exclusive_compar(const nghttp3_ksl_key *lhs, - const nghttp3_ksl_key *rhs); +static inline int +nghttp3_ksl_range_exclusive_compar(const nghttp3_ksl_key *lhs, + const nghttp3_ksl_key *rhs) { + const nghttp3_range *a = (const nghttp3_range *)lhs, + *b = (const nghttp3_range *)rhs; + return a->begin < b->begin && + !(nghttp3_max(a->begin, b->begin) < nghttp3_min(a->end, b->end)); +} /* * nghttp3_ksl_range_exclusive_search is an implementation of @@ -393,8 +404,10 @@ size_t nghttp3_ksl_range_exclusive_search(const nghttp3_ksl *ksl, * |lhs| and |rhs| must point to uint64_t objects, and the function * returns nonzero if *(uint64_t *)|lhs| < *(uint64_t *)|rhs|. */ -int nghttp3_ksl_uint64_less(const nghttp3_ksl_key *lhs, - const nghttp3_ksl_key *rhs); +static inline int nghttp3_ksl_uint64_less(const nghttp3_ksl_key *lhs, + const nghttp3_ksl_key *rhs) { + return *(const uint64_t *)lhs < *(const uint64_t *)rhs; +} /* * nghttp3_ksl_uint64_less_search is an implementation of @@ -410,8 +423,10 @@ size_t nghttp3_ksl_uint64_less_search(const nghttp3_ksl *ksl, * and the function returns nonzero if *(int64_t *)|lhs| > *(int64_t * *)|rhs|. */ -int nghttp3_ksl_int64_greater(const nghttp3_ksl_key *lhs, - const nghttp3_ksl_key *rhs); +static inline int nghttp3_ksl_int64_greater(const nghttp3_ksl_key *lhs, + const nghttp3_ksl_key *rhs) { + return *(const int64_t *)lhs > *(const int64_t *)rhs; +} /* * nghttp3_ksl_int64_greater_search is an implementation of diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_macro.h b/deps/ngtcp2/nghttp3/lib/nghttp3_macro.h index ffe0ea70924327..2b03dc53dbb7a0 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_macro.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_macro.h @@ -39,7 +39,15 @@ #define nghttp3_arraylen(A) (sizeof(A) / sizeof(*(A))) -#define lstreq(A, B, N) ((sizeof((A)) - 1) == (N) && memcmp((A), (B), (N)) == 0) +/* + * nghttp3_strlen_lit returns the length of string literal |S|. This + * macro assumes |S| is NULL-terminated string literal. It must not + * be used with pointers. + */ +#define nghttp3_strlen_lit(S) (sizeof(S) - 1) + +#define lstreq(A, B, N) \ + (nghttp3_strlen_lit((A)) == (N) && memcmp((A), (B), (N)) == 0) /* NGHTTP3_MAX_VARINT` is the maximum value which can be encoded in variable-length integer encoding. */ @@ -48,27 +56,81 @@ #define nghttp3_max_def(SUFFIX, T) \ static inline T nghttp3_max_##SUFFIX(T a, T b) { return a < b ? b : a; } -nghttp3_max_def(int8, int8_t) -nghttp3_max_def(int16, int16_t) -nghttp3_max_def(int32, int32_t) -nghttp3_max_def(int64, int64_t) -nghttp3_max_def(uint8, uint8_t) -nghttp3_max_def(uint16, uint16_t) -nghttp3_max_def(uint32, uint32_t) -nghttp3_max_def(uint64, uint64_t) -nghttp3_max_def(size, size_t) +nghttp3_max_def(long_long_int, long long int) +nghttp3_max_def(long_int, long int) +nghttp3_max_def(int, int) +nghttp3_max_def(short_int, short int) +nghttp3_max_def(signed_char, signed char) +nghttp3_max_def(char, char) +nghttp3_max_def(unsigned_long_long_int, unsigned long long int) +nghttp3_max_def(unsigned_long_int, unsigned long int) +nghttp3_max_def(unsigned_int, unsigned int) +nghttp3_max_def(unsigned_short_int, unsigned short int) +nghttp3_max_def(unsigned_char, unsigned char) + +#define nghttp3_max(A, B) \ + _Generic((A), \ + long long int: nghttp3_max_long_long_int, \ + long int: nghttp3_max_long_int, \ + int: _Generic((B), \ + long long int: nghttp3_max_long_long_int, \ + long int: nghttp3_max_long_int, \ + int: nghttp3_max_int, \ + short int: nghttp3_max_short_int, \ + signed char: nghttp3_max_signed_char, \ + char: nghttp3_max_char, \ + unsigned long long int: nghttp3_max_unsigned_long_long_int, \ + unsigned long int: nghttp3_max_unsigned_long_int, \ + unsigned int: nghttp3_max_unsigned_int, \ + unsigned short int: nghttp3_max_unsigned_short_int, \ + unsigned char: nghttp3_max_unsigned_char), \ + short int: nghttp3_max_short_int, \ + signed char: nghttp3_max_signed_char, \ + char: nghttp3_max_char, \ + unsigned long long int: nghttp3_max_unsigned_long_long_int, \ + unsigned long int: nghttp3_max_unsigned_long_int, \ + unsigned int: nghttp3_max_unsigned_int, \ + unsigned short int: nghttp3_max_unsigned_short_int, \ + unsigned char: nghttp3_max_unsigned_char)((A), (B)) #define nghttp3_min_def(SUFFIX, T) \ static inline T nghttp3_min_##SUFFIX(T a, T b) { return a < b ? a : b; } -nghttp3_min_def(int8, int8_t) -nghttp3_min_def(int16, int16_t) -nghttp3_min_def(int32, int32_t) -nghttp3_min_def(int64, int64_t) -nghttp3_min_def(uint8, uint8_t) -nghttp3_min_def(uint16, uint16_t) -nghttp3_min_def(uint32, uint32_t) -nghttp3_min_def(uint64, uint64_t) -nghttp3_min_def(size, size_t) +nghttp3_min_def(long_long_int, long long int) +nghttp3_min_def(long_int, long int) +nghttp3_min_def(int, int) +nghttp3_min_def(short_int, short int) +nghttp3_min_def(signed_char, signed char) +nghttp3_min_def(char, char) +nghttp3_min_def(unsigned_long_long_int, unsigned long long int) +nghttp3_min_def(unsigned_long_int, unsigned long int) +nghttp3_min_def(unsigned_int, unsigned int) +nghttp3_min_def(unsigned_short_int, unsigned short int) +nghttp3_min_def(unsigned_char, unsigned char) + +#define nghttp3_min(A, B) \ + _Generic((A), \ + long long int: nghttp3_min_long_long_int, \ + long int: nghttp3_min_long_int, \ + int: _Generic((B), \ + long long int: nghttp3_min_long_long_int, \ + long int: nghttp3_min_long_int, \ + int: nghttp3_min_int, \ + short int: nghttp3_min_short_int, \ + signed char: nghttp3_min_signed_char, \ + char: nghttp3_min_char, \ + unsigned long long int: nghttp3_min_unsigned_long_long_int, \ + unsigned long int: nghttp3_min_unsigned_long_int, \ + unsigned int: nghttp3_min_unsigned_int, \ + unsigned short int: nghttp3_min_unsigned_short_int, \ + unsigned char: nghttp3_min_unsigned_char), \ + short int: nghttp3_min_short_int, \ + signed char: nghttp3_min_signed_char, \ + char: nghttp3_min_char, \ + unsigned long long int: nghttp3_min_unsigned_long_long_int, \ + unsigned long int: nghttp3_min_unsigned_long_int, \ + unsigned int: nghttp3_min_unsigned_int, \ + unsigned short int: nghttp3_min_unsigned_short_int, \ + unsigned char: nghttp3_min_unsigned_char)((A), (B)) #endif /* !defined(NGHTTP3_MACRO_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_map.c b/deps/ngtcp2/nghttp3/lib/nghttp3_map.c index d5b720b148f89a..7858d4cc3eb10b 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_map.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_map.c @@ -78,10 +78,10 @@ int nghttp3_map_each(const nghttp3_map *map, int (*func)(void *data, void *ptr), /* Hasher from https://github.com/rust-lang/rustc-hash/blob/dc5c33f1283de2da64d8d7a06401d91aded03ad4/src/lib.rs to maximize the output's sensitivity to all input bits. */ -#define NGHTTP3_MAP_HASHER 0xf1357aea2e62a9c5ull +#define NGHTTP3_MAP_HASHER 0xF1357AEA2E62A9C5ULL /* 64-bit Fibonacci hashing constant, Golden Ratio constant, to get the high bits with the good distribution. */ -#define NGHTTP3_MAP_FIBO 0x9e3779b97f4a7c15ull +#define NGHTTP3_MAP_FIBO 0x9E3779B97F4A7C15ULL static size_t map_index(const nghttp3_map *map, nghttp3_map_key_type key) { key += map->seed; diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_objalloc.h b/deps/ngtcp2/nghttp3/lib/nghttp3_objalloc.h index 4f8ffa093751a7..b3c17046607218 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_objalloc.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_objalloc.h @@ -70,7 +70,7 @@ void nghttp3_objalloc_clear(nghttp3_objalloc *objalloc); inline static void nghttp3_objalloc_##NAME##_init( \ nghttp3_objalloc *objalloc, size_t nmemb, const nghttp3_mem *mem) { \ nghttp3_objalloc_init( \ - objalloc, ((sizeof(TYPE) + 0xfu) & ~(uintptr_t)0xfu) * nmemb, mem); \ + objalloc, ((sizeof(TYPE) + 0xFU) & ~(size_t)0xFU) * nmemb, mem); \ } \ \ TYPE *nghttp3_objalloc_##NAME##_get(nghttp3_objalloc *objalloc); \ @@ -124,7 +124,7 @@ void nghttp3_objalloc_clear(nghttp3_objalloc *objalloc); inline static void nghttp3_objalloc_##NAME##_init( \ nghttp3_objalloc *objalloc, size_t nmemb, const nghttp3_mem *mem) { \ nghttp3_objalloc_init( \ - objalloc, ((sizeof(TYPE) + 0xfu) & ~(uintptr_t)0xfu) * nmemb, mem); \ + objalloc, ((sizeof(TYPE) + 0xFU) & ~(size_t)0xFU) * nmemb, mem); \ } \ \ inline static TYPE *nghttp3_objalloc_##NAME##_get( \ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_pq.c b/deps/ngtcp2/nghttp3/lib/nghttp3_pq.c index e35bcac4e43b8f..c5b657bc6d9fb3 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_pq.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_pq.c @@ -76,7 +76,7 @@ int nghttp3_pq_push(nghttp3_pq *pq, nghttp3_pq_entry *item) { void *nq; size_t ncapacity; - ncapacity = nghttp3_max_size(4, pq->capacity * 2); + ncapacity = nghttp3_max(4, pq->capacity * 2); nq = nghttp3_mem_realloc(pq->mem, pq->q, ncapacity * sizeof(nghttp3_pq_entry *)); diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c index ece873a8f2cf5f..59b633ea057254 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c @@ -50,122 +50,122 @@ /* Generated by mkstatichdtbl.py */ static nghttp3_qpack_static_entry token_stable[] = { - MAKE_STATIC_ENT(0, NGHTTP3_QPACK_TOKEN__AUTHORITY, 3153725150u), - MAKE_STATIC_ENT(15, NGHTTP3_QPACK_TOKEN__METHOD, 695666056u), - MAKE_STATIC_ENT(16, NGHTTP3_QPACK_TOKEN__METHOD, 695666056u), - MAKE_STATIC_ENT(17, NGHTTP3_QPACK_TOKEN__METHOD, 695666056u), - MAKE_STATIC_ENT(18, NGHTTP3_QPACK_TOKEN__METHOD, 695666056u), - MAKE_STATIC_ENT(19, NGHTTP3_QPACK_TOKEN__METHOD, 695666056u), - MAKE_STATIC_ENT(20, NGHTTP3_QPACK_TOKEN__METHOD, 695666056u), - MAKE_STATIC_ENT(21, NGHTTP3_QPACK_TOKEN__METHOD, 695666056u), - MAKE_STATIC_ENT(1, NGHTTP3_QPACK_TOKEN__PATH, 3292848686u), - MAKE_STATIC_ENT(22, NGHTTP3_QPACK_TOKEN__SCHEME, 2510477674u), - MAKE_STATIC_ENT(23, NGHTTP3_QPACK_TOKEN__SCHEME, 2510477674u), - MAKE_STATIC_ENT(24, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(25, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(26, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(27, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(28, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(63, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(64, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(65, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(66, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(67, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(68, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(69, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(70, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(71, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983u), - MAKE_STATIC_ENT(29, NGHTTP3_QPACK_TOKEN_ACCEPT, 136609321u), - MAKE_STATIC_ENT(30, NGHTTP3_QPACK_TOKEN_ACCEPT, 136609321u), - MAKE_STATIC_ENT(31, NGHTTP3_QPACK_TOKEN_ACCEPT_ENCODING, 3379649177u), - MAKE_STATIC_ENT(72, NGHTTP3_QPACK_TOKEN_ACCEPT_LANGUAGE, 1979086614u), - MAKE_STATIC_ENT(32, NGHTTP3_QPACK_TOKEN_ACCEPT_RANGES, 1713753958u), + MAKE_STATIC_ENT(0, NGHTTP3_QPACK_TOKEN__AUTHORITY, 3153725150U), + MAKE_STATIC_ENT(15, NGHTTP3_QPACK_TOKEN__METHOD, 695666056U), + MAKE_STATIC_ENT(16, NGHTTP3_QPACK_TOKEN__METHOD, 695666056U), + MAKE_STATIC_ENT(17, NGHTTP3_QPACK_TOKEN__METHOD, 695666056U), + MAKE_STATIC_ENT(18, NGHTTP3_QPACK_TOKEN__METHOD, 695666056U), + MAKE_STATIC_ENT(19, NGHTTP3_QPACK_TOKEN__METHOD, 695666056U), + MAKE_STATIC_ENT(20, NGHTTP3_QPACK_TOKEN__METHOD, 695666056U), + MAKE_STATIC_ENT(21, NGHTTP3_QPACK_TOKEN__METHOD, 695666056U), + MAKE_STATIC_ENT(1, NGHTTP3_QPACK_TOKEN__PATH, 3292848686U), + MAKE_STATIC_ENT(22, NGHTTP3_QPACK_TOKEN__SCHEME, 2510477674U), + MAKE_STATIC_ENT(23, NGHTTP3_QPACK_TOKEN__SCHEME, 2510477674U), + MAKE_STATIC_ENT(24, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(25, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(26, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(27, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(28, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(63, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(64, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(65, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(66, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(67, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(68, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(69, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(70, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(71, NGHTTP3_QPACK_TOKEN__STATUS, 4000288983U), + MAKE_STATIC_ENT(29, NGHTTP3_QPACK_TOKEN_ACCEPT, 136609321U), + MAKE_STATIC_ENT(30, NGHTTP3_QPACK_TOKEN_ACCEPT, 136609321U), + MAKE_STATIC_ENT(31, NGHTTP3_QPACK_TOKEN_ACCEPT_ENCODING, 3379649177U), + MAKE_STATIC_ENT(72, NGHTTP3_QPACK_TOKEN_ACCEPT_LANGUAGE, 1979086614U), + MAKE_STATIC_ENT(32, NGHTTP3_QPACK_TOKEN_ACCEPT_RANGES, 1713753958U), MAKE_STATIC_ENT(73, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_ALLOW_CREDENTIALS, - 901040780u), + 901040780U), MAKE_STATIC_ENT(74, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_ALLOW_CREDENTIALS, - 901040780u), + 901040780U), MAKE_STATIC_ENT(33, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_ALLOW_HEADERS, - 1524311232u), + 1524311232U), MAKE_STATIC_ENT(34, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_ALLOW_HEADERS, - 1524311232u), + 1524311232U), MAKE_STATIC_ENT(75, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_ALLOW_HEADERS, - 1524311232u), + 1524311232U), MAKE_STATIC_ENT(76, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_ALLOW_METHODS, - 2175229868u), + 2175229868U), MAKE_STATIC_ENT(77, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_ALLOW_METHODS, - 2175229868u), + 2175229868U), MAKE_STATIC_ENT(78, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_ALLOW_METHODS, - 2175229868u), + 2175229868U), MAKE_STATIC_ENT(35, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_ALLOW_ORIGIN, - 2710797292u), + 2710797292U), MAKE_STATIC_ENT(79, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_EXPOSE_HEADERS, - 2449824425u), + 2449824425U), MAKE_STATIC_ENT(80, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_REQUEST_HEADERS, - 3599549072u), + 3599549072U), MAKE_STATIC_ENT(81, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_REQUEST_METHOD, - 2417078055u), + 2417078055U), MAKE_STATIC_ENT(82, NGHTTP3_QPACK_TOKEN_ACCESS_CONTROL_REQUEST_METHOD, - 2417078055u), - MAKE_STATIC_ENT(2, NGHTTP3_QPACK_TOKEN_AGE, 742476188u), - MAKE_STATIC_ENT(83, NGHTTP3_QPACK_TOKEN_ALT_SVC, 2148877059u), - MAKE_STATIC_ENT(84, NGHTTP3_QPACK_TOKEN_AUTHORIZATION, 2436257726u), - MAKE_STATIC_ENT(36, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669u), - MAKE_STATIC_ENT(37, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669u), - MAKE_STATIC_ENT(38, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669u), - MAKE_STATIC_ENT(39, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669u), - MAKE_STATIC_ENT(40, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669u), - MAKE_STATIC_ENT(41, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669u), - MAKE_STATIC_ENT(3, NGHTTP3_QPACK_TOKEN_CONTENT_DISPOSITION, 3889184348u), - MAKE_STATIC_ENT(42, NGHTTP3_QPACK_TOKEN_CONTENT_ENCODING, 65203592u), - MAKE_STATIC_ENT(43, NGHTTP3_QPACK_TOKEN_CONTENT_ENCODING, 65203592u), - MAKE_STATIC_ENT(4, NGHTTP3_QPACK_TOKEN_CONTENT_LENGTH, 1308181789u), - MAKE_STATIC_ENT(85, NGHTTP3_QPACK_TOKEN_CONTENT_SECURITY_POLICY, 1569039836u), - MAKE_STATIC_ENT(44, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277u), - MAKE_STATIC_ENT(45, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277u), - MAKE_STATIC_ENT(46, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277u), - MAKE_STATIC_ENT(47, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277u), - MAKE_STATIC_ENT(48, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277u), - MAKE_STATIC_ENT(49, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277u), - MAKE_STATIC_ENT(50, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277u), - MAKE_STATIC_ENT(51, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277u), - MAKE_STATIC_ENT(52, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277u), - MAKE_STATIC_ENT(53, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277u), - MAKE_STATIC_ENT(54, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277u), - MAKE_STATIC_ENT(5, NGHTTP3_QPACK_TOKEN_COOKIE, 2007449791u), - MAKE_STATIC_ENT(6, NGHTTP3_QPACK_TOKEN_DATE, 3564297305u), - MAKE_STATIC_ENT(86, NGHTTP3_QPACK_TOKEN_EARLY_DATA, 4080895051u), - MAKE_STATIC_ENT(7, NGHTTP3_QPACK_TOKEN_ETAG, 113792960u), - MAKE_STATIC_ENT(87, NGHTTP3_QPACK_TOKEN_EXPECT_CT, 1183214960u), - MAKE_STATIC_ENT(88, NGHTTP3_QPACK_TOKEN_FORWARDED, 1485178027u), - MAKE_STATIC_ENT(8, NGHTTP3_QPACK_TOKEN_IF_MODIFIED_SINCE, 2213050793u), - MAKE_STATIC_ENT(9, NGHTTP3_QPACK_TOKEN_IF_NONE_MATCH, 2536202615u), - MAKE_STATIC_ENT(89, NGHTTP3_QPACK_TOKEN_IF_RANGE, 2340978238u), - MAKE_STATIC_ENT(10, NGHTTP3_QPACK_TOKEN_LAST_MODIFIED, 3226950251u), - MAKE_STATIC_ENT(11, NGHTTP3_QPACK_TOKEN_LINK, 232457833u), - MAKE_STATIC_ENT(12, NGHTTP3_QPACK_TOKEN_LOCATION, 200649126u), - MAKE_STATIC_ENT(90, NGHTTP3_QPACK_TOKEN_ORIGIN, 3649018447u), - MAKE_STATIC_ENT(91, NGHTTP3_QPACK_TOKEN_PURPOSE, 4212263681u), - MAKE_STATIC_ENT(55, NGHTTP3_QPACK_TOKEN_RANGE, 4208725202u), - MAKE_STATIC_ENT(13, NGHTTP3_QPACK_TOKEN_REFERER, 3969579366u), - MAKE_STATIC_ENT(92, NGHTTP3_QPACK_TOKEN_SERVER, 1085029842u), - MAKE_STATIC_ENT(14, NGHTTP3_QPACK_TOKEN_SET_COOKIE, 1848371000u), + 2417078055U), + MAKE_STATIC_ENT(2, NGHTTP3_QPACK_TOKEN_AGE, 742476188U), + MAKE_STATIC_ENT(83, NGHTTP3_QPACK_TOKEN_ALT_SVC, 2148877059U), + MAKE_STATIC_ENT(84, NGHTTP3_QPACK_TOKEN_AUTHORIZATION, 2436257726U), + MAKE_STATIC_ENT(36, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669U), + MAKE_STATIC_ENT(37, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669U), + MAKE_STATIC_ENT(38, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669U), + MAKE_STATIC_ENT(39, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669U), + MAKE_STATIC_ENT(40, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669U), + MAKE_STATIC_ENT(41, NGHTTP3_QPACK_TOKEN_CACHE_CONTROL, 1355326669U), + MAKE_STATIC_ENT(3, NGHTTP3_QPACK_TOKEN_CONTENT_DISPOSITION, 3889184348U), + MAKE_STATIC_ENT(42, NGHTTP3_QPACK_TOKEN_CONTENT_ENCODING, 65203592U), + MAKE_STATIC_ENT(43, NGHTTP3_QPACK_TOKEN_CONTENT_ENCODING, 65203592U), + MAKE_STATIC_ENT(4, NGHTTP3_QPACK_TOKEN_CONTENT_LENGTH, 1308181789U), + MAKE_STATIC_ENT(85, NGHTTP3_QPACK_TOKEN_CONTENT_SECURITY_POLICY, 1569039836U), + MAKE_STATIC_ENT(44, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277U), + MAKE_STATIC_ENT(45, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277U), + MAKE_STATIC_ENT(46, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277U), + MAKE_STATIC_ENT(47, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277U), + MAKE_STATIC_ENT(48, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277U), + MAKE_STATIC_ENT(49, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277U), + MAKE_STATIC_ENT(50, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277U), + MAKE_STATIC_ENT(51, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277U), + MAKE_STATIC_ENT(52, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277U), + MAKE_STATIC_ENT(53, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277U), + MAKE_STATIC_ENT(54, NGHTTP3_QPACK_TOKEN_CONTENT_TYPE, 4244048277U), + MAKE_STATIC_ENT(5, NGHTTP3_QPACK_TOKEN_COOKIE, 2007449791U), + MAKE_STATIC_ENT(6, NGHTTP3_QPACK_TOKEN_DATE, 3564297305U), + MAKE_STATIC_ENT(86, NGHTTP3_QPACK_TOKEN_EARLY_DATA, 4080895051U), + MAKE_STATIC_ENT(7, NGHTTP3_QPACK_TOKEN_ETAG, 113792960U), + MAKE_STATIC_ENT(87, NGHTTP3_QPACK_TOKEN_EXPECT_CT, 1183214960U), + MAKE_STATIC_ENT(88, NGHTTP3_QPACK_TOKEN_FORWARDED, 1485178027U), + MAKE_STATIC_ENT(8, NGHTTP3_QPACK_TOKEN_IF_MODIFIED_SINCE, 2213050793U), + MAKE_STATIC_ENT(9, NGHTTP3_QPACK_TOKEN_IF_NONE_MATCH, 2536202615U), + MAKE_STATIC_ENT(89, NGHTTP3_QPACK_TOKEN_IF_RANGE, 2340978238U), + MAKE_STATIC_ENT(10, NGHTTP3_QPACK_TOKEN_LAST_MODIFIED, 3226950251U), + MAKE_STATIC_ENT(11, NGHTTP3_QPACK_TOKEN_LINK, 232457833U), + MAKE_STATIC_ENT(12, NGHTTP3_QPACK_TOKEN_LOCATION, 200649126U), + MAKE_STATIC_ENT(90, NGHTTP3_QPACK_TOKEN_ORIGIN, 3649018447U), + MAKE_STATIC_ENT(91, NGHTTP3_QPACK_TOKEN_PURPOSE, 4212263681U), + MAKE_STATIC_ENT(55, NGHTTP3_QPACK_TOKEN_RANGE, 4208725202U), + MAKE_STATIC_ENT(13, NGHTTP3_QPACK_TOKEN_REFERER, 3969579366U), + MAKE_STATIC_ENT(92, NGHTTP3_QPACK_TOKEN_SERVER, 1085029842U), + MAKE_STATIC_ENT(14, NGHTTP3_QPACK_TOKEN_SET_COOKIE, 1848371000U), MAKE_STATIC_ENT(56, NGHTTP3_QPACK_TOKEN_STRICT_TRANSPORT_SECURITY, - 4138147361u), + 4138147361U), MAKE_STATIC_ENT(57, NGHTTP3_QPACK_TOKEN_STRICT_TRANSPORT_SECURITY, - 4138147361u), + 4138147361U), MAKE_STATIC_ENT(58, NGHTTP3_QPACK_TOKEN_STRICT_TRANSPORT_SECURITY, - 4138147361u), - MAKE_STATIC_ENT(93, NGHTTP3_QPACK_TOKEN_TIMING_ALLOW_ORIGIN, 2432297564u), + 4138147361U), + MAKE_STATIC_ENT(93, NGHTTP3_QPACK_TOKEN_TIMING_ALLOW_ORIGIN, 2432297564U), MAKE_STATIC_ENT(94, NGHTTP3_QPACK_TOKEN_UPGRADE_INSECURE_REQUESTS, - 2479169413u), - MAKE_STATIC_ENT(95, NGHTTP3_QPACK_TOKEN_USER_AGENT, 606444526u), - MAKE_STATIC_ENT(59, NGHTTP3_QPACK_TOKEN_VARY, 1085005381u), - MAKE_STATIC_ENT(60, NGHTTP3_QPACK_TOKEN_VARY, 1085005381u), - MAKE_STATIC_ENT(61, NGHTTP3_QPACK_TOKEN_X_CONTENT_TYPE_OPTIONS, 3644557769u), - MAKE_STATIC_ENT(96, NGHTTP3_QPACK_TOKEN_X_FORWARDED_FOR, 2914187656u), - MAKE_STATIC_ENT(97, NGHTTP3_QPACK_TOKEN_X_FRAME_OPTIONS, 3993834824u), - MAKE_STATIC_ENT(98, NGHTTP3_QPACK_TOKEN_X_FRAME_OPTIONS, 3993834824u), - MAKE_STATIC_ENT(62, NGHTTP3_QPACK_TOKEN_X_XSS_PROTECTION, 2501058888u), + 2479169413U), + MAKE_STATIC_ENT(95, NGHTTP3_QPACK_TOKEN_USER_AGENT, 606444526U), + MAKE_STATIC_ENT(59, NGHTTP3_QPACK_TOKEN_VARY, 1085005381U), + MAKE_STATIC_ENT(60, NGHTTP3_QPACK_TOKEN_VARY, 1085005381U), + MAKE_STATIC_ENT(61, NGHTTP3_QPACK_TOKEN_X_CONTENT_TYPE_OPTIONS, 3644557769U), + MAKE_STATIC_ENT(96, NGHTTP3_QPACK_TOKEN_X_FORWARDED_FOR, 2914187656U), + MAKE_STATIC_ENT(97, NGHTTP3_QPACK_TOKEN_X_FRAME_OPTIONS, 3993834824U), + MAKE_STATIC_ENT(98, NGHTTP3_QPACK_TOKEN_X_FRAME_OPTIONS, 3993834824U), + MAKE_STATIC_ENT(62, NGHTTP3_QPACK_TOKEN_X_XSS_PROTECTION, 2501058888U), }; /* Make scalar initialization form of nghttp3_qpack_static_entry */ @@ -174,13 +174,13 @@ static nghttp3_qpack_static_entry token_stable[] = { .name = \ { \ .base = (uint8_t *)(N), \ - .len = sizeof((N)) - 1, \ + .len = nghttp3_strlen_lit((N)), \ .ref = -1, \ }, \ .value = \ { \ .base = (uint8_t *)(V), \ - .len = sizeof((V)) - 1, \ + .len = nghttp3_strlen_lit((V)), \ .ref = -1, \ }, \ .token = T, \ @@ -749,7 +749,7 @@ static int qpack_nv_value_eq(const nghttp3_qpack_nv *a, const nghttp3_nv *b) { } static void qpack_map_init(nghttp3_qpack_map *map) { - memset(map, 0, sizeof(nghttp3_qpack_map)); + *map = (nghttp3_qpack_map){0}; } static void qpack_map_insert(nghttp3_qpack_map *map, nghttp3_qpack_entry *ent) { @@ -940,8 +940,8 @@ void nghttp3_qpack_encoder_free(nghttp3_qpack_encoder *encoder) { void nghttp3_qpack_encoder_set_max_dtable_capacity( nghttp3_qpack_encoder *encoder, size_t max_dtable_capacity) { - max_dtable_capacity = nghttp3_min_size(max_dtable_capacity, - encoder->ctx.hard_max_dtable_capacity); + max_dtable_capacity = + nghttp3_min(max_dtable_capacity, encoder->ctx.hard_max_dtable_capacity); if (encoder->ctx.max_dtable_capacity == max_dtable_capacity) { return; @@ -966,7 +966,8 @@ void nghttp3_qpack_encoder_set_indexing_strat( encoder->indexing_strat = strat; } -uint64_t nghttp3_qpack_encoder_get_min_cnt(nghttp3_qpack_encoder *encoder) { +uint64_t +nghttp3_qpack_encoder_get_min_cnt(const nghttp3_qpack_encoder *encoder) { assert(!nghttp3_pq_empty(&encoder->min_cnts)); return nghttp3_struct_of(nghttp3_pq_top(&encoder->min_cnts), @@ -1111,15 +1112,15 @@ static int reserve_buf(nghttp3_buf *buf, size_t extra_size, return 0; } - n = nghttp3_max_size(n, nghttp3_buf_cap(buf) + extra_size - left); + n = nghttp3_max(n, nghttp3_buf_cap(buf) + extra_size - left); /* Check whether we are requesting too much memory */ - if (n > (1u << 31)) { + if (n > (1U << 31)) { return NGHTTP3_ERR_NOMEM; } #ifndef WIN32 - n = 1u << (32 - __builtin_clz((uint32_t)n - 1)); + n = 1U << (32 - __builtin_clz((uint32_t)n - 1)); #else /* defined(WIN32) */ /* Round up to the next highest power of 2 from Bit Twiddling Hacks */ @@ -1271,23 +1272,23 @@ int nghttp3_qpack_encoder_process_dtable_update(nghttp3_qpack_encoder *encoder, int nghttp3_qpack_encoder_write_set_dtable_cap(nghttp3_qpack_encoder *encoder, nghttp3_buf *ebuf, size_t cap) { DEBUGF("qpack::encode: Set Dynamic Table Capacity capacity=%zu\n", cap); - return qpack_write_number(ebuf, 0x20, cap, 5, encoder->ctx.mem); + return qpack_write_number(ebuf, 0x20U, cap, 5, encoder->ctx.mem); } nghttp3_qpack_stream * -nghttp3_qpack_encoder_find_stream(nghttp3_qpack_encoder *encoder, +nghttp3_qpack_encoder_find_stream(const nghttp3_qpack_encoder *encoder, int64_t stream_id) { return nghttp3_map_find(&encoder->streams, (nghttp3_map_key_type)stream_id); } -int nghttp3_qpack_encoder_stream_is_blocked(nghttp3_qpack_encoder *encoder, - nghttp3_qpack_stream *stream) { +int nghttp3_qpack_encoder_stream_is_blocked( + const nghttp3_qpack_encoder *encoder, const nghttp3_qpack_stream *stream) { return stream && encoder->krcnt < nghttp3_qpack_stream_get_max_cnt(stream); } static uint32_t qpack_hash_name(const nghttp3_nv *nv) { /* 32 bit FNV-1a: http://isthe.com/chongo/tech/comp/fnv/ */ - uint32_t h = 2166136261u; + uint32_t h = 2166136261U; size_t i; for (i = 0; i < nv->namelen; ++i) { @@ -1304,7 +1305,7 @@ static uint32_t qpack_hash_name(const nghttp3_nv *nv) { * name. */ static nghttp3_qpack_indexing_mode -qpack_encoder_decide_indexing_mode(nghttp3_qpack_encoder *encoder, +qpack_encoder_decide_indexing_mode(const nghttp3_qpack_encoder *encoder, const nghttp3_nv *nv, int32_t token) { if (nv->flags & NGHTTP3_NV_FLAG_NEVER_INDEX) { return NGHTTP3_QPACK_INDEXING_MODE_NEVER; @@ -1391,7 +1392,7 @@ static int qpack_encoder_can_index(nghttp3_qpack_encoder *encoder, size_t need, if (!nghttp3_pq_empty(&encoder->min_cnts)) { gmin_cnt = nghttp3_qpack_encoder_get_min_cnt(encoder); - min_cnt = nghttp3_min_uint64(min_cnt, gmin_cnt); + min_cnt = nghttp3_min(min_cnt, gmin_cnt); } if (min_cnt == UINT64_MAX) { @@ -1445,7 +1446,7 @@ static int qpack_encoder_can_index_duplicate(nghttp3_qpack_encoder *encoder, static int qpack_context_check_draining(nghttp3_qpack_context *ctx, uint64_t absidx) { const size_t safe = ctx->max_dtable_capacity - - nghttp3_min_size(512, ctx->max_dtable_capacity * 1 / 8); + nghttp3_min(512, ctx->max_dtable_capacity * 1 / 8); nghttp3_qpack_entry *ent = nghttp3_qpack_context_dtable_get(ctx, absidx); return ctx->dtable_sum - ent->sum > safe; @@ -1490,16 +1491,16 @@ int nghttp3_qpack_encoder_encode_nv(nghttp3_qpack_encoder *encoder, } else { switch (token) { case NGHTTP3_QPACK_TOKEN_HOST: - hash = 2952701295u; + hash = 2952701295U; break; case NGHTTP3_QPACK_TOKEN_TE: - hash = 1011170994u; + hash = 1011170994U; break; case NGHTTP3_QPACK_TOKEN__PROTOCOL: - hash = 1128642621u; + hash = 1128642621U; break; case NGHTTP3_QPACK_TOKEN_PRIORITY: - hash = 2498028297u; + hash = 2498028297U; break; default: hash = qpack_hash_name(nv); @@ -1532,8 +1533,8 @@ int nghttp3_qpack_encoder_encode_nv(nghttp3_qpack_encoder *encoder, new_ent = nghttp3_qpack_context_dtable_top(&encoder->ctx); dres.index = (nghttp3_ssize)new_ent->absidx; } - *pmax_cnt = nghttp3_max_uint64(*pmax_cnt, (uint64_t)(dres.index + 1)); - *pmin_cnt = nghttp3_min_uint64(*pmin_cnt, (uint64_t)(dres.index + 1)); + *pmax_cnt = nghttp3_max(*pmax_cnt, (uint64_t)(dres.index + 1)); + *pmin_cnt = nghttp3_min(*pmin_cnt, (uint64_t)(dres.index + 1)); return nghttp3_qpack_encoder_write_dynamic_indexed( encoder, rbuf, (size_t)dres.index, base); @@ -1553,8 +1554,8 @@ int nghttp3_qpack_encoder_encode_nv(nghttp3_qpack_encoder *encoder, } if (allow_blocking) { new_ent = nghttp3_qpack_context_dtable_top(&encoder->ctx); - *pmax_cnt = nghttp3_max_uint64(*pmax_cnt, new_ent->absidx + 1); - *pmin_cnt = nghttp3_min_uint64(*pmin_cnt, new_ent->absidx + 1); + *pmax_cnt = nghttp3_max(*pmax_cnt, new_ent->absidx + 1); + *pmin_cnt = nghttp3_min(*pmin_cnt, new_ent->absidx + 1); return nghttp3_qpack_encoder_write_dynamic_indexed( encoder, rbuf, new_ent->absidx, base); @@ -1569,9 +1570,8 @@ int nghttp3_qpack_encoder_encode_nv(nghttp3_qpack_encoder *encoder, if (just_index && qpack_encoder_can_index_nv( encoder, nv, - allow_blocking - ? *pmin_cnt - : nghttp3_min_uint64((uint64_t)dres.index + 1, *pmin_cnt))) { + allow_blocking ? *pmin_cnt + : nghttp3_min((uint64_t)dres.index + 1, *pmin_cnt))) { rv = nghttp3_qpack_encoder_write_dynamic_insert(encoder, ebuf, (size_t)dres.index, nv); if (rv != 0) { @@ -1579,7 +1579,7 @@ int nghttp3_qpack_encoder_encode_nv(nghttp3_qpack_encoder *encoder, } if (!allow_blocking) { - *pmin_cnt = nghttp3_min_uint64(*pmin_cnt, (uint64_t)dres.index + 1); + *pmin_cnt = nghttp3_min(*pmin_cnt, (uint64_t)dres.index + 1); } rv = nghttp3_qpack_encoder_dtable_dynamic_add(encoder, (size_t)dres.index, @@ -1590,16 +1590,16 @@ int nghttp3_qpack_encoder_encode_nv(nghttp3_qpack_encoder *encoder, if (allow_blocking) { new_ent = nghttp3_qpack_context_dtable_top(&encoder->ctx); - *pmax_cnt = nghttp3_max_uint64(*pmax_cnt, new_ent->absidx + 1); - *pmin_cnt = nghttp3_min_uint64(*pmin_cnt, new_ent->absidx + 1); + *pmax_cnt = nghttp3_max(*pmax_cnt, new_ent->absidx + 1); + *pmin_cnt = nghttp3_min(*pmin_cnt, new_ent->absidx + 1); return nghttp3_qpack_encoder_write_dynamic_indexed( encoder, rbuf, new_ent->absidx, base); } } - *pmax_cnt = nghttp3_max_uint64(*pmax_cnt, (uint64_t)(dres.index + 1)); - *pmin_cnt = nghttp3_min_uint64(*pmin_cnt, (uint64_t)(dres.index + 1)); + *pmax_cnt = nghttp3_max(*pmax_cnt, (uint64_t)(dres.index + 1)); + *pmin_cnt = nghttp3_min(*pmin_cnt, (uint64_t)(dres.index + 1)); return nghttp3_qpack_encoder_write_dynamic_indexed_name( encoder, rbuf, (size_t)dres.index, base, nv); @@ -1616,8 +1616,8 @@ int nghttp3_qpack_encoder_encode_nv(nghttp3_qpack_encoder *encoder, } if (allow_blocking) { new_ent = nghttp3_qpack_context_dtable_top(&encoder->ctx); - *pmax_cnt = nghttp3_max_uint64(*pmax_cnt, new_ent->absidx + 1); - *pmin_cnt = nghttp3_min_uint64(*pmin_cnt, new_ent->absidx + 1); + *pmax_cnt = nghttp3_max(*pmax_cnt, new_ent->absidx + 1); + *pmin_cnt = nghttp3_min(*pmin_cnt, new_ent->absidx + 1); return nghttp3_qpack_encoder_write_dynamic_indexed(encoder, rbuf, new_ent->absidx, base); @@ -1782,8 +1782,7 @@ int nghttp3_qpack_stream_add_ref(nghttp3_qpack_stream *stream, if (nghttp3_ringbuf_full(&stream->refs)) { rv = nghttp3_ringbuf_reserve( - &stream->refs, - nghttp3_max_size(4, nghttp3_ringbuf_len(&stream->refs) * 2)); + &stream->refs, nghttp3_max(4, nghttp3_ringbuf_len(&stream->refs) * 2)); if (rv != 0) { return rv; } @@ -1815,28 +1814,26 @@ void nghttp3_qpack_stream_pop_ref(nghttp3_qpack_stream *stream) { nghttp3_ringbuf_pop_front(&stream->refs); } -int nghttp3_qpack_encoder_write_static_indexed(nghttp3_qpack_encoder *encoder, - nghttp3_buf *rbuf, - uint64_t absidx) { +int nghttp3_qpack_encoder_write_static_indexed( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx) { DEBUGF("qpack::encode: Indexed Field Line (static) absidx=%" PRIu64 "\n", absidx); - return qpack_write_number(rbuf, 0xc0, absidx, 6, encoder->ctx.mem); + return qpack_write_number(rbuf, 0xC0U, absidx, 6, encoder->ctx.mem); } -int nghttp3_qpack_encoder_write_dynamic_indexed(nghttp3_qpack_encoder *encoder, - nghttp3_buf *rbuf, - uint64_t absidx, - uint64_t base) { +int nghttp3_qpack_encoder_write_dynamic_indexed( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx, + uint64_t base) { DEBUGF("qpack::encode: Indexed Field Line (dynamic) absidx=%" PRIu64 " base=%" PRIu64 "\n", absidx, base); if (absidx < base) { - return qpack_write_number(rbuf, 0x80, base - absidx - 1, 6, + return qpack_write_number(rbuf, 0x80U, base - absidx - 1, 6, encoder->ctx.mem); } - return qpack_write_number(rbuf, 0x10, absidx - base, 4, encoder->ctx.mem); + return qpack_write_number(rbuf, 0x10U, absidx - base, 4, encoder->ctx.mem); } /* @@ -1851,10 +1848,10 @@ int nghttp3_qpack_encoder_write_dynamic_indexed(nghttp3_qpack_encoder *encoder, * NGHTTP3_ERR_NOMEM * Out of memory. */ -static int qpack_encoder_write_indexed_name(nghttp3_qpack_encoder *encoder, - nghttp3_buf *buf, uint8_t fb, - uint64_t nameidx, size_t prefix, - const nghttp3_nv *nv) { +static int +qpack_encoder_write_indexed_name(const nghttp3_qpack_encoder *encoder, + nghttp3_buf *buf, uint8_t fb, uint64_t nameidx, + size_t prefix, const nghttp3_nv *nv) { int rv; size_t len = nghttp3_qpack_put_varint_len(nameidx, prefix); uint8_t *p; @@ -1880,7 +1877,7 @@ static int qpack_encoder_write_indexed_name(nghttp3_qpack_encoder *encoder, p = nghttp3_qpack_put_varint(p, nameidx, prefix); if (h) { - *p = 0x80; + *p = 0x80U; p = nghttp3_qpack_put_varint(p, hlen, 7); p = nghttp3_qpack_huffman_encode(p, nv->value, nv->valuelen); } else { @@ -1899,10 +1896,11 @@ static int qpack_encoder_write_indexed_name(nghttp3_qpack_encoder *encoder, } int nghttp3_qpack_encoder_write_static_indexed_name( - nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx, + const nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx, const nghttp3_nv *nv) { uint8_t fb = - (uint8_t)(0x50 | ((nv->flags & NGHTTP3_NV_FLAG_NEVER_INDEX) ? 0x20 : 0)); + (uint8_t)(0x50U | + ((nv->flags & NGHTTP3_NV_FLAG_NEVER_INDEX) ? 0x20U : 0x00U)); DEBUGF("qpack::encode: Literal Field Line With Name Reference (static) " "absidx=%" PRIu64 " never=%d\n", @@ -1911,7 +1909,7 @@ int nghttp3_qpack_encoder_write_static_indexed_name( } int nghttp3_qpack_encoder_write_dynamic_indexed_name( - nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx, + const nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx, uint64_t base, const nghttp3_nv *nv) { uint8_t fb; @@ -1920,13 +1918,13 @@ int nghttp3_qpack_encoder_write_dynamic_indexed_name( absidx, base, (nv->flags & NGHTTP3_NV_FLAG_NEVER_INDEX) != 0); if (absidx < base) { - fb = - (uint8_t)(0x40 | ((nv->flags & NGHTTP3_NV_FLAG_NEVER_INDEX) ? 0x20 : 0)); + fb = (uint8_t)(0x40U | + ((nv->flags & NGHTTP3_NV_FLAG_NEVER_INDEX) ? 0x20U : 0x00U)); return qpack_encoder_write_indexed_name(encoder, rbuf, fb, base - absidx - 1, 4, nv); } - fb = (nv->flags & NGHTTP3_NV_FLAG_NEVER_INDEX) ? 0x08 : 0; + fb = (nv->flags & NGHTTP3_NV_FLAG_NEVER_INDEX) ? 0x08U : 0x0U; return qpack_encoder_write_indexed_name(encoder, rbuf, fb, absidx - base, 3, nv); } @@ -1943,7 +1941,7 @@ int nghttp3_qpack_encoder_write_dynamic_indexed_name( * NGHTTP3_ERR_NOMEM * Out of memory. */ -static int qpack_encoder_write_literal(nghttp3_qpack_encoder *encoder, +static int qpack_encoder_write_literal(const nghttp3_qpack_encoder *encoder, nghttp3_buf *buf, uint8_t fb, size_t prefix, const nghttp3_nv *nv) { int rv; @@ -1990,7 +1988,7 @@ static int qpack_encoder_write_literal(nghttp3_qpack_encoder *encoder, *p = 0; if (vh) { - *p |= 0x80; + *p |= 0x80U; p = nghttp3_qpack_put_varint(p, vhlen, 7); p = nghttp3_qpack_huffman_encode(p, nv->value, nv->valuelen); } else { @@ -2007,40 +2005,38 @@ static int qpack_encoder_write_literal(nghttp3_qpack_encoder *encoder, return 0; } -int nghttp3_qpack_encoder_write_literal(nghttp3_qpack_encoder *encoder, +int nghttp3_qpack_encoder_write_literal(const nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, const nghttp3_nv *nv) { uint8_t fb = - (uint8_t)(0x20 | ((nv->flags & NGHTTP3_NV_FLAG_NEVER_INDEX) ? 0x10 : 0)); + (uint8_t)(0x20U | + ((nv->flags & NGHTTP3_NV_FLAG_NEVER_INDEX) ? 0x10U : 0x0U)); DEBUGF("qpack::encode: Literal Field Line With Literal Name\n"); return qpack_encoder_write_literal(encoder, rbuf, fb, 3, nv); } -int nghttp3_qpack_encoder_write_static_insert(nghttp3_qpack_encoder *encoder, - nghttp3_buf *ebuf, - uint64_t absidx, - const nghttp3_nv *nv) { +int nghttp3_qpack_encoder_write_static_insert( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *ebuf, uint64_t absidx, + const nghttp3_nv *nv) { DEBUGF("qpack::encode: Insert With Name Reference (static) absidx=%" PRIu64 "\n", absidx); - return qpack_encoder_write_indexed_name(encoder, ebuf, 0xc0, absidx, 6, nv); + return qpack_encoder_write_indexed_name(encoder, ebuf, 0xC0U, absidx, 6, nv); } -int nghttp3_qpack_encoder_write_dynamic_insert(nghttp3_qpack_encoder *encoder, - nghttp3_buf *ebuf, - uint64_t absidx, - const nghttp3_nv *nv) { +int nghttp3_qpack_encoder_write_dynamic_insert( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *ebuf, uint64_t absidx, + const nghttp3_nv *nv) { DEBUGF("qpack::encode: Insert With Name Reference (dynamic) absidx=%" PRIu64 "\n", absidx); return qpack_encoder_write_indexed_name( - encoder, ebuf, 0x80, encoder->ctx.next_absidx - absidx - 1, 6, nv); + encoder, ebuf, 0x80U, encoder->ctx.next_absidx - absidx - 1, 6, nv); } -int nghttp3_qpack_encoder_write_duplicate_insert(nghttp3_qpack_encoder *encoder, - nghttp3_buf *ebuf, - uint64_t absidx) { +int nghttp3_qpack_encoder_write_duplicate_insert( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *ebuf, uint64_t absidx) { uint64_t idx = encoder->ctx.next_absidx - absidx - 1; size_t len = nghttp3_qpack_put_varint_len(idx, 5); uint8_t *p; @@ -2065,11 +2061,11 @@ int nghttp3_qpack_encoder_write_duplicate_insert(nghttp3_qpack_encoder *encoder, return 0; } -int nghttp3_qpack_encoder_write_literal_insert(nghttp3_qpack_encoder *encoder, - nghttp3_buf *ebuf, - const nghttp3_nv *nv) { +int nghttp3_qpack_encoder_write_literal_insert( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *ebuf, + const nghttp3_nv *nv) { DEBUGF("qpack::encode: Insert With Literal Name\n"); - return qpack_encoder_write_literal(encoder, ebuf, 0x40, 5, nv); + return qpack_encoder_write_literal(encoder, ebuf, 0x40U, 5, nv); } int nghttp3_qpack_context_dtable_add(nghttp3_qpack_context *ctx, @@ -2112,8 +2108,7 @@ int nghttp3_qpack_context_dtable_add(nghttp3_qpack_context *ctx, if (nghttp3_ringbuf_full(&ctx->dtable)) { rv = nghttp3_ringbuf_reserve( - &ctx->dtable, - nghttp3_max_size(128, nghttp3_ringbuf_len(&ctx->dtable) * 2)); + &ctx->dtable, nghttp3_max(128, nghttp3_ringbuf_len(&ctx->dtable) * 2)); if (rv != 0) { goto fail; } @@ -2418,11 +2413,16 @@ void nghttp3_qpack_encoder_cancel_stream(nghttp3_qpack_encoder *encoder, size_t nghttp3_qpack_encoder_get_num_blocked_streams(nghttp3_qpack_encoder *encoder) { + return nghttp3_qpack_encoder_get_num_blocked_streams2(encoder); +} + +size_t nghttp3_qpack_encoder_get_num_blocked_streams2( + const nghttp3_qpack_encoder *encoder) { return nghttp3_ksl_len(&encoder->blocked_streams); } int nghttp3_qpack_encoder_write_field_section_prefix( - nghttp3_qpack_encoder *encoder, nghttp3_buf *pbuf, uint64_t ricnt, + const nghttp3_qpack_encoder *encoder, nghttp3_buf *pbuf, uint64_t ricnt, uint64_t base) { size_t max_ents = encoder->ctx.hard_max_dtable_capacity / NGHTTP3_QPACK_ENTRY_OVERHEAD; @@ -2446,7 +2446,7 @@ int nghttp3_qpack_encoder_write_field_section_prefix( p = nghttp3_qpack_put_varint(p, encricnt, 8); if (sign) { - *p = 0x80; + *p = 0x80U; } else { *p = 0; } @@ -2507,7 +2507,7 @@ static nghttp3_ssize qpack_read_varint(int *fin, } for (; p != end; ++p, shift += 7) { - add = (*p) & 0x7f; + add = (*p) & 0x7FU; if (shift > 62) { return NGHTTP3_ERR_QPACK_FATAL; @@ -2568,15 +2568,15 @@ nghttp3_ssize nghttp3_qpack_encoder_read_decoder(nghttp3_qpack_encoder *encoder, for (; p != end;) { switch (encoder->state) { case NGHTTP3_QPACK_DS_STATE_OPCODE: - switch ((*p) & 0xc0) { - case 0x80: - case 0xc0: + switch ((*p) & 0xC0U) { + case 0x80U: + case 0xC0U: DEBUGF("qpack::encode: OPCODE_SECTION_ACK\n"); encoder->opcode = NGHTTP3_QPACK_DS_OPCODE_SECTION_ACK; encoder->rstate.prefix = 7; break; - case 0x40: + case 0x40U: DEBUGF("qpack::encode: OPCODE_STREAM_CANCEL\n"); encoder->opcode = NGHTTP3_QPACK_DS_OPCODE_STREAM_CANCEL; encoder->rstate.prefix = 6; @@ -2673,7 +2673,7 @@ uint8_t *nghttp3_qpack_put_varint(uint8_t *buf, uint64_t n, size_t prefix) { n -= k; for (; n >= 128; n >>= 7) { - *buf++ = (uint8_t)((1 << 7) | (n & 0x7f)); + *buf++ = (uint8_t)(0x80U | (n & 0x7FU)); } *buf++ = (uint8_t)n; @@ -2766,7 +2766,7 @@ static nghttp3_ssize qpack_read_string(nghttp3_qpack_read_state *rstate, nghttp3_buf *dest, const uint8_t *begin, const uint8_t *end) { size_t len = (size_t)(end - begin); - size_t n = (size_t)nghttp3_min_uint64((uint64_t)len, rstate->left); + size_t n = (size_t)nghttp3_min((uint64_t)len, rstate->left); dest->last = nghttp3_cpymem(dest->last, begin, n); @@ -2783,8 +2783,9 @@ static nghttp3_ssize qpack_read_string(nghttp3_qpack_read_state *rstate, * NGHTTP3_ERR_QPACK_FATAL * rstate->absidx is invalid. */ -static int qpack_decoder_validate_index(nghttp3_qpack_decoder *decoder, - nghttp3_qpack_read_state *rstate) { +static int +qpack_decoder_validate_index(const nghttp3_qpack_decoder *decoder, + const nghttp3_qpack_read_state *rstate) { if (rstate->dynamic) { return rstate->absidx < decoder->ctx.next_absidx && decoder->ctx.next_absidx - rstate->absidx - 1 < @@ -2840,20 +2841,20 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, busy = 0; switch (decoder->state) { case NGHTTP3_QPACK_ES_STATE_OPCODE: - switch ((*p) & 0xe0) { - case 0x80: - case 0xa0: - case 0xc0: - case 0xe0: + switch ((*p) & 0xE0U) { + case 0x80U: + case 0xA0U: + case 0xC0U: + case 0xE0U: DEBUGF("qpack::decode: OPCODE_INSERT_INDEXED\n"); decoder->opcode = NGHTTP3_QPACK_ES_OPCODE_INSERT_INDEXED; - decoder->rstate.dynamic = !((*p) & 0x40); + decoder->rstate.dynamic = !((*p) & 0x40U); decoder->rstate.prefix = 6; decoder->state = NGHTTP3_QPACK_ES_STATE_READ_INDEX; break; - case 0x40: - case 0x60: + case 0x40U: + case 0x60U: DEBUGF("qpack::decode: OPCODE_INSERT\n"); decoder->opcode = NGHTTP3_QPACK_ES_OPCODE_INSERT; decoder->rstate.dynamic = 0; @@ -2861,7 +2862,7 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, decoder->state = NGHTTP3_QPACK_ES_STATE_CHECK_NAME_HUFFMAN; break; - case 0x20: + case 0x20U: DEBUGF("qpack::decode: OPCODE_SET_DTABLE_TABLE_CAP\n"); decoder->opcode = NGHTTP3_QPACK_ES_OPCODE_SET_DTABLE_CAP; decoder->rstate.prefix = 5; @@ -2960,7 +2961,10 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, decoder->state = NGHTTP3_QPACK_ES_STATE_READ_NAME_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&decoder->rstate.huffman_ctx); rv = nghttp3_rcbuf_new(&decoder->rstate.name, - (size_t)decoder->rstate.left * 2 + 1, mem); + nghttp3_qpack_huffman_estimate_decode_length( + (size_t)decoder->rstate.left) + + 1, + mem); } else { decoder->state = NGHTTP3_QPACK_ES_STATE_READ_NAME; rv = nghttp3_rcbuf_new(&decoder->rstate.name, @@ -3042,7 +3046,10 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, decoder->state = NGHTTP3_QPACK_ES_STATE_READ_VALUE_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&decoder->rstate.huffman_ctx); rv = nghttp3_rcbuf_new(&decoder->rstate.value, - (size_t)decoder->rstate.left * 2 + 1, mem); + nghttp3_qpack_huffman_estimate_decode_length( + (size_t)decoder->rstate.left) + + 1, + mem); } else { decoder->state = NGHTTP3_QPACK_ES_STATE_READ_VALUE; rv = nghttp3_rcbuf_new(&decoder->rstate.value, @@ -3288,7 +3295,7 @@ int nghttp3_qpack_decoder_dtable_literal_add(nghttp3_qpack_decoder *decoder) { void nghttp3_qpack_decoder_set_max_concurrent_streams( nghttp3_qpack_decoder *decoder, size_t max_concurrent_streams) { decoder->max_concurrent_streams = - nghttp3_max_size(decoder->max_concurrent_streams, max_concurrent_streams); + nghttp3_max(decoder->max_concurrent_streams, max_concurrent_streams); } void nghttp3_qpack_stream_context_init(nghttp3_qpack_stream_context *sctx, @@ -3316,6 +3323,11 @@ void nghttp3_qpack_stream_context_reset(nghttp3_qpack_stream_context *sctx) { uint64_t nghttp3_qpack_stream_context_get_ricnt(nghttp3_qpack_stream_context *sctx) { + return nghttp3_qpack_stream_context_get_ricnt2(sctx); +} + +uint64_t nghttp3_qpack_stream_context_get_ricnt2( + const nghttp3_qpack_stream_context *sctx) { return sctx->ricnt; } @@ -3363,7 +3375,7 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, sctx->state = NGHTTP3_QPACK_RS_STATE_DBASE_SIGN; break; case NGHTTP3_QPACK_RS_STATE_DBASE_SIGN: - if ((*p) & 0x80) { + if ((*p) & 0x80U) { sctx->dbase_sign = 1; } sctx->state = NGHTTP3_QPACK_RS_STATE_DBASE; @@ -3413,45 +3425,45 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, case NGHTTP3_QPACK_RS_STATE_OPCODE: assert(sctx->rstate.left == 0); assert(sctx->rstate.shift == 0); - switch ((*p) & 0xf0) { - case 0x80: - case 0x90: - case 0xa0: - case 0xb0: - case 0xc0: - case 0xd0: - case 0xe0: - case 0xf0: + switch ((*p) & 0xF0U) { + case 0x80U: + case 0x90U: + case 0xA0U: + case 0xB0U: + case 0xC0U: + case 0xD0U: + case 0xE0U: + case 0xF0U: DEBUGF("qpack::decode: OPCODE_INDEXED\n"); sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED; - sctx->rstate.dynamic = !((*p) & 0x40); + sctx->rstate.dynamic = !((*p) & 0x40U); sctx->rstate.prefix = 6; sctx->state = NGHTTP3_QPACK_RS_STATE_READ_INDEX; break; - case 0x40: - case 0x50: - case 0x60: - case 0x70: + case 0x40U: + case 0x50U: + case 0x60U: + case 0x70U: DEBUGF("qpack::decode: OPCODE_INDEXED_NAME\n"); sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED_NAME; - sctx->rstate.never = (*p) & 0x20; - sctx->rstate.dynamic = !((*p) & 0x10); + sctx->rstate.never = (*p) & 0x20U; + sctx->rstate.dynamic = !((*p) & 0x10U); sctx->rstate.prefix = 4; sctx->state = NGHTTP3_QPACK_RS_STATE_READ_INDEX; break; - case 0x20: - case 0x30: + case 0x20U: + case 0x30U: DEBUGF("qpack::decode: OPCODE_LITERAL\n"); sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_LITERAL; - sctx->rstate.never = (*p) & 0x10; + sctx->rstate.never = (*p) & 0x10U; sctx->rstate.dynamic = 0; sctx->rstate.prefix = 3; sctx->state = NGHTTP3_QPACK_RS_STATE_CHECK_NAME_HUFFMAN; break; - case 0x10: + case 0x10U: DEBUGF("qpack::decode: OPCODE_INDEXED_PB\n"); sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED_PB; sctx->rstate.dynamic = 1; @@ -3462,7 +3474,7 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, default: DEBUGF("qpack::decode: OPCODE_INDEXED_NAME_PB\n"); sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED_NAME_PB; - sctx->rstate.never = (*p) & 0x08; + sctx->rstate.never = (*p) & 0x08U; sctx->rstate.dynamic = 1; sctx->rstate.prefix = 3; sctx->state = NGHTTP3_QPACK_RS_STATE_READ_INDEX; @@ -3556,7 +3568,10 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, sctx->state = NGHTTP3_QPACK_RS_STATE_READ_NAME_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&sctx->rstate.huffman_ctx); rv = nghttp3_rcbuf_new(&sctx->rstate.name, - (size_t)sctx->rstate.left * 2 + 1, mem); + nghttp3_qpack_huffman_estimate_decode_length( + (size_t)sctx->rstate.left) + + 1, + mem); } else { sctx->state = NGHTTP3_QPACK_RS_STATE_READ_NAME; rv = nghttp3_rcbuf_new(&sctx->rstate.name, @@ -3636,7 +3651,10 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, sctx->state = NGHTTP3_QPACK_RS_STATE_READ_VALUE_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&sctx->rstate.huffman_ctx); rv = nghttp3_rcbuf_new(&sctx->rstate.value, - (size_t)sctx->rstate.left * 2 + 1, mem); + nghttp3_qpack_huffman_estimate_decode_length( + (size_t)sctx->rstate.left) + + 1, + mem); } else { sctx->state = NGHTTP3_QPACK_RS_STATE_READ_VALUE; rv = nghttp3_rcbuf_new(&sctx->rstate.value, @@ -3766,8 +3784,8 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, return rv; } -static int qpack_decoder_dbuf_overflow(nghttp3_qpack_decoder *decoder) { - size_t limit = nghttp3_max_size(decoder->max_concurrent_streams, 100); +static int qpack_decoder_dbuf_overflow(const nghttp3_qpack_decoder *decoder) { + size_t limit = nghttp3_max(decoder->max_concurrent_streams, 100); /* 10 = nghttp3_qpack_put_varint_len((1ULL << 62) - 1, 2)) */ return nghttp3_buf_len(&decoder->dbuf) > limit * 2 * 10; } @@ -3790,7 +3808,7 @@ int nghttp3_qpack_decoder_write_section_ack( } p = dbuf->last; - *p = 0x80; + *p = 0x80U; dbuf->last = nghttp3_qpack_put_varint(p, (uint64_t)sctx->stream_id, 7); if (decoder->written_icnt < sctx->ricnt) { @@ -3802,6 +3820,11 @@ int nghttp3_qpack_decoder_write_section_ack( size_t nghttp3_qpack_decoder_get_decoder_streamlen(nghttp3_qpack_decoder *decoder) { + return nghttp3_qpack_decoder_get_decoder_streamlen2(decoder); +} + +size_t nghttp3_qpack_decoder_get_decoder_streamlen2( + const nghttp3_qpack_decoder *decoder) { uint64_t n; size_t len = 0; @@ -3863,14 +3886,14 @@ int nghttp3_qpack_decoder_cancel_stream(nghttp3_qpack_decoder *decoder, } p = decoder->dbuf.last; - *p = 0x40; + *p = 0x40U; decoder->dbuf.last = nghttp3_qpack_put_varint(p, (uint64_t)stream_id, 6); return 0; } -int nghttp3_qpack_decoder_reconstruct_ricnt(nghttp3_qpack_decoder *decoder, - uint64_t *dest, uint64_t encricnt) { +int nghttp3_qpack_decoder_reconstruct_ricnt( + const nghttp3_qpack_decoder *decoder, uint64_t *dest, uint64_t encricnt) { uint64_t max_ents, full, max, max_wrapped, ricnt; if (encricnt == 0) { @@ -3906,7 +3929,7 @@ int nghttp3_qpack_decoder_reconstruct_ricnt(nghttp3_qpack_decoder *decoder, return 0; } -int nghttp3_qpack_decoder_rel2abs(nghttp3_qpack_decoder *decoder, +int nghttp3_qpack_decoder_rel2abs(const nghttp3_qpack_decoder *decoder, nghttp3_qpack_read_state *rstate) { DEBUGF("qpack::decode: dynamic=%d relidx=%" PRIu64 " icnt=%" PRIu64 "\n", rstate->dynamic, rstate->left, decoder->ctx.next_absidx); @@ -3925,7 +3948,7 @@ int nghttp3_qpack_decoder_rel2abs(nghttp3_qpack_decoder *decoder, return 0; } -int nghttp3_qpack_decoder_brel2abs(nghttp3_qpack_decoder *decoder, +int nghttp3_qpack_decoder_brel2abs(const nghttp3_qpack_decoder *decoder, nghttp3_qpack_stream_context *sctx) { nghttp3_qpack_read_state *rstate = &sctx->rstate; @@ -3952,7 +3975,7 @@ int nghttp3_qpack_decoder_brel2abs(nghttp3_qpack_decoder *decoder, return 0; } -int nghttp3_qpack_decoder_pbrel2abs(nghttp3_qpack_decoder *decoder, +int nghttp3_qpack_decoder_pbrel2abs(const nghttp3_qpack_decoder *decoder, nghttp3_qpack_stream_context *sctx) { nghttp3_qpack_read_state *rstate = &sctx->rstate; @@ -3974,8 +3997,8 @@ int nghttp3_qpack_decoder_pbrel2abs(nghttp3_qpack_decoder *decoder, } static void -qpack_decoder_emit_static_indexed(nghttp3_qpack_decoder *decoder, - nghttp3_qpack_stream_context *sctx, +qpack_decoder_emit_static_indexed(const nghttp3_qpack_decoder *decoder, + const nghttp3_qpack_stream_context *sctx, nghttp3_qpack_nv *nv) { const nghttp3_qpack_static_header *shd = &stable[sctx->rstate.absidx]; (void)decoder; @@ -3988,7 +4011,7 @@ qpack_decoder_emit_static_indexed(nghttp3_qpack_decoder *decoder, static void qpack_decoder_emit_dynamic_indexed(nghttp3_qpack_decoder *decoder, - nghttp3_qpack_stream_context *sctx, + const nghttp3_qpack_stream_context *sctx, nghttp3_qpack_nv *nv) { nghttp3_qpack_entry *ent = nghttp3_qpack_context_dtable_get(&decoder->ctx, sctx->rstate.absidx); @@ -3999,9 +4022,9 @@ qpack_decoder_emit_dynamic_indexed(nghttp3_qpack_decoder *decoder, nghttp3_rcbuf_incref(nv->value); } -void nghttp3_qpack_decoder_emit_indexed(nghttp3_qpack_decoder *decoder, - nghttp3_qpack_stream_context *sctx, - nghttp3_qpack_nv *nv) { +void nghttp3_qpack_decoder_emit_indexed( + nghttp3_qpack_decoder *decoder, const nghttp3_qpack_stream_context *sctx, + nghttp3_qpack_nv *nv) { DEBUGF("qpack::decode: Indexed (%s) absidx=%" PRIu64 "\n", sctx->rstate.dynamic ? "dynamic" : "static", sctx->rstate.absidx); @@ -4013,7 +4036,7 @@ void nghttp3_qpack_decoder_emit_indexed(nghttp3_qpack_decoder *decoder, } static void -qpack_decoder_emit_static_indexed_name(nghttp3_qpack_decoder *decoder, +qpack_decoder_emit_static_indexed_name(const nghttp3_qpack_decoder *decoder, nghttp3_qpack_stream_context *sctx, nghttp3_qpack_nv *nv) { const nghttp3_qpack_static_header *shd = &stable[sctx->rstate.absidx]; @@ -4073,7 +4096,7 @@ int nghttp3_qpack_decoder_emit_indexed_name(nghttp3_qpack_decoder *decoder, return 0; } -void nghttp3_qpack_decoder_emit_literal(nghttp3_qpack_decoder *decoder, +void nghttp3_qpack_decoder_emit_literal(const nghttp3_qpack_decoder *decoder, nghttp3_qpack_stream_context *sctx, nghttp3_qpack_nv *nv) { (void)decoder; diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h index 1c13849a8aa55a..2b8dca9d5cf746 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h @@ -40,7 +40,7 @@ #include "nghttp3_ksl.h" #include "nghttp3_qpack_huffman.h" -#define NGHTTP3_QPACK_INT_MAX ((1ull << 62) - 1) +#define NGHTTP3_QPACK_INT_MAX ((1ULL << 62) - 1) /* NGHTTP3_QPACK_MAX_NAMELEN is the maximum (compressed) length of header name this library can decode. */ @@ -223,10 +223,10 @@ typedef enum nghttp3_qpack_decoder_stream_opcode { /* QPACK encoder flags */ /* NGHTTP3_QPACK_ENCODER_FLAG_NONE indicates that no flag is set. */ -#define NGHTTP3_QPACK_ENCODER_FLAG_NONE 0x00u +#define NGHTTP3_QPACK_ENCODER_FLAG_NONE 0x00U /* NGHTTP3_QPACK_ENCODER_FLAG_PENDING_SET_DTABLE_CAP indicates that Set Dynamic Table Capacity is required. */ -#define NGHTTP3_QPACK_ENCODER_FLAG_PENDING_SET_DTABLE_CAP 0x01u +#define NGHTTP3_QPACK_ENCODER_FLAG_PENDING_SET_DTABLE_CAP 0x01U struct nghttp3_qpack_encoder { nghttp3_qpack_context ctx; @@ -350,7 +350,7 @@ nghttp3_qpack_lookup_result nghttp3_qpack_encoder_lookup_dtable( * Out of memory. */ int nghttp3_qpack_encoder_write_field_section_prefix( - nghttp3_qpack_encoder *encoder, nghttp3_buf *pbuf, uint64_t ricnt, + const nghttp3_qpack_encoder *encoder, nghttp3_buf *pbuf, uint64_t ricnt, uint64_t base); /* @@ -363,9 +363,8 @@ int nghttp3_qpack_encoder_write_field_section_prefix( * NGHTTP3_ERR_NOMEM * Out of memory. */ -int nghttp3_qpack_encoder_write_static_indexed(nghttp3_qpack_encoder *encoder, - nghttp3_buf *rbuf, - uint64_t absidx); +int nghttp3_qpack_encoder_write_static_indexed( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx); /* * nghttp3_qpack_encoder_write_dynamic_indexed writes Indexed Header @@ -378,9 +377,9 @@ int nghttp3_qpack_encoder_write_static_indexed(nghttp3_qpack_encoder *encoder, * NGHTTP3_ERR_NOMEM * Out of memory. */ -int nghttp3_qpack_encoder_write_dynamic_indexed(nghttp3_qpack_encoder *encoder, - nghttp3_buf *rbuf, - uint64_t absidx, uint64_t base); +int nghttp3_qpack_encoder_write_dynamic_indexed( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx, + uint64_t base); /* * nghttp3_qpack_encoder_write_static_indexed writes Literal Header @@ -395,7 +394,7 @@ int nghttp3_qpack_encoder_write_dynamic_indexed(nghttp3_qpack_encoder *encoder, * Out of memory. */ int nghttp3_qpack_encoder_write_static_indexed_name( - nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx, + const nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx, const nghttp3_nv *nv); /* @@ -411,7 +410,7 @@ int nghttp3_qpack_encoder_write_static_indexed_name( * Out of memory. */ int nghttp3_qpack_encoder_write_dynamic_indexed_name( - nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx, + const nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, uint64_t absidx, uint64_t base, const nghttp3_nv *nv); /* @@ -424,7 +423,7 @@ int nghttp3_qpack_encoder_write_dynamic_indexed_name( * NGHTTP3_ERR_NOMEM * Out of memory. */ -int nghttp3_qpack_encoder_write_literal(nghttp3_qpack_encoder *encoder, +int nghttp3_qpack_encoder_write_literal(const nghttp3_qpack_encoder *encoder, nghttp3_buf *rbuf, const nghttp3_nv *nv); @@ -439,10 +438,9 @@ int nghttp3_qpack_encoder_write_literal(nghttp3_qpack_encoder *encoder, * NGHTTP3_ERR_NOMEM * Out of memory. */ -int nghttp3_qpack_encoder_write_static_insert(nghttp3_qpack_encoder *encoder, - nghttp3_buf *ebuf, - uint64_t absidx, - const nghttp3_nv *nv); +int nghttp3_qpack_encoder_write_static_insert( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *ebuf, uint64_t absidx, + const nghttp3_nv *nv); /* * nghttp3_qpack_encoder_write_dynamic_insert writes Insert With Name @@ -455,10 +453,9 @@ int nghttp3_qpack_encoder_write_static_insert(nghttp3_qpack_encoder *encoder, * NGHTTP3_ERR_NOMEM * Out of memory. */ -int nghttp3_qpack_encoder_write_dynamic_insert(nghttp3_qpack_encoder *encoder, - nghttp3_buf *ebuf, - uint64_t absidx, - const nghttp3_nv *nv); +int nghttp3_qpack_encoder_write_dynamic_insert( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *ebuf, uint64_t absidx, + const nghttp3_nv *nv); /* * nghttp3_qpack_encoder_write_duplicate_insert writes Duplicate to @@ -471,9 +468,8 @@ int nghttp3_qpack_encoder_write_dynamic_insert(nghttp3_qpack_encoder *encoder, * NGHTTP3_ERR_NOMEM * Out of memory. */ -int nghttp3_qpack_encoder_write_duplicate_insert(nghttp3_qpack_encoder *encoder, - nghttp3_buf *ebuf, - uint64_t absidx); +int nghttp3_qpack_encoder_write_duplicate_insert( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *ebuf, uint64_t absidx); /* * nghttp3_qpack_encoder_write_literal_insert writes Insert With @@ -485,12 +481,12 @@ int nghttp3_qpack_encoder_write_duplicate_insert(nghttp3_qpack_encoder *encoder, * NGHTTP3_ERR_NOMEM * Out of memory. */ -int nghttp3_qpack_encoder_write_literal_insert(nghttp3_qpack_encoder *encoder, - nghttp3_buf *ebuf, - const nghttp3_nv *nv); +int nghttp3_qpack_encoder_write_literal_insert( + const nghttp3_qpack_encoder *encoder, nghttp3_buf *ebuf, + const nghttp3_nv *nv); -int nghttp3_qpack_encoder_stream_is_blocked(nghttp3_qpack_encoder *encoder, - nghttp3_qpack_stream *stream); +int nghttp3_qpack_encoder_stream_is_blocked( + const nghttp3_qpack_encoder *encoder, const nghttp3_qpack_stream *stream); /* * nghttp3_qpack_encoder_block_stream blocks |stream|. @@ -523,10 +519,11 @@ void nghttp3_qpack_encoder_unblock(nghttp3_qpack_encoder *encoder, * stream. */ nghttp3_qpack_stream * -nghttp3_qpack_encoder_find_stream(nghttp3_qpack_encoder *encoder, +nghttp3_qpack_encoder_find_stream(const nghttp3_qpack_encoder *encoder, int64_t stream_id); -uint64_t nghttp3_qpack_encoder_get_min_cnt(nghttp3_qpack_encoder *encoder); +uint64_t +nghttp3_qpack_encoder_get_min_cnt(const nghttp3_qpack_encoder *encoder); /* * nghttp3_qpack_encoder_shrink_dtable shrinks dynamic table so that @@ -927,8 +924,8 @@ void nghttp3_qpack_stream_context_free(nghttp3_qpack_stream_context *sctx); * NGHTTP3_ERR_QPACK_DECOMPRESSION_FAILED * Unable to reconstruct Required Insert Count. */ -int nghttp3_qpack_decoder_reconstruct_ricnt(nghttp3_qpack_decoder *decoder, - uint64_t *dest, uint64_t encricnt); +int nghttp3_qpack_decoder_reconstruct_ricnt( + const nghttp3_qpack_decoder *decoder, uint64_t *dest, uint64_t encricnt); /* * nghttp3_qpack_decoder_rel2abs converts relative index rstate->left @@ -941,7 +938,7 @@ int nghttp3_qpack_decoder_reconstruct_ricnt(nghttp3_qpack_decoder *decoder, * NGHTTP3_ERR_QPACK_ENCODER_STREAM * Relative index is invalid. */ -int nghttp3_qpack_decoder_rel2abs(nghttp3_qpack_decoder *decoder, +int nghttp3_qpack_decoder_rel2abs(const nghttp3_qpack_decoder *decoder, nghttp3_qpack_read_state *rstate); /* @@ -955,7 +952,7 @@ int nghttp3_qpack_decoder_rel2abs(nghttp3_qpack_decoder *decoder, * NGHTTP3_ERR_QPACK_DECOMPRESSION_FAILED * Base relative index is invalid. */ -int nghttp3_qpack_decoder_brel2abs(nghttp3_qpack_decoder *decoder, +int nghttp3_qpack_decoder_brel2abs(const nghttp3_qpack_decoder *decoder, nghttp3_qpack_stream_context *sctx); /* @@ -969,18 +966,18 @@ int nghttp3_qpack_decoder_brel2abs(nghttp3_qpack_decoder *decoder, * NGHTTP3_ERR_QPACK_DECOMPRESSION_FAILED * Post-Base relative index is invalid. */ -int nghttp3_qpack_decoder_pbrel2abs(nghttp3_qpack_decoder *decoder, +int nghttp3_qpack_decoder_pbrel2abs(const nghttp3_qpack_decoder *decoder, nghttp3_qpack_stream_context *sctx); -void nghttp3_qpack_decoder_emit_indexed(nghttp3_qpack_decoder *decoder, - nghttp3_qpack_stream_context *sctx, - nghttp3_qpack_nv *nv); +void nghttp3_qpack_decoder_emit_indexed( + nghttp3_qpack_decoder *decoder, const nghttp3_qpack_stream_context *sctx, + nghttp3_qpack_nv *nv); int nghttp3_qpack_decoder_emit_indexed_name(nghttp3_qpack_decoder *decoder, nghttp3_qpack_stream_context *sctx, nghttp3_qpack_nv *nv); -void nghttp3_qpack_decoder_emit_literal(nghttp3_qpack_decoder *decoder, +void nghttp3_qpack_decoder_emit_literal(const nghttp3_qpack_decoder *decoder, nghttp3_qpack_stream_context *sctx, nghttp3_qpack_nv *nv); diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman.c b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman.c index 8b9b6a451b6015..618e95cc4ee235 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman.c @@ -80,7 +80,7 @@ uint8_t *nghttp3_qpack_huffman_encode(uint8_t *dest, const uint8_t *src, void nghttp3_qpack_huffman_decode_context_init( nghttp3_qpack_huffman_decode_context *ctx) { *ctx = (nghttp3_qpack_huffman_decode_context){ - .flags = NGHTTP3_QPACK_HUFFMAN_ACCEPTED, + .flags = NGHTTP3_QPACK_HUFFMAN_FLAG_ACCEPTED, }; } @@ -103,12 +103,12 @@ nghttp3_qpack_huffman_decode(nghttp3_qpack_huffman_decode_context *ctx, for (; src != end;) { c = *src++; t = qpack_huffman_decode_table[t.fstate][c >> 4]; - if (t.flags & NGHTTP3_QPACK_HUFFMAN_SYM) { + if (t.flags & NGHTTP3_QPACK_HUFFMAN_FLAG_SYM) { *p++ = t.sym; } - t = qpack_huffman_decode_table[t.fstate][c & 0xf]; - if (t.flags & NGHTTP3_QPACK_HUFFMAN_SYM) { + t = qpack_huffman_decode_table[t.fstate][c & 0xFU]; + if (t.flags & NGHTTP3_QPACK_HUFFMAN_FLAG_SYM) { *p++ = t.sym; } } @@ -116,7 +116,7 @@ nghttp3_qpack_huffman_decode(nghttp3_qpack_huffman_decode_context *ctx, ctx->fstate = t.fstate; ctx->flags = t.flags; - if (fin && !(ctx->flags & NGHTTP3_QPACK_HUFFMAN_ACCEPTED)) { + if (fin && !(ctx->flags & NGHTTP3_QPACK_HUFFMAN_FLAG_ACCEPTED)) { return NGHTTP3_ERR_QPACK_FATAL; } @@ -124,6 +124,6 @@ nghttp3_qpack_huffman_decode(nghttp3_qpack_huffman_decode_context *ctx, } int nghttp3_qpack_huffman_decode_failure_state( - nghttp3_qpack_huffman_decode_context *ctx) { - return ctx->fstate == 0x100; + const nghttp3_qpack_huffman_decode_context *ctx) { + return ctx->fstate == 0x100U; } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman.h b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman.h index aa262179521165..a7d788dcb964be 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman.h @@ -46,25 +46,24 @@ size_t nghttp3_qpack_huffman_encode_count(const uint8_t *src, size_t len); uint8_t *nghttp3_qpack_huffman_encode(uint8_t *dest, const uint8_t *src, size_t srclen); -typedef enum nghttp3_qpack_huffman_decode_flag { - /* FSA accepts this state as the end of huffman encoding - sequence. */ - NGHTTP3_QPACK_HUFFMAN_ACCEPTED = 1, - /* This state emits symbol */ - NGHTTP3_QPACK_HUFFMAN_SYM = 1 << 1, -} nghttp3_qpack_huffman_decode_flag; +/* NGHTTP3_QPACK_HUFFMAN_FLAG_ACCEPTED indicates that FSA accepts this + state as the end of huffman encoding sequence. */ +#define NGHTTP3_QPACK_HUFFMAN_FLAG_ACCEPTED 0x01U +/* NGHTTP3_QPACK_HUFFMAN_FLAG_SYM indicates that this state emits + symbol */ +#define NGHTTP3_QPACK_HUFFMAN_FLAG_SYM 0x02U typedef struct nghttp3_qpack_huffman_decode_node { /* fstate is the current huffman decoding state, which is actually the node ID of internal huffman tree with - nghttp3_qpack_huffman_decode_flag OR-ed. We have 257 leaf nodes, - but they are identical to root node other than emitting a symbol, - so we have 256 internal nodes [1..256], inclusive. The node ID - 256 is a special node and it is a terminal state that means - decoding failed. */ + NGHTTP3_QPACK_HUFFMAN_FLAG_* flags OR-ed. We have 257 leaf + nodes, but they are identical to root node other than emitting a + symbol, so we have 256 internal nodes [1..256], inclusive. The + node ID 256 is a special node and it is a terminal state that + means decoding failed. */ uint16_t fstate; uint8_t flags; - /* symbol if NGHTTP3_QPACK_HUFFMAN_SYM flag set */ + /* symbol if NGHTTP3_QPACK_HUFFMAN_FLAG_SYM flag set */ uint8_t sym; } nghttp3_qpack_huffman_decode_node; @@ -105,6 +104,14 @@ nghttp3_qpack_huffman_decode(nghttp3_qpack_huffman_decode_context *ctx, * indicates that huffman decoding context is in failure state. */ int nghttp3_qpack_huffman_decode_failure_state( - nghttp3_qpack_huffman_decode_context *ctx); + const nghttp3_qpack_huffman_decode_context *ctx); + +/* + * nghttp3_qpack_huffman_estimate_decode_length returns the estimated + * decoded length of the huffman encoded string of length |len|. + */ +static inline size_t nghttp3_qpack_huffman_estimate_decode_length(size_t len) { + return len * 8 / 5; +} #endif /* !defined(NGHTTP3_QPACK_HUFFMAN_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman_data.c b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman_data.c index 9eae37eb3ea1f8..9a6614a1539783 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman_data.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack_huffman_data.c @@ -28,4954 +28,4955 @@ /* Generated by mkhufftbl.py */ const nghttp3_qpack_huffman_sym huffman_sym_table[] = { - {13, 0xffc00000u}, {23, 0xffffb000u}, {28, 0xfffffe20u}, {28, 0xfffffe30u}, - {28, 0xfffffe40u}, {28, 0xfffffe50u}, {28, 0xfffffe60u}, {28, 0xfffffe70u}, - {28, 0xfffffe80u}, {24, 0xffffea00u}, {30, 0xfffffff0u}, {28, 0xfffffe90u}, - {28, 0xfffffea0u}, {30, 0xfffffff4u}, {28, 0xfffffeb0u}, {28, 0xfffffec0u}, - {28, 0xfffffed0u}, {28, 0xfffffee0u}, {28, 0xfffffef0u}, {28, 0xffffff00u}, - {28, 0xffffff10u}, {28, 0xffffff20u}, {30, 0xfffffff8u}, {28, 0xffffff30u}, - {28, 0xffffff40u}, {28, 0xffffff50u}, {28, 0xffffff60u}, {28, 0xffffff70u}, - {28, 0xffffff80u}, {28, 0xffffff90u}, {28, 0xffffffa0u}, {28, 0xffffffb0u}, - {6, 0x50000000u}, {10, 0xfe000000u}, {10, 0xfe400000u}, {12, 0xffa00000u}, - {13, 0xffc80000u}, {6, 0x54000000u}, {8, 0xf8000000u}, {11, 0xff400000u}, - {10, 0xfe800000u}, {10, 0xfec00000u}, {8, 0xf9000000u}, {11, 0xff600000u}, - {8, 0xfa000000u}, {6, 0x58000000u}, {6, 0x5c000000u}, {6, 0x60000000u}, - {5, 0x0u}, {5, 0x8000000u}, {5, 0x10000000u}, {6, 0x64000000u}, - {6, 0x68000000u}, {6, 0x6c000000u}, {6, 0x70000000u}, {6, 0x74000000u}, - {6, 0x78000000u}, {6, 0x7c000000u}, {7, 0xb8000000u}, {8, 0xfb000000u}, - {15, 0xfff80000u}, {6, 0x80000000u}, {12, 0xffb00000u}, {10, 0xff000000u}, - {13, 0xffd00000u}, {6, 0x84000000u}, {7, 0xba000000u}, {7, 0xbc000000u}, - {7, 0xbe000000u}, {7, 0xc0000000u}, {7, 0xc2000000u}, {7, 0xc4000000u}, - {7, 0xc6000000u}, {7, 0xc8000000u}, {7, 0xca000000u}, {7, 0xcc000000u}, - {7, 0xce000000u}, {7, 0xd0000000u}, {7, 0xd2000000u}, {7, 0xd4000000u}, - {7, 0xd6000000u}, {7, 0xd8000000u}, {7, 0xda000000u}, {7, 0xdc000000u}, - {7, 0xde000000u}, {7, 0xe0000000u}, {7, 0xe2000000u}, {7, 0xe4000000u}, - {8, 0xfc000000u}, {7, 0xe6000000u}, {8, 0xfd000000u}, {13, 0xffd80000u}, - {19, 0xfffe0000u}, {13, 0xffe00000u}, {14, 0xfff00000u}, {6, 0x88000000u}, - {15, 0xfffa0000u}, {5, 0x18000000u}, {6, 0x8c000000u}, {5, 0x20000000u}, - {6, 0x90000000u}, {5, 0x28000000u}, {6, 0x94000000u}, {6, 0x98000000u}, - {6, 0x9c000000u}, {5, 0x30000000u}, {7, 0xe8000000u}, {7, 0xea000000u}, - {6, 0xa0000000u}, {6, 0xa4000000u}, {6, 0xa8000000u}, {5, 0x38000000u}, - {6, 0xac000000u}, {7, 0xec000000u}, {6, 0xb0000000u}, {5, 0x40000000u}, - {5, 0x48000000u}, {6, 0xb4000000u}, {7, 0xee000000u}, {7, 0xf0000000u}, - {7, 0xf2000000u}, {7, 0xf4000000u}, {7, 0xf6000000u}, {15, 0xfffc0000u}, - {11, 0xff800000u}, {14, 0xfff40000u}, {13, 0xffe80000u}, {28, 0xffffffc0u}, - {20, 0xfffe6000u}, {22, 0xffff4800u}, {20, 0xfffe7000u}, {20, 0xfffe8000u}, - {22, 0xffff4c00u}, {22, 0xffff5000u}, {22, 0xffff5400u}, {23, 0xffffb200u}, - {22, 0xffff5800u}, {23, 0xffffb400u}, {23, 0xffffb600u}, {23, 0xffffb800u}, - {23, 0xffffba00u}, {23, 0xffffbc00u}, {24, 0xffffeb00u}, {23, 0xffffbe00u}, - {24, 0xffffec00u}, {24, 0xffffed00u}, {22, 0xffff5c00u}, {23, 0xffffc000u}, - {24, 0xffffee00u}, {23, 0xffffc200u}, {23, 0xffffc400u}, {23, 0xffffc600u}, - {23, 0xffffc800u}, {21, 0xfffee000u}, {22, 0xffff6000u}, {23, 0xffffca00u}, - {22, 0xffff6400u}, {23, 0xffffcc00u}, {23, 0xffffce00u}, {24, 0xffffef00u}, - {22, 0xffff6800u}, {21, 0xfffee800u}, {20, 0xfffe9000u}, {22, 0xffff6c00u}, - {22, 0xffff7000u}, {23, 0xffffd000u}, {23, 0xffffd200u}, {21, 0xfffef000u}, - {23, 0xffffd400u}, {22, 0xffff7400u}, {22, 0xffff7800u}, {24, 0xfffff000u}, - {21, 0xfffef800u}, {22, 0xffff7c00u}, {23, 0xffffd600u}, {23, 0xffffd800u}, - {21, 0xffff0000u}, {21, 0xffff0800u}, {22, 0xffff8000u}, {21, 0xffff1000u}, - {23, 0xffffda00u}, {22, 0xffff8400u}, {23, 0xffffdc00u}, {23, 0xffffde00u}, - {20, 0xfffea000u}, {22, 0xffff8800u}, {22, 0xffff8c00u}, {22, 0xffff9000u}, - {23, 0xffffe000u}, {22, 0xffff9400u}, {22, 0xffff9800u}, {23, 0xffffe200u}, - {26, 0xfffff800u}, {26, 0xfffff840u}, {20, 0xfffeb000u}, {19, 0xfffe2000u}, - {22, 0xffff9c00u}, {23, 0xffffe400u}, {22, 0xffffa000u}, {25, 0xfffff600u}, - {26, 0xfffff880u}, {26, 0xfffff8c0u}, {26, 0xfffff900u}, {27, 0xfffffbc0u}, - {27, 0xfffffbe0u}, {26, 0xfffff940u}, {24, 0xfffff100u}, {25, 0xfffff680u}, - {19, 0xfffe4000u}, {21, 0xffff1800u}, {26, 0xfffff980u}, {27, 0xfffffc00u}, - {27, 0xfffffc20u}, {26, 0xfffff9c0u}, {27, 0xfffffc40u}, {24, 0xfffff200u}, - {21, 0xffff2000u}, {21, 0xffff2800u}, {26, 0xfffffa00u}, {26, 0xfffffa40u}, - {28, 0xffffffd0u}, {27, 0xfffffc60u}, {27, 0xfffffc80u}, {27, 0xfffffca0u}, - {20, 0xfffec000u}, {24, 0xfffff300u}, {20, 0xfffed000u}, {21, 0xffff3000u}, - {22, 0xffffa400u}, {21, 0xffff3800u}, {21, 0xffff4000u}, {23, 0xffffe600u}, - {22, 0xffffa800u}, {22, 0xffffac00u}, {25, 0xfffff700u}, {25, 0xfffff780u}, - {24, 0xfffff400u}, {24, 0xfffff500u}, {26, 0xfffffa80u}, {23, 0xffffe800u}, - {26, 0xfffffac0u}, {27, 0xfffffcc0u}, {26, 0xfffffb00u}, {26, 0xfffffb40u}, - {27, 0xfffffce0u}, {27, 0xfffffd00u}, {27, 0xfffffd20u}, {27, 0xfffffd40u}, - {27, 0xfffffd60u}, {28, 0xffffffe0u}, {27, 0xfffffd80u}, {27, 0xfffffda0u}, - {27, 0xfffffdc0u}, {27, 0xfffffde0u}, {27, 0xfffffe00u}, {26, 0xfffffb80u}, - {30, 0xfffffffcu}}; + {13, 0xFFC00000U}, {23, 0xFFFFB000U}, {28, 0xFFFFFE20U}, {28, 0xFFFFFE30U}, + {28, 0xFFFFFE40U}, {28, 0xFFFFFE50U}, {28, 0xFFFFFE60U}, {28, 0xFFFFFE70U}, + {28, 0xFFFFFE80U}, {24, 0xFFFFEA00U}, {30, 0xFFFFFFF0U}, {28, 0xFFFFFE90U}, + {28, 0xFFFFFEA0U}, {30, 0xFFFFFFF4U}, {28, 0xFFFFFEB0U}, {28, 0xFFFFFEC0U}, + {28, 0xFFFFFED0U}, {28, 0xFFFFFEE0U}, {28, 0xFFFFFEF0U}, {28, 0xFFFFFF00U}, + {28, 0xFFFFFF10U}, {28, 0xFFFFFF20U}, {30, 0xFFFFFFF8U}, {28, 0xFFFFFF30U}, + {28, 0xFFFFFF40U}, {28, 0xFFFFFF50U}, {28, 0xFFFFFF60U}, {28, 0xFFFFFF70U}, + {28, 0xFFFFFF80U}, {28, 0xFFFFFF90U}, {28, 0xFFFFFFA0U}, {28, 0xFFFFFFB0U}, + {6, 0x50000000U}, {10, 0xFE000000U}, {10, 0xFE400000U}, {12, 0xFFA00000U}, + {13, 0xFFC80000U}, {6, 0x54000000U}, {8, 0xF8000000U}, {11, 0xFF400000U}, + {10, 0xFE800000U}, {10, 0xFEC00000U}, {8, 0xF9000000U}, {11, 0xFF600000U}, + {8, 0xFA000000U}, {6, 0x58000000U}, {6, 0x5C000000U}, {6, 0x60000000U}, + {5, 0x0U}, {5, 0x8000000U}, {5, 0x10000000U}, {6, 0x64000000U}, + {6, 0x68000000U}, {6, 0x6C000000U}, {6, 0x70000000U}, {6, 0x74000000U}, + {6, 0x78000000U}, {6, 0x7C000000U}, {7, 0xB8000000U}, {8, 0xFB000000U}, + {15, 0xFFF80000U}, {6, 0x80000000U}, {12, 0xFFB00000U}, {10, 0xFF000000U}, + {13, 0xFFD00000U}, {6, 0x84000000U}, {7, 0xBA000000U}, {7, 0xBC000000U}, + {7, 0xBE000000U}, {7, 0xC0000000U}, {7, 0xC2000000U}, {7, 0xC4000000U}, + {7, 0xC6000000U}, {7, 0xC8000000U}, {7, 0xCA000000U}, {7, 0xCC000000U}, + {7, 0xCE000000U}, {7, 0xD0000000U}, {7, 0xD2000000U}, {7, 0xD4000000U}, + {7, 0xD6000000U}, {7, 0xD8000000U}, {7, 0xDA000000U}, {7, 0xDC000000U}, + {7, 0xDE000000U}, {7, 0xE0000000U}, {7, 0xE2000000U}, {7, 0xE4000000U}, + {8, 0xFC000000U}, {7, 0xE6000000U}, {8, 0xFD000000U}, {13, 0xFFD80000U}, + {19, 0xFFFE0000U}, {13, 0xFFE00000U}, {14, 0xFFF00000U}, {6, 0x88000000U}, + {15, 0xFFFA0000U}, {5, 0x18000000U}, {6, 0x8C000000U}, {5, 0x20000000U}, + {6, 0x90000000U}, {5, 0x28000000U}, {6, 0x94000000U}, {6, 0x98000000U}, + {6, 0x9C000000U}, {5, 0x30000000U}, {7, 0xE8000000U}, {7, 0xEA000000U}, + {6, 0xA0000000U}, {6, 0xA4000000U}, {6, 0xA8000000U}, {5, 0x38000000U}, + {6, 0xAC000000U}, {7, 0xEC000000U}, {6, 0xB0000000U}, {5, 0x40000000U}, + {5, 0x48000000U}, {6, 0xB4000000U}, {7, 0xEE000000U}, {7, 0xF0000000U}, + {7, 0xF2000000U}, {7, 0xF4000000U}, {7, 0xF6000000U}, {15, 0xFFFC0000U}, + {11, 0xFF800000U}, {14, 0xFFF40000U}, {13, 0xFFE80000U}, {28, 0xFFFFFFC0U}, + {20, 0xFFFE6000U}, {22, 0xFFFF4800U}, {20, 0xFFFE7000U}, {20, 0xFFFE8000U}, + {22, 0xFFFF4C00U}, {22, 0xFFFF5000U}, {22, 0xFFFF5400U}, {23, 0xFFFFB200U}, + {22, 0xFFFF5800U}, {23, 0xFFFFB400U}, {23, 0xFFFFB600U}, {23, 0xFFFFB800U}, + {23, 0xFFFFBA00U}, {23, 0xFFFFBC00U}, {24, 0xFFFFEB00U}, {23, 0xFFFFBE00U}, + {24, 0xFFFFEC00U}, {24, 0xFFFFED00U}, {22, 0xFFFF5C00U}, {23, 0xFFFFC000U}, + {24, 0xFFFFEE00U}, {23, 0xFFFFC200U}, {23, 0xFFFFC400U}, {23, 0xFFFFC600U}, + {23, 0xFFFFC800U}, {21, 0xFFFEE000U}, {22, 0xFFFF6000U}, {23, 0xFFFFCA00U}, + {22, 0xFFFF6400U}, {23, 0xFFFFCC00U}, {23, 0xFFFFCE00U}, {24, 0xFFFFEF00U}, + {22, 0xFFFF6800U}, {21, 0xFFFEE800U}, {20, 0xFFFE9000U}, {22, 0xFFFF6C00U}, + {22, 0xFFFF7000U}, {23, 0xFFFFD000U}, {23, 0xFFFFD200U}, {21, 0xFFFEF000U}, + {23, 0xFFFFD400U}, {22, 0xFFFF7400U}, {22, 0xFFFF7800U}, {24, 0xFFFFF000U}, + {21, 0xFFFEF800U}, {22, 0xFFFF7C00U}, {23, 0xFFFFD600U}, {23, 0xFFFFD800U}, + {21, 0xFFFF0000U}, {21, 0xFFFF0800U}, {22, 0xFFFF8000U}, {21, 0xFFFF1000U}, + {23, 0xFFFFDA00U}, {22, 0xFFFF8400U}, {23, 0xFFFFDC00U}, {23, 0xFFFFDE00U}, + {20, 0xFFFEA000U}, {22, 0xFFFF8800U}, {22, 0xFFFF8C00U}, {22, 0xFFFF9000U}, + {23, 0xFFFFE000U}, {22, 0xFFFF9400U}, {22, 0xFFFF9800U}, {23, 0xFFFFE200U}, + {26, 0xFFFFF800U}, {26, 0xFFFFF840U}, {20, 0xFFFEB000U}, {19, 0xFFFE2000U}, + {22, 0xFFFF9C00U}, {23, 0xFFFFE400U}, {22, 0xFFFFA000U}, {25, 0xFFFFF600U}, + {26, 0xFFFFF880U}, {26, 0xFFFFF8C0U}, {26, 0xFFFFF900U}, {27, 0xFFFFFBC0U}, + {27, 0xFFFFFBE0U}, {26, 0xFFFFF940U}, {24, 0xFFFFF100U}, {25, 0xFFFFF680U}, + {19, 0xFFFE4000U}, {21, 0xFFFF1800U}, {26, 0xFFFFF980U}, {27, 0xFFFFFC00U}, + {27, 0xFFFFFC20U}, {26, 0xFFFFF9C0U}, {27, 0xFFFFFC40U}, {24, 0xFFFFF200U}, + {21, 0xFFFF2000U}, {21, 0xFFFF2800U}, {26, 0xFFFFFA00U}, {26, 0xFFFFFA40U}, + {28, 0xFFFFFFD0U}, {27, 0xFFFFFC60U}, {27, 0xFFFFFC80U}, {27, 0xFFFFFCA0U}, + {20, 0xFFFEC000U}, {24, 0xFFFFF300U}, {20, 0xFFFED000U}, {21, 0xFFFF3000U}, + {22, 0xFFFFA400U}, {21, 0xFFFF3800U}, {21, 0xFFFF4000U}, {23, 0xFFFFE600U}, + {22, 0xFFFFA800U}, {22, 0xFFFFAC00U}, {25, 0xFFFFF700U}, {25, 0xFFFFF780U}, + {24, 0xFFFFF400U}, {24, 0xFFFFF500U}, {26, 0xFFFFFA80U}, {23, 0xFFFFE800U}, + {26, 0xFFFFFAC0U}, {27, 0xFFFFFCC0U}, {26, 0xFFFFFB00U}, {26, 0xFFFFFB40U}, + {27, 0xFFFFFCE0U}, {27, 0xFFFFFD00U}, {27, 0xFFFFFD20U}, {27, 0xFFFFFD40U}, + {27, 0xFFFFFD60U}, {28, 0xFFFFFFE0U}, {27, 0xFFFFFD80U}, {27, 0xFFFFFDA0U}, + {27, 0xFFFFFDC0U}, {27, 0xFFFFFDE0U}, {27, 0xFFFFFE00U}, {26, 0xFFFFFB80U}, + {30, 0xFFFFFFFCU}, +}; const nghttp3_qpack_huffman_decode_node qpack_huffman_decode_table[][16] = { /* 0 */ { - {0x04, 0, 0}, - {0x05, 0, 0}, - {0x07, 0, 0}, - {0x08, 0, 0}, - {0x0b, 0, 0}, - {0x0c, 0, 0}, - {0x10, 0, 0}, - {0x13, 0, 0}, - {0x19, 0, 0}, - {0x1c, 0, 0}, - {0x20, 0, 0}, - {0x23, 0, 0}, - {0x2a, 0, 0}, - {0x31, 0, 0}, - {0x39, 0, 0}, - {0x40, 1, 0}, + {0x04, 0x00, 0x00}, + {0x05, 0x00, 0x00}, + {0x07, 0x00, 0x00}, + {0x08, 0x00, 0x00}, + {0x0B, 0x00, 0x00}, + {0x0C, 0x00, 0x00}, + {0x10, 0x00, 0x00}, + {0x13, 0x00, 0x00}, + {0x19, 0x00, 0x00}, + {0x1C, 0x00, 0x00}, + {0x20, 0x00, 0x00}, + {0x23, 0x00, 0x00}, + {0x2A, 0x00, 0x00}, + {0x31, 0x00, 0x00}, + {0x39, 0x00, 0x00}, + {0x40, 0x01, 0x00}, }, /* 1 */ { - {0x00, 3, 48}, - {0x00, 3, 49}, - {0x00, 3, 50}, - {0x00, 3, 97}, - {0x00, 3, 99}, - {0x00, 3, 101}, - {0x00, 3, 105}, - {0x00, 3, 111}, - {0x00, 3, 115}, - {0x00, 3, 116}, - {0x0d, 0, 0}, - {0x0e, 0, 0}, - {0x11, 0, 0}, - {0x12, 0, 0}, - {0x14, 0, 0}, - {0x15, 0, 0}, + {0x00, 0x03, 0x30}, + {0x00, 0x03, 0x31}, + {0x00, 0x03, 0x32}, + {0x00, 0x03, 0x61}, + {0x00, 0x03, 0x63}, + {0x00, 0x03, 0x65}, + {0x00, 0x03, 0x69}, + {0x00, 0x03, 0x6F}, + {0x00, 0x03, 0x73}, + {0x00, 0x03, 0x74}, + {0x0D, 0x00, 0x00}, + {0x0E, 0x00, 0x00}, + {0x11, 0x00, 0x00}, + {0x12, 0x00, 0x00}, + {0x14, 0x00, 0x00}, + {0x15, 0x00, 0x00}, }, /* 2 */ { - {0x01, 2, 48}, - {0x16, 3, 48}, - {0x01, 2, 49}, - {0x16, 3, 49}, - {0x01, 2, 50}, - {0x16, 3, 50}, - {0x01, 2, 97}, - {0x16, 3, 97}, - {0x01, 2, 99}, - {0x16, 3, 99}, - {0x01, 2, 101}, - {0x16, 3, 101}, - {0x01, 2, 105}, - {0x16, 3, 105}, - {0x01, 2, 111}, - {0x16, 3, 111}, + {0x01, 0x02, 0x30}, + {0x16, 0x03, 0x30}, + {0x01, 0x02, 0x31}, + {0x16, 0x03, 0x31}, + {0x01, 0x02, 0x32}, + {0x16, 0x03, 0x32}, + {0x01, 0x02, 0x61}, + {0x16, 0x03, 0x61}, + {0x01, 0x02, 0x63}, + {0x16, 0x03, 0x63}, + {0x01, 0x02, 0x65}, + {0x16, 0x03, 0x65}, + {0x01, 0x02, 0x69}, + {0x16, 0x03, 0x69}, + {0x01, 0x02, 0x6F}, + {0x16, 0x03, 0x6F}, }, /* 3 */ { - {0x02, 2, 48}, - {0x09, 2, 48}, - {0x17, 2, 48}, - {0x28, 3, 48}, - {0x02, 2, 49}, - {0x09, 2, 49}, - {0x17, 2, 49}, - {0x28, 3, 49}, - {0x02, 2, 50}, - {0x09, 2, 50}, - {0x17, 2, 50}, - {0x28, 3, 50}, - {0x02, 2, 97}, - {0x09, 2, 97}, - {0x17, 2, 97}, - {0x28, 3, 97}, + {0x02, 0x02, 0x30}, + {0x09, 0x02, 0x30}, + {0x17, 0x02, 0x30}, + {0x28, 0x03, 0x30}, + {0x02, 0x02, 0x31}, + {0x09, 0x02, 0x31}, + {0x17, 0x02, 0x31}, + {0x28, 0x03, 0x31}, + {0x02, 0x02, 0x32}, + {0x09, 0x02, 0x32}, + {0x17, 0x02, 0x32}, + {0x28, 0x03, 0x32}, + {0x02, 0x02, 0x61}, + {0x09, 0x02, 0x61}, + {0x17, 0x02, 0x61}, + {0x28, 0x03, 0x61}, }, /* 4 */ { - {0x03, 2, 48}, - {0x06, 2, 48}, - {0x0a, 2, 48}, - {0x0f, 2, 48}, - {0x18, 2, 48}, - {0x1f, 2, 48}, - {0x29, 2, 48}, - {0x38, 3, 48}, - {0x03, 2, 49}, - {0x06, 2, 49}, - {0x0a, 2, 49}, - {0x0f, 2, 49}, - {0x18, 2, 49}, - {0x1f, 2, 49}, - {0x29, 2, 49}, - {0x38, 3, 49}, + {0x03, 0x02, 0x30}, + {0x06, 0x02, 0x30}, + {0x0A, 0x02, 0x30}, + {0x0F, 0x02, 0x30}, + {0x18, 0x02, 0x30}, + {0x1F, 0x02, 0x30}, + {0x29, 0x02, 0x30}, + {0x38, 0x03, 0x30}, + {0x03, 0x02, 0x31}, + {0x06, 0x02, 0x31}, + {0x0A, 0x02, 0x31}, + {0x0F, 0x02, 0x31}, + {0x18, 0x02, 0x31}, + {0x1F, 0x02, 0x31}, + {0x29, 0x02, 0x31}, + {0x38, 0x03, 0x31}, }, /* 5 */ { - {0x03, 2, 50}, - {0x06, 2, 50}, - {0x0a, 2, 50}, - {0x0f, 2, 50}, - {0x18, 2, 50}, - {0x1f, 2, 50}, - {0x29, 2, 50}, - {0x38, 3, 50}, - {0x03, 2, 97}, - {0x06, 2, 97}, - {0x0a, 2, 97}, - {0x0f, 2, 97}, - {0x18, 2, 97}, - {0x1f, 2, 97}, - {0x29, 2, 97}, - {0x38, 3, 97}, + {0x03, 0x02, 0x32}, + {0x06, 0x02, 0x32}, + {0x0A, 0x02, 0x32}, + {0x0F, 0x02, 0x32}, + {0x18, 0x02, 0x32}, + {0x1F, 0x02, 0x32}, + {0x29, 0x02, 0x32}, + {0x38, 0x03, 0x32}, + {0x03, 0x02, 0x61}, + {0x06, 0x02, 0x61}, + {0x0A, 0x02, 0x61}, + {0x0F, 0x02, 0x61}, + {0x18, 0x02, 0x61}, + {0x1F, 0x02, 0x61}, + {0x29, 0x02, 0x61}, + {0x38, 0x03, 0x61}, }, /* 6 */ { - {0x02, 2, 99}, - {0x09, 2, 99}, - {0x17, 2, 99}, - {0x28, 3, 99}, - {0x02, 2, 101}, - {0x09, 2, 101}, - {0x17, 2, 101}, - {0x28, 3, 101}, - {0x02, 2, 105}, - {0x09, 2, 105}, - {0x17, 2, 105}, - {0x28, 3, 105}, - {0x02, 2, 111}, - {0x09, 2, 111}, - {0x17, 2, 111}, - {0x28, 3, 111}, + {0x02, 0x02, 0x63}, + {0x09, 0x02, 0x63}, + {0x17, 0x02, 0x63}, + {0x28, 0x03, 0x63}, + {0x02, 0x02, 0x65}, + {0x09, 0x02, 0x65}, + {0x17, 0x02, 0x65}, + {0x28, 0x03, 0x65}, + {0x02, 0x02, 0x69}, + {0x09, 0x02, 0x69}, + {0x17, 0x02, 0x69}, + {0x28, 0x03, 0x69}, + {0x02, 0x02, 0x6F}, + {0x09, 0x02, 0x6F}, + {0x17, 0x02, 0x6F}, + {0x28, 0x03, 0x6F}, }, /* 7 */ { - {0x03, 2, 99}, - {0x06, 2, 99}, - {0x0a, 2, 99}, - {0x0f, 2, 99}, - {0x18, 2, 99}, - {0x1f, 2, 99}, - {0x29, 2, 99}, - {0x38, 3, 99}, - {0x03, 2, 101}, - {0x06, 2, 101}, - {0x0a, 2, 101}, - {0x0f, 2, 101}, - {0x18, 2, 101}, - {0x1f, 2, 101}, - {0x29, 2, 101}, - {0x38, 3, 101}, + {0x03, 0x02, 0x63}, + {0x06, 0x02, 0x63}, + {0x0A, 0x02, 0x63}, + {0x0F, 0x02, 0x63}, + {0x18, 0x02, 0x63}, + {0x1F, 0x02, 0x63}, + {0x29, 0x02, 0x63}, + {0x38, 0x03, 0x63}, + {0x03, 0x02, 0x65}, + {0x06, 0x02, 0x65}, + {0x0A, 0x02, 0x65}, + {0x0F, 0x02, 0x65}, + {0x18, 0x02, 0x65}, + {0x1F, 0x02, 0x65}, + {0x29, 0x02, 0x65}, + {0x38, 0x03, 0x65}, }, /* 8 */ { - {0x03, 2, 105}, - {0x06, 2, 105}, - {0x0a, 2, 105}, - {0x0f, 2, 105}, - {0x18, 2, 105}, - {0x1f, 2, 105}, - {0x29, 2, 105}, - {0x38, 3, 105}, - {0x03, 2, 111}, - {0x06, 2, 111}, - {0x0a, 2, 111}, - {0x0f, 2, 111}, - {0x18, 2, 111}, - {0x1f, 2, 111}, - {0x29, 2, 111}, - {0x38, 3, 111}, + {0x03, 0x02, 0x69}, + {0x06, 0x02, 0x69}, + {0x0A, 0x02, 0x69}, + {0x0F, 0x02, 0x69}, + {0x18, 0x02, 0x69}, + {0x1F, 0x02, 0x69}, + {0x29, 0x02, 0x69}, + {0x38, 0x03, 0x69}, + {0x03, 0x02, 0x6F}, + {0x06, 0x02, 0x6F}, + {0x0A, 0x02, 0x6F}, + {0x0F, 0x02, 0x6F}, + {0x18, 0x02, 0x6F}, + {0x1F, 0x02, 0x6F}, + {0x29, 0x02, 0x6F}, + {0x38, 0x03, 0x6F}, }, /* 9 */ { - {0x01, 2, 115}, - {0x16, 3, 115}, - {0x01, 2, 116}, - {0x16, 3, 116}, - {0x00, 3, 32}, - {0x00, 3, 37}, - {0x00, 3, 45}, - {0x00, 3, 46}, - {0x00, 3, 47}, - {0x00, 3, 51}, - {0x00, 3, 52}, - {0x00, 3, 53}, - {0x00, 3, 54}, - {0x00, 3, 55}, - {0x00, 3, 56}, - {0x00, 3, 57}, + {0x01, 0x02, 0x73}, + {0x16, 0x03, 0x73}, + {0x01, 0x02, 0x74}, + {0x16, 0x03, 0x74}, + {0x00, 0x03, 0x20}, + {0x00, 0x03, 0x25}, + {0x00, 0x03, 0x2D}, + {0x00, 0x03, 0x2E}, + {0x00, 0x03, 0x2F}, + {0x00, 0x03, 0x33}, + {0x00, 0x03, 0x34}, + {0x00, 0x03, 0x35}, + {0x00, 0x03, 0x36}, + {0x00, 0x03, 0x37}, + {0x00, 0x03, 0x38}, + {0x00, 0x03, 0x39}, }, /* 10 */ { - {0x02, 2, 115}, - {0x09, 2, 115}, - {0x17, 2, 115}, - {0x28, 3, 115}, - {0x02, 2, 116}, - {0x09, 2, 116}, - {0x17, 2, 116}, - {0x28, 3, 116}, - {0x01, 2, 32}, - {0x16, 3, 32}, - {0x01, 2, 37}, - {0x16, 3, 37}, - {0x01, 2, 45}, - {0x16, 3, 45}, - {0x01, 2, 46}, - {0x16, 3, 46}, + {0x02, 0x02, 0x73}, + {0x09, 0x02, 0x73}, + {0x17, 0x02, 0x73}, + {0x28, 0x03, 0x73}, + {0x02, 0x02, 0x74}, + {0x09, 0x02, 0x74}, + {0x17, 0x02, 0x74}, + {0x28, 0x03, 0x74}, + {0x01, 0x02, 0x20}, + {0x16, 0x03, 0x20}, + {0x01, 0x02, 0x25}, + {0x16, 0x03, 0x25}, + {0x01, 0x02, 0x2D}, + {0x16, 0x03, 0x2D}, + {0x01, 0x02, 0x2E}, + {0x16, 0x03, 0x2E}, }, /* 11 */ { - {0x03, 2, 115}, - {0x06, 2, 115}, - {0x0a, 2, 115}, - {0x0f, 2, 115}, - {0x18, 2, 115}, - {0x1f, 2, 115}, - {0x29, 2, 115}, - {0x38, 3, 115}, - {0x03, 2, 116}, - {0x06, 2, 116}, - {0x0a, 2, 116}, - {0x0f, 2, 116}, - {0x18, 2, 116}, - {0x1f, 2, 116}, - {0x29, 2, 116}, - {0x38, 3, 116}, + {0x03, 0x02, 0x73}, + {0x06, 0x02, 0x73}, + {0x0A, 0x02, 0x73}, + {0x0F, 0x02, 0x73}, + {0x18, 0x02, 0x73}, + {0x1F, 0x02, 0x73}, + {0x29, 0x02, 0x73}, + {0x38, 0x03, 0x73}, + {0x03, 0x02, 0x74}, + {0x06, 0x02, 0x74}, + {0x0A, 0x02, 0x74}, + {0x0F, 0x02, 0x74}, + {0x18, 0x02, 0x74}, + {0x1F, 0x02, 0x74}, + {0x29, 0x02, 0x74}, + {0x38, 0x03, 0x74}, }, /* 12 */ { - {0x02, 2, 32}, - {0x09, 2, 32}, - {0x17, 2, 32}, - {0x28, 3, 32}, - {0x02, 2, 37}, - {0x09, 2, 37}, - {0x17, 2, 37}, - {0x28, 3, 37}, - {0x02, 2, 45}, - {0x09, 2, 45}, - {0x17, 2, 45}, - {0x28, 3, 45}, - {0x02, 2, 46}, - {0x09, 2, 46}, - {0x17, 2, 46}, - {0x28, 3, 46}, + {0x02, 0x02, 0x20}, + {0x09, 0x02, 0x20}, + {0x17, 0x02, 0x20}, + {0x28, 0x03, 0x20}, + {0x02, 0x02, 0x25}, + {0x09, 0x02, 0x25}, + {0x17, 0x02, 0x25}, + {0x28, 0x03, 0x25}, + {0x02, 0x02, 0x2D}, + {0x09, 0x02, 0x2D}, + {0x17, 0x02, 0x2D}, + {0x28, 0x03, 0x2D}, + {0x02, 0x02, 0x2E}, + {0x09, 0x02, 0x2E}, + {0x17, 0x02, 0x2E}, + {0x28, 0x03, 0x2E}, }, /* 13 */ { - {0x03, 2, 32}, - {0x06, 2, 32}, - {0x0a, 2, 32}, - {0x0f, 2, 32}, - {0x18, 2, 32}, - {0x1f, 2, 32}, - {0x29, 2, 32}, - {0x38, 3, 32}, - {0x03, 2, 37}, - {0x06, 2, 37}, - {0x0a, 2, 37}, - {0x0f, 2, 37}, - {0x18, 2, 37}, - {0x1f, 2, 37}, - {0x29, 2, 37}, - {0x38, 3, 37}, + {0x03, 0x02, 0x20}, + {0x06, 0x02, 0x20}, + {0x0A, 0x02, 0x20}, + {0x0F, 0x02, 0x20}, + {0x18, 0x02, 0x20}, + {0x1F, 0x02, 0x20}, + {0x29, 0x02, 0x20}, + {0x38, 0x03, 0x20}, + {0x03, 0x02, 0x25}, + {0x06, 0x02, 0x25}, + {0x0A, 0x02, 0x25}, + {0x0F, 0x02, 0x25}, + {0x18, 0x02, 0x25}, + {0x1F, 0x02, 0x25}, + {0x29, 0x02, 0x25}, + {0x38, 0x03, 0x25}, }, /* 14 */ { - {0x03, 2, 45}, - {0x06, 2, 45}, - {0x0a, 2, 45}, - {0x0f, 2, 45}, - {0x18, 2, 45}, - {0x1f, 2, 45}, - {0x29, 2, 45}, - {0x38, 3, 45}, - {0x03, 2, 46}, - {0x06, 2, 46}, - {0x0a, 2, 46}, - {0x0f, 2, 46}, - {0x18, 2, 46}, - {0x1f, 2, 46}, - {0x29, 2, 46}, - {0x38, 3, 46}, + {0x03, 0x02, 0x2D}, + {0x06, 0x02, 0x2D}, + {0x0A, 0x02, 0x2D}, + {0x0F, 0x02, 0x2D}, + {0x18, 0x02, 0x2D}, + {0x1F, 0x02, 0x2D}, + {0x29, 0x02, 0x2D}, + {0x38, 0x03, 0x2D}, + {0x03, 0x02, 0x2E}, + {0x06, 0x02, 0x2E}, + {0x0A, 0x02, 0x2E}, + {0x0F, 0x02, 0x2E}, + {0x18, 0x02, 0x2E}, + {0x1F, 0x02, 0x2E}, + {0x29, 0x02, 0x2E}, + {0x38, 0x03, 0x2E}, }, /* 15 */ { - {0x01, 2, 47}, - {0x16, 3, 47}, - {0x01, 2, 51}, - {0x16, 3, 51}, - {0x01, 2, 52}, - {0x16, 3, 52}, - {0x01, 2, 53}, - {0x16, 3, 53}, - {0x01, 2, 54}, - {0x16, 3, 54}, - {0x01, 2, 55}, - {0x16, 3, 55}, - {0x01, 2, 56}, - {0x16, 3, 56}, - {0x01, 2, 57}, - {0x16, 3, 57}, + {0x01, 0x02, 0x2F}, + {0x16, 0x03, 0x2F}, + {0x01, 0x02, 0x33}, + {0x16, 0x03, 0x33}, + {0x01, 0x02, 0x34}, + {0x16, 0x03, 0x34}, + {0x01, 0x02, 0x35}, + {0x16, 0x03, 0x35}, + {0x01, 0x02, 0x36}, + {0x16, 0x03, 0x36}, + {0x01, 0x02, 0x37}, + {0x16, 0x03, 0x37}, + {0x01, 0x02, 0x38}, + {0x16, 0x03, 0x38}, + {0x01, 0x02, 0x39}, + {0x16, 0x03, 0x39}, }, /* 16 */ { - {0x02, 2, 47}, - {0x09, 2, 47}, - {0x17, 2, 47}, - {0x28, 3, 47}, - {0x02, 2, 51}, - {0x09, 2, 51}, - {0x17, 2, 51}, - {0x28, 3, 51}, - {0x02, 2, 52}, - {0x09, 2, 52}, - {0x17, 2, 52}, - {0x28, 3, 52}, - {0x02, 2, 53}, - {0x09, 2, 53}, - {0x17, 2, 53}, - {0x28, 3, 53}, + {0x02, 0x02, 0x2F}, + {0x09, 0x02, 0x2F}, + {0x17, 0x02, 0x2F}, + {0x28, 0x03, 0x2F}, + {0x02, 0x02, 0x33}, + {0x09, 0x02, 0x33}, + {0x17, 0x02, 0x33}, + {0x28, 0x03, 0x33}, + {0x02, 0x02, 0x34}, + {0x09, 0x02, 0x34}, + {0x17, 0x02, 0x34}, + {0x28, 0x03, 0x34}, + {0x02, 0x02, 0x35}, + {0x09, 0x02, 0x35}, + {0x17, 0x02, 0x35}, + {0x28, 0x03, 0x35}, }, /* 17 */ { - {0x03, 2, 47}, - {0x06, 2, 47}, - {0x0a, 2, 47}, - {0x0f, 2, 47}, - {0x18, 2, 47}, - {0x1f, 2, 47}, - {0x29, 2, 47}, - {0x38, 3, 47}, - {0x03, 2, 51}, - {0x06, 2, 51}, - {0x0a, 2, 51}, - {0x0f, 2, 51}, - {0x18, 2, 51}, - {0x1f, 2, 51}, - {0x29, 2, 51}, - {0x38, 3, 51}, + {0x03, 0x02, 0x2F}, + {0x06, 0x02, 0x2F}, + {0x0A, 0x02, 0x2F}, + {0x0F, 0x02, 0x2F}, + {0x18, 0x02, 0x2F}, + {0x1F, 0x02, 0x2F}, + {0x29, 0x02, 0x2F}, + {0x38, 0x03, 0x2F}, + {0x03, 0x02, 0x33}, + {0x06, 0x02, 0x33}, + {0x0A, 0x02, 0x33}, + {0x0F, 0x02, 0x33}, + {0x18, 0x02, 0x33}, + {0x1F, 0x02, 0x33}, + {0x29, 0x02, 0x33}, + {0x38, 0x03, 0x33}, }, /* 18 */ { - {0x03, 2, 52}, - {0x06, 2, 52}, - {0x0a, 2, 52}, - {0x0f, 2, 52}, - {0x18, 2, 52}, - {0x1f, 2, 52}, - {0x29, 2, 52}, - {0x38, 3, 52}, - {0x03, 2, 53}, - {0x06, 2, 53}, - {0x0a, 2, 53}, - {0x0f, 2, 53}, - {0x18, 2, 53}, - {0x1f, 2, 53}, - {0x29, 2, 53}, - {0x38, 3, 53}, + {0x03, 0x02, 0x34}, + {0x06, 0x02, 0x34}, + {0x0A, 0x02, 0x34}, + {0x0F, 0x02, 0x34}, + {0x18, 0x02, 0x34}, + {0x1F, 0x02, 0x34}, + {0x29, 0x02, 0x34}, + {0x38, 0x03, 0x34}, + {0x03, 0x02, 0x35}, + {0x06, 0x02, 0x35}, + {0x0A, 0x02, 0x35}, + {0x0F, 0x02, 0x35}, + {0x18, 0x02, 0x35}, + {0x1F, 0x02, 0x35}, + {0x29, 0x02, 0x35}, + {0x38, 0x03, 0x35}, }, /* 19 */ { - {0x02, 2, 54}, - {0x09, 2, 54}, - {0x17, 2, 54}, - {0x28, 3, 54}, - {0x02, 2, 55}, - {0x09, 2, 55}, - {0x17, 2, 55}, - {0x28, 3, 55}, - {0x02, 2, 56}, - {0x09, 2, 56}, - {0x17, 2, 56}, - {0x28, 3, 56}, - {0x02, 2, 57}, - {0x09, 2, 57}, - {0x17, 2, 57}, - {0x28, 3, 57}, + {0x02, 0x02, 0x36}, + {0x09, 0x02, 0x36}, + {0x17, 0x02, 0x36}, + {0x28, 0x03, 0x36}, + {0x02, 0x02, 0x37}, + {0x09, 0x02, 0x37}, + {0x17, 0x02, 0x37}, + {0x28, 0x03, 0x37}, + {0x02, 0x02, 0x38}, + {0x09, 0x02, 0x38}, + {0x17, 0x02, 0x38}, + {0x28, 0x03, 0x38}, + {0x02, 0x02, 0x39}, + {0x09, 0x02, 0x39}, + {0x17, 0x02, 0x39}, + {0x28, 0x03, 0x39}, }, /* 20 */ { - {0x03, 2, 54}, - {0x06, 2, 54}, - {0x0a, 2, 54}, - {0x0f, 2, 54}, - {0x18, 2, 54}, - {0x1f, 2, 54}, - {0x29, 2, 54}, - {0x38, 3, 54}, - {0x03, 2, 55}, - {0x06, 2, 55}, - {0x0a, 2, 55}, - {0x0f, 2, 55}, - {0x18, 2, 55}, - {0x1f, 2, 55}, - {0x29, 2, 55}, - {0x38, 3, 55}, + {0x03, 0x02, 0x36}, + {0x06, 0x02, 0x36}, + {0x0A, 0x02, 0x36}, + {0x0F, 0x02, 0x36}, + {0x18, 0x02, 0x36}, + {0x1F, 0x02, 0x36}, + {0x29, 0x02, 0x36}, + {0x38, 0x03, 0x36}, + {0x03, 0x02, 0x37}, + {0x06, 0x02, 0x37}, + {0x0A, 0x02, 0x37}, + {0x0F, 0x02, 0x37}, + {0x18, 0x02, 0x37}, + {0x1F, 0x02, 0x37}, + {0x29, 0x02, 0x37}, + {0x38, 0x03, 0x37}, }, /* 21 */ { - {0x03, 2, 56}, - {0x06, 2, 56}, - {0x0a, 2, 56}, - {0x0f, 2, 56}, - {0x18, 2, 56}, - {0x1f, 2, 56}, - {0x29, 2, 56}, - {0x38, 3, 56}, - {0x03, 2, 57}, - {0x06, 2, 57}, - {0x0a, 2, 57}, - {0x0f, 2, 57}, - {0x18, 2, 57}, - {0x1f, 2, 57}, - {0x29, 2, 57}, - {0x38, 3, 57}, + {0x03, 0x02, 0x38}, + {0x06, 0x02, 0x38}, + {0x0A, 0x02, 0x38}, + {0x0F, 0x02, 0x38}, + {0x18, 0x02, 0x38}, + {0x1F, 0x02, 0x38}, + {0x29, 0x02, 0x38}, + {0x38, 0x03, 0x38}, + {0x03, 0x02, 0x39}, + {0x06, 0x02, 0x39}, + {0x0A, 0x02, 0x39}, + {0x0F, 0x02, 0x39}, + {0x18, 0x02, 0x39}, + {0x1F, 0x02, 0x39}, + {0x29, 0x02, 0x39}, + {0x38, 0x03, 0x39}, }, /* 22 */ { - {0x1a, 0, 0}, - {0x1b, 0, 0}, - {0x1d, 0, 0}, - {0x1e, 0, 0}, - {0x21, 0, 0}, - {0x22, 0, 0}, - {0x24, 0, 0}, - {0x25, 0, 0}, - {0x2b, 0, 0}, - {0x2e, 0, 0}, - {0x32, 0, 0}, - {0x35, 0, 0}, - {0x3a, 0, 0}, - {0x3d, 0, 0}, - {0x41, 0, 0}, - {0x44, 1, 0}, + {0x1A, 0x00, 0x00}, + {0x1B, 0x00, 0x00}, + {0x1D, 0x00, 0x00}, + {0x1E, 0x00, 0x00}, + {0x21, 0x00, 0x00}, + {0x22, 0x00, 0x00}, + {0x24, 0x00, 0x00}, + {0x25, 0x00, 0x00}, + {0x2B, 0x00, 0x00}, + {0x2E, 0x00, 0x00}, + {0x32, 0x00, 0x00}, + {0x35, 0x00, 0x00}, + {0x3A, 0x00, 0x00}, + {0x3D, 0x00, 0x00}, + {0x41, 0x00, 0x00}, + {0x44, 0x01, 0x00}, }, /* 23 */ { - {0x00, 3, 61}, - {0x00, 3, 65}, - {0x00, 3, 95}, - {0x00, 3, 98}, - {0x00, 3, 100}, - {0x00, 3, 102}, - {0x00, 3, 103}, - {0x00, 3, 104}, - {0x00, 3, 108}, - {0x00, 3, 109}, - {0x00, 3, 110}, - {0x00, 3, 112}, - {0x00, 3, 114}, - {0x00, 3, 117}, - {0x26, 0, 0}, - {0x27, 0, 0}, + {0x00, 0x03, 0x3D}, + {0x00, 0x03, 0x41}, + {0x00, 0x03, 0x5F}, + {0x00, 0x03, 0x62}, + {0x00, 0x03, 0x64}, + {0x00, 0x03, 0x66}, + {0x00, 0x03, 0x67}, + {0x00, 0x03, 0x68}, + {0x00, 0x03, 0x6C}, + {0x00, 0x03, 0x6D}, + {0x00, 0x03, 0x6E}, + {0x00, 0x03, 0x70}, + {0x00, 0x03, 0x72}, + {0x00, 0x03, 0x75}, + {0x26, 0x00, 0x00}, + {0x27, 0x00, 0x00}, }, /* 24 */ { - {0x01, 2, 61}, - {0x16, 3, 61}, - {0x01, 2, 65}, - {0x16, 3, 65}, - {0x01, 2, 95}, - {0x16, 3, 95}, - {0x01, 2, 98}, - {0x16, 3, 98}, - {0x01, 2, 100}, - {0x16, 3, 100}, - {0x01, 2, 102}, - {0x16, 3, 102}, - {0x01, 2, 103}, - {0x16, 3, 103}, - {0x01, 2, 104}, - {0x16, 3, 104}, + {0x01, 0x02, 0x3D}, + {0x16, 0x03, 0x3D}, + {0x01, 0x02, 0x41}, + {0x16, 0x03, 0x41}, + {0x01, 0x02, 0x5F}, + {0x16, 0x03, 0x5F}, + {0x01, 0x02, 0x62}, + {0x16, 0x03, 0x62}, + {0x01, 0x02, 0x64}, + {0x16, 0x03, 0x64}, + {0x01, 0x02, 0x66}, + {0x16, 0x03, 0x66}, + {0x01, 0x02, 0x67}, + {0x16, 0x03, 0x67}, + {0x01, 0x02, 0x68}, + {0x16, 0x03, 0x68}, }, /* 25 */ { - {0x02, 2, 61}, - {0x09, 2, 61}, - {0x17, 2, 61}, - {0x28, 3, 61}, - {0x02, 2, 65}, - {0x09, 2, 65}, - {0x17, 2, 65}, - {0x28, 3, 65}, - {0x02, 2, 95}, - {0x09, 2, 95}, - {0x17, 2, 95}, - {0x28, 3, 95}, - {0x02, 2, 98}, - {0x09, 2, 98}, - {0x17, 2, 98}, - {0x28, 3, 98}, + {0x02, 0x02, 0x3D}, + {0x09, 0x02, 0x3D}, + {0x17, 0x02, 0x3D}, + {0x28, 0x03, 0x3D}, + {0x02, 0x02, 0x41}, + {0x09, 0x02, 0x41}, + {0x17, 0x02, 0x41}, + {0x28, 0x03, 0x41}, + {0x02, 0x02, 0x5F}, + {0x09, 0x02, 0x5F}, + {0x17, 0x02, 0x5F}, + {0x28, 0x03, 0x5F}, + {0x02, 0x02, 0x62}, + {0x09, 0x02, 0x62}, + {0x17, 0x02, 0x62}, + {0x28, 0x03, 0x62}, }, /* 26 */ { - {0x03, 2, 61}, - {0x06, 2, 61}, - {0x0a, 2, 61}, - {0x0f, 2, 61}, - {0x18, 2, 61}, - {0x1f, 2, 61}, - {0x29, 2, 61}, - {0x38, 3, 61}, - {0x03, 2, 65}, - {0x06, 2, 65}, - {0x0a, 2, 65}, - {0x0f, 2, 65}, - {0x18, 2, 65}, - {0x1f, 2, 65}, - {0x29, 2, 65}, - {0x38, 3, 65}, + {0x03, 0x02, 0x3D}, + {0x06, 0x02, 0x3D}, + {0x0A, 0x02, 0x3D}, + {0x0F, 0x02, 0x3D}, + {0x18, 0x02, 0x3D}, + {0x1F, 0x02, 0x3D}, + {0x29, 0x02, 0x3D}, + {0x38, 0x03, 0x3D}, + {0x03, 0x02, 0x41}, + {0x06, 0x02, 0x41}, + {0x0A, 0x02, 0x41}, + {0x0F, 0x02, 0x41}, + {0x18, 0x02, 0x41}, + {0x1F, 0x02, 0x41}, + {0x29, 0x02, 0x41}, + {0x38, 0x03, 0x41}, }, /* 27 */ { - {0x03, 2, 95}, - {0x06, 2, 95}, - {0x0a, 2, 95}, - {0x0f, 2, 95}, - {0x18, 2, 95}, - {0x1f, 2, 95}, - {0x29, 2, 95}, - {0x38, 3, 95}, - {0x03, 2, 98}, - {0x06, 2, 98}, - {0x0a, 2, 98}, - {0x0f, 2, 98}, - {0x18, 2, 98}, - {0x1f, 2, 98}, - {0x29, 2, 98}, - {0x38, 3, 98}, + {0x03, 0x02, 0x5F}, + {0x06, 0x02, 0x5F}, + {0x0A, 0x02, 0x5F}, + {0x0F, 0x02, 0x5F}, + {0x18, 0x02, 0x5F}, + {0x1F, 0x02, 0x5F}, + {0x29, 0x02, 0x5F}, + {0x38, 0x03, 0x5F}, + {0x03, 0x02, 0x62}, + {0x06, 0x02, 0x62}, + {0x0A, 0x02, 0x62}, + {0x0F, 0x02, 0x62}, + {0x18, 0x02, 0x62}, + {0x1F, 0x02, 0x62}, + {0x29, 0x02, 0x62}, + {0x38, 0x03, 0x62}, }, /* 28 */ { - {0x02, 2, 100}, - {0x09, 2, 100}, - {0x17, 2, 100}, - {0x28, 3, 100}, - {0x02, 2, 102}, - {0x09, 2, 102}, - {0x17, 2, 102}, - {0x28, 3, 102}, - {0x02, 2, 103}, - {0x09, 2, 103}, - {0x17, 2, 103}, - {0x28, 3, 103}, - {0x02, 2, 104}, - {0x09, 2, 104}, - {0x17, 2, 104}, - {0x28, 3, 104}, + {0x02, 0x02, 0x64}, + {0x09, 0x02, 0x64}, + {0x17, 0x02, 0x64}, + {0x28, 0x03, 0x64}, + {0x02, 0x02, 0x66}, + {0x09, 0x02, 0x66}, + {0x17, 0x02, 0x66}, + {0x28, 0x03, 0x66}, + {0x02, 0x02, 0x67}, + {0x09, 0x02, 0x67}, + {0x17, 0x02, 0x67}, + {0x28, 0x03, 0x67}, + {0x02, 0x02, 0x68}, + {0x09, 0x02, 0x68}, + {0x17, 0x02, 0x68}, + {0x28, 0x03, 0x68}, }, /* 29 */ { - {0x03, 2, 100}, - {0x06, 2, 100}, - {0x0a, 2, 100}, - {0x0f, 2, 100}, - {0x18, 2, 100}, - {0x1f, 2, 100}, - {0x29, 2, 100}, - {0x38, 3, 100}, - {0x03, 2, 102}, - {0x06, 2, 102}, - {0x0a, 2, 102}, - {0x0f, 2, 102}, - {0x18, 2, 102}, - {0x1f, 2, 102}, - {0x29, 2, 102}, - {0x38, 3, 102}, + {0x03, 0x02, 0x64}, + {0x06, 0x02, 0x64}, + {0x0A, 0x02, 0x64}, + {0x0F, 0x02, 0x64}, + {0x18, 0x02, 0x64}, + {0x1F, 0x02, 0x64}, + {0x29, 0x02, 0x64}, + {0x38, 0x03, 0x64}, + {0x03, 0x02, 0x66}, + {0x06, 0x02, 0x66}, + {0x0A, 0x02, 0x66}, + {0x0F, 0x02, 0x66}, + {0x18, 0x02, 0x66}, + {0x1F, 0x02, 0x66}, + {0x29, 0x02, 0x66}, + {0x38, 0x03, 0x66}, }, /* 30 */ { - {0x03, 2, 103}, - {0x06, 2, 103}, - {0x0a, 2, 103}, - {0x0f, 2, 103}, - {0x18, 2, 103}, - {0x1f, 2, 103}, - {0x29, 2, 103}, - {0x38, 3, 103}, - {0x03, 2, 104}, - {0x06, 2, 104}, - {0x0a, 2, 104}, - {0x0f, 2, 104}, - {0x18, 2, 104}, - {0x1f, 2, 104}, - {0x29, 2, 104}, - {0x38, 3, 104}, + {0x03, 0x02, 0x67}, + {0x06, 0x02, 0x67}, + {0x0A, 0x02, 0x67}, + {0x0F, 0x02, 0x67}, + {0x18, 0x02, 0x67}, + {0x1F, 0x02, 0x67}, + {0x29, 0x02, 0x67}, + {0x38, 0x03, 0x67}, + {0x03, 0x02, 0x68}, + {0x06, 0x02, 0x68}, + {0x0A, 0x02, 0x68}, + {0x0F, 0x02, 0x68}, + {0x18, 0x02, 0x68}, + {0x1F, 0x02, 0x68}, + {0x29, 0x02, 0x68}, + {0x38, 0x03, 0x68}, }, /* 31 */ { - {0x01, 2, 108}, - {0x16, 3, 108}, - {0x01, 2, 109}, - {0x16, 3, 109}, - {0x01, 2, 110}, - {0x16, 3, 110}, - {0x01, 2, 112}, - {0x16, 3, 112}, - {0x01, 2, 114}, - {0x16, 3, 114}, - {0x01, 2, 117}, - {0x16, 3, 117}, - {0x00, 3, 58}, - {0x00, 3, 66}, - {0x00, 3, 67}, - {0x00, 3, 68}, + {0x01, 0x02, 0x6C}, + {0x16, 0x03, 0x6C}, + {0x01, 0x02, 0x6D}, + {0x16, 0x03, 0x6D}, + {0x01, 0x02, 0x6E}, + {0x16, 0x03, 0x6E}, + {0x01, 0x02, 0x70}, + {0x16, 0x03, 0x70}, + {0x01, 0x02, 0x72}, + {0x16, 0x03, 0x72}, + {0x01, 0x02, 0x75}, + {0x16, 0x03, 0x75}, + {0x00, 0x03, 0x3A}, + {0x00, 0x03, 0x42}, + {0x00, 0x03, 0x43}, + {0x00, 0x03, 0x44}, }, /* 32 */ { - {0x02, 2, 108}, - {0x09, 2, 108}, - {0x17, 2, 108}, - {0x28, 3, 108}, - {0x02, 2, 109}, - {0x09, 2, 109}, - {0x17, 2, 109}, - {0x28, 3, 109}, - {0x02, 2, 110}, - {0x09, 2, 110}, - {0x17, 2, 110}, - {0x28, 3, 110}, - {0x02, 2, 112}, - {0x09, 2, 112}, - {0x17, 2, 112}, - {0x28, 3, 112}, + {0x02, 0x02, 0x6C}, + {0x09, 0x02, 0x6C}, + {0x17, 0x02, 0x6C}, + {0x28, 0x03, 0x6C}, + {0x02, 0x02, 0x6D}, + {0x09, 0x02, 0x6D}, + {0x17, 0x02, 0x6D}, + {0x28, 0x03, 0x6D}, + {0x02, 0x02, 0x6E}, + {0x09, 0x02, 0x6E}, + {0x17, 0x02, 0x6E}, + {0x28, 0x03, 0x6E}, + {0x02, 0x02, 0x70}, + {0x09, 0x02, 0x70}, + {0x17, 0x02, 0x70}, + {0x28, 0x03, 0x70}, }, /* 33 */ { - {0x03, 2, 108}, - {0x06, 2, 108}, - {0x0a, 2, 108}, - {0x0f, 2, 108}, - {0x18, 2, 108}, - {0x1f, 2, 108}, - {0x29, 2, 108}, - {0x38, 3, 108}, - {0x03, 2, 109}, - {0x06, 2, 109}, - {0x0a, 2, 109}, - {0x0f, 2, 109}, - {0x18, 2, 109}, - {0x1f, 2, 109}, - {0x29, 2, 109}, - {0x38, 3, 109}, + {0x03, 0x02, 0x6C}, + {0x06, 0x02, 0x6C}, + {0x0A, 0x02, 0x6C}, + {0x0F, 0x02, 0x6C}, + {0x18, 0x02, 0x6C}, + {0x1F, 0x02, 0x6C}, + {0x29, 0x02, 0x6C}, + {0x38, 0x03, 0x6C}, + {0x03, 0x02, 0x6D}, + {0x06, 0x02, 0x6D}, + {0x0A, 0x02, 0x6D}, + {0x0F, 0x02, 0x6D}, + {0x18, 0x02, 0x6D}, + {0x1F, 0x02, 0x6D}, + {0x29, 0x02, 0x6D}, + {0x38, 0x03, 0x6D}, }, /* 34 */ { - {0x03, 2, 110}, - {0x06, 2, 110}, - {0x0a, 2, 110}, - {0x0f, 2, 110}, - {0x18, 2, 110}, - {0x1f, 2, 110}, - {0x29, 2, 110}, - {0x38, 3, 110}, - {0x03, 2, 112}, - {0x06, 2, 112}, - {0x0a, 2, 112}, - {0x0f, 2, 112}, - {0x18, 2, 112}, - {0x1f, 2, 112}, - {0x29, 2, 112}, - {0x38, 3, 112}, + {0x03, 0x02, 0x6E}, + {0x06, 0x02, 0x6E}, + {0x0A, 0x02, 0x6E}, + {0x0F, 0x02, 0x6E}, + {0x18, 0x02, 0x6E}, + {0x1F, 0x02, 0x6E}, + {0x29, 0x02, 0x6E}, + {0x38, 0x03, 0x6E}, + {0x03, 0x02, 0x70}, + {0x06, 0x02, 0x70}, + {0x0A, 0x02, 0x70}, + {0x0F, 0x02, 0x70}, + {0x18, 0x02, 0x70}, + {0x1F, 0x02, 0x70}, + {0x29, 0x02, 0x70}, + {0x38, 0x03, 0x70}, }, /* 35 */ { - {0x02, 2, 114}, - {0x09, 2, 114}, - {0x17, 2, 114}, - {0x28, 3, 114}, - {0x02, 2, 117}, - {0x09, 2, 117}, - {0x17, 2, 117}, - {0x28, 3, 117}, - {0x01, 2, 58}, - {0x16, 3, 58}, - {0x01, 2, 66}, - {0x16, 3, 66}, - {0x01, 2, 67}, - {0x16, 3, 67}, - {0x01, 2, 68}, - {0x16, 3, 68}, + {0x02, 0x02, 0x72}, + {0x09, 0x02, 0x72}, + {0x17, 0x02, 0x72}, + {0x28, 0x03, 0x72}, + {0x02, 0x02, 0x75}, + {0x09, 0x02, 0x75}, + {0x17, 0x02, 0x75}, + {0x28, 0x03, 0x75}, + {0x01, 0x02, 0x3A}, + {0x16, 0x03, 0x3A}, + {0x01, 0x02, 0x42}, + {0x16, 0x03, 0x42}, + {0x01, 0x02, 0x43}, + {0x16, 0x03, 0x43}, + {0x01, 0x02, 0x44}, + {0x16, 0x03, 0x44}, }, /* 36 */ { - {0x03, 2, 114}, - {0x06, 2, 114}, - {0x0a, 2, 114}, - {0x0f, 2, 114}, - {0x18, 2, 114}, - {0x1f, 2, 114}, - {0x29, 2, 114}, - {0x38, 3, 114}, - {0x03, 2, 117}, - {0x06, 2, 117}, - {0x0a, 2, 117}, - {0x0f, 2, 117}, - {0x18, 2, 117}, - {0x1f, 2, 117}, - {0x29, 2, 117}, - {0x38, 3, 117}, + {0x03, 0x02, 0x72}, + {0x06, 0x02, 0x72}, + {0x0A, 0x02, 0x72}, + {0x0F, 0x02, 0x72}, + {0x18, 0x02, 0x72}, + {0x1F, 0x02, 0x72}, + {0x29, 0x02, 0x72}, + {0x38, 0x03, 0x72}, + {0x03, 0x02, 0x75}, + {0x06, 0x02, 0x75}, + {0x0A, 0x02, 0x75}, + {0x0F, 0x02, 0x75}, + {0x18, 0x02, 0x75}, + {0x1F, 0x02, 0x75}, + {0x29, 0x02, 0x75}, + {0x38, 0x03, 0x75}, }, /* 37 */ { - {0x02, 2, 58}, - {0x09, 2, 58}, - {0x17, 2, 58}, - {0x28, 3, 58}, - {0x02, 2, 66}, - {0x09, 2, 66}, - {0x17, 2, 66}, - {0x28, 3, 66}, - {0x02, 2, 67}, - {0x09, 2, 67}, - {0x17, 2, 67}, - {0x28, 3, 67}, - {0x02, 2, 68}, - {0x09, 2, 68}, - {0x17, 2, 68}, - {0x28, 3, 68}, + {0x02, 0x02, 0x3A}, + {0x09, 0x02, 0x3A}, + {0x17, 0x02, 0x3A}, + {0x28, 0x03, 0x3A}, + {0x02, 0x02, 0x42}, + {0x09, 0x02, 0x42}, + {0x17, 0x02, 0x42}, + {0x28, 0x03, 0x42}, + {0x02, 0x02, 0x43}, + {0x09, 0x02, 0x43}, + {0x17, 0x02, 0x43}, + {0x28, 0x03, 0x43}, + {0x02, 0x02, 0x44}, + {0x09, 0x02, 0x44}, + {0x17, 0x02, 0x44}, + {0x28, 0x03, 0x44}, }, /* 38 */ { - {0x03, 2, 58}, - {0x06, 2, 58}, - {0x0a, 2, 58}, - {0x0f, 2, 58}, - {0x18, 2, 58}, - {0x1f, 2, 58}, - {0x29, 2, 58}, - {0x38, 3, 58}, - {0x03, 2, 66}, - {0x06, 2, 66}, - {0x0a, 2, 66}, - {0x0f, 2, 66}, - {0x18, 2, 66}, - {0x1f, 2, 66}, - {0x29, 2, 66}, - {0x38, 3, 66}, + {0x03, 0x02, 0x3A}, + {0x06, 0x02, 0x3A}, + {0x0A, 0x02, 0x3A}, + {0x0F, 0x02, 0x3A}, + {0x18, 0x02, 0x3A}, + {0x1F, 0x02, 0x3A}, + {0x29, 0x02, 0x3A}, + {0x38, 0x03, 0x3A}, + {0x03, 0x02, 0x42}, + {0x06, 0x02, 0x42}, + {0x0A, 0x02, 0x42}, + {0x0F, 0x02, 0x42}, + {0x18, 0x02, 0x42}, + {0x1F, 0x02, 0x42}, + {0x29, 0x02, 0x42}, + {0x38, 0x03, 0x42}, }, /* 39 */ { - {0x03, 2, 67}, - {0x06, 2, 67}, - {0x0a, 2, 67}, - {0x0f, 2, 67}, - {0x18, 2, 67}, - {0x1f, 2, 67}, - {0x29, 2, 67}, - {0x38, 3, 67}, - {0x03, 2, 68}, - {0x06, 2, 68}, - {0x0a, 2, 68}, - {0x0f, 2, 68}, - {0x18, 2, 68}, - {0x1f, 2, 68}, - {0x29, 2, 68}, - {0x38, 3, 68}, + {0x03, 0x02, 0x43}, + {0x06, 0x02, 0x43}, + {0x0A, 0x02, 0x43}, + {0x0F, 0x02, 0x43}, + {0x18, 0x02, 0x43}, + {0x1F, 0x02, 0x43}, + {0x29, 0x02, 0x43}, + {0x38, 0x03, 0x43}, + {0x03, 0x02, 0x44}, + {0x06, 0x02, 0x44}, + {0x0A, 0x02, 0x44}, + {0x0F, 0x02, 0x44}, + {0x18, 0x02, 0x44}, + {0x1F, 0x02, 0x44}, + {0x29, 0x02, 0x44}, + {0x38, 0x03, 0x44}, }, /* 40 */ { - {0x2c, 0, 0}, - {0x2d, 0, 0}, - {0x2f, 0, 0}, - {0x30, 0, 0}, - {0x33, 0, 0}, - {0x34, 0, 0}, - {0x36, 0, 0}, - {0x37, 0, 0}, - {0x3b, 0, 0}, - {0x3c, 0, 0}, - {0x3e, 0, 0}, - {0x3f, 0, 0}, - {0x42, 0, 0}, - {0x43, 0, 0}, - {0x45, 0, 0}, - {0x48, 1, 0}, + {0x2C, 0x00, 0x00}, + {0x2D, 0x00, 0x00}, + {0x2F, 0x00, 0x00}, + {0x30, 0x00, 0x00}, + {0x33, 0x00, 0x00}, + {0x34, 0x00, 0x00}, + {0x36, 0x00, 0x00}, + {0x37, 0x00, 0x00}, + {0x3B, 0x00, 0x00}, + {0x3C, 0x00, 0x00}, + {0x3E, 0x00, 0x00}, + {0x3F, 0x00, 0x00}, + {0x42, 0x00, 0x00}, + {0x43, 0x00, 0x00}, + {0x45, 0x00, 0x00}, + {0x48, 0x01, 0x00}, }, /* 41 */ { - {0x00, 3, 69}, - {0x00, 3, 70}, - {0x00, 3, 71}, - {0x00, 3, 72}, - {0x00, 3, 73}, - {0x00, 3, 74}, - {0x00, 3, 75}, - {0x00, 3, 76}, - {0x00, 3, 77}, - {0x00, 3, 78}, - {0x00, 3, 79}, - {0x00, 3, 80}, - {0x00, 3, 81}, - {0x00, 3, 82}, - {0x00, 3, 83}, - {0x00, 3, 84}, + {0x00, 0x03, 0x45}, + {0x00, 0x03, 0x46}, + {0x00, 0x03, 0x47}, + {0x00, 0x03, 0x48}, + {0x00, 0x03, 0x49}, + {0x00, 0x03, 0x4A}, + {0x00, 0x03, 0x4B}, + {0x00, 0x03, 0x4C}, + {0x00, 0x03, 0x4D}, + {0x00, 0x03, 0x4E}, + {0x00, 0x03, 0x4F}, + {0x00, 0x03, 0x50}, + {0x00, 0x03, 0x51}, + {0x00, 0x03, 0x52}, + {0x00, 0x03, 0x53}, + {0x00, 0x03, 0x54}, }, /* 42 */ { - {0x01, 2, 69}, - {0x16, 3, 69}, - {0x01, 2, 70}, - {0x16, 3, 70}, - {0x01, 2, 71}, - {0x16, 3, 71}, - {0x01, 2, 72}, - {0x16, 3, 72}, - {0x01, 2, 73}, - {0x16, 3, 73}, - {0x01, 2, 74}, - {0x16, 3, 74}, - {0x01, 2, 75}, - {0x16, 3, 75}, - {0x01, 2, 76}, - {0x16, 3, 76}, + {0x01, 0x02, 0x45}, + {0x16, 0x03, 0x45}, + {0x01, 0x02, 0x46}, + {0x16, 0x03, 0x46}, + {0x01, 0x02, 0x47}, + {0x16, 0x03, 0x47}, + {0x01, 0x02, 0x48}, + {0x16, 0x03, 0x48}, + {0x01, 0x02, 0x49}, + {0x16, 0x03, 0x49}, + {0x01, 0x02, 0x4A}, + {0x16, 0x03, 0x4A}, + {0x01, 0x02, 0x4B}, + {0x16, 0x03, 0x4B}, + {0x01, 0x02, 0x4C}, + {0x16, 0x03, 0x4C}, }, /* 43 */ { - {0x02, 2, 69}, - {0x09, 2, 69}, - {0x17, 2, 69}, - {0x28, 3, 69}, - {0x02, 2, 70}, - {0x09, 2, 70}, - {0x17, 2, 70}, - {0x28, 3, 70}, - {0x02, 2, 71}, - {0x09, 2, 71}, - {0x17, 2, 71}, - {0x28, 3, 71}, - {0x02, 2, 72}, - {0x09, 2, 72}, - {0x17, 2, 72}, - {0x28, 3, 72}, + {0x02, 0x02, 0x45}, + {0x09, 0x02, 0x45}, + {0x17, 0x02, 0x45}, + {0x28, 0x03, 0x45}, + {0x02, 0x02, 0x46}, + {0x09, 0x02, 0x46}, + {0x17, 0x02, 0x46}, + {0x28, 0x03, 0x46}, + {0x02, 0x02, 0x47}, + {0x09, 0x02, 0x47}, + {0x17, 0x02, 0x47}, + {0x28, 0x03, 0x47}, + {0x02, 0x02, 0x48}, + {0x09, 0x02, 0x48}, + {0x17, 0x02, 0x48}, + {0x28, 0x03, 0x48}, }, /* 44 */ { - {0x03, 2, 69}, - {0x06, 2, 69}, - {0x0a, 2, 69}, - {0x0f, 2, 69}, - {0x18, 2, 69}, - {0x1f, 2, 69}, - {0x29, 2, 69}, - {0x38, 3, 69}, - {0x03, 2, 70}, - {0x06, 2, 70}, - {0x0a, 2, 70}, - {0x0f, 2, 70}, - {0x18, 2, 70}, - {0x1f, 2, 70}, - {0x29, 2, 70}, - {0x38, 3, 70}, + {0x03, 0x02, 0x45}, + {0x06, 0x02, 0x45}, + {0x0A, 0x02, 0x45}, + {0x0F, 0x02, 0x45}, + {0x18, 0x02, 0x45}, + {0x1F, 0x02, 0x45}, + {0x29, 0x02, 0x45}, + {0x38, 0x03, 0x45}, + {0x03, 0x02, 0x46}, + {0x06, 0x02, 0x46}, + {0x0A, 0x02, 0x46}, + {0x0F, 0x02, 0x46}, + {0x18, 0x02, 0x46}, + {0x1F, 0x02, 0x46}, + {0x29, 0x02, 0x46}, + {0x38, 0x03, 0x46}, }, /* 45 */ { - {0x03, 2, 71}, - {0x06, 2, 71}, - {0x0a, 2, 71}, - {0x0f, 2, 71}, - {0x18, 2, 71}, - {0x1f, 2, 71}, - {0x29, 2, 71}, - {0x38, 3, 71}, - {0x03, 2, 72}, - {0x06, 2, 72}, - {0x0a, 2, 72}, - {0x0f, 2, 72}, - {0x18, 2, 72}, - {0x1f, 2, 72}, - {0x29, 2, 72}, - {0x38, 3, 72}, + {0x03, 0x02, 0x47}, + {0x06, 0x02, 0x47}, + {0x0A, 0x02, 0x47}, + {0x0F, 0x02, 0x47}, + {0x18, 0x02, 0x47}, + {0x1F, 0x02, 0x47}, + {0x29, 0x02, 0x47}, + {0x38, 0x03, 0x47}, + {0x03, 0x02, 0x48}, + {0x06, 0x02, 0x48}, + {0x0A, 0x02, 0x48}, + {0x0F, 0x02, 0x48}, + {0x18, 0x02, 0x48}, + {0x1F, 0x02, 0x48}, + {0x29, 0x02, 0x48}, + {0x38, 0x03, 0x48}, }, /* 46 */ { - {0x02, 2, 73}, - {0x09, 2, 73}, - {0x17, 2, 73}, - {0x28, 3, 73}, - {0x02, 2, 74}, - {0x09, 2, 74}, - {0x17, 2, 74}, - {0x28, 3, 74}, - {0x02, 2, 75}, - {0x09, 2, 75}, - {0x17, 2, 75}, - {0x28, 3, 75}, - {0x02, 2, 76}, - {0x09, 2, 76}, - {0x17, 2, 76}, - {0x28, 3, 76}, + {0x02, 0x02, 0x49}, + {0x09, 0x02, 0x49}, + {0x17, 0x02, 0x49}, + {0x28, 0x03, 0x49}, + {0x02, 0x02, 0x4A}, + {0x09, 0x02, 0x4A}, + {0x17, 0x02, 0x4A}, + {0x28, 0x03, 0x4A}, + {0x02, 0x02, 0x4B}, + {0x09, 0x02, 0x4B}, + {0x17, 0x02, 0x4B}, + {0x28, 0x03, 0x4B}, + {0x02, 0x02, 0x4C}, + {0x09, 0x02, 0x4C}, + {0x17, 0x02, 0x4C}, + {0x28, 0x03, 0x4C}, }, /* 47 */ { - {0x03, 2, 73}, - {0x06, 2, 73}, - {0x0a, 2, 73}, - {0x0f, 2, 73}, - {0x18, 2, 73}, - {0x1f, 2, 73}, - {0x29, 2, 73}, - {0x38, 3, 73}, - {0x03, 2, 74}, - {0x06, 2, 74}, - {0x0a, 2, 74}, - {0x0f, 2, 74}, - {0x18, 2, 74}, - {0x1f, 2, 74}, - {0x29, 2, 74}, - {0x38, 3, 74}, + {0x03, 0x02, 0x49}, + {0x06, 0x02, 0x49}, + {0x0A, 0x02, 0x49}, + {0x0F, 0x02, 0x49}, + {0x18, 0x02, 0x49}, + {0x1F, 0x02, 0x49}, + {0x29, 0x02, 0x49}, + {0x38, 0x03, 0x49}, + {0x03, 0x02, 0x4A}, + {0x06, 0x02, 0x4A}, + {0x0A, 0x02, 0x4A}, + {0x0F, 0x02, 0x4A}, + {0x18, 0x02, 0x4A}, + {0x1F, 0x02, 0x4A}, + {0x29, 0x02, 0x4A}, + {0x38, 0x03, 0x4A}, }, /* 48 */ { - {0x03, 2, 75}, - {0x06, 2, 75}, - {0x0a, 2, 75}, - {0x0f, 2, 75}, - {0x18, 2, 75}, - {0x1f, 2, 75}, - {0x29, 2, 75}, - {0x38, 3, 75}, - {0x03, 2, 76}, - {0x06, 2, 76}, - {0x0a, 2, 76}, - {0x0f, 2, 76}, - {0x18, 2, 76}, - {0x1f, 2, 76}, - {0x29, 2, 76}, - {0x38, 3, 76}, + {0x03, 0x02, 0x4B}, + {0x06, 0x02, 0x4B}, + {0x0A, 0x02, 0x4B}, + {0x0F, 0x02, 0x4B}, + {0x18, 0x02, 0x4B}, + {0x1F, 0x02, 0x4B}, + {0x29, 0x02, 0x4B}, + {0x38, 0x03, 0x4B}, + {0x03, 0x02, 0x4C}, + {0x06, 0x02, 0x4C}, + {0x0A, 0x02, 0x4C}, + {0x0F, 0x02, 0x4C}, + {0x18, 0x02, 0x4C}, + {0x1F, 0x02, 0x4C}, + {0x29, 0x02, 0x4C}, + {0x38, 0x03, 0x4C}, }, /* 49 */ { - {0x01, 2, 77}, - {0x16, 3, 77}, - {0x01, 2, 78}, - {0x16, 3, 78}, - {0x01, 2, 79}, - {0x16, 3, 79}, - {0x01, 2, 80}, - {0x16, 3, 80}, - {0x01, 2, 81}, - {0x16, 3, 81}, - {0x01, 2, 82}, - {0x16, 3, 82}, - {0x01, 2, 83}, - {0x16, 3, 83}, - {0x01, 2, 84}, - {0x16, 3, 84}, + {0x01, 0x02, 0x4D}, + {0x16, 0x03, 0x4D}, + {0x01, 0x02, 0x4E}, + {0x16, 0x03, 0x4E}, + {0x01, 0x02, 0x4F}, + {0x16, 0x03, 0x4F}, + {0x01, 0x02, 0x50}, + {0x16, 0x03, 0x50}, + {0x01, 0x02, 0x51}, + {0x16, 0x03, 0x51}, + {0x01, 0x02, 0x52}, + {0x16, 0x03, 0x52}, + {0x01, 0x02, 0x53}, + {0x16, 0x03, 0x53}, + {0x01, 0x02, 0x54}, + {0x16, 0x03, 0x54}, }, /* 50 */ { - {0x02, 2, 77}, - {0x09, 2, 77}, - {0x17, 2, 77}, - {0x28, 3, 77}, - {0x02, 2, 78}, - {0x09, 2, 78}, - {0x17, 2, 78}, - {0x28, 3, 78}, - {0x02, 2, 79}, - {0x09, 2, 79}, - {0x17, 2, 79}, - {0x28, 3, 79}, - {0x02, 2, 80}, - {0x09, 2, 80}, - {0x17, 2, 80}, - {0x28, 3, 80}, + {0x02, 0x02, 0x4D}, + {0x09, 0x02, 0x4D}, + {0x17, 0x02, 0x4D}, + {0x28, 0x03, 0x4D}, + {0x02, 0x02, 0x4E}, + {0x09, 0x02, 0x4E}, + {0x17, 0x02, 0x4E}, + {0x28, 0x03, 0x4E}, + {0x02, 0x02, 0x4F}, + {0x09, 0x02, 0x4F}, + {0x17, 0x02, 0x4F}, + {0x28, 0x03, 0x4F}, + {0x02, 0x02, 0x50}, + {0x09, 0x02, 0x50}, + {0x17, 0x02, 0x50}, + {0x28, 0x03, 0x50}, }, /* 51 */ { - {0x03, 2, 77}, - {0x06, 2, 77}, - {0x0a, 2, 77}, - {0x0f, 2, 77}, - {0x18, 2, 77}, - {0x1f, 2, 77}, - {0x29, 2, 77}, - {0x38, 3, 77}, - {0x03, 2, 78}, - {0x06, 2, 78}, - {0x0a, 2, 78}, - {0x0f, 2, 78}, - {0x18, 2, 78}, - {0x1f, 2, 78}, - {0x29, 2, 78}, - {0x38, 3, 78}, + {0x03, 0x02, 0x4D}, + {0x06, 0x02, 0x4D}, + {0x0A, 0x02, 0x4D}, + {0x0F, 0x02, 0x4D}, + {0x18, 0x02, 0x4D}, + {0x1F, 0x02, 0x4D}, + {0x29, 0x02, 0x4D}, + {0x38, 0x03, 0x4D}, + {0x03, 0x02, 0x4E}, + {0x06, 0x02, 0x4E}, + {0x0A, 0x02, 0x4E}, + {0x0F, 0x02, 0x4E}, + {0x18, 0x02, 0x4E}, + {0x1F, 0x02, 0x4E}, + {0x29, 0x02, 0x4E}, + {0x38, 0x03, 0x4E}, }, /* 52 */ { - {0x03, 2, 79}, - {0x06, 2, 79}, - {0x0a, 2, 79}, - {0x0f, 2, 79}, - {0x18, 2, 79}, - {0x1f, 2, 79}, - {0x29, 2, 79}, - {0x38, 3, 79}, - {0x03, 2, 80}, - {0x06, 2, 80}, - {0x0a, 2, 80}, - {0x0f, 2, 80}, - {0x18, 2, 80}, - {0x1f, 2, 80}, - {0x29, 2, 80}, - {0x38, 3, 80}, + {0x03, 0x02, 0x4F}, + {0x06, 0x02, 0x4F}, + {0x0A, 0x02, 0x4F}, + {0x0F, 0x02, 0x4F}, + {0x18, 0x02, 0x4F}, + {0x1F, 0x02, 0x4F}, + {0x29, 0x02, 0x4F}, + {0x38, 0x03, 0x4F}, + {0x03, 0x02, 0x50}, + {0x06, 0x02, 0x50}, + {0x0A, 0x02, 0x50}, + {0x0F, 0x02, 0x50}, + {0x18, 0x02, 0x50}, + {0x1F, 0x02, 0x50}, + {0x29, 0x02, 0x50}, + {0x38, 0x03, 0x50}, }, /* 53 */ { - {0x02, 2, 81}, - {0x09, 2, 81}, - {0x17, 2, 81}, - {0x28, 3, 81}, - {0x02, 2, 82}, - {0x09, 2, 82}, - {0x17, 2, 82}, - {0x28, 3, 82}, - {0x02, 2, 83}, - {0x09, 2, 83}, - {0x17, 2, 83}, - {0x28, 3, 83}, - {0x02, 2, 84}, - {0x09, 2, 84}, - {0x17, 2, 84}, - {0x28, 3, 84}, + {0x02, 0x02, 0x51}, + {0x09, 0x02, 0x51}, + {0x17, 0x02, 0x51}, + {0x28, 0x03, 0x51}, + {0x02, 0x02, 0x52}, + {0x09, 0x02, 0x52}, + {0x17, 0x02, 0x52}, + {0x28, 0x03, 0x52}, + {0x02, 0x02, 0x53}, + {0x09, 0x02, 0x53}, + {0x17, 0x02, 0x53}, + {0x28, 0x03, 0x53}, + {0x02, 0x02, 0x54}, + {0x09, 0x02, 0x54}, + {0x17, 0x02, 0x54}, + {0x28, 0x03, 0x54}, }, /* 54 */ { - {0x03, 2, 81}, - {0x06, 2, 81}, - {0x0a, 2, 81}, - {0x0f, 2, 81}, - {0x18, 2, 81}, - {0x1f, 2, 81}, - {0x29, 2, 81}, - {0x38, 3, 81}, - {0x03, 2, 82}, - {0x06, 2, 82}, - {0x0a, 2, 82}, - {0x0f, 2, 82}, - {0x18, 2, 82}, - {0x1f, 2, 82}, - {0x29, 2, 82}, - {0x38, 3, 82}, + {0x03, 0x02, 0x51}, + {0x06, 0x02, 0x51}, + {0x0A, 0x02, 0x51}, + {0x0F, 0x02, 0x51}, + {0x18, 0x02, 0x51}, + {0x1F, 0x02, 0x51}, + {0x29, 0x02, 0x51}, + {0x38, 0x03, 0x51}, + {0x03, 0x02, 0x52}, + {0x06, 0x02, 0x52}, + {0x0A, 0x02, 0x52}, + {0x0F, 0x02, 0x52}, + {0x18, 0x02, 0x52}, + {0x1F, 0x02, 0x52}, + {0x29, 0x02, 0x52}, + {0x38, 0x03, 0x52}, }, /* 55 */ { - {0x03, 2, 83}, - {0x06, 2, 83}, - {0x0a, 2, 83}, - {0x0f, 2, 83}, - {0x18, 2, 83}, - {0x1f, 2, 83}, - {0x29, 2, 83}, - {0x38, 3, 83}, - {0x03, 2, 84}, - {0x06, 2, 84}, - {0x0a, 2, 84}, - {0x0f, 2, 84}, - {0x18, 2, 84}, - {0x1f, 2, 84}, - {0x29, 2, 84}, - {0x38, 3, 84}, + {0x03, 0x02, 0x53}, + {0x06, 0x02, 0x53}, + {0x0A, 0x02, 0x53}, + {0x0F, 0x02, 0x53}, + {0x18, 0x02, 0x53}, + {0x1F, 0x02, 0x53}, + {0x29, 0x02, 0x53}, + {0x38, 0x03, 0x53}, + {0x03, 0x02, 0x54}, + {0x06, 0x02, 0x54}, + {0x0A, 0x02, 0x54}, + {0x0F, 0x02, 0x54}, + {0x18, 0x02, 0x54}, + {0x1F, 0x02, 0x54}, + {0x29, 0x02, 0x54}, + {0x38, 0x03, 0x54}, }, /* 56 */ { - {0x00, 3, 85}, - {0x00, 3, 86}, - {0x00, 3, 87}, - {0x00, 3, 89}, - {0x00, 3, 106}, - {0x00, 3, 107}, - {0x00, 3, 113}, - {0x00, 3, 118}, - {0x00, 3, 119}, - {0x00, 3, 120}, - {0x00, 3, 121}, - {0x00, 3, 122}, - {0x46, 0, 0}, - {0x47, 0, 0}, - {0x49, 0, 0}, - {0x4a, 1, 0}, + {0x00, 0x03, 0x55}, + {0x00, 0x03, 0x56}, + {0x00, 0x03, 0x57}, + {0x00, 0x03, 0x59}, + {0x00, 0x03, 0x6A}, + {0x00, 0x03, 0x6B}, + {0x00, 0x03, 0x71}, + {0x00, 0x03, 0x76}, + {0x00, 0x03, 0x77}, + {0x00, 0x03, 0x78}, + {0x00, 0x03, 0x79}, + {0x00, 0x03, 0x7A}, + {0x46, 0x00, 0x00}, + {0x47, 0x00, 0x00}, + {0x49, 0x00, 0x00}, + {0x4A, 0x01, 0x00}, }, /* 57 */ { - {0x01, 2, 85}, - {0x16, 3, 85}, - {0x01, 2, 86}, - {0x16, 3, 86}, - {0x01, 2, 87}, - {0x16, 3, 87}, - {0x01, 2, 89}, - {0x16, 3, 89}, - {0x01, 2, 106}, - {0x16, 3, 106}, - {0x01, 2, 107}, - {0x16, 3, 107}, - {0x01, 2, 113}, - {0x16, 3, 113}, - {0x01, 2, 118}, - {0x16, 3, 118}, + {0x01, 0x02, 0x55}, + {0x16, 0x03, 0x55}, + {0x01, 0x02, 0x56}, + {0x16, 0x03, 0x56}, + {0x01, 0x02, 0x57}, + {0x16, 0x03, 0x57}, + {0x01, 0x02, 0x59}, + {0x16, 0x03, 0x59}, + {0x01, 0x02, 0x6A}, + {0x16, 0x03, 0x6A}, + {0x01, 0x02, 0x6B}, + {0x16, 0x03, 0x6B}, + {0x01, 0x02, 0x71}, + {0x16, 0x03, 0x71}, + {0x01, 0x02, 0x76}, + {0x16, 0x03, 0x76}, }, /* 58 */ { - {0x02, 2, 85}, - {0x09, 2, 85}, - {0x17, 2, 85}, - {0x28, 3, 85}, - {0x02, 2, 86}, - {0x09, 2, 86}, - {0x17, 2, 86}, - {0x28, 3, 86}, - {0x02, 2, 87}, - {0x09, 2, 87}, - {0x17, 2, 87}, - {0x28, 3, 87}, - {0x02, 2, 89}, - {0x09, 2, 89}, - {0x17, 2, 89}, - {0x28, 3, 89}, + {0x02, 0x02, 0x55}, + {0x09, 0x02, 0x55}, + {0x17, 0x02, 0x55}, + {0x28, 0x03, 0x55}, + {0x02, 0x02, 0x56}, + {0x09, 0x02, 0x56}, + {0x17, 0x02, 0x56}, + {0x28, 0x03, 0x56}, + {0x02, 0x02, 0x57}, + {0x09, 0x02, 0x57}, + {0x17, 0x02, 0x57}, + {0x28, 0x03, 0x57}, + {0x02, 0x02, 0x59}, + {0x09, 0x02, 0x59}, + {0x17, 0x02, 0x59}, + {0x28, 0x03, 0x59}, }, /* 59 */ { - {0x03, 2, 85}, - {0x06, 2, 85}, - {0x0a, 2, 85}, - {0x0f, 2, 85}, - {0x18, 2, 85}, - {0x1f, 2, 85}, - {0x29, 2, 85}, - {0x38, 3, 85}, - {0x03, 2, 86}, - {0x06, 2, 86}, - {0x0a, 2, 86}, - {0x0f, 2, 86}, - {0x18, 2, 86}, - {0x1f, 2, 86}, - {0x29, 2, 86}, - {0x38, 3, 86}, + {0x03, 0x02, 0x55}, + {0x06, 0x02, 0x55}, + {0x0A, 0x02, 0x55}, + {0x0F, 0x02, 0x55}, + {0x18, 0x02, 0x55}, + {0x1F, 0x02, 0x55}, + {0x29, 0x02, 0x55}, + {0x38, 0x03, 0x55}, + {0x03, 0x02, 0x56}, + {0x06, 0x02, 0x56}, + {0x0A, 0x02, 0x56}, + {0x0F, 0x02, 0x56}, + {0x18, 0x02, 0x56}, + {0x1F, 0x02, 0x56}, + {0x29, 0x02, 0x56}, + {0x38, 0x03, 0x56}, }, /* 60 */ { - {0x03, 2, 87}, - {0x06, 2, 87}, - {0x0a, 2, 87}, - {0x0f, 2, 87}, - {0x18, 2, 87}, - {0x1f, 2, 87}, - {0x29, 2, 87}, - {0x38, 3, 87}, - {0x03, 2, 89}, - {0x06, 2, 89}, - {0x0a, 2, 89}, - {0x0f, 2, 89}, - {0x18, 2, 89}, - {0x1f, 2, 89}, - {0x29, 2, 89}, - {0x38, 3, 89}, + {0x03, 0x02, 0x57}, + {0x06, 0x02, 0x57}, + {0x0A, 0x02, 0x57}, + {0x0F, 0x02, 0x57}, + {0x18, 0x02, 0x57}, + {0x1F, 0x02, 0x57}, + {0x29, 0x02, 0x57}, + {0x38, 0x03, 0x57}, + {0x03, 0x02, 0x59}, + {0x06, 0x02, 0x59}, + {0x0A, 0x02, 0x59}, + {0x0F, 0x02, 0x59}, + {0x18, 0x02, 0x59}, + {0x1F, 0x02, 0x59}, + {0x29, 0x02, 0x59}, + {0x38, 0x03, 0x59}, }, /* 61 */ { - {0x02, 2, 106}, - {0x09, 2, 106}, - {0x17, 2, 106}, - {0x28, 3, 106}, - {0x02, 2, 107}, - {0x09, 2, 107}, - {0x17, 2, 107}, - {0x28, 3, 107}, - {0x02, 2, 113}, - {0x09, 2, 113}, - {0x17, 2, 113}, - {0x28, 3, 113}, - {0x02, 2, 118}, - {0x09, 2, 118}, - {0x17, 2, 118}, - {0x28, 3, 118}, + {0x02, 0x02, 0x6A}, + {0x09, 0x02, 0x6A}, + {0x17, 0x02, 0x6A}, + {0x28, 0x03, 0x6A}, + {0x02, 0x02, 0x6B}, + {0x09, 0x02, 0x6B}, + {0x17, 0x02, 0x6B}, + {0x28, 0x03, 0x6B}, + {0x02, 0x02, 0x71}, + {0x09, 0x02, 0x71}, + {0x17, 0x02, 0x71}, + {0x28, 0x03, 0x71}, + {0x02, 0x02, 0x76}, + {0x09, 0x02, 0x76}, + {0x17, 0x02, 0x76}, + {0x28, 0x03, 0x76}, }, /* 62 */ { - {0x03, 2, 106}, - {0x06, 2, 106}, - {0x0a, 2, 106}, - {0x0f, 2, 106}, - {0x18, 2, 106}, - {0x1f, 2, 106}, - {0x29, 2, 106}, - {0x38, 3, 106}, - {0x03, 2, 107}, - {0x06, 2, 107}, - {0x0a, 2, 107}, - {0x0f, 2, 107}, - {0x18, 2, 107}, - {0x1f, 2, 107}, - {0x29, 2, 107}, - {0x38, 3, 107}, + {0x03, 0x02, 0x6A}, + {0x06, 0x02, 0x6A}, + {0x0A, 0x02, 0x6A}, + {0x0F, 0x02, 0x6A}, + {0x18, 0x02, 0x6A}, + {0x1F, 0x02, 0x6A}, + {0x29, 0x02, 0x6A}, + {0x38, 0x03, 0x6A}, + {0x03, 0x02, 0x6B}, + {0x06, 0x02, 0x6B}, + {0x0A, 0x02, 0x6B}, + {0x0F, 0x02, 0x6B}, + {0x18, 0x02, 0x6B}, + {0x1F, 0x02, 0x6B}, + {0x29, 0x02, 0x6B}, + {0x38, 0x03, 0x6B}, }, /* 63 */ { - {0x03, 2, 113}, - {0x06, 2, 113}, - {0x0a, 2, 113}, - {0x0f, 2, 113}, - {0x18, 2, 113}, - {0x1f, 2, 113}, - {0x29, 2, 113}, - {0x38, 3, 113}, - {0x03, 2, 118}, - {0x06, 2, 118}, - {0x0a, 2, 118}, - {0x0f, 2, 118}, - {0x18, 2, 118}, - {0x1f, 2, 118}, - {0x29, 2, 118}, - {0x38, 3, 118}, + {0x03, 0x02, 0x71}, + {0x06, 0x02, 0x71}, + {0x0A, 0x02, 0x71}, + {0x0F, 0x02, 0x71}, + {0x18, 0x02, 0x71}, + {0x1F, 0x02, 0x71}, + {0x29, 0x02, 0x71}, + {0x38, 0x03, 0x71}, + {0x03, 0x02, 0x76}, + {0x06, 0x02, 0x76}, + {0x0A, 0x02, 0x76}, + {0x0F, 0x02, 0x76}, + {0x18, 0x02, 0x76}, + {0x1F, 0x02, 0x76}, + {0x29, 0x02, 0x76}, + {0x38, 0x03, 0x76}, }, /* 64 */ { - {0x01, 2, 119}, - {0x16, 3, 119}, - {0x01, 2, 120}, - {0x16, 3, 120}, - {0x01, 2, 121}, - {0x16, 3, 121}, - {0x01, 2, 122}, - {0x16, 3, 122}, - {0x00, 3, 38}, - {0x00, 3, 42}, - {0x00, 3, 44}, - {0x00, 3, 59}, - {0x00, 3, 88}, - {0x00, 3, 90}, - {0x4b, 0, 0}, - {0x4e, 0, 0}, + {0x01, 0x02, 0x77}, + {0x16, 0x03, 0x77}, + {0x01, 0x02, 0x78}, + {0x16, 0x03, 0x78}, + {0x01, 0x02, 0x79}, + {0x16, 0x03, 0x79}, + {0x01, 0x02, 0x7A}, + {0x16, 0x03, 0x7A}, + {0x00, 0x03, 0x26}, + {0x00, 0x03, 0x2A}, + {0x00, 0x03, 0x2C}, + {0x00, 0x03, 0x3B}, + {0x00, 0x03, 0x58}, + {0x00, 0x03, 0x5A}, + {0x4B, 0x00, 0x00}, + {0x4E, 0x00, 0x00}, }, /* 65 */ { - {0x02, 2, 119}, - {0x09, 2, 119}, - {0x17, 2, 119}, - {0x28, 3, 119}, - {0x02, 2, 120}, - {0x09, 2, 120}, - {0x17, 2, 120}, - {0x28, 3, 120}, - {0x02, 2, 121}, - {0x09, 2, 121}, - {0x17, 2, 121}, - {0x28, 3, 121}, - {0x02, 2, 122}, - {0x09, 2, 122}, - {0x17, 2, 122}, - {0x28, 3, 122}, + {0x02, 0x02, 0x77}, + {0x09, 0x02, 0x77}, + {0x17, 0x02, 0x77}, + {0x28, 0x03, 0x77}, + {0x02, 0x02, 0x78}, + {0x09, 0x02, 0x78}, + {0x17, 0x02, 0x78}, + {0x28, 0x03, 0x78}, + {0x02, 0x02, 0x79}, + {0x09, 0x02, 0x79}, + {0x17, 0x02, 0x79}, + {0x28, 0x03, 0x79}, + {0x02, 0x02, 0x7A}, + {0x09, 0x02, 0x7A}, + {0x17, 0x02, 0x7A}, + {0x28, 0x03, 0x7A}, }, /* 66 */ { - {0x03, 2, 119}, - {0x06, 2, 119}, - {0x0a, 2, 119}, - {0x0f, 2, 119}, - {0x18, 2, 119}, - {0x1f, 2, 119}, - {0x29, 2, 119}, - {0x38, 3, 119}, - {0x03, 2, 120}, - {0x06, 2, 120}, - {0x0a, 2, 120}, - {0x0f, 2, 120}, - {0x18, 2, 120}, - {0x1f, 2, 120}, - {0x29, 2, 120}, - {0x38, 3, 120}, + {0x03, 0x02, 0x77}, + {0x06, 0x02, 0x77}, + {0x0A, 0x02, 0x77}, + {0x0F, 0x02, 0x77}, + {0x18, 0x02, 0x77}, + {0x1F, 0x02, 0x77}, + {0x29, 0x02, 0x77}, + {0x38, 0x03, 0x77}, + {0x03, 0x02, 0x78}, + {0x06, 0x02, 0x78}, + {0x0A, 0x02, 0x78}, + {0x0F, 0x02, 0x78}, + {0x18, 0x02, 0x78}, + {0x1F, 0x02, 0x78}, + {0x29, 0x02, 0x78}, + {0x38, 0x03, 0x78}, }, /* 67 */ { - {0x03, 2, 121}, - {0x06, 2, 121}, - {0x0a, 2, 121}, - {0x0f, 2, 121}, - {0x18, 2, 121}, - {0x1f, 2, 121}, - {0x29, 2, 121}, - {0x38, 3, 121}, - {0x03, 2, 122}, - {0x06, 2, 122}, - {0x0a, 2, 122}, - {0x0f, 2, 122}, - {0x18, 2, 122}, - {0x1f, 2, 122}, - {0x29, 2, 122}, - {0x38, 3, 122}, + {0x03, 0x02, 0x79}, + {0x06, 0x02, 0x79}, + {0x0A, 0x02, 0x79}, + {0x0F, 0x02, 0x79}, + {0x18, 0x02, 0x79}, + {0x1F, 0x02, 0x79}, + {0x29, 0x02, 0x79}, + {0x38, 0x03, 0x79}, + {0x03, 0x02, 0x7A}, + {0x06, 0x02, 0x7A}, + {0x0A, 0x02, 0x7A}, + {0x0F, 0x02, 0x7A}, + {0x18, 0x02, 0x7A}, + {0x1F, 0x02, 0x7A}, + {0x29, 0x02, 0x7A}, + {0x38, 0x03, 0x7A}, }, /* 68 */ { - {0x01, 2, 38}, - {0x16, 3, 38}, - {0x01, 2, 42}, - {0x16, 3, 42}, - {0x01, 2, 44}, - {0x16, 3, 44}, - {0x01, 2, 59}, - {0x16, 3, 59}, - {0x01, 2, 88}, - {0x16, 3, 88}, - {0x01, 2, 90}, - {0x16, 3, 90}, - {0x4c, 0, 0}, - {0x4d, 0, 0}, - {0x4f, 0, 0}, - {0x51, 0, 0}, + {0x01, 0x02, 0x26}, + {0x16, 0x03, 0x26}, + {0x01, 0x02, 0x2A}, + {0x16, 0x03, 0x2A}, + {0x01, 0x02, 0x2C}, + {0x16, 0x03, 0x2C}, + {0x01, 0x02, 0x3B}, + {0x16, 0x03, 0x3B}, + {0x01, 0x02, 0x58}, + {0x16, 0x03, 0x58}, + {0x01, 0x02, 0x5A}, + {0x16, 0x03, 0x5A}, + {0x4C, 0x00, 0x00}, + {0x4D, 0x00, 0x00}, + {0x4F, 0x00, 0x00}, + {0x51, 0x00, 0x00}, }, /* 69 */ { - {0x02, 2, 38}, - {0x09, 2, 38}, - {0x17, 2, 38}, - {0x28, 3, 38}, - {0x02, 2, 42}, - {0x09, 2, 42}, - {0x17, 2, 42}, - {0x28, 3, 42}, - {0x02, 2, 44}, - {0x09, 2, 44}, - {0x17, 2, 44}, - {0x28, 3, 44}, - {0x02, 2, 59}, - {0x09, 2, 59}, - {0x17, 2, 59}, - {0x28, 3, 59}, + {0x02, 0x02, 0x26}, + {0x09, 0x02, 0x26}, + {0x17, 0x02, 0x26}, + {0x28, 0x03, 0x26}, + {0x02, 0x02, 0x2A}, + {0x09, 0x02, 0x2A}, + {0x17, 0x02, 0x2A}, + {0x28, 0x03, 0x2A}, + {0x02, 0x02, 0x2C}, + {0x09, 0x02, 0x2C}, + {0x17, 0x02, 0x2C}, + {0x28, 0x03, 0x2C}, + {0x02, 0x02, 0x3B}, + {0x09, 0x02, 0x3B}, + {0x17, 0x02, 0x3B}, + {0x28, 0x03, 0x3B}, }, /* 70 */ { - {0x03, 2, 38}, - {0x06, 2, 38}, - {0x0a, 2, 38}, - {0x0f, 2, 38}, - {0x18, 2, 38}, - {0x1f, 2, 38}, - {0x29, 2, 38}, - {0x38, 3, 38}, - {0x03, 2, 42}, - {0x06, 2, 42}, - {0x0a, 2, 42}, - {0x0f, 2, 42}, - {0x18, 2, 42}, - {0x1f, 2, 42}, - {0x29, 2, 42}, - {0x38, 3, 42}, + {0x03, 0x02, 0x26}, + {0x06, 0x02, 0x26}, + {0x0A, 0x02, 0x26}, + {0x0F, 0x02, 0x26}, + {0x18, 0x02, 0x26}, + {0x1F, 0x02, 0x26}, + {0x29, 0x02, 0x26}, + {0x38, 0x03, 0x26}, + {0x03, 0x02, 0x2A}, + {0x06, 0x02, 0x2A}, + {0x0A, 0x02, 0x2A}, + {0x0F, 0x02, 0x2A}, + {0x18, 0x02, 0x2A}, + {0x1F, 0x02, 0x2A}, + {0x29, 0x02, 0x2A}, + {0x38, 0x03, 0x2A}, }, /* 71 */ { - {0x03, 2, 44}, - {0x06, 2, 44}, - {0x0a, 2, 44}, - {0x0f, 2, 44}, - {0x18, 2, 44}, - {0x1f, 2, 44}, - {0x29, 2, 44}, - {0x38, 3, 44}, - {0x03, 2, 59}, - {0x06, 2, 59}, - {0x0a, 2, 59}, - {0x0f, 2, 59}, - {0x18, 2, 59}, - {0x1f, 2, 59}, - {0x29, 2, 59}, - {0x38, 3, 59}, + {0x03, 0x02, 0x2C}, + {0x06, 0x02, 0x2C}, + {0x0A, 0x02, 0x2C}, + {0x0F, 0x02, 0x2C}, + {0x18, 0x02, 0x2C}, + {0x1F, 0x02, 0x2C}, + {0x29, 0x02, 0x2C}, + {0x38, 0x03, 0x2C}, + {0x03, 0x02, 0x3B}, + {0x06, 0x02, 0x3B}, + {0x0A, 0x02, 0x3B}, + {0x0F, 0x02, 0x3B}, + {0x18, 0x02, 0x3B}, + {0x1F, 0x02, 0x3B}, + {0x29, 0x02, 0x3B}, + {0x38, 0x03, 0x3B}, }, /* 72 */ { - {0x02, 2, 88}, - {0x09, 2, 88}, - {0x17, 2, 88}, - {0x28, 3, 88}, - {0x02, 2, 90}, - {0x09, 2, 90}, - {0x17, 2, 90}, - {0x28, 3, 90}, - {0x00, 3, 33}, - {0x00, 3, 34}, - {0x00, 3, 40}, - {0x00, 3, 41}, - {0x00, 3, 63}, - {0x50, 0, 0}, - {0x52, 0, 0}, - {0x54, 0, 0}, + {0x02, 0x02, 0x58}, + {0x09, 0x02, 0x58}, + {0x17, 0x02, 0x58}, + {0x28, 0x03, 0x58}, + {0x02, 0x02, 0x5A}, + {0x09, 0x02, 0x5A}, + {0x17, 0x02, 0x5A}, + {0x28, 0x03, 0x5A}, + {0x00, 0x03, 0x21}, + {0x00, 0x03, 0x22}, + {0x00, 0x03, 0x28}, + {0x00, 0x03, 0x29}, + {0x00, 0x03, 0x3F}, + {0x50, 0x00, 0x00}, + {0x52, 0x00, 0x00}, + {0x54, 0x00, 0x00}, }, /* 73 */ { - {0x03, 2, 88}, - {0x06, 2, 88}, - {0x0a, 2, 88}, - {0x0f, 2, 88}, - {0x18, 2, 88}, - {0x1f, 2, 88}, - {0x29, 2, 88}, - {0x38, 3, 88}, - {0x03, 2, 90}, - {0x06, 2, 90}, - {0x0a, 2, 90}, - {0x0f, 2, 90}, - {0x18, 2, 90}, - {0x1f, 2, 90}, - {0x29, 2, 90}, - {0x38, 3, 90}, + {0x03, 0x02, 0x58}, + {0x06, 0x02, 0x58}, + {0x0A, 0x02, 0x58}, + {0x0F, 0x02, 0x58}, + {0x18, 0x02, 0x58}, + {0x1F, 0x02, 0x58}, + {0x29, 0x02, 0x58}, + {0x38, 0x03, 0x58}, + {0x03, 0x02, 0x5A}, + {0x06, 0x02, 0x5A}, + {0x0A, 0x02, 0x5A}, + {0x0F, 0x02, 0x5A}, + {0x18, 0x02, 0x5A}, + {0x1F, 0x02, 0x5A}, + {0x29, 0x02, 0x5A}, + {0x38, 0x03, 0x5A}, }, /* 74 */ { - {0x01, 2, 33}, - {0x16, 3, 33}, - {0x01, 2, 34}, - {0x16, 3, 34}, - {0x01, 2, 40}, - {0x16, 3, 40}, - {0x01, 2, 41}, - {0x16, 3, 41}, - {0x01, 2, 63}, - {0x16, 3, 63}, - {0x00, 3, 39}, - {0x00, 3, 43}, - {0x00, 3, 124}, - {0x53, 0, 0}, - {0x55, 0, 0}, - {0x58, 0, 0}, + {0x01, 0x02, 0x21}, + {0x16, 0x03, 0x21}, + {0x01, 0x02, 0x22}, + {0x16, 0x03, 0x22}, + {0x01, 0x02, 0x28}, + {0x16, 0x03, 0x28}, + {0x01, 0x02, 0x29}, + {0x16, 0x03, 0x29}, + {0x01, 0x02, 0x3F}, + {0x16, 0x03, 0x3F}, + {0x00, 0x03, 0x27}, + {0x00, 0x03, 0x2B}, + {0x00, 0x03, 0x7C}, + {0x53, 0x00, 0x00}, + {0x55, 0x00, 0x00}, + {0x58, 0x00, 0x00}, }, /* 75 */ { - {0x02, 2, 33}, - {0x09, 2, 33}, - {0x17, 2, 33}, - {0x28, 3, 33}, - {0x02, 2, 34}, - {0x09, 2, 34}, - {0x17, 2, 34}, - {0x28, 3, 34}, - {0x02, 2, 40}, - {0x09, 2, 40}, - {0x17, 2, 40}, - {0x28, 3, 40}, - {0x02, 2, 41}, - {0x09, 2, 41}, - {0x17, 2, 41}, - {0x28, 3, 41}, + {0x02, 0x02, 0x21}, + {0x09, 0x02, 0x21}, + {0x17, 0x02, 0x21}, + {0x28, 0x03, 0x21}, + {0x02, 0x02, 0x22}, + {0x09, 0x02, 0x22}, + {0x17, 0x02, 0x22}, + {0x28, 0x03, 0x22}, + {0x02, 0x02, 0x28}, + {0x09, 0x02, 0x28}, + {0x17, 0x02, 0x28}, + {0x28, 0x03, 0x28}, + {0x02, 0x02, 0x29}, + {0x09, 0x02, 0x29}, + {0x17, 0x02, 0x29}, + {0x28, 0x03, 0x29}, }, /* 76 */ { - {0x03, 2, 33}, - {0x06, 2, 33}, - {0x0a, 2, 33}, - {0x0f, 2, 33}, - {0x18, 2, 33}, - {0x1f, 2, 33}, - {0x29, 2, 33}, - {0x38, 3, 33}, - {0x03, 2, 34}, - {0x06, 2, 34}, - {0x0a, 2, 34}, - {0x0f, 2, 34}, - {0x18, 2, 34}, - {0x1f, 2, 34}, - {0x29, 2, 34}, - {0x38, 3, 34}, + {0x03, 0x02, 0x21}, + {0x06, 0x02, 0x21}, + {0x0A, 0x02, 0x21}, + {0x0F, 0x02, 0x21}, + {0x18, 0x02, 0x21}, + {0x1F, 0x02, 0x21}, + {0x29, 0x02, 0x21}, + {0x38, 0x03, 0x21}, + {0x03, 0x02, 0x22}, + {0x06, 0x02, 0x22}, + {0x0A, 0x02, 0x22}, + {0x0F, 0x02, 0x22}, + {0x18, 0x02, 0x22}, + {0x1F, 0x02, 0x22}, + {0x29, 0x02, 0x22}, + {0x38, 0x03, 0x22}, }, /* 77 */ { - {0x03, 2, 40}, - {0x06, 2, 40}, - {0x0a, 2, 40}, - {0x0f, 2, 40}, - {0x18, 2, 40}, - {0x1f, 2, 40}, - {0x29, 2, 40}, - {0x38, 3, 40}, - {0x03, 2, 41}, - {0x06, 2, 41}, - {0x0a, 2, 41}, - {0x0f, 2, 41}, - {0x18, 2, 41}, - {0x1f, 2, 41}, - {0x29, 2, 41}, - {0x38, 3, 41}, + {0x03, 0x02, 0x28}, + {0x06, 0x02, 0x28}, + {0x0A, 0x02, 0x28}, + {0x0F, 0x02, 0x28}, + {0x18, 0x02, 0x28}, + {0x1F, 0x02, 0x28}, + {0x29, 0x02, 0x28}, + {0x38, 0x03, 0x28}, + {0x03, 0x02, 0x29}, + {0x06, 0x02, 0x29}, + {0x0A, 0x02, 0x29}, + {0x0F, 0x02, 0x29}, + {0x18, 0x02, 0x29}, + {0x1F, 0x02, 0x29}, + {0x29, 0x02, 0x29}, + {0x38, 0x03, 0x29}, }, /* 78 */ { - {0x02, 2, 63}, - {0x09, 2, 63}, - {0x17, 2, 63}, - {0x28, 3, 63}, - {0x01, 2, 39}, - {0x16, 3, 39}, - {0x01, 2, 43}, - {0x16, 3, 43}, - {0x01, 2, 124}, - {0x16, 3, 124}, - {0x00, 3, 35}, - {0x00, 3, 62}, - {0x56, 0, 0}, - {0x57, 0, 0}, - {0x59, 0, 0}, - {0x5a, 0, 0}, + {0x02, 0x02, 0x3F}, + {0x09, 0x02, 0x3F}, + {0x17, 0x02, 0x3F}, + {0x28, 0x03, 0x3F}, + {0x01, 0x02, 0x27}, + {0x16, 0x03, 0x27}, + {0x01, 0x02, 0x2B}, + {0x16, 0x03, 0x2B}, + {0x01, 0x02, 0x7C}, + {0x16, 0x03, 0x7C}, + {0x00, 0x03, 0x23}, + {0x00, 0x03, 0x3E}, + {0x56, 0x00, 0x00}, + {0x57, 0x00, 0x00}, + {0x59, 0x00, 0x00}, + {0x5A, 0x00, 0x00}, }, /* 79 */ { - {0x03, 2, 63}, - {0x06, 2, 63}, - {0x0a, 2, 63}, - {0x0f, 2, 63}, - {0x18, 2, 63}, - {0x1f, 2, 63}, - {0x29, 2, 63}, - {0x38, 3, 63}, - {0x02, 2, 39}, - {0x09, 2, 39}, - {0x17, 2, 39}, - {0x28, 3, 39}, - {0x02, 2, 43}, - {0x09, 2, 43}, - {0x17, 2, 43}, - {0x28, 3, 43}, + {0x03, 0x02, 0x3F}, + {0x06, 0x02, 0x3F}, + {0x0A, 0x02, 0x3F}, + {0x0F, 0x02, 0x3F}, + {0x18, 0x02, 0x3F}, + {0x1F, 0x02, 0x3F}, + {0x29, 0x02, 0x3F}, + {0x38, 0x03, 0x3F}, + {0x02, 0x02, 0x27}, + {0x09, 0x02, 0x27}, + {0x17, 0x02, 0x27}, + {0x28, 0x03, 0x27}, + {0x02, 0x02, 0x2B}, + {0x09, 0x02, 0x2B}, + {0x17, 0x02, 0x2B}, + {0x28, 0x03, 0x2B}, }, /* 80 */ { - {0x03, 2, 39}, - {0x06, 2, 39}, - {0x0a, 2, 39}, - {0x0f, 2, 39}, - {0x18, 2, 39}, - {0x1f, 2, 39}, - {0x29, 2, 39}, - {0x38, 3, 39}, - {0x03, 2, 43}, - {0x06, 2, 43}, - {0x0a, 2, 43}, - {0x0f, 2, 43}, - {0x18, 2, 43}, - {0x1f, 2, 43}, - {0x29, 2, 43}, - {0x38, 3, 43}, + {0x03, 0x02, 0x27}, + {0x06, 0x02, 0x27}, + {0x0A, 0x02, 0x27}, + {0x0F, 0x02, 0x27}, + {0x18, 0x02, 0x27}, + {0x1F, 0x02, 0x27}, + {0x29, 0x02, 0x27}, + {0x38, 0x03, 0x27}, + {0x03, 0x02, 0x2B}, + {0x06, 0x02, 0x2B}, + {0x0A, 0x02, 0x2B}, + {0x0F, 0x02, 0x2B}, + {0x18, 0x02, 0x2B}, + {0x1F, 0x02, 0x2B}, + {0x29, 0x02, 0x2B}, + {0x38, 0x03, 0x2B}, }, /* 81 */ { - {0x02, 2, 124}, - {0x09, 2, 124}, - {0x17, 2, 124}, - {0x28, 3, 124}, - {0x01, 2, 35}, - {0x16, 3, 35}, - {0x01, 2, 62}, - {0x16, 3, 62}, - {0x00, 3, 0}, - {0x00, 3, 36}, - {0x00, 3, 64}, - {0x00, 3, 91}, - {0x00, 3, 93}, - {0x00, 3, 126}, - {0x5b, 0, 0}, - {0x5c, 0, 0}, + {0x02, 0x02, 0x7C}, + {0x09, 0x02, 0x7C}, + {0x17, 0x02, 0x7C}, + {0x28, 0x03, 0x7C}, + {0x01, 0x02, 0x23}, + {0x16, 0x03, 0x23}, + {0x01, 0x02, 0x3E}, + {0x16, 0x03, 0x3E}, + {0x00, 0x03, 0x00}, + {0x00, 0x03, 0x24}, + {0x00, 0x03, 0x40}, + {0x00, 0x03, 0x5B}, + {0x00, 0x03, 0x5D}, + {0x00, 0x03, 0x7E}, + {0x5B, 0x00, 0x00}, + {0x5C, 0x00, 0x00}, }, /* 82 */ { - {0x03, 2, 124}, - {0x06, 2, 124}, - {0x0a, 2, 124}, - {0x0f, 2, 124}, - {0x18, 2, 124}, - {0x1f, 2, 124}, - {0x29, 2, 124}, - {0x38, 3, 124}, - {0x02, 2, 35}, - {0x09, 2, 35}, - {0x17, 2, 35}, - {0x28, 3, 35}, - {0x02, 2, 62}, - {0x09, 2, 62}, - {0x17, 2, 62}, - {0x28, 3, 62}, + {0x03, 0x02, 0x7C}, + {0x06, 0x02, 0x7C}, + {0x0A, 0x02, 0x7C}, + {0x0F, 0x02, 0x7C}, + {0x18, 0x02, 0x7C}, + {0x1F, 0x02, 0x7C}, + {0x29, 0x02, 0x7C}, + {0x38, 0x03, 0x7C}, + {0x02, 0x02, 0x23}, + {0x09, 0x02, 0x23}, + {0x17, 0x02, 0x23}, + {0x28, 0x03, 0x23}, + {0x02, 0x02, 0x3E}, + {0x09, 0x02, 0x3E}, + {0x17, 0x02, 0x3E}, + {0x28, 0x03, 0x3E}, }, /* 83 */ { - {0x03, 2, 35}, - {0x06, 2, 35}, - {0x0a, 2, 35}, - {0x0f, 2, 35}, - {0x18, 2, 35}, - {0x1f, 2, 35}, - {0x29, 2, 35}, - {0x38, 3, 35}, - {0x03, 2, 62}, - {0x06, 2, 62}, - {0x0a, 2, 62}, - {0x0f, 2, 62}, - {0x18, 2, 62}, - {0x1f, 2, 62}, - {0x29, 2, 62}, - {0x38, 3, 62}, + {0x03, 0x02, 0x23}, + {0x06, 0x02, 0x23}, + {0x0A, 0x02, 0x23}, + {0x0F, 0x02, 0x23}, + {0x18, 0x02, 0x23}, + {0x1F, 0x02, 0x23}, + {0x29, 0x02, 0x23}, + {0x38, 0x03, 0x23}, + {0x03, 0x02, 0x3E}, + {0x06, 0x02, 0x3E}, + {0x0A, 0x02, 0x3E}, + {0x0F, 0x02, 0x3E}, + {0x18, 0x02, 0x3E}, + {0x1F, 0x02, 0x3E}, + {0x29, 0x02, 0x3E}, + {0x38, 0x03, 0x3E}, }, /* 84 */ { - {0x01, 2, 0}, - {0x16, 3, 0}, - {0x01, 2, 36}, - {0x16, 3, 36}, - {0x01, 2, 64}, - {0x16, 3, 64}, - {0x01, 2, 91}, - {0x16, 3, 91}, - {0x01, 2, 93}, - {0x16, 3, 93}, - {0x01, 2, 126}, - {0x16, 3, 126}, - {0x00, 3, 94}, - {0x00, 3, 125}, - {0x5d, 0, 0}, - {0x5e, 0, 0}, + {0x01, 0x02, 0x00}, + {0x16, 0x03, 0x00}, + {0x01, 0x02, 0x24}, + {0x16, 0x03, 0x24}, + {0x01, 0x02, 0x40}, + {0x16, 0x03, 0x40}, + {0x01, 0x02, 0x5B}, + {0x16, 0x03, 0x5B}, + {0x01, 0x02, 0x5D}, + {0x16, 0x03, 0x5D}, + {0x01, 0x02, 0x7E}, + {0x16, 0x03, 0x7E}, + {0x00, 0x03, 0x5E}, + {0x00, 0x03, 0x7D}, + {0x5D, 0x00, 0x00}, + {0x5E, 0x00, 0x00}, }, /* 85 */ { - {0x02, 2, 0}, - {0x09, 2, 0}, - {0x17, 2, 0}, - {0x28, 3, 0}, - {0x02, 2, 36}, - {0x09, 2, 36}, - {0x17, 2, 36}, - {0x28, 3, 36}, - {0x02, 2, 64}, - {0x09, 2, 64}, - {0x17, 2, 64}, - {0x28, 3, 64}, - {0x02, 2, 91}, - {0x09, 2, 91}, - {0x17, 2, 91}, - {0x28, 3, 91}, + {0x02, 0x02, 0x00}, + {0x09, 0x02, 0x00}, + {0x17, 0x02, 0x00}, + {0x28, 0x03, 0x00}, + {0x02, 0x02, 0x24}, + {0x09, 0x02, 0x24}, + {0x17, 0x02, 0x24}, + {0x28, 0x03, 0x24}, + {0x02, 0x02, 0x40}, + {0x09, 0x02, 0x40}, + {0x17, 0x02, 0x40}, + {0x28, 0x03, 0x40}, + {0x02, 0x02, 0x5B}, + {0x09, 0x02, 0x5B}, + {0x17, 0x02, 0x5B}, + {0x28, 0x03, 0x5B}, }, /* 86 */ { - {0x03, 2, 0}, - {0x06, 2, 0}, - {0x0a, 2, 0}, - {0x0f, 2, 0}, - {0x18, 2, 0}, - {0x1f, 2, 0}, - {0x29, 2, 0}, - {0x38, 3, 0}, - {0x03, 2, 36}, - {0x06, 2, 36}, - {0x0a, 2, 36}, - {0x0f, 2, 36}, - {0x18, 2, 36}, - {0x1f, 2, 36}, - {0x29, 2, 36}, - {0x38, 3, 36}, + {0x03, 0x02, 0x00}, + {0x06, 0x02, 0x00}, + {0x0A, 0x02, 0x00}, + {0x0F, 0x02, 0x00}, + {0x18, 0x02, 0x00}, + {0x1F, 0x02, 0x00}, + {0x29, 0x02, 0x00}, + {0x38, 0x03, 0x00}, + {0x03, 0x02, 0x24}, + {0x06, 0x02, 0x24}, + {0x0A, 0x02, 0x24}, + {0x0F, 0x02, 0x24}, + {0x18, 0x02, 0x24}, + {0x1F, 0x02, 0x24}, + {0x29, 0x02, 0x24}, + {0x38, 0x03, 0x24}, }, /* 87 */ { - {0x03, 2, 64}, - {0x06, 2, 64}, - {0x0a, 2, 64}, - {0x0f, 2, 64}, - {0x18, 2, 64}, - {0x1f, 2, 64}, - {0x29, 2, 64}, - {0x38, 3, 64}, - {0x03, 2, 91}, - {0x06, 2, 91}, - {0x0a, 2, 91}, - {0x0f, 2, 91}, - {0x18, 2, 91}, - {0x1f, 2, 91}, - {0x29, 2, 91}, - {0x38, 3, 91}, + {0x03, 0x02, 0x40}, + {0x06, 0x02, 0x40}, + {0x0A, 0x02, 0x40}, + {0x0F, 0x02, 0x40}, + {0x18, 0x02, 0x40}, + {0x1F, 0x02, 0x40}, + {0x29, 0x02, 0x40}, + {0x38, 0x03, 0x40}, + {0x03, 0x02, 0x5B}, + {0x06, 0x02, 0x5B}, + {0x0A, 0x02, 0x5B}, + {0x0F, 0x02, 0x5B}, + {0x18, 0x02, 0x5B}, + {0x1F, 0x02, 0x5B}, + {0x29, 0x02, 0x5B}, + {0x38, 0x03, 0x5B}, }, /* 88 */ { - {0x02, 2, 93}, - {0x09, 2, 93}, - {0x17, 2, 93}, - {0x28, 3, 93}, - {0x02, 2, 126}, - {0x09, 2, 126}, - {0x17, 2, 126}, - {0x28, 3, 126}, - {0x01, 2, 94}, - {0x16, 3, 94}, - {0x01, 2, 125}, - {0x16, 3, 125}, - {0x00, 3, 60}, - {0x00, 3, 96}, - {0x00, 3, 123}, - {0x5f, 0, 0}, + {0x02, 0x02, 0x5D}, + {0x09, 0x02, 0x5D}, + {0x17, 0x02, 0x5D}, + {0x28, 0x03, 0x5D}, + {0x02, 0x02, 0x7E}, + {0x09, 0x02, 0x7E}, + {0x17, 0x02, 0x7E}, + {0x28, 0x03, 0x7E}, + {0x01, 0x02, 0x5E}, + {0x16, 0x03, 0x5E}, + {0x01, 0x02, 0x7D}, + {0x16, 0x03, 0x7D}, + {0x00, 0x03, 0x3C}, + {0x00, 0x03, 0x60}, + {0x00, 0x03, 0x7B}, + {0x5F, 0x00, 0x00}, }, /* 89 */ { - {0x03, 2, 93}, - {0x06, 2, 93}, - {0x0a, 2, 93}, - {0x0f, 2, 93}, - {0x18, 2, 93}, - {0x1f, 2, 93}, - {0x29, 2, 93}, - {0x38, 3, 93}, - {0x03, 2, 126}, - {0x06, 2, 126}, - {0x0a, 2, 126}, - {0x0f, 2, 126}, - {0x18, 2, 126}, - {0x1f, 2, 126}, - {0x29, 2, 126}, - {0x38, 3, 126}, + {0x03, 0x02, 0x5D}, + {0x06, 0x02, 0x5D}, + {0x0A, 0x02, 0x5D}, + {0x0F, 0x02, 0x5D}, + {0x18, 0x02, 0x5D}, + {0x1F, 0x02, 0x5D}, + {0x29, 0x02, 0x5D}, + {0x38, 0x03, 0x5D}, + {0x03, 0x02, 0x7E}, + {0x06, 0x02, 0x7E}, + {0x0A, 0x02, 0x7E}, + {0x0F, 0x02, 0x7E}, + {0x18, 0x02, 0x7E}, + {0x1F, 0x02, 0x7E}, + {0x29, 0x02, 0x7E}, + {0x38, 0x03, 0x7E}, }, /* 90 */ { - {0x02, 2, 94}, - {0x09, 2, 94}, - {0x17, 2, 94}, - {0x28, 3, 94}, - {0x02, 2, 125}, - {0x09, 2, 125}, - {0x17, 2, 125}, - {0x28, 3, 125}, - {0x01, 2, 60}, - {0x16, 3, 60}, - {0x01, 2, 96}, - {0x16, 3, 96}, - {0x01, 2, 123}, - {0x16, 3, 123}, - {0x60, 0, 0}, - {0x6e, 0, 0}, + {0x02, 0x02, 0x5E}, + {0x09, 0x02, 0x5E}, + {0x17, 0x02, 0x5E}, + {0x28, 0x03, 0x5E}, + {0x02, 0x02, 0x7D}, + {0x09, 0x02, 0x7D}, + {0x17, 0x02, 0x7D}, + {0x28, 0x03, 0x7D}, + {0x01, 0x02, 0x3C}, + {0x16, 0x03, 0x3C}, + {0x01, 0x02, 0x60}, + {0x16, 0x03, 0x60}, + {0x01, 0x02, 0x7B}, + {0x16, 0x03, 0x7B}, + {0x60, 0x00, 0x00}, + {0x6E, 0x00, 0x00}, }, /* 91 */ { - {0x03, 2, 94}, - {0x06, 2, 94}, - {0x0a, 2, 94}, - {0x0f, 2, 94}, - {0x18, 2, 94}, - {0x1f, 2, 94}, - {0x29, 2, 94}, - {0x38, 3, 94}, - {0x03, 2, 125}, - {0x06, 2, 125}, - {0x0a, 2, 125}, - {0x0f, 2, 125}, - {0x18, 2, 125}, - {0x1f, 2, 125}, - {0x29, 2, 125}, - {0x38, 3, 125}, + {0x03, 0x02, 0x5E}, + {0x06, 0x02, 0x5E}, + {0x0A, 0x02, 0x5E}, + {0x0F, 0x02, 0x5E}, + {0x18, 0x02, 0x5E}, + {0x1F, 0x02, 0x5E}, + {0x29, 0x02, 0x5E}, + {0x38, 0x03, 0x5E}, + {0x03, 0x02, 0x7D}, + {0x06, 0x02, 0x7D}, + {0x0A, 0x02, 0x7D}, + {0x0F, 0x02, 0x7D}, + {0x18, 0x02, 0x7D}, + {0x1F, 0x02, 0x7D}, + {0x29, 0x02, 0x7D}, + {0x38, 0x03, 0x7D}, }, /* 92 */ { - {0x02, 2, 60}, - {0x09, 2, 60}, - {0x17, 2, 60}, - {0x28, 3, 60}, - {0x02, 2, 96}, - {0x09, 2, 96}, - {0x17, 2, 96}, - {0x28, 3, 96}, - {0x02, 2, 123}, - {0x09, 2, 123}, - {0x17, 2, 123}, - {0x28, 3, 123}, - {0x61, 0, 0}, - {0x65, 0, 0}, - {0x6f, 0, 0}, - {0x85, 0, 0}, + {0x02, 0x02, 0x3C}, + {0x09, 0x02, 0x3C}, + {0x17, 0x02, 0x3C}, + {0x28, 0x03, 0x3C}, + {0x02, 0x02, 0x60}, + {0x09, 0x02, 0x60}, + {0x17, 0x02, 0x60}, + {0x28, 0x03, 0x60}, + {0x02, 0x02, 0x7B}, + {0x09, 0x02, 0x7B}, + {0x17, 0x02, 0x7B}, + {0x28, 0x03, 0x7B}, + {0x61, 0x00, 0x00}, + {0x65, 0x00, 0x00}, + {0x6F, 0x00, 0x00}, + {0x85, 0x00, 0x00}, }, /* 93 */ { - {0x03, 2, 60}, - {0x06, 2, 60}, - {0x0a, 2, 60}, - {0x0f, 2, 60}, - {0x18, 2, 60}, - {0x1f, 2, 60}, - {0x29, 2, 60}, - {0x38, 3, 60}, - {0x03, 2, 96}, - {0x06, 2, 96}, - {0x0a, 2, 96}, - {0x0f, 2, 96}, - {0x18, 2, 96}, - {0x1f, 2, 96}, - {0x29, 2, 96}, - {0x38, 3, 96}, + {0x03, 0x02, 0x3C}, + {0x06, 0x02, 0x3C}, + {0x0A, 0x02, 0x3C}, + {0x0F, 0x02, 0x3C}, + {0x18, 0x02, 0x3C}, + {0x1F, 0x02, 0x3C}, + {0x29, 0x02, 0x3C}, + {0x38, 0x03, 0x3C}, + {0x03, 0x02, 0x60}, + {0x06, 0x02, 0x60}, + {0x0A, 0x02, 0x60}, + {0x0F, 0x02, 0x60}, + {0x18, 0x02, 0x60}, + {0x1F, 0x02, 0x60}, + {0x29, 0x02, 0x60}, + {0x38, 0x03, 0x60}, }, /* 94 */ { - {0x03, 2, 123}, - {0x06, 2, 123}, - {0x0a, 2, 123}, - {0x0f, 2, 123}, - {0x18, 2, 123}, - {0x1f, 2, 123}, - {0x29, 2, 123}, - {0x38, 3, 123}, - {0x62, 0, 0}, - {0x63, 0, 0}, - {0x66, 0, 0}, - {0x69, 0, 0}, - {0x70, 0, 0}, - {0x77, 0, 0}, - {0x86, 0, 0}, - {0x99, 0, 0}, + {0x03, 0x02, 0x7B}, + {0x06, 0x02, 0x7B}, + {0x0A, 0x02, 0x7B}, + {0x0F, 0x02, 0x7B}, + {0x18, 0x02, 0x7B}, + {0x1F, 0x02, 0x7B}, + {0x29, 0x02, 0x7B}, + {0x38, 0x03, 0x7B}, + {0x62, 0x00, 0x00}, + {0x63, 0x00, 0x00}, + {0x66, 0x00, 0x00}, + {0x69, 0x00, 0x00}, + {0x70, 0x00, 0x00}, + {0x77, 0x00, 0x00}, + {0x86, 0x00, 0x00}, + {0x99, 0x00, 0x00}, }, /* 95 */ { - {0x00, 3, 92}, - {0x00, 3, 195}, - {0x00, 3, 208}, - {0x64, 0, 0}, - {0x67, 0, 0}, - {0x68, 0, 0}, - {0x6a, 0, 0}, - {0x6b, 0, 0}, - {0x71, 0, 0}, - {0x74, 0, 0}, - {0x78, 0, 0}, - {0x7e, 0, 0}, - {0x87, 0, 0}, - {0x8e, 0, 0}, - {0x9a, 0, 0}, - {0xa9, 0, 0}, + {0x00, 0x03, 0x5C}, + {0x00, 0x03, 0xC3}, + {0x00, 0x03, 0xD0}, + {0x64, 0x00, 0x00}, + {0x67, 0x00, 0x00}, + {0x68, 0x00, 0x00}, + {0x6A, 0x00, 0x00}, + {0x6B, 0x00, 0x00}, + {0x71, 0x00, 0x00}, + {0x74, 0x00, 0x00}, + {0x78, 0x00, 0x00}, + {0x7E, 0x00, 0x00}, + {0x87, 0x00, 0x00}, + {0x8E, 0x00, 0x00}, + {0x9A, 0x00, 0x00}, + {0xA9, 0x00, 0x00}, }, /* 96 */ { - {0x01, 2, 92}, - {0x16, 3, 92}, - {0x01, 2, 195}, - {0x16, 3, 195}, - {0x01, 2, 208}, - {0x16, 3, 208}, - {0x00, 3, 128}, - {0x00, 3, 130}, - {0x00, 3, 131}, - {0x00, 3, 162}, - {0x00, 3, 184}, - {0x00, 3, 194}, - {0x00, 3, 224}, - {0x00, 3, 226}, - {0x6c, 0, 0}, - {0x6d, 0, 0}, + {0x01, 0x02, 0x5C}, + {0x16, 0x03, 0x5C}, + {0x01, 0x02, 0xC3}, + {0x16, 0x03, 0xC3}, + {0x01, 0x02, 0xD0}, + {0x16, 0x03, 0xD0}, + {0x00, 0x03, 0x80}, + {0x00, 0x03, 0x82}, + {0x00, 0x03, 0x83}, + {0x00, 0x03, 0xA2}, + {0x00, 0x03, 0xB8}, + {0x00, 0x03, 0xC2}, + {0x00, 0x03, 0xE0}, + {0x00, 0x03, 0xE2}, + {0x6C, 0x00, 0x00}, + {0x6D, 0x00, 0x00}, }, /* 97 */ { - {0x02, 2, 92}, - {0x09, 2, 92}, - {0x17, 2, 92}, - {0x28, 3, 92}, - {0x02, 2, 195}, - {0x09, 2, 195}, - {0x17, 2, 195}, - {0x28, 3, 195}, - {0x02, 2, 208}, - {0x09, 2, 208}, - {0x17, 2, 208}, - {0x28, 3, 208}, - {0x01, 2, 128}, - {0x16, 3, 128}, - {0x01, 2, 130}, - {0x16, 3, 130}, + {0x02, 0x02, 0x5C}, + {0x09, 0x02, 0x5C}, + {0x17, 0x02, 0x5C}, + {0x28, 0x03, 0x5C}, + {0x02, 0x02, 0xC3}, + {0x09, 0x02, 0xC3}, + {0x17, 0x02, 0xC3}, + {0x28, 0x03, 0xC3}, + {0x02, 0x02, 0xD0}, + {0x09, 0x02, 0xD0}, + {0x17, 0x02, 0xD0}, + {0x28, 0x03, 0xD0}, + {0x01, 0x02, 0x80}, + {0x16, 0x03, 0x80}, + {0x01, 0x02, 0x82}, + {0x16, 0x03, 0x82}, }, /* 98 */ { - {0x03, 2, 92}, - {0x06, 2, 92}, - {0x0a, 2, 92}, - {0x0f, 2, 92}, - {0x18, 2, 92}, - {0x1f, 2, 92}, - {0x29, 2, 92}, - {0x38, 3, 92}, - {0x03, 2, 195}, - {0x06, 2, 195}, - {0x0a, 2, 195}, - {0x0f, 2, 195}, - {0x18, 2, 195}, - {0x1f, 2, 195}, - {0x29, 2, 195}, - {0x38, 3, 195}, + {0x03, 0x02, 0x5C}, + {0x06, 0x02, 0x5C}, + {0x0A, 0x02, 0x5C}, + {0x0F, 0x02, 0x5C}, + {0x18, 0x02, 0x5C}, + {0x1F, 0x02, 0x5C}, + {0x29, 0x02, 0x5C}, + {0x38, 0x03, 0x5C}, + {0x03, 0x02, 0xC3}, + {0x06, 0x02, 0xC3}, + {0x0A, 0x02, 0xC3}, + {0x0F, 0x02, 0xC3}, + {0x18, 0x02, 0xC3}, + {0x1F, 0x02, 0xC3}, + {0x29, 0x02, 0xC3}, + {0x38, 0x03, 0xC3}, }, /* 99 */ { - {0x03, 2, 208}, - {0x06, 2, 208}, - {0x0a, 2, 208}, - {0x0f, 2, 208}, - {0x18, 2, 208}, - {0x1f, 2, 208}, - {0x29, 2, 208}, - {0x38, 3, 208}, - {0x02, 2, 128}, - {0x09, 2, 128}, - {0x17, 2, 128}, - {0x28, 3, 128}, - {0x02, 2, 130}, - {0x09, 2, 130}, - {0x17, 2, 130}, - {0x28, 3, 130}, + {0x03, 0x02, 0xD0}, + {0x06, 0x02, 0xD0}, + {0x0A, 0x02, 0xD0}, + {0x0F, 0x02, 0xD0}, + {0x18, 0x02, 0xD0}, + {0x1F, 0x02, 0xD0}, + {0x29, 0x02, 0xD0}, + {0x38, 0x03, 0xD0}, + {0x02, 0x02, 0x80}, + {0x09, 0x02, 0x80}, + {0x17, 0x02, 0x80}, + {0x28, 0x03, 0x80}, + {0x02, 0x02, 0x82}, + {0x09, 0x02, 0x82}, + {0x17, 0x02, 0x82}, + {0x28, 0x03, 0x82}, }, /* 100 */ { - {0x03, 2, 128}, - {0x06, 2, 128}, - {0x0a, 2, 128}, - {0x0f, 2, 128}, - {0x18, 2, 128}, - {0x1f, 2, 128}, - {0x29, 2, 128}, - {0x38, 3, 128}, - {0x03, 2, 130}, - {0x06, 2, 130}, - {0x0a, 2, 130}, - {0x0f, 2, 130}, - {0x18, 2, 130}, - {0x1f, 2, 130}, - {0x29, 2, 130}, - {0x38, 3, 130}, + {0x03, 0x02, 0x80}, + {0x06, 0x02, 0x80}, + {0x0A, 0x02, 0x80}, + {0x0F, 0x02, 0x80}, + {0x18, 0x02, 0x80}, + {0x1F, 0x02, 0x80}, + {0x29, 0x02, 0x80}, + {0x38, 0x03, 0x80}, + {0x03, 0x02, 0x82}, + {0x06, 0x02, 0x82}, + {0x0A, 0x02, 0x82}, + {0x0F, 0x02, 0x82}, + {0x18, 0x02, 0x82}, + {0x1F, 0x02, 0x82}, + {0x29, 0x02, 0x82}, + {0x38, 0x03, 0x82}, }, /* 101 */ { - {0x01, 2, 131}, - {0x16, 3, 131}, - {0x01, 2, 162}, - {0x16, 3, 162}, - {0x01, 2, 184}, - {0x16, 3, 184}, - {0x01, 2, 194}, - {0x16, 3, 194}, - {0x01, 2, 224}, - {0x16, 3, 224}, - {0x01, 2, 226}, - {0x16, 3, 226}, - {0x00, 3, 153}, - {0x00, 3, 161}, - {0x00, 3, 167}, - {0x00, 3, 172}, + {0x01, 0x02, 0x83}, + {0x16, 0x03, 0x83}, + {0x01, 0x02, 0xA2}, + {0x16, 0x03, 0xA2}, + {0x01, 0x02, 0xB8}, + {0x16, 0x03, 0xB8}, + {0x01, 0x02, 0xC2}, + {0x16, 0x03, 0xC2}, + {0x01, 0x02, 0xE0}, + {0x16, 0x03, 0xE0}, + {0x01, 0x02, 0xE2}, + {0x16, 0x03, 0xE2}, + {0x00, 0x03, 0x99}, + {0x00, 0x03, 0xA1}, + {0x00, 0x03, 0xA7}, + {0x00, 0x03, 0xAC}, }, /* 102 */ { - {0x02, 2, 131}, - {0x09, 2, 131}, - {0x17, 2, 131}, - {0x28, 3, 131}, - {0x02, 2, 162}, - {0x09, 2, 162}, - {0x17, 2, 162}, - {0x28, 3, 162}, - {0x02, 2, 184}, - {0x09, 2, 184}, - {0x17, 2, 184}, - {0x28, 3, 184}, - {0x02, 2, 194}, - {0x09, 2, 194}, - {0x17, 2, 194}, - {0x28, 3, 194}, + {0x02, 0x02, 0x83}, + {0x09, 0x02, 0x83}, + {0x17, 0x02, 0x83}, + {0x28, 0x03, 0x83}, + {0x02, 0x02, 0xA2}, + {0x09, 0x02, 0xA2}, + {0x17, 0x02, 0xA2}, + {0x28, 0x03, 0xA2}, + {0x02, 0x02, 0xB8}, + {0x09, 0x02, 0xB8}, + {0x17, 0x02, 0xB8}, + {0x28, 0x03, 0xB8}, + {0x02, 0x02, 0xC2}, + {0x09, 0x02, 0xC2}, + {0x17, 0x02, 0xC2}, + {0x28, 0x03, 0xC2}, }, /* 103 */ { - {0x03, 2, 131}, - {0x06, 2, 131}, - {0x0a, 2, 131}, - {0x0f, 2, 131}, - {0x18, 2, 131}, - {0x1f, 2, 131}, - {0x29, 2, 131}, - {0x38, 3, 131}, - {0x03, 2, 162}, - {0x06, 2, 162}, - {0x0a, 2, 162}, - {0x0f, 2, 162}, - {0x18, 2, 162}, - {0x1f, 2, 162}, - {0x29, 2, 162}, - {0x38, 3, 162}, + {0x03, 0x02, 0x83}, + {0x06, 0x02, 0x83}, + {0x0A, 0x02, 0x83}, + {0x0F, 0x02, 0x83}, + {0x18, 0x02, 0x83}, + {0x1F, 0x02, 0x83}, + {0x29, 0x02, 0x83}, + {0x38, 0x03, 0x83}, + {0x03, 0x02, 0xA2}, + {0x06, 0x02, 0xA2}, + {0x0A, 0x02, 0xA2}, + {0x0F, 0x02, 0xA2}, + {0x18, 0x02, 0xA2}, + {0x1F, 0x02, 0xA2}, + {0x29, 0x02, 0xA2}, + {0x38, 0x03, 0xA2}, }, /* 104 */ { - {0x03, 2, 184}, - {0x06, 2, 184}, - {0x0a, 2, 184}, - {0x0f, 2, 184}, - {0x18, 2, 184}, - {0x1f, 2, 184}, - {0x29, 2, 184}, - {0x38, 3, 184}, - {0x03, 2, 194}, - {0x06, 2, 194}, - {0x0a, 2, 194}, - {0x0f, 2, 194}, - {0x18, 2, 194}, - {0x1f, 2, 194}, - {0x29, 2, 194}, - {0x38, 3, 194}, + {0x03, 0x02, 0xB8}, + {0x06, 0x02, 0xB8}, + {0x0A, 0x02, 0xB8}, + {0x0F, 0x02, 0xB8}, + {0x18, 0x02, 0xB8}, + {0x1F, 0x02, 0xB8}, + {0x29, 0x02, 0xB8}, + {0x38, 0x03, 0xB8}, + {0x03, 0x02, 0xC2}, + {0x06, 0x02, 0xC2}, + {0x0A, 0x02, 0xC2}, + {0x0F, 0x02, 0xC2}, + {0x18, 0x02, 0xC2}, + {0x1F, 0x02, 0xC2}, + {0x29, 0x02, 0xC2}, + {0x38, 0x03, 0xC2}, }, /* 105 */ { - {0x02, 2, 224}, - {0x09, 2, 224}, - {0x17, 2, 224}, - {0x28, 3, 224}, - {0x02, 2, 226}, - {0x09, 2, 226}, - {0x17, 2, 226}, - {0x28, 3, 226}, - {0x01, 2, 153}, - {0x16, 3, 153}, - {0x01, 2, 161}, - {0x16, 3, 161}, - {0x01, 2, 167}, - {0x16, 3, 167}, - {0x01, 2, 172}, - {0x16, 3, 172}, + {0x02, 0x02, 0xE0}, + {0x09, 0x02, 0xE0}, + {0x17, 0x02, 0xE0}, + {0x28, 0x03, 0xE0}, + {0x02, 0x02, 0xE2}, + {0x09, 0x02, 0xE2}, + {0x17, 0x02, 0xE2}, + {0x28, 0x03, 0xE2}, + {0x01, 0x02, 0x99}, + {0x16, 0x03, 0x99}, + {0x01, 0x02, 0xA1}, + {0x16, 0x03, 0xA1}, + {0x01, 0x02, 0xA7}, + {0x16, 0x03, 0xA7}, + {0x01, 0x02, 0xAC}, + {0x16, 0x03, 0xAC}, }, /* 106 */ { - {0x03, 2, 224}, - {0x06, 2, 224}, - {0x0a, 2, 224}, - {0x0f, 2, 224}, - {0x18, 2, 224}, - {0x1f, 2, 224}, - {0x29, 2, 224}, - {0x38, 3, 224}, - {0x03, 2, 226}, - {0x06, 2, 226}, - {0x0a, 2, 226}, - {0x0f, 2, 226}, - {0x18, 2, 226}, - {0x1f, 2, 226}, - {0x29, 2, 226}, - {0x38, 3, 226}, + {0x03, 0x02, 0xE0}, + {0x06, 0x02, 0xE0}, + {0x0A, 0x02, 0xE0}, + {0x0F, 0x02, 0xE0}, + {0x18, 0x02, 0xE0}, + {0x1F, 0x02, 0xE0}, + {0x29, 0x02, 0xE0}, + {0x38, 0x03, 0xE0}, + {0x03, 0x02, 0xE2}, + {0x06, 0x02, 0xE2}, + {0x0A, 0x02, 0xE2}, + {0x0F, 0x02, 0xE2}, + {0x18, 0x02, 0xE2}, + {0x1F, 0x02, 0xE2}, + {0x29, 0x02, 0xE2}, + {0x38, 0x03, 0xE2}, }, /* 107 */ { - {0x02, 2, 153}, - {0x09, 2, 153}, - {0x17, 2, 153}, - {0x28, 3, 153}, - {0x02, 2, 161}, - {0x09, 2, 161}, - {0x17, 2, 161}, - {0x28, 3, 161}, - {0x02, 2, 167}, - {0x09, 2, 167}, - {0x17, 2, 167}, - {0x28, 3, 167}, - {0x02, 2, 172}, - {0x09, 2, 172}, - {0x17, 2, 172}, - {0x28, 3, 172}, + {0x02, 0x02, 0x99}, + {0x09, 0x02, 0x99}, + {0x17, 0x02, 0x99}, + {0x28, 0x03, 0x99}, + {0x02, 0x02, 0xA1}, + {0x09, 0x02, 0xA1}, + {0x17, 0x02, 0xA1}, + {0x28, 0x03, 0xA1}, + {0x02, 0x02, 0xA7}, + {0x09, 0x02, 0xA7}, + {0x17, 0x02, 0xA7}, + {0x28, 0x03, 0xA7}, + {0x02, 0x02, 0xAC}, + {0x09, 0x02, 0xAC}, + {0x17, 0x02, 0xAC}, + {0x28, 0x03, 0xAC}, }, /* 108 */ { - {0x03, 2, 153}, - {0x06, 2, 153}, - {0x0a, 2, 153}, - {0x0f, 2, 153}, - {0x18, 2, 153}, - {0x1f, 2, 153}, - {0x29, 2, 153}, - {0x38, 3, 153}, - {0x03, 2, 161}, - {0x06, 2, 161}, - {0x0a, 2, 161}, - {0x0f, 2, 161}, - {0x18, 2, 161}, - {0x1f, 2, 161}, - {0x29, 2, 161}, - {0x38, 3, 161}, + {0x03, 0x02, 0x99}, + {0x06, 0x02, 0x99}, + {0x0A, 0x02, 0x99}, + {0x0F, 0x02, 0x99}, + {0x18, 0x02, 0x99}, + {0x1F, 0x02, 0x99}, + {0x29, 0x02, 0x99}, + {0x38, 0x03, 0x99}, + {0x03, 0x02, 0xA1}, + {0x06, 0x02, 0xA1}, + {0x0A, 0x02, 0xA1}, + {0x0F, 0x02, 0xA1}, + {0x18, 0x02, 0xA1}, + {0x1F, 0x02, 0xA1}, + {0x29, 0x02, 0xA1}, + {0x38, 0x03, 0xA1}, }, /* 109 */ { - {0x03, 2, 167}, - {0x06, 2, 167}, - {0x0a, 2, 167}, - {0x0f, 2, 167}, - {0x18, 2, 167}, - {0x1f, 2, 167}, - {0x29, 2, 167}, - {0x38, 3, 167}, - {0x03, 2, 172}, - {0x06, 2, 172}, - {0x0a, 2, 172}, - {0x0f, 2, 172}, - {0x18, 2, 172}, - {0x1f, 2, 172}, - {0x29, 2, 172}, - {0x38, 3, 172}, + {0x03, 0x02, 0xA7}, + {0x06, 0x02, 0xA7}, + {0x0A, 0x02, 0xA7}, + {0x0F, 0x02, 0xA7}, + {0x18, 0x02, 0xA7}, + {0x1F, 0x02, 0xA7}, + {0x29, 0x02, 0xA7}, + {0x38, 0x03, 0xA7}, + {0x03, 0x02, 0xAC}, + {0x06, 0x02, 0xAC}, + {0x0A, 0x02, 0xAC}, + {0x0F, 0x02, 0xAC}, + {0x18, 0x02, 0xAC}, + {0x1F, 0x02, 0xAC}, + {0x29, 0x02, 0xAC}, + {0x38, 0x03, 0xAC}, }, /* 110 */ { - {0x72, 0, 0}, - {0x73, 0, 0}, - {0x75, 0, 0}, - {0x76, 0, 0}, - {0x79, 0, 0}, - {0x7b, 0, 0}, - {0x7f, 0, 0}, - {0x82, 0, 0}, - {0x88, 0, 0}, - {0x8b, 0, 0}, - {0x8f, 0, 0}, - {0x92, 0, 0}, - {0x9b, 0, 0}, - {0xa2, 0, 0}, - {0xaa, 0, 0}, - {0xb4, 0, 0}, + {0x72, 0x00, 0x00}, + {0x73, 0x00, 0x00}, + {0x75, 0x00, 0x00}, + {0x76, 0x00, 0x00}, + {0x79, 0x00, 0x00}, + {0x7B, 0x00, 0x00}, + {0x7F, 0x00, 0x00}, + {0x82, 0x00, 0x00}, + {0x88, 0x00, 0x00}, + {0x8B, 0x00, 0x00}, + {0x8F, 0x00, 0x00}, + {0x92, 0x00, 0x00}, + {0x9B, 0x00, 0x00}, + {0xA2, 0x00, 0x00}, + {0xAA, 0x00, 0x00}, + {0xB4, 0x00, 0x00}, }, /* 111 */ { - {0x00, 3, 176}, - {0x00, 3, 177}, - {0x00, 3, 179}, - {0x00, 3, 209}, - {0x00, 3, 216}, - {0x00, 3, 217}, - {0x00, 3, 227}, - {0x00, 3, 229}, - {0x00, 3, 230}, - {0x7a, 0, 0}, - {0x7c, 0, 0}, - {0x7d, 0, 0}, - {0x80, 0, 0}, - {0x81, 0, 0}, - {0x83, 0, 0}, - {0x84, 0, 0}, + {0x00, 0x03, 0xB0}, + {0x00, 0x03, 0xB1}, + {0x00, 0x03, 0xB3}, + {0x00, 0x03, 0xD1}, + {0x00, 0x03, 0xD8}, + {0x00, 0x03, 0xD9}, + {0x00, 0x03, 0xE3}, + {0x00, 0x03, 0xE5}, + {0x00, 0x03, 0xE6}, + {0x7A, 0x00, 0x00}, + {0x7C, 0x00, 0x00}, + {0x7D, 0x00, 0x00}, + {0x80, 0x00, 0x00}, + {0x81, 0x00, 0x00}, + {0x83, 0x00, 0x00}, + {0x84, 0x00, 0x00}, }, /* 112 */ { - {0x01, 2, 176}, - {0x16, 3, 176}, - {0x01, 2, 177}, - {0x16, 3, 177}, - {0x01, 2, 179}, - {0x16, 3, 179}, - {0x01, 2, 209}, - {0x16, 3, 209}, - {0x01, 2, 216}, - {0x16, 3, 216}, - {0x01, 2, 217}, - {0x16, 3, 217}, - {0x01, 2, 227}, - {0x16, 3, 227}, - {0x01, 2, 229}, - {0x16, 3, 229}, + {0x01, 0x02, 0xB0}, + {0x16, 0x03, 0xB0}, + {0x01, 0x02, 0xB1}, + {0x16, 0x03, 0xB1}, + {0x01, 0x02, 0xB3}, + {0x16, 0x03, 0xB3}, + {0x01, 0x02, 0xD1}, + {0x16, 0x03, 0xD1}, + {0x01, 0x02, 0xD8}, + {0x16, 0x03, 0xD8}, + {0x01, 0x02, 0xD9}, + {0x16, 0x03, 0xD9}, + {0x01, 0x02, 0xE3}, + {0x16, 0x03, 0xE3}, + {0x01, 0x02, 0xE5}, + {0x16, 0x03, 0xE5}, }, /* 113 */ { - {0x02, 2, 176}, - {0x09, 2, 176}, - {0x17, 2, 176}, - {0x28, 3, 176}, - {0x02, 2, 177}, - {0x09, 2, 177}, - {0x17, 2, 177}, - {0x28, 3, 177}, - {0x02, 2, 179}, - {0x09, 2, 179}, - {0x17, 2, 179}, - {0x28, 3, 179}, - {0x02, 2, 209}, - {0x09, 2, 209}, - {0x17, 2, 209}, - {0x28, 3, 209}, + {0x02, 0x02, 0xB0}, + {0x09, 0x02, 0xB0}, + {0x17, 0x02, 0xB0}, + {0x28, 0x03, 0xB0}, + {0x02, 0x02, 0xB1}, + {0x09, 0x02, 0xB1}, + {0x17, 0x02, 0xB1}, + {0x28, 0x03, 0xB1}, + {0x02, 0x02, 0xB3}, + {0x09, 0x02, 0xB3}, + {0x17, 0x02, 0xB3}, + {0x28, 0x03, 0xB3}, + {0x02, 0x02, 0xD1}, + {0x09, 0x02, 0xD1}, + {0x17, 0x02, 0xD1}, + {0x28, 0x03, 0xD1}, }, /* 114 */ { - {0x03, 2, 176}, - {0x06, 2, 176}, - {0x0a, 2, 176}, - {0x0f, 2, 176}, - {0x18, 2, 176}, - {0x1f, 2, 176}, - {0x29, 2, 176}, - {0x38, 3, 176}, - {0x03, 2, 177}, - {0x06, 2, 177}, - {0x0a, 2, 177}, - {0x0f, 2, 177}, - {0x18, 2, 177}, - {0x1f, 2, 177}, - {0x29, 2, 177}, - {0x38, 3, 177}, + {0x03, 0x02, 0xB0}, + {0x06, 0x02, 0xB0}, + {0x0A, 0x02, 0xB0}, + {0x0F, 0x02, 0xB0}, + {0x18, 0x02, 0xB0}, + {0x1F, 0x02, 0xB0}, + {0x29, 0x02, 0xB0}, + {0x38, 0x03, 0xB0}, + {0x03, 0x02, 0xB1}, + {0x06, 0x02, 0xB1}, + {0x0A, 0x02, 0xB1}, + {0x0F, 0x02, 0xB1}, + {0x18, 0x02, 0xB1}, + {0x1F, 0x02, 0xB1}, + {0x29, 0x02, 0xB1}, + {0x38, 0x03, 0xB1}, }, /* 115 */ { - {0x03, 2, 179}, - {0x06, 2, 179}, - {0x0a, 2, 179}, - {0x0f, 2, 179}, - {0x18, 2, 179}, - {0x1f, 2, 179}, - {0x29, 2, 179}, - {0x38, 3, 179}, - {0x03, 2, 209}, - {0x06, 2, 209}, - {0x0a, 2, 209}, - {0x0f, 2, 209}, - {0x18, 2, 209}, - {0x1f, 2, 209}, - {0x29, 2, 209}, - {0x38, 3, 209}, + {0x03, 0x02, 0xB3}, + {0x06, 0x02, 0xB3}, + {0x0A, 0x02, 0xB3}, + {0x0F, 0x02, 0xB3}, + {0x18, 0x02, 0xB3}, + {0x1F, 0x02, 0xB3}, + {0x29, 0x02, 0xB3}, + {0x38, 0x03, 0xB3}, + {0x03, 0x02, 0xD1}, + {0x06, 0x02, 0xD1}, + {0x0A, 0x02, 0xD1}, + {0x0F, 0x02, 0xD1}, + {0x18, 0x02, 0xD1}, + {0x1F, 0x02, 0xD1}, + {0x29, 0x02, 0xD1}, + {0x38, 0x03, 0xD1}, }, /* 116 */ { - {0x02, 2, 216}, - {0x09, 2, 216}, - {0x17, 2, 216}, - {0x28, 3, 216}, - {0x02, 2, 217}, - {0x09, 2, 217}, - {0x17, 2, 217}, - {0x28, 3, 217}, - {0x02, 2, 227}, - {0x09, 2, 227}, - {0x17, 2, 227}, - {0x28, 3, 227}, - {0x02, 2, 229}, - {0x09, 2, 229}, - {0x17, 2, 229}, - {0x28, 3, 229}, + {0x02, 0x02, 0xD8}, + {0x09, 0x02, 0xD8}, + {0x17, 0x02, 0xD8}, + {0x28, 0x03, 0xD8}, + {0x02, 0x02, 0xD9}, + {0x09, 0x02, 0xD9}, + {0x17, 0x02, 0xD9}, + {0x28, 0x03, 0xD9}, + {0x02, 0x02, 0xE3}, + {0x09, 0x02, 0xE3}, + {0x17, 0x02, 0xE3}, + {0x28, 0x03, 0xE3}, + {0x02, 0x02, 0xE5}, + {0x09, 0x02, 0xE5}, + {0x17, 0x02, 0xE5}, + {0x28, 0x03, 0xE5}, }, /* 117 */ { - {0x03, 2, 216}, - {0x06, 2, 216}, - {0x0a, 2, 216}, - {0x0f, 2, 216}, - {0x18, 2, 216}, - {0x1f, 2, 216}, - {0x29, 2, 216}, - {0x38, 3, 216}, - {0x03, 2, 217}, - {0x06, 2, 217}, - {0x0a, 2, 217}, - {0x0f, 2, 217}, - {0x18, 2, 217}, - {0x1f, 2, 217}, - {0x29, 2, 217}, - {0x38, 3, 217}, + {0x03, 0x02, 0xD8}, + {0x06, 0x02, 0xD8}, + {0x0A, 0x02, 0xD8}, + {0x0F, 0x02, 0xD8}, + {0x18, 0x02, 0xD8}, + {0x1F, 0x02, 0xD8}, + {0x29, 0x02, 0xD8}, + {0x38, 0x03, 0xD8}, + {0x03, 0x02, 0xD9}, + {0x06, 0x02, 0xD9}, + {0x0A, 0x02, 0xD9}, + {0x0F, 0x02, 0xD9}, + {0x18, 0x02, 0xD9}, + {0x1F, 0x02, 0xD9}, + {0x29, 0x02, 0xD9}, + {0x38, 0x03, 0xD9}, }, /* 118 */ { - {0x03, 2, 227}, - {0x06, 2, 227}, - {0x0a, 2, 227}, - {0x0f, 2, 227}, - {0x18, 2, 227}, - {0x1f, 2, 227}, - {0x29, 2, 227}, - {0x38, 3, 227}, - {0x03, 2, 229}, - {0x06, 2, 229}, - {0x0a, 2, 229}, - {0x0f, 2, 229}, - {0x18, 2, 229}, - {0x1f, 2, 229}, - {0x29, 2, 229}, - {0x38, 3, 229}, + {0x03, 0x02, 0xE3}, + {0x06, 0x02, 0xE3}, + {0x0A, 0x02, 0xE3}, + {0x0F, 0x02, 0xE3}, + {0x18, 0x02, 0xE3}, + {0x1F, 0x02, 0xE3}, + {0x29, 0x02, 0xE3}, + {0x38, 0x03, 0xE3}, + {0x03, 0x02, 0xE5}, + {0x06, 0x02, 0xE5}, + {0x0A, 0x02, 0xE5}, + {0x0F, 0x02, 0xE5}, + {0x18, 0x02, 0xE5}, + {0x1F, 0x02, 0xE5}, + {0x29, 0x02, 0xE5}, + {0x38, 0x03, 0xE5}, }, /* 119 */ { - {0x01, 2, 230}, - {0x16, 3, 230}, - {0x00, 3, 129}, - {0x00, 3, 132}, - {0x00, 3, 133}, - {0x00, 3, 134}, - {0x00, 3, 136}, - {0x00, 3, 146}, - {0x00, 3, 154}, - {0x00, 3, 156}, - {0x00, 3, 160}, - {0x00, 3, 163}, - {0x00, 3, 164}, - {0x00, 3, 169}, - {0x00, 3, 170}, - {0x00, 3, 173}, + {0x01, 0x02, 0xE6}, + {0x16, 0x03, 0xE6}, + {0x00, 0x03, 0x81}, + {0x00, 0x03, 0x84}, + {0x00, 0x03, 0x85}, + {0x00, 0x03, 0x86}, + {0x00, 0x03, 0x88}, + {0x00, 0x03, 0x92}, + {0x00, 0x03, 0x9A}, + {0x00, 0x03, 0x9C}, + {0x00, 0x03, 0xA0}, + {0x00, 0x03, 0xA3}, + {0x00, 0x03, 0xA4}, + {0x00, 0x03, 0xA9}, + {0x00, 0x03, 0xAA}, + {0x00, 0x03, 0xAD}, }, /* 120 */ { - {0x02, 2, 230}, - {0x09, 2, 230}, - {0x17, 2, 230}, - {0x28, 3, 230}, - {0x01, 2, 129}, - {0x16, 3, 129}, - {0x01, 2, 132}, - {0x16, 3, 132}, - {0x01, 2, 133}, - {0x16, 3, 133}, - {0x01, 2, 134}, - {0x16, 3, 134}, - {0x01, 2, 136}, - {0x16, 3, 136}, - {0x01, 2, 146}, - {0x16, 3, 146}, + {0x02, 0x02, 0xE6}, + {0x09, 0x02, 0xE6}, + {0x17, 0x02, 0xE6}, + {0x28, 0x03, 0xE6}, + {0x01, 0x02, 0x81}, + {0x16, 0x03, 0x81}, + {0x01, 0x02, 0x84}, + {0x16, 0x03, 0x84}, + {0x01, 0x02, 0x85}, + {0x16, 0x03, 0x85}, + {0x01, 0x02, 0x86}, + {0x16, 0x03, 0x86}, + {0x01, 0x02, 0x88}, + {0x16, 0x03, 0x88}, + {0x01, 0x02, 0x92}, + {0x16, 0x03, 0x92}, }, /* 121 */ { - {0x03, 2, 230}, - {0x06, 2, 230}, - {0x0a, 2, 230}, - {0x0f, 2, 230}, - {0x18, 2, 230}, - {0x1f, 2, 230}, - {0x29, 2, 230}, - {0x38, 3, 230}, - {0x02, 2, 129}, - {0x09, 2, 129}, - {0x17, 2, 129}, - {0x28, 3, 129}, - {0x02, 2, 132}, - {0x09, 2, 132}, - {0x17, 2, 132}, - {0x28, 3, 132}, + {0x03, 0x02, 0xE6}, + {0x06, 0x02, 0xE6}, + {0x0A, 0x02, 0xE6}, + {0x0F, 0x02, 0xE6}, + {0x18, 0x02, 0xE6}, + {0x1F, 0x02, 0xE6}, + {0x29, 0x02, 0xE6}, + {0x38, 0x03, 0xE6}, + {0x02, 0x02, 0x81}, + {0x09, 0x02, 0x81}, + {0x17, 0x02, 0x81}, + {0x28, 0x03, 0x81}, + {0x02, 0x02, 0x84}, + {0x09, 0x02, 0x84}, + {0x17, 0x02, 0x84}, + {0x28, 0x03, 0x84}, }, /* 122 */ { - {0x03, 2, 129}, - {0x06, 2, 129}, - {0x0a, 2, 129}, - {0x0f, 2, 129}, - {0x18, 2, 129}, - {0x1f, 2, 129}, - {0x29, 2, 129}, - {0x38, 3, 129}, - {0x03, 2, 132}, - {0x06, 2, 132}, - {0x0a, 2, 132}, - {0x0f, 2, 132}, - {0x18, 2, 132}, - {0x1f, 2, 132}, - {0x29, 2, 132}, - {0x38, 3, 132}, + {0x03, 0x02, 0x81}, + {0x06, 0x02, 0x81}, + {0x0A, 0x02, 0x81}, + {0x0F, 0x02, 0x81}, + {0x18, 0x02, 0x81}, + {0x1F, 0x02, 0x81}, + {0x29, 0x02, 0x81}, + {0x38, 0x03, 0x81}, + {0x03, 0x02, 0x84}, + {0x06, 0x02, 0x84}, + {0x0A, 0x02, 0x84}, + {0x0F, 0x02, 0x84}, + {0x18, 0x02, 0x84}, + {0x1F, 0x02, 0x84}, + {0x29, 0x02, 0x84}, + {0x38, 0x03, 0x84}, }, /* 123 */ { - {0x02, 2, 133}, - {0x09, 2, 133}, - {0x17, 2, 133}, - {0x28, 3, 133}, - {0x02, 2, 134}, - {0x09, 2, 134}, - {0x17, 2, 134}, - {0x28, 3, 134}, - {0x02, 2, 136}, - {0x09, 2, 136}, - {0x17, 2, 136}, - {0x28, 3, 136}, - {0x02, 2, 146}, - {0x09, 2, 146}, - {0x17, 2, 146}, - {0x28, 3, 146}, + {0x02, 0x02, 0x85}, + {0x09, 0x02, 0x85}, + {0x17, 0x02, 0x85}, + {0x28, 0x03, 0x85}, + {0x02, 0x02, 0x86}, + {0x09, 0x02, 0x86}, + {0x17, 0x02, 0x86}, + {0x28, 0x03, 0x86}, + {0x02, 0x02, 0x88}, + {0x09, 0x02, 0x88}, + {0x17, 0x02, 0x88}, + {0x28, 0x03, 0x88}, + {0x02, 0x02, 0x92}, + {0x09, 0x02, 0x92}, + {0x17, 0x02, 0x92}, + {0x28, 0x03, 0x92}, }, /* 124 */ { - {0x03, 2, 133}, - {0x06, 2, 133}, - {0x0a, 2, 133}, - {0x0f, 2, 133}, - {0x18, 2, 133}, - {0x1f, 2, 133}, - {0x29, 2, 133}, - {0x38, 3, 133}, - {0x03, 2, 134}, - {0x06, 2, 134}, - {0x0a, 2, 134}, - {0x0f, 2, 134}, - {0x18, 2, 134}, - {0x1f, 2, 134}, - {0x29, 2, 134}, - {0x38, 3, 134}, + {0x03, 0x02, 0x85}, + {0x06, 0x02, 0x85}, + {0x0A, 0x02, 0x85}, + {0x0F, 0x02, 0x85}, + {0x18, 0x02, 0x85}, + {0x1F, 0x02, 0x85}, + {0x29, 0x02, 0x85}, + {0x38, 0x03, 0x85}, + {0x03, 0x02, 0x86}, + {0x06, 0x02, 0x86}, + {0x0A, 0x02, 0x86}, + {0x0F, 0x02, 0x86}, + {0x18, 0x02, 0x86}, + {0x1F, 0x02, 0x86}, + {0x29, 0x02, 0x86}, + {0x38, 0x03, 0x86}, }, /* 125 */ { - {0x03, 2, 136}, - {0x06, 2, 136}, - {0x0a, 2, 136}, - {0x0f, 2, 136}, - {0x18, 2, 136}, - {0x1f, 2, 136}, - {0x29, 2, 136}, - {0x38, 3, 136}, - {0x03, 2, 146}, - {0x06, 2, 146}, - {0x0a, 2, 146}, - {0x0f, 2, 146}, - {0x18, 2, 146}, - {0x1f, 2, 146}, - {0x29, 2, 146}, - {0x38, 3, 146}, + {0x03, 0x02, 0x88}, + {0x06, 0x02, 0x88}, + {0x0A, 0x02, 0x88}, + {0x0F, 0x02, 0x88}, + {0x18, 0x02, 0x88}, + {0x1F, 0x02, 0x88}, + {0x29, 0x02, 0x88}, + {0x38, 0x03, 0x88}, + {0x03, 0x02, 0x92}, + {0x06, 0x02, 0x92}, + {0x0A, 0x02, 0x92}, + {0x0F, 0x02, 0x92}, + {0x18, 0x02, 0x92}, + {0x1F, 0x02, 0x92}, + {0x29, 0x02, 0x92}, + {0x38, 0x03, 0x92}, }, /* 126 */ { - {0x01, 2, 154}, - {0x16, 3, 154}, - {0x01, 2, 156}, - {0x16, 3, 156}, - {0x01, 2, 160}, - {0x16, 3, 160}, - {0x01, 2, 163}, - {0x16, 3, 163}, - {0x01, 2, 164}, - {0x16, 3, 164}, - {0x01, 2, 169}, - {0x16, 3, 169}, - {0x01, 2, 170}, - {0x16, 3, 170}, - {0x01, 2, 173}, - {0x16, 3, 173}, + {0x01, 0x02, 0x9A}, + {0x16, 0x03, 0x9A}, + {0x01, 0x02, 0x9C}, + {0x16, 0x03, 0x9C}, + {0x01, 0x02, 0xA0}, + {0x16, 0x03, 0xA0}, + {0x01, 0x02, 0xA3}, + {0x16, 0x03, 0xA3}, + {0x01, 0x02, 0xA4}, + {0x16, 0x03, 0xA4}, + {0x01, 0x02, 0xA9}, + {0x16, 0x03, 0xA9}, + {0x01, 0x02, 0xAA}, + {0x16, 0x03, 0xAA}, + {0x01, 0x02, 0xAD}, + {0x16, 0x03, 0xAD}, }, /* 127 */ { - {0x02, 2, 154}, - {0x09, 2, 154}, - {0x17, 2, 154}, - {0x28, 3, 154}, - {0x02, 2, 156}, - {0x09, 2, 156}, - {0x17, 2, 156}, - {0x28, 3, 156}, - {0x02, 2, 160}, - {0x09, 2, 160}, - {0x17, 2, 160}, - {0x28, 3, 160}, - {0x02, 2, 163}, - {0x09, 2, 163}, - {0x17, 2, 163}, - {0x28, 3, 163}, + {0x02, 0x02, 0x9A}, + {0x09, 0x02, 0x9A}, + {0x17, 0x02, 0x9A}, + {0x28, 0x03, 0x9A}, + {0x02, 0x02, 0x9C}, + {0x09, 0x02, 0x9C}, + {0x17, 0x02, 0x9C}, + {0x28, 0x03, 0x9C}, + {0x02, 0x02, 0xA0}, + {0x09, 0x02, 0xA0}, + {0x17, 0x02, 0xA0}, + {0x28, 0x03, 0xA0}, + {0x02, 0x02, 0xA3}, + {0x09, 0x02, 0xA3}, + {0x17, 0x02, 0xA3}, + {0x28, 0x03, 0xA3}, }, /* 128 */ { - {0x03, 2, 154}, - {0x06, 2, 154}, - {0x0a, 2, 154}, - {0x0f, 2, 154}, - {0x18, 2, 154}, - {0x1f, 2, 154}, - {0x29, 2, 154}, - {0x38, 3, 154}, - {0x03, 2, 156}, - {0x06, 2, 156}, - {0x0a, 2, 156}, - {0x0f, 2, 156}, - {0x18, 2, 156}, - {0x1f, 2, 156}, - {0x29, 2, 156}, - {0x38, 3, 156}, + {0x03, 0x02, 0x9A}, + {0x06, 0x02, 0x9A}, + {0x0A, 0x02, 0x9A}, + {0x0F, 0x02, 0x9A}, + {0x18, 0x02, 0x9A}, + {0x1F, 0x02, 0x9A}, + {0x29, 0x02, 0x9A}, + {0x38, 0x03, 0x9A}, + {0x03, 0x02, 0x9C}, + {0x06, 0x02, 0x9C}, + {0x0A, 0x02, 0x9C}, + {0x0F, 0x02, 0x9C}, + {0x18, 0x02, 0x9C}, + {0x1F, 0x02, 0x9C}, + {0x29, 0x02, 0x9C}, + {0x38, 0x03, 0x9C}, }, /* 129 */ { - {0x03, 2, 160}, - {0x06, 2, 160}, - {0x0a, 2, 160}, - {0x0f, 2, 160}, - {0x18, 2, 160}, - {0x1f, 2, 160}, - {0x29, 2, 160}, - {0x38, 3, 160}, - {0x03, 2, 163}, - {0x06, 2, 163}, - {0x0a, 2, 163}, - {0x0f, 2, 163}, - {0x18, 2, 163}, - {0x1f, 2, 163}, - {0x29, 2, 163}, - {0x38, 3, 163}, + {0x03, 0x02, 0xA0}, + {0x06, 0x02, 0xA0}, + {0x0A, 0x02, 0xA0}, + {0x0F, 0x02, 0xA0}, + {0x18, 0x02, 0xA0}, + {0x1F, 0x02, 0xA0}, + {0x29, 0x02, 0xA0}, + {0x38, 0x03, 0xA0}, + {0x03, 0x02, 0xA3}, + {0x06, 0x02, 0xA3}, + {0x0A, 0x02, 0xA3}, + {0x0F, 0x02, 0xA3}, + {0x18, 0x02, 0xA3}, + {0x1F, 0x02, 0xA3}, + {0x29, 0x02, 0xA3}, + {0x38, 0x03, 0xA3}, }, /* 130 */ { - {0x02, 2, 164}, - {0x09, 2, 164}, - {0x17, 2, 164}, - {0x28, 3, 164}, - {0x02, 2, 169}, - {0x09, 2, 169}, - {0x17, 2, 169}, - {0x28, 3, 169}, - {0x02, 2, 170}, - {0x09, 2, 170}, - {0x17, 2, 170}, - {0x28, 3, 170}, - {0x02, 2, 173}, - {0x09, 2, 173}, - {0x17, 2, 173}, - {0x28, 3, 173}, + {0x02, 0x02, 0xA4}, + {0x09, 0x02, 0xA4}, + {0x17, 0x02, 0xA4}, + {0x28, 0x03, 0xA4}, + {0x02, 0x02, 0xA9}, + {0x09, 0x02, 0xA9}, + {0x17, 0x02, 0xA9}, + {0x28, 0x03, 0xA9}, + {0x02, 0x02, 0xAA}, + {0x09, 0x02, 0xAA}, + {0x17, 0x02, 0xAA}, + {0x28, 0x03, 0xAA}, + {0x02, 0x02, 0xAD}, + {0x09, 0x02, 0xAD}, + {0x17, 0x02, 0xAD}, + {0x28, 0x03, 0xAD}, }, /* 131 */ { - {0x03, 2, 164}, - {0x06, 2, 164}, - {0x0a, 2, 164}, - {0x0f, 2, 164}, - {0x18, 2, 164}, - {0x1f, 2, 164}, - {0x29, 2, 164}, - {0x38, 3, 164}, - {0x03, 2, 169}, - {0x06, 2, 169}, - {0x0a, 2, 169}, - {0x0f, 2, 169}, - {0x18, 2, 169}, - {0x1f, 2, 169}, - {0x29, 2, 169}, - {0x38, 3, 169}, + {0x03, 0x02, 0xA4}, + {0x06, 0x02, 0xA4}, + {0x0A, 0x02, 0xA4}, + {0x0F, 0x02, 0xA4}, + {0x18, 0x02, 0xA4}, + {0x1F, 0x02, 0xA4}, + {0x29, 0x02, 0xA4}, + {0x38, 0x03, 0xA4}, + {0x03, 0x02, 0xA9}, + {0x06, 0x02, 0xA9}, + {0x0A, 0x02, 0xA9}, + {0x0F, 0x02, 0xA9}, + {0x18, 0x02, 0xA9}, + {0x1F, 0x02, 0xA9}, + {0x29, 0x02, 0xA9}, + {0x38, 0x03, 0xA9}, }, /* 132 */ { - {0x03, 2, 170}, - {0x06, 2, 170}, - {0x0a, 2, 170}, - {0x0f, 2, 170}, - {0x18, 2, 170}, - {0x1f, 2, 170}, - {0x29, 2, 170}, - {0x38, 3, 170}, - {0x03, 2, 173}, - {0x06, 2, 173}, - {0x0a, 2, 173}, - {0x0f, 2, 173}, - {0x18, 2, 173}, - {0x1f, 2, 173}, - {0x29, 2, 173}, - {0x38, 3, 173}, + {0x03, 0x02, 0xAA}, + {0x06, 0x02, 0xAA}, + {0x0A, 0x02, 0xAA}, + {0x0F, 0x02, 0xAA}, + {0x18, 0x02, 0xAA}, + {0x1F, 0x02, 0xAA}, + {0x29, 0x02, 0xAA}, + {0x38, 0x03, 0xAA}, + {0x03, 0x02, 0xAD}, + {0x06, 0x02, 0xAD}, + {0x0A, 0x02, 0xAD}, + {0x0F, 0x02, 0xAD}, + {0x18, 0x02, 0xAD}, + {0x1F, 0x02, 0xAD}, + {0x29, 0x02, 0xAD}, + {0x38, 0x03, 0xAD}, }, /* 133 */ { - {0x89, 0, 0}, - {0x8a, 0, 0}, - {0x8c, 0, 0}, - {0x8d, 0, 0}, - {0x90, 0, 0}, - {0x91, 0, 0}, - {0x93, 0, 0}, - {0x96, 0, 0}, - {0x9c, 0, 0}, - {0x9f, 0, 0}, - {0xa3, 0, 0}, - {0xa6, 0, 0}, - {0xab, 0, 0}, - {0xae, 0, 0}, - {0xb5, 0, 0}, - {0xbe, 0, 0}, + {0x89, 0x00, 0x00}, + {0x8A, 0x00, 0x00}, + {0x8C, 0x00, 0x00}, + {0x8D, 0x00, 0x00}, + {0x90, 0x00, 0x00}, + {0x91, 0x00, 0x00}, + {0x93, 0x00, 0x00}, + {0x96, 0x00, 0x00}, + {0x9C, 0x00, 0x00}, + {0x9F, 0x00, 0x00}, + {0xA3, 0x00, 0x00}, + {0xA6, 0x00, 0x00}, + {0xAB, 0x00, 0x00}, + {0xAE, 0x00, 0x00}, + {0xB5, 0x00, 0x00}, + {0xBE, 0x00, 0x00}, }, /* 134 */ { - {0x00, 3, 178}, - {0x00, 3, 181}, - {0x00, 3, 185}, - {0x00, 3, 186}, - {0x00, 3, 187}, - {0x00, 3, 189}, - {0x00, 3, 190}, - {0x00, 3, 196}, - {0x00, 3, 198}, - {0x00, 3, 228}, - {0x00, 3, 232}, - {0x00, 3, 233}, - {0x94, 0, 0}, - {0x95, 0, 0}, - {0x97, 0, 0}, - {0x98, 0, 0}, + {0x00, 0x03, 0xB2}, + {0x00, 0x03, 0xB5}, + {0x00, 0x03, 0xB9}, + {0x00, 0x03, 0xBA}, + {0x00, 0x03, 0xBB}, + {0x00, 0x03, 0xBD}, + {0x00, 0x03, 0xBE}, + {0x00, 0x03, 0xC4}, + {0x00, 0x03, 0xC6}, + {0x00, 0x03, 0xE4}, + {0x00, 0x03, 0xE8}, + {0x00, 0x03, 0xE9}, + {0x94, 0x00, 0x00}, + {0x95, 0x00, 0x00}, + {0x97, 0x00, 0x00}, + {0x98, 0x00, 0x00}, }, /* 135 */ { - {0x01, 2, 178}, - {0x16, 3, 178}, - {0x01, 2, 181}, - {0x16, 3, 181}, - {0x01, 2, 185}, - {0x16, 3, 185}, - {0x01, 2, 186}, - {0x16, 3, 186}, - {0x01, 2, 187}, - {0x16, 3, 187}, - {0x01, 2, 189}, - {0x16, 3, 189}, - {0x01, 2, 190}, - {0x16, 3, 190}, - {0x01, 2, 196}, - {0x16, 3, 196}, + {0x01, 0x02, 0xB2}, + {0x16, 0x03, 0xB2}, + {0x01, 0x02, 0xB5}, + {0x16, 0x03, 0xB5}, + {0x01, 0x02, 0xB9}, + {0x16, 0x03, 0xB9}, + {0x01, 0x02, 0xBA}, + {0x16, 0x03, 0xBA}, + {0x01, 0x02, 0xBB}, + {0x16, 0x03, 0xBB}, + {0x01, 0x02, 0xBD}, + {0x16, 0x03, 0xBD}, + {0x01, 0x02, 0xBE}, + {0x16, 0x03, 0xBE}, + {0x01, 0x02, 0xC4}, + {0x16, 0x03, 0xC4}, }, /* 136 */ { - {0x02, 2, 178}, - {0x09, 2, 178}, - {0x17, 2, 178}, - {0x28, 3, 178}, - {0x02, 2, 181}, - {0x09, 2, 181}, - {0x17, 2, 181}, - {0x28, 3, 181}, - {0x02, 2, 185}, - {0x09, 2, 185}, - {0x17, 2, 185}, - {0x28, 3, 185}, - {0x02, 2, 186}, - {0x09, 2, 186}, - {0x17, 2, 186}, - {0x28, 3, 186}, + {0x02, 0x02, 0xB2}, + {0x09, 0x02, 0xB2}, + {0x17, 0x02, 0xB2}, + {0x28, 0x03, 0xB2}, + {0x02, 0x02, 0xB5}, + {0x09, 0x02, 0xB5}, + {0x17, 0x02, 0xB5}, + {0x28, 0x03, 0xB5}, + {0x02, 0x02, 0xB9}, + {0x09, 0x02, 0xB9}, + {0x17, 0x02, 0xB9}, + {0x28, 0x03, 0xB9}, + {0x02, 0x02, 0xBA}, + {0x09, 0x02, 0xBA}, + {0x17, 0x02, 0xBA}, + {0x28, 0x03, 0xBA}, }, /* 137 */ { - {0x03, 2, 178}, - {0x06, 2, 178}, - {0x0a, 2, 178}, - {0x0f, 2, 178}, - {0x18, 2, 178}, - {0x1f, 2, 178}, - {0x29, 2, 178}, - {0x38, 3, 178}, - {0x03, 2, 181}, - {0x06, 2, 181}, - {0x0a, 2, 181}, - {0x0f, 2, 181}, - {0x18, 2, 181}, - {0x1f, 2, 181}, - {0x29, 2, 181}, - {0x38, 3, 181}, + {0x03, 0x02, 0xB2}, + {0x06, 0x02, 0xB2}, + {0x0A, 0x02, 0xB2}, + {0x0F, 0x02, 0xB2}, + {0x18, 0x02, 0xB2}, + {0x1F, 0x02, 0xB2}, + {0x29, 0x02, 0xB2}, + {0x38, 0x03, 0xB2}, + {0x03, 0x02, 0xB5}, + {0x06, 0x02, 0xB5}, + {0x0A, 0x02, 0xB5}, + {0x0F, 0x02, 0xB5}, + {0x18, 0x02, 0xB5}, + {0x1F, 0x02, 0xB5}, + {0x29, 0x02, 0xB5}, + {0x38, 0x03, 0xB5}, }, /* 138 */ { - {0x03, 2, 185}, - {0x06, 2, 185}, - {0x0a, 2, 185}, - {0x0f, 2, 185}, - {0x18, 2, 185}, - {0x1f, 2, 185}, - {0x29, 2, 185}, - {0x38, 3, 185}, - {0x03, 2, 186}, - {0x06, 2, 186}, - {0x0a, 2, 186}, - {0x0f, 2, 186}, - {0x18, 2, 186}, - {0x1f, 2, 186}, - {0x29, 2, 186}, - {0x38, 3, 186}, + {0x03, 0x02, 0xB9}, + {0x06, 0x02, 0xB9}, + {0x0A, 0x02, 0xB9}, + {0x0F, 0x02, 0xB9}, + {0x18, 0x02, 0xB9}, + {0x1F, 0x02, 0xB9}, + {0x29, 0x02, 0xB9}, + {0x38, 0x03, 0xB9}, + {0x03, 0x02, 0xBA}, + {0x06, 0x02, 0xBA}, + {0x0A, 0x02, 0xBA}, + {0x0F, 0x02, 0xBA}, + {0x18, 0x02, 0xBA}, + {0x1F, 0x02, 0xBA}, + {0x29, 0x02, 0xBA}, + {0x38, 0x03, 0xBA}, }, /* 139 */ { - {0x02, 2, 187}, - {0x09, 2, 187}, - {0x17, 2, 187}, - {0x28, 3, 187}, - {0x02, 2, 189}, - {0x09, 2, 189}, - {0x17, 2, 189}, - {0x28, 3, 189}, - {0x02, 2, 190}, - {0x09, 2, 190}, - {0x17, 2, 190}, - {0x28, 3, 190}, - {0x02, 2, 196}, - {0x09, 2, 196}, - {0x17, 2, 196}, - {0x28, 3, 196}, + {0x02, 0x02, 0xBB}, + {0x09, 0x02, 0xBB}, + {0x17, 0x02, 0xBB}, + {0x28, 0x03, 0xBB}, + {0x02, 0x02, 0xBD}, + {0x09, 0x02, 0xBD}, + {0x17, 0x02, 0xBD}, + {0x28, 0x03, 0xBD}, + {0x02, 0x02, 0xBE}, + {0x09, 0x02, 0xBE}, + {0x17, 0x02, 0xBE}, + {0x28, 0x03, 0xBE}, + {0x02, 0x02, 0xC4}, + {0x09, 0x02, 0xC4}, + {0x17, 0x02, 0xC4}, + {0x28, 0x03, 0xC4}, }, /* 140 */ { - {0x03, 2, 187}, - {0x06, 2, 187}, - {0x0a, 2, 187}, - {0x0f, 2, 187}, - {0x18, 2, 187}, - {0x1f, 2, 187}, - {0x29, 2, 187}, - {0x38, 3, 187}, - {0x03, 2, 189}, - {0x06, 2, 189}, - {0x0a, 2, 189}, - {0x0f, 2, 189}, - {0x18, 2, 189}, - {0x1f, 2, 189}, - {0x29, 2, 189}, - {0x38, 3, 189}, + {0x03, 0x02, 0xBB}, + {0x06, 0x02, 0xBB}, + {0x0A, 0x02, 0xBB}, + {0x0F, 0x02, 0xBB}, + {0x18, 0x02, 0xBB}, + {0x1F, 0x02, 0xBB}, + {0x29, 0x02, 0xBB}, + {0x38, 0x03, 0xBB}, + {0x03, 0x02, 0xBD}, + {0x06, 0x02, 0xBD}, + {0x0A, 0x02, 0xBD}, + {0x0F, 0x02, 0xBD}, + {0x18, 0x02, 0xBD}, + {0x1F, 0x02, 0xBD}, + {0x29, 0x02, 0xBD}, + {0x38, 0x03, 0xBD}, }, /* 141 */ { - {0x03, 2, 190}, - {0x06, 2, 190}, - {0x0a, 2, 190}, - {0x0f, 2, 190}, - {0x18, 2, 190}, - {0x1f, 2, 190}, - {0x29, 2, 190}, - {0x38, 3, 190}, - {0x03, 2, 196}, - {0x06, 2, 196}, - {0x0a, 2, 196}, - {0x0f, 2, 196}, - {0x18, 2, 196}, - {0x1f, 2, 196}, - {0x29, 2, 196}, - {0x38, 3, 196}, + {0x03, 0x02, 0xBE}, + {0x06, 0x02, 0xBE}, + {0x0A, 0x02, 0xBE}, + {0x0F, 0x02, 0xBE}, + {0x18, 0x02, 0xBE}, + {0x1F, 0x02, 0xBE}, + {0x29, 0x02, 0xBE}, + {0x38, 0x03, 0xBE}, + {0x03, 0x02, 0xC4}, + {0x06, 0x02, 0xC4}, + {0x0A, 0x02, 0xC4}, + {0x0F, 0x02, 0xC4}, + {0x18, 0x02, 0xC4}, + {0x1F, 0x02, 0xC4}, + {0x29, 0x02, 0xC4}, + {0x38, 0x03, 0xC4}, }, /* 142 */ { - {0x01, 2, 198}, - {0x16, 3, 198}, - {0x01, 2, 228}, - {0x16, 3, 228}, - {0x01, 2, 232}, - {0x16, 3, 232}, - {0x01, 2, 233}, - {0x16, 3, 233}, - {0x00, 3, 1}, - {0x00, 3, 135}, - {0x00, 3, 137}, - {0x00, 3, 138}, - {0x00, 3, 139}, - {0x00, 3, 140}, - {0x00, 3, 141}, - {0x00, 3, 143}, + {0x01, 0x02, 0xC6}, + {0x16, 0x03, 0xC6}, + {0x01, 0x02, 0xE4}, + {0x16, 0x03, 0xE4}, + {0x01, 0x02, 0xE8}, + {0x16, 0x03, 0xE8}, + {0x01, 0x02, 0xE9}, + {0x16, 0x03, 0xE9}, + {0x00, 0x03, 0x01}, + {0x00, 0x03, 0x87}, + {0x00, 0x03, 0x89}, + {0x00, 0x03, 0x8A}, + {0x00, 0x03, 0x8B}, + {0x00, 0x03, 0x8C}, + {0x00, 0x03, 0x8D}, + {0x00, 0x03, 0x8F}, }, /* 143 */ { - {0x02, 2, 198}, - {0x09, 2, 198}, - {0x17, 2, 198}, - {0x28, 3, 198}, - {0x02, 2, 228}, - {0x09, 2, 228}, - {0x17, 2, 228}, - {0x28, 3, 228}, - {0x02, 2, 232}, - {0x09, 2, 232}, - {0x17, 2, 232}, - {0x28, 3, 232}, - {0x02, 2, 233}, - {0x09, 2, 233}, - {0x17, 2, 233}, - {0x28, 3, 233}, + {0x02, 0x02, 0xC6}, + {0x09, 0x02, 0xC6}, + {0x17, 0x02, 0xC6}, + {0x28, 0x03, 0xC6}, + {0x02, 0x02, 0xE4}, + {0x09, 0x02, 0xE4}, + {0x17, 0x02, 0xE4}, + {0x28, 0x03, 0xE4}, + {0x02, 0x02, 0xE8}, + {0x09, 0x02, 0xE8}, + {0x17, 0x02, 0xE8}, + {0x28, 0x03, 0xE8}, + {0x02, 0x02, 0xE9}, + {0x09, 0x02, 0xE9}, + {0x17, 0x02, 0xE9}, + {0x28, 0x03, 0xE9}, }, /* 144 */ { - {0x03, 2, 198}, - {0x06, 2, 198}, - {0x0a, 2, 198}, - {0x0f, 2, 198}, - {0x18, 2, 198}, - {0x1f, 2, 198}, - {0x29, 2, 198}, - {0x38, 3, 198}, - {0x03, 2, 228}, - {0x06, 2, 228}, - {0x0a, 2, 228}, - {0x0f, 2, 228}, - {0x18, 2, 228}, - {0x1f, 2, 228}, - {0x29, 2, 228}, - {0x38, 3, 228}, + {0x03, 0x02, 0xC6}, + {0x06, 0x02, 0xC6}, + {0x0A, 0x02, 0xC6}, + {0x0F, 0x02, 0xC6}, + {0x18, 0x02, 0xC6}, + {0x1F, 0x02, 0xC6}, + {0x29, 0x02, 0xC6}, + {0x38, 0x03, 0xC6}, + {0x03, 0x02, 0xE4}, + {0x06, 0x02, 0xE4}, + {0x0A, 0x02, 0xE4}, + {0x0F, 0x02, 0xE4}, + {0x18, 0x02, 0xE4}, + {0x1F, 0x02, 0xE4}, + {0x29, 0x02, 0xE4}, + {0x38, 0x03, 0xE4}, }, /* 145 */ { - {0x03, 2, 232}, - {0x06, 2, 232}, - {0x0a, 2, 232}, - {0x0f, 2, 232}, - {0x18, 2, 232}, - {0x1f, 2, 232}, - {0x29, 2, 232}, - {0x38, 3, 232}, - {0x03, 2, 233}, - {0x06, 2, 233}, - {0x0a, 2, 233}, - {0x0f, 2, 233}, - {0x18, 2, 233}, - {0x1f, 2, 233}, - {0x29, 2, 233}, - {0x38, 3, 233}, + {0x03, 0x02, 0xE8}, + {0x06, 0x02, 0xE8}, + {0x0A, 0x02, 0xE8}, + {0x0F, 0x02, 0xE8}, + {0x18, 0x02, 0xE8}, + {0x1F, 0x02, 0xE8}, + {0x29, 0x02, 0xE8}, + {0x38, 0x03, 0xE8}, + {0x03, 0x02, 0xE9}, + {0x06, 0x02, 0xE9}, + {0x0A, 0x02, 0xE9}, + {0x0F, 0x02, 0xE9}, + {0x18, 0x02, 0xE9}, + {0x1F, 0x02, 0xE9}, + {0x29, 0x02, 0xE9}, + {0x38, 0x03, 0xE9}, }, /* 146 */ { - {0x01, 2, 1}, - {0x16, 3, 1}, - {0x01, 2, 135}, - {0x16, 3, 135}, - {0x01, 2, 137}, - {0x16, 3, 137}, - {0x01, 2, 138}, - {0x16, 3, 138}, - {0x01, 2, 139}, - {0x16, 3, 139}, - {0x01, 2, 140}, - {0x16, 3, 140}, - {0x01, 2, 141}, - {0x16, 3, 141}, - {0x01, 2, 143}, - {0x16, 3, 143}, + {0x01, 0x02, 0x01}, + {0x16, 0x03, 0x01}, + {0x01, 0x02, 0x87}, + {0x16, 0x03, 0x87}, + {0x01, 0x02, 0x89}, + {0x16, 0x03, 0x89}, + {0x01, 0x02, 0x8A}, + {0x16, 0x03, 0x8A}, + {0x01, 0x02, 0x8B}, + {0x16, 0x03, 0x8B}, + {0x01, 0x02, 0x8C}, + {0x16, 0x03, 0x8C}, + {0x01, 0x02, 0x8D}, + {0x16, 0x03, 0x8D}, + {0x01, 0x02, 0x8F}, + {0x16, 0x03, 0x8F}, }, /* 147 */ { - {0x02, 2, 1}, - {0x09, 2, 1}, - {0x17, 2, 1}, - {0x28, 3, 1}, - {0x02, 2, 135}, - {0x09, 2, 135}, - {0x17, 2, 135}, - {0x28, 3, 135}, - {0x02, 2, 137}, - {0x09, 2, 137}, - {0x17, 2, 137}, - {0x28, 3, 137}, - {0x02, 2, 138}, - {0x09, 2, 138}, - {0x17, 2, 138}, - {0x28, 3, 138}, + {0x02, 0x02, 0x01}, + {0x09, 0x02, 0x01}, + {0x17, 0x02, 0x01}, + {0x28, 0x03, 0x01}, + {0x02, 0x02, 0x87}, + {0x09, 0x02, 0x87}, + {0x17, 0x02, 0x87}, + {0x28, 0x03, 0x87}, + {0x02, 0x02, 0x89}, + {0x09, 0x02, 0x89}, + {0x17, 0x02, 0x89}, + {0x28, 0x03, 0x89}, + {0x02, 0x02, 0x8A}, + {0x09, 0x02, 0x8A}, + {0x17, 0x02, 0x8A}, + {0x28, 0x03, 0x8A}, }, /* 148 */ { - {0x03, 2, 1}, - {0x06, 2, 1}, - {0x0a, 2, 1}, - {0x0f, 2, 1}, - {0x18, 2, 1}, - {0x1f, 2, 1}, - {0x29, 2, 1}, - {0x38, 3, 1}, - {0x03, 2, 135}, - {0x06, 2, 135}, - {0x0a, 2, 135}, - {0x0f, 2, 135}, - {0x18, 2, 135}, - {0x1f, 2, 135}, - {0x29, 2, 135}, - {0x38, 3, 135}, + {0x03, 0x02, 0x01}, + {0x06, 0x02, 0x01}, + {0x0A, 0x02, 0x01}, + {0x0F, 0x02, 0x01}, + {0x18, 0x02, 0x01}, + {0x1F, 0x02, 0x01}, + {0x29, 0x02, 0x01}, + {0x38, 0x03, 0x01}, + {0x03, 0x02, 0x87}, + {0x06, 0x02, 0x87}, + {0x0A, 0x02, 0x87}, + {0x0F, 0x02, 0x87}, + {0x18, 0x02, 0x87}, + {0x1F, 0x02, 0x87}, + {0x29, 0x02, 0x87}, + {0x38, 0x03, 0x87}, }, /* 149 */ { - {0x03, 2, 137}, - {0x06, 2, 137}, - {0x0a, 2, 137}, - {0x0f, 2, 137}, - {0x18, 2, 137}, - {0x1f, 2, 137}, - {0x29, 2, 137}, - {0x38, 3, 137}, - {0x03, 2, 138}, - {0x06, 2, 138}, - {0x0a, 2, 138}, - {0x0f, 2, 138}, - {0x18, 2, 138}, - {0x1f, 2, 138}, - {0x29, 2, 138}, - {0x38, 3, 138}, + {0x03, 0x02, 0x89}, + {0x06, 0x02, 0x89}, + {0x0A, 0x02, 0x89}, + {0x0F, 0x02, 0x89}, + {0x18, 0x02, 0x89}, + {0x1F, 0x02, 0x89}, + {0x29, 0x02, 0x89}, + {0x38, 0x03, 0x89}, + {0x03, 0x02, 0x8A}, + {0x06, 0x02, 0x8A}, + {0x0A, 0x02, 0x8A}, + {0x0F, 0x02, 0x8A}, + {0x18, 0x02, 0x8A}, + {0x1F, 0x02, 0x8A}, + {0x29, 0x02, 0x8A}, + {0x38, 0x03, 0x8A}, }, /* 150 */ { - {0x02, 2, 139}, - {0x09, 2, 139}, - {0x17, 2, 139}, - {0x28, 3, 139}, - {0x02, 2, 140}, - {0x09, 2, 140}, - {0x17, 2, 140}, - {0x28, 3, 140}, - {0x02, 2, 141}, - {0x09, 2, 141}, - {0x17, 2, 141}, - {0x28, 3, 141}, - {0x02, 2, 143}, - {0x09, 2, 143}, - {0x17, 2, 143}, - {0x28, 3, 143}, + {0x02, 0x02, 0x8B}, + {0x09, 0x02, 0x8B}, + {0x17, 0x02, 0x8B}, + {0x28, 0x03, 0x8B}, + {0x02, 0x02, 0x8C}, + {0x09, 0x02, 0x8C}, + {0x17, 0x02, 0x8C}, + {0x28, 0x03, 0x8C}, + {0x02, 0x02, 0x8D}, + {0x09, 0x02, 0x8D}, + {0x17, 0x02, 0x8D}, + {0x28, 0x03, 0x8D}, + {0x02, 0x02, 0x8F}, + {0x09, 0x02, 0x8F}, + {0x17, 0x02, 0x8F}, + {0x28, 0x03, 0x8F}, }, /* 151 */ { - {0x03, 2, 139}, - {0x06, 2, 139}, - {0x0a, 2, 139}, - {0x0f, 2, 139}, - {0x18, 2, 139}, - {0x1f, 2, 139}, - {0x29, 2, 139}, - {0x38, 3, 139}, - {0x03, 2, 140}, - {0x06, 2, 140}, - {0x0a, 2, 140}, - {0x0f, 2, 140}, - {0x18, 2, 140}, - {0x1f, 2, 140}, - {0x29, 2, 140}, - {0x38, 3, 140}, + {0x03, 0x02, 0x8B}, + {0x06, 0x02, 0x8B}, + {0x0A, 0x02, 0x8B}, + {0x0F, 0x02, 0x8B}, + {0x18, 0x02, 0x8B}, + {0x1F, 0x02, 0x8B}, + {0x29, 0x02, 0x8B}, + {0x38, 0x03, 0x8B}, + {0x03, 0x02, 0x8C}, + {0x06, 0x02, 0x8C}, + {0x0A, 0x02, 0x8C}, + {0x0F, 0x02, 0x8C}, + {0x18, 0x02, 0x8C}, + {0x1F, 0x02, 0x8C}, + {0x29, 0x02, 0x8C}, + {0x38, 0x03, 0x8C}, }, /* 152 */ { - {0x03, 2, 141}, - {0x06, 2, 141}, - {0x0a, 2, 141}, - {0x0f, 2, 141}, - {0x18, 2, 141}, - {0x1f, 2, 141}, - {0x29, 2, 141}, - {0x38, 3, 141}, - {0x03, 2, 143}, - {0x06, 2, 143}, - {0x0a, 2, 143}, - {0x0f, 2, 143}, - {0x18, 2, 143}, - {0x1f, 2, 143}, - {0x29, 2, 143}, - {0x38, 3, 143}, + {0x03, 0x02, 0x8D}, + {0x06, 0x02, 0x8D}, + {0x0A, 0x02, 0x8D}, + {0x0F, 0x02, 0x8D}, + {0x18, 0x02, 0x8D}, + {0x1F, 0x02, 0x8D}, + {0x29, 0x02, 0x8D}, + {0x38, 0x03, 0x8D}, + {0x03, 0x02, 0x8F}, + {0x06, 0x02, 0x8F}, + {0x0A, 0x02, 0x8F}, + {0x0F, 0x02, 0x8F}, + {0x18, 0x02, 0x8F}, + {0x1F, 0x02, 0x8F}, + {0x29, 0x02, 0x8F}, + {0x38, 0x03, 0x8F}, }, /* 153 */ { - {0x9d, 0, 0}, - {0x9e, 0, 0}, - {0xa0, 0, 0}, - {0xa1, 0, 0}, - {0xa4, 0, 0}, - {0xa5, 0, 0}, - {0xa7, 0, 0}, - {0xa8, 0, 0}, - {0xac, 0, 0}, - {0xad, 0, 0}, - {0xaf, 0, 0}, - {0xb1, 0, 0}, - {0xb6, 0, 0}, - {0xb9, 0, 0}, - {0xbf, 0, 0}, - {0xcf, 0, 0}, + {0x9D, 0x00, 0x00}, + {0x9E, 0x00, 0x00}, + {0xA0, 0x00, 0x00}, + {0xA1, 0x00, 0x00}, + {0xA4, 0x00, 0x00}, + {0xA5, 0x00, 0x00}, + {0xA7, 0x00, 0x00}, + {0xA8, 0x00, 0x00}, + {0xAC, 0x00, 0x00}, + {0xAD, 0x00, 0x00}, + {0xAF, 0x00, 0x00}, + {0xB1, 0x00, 0x00}, + {0xB6, 0x00, 0x00}, + {0xB9, 0x00, 0x00}, + {0xBF, 0x00, 0x00}, + {0xCF, 0x00, 0x00}, }, /* 154 */ { - {0x00, 3, 147}, - {0x00, 3, 149}, - {0x00, 3, 150}, - {0x00, 3, 151}, - {0x00, 3, 152}, - {0x00, 3, 155}, - {0x00, 3, 157}, - {0x00, 3, 158}, - {0x00, 3, 165}, - {0x00, 3, 166}, - {0x00, 3, 168}, - {0x00, 3, 174}, - {0x00, 3, 175}, - {0x00, 3, 180}, - {0x00, 3, 182}, - {0x00, 3, 183}, + {0x00, 0x03, 0x93}, + {0x00, 0x03, 0x95}, + {0x00, 0x03, 0x96}, + {0x00, 0x03, 0x97}, + {0x00, 0x03, 0x98}, + {0x00, 0x03, 0x9B}, + {0x00, 0x03, 0x9D}, + {0x00, 0x03, 0x9E}, + {0x00, 0x03, 0xA5}, + {0x00, 0x03, 0xA6}, + {0x00, 0x03, 0xA8}, + {0x00, 0x03, 0xAE}, + {0x00, 0x03, 0xAF}, + {0x00, 0x03, 0xB4}, + {0x00, 0x03, 0xB6}, + {0x00, 0x03, 0xB7}, }, /* 155 */ { - {0x01, 2, 147}, - {0x16, 3, 147}, - {0x01, 2, 149}, - {0x16, 3, 149}, - {0x01, 2, 150}, - {0x16, 3, 150}, - {0x01, 2, 151}, - {0x16, 3, 151}, - {0x01, 2, 152}, - {0x16, 3, 152}, - {0x01, 2, 155}, - {0x16, 3, 155}, - {0x01, 2, 157}, - {0x16, 3, 157}, - {0x01, 2, 158}, - {0x16, 3, 158}, + {0x01, 0x02, 0x93}, + {0x16, 0x03, 0x93}, + {0x01, 0x02, 0x95}, + {0x16, 0x03, 0x95}, + {0x01, 0x02, 0x96}, + {0x16, 0x03, 0x96}, + {0x01, 0x02, 0x97}, + {0x16, 0x03, 0x97}, + {0x01, 0x02, 0x98}, + {0x16, 0x03, 0x98}, + {0x01, 0x02, 0x9B}, + {0x16, 0x03, 0x9B}, + {0x01, 0x02, 0x9D}, + {0x16, 0x03, 0x9D}, + {0x01, 0x02, 0x9E}, + {0x16, 0x03, 0x9E}, }, /* 156 */ { - {0x02, 2, 147}, - {0x09, 2, 147}, - {0x17, 2, 147}, - {0x28, 3, 147}, - {0x02, 2, 149}, - {0x09, 2, 149}, - {0x17, 2, 149}, - {0x28, 3, 149}, - {0x02, 2, 150}, - {0x09, 2, 150}, - {0x17, 2, 150}, - {0x28, 3, 150}, - {0x02, 2, 151}, - {0x09, 2, 151}, - {0x17, 2, 151}, - {0x28, 3, 151}, + {0x02, 0x02, 0x93}, + {0x09, 0x02, 0x93}, + {0x17, 0x02, 0x93}, + {0x28, 0x03, 0x93}, + {0x02, 0x02, 0x95}, + {0x09, 0x02, 0x95}, + {0x17, 0x02, 0x95}, + {0x28, 0x03, 0x95}, + {0x02, 0x02, 0x96}, + {0x09, 0x02, 0x96}, + {0x17, 0x02, 0x96}, + {0x28, 0x03, 0x96}, + {0x02, 0x02, 0x97}, + {0x09, 0x02, 0x97}, + {0x17, 0x02, 0x97}, + {0x28, 0x03, 0x97}, }, /* 157 */ { - {0x03, 2, 147}, - {0x06, 2, 147}, - {0x0a, 2, 147}, - {0x0f, 2, 147}, - {0x18, 2, 147}, - {0x1f, 2, 147}, - {0x29, 2, 147}, - {0x38, 3, 147}, - {0x03, 2, 149}, - {0x06, 2, 149}, - {0x0a, 2, 149}, - {0x0f, 2, 149}, - {0x18, 2, 149}, - {0x1f, 2, 149}, - {0x29, 2, 149}, - {0x38, 3, 149}, + {0x03, 0x02, 0x93}, + {0x06, 0x02, 0x93}, + {0x0A, 0x02, 0x93}, + {0x0F, 0x02, 0x93}, + {0x18, 0x02, 0x93}, + {0x1F, 0x02, 0x93}, + {0x29, 0x02, 0x93}, + {0x38, 0x03, 0x93}, + {0x03, 0x02, 0x95}, + {0x06, 0x02, 0x95}, + {0x0A, 0x02, 0x95}, + {0x0F, 0x02, 0x95}, + {0x18, 0x02, 0x95}, + {0x1F, 0x02, 0x95}, + {0x29, 0x02, 0x95}, + {0x38, 0x03, 0x95}, }, /* 158 */ { - {0x03, 2, 150}, - {0x06, 2, 150}, - {0x0a, 2, 150}, - {0x0f, 2, 150}, - {0x18, 2, 150}, - {0x1f, 2, 150}, - {0x29, 2, 150}, - {0x38, 3, 150}, - {0x03, 2, 151}, - {0x06, 2, 151}, - {0x0a, 2, 151}, - {0x0f, 2, 151}, - {0x18, 2, 151}, - {0x1f, 2, 151}, - {0x29, 2, 151}, - {0x38, 3, 151}, + {0x03, 0x02, 0x96}, + {0x06, 0x02, 0x96}, + {0x0A, 0x02, 0x96}, + {0x0F, 0x02, 0x96}, + {0x18, 0x02, 0x96}, + {0x1F, 0x02, 0x96}, + {0x29, 0x02, 0x96}, + {0x38, 0x03, 0x96}, + {0x03, 0x02, 0x97}, + {0x06, 0x02, 0x97}, + {0x0A, 0x02, 0x97}, + {0x0F, 0x02, 0x97}, + {0x18, 0x02, 0x97}, + {0x1F, 0x02, 0x97}, + {0x29, 0x02, 0x97}, + {0x38, 0x03, 0x97}, }, /* 159 */ { - {0x02, 2, 152}, - {0x09, 2, 152}, - {0x17, 2, 152}, - {0x28, 3, 152}, - {0x02, 2, 155}, - {0x09, 2, 155}, - {0x17, 2, 155}, - {0x28, 3, 155}, - {0x02, 2, 157}, - {0x09, 2, 157}, - {0x17, 2, 157}, - {0x28, 3, 157}, - {0x02, 2, 158}, - {0x09, 2, 158}, - {0x17, 2, 158}, - {0x28, 3, 158}, + {0x02, 0x02, 0x98}, + {0x09, 0x02, 0x98}, + {0x17, 0x02, 0x98}, + {0x28, 0x03, 0x98}, + {0x02, 0x02, 0x9B}, + {0x09, 0x02, 0x9B}, + {0x17, 0x02, 0x9B}, + {0x28, 0x03, 0x9B}, + {0x02, 0x02, 0x9D}, + {0x09, 0x02, 0x9D}, + {0x17, 0x02, 0x9D}, + {0x28, 0x03, 0x9D}, + {0x02, 0x02, 0x9E}, + {0x09, 0x02, 0x9E}, + {0x17, 0x02, 0x9E}, + {0x28, 0x03, 0x9E}, }, /* 160 */ { - {0x03, 2, 152}, - {0x06, 2, 152}, - {0x0a, 2, 152}, - {0x0f, 2, 152}, - {0x18, 2, 152}, - {0x1f, 2, 152}, - {0x29, 2, 152}, - {0x38, 3, 152}, - {0x03, 2, 155}, - {0x06, 2, 155}, - {0x0a, 2, 155}, - {0x0f, 2, 155}, - {0x18, 2, 155}, - {0x1f, 2, 155}, - {0x29, 2, 155}, - {0x38, 3, 155}, + {0x03, 0x02, 0x98}, + {0x06, 0x02, 0x98}, + {0x0A, 0x02, 0x98}, + {0x0F, 0x02, 0x98}, + {0x18, 0x02, 0x98}, + {0x1F, 0x02, 0x98}, + {0x29, 0x02, 0x98}, + {0x38, 0x03, 0x98}, + {0x03, 0x02, 0x9B}, + {0x06, 0x02, 0x9B}, + {0x0A, 0x02, 0x9B}, + {0x0F, 0x02, 0x9B}, + {0x18, 0x02, 0x9B}, + {0x1F, 0x02, 0x9B}, + {0x29, 0x02, 0x9B}, + {0x38, 0x03, 0x9B}, }, /* 161 */ { - {0x03, 2, 157}, - {0x06, 2, 157}, - {0x0a, 2, 157}, - {0x0f, 2, 157}, - {0x18, 2, 157}, - {0x1f, 2, 157}, - {0x29, 2, 157}, - {0x38, 3, 157}, - {0x03, 2, 158}, - {0x06, 2, 158}, - {0x0a, 2, 158}, - {0x0f, 2, 158}, - {0x18, 2, 158}, - {0x1f, 2, 158}, - {0x29, 2, 158}, - {0x38, 3, 158}, + {0x03, 0x02, 0x9D}, + {0x06, 0x02, 0x9D}, + {0x0A, 0x02, 0x9D}, + {0x0F, 0x02, 0x9D}, + {0x18, 0x02, 0x9D}, + {0x1F, 0x02, 0x9D}, + {0x29, 0x02, 0x9D}, + {0x38, 0x03, 0x9D}, + {0x03, 0x02, 0x9E}, + {0x06, 0x02, 0x9E}, + {0x0A, 0x02, 0x9E}, + {0x0F, 0x02, 0x9E}, + {0x18, 0x02, 0x9E}, + {0x1F, 0x02, 0x9E}, + {0x29, 0x02, 0x9E}, + {0x38, 0x03, 0x9E}, }, /* 162 */ { - {0x01, 2, 165}, - {0x16, 3, 165}, - {0x01, 2, 166}, - {0x16, 3, 166}, - {0x01, 2, 168}, - {0x16, 3, 168}, - {0x01, 2, 174}, - {0x16, 3, 174}, - {0x01, 2, 175}, - {0x16, 3, 175}, - {0x01, 2, 180}, - {0x16, 3, 180}, - {0x01, 2, 182}, - {0x16, 3, 182}, - {0x01, 2, 183}, - {0x16, 3, 183}, + {0x01, 0x02, 0xA5}, + {0x16, 0x03, 0xA5}, + {0x01, 0x02, 0xA6}, + {0x16, 0x03, 0xA6}, + {0x01, 0x02, 0xA8}, + {0x16, 0x03, 0xA8}, + {0x01, 0x02, 0xAE}, + {0x16, 0x03, 0xAE}, + {0x01, 0x02, 0xAF}, + {0x16, 0x03, 0xAF}, + {0x01, 0x02, 0xB4}, + {0x16, 0x03, 0xB4}, + {0x01, 0x02, 0xB6}, + {0x16, 0x03, 0xB6}, + {0x01, 0x02, 0xB7}, + {0x16, 0x03, 0xB7}, }, /* 163 */ { - {0x02, 2, 165}, - {0x09, 2, 165}, - {0x17, 2, 165}, - {0x28, 3, 165}, - {0x02, 2, 166}, - {0x09, 2, 166}, - {0x17, 2, 166}, - {0x28, 3, 166}, - {0x02, 2, 168}, - {0x09, 2, 168}, - {0x17, 2, 168}, - {0x28, 3, 168}, - {0x02, 2, 174}, - {0x09, 2, 174}, - {0x17, 2, 174}, - {0x28, 3, 174}, + {0x02, 0x02, 0xA5}, + {0x09, 0x02, 0xA5}, + {0x17, 0x02, 0xA5}, + {0x28, 0x03, 0xA5}, + {0x02, 0x02, 0xA6}, + {0x09, 0x02, 0xA6}, + {0x17, 0x02, 0xA6}, + {0x28, 0x03, 0xA6}, + {0x02, 0x02, 0xA8}, + {0x09, 0x02, 0xA8}, + {0x17, 0x02, 0xA8}, + {0x28, 0x03, 0xA8}, + {0x02, 0x02, 0xAE}, + {0x09, 0x02, 0xAE}, + {0x17, 0x02, 0xAE}, + {0x28, 0x03, 0xAE}, }, /* 164 */ { - {0x03, 2, 165}, - {0x06, 2, 165}, - {0x0a, 2, 165}, - {0x0f, 2, 165}, - {0x18, 2, 165}, - {0x1f, 2, 165}, - {0x29, 2, 165}, - {0x38, 3, 165}, - {0x03, 2, 166}, - {0x06, 2, 166}, - {0x0a, 2, 166}, - {0x0f, 2, 166}, - {0x18, 2, 166}, - {0x1f, 2, 166}, - {0x29, 2, 166}, - {0x38, 3, 166}, + {0x03, 0x02, 0xA5}, + {0x06, 0x02, 0xA5}, + {0x0A, 0x02, 0xA5}, + {0x0F, 0x02, 0xA5}, + {0x18, 0x02, 0xA5}, + {0x1F, 0x02, 0xA5}, + {0x29, 0x02, 0xA5}, + {0x38, 0x03, 0xA5}, + {0x03, 0x02, 0xA6}, + {0x06, 0x02, 0xA6}, + {0x0A, 0x02, 0xA6}, + {0x0F, 0x02, 0xA6}, + {0x18, 0x02, 0xA6}, + {0x1F, 0x02, 0xA6}, + {0x29, 0x02, 0xA6}, + {0x38, 0x03, 0xA6}, }, /* 165 */ { - {0x03, 2, 168}, - {0x06, 2, 168}, - {0x0a, 2, 168}, - {0x0f, 2, 168}, - {0x18, 2, 168}, - {0x1f, 2, 168}, - {0x29, 2, 168}, - {0x38, 3, 168}, - {0x03, 2, 174}, - {0x06, 2, 174}, - {0x0a, 2, 174}, - {0x0f, 2, 174}, - {0x18, 2, 174}, - {0x1f, 2, 174}, - {0x29, 2, 174}, - {0x38, 3, 174}, + {0x03, 0x02, 0xA8}, + {0x06, 0x02, 0xA8}, + {0x0A, 0x02, 0xA8}, + {0x0F, 0x02, 0xA8}, + {0x18, 0x02, 0xA8}, + {0x1F, 0x02, 0xA8}, + {0x29, 0x02, 0xA8}, + {0x38, 0x03, 0xA8}, + {0x03, 0x02, 0xAE}, + {0x06, 0x02, 0xAE}, + {0x0A, 0x02, 0xAE}, + {0x0F, 0x02, 0xAE}, + {0x18, 0x02, 0xAE}, + {0x1F, 0x02, 0xAE}, + {0x29, 0x02, 0xAE}, + {0x38, 0x03, 0xAE}, }, /* 166 */ { - {0x02, 2, 175}, - {0x09, 2, 175}, - {0x17, 2, 175}, - {0x28, 3, 175}, - {0x02, 2, 180}, - {0x09, 2, 180}, - {0x17, 2, 180}, - {0x28, 3, 180}, - {0x02, 2, 182}, - {0x09, 2, 182}, - {0x17, 2, 182}, - {0x28, 3, 182}, - {0x02, 2, 183}, - {0x09, 2, 183}, - {0x17, 2, 183}, - {0x28, 3, 183}, + {0x02, 0x02, 0xAF}, + {0x09, 0x02, 0xAF}, + {0x17, 0x02, 0xAF}, + {0x28, 0x03, 0xAF}, + {0x02, 0x02, 0xB4}, + {0x09, 0x02, 0xB4}, + {0x17, 0x02, 0xB4}, + {0x28, 0x03, 0xB4}, + {0x02, 0x02, 0xB6}, + {0x09, 0x02, 0xB6}, + {0x17, 0x02, 0xB6}, + {0x28, 0x03, 0xB6}, + {0x02, 0x02, 0xB7}, + {0x09, 0x02, 0xB7}, + {0x17, 0x02, 0xB7}, + {0x28, 0x03, 0xB7}, }, /* 167 */ { - {0x03, 2, 175}, - {0x06, 2, 175}, - {0x0a, 2, 175}, - {0x0f, 2, 175}, - {0x18, 2, 175}, - {0x1f, 2, 175}, - {0x29, 2, 175}, - {0x38, 3, 175}, - {0x03, 2, 180}, - {0x06, 2, 180}, - {0x0a, 2, 180}, - {0x0f, 2, 180}, - {0x18, 2, 180}, - {0x1f, 2, 180}, - {0x29, 2, 180}, - {0x38, 3, 180}, + {0x03, 0x02, 0xAF}, + {0x06, 0x02, 0xAF}, + {0x0A, 0x02, 0xAF}, + {0x0F, 0x02, 0xAF}, + {0x18, 0x02, 0xAF}, + {0x1F, 0x02, 0xAF}, + {0x29, 0x02, 0xAF}, + {0x38, 0x03, 0xAF}, + {0x03, 0x02, 0xB4}, + {0x06, 0x02, 0xB4}, + {0x0A, 0x02, 0xB4}, + {0x0F, 0x02, 0xB4}, + {0x18, 0x02, 0xB4}, + {0x1F, 0x02, 0xB4}, + {0x29, 0x02, 0xB4}, + {0x38, 0x03, 0xB4}, }, /* 168 */ { - {0x03, 2, 182}, - {0x06, 2, 182}, - {0x0a, 2, 182}, - {0x0f, 2, 182}, - {0x18, 2, 182}, - {0x1f, 2, 182}, - {0x29, 2, 182}, - {0x38, 3, 182}, - {0x03, 2, 183}, - {0x06, 2, 183}, - {0x0a, 2, 183}, - {0x0f, 2, 183}, - {0x18, 2, 183}, - {0x1f, 2, 183}, - {0x29, 2, 183}, - {0x38, 3, 183}, + {0x03, 0x02, 0xB6}, + {0x06, 0x02, 0xB6}, + {0x0A, 0x02, 0xB6}, + {0x0F, 0x02, 0xB6}, + {0x18, 0x02, 0xB6}, + {0x1F, 0x02, 0xB6}, + {0x29, 0x02, 0xB6}, + {0x38, 0x03, 0xB6}, + {0x03, 0x02, 0xB7}, + {0x06, 0x02, 0xB7}, + {0x0A, 0x02, 0xB7}, + {0x0F, 0x02, 0xB7}, + {0x18, 0x02, 0xB7}, + {0x1F, 0x02, 0xB7}, + {0x29, 0x02, 0xB7}, + {0x38, 0x03, 0xB7}, }, /* 169 */ { - {0x00, 3, 188}, - {0x00, 3, 191}, - {0x00, 3, 197}, - {0x00, 3, 231}, - {0x00, 3, 239}, - {0xb0, 0, 0}, - {0xb2, 0, 0}, - {0xb3, 0, 0}, - {0xb7, 0, 0}, - {0xb8, 0, 0}, - {0xba, 0, 0}, - {0xbb, 0, 0}, - {0xc0, 0, 0}, - {0xc7, 0, 0}, - {0xd0, 0, 0}, - {0xdf, 0, 0}, + {0x00, 0x03, 0xBC}, + {0x00, 0x03, 0xBF}, + {0x00, 0x03, 0xC5}, + {0x00, 0x03, 0xE7}, + {0x00, 0x03, 0xEF}, + {0xB0, 0x00, 0x00}, + {0xB2, 0x00, 0x00}, + {0xB3, 0x00, 0x00}, + {0xB7, 0x00, 0x00}, + {0xB8, 0x00, 0x00}, + {0xBA, 0x00, 0x00}, + {0xBB, 0x00, 0x00}, + {0xC0, 0x00, 0x00}, + {0xC7, 0x00, 0x00}, + {0xD0, 0x00, 0x00}, + {0xDF, 0x00, 0x00}, }, /* 170 */ { - {0x01, 2, 188}, - {0x16, 3, 188}, - {0x01, 2, 191}, - {0x16, 3, 191}, - {0x01, 2, 197}, - {0x16, 3, 197}, - {0x01, 2, 231}, - {0x16, 3, 231}, - {0x01, 2, 239}, - {0x16, 3, 239}, - {0x00, 3, 9}, - {0x00, 3, 142}, - {0x00, 3, 144}, - {0x00, 3, 145}, - {0x00, 3, 148}, - {0x00, 3, 159}, + {0x01, 0x02, 0xBC}, + {0x16, 0x03, 0xBC}, + {0x01, 0x02, 0xBF}, + {0x16, 0x03, 0xBF}, + {0x01, 0x02, 0xC5}, + {0x16, 0x03, 0xC5}, + {0x01, 0x02, 0xE7}, + {0x16, 0x03, 0xE7}, + {0x01, 0x02, 0xEF}, + {0x16, 0x03, 0xEF}, + {0x00, 0x03, 0x09}, + {0x00, 0x03, 0x8E}, + {0x00, 0x03, 0x90}, + {0x00, 0x03, 0x91}, + {0x00, 0x03, 0x94}, + {0x00, 0x03, 0x9F}, }, /* 171 */ { - {0x02, 2, 188}, - {0x09, 2, 188}, - {0x17, 2, 188}, - {0x28, 3, 188}, - {0x02, 2, 191}, - {0x09, 2, 191}, - {0x17, 2, 191}, - {0x28, 3, 191}, - {0x02, 2, 197}, - {0x09, 2, 197}, - {0x17, 2, 197}, - {0x28, 3, 197}, - {0x02, 2, 231}, - {0x09, 2, 231}, - {0x17, 2, 231}, - {0x28, 3, 231}, + {0x02, 0x02, 0xBC}, + {0x09, 0x02, 0xBC}, + {0x17, 0x02, 0xBC}, + {0x28, 0x03, 0xBC}, + {0x02, 0x02, 0xBF}, + {0x09, 0x02, 0xBF}, + {0x17, 0x02, 0xBF}, + {0x28, 0x03, 0xBF}, + {0x02, 0x02, 0xC5}, + {0x09, 0x02, 0xC5}, + {0x17, 0x02, 0xC5}, + {0x28, 0x03, 0xC5}, + {0x02, 0x02, 0xE7}, + {0x09, 0x02, 0xE7}, + {0x17, 0x02, 0xE7}, + {0x28, 0x03, 0xE7}, }, /* 172 */ { - {0x03, 2, 188}, - {0x06, 2, 188}, - {0x0a, 2, 188}, - {0x0f, 2, 188}, - {0x18, 2, 188}, - {0x1f, 2, 188}, - {0x29, 2, 188}, - {0x38, 3, 188}, - {0x03, 2, 191}, - {0x06, 2, 191}, - {0x0a, 2, 191}, - {0x0f, 2, 191}, - {0x18, 2, 191}, - {0x1f, 2, 191}, - {0x29, 2, 191}, - {0x38, 3, 191}, + {0x03, 0x02, 0xBC}, + {0x06, 0x02, 0xBC}, + {0x0A, 0x02, 0xBC}, + {0x0F, 0x02, 0xBC}, + {0x18, 0x02, 0xBC}, + {0x1F, 0x02, 0xBC}, + {0x29, 0x02, 0xBC}, + {0x38, 0x03, 0xBC}, + {0x03, 0x02, 0xBF}, + {0x06, 0x02, 0xBF}, + {0x0A, 0x02, 0xBF}, + {0x0F, 0x02, 0xBF}, + {0x18, 0x02, 0xBF}, + {0x1F, 0x02, 0xBF}, + {0x29, 0x02, 0xBF}, + {0x38, 0x03, 0xBF}, }, /* 173 */ { - {0x03, 2, 197}, - {0x06, 2, 197}, - {0x0a, 2, 197}, - {0x0f, 2, 197}, - {0x18, 2, 197}, - {0x1f, 2, 197}, - {0x29, 2, 197}, - {0x38, 3, 197}, - {0x03, 2, 231}, - {0x06, 2, 231}, - {0x0a, 2, 231}, - {0x0f, 2, 231}, - {0x18, 2, 231}, - {0x1f, 2, 231}, - {0x29, 2, 231}, - {0x38, 3, 231}, + {0x03, 0x02, 0xC5}, + {0x06, 0x02, 0xC5}, + {0x0A, 0x02, 0xC5}, + {0x0F, 0x02, 0xC5}, + {0x18, 0x02, 0xC5}, + {0x1F, 0x02, 0xC5}, + {0x29, 0x02, 0xC5}, + {0x38, 0x03, 0xC5}, + {0x03, 0x02, 0xE7}, + {0x06, 0x02, 0xE7}, + {0x0A, 0x02, 0xE7}, + {0x0F, 0x02, 0xE7}, + {0x18, 0x02, 0xE7}, + {0x1F, 0x02, 0xE7}, + {0x29, 0x02, 0xE7}, + {0x38, 0x03, 0xE7}, }, /* 174 */ { - {0x02, 2, 239}, - {0x09, 2, 239}, - {0x17, 2, 239}, - {0x28, 3, 239}, - {0x01, 2, 9}, - {0x16, 3, 9}, - {0x01, 2, 142}, - {0x16, 3, 142}, - {0x01, 2, 144}, - {0x16, 3, 144}, - {0x01, 2, 145}, - {0x16, 3, 145}, - {0x01, 2, 148}, - {0x16, 3, 148}, - {0x01, 2, 159}, - {0x16, 3, 159}, + {0x02, 0x02, 0xEF}, + {0x09, 0x02, 0xEF}, + {0x17, 0x02, 0xEF}, + {0x28, 0x03, 0xEF}, + {0x01, 0x02, 0x09}, + {0x16, 0x03, 0x09}, + {0x01, 0x02, 0x8E}, + {0x16, 0x03, 0x8E}, + {0x01, 0x02, 0x90}, + {0x16, 0x03, 0x90}, + {0x01, 0x02, 0x91}, + {0x16, 0x03, 0x91}, + {0x01, 0x02, 0x94}, + {0x16, 0x03, 0x94}, + {0x01, 0x02, 0x9F}, + {0x16, 0x03, 0x9F}, }, /* 175 */ { - {0x03, 2, 239}, - {0x06, 2, 239}, - {0x0a, 2, 239}, - {0x0f, 2, 239}, - {0x18, 2, 239}, - {0x1f, 2, 239}, - {0x29, 2, 239}, - {0x38, 3, 239}, - {0x02, 2, 9}, - {0x09, 2, 9}, - {0x17, 2, 9}, - {0x28, 3, 9}, - {0x02, 2, 142}, - {0x09, 2, 142}, - {0x17, 2, 142}, - {0x28, 3, 142}, + {0x03, 0x02, 0xEF}, + {0x06, 0x02, 0xEF}, + {0x0A, 0x02, 0xEF}, + {0x0F, 0x02, 0xEF}, + {0x18, 0x02, 0xEF}, + {0x1F, 0x02, 0xEF}, + {0x29, 0x02, 0xEF}, + {0x38, 0x03, 0xEF}, + {0x02, 0x02, 0x09}, + {0x09, 0x02, 0x09}, + {0x17, 0x02, 0x09}, + {0x28, 0x03, 0x09}, + {0x02, 0x02, 0x8E}, + {0x09, 0x02, 0x8E}, + {0x17, 0x02, 0x8E}, + {0x28, 0x03, 0x8E}, }, /* 176 */ { - {0x03, 2, 9}, - {0x06, 2, 9}, - {0x0a, 2, 9}, - {0x0f, 2, 9}, - {0x18, 2, 9}, - {0x1f, 2, 9}, - {0x29, 2, 9}, - {0x38, 3, 9}, - {0x03, 2, 142}, - {0x06, 2, 142}, - {0x0a, 2, 142}, - {0x0f, 2, 142}, - {0x18, 2, 142}, - {0x1f, 2, 142}, - {0x29, 2, 142}, - {0x38, 3, 142}, + {0x03, 0x02, 0x09}, + {0x06, 0x02, 0x09}, + {0x0A, 0x02, 0x09}, + {0x0F, 0x02, 0x09}, + {0x18, 0x02, 0x09}, + {0x1F, 0x02, 0x09}, + {0x29, 0x02, 0x09}, + {0x38, 0x03, 0x09}, + {0x03, 0x02, 0x8E}, + {0x06, 0x02, 0x8E}, + {0x0A, 0x02, 0x8E}, + {0x0F, 0x02, 0x8E}, + {0x18, 0x02, 0x8E}, + {0x1F, 0x02, 0x8E}, + {0x29, 0x02, 0x8E}, + {0x38, 0x03, 0x8E}, }, /* 177 */ { - {0x02, 2, 144}, - {0x09, 2, 144}, - {0x17, 2, 144}, - {0x28, 3, 144}, - {0x02, 2, 145}, - {0x09, 2, 145}, - {0x17, 2, 145}, - {0x28, 3, 145}, - {0x02, 2, 148}, - {0x09, 2, 148}, - {0x17, 2, 148}, - {0x28, 3, 148}, - {0x02, 2, 159}, - {0x09, 2, 159}, - {0x17, 2, 159}, - {0x28, 3, 159}, + {0x02, 0x02, 0x90}, + {0x09, 0x02, 0x90}, + {0x17, 0x02, 0x90}, + {0x28, 0x03, 0x90}, + {0x02, 0x02, 0x91}, + {0x09, 0x02, 0x91}, + {0x17, 0x02, 0x91}, + {0x28, 0x03, 0x91}, + {0x02, 0x02, 0x94}, + {0x09, 0x02, 0x94}, + {0x17, 0x02, 0x94}, + {0x28, 0x03, 0x94}, + {0x02, 0x02, 0x9F}, + {0x09, 0x02, 0x9F}, + {0x17, 0x02, 0x9F}, + {0x28, 0x03, 0x9F}, }, /* 178 */ { - {0x03, 2, 144}, - {0x06, 2, 144}, - {0x0a, 2, 144}, - {0x0f, 2, 144}, - {0x18, 2, 144}, - {0x1f, 2, 144}, - {0x29, 2, 144}, - {0x38, 3, 144}, - {0x03, 2, 145}, - {0x06, 2, 145}, - {0x0a, 2, 145}, - {0x0f, 2, 145}, - {0x18, 2, 145}, - {0x1f, 2, 145}, - {0x29, 2, 145}, - {0x38, 3, 145}, + {0x03, 0x02, 0x90}, + {0x06, 0x02, 0x90}, + {0x0A, 0x02, 0x90}, + {0x0F, 0x02, 0x90}, + {0x18, 0x02, 0x90}, + {0x1F, 0x02, 0x90}, + {0x29, 0x02, 0x90}, + {0x38, 0x03, 0x90}, + {0x03, 0x02, 0x91}, + {0x06, 0x02, 0x91}, + {0x0A, 0x02, 0x91}, + {0x0F, 0x02, 0x91}, + {0x18, 0x02, 0x91}, + {0x1F, 0x02, 0x91}, + {0x29, 0x02, 0x91}, + {0x38, 0x03, 0x91}, }, /* 179 */ { - {0x03, 2, 148}, - {0x06, 2, 148}, - {0x0a, 2, 148}, - {0x0f, 2, 148}, - {0x18, 2, 148}, - {0x1f, 2, 148}, - {0x29, 2, 148}, - {0x38, 3, 148}, - {0x03, 2, 159}, - {0x06, 2, 159}, - {0x0a, 2, 159}, - {0x0f, 2, 159}, - {0x18, 2, 159}, - {0x1f, 2, 159}, - {0x29, 2, 159}, - {0x38, 3, 159}, + {0x03, 0x02, 0x94}, + {0x06, 0x02, 0x94}, + {0x0A, 0x02, 0x94}, + {0x0F, 0x02, 0x94}, + {0x18, 0x02, 0x94}, + {0x1F, 0x02, 0x94}, + {0x29, 0x02, 0x94}, + {0x38, 0x03, 0x94}, + {0x03, 0x02, 0x9F}, + {0x06, 0x02, 0x9F}, + {0x0A, 0x02, 0x9F}, + {0x0F, 0x02, 0x9F}, + {0x18, 0x02, 0x9F}, + {0x1F, 0x02, 0x9F}, + {0x29, 0x02, 0x9F}, + {0x38, 0x03, 0x9F}, }, /* 180 */ { - {0x00, 3, 171}, - {0x00, 3, 206}, - {0x00, 3, 215}, - {0x00, 3, 225}, - {0x00, 3, 236}, - {0x00, 3, 237}, - {0xbc, 0, 0}, - {0xbd, 0, 0}, - {0xc1, 0, 0}, - {0xc4, 0, 0}, - {0xc8, 0, 0}, - {0xcb, 0, 0}, - {0xd1, 0, 0}, - {0xd8, 0, 0}, - {0xe0, 0, 0}, - {0xee, 0, 0}, + {0x00, 0x03, 0xAB}, + {0x00, 0x03, 0xCE}, + {0x00, 0x03, 0xD7}, + {0x00, 0x03, 0xE1}, + {0x00, 0x03, 0xEC}, + {0x00, 0x03, 0xED}, + {0xBC, 0x00, 0x00}, + {0xBD, 0x00, 0x00}, + {0xC1, 0x00, 0x00}, + {0xC4, 0x00, 0x00}, + {0xC8, 0x00, 0x00}, + {0xCB, 0x00, 0x00}, + {0xD1, 0x00, 0x00}, + {0xD8, 0x00, 0x00}, + {0xE0, 0x00, 0x00}, + {0xEE, 0x00, 0x00}, }, /* 181 */ { - {0x01, 2, 171}, - {0x16, 3, 171}, - {0x01, 2, 206}, - {0x16, 3, 206}, - {0x01, 2, 215}, - {0x16, 3, 215}, - {0x01, 2, 225}, - {0x16, 3, 225}, - {0x01, 2, 236}, - {0x16, 3, 236}, - {0x01, 2, 237}, - {0x16, 3, 237}, - {0x00, 3, 199}, - {0x00, 3, 207}, - {0x00, 3, 234}, - {0x00, 3, 235}, + {0x01, 0x02, 0xAB}, + {0x16, 0x03, 0xAB}, + {0x01, 0x02, 0xCE}, + {0x16, 0x03, 0xCE}, + {0x01, 0x02, 0xD7}, + {0x16, 0x03, 0xD7}, + {0x01, 0x02, 0xE1}, + {0x16, 0x03, 0xE1}, + {0x01, 0x02, 0xEC}, + {0x16, 0x03, 0xEC}, + {0x01, 0x02, 0xED}, + {0x16, 0x03, 0xED}, + {0x00, 0x03, 0xC7}, + {0x00, 0x03, 0xCF}, + {0x00, 0x03, 0xEA}, + {0x00, 0x03, 0xEB}, }, /* 182 */ { - {0x02, 2, 171}, - {0x09, 2, 171}, - {0x17, 2, 171}, - {0x28, 3, 171}, - {0x02, 2, 206}, - {0x09, 2, 206}, - {0x17, 2, 206}, - {0x28, 3, 206}, - {0x02, 2, 215}, - {0x09, 2, 215}, - {0x17, 2, 215}, - {0x28, 3, 215}, - {0x02, 2, 225}, - {0x09, 2, 225}, - {0x17, 2, 225}, - {0x28, 3, 225}, + {0x02, 0x02, 0xAB}, + {0x09, 0x02, 0xAB}, + {0x17, 0x02, 0xAB}, + {0x28, 0x03, 0xAB}, + {0x02, 0x02, 0xCE}, + {0x09, 0x02, 0xCE}, + {0x17, 0x02, 0xCE}, + {0x28, 0x03, 0xCE}, + {0x02, 0x02, 0xD7}, + {0x09, 0x02, 0xD7}, + {0x17, 0x02, 0xD7}, + {0x28, 0x03, 0xD7}, + {0x02, 0x02, 0xE1}, + {0x09, 0x02, 0xE1}, + {0x17, 0x02, 0xE1}, + {0x28, 0x03, 0xE1}, }, /* 183 */ { - {0x03, 2, 171}, - {0x06, 2, 171}, - {0x0a, 2, 171}, - {0x0f, 2, 171}, - {0x18, 2, 171}, - {0x1f, 2, 171}, - {0x29, 2, 171}, - {0x38, 3, 171}, - {0x03, 2, 206}, - {0x06, 2, 206}, - {0x0a, 2, 206}, - {0x0f, 2, 206}, - {0x18, 2, 206}, - {0x1f, 2, 206}, - {0x29, 2, 206}, - {0x38, 3, 206}, + {0x03, 0x02, 0xAB}, + {0x06, 0x02, 0xAB}, + {0x0A, 0x02, 0xAB}, + {0x0F, 0x02, 0xAB}, + {0x18, 0x02, 0xAB}, + {0x1F, 0x02, 0xAB}, + {0x29, 0x02, 0xAB}, + {0x38, 0x03, 0xAB}, + {0x03, 0x02, 0xCE}, + {0x06, 0x02, 0xCE}, + {0x0A, 0x02, 0xCE}, + {0x0F, 0x02, 0xCE}, + {0x18, 0x02, 0xCE}, + {0x1F, 0x02, 0xCE}, + {0x29, 0x02, 0xCE}, + {0x38, 0x03, 0xCE}, }, /* 184 */ { - {0x03, 2, 215}, - {0x06, 2, 215}, - {0x0a, 2, 215}, - {0x0f, 2, 215}, - {0x18, 2, 215}, - {0x1f, 2, 215}, - {0x29, 2, 215}, - {0x38, 3, 215}, - {0x03, 2, 225}, - {0x06, 2, 225}, - {0x0a, 2, 225}, - {0x0f, 2, 225}, - {0x18, 2, 225}, - {0x1f, 2, 225}, - {0x29, 2, 225}, - {0x38, 3, 225}, + {0x03, 0x02, 0xD7}, + {0x06, 0x02, 0xD7}, + {0x0A, 0x02, 0xD7}, + {0x0F, 0x02, 0xD7}, + {0x18, 0x02, 0xD7}, + {0x1F, 0x02, 0xD7}, + {0x29, 0x02, 0xD7}, + {0x38, 0x03, 0xD7}, + {0x03, 0x02, 0xE1}, + {0x06, 0x02, 0xE1}, + {0x0A, 0x02, 0xE1}, + {0x0F, 0x02, 0xE1}, + {0x18, 0x02, 0xE1}, + {0x1F, 0x02, 0xE1}, + {0x29, 0x02, 0xE1}, + {0x38, 0x03, 0xE1}, }, /* 185 */ { - {0x02, 2, 236}, - {0x09, 2, 236}, - {0x17, 2, 236}, - {0x28, 3, 236}, - {0x02, 2, 237}, - {0x09, 2, 237}, - {0x17, 2, 237}, - {0x28, 3, 237}, - {0x01, 2, 199}, - {0x16, 3, 199}, - {0x01, 2, 207}, - {0x16, 3, 207}, - {0x01, 2, 234}, - {0x16, 3, 234}, - {0x01, 2, 235}, - {0x16, 3, 235}, + {0x02, 0x02, 0xEC}, + {0x09, 0x02, 0xEC}, + {0x17, 0x02, 0xEC}, + {0x28, 0x03, 0xEC}, + {0x02, 0x02, 0xED}, + {0x09, 0x02, 0xED}, + {0x17, 0x02, 0xED}, + {0x28, 0x03, 0xED}, + {0x01, 0x02, 0xC7}, + {0x16, 0x03, 0xC7}, + {0x01, 0x02, 0xCF}, + {0x16, 0x03, 0xCF}, + {0x01, 0x02, 0xEA}, + {0x16, 0x03, 0xEA}, + {0x01, 0x02, 0xEB}, + {0x16, 0x03, 0xEB}, }, /* 186 */ { - {0x03, 2, 236}, - {0x06, 2, 236}, - {0x0a, 2, 236}, - {0x0f, 2, 236}, - {0x18, 2, 236}, - {0x1f, 2, 236}, - {0x29, 2, 236}, - {0x38, 3, 236}, - {0x03, 2, 237}, - {0x06, 2, 237}, - {0x0a, 2, 237}, - {0x0f, 2, 237}, - {0x18, 2, 237}, - {0x1f, 2, 237}, - {0x29, 2, 237}, - {0x38, 3, 237}, + {0x03, 0x02, 0xEC}, + {0x06, 0x02, 0xEC}, + {0x0A, 0x02, 0xEC}, + {0x0F, 0x02, 0xEC}, + {0x18, 0x02, 0xEC}, + {0x1F, 0x02, 0xEC}, + {0x29, 0x02, 0xEC}, + {0x38, 0x03, 0xEC}, + {0x03, 0x02, 0xED}, + {0x06, 0x02, 0xED}, + {0x0A, 0x02, 0xED}, + {0x0F, 0x02, 0xED}, + {0x18, 0x02, 0xED}, + {0x1F, 0x02, 0xED}, + {0x29, 0x02, 0xED}, + {0x38, 0x03, 0xED}, }, /* 187 */ { - {0x02, 2, 199}, - {0x09, 2, 199}, - {0x17, 2, 199}, - {0x28, 3, 199}, - {0x02, 2, 207}, - {0x09, 2, 207}, - {0x17, 2, 207}, - {0x28, 3, 207}, - {0x02, 2, 234}, - {0x09, 2, 234}, - {0x17, 2, 234}, - {0x28, 3, 234}, - {0x02, 2, 235}, - {0x09, 2, 235}, - {0x17, 2, 235}, - {0x28, 3, 235}, + {0x02, 0x02, 0xC7}, + {0x09, 0x02, 0xC7}, + {0x17, 0x02, 0xC7}, + {0x28, 0x03, 0xC7}, + {0x02, 0x02, 0xCF}, + {0x09, 0x02, 0xCF}, + {0x17, 0x02, 0xCF}, + {0x28, 0x03, 0xCF}, + {0x02, 0x02, 0xEA}, + {0x09, 0x02, 0xEA}, + {0x17, 0x02, 0xEA}, + {0x28, 0x03, 0xEA}, + {0x02, 0x02, 0xEB}, + {0x09, 0x02, 0xEB}, + {0x17, 0x02, 0xEB}, + {0x28, 0x03, 0xEB}, }, /* 188 */ { - {0x03, 2, 199}, - {0x06, 2, 199}, - {0x0a, 2, 199}, - {0x0f, 2, 199}, - {0x18, 2, 199}, - {0x1f, 2, 199}, - {0x29, 2, 199}, - {0x38, 3, 199}, - {0x03, 2, 207}, - {0x06, 2, 207}, - {0x0a, 2, 207}, - {0x0f, 2, 207}, - {0x18, 2, 207}, - {0x1f, 2, 207}, - {0x29, 2, 207}, - {0x38, 3, 207}, + {0x03, 0x02, 0xC7}, + {0x06, 0x02, 0xC7}, + {0x0A, 0x02, 0xC7}, + {0x0F, 0x02, 0xC7}, + {0x18, 0x02, 0xC7}, + {0x1F, 0x02, 0xC7}, + {0x29, 0x02, 0xC7}, + {0x38, 0x03, 0xC7}, + {0x03, 0x02, 0xCF}, + {0x06, 0x02, 0xCF}, + {0x0A, 0x02, 0xCF}, + {0x0F, 0x02, 0xCF}, + {0x18, 0x02, 0xCF}, + {0x1F, 0x02, 0xCF}, + {0x29, 0x02, 0xCF}, + {0x38, 0x03, 0xCF}, }, /* 189 */ { - {0x03, 2, 234}, - {0x06, 2, 234}, - {0x0a, 2, 234}, - {0x0f, 2, 234}, - {0x18, 2, 234}, - {0x1f, 2, 234}, - {0x29, 2, 234}, - {0x38, 3, 234}, - {0x03, 2, 235}, - {0x06, 2, 235}, - {0x0a, 2, 235}, - {0x0f, 2, 235}, - {0x18, 2, 235}, - {0x1f, 2, 235}, - {0x29, 2, 235}, - {0x38, 3, 235}, + {0x03, 0x02, 0xEA}, + {0x06, 0x02, 0xEA}, + {0x0A, 0x02, 0xEA}, + {0x0F, 0x02, 0xEA}, + {0x18, 0x02, 0xEA}, + {0x1F, 0x02, 0xEA}, + {0x29, 0x02, 0xEA}, + {0x38, 0x03, 0xEA}, + {0x03, 0x02, 0xEB}, + {0x06, 0x02, 0xEB}, + {0x0A, 0x02, 0xEB}, + {0x0F, 0x02, 0xEB}, + {0x18, 0x02, 0xEB}, + {0x1F, 0x02, 0xEB}, + {0x29, 0x02, 0xEB}, + {0x38, 0x03, 0xEB}, }, /* 190 */ { - {0xc2, 0, 0}, - {0xc3, 0, 0}, - {0xc5, 0, 0}, - {0xc6, 0, 0}, - {0xc9, 0, 0}, - {0xca, 0, 0}, - {0xcc, 0, 0}, - {0xcd, 0, 0}, - {0xd2, 0, 0}, - {0xd5, 0, 0}, - {0xd9, 0, 0}, - {0xdc, 0, 0}, - {0xe1, 0, 0}, - {0xe7, 0, 0}, - {0xef, 0, 0}, - {0xf6, 0, 0}, + {0xC2, 0x00, 0x00}, + {0xC3, 0x00, 0x00}, + {0xC5, 0x00, 0x00}, + {0xC6, 0x00, 0x00}, + {0xC9, 0x00, 0x00}, + {0xCA, 0x00, 0x00}, + {0xCC, 0x00, 0x00}, + {0xCD, 0x00, 0x00}, + {0xD2, 0x00, 0x00}, + {0xD5, 0x00, 0x00}, + {0xD9, 0x00, 0x00}, + {0xDC, 0x00, 0x00}, + {0xE1, 0x00, 0x00}, + {0xE7, 0x00, 0x00}, + {0xEF, 0x00, 0x00}, + {0xF6, 0x00, 0x00}, }, /* 191 */ { - {0x00, 3, 192}, - {0x00, 3, 193}, - {0x00, 3, 200}, - {0x00, 3, 201}, - {0x00, 3, 202}, - {0x00, 3, 205}, - {0x00, 3, 210}, - {0x00, 3, 213}, - {0x00, 3, 218}, - {0x00, 3, 219}, - {0x00, 3, 238}, - {0x00, 3, 240}, - {0x00, 3, 242}, - {0x00, 3, 243}, - {0x00, 3, 255}, - {0xce, 0, 0}, + {0x00, 0x03, 0xC0}, + {0x00, 0x03, 0xC1}, + {0x00, 0x03, 0xC8}, + {0x00, 0x03, 0xC9}, + {0x00, 0x03, 0xCA}, + {0x00, 0x03, 0xCD}, + {0x00, 0x03, 0xD2}, + {0x00, 0x03, 0xD5}, + {0x00, 0x03, 0xDA}, + {0x00, 0x03, 0xDB}, + {0x00, 0x03, 0xEE}, + {0x00, 0x03, 0xF0}, + {0x00, 0x03, 0xF2}, + {0x00, 0x03, 0xF3}, + {0x00, 0x03, 0xFF}, + {0xCE, 0x00, 0x00}, }, /* 192 */ { - {0x01, 2, 192}, - {0x16, 3, 192}, - {0x01, 2, 193}, - {0x16, 3, 193}, - {0x01, 2, 200}, - {0x16, 3, 200}, - {0x01, 2, 201}, - {0x16, 3, 201}, - {0x01, 2, 202}, - {0x16, 3, 202}, - {0x01, 2, 205}, - {0x16, 3, 205}, - {0x01, 2, 210}, - {0x16, 3, 210}, - {0x01, 2, 213}, - {0x16, 3, 213}, + {0x01, 0x02, 0xC0}, + {0x16, 0x03, 0xC0}, + {0x01, 0x02, 0xC1}, + {0x16, 0x03, 0xC1}, + {0x01, 0x02, 0xC8}, + {0x16, 0x03, 0xC8}, + {0x01, 0x02, 0xC9}, + {0x16, 0x03, 0xC9}, + {0x01, 0x02, 0xCA}, + {0x16, 0x03, 0xCA}, + {0x01, 0x02, 0xCD}, + {0x16, 0x03, 0xCD}, + {0x01, 0x02, 0xD2}, + {0x16, 0x03, 0xD2}, + {0x01, 0x02, 0xD5}, + {0x16, 0x03, 0xD5}, }, /* 193 */ { - {0x02, 2, 192}, - {0x09, 2, 192}, - {0x17, 2, 192}, - {0x28, 3, 192}, - {0x02, 2, 193}, - {0x09, 2, 193}, - {0x17, 2, 193}, - {0x28, 3, 193}, - {0x02, 2, 200}, - {0x09, 2, 200}, - {0x17, 2, 200}, - {0x28, 3, 200}, - {0x02, 2, 201}, - {0x09, 2, 201}, - {0x17, 2, 201}, - {0x28, 3, 201}, + {0x02, 0x02, 0xC0}, + {0x09, 0x02, 0xC0}, + {0x17, 0x02, 0xC0}, + {0x28, 0x03, 0xC0}, + {0x02, 0x02, 0xC1}, + {0x09, 0x02, 0xC1}, + {0x17, 0x02, 0xC1}, + {0x28, 0x03, 0xC1}, + {0x02, 0x02, 0xC8}, + {0x09, 0x02, 0xC8}, + {0x17, 0x02, 0xC8}, + {0x28, 0x03, 0xC8}, + {0x02, 0x02, 0xC9}, + {0x09, 0x02, 0xC9}, + {0x17, 0x02, 0xC9}, + {0x28, 0x03, 0xC9}, }, /* 194 */ { - {0x03, 2, 192}, - {0x06, 2, 192}, - {0x0a, 2, 192}, - {0x0f, 2, 192}, - {0x18, 2, 192}, - {0x1f, 2, 192}, - {0x29, 2, 192}, - {0x38, 3, 192}, - {0x03, 2, 193}, - {0x06, 2, 193}, - {0x0a, 2, 193}, - {0x0f, 2, 193}, - {0x18, 2, 193}, - {0x1f, 2, 193}, - {0x29, 2, 193}, - {0x38, 3, 193}, + {0x03, 0x02, 0xC0}, + {0x06, 0x02, 0xC0}, + {0x0A, 0x02, 0xC0}, + {0x0F, 0x02, 0xC0}, + {0x18, 0x02, 0xC0}, + {0x1F, 0x02, 0xC0}, + {0x29, 0x02, 0xC0}, + {0x38, 0x03, 0xC0}, + {0x03, 0x02, 0xC1}, + {0x06, 0x02, 0xC1}, + {0x0A, 0x02, 0xC1}, + {0x0F, 0x02, 0xC1}, + {0x18, 0x02, 0xC1}, + {0x1F, 0x02, 0xC1}, + {0x29, 0x02, 0xC1}, + {0x38, 0x03, 0xC1}, }, /* 195 */ { - {0x03, 2, 200}, - {0x06, 2, 200}, - {0x0a, 2, 200}, - {0x0f, 2, 200}, - {0x18, 2, 200}, - {0x1f, 2, 200}, - {0x29, 2, 200}, - {0x38, 3, 200}, - {0x03, 2, 201}, - {0x06, 2, 201}, - {0x0a, 2, 201}, - {0x0f, 2, 201}, - {0x18, 2, 201}, - {0x1f, 2, 201}, - {0x29, 2, 201}, - {0x38, 3, 201}, + {0x03, 0x02, 0xC8}, + {0x06, 0x02, 0xC8}, + {0x0A, 0x02, 0xC8}, + {0x0F, 0x02, 0xC8}, + {0x18, 0x02, 0xC8}, + {0x1F, 0x02, 0xC8}, + {0x29, 0x02, 0xC8}, + {0x38, 0x03, 0xC8}, + {0x03, 0x02, 0xC9}, + {0x06, 0x02, 0xC9}, + {0x0A, 0x02, 0xC9}, + {0x0F, 0x02, 0xC9}, + {0x18, 0x02, 0xC9}, + {0x1F, 0x02, 0xC9}, + {0x29, 0x02, 0xC9}, + {0x38, 0x03, 0xC9}, }, /* 196 */ { - {0x02, 2, 202}, - {0x09, 2, 202}, - {0x17, 2, 202}, - {0x28, 3, 202}, - {0x02, 2, 205}, - {0x09, 2, 205}, - {0x17, 2, 205}, - {0x28, 3, 205}, - {0x02, 2, 210}, - {0x09, 2, 210}, - {0x17, 2, 210}, - {0x28, 3, 210}, - {0x02, 2, 213}, - {0x09, 2, 213}, - {0x17, 2, 213}, - {0x28, 3, 213}, + {0x02, 0x02, 0xCA}, + {0x09, 0x02, 0xCA}, + {0x17, 0x02, 0xCA}, + {0x28, 0x03, 0xCA}, + {0x02, 0x02, 0xCD}, + {0x09, 0x02, 0xCD}, + {0x17, 0x02, 0xCD}, + {0x28, 0x03, 0xCD}, + {0x02, 0x02, 0xD2}, + {0x09, 0x02, 0xD2}, + {0x17, 0x02, 0xD2}, + {0x28, 0x03, 0xD2}, + {0x02, 0x02, 0xD5}, + {0x09, 0x02, 0xD5}, + {0x17, 0x02, 0xD5}, + {0x28, 0x03, 0xD5}, }, /* 197 */ { - {0x03, 2, 202}, - {0x06, 2, 202}, - {0x0a, 2, 202}, - {0x0f, 2, 202}, - {0x18, 2, 202}, - {0x1f, 2, 202}, - {0x29, 2, 202}, - {0x38, 3, 202}, - {0x03, 2, 205}, - {0x06, 2, 205}, - {0x0a, 2, 205}, - {0x0f, 2, 205}, - {0x18, 2, 205}, - {0x1f, 2, 205}, - {0x29, 2, 205}, - {0x38, 3, 205}, + {0x03, 0x02, 0xCA}, + {0x06, 0x02, 0xCA}, + {0x0A, 0x02, 0xCA}, + {0x0F, 0x02, 0xCA}, + {0x18, 0x02, 0xCA}, + {0x1F, 0x02, 0xCA}, + {0x29, 0x02, 0xCA}, + {0x38, 0x03, 0xCA}, + {0x03, 0x02, 0xCD}, + {0x06, 0x02, 0xCD}, + {0x0A, 0x02, 0xCD}, + {0x0F, 0x02, 0xCD}, + {0x18, 0x02, 0xCD}, + {0x1F, 0x02, 0xCD}, + {0x29, 0x02, 0xCD}, + {0x38, 0x03, 0xCD}, }, /* 198 */ { - {0x03, 2, 210}, - {0x06, 2, 210}, - {0x0a, 2, 210}, - {0x0f, 2, 210}, - {0x18, 2, 210}, - {0x1f, 2, 210}, - {0x29, 2, 210}, - {0x38, 3, 210}, - {0x03, 2, 213}, - {0x06, 2, 213}, - {0x0a, 2, 213}, - {0x0f, 2, 213}, - {0x18, 2, 213}, - {0x1f, 2, 213}, - {0x29, 2, 213}, - {0x38, 3, 213}, + {0x03, 0x02, 0xD2}, + {0x06, 0x02, 0xD2}, + {0x0A, 0x02, 0xD2}, + {0x0F, 0x02, 0xD2}, + {0x18, 0x02, 0xD2}, + {0x1F, 0x02, 0xD2}, + {0x29, 0x02, 0xD2}, + {0x38, 0x03, 0xD2}, + {0x03, 0x02, 0xD5}, + {0x06, 0x02, 0xD5}, + {0x0A, 0x02, 0xD5}, + {0x0F, 0x02, 0xD5}, + {0x18, 0x02, 0xD5}, + {0x1F, 0x02, 0xD5}, + {0x29, 0x02, 0xD5}, + {0x38, 0x03, 0xD5}, }, /* 199 */ { - {0x01, 2, 218}, - {0x16, 3, 218}, - {0x01, 2, 219}, - {0x16, 3, 219}, - {0x01, 2, 238}, - {0x16, 3, 238}, - {0x01, 2, 240}, - {0x16, 3, 240}, - {0x01, 2, 242}, - {0x16, 3, 242}, - {0x01, 2, 243}, - {0x16, 3, 243}, - {0x01, 2, 255}, - {0x16, 3, 255}, - {0x00, 3, 203}, - {0x00, 3, 204}, + {0x01, 0x02, 0xDA}, + {0x16, 0x03, 0xDA}, + {0x01, 0x02, 0xDB}, + {0x16, 0x03, 0xDB}, + {0x01, 0x02, 0xEE}, + {0x16, 0x03, 0xEE}, + {0x01, 0x02, 0xF0}, + {0x16, 0x03, 0xF0}, + {0x01, 0x02, 0xF2}, + {0x16, 0x03, 0xF2}, + {0x01, 0x02, 0xF3}, + {0x16, 0x03, 0xF3}, + {0x01, 0x02, 0xFF}, + {0x16, 0x03, 0xFF}, + {0x00, 0x03, 0xCB}, + {0x00, 0x03, 0xCC}, }, /* 200 */ { - {0x02, 2, 218}, - {0x09, 2, 218}, - {0x17, 2, 218}, - {0x28, 3, 218}, - {0x02, 2, 219}, - {0x09, 2, 219}, - {0x17, 2, 219}, - {0x28, 3, 219}, - {0x02, 2, 238}, - {0x09, 2, 238}, - {0x17, 2, 238}, - {0x28, 3, 238}, - {0x02, 2, 240}, - {0x09, 2, 240}, - {0x17, 2, 240}, - {0x28, 3, 240}, + {0x02, 0x02, 0xDA}, + {0x09, 0x02, 0xDA}, + {0x17, 0x02, 0xDA}, + {0x28, 0x03, 0xDA}, + {0x02, 0x02, 0xDB}, + {0x09, 0x02, 0xDB}, + {0x17, 0x02, 0xDB}, + {0x28, 0x03, 0xDB}, + {0x02, 0x02, 0xEE}, + {0x09, 0x02, 0xEE}, + {0x17, 0x02, 0xEE}, + {0x28, 0x03, 0xEE}, + {0x02, 0x02, 0xF0}, + {0x09, 0x02, 0xF0}, + {0x17, 0x02, 0xF0}, + {0x28, 0x03, 0xF0}, }, /* 201 */ { - {0x03, 2, 218}, - {0x06, 2, 218}, - {0x0a, 2, 218}, - {0x0f, 2, 218}, - {0x18, 2, 218}, - {0x1f, 2, 218}, - {0x29, 2, 218}, - {0x38, 3, 218}, - {0x03, 2, 219}, - {0x06, 2, 219}, - {0x0a, 2, 219}, - {0x0f, 2, 219}, - {0x18, 2, 219}, - {0x1f, 2, 219}, - {0x29, 2, 219}, - {0x38, 3, 219}, + {0x03, 0x02, 0xDA}, + {0x06, 0x02, 0xDA}, + {0x0A, 0x02, 0xDA}, + {0x0F, 0x02, 0xDA}, + {0x18, 0x02, 0xDA}, + {0x1F, 0x02, 0xDA}, + {0x29, 0x02, 0xDA}, + {0x38, 0x03, 0xDA}, + {0x03, 0x02, 0xDB}, + {0x06, 0x02, 0xDB}, + {0x0A, 0x02, 0xDB}, + {0x0F, 0x02, 0xDB}, + {0x18, 0x02, 0xDB}, + {0x1F, 0x02, 0xDB}, + {0x29, 0x02, 0xDB}, + {0x38, 0x03, 0xDB}, }, /* 202 */ { - {0x03, 2, 238}, - {0x06, 2, 238}, - {0x0a, 2, 238}, - {0x0f, 2, 238}, - {0x18, 2, 238}, - {0x1f, 2, 238}, - {0x29, 2, 238}, - {0x38, 3, 238}, - {0x03, 2, 240}, - {0x06, 2, 240}, - {0x0a, 2, 240}, - {0x0f, 2, 240}, - {0x18, 2, 240}, - {0x1f, 2, 240}, - {0x29, 2, 240}, - {0x38, 3, 240}, + {0x03, 0x02, 0xEE}, + {0x06, 0x02, 0xEE}, + {0x0A, 0x02, 0xEE}, + {0x0F, 0x02, 0xEE}, + {0x18, 0x02, 0xEE}, + {0x1F, 0x02, 0xEE}, + {0x29, 0x02, 0xEE}, + {0x38, 0x03, 0xEE}, + {0x03, 0x02, 0xF0}, + {0x06, 0x02, 0xF0}, + {0x0A, 0x02, 0xF0}, + {0x0F, 0x02, 0xF0}, + {0x18, 0x02, 0xF0}, + {0x1F, 0x02, 0xF0}, + {0x29, 0x02, 0xF0}, + {0x38, 0x03, 0xF0}, }, /* 203 */ { - {0x02, 2, 242}, - {0x09, 2, 242}, - {0x17, 2, 242}, - {0x28, 3, 242}, - {0x02, 2, 243}, - {0x09, 2, 243}, - {0x17, 2, 243}, - {0x28, 3, 243}, - {0x02, 2, 255}, - {0x09, 2, 255}, - {0x17, 2, 255}, - {0x28, 3, 255}, - {0x01, 2, 203}, - {0x16, 3, 203}, - {0x01, 2, 204}, - {0x16, 3, 204}, + {0x02, 0x02, 0xF2}, + {0x09, 0x02, 0xF2}, + {0x17, 0x02, 0xF2}, + {0x28, 0x03, 0xF2}, + {0x02, 0x02, 0xF3}, + {0x09, 0x02, 0xF3}, + {0x17, 0x02, 0xF3}, + {0x28, 0x03, 0xF3}, + {0x02, 0x02, 0xFF}, + {0x09, 0x02, 0xFF}, + {0x17, 0x02, 0xFF}, + {0x28, 0x03, 0xFF}, + {0x01, 0x02, 0xCB}, + {0x16, 0x03, 0xCB}, + {0x01, 0x02, 0xCC}, + {0x16, 0x03, 0xCC}, }, /* 204 */ { - {0x03, 2, 242}, - {0x06, 2, 242}, - {0x0a, 2, 242}, - {0x0f, 2, 242}, - {0x18, 2, 242}, - {0x1f, 2, 242}, - {0x29, 2, 242}, - {0x38, 3, 242}, - {0x03, 2, 243}, - {0x06, 2, 243}, - {0x0a, 2, 243}, - {0x0f, 2, 243}, - {0x18, 2, 243}, - {0x1f, 2, 243}, - {0x29, 2, 243}, - {0x38, 3, 243}, + {0x03, 0x02, 0xF2}, + {0x06, 0x02, 0xF2}, + {0x0A, 0x02, 0xF2}, + {0x0F, 0x02, 0xF2}, + {0x18, 0x02, 0xF2}, + {0x1F, 0x02, 0xF2}, + {0x29, 0x02, 0xF2}, + {0x38, 0x03, 0xF2}, + {0x03, 0x02, 0xF3}, + {0x06, 0x02, 0xF3}, + {0x0A, 0x02, 0xF3}, + {0x0F, 0x02, 0xF3}, + {0x18, 0x02, 0xF3}, + {0x1F, 0x02, 0xF3}, + {0x29, 0x02, 0xF3}, + {0x38, 0x03, 0xF3}, }, /* 205 */ { - {0x03, 2, 255}, - {0x06, 2, 255}, - {0x0a, 2, 255}, - {0x0f, 2, 255}, - {0x18, 2, 255}, - {0x1f, 2, 255}, - {0x29, 2, 255}, - {0x38, 3, 255}, - {0x02, 2, 203}, - {0x09, 2, 203}, - {0x17, 2, 203}, - {0x28, 3, 203}, - {0x02, 2, 204}, - {0x09, 2, 204}, - {0x17, 2, 204}, - {0x28, 3, 204}, + {0x03, 0x02, 0xFF}, + {0x06, 0x02, 0xFF}, + {0x0A, 0x02, 0xFF}, + {0x0F, 0x02, 0xFF}, + {0x18, 0x02, 0xFF}, + {0x1F, 0x02, 0xFF}, + {0x29, 0x02, 0xFF}, + {0x38, 0x03, 0xFF}, + {0x02, 0x02, 0xCB}, + {0x09, 0x02, 0xCB}, + {0x17, 0x02, 0xCB}, + {0x28, 0x03, 0xCB}, + {0x02, 0x02, 0xCC}, + {0x09, 0x02, 0xCC}, + {0x17, 0x02, 0xCC}, + {0x28, 0x03, 0xCC}, }, /* 206 */ { - {0x03, 2, 203}, - {0x06, 2, 203}, - {0x0a, 2, 203}, - {0x0f, 2, 203}, - {0x18, 2, 203}, - {0x1f, 2, 203}, - {0x29, 2, 203}, - {0x38, 3, 203}, - {0x03, 2, 204}, - {0x06, 2, 204}, - {0x0a, 2, 204}, - {0x0f, 2, 204}, - {0x18, 2, 204}, - {0x1f, 2, 204}, - {0x29, 2, 204}, - {0x38, 3, 204}, + {0x03, 0x02, 0xCB}, + {0x06, 0x02, 0xCB}, + {0x0A, 0x02, 0xCB}, + {0x0F, 0x02, 0xCB}, + {0x18, 0x02, 0xCB}, + {0x1F, 0x02, 0xCB}, + {0x29, 0x02, 0xCB}, + {0x38, 0x03, 0xCB}, + {0x03, 0x02, 0xCC}, + {0x06, 0x02, 0xCC}, + {0x0A, 0x02, 0xCC}, + {0x0F, 0x02, 0xCC}, + {0x18, 0x02, 0xCC}, + {0x1F, 0x02, 0xCC}, + {0x29, 0x02, 0xCC}, + {0x38, 0x03, 0xCC}, }, /* 207 */ { - {0xd3, 0, 0}, - {0xd4, 0, 0}, - {0xd6, 0, 0}, - {0xd7, 0, 0}, - {0xda, 0, 0}, - {0xdb, 0, 0}, - {0xdd, 0, 0}, - {0xde, 0, 0}, - {0xe2, 0, 0}, - {0xe4, 0, 0}, - {0xe8, 0, 0}, - {0xeb, 0, 0}, - {0xf0, 0, 0}, - {0xf3, 0, 0}, - {0xf7, 0, 0}, - {0xfa, 0, 0}, + {0xD3, 0x00, 0x00}, + {0xD4, 0x00, 0x00}, + {0xD6, 0x00, 0x00}, + {0xD7, 0x00, 0x00}, + {0xDA, 0x00, 0x00}, + {0xDB, 0x00, 0x00}, + {0xDD, 0x00, 0x00}, + {0xDE, 0x00, 0x00}, + {0xE2, 0x00, 0x00}, + {0xE4, 0x00, 0x00}, + {0xE8, 0x00, 0x00}, + {0xEB, 0x00, 0x00}, + {0xF0, 0x00, 0x00}, + {0xF3, 0x00, 0x00}, + {0xF7, 0x00, 0x00}, + {0xFA, 0x00, 0x00}, }, /* 208 */ { - {0x00, 3, 211}, - {0x00, 3, 212}, - {0x00, 3, 214}, - {0x00, 3, 221}, - {0x00, 3, 222}, - {0x00, 3, 223}, - {0x00, 3, 241}, - {0x00, 3, 244}, - {0x00, 3, 245}, - {0x00, 3, 246}, - {0x00, 3, 247}, - {0x00, 3, 248}, - {0x00, 3, 250}, - {0x00, 3, 251}, - {0x00, 3, 252}, - {0x00, 3, 253}, + {0x00, 0x03, 0xD3}, + {0x00, 0x03, 0xD4}, + {0x00, 0x03, 0xD6}, + {0x00, 0x03, 0xDD}, + {0x00, 0x03, 0xDE}, + {0x00, 0x03, 0xDF}, + {0x00, 0x03, 0xF1}, + {0x00, 0x03, 0xF4}, + {0x00, 0x03, 0xF5}, + {0x00, 0x03, 0xF6}, + {0x00, 0x03, 0xF7}, + {0x00, 0x03, 0xF8}, + {0x00, 0x03, 0xFA}, + {0x00, 0x03, 0xFB}, + {0x00, 0x03, 0xFC}, + {0x00, 0x03, 0xFD}, }, /* 209 */ { - {0x01, 2, 211}, - {0x16, 3, 211}, - {0x01, 2, 212}, - {0x16, 3, 212}, - {0x01, 2, 214}, - {0x16, 3, 214}, - {0x01, 2, 221}, - {0x16, 3, 221}, - {0x01, 2, 222}, - {0x16, 3, 222}, - {0x01, 2, 223}, - {0x16, 3, 223}, - {0x01, 2, 241}, - {0x16, 3, 241}, - {0x01, 2, 244}, - {0x16, 3, 244}, + {0x01, 0x02, 0xD3}, + {0x16, 0x03, 0xD3}, + {0x01, 0x02, 0xD4}, + {0x16, 0x03, 0xD4}, + {0x01, 0x02, 0xD6}, + {0x16, 0x03, 0xD6}, + {0x01, 0x02, 0xDD}, + {0x16, 0x03, 0xDD}, + {0x01, 0x02, 0xDE}, + {0x16, 0x03, 0xDE}, + {0x01, 0x02, 0xDF}, + {0x16, 0x03, 0xDF}, + {0x01, 0x02, 0xF1}, + {0x16, 0x03, 0xF1}, + {0x01, 0x02, 0xF4}, + {0x16, 0x03, 0xF4}, }, /* 210 */ { - {0x02, 2, 211}, - {0x09, 2, 211}, - {0x17, 2, 211}, - {0x28, 3, 211}, - {0x02, 2, 212}, - {0x09, 2, 212}, - {0x17, 2, 212}, - {0x28, 3, 212}, - {0x02, 2, 214}, - {0x09, 2, 214}, - {0x17, 2, 214}, - {0x28, 3, 214}, - {0x02, 2, 221}, - {0x09, 2, 221}, - {0x17, 2, 221}, - {0x28, 3, 221}, + {0x02, 0x02, 0xD3}, + {0x09, 0x02, 0xD3}, + {0x17, 0x02, 0xD3}, + {0x28, 0x03, 0xD3}, + {0x02, 0x02, 0xD4}, + {0x09, 0x02, 0xD4}, + {0x17, 0x02, 0xD4}, + {0x28, 0x03, 0xD4}, + {0x02, 0x02, 0xD6}, + {0x09, 0x02, 0xD6}, + {0x17, 0x02, 0xD6}, + {0x28, 0x03, 0xD6}, + {0x02, 0x02, 0xDD}, + {0x09, 0x02, 0xDD}, + {0x17, 0x02, 0xDD}, + {0x28, 0x03, 0xDD}, }, /* 211 */ { - {0x03, 2, 211}, - {0x06, 2, 211}, - {0x0a, 2, 211}, - {0x0f, 2, 211}, - {0x18, 2, 211}, - {0x1f, 2, 211}, - {0x29, 2, 211}, - {0x38, 3, 211}, - {0x03, 2, 212}, - {0x06, 2, 212}, - {0x0a, 2, 212}, - {0x0f, 2, 212}, - {0x18, 2, 212}, - {0x1f, 2, 212}, - {0x29, 2, 212}, - {0x38, 3, 212}, + {0x03, 0x02, 0xD3}, + {0x06, 0x02, 0xD3}, + {0x0A, 0x02, 0xD3}, + {0x0F, 0x02, 0xD3}, + {0x18, 0x02, 0xD3}, + {0x1F, 0x02, 0xD3}, + {0x29, 0x02, 0xD3}, + {0x38, 0x03, 0xD3}, + {0x03, 0x02, 0xD4}, + {0x06, 0x02, 0xD4}, + {0x0A, 0x02, 0xD4}, + {0x0F, 0x02, 0xD4}, + {0x18, 0x02, 0xD4}, + {0x1F, 0x02, 0xD4}, + {0x29, 0x02, 0xD4}, + {0x38, 0x03, 0xD4}, }, /* 212 */ { - {0x03, 2, 214}, - {0x06, 2, 214}, - {0x0a, 2, 214}, - {0x0f, 2, 214}, - {0x18, 2, 214}, - {0x1f, 2, 214}, - {0x29, 2, 214}, - {0x38, 3, 214}, - {0x03, 2, 221}, - {0x06, 2, 221}, - {0x0a, 2, 221}, - {0x0f, 2, 221}, - {0x18, 2, 221}, - {0x1f, 2, 221}, - {0x29, 2, 221}, - {0x38, 3, 221}, + {0x03, 0x02, 0xD6}, + {0x06, 0x02, 0xD6}, + {0x0A, 0x02, 0xD6}, + {0x0F, 0x02, 0xD6}, + {0x18, 0x02, 0xD6}, + {0x1F, 0x02, 0xD6}, + {0x29, 0x02, 0xD6}, + {0x38, 0x03, 0xD6}, + {0x03, 0x02, 0xDD}, + {0x06, 0x02, 0xDD}, + {0x0A, 0x02, 0xDD}, + {0x0F, 0x02, 0xDD}, + {0x18, 0x02, 0xDD}, + {0x1F, 0x02, 0xDD}, + {0x29, 0x02, 0xDD}, + {0x38, 0x03, 0xDD}, }, /* 213 */ { - {0x02, 2, 222}, - {0x09, 2, 222}, - {0x17, 2, 222}, - {0x28, 3, 222}, - {0x02, 2, 223}, - {0x09, 2, 223}, - {0x17, 2, 223}, - {0x28, 3, 223}, - {0x02, 2, 241}, - {0x09, 2, 241}, - {0x17, 2, 241}, - {0x28, 3, 241}, - {0x02, 2, 244}, - {0x09, 2, 244}, - {0x17, 2, 244}, - {0x28, 3, 244}, + {0x02, 0x02, 0xDE}, + {0x09, 0x02, 0xDE}, + {0x17, 0x02, 0xDE}, + {0x28, 0x03, 0xDE}, + {0x02, 0x02, 0xDF}, + {0x09, 0x02, 0xDF}, + {0x17, 0x02, 0xDF}, + {0x28, 0x03, 0xDF}, + {0x02, 0x02, 0xF1}, + {0x09, 0x02, 0xF1}, + {0x17, 0x02, 0xF1}, + {0x28, 0x03, 0xF1}, + {0x02, 0x02, 0xF4}, + {0x09, 0x02, 0xF4}, + {0x17, 0x02, 0xF4}, + {0x28, 0x03, 0xF4}, }, /* 214 */ { - {0x03, 2, 222}, - {0x06, 2, 222}, - {0x0a, 2, 222}, - {0x0f, 2, 222}, - {0x18, 2, 222}, - {0x1f, 2, 222}, - {0x29, 2, 222}, - {0x38, 3, 222}, - {0x03, 2, 223}, - {0x06, 2, 223}, - {0x0a, 2, 223}, - {0x0f, 2, 223}, - {0x18, 2, 223}, - {0x1f, 2, 223}, - {0x29, 2, 223}, - {0x38, 3, 223}, + {0x03, 0x02, 0xDE}, + {0x06, 0x02, 0xDE}, + {0x0A, 0x02, 0xDE}, + {0x0F, 0x02, 0xDE}, + {0x18, 0x02, 0xDE}, + {0x1F, 0x02, 0xDE}, + {0x29, 0x02, 0xDE}, + {0x38, 0x03, 0xDE}, + {0x03, 0x02, 0xDF}, + {0x06, 0x02, 0xDF}, + {0x0A, 0x02, 0xDF}, + {0x0F, 0x02, 0xDF}, + {0x18, 0x02, 0xDF}, + {0x1F, 0x02, 0xDF}, + {0x29, 0x02, 0xDF}, + {0x38, 0x03, 0xDF}, }, /* 215 */ { - {0x03, 2, 241}, - {0x06, 2, 241}, - {0x0a, 2, 241}, - {0x0f, 2, 241}, - {0x18, 2, 241}, - {0x1f, 2, 241}, - {0x29, 2, 241}, - {0x38, 3, 241}, - {0x03, 2, 244}, - {0x06, 2, 244}, - {0x0a, 2, 244}, - {0x0f, 2, 244}, - {0x18, 2, 244}, - {0x1f, 2, 244}, - {0x29, 2, 244}, - {0x38, 3, 244}, + {0x03, 0x02, 0xF1}, + {0x06, 0x02, 0xF1}, + {0x0A, 0x02, 0xF1}, + {0x0F, 0x02, 0xF1}, + {0x18, 0x02, 0xF1}, + {0x1F, 0x02, 0xF1}, + {0x29, 0x02, 0xF1}, + {0x38, 0x03, 0xF1}, + {0x03, 0x02, 0xF4}, + {0x06, 0x02, 0xF4}, + {0x0A, 0x02, 0xF4}, + {0x0F, 0x02, 0xF4}, + {0x18, 0x02, 0xF4}, + {0x1F, 0x02, 0xF4}, + {0x29, 0x02, 0xF4}, + {0x38, 0x03, 0xF4}, }, /* 216 */ { - {0x01, 2, 245}, - {0x16, 3, 245}, - {0x01, 2, 246}, - {0x16, 3, 246}, - {0x01, 2, 247}, - {0x16, 3, 247}, - {0x01, 2, 248}, - {0x16, 3, 248}, - {0x01, 2, 250}, - {0x16, 3, 250}, - {0x01, 2, 251}, - {0x16, 3, 251}, - {0x01, 2, 252}, - {0x16, 3, 252}, - {0x01, 2, 253}, - {0x16, 3, 253}, + {0x01, 0x02, 0xF5}, + {0x16, 0x03, 0xF5}, + {0x01, 0x02, 0xF6}, + {0x16, 0x03, 0xF6}, + {0x01, 0x02, 0xF7}, + {0x16, 0x03, 0xF7}, + {0x01, 0x02, 0xF8}, + {0x16, 0x03, 0xF8}, + {0x01, 0x02, 0xFA}, + {0x16, 0x03, 0xFA}, + {0x01, 0x02, 0xFB}, + {0x16, 0x03, 0xFB}, + {0x01, 0x02, 0xFC}, + {0x16, 0x03, 0xFC}, + {0x01, 0x02, 0xFD}, + {0x16, 0x03, 0xFD}, }, /* 217 */ { - {0x02, 2, 245}, - {0x09, 2, 245}, - {0x17, 2, 245}, - {0x28, 3, 245}, - {0x02, 2, 246}, - {0x09, 2, 246}, - {0x17, 2, 246}, - {0x28, 3, 246}, - {0x02, 2, 247}, - {0x09, 2, 247}, - {0x17, 2, 247}, - {0x28, 3, 247}, - {0x02, 2, 248}, - {0x09, 2, 248}, - {0x17, 2, 248}, - {0x28, 3, 248}, + {0x02, 0x02, 0xF5}, + {0x09, 0x02, 0xF5}, + {0x17, 0x02, 0xF5}, + {0x28, 0x03, 0xF5}, + {0x02, 0x02, 0xF6}, + {0x09, 0x02, 0xF6}, + {0x17, 0x02, 0xF6}, + {0x28, 0x03, 0xF6}, + {0x02, 0x02, 0xF7}, + {0x09, 0x02, 0xF7}, + {0x17, 0x02, 0xF7}, + {0x28, 0x03, 0xF7}, + {0x02, 0x02, 0xF8}, + {0x09, 0x02, 0xF8}, + {0x17, 0x02, 0xF8}, + {0x28, 0x03, 0xF8}, }, /* 218 */ { - {0x03, 2, 245}, - {0x06, 2, 245}, - {0x0a, 2, 245}, - {0x0f, 2, 245}, - {0x18, 2, 245}, - {0x1f, 2, 245}, - {0x29, 2, 245}, - {0x38, 3, 245}, - {0x03, 2, 246}, - {0x06, 2, 246}, - {0x0a, 2, 246}, - {0x0f, 2, 246}, - {0x18, 2, 246}, - {0x1f, 2, 246}, - {0x29, 2, 246}, - {0x38, 3, 246}, + {0x03, 0x02, 0xF5}, + {0x06, 0x02, 0xF5}, + {0x0A, 0x02, 0xF5}, + {0x0F, 0x02, 0xF5}, + {0x18, 0x02, 0xF5}, + {0x1F, 0x02, 0xF5}, + {0x29, 0x02, 0xF5}, + {0x38, 0x03, 0xF5}, + {0x03, 0x02, 0xF6}, + {0x06, 0x02, 0xF6}, + {0x0A, 0x02, 0xF6}, + {0x0F, 0x02, 0xF6}, + {0x18, 0x02, 0xF6}, + {0x1F, 0x02, 0xF6}, + {0x29, 0x02, 0xF6}, + {0x38, 0x03, 0xF6}, }, /* 219 */ { - {0x03, 2, 247}, - {0x06, 2, 247}, - {0x0a, 2, 247}, - {0x0f, 2, 247}, - {0x18, 2, 247}, - {0x1f, 2, 247}, - {0x29, 2, 247}, - {0x38, 3, 247}, - {0x03, 2, 248}, - {0x06, 2, 248}, - {0x0a, 2, 248}, - {0x0f, 2, 248}, - {0x18, 2, 248}, - {0x1f, 2, 248}, - {0x29, 2, 248}, - {0x38, 3, 248}, + {0x03, 0x02, 0xF7}, + {0x06, 0x02, 0xF7}, + {0x0A, 0x02, 0xF7}, + {0x0F, 0x02, 0xF7}, + {0x18, 0x02, 0xF7}, + {0x1F, 0x02, 0xF7}, + {0x29, 0x02, 0xF7}, + {0x38, 0x03, 0xF7}, + {0x03, 0x02, 0xF8}, + {0x06, 0x02, 0xF8}, + {0x0A, 0x02, 0xF8}, + {0x0F, 0x02, 0xF8}, + {0x18, 0x02, 0xF8}, + {0x1F, 0x02, 0xF8}, + {0x29, 0x02, 0xF8}, + {0x38, 0x03, 0xF8}, }, /* 220 */ { - {0x02, 2, 250}, - {0x09, 2, 250}, - {0x17, 2, 250}, - {0x28, 3, 250}, - {0x02, 2, 251}, - {0x09, 2, 251}, - {0x17, 2, 251}, - {0x28, 3, 251}, - {0x02, 2, 252}, - {0x09, 2, 252}, - {0x17, 2, 252}, - {0x28, 3, 252}, - {0x02, 2, 253}, - {0x09, 2, 253}, - {0x17, 2, 253}, - {0x28, 3, 253}, + {0x02, 0x02, 0xFA}, + {0x09, 0x02, 0xFA}, + {0x17, 0x02, 0xFA}, + {0x28, 0x03, 0xFA}, + {0x02, 0x02, 0xFB}, + {0x09, 0x02, 0xFB}, + {0x17, 0x02, 0xFB}, + {0x28, 0x03, 0xFB}, + {0x02, 0x02, 0xFC}, + {0x09, 0x02, 0xFC}, + {0x17, 0x02, 0xFC}, + {0x28, 0x03, 0xFC}, + {0x02, 0x02, 0xFD}, + {0x09, 0x02, 0xFD}, + {0x17, 0x02, 0xFD}, + {0x28, 0x03, 0xFD}, }, /* 221 */ { - {0x03, 2, 250}, - {0x06, 2, 250}, - {0x0a, 2, 250}, - {0x0f, 2, 250}, - {0x18, 2, 250}, - {0x1f, 2, 250}, - {0x29, 2, 250}, - {0x38, 3, 250}, - {0x03, 2, 251}, - {0x06, 2, 251}, - {0x0a, 2, 251}, - {0x0f, 2, 251}, - {0x18, 2, 251}, - {0x1f, 2, 251}, - {0x29, 2, 251}, - {0x38, 3, 251}, + {0x03, 0x02, 0xFA}, + {0x06, 0x02, 0xFA}, + {0x0A, 0x02, 0xFA}, + {0x0F, 0x02, 0xFA}, + {0x18, 0x02, 0xFA}, + {0x1F, 0x02, 0xFA}, + {0x29, 0x02, 0xFA}, + {0x38, 0x03, 0xFA}, + {0x03, 0x02, 0xFB}, + {0x06, 0x02, 0xFB}, + {0x0A, 0x02, 0xFB}, + {0x0F, 0x02, 0xFB}, + {0x18, 0x02, 0xFB}, + {0x1F, 0x02, 0xFB}, + {0x29, 0x02, 0xFB}, + {0x38, 0x03, 0xFB}, }, /* 222 */ { - {0x03, 2, 252}, - {0x06, 2, 252}, - {0x0a, 2, 252}, - {0x0f, 2, 252}, - {0x18, 2, 252}, - {0x1f, 2, 252}, - {0x29, 2, 252}, - {0x38, 3, 252}, - {0x03, 2, 253}, - {0x06, 2, 253}, - {0x0a, 2, 253}, - {0x0f, 2, 253}, - {0x18, 2, 253}, - {0x1f, 2, 253}, - {0x29, 2, 253}, - {0x38, 3, 253}, + {0x03, 0x02, 0xFC}, + {0x06, 0x02, 0xFC}, + {0x0A, 0x02, 0xFC}, + {0x0F, 0x02, 0xFC}, + {0x18, 0x02, 0xFC}, + {0x1F, 0x02, 0xFC}, + {0x29, 0x02, 0xFC}, + {0x38, 0x03, 0xFC}, + {0x03, 0x02, 0xFD}, + {0x06, 0x02, 0xFD}, + {0x0A, 0x02, 0xFD}, + {0x0F, 0x02, 0xFD}, + {0x18, 0x02, 0xFD}, + {0x1F, 0x02, 0xFD}, + {0x29, 0x02, 0xFD}, + {0x38, 0x03, 0xFD}, }, /* 223 */ { - {0x00, 3, 254}, - {0xe3, 0, 0}, - {0xe5, 0, 0}, - {0xe6, 0, 0}, - {0xe9, 0, 0}, - {0xea, 0, 0}, - {0xec, 0, 0}, - {0xed, 0, 0}, - {0xf1, 0, 0}, - {0xf2, 0, 0}, - {0xf4, 0, 0}, - {0xf5, 0, 0}, - {0xf8, 0, 0}, - {0xf9, 0, 0}, - {0xfb, 0, 0}, - {0xfc, 0, 0}, + {0x00, 0x03, 0xFE}, + {0xE3, 0x00, 0x00}, + {0xE5, 0x00, 0x00}, + {0xE6, 0x00, 0x00}, + {0xE9, 0x00, 0x00}, + {0xEA, 0x00, 0x00}, + {0xEC, 0x00, 0x00}, + {0xED, 0x00, 0x00}, + {0xF1, 0x00, 0x00}, + {0xF2, 0x00, 0x00}, + {0xF4, 0x00, 0x00}, + {0xF5, 0x00, 0x00}, + {0xF8, 0x00, 0x00}, + {0xF9, 0x00, 0x00}, + {0xFB, 0x00, 0x00}, + {0xFC, 0x00, 0x00}, }, /* 224 */ { - {0x01, 2, 254}, - {0x16, 3, 254}, - {0x00, 3, 2}, - {0x00, 3, 3}, - {0x00, 3, 4}, - {0x00, 3, 5}, - {0x00, 3, 6}, - {0x00, 3, 7}, - {0x00, 3, 8}, - {0x00, 3, 11}, - {0x00, 3, 12}, - {0x00, 3, 14}, - {0x00, 3, 15}, - {0x00, 3, 16}, - {0x00, 3, 17}, - {0x00, 3, 18}, + {0x01, 0x02, 0xFE}, + {0x16, 0x03, 0xFE}, + {0x00, 0x03, 0x02}, + {0x00, 0x03, 0x03}, + {0x00, 0x03, 0x04}, + {0x00, 0x03, 0x05}, + {0x00, 0x03, 0x06}, + {0x00, 0x03, 0x07}, + {0x00, 0x03, 0x08}, + {0x00, 0x03, 0x0B}, + {0x00, 0x03, 0x0C}, + {0x00, 0x03, 0x0E}, + {0x00, 0x03, 0x0F}, + {0x00, 0x03, 0x10}, + {0x00, 0x03, 0x11}, + {0x00, 0x03, 0x12}, }, /* 225 */ { - {0x02, 2, 254}, - {0x09, 2, 254}, - {0x17, 2, 254}, - {0x28, 3, 254}, - {0x01, 2, 2}, - {0x16, 3, 2}, - {0x01, 2, 3}, - {0x16, 3, 3}, - {0x01, 2, 4}, - {0x16, 3, 4}, - {0x01, 2, 5}, - {0x16, 3, 5}, - {0x01, 2, 6}, - {0x16, 3, 6}, - {0x01, 2, 7}, - {0x16, 3, 7}, + {0x02, 0x02, 0xFE}, + {0x09, 0x02, 0xFE}, + {0x17, 0x02, 0xFE}, + {0x28, 0x03, 0xFE}, + {0x01, 0x02, 0x02}, + {0x16, 0x03, 0x02}, + {0x01, 0x02, 0x03}, + {0x16, 0x03, 0x03}, + {0x01, 0x02, 0x04}, + {0x16, 0x03, 0x04}, + {0x01, 0x02, 0x05}, + {0x16, 0x03, 0x05}, + {0x01, 0x02, 0x06}, + {0x16, 0x03, 0x06}, + {0x01, 0x02, 0x07}, + {0x16, 0x03, 0x07}, }, /* 226 */ { - {0x03, 2, 254}, - {0x06, 2, 254}, - {0x0a, 2, 254}, - {0x0f, 2, 254}, - {0x18, 2, 254}, - {0x1f, 2, 254}, - {0x29, 2, 254}, - {0x38, 3, 254}, - {0x02, 2, 2}, - {0x09, 2, 2}, - {0x17, 2, 2}, - {0x28, 3, 2}, - {0x02, 2, 3}, - {0x09, 2, 3}, - {0x17, 2, 3}, - {0x28, 3, 3}, + {0x03, 0x02, 0xFE}, + {0x06, 0x02, 0xFE}, + {0x0A, 0x02, 0xFE}, + {0x0F, 0x02, 0xFE}, + {0x18, 0x02, 0xFE}, + {0x1F, 0x02, 0xFE}, + {0x29, 0x02, 0xFE}, + {0x38, 0x03, 0xFE}, + {0x02, 0x02, 0x02}, + {0x09, 0x02, 0x02}, + {0x17, 0x02, 0x02}, + {0x28, 0x03, 0x02}, + {0x02, 0x02, 0x03}, + {0x09, 0x02, 0x03}, + {0x17, 0x02, 0x03}, + {0x28, 0x03, 0x03}, }, /* 227 */ { - {0x03, 2, 2}, - {0x06, 2, 2}, - {0x0a, 2, 2}, - {0x0f, 2, 2}, - {0x18, 2, 2}, - {0x1f, 2, 2}, - {0x29, 2, 2}, - {0x38, 3, 2}, - {0x03, 2, 3}, - {0x06, 2, 3}, - {0x0a, 2, 3}, - {0x0f, 2, 3}, - {0x18, 2, 3}, - {0x1f, 2, 3}, - {0x29, 2, 3}, - {0x38, 3, 3}, + {0x03, 0x02, 0x02}, + {0x06, 0x02, 0x02}, + {0x0A, 0x02, 0x02}, + {0x0F, 0x02, 0x02}, + {0x18, 0x02, 0x02}, + {0x1F, 0x02, 0x02}, + {0x29, 0x02, 0x02}, + {0x38, 0x03, 0x02}, + {0x03, 0x02, 0x03}, + {0x06, 0x02, 0x03}, + {0x0A, 0x02, 0x03}, + {0x0F, 0x02, 0x03}, + {0x18, 0x02, 0x03}, + {0x1F, 0x02, 0x03}, + {0x29, 0x02, 0x03}, + {0x38, 0x03, 0x03}, }, /* 228 */ { - {0x02, 2, 4}, - {0x09, 2, 4}, - {0x17, 2, 4}, - {0x28, 3, 4}, - {0x02, 2, 5}, - {0x09, 2, 5}, - {0x17, 2, 5}, - {0x28, 3, 5}, - {0x02, 2, 6}, - {0x09, 2, 6}, - {0x17, 2, 6}, - {0x28, 3, 6}, - {0x02, 2, 7}, - {0x09, 2, 7}, - {0x17, 2, 7}, - {0x28, 3, 7}, + {0x02, 0x02, 0x04}, + {0x09, 0x02, 0x04}, + {0x17, 0x02, 0x04}, + {0x28, 0x03, 0x04}, + {0x02, 0x02, 0x05}, + {0x09, 0x02, 0x05}, + {0x17, 0x02, 0x05}, + {0x28, 0x03, 0x05}, + {0x02, 0x02, 0x06}, + {0x09, 0x02, 0x06}, + {0x17, 0x02, 0x06}, + {0x28, 0x03, 0x06}, + {0x02, 0x02, 0x07}, + {0x09, 0x02, 0x07}, + {0x17, 0x02, 0x07}, + {0x28, 0x03, 0x07}, }, /* 229 */ { - {0x03, 2, 4}, - {0x06, 2, 4}, - {0x0a, 2, 4}, - {0x0f, 2, 4}, - {0x18, 2, 4}, - {0x1f, 2, 4}, - {0x29, 2, 4}, - {0x38, 3, 4}, - {0x03, 2, 5}, - {0x06, 2, 5}, - {0x0a, 2, 5}, - {0x0f, 2, 5}, - {0x18, 2, 5}, - {0x1f, 2, 5}, - {0x29, 2, 5}, - {0x38, 3, 5}, + {0x03, 0x02, 0x04}, + {0x06, 0x02, 0x04}, + {0x0A, 0x02, 0x04}, + {0x0F, 0x02, 0x04}, + {0x18, 0x02, 0x04}, + {0x1F, 0x02, 0x04}, + {0x29, 0x02, 0x04}, + {0x38, 0x03, 0x04}, + {0x03, 0x02, 0x05}, + {0x06, 0x02, 0x05}, + {0x0A, 0x02, 0x05}, + {0x0F, 0x02, 0x05}, + {0x18, 0x02, 0x05}, + {0x1F, 0x02, 0x05}, + {0x29, 0x02, 0x05}, + {0x38, 0x03, 0x05}, }, /* 230 */ { - {0x03, 2, 6}, - {0x06, 2, 6}, - {0x0a, 2, 6}, - {0x0f, 2, 6}, - {0x18, 2, 6}, - {0x1f, 2, 6}, - {0x29, 2, 6}, - {0x38, 3, 6}, - {0x03, 2, 7}, - {0x06, 2, 7}, - {0x0a, 2, 7}, - {0x0f, 2, 7}, - {0x18, 2, 7}, - {0x1f, 2, 7}, - {0x29, 2, 7}, - {0x38, 3, 7}, + {0x03, 0x02, 0x06}, + {0x06, 0x02, 0x06}, + {0x0A, 0x02, 0x06}, + {0x0F, 0x02, 0x06}, + {0x18, 0x02, 0x06}, + {0x1F, 0x02, 0x06}, + {0x29, 0x02, 0x06}, + {0x38, 0x03, 0x06}, + {0x03, 0x02, 0x07}, + {0x06, 0x02, 0x07}, + {0x0A, 0x02, 0x07}, + {0x0F, 0x02, 0x07}, + {0x18, 0x02, 0x07}, + {0x1F, 0x02, 0x07}, + {0x29, 0x02, 0x07}, + {0x38, 0x03, 0x07}, }, /* 231 */ { - {0x01, 2, 8}, - {0x16, 3, 8}, - {0x01, 2, 11}, - {0x16, 3, 11}, - {0x01, 2, 12}, - {0x16, 3, 12}, - {0x01, 2, 14}, - {0x16, 3, 14}, - {0x01, 2, 15}, - {0x16, 3, 15}, - {0x01, 2, 16}, - {0x16, 3, 16}, - {0x01, 2, 17}, - {0x16, 3, 17}, - {0x01, 2, 18}, - {0x16, 3, 18}, + {0x01, 0x02, 0x08}, + {0x16, 0x03, 0x08}, + {0x01, 0x02, 0x0B}, + {0x16, 0x03, 0x0B}, + {0x01, 0x02, 0x0C}, + {0x16, 0x03, 0x0C}, + {0x01, 0x02, 0x0E}, + {0x16, 0x03, 0x0E}, + {0x01, 0x02, 0x0F}, + {0x16, 0x03, 0x0F}, + {0x01, 0x02, 0x10}, + {0x16, 0x03, 0x10}, + {0x01, 0x02, 0x11}, + {0x16, 0x03, 0x11}, + {0x01, 0x02, 0x12}, + {0x16, 0x03, 0x12}, }, /* 232 */ { - {0x02, 2, 8}, - {0x09, 2, 8}, - {0x17, 2, 8}, - {0x28, 3, 8}, - {0x02, 2, 11}, - {0x09, 2, 11}, - {0x17, 2, 11}, - {0x28, 3, 11}, - {0x02, 2, 12}, - {0x09, 2, 12}, - {0x17, 2, 12}, - {0x28, 3, 12}, - {0x02, 2, 14}, - {0x09, 2, 14}, - {0x17, 2, 14}, - {0x28, 3, 14}, + {0x02, 0x02, 0x08}, + {0x09, 0x02, 0x08}, + {0x17, 0x02, 0x08}, + {0x28, 0x03, 0x08}, + {0x02, 0x02, 0x0B}, + {0x09, 0x02, 0x0B}, + {0x17, 0x02, 0x0B}, + {0x28, 0x03, 0x0B}, + {0x02, 0x02, 0x0C}, + {0x09, 0x02, 0x0C}, + {0x17, 0x02, 0x0C}, + {0x28, 0x03, 0x0C}, + {0x02, 0x02, 0x0E}, + {0x09, 0x02, 0x0E}, + {0x17, 0x02, 0x0E}, + {0x28, 0x03, 0x0E}, }, /* 233 */ { - {0x03, 2, 8}, - {0x06, 2, 8}, - {0x0a, 2, 8}, - {0x0f, 2, 8}, - {0x18, 2, 8}, - {0x1f, 2, 8}, - {0x29, 2, 8}, - {0x38, 3, 8}, - {0x03, 2, 11}, - {0x06, 2, 11}, - {0x0a, 2, 11}, - {0x0f, 2, 11}, - {0x18, 2, 11}, - {0x1f, 2, 11}, - {0x29, 2, 11}, - {0x38, 3, 11}, + {0x03, 0x02, 0x08}, + {0x06, 0x02, 0x08}, + {0x0A, 0x02, 0x08}, + {0x0F, 0x02, 0x08}, + {0x18, 0x02, 0x08}, + {0x1F, 0x02, 0x08}, + {0x29, 0x02, 0x08}, + {0x38, 0x03, 0x08}, + {0x03, 0x02, 0x0B}, + {0x06, 0x02, 0x0B}, + {0x0A, 0x02, 0x0B}, + {0x0F, 0x02, 0x0B}, + {0x18, 0x02, 0x0B}, + {0x1F, 0x02, 0x0B}, + {0x29, 0x02, 0x0B}, + {0x38, 0x03, 0x0B}, }, /* 234 */ { - {0x03, 2, 12}, - {0x06, 2, 12}, - {0x0a, 2, 12}, - {0x0f, 2, 12}, - {0x18, 2, 12}, - {0x1f, 2, 12}, - {0x29, 2, 12}, - {0x38, 3, 12}, - {0x03, 2, 14}, - {0x06, 2, 14}, - {0x0a, 2, 14}, - {0x0f, 2, 14}, - {0x18, 2, 14}, - {0x1f, 2, 14}, - {0x29, 2, 14}, - {0x38, 3, 14}, + {0x03, 0x02, 0x0C}, + {0x06, 0x02, 0x0C}, + {0x0A, 0x02, 0x0C}, + {0x0F, 0x02, 0x0C}, + {0x18, 0x02, 0x0C}, + {0x1F, 0x02, 0x0C}, + {0x29, 0x02, 0x0C}, + {0x38, 0x03, 0x0C}, + {0x03, 0x02, 0x0E}, + {0x06, 0x02, 0x0E}, + {0x0A, 0x02, 0x0E}, + {0x0F, 0x02, 0x0E}, + {0x18, 0x02, 0x0E}, + {0x1F, 0x02, 0x0E}, + {0x29, 0x02, 0x0E}, + {0x38, 0x03, 0x0E}, }, /* 235 */ { - {0x02, 2, 15}, - {0x09, 2, 15}, - {0x17, 2, 15}, - {0x28, 3, 15}, - {0x02, 2, 16}, - {0x09, 2, 16}, - {0x17, 2, 16}, - {0x28, 3, 16}, - {0x02, 2, 17}, - {0x09, 2, 17}, - {0x17, 2, 17}, - {0x28, 3, 17}, - {0x02, 2, 18}, - {0x09, 2, 18}, - {0x17, 2, 18}, - {0x28, 3, 18}, + {0x02, 0x02, 0x0F}, + {0x09, 0x02, 0x0F}, + {0x17, 0x02, 0x0F}, + {0x28, 0x03, 0x0F}, + {0x02, 0x02, 0x10}, + {0x09, 0x02, 0x10}, + {0x17, 0x02, 0x10}, + {0x28, 0x03, 0x10}, + {0x02, 0x02, 0x11}, + {0x09, 0x02, 0x11}, + {0x17, 0x02, 0x11}, + {0x28, 0x03, 0x11}, + {0x02, 0x02, 0x12}, + {0x09, 0x02, 0x12}, + {0x17, 0x02, 0x12}, + {0x28, 0x03, 0x12}, }, /* 236 */ { - {0x03, 2, 15}, - {0x06, 2, 15}, - {0x0a, 2, 15}, - {0x0f, 2, 15}, - {0x18, 2, 15}, - {0x1f, 2, 15}, - {0x29, 2, 15}, - {0x38, 3, 15}, - {0x03, 2, 16}, - {0x06, 2, 16}, - {0x0a, 2, 16}, - {0x0f, 2, 16}, - {0x18, 2, 16}, - {0x1f, 2, 16}, - {0x29, 2, 16}, - {0x38, 3, 16}, + {0x03, 0x02, 0x0F}, + {0x06, 0x02, 0x0F}, + {0x0A, 0x02, 0x0F}, + {0x0F, 0x02, 0x0F}, + {0x18, 0x02, 0x0F}, + {0x1F, 0x02, 0x0F}, + {0x29, 0x02, 0x0F}, + {0x38, 0x03, 0x0F}, + {0x03, 0x02, 0x10}, + {0x06, 0x02, 0x10}, + {0x0A, 0x02, 0x10}, + {0x0F, 0x02, 0x10}, + {0x18, 0x02, 0x10}, + {0x1F, 0x02, 0x10}, + {0x29, 0x02, 0x10}, + {0x38, 0x03, 0x10}, }, /* 237 */ { - {0x03, 2, 17}, - {0x06, 2, 17}, - {0x0a, 2, 17}, - {0x0f, 2, 17}, - {0x18, 2, 17}, - {0x1f, 2, 17}, - {0x29, 2, 17}, - {0x38, 3, 17}, - {0x03, 2, 18}, - {0x06, 2, 18}, - {0x0a, 2, 18}, - {0x0f, 2, 18}, - {0x18, 2, 18}, - {0x1f, 2, 18}, - {0x29, 2, 18}, - {0x38, 3, 18}, + {0x03, 0x02, 0x11}, + {0x06, 0x02, 0x11}, + {0x0A, 0x02, 0x11}, + {0x0F, 0x02, 0x11}, + {0x18, 0x02, 0x11}, + {0x1F, 0x02, 0x11}, + {0x29, 0x02, 0x11}, + {0x38, 0x03, 0x11}, + {0x03, 0x02, 0x12}, + {0x06, 0x02, 0x12}, + {0x0A, 0x02, 0x12}, + {0x0F, 0x02, 0x12}, + {0x18, 0x02, 0x12}, + {0x1F, 0x02, 0x12}, + {0x29, 0x02, 0x12}, + {0x38, 0x03, 0x12}, }, /* 238 */ { - {0x00, 3, 19}, - {0x00, 3, 20}, - {0x00, 3, 21}, - {0x00, 3, 23}, - {0x00, 3, 24}, - {0x00, 3, 25}, - {0x00, 3, 26}, - {0x00, 3, 27}, - {0x00, 3, 28}, - {0x00, 3, 29}, - {0x00, 3, 30}, - {0x00, 3, 31}, - {0x00, 3, 127}, - {0x00, 3, 220}, - {0x00, 3, 249}, - {0xfd, 0, 0}, + {0x00, 0x03, 0x13}, + {0x00, 0x03, 0x14}, + {0x00, 0x03, 0x15}, + {0x00, 0x03, 0x17}, + {0x00, 0x03, 0x18}, + {0x00, 0x03, 0x19}, + {0x00, 0x03, 0x1A}, + {0x00, 0x03, 0x1B}, + {0x00, 0x03, 0x1C}, + {0x00, 0x03, 0x1D}, + {0x00, 0x03, 0x1E}, + {0x00, 0x03, 0x1F}, + {0x00, 0x03, 0x7F}, + {0x00, 0x03, 0xDC}, + {0x00, 0x03, 0xF9}, + {0xFD, 0x00, 0x00}, }, /* 239 */ { - {0x01, 2, 19}, - {0x16, 3, 19}, - {0x01, 2, 20}, - {0x16, 3, 20}, - {0x01, 2, 21}, - {0x16, 3, 21}, - {0x01, 2, 23}, - {0x16, 3, 23}, - {0x01, 2, 24}, - {0x16, 3, 24}, - {0x01, 2, 25}, - {0x16, 3, 25}, - {0x01, 2, 26}, - {0x16, 3, 26}, - {0x01, 2, 27}, - {0x16, 3, 27}, + {0x01, 0x02, 0x13}, + {0x16, 0x03, 0x13}, + {0x01, 0x02, 0x14}, + {0x16, 0x03, 0x14}, + {0x01, 0x02, 0x15}, + {0x16, 0x03, 0x15}, + {0x01, 0x02, 0x17}, + {0x16, 0x03, 0x17}, + {0x01, 0x02, 0x18}, + {0x16, 0x03, 0x18}, + {0x01, 0x02, 0x19}, + {0x16, 0x03, 0x19}, + {0x01, 0x02, 0x1A}, + {0x16, 0x03, 0x1A}, + {0x01, 0x02, 0x1B}, + {0x16, 0x03, 0x1B}, }, /* 240 */ { - {0x02, 2, 19}, - {0x09, 2, 19}, - {0x17, 2, 19}, - {0x28, 3, 19}, - {0x02, 2, 20}, - {0x09, 2, 20}, - {0x17, 2, 20}, - {0x28, 3, 20}, - {0x02, 2, 21}, - {0x09, 2, 21}, - {0x17, 2, 21}, - {0x28, 3, 21}, - {0x02, 2, 23}, - {0x09, 2, 23}, - {0x17, 2, 23}, - {0x28, 3, 23}, + {0x02, 0x02, 0x13}, + {0x09, 0x02, 0x13}, + {0x17, 0x02, 0x13}, + {0x28, 0x03, 0x13}, + {0x02, 0x02, 0x14}, + {0x09, 0x02, 0x14}, + {0x17, 0x02, 0x14}, + {0x28, 0x03, 0x14}, + {0x02, 0x02, 0x15}, + {0x09, 0x02, 0x15}, + {0x17, 0x02, 0x15}, + {0x28, 0x03, 0x15}, + {0x02, 0x02, 0x17}, + {0x09, 0x02, 0x17}, + {0x17, 0x02, 0x17}, + {0x28, 0x03, 0x17}, }, /* 241 */ { - {0x03, 2, 19}, - {0x06, 2, 19}, - {0x0a, 2, 19}, - {0x0f, 2, 19}, - {0x18, 2, 19}, - {0x1f, 2, 19}, - {0x29, 2, 19}, - {0x38, 3, 19}, - {0x03, 2, 20}, - {0x06, 2, 20}, - {0x0a, 2, 20}, - {0x0f, 2, 20}, - {0x18, 2, 20}, - {0x1f, 2, 20}, - {0x29, 2, 20}, - {0x38, 3, 20}, + {0x03, 0x02, 0x13}, + {0x06, 0x02, 0x13}, + {0x0A, 0x02, 0x13}, + {0x0F, 0x02, 0x13}, + {0x18, 0x02, 0x13}, + {0x1F, 0x02, 0x13}, + {0x29, 0x02, 0x13}, + {0x38, 0x03, 0x13}, + {0x03, 0x02, 0x14}, + {0x06, 0x02, 0x14}, + {0x0A, 0x02, 0x14}, + {0x0F, 0x02, 0x14}, + {0x18, 0x02, 0x14}, + {0x1F, 0x02, 0x14}, + {0x29, 0x02, 0x14}, + {0x38, 0x03, 0x14}, }, /* 242 */ { - {0x03, 2, 21}, - {0x06, 2, 21}, - {0x0a, 2, 21}, - {0x0f, 2, 21}, - {0x18, 2, 21}, - {0x1f, 2, 21}, - {0x29, 2, 21}, - {0x38, 3, 21}, - {0x03, 2, 23}, - {0x06, 2, 23}, - {0x0a, 2, 23}, - {0x0f, 2, 23}, - {0x18, 2, 23}, - {0x1f, 2, 23}, - {0x29, 2, 23}, - {0x38, 3, 23}, + {0x03, 0x02, 0x15}, + {0x06, 0x02, 0x15}, + {0x0A, 0x02, 0x15}, + {0x0F, 0x02, 0x15}, + {0x18, 0x02, 0x15}, + {0x1F, 0x02, 0x15}, + {0x29, 0x02, 0x15}, + {0x38, 0x03, 0x15}, + {0x03, 0x02, 0x17}, + {0x06, 0x02, 0x17}, + {0x0A, 0x02, 0x17}, + {0x0F, 0x02, 0x17}, + {0x18, 0x02, 0x17}, + {0x1F, 0x02, 0x17}, + {0x29, 0x02, 0x17}, + {0x38, 0x03, 0x17}, }, /* 243 */ { - {0x02, 2, 24}, - {0x09, 2, 24}, - {0x17, 2, 24}, - {0x28, 3, 24}, - {0x02, 2, 25}, - {0x09, 2, 25}, - {0x17, 2, 25}, - {0x28, 3, 25}, - {0x02, 2, 26}, - {0x09, 2, 26}, - {0x17, 2, 26}, - {0x28, 3, 26}, - {0x02, 2, 27}, - {0x09, 2, 27}, - {0x17, 2, 27}, - {0x28, 3, 27}, + {0x02, 0x02, 0x18}, + {0x09, 0x02, 0x18}, + {0x17, 0x02, 0x18}, + {0x28, 0x03, 0x18}, + {0x02, 0x02, 0x19}, + {0x09, 0x02, 0x19}, + {0x17, 0x02, 0x19}, + {0x28, 0x03, 0x19}, + {0x02, 0x02, 0x1A}, + {0x09, 0x02, 0x1A}, + {0x17, 0x02, 0x1A}, + {0x28, 0x03, 0x1A}, + {0x02, 0x02, 0x1B}, + {0x09, 0x02, 0x1B}, + {0x17, 0x02, 0x1B}, + {0x28, 0x03, 0x1B}, }, /* 244 */ { - {0x03, 2, 24}, - {0x06, 2, 24}, - {0x0a, 2, 24}, - {0x0f, 2, 24}, - {0x18, 2, 24}, - {0x1f, 2, 24}, - {0x29, 2, 24}, - {0x38, 3, 24}, - {0x03, 2, 25}, - {0x06, 2, 25}, - {0x0a, 2, 25}, - {0x0f, 2, 25}, - {0x18, 2, 25}, - {0x1f, 2, 25}, - {0x29, 2, 25}, - {0x38, 3, 25}, + {0x03, 0x02, 0x18}, + {0x06, 0x02, 0x18}, + {0x0A, 0x02, 0x18}, + {0x0F, 0x02, 0x18}, + {0x18, 0x02, 0x18}, + {0x1F, 0x02, 0x18}, + {0x29, 0x02, 0x18}, + {0x38, 0x03, 0x18}, + {0x03, 0x02, 0x19}, + {0x06, 0x02, 0x19}, + {0x0A, 0x02, 0x19}, + {0x0F, 0x02, 0x19}, + {0x18, 0x02, 0x19}, + {0x1F, 0x02, 0x19}, + {0x29, 0x02, 0x19}, + {0x38, 0x03, 0x19}, }, /* 245 */ { - {0x03, 2, 26}, - {0x06, 2, 26}, - {0x0a, 2, 26}, - {0x0f, 2, 26}, - {0x18, 2, 26}, - {0x1f, 2, 26}, - {0x29, 2, 26}, - {0x38, 3, 26}, - {0x03, 2, 27}, - {0x06, 2, 27}, - {0x0a, 2, 27}, - {0x0f, 2, 27}, - {0x18, 2, 27}, - {0x1f, 2, 27}, - {0x29, 2, 27}, - {0x38, 3, 27}, + {0x03, 0x02, 0x1A}, + {0x06, 0x02, 0x1A}, + {0x0A, 0x02, 0x1A}, + {0x0F, 0x02, 0x1A}, + {0x18, 0x02, 0x1A}, + {0x1F, 0x02, 0x1A}, + {0x29, 0x02, 0x1A}, + {0x38, 0x03, 0x1A}, + {0x03, 0x02, 0x1B}, + {0x06, 0x02, 0x1B}, + {0x0A, 0x02, 0x1B}, + {0x0F, 0x02, 0x1B}, + {0x18, 0x02, 0x1B}, + {0x1F, 0x02, 0x1B}, + {0x29, 0x02, 0x1B}, + {0x38, 0x03, 0x1B}, }, /* 246 */ { - {0x01, 2, 28}, - {0x16, 3, 28}, - {0x01, 2, 29}, - {0x16, 3, 29}, - {0x01, 2, 30}, - {0x16, 3, 30}, - {0x01, 2, 31}, - {0x16, 3, 31}, - {0x01, 2, 127}, - {0x16, 3, 127}, - {0x01, 2, 220}, - {0x16, 3, 220}, - {0x01, 2, 249}, - {0x16, 3, 249}, - {0xfe, 0, 0}, - {0xff, 0, 0}, + {0x01, 0x02, 0x1C}, + {0x16, 0x03, 0x1C}, + {0x01, 0x02, 0x1D}, + {0x16, 0x03, 0x1D}, + {0x01, 0x02, 0x1E}, + {0x16, 0x03, 0x1E}, + {0x01, 0x02, 0x1F}, + {0x16, 0x03, 0x1F}, + {0x01, 0x02, 0x7F}, + {0x16, 0x03, 0x7F}, + {0x01, 0x02, 0xDC}, + {0x16, 0x03, 0xDC}, + {0x01, 0x02, 0xF9}, + {0x16, 0x03, 0xF9}, + {0xFE, 0x00, 0x00}, + {0xFF, 0x00, 0x00}, }, /* 247 */ { - {0x02, 2, 28}, - {0x09, 2, 28}, - {0x17, 2, 28}, - {0x28, 3, 28}, - {0x02, 2, 29}, - {0x09, 2, 29}, - {0x17, 2, 29}, - {0x28, 3, 29}, - {0x02, 2, 30}, - {0x09, 2, 30}, - {0x17, 2, 30}, - {0x28, 3, 30}, - {0x02, 2, 31}, - {0x09, 2, 31}, - {0x17, 2, 31}, - {0x28, 3, 31}, + {0x02, 0x02, 0x1C}, + {0x09, 0x02, 0x1C}, + {0x17, 0x02, 0x1C}, + {0x28, 0x03, 0x1C}, + {0x02, 0x02, 0x1D}, + {0x09, 0x02, 0x1D}, + {0x17, 0x02, 0x1D}, + {0x28, 0x03, 0x1D}, + {0x02, 0x02, 0x1E}, + {0x09, 0x02, 0x1E}, + {0x17, 0x02, 0x1E}, + {0x28, 0x03, 0x1E}, + {0x02, 0x02, 0x1F}, + {0x09, 0x02, 0x1F}, + {0x17, 0x02, 0x1F}, + {0x28, 0x03, 0x1F}, }, /* 248 */ { - {0x03, 2, 28}, - {0x06, 2, 28}, - {0x0a, 2, 28}, - {0x0f, 2, 28}, - {0x18, 2, 28}, - {0x1f, 2, 28}, - {0x29, 2, 28}, - {0x38, 3, 28}, - {0x03, 2, 29}, - {0x06, 2, 29}, - {0x0a, 2, 29}, - {0x0f, 2, 29}, - {0x18, 2, 29}, - {0x1f, 2, 29}, - {0x29, 2, 29}, - {0x38, 3, 29}, + {0x03, 0x02, 0x1C}, + {0x06, 0x02, 0x1C}, + {0x0A, 0x02, 0x1C}, + {0x0F, 0x02, 0x1C}, + {0x18, 0x02, 0x1C}, + {0x1F, 0x02, 0x1C}, + {0x29, 0x02, 0x1C}, + {0x38, 0x03, 0x1C}, + {0x03, 0x02, 0x1D}, + {0x06, 0x02, 0x1D}, + {0x0A, 0x02, 0x1D}, + {0x0F, 0x02, 0x1D}, + {0x18, 0x02, 0x1D}, + {0x1F, 0x02, 0x1D}, + {0x29, 0x02, 0x1D}, + {0x38, 0x03, 0x1D}, }, /* 249 */ { - {0x03, 2, 30}, - {0x06, 2, 30}, - {0x0a, 2, 30}, - {0x0f, 2, 30}, - {0x18, 2, 30}, - {0x1f, 2, 30}, - {0x29, 2, 30}, - {0x38, 3, 30}, - {0x03, 2, 31}, - {0x06, 2, 31}, - {0x0a, 2, 31}, - {0x0f, 2, 31}, - {0x18, 2, 31}, - {0x1f, 2, 31}, - {0x29, 2, 31}, - {0x38, 3, 31}, + {0x03, 0x02, 0x1E}, + {0x06, 0x02, 0x1E}, + {0x0A, 0x02, 0x1E}, + {0x0F, 0x02, 0x1E}, + {0x18, 0x02, 0x1E}, + {0x1F, 0x02, 0x1E}, + {0x29, 0x02, 0x1E}, + {0x38, 0x03, 0x1E}, + {0x03, 0x02, 0x1F}, + {0x06, 0x02, 0x1F}, + {0x0A, 0x02, 0x1F}, + {0x0F, 0x02, 0x1F}, + {0x18, 0x02, 0x1F}, + {0x1F, 0x02, 0x1F}, + {0x29, 0x02, 0x1F}, + {0x38, 0x03, 0x1F}, }, /* 250 */ { - {0x02, 2, 127}, - {0x09, 2, 127}, - {0x17, 2, 127}, - {0x28, 3, 127}, - {0x02, 2, 220}, - {0x09, 2, 220}, - {0x17, 2, 220}, - {0x28, 3, 220}, - {0x02, 2, 249}, - {0x09, 2, 249}, - {0x17, 2, 249}, - {0x28, 3, 249}, - {0x00, 3, 10}, - {0x00, 3, 13}, - {0x00, 3, 22}, - {0x100, 0, 0}, + {0x02, 0x02, 0x7F}, + {0x09, 0x02, 0x7F}, + {0x17, 0x02, 0x7F}, + {0x28, 0x03, 0x7F}, + {0x02, 0x02, 0xDC}, + {0x09, 0x02, 0xDC}, + {0x17, 0x02, 0xDC}, + {0x28, 0x03, 0xDC}, + {0x02, 0x02, 0xF9}, + {0x09, 0x02, 0xF9}, + {0x17, 0x02, 0xF9}, + {0x28, 0x03, 0xF9}, + {0x00, 0x03, 0x0A}, + {0x00, 0x03, 0x0D}, + {0x00, 0x03, 0x16}, + {0x100, 0x00, 0x00}, }, /* 251 */ { - {0x03, 2, 127}, - {0x06, 2, 127}, - {0x0a, 2, 127}, - {0x0f, 2, 127}, - {0x18, 2, 127}, - {0x1f, 2, 127}, - {0x29, 2, 127}, - {0x38, 3, 127}, - {0x03, 2, 220}, - {0x06, 2, 220}, - {0x0a, 2, 220}, - {0x0f, 2, 220}, - {0x18, 2, 220}, - {0x1f, 2, 220}, - {0x29, 2, 220}, - {0x38, 3, 220}, + {0x03, 0x02, 0x7F}, + {0x06, 0x02, 0x7F}, + {0x0A, 0x02, 0x7F}, + {0x0F, 0x02, 0x7F}, + {0x18, 0x02, 0x7F}, + {0x1F, 0x02, 0x7F}, + {0x29, 0x02, 0x7F}, + {0x38, 0x03, 0x7F}, + {0x03, 0x02, 0xDC}, + {0x06, 0x02, 0xDC}, + {0x0A, 0x02, 0xDC}, + {0x0F, 0x02, 0xDC}, + {0x18, 0x02, 0xDC}, + {0x1F, 0x02, 0xDC}, + {0x29, 0x02, 0xDC}, + {0x38, 0x03, 0xDC}, }, /* 252 */ { - {0x03, 2, 249}, - {0x06, 2, 249}, - {0x0a, 2, 249}, - {0x0f, 2, 249}, - {0x18, 2, 249}, - {0x1f, 2, 249}, - {0x29, 2, 249}, - {0x38, 3, 249}, - {0x01, 2, 10}, - {0x16, 3, 10}, - {0x01, 2, 13}, - {0x16, 3, 13}, - {0x01, 2, 22}, - {0x16, 3, 22}, - {0x100, 0, 0}, - {0x100, 0, 0}, + {0x03, 0x02, 0xF9}, + {0x06, 0x02, 0xF9}, + {0x0A, 0x02, 0xF9}, + {0x0F, 0x02, 0xF9}, + {0x18, 0x02, 0xF9}, + {0x1F, 0x02, 0xF9}, + {0x29, 0x02, 0xF9}, + {0x38, 0x03, 0xF9}, + {0x01, 0x02, 0x0A}, + {0x16, 0x03, 0x0A}, + {0x01, 0x02, 0x0D}, + {0x16, 0x03, 0x0D}, + {0x01, 0x02, 0x16}, + {0x16, 0x03, 0x16}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, }, /* 253 */ { - {0x02, 2, 10}, - {0x09, 2, 10}, - {0x17, 2, 10}, - {0x28, 3, 10}, - {0x02, 2, 13}, - {0x09, 2, 13}, - {0x17, 2, 13}, - {0x28, 3, 13}, - {0x02, 2, 22}, - {0x09, 2, 22}, - {0x17, 2, 22}, - {0x28, 3, 22}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, + {0x02, 0x02, 0x0A}, + {0x09, 0x02, 0x0A}, + {0x17, 0x02, 0x0A}, + {0x28, 0x03, 0x0A}, + {0x02, 0x02, 0x0D}, + {0x09, 0x02, 0x0D}, + {0x17, 0x02, 0x0D}, + {0x28, 0x03, 0x0D}, + {0x02, 0x02, 0x16}, + {0x09, 0x02, 0x16}, + {0x17, 0x02, 0x16}, + {0x28, 0x03, 0x16}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, }, /* 254 */ { - {0x03, 2, 10}, - {0x06, 2, 10}, - {0x0a, 2, 10}, - {0x0f, 2, 10}, - {0x18, 2, 10}, - {0x1f, 2, 10}, - {0x29, 2, 10}, - {0x38, 3, 10}, - {0x03, 2, 13}, - {0x06, 2, 13}, - {0x0a, 2, 13}, - {0x0f, 2, 13}, - {0x18, 2, 13}, - {0x1f, 2, 13}, - {0x29, 2, 13}, - {0x38, 3, 13}, + {0x03, 0x02, 0x0A}, + {0x06, 0x02, 0x0A}, + {0x0A, 0x02, 0x0A}, + {0x0F, 0x02, 0x0A}, + {0x18, 0x02, 0x0A}, + {0x1F, 0x02, 0x0A}, + {0x29, 0x02, 0x0A}, + {0x38, 0x03, 0x0A}, + {0x03, 0x02, 0x0D}, + {0x06, 0x02, 0x0D}, + {0x0A, 0x02, 0x0D}, + {0x0F, 0x02, 0x0D}, + {0x18, 0x02, 0x0D}, + {0x1F, 0x02, 0x0D}, + {0x29, 0x02, 0x0D}, + {0x38, 0x03, 0x0D}, }, /* 255 */ { - {0x03, 2, 22}, - {0x06, 2, 22}, - {0x0a, 2, 22}, - {0x0f, 2, 22}, - {0x18, 2, 22}, - {0x1f, 2, 22}, - {0x29, 2, 22}, - {0x38, 3, 22}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, + {0x03, 0x02, 0x16}, + {0x06, 0x02, 0x16}, + {0x0A, 0x02, 0x16}, + {0x0F, 0x02, 0x16}, + {0x18, 0x02, 0x16}, + {0x1F, 0x02, 0x16}, + {0x29, 0x02, 0x16}, + {0x38, 0x03, 0x16}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, }, /* 256 */ { - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, - {0x100, 0, 0}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, + {0x100, 0x00, 0x00}, }, }; diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_range.c b/deps/ngtcp2/nghttp3/lib/nghttp3_range.c index f3039d81c4e542..d54d9c3d7385e4 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_range.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_range.c @@ -36,8 +36,8 @@ void nghttp3_range_init(nghttp3_range *r, uint64_t begin, uint64_t end) { nghttp3_range nghttp3_range_intersect(const nghttp3_range *a, const nghttp3_range *b) { nghttp3_range r; - uint64_t begin = nghttp3_max_uint64(a->begin, b->begin); - uint64_t end = nghttp3_min_uint64(a->end, b->end); + uint64_t begin = nghttp3_max(a->begin, b->begin); + uint64_t end = nghttp3_min(a->end, b->end); if (begin < end) { nghttp3_range_init(&r, begin, end); diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.c b/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.c index dac89ef12e51c7..f43714d2c20d30 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.c @@ -70,7 +70,7 @@ void nghttp3_ringbuf_free(nghttp3_ringbuf *rb) { void *nghttp3_ringbuf_push_front(nghttp3_ringbuf *rb) { rb->first = (rb->first - 1) & (rb->nmemb - 1); - rb->len = nghttp3_min_size(rb->nmemb, rb->len + 1); + rb->len = nghttp3_min(rb->nmemb, rb->len + 1); return (void *)&rb->buf[rb->first * rb->size]; } @@ -108,7 +108,9 @@ void *nghttp3_ringbuf_get(nghttp3_ringbuf *rb, size_t offset) { return &rb->buf[offset * rb->size]; } -int nghttp3_ringbuf_full(nghttp3_ringbuf *rb) { return rb->len == rb->nmemb; } +int nghttp3_ringbuf_full(const nghttp3_ringbuf *rb) { + return rb->len == rb->nmemb; +} int nghttp3_ringbuf_reserve(nghttp3_ringbuf *rb, size_t nmemb) { uint8_t *buf; diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.h b/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.h index 691c40b93d61e0..aae22aa8603f50 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.h @@ -108,7 +108,7 @@ static inline size_t nghttp3_ringbuf_len(const nghttp3_ringbuf *rb) { } /* nghttp3_ringbuf_full returns nonzero if |rb| is full. */ -int nghttp3_ringbuf_full(nghttp3_ringbuf *rb); +int nghttp3_ringbuf_full(const nghttp3_ringbuf *rb); int nghttp3_ringbuf_reserve(nghttp3_ringbuf *rb, size_t nmemb); diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_str.c b/deps/ngtcp2/nghttp3/lib/nghttp3_str.c index fc131404d13754..7564125cfdb3e8 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_str.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_str.c @@ -35,76 +35,76 @@ uint8_t *nghttp3_cpymem(uint8_t *dest, const uint8_t *src, size_t n) { } /* Generated by gendowncasetbl.py */ -static const uint8_t DOWNCASE_TBL[] = { - 0 /* NUL */, 1 /* SOH */, 2 /* STX */, 3 /* ETX */, - 4 /* EOT */, 5 /* ENQ */, 6 /* ACK */, 7 /* BEL */, - 8 /* BS */, 9 /* HT */, 10 /* LF */, 11 /* VT */, - 12 /* FF */, 13 /* CR */, 14 /* SO */, 15 /* SI */, - 16 /* DLE */, 17 /* DC1 */, 18 /* DC2 */, 19 /* DC3 */, - 20 /* DC4 */, 21 /* NAK */, 22 /* SYN */, 23 /* ETB */, - 24 /* CAN */, 25 /* EM */, 26 /* SUB */, 27 /* ESC */, - 28 /* FS */, 29 /* GS */, 30 /* RS */, 31 /* US */, - 32 /* SPC */, 33 /* ! */, 34 /* " */, 35 /* # */, - 36 /* $ */, 37 /* % */, 38 /* & */, 39 /* ' */, - 40 /* ( */, 41 /* ) */, 42 /* * */, 43 /* + */, - 44 /* , */, 45 /* - */, 46 /* . */, 47 /* / */, - 48 /* 0 */, 49 /* 1 */, 50 /* 2 */, 51 /* 3 */, - 52 /* 4 */, 53 /* 5 */, 54 /* 6 */, 55 /* 7 */, - 56 /* 8 */, 57 /* 9 */, 58 /* : */, 59 /* ; */, - 60 /* < */, 61 /* = */, 62 /* > */, 63 /* ? */, - 64 /* @ */, 97 /* A */, 98 /* B */, 99 /* C */, - 100 /* D */, 101 /* E */, 102 /* F */, 103 /* G */, - 104 /* H */, 105 /* I */, 106 /* J */, 107 /* K */, - 108 /* L */, 109 /* M */, 110 /* N */, 111 /* O */, - 112 /* P */, 113 /* Q */, 114 /* R */, 115 /* S */, - 116 /* T */, 117 /* U */, 118 /* V */, 119 /* W */, - 120 /* X */, 121 /* Y */, 122 /* Z */, 91 /* [ */, - 92 /* \ */, 93 /* ] */, 94 /* ^ */, 95 /* _ */, - 96 /* ` */, 97 /* a */, 98 /* b */, 99 /* c */, - 100 /* d */, 101 /* e */, 102 /* f */, 103 /* g */, - 104 /* h */, 105 /* i */, 106 /* j */, 107 /* k */, - 108 /* l */, 109 /* m */, 110 /* n */, 111 /* o */, - 112 /* p */, 113 /* q */, 114 /* r */, 115 /* s */, - 116 /* t */, 117 /* u */, 118 /* v */, 119 /* w */, - 120 /* x */, 121 /* y */, 122 /* z */, 123 /* { */, - 124 /* | */, 125 /* } */, 126 /* ~ */, 127 /* DEL */, - 128 /* 0x80 */, 129 /* 0x81 */, 130 /* 0x82 */, 131 /* 0x83 */, - 132 /* 0x84 */, 133 /* 0x85 */, 134 /* 0x86 */, 135 /* 0x87 */, - 136 /* 0x88 */, 137 /* 0x89 */, 138 /* 0x8a */, 139 /* 0x8b */, - 140 /* 0x8c */, 141 /* 0x8d */, 142 /* 0x8e */, 143 /* 0x8f */, - 144 /* 0x90 */, 145 /* 0x91 */, 146 /* 0x92 */, 147 /* 0x93 */, - 148 /* 0x94 */, 149 /* 0x95 */, 150 /* 0x96 */, 151 /* 0x97 */, - 152 /* 0x98 */, 153 /* 0x99 */, 154 /* 0x9a */, 155 /* 0x9b */, - 156 /* 0x9c */, 157 /* 0x9d */, 158 /* 0x9e */, 159 /* 0x9f */, - 160 /* 0xa0 */, 161 /* 0xa1 */, 162 /* 0xa2 */, 163 /* 0xa3 */, - 164 /* 0xa4 */, 165 /* 0xa5 */, 166 /* 0xa6 */, 167 /* 0xa7 */, - 168 /* 0xa8 */, 169 /* 0xa9 */, 170 /* 0xaa */, 171 /* 0xab */, - 172 /* 0xac */, 173 /* 0xad */, 174 /* 0xae */, 175 /* 0xaf */, - 176 /* 0xb0 */, 177 /* 0xb1 */, 178 /* 0xb2 */, 179 /* 0xb3 */, - 180 /* 0xb4 */, 181 /* 0xb5 */, 182 /* 0xb6 */, 183 /* 0xb7 */, - 184 /* 0xb8 */, 185 /* 0xb9 */, 186 /* 0xba */, 187 /* 0xbb */, - 188 /* 0xbc */, 189 /* 0xbd */, 190 /* 0xbe */, 191 /* 0xbf */, - 192 /* 0xc0 */, 193 /* 0xc1 */, 194 /* 0xc2 */, 195 /* 0xc3 */, - 196 /* 0xc4 */, 197 /* 0xc5 */, 198 /* 0xc6 */, 199 /* 0xc7 */, - 200 /* 0xc8 */, 201 /* 0xc9 */, 202 /* 0xca */, 203 /* 0xcb */, - 204 /* 0xcc */, 205 /* 0xcd */, 206 /* 0xce */, 207 /* 0xcf */, - 208 /* 0xd0 */, 209 /* 0xd1 */, 210 /* 0xd2 */, 211 /* 0xd3 */, - 212 /* 0xd4 */, 213 /* 0xd5 */, 214 /* 0xd6 */, 215 /* 0xd7 */, - 216 /* 0xd8 */, 217 /* 0xd9 */, 218 /* 0xda */, 219 /* 0xdb */, - 220 /* 0xdc */, 221 /* 0xdd */, 222 /* 0xde */, 223 /* 0xdf */, - 224 /* 0xe0 */, 225 /* 0xe1 */, 226 /* 0xe2 */, 227 /* 0xe3 */, - 228 /* 0xe4 */, 229 /* 0xe5 */, 230 /* 0xe6 */, 231 /* 0xe7 */, - 232 /* 0xe8 */, 233 /* 0xe9 */, 234 /* 0xea */, 235 /* 0xeb */, - 236 /* 0xec */, 237 /* 0xed */, 238 /* 0xee */, 239 /* 0xef */, - 240 /* 0xf0 */, 241 /* 0xf1 */, 242 /* 0xf2 */, 243 /* 0xf3 */, - 244 /* 0xf4 */, 245 /* 0xf5 */, 246 /* 0xf6 */, 247 /* 0xf7 */, - 248 /* 0xf8 */, 249 /* 0xf9 */, 250 /* 0xfa */, 251 /* 0xfb */, - 252 /* 0xfc */, 253 /* 0xfd */, 254 /* 0xfe */, 255 /* 0xff */, +const uint8_t nghttp3_downcase_tbl[] = { + 0x00 /* NUL */, 0x01 /* SOH */, 0x02 /* STX */, 0x03 /* ETX */, + 0x04 /* EOT */, 0x05 /* ENQ */, 0x06 /* ACK */, 0x07 /* BEL */, + 0x08 /* BS */, 0x09 /* HT */, 0x0A /* LF */, 0x0B /* VT */, + 0x0C /* FF */, 0x0D /* CR */, 0x0E /* SO */, 0x0F /* SI */, + 0x10 /* DLE */, 0x11 /* DC1 */, 0x12 /* DC2 */, 0x13 /* DC3 */, + 0x14 /* DC4 */, 0x15 /* NAK */, 0x16 /* SYN */, 0x17 /* ETB */, + 0x18 /* CAN */, 0x19 /* EM */, 0x1A /* SUB */, 0x1B /* ESC */, + 0x1C /* FS */, 0x1D /* GS */, 0x1E /* RS */, 0x1F /* US */, + 0x20 /* SPC */, 0x21 /* ! */, 0x22 /* " */, 0x23 /* # */, + 0x24 /* $ */, 0x25 /* % */, 0x26 /* & */, 0x27 /* ' */, + 0x28 /* ( */, 0x29 /* ) */, 0x2A /* * */, 0x2B /* + */, + 0x2C /* , */, 0x2D /* - */, 0x2E /* . */, 0x2F /* / */, + 0x30 /* 0 */, 0x31 /* 1 */, 0x32 /* 2 */, 0x33 /* 3 */, + 0x34 /* 4 */, 0x35 /* 5 */, 0x36 /* 6 */, 0x37 /* 7 */, + 0x38 /* 8 */, 0x39 /* 9 */, 0x3A /* : */, 0x3B /* ; */, + 0x3C /* < */, 0x3D /* = */, 0x3E /* > */, 0x3F /* ? */, + 0x40 /* @ */, 0x61 /* A */, 0x62 /* B */, 0x63 /* C */, + 0x64 /* D */, 0x65 /* E */, 0x66 /* F */, 0x67 /* G */, + 0x68 /* H */, 0x69 /* I */, 0x6A /* J */, 0x6B /* K */, + 0x6C /* L */, 0x6D /* M */, 0x6E /* N */, 0x6F /* O */, + 0x70 /* P */, 0x71 /* Q */, 0x72 /* R */, 0x73 /* S */, + 0x74 /* T */, 0x75 /* U */, 0x76 /* V */, 0x77 /* W */, + 0x78 /* X */, 0x79 /* Y */, 0x7A /* Z */, 0x5B /* [ */, + 0x5C /* \ */, 0x5D /* ] */, 0x5E /* ^ */, 0x5F /* _ */, + 0x60 /* ` */, 0x61 /* a */, 0x62 /* b */, 0x63 /* c */, + 0x64 /* d */, 0x65 /* e */, 0x66 /* f */, 0x67 /* g */, + 0x68 /* h */, 0x69 /* i */, 0x6A /* j */, 0x6B /* k */, + 0x6C /* l */, 0x6D /* m */, 0x6E /* n */, 0x6F /* o */, + 0x70 /* p */, 0x71 /* q */, 0x72 /* r */, 0x73 /* s */, + 0x74 /* t */, 0x75 /* u */, 0x76 /* v */, 0x77 /* w */, + 0x78 /* x */, 0x79 /* y */, 0x7A /* z */, 0x7B /* { */, + 0x7C /* | */, 0x7D /* } */, 0x7E /* ~ */, 0x7F /* DEL */, + 0x80 /* 0x80 */, 0x81 /* 0x81 */, 0x82 /* 0x82 */, 0x83 /* 0x83 */, + 0x84 /* 0x84 */, 0x85 /* 0x85 */, 0x86 /* 0x86 */, 0x87 /* 0x87 */, + 0x88 /* 0x88 */, 0x89 /* 0x89 */, 0x8A /* 0x8A */, 0x8B /* 0x8B */, + 0x8C /* 0x8C */, 0x8D /* 0x8D */, 0x8E /* 0x8E */, 0x8F /* 0x8F */, + 0x90 /* 0x90 */, 0x91 /* 0x91 */, 0x92 /* 0x92 */, 0x93 /* 0x93 */, + 0x94 /* 0x94 */, 0x95 /* 0x95 */, 0x96 /* 0x96 */, 0x97 /* 0x97 */, + 0x98 /* 0x98 */, 0x99 /* 0x99 */, 0x9A /* 0x9A */, 0x9B /* 0x9B */, + 0x9C /* 0x9C */, 0x9D /* 0x9D */, 0x9E /* 0x9E */, 0x9F /* 0x9F */, + 0xA0 /* 0xA0 */, 0xA1 /* 0xA1 */, 0xA2 /* 0xA2 */, 0xA3 /* 0xA3 */, + 0xA4 /* 0xA4 */, 0xA5 /* 0xA5 */, 0xA6 /* 0xA6 */, 0xA7 /* 0xA7 */, + 0xA8 /* 0xA8 */, 0xA9 /* 0xA9 */, 0xAA /* 0xAA */, 0xAB /* 0xAB */, + 0xAC /* 0xAC */, 0xAD /* 0xAD */, 0xAE /* 0xAE */, 0xAF /* 0xAF */, + 0xB0 /* 0xB0 */, 0xB1 /* 0xB1 */, 0xB2 /* 0xB2 */, 0xB3 /* 0xB3 */, + 0xB4 /* 0xB4 */, 0xB5 /* 0xB5 */, 0xB6 /* 0xB6 */, 0xB7 /* 0xB7 */, + 0xB8 /* 0xB8 */, 0xB9 /* 0xB9 */, 0xBA /* 0xBA */, 0xBB /* 0xBB */, + 0xBC /* 0xBC */, 0xBD /* 0xBD */, 0xBE /* 0xBE */, 0xBF /* 0xBF */, + 0xC0 /* 0xC0 */, 0xC1 /* 0xC1 */, 0xC2 /* 0xC2 */, 0xC3 /* 0xC3 */, + 0xC4 /* 0xC4 */, 0xC5 /* 0xC5 */, 0xC6 /* 0xC6 */, 0xC7 /* 0xC7 */, + 0xC8 /* 0xC8 */, 0xC9 /* 0xC9 */, 0xCA /* 0xCA */, 0xCB /* 0xCB */, + 0xCC /* 0xCC */, 0xCD /* 0xCD */, 0xCE /* 0xCE */, 0xCF /* 0xCF */, + 0xD0 /* 0xD0 */, 0xD1 /* 0xD1 */, 0xD2 /* 0xD2 */, 0xD3 /* 0xD3 */, + 0xD4 /* 0xD4 */, 0xD5 /* 0xD5 */, 0xD6 /* 0xD6 */, 0xD7 /* 0xD7 */, + 0xD8 /* 0xD8 */, 0xD9 /* 0xD9 */, 0xDA /* 0xDA */, 0xDB /* 0xDB */, + 0xDC /* 0xDC */, 0xDD /* 0xDD */, 0xDE /* 0xDE */, 0xDF /* 0xDF */, + 0xE0 /* 0xE0 */, 0xE1 /* 0xE1 */, 0xE2 /* 0xE2 */, 0xE3 /* 0xE3 */, + 0xE4 /* 0xE4 */, 0xE5 /* 0xE5 */, 0xE6 /* 0xE6 */, 0xE7 /* 0xE7 */, + 0xE8 /* 0xE8 */, 0xE9 /* 0xE9 */, 0xEA /* 0xEA */, 0xEB /* 0xEB */, + 0xEC /* 0xEC */, 0xED /* 0xED */, 0xEE /* 0xEE */, 0xEF /* 0xEF */, + 0xF0 /* 0xF0 */, 0xF1 /* 0xF1 */, 0xF2 /* 0xF2 */, 0xF3 /* 0xF3 */, + 0xF4 /* 0xF4 */, 0xF5 /* 0xF5 */, 0xF6 /* 0xF6 */, 0xF7 /* 0xF7 */, + 0xF8 /* 0xF8 */, 0xF9 /* 0xF9 */, 0xFA /* 0xFA */, 0xFB /* 0xFB */, + 0xFC /* 0xFC */, 0xFD /* 0xFD */, 0xFE /* 0xFE */, 0xFF /* 0xFF */, }; void nghttp3_downcase(uint8_t *s, size_t len) { size_t i; for (i = 0; i < len; ++i) { - s[i] = DOWNCASE_TBL[s[i]]; + s[i] = nghttp3_downcase_byte(s[i]); } } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_str.h b/deps/ngtcp2/nghttp3/lib/nghttp3_str.h index 280749a3a9a3d9..ee0562f26a3b0d 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_str.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_str.h @@ -37,4 +37,14 @@ uint8_t *nghttp3_cpymem(uint8_t *dest, const uint8_t *src, size_t n); void nghttp3_downcase(uint8_t *s, size_t len); +extern const uint8_t nghttp3_downcase_tbl[]; + +/* + * nghttp3_downcase_byte returns the lower case version of |c| if 'A' + * <= |c| && |c| <= 'Z'. Otherwise, it returns |c|. + */ +static inline uint8_t nghttp3_downcase_byte(uint8_t c) { + return nghttp3_downcase_tbl[c]; +} + #endif /* !defined(NGHTTP3_STR_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c index 9af7d009adf310..76db6fe303da6c 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c @@ -73,7 +73,7 @@ int nghttp3_stream_new(nghttp3_stream **pstream, int64_t stream_id, nghttp3_tnode_init(&stream->node, stream_id); - nghttp3_ringbuf_init(&stream->frq, 0, sizeof(nghttp3_frame_entry), mem); + nghttp3_ringbuf_init(&stream->frq, 0, sizeof(nghttp3_frame), mem); nghttp3_ringbuf_init(&stream->chunks, 0, sizeof(nghttp3_buf), mem); nghttp3_ringbuf_init(&stream->outq, 0, sizeof(nghttp3_typed_buf), mem); nghttp3_ringbuf_init(&stream->inq, 0, sizeof(nghttp3_buf), mem); @@ -136,17 +136,17 @@ static void delete_out_chunks(nghttp3_ringbuf *chunks, } static void delete_frq(nghttp3_ringbuf *frq, const nghttp3_mem *mem) { - nghttp3_frame_entry *frent; + nghttp3_frame *fr; size_t i, len = nghttp3_ringbuf_len(frq); for (i = 0; i < len; ++i) { - frent = nghttp3_ringbuf_get(frq, i); - switch (frent->fr.hd.type) { + fr = nghttp3_ringbuf_get(frq, i); + switch (fr->hd.type) { case NGHTTP3_FRAME_HEADERS: - nghttp3_frame_headers_free(&frent->fr.headers, mem); + nghttp3_frame_headers_free(&fr->headers, mem); break; case NGHTTP3_FRAME_PRIORITY_UPDATE: - nghttp3_frame_priority_update_free(&frent->fr.priority_update, mem); + nghttp3_frame_priority_update_free(&fr->priority_update, mem); break; default: break; @@ -172,11 +172,11 @@ void nghttp3_stream_del(nghttp3_stream *stream) { } void nghttp3_varint_read_state_reset(nghttp3_varint_read_state *rvint) { - memset(rvint, 0, sizeof(*rvint)); + *rvint = (nghttp3_varint_read_state){0}; } void nghttp3_stream_read_state_reset(nghttp3_stream_read_state *rstate) { - memset(rstate, 0, sizeof(*rstate)); + *rstate = (nghttp3_stream_read_state){0}; } nghttp3_ssize nghttp3_read_varint(nghttp3_varint_read_state *rvint, @@ -190,10 +190,10 @@ nghttp3_ssize nghttp3_read_varint(nghttp3_varint_read_state *rvint, if (rvint->left == 0) { assert(rvint->acc == 0); - vlen = nghttp3_get_varintlen(begin); - len = nghttp3_min_size(vlen, (size_t)(end - begin)); + vlen = nghttp3_get_uvarintlen(begin); + len = nghttp3_min(vlen, (size_t)(end - begin)); if (vlen <= len) { - nghttp3_get_varint(&rvint->acc, begin); + nghttp3_get_uvarint(&rvint->acc, begin); return (nghttp3_ssize)vlen; } @@ -203,19 +203,19 @@ nghttp3_ssize nghttp3_read_varint(nghttp3_varint_read_state *rvint, p = (uint8_t *)&rvint->acc + (sizeof(rvint->acc) - vlen); memcpy(p, begin, len); - *p &= 0x3f; + *p &= 0x3FU; rvint->left = vlen - len; return (nghttp3_ssize)len; } - len = nghttp3_min_size(rvint->left, (size_t)(end - begin)); + len = nghttp3_min(rvint->left, (size_t)(end - begin)); p = (uint8_t *)&rvint->acc + (sizeof(rvint->acc) - rvint->left); memcpy(p, begin, len); rvint->left -= len; if (rvint->left == 0) { - rvint->acc = (int64_t)nghttp3_ntohl64((uint64_t)rvint->acc); + rvint->acc = nghttp3_ntohl64(rvint->acc); } else if (fin) { return NGHTTP3_ERR_INVALID_ARGUMENT; } @@ -223,53 +223,49 @@ nghttp3_ssize nghttp3_read_varint(nghttp3_varint_read_state *rvint, return (nghttp3_ssize)len; } -int nghttp3_stream_frq_add(nghttp3_stream *stream, - const nghttp3_frame_entry *frent) { +int nghttp3_stream_frq_emplace(nghttp3_stream *stream, nghttp3_frame **pfr) { nghttp3_ringbuf *frq = &stream->frq; - nghttp3_frame_entry *dest; int rv; if (nghttp3_ringbuf_full(frq)) { - size_t nlen = - nghttp3_max_size(NGHTTP3_MIN_RBLEN, nghttp3_ringbuf_len(frq) * 2); + size_t nlen = nghttp3_max(NGHTTP3_MIN_RBLEN, nghttp3_ringbuf_len(frq) * 2); rv = nghttp3_ringbuf_reserve(frq, nlen); if (rv != 0) { return rv; } } - dest = nghttp3_ringbuf_push_back(frq); - *dest = *frent; + *pfr = nghttp3_ringbuf_push_back(frq); return 0; } int nghttp3_stream_fill_outq(nghttp3_stream *stream) { nghttp3_ringbuf *frq = &stream->frq; - nghttp3_frame_entry *frent; + nghttp3_frame *fr; int data_eof; int rv; for (; nghttp3_ringbuf_len(frq) && stream->unsent_bytes < NGHTTP3_MIN_UNSENT_BYTES;) { - frent = nghttp3_ringbuf_get(frq, 0); + fr = nghttp3_ringbuf_get(frq, 0); - switch (frent->fr.hd.type) { + switch (fr->hd.type) { case NGHTTP3_FRAME_SETTINGS: - rv = nghttp3_stream_write_settings(stream, frent); + rv = nghttp3_stream_write_settings(stream, &fr->settings); if (rv != 0) { return rv; } break; case NGHTTP3_FRAME_HEADERS: - rv = nghttp3_stream_write_headers(stream, frent); + rv = nghttp3_stream_write_headers(stream, &fr->headers); if (rv != 0) { return rv; } - nghttp3_frame_headers_free(&frent->fr.headers, stream->mem); + nghttp3_frame_headers_free(&fr->headers, stream->mem); break; case NGHTTP3_FRAME_DATA: - rv = nghttp3_stream_write_data(stream, &data_eof, frent); + rv = nghttp3_stream_write_data(stream, &data_eof, &fr->data); if (rv != 0) { return rv; } @@ -281,21 +277,20 @@ int nghttp3_stream_fill_outq(nghttp3_stream *stream) { } break; case NGHTTP3_FRAME_GOAWAY: - rv = nghttp3_stream_write_goaway(stream, frent); + rv = nghttp3_stream_write_goaway(stream, &fr->goaway); if (rv != 0) { return rv; } break; case NGHTTP3_FRAME_PRIORITY_UPDATE: - rv = nghttp3_stream_write_priority_update(stream, frent); + rv = nghttp3_stream_write_priority_update(stream, &fr->priority_update); if (rv != 0) { return rv; } - nghttp3_frame_priority_update_free(&frent->fr.priority_update, - stream->mem); + nghttp3_frame_priority_update_free(&fr->priority_update, stream->mem); break; case NGHTTP3_FRAME_ORIGIN: - rv = nghttp3_stream_write_origin(stream, frent); + rv = nghttp3_stream_write_origin(stream, &fr->origin); if (rv != 0) { return rv; } @@ -313,7 +308,7 @@ int nghttp3_stream_fill_outq(nghttp3_stream *stream) { } int nghttp3_stream_write_stream_type(nghttp3_stream *stream) { - size_t len = nghttp3_put_varintlen((int64_t)stream->type); + size_t len = nghttp3_put_uvarintlen(stream->type); nghttp3_buf *chunk; nghttp3_typed_buf tbuf; int rv; @@ -326,14 +321,14 @@ int nghttp3_stream_write_stream_type(nghttp3_stream *stream) { chunk = nghttp3_stream_get_chunk(stream); nghttp3_typed_buf_shared_init(&tbuf, chunk); - chunk->last = nghttp3_put_varint(chunk->last, (int64_t)stream->type); + chunk->last = nghttp3_put_uvarint(chunk->last, stream->type); tbuf.buf.last = chunk->last; return nghttp3_stream_outq_add(stream, &tbuf); } int nghttp3_stream_write_settings(nghttp3_stream *stream, - nghttp3_frame_entry *frent) { + const nghttp3_frame_settings *infr) { size_t len; int rv; nghttp3_buf *chunk; @@ -344,8 +339,8 @@ int nghttp3_stream_write_settings(nghttp3_stream *stream, .niv = 3, .iv = ents, }; - nghttp3_settings *local_settings = frent->aux.settings.local_settings; - int64_t payloadlen; + const nghttp3_settings *local_settings = infr->local_settings; + uint64_t payloadlen; ents[0] = (nghttp3_settings_entry){ .id = NGHTTP3_SETTINGS_ID_MAX_FIELD_SECTION_SIZE, @@ -396,13 +391,12 @@ int nghttp3_stream_write_settings(nghttp3_stream *stream, } int nghttp3_stream_write_goaway(nghttp3_stream *stream, - nghttp3_frame_entry *frent) { - nghttp3_frame_goaway *fr = &frent->fr.goaway; + const nghttp3_frame_goaway *fr) { size_t len; int rv; nghttp3_buf *chunk; nghttp3_typed_buf tbuf; - int64_t payloadlen; + uint64_t payloadlen; len = nghttp3_frame_write_goaway_len(&payloadlen, fr); @@ -421,14 +415,13 @@ int nghttp3_stream_write_goaway(nghttp3_stream *stream, return nghttp3_stream_outq_add(stream, &tbuf); } -int nghttp3_stream_write_priority_update(nghttp3_stream *stream, - nghttp3_frame_entry *frent) { - nghttp3_frame_priority_update *fr = &frent->fr.priority_update; +int nghttp3_stream_write_priority_update( + nghttp3_stream *stream, const nghttp3_frame_priority_update *fr) { size_t len; int rv; nghttp3_buf *chunk; nghttp3_typed_buf tbuf; - int64_t payloadlen; + uint64_t payloadlen; len = nghttp3_frame_write_priority_update_len(&payloadlen, fr); @@ -449,15 +442,14 @@ int nghttp3_stream_write_priority_update(nghttp3_stream *stream, } int nghttp3_stream_write_origin(nghttp3_stream *stream, - nghttp3_frame_entry *frent) { - nghttp3_frame_origin *fr = &frent->fr.origin; + const nghttp3_frame_origin *fr) { nghttp3_buf *chunk; nghttp3_buf buf; nghttp3_typed_buf tbuf; int rv; rv = nghttp3_stream_ensure_chunk( - stream, nghttp3_frame_write_hd_len(fr->type, (int64_t)fr->origin_list.len)); + stream, nghttp3_frame_write_hd_len(fr->type, fr->origin_list.len)); if (rv != 0) { return rv; } @@ -466,7 +458,7 @@ int nghttp3_stream_write_origin(nghttp3_stream *stream, nghttp3_typed_buf_shared_init(&tbuf, chunk); chunk->last = - nghttp3_frame_write_hd(chunk->last, fr->type, (int64_t)fr->origin_list.len); + nghttp3_frame_write_hd(chunk->last, fr->type, fr->origin_list.len); tbuf.buf.last = chunk->last; @@ -488,8 +480,7 @@ int nghttp3_stream_write_origin(nghttp3_stream *stream, } int nghttp3_stream_write_headers(nghttp3_stream *stream, - nghttp3_frame_entry *frent) { - nghttp3_frame_headers *fr = &frent->fr.headers; + const nghttp3_frame_headers *fr) { nghttp3_conn *conn = stream->conn; assert(conn); @@ -503,8 +494,8 @@ int nghttp3_stream_write_header_block(nghttp3_stream *stream, nghttp3_qpack_encoder *qenc, nghttp3_stream *qenc_stream, nghttp3_buf *rbuf, nghttp3_buf *ebuf, - int64_t frame_type, const nghttp3_nv *nva, - size_t nvlen) { + uint64_t frame_type, + const nghttp3_nv *nva, size_t nvlen) { nghttp3_buf pbuf; int rv; size_t len; @@ -512,7 +503,7 @@ int nghttp3_stream_write_header_block(nghttp3_stream *stream, nghttp3_typed_buf tbuf; uint8_t raw_pbuf[16]; size_t pbuflen, rbuflen, ebuflen; - int64_t payloadlen; + uint64_t payloadlen; nghttp3_buf_wrap_init(&pbuf, raw_pbuf, sizeof(raw_pbuf)); @@ -526,7 +517,7 @@ int nghttp3_stream_write_header_block(nghttp3_stream *stream, rbuflen = nghttp3_buf_len(rbuf); ebuflen = nghttp3_buf_len(ebuf); - payloadlen = (int64_t)(pbuflen + rbuflen); + payloadlen = pbuflen + rbuflen; len = nghttp3_frame_write_hd_len(frame_type, payloadlen) + pbuflen; @@ -610,15 +601,15 @@ int nghttp3_stream_write_header_block(nghttp3_stream *stream, } int nghttp3_stream_write_data(nghttp3_stream *stream, int *peof, - nghttp3_frame_entry *frent) { + const nghttp3_frame_data *fr) { int rv; size_t len; nghttp3_typed_buf tbuf; nghttp3_buf buf; nghttp3_buf *chunk; - nghttp3_read_data_callback read_data = frent->aux.data.dr.read_data; + nghttp3_read_data_callback read_data = fr->dr.read_data; nghttp3_conn *conn = stream->conn; - int64_t datalen; + uint64_t datalen; uint32_t flags = 0; nghttp3_vec vec[8]; nghttp3_vec *v; @@ -641,8 +632,8 @@ int nghttp3_stream_write_data(nghttp3_stream *stream, int *peof, return NGHTTP3_ERR_CALLBACK_FAILURE; } - datalen = nghttp3_vec_len_varint(vec, (size_t)sveccnt); - if (datalen == -1) { + rv = nghttp3_vec_len_uvarint(&datalen, vec, (size_t)sveccnt); + if (rv == -1) { return NGHTTP3_ERR_STREAM_DATA_OVERFLOW; } @@ -726,7 +717,7 @@ int nghttp3_stream_write_qpack_decoder_stream(nghttp3_stream *stream) { assert(qdec); - len = nghttp3_qpack_decoder_get_decoder_streamlen(qdec); + len = nghttp3_qpack_decoder_get_decoder_streamlen2(qdec); if (len == 0) { return 0; } @@ -780,7 +771,7 @@ int nghttp3_stream_outq_add(nghttp3_stream *stream, } if (nghttp3_ringbuf_full(outq)) { - size_t nlen = nghttp3_max_size(NGHTTP3_MIN_RBLEN, len * 2); + size_t nlen = nghttp3_max(NGHTTP3_MIN_RBLEN, len * 2); rv = nghttp3_ringbuf_reserve(outq, nlen); if (rv != 0) { return rv; @@ -822,7 +813,7 @@ int nghttp3_stream_ensure_chunk(nghttp3_stream *stream, size_t need) { } if (nghttp3_ringbuf_full(chunks)) { - size_t nlen = nghttp3_max_size(NGHTTP3_MIN_RBLEN, len * 2); + size_t nlen = nghttp3_max(NGHTTP3_MIN_RBLEN, len * 2); rv = nghttp3_ringbuf_reserve(chunks, nlen); if (rv != 0) { return rv; @@ -844,13 +835,13 @@ nghttp3_buf *nghttp3_stream_get_chunk(nghttp3_stream *stream) { return nghttp3_ringbuf_get(chunks, len - 1); } -int nghttp3_stream_is_blocked(nghttp3_stream *stream) { +int nghttp3_stream_is_blocked(const nghttp3_stream *stream) { return (stream->flags & NGHTTP3_STREAM_FLAG_FC_BLOCKED) || (stream->flags & NGHTTP3_STREAM_FLAG_SHUT_WR) || (stream->flags & NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED); } -int nghttp3_stream_require_schedule(nghttp3_stream *stream) { +int nghttp3_stream_require_schedule(const nghttp3_stream *stream) { return (!nghttp3_stream_outq_write_done(stream) && !(stream->flags & NGHTTP3_STREAM_FLAG_FC_BLOCKED) && !(stream->flags & NGHTTP3_STREAM_FLAG_SHUT_WR)) || @@ -918,8 +909,8 @@ void nghttp3_stream_add_outq_offset(nghttp3_stream *stream, size_t n) { stream->outq_idx = i; } -int nghttp3_stream_outq_write_done(nghttp3_stream *stream) { - nghttp3_ringbuf *outq = &stream->outq; +int nghttp3_stream_outq_write_done(const nghttp3_stream *stream) { + const nghttp3_ringbuf *outq = &stream->outq; size_t len = nghttp3_ringbuf_len(outq); return len == 0 || stream->outq_idx >= len; @@ -975,8 +966,8 @@ int nghttp3_stream_update_ack_offset(nghttp3_stream *stream, uint64_t offset) { /* For NGHTTP3_BUF_TYPE_ALIEN, we never add 0 length buffer. */ if (tbuf->type == NGHTTP3_BUF_TYPE_ALIEN && stream->ack_offset < offset && stream->callbacks.acked_data) { - nack = nghttp3_min_uint64(offset, stream->ack_base + buflen) - - stream->ack_offset; + nack = + nghttp3_min(offset, stream->ack_base + buflen) - stream->ack_offset; rv = stream->callbacks.acked_data(stream, stream->node.id, nack, stream->user_data); @@ -1017,7 +1008,7 @@ int nghttp3_stream_buffer_data(nghttp3_stream *stream, const uint8_t *data, if (len) { buf = nghttp3_ringbuf_get(inq, len - 1); bufleft = nghttp3_buf_left(buf); - nwrite = nghttp3_min_size(datalen, bufleft); + nwrite = nghttp3_min(datalen, bufleft); buf->last = nghttp3_cpymem(buf->last, data, nwrite); data += nwrite; datalen -= nwrite; @@ -1026,7 +1017,7 @@ int nghttp3_stream_buffer_data(nghttp3_stream *stream, const uint8_t *data, for (; datalen;) { if (nghttp3_ringbuf_full(inq)) { size_t nlen = - nghttp3_max_size(NGHTTP3_MIN_RBLEN, nghttp3_ringbuf_len(inq) * 2); + nghttp3_max(NGHTTP3_MIN_RBLEN, nghttp3_ringbuf_len(inq) * 2); rv = nghttp3_ringbuf_reserve(inq, nlen); if (rv != 0) { return rv; @@ -1041,7 +1032,7 @@ int nghttp3_stream_buffer_data(nghttp3_stream *stream, const uint8_t *data, buf = nghttp3_ringbuf_push_back(inq); nghttp3_buf_wrap_init(buf, rawbuf, 16384); bufleft = nghttp3_buf_left(buf); - nwrite = nghttp3_min_size(datalen, bufleft); + nwrite = nghttp3_min(datalen, bufleft); buf->last = nghttp3_cpymem(buf->last, data, nwrite); data += nwrite; datalen -= nwrite; @@ -1235,7 +1226,7 @@ int nghttp3_stream_transit_rx_http_state(nghttp3_stream *stream, } } -int nghttp3_stream_empty_headers_allowed(nghttp3_stream *stream) { +int nghttp3_stream_empty_headers_allowed(const nghttp3_stream *stream) { switch (stream->rx.hstate) { case NGHTTP3_HTTP_STATE_REQ_TRAILERS_BEGIN: case NGHTTP3_HTTP_STATE_RESP_TRAILERS_BEGIN: diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h index 45c4c257670289..61a1f085ac8709 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h @@ -52,10 +52,10 @@ /* nghttp3_stream_type is unidirectional stream type. */ typedef uint64_t nghttp3_stream_type; -#define NGHTTP3_STREAM_TYPE_CONTROL 0x00 -#define NGHTTP3_STREAM_TYPE_PUSH 0x01 -#define NGHTTP3_STREAM_TYPE_QPACK_ENCODER 0x02 -#define NGHTTP3_STREAM_TYPE_QPACK_DECODER 0x03 +#define NGHTTP3_STREAM_TYPE_CONTROL 0x00U +#define NGHTTP3_STREAM_TYPE_PUSH 0x01U +#define NGHTTP3_STREAM_TYPE_QPACK_ENCODER 0x02U +#define NGHTTP3_STREAM_TYPE_QPACK_DECODER 0x03U #define NGHTTP3_STREAM_TYPE_UNKNOWN UINT64_MAX typedef enum nghttp3_ctrl_stream_state { @@ -83,7 +83,7 @@ typedef enum nghttp3_req_stream_state { } nghttp3_req_stream_state; typedef struct nghttp3_varint_read_state { - int64_t acc; + uint64_t acc; size_t left; } nghttp3_varint_read_state; @@ -91,43 +91,43 @@ typedef struct nghttp3_stream_read_state { nghttp3_varint_read_state rvint; nghttp3_settings_entry iv; nghttp3_frame fr; - int64_t left; + uint64_t left; int state; } nghttp3_stream_read_state; /* NGHTTP3_STREAM_FLAG_NONE indicates that no flag is set. */ -#define NGHTTP3_STREAM_FLAG_NONE 0x0000u +#define NGHTTP3_STREAM_FLAG_NONE 0x0000U /* NGHTTP3_STREAM_FLAG_TYPE_IDENTIFIED is set when a unidirectional stream type is identified. */ -#define NGHTTP3_STREAM_FLAG_TYPE_IDENTIFIED 0x0001u +#define NGHTTP3_STREAM_FLAG_TYPE_IDENTIFIED 0x0001U /* NGHTTP3_STREAM_FLAG_FC_BLOCKED indicates that stream is blocked by QUIC flow control. */ -#define NGHTTP3_STREAM_FLAG_FC_BLOCKED 0x0002u +#define NGHTTP3_STREAM_FLAG_FC_BLOCKED 0x0002U /* NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED indicates that application is temporarily unable to provide data. */ -#define NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED 0x0004u +#define NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED 0x0004U /* NGHTTP3_STREAM_FLAG_WRITE_END_STREAM indicates that application finished to feed outgoing data. */ -#define NGHTTP3_STREAM_FLAG_WRITE_END_STREAM 0x0008u +#define NGHTTP3_STREAM_FLAG_WRITE_END_STREAM 0x0008U /* NGHTTP3_STREAM_FLAG_QPACK_DECODE_BLOCKED indicates that stream is blocked due to QPACK decoding. */ -#define NGHTTP3_STREAM_FLAG_QPACK_DECODE_BLOCKED 0x0010u +#define NGHTTP3_STREAM_FLAG_QPACK_DECODE_BLOCKED 0x0010U /* NGHTTP3_STREAM_FLAG_READ_EOF indicates that remote endpoint sent fin. */ -#define NGHTTP3_STREAM_FLAG_READ_EOF 0x0020u +#define NGHTTP3_STREAM_FLAG_READ_EOF 0x0020U /* NGHTTP3_STREAM_FLAG_SHUT_WR indicates that any further write operation to a stream is prohibited. */ -#define NGHTTP3_STREAM_FLAG_SHUT_WR 0x0100u +#define NGHTTP3_STREAM_FLAG_SHUT_WR 0x0100U /* NGHTTP3_STREAM_FLAG_SHUT_RD indicates that a read-side stream is closed abruptly and any incoming and pending stream data is just discarded for a stream. */ -#define NGHTTP3_STREAM_FLAG_SHUT_RD 0x0200u +#define NGHTTP3_STREAM_FLAG_SHUT_RD 0x0200U /* NGHTTP3_STREAM_FLAG_SERVER_PRIORITY_SET indicates that server overrides stream priority. */ -#define NGHTTP3_STREAM_FLAG_SERVER_PRIORITY_SET 0x0400u +#define NGHTTP3_STREAM_FLAG_SERVER_PRIORITY_SET 0x0400U /* NGHTTP3_STREAM_FLAG_PRIORITY_UPDATE_RECVED indicates that server received PRIORITY_UPDATE frame for this stream. */ -#define NGHTTP3_STREAM_FLAG_PRIORITY_UPDATE_RECVED 0x0800u +#define NGHTTP3_STREAM_FLAG_PRIORITY_UPDATE_RECVED 0x0800U typedef enum nghttp3_stream_http_state { NGHTTP3_HTTP_STATE_NONE, @@ -246,18 +246,6 @@ struct nghttp3_stream { nghttp3_objalloc_decl(stream, nghttp3_stream, oplent) -typedef struct nghttp3_frame_entry { - nghttp3_frame fr; - union { - struct { - nghttp3_settings *local_settings; - } settings; - struct { - nghttp3_data_reader dr; - } data; - } aux; -} nghttp3_frame_entry; - int nghttp3_stream_new(nghttp3_stream **pstream, int64_t stream_id, const nghttp3_stream_callbacks *callbacks, nghttp3_objalloc *out_chunk_objalloc, @@ -274,8 +262,18 @@ nghttp3_ssize nghttp3_read_varint(nghttp3_varint_read_state *rvint, const uint8_t *begin, const uint8_t *end, int fin); -int nghttp3_stream_frq_add(nghttp3_stream *stream, - const nghttp3_frame_entry *frent); +/* + * nghttp3_stream_frq_emplace adds new space for nghttp3_frame to + * stream->frq, and assigns the pointer to the space to |*pfr| if it + * succeeds. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * NGHTTP3_ERR_NOMEM + * Out of memory. + */ +int nghttp3_stream_frq_emplace(nghttp3_stream *stream, nghttp3_frame **pfr); int nghttp3_stream_fill_outq(nghttp3_stream *stream); @@ -290,35 +288,35 @@ int nghttp3_stream_outq_add(nghttp3_stream *stream, const nghttp3_typed_buf *tbuf); int nghttp3_stream_write_headers(nghttp3_stream *stream, - nghttp3_frame_entry *frent); + const nghttp3_frame_headers *fr); int nghttp3_stream_write_header_block(nghttp3_stream *stream, nghttp3_qpack_encoder *qenc, nghttp3_stream *qenc_stream, nghttp3_buf *rbuf, nghttp3_buf *ebuf, - int64_t frame_type, const nghttp3_nv *nva, - size_t nvlen); + uint64_t frame_type, + const nghttp3_nv *nva, size_t nvlen); int nghttp3_stream_write_data(nghttp3_stream *stream, int *peof, - nghttp3_frame_entry *frent); + const nghttp3_frame_data *fr); int nghttp3_stream_write_settings(nghttp3_stream *stream, - nghttp3_frame_entry *frent); + const nghttp3_frame_settings *fr); int nghttp3_stream_write_goaway(nghttp3_stream *stream, - nghttp3_frame_entry *frent); + const nghttp3_frame_goaway *fr); -int nghttp3_stream_write_priority_update(nghttp3_stream *stream, - nghttp3_frame_entry *frent); +int nghttp3_stream_write_priority_update( + nghttp3_stream *stream, const nghttp3_frame_priority_update *fr); int nghttp3_stream_write_origin(nghttp3_stream *stream, - nghttp3_frame_entry *frent); + const nghttp3_frame_origin *fr); int nghttp3_stream_ensure_chunk(nghttp3_stream *stream, size_t need); nghttp3_buf *nghttp3_stream_get_chunk(nghttp3_stream *stream); -int nghttp3_stream_is_blocked(nghttp3_stream *stream); +int nghttp3_stream_is_blocked(const nghttp3_stream *stream); void nghttp3_stream_add_outq_offset(nghttp3_stream *stream, size_t n); @@ -326,7 +324,7 @@ void nghttp3_stream_add_outq_offset(nghttp3_stream *stream, size_t n); * nghttp3_stream_outq_write_done returns nonzero if all contents in * outq have been written. */ -int nghttp3_stream_outq_write_done(nghttp3_stream *stream); +int nghttp3_stream_outq_write_done(const nghttp3_stream *stream); /* * nghttp2_stream_update_ack_offset updates the last acknowledged @@ -346,7 +344,7 @@ int nghttp3_stream_is_active(nghttp3_stream *stream); * be scheduled. In other words, |stream| or its descendants have * something to send. */ -int nghttp3_stream_require_schedule(nghttp3_stream *stream); +int nghttp3_stream_require_schedule(const nghttp3_stream *stream); int nghttp3_stream_buffer_data(nghttp3_stream *stream, const uint8_t *src, size_t srclen); @@ -360,7 +358,7 @@ void nghttp3_stream_delete_qpack_stream_context(nghttp3_stream *stream); int nghttp3_stream_transit_rx_http_state(nghttp3_stream *stream, nghttp3_stream_http_event event); -int nghttp3_stream_empty_headers_allowed(nghttp3_stream *stream); +int nghttp3_stream_empty_headers_allowed(const nghttp3_stream *stream); /* * nghttp3_stream_uni returns nonzero if stream identified by diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_tnode.c b/deps/ngtcp2/nghttp3/lib/nghttp3_tnode.c index 4a969df1946386..f8f4be9477c4f2 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_tnode.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_tnode.c @@ -56,7 +56,7 @@ void nghttp3_tnode_unschedule(nghttp3_tnode *tnode, nghttp3_pq *pq) { tnode_unschedule(tnode, pq); } -static uint64_t pq_get_first_cycle(nghttp3_pq *pq) { +static uint64_t pq_get_first_cycle(const nghttp3_pq *pq) { nghttp3_tnode *top; if (nghttp3_pq_empty(pq)) { @@ -74,7 +74,7 @@ int nghttp3_tnode_schedule(nghttp3_tnode *tnode, nghttp3_pq *pq, if (tnode->pe.index == NGHTTP3_PQ_BAD_INDEX) { tnode->cycle = pq_get_first_cycle(pq) + - ((nwrite == 0 || !tnode->pri.inc) ? 0 : nghttp3_max_uint64(1, penalty)); + ((nwrite == 0 || !tnode->pri.inc) ? 0 : nghttp3_max(1, penalty)); } else if (nwrite > 0) { if (!tnode->pri.inc || nghttp3_pq_size(pq) == 1) { return 0; @@ -82,7 +82,7 @@ int nghttp3_tnode_schedule(nghttp3_tnode *tnode, nghttp3_pq *pq, nghttp3_pq_remove(pq, &tnode->pe); tnode->pe.index = NGHTTP3_PQ_BAD_INDEX; - tnode->cycle += nghttp3_max_uint64(1, penalty); + tnode->cycle += nghttp3_max(1, penalty); } else { return 0; } @@ -90,6 +90,6 @@ int nghttp3_tnode_schedule(nghttp3_tnode *tnode, nghttp3_pq *pq, return nghttp3_pq_push(pq, &tnode->pe); } -int nghttp3_tnode_is_scheduled(nghttp3_tnode *tnode) { +int nghttp3_tnode_is_scheduled(const nghttp3_tnode *tnode) { return tnode->pe.index != NGHTTP3_PQ_BAD_INDEX; } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_tnode.h b/deps/ngtcp2/nghttp3/lib/nghttp3_tnode.h index 297067d7ca0332..954f6fff040dc3 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_tnode.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_tnode.h @@ -33,13 +33,12 @@ #include "nghttp3_pq.h" -#define NGHTTP3_TNODE_MAX_CYCLE_GAP (1llu << 24) +#define NGHTTP3_TNODE_MAX_CYCLE_GAP (1ULL << 24) typedef struct nghttp3_tnode { nghttp3_pq_entry pe; int64_t id; uint64_t cycle; - /* pri is a stream priority produced by nghttp3_pri_to_uint8. */ nghttp3_pri pri; } nghttp3_tnode; @@ -60,6 +59,6 @@ int nghttp3_tnode_schedule(nghttp3_tnode *tnode, nghttp3_pq *pq, /* * nghttp3_tnode_is_scheduled returns nonzero if |tnode| is scheduled. */ -int nghttp3_tnode_is_scheduled(nghttp3_tnode *tnode); +int nghttp3_tnode_is_scheduled(const nghttp3_tnode *tnode); #endif /* !defined(NGHTTP3_TNODE_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_vec.c b/deps/ngtcp2/nghttp3/lib/nghttp3_vec.c index ab58ff5832bea9..7302e88111bd49 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_vec.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_vec.c @@ -37,7 +37,7 @@ uint64_t nghttp3_vec_len(const nghttp3_vec *vec, size_t n) { return res; } -int64_t nghttp3_vec_len_varint(const nghttp3_vec *vec, size_t n) { +int nghttp3_vec_len_uvarint(uint64_t *dest, const nghttp3_vec *vec, size_t n) { uint64_t res = 0; size_t len; size_t i; @@ -51,5 +51,7 @@ int64_t nghttp3_vec_len_varint(const nghttp3_vec *vec, size_t n) { res += len; } - return (int64_t)res; + *dest = res; + + return 0; } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_vec.h b/deps/ngtcp2/nghttp3/lib/nghttp3_vec.h index f36eabc1052621..ad63f4fedfb2b6 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_vec.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_vec.h @@ -33,9 +33,11 @@ #include /* - * nghttp3_vec_len_varint is similar to nghttp3_vec_len, but it - * returns -1 if the sum of the length exceeds NGHTTP3_MAX_VARINT. + * nghttp3_vec_len_uvarint is similar to nghttp3_vec_len, but it + * stores the return value from nghttp3_vec_len(|vec|, |n|) into + * |dest| if it does not exceed NGHTTP3_MAX_VARINT, and returns 0. + * Otherwise returns -1. */ -int64_t nghttp3_vec_len_varint(const nghttp3_vec *vec, size_t n); +int nghttp3_vec_len_uvarint(uint64_t *dest, const nghttp3_vec *vec, size_t n); #endif /* !defined(NGHTTP3_VEC_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/sfparse/sfparse.c b/deps/ngtcp2/nghttp3/lib/sfparse/sfparse.c index cbcf813e23a094..c312c04f4a8f0a 100644 --- a/deps/ngtcp2/nghttp3/lib/sfparse/sfparse.c +++ b/deps/ngtcp2/nghttp3/lib/sfparse/sfparse.c @@ -34,18 +34,18 @@ # include #endif /* __AVX2__ */ -#define SFPARSE_STATE_DICT 0x08u -#define SFPARSE_STATE_LIST 0x10u -#define SFPARSE_STATE_ITEM 0x18u +#define SFPARSE_STATE_DICT 0x08U +#define SFPARSE_STATE_LIST 0x10U +#define SFPARSE_STATE_ITEM 0x18U -#define SFPARSE_STATE_INNER_LIST 0x04u +#define SFPARSE_STATE_INNER_LIST 0x04U -#define SFPARSE_STATE_BEFORE 0x00u -#define SFPARSE_STATE_BEFORE_PARAMS 0x01u -#define SFPARSE_STATE_PARAMS 0x02u -#define SFPARSE_STATE_AFTER 0x03u +#define SFPARSE_STATE_BEFORE 0x00U +#define SFPARSE_STATE_BEFORE_PARAMS 0x01U +#define SFPARSE_STATE_PARAMS 0x02U +#define SFPARSE_STATE_AFTER 0x03U -#define SFPARSE_STATE_OP_MASK 0x03u +#define SFPARSE_STATE_OP_MASK 0x03U #define SFPARSE_SET_STATE_AFTER(NAME) \ (SFPARSE_STATE_##NAME | SFPARSE_STATE_AFTER) @@ -69,7 +69,7 @@ #define SFPARSE_STATE_ITEM_INNER_LIST_BEFORE \ SFPARSE_SET_STATE_INNER_LIST_BEFORE(ITEM) -#define SFPARSE_STATE_INITIAL 0x00u +#define SFPARSE_STATE_INITIAL 0x00U #define LCALPHAS \ ['a'] = 1, ['b'] = 1, ['c'] = 1, ['d'] = 1, ['e'] = 1, ['f'] = 1, ['g'] = 1, \ @@ -200,7 +200,7 @@ static int parser_key(sfparse_parser *sfp, sfparse_vec *dest) { #ifdef __AVX2__ if (sfp->end - sfp->pos >= 32) { - last = sfp->pos + ((sfp->end - sfp->pos) & ~0x1fu); + last = sfp->pos + ((sfp->end - sfp->pos) & ~0x1FU); sfp->pos = find_char_key(sfp->pos, last); if (sfp->pos != last) { @@ -349,7 +349,7 @@ static const uint8_t *find_char_string(const uint8_t *first, const uint8_t *last) { const __m256i bs = _mm256_set1_epi8('\\'); const __m256i dq = _mm256_set1_epi8('"'); - const __m256i del = _mm256_set1_epi8(0x7f); + const __m256i del = _mm256_set1_epi8(0x7F); const __m256i sp = _mm256_set1_epi8(' '); __m256i s, x; uint32_t m; @@ -394,7 +394,7 @@ static int parser_string(sfparse_parser *sfp, sfparse_value *dest) { #ifdef __AVX2__ for (; sfp->end - sfp->pos >= 32; ++sfp->pos) { - last = sfp->pos + ((sfp->end - sfp->pos) & ~0x1fu); + last = sfp->pos + ((sfp->end - sfp->pos) & ~0x1FU); sfp->pos = find_char_string(sfp->pos, last); if (sfp->pos == last) { @@ -541,7 +541,7 @@ static int parser_token(sfparse_parser *sfp, sfparse_value *dest) { #ifdef __AVX2__ if (sfp->end - sfp->pos >= 32) { - last = sfp->pos + ((sfp->end - sfp->pos) & ~0x1fu); + last = sfp->pos + ((sfp->end - sfp->pos) & ~0x1FU); sfp->pos = find_char_token(sfp->pos, last); if (sfp->pos != last) { @@ -622,7 +622,7 @@ static int parser_byteseq(sfparse_parser *sfp, sfparse_value *dest) { #ifdef __AVX2__ if (sfp->end - sfp->pos >= 32) { - last = sfp->pos + ((sfp->end - sfp->pos) & ~0x1fu); + last = sfp->pos + ((sfp->end - sfp->pos) & ~0x1FU); sfp->pos = find_char_byteseq(sfp->pos, last); } #endif /* __AVX2__ */ @@ -1429,8 +1429,8 @@ void sfparse_base64decode(sfparse_vec *dest, const sfparse_vec *src) { } *o++ = (uint8_t)(n >> 16); - *o++ = (n >> 8) & 0xffu; - *o++ = n & 0xffu; + *o++ = (n >> 8) & 0xFFU; + *o++ = n & 0xFFU; } switch (left) { @@ -1467,8 +1467,8 @@ void sfparse_base64decode(sfparse_vec *dest, const sfparse_vec *src) { n = (uint32_t)(index_tbl[*p++] << 10); n += (uint32_t)(index_tbl[*p++] << 4); n += (uint32_t)(index_tbl[*p++] >> 2); - *o++ = (n >> 8) & 0xffu; - *o++ = n & 0xffu; + *o++ = (n >> 8) & 0xFFU; + *o++ = n & 0xFFU; break; } diff --git a/deps/ngtcp2/nghttp3/lib/sfparse/sfparse.h b/deps/ngtcp2/nghttp3/lib/sfparse/sfparse.h index 9341221a099438..7f36ce8a08c56b 100644 --- a/deps/ngtcp2/nghttp3/lib/sfparse/sfparse.h +++ b/deps/ngtcp2/nghttp3/lib/sfparse/sfparse.h @@ -130,7 +130,7 @@ typedef struct sfparse_vec { * * :macro:`SFPARSE_VALUE_FLAG_NONE` indicates no flag set. */ -#define SFPARSE_VALUE_FLAG_NONE 0x0u +#define SFPARSE_VALUE_FLAG_NONE 0x0U /** * @macro @@ -138,7 +138,7 @@ typedef struct sfparse_vec { * :macro:`SFPARSE_VALUE_FLAG_ESCAPED_STRING` indicates that a string * contains escaped character(s). */ -#define SFPARSE_VALUE_FLAG_ESCAPED_STRING 0x1u +#define SFPARSE_VALUE_FLAG_ESCAPED_STRING 0x1U /** * @struct diff --git a/deps/ngtcp2/ngtcp2.gyp b/deps/ngtcp2/ngtcp2.gyp index be177717efa2c8..dd0a64d5852b1b 100644 --- a/deps/ngtcp2/ngtcp2.gyp +++ b/deps/ngtcp2/ngtcp2.gyp @@ -13,6 +13,7 @@ 'ngtcp2/lib/ngtcp2_cc.c', 'ngtcp2/lib/ngtcp2_cid.c', 'ngtcp2/lib/ngtcp2_conn.c', + 'ngtcp2/lib/ngtcp2_conn_info.c', 'ngtcp2/lib/ngtcp2_conv.c', 'ngtcp2/lib/ngtcp2_crypto.c', 'ngtcp2/lib/ngtcp2_dcidtr.c', @@ -89,6 +90,36 @@ 'nghttp3/lib/nghttp3_unreachable.c', 'nghttp3/lib/nghttp3_vec.c', 'nghttp3/lib/nghttp3_version.c', + ], + 'ngtcp2_test_server_sources': [ + 'ngtcp2/examples/tls_server_session_ossl.cc', + 'ngtcp2/examples/tls_server_context_ossl.cc', + 'ngtcp2/examples/tls_session_base_ossl.cc', + 'ngtcp2/examples/util_openssl.cc', + 'ngtcp2/examples/http.cc', + 'ngtcp2/examples/server_base.cc', + 'ngtcp2/examples/shared.cc', + 'ngtcp2/examples/server.cc', + 'ngtcp2/examples/util.cc', + 'ngtcp2/examples/debug.cc', + 'ngtcp2/examples/siphash.cc', + 'ngtcp2/third-party/urlparse/urlparse.c', + 'ngtcp2/third-party/libev/ev.c', + ], + 'ngtcp2_test_client_sources': [ + 'ngtcp2/examples/tls_client_session_ossl.cc', + 'ngtcp2/examples/tls_client_context_ossl.cc', + 'ngtcp2/examples/tls_session_base_ossl.cc', + 'ngtcp2/examples/util_openssl.cc', + 'ngtcp2/examples/http.cc', + 'ngtcp2/examples/client_base.cc', + 'ngtcp2/examples/shared.cc', + 'ngtcp2/examples/client.cc', + 'ngtcp2/examples/util.cc', + 'ngtcp2/examples/debug.cc', + 'ngtcp2/examples/siphash.cc', + 'ngtcp2/third-party/urlparse/urlparse.c', + 'ngtcp2/third-party/libev/ev.c', ] }, 'targets': [ @@ -133,19 +164,6 @@ 'HAVE_NETINET_IN_H', ], }], - # TODO: Support OpenSSL 3.5 shared library builds. - # The complexity here is that we need to use the ngtcp2 ossl - # adapter, which does not include any conditional checks to - # see if the version of OpenSSL used has the necessary QUIC - # APIs, so we need to ensure that we conditionally enable use - # of the adapter only when we know that the OpenSSL version we - # are compiling against has the necessary APIs. We can do that - # by checkig the OpenSSL version number but, currently, the - # code that does so checks only the VERSION.dat file that is - # bundled with the openssl dependency. We'll need to update - # that to support the shared library case, where the version - # of the shared library needs to be determined. - # # TODO: Support Boringssl here also. ngtcp2 provides an adapter # for Boringssl. If we can detect that boringssl is being used # here then we can use that adapter and also set the @@ -189,6 +207,7 @@ 'defines': [ 'BUILDING_NGHTTP3', 'NGHTTP3_STATICLIB', + 'DEBUGBUILD', ], 'dependencies': [ 'ngtcp2' @@ -227,6 +246,128 @@ 'sources': [ '<@(nghttp3_sources)' ] + }, + { + 'target_name': 'ngtcp2_test_server', + # Disabled: ngtcp2 examples now require C++23 (, , + # std::println, std::expected) which is not yet supported on all + # Node.js platforms. Re-enable when C++23 is available. + 'type': 'none', + 'cflags': [ '-Wno-everything' ], + 'include_dirs': [ + '', + 'ngtcp2/examples/', + 'ngtcp2/lib/includes/', + 'ngtcp2/crypto/includes/', + 'ngtcp2/third-party/urlparse/', + 'ngtcp2/third-party/libev/', + '../../nghttp2/lib', + ], + 'dependencies': [ + 'ngtcp2', + 'nghttp3', + '../openssl/openssl.gyp:openssl', + '../nghttp2/nghttp2.gyp:sfparse', + ], + 'defines': [ + 'HAVE_CONFIG_H', + 'WITH_EXAMPLE_OSSL', + 'EV_STANDALONE=1', + 'HAVE_UNISTD_H', + 'HAVE_ARPA_INET_H', + 'HAVE_NETINET_IN_H', + 'HAVE_NETINET_IP_H', + ], + 'conditions': [ + ['OS=="aix" or OS=="win" or OS=="os400"', { + # AIX does not support some of the networking features used in + # the test server. Windows also lacks the Unix-specific headers + # and system calls required by the ngtcp2 examples. + 'type': 'none', # Disable as executable on AIX and Windows + }], + ['OS=="mac"', { + 'defines': [ + '__APPLE_USE_RFC_3542', + ] + }], + ['OS=="solaris"', { + 'defines': [ + 'IPTOS_ECN_MASK=0x03', + ], + 'link_settings': { + 'libraries': [ '-lsocket', '-lnsl' ], + }, + }], + [ 'OS=="linux" or OS=="openharmony"', { + 'link_settings': { + 'libraries': [ '-ldl', '-lrt' ], + }, + }], + ], + 'sources': [ + '<@(ngtcp2_test_server_sources)' + ] + }, + { + 'target_name': 'ngtcp2_test_client', + # Disabled: ngtcp2 examples now require C++23 (, , + # std::println, std::expected) which is not yet supported on all + # Node.js platforms. Re-enable when C++23 is available. + 'type': 'none', + 'cflags': [ '-Wno-everything' ], + 'include_dirs': [ + '', + 'ngtcp2/examples/', + 'ngtcp2/lib/includes/', + 'ngtcp2/crypto/includes/', + 'ngtcp2/third-party/urlparse/', + 'ngtcp2/third-party/libev/', + '../../nghttp2/lib', + ], + 'dependencies': [ + 'ngtcp2', + 'nghttp3', + '../openssl/openssl.gyp:openssl', + '../nghttp2/nghttp2.gyp:sfparse', + ], + 'defines': [ + 'HAVE_CONFIG_H', + 'WITH_EXAMPLE_OSSL', + 'EV_STANDALONE=1', + 'HAVE_UNISTD_H', + 'HAVE_ARPA_INET_H', + 'HAVE_NETINET_IN_H', + 'HAVE_NETINET_IP_H', + ], + 'conditions': [ + ['OS=="aix" or OS=="win" or OS=="os400"', { + # AIX does not support some of the networking features used in + # the test client. Windows also lacks the Unix-specific headers + # and system calls required by the ngtcp2 examples. + 'type': 'none', # Disable as executable on AIX and Windows + }], + ['OS=="mac"', { + 'defines': [ + '__APPLE_USE_RFC_3542', + ] + }], + ['OS=="solaris"', { + 'defines': [ + 'IPTOS_ECN_MASK=0x03', + ], + 'link_settings': { + 'libraries': [ '-lsocket', '-lnsl' ], + }, + }], + [ 'OS=="linux" or OS=="openharmony"', { + 'link_settings': { + 'libraries': [ '-ldl', '-lrt' ], + }, + }], + ], + 'sources': [ + '<@(ngtcp2_test_client_sources)' + ] } ] } diff --git a/deps/ngtcp2/ngtcp2/crypto/boringssl/boringssl.c b/deps/ngtcp2/ngtcp2/crypto/boringssl/boringssl.c index 283063f738e2af..6daedf1075557b 100644 --- a/deps/ngtcp2/ngtcp2/crypto/boringssl/boringssl.c +++ b/deps/ngtcp2/ngtcp2/crypto/boringssl/boringssl.c @@ -39,6 +39,7 @@ #include #include +#include "ngtcp2_macro.h" #include "shared.h" typedef enum ngtcp2_crypto_boringssl_cipher_type { @@ -401,7 +402,7 @@ int ngtcp2_crypto_decrypt(uint8_t *dest, const ngtcp2_crypto_aead *aead, int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, const ngtcp2_crypto_cipher_ctx *hp_ctx, const uint8_t *sample) { - static const uint8_t PLAINTEXT[] = "\x00\x00\x00\x00\x00"; + static const uint8_t PLAINTEXT[16] = {0}; ngtcp2_crypto_boringssl_cipher_ctx *ctx = hp_ctx->native_handle; uint32_t counter; @@ -419,7 +420,7 @@ int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, #else /* !defined(WORDS_BIGENDIAN) */ memcpy(&counter, sample, sizeof(counter)); #endif /* !defined(WORDS_BIGENDIAN) */ - CRYPTO_chacha_20(dest, PLAINTEXT, sizeof(PLAINTEXT) - 1, ctx->key, + CRYPTO_chacha_20(dest, PLAINTEXT, sizeof(PLAINTEXT), ctx->key, sample + sizeof(counter), counter); return 0; default: @@ -431,18 +432,19 @@ int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, int ngtcp2_crypto_read_write_crypto_data( ngtcp2_conn *conn, ngtcp2_encryption_level encryption_level, const uint8_t *data, size_t datalen) { - SSL *ssl = ngtcp2_conn_get_tls_native_handle(conn); + SSL *ssl = ngtcp2_conn_get_tls_native_handle2(conn); int rv; int err; - if (SSL_provide_quic_data( + if (datalen && + SSL_provide_quic_data( ssl, ngtcp2_crypto_boringssl_from_ngtcp2_encryption_level(encryption_level), data, datalen) != 1) { return -1; } - if (!ngtcp2_conn_get_handshake_completed(conn)) { + if (!ngtcp2_conn_get_handshake_completed2(conn)) { retry: rv = SSL_do_handshake(ssl); if (rv <= 0) { @@ -454,7 +456,7 @@ int ngtcp2_crypto_read_write_crypto_data( case SSL_ERROR_SSL: return -1; case SSL_ERROR_EARLY_DATA_REJECTED: - assert(!ngtcp2_conn_is_server(conn)); + assert(!ngtcp2_conn_is_server2(conn)); SSL_reset_early_data_reject(ssl); @@ -464,6 +466,16 @@ int ngtcp2_crypto_read_write_crypto_data( } goto retry; + case SSL_ERROR_WANT_X509_LOOKUP: + case SSL_ERROR_WANT_PRIVATE_KEY_OPERATION: + case SSL_ERROR_WANT_CERTIFICATE_VERIFY: + /* It might be better to return this error, but ngtcp2 does + not need to know whether handshake has been interrupted or + not. We expect that necessary plumbing should be done by + application when handshake is interrupted (e.g., via + SSL_PRIVATE_KEY_METHOD). If it does not work, we will + reconsider this. */ + return 0; default: return -1; } @@ -567,6 +579,19 @@ int ngtcp2_crypto_get_path_challenge_data_cb(ngtcp2_conn *conn, uint8_t *data, return 0; } +int ngtcp2_crypto_get_path_challenge_data2_cb(ngtcp2_conn *conn, + ngtcp2_path_challenge_data *data, + void *user_data) { + (void)conn; + (void)user_data; + + if (RAND_bytes(data->data, NGTCP2_PATH_CHALLENGE_DATALEN) != 1) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + int ngtcp2_crypto_random(uint8_t *data, size_t datalen) { if (RAND_bytes(data, datalen) != 1) { return -1; diff --git a/deps/ngtcp2/ngtcp2/crypto/cryptotest.c b/deps/ngtcp2/ngtcp2/crypto/cryptotest.c new file mode 100644 index 00000000000000..0752c9357d5f7d --- /dev/null +++ b/deps/ngtcp2/ngtcp2/crypto/cryptotest.c @@ -0,0 +1,46 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include +#endif /* defined(HAVE_CONFIG_H) */ + +#include "munit.h" + +/* include test cases' include files here */ +#include "shared_test.h" + +int main(int argc, char *argv[]) { + const MunitSuite suites[] = { + shared_suite, + {0}, + }; + const MunitSuite suite = { + .prefix = "", + .suites = suites, + .iterations = 1, + }; + + return munit_suite_main(&suite, NULL, argc, argv); +} diff --git a/deps/ngtcp2/ngtcp2/crypto/includes/ngtcp2/ngtcp2_crypto.h b/deps/ngtcp2/ngtcp2/crypto/includes/ngtcp2/ngtcp2_crypto.h index 003ec6b4c3f23c..9ff353ef46b102 100644 --- a/deps/ngtcp2/ngtcp2/crypto/includes/ngtcp2/ngtcp2_crypto.h +++ b/deps/ngtcp2/ngtcp2/crypto/includes/ngtcp2/ngtcp2_crypto.h @@ -338,16 +338,16 @@ ngtcp2_crypto_hp_mask_cb(uint8_t *dest, const ngtcp2_crypto_cipher *hp, * :enum:`ngtcp2_encryption_level.NGTCP2_ENCRYPTION_LEVEL_0RTT`) to * set negotiated AEAD and message digest algorithm. After the * successful call of this function, application can use - * `ngtcp2_conn_get_crypto_ctx` (or `ngtcp2_conn_get_0rtt_crypto_ctx` - * if |level| == + * `ngtcp2_conn_get_crypto_ctx2` (or + * `ngtcp2_conn_get_0rtt_crypto_ctx2` if |level| == * :enum:`ngtcp2_encryption_level.NGTCP2_ENCRYPTION_LEVEL_0RTT`) to * get :type:`ngtcp2_crypto_ctx`. * * If |conn| is initialized as client, and |level| is * :enum:`ngtcp2_encryption_level.NGTCP2_ENCRYPTION_LEVEL_1RTT`, this * function retrieves a remote QUIC transport parameters extension - * from an object obtained by `ngtcp2_conn_get_tls_native_handle`, and - * sets it to |conn| by calling + * from an object obtained by `ngtcp2_conn_get_tls_native_handle2`, + * and sets it to |conn| by calling * `ngtcp2_conn_decode_and_set_remote_transport_params`. * * This function returns 0 if it succeeds, or -1. @@ -385,16 +385,16 @@ NGTCP2_EXTERN int ngtcp2_crypto_derive_and_install_rx_key( * :enum:`ngtcp2_encryption_level.NGTCP2_ENCRYPTION_LEVEL_0RTT`) to * set negotiated AEAD and message digest algorithm. After the * successful call of this function, application can use - * `ngtcp2_conn_get_crypto_ctx` (or `ngtcp2_conn_get_0rtt_crypto_ctx` - * if |level| == + * `ngtcp2_conn_get_crypto_ctx2` (or + * `ngtcp2_conn_get_0rtt_crypto_ctx2` if |level| == * :enum:`ngtcp2_encryption_level.NGTCP2_ENCRYPTION_LEVEL_0RTT`) to * get :type:`ngtcp2_crypto_ctx`. * * If |conn| is initialized as server, and |level| is * :enum:`ngtcp2_encryption_level.NGTCP2_ENCRYPTION_LEVEL_1RTT`, this * function retrieves a remote QUIC transport parameters extension - * from an object obtained by `ngtcp2_conn_get_tls_native_handle`, and - * sets it to |conn| by calling + * from an object obtained by `ngtcp2_conn_get_tls_native_handle2`, + * and sets it to |conn| by calling * `ngtcp2_conn_decode_and_set_remote_transport_params`. * * This function returns 0 if it succeeds, or -1. @@ -524,11 +524,14 @@ NGTCP2_EXTERN int ngtcp2_crypto_recv_client_initial_cb(ngtcp2_conn *conn, * completes. It is allowed to call this function with |datalen| == * 0. In this case, no additional read operation is done. * + * This function is implemented per TLS backend. See + * :ref:`tls-integration` for more details. + * * This function returns 0 if it succeeds, or a negative error code. * The generic error code is -1 if a specific error code is not * suitable. The error codes less than -10000 are specific to - * underlying TLS implementation. For quictls, the error codes are - * defined in *ngtcp2_crypto_quictls.h*. + * underlying TLS implementation. Refer to the implementation + * specific header files for error codes. */ NGTCP2_EXTERN int ngtcp2_crypto_read_write_crypto_data(ngtcp2_conn *conn, @@ -542,11 +545,22 @@ ngtcp2_crypto_read_write_crypto_data(ngtcp2_conn *conn, * `ngtcp2_crypto_read_write_crypto_data`. It can be directly passed * to :member:`ngtcp2_callbacks.recv_crypto_data` field. * + * For quictls and OpenSSL, the following error codes are treated as + * success: + * + * - -10001 (e.g., :macro:`NGTCP2_CRYPTO_QUICTLS_ERR_TLS_WANT_X509_LOOKUP`) + * - -10002 (e.g., :macro:`NGTCP2_CRYPTO_QUICTLS_ERR_TLS_WANT_CLIENT_HELLO_CB`) + * + * To continue the interrupted handshake, call + * `ngtcp2_conn_continue_handshake`. + * + * See :ref:`tls-integration` for more details. + * * If this function is used, the TLS implementation specific error * codes described in `ngtcp2_crypto_read_write_crypto_data` are - * treated as if it returns -1. Do not use this function if an - * application wishes to use the TLS implementation specific error - * codes. + * treated as if it returns -1 except for those that are listed above. + * Do not use this function if an application wishes to use the TLS + * implementation specific error codes. */ NGTCP2_EXTERN int ngtcp2_crypto_recv_crypto_data_cb( ngtcp2_conn *conn, ngtcp2_encryption_level encryption_level, uint64_t offset, @@ -583,7 +597,7 @@ NGTCP2_EXTERN int ngtcp2_crypto_generate_stateless_reset_token( * :macro:`NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY` is the magic byte for * Retry token generated by `ngtcp2_crypto_generate_retry_token`. */ -#define NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY 0xb6 +#define NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY 0xB6 /** * @macro @@ -591,7 +605,7 @@ NGTCP2_EXTERN int ngtcp2_crypto_generate_stateless_reset_token( * :macro:`NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY2` is the magic byte for * Retry token generated by `ngtcp2_crypto_generate_retry_token2`. */ -#define NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY2 0xb7 +#define NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY2 0xB7 /** * @macro @@ -627,7 +641,10 @@ NGTCP2_EXTERN int ngtcp2_crypto_generate_stateless_reset_token( * @macro * * :macro:`NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN` is the maximum length - * of a token generated by `ngtcp2_crypto_generate_regular_token`. + * of a token generated by `ngtcp2_crypto_generate_regular_token`. + * `ngtcp2_crypto_generate_regular_token2` generates a token of length + * at most :macro:`NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN` bytes + the + * length of the provided opaque data. */ #define NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN \ (/* magic = */ 1 + sizeof(ngtcp2_tstamp) + /* aead tag = */ 16 + \ @@ -787,6 +804,77 @@ NGTCP2_EXTERN int ngtcp2_crypto_verify_regular_token( size_t secretlen, const ngtcp2_sockaddr *remote_addr, ngtcp2_socklen remote_addrlen, ngtcp2_duration timeout, ngtcp2_tstamp ts); +/** + * @function + * + * `ngtcp2_crypto_generate_regular_token2` generates a token in the + * buffer pointed by |token| that is sent with NEW_TOKEN frame. The + * buffer pointed by |token| must have at least + * :macro:`NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN` + |datalen| bytes long. + * The successfully generated token starts with + * :macro:`NGTCP2_CRYPTO_TOKEN_MAGIC_REGULAR`. |secret| of length + * |secretlen| is a keying material to generate keys to encrypt the + * token. |remote_addr| of length |remote_addrlen| is an address of + * client. |ts| is the timestamp when the token is generated. |data| + * of length |datalen| is an opaque data embedded in the token. + * |datalen| must be less than or equal to 256. + * + * Calling this function with |datalen| = 0 is equivalent to calling + * `ngtcp2_crypto_generate_regular_token`. + * + * To get the opaque data after successful verification, use + * `ngtcp2_crypto_verify_regular_token2`. + * `ngtcp2_crypto_verify_regular_token` can verify the token with + * |datalen| > 0, but it discards the opaque data. + * + * This function returns the length of generated token if it succeeds, + * or -1. + */ +NGTCP2_EXTERN ngtcp2_ssize ngtcp2_crypto_generate_regular_token2( + uint8_t *token, const uint8_t *secret, size_t secretlen, + const ngtcp2_sockaddr *remote_addr, ngtcp2_socklen remote_addrlen, + const void *data, size_t datalen, ngtcp2_tstamp ts); + +/** + * @function + * + * `ngtcp2_crypto_verify_regular_token2` verifies a regular token + * stored in the buffer pointed by |token| of length |tokenlen|. + * |secret| of length |secretlen| is a keying material to generate + * keys to decrypt the token. |remote_addr| of length + * |remote_addrlen| is an address of client. |timeout| is the period + * during which the token is valid. |ts| is the current timestamp. + * |data| is the pointer to the buffer of length at least + * |max_datalen| bytes. If the token is verified successfully, the + * opaque data embedded in the token is copied to the buffer pointed + * by |data|. + * + * If |tokenlen| is less than + * :macro:`NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN`, this function returns + * :macro:`NGTCP2_CRYPTO_ERR_UNREADABLE_TOKEN`. + * + * If the length of opaque data is larger than |max_datalen|, the + * verification still succeeds, but nothing is written to the buffer + * pointed by |data|, and this function returns 0. In other words, + * the opaque data is discarded. + * + * This function returns the number of the opaque data written to the + * buffer pointed by |data| if it succeeds, or one of the following + * negative error codes: + * + * :macro:`NGTCP2_CRYPTO_ERR_UNREADABLE_TOKEN` + * A token is badly formatted; or verifying the integrity + * protection failed. + * :macro:`NGTCP2_CRYPTO_ERR_VERIFY_TOKEN` + * A token validity has expired. + * :macro:`NGTCP2_CRYPTO_ERR_INTERNAL` + * Internal error occurred. + */ +NGTCP2_EXTERN ngtcp2_ssize ngtcp2_crypto_verify_regular_token2( + void *data, size_t max_datalen, const uint8_t *token, size_t tokenlen, + const uint8_t *secret, size_t secretlen, const ngtcp2_sockaddr *remote_addr, + ngtcp2_socklen remote_addrlen, ngtcp2_duration timeout, ngtcp2_tstamp ts); + /** * @function * @@ -898,6 +986,11 @@ NGTCP2_EXTERN void ngtcp2_crypto_delete_crypto_cipher_ctx_cb( /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use `ngtcp2_crypto_get_path_challenge_data2_cb` instead. + * * `ngtcp2_crypto_get_path_challenge_data_cb` writes unpredictable * sequence of :macro:`NGTCP2_PATH_CHALLENGE_DATALEN` bytes to |data| * which is sent with PATH_CHALLENGE frame. @@ -909,6 +1002,21 @@ NGTCP2_EXTERN int ngtcp2_crypto_get_path_challenge_data_cb(ngtcp2_conn *conn, uint8_t *data, void *user_data); +/** + * @function + * + * `ngtcp2_crypto_get_path_challenge_data2_cb` writes unpredictable + * sequence of :macro:`NGTCP2_PATH_CHALLENGE_DATALEN` bytes to |data| + * which is sent with PATH_CHALLENGE frame. + * + * This function can be directly passed to + * :member:`ngtcp2_callbacks.get_path_challenge_data2` field. + * + * .. version-added:: 1.22.0 + */ +NGTCP2_EXTERN int ngtcp2_crypto_get_path_challenge_data2_cb( + ngtcp2_conn *conn, ngtcp2_path_challenge_data *data, void *user_data); + /** * @function * diff --git a/deps/ngtcp2/ngtcp2/crypto/ossl/ossl.c b/deps/ngtcp2/ngtcp2/crypto/ossl/ossl.c index 061d6acd21d976..6159567aceb6af 100644 --- a/deps/ngtcp2/ngtcp2/crypto/ossl/ossl.c +++ b/deps/ngtcp2/ngtcp2/crypto/ossl/ossl.c @@ -41,70 +41,40 @@ #include "ngtcp2_macro.h" #include "shared.h" -static int crypto_initialized; +#if defined(OPENSSL_NO_CHACHA) || defined(OPENSSL_NO_POLY1305) +# define NGTCP2_NO_CHACHA_POLY1305 +#endif /* defined(OPENSSL_NO_CHACHA) || \ + defined(OPENSSL_NO_POLY1305) */ + static EVP_CIPHER *crypto_aes_128_gcm; static EVP_CIPHER *crypto_aes_256_gcm; -static EVP_CIPHER *crypto_chacha20_poly1305; static EVP_CIPHER *crypto_aes_128_ccm; -static EVP_CIPHER *crypto_aes_128_ctr; -static EVP_CIPHER *crypto_aes_256_ctr; +static EVP_CIPHER *crypto_aes_128_ecb; +static EVP_CIPHER *crypto_aes_256_ecb; +#ifndef NGTCP2_NO_CHACHA_POLY1305 +static EVP_CIPHER *crypto_chacha20_poly1305; static EVP_CIPHER *crypto_chacha20; +#endif /* !defined(NGTCP2_NO_CHACHA_POLY1305) */ static EVP_MD *crypto_sha256; static EVP_MD *crypto_sha384; static EVP_KDF *crypto_hkdf; int ngtcp2_crypto_ossl_init(void) { + /* We do not care whether the pre-fetch succeeds or not. If it + fails, it returns NULL, which is still the default value, and our + code should still work with it. */ crypto_aes_128_gcm = EVP_CIPHER_fetch(NULL, "AES-128-GCM", NULL); - if (crypto_aes_128_gcm == NULL) { - return -1; - } - crypto_aes_256_gcm = EVP_CIPHER_fetch(NULL, "AES-256-GCM", NULL); - if (crypto_aes_256_gcm == NULL) { - return -1; - } - - crypto_chacha20_poly1305 = EVP_CIPHER_fetch(NULL, "ChaCha20-Poly1305", NULL); - if (crypto_chacha20_poly1305 == NULL) { - return -1; - } - crypto_aes_128_ccm = EVP_CIPHER_fetch(NULL, "AES-128-CCM", NULL); - if (crypto_aes_128_ccm == NULL) { - return -1; - } - - crypto_aes_128_ctr = EVP_CIPHER_fetch(NULL, "AES-128-CTR", NULL); - if (crypto_aes_128_ctr == NULL) { - return -1; - } - - crypto_aes_256_ctr = EVP_CIPHER_fetch(NULL, "AES-256-CTR", NULL); - if (crypto_aes_256_ctr == NULL) { - return -1; - } - + crypto_aes_128_ecb = EVP_CIPHER_fetch(NULL, "AES-128-ECB", NULL); + crypto_aes_256_ecb = EVP_CIPHER_fetch(NULL, "AES-256-ECB", NULL); +#ifndef NGTCP2_NO_CHACHA_POLY1305 + crypto_chacha20_poly1305 = EVP_CIPHER_fetch(NULL, "ChaCha20-Poly1305", NULL); crypto_chacha20 = EVP_CIPHER_fetch(NULL, "ChaCha20", NULL); - if (crypto_chacha20 == NULL) { - return -1; - } - +#endif /* !defined(NGTCP2_NO_CHACHA_POLY1305) */ crypto_sha256 = EVP_MD_fetch(NULL, "sha256", NULL); - if (crypto_sha256 == NULL) { - return -1; - } - crypto_sha384 = EVP_MD_fetch(NULL, "sha384", NULL); - if (crypto_sha384 == NULL) { - return -1; - } - crypto_hkdf = EVP_KDF_fetch(NULL, "hkdf", NULL); - if (crypto_hkdf == NULL) { - return -1; - } - - crypto_initialized = 1; return 0; } @@ -125,6 +95,7 @@ static const EVP_CIPHER *crypto_aead_aes_256_gcm(void) { return EVP_aes_256_gcm(); } +#ifndef NGTCP2_NO_CHACHA_POLY1305 static const EVP_CIPHER *crypto_aead_chacha20_poly1305(void) { if (crypto_chacha20_poly1305) { return crypto_chacha20_poly1305; @@ -132,6 +103,7 @@ static const EVP_CIPHER *crypto_aead_chacha20_poly1305(void) { return EVP_chacha20_poly1305(); } +#endif /* !defined(NGTCP2_NO_CHACHA_POLY1305) */ static const EVP_CIPHER *crypto_aead_aes_128_ccm(void) { if (crypto_aes_128_ccm) { @@ -141,22 +113,23 @@ static const EVP_CIPHER *crypto_aead_aes_128_ccm(void) { return EVP_aes_128_ccm(); } -static const EVP_CIPHER *crypto_cipher_aes_128_ctr(void) { - if (crypto_aes_128_ctr) { - return crypto_aes_128_ctr; +static const EVP_CIPHER *crypto_cipher_aes_128_ecb(void) { + if (crypto_aes_128_ecb) { + return crypto_aes_128_ecb; } - return EVP_aes_128_ctr(); + return EVP_aes_128_ecb(); } -static const EVP_CIPHER *crypto_cipher_aes_256_ctr(void) { - if (crypto_aes_256_ctr) { - return crypto_aes_256_ctr; +static const EVP_CIPHER *crypto_cipher_aes_256_ecb(void) { + if (crypto_aes_256_ecb) { + return crypto_aes_256_ecb; } - return EVP_aes_256_ctr(); + return EVP_aes_256_ecb(); } +#ifndef NGTCP2_NO_CHACHA_POLY1305 static const EVP_CIPHER *crypto_cipher_chacha20(void) { if (crypto_chacha20) { return crypto_chacha20; @@ -164,6 +137,7 @@ static const EVP_CIPHER *crypto_cipher_chacha20(void) { return EVP_chacha20(); } +#endif /* !defined(NGTCP2_NO_CHACHA_POLY1305) */ static const EVP_MD *crypto_md_sha256(void) { if (crypto_sha256) { @@ -189,13 +163,21 @@ static EVP_KDF *crypto_kdf_hkdf(void) { return EVP_KDF_fetch(NULL, "hkdf", NULL); } +static void crypto_kdf_hkdf_free(EVP_KDF *kdf) { + if (kdf && crypto_hkdf != kdf) { + EVP_KDF_free(kdf); + } +} + static size_t crypto_aead_max_overhead(const EVP_CIPHER *aead) { switch (EVP_CIPHER_nid(aead)) { case NID_aes_128_gcm: case NID_aes_256_gcm: return EVP_GCM_TLS_TAG_LEN; +#ifndef NGTCP2_NO_CHACHA_POLY1305 case NID_chacha20_poly1305: return EVP_CHACHAPOLY_TLS_TAG_LEN; +#endif /* !defined(NGTCP2_NO_CHACHA_POLY1305) */ case NID_aes_128_ccm: return EVP_CCM_TLS_TAG_LEN; default: @@ -216,7 +198,7 @@ ngtcp2_crypto_md *ngtcp2_crypto_md_sha256(ngtcp2_crypto_md *md) { ngtcp2_crypto_ctx *ngtcp2_crypto_ctx_initial(ngtcp2_crypto_ctx *ctx) { ngtcp2_crypto_aead_init(&ctx->aead, (void *)crypto_aead_aes_128_gcm()); ctx->md.native_handle = (void *)crypto_md_sha256(); - ctx->hp.native_handle = (void *)crypto_cipher_aes_128_ctr(); + ctx->hp.native_handle = (void *)crypto_cipher_aes_128_ecb(); ctx->max_encryption = 0; ctx->max_decryption_failure = 0; return ctx; @@ -239,8 +221,10 @@ static const EVP_CIPHER *crypto_cipher_id_get_aead(uint32_t cipher_id) { return crypto_aead_aes_128_gcm(); case TLS1_3_CK_AES_256_GCM_SHA384: return crypto_aead_aes_256_gcm(); +#ifndef NGTCP2_NO_CHACHA_POLY1305 case TLS1_3_CK_CHACHA20_POLY1305_SHA256: return crypto_aead_chacha20_poly1305(); +#endif /* !defined(NGTCP2_NO_CHACHA_POLY1305) */ case TLS1_3_CK_AES_128_CCM_SHA256: return crypto_aead_aes_128_ccm(); default: @@ -253,8 +237,10 @@ static uint64_t crypto_cipher_id_get_aead_max_encryption(uint32_t cipher_id) { case TLS1_3_CK_AES_128_GCM_SHA256: case TLS1_3_CK_AES_256_GCM_SHA384: return NGTCP2_CRYPTO_MAX_ENCRYPTION_AES_GCM; +#ifndef NGTCP2_NO_CHACHA_POLY1305 case TLS1_3_CK_CHACHA20_POLY1305_SHA256: return NGTCP2_CRYPTO_MAX_ENCRYPTION_CHACHA20_POLY1305; +#endif /* !defined(NGTCP2_NO_CHACHA_POLY1305) */ case TLS1_3_CK_AES_128_CCM_SHA256: return NGTCP2_CRYPTO_MAX_ENCRYPTION_AES_CCM; default: @@ -268,8 +254,10 @@ crypto_cipher_id_get_aead_max_decryption_failure(uint32_t cipher_id) { case TLS1_3_CK_AES_128_GCM_SHA256: case TLS1_3_CK_AES_256_GCM_SHA384: return NGTCP2_CRYPTO_MAX_DECRYPTION_FAILURE_AES_GCM; +#ifndef NGTCP2_NO_CHACHA_POLY1305 case TLS1_3_CK_CHACHA20_POLY1305_SHA256: return NGTCP2_CRYPTO_MAX_DECRYPTION_FAILURE_CHACHA20_POLY1305; +#endif /* !defined(NGTCP2_NO_CHACHA_POLY1305) */ case TLS1_3_CK_AES_128_CCM_SHA256: return NGTCP2_CRYPTO_MAX_DECRYPTION_FAILURE_AES_CCM; default: @@ -281,11 +269,13 @@ static const EVP_CIPHER *crypto_cipher_id_get_hp(uint32_t cipher_id) { switch (cipher_id) { case TLS1_3_CK_AES_128_GCM_SHA256: case TLS1_3_CK_AES_128_CCM_SHA256: - return crypto_cipher_aes_128_ctr(); + return crypto_cipher_aes_128_ecb(); case TLS1_3_CK_AES_256_GCM_SHA384: - return crypto_cipher_aes_256_ctr(); + return crypto_cipher_aes_256_ecb(); +#ifndef NGTCP2_NO_CHACHA_POLY1305 case TLS1_3_CK_CHACHA20_POLY1305_SHA256: return crypto_cipher_chacha20(); +#endif /* !defined(NGTCP2_NO_CHACHA_POLY1305) */ default: return NULL; } @@ -294,7 +284,9 @@ static const EVP_CIPHER *crypto_cipher_id_get_hp(uint32_t cipher_id) { static const EVP_MD *crypto_cipher_id_get_md(uint32_t cipher_id) { switch (cipher_id) { case TLS1_3_CK_AES_128_GCM_SHA256: +#ifndef NGTCP2_NO_CHACHA_POLY1305 case TLS1_3_CK_CHACHA20_POLY1305_SHA256: +#endif /* !defined(NGTCP2_NO_CHACHA_POLY1305) */ case TLS1_3_CK_AES_128_CCM_SHA256: return crypto_md_sha256(); case TLS1_3_CK_AES_256_GCM_SHA384: @@ -308,7 +300,9 @@ static int supported_cipher_id(uint32_t cipher_id) { switch (cipher_id) { case TLS1_3_CK_AES_128_GCM_SHA256: case TLS1_3_CK_AES_256_GCM_SHA384: +#ifndef NGTCP2_NO_CHACHA_POLY1305 case TLS1_3_CK_CHACHA20_POLY1305_SHA256: +#endif /* !defined(NGTCP2_NO_CHACHA_POLY1305) */ case TLS1_3_CK_AES_128_CCM_SHA256: return 1; default: @@ -446,7 +440,7 @@ static int crypto_ossl_ctx_write_crypto_data(ngtcp2_crypto_ossl_ctx *ossl_ctx, left = crypto_buf_left(ossl_ctx->crypto_write); } - n = ngtcp2_min_size((size_t)(end - data), left); + n = ngtcp2_min((size_t)(end - data), left); crypto_buf_write(ossl_ctx->crypto_write, data, n); data += n; } @@ -697,9 +691,7 @@ int ngtcp2_crypto_hkdf_extract(uint8_t *dest, const ngtcp2_crypto_md *md, }; int rv = 0; - if (!crypto_initialized) { - EVP_KDF_free(kdf); - } + crypto_kdf_hkdf_free(kdf); if (EVP_KDF_derive(kctx, dest, (size_t)EVP_MD_size(prf), params) <= 0) { rv = -1; @@ -730,9 +722,7 @@ int ngtcp2_crypto_hkdf_expand(uint8_t *dest, size_t destlen, }; int rv = 0; - if (!crypto_initialized) { - EVP_KDF_free(kdf); - } + crypto_kdf_hkdf_free(kdf); if (EVP_KDF_derive(kctx, dest, destlen, params) <= 0) { rv = -1; @@ -763,9 +753,7 @@ int ngtcp2_crypto_hkdf(uint8_t *dest, size_t destlen, }; int rv = 0; - if (!crypto_initialized) { - EVP_KDF_free(kdf); - } + crypto_kdf_hkdf_free(kdf); if (EVP_KDF_derive(kctx, dest, destlen, params) <= 0) { rv = -1; @@ -850,16 +838,31 @@ int ngtcp2_crypto_decrypt(uint8_t *dest, const ngtcp2_crypto_aead *aead, int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, const ngtcp2_crypto_cipher_ctx *hp_ctx, const uint8_t *sample) { - static const uint8_t PLAINTEXT[] = "\x00\x00\x00\x00\x00"; + static const uint8_t PLAINTEXT[16] = {0}; EVP_CIPHER_CTX *actx = hp_ctx->native_handle; int len; (void)hp; - if (!EVP_EncryptInit_ex(actx, NULL, NULL, NULL, sample) || - !EVP_EncryptUpdate(actx, dest, &len, PLAINTEXT, sizeof(PLAINTEXT) - 1) || - !EVP_EncryptFinal_ex(actx, dest + sizeof(PLAINTEXT) - 1, &len)) { - return -1; + switch (EVP_CIPHER_CTX_nid(actx)) { + case NID_aes_128_ecb: + case NID_aes_256_ecb: + if (!EVP_EncryptUpdate(actx, dest, &len, sample, NGTCP2_HP_SAMPLELEN)) { + return -1; + } + + break; + case NID_chacha20: + if (!EVP_EncryptInit_ex(actx, NULL, NULL, NULL, sample) || + !EVP_EncryptUpdate(actx, dest, &len, PLAINTEXT, sizeof(PLAINTEXT)) || + !EVP_EncryptFinal_ex(actx, dest + sizeof(PLAINTEXT), &len)) { + return -1; + } + + break; + default: + assert(0); + abort(); } return 0; @@ -868,7 +871,7 @@ int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, int ngtcp2_crypto_read_write_crypto_data( ngtcp2_conn *conn, ngtcp2_encryption_level encryption_level, const uint8_t *data, size_t datalen) { - ngtcp2_crypto_ossl_ctx *ossl_ctx = ngtcp2_conn_get_tls_native_handle(conn); + ngtcp2_crypto_ossl_ctx *ossl_ctx = ngtcp2_conn_get_tls_native_handle2(conn); SSL *ssl = ossl_ctx->ssl; int rv; int err; @@ -878,7 +881,7 @@ int ngtcp2_crypto_read_write_crypto_data( return -1; } - if (!ngtcp2_conn_get_handshake_completed(conn)) { + if (!ngtcp2_conn_get_handshake_completed2(conn)) { rv = SSL_do_handshake(ssl); if (rv <= 0) { err = SSL_get_error(ssl, rv); @@ -994,6 +997,19 @@ int ngtcp2_crypto_get_path_challenge_data_cb(ngtcp2_conn *conn, uint8_t *data, return 0; } +int ngtcp2_crypto_get_path_challenge_data2_cb(ngtcp2_conn *conn, + ngtcp2_path_challenge_data *data, + void *user_data) { + (void)conn; + (void)user_data; + + if (RAND_bytes(data->data, NGTCP2_PATH_CHALLENGE_DATALEN) != 1) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + int ngtcp2_crypto_random(uint8_t *data, size_t datalen) { if (RAND_bytes(data, (int)datalen) != 1) { return -1; @@ -1017,7 +1033,7 @@ static int ossl_yield_secret(SSL *ssl, uint32_t ossl_level, int direction, } conn = conn_ref->get_conn(conn_ref); - ossl_ctx = ngtcp2_conn_get_tls_native_handle(conn); + ossl_ctx = ngtcp2_conn_get_tls_native_handle2(conn); if (direction) { if (ngtcp2_crypto_derive_and_install_tx_key(conn, NULL, NULL, NULL, level, @@ -1051,7 +1067,7 @@ static int ossl_crypto_send(SSL *ssl, const unsigned char *buf, size_t buflen, } conn = conn_ref->get_conn(conn_ref); - ossl_ctx = ngtcp2_conn_get_tls_native_handle(conn); + ossl_ctx = ngtcp2_conn_get_tls_native_handle2(conn); rv = ngtcp2_conn_submit_crypto_data(conn, ossl_ctx->tx_level, buf, buflen); if (rv != 0) { @@ -1078,7 +1094,7 @@ static int ossl_crypto_recv_rcd(SSL *ssl, const unsigned char **buf, } conn = conn_ref->get_conn(conn_ref); - ossl_ctx = ngtcp2_conn_get_tls_native_handle(conn); + ossl_ctx = ngtcp2_conn_get_tls_native_handle2(conn); crypto_ossl_ctx_read_crypto_data(ossl_ctx, buf, bytes_read); @@ -1100,7 +1116,7 @@ static int ossl_crypto_release_rcd(SSL *ssl, size_t released, void *arg) { } conn = conn_ref->get_conn(conn_ref); - ossl_ctx = ngtcp2_conn_get_tls_native_handle(conn); + ossl_ctx = ngtcp2_conn_get_tls_native_handle2(conn); crypto_ossl_ctx_release_crypto_data(ossl_ctx, released); diff --git a/deps/ngtcp2/ngtcp2/crypto/picotls/picotls.c b/deps/ngtcp2/ngtcp2/crypto/picotls/picotls.c index 98ebf9e876c053..af934bf10463b0 100644 --- a/deps/ngtcp2/ngtcp2/crypto/picotls/picotls.c +++ b/deps/ngtcp2/ngtcp2/crypto/picotls/picotls.c @@ -35,6 +35,7 @@ #include #include +#include "ngtcp2_macro.h" #include "shared.h" ngtcp2_crypto_aead *ngtcp2_crypto_aead_aes_128_gcm(ngtcp2_crypto_aead *aead) { @@ -49,7 +50,7 @@ ngtcp2_crypto_md *ngtcp2_crypto_md_sha256(ngtcp2_crypto_md *md) { ngtcp2_crypto_ctx *ngtcp2_crypto_ctx_initial(ngtcp2_crypto_ctx *ctx) { ngtcp2_crypto_aead_init(&ctx->aead, (void *)&ptls_openssl_aes128gcm); ctx->md.native_handle = (void *)&ptls_openssl_sha256; - ctx->hp.native_handle = (void *)&ptls_openssl_aes128ctr; + ctx->hp.native_handle = (void *)&ptls_openssl_aes128ecb; ctx->max_encryption = 0; ctx->max_decryption_failure = 0; return ctx; @@ -103,11 +104,11 @@ crypto_cipher_suite_get_aead_max_decryption_failure(ptls_cipher_suite_t *cs) { static const ptls_cipher_algorithm_t * crypto_cipher_suite_get_hp(ptls_cipher_suite_t *cs) { if (cs->aead == &ptls_openssl_aes128gcm) { - return &ptls_openssl_aes128ctr; + return &ptls_openssl_aes128ecb; } if (cs->aead == &ptls_openssl_aes256gcm) { - return &ptls_openssl_aes256ctr; + return &ptls_openssl_aes256ecb; } #ifdef PTLS_OPENSSL_HAVE_CHACHA20_POLY1305 @@ -237,6 +238,11 @@ int ngtcp2_crypto_cipher_ctx_encrypt_init(ngtcp2_crypto_cipher_ctx *cipher_ctx, return -1; } + if (cipher->native_handle == &ptls_openssl_aes128ecb || + cipher->native_handle == &ptls_openssl_aes256ecb) { + ptls_cipher_init(actx, NULL); + } + cipher_ctx->native_handle = actx; return 0; @@ -351,13 +357,20 @@ int ngtcp2_crypto_decrypt(uint8_t *dest, const ngtcp2_crypto_aead *aead, int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, const ngtcp2_crypto_cipher_ctx *hp_ctx, const uint8_t *sample) { + static const uint8_t PLAINTEXT[16] = {0}; ptls_cipher_context_t *actx = hp_ctx->native_handle; - static const uint8_t PLAINTEXT[] = "\x00\x00\x00\x00\x00"; (void)hp; + if (hp->native_handle == &ptls_openssl_aes128ecb || + hp->native_handle == &ptls_openssl_aes256ecb) { + ptls_cipher_encrypt(actx, dest, sample, NGTCP2_HP_SAMPLELEN); + + return 0; + } + ptls_cipher_init(actx, sample); - ptls_cipher_encrypt(actx, dest, PLAINTEXT, sizeof(PLAINTEXT) - 1); + ptls_cipher_encrypt(actx, dest, PLAINTEXT, sizeof(PLAINTEXT)); return 0; } @@ -365,7 +378,7 @@ int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, int ngtcp2_crypto_read_write_crypto_data( ngtcp2_conn *conn, ngtcp2_encryption_level encryption_level, const uint8_t *data, size_t datalen) { - ngtcp2_crypto_picotls_ctx *cptls = ngtcp2_conn_get_tls_native_handle(conn); + ngtcp2_crypto_picotls_ctx *cptls = ngtcp2_conn_get_tls_native_handle2(conn); ptls_buffer_t sendbuf; size_t epoch_offsets[5] = {0}; size_t epoch = @@ -376,7 +389,7 @@ int ngtcp2_crypto_read_write_crypto_data( ptls_buffer_init(&sendbuf, (void *)"", 0); - assert(epoch == ptls_get_read_epoch(cptls->ptls)); + assert(datalen == 0 || epoch == ptls_get_read_epoch(cptls->ptls)); rv = ptls_handle_message(cptls->ptls, &sendbuf, epoch_offsets, epoch, data, datalen, &cptls->handshake_properties); @@ -389,7 +402,7 @@ int ngtcp2_crypto_read_write_crypto_data( goto fin; } - if (!ngtcp2_conn_is_server(conn) && + if (!ngtcp2_conn_is_server2(conn) && cptls->handshake_properties.client.early_data_acceptance == PTLS_EARLY_DATA_REJECTED) { rv = ngtcp2_conn_tls_early_data_rejected(conn); @@ -492,6 +505,17 @@ int ngtcp2_crypto_get_path_challenge_data_cb(ngtcp2_conn *conn, uint8_t *data, return 0; } +int ngtcp2_crypto_get_path_challenge_data2_cb(ngtcp2_conn *conn, + ngtcp2_path_challenge_data *data, + void *user_data) { + (void)conn; + (void)user_data; + + ptls_openssl_random_bytes(data->data, NGTCP2_PATH_CHALLENGE_DATALEN); + + return 0; +} + int ngtcp2_crypto_random(uint8_t *data, size_t datalen) { ptls_openssl_random_bytes(data, datalen); @@ -499,8 +523,7 @@ int ngtcp2_crypto_random(uint8_t *data, size_t datalen) { } void ngtcp2_crypto_picotls_ctx_init(ngtcp2_crypto_picotls_ctx *cptls) { - cptls->ptls = NULL; - memset(&cptls->handshake_properties, 0, sizeof(cptls->handshake_properties)); + *cptls = (ngtcp2_crypto_picotls_ctx){0}; } static int set_additional_extensions(ptls_handshake_properties_t *hsprops, @@ -517,7 +540,7 @@ static int set_additional_extensions(ptls_handshake_properties_t *hsprops, return -1; } - nwrite = ngtcp2_conn_encode_local_transport_params(conn, buf, buflen); + nwrite = ngtcp2_conn_encode_local_transport_params2(conn, buf, buflen); if (nwrite < 0) { goto fail; } @@ -597,7 +620,7 @@ static int update_traffic_key_server_cb(ptls_update_traffic_key_t *self, * wait for the key to get the correct local transport * parameters from ngtcp2_conn. */ - cptls = ngtcp2_conn_get_tls_native_handle(conn); + cptls = ngtcp2_conn_get_tls_native_handle2(conn); if (set_additional_extensions(&cptls->handshake_properties, conn) != 0) { return -1; diff --git a/deps/ngtcp2/ngtcp2/crypto/quictls/quictls.c b/deps/ngtcp2/ngtcp2/crypto/quictls/quictls.c index 592e5a86535356..64669168d3cf60 100644 --- a/deps/ngtcp2/ngtcp2/crypto/quictls/quictls.c +++ b/deps/ngtcp2/ngtcp2/crypto/quictls/quictls.c @@ -40,6 +40,7 @@ # include #endif /* OPENSSL_VERSION_NUMBER >= 0x30000000L */ +#include "ngtcp2_macro.h" #include "shared.h" #if OPENSSL_VERSION_NUMBER >= 0x30000000L @@ -48,8 +49,8 @@ static EVP_CIPHER *crypto_aes_128_gcm; static EVP_CIPHER *crypto_aes_256_gcm; static EVP_CIPHER *crypto_chacha20_poly1305; static EVP_CIPHER *crypto_aes_128_ccm; -static EVP_CIPHER *crypto_aes_128_ctr; -static EVP_CIPHER *crypto_aes_256_ctr; +static EVP_CIPHER *crypto_aes_128_ecb; +static EVP_CIPHER *crypto_aes_256_ecb; static EVP_CIPHER *crypto_chacha20; static EVP_MD *crypto_sha256; static EVP_MD *crypto_sha384; @@ -76,13 +77,13 @@ int ngtcp2_crypto_quictls_init(void) { return -1; } - crypto_aes_128_ctr = EVP_CIPHER_fetch(NULL, "AES-128-CTR", NULL); - if (crypto_aes_128_ctr == NULL) { + crypto_aes_128_ecb = EVP_CIPHER_fetch(NULL, "AES-128-ECB", NULL); + if (crypto_aes_128_ecb == NULL) { return -1; } - crypto_aes_256_ctr = EVP_CIPHER_fetch(NULL, "AES-256-CTR", NULL); - if (crypto_aes_256_ctr == NULL) { + crypto_aes_256_ecb = EVP_CIPHER_fetch(NULL, "AES-256-ECB", NULL); + if (crypto_aes_256_ecb == NULL) { return -1; } @@ -143,20 +144,20 @@ static const EVP_CIPHER *crypto_aead_aes_128_ccm(void) { return EVP_aes_128_ccm(); } -static const EVP_CIPHER *crypto_cipher_aes_128_ctr(void) { - if (crypto_aes_128_ctr) { - return crypto_aes_128_ctr; +static const EVP_CIPHER *crypto_cipher_aes_128_ecb(void) { + if (crypto_aes_128_ecb) { + return crypto_aes_128_ecb; } - return EVP_aes_128_ctr(); + return EVP_aes_128_ecb(); } -static const EVP_CIPHER *crypto_cipher_aes_256_ctr(void) { - if (crypto_aes_256_ctr) { - return crypto_aes_256_ctr; +static const EVP_CIPHER *crypto_cipher_aes_256_ecb(void) { + if (crypto_aes_256_ecb) { + return crypto_aes_256_ecb; } - return EVP_aes_256_ctr(); + return EVP_aes_256_ecb(); } static const EVP_CIPHER *crypto_cipher_chacha20(void) { @@ -195,8 +196,8 @@ static EVP_KDF *crypto_kdf_hkdf(void) { # define crypto_aead_aes_256_gcm EVP_aes_256_gcm # define crypto_aead_chacha20_poly1305 EVP_chacha20_poly1305 # define crypto_aead_aes_128_ccm EVP_aes_128_ccm -# define crypto_cipher_aes_128_ctr EVP_aes_128_ctr -# define crypto_cipher_aes_256_ctr EVP_aes_256_ctr +# define crypto_cipher_aes_128_ecb EVP_aes_128_ecb +# define crypto_cipher_aes_256_ecb EVP_aes_256_ecb # define crypto_cipher_chacha20 EVP_chacha20 # define crypto_md_sha256 EVP_sha256 # define crypto_md_sha384 EVP_sha384 @@ -231,7 +232,7 @@ ngtcp2_crypto_md *ngtcp2_crypto_md_sha256(ngtcp2_crypto_md *md) { ngtcp2_crypto_ctx *ngtcp2_crypto_ctx_initial(ngtcp2_crypto_ctx *ctx) { ngtcp2_crypto_aead_init(&ctx->aead, (void *)crypto_aead_aes_128_gcm()); ctx->md.native_handle = (void *)crypto_md_sha256(); - ctx->hp.native_handle = (void *)crypto_cipher_aes_128_ctr(); + ctx->hp.native_handle = (void *)crypto_cipher_aes_128_ecb(); ctx->max_encryption = 0; ctx->max_decryption_failure = 0; return ctx; @@ -296,9 +297,9 @@ static const EVP_CIPHER *crypto_cipher_id_get_hp(uint32_t cipher_id) { switch (cipher_id) { case TLS1_3_CK_AES_128_GCM_SHA256: case TLS1_3_CK_AES_128_CCM_SHA256: - return crypto_cipher_aes_128_ctr(); + return crypto_cipher_aes_128_ecb(); case TLS1_3_CK_AES_256_GCM_SHA384: - return crypto_cipher_aes_256_ctr(); + return crypto_cipher_aes_256_ecb(); case TLS1_3_CK_CHACHA20_POLY1305_SHA256: return crypto_cipher_chacha20(); default: @@ -778,16 +779,31 @@ int ngtcp2_crypto_decrypt(uint8_t *dest, const ngtcp2_crypto_aead *aead, int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, const ngtcp2_crypto_cipher_ctx *hp_ctx, const uint8_t *sample) { - static const uint8_t PLAINTEXT[] = "\x00\x00\x00\x00\x00"; + static const uint8_t PLAINTEXT[16] = {0}; EVP_CIPHER_CTX *actx = hp_ctx->native_handle; int len; (void)hp; - if (!EVP_EncryptInit_ex(actx, NULL, NULL, NULL, sample) || - !EVP_EncryptUpdate(actx, dest, &len, PLAINTEXT, sizeof(PLAINTEXT) - 1) || - !EVP_EncryptFinal_ex(actx, dest + sizeof(PLAINTEXT) - 1, &len)) { - return -1; + switch (EVP_CIPHER_CTX_nid(actx)) { + case NID_aes_128_ecb: + case NID_aes_256_ecb: + if (!EVP_EncryptUpdate(actx, dest, &len, sample, NGTCP2_HP_SAMPLELEN)) { + return -1; + } + + break; + case NID_chacha20: + if (!EVP_EncryptInit_ex(actx, NULL, NULL, NULL, sample) || + !EVP_EncryptUpdate(actx, dest, &len, PLAINTEXT, sizeof(PLAINTEXT)) || + !EVP_EncryptFinal_ex(actx, dest + sizeof(PLAINTEXT), &len)) { + return -1; + } + + break; + default: + assert(0); + abort(); } return 0; @@ -796,18 +812,19 @@ int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, int ngtcp2_crypto_read_write_crypto_data( ngtcp2_conn *conn, ngtcp2_encryption_level encryption_level, const uint8_t *data, size_t datalen) { - SSL *ssl = ngtcp2_conn_get_tls_native_handle(conn); + SSL *ssl = ngtcp2_conn_get_tls_native_handle2(conn); int rv; int err; - if (SSL_provide_quic_data( + if (datalen && + SSL_provide_quic_data( ssl, ngtcp2_crypto_quictls_from_ngtcp2_encryption_level(encryption_level), data, datalen) != 1) { return -1; } - if (!ngtcp2_conn_get_handshake_completed(conn)) { + if (!ngtcp2_conn_get_handshake_completed2(conn)) { rv = SSL_do_handshake(ssl); if (rv <= 0) { err = SSL_get_error(ssl, rv); @@ -920,6 +937,19 @@ int ngtcp2_crypto_get_path_challenge_data_cb(ngtcp2_conn *conn, uint8_t *data, return 0; } +int ngtcp2_crypto_get_path_challenge_data2_cb(ngtcp2_conn *conn, + ngtcp2_path_challenge_data *data, + void *user_data) { + (void)conn; + (void)user_data; + + if (RAND_bytes(data->data, NGTCP2_PATH_CHALLENGE_DATALEN) != 1) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + int ngtcp2_crypto_random(uint8_t *data, size_t datalen) { if (RAND_bytes(data, (int)datalen) != 1) { return -1; diff --git a/deps/ngtcp2/ngtcp2/crypto/shared.c b/deps/ngtcp2/ngtcp2/crypto/shared.c index 98cd4de7e8097d..925461e239788b 100644 --- a/deps/ngtcp2/ngtcp2/crypto/shared.c +++ b/deps/ngtcp2/ngtcp2/crypto/shared.c @@ -37,6 +37,22 @@ #include "ngtcp2_macro.h" #include "ngtcp2_net.h" +/* + * NGTCP2_INITIAL_SALT_V1 is a salt value which is used to derive + * initial secret. It is used for QUIC v1. + */ +static const uint8_t NGTCP2_INITIAL_SALT_V1[] = { + 0x38, 0x76, 0x2C, 0xF7, 0xF5, 0x59, 0x34, 0xB3, 0x4D, 0x17, + 0x9A, 0xE6, 0xA4, 0xC8, 0x0C, 0xAD, 0xCC, 0xBB, 0x7F, 0x0A}; + +/* + * NGTCP2_INITIAL_SALT_V2 is a salt value which is used to derive + * initial secret. It is used for QUIC v2. + */ +static const uint8_t NGTCP2_INITIAL_SALT_V2[] = { + 0x0D, 0xED, 0xE3, 0xDE, 0xF7, 0x00, 0xA6, 0xDB, 0x81, 0x93, + 0x81, 0xBE, 0x6E, 0x26, 0x9D, 0xCB, 0xF9, 0xBD, 0x2E, 0xD9}; + ngtcp2_crypto_md *ngtcp2_crypto_md_init(ngtcp2_crypto_md *md, void *md_native_handle) { md->native_handle = md_native_handle; @@ -53,9 +69,9 @@ int ngtcp2_crypto_hkdf_expand_label(uint8_t *dest, size_t destlen, *p++ = (uint8_t)(destlen / 256); *p++ = (uint8_t)(destlen % 256); - *p++ = (uint8_t)(sizeof(LABEL) - 1 + labellen); - memcpy(p, LABEL, sizeof(LABEL) - 1); - p += sizeof(LABEL) - 1; + *p++ = (uint8_t)(ngtcp2_strlen_lit(LABEL) + labellen); + memcpy(p, LABEL, ngtcp2_strlen_lit(LABEL)); + p += ngtcp2_strlen_lit(LABEL); memcpy(p, label, labellen); p += labellen; *p++ = 0; @@ -87,12 +103,12 @@ int ngtcp2_crypto_derive_initial_secrets(uint8_t *rx_secret, uint8_t *tx_secret, switch (version) { case NGTCP2_PROTO_VER_V1: default: - salt = (const uint8_t *)NGTCP2_INITIAL_SALT_V1; - saltlen = sizeof(NGTCP2_INITIAL_SALT_V1) - 1; + salt = NGTCP2_INITIAL_SALT_V1; + saltlen = sizeof(NGTCP2_INITIAL_SALT_V1); break; case NGTCP2_PROTO_VER_V2: - salt = (const uint8_t *)NGTCP2_INITIAL_SALT_V2; - saltlen = sizeof(NGTCP2_INITIAL_SALT_V2) - 1; + salt = NGTCP2_INITIAL_SALT_V2; + saltlen = sizeof(NGTCP2_INITIAL_SALT_V2); break; } @@ -111,10 +127,12 @@ int ngtcp2_crypto_derive_initial_secrets(uint8_t *rx_secret, uint8_t *tx_secret, if (ngtcp2_crypto_hkdf_expand_label( client_secret, NGTCP2_CRYPTO_INITIAL_SECRETLEN, &ctx.md, initial_secret, - NGTCP2_CRYPTO_INITIAL_SECRETLEN, CLABEL, sizeof(CLABEL) - 1) != 0 || + NGTCP2_CRYPTO_INITIAL_SECRETLEN, CLABEL, + ngtcp2_strlen_lit(CLABEL)) != 0 || ngtcp2_crypto_hkdf_expand_label( server_secret, NGTCP2_CRYPTO_INITIAL_SECRETLEN, &ctx.md, initial_secret, - NGTCP2_CRYPTO_INITIAL_SECRETLEN, SLABEL, sizeof(SLABEL) - 1) != 0) { + NGTCP2_CRYPTO_INITIAL_SECRETLEN, SLABEL, + ngtcp2_strlen_lit(SLABEL)) != 0) { return -1; } @@ -123,7 +141,7 @@ int ngtcp2_crypto_derive_initial_secrets(uint8_t *rx_secret, uint8_t *tx_secret, size_t ngtcp2_crypto_packet_protection_ivlen(const ngtcp2_crypto_aead *aead) { size_t noncelen = ngtcp2_crypto_aead_noncelen(aead); - return ngtcp2_max_size(8, noncelen); + return ngtcp2_max(8, noncelen); } int ngtcp2_crypto_derive_packet_protection_key( @@ -148,38 +166,47 @@ int ngtcp2_crypto_derive_packet_protection_key( switch (version) { case NGTCP2_PROTO_VER_V2: key_label = KEY_LABEL_V2; - key_labellen = sizeof(KEY_LABEL_V2) - 1; + key_labellen = ngtcp2_strlen_lit(KEY_LABEL_V2); iv_label = IV_LABEL_V2; - iv_labellen = sizeof(IV_LABEL_V2) - 1; + iv_labellen = ngtcp2_strlen_lit(IV_LABEL_V2); hp_key_label = HP_KEY_LABEL_V2; - hp_key_labellen = sizeof(HP_KEY_LABEL_V2) - 1; + hp_key_labellen = ngtcp2_strlen_lit(HP_KEY_LABEL_V2); break; default: key_label = KEY_LABEL_V1; - key_labellen = sizeof(KEY_LABEL_V1) - 1; + key_labellen = ngtcp2_strlen_lit(KEY_LABEL_V1); iv_label = IV_LABEL_V1; - iv_labellen = sizeof(IV_LABEL_V1) - 1; + iv_labellen = ngtcp2_strlen_lit(IV_LABEL_V1); hp_key_label = HP_KEY_LABEL_V1; - hp_key_labellen = sizeof(HP_KEY_LABEL_V1) - 1; + hp_key_labellen = ngtcp2_strlen_lit(HP_KEY_LABEL_V1); } if (ngtcp2_crypto_hkdf_expand_label(key, keylen, md, secret, secretlen, key_label, key_labellen) != 0) { - return -1; + goto cleanup; } if (ngtcp2_crypto_hkdf_expand_label(iv, ivlen, md, secret, secretlen, iv_label, iv_labellen) != 0) { - return -1; + goto cleanup; } if (hp_key != NULL && ngtcp2_crypto_hkdf_expand_label(hp_key, keylen, md, secret, secretlen, hp_key_label, hp_key_labellen) != 0) { - return -1; + goto cleanup; } return 0; + +cleanup: + ngtcp2_secure_clear(key, keylen); + + if (hp_key) { + ngtcp2_secure_clear(hp_key, keylen); + } + + return -1; } int ngtcp2_crypto_update_traffic_secret(uint8_t *dest, uint32_t version, @@ -194,11 +221,11 @@ int ngtcp2_crypto_update_traffic_secret(uint8_t *dest, uint32_t version, switch (version) { case NGTCP2_PROTO_VER_V2: label = LABEL_V2; - labellen = sizeof(LABEL_V2) - 1; + labellen = ngtcp2_strlen_lit(LABEL_V2); break; default: label = LABEL; - labellen = sizeof(LABEL) - 1; + labellen = ngtcp2_strlen_lit(LABEL); } if (ngtcp2_crypto_hkdf_expand_label(dest, secretlen, md, secret, secretlen, @@ -220,14 +247,15 @@ int ngtcp2_crypto_derive_and_install_rx_key(ngtcp2_conn *conn, uint8_t *key, const ngtcp2_crypto_cipher *hp; ngtcp2_crypto_aead_ctx aead_ctx = {0}; ngtcp2_crypto_cipher_ctx hp_ctx = {0}; - void *tls = ngtcp2_conn_get_tls_native_handle(conn); + void *tls = ngtcp2_conn_get_tls_native_handle2(conn); uint8_t keybuf[64], ivbuf[64], hp_keybuf[64]; + size_t keylen; size_t ivlen; int rv; ngtcp2_crypto_ctx cctx; uint32_t version; - if (level == NGTCP2_ENCRYPTION_LEVEL_0RTT && !ngtcp2_conn_is_server(conn)) { + if (level == NGTCP2_ENCRYPTION_LEVEL_0RTT && !ngtcp2_conn_is_server2(conn)) { return 0; } @@ -248,12 +276,12 @@ int ngtcp2_crypto_derive_and_install_rx_key(ngtcp2_conn *conn, uint8_t *key, } ngtcp2_conn_set_0rtt_crypto_ctx(conn, &cctx); - ctx = ngtcp2_conn_get_0rtt_crypto_ctx(conn); - version = ngtcp2_conn_get_client_chosen_version(conn); + ctx = ngtcp2_conn_get_0rtt_crypto_ctx2(conn); + version = ngtcp2_conn_get_client_chosen_version2(conn); break; case NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE: - if (ngtcp2_conn_is_server(conn) && - !ngtcp2_conn_get_negotiated_version(conn)) { + if (ngtcp2_conn_is_server2(conn) && + !ngtcp2_conn_get_negotiated_version2(conn)) { rv = ngtcp2_crypto_set_remote_transport_params(conn, tls); if (rv != 0) { return -1; @@ -261,8 +289,8 @@ int ngtcp2_crypto_derive_and_install_rx_key(ngtcp2_conn *conn, uint8_t *key, } /* fall through */ case NGTCP2_ENCRYPTION_LEVEL_1RTT: - ctx = ngtcp2_conn_get_crypto_ctx(conn); - version = ngtcp2_conn_get_negotiated_version(conn); + ctx = ngtcp2_conn_get_crypto_ctx2(conn); + version = ngtcp2_conn_get_negotiated_version2(conn); if (!ctx->aead.native_handle) { if (ngtcp2_crypto_ctx_tls(&cctx, tls) == NULL) { @@ -270,7 +298,7 @@ int ngtcp2_crypto_derive_and_install_rx_key(ngtcp2_conn *conn, uint8_t *key, } ngtcp2_conn_set_crypto_ctx(conn, &cctx); - ctx = ngtcp2_conn_get_crypto_ctx(conn); + ctx = ngtcp2_conn_get_crypto_ctx2(conn); } break; default: @@ -280,6 +308,7 @@ int ngtcp2_crypto_derive_and_install_rx_key(ngtcp2_conn *conn, uint8_t *key, aead = &ctx->aead; md = &ctx->md; hp = &ctx->hp; + keylen = ngtcp2_crypto_aead_keylen(aead); ivlen = ngtcp2_crypto_packet_protection_ivlen(aead); if (ngtcp2_crypto_derive_packet_protection_key(key, iv, hp_key, version, aead, @@ -310,7 +339,7 @@ int ngtcp2_crypto_derive_and_install_rx_key(ngtcp2_conn *conn, uint8_t *key, } break; case NGTCP2_ENCRYPTION_LEVEL_1RTT: - if (!ngtcp2_conn_is_server(conn)) { + if (!ngtcp2_conn_is_server2(conn)) { rv = ngtcp2_crypto_set_remote_transport_params(conn, tls); if (rv != 0) { goto fail; @@ -328,12 +357,21 @@ int ngtcp2_crypto_derive_and_install_rx_key(ngtcp2_conn *conn, uint8_t *key, goto fail; } + /* Clear keys in the stack buffers. */ + ngtcp2_secure_clear(keybuf, keylen); + ngtcp2_secure_clear(hp_keybuf, keylen); + return 0; fail: ngtcp2_crypto_cipher_ctx_free(&hp_ctx); ngtcp2_crypto_aead_ctx_free(&aead_ctx); + /* Clear keys from the given buffers as well as the stack buffers if + used. */ + ngtcp2_secure_clear(key, keylen); + ngtcp2_secure_clear(hp_key, keylen); + return -1; } @@ -347,7 +385,7 @@ static int crypto_set_local_transport_params(ngtcp2_conn *conn, void *tls) { ngtcp2_ssize nwrite; uint8_t buf[256]; - nwrite = ngtcp2_conn_encode_local_transport_params(conn, buf, sizeof(buf)); + nwrite = ngtcp2_conn_encode_local_transport_params2(conn, buf, sizeof(buf)); if (nwrite < 0) { return -1; } @@ -370,14 +408,15 @@ int ngtcp2_crypto_derive_and_install_tx_key(ngtcp2_conn *conn, uint8_t *key, const ngtcp2_crypto_cipher *hp; ngtcp2_crypto_aead_ctx aead_ctx = {0}; ngtcp2_crypto_cipher_ctx hp_ctx = {0}; - void *tls = ngtcp2_conn_get_tls_native_handle(conn); + void *tls = ngtcp2_conn_get_tls_native_handle2(conn); uint8_t keybuf[64], ivbuf[64], hp_keybuf[64]; + size_t keylen; size_t ivlen; int rv; ngtcp2_crypto_ctx cctx; uint32_t version; - if (level == NGTCP2_ENCRYPTION_LEVEL_0RTT && ngtcp2_conn_is_server(conn)) { + if (level == NGTCP2_ENCRYPTION_LEVEL_0RTT && ngtcp2_conn_is_server2(conn)) { return 0; } @@ -398,12 +437,12 @@ int ngtcp2_crypto_derive_and_install_tx_key(ngtcp2_conn *conn, uint8_t *key, } ngtcp2_conn_set_0rtt_crypto_ctx(conn, &cctx); - ctx = ngtcp2_conn_get_0rtt_crypto_ctx(conn); - version = ngtcp2_conn_get_client_chosen_version(conn); + ctx = ngtcp2_conn_get_0rtt_crypto_ctx2(conn); + version = ngtcp2_conn_get_client_chosen_version2(conn); break; case NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE: - if (ngtcp2_conn_is_server(conn) && - !ngtcp2_conn_get_negotiated_version(conn)) { + if (ngtcp2_conn_is_server2(conn) && + !ngtcp2_conn_get_negotiated_version2(conn)) { rv = ngtcp2_crypto_set_remote_transport_params(conn, tls); if (rv != 0) { return -1; @@ -411,8 +450,8 @@ int ngtcp2_crypto_derive_and_install_tx_key(ngtcp2_conn *conn, uint8_t *key, } /* fall through */ case NGTCP2_ENCRYPTION_LEVEL_1RTT: - ctx = ngtcp2_conn_get_crypto_ctx(conn); - version = ngtcp2_conn_get_negotiated_version(conn); + ctx = ngtcp2_conn_get_crypto_ctx2(conn); + version = ngtcp2_conn_get_negotiated_version2(conn); if (!ctx->aead.native_handle) { if (ngtcp2_crypto_ctx_tls(&cctx, tls) == NULL) { @@ -420,7 +459,7 @@ int ngtcp2_crypto_derive_and_install_tx_key(ngtcp2_conn *conn, uint8_t *key, } ngtcp2_conn_set_crypto_ctx(conn, &cctx); - ctx = ngtcp2_conn_get_crypto_ctx(conn); + ctx = ngtcp2_conn_get_crypto_ctx2(conn); } break; default: @@ -430,6 +469,7 @@ int ngtcp2_crypto_derive_and_install_tx_key(ngtcp2_conn *conn, uint8_t *key, aead = &ctx->aead; md = &ctx->md; hp = &ctx->hp; + keylen = ngtcp2_crypto_aead_keylen(aead); ivlen = ngtcp2_crypto_packet_protection_ivlen(aead); if (ngtcp2_crypto_derive_packet_protection_key(key, iv, hp_key, version, aead, @@ -459,9 +499,11 @@ int ngtcp2_crypto_derive_and_install_tx_key(ngtcp2_conn *conn, uint8_t *key, goto fail; } - if (ngtcp2_conn_is_server(conn) && + if (ngtcp2_conn_is_server2(conn) && crypto_set_local_transport_params(conn, tls) != 0) { - goto fail; + /* Just return -1 because aead_ctx and hp_ctx are now owned by + conn. */ + goto fail_cleanup; } break; @@ -477,12 +519,22 @@ int ngtcp2_crypto_derive_and_install_tx_key(ngtcp2_conn *conn, uint8_t *key, goto fail; } + /* Clear keys in the stack buffers. */ + ngtcp2_secure_clear(keybuf, keylen); + ngtcp2_secure_clear(hp_keybuf, keylen); + return 0; fail: ngtcp2_crypto_cipher_ctx_free(&hp_ctx); ngtcp2_crypto_aead_ctx_free(&aead_ctx); +fail_cleanup: + /* Clear keys from the given buffers as well as the stack buffers if + used. */ + ngtcp2_secure_clear(key, keylen); + ngtcp2_secure_clear(hp_key, keylen); + return -1; } @@ -508,7 +560,7 @@ int ngtcp2_crypto_derive_and_install_initial_key( ngtcp2_crypto_cipher_ctx tx_hp_ctx = {0}; ngtcp2_crypto_aead_ctx retry_aead_ctx = {0}; int rv; - int server = ngtcp2_conn_is_server(conn); + int server = ngtcp2_conn_is_server2(conn); const uint8_t *retry_key; size_t retry_noncelen; @@ -583,18 +635,18 @@ int ngtcp2_crypto_derive_and_install_initial_key( goto fail; } - if (!server && !ngtcp2_conn_after_retry(conn)) { + if (!server && !ngtcp2_conn_after_retry2(conn)) { ngtcp2_crypto_aead_retry(&retry_aead); switch (version) { case NGTCP2_PROTO_VER_V1: default: retry_key = (const uint8_t *)NGTCP2_RETRY_KEY_V1; - retry_noncelen = sizeof(NGTCP2_RETRY_NONCE_V1) - 1; + retry_noncelen = ngtcp2_strlen_lit(NGTCP2_RETRY_NONCE_V1); break; case NGTCP2_PROTO_VER_V2: retry_key = (const uint8_t *)NGTCP2_RETRY_KEY_V2; - retry_noncelen = sizeof(NGTCP2_RETRY_NONCE_V2) - 1; + retry_noncelen = ngtcp2_strlen_lit(NGTCP2_RETRY_NONCE_V2); break; } @@ -641,13 +693,13 @@ int ngtcp2_crypto_derive_and_install_vneg_initial_key( uint8_t tx_keybuf[NGTCP2_CRYPTO_INITIAL_KEYLEN]; uint8_t tx_ivbuf[NGTCP2_CRYPTO_INITIAL_IVLEN]; uint8_t tx_hp_keybuf[NGTCP2_CRYPTO_INITIAL_KEYLEN]; - const ngtcp2_crypto_ctx *ctx = ngtcp2_conn_get_initial_crypto_ctx(conn); + const ngtcp2_crypto_ctx *ctx = ngtcp2_conn_get_initial_crypto_ctx2(conn); ngtcp2_crypto_aead_ctx rx_aead_ctx = {0}; ngtcp2_crypto_cipher_ctx rx_hp_ctx = {0}; ngtcp2_crypto_aead_ctx tx_aead_ctx = {0}; ngtcp2_crypto_cipher_ctx tx_hp_ctx = {0}; int rv; - int server = ngtcp2_conn_is_server(conn); + int server = ngtcp2_conn_is_server2(conn); if (!rx_secret) { rx_secret = rx_secretbuf; @@ -740,45 +792,54 @@ int ngtcp2_crypto_update_key( ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_key, uint8_t *tx_iv, const uint8_t *current_rx_secret, const uint8_t *current_tx_secret, size_t secretlen) { - const ngtcp2_crypto_ctx *ctx = ngtcp2_conn_get_crypto_ctx(conn); + const ngtcp2_crypto_ctx *ctx = ngtcp2_conn_get_crypto_ctx2(conn); const ngtcp2_crypto_aead *aead = &ctx->aead; const ngtcp2_crypto_md *md = &ctx->md; + size_t keylen = ngtcp2_crypto_aead_keylen(aead); size_t ivlen = ngtcp2_crypto_packet_protection_ivlen(aead); - uint32_t version = ngtcp2_conn_get_negotiated_version(conn); + uint32_t version = ngtcp2_conn_get_negotiated_version2(conn); if (ngtcp2_crypto_update_traffic_secret(rx_secret, version, md, current_rx_secret, secretlen) != 0) { - return -1; + goto cleanup; } if (ngtcp2_crypto_derive_packet_protection_key( rx_key, rx_iv, NULL, version, aead, md, rx_secret, secretlen) != 0) { - return -1; + goto cleanup; } if (ngtcp2_crypto_update_traffic_secret(tx_secret, version, md, current_tx_secret, secretlen) != 0) { - return -1; + goto cleanup; } if (ngtcp2_crypto_derive_packet_protection_key( tx_key, tx_iv, NULL, version, aead, md, tx_secret, secretlen) != 0) { - return -1; + goto cleanup; } if (ngtcp2_crypto_aead_ctx_decrypt_init(rx_aead_ctx, aead, rx_key, ivlen) != 0) { - return -1; + goto cleanup; } if (ngtcp2_crypto_aead_ctx_encrypt_init(tx_aead_ctx, aead, tx_key, ivlen) != 0) { ngtcp2_crypto_aead_ctx_free(rx_aead_ctx); rx_aead_ctx->native_handle = NULL; - return -1; + goto cleanup; } return 0; + +cleanup: + ngtcp2_secure_clear(rx_secret, secretlen); + ngtcp2_secure_clear(tx_secret, secretlen); + ngtcp2_secure_clear(rx_key, keylen); + ngtcp2_secure_clear(tx_key, keylen); + + return -1; } int ngtcp2_crypto_encrypt_cb(uint8_t *dest, const ngtcp2_crypto_aead *aead, @@ -822,12 +883,18 @@ int ngtcp2_crypto_update_key_cb( size_t secretlen, void *user_data) { uint8_t rx_key[64]; uint8_t tx_key[64]; + int rv; (void)conn; (void)user_data; - if (ngtcp2_crypto_update_key( - conn, rx_secret, tx_secret, rx_aead_ctx, rx_key, rx_iv, tx_aead_ctx, - tx_key, tx_iv, current_rx_secret, current_tx_secret, secretlen) != 0) { + rv = ngtcp2_crypto_update_key( + conn, rx_secret, tx_secret, rx_aead_ctx, rx_key, rx_iv, tx_aead_ctx, tx_key, + tx_iv, current_rx_secret, current_tx_secret, secretlen); + + ngtcp2_secure_clear(rx_key, sizeof(rx_key)); + ngtcp2_secure_clear(tx_key, sizeof(tx_key)); + + if (rv != 0) { return NGTCP2_ERR_CALLBACK_FAILURE; } return 0; @@ -843,7 +910,7 @@ int ngtcp2_crypto_generate_stateless_reset_token(uint8_t *token, if (ngtcp2_crypto_hkdf(token, NGTCP2_STATELESS_RESET_TOKENLEN, ngtcp2_crypto_md_sha256(&md), secret, secretlen, cid->data, cid->datalen, info, - sizeof(info) - 1) != 0) { + ngtcp2_strlen_lit(info)) != 0) { return -1; } @@ -863,8 +930,8 @@ static int crypto_derive_token_key(uint8_t *key, size_t keylen, uint8_t *iv, uint8_t *p; assert(ngtcp2_crypto_md_hashlen(md) == sizeof(intsecret)); - assert(info_prefixlen + sizeof(key_info_suffix) - 1 <= sizeof(info)); - assert(info_prefixlen + sizeof(iv_info_suffix) - 1 <= sizeof(info)); + assert(info_prefixlen + ngtcp2_strlen_lit(key_info_suffix) <= sizeof(info)); + assert(info_prefixlen + ngtcp2_strlen_lit(iv_info_suffix) <= sizeof(info)); if (ngtcp2_crypto_hkdf_extract(intsecret, md, secret, secretlen, salt, saltlen) != 0) { @@ -874,8 +941,8 @@ static int crypto_derive_token_key(uint8_t *key, size_t keylen, uint8_t *iv, memcpy(info, info_prefix, info_prefixlen); p = info + info_prefixlen; - memcpy(p, key_info_suffix, sizeof(key_info_suffix) - 1); - p += sizeof(key_info_suffix) - 1; + memcpy(p, key_info_suffix, ngtcp2_strlen_lit(key_info_suffix)); + p += ngtcp2_strlen_lit(key_info_suffix); if (ngtcp2_crypto_hkdf_expand(key, keylen, md, intsecret, sizeof(intsecret), info, (size_t)(p - info)) != 0) { @@ -884,8 +951,8 @@ static int crypto_derive_token_key(uint8_t *key, size_t keylen, uint8_t *iv, p = info + info_prefixlen; - memcpy(p, iv_info_suffix, sizeof(iv_info_suffix) - 1); - p += sizeof(iv_info_suffix) - 1; + memcpy(p, iv_info_suffix, ngtcp2_strlen_lit(iv_info_suffix)); + p += ngtcp2_strlen_lit(iv_info_suffix); if (ngtcp2_crypto_hkdf_expand(iv, ivlen, md, intsecret, sizeof(intsecret), info, (size_t)(p - info)) != 0) { @@ -918,8 +985,8 @@ ngtcp2_ssize ngtcp2_crypto_generate_retry_token( uint8_t *token, const uint8_t *secret, size_t secretlen, uint32_t version, const ngtcp2_sockaddr *remote_addr, ngtcp2_socklen remote_addrlen, const ngtcp2_cid *retry_scid, const ngtcp2_cid *odcid, ngtcp2_tstamp ts) { - uint8_t - plaintext[/* cid len = */ 1 + NGTCP2_MAX_CIDLEN + sizeof(ngtcp2_tstamp)]; + uint8_t plaintext[/* cid len = */ 1 + NGTCP2_MAX_CIDLEN + + sizeof(ngtcp2_tstamp)] = {0}; uint8_t rand_data[NGTCP2_CRYPTO_TOKEN_RAND_DATALEN]; uint8_t key[16]; uint8_t iv[12]; @@ -938,8 +1005,6 @@ ngtcp2_ssize ngtcp2_crypto_generate_retry_token( assert((size_t)remote_addrlen <= sizeof(ngtcp2_sockaddr_union)); - memset(plaintext, 0, sizeof(plaintext)); - *p++ = (uint8_t)odcid->datalen; memcpy(p, odcid->data, odcid->datalen); p += NGTCP2_MAX_CIDLEN; @@ -961,10 +1026,10 @@ ngtcp2_ssize ngtcp2_crypto_generate_retry_token( assert(sizeof(key) == keylen); assert(sizeof(iv) == ivlen); - if (crypto_derive_token_key(key, keylen, iv, ivlen, &md, secret, secretlen, - rand_data, sizeof(rand_data), - retry_token_info_prefix, - sizeof(retry_token_info_prefix) - 1) != 0) { + if (crypto_derive_token_key( + key, keylen, iv, ivlen, &md, secret, secretlen, rand_data, + sizeof(rand_data), retry_token_info_prefix, + ngtcp2_strlen_lit(retry_token_info_prefix)) != 0) { return -1; } @@ -994,6 +1059,11 @@ ngtcp2_ssize ngtcp2_crypto_generate_retry_token( return p - token; } +static int crypto_token_expired(ngtcp2_tstamp gen_ts, ngtcp2_duration timeout, + ngtcp2_tstamp ts) { + return ts >= timeout && gen_ts <= ts - timeout; +} + int ngtcp2_crypto_verify_retry_token( ngtcp2_cid *odcid, const uint8_t *token, size_t tokenlen, const uint8_t *secret, size_t secretlen, uint32_t version, @@ -1038,10 +1108,10 @@ int ngtcp2_crypto_verify_retry_token( assert(sizeof(key) == keylen); assert(sizeof(iv) == ivlen); - if (crypto_derive_token_key(key, keylen, iv, ivlen, &md, secret, secretlen, - rand_data, NGTCP2_CRYPTO_TOKEN_RAND_DATALEN, - retry_token_info_prefix, - sizeof(retry_token_info_prefix) - 1) != 0) { + if (crypto_derive_token_key( + key, keylen, iv, ivlen, &md, secret, secretlen, rand_data, + NGTCP2_CRYPTO_TOKEN_RAND_DATALEN, retry_token_info_prefix, + ngtcp2_strlen_lit(retry_token_info_prefix)) != 0) { return -1; } @@ -1071,7 +1141,7 @@ int ngtcp2_crypto_verify_retry_token( sizeof(gen_ts)); gen_ts = ngtcp2_ntohl64(gen_ts); - if (gen_ts + timeout <= ts) { + if (crypto_token_expired(gen_ts, timeout, ts)) { return -1; } @@ -1100,7 +1170,7 @@ ngtcp2_ssize ngtcp2_crypto_generate_retry_token2( const ngtcp2_sockaddr *remote_addr, ngtcp2_socklen remote_addrlen, const ngtcp2_cid *retry_scid, const ngtcp2_cid *odcid, ngtcp2_tstamp ts) { uint8_t plaintext[sizeof(ngtcp2_sockaddr_union) + /* cid len = */ 1 + - NGTCP2_MAX_CIDLEN + sizeof(ngtcp2_tstamp)]; + NGTCP2_MAX_CIDLEN + sizeof(ngtcp2_tstamp)] = {0}; uint8_t rand_data[NGTCP2_CRYPTO_TOKEN_RAND_DATALEN]; uint8_t key[16]; uint8_t iv[12]; @@ -1117,8 +1187,6 @@ ngtcp2_ssize ngtcp2_crypto_generate_retry_token2( assert((size_t)remote_addrlen <= sizeof(ngtcp2_sockaddr_union)); - memset(plaintext, 0, sizeof(plaintext)); - memcpy(p, remote_addr, (size_t)remote_addrlen); p += sizeof(ngtcp2_sockaddr_union); *p++ = (uint8_t)odcid->datalen; @@ -1141,10 +1209,10 @@ ngtcp2_ssize ngtcp2_crypto_generate_retry_token2( assert(sizeof(key) == keylen); assert(sizeof(iv) == ivlen); - if (crypto_derive_token_key(key, keylen, iv, ivlen, &md, secret, secretlen, - rand_data, sizeof(rand_data), - retry_token_info_prefix2, - sizeof(retry_token_info_prefix2) - 1) != 0) { + if (crypto_derive_token_key( + key, keylen, iv, ivlen, &md, secret, secretlen, rand_data, + sizeof(rand_data), retry_token_info_prefix2, + ngtcp2_strlen_lit(retry_token_info_prefix2)) != 0) { return -1; } @@ -1219,10 +1287,10 @@ int ngtcp2_crypto_verify_retry_token2( assert(sizeof(key) == keylen); assert(sizeof(iv) == ivlen); - if (crypto_derive_token_key(key, keylen, iv, ivlen, &md, secret, secretlen, - rand_data, NGTCP2_CRYPTO_TOKEN_RAND_DATALEN, - retry_token_info_prefix2, - sizeof(retry_token_info_prefix2) - 1) != 0) { + if (crypto_derive_token_key( + key, keylen, iv, ivlen, &md, secret, secretlen, rand_data, + NGTCP2_CRYPTO_TOKEN_RAND_DATALEN, retry_token_info_prefix2, + ngtcp2_strlen_lit(retry_token_info_prefix2)) != 0) { return NGTCP2_CRYPTO_ERR_INTERNAL; } @@ -1271,7 +1339,7 @@ int ngtcp2_crypto_verify_retry_token2( memcpy(&gen_ts, p + NGTCP2_MAX_CIDLEN, sizeof(gen_ts)); gen_ts = ngtcp2_ntohl64(gen_ts); - if (gen_ts + timeout <= ts) { + if (crypto_token_expired(gen_ts, timeout, ts)) { return NGTCP2_CRYPTO_ERR_VERIFY_TOKEN; } @@ -1305,13 +1373,22 @@ static size_t crypto_generate_regular_token_aad(uint8_t *dest, return addrlen; } +/* NGTCP2_CRYPTO_MAX_REGULAR_TOKEN_DATALEN is the maximum length of + opaque data embedded in a regular token. */ +#define NGTCP2_CRYPTO_MAX_REGULAR_TOKEN_DATALEN 256 + +/* NGTCP2_CRYPTO_MAX_REGULAR_TOKEN_PLAINTEXTLEN is the maximum length + of plaintext included in a regular token. */ +#define NGTCP2_CRYPTO_MAX_REGULAR_TOKEN_PLAINTEXTLEN \ + (sizeof(ngtcp2_tstamp) + NGTCP2_CRYPTO_MAX_REGULAR_TOKEN_DATALEN) + static const uint8_t regular_token_info_prefix[] = "regular_token"; -ngtcp2_ssize ngtcp2_crypto_generate_regular_token( +static ngtcp2_ssize crypto_generate_regular_token( uint8_t *token, const uint8_t *secret, size_t secretlen, const ngtcp2_sockaddr *remote_addr, ngtcp2_socklen remote_addrlen, - ngtcp2_tstamp ts) { - uint8_t plaintext[sizeof(ngtcp2_tstamp)]; + const void *data, size_t datalen, ngtcp2_tstamp ts) { + uint8_t plaintext[NGTCP2_CRYPTO_MAX_REGULAR_TOKEN_PLAINTEXTLEN]; uint8_t rand_data[NGTCP2_CRYPTO_TOKEN_RAND_DATALEN]; uint8_t key[16]; uint8_t iv[12]; @@ -1328,9 +1405,18 @@ ngtcp2_ssize ngtcp2_crypto_generate_regular_token( int rv; (void)remote_addrlen; + if (datalen > NGTCP2_CRYPTO_MAX_REGULAR_TOKEN_DATALEN) { + return -1; + } + memcpy(p, &ts_be, sizeof(ts_be)); p += sizeof(ts_be); + if (datalen) { + memcpy(p, data, datalen); + p += datalen; + } + plaintextlen = (size_t)(p - plaintext); if (ngtcp2_crypto_random(rand_data, sizeof(rand_data)) != 0) { @@ -1346,10 +1432,10 @@ ngtcp2_ssize ngtcp2_crypto_generate_regular_token( assert(sizeof(key) == keylen); assert(sizeof(iv) == ivlen); - if (crypto_derive_token_key(key, keylen, iv, ivlen, &md, secret, secretlen, - rand_data, sizeof(rand_data), - regular_token_info_prefix, - sizeof(regular_token_info_prefix) - 1) != 0) { + if (crypto_derive_token_key( + key, keylen, iv, ivlen, &md, secret, secretlen, rand_data, + sizeof(rand_data), regular_token_info_prefix, + ngtcp2_strlen_lit(regular_token_info_prefix)) != 0) { return -1; } @@ -1378,13 +1464,11 @@ ngtcp2_ssize ngtcp2_crypto_generate_regular_token( return p - token; } -int ngtcp2_crypto_verify_regular_token(const uint8_t *token, size_t tokenlen, - const uint8_t *secret, size_t secretlen, - const ngtcp2_sockaddr *remote_addr, - ngtcp2_socklen remote_addrlen, - ngtcp2_duration timeout, - ngtcp2_tstamp ts) { - uint8_t plaintext[sizeof(ngtcp2_tstamp)]; +static ngtcp2_ssize crypto_verify_regular_token( + void *data, size_t max_datalen, const uint8_t *token, size_t tokenlen, + const uint8_t *secret, size_t secretlen, const ngtcp2_sockaddr *remote_addr, + ngtcp2_socklen remote_addrlen, ngtcp2_duration timeout, ngtcp2_tstamp ts) { + uint8_t plaintext[NGTCP2_CRYPTO_MAX_REGULAR_TOKEN_PLAINTEXTLEN]; uint8_t key[16]; uint8_t iv[12]; size_t keylen; @@ -1397,13 +1481,14 @@ int ngtcp2_crypto_verify_regular_token(const uint8_t *token, size_t tokenlen, const uint8_t *rand_data; const uint8_t *ciphertext; size_t ciphertextlen; + size_t datalen; int rv; ngtcp2_tstamp gen_ts; (void)remote_addrlen; - if (tokenlen != NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN || + if (tokenlen < NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN || token[0] != NGTCP2_CRYPTO_TOKEN_MAGIC_REGULAR) { - return -1; + return NGTCP2_CRYPTO_ERR_UNREADABLE_TOKEN; } rand_data = token + tokenlen - NGTCP2_CRYPTO_TOKEN_RAND_DATALEN; @@ -1413,23 +1498,27 @@ int ngtcp2_crypto_verify_regular_token(const uint8_t *token, size_t tokenlen, ngtcp2_crypto_aead_aes_128_gcm(&aead); ngtcp2_crypto_md_sha256(&md); + if (ciphertextlen > sizeof(plaintext) + aead.max_overhead) { + return NGTCP2_CRYPTO_ERR_UNREADABLE_TOKEN; + } + keylen = ngtcp2_crypto_aead_keylen(&aead); ivlen = ngtcp2_crypto_aead_noncelen(&aead); assert(sizeof(key) == keylen); assert(sizeof(iv) == ivlen); - if (crypto_derive_token_key(key, keylen, iv, ivlen, &md, secret, secretlen, - rand_data, NGTCP2_CRYPTO_TOKEN_RAND_DATALEN, - regular_token_info_prefix, - sizeof(regular_token_info_prefix) - 1) != 0) { - return -1; + if (crypto_derive_token_key( + key, keylen, iv, ivlen, &md, secret, secretlen, rand_data, + NGTCP2_CRYPTO_TOKEN_RAND_DATALEN, regular_token_info_prefix, + ngtcp2_strlen_lit(regular_token_info_prefix)) != 0) { + return NGTCP2_CRYPTO_ERR_INTERNAL; } aadlen = crypto_generate_regular_token_aad(aad, remote_addr); if (ngtcp2_crypto_aead_ctx_decrypt_init(&aead_ctx, &aead, key, ivlen) != 0) { - return -1; + return NGTCP2_CRYPTO_ERR_INTERNAL; } rv = ngtcp2_crypto_decrypt(plaintext, &aead, &aead_ctx, ciphertext, @@ -1438,19 +1527,74 @@ int ngtcp2_crypto_verify_regular_token(const uint8_t *token, size_t tokenlen, ngtcp2_crypto_aead_ctx_free(&aead_ctx); if (rv != 0) { - return -1; + return NGTCP2_CRYPTO_ERR_UNREADABLE_TOKEN; } memcpy(&gen_ts, plaintext, sizeof(gen_ts)); gen_ts = ngtcp2_ntohl64(gen_ts); - if (gen_ts + timeout <= ts) { + if (crypto_token_expired(gen_ts, timeout, ts)) { + return NGTCP2_CRYPTO_ERR_VERIFY_TOKEN; + } + + if (max_datalen == 0) { + return 0; + } + + datalen = ciphertextlen - aead.max_overhead - sizeof(gen_ts); + if (datalen > max_datalen) { + return 0; + } + + memcpy(data, plaintext + sizeof(gen_ts), datalen); + + return (ngtcp2_ssize)datalen; +} + +ngtcp2_ssize ngtcp2_crypto_generate_regular_token( + uint8_t *token, const uint8_t *secret, size_t secretlen, + const ngtcp2_sockaddr *remote_addr, ngtcp2_socklen remote_addrlen, + ngtcp2_tstamp ts) { + return crypto_generate_regular_token(token, secret, secretlen, remote_addr, + remote_addrlen, NULL, 0, ts); +} + +int ngtcp2_crypto_verify_regular_token(const uint8_t *token, size_t tokenlen, + const uint8_t *secret, size_t secretlen, + const ngtcp2_sockaddr *remote_addr, + ngtcp2_socklen remote_addrlen, + ngtcp2_duration timeout, + ngtcp2_tstamp ts) { + ngtcp2_ssize datalen = + crypto_verify_regular_token(NULL, 0, token, tokenlen, secret, secretlen, + remote_addr, remote_addrlen, timeout, ts); + + if (datalen < 0) { return -1; } + assert(0 == datalen); + return 0; } +ngtcp2_ssize ngtcp2_crypto_generate_regular_token2( + uint8_t *token, const uint8_t *secret, size_t secretlen, + const ngtcp2_sockaddr *remote_addr, ngtcp2_socklen remote_addrlen, + const void *data, size_t datalen, ngtcp2_tstamp ts) { + return crypto_generate_regular_token(token, secret, secretlen, remote_addr, + remote_addrlen, data, datalen, ts); +} + +ngtcp2_ssize ngtcp2_crypto_verify_regular_token2( + void *data, size_t max_datalen, const uint8_t *token, size_t tokenlen, + const uint8_t *secret, size_t secretlen, const ngtcp2_sockaddr *remote_addr, + ngtcp2_socklen remote_addrlen, ngtcp2_duration timeout, ngtcp2_tstamp ts) { + return crypto_verify_regular_token(data, max_datalen, token, tokenlen, secret, + secretlen, remote_addr, remote_addrlen, + timeout, ts); +} + ngtcp2_ssize ngtcp2_crypto_write_connection_close( uint8_t *dest, size_t destlen, uint32_t version, const ngtcp2_cid *dcid, const ngtcp2_cid *scid, uint64_t error_code, const uint8_t *reason, @@ -1523,11 +1667,11 @@ ngtcp2_ssize ngtcp2_crypto_write_retry(uint8_t *dest, size_t destlen, case NGTCP2_PROTO_VER_V1: default: key = (const uint8_t *)NGTCP2_RETRY_KEY_V1; - noncelen = sizeof(NGTCP2_RETRY_NONCE_V1) - 1; + noncelen = ngtcp2_strlen_lit(NGTCP2_RETRY_NONCE_V1); break; case NGTCP2_PROTO_VER_V2: key = (const uint8_t *)NGTCP2_RETRY_KEY_V2; - noncelen = sizeof(NGTCP2_RETRY_NONCE_V2) - 1; + noncelen = ngtcp2_strlen_lit(NGTCP2_RETRY_NONCE_V2); break; } @@ -1549,13 +1693,13 @@ ngtcp2_ssize ngtcp2_crypto_write_retry(uint8_t *dest, size_t destlen, } int ngtcp2_crypto_client_initial_cb(ngtcp2_conn *conn, void *user_data) { - const ngtcp2_cid *dcid = ngtcp2_conn_get_dcid(conn); - void *tls = ngtcp2_conn_get_tls_native_handle(conn); + const ngtcp2_cid *dcid = ngtcp2_conn_get_dcid2(conn); + void *tls = ngtcp2_conn_get_tls_native_handle2(conn); (void)user_data; if (ngtcp2_crypto_derive_and_install_initial_key( conn, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - ngtcp2_conn_get_client_chosen_version(conn), dcid) != 0) { + ngtcp2_conn_get_client_chosen_version2(conn), dcid) != 0) { return NGTCP2_ERR_CALLBACK_FAILURE; } @@ -1577,7 +1721,7 @@ int ngtcp2_crypto_recv_retry_cb(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd, if (ngtcp2_crypto_derive_and_install_initial_key( conn, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - ngtcp2_conn_get_client_chosen_version(conn), &hd->scid) != 0) { + ngtcp2_conn_get_client_chosen_version2(conn), &hd->scid) != 0) { return NGTCP2_ERR_CALLBACK_FAILURE; } @@ -1591,7 +1735,7 @@ int ngtcp2_crypto_recv_client_initial_cb(ngtcp2_conn *conn, if (ngtcp2_crypto_derive_and_install_initial_key( conn, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - ngtcp2_conn_get_client_chosen_version(conn), dcid) != 0) { + ngtcp2_conn_get_client_chosen_version2(conn), dcid) != 0) { return NGTCP2_ERR_CALLBACK_FAILURE; } @@ -1637,9 +1781,22 @@ int ngtcp2_crypto_recv_crypto_data_cb(ngtcp2_conn *conn, (void)offset; (void)user_data; - if (ngtcp2_crypto_read_write_crypto_data(conn, encryption_level, data, - datalen) != 0) { - rv = ngtcp2_conn_get_tls_error(conn); + rv = + ngtcp2_crypto_read_write_crypto_data(conn, encryption_level, data, datalen); + if (rv != 0) { + switch (rv) { + case /* NGTCP2_CRYPTO_QUICTLS_ERR_TLS_WANT_CLIENT_HELLO_CB */ -10001: + case /* NGTCP2_CRYPTO_QUICTLS_ERR_TLS_WANT_X509_LOOKUP */ -10002: + /* These errors are not unrecoverable error, and they just + indicate that handshake has been interrupted. ngtcp2 does + not mind whether handshake is interrupted or not. Just + return 0 in this case. There are OSSL version and they have + the same enum value, therefore we cannot enumerate them + here. */ + return 0; + } + + rv = ngtcp2_conn_get_tls_error2(conn); if (rv) { return rv; } diff --git a/deps/ngtcp2/ngtcp2/crypto/shared.h b/deps/ngtcp2/ngtcp2/crypto/shared.h index 34158d3d02dbc0..e853c8140fea76 100644 --- a/deps/ngtcp2/ngtcp2/crypto/shared.h +++ b/deps/ngtcp2/ngtcp2/crypto/shared.h @@ -31,26 +31,6 @@ #include -/** - * @macro - * - * :macro:`NGTCP2_INITIAL_SALT_V1` is a salt value which is used to - * derive initial secret. It is used for QUIC v1. - */ -#define NGTCP2_INITIAL_SALT_V1 \ - "\x38\x76\x2c\xf7\xf5\x59\x34\xb3\x4d\x17\x9a\xe6\xa4\xc8\x0c\xad\xcc\xbb" \ - "\x7f\x0a" - -/** - * @macro - * - * :macro:`NGTCP2_INITIAL_SALT_V2` is a salt value which is used to - * derive initial secret. It is used for QUIC v2. - */ -#define NGTCP2_INITIAL_SALT_V2 \ - "\x0d\xed\xe3\xde\xf7\x00\xa6\xdb\x81\x93\x81\xbe\x6e\x26\x9d\xcb\xf9\xbd" \ - "\x2e\xd9" - /* Maximum key usage (encryption) limits */ #define NGTCP2_CRYPTO_MAX_ENCRYPTION_AES_GCM (1ULL << 23) #define NGTCP2_CRYPTO_MAX_ENCRYPTION_CHACHA20_POLY1305 (1ULL << 62) @@ -281,7 +261,7 @@ int ngtcp2_crypto_set_remote_transport_params(ngtcp2_conn *conn, void *tls); * This function calls `ngtcp2_conn_set_initial_crypto_ctx` to set * initial AEAD and message digest algorithm. After the successful * call of this function, application can use - * `ngtcp2_conn_get_initial_crypto_ctx` to get the object. + * `ngtcp2_conn_get_initial_crypto_ctx2` to get the object. * * This function returns 0 if it succeeds, or -1. */ diff --git a/deps/ngtcp2/ngtcp2/crypto/shared_test.c b/deps/ngtcp2/ngtcp2/crypto/shared_test.c new file mode 100644 index 00000000000000..68e78b0cad0cf0 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/crypto/shared_test.c @@ -0,0 +1,218 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shared_test.h" + +#include + +#include "shared.h" +#include "ngtcp2_macro.h" + +static const MunitTest tests[] = { + munit_void_test(test_ngtcp2_crypto_verify_retry_token), + munit_void_test(test_ngtcp2_crypto_verify_regular_token), + munit_test_end(), +}; + +const MunitSuite shared_suite = { + .prefix = "/shared", + .tests = tests, +}; + +void test_ngtcp2_crypto_verify_retry_token(void) { + const uint8_t secret[] = "retry-token-secret"; + const ngtcp2_sockaddr_in6 in6addr = { + .sin6_family = NGTCP2_AF_INET6, + .sin6_port = 39918, + }; + const ngtcp2_sockaddr_in inaddr = { + .sin_family = NGTCP2_AF_INET, + .sin_port = 39918, + }; + const ngtcp2_cid retry_scid = { + .datalen = NGTCP2_MAX_CIDLEN, + .data = {0xBA, 0xAD, 0xF0, 0x0D}, + }; + const ngtcp2_cid odcid = { + .datalen = NGTCP2_MAX_CIDLEN, + .data = {0xBA, 0xAD, 0xCA, 0xCE}, + }; + const ngtcp2_cid dcid = { + .datalen = NGTCP2_MAX_CIDLEN, + .data = {0xDE, 0xAD, 0xF1, 0x5b}, + }; + ngtcp2_cid decoded_odcid; + ngtcp2_tstamp t = 3600 * NGTCP2_SECONDS; + uint8_t token[NGTCP2_CRYPTO_MAX_RETRY_TOKENLEN2]; + ngtcp2_ssize tokenlen; + int rv; + + tokenlen = ngtcp2_crypto_generate_retry_token2( + token, secret, ngtcp2_strlen_lit(secret), NGTCP2_PROTO_VER_V1, + (const ngtcp2_sockaddr *)&in6addr, sizeof(in6addr), &retry_scid, &odcid, t); + + assert_ptrdiff(NGTCP2_CRYPTO_MAX_RETRY_TOKENLEN2, ==, tokenlen); + + /* Successful validation */ + rv = ngtcp2_crypto_verify_retry_token2( + &decoded_odcid, token, (size_t)tokenlen, secret, ngtcp2_strlen_lit(secret), + NGTCP2_PROTO_VER_V1, (const ngtcp2_sockaddr *)&in6addr, sizeof(in6addr), + &retry_scid, 10 * NGTCP2_SECONDS, t); + + assert_int(0, ==, rv); + assert_true(ngtcp2_cid_eq(&odcid, &decoded_odcid)); + + /* Timeout */ + rv = ngtcp2_crypto_verify_retry_token2( + &decoded_odcid, token, (size_t)tokenlen, secret, ngtcp2_strlen_lit(secret), + NGTCP2_PROTO_VER_V1, (const ngtcp2_sockaddr *)&in6addr, sizeof(in6addr), + &retry_scid, 10 * NGTCP2_SECONDS, t + 10 * NGTCP2_SECONDS); + + assert_int(NGTCP2_CRYPTO_ERR_VERIFY_TOKEN, ==, rv); + + /* Bad DCID */ + rv = ngtcp2_crypto_verify_retry_token2( + &decoded_odcid, token, (size_t)tokenlen, secret, ngtcp2_strlen_lit(secret), + NGTCP2_PROTO_VER_V1, (const ngtcp2_sockaddr *)&in6addr, sizeof(in6addr), + &dcid, 10 * NGTCP2_SECONDS, t); + + assert_int(NGTCP2_CRYPTO_ERR_UNREADABLE_TOKEN, ==, rv); + + /* Bad address */ + rv = ngtcp2_crypto_verify_retry_token2( + &decoded_odcid, token, (size_t)tokenlen, secret, ngtcp2_strlen_lit(secret), + NGTCP2_PROTO_VER_V1, (const ngtcp2_sockaddr *)&inaddr, sizeof(inaddr), + &retry_scid, 10 * NGTCP2_SECONDS, t); + + assert_int(NGTCP2_CRYPTO_ERR_VERIFY_TOKEN, ==, rv); + + /* Truncated token */ + rv = ngtcp2_crypto_verify_retry_token2( + &decoded_odcid, token, (size_t)tokenlen - 1, secret, + ngtcp2_strlen_lit(secret), NGTCP2_PROTO_VER_V1, + (const ngtcp2_sockaddr *)&in6addr, sizeof(in6addr), &retry_scid, + 10 * NGTCP2_SECONDS, t); + + assert_int(NGTCP2_CRYPTO_ERR_UNREADABLE_TOKEN, ==, rv); + + /* Bad magic */ + token[0] = 0; + + rv = ngtcp2_crypto_verify_retry_token2( + &decoded_odcid, token, (size_t)tokenlen, secret, ngtcp2_strlen_lit(secret), + NGTCP2_PROTO_VER_V1, (const ngtcp2_sockaddr *)&in6addr, sizeof(in6addr), + &retry_scid, 10 * NGTCP2_SECONDS, t); + + assert_int(NGTCP2_CRYPTO_ERR_UNREADABLE_TOKEN, ==, rv); +} + +void test_ngtcp2_crypto_verify_regular_token(void) { + const uint8_t secret[] = "regular-token-secret"; + const ngtcp2_sockaddr_in6 in6addr = { + .sin6_family = NGTCP2_AF_INET6, + .sin6_port = 39918, + }; + const ngtcp2_sockaddr_in inaddr = { + .sin_family = NGTCP2_AF_INET, + .sin_port = 39918, + }; + const uint8_t token_data[] = "I am the token data"; + const ngtcp2_tstamp timeout = 10 * NGTCP2_SECONDS; + ngtcp2_tstamp t = 3600 * NGTCP2_SECONDS; + uint8_t token[NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN + 256]; + ngtcp2_ssize tokenlen; + ngtcp2_ssize token_datalen; + uint8_t decoded_token_data[256]; + + tokenlen = ngtcp2_crypto_generate_regular_token2( + token, secret, ngtcp2_strlen_lit(secret), (const ngtcp2_sockaddr *)&in6addr, + sizeof(in6addr), token_data, ngtcp2_strlen_lit(token_data), t); + + assert_ptrdiff(NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN + + ngtcp2_strlen_lit(token_data), + ==, tokenlen); + + /* Successful validation */ + token_datalen = ngtcp2_crypto_verify_regular_token2( + decoded_token_data, sizeof(decoded_token_data), token, (size_t)tokenlen, + secret, ngtcp2_strlen_lit(secret), (const ngtcp2_sockaddr *)&in6addr, + sizeof(in6addr), timeout, t); + + assert_ptrdiff(ngtcp2_strlen_lit(token_data), ==, token_datalen); + assert_memory_equal(ngtcp2_strlen_lit(token_data), token_data, + decoded_token_data); + + /* Timeout */ + token_datalen = ngtcp2_crypto_verify_regular_token2( + decoded_token_data, sizeof(decoded_token_data), token, (size_t)tokenlen, + secret, ngtcp2_strlen_lit(secret), (const ngtcp2_sockaddr *)&in6addr, + sizeof(in6addr), timeout, t + timeout); + + assert_ptrdiff(NGTCP2_CRYPTO_ERR_VERIFY_TOKEN, ==, token_datalen); + + /* Bad address */ + token_datalen = ngtcp2_crypto_verify_regular_token2( + decoded_token_data, sizeof(decoded_token_data), token, (size_t)tokenlen, + secret, ngtcp2_strlen_lit(secret), (const ngtcp2_sockaddr *)&inaddr, + sizeof(inaddr), timeout, t); + + assert_ptrdiff(NGTCP2_CRYPTO_ERR_UNREADABLE_TOKEN, ==, token_datalen); + + /* Insufficient data buffer */ + memset(decoded_token_data, 0, sizeof(decoded_token_data)); + + token_datalen = ngtcp2_crypto_verify_regular_token2( + decoded_token_data, ngtcp2_strlen_lit(token_data) - 1, token, + (size_t)tokenlen, secret, ngtcp2_strlen_lit(secret), + (const ngtcp2_sockaddr *)&in6addr, sizeof(in6addr), timeout, t); + + assert_ptrdiff(0, ==, token_datalen); + + /* NULL buffer */ + memset(decoded_token_data, 0, sizeof(decoded_token_data)); + + token_datalen = ngtcp2_crypto_verify_regular_token2( + NULL, 0, token, (size_t)tokenlen, secret, ngtcp2_strlen_lit(secret), + (const ngtcp2_sockaddr *)&in6addr, sizeof(in6addr), timeout, t); + + assert_ptrdiff(0, ==, token_datalen); + + /* Truncated token */ + token_datalen = ngtcp2_crypto_verify_regular_token2( + decoded_token_data, sizeof(decoded_token_data), token, (size_t)tokenlen - 1, + secret, ngtcp2_strlen_lit(secret), (const ngtcp2_sockaddr *)&in6addr, + sizeof(in6addr), timeout, t); + + assert_ptrdiff(NGTCP2_CRYPTO_ERR_UNREADABLE_TOKEN, ==, token_datalen); + + /* Bad magic */ + token[0] = 0; + + token_datalen = ngtcp2_crypto_verify_regular_token2( + decoded_token_data, sizeof(decoded_token_data), token, (size_t)tokenlen, + secret, ngtcp2_strlen_lit(secret), (const ngtcp2_sockaddr *)&in6addr, + sizeof(in6addr), timeout, t); + + assert_ptrdiff(NGTCP2_CRYPTO_ERR_UNREADABLE_TOKEN, ==, token_datalen); +} diff --git a/deps/ngtcp2/ngtcp2/crypto/shared_test.h b/deps/ngtcp2/ngtcp2/crypto/shared_test.h new file mode 100644 index 00000000000000..fe3d023cd9efdc --- /dev/null +++ b/deps/ngtcp2/ngtcp2/crypto/shared_test.h @@ -0,0 +1,41 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGTCP2_SHARED_TEST_H +#define NGTCP2_SHARED_TEST_H + +#ifdef HAVE_CONFIG_H +# include +#endif /* defined(HAVE_CONFIG_H) */ + +#define MUNIT_ENABLE_ASSERT_ALIASES + +#include "munit.h" + +extern const MunitSuite shared_suite; + +munit_void_test_decl(test_ngtcp2_crypto_verify_retry_token) +munit_void_test_decl(test_ngtcp2_crypto_verify_regular_token) + +#endif /* !defined(NGTCP2_SHARED_TEST_H) */ diff --git a/deps/ngtcp2/ngtcp2/crypto/wolfssl/wolfssl.c b/deps/ngtcp2/ngtcp2/crypto/wolfssl/wolfssl.c index bc9d9d84a862c5..16161597360c26 100644 --- a/deps/ngtcp2/ngtcp2/crypto/wolfssl/wolfssl.c +++ b/deps/ngtcp2/ngtcp2/crypto/wolfssl/wolfssl.c @@ -34,6 +34,7 @@ #include #include +#include "ngtcp2_macro.h" #include "shared.h" #define PRINTF_DEBUG 0 @@ -55,7 +56,7 @@ ngtcp2_crypto_md *ngtcp2_crypto_md_sha256(ngtcp2_crypto_md *md) { ngtcp2_crypto_ctx *ngtcp2_crypto_ctx_initial(ngtcp2_crypto_ctx *ctx) { ngtcp2_crypto_aead_init(&ctx->aead, (void *)wolfSSL_EVP_aes_128_gcm()); ctx->md.native_handle = (void *)wolfSSL_EVP_sha256(); - ctx->hp.native_handle = (void *)wolfSSL_EVP_aes_128_ctr(); + ctx->hp.native_handle = (void *)wolfSSL_EVP_aes_128_ecb(); ctx->max_encryption = 0; ctx->max_decryption_failure = 0; return ctx; @@ -106,6 +107,21 @@ static int supported_aead(const WOLFSSL_EVP_CIPHER *aead) { wolfSSL_quic_aead_is_chacha20(aead) || wolfSSL_quic_aead_is_ccm(aead); } +static const WOLFSSL_EVP_CIPHER * +crypto_aead_get_hp(const WOLFSSL_EVP_CIPHER *aead) { + switch (wolfSSL_EVP_CIPHER_nid(aead)) { + case NID_aes_128_gcm: + case NID_aes_128_ccm: + return wolfSSL_EVP_aes_128_ecb(); + case NID_aes_256_gcm: + return wolfSSL_EVP_aes_256_ecb(); + case NID_chacha20_poly1305: + return wolfSSL_EVP_chacha20(); + default: + return NULL; + } +} + ngtcp2_crypto_ctx *ngtcp2_crypto_ctx_tls(ngtcp2_crypto_ctx *ctx, void *tls_native_handle) { WOLFSSL *ssl = tls_native_handle; @@ -121,7 +137,7 @@ ngtcp2_crypto_ctx *ngtcp2_crypto_ctx_tls(ngtcp2_crypto_ctx *ctx, ngtcp2_crypto_aead_init(&ctx->aead, (void *)aead); ctx->md.native_handle = (void *)wolfSSL_quic_get_md(ssl); - ctx->hp.native_handle = (void *)wolfSSL_quic_get_hp(ssl); + ctx->hp.native_handle = (void *)crypto_aead_get_hp(aead); ctx->max_encryption = crypto_aead_get_aead_max_encryption(aead); ctx->max_decryption_failure = crypto_aead_get_aead_max_decryption_failure(aead); @@ -288,20 +304,33 @@ int ngtcp2_crypto_decrypt(uint8_t *dest, const ngtcp2_crypto_aead *aead, int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, const ngtcp2_crypto_cipher_ctx *hp_ctx, const uint8_t *sample) { - static const uint8_t PLAINTEXT[] = "\x00\x00\x00\x00\x00"; + static const uint8_t PLAINTEXT[16] = {0}; WOLFSSL_EVP_CIPHER_CTX *actx = hp_ctx->native_handle; int len; (void)hp; - if (wolfSSL_EVP_EncryptInit_ex(actx, NULL, NULL, NULL, sample) != - WOLFSSL_SUCCESS || - wolfSSL_EVP_CipherUpdate(actx, dest, &len, PLAINTEXT, - sizeof(PLAINTEXT) - 1) != WOLFSSL_SUCCESS || - wolfSSL_EVP_EncryptFinal_ex(actx, dest + sizeof(PLAINTEXT) - 1, &len) != - WOLFSSL_SUCCESS) { - DEBUG_MSG("WOLFSSL: hp_mask FAILED\n"); - return -1; + switch (wolfSSL_EVP_CIPHER_CTX_nid(actx)) { + case NID_aes_128_ecb: + case NID_aes_256_ecb: + if (!wolfSSL_EVP_CipherUpdate(actx, dest, &len, sample, + NGTCP2_HP_SAMPLELEN)) { + return -1; + } + + break; + case NID_chacha20: + if (wolfSSL_EVP_EncryptInit_ex(actx, NULL, NULL, NULL, sample) != + WOLFSSL_SUCCESS || + wolfSSL_EVP_CipherUpdate(actx, dest, &len, PLAINTEXT, + sizeof(PLAINTEXT)) != WOLFSSL_SUCCESS || + wolfSSL_EVP_EncryptFinal_ex(actx, dest + sizeof(PLAINTEXT), &len) != + WOLFSSL_SUCCESS) { + DEBUG_MSG("WOLFSSL: hp_mask FAILED\n"); + return -1; + } + + break; } return 0; @@ -310,7 +339,7 @@ int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, int ngtcp2_crypto_read_write_crypto_data( ngtcp2_conn *conn, ngtcp2_encryption_level encryption_level, const uint8_t *data, size_t datalen) { - WOLFSSL *ssl = ngtcp2_conn_get_tls_native_handle(conn); + WOLFSSL *ssl = ngtcp2_conn_get_tls_native_handle2(conn); WOLFSSL_ENCRYPTION_LEVEL level = ngtcp2_crypto_wolfssl_from_ngtcp2_encryption_level(encryption_level); int rv; @@ -326,7 +355,7 @@ int ngtcp2_crypto_read_write_crypto_data( } } - if (!ngtcp2_conn_get_handshake_completed(conn)) { + if (!ngtcp2_conn_get_handshake_completed2(conn)) { rv = wolfSSL_quic_do_handshake(ssl); if (rv <= 0) { err = wolfSSL_get_error(ssl, rv); @@ -442,6 +471,19 @@ int ngtcp2_crypto_get_path_challenge_data_cb(ngtcp2_conn *conn, uint8_t *data, return 0; } +int ngtcp2_crypto_get_path_challenge_data2_cb(ngtcp2_conn *conn, + ngtcp2_path_challenge_data *data, + void *user_data) { + (void)conn; + (void)user_data; + + DEBUG_MSG("WOLFSSL: get path challenge data\n"); + if (wolfSSL_RAND_bytes(data->data, NGTCP2_PATH_CHALLENGE_DATALEN) != 1) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + int ngtcp2_crypto_random(uint8_t *data, size_t datalen) { DEBUG_MSG("WOLFSSL: get random\n"); if (wolfSSL_RAND_bytes(data, (int)datalen) != 1) { diff --git a/deps/ngtcp2/ngtcp2/examples/client.cc b/deps/ngtcp2/ngtcp2/examples/client.cc new file mode 100644 index 00000000000000..30e078c8e673e6 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/client.cc @@ -0,0 +1,2904 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "client.h" +#include "network.h" +#include "debug.h" +#include "util.h" +#include "shared.h" + +using namespace ngtcp2; +using namespace std::literals; + +namespace { +auto randgen = util::make_mt19937(); +} // namespace + +constexpr auto max_preferred_versionslen = 4UZ; + +Config config; + +Stream::Stream(const Request &req, int64_t stream_id) + : req{req}, stream_id{stream_id} {} + +Stream::~Stream() { + if (fd != -1) { + close(fd); + } +} + +std::expected Stream::open_file(std::string_view path) { + assert(fd == -1); + + std::string_view filename; + + auto it = std::ranges::find(std::rbegin(path), std::rend(path), '/').base(); + if (it == std::ranges::end(path)) { + filename = "index.html"sv; + } else { + filename = std::string_view{it, std::ranges::end(path)}; + if (filename == ".."sv || filename == "."sv) { + std::println(stderr, "Invalid file name: {}", filename); + return std::unexpected{Error::INVALID_ARGUMENT}; + } + } + + auto fpath = config.download; + fpath /= filename; + + fd = open(fpath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (fd == -1) { + std::println(stderr, "open: Could not open file {}: {}", fpath.native(), + strerror(errno)); + return std::unexpected{Error::IO}; + } + + return {}; +} + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto c = static_cast(w->data); + + c->on_write(); +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto ep = static_cast(w->data); + auto c = ep->client; + + if (!c->on_read(*ep)) { + return; + } + + c->on_write(); +} +} // namespace + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast(w->data); + + if (auto rv = c->handle_expiry(); !rv) { + return; + } + + c->on_write(); +} +} // namespace + +namespace { +void change_local_addrcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast(w->data); + + c->change_local_addr(); +} +} // namespace + +namespace { +void key_updatecb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast(w->data); + + if (!c->initiate_key_update()) { + c->disconnect(); + } +} +} // namespace + +namespace { +void delay_streamcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast(w->data); + + ev_timer_stop(loop, w); + c->on_extend_max_streams(); + c->on_write(); +} +} // namespace + +namespace { +void siginthandler(struct ev_loop *loop, ev_signal *w, int revents) { + ev_break(loop, EVBREAK_ALL); +} +} // namespace + +Client::Client(struct ev_loop *loop, uint32_t client_chosen_version, + uint32_t original_version) + : loop_{loop}, + client_chosen_version_{client_chosen_version}, + original_version_{original_version}, + no_gso_{ +#ifdef UDP_SEGMENT + config.no_gso +#else // !defined(UDP_SEGMENT) + true +#endif // !defined(UDP_SEGMENT) + } { + ev_io_init(&wev_, writecb, 0, EV_WRITE); + wev_.data = this; + ev_timer_init(&timer_, timeoutcb, 0., 0.); + timer_.data = this; + ev_timer_init(&change_local_addr_timer_, change_local_addrcb, + static_cast(config.change_local_addr) / NGTCP2_SECONDS, + 0.); + change_local_addr_timer_.data = this; + ev_timer_init(&key_update_timer_, key_updatecb, + static_cast(config.key_update) / NGTCP2_SECONDS, 0.); + key_update_timer_.data = this; + ev_timer_init(&delay_stream_timer_, delay_streamcb, + static_cast(config.delay_stream) / NGTCP2_SECONDS, 0.); + delay_stream_timer_.data = this; + ev_signal_init(&sigintev_, siginthandler, SIGINT); +} + +Client::~Client() { disconnect(); } + +void Client::disconnect() { + tx_.send_blocked = false; + + handle_error(); + + config.tx_loss_prob = 0; + + ev_timer_stop(loop_, &delay_stream_timer_); + ev_timer_stop(loop_, &key_update_timer_); + ev_timer_stop(loop_, &change_local_addr_timer_); + ev_timer_stop(loop_, &timer_); + + ev_io_stop(loop_, &wev_); + + for (auto &ep : endpoints_) { + ev_io_stop(loop_, &ep.rev); + close(ep.fd); + } + + endpoints_.clear(); + + ev_signal_stop(loop_, &sigintev_); +} + +namespace { +int recv_crypto_data(ngtcp2_conn *conn, + ngtcp2_encryption_level encryption_level, uint64_t offset, + const uint8_t *data, size_t datalen, void *user_data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_crypto_data(encryption_level, {data, datalen}); + } + + return ngtcp2_crypto_recv_crypto_data_cb(conn, encryption_level, offset, data, + datalen, user_data); +} +} // namespace + +namespace { +int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data, void *stream_user_data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_stream_data(stream_id, {data, datalen}); + } + + auto c = static_cast(user_data); + + if (!c->recv_stream_data(flags, stream_id, {data, datalen})) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int acked_stream_data_offset(ngtcp2_conn *conn, int64_t stream_id, + uint64_t offset, uint64_t datalen, void *user_data, + void *stream_user_data) { + auto c = static_cast(user_data); + if (!c->acked_stream_data_offset(stream_id, datalen)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +namespace { +int handshake_completed(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast(user_data); + + if (!config.quiet) { + debug::handshake_completed(conn, user_data); + } + + if (!c->handshake_completed()) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +std::expected Client::handshake_completed() { + if (early_data_ && !tls_session_.get_early_data_accepted()) { + if (!config.quiet) { + std::println(stderr, "Early data was rejected by server"); + } + + // Some TLS backends only report early data rejection after + // handshake completion (e.g., OpenSSL). For TLS backends which + // report it early (e.g., BoringSSL and PicoTLS), the following + // functions are noop. + if (auto rv = ngtcp2_conn_tls_early_data_rejected(conn_); rv != 0) { + std::println(stderr, "ngtcp2_conn_tls_early_data_rejected: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + if (auto rv = proto_codec_->setup_codec(); !rv) { + return rv; + } + } + + if (!config.quiet) { + std::println(stderr, "Negotiated cipher suite is {}", + tls_session_.get_cipher_name()); + if (auto group = tls_session_.get_negotiated_group(); !group.empty()) { + std::println(stderr, "Negotiated group is {}", group); + } + std::println(stderr, "Negotiated ALPN is {}", + tls_session_.get_selected_alpn()); + + if (!config.ech_config_list.empty() && tls_session_.get_ech_accepted()) { + std::println(stderr, "ECH was accepted"); + } + } + + if (!config.tp_file.empty()) { + std::array data; + auto datalen = ngtcp2_conn_encode_0rtt_transport_params2(conn_, data.data(), + data.size()); + if (datalen < 0) { + std::println(stderr, "Could not encode 0-RTT transport parameters: {}", + ngtcp2_strerror(static_cast(datalen))); + } else if (!util::write_transport_params( + config.tp_file, {data.data(), static_cast(datalen)})) { + std::println(stderr, "Could not write transport parameters to {}", + config.tp_file.native()); + } + } + + return {}; +} + +namespace { +int handshake_confirmed(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast(user_data); + + if (!config.quiet) { + debug::handshake_confirmed(conn, user_data); + } + + c->handshake_confirmed(); + + return 0; +} +} // namespace + +bool Client::should_exit() const { + return handshake_confirmed_ && + (!config.wait_for_ticket || ticket_received_) && + ((config.exit_on_first_stream_close && + (config.nstreams == 0 || nstreams_closed_)) || + (config.exit_on_all_streams_close && + config.nstreams == nstreams_done_ && + nstreams_closed_ == nstreams_done_)); +} + +void Client::handshake_confirmed() { + handshake_confirmed_ = true; + + if (config.change_local_addr) { + start_change_local_addr_timer(); + } + if (config.key_update) { + start_key_update_timer(); + } + if (config.delay_stream) { + start_delay_stream_timer(); + } +} + +namespace { +int recv_version_negotiation(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd, + const uint32_t *sv, size_t nsv, void *user_data) { + auto c = static_cast(user_data); + + c->recv_version_negotiation(sv, nsv); + + return 0; +} +} // namespace + +void Client::recv_version_negotiation(const uint32_t *sv, size_t nsv) { + offered_versions_.resize(nsv); + std::ranges::copy_n(sv, as_signed(nsv), + std::ranges::begin(offered_versions_)); +} + +namespace { +int stream_close(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast(user_data); + + if (!c->on_stream_close(stream_id, app_error_code)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int stream_reset(ngtcp2_conn *conn, int64_t stream_id, uint64_t final_size, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast(user_data); + + if (!c->on_stream_reset(stream_id)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int stream_stop_sending(ngtcp2_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast(user_data); + + if (!c->on_stream_stop_sending(stream_id)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int extend_max_local_streams_bidi(ngtcp2_conn *conn, uint64_t max_streams, + void *user_data) { + auto c = static_cast(user_data); + + c->on_extend_max_streams(); + + return 0; +} +} // namespace + +namespace { +void rand_bytes(uint8_t *dest, size_t destlen) { + if (!util::generate_secure_random({dest, destlen})) { + assert(0); + abort(); + } +} +} // namespace + +namespace { +void rand(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx *rand_ctx) { + rand_bytes(dest, destlen); +} +} // namespace + +namespace { +int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, + ngtcp2_stateless_reset_token *token, size_t cidlen, + void *user_data) { + if (!util::generate_secure_random({cid->data, cidlen})) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + if (ngtcp2_crypto_generate_stateless_reset_token( + token->data, config.static_secret.data(), config.static_secret.size(), + cid) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int do_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, + const ngtcp2_crypto_cipher_ctx *hp_ctx, const uint8_t *sample) { + if (ngtcp2_crypto_hp_mask(dest, hp, hp_ctx, sample) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (!config.quiet && config.show_secret) { + debug::print_hp_mask({dest, NGTCP2_HP_MASKLEN}, + {sample, NGTCP2_HP_SAMPLELEN}); + } + + return 0; +} +} // namespace + +namespace { +int update_key(ngtcp2_conn *conn, uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen, + void *user_data) { + auto c = static_cast(user_data); + + if (auto rv = + c->update_key(rx_secret, tx_secret, rx_aead_ctx, rx_iv, tx_aead_ctx, + tx_iv, current_rx_secret, current_tx_secret, secretlen); + !rv) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int path_validation(ngtcp2_conn *conn, uint32_t flags, const ngtcp2_path *path, + const ngtcp2_path *old_path, + ngtcp2_path_validation_result res, void *user_data) { + if (!config.quiet) { + debug::path_validation(path, res); + } + + if (flags & NGTCP2_PATH_VALIDATION_FLAG_PREFERRED_ADDR) { + auto c = static_cast(user_data); + + c->set_remote_addr(path->remote); + } + + return 0; +} +} // namespace + +void Client::set_remote_addr(const ngtcp2_addr &remote_addr) { + remote_addr_.set(remote_addr.addr); +} + +namespace { +int select_preferred_address(ngtcp2_conn *conn, ngtcp2_path *dest, + const ngtcp2_preferred_addr *paddr, + void *user_data) { + auto c = static_cast(user_data); + Address remote_addr; + + if (config.no_preferred_addr) { + return 0; + } + + if (auto rv = c->select_preferred_address(remote_addr, paddr); !rv) { + return 0; + } + + auto ep = c->endpoint_for(remote_addr); + if (!ep) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + ngtcp2_addr_copy_byte(&dest->local, (*ep)->addr.as_sockaddr(), + (*ep)->addr.size()); + ngtcp2_addr_copy_byte(&dest->remote, remote_addr.as_sockaddr(), + remote_addr.size()); + dest->user_data = *ep; + + return 0; +} +} // namespace + +namespace { +int extend_max_stream_data(ngtcp2_conn *conn, int64_t stream_id, + uint64_t max_data, void *user_data, + void *stream_user_data) { + auto c = static_cast(user_data); + if (!c->extend_max_stream_data(stream_id, max_data)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +std::expected Client::extend_max_stream_data(int64_t stream_id, + uint64_t max_data) { + return proto_codec_->extend_max_stream_data(stream_id, max_data); +} + +namespace { +int recv_new_token(ngtcp2_conn *conn, const uint8_t *token, size_t tokenlen, + void *user_data) { + if (config.token_file.empty()) { + return 0; + } + + util::write_token(config.token_file, {token, tokenlen}); + + return 0; +} +} // namespace + +namespace { +int recv_rx_key(ngtcp2_conn *conn, ngtcp2_encryption_level level, + void *user_data) { + if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) { + return 0; + } + + auto c = static_cast(user_data); + if (!c->setup_codec()) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int early_data_rejected(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast(user_data); + + c->early_data_rejected(); + + return 0; +} +} // namespace + +void Client::early_data_rejected() { + proto_codec_->early_data_rejected(); + + nstreams_done_ = 0; + streams_.clear(); +} + +std::expected Client::init(int fd, const Address &local_addr, + const Address &remote_addr, + const char *addr, const char *port, + TLSClientContext &tls_ctx) { + endpoints_.reserve(4); + + endpoints_.emplace_back(); + auto &ep = endpoints_.back(); + ep.addr = local_addr; + ep.client = this; + ep.fd = fd; + ev_io_init(&ep.rev, readcb, fd, EV_READ); + ep.rev.data = &ep; + + remote_addr_ = remote_addr; + addr_ = addr; + port_ = port; + + static constexpr auto callbacks = ngtcp2_callbacks{ + .client_initial = ngtcp2_crypto_client_initial_cb, + .recv_crypto_data = ::recv_crypto_data, + .handshake_completed = ::handshake_completed, + .recv_version_negotiation = ::recv_version_negotiation, + .encrypt = ngtcp2_crypto_encrypt_cb, + .decrypt = ngtcp2_crypto_decrypt_cb, + .hp_mask = do_hp_mask, + .recv_stream_data = ::recv_stream_data, + .acked_stream_data_offset = ::acked_stream_data_offset, + .stream_close = stream_close, + .recv_retry = ngtcp2_crypto_recv_retry_cb, + .extend_max_local_streams_bidi = extend_max_local_streams_bidi, + .rand = rand, + .update_key = ::update_key, + .path_validation = path_validation, + .select_preferred_addr = ::select_preferred_address, + .stream_reset = stream_reset, + .extend_max_stream_data = ::extend_max_stream_data, + .handshake_confirmed = ::handshake_confirmed, + .recv_new_token = ::recv_new_token, + .delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb, + .delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + .stream_stop_sending = stream_stop_sending, + .version_negotiation = ngtcp2_crypto_version_negotiation_cb, + .recv_rx_key = ::recv_rx_key, + .tls_early_data_rejected = ::early_data_rejected, + .get_new_connection_id2 = get_new_connection_id, + .get_path_challenge_data2 = ngtcp2_crypto_get_path_challenge_data2_cb, + }; + + ngtcp2_cid scid, dcid; + if (config.scid_present) { + scid = config.scid; + } else { + scid.datalen = 17; + if (auto rv = util::generate_secure_random({scid.data, scid.datalen}); + !rv) { + std::println(stderr, "Could not generate source connection ID"); + return rv; + } + } + if (config.dcid.datalen == 0) { + dcid.datalen = 18; + if (auto rv = util::generate_secure_random({dcid.data, dcid.datalen}); + !rv) { + std::println(stderr, "Could not generate destination connection ID"); + return rv; + } + } else { + dcid = config.dcid; + } + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.log_write = config.quiet ? nullptr : debug::log_write; + if (!config.qlog_file.empty() || !config.qlog_dir.empty()) { + std::filesystem::path path; + if (!config.qlog_file.empty()) { + path = config.qlog_file; + } else { + path = config.qlog_dir; + path /= util::format_hex(scid.data, as_signed(scid.datalen)); + path += ".sqlog"; + } + qlog_ = fopen(path.c_str(), "w"); + if (qlog_ == nullptr) { + std::println(stderr, "Could not open qlog file {}: {}", path.native(), + strerror(errno)); + return std::unexpected{Error::IO}; + } + settings.qlog_write = qlog_write_cb; + } + + settings.cc_algo = config.cc_algo; + settings.initial_ts = util::timestamp(); + settings.initial_rtt = config.initial_rtt; + settings.max_window = config.max_window; + settings.max_stream_window = config.max_stream_window; + if (config.max_udp_payload_size) { + settings.max_tx_udp_payload_size = config.max_udp_payload_size; + settings.no_tx_udp_payload_size_shaping = 1; + } + settings.handshake_timeout = config.handshake_timeout; + settings.no_pmtud = config.no_pmtud; + settings.ack_thresh = config.ack_thresh; + if (config.initial_pkt_num == UINT32_MAX) { + auto dis = std::uniform_int_distribution(0, INT32_MAX); + settings.initial_pkt_num = dis(randgen); + } else { + settings.initial_pkt_num = config.initial_pkt_num; + } + + std::vector token; + + if (!config.token_file.empty()) { + std::println(stderr, "Reading token file {}", config.token_file.native()); + + auto t = util::read_token(config.token_file); + if (t) { + token = std::move(*t); + settings.token = token.data(); + settings.tokenlen = token.size(); + } + } + + if (!config.available_versions.empty()) { + settings.available_versions = config.available_versions.data(); + settings.available_versionslen = config.available_versions.size(); + } + + if (!config.preferred_versions.empty()) { + settings.preferred_versions = config.preferred_versions.data(); + settings.preferred_versionslen = config.preferred_versions.size(); + } + + settings.original_version = original_version_; + + if (!config.pmtud_probes.empty()) { + settings.pmtud_probes = config.pmtud_probes.data(); + settings.pmtud_probeslen = config.pmtud_probes.size(); + + if (!config.max_udp_payload_size) { + settings.max_tx_udp_payload_size = + *std::ranges::max_element(config.pmtud_probes); + } + } + + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + params.initial_max_stream_data_bidi_local = config.max_stream_data_bidi_local; + params.initial_max_stream_data_bidi_remote = + config.max_stream_data_bidi_remote; + params.initial_max_stream_data_uni = config.max_stream_data_uni; + params.initial_max_data = config.max_data; + params.initial_max_streams_bidi = config.max_streams_bidi; + params.initial_max_streams_uni = config.max_streams_uni; + params.max_idle_timeout = config.timeout; + params.active_connection_id_limit = 7; + params.grease_quic_bit = 1; + + auto path = ngtcp2_path{ + .local = as_ngtcp2_addr(ep.addr), + .remote = as_ngtcp2_addr(remote_addr), + .user_data = &ep, + }; + auto rv = + ngtcp2_conn_client_new(&conn_, &dcid, &scid, &path, client_chosen_version_, + &callbacks, &settings, ¶ms, nullptr, this); + + if (rv != 0) { + std::println(stderr, "ngtcp2_conn_client_new: {}", ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + proto_codec_ = std::make_unique(this, last_error_); + + if (auto rv = tls_session_.init(early_data_, tls_ctx, addr_, this, + client_chosen_version_, ProtoCodec::protocol); + !rv) { + return rv; + } + + ngtcp2_conn_set_tls_native_handle(conn_, tls_session_.get_native_handle()); + + if (early_data_ && !config.tp_file.empty()) { + auto params = util::read_transport_params(config.tp_file); + if (!params) { + early_data_ = false; + } else { + auto rv = ngtcp2_conn_decode_and_set_0rtt_transport_params( + conn_, params->data(), params->size()); + if (rv != 0) { + std::println(stderr, + "ngtcp2_conn_decode_and_set_0rtt_transport_params: {}", + ngtcp2_strerror(rv)); + early_data_ = false; + } else if (auto rv = make_stream_early(); !rv) { + return rv; + } + } + } + + ev_io_start(loop_, &ep.rev); + + ev_signal_start(loop_, &sigintev_); + + return {}; +} + +std::expected +Client::feed_data(const Endpoint &ep, const sockaddr *sa, socklen_t salen, + const ngtcp2_pkt_info *pi, std::span data) { + auto path = ngtcp2_path{ + .local = as_ngtcp2_addr(ep.addr), + .remote{ + .addr = const_cast(sa), + .addrlen = salen, + }, + .user_data = const_cast(&ep), + }; + if (auto rv = ngtcp2_conn_read_pkt(conn_, &path, pi, data.data(), data.size(), + util::timestamp()); + rv != 0) { + std::println(stderr, "ngtcp2_conn_read_pkt: {}", ngtcp2_strerror(rv)); + if (!last_error_.error_code) { + if (rv == NGTCP2_ERR_CRYPTO) { + auto alert = ngtcp2_conn_get_tls_alert2(conn_); + ngtcp2_ccerr_set_tls_alert(&last_error_, alert, nullptr, 0); + + if (alert == TLS_ALERT_ECH_REQUIRED && + !config.ech_config_list_file.empty() && + !tls_session_.write_ech_config_list(config.ech_config_list_file)) { + std::println(stderr, "Could not write ECH retry configs in {}", + config.ech_config_list_file.native()); + } + } else { + ngtcp2_ccerr_set_liberr(&last_error_, rv, nullptr, 0); + } + } + disconnect(); + return std::unexpected{Error::QUIC}; + } + return {}; +} + +std::expected Client::on_read(const Endpoint &ep) { + std::array buf; + sockaddr_storage ss; + size_t pktcnt = 0; + ngtcp2_pkt_info pi; + + iovec msg_iov{ + .iov_base = buf.data(), + .iov_len = buf.size(), + }; + + uint8_t msg_ctrl[CMSG_SPACE(sizeof(int)) + CMSG_SPACE(sizeof(int))]; + + msghdr msg{ + .msg_name = &ss, + .msg_iov = &msg_iov, + .msg_iovlen = 1, + .msg_control = msg_ctrl, + }; + + auto start = util::timestamp(); + + for (; pktcnt < MAX_RECV_PKTS;) { + if (util::recv_pkt_time_threshold_exceeded( + config.cc_algo == NGTCP2_CC_ALGO_BBR, start, pktcnt)) { + break; + } + + msg.msg_namelen = sizeof(ss); + msg.msg_controllen = sizeof(msg_ctrl); + + auto nread = recvmsg(ep.fd, &msg, 0); + + if (nread == -1) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + std::println(stderr, "recvmsg: {}", strerror(errno)); + } + break; + } + + // Packets less than 21 bytes never be a valid QUIC packet. + if (nread < 21) { + ++pktcnt; + + continue; + } + + pi.ecn = msghdr_get_ecn(&msg, ss.ss_family); + auto gso_size = msghdr_get_udp_gro(&msg); + if (gso_size == 0) { + gso_size = static_cast(nread); + } + + auto data = std::span{buf.data(), static_cast(nread)}; + + for (;;) { + auto datalen = std::min(data.size(), gso_size); + + ++pktcnt; + + if (!config.quiet) { + std::println(stderr, + "Received packet: local={} remote={} ecn={:#x} {} bytes", + util::straddr(ep.addr), + util::straddr(reinterpret_cast(&ss), + msg.msg_namelen), + pi.ecn, datalen); + } + + // Packets less than 21 bytes never be a valid QUIC packet. + if (datalen < 21) { + break; + } + + if (debug::packet_lost(config.rx_loss_prob)) { + if (!config.quiet) { + std::println(stderr, "** Simulated incoming packet loss **"); + } + } else if (auto rv = + feed_data(ep, reinterpret_cast(&ss), + msg.msg_namelen, &pi, {data.data(), datalen}); + !rv) { + return rv; + } + + data = data.subspan(datalen); + + if (data.empty()) { + break; + } + } + } + + if (should_exit()) { + ngtcp2_ccerr_set_application_error(&last_error_, ProtoCodec::no_error, + nullptr, 0); + disconnect(); + return std::unexpected{Error::INTERNAL}; + } + + update_timer(); + + return {}; +} + +std::expected Client::handle_expiry() { + auto now = util::timestamp(); + if (auto rv = ngtcp2_conn_handle_expiry(conn_, now); rv != 0) { + std::println(stderr, "ngtcp2_conn_handle_expiry: {}", ngtcp2_strerror(rv)); + ngtcp2_ccerr_set_liberr(&last_error_, rv, nullptr, 0); + disconnect(); + return std::unexpected{Error::QUIC}; + } + + return {}; +} + +std::expected Client::on_write() { + if (tx_.send_blocked) { + send_blocked_packet(); + + if (tx_.send_blocked) { + return {}; + } + } + + ev_io_stop(loop_, &wev_); + + if (auto rv = write_streams(); !rv) { + return rv; + } + + if (should_exit()) { + ngtcp2_ccerr_set_application_error(&last_error_, ProtoCodec::no_error, + nullptr, 0); + disconnect(); + return std::unexpected{Error::INTERNAL}; + } + + update_timer(); + return {}; +} + +namespace { +ngtcp2_ssize write_pkt(ngtcp2_conn *conn, ngtcp2_path *path, + ngtcp2_pkt_info *pi, uint8_t *dest, size_t destlen, + ngtcp2_tstamp ts, void *user_data) { + auto c = static_cast(user_data); + + return c->write_pkt(path, pi, dest, destlen, ts); +} +} // namespace + +ngtcp2_ssize Client::write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, + uint8_t *dest, size_t destlen, + ngtcp2_tstamp ts) { + return proto_codec_->write_pkt(path, pi, dest, destlen, ts); +} + +std::expected Client::write_streams() { + ngtcp2_path_storage ps; + ngtcp2_pkt_info pi; + size_t gso_size; + auto ts = util::timestamp(); + auto txbuf = std::span{txbuf_}; + auto buflen = util::clamp_buffer_size(conn_, txbuf.size(), config.gso_burst); + + ngtcp2_path_storage_zero(&ps); + + auto nwrite = ngtcp2_conn_write_aggregate_pkt2( + conn_, &ps.path, &pi, txbuf.data(), buflen, &gso_size, ::write_pkt, + config.gso_burst, ts); + if (nwrite < 0) { + disconnect(); + return std::unexpected{Error::QUIC}; + } + + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + + if (nwrite == 0) { + return {}; + } + + send_packet_or_blocked(ps.path, pi.ecn, + txbuf.first(static_cast(nwrite)), gso_size); + + return {}; +} + +std::expected +Client::send_packet_or_blocked(const ngtcp2_path &path, unsigned int ecn, + std::span data, size_t gso_size) { + auto &ep = *static_cast(path.user_data); + + auto rest = send_packet(ep, path.remote, ecn, data, gso_size); + if (!rest.empty()) { + on_send_blocked(path, ecn, rest, gso_size); + + return std::unexpected{Error::SEND_BLOCKED}; + } + + return {}; +} + +void Client::update_timer() { + auto expiry = ngtcp2_conn_get_expiry2(conn_); + auto now = util::timestamp(); + + if (expiry <= now) { + if (!config.quiet) { + auto t = static_cast(now - expiry) / NGTCP2_SECONDS; + std::println(stderr, "Timer has already expired: {:.9f}s", t); + } + + ev_feed_event(loop_, &timer_, EV_TIMER); + + return; + } + + auto t = static_cast(expiry - now) / NGTCP2_SECONDS; + if (!config.quiet) { + std::println(stderr, "Set timer={:.9f}s", t); + } + timer_.repeat = t; + ev_timer_again(loop_, &timer_); +} + +#ifdef HAVE_LINUX_RTNETLINK_H +namespace { +std::expected bind_addr(Address &local_addr, int fd, + const InAddr &ia, int family) { + addrinfo hints{ + .ai_flags = AI_PASSIVE, + .ai_family = family, + .ai_socktype = SOCK_DGRAM, + }; + addrinfo *res, *rp; + char *node; + std::array nodebuf; + + if (in_addr_empty(ia)) { + node = nullptr; + } else { + if (inet_ntop(family, in_addr_get_ptr(ia), nodebuf.data(), + nodebuf.size()) == nullptr) { + std::println(stderr, "inet_ntop: {}", strerror(errno)); + return std::unexpected{Error::LIBC}; + } + + node = nodebuf.data(); + } + + if (auto rv = getaddrinfo(node, "0", &hints, &res); rv != 0) { + std::println(stderr, "getaddrinfo: {}", gai_strerror(rv)); + return std::unexpected{Error::LIBC}; + } + + auto res_d = defer([res] { freeaddrinfo(res); }); + + for (rp = res; rp; rp = rp->ai_next) { + if (bind(fd, rp->ai_addr, rp->ai_addrlen) != -1) { + break; + } + } + + if (!rp) { + std::println(stderr, "Could not bind"); + return std::unexpected{Error::SYSCALL}; + } + + sockaddr_storage ss; + socklen_t len = sizeof(ss); + if (getsockname(fd, reinterpret_cast(&ss), &len) == -1) { + std::println(stderr, "getsockname: {}", strerror(errno)); + return std::unexpected{Error::SYSCALL}; + } + + local_addr.set(reinterpret_cast(&ss)); + + return {}; +} +} // namespace +#endif // defined(HAVE_LINUX_RTNETLINK_H) + +#ifndef HAVE_LINUX_RTNETLINK_H +namespace { +std::expected connect_sock(Address &local_addr, int fd, + const Address &remote_addr) { + if (connect(fd, remote_addr.as_sockaddr(), remote_addr.size()) != 0) { + std::println(stderr, "connect: {}", strerror(errno)); + return std::unexpected{Error::SYSCALL}; + } + + sockaddr_storage ss; + socklen_t len = sizeof(ss); + if (getsockname(fd, reinterpret_cast(&ss), &len) == -1) { + std::println(stderr, "getsockname: {}", strerror(errno)); + return std::unexpected{Error::SYSCALL}; + } + + local_addr.set(reinterpret_cast(&ss)); + + return {}; +} +} // namespace +#endif // !defined(HAVE_LINUX_RTNETLINK_H) + +namespace { +std::expected udp_sock(int family) { + auto maybe_fd = util::create_nonblock_socket(family, SOCK_DGRAM, IPPROTO_UDP); + if (!maybe_fd) { + return maybe_fd; + } + + auto fd = *maybe_fd; + + fd_set_recv_ecn(fd, family); + fd_set_ip_mtu_discover(fd, family); + fd_set_ip_dontfrag(fd, family); + fd_set_udp_gro(fd); + + return fd; +} +} // namespace + +namespace { +std::expected create_sock(Address &remote_addr, const char *addr, + const char *port) { + addrinfo hints{ + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_DGRAM, + }; + addrinfo *res, *rp; + + if (auto rv = getaddrinfo(addr, port, &hints, &res); rv != 0) { + std::println(stderr, "getaddrinfo: {}", gai_strerror(rv)); + return std::unexpected{Error::LIBC}; + } + + auto res_d = defer([res] { freeaddrinfo(res); }); + + int fd = -1; + + for (rp = res; rp; rp = rp->ai_next) { + auto maybe_fd = udp_sock(rp->ai_family); + if (!maybe_fd) { + continue; + } + + fd = *maybe_fd; + + break; + } + + if (!rp) { + std::println(stderr, "Could not create socket"); + return std::unexpected{Error::SYSCALL}; + } + + remote_addr.set(rp->ai_addr); + + return fd; +} +} // namespace + +std::expected +Client::endpoint_for(const Address &remote_addr) { +#ifdef HAVE_LINUX_RTNETLINK_H + auto maybe_ia = get_local_addr(remote_addr); + if (!maybe_ia) { + std::println( + stderr, "Could not get local address for a selected preferred address"); + return std::unexpected{maybe_ia.error()}; + } + + const auto &ia = *maybe_ia; + + auto current_path = ngtcp2_conn_get_path2(conn_); + auto current_ep = static_cast(current_path->user_data); + if (addreq(current_ep->addr, ia)) { + return current_ep; + } +#endif // defined(HAVE_LINUX_RTNETLINK_H) + + auto family = remote_addr.family(); + + auto maybe_fd = udp_sock(family); + if (!maybe_fd) { + return std::unexpected{maybe_fd.error()}; + } + + auto fd = *maybe_fd; + + Address local_addr; + +#ifdef HAVE_LINUX_RTNETLINK_H + if (auto rv = bind_addr(local_addr, fd, ia, family); !rv) { + close(fd); + return std::unexpected{rv.error()}; + } +#else // !defined(HAVE_LINUX_RTNETLINK_H) + if (auto rv = connect_sock(local_addr, fd, remote_addr); !rv) { + close(fd); + return std::unexpected{rv.error()}; + } +#endif // !defined(HAVE_LINUX_RTNETLINK_H) + + endpoints_.emplace_back(); + auto &ep = endpoints_.back(); + ep.addr = local_addr; + ep.client = this; + ep.fd = fd; + ev_io_init(&ep.rev, readcb, fd, EV_READ); + ep.rev.data = &ep; + + ev_io_start(loop_, &ep.rev); + + return &ep; +} + +void Client::start_change_local_addr_timer() { + ev_timer_start(loop_, &change_local_addr_timer_); +} + +std::expected Client::change_local_addr() { + Address local_addr; + + if (!config.quiet) { + std::println(stderr, "Changing local address"); + } + + auto family = remote_addr_.family(); + + auto maybe_nfd = udp_sock(family); + if (!maybe_nfd) { + return std::unexpected{maybe_nfd.error()}; + } + + auto nfd = *maybe_nfd; + +#ifdef HAVE_LINUX_RTNETLINK_H + auto maybe_ia = get_local_addr(remote_addr_); + if (!maybe_ia) { + std::println(stderr, "Could not get local address"); + close(nfd); + return std::unexpected{maybe_ia.error()}; + } + + if (auto rv = bind_addr(local_addr, nfd, *maybe_ia, family); !rv) { + close(nfd); + return rv; + } +#else // !defined(HAVE_LINUX_RTNETLINK_H) + if (auto rv = connect_sock(local_addr, nfd, remote_addr_); !rv) { + close(nfd); + return rv; + } +#endif // !defined(HAVE_LINUX_RTNETLINK_H) + + if (!config.quiet) { + std::println(stderr, "Local address is now {}", util::straddr(local_addr)); + } + + endpoints_.emplace_back(); + auto &ep = endpoints_.back(); + ep.addr = local_addr; + ep.client = this; + ep.fd = nfd; + ev_io_init(&ep.rev, readcb, nfd, EV_READ); + ep.rev.data = &ep; + + auto addr = as_ngtcp2_addr(local_addr); + + if (config.nat_rebinding) { + ngtcp2_conn_set_local_addr(conn_, &addr); + ngtcp2_conn_set_path_user_data(conn_, &ep); + } else { + auto path = ngtcp2_path{ + .local = addr, + .remote = as_ngtcp2_addr(remote_addr_), + .user_data = &ep, + }; + if (auto rv = ngtcp2_conn_initiate_immediate_migration(conn_, &path, + util::timestamp()); + rv != 0) { + std::println(stderr, "ngtcp2_conn_initiate_immediate_migration: {}", + ngtcp2_strerror(rv)); + } + } + + ev_io_start(loop_, &ep.rev); + + return {}; +} + +void Client::start_key_update_timer() { + ev_timer_start(loop_, &key_update_timer_); +} + +std::expected +Client::update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen) { + if (!config.quiet) { + std::println(stderr, "Updating traffic key"); + } + + auto crypto_ctx = ngtcp2_conn_get_crypto_ctx2(conn_); + auto aead = &crypto_ctx->aead; + auto keylen = ngtcp2_crypto_aead_keylen(aead); + auto ivlen = ngtcp2_crypto_packet_protection_ivlen(aead); + + ++nkey_update_; + + std::array rx_key, tx_key; + + if (ngtcp2_crypto_update_key(conn_, rx_secret, tx_secret, rx_aead_ctx, + rx_key.data(), rx_iv, tx_aead_ctx, tx_key.data(), + tx_iv, current_rx_secret, current_tx_secret, + secretlen) != 0) { + return std::unexpected{Error::QUIC}; + } + + if (!config.quiet && config.show_secret) { + std::println(stderr, "application_traffic rx secret {}", nkey_update_); + debug::print_secrets({rx_secret, secretlen}, {rx_key.data(), keylen}, + {rx_iv, ivlen}); + std::println(stderr, "application_traffic tx secret {}", nkey_update_); + debug::print_secrets({tx_secret, secretlen}, {tx_key.data(), keylen}, + {tx_iv, ivlen}); + } + + return {}; +} + +std::expected Client::initiate_key_update() { + if (!config.quiet) { + std::println(stderr, "Initiate key update"); + } + + if (auto rv = ngtcp2_conn_initiate_key_update(conn_, util::timestamp()); + rv != 0) { + std::println(stderr, "ngtcp2_conn_initiate_key_update: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + return {}; +} + +void Client::start_delay_stream_timer() { + ev_timer_start(loop_, &delay_stream_timer_); +} + +std::expected Client::send_packet(const Endpoint &ep, + const ngtcp2_addr &remote_addr, + unsigned int ecn, + std::span data) { + auto rest = send_packet(ep, remote_addr, ecn, data, data.size()); + if (!rest.empty()) { + return std::unexpected{Error::SEND_BLOCKED}; + } + + return {}; +} + +std::span Client::send_packet(const Endpoint &ep, + const ngtcp2_addr &remote_addr, + unsigned int ecn, + std::span data, + size_t gso_size) { + assert(gso_size); + + if (debug::packet_lost(config.tx_loss_prob)) { + if (!config.quiet) { + std::println(stderr, "** Simulated outgoing packet loss **"); + } + return {}; + } + + if (no_gso_ && data.size() > gso_size) { + for (; !data.empty();) { + auto len = std::min(gso_size, data.size()); + + auto rest = send_packet(ep, remote_addr, ecn, data.first(len), len); + if (!rest.empty()) { + assert(rest.size() == len); + + return data; + } + + data = data.subspan(len); + } + + return {}; + } + + iovec msg_iov{ + .iov_base = const_cast(data.data()), + .iov_len = data.size(), + }; + + uint8_t msg_ctrl[CMSG_SPACE(sizeof(int)) + CMSG_SPACE(sizeof(uint16_t))]{}; + + msghdr msg{ +#ifdef HAVE_LINUX_RTNETLINK_H + .msg_name = const_cast(remote_addr.addr), + .msg_namelen = remote_addr.addrlen, +#endif // defined(HAVE_LINUX_RTNETLINK_H) + .msg_iov = &msg_iov, + .msg_iovlen = 1, + .msg_control = msg_ctrl, + .msg_controllen = sizeof(msg_ctrl), + }; + + size_t controllen = 0; + + auto cm = CMSG_FIRSTHDR(&msg); + controllen += CMSG_SPACE(sizeof(int)); + cm->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(cm), &ecn, sizeof(ecn)); + + switch (remote_addr.addr->sa_family) { + case AF_INET: + cm->cmsg_level = IPPROTO_IP; + cm->cmsg_type = IP_TOS; + + break; + case AF_INET6: + cm->cmsg_level = IPPROTO_IPV6; + cm->cmsg_type = IPV6_TCLASS; + + break; + default: + assert(0); + } + +#ifdef UDP_SEGMENT + if (data.size() > gso_size) { + controllen += CMSG_SPACE(sizeof(uint16_t)); + cm = CMSG_NXTHDR(&msg, cm); + cm->cmsg_level = SOL_UDP; + cm->cmsg_type = UDP_SEGMENT; + cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); + auto n = static_cast(gso_size); + memcpy(CMSG_DATA(cm), &n, sizeof(n)); + } +#endif // defined(UDP_SEGMENT) + + msg.msg_controllen = +#ifndef __APPLE__ + controllen +#else // defined(__APPLE__) + static_cast(controllen) +#endif // defined(__APPLE__) + ; + + ssize_t nwrite = 0; + + do { + nwrite = sendmsg(ep.fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + switch (errno) { + case EAGAIN: +#if EAGAIN != EWOULDBLOCK + case EWOULDBLOCK: +#endif // EAGAIN != EWOULDBLOCK + return data; +#ifdef UDP_SEGMENT + case EIO: + if (data.size() > gso_size) { + // GSO failure; send each packet in a separate sendmsg call. + std::println(stderr, "sendmsg: disabling GSO due to {}", + strerror(errno)); + + no_gso_ = true; + + return send_packet(ep, remote_addr, ecn, data, gso_size); + } + break; +#endif // defined(UDP_SEGMENT) + } + + std::println(stderr, "sendmsg: {}", strerror(errno)); + + // TODO We have packet which is expected to fail to send (e.g., + // path validation to old path). + return {}; + } + + assert(static_cast(nwrite) == data.size()); + + if (!config.quiet) { + std::println(stderr, "Sent packet: local={} remote={} ecn={:#x} {} bytes", + util::straddr(ep.addr), + util::straddr(remote_addr.addr, remote_addr.addrlen), ecn, + nwrite); + } + + return {}; +} + +void Client::on_send_blocked(const ngtcp2_path &path, unsigned int ecn, + std::span data, size_t gso_size) { + assert(!tx_.send_blocked); + assert(gso_size); + + tx_.send_blocked = true; + + auto &p = tx_.blocked; + + p.remote_addr.set(path.remote.addr); + + auto &ep = *static_cast(path.user_data); + + p.endpoint = &ep; + p.ecn = ecn; + p.data = data; + p.gso_size = gso_size; + + start_wev_endpoint(ep); +} + +void Client::start_wev_endpoint(const Endpoint &ep) { + // We do not close ep.fd, so we can expect that each Endpoint has + // unique fd. + if (ep.fd != wev_.fd) { + if (ev_is_active(&wev_)) { + ev_io_stop(loop_, &wev_); + } + + ev_io_set(&wev_, ep.fd, EV_WRITE); + } + + ev_io_start(loop_, &wev_); +} + +void Client::send_blocked_packet() { + assert(tx_.send_blocked); + + auto &p = tx_.blocked; + + auto rest = send_packet(*p.endpoint, as_ngtcp2_addr(p.remote_addr), p.ecn, + p.data, p.gso_size); + if (!rest.empty()) { + p.data = rest; + + start_wev_endpoint(*p.endpoint); + + return; + } + + tx_.send_blocked = false; +} + +std::expected Client::handle_error() { + if (!conn_ || ngtcp2_conn_in_closing_period2(conn_) || + ngtcp2_conn_in_draining_period2(conn_)) { + return {}; + } + + std::array buf; + + ngtcp2_path_storage ps; + + ngtcp2_path_storage_zero(&ps); + + ngtcp2_pkt_info pi; + + auto nwrite = ngtcp2_conn_write_connection_close( + conn_, &ps.path, &pi, buf.data(), buf.size(), &last_error_, + util::timestamp()); + if (nwrite < 0) { + std::println(stderr, "ngtcp2_conn_write_connection_close: {}", + ngtcp2_strerror(static_cast(nwrite))); + return std::unexpected{Error::QUIC}; + } + + if (nwrite == 0) { + return {}; + } + + return send_packet(*static_cast(ps.path.user_data), + ps.path.remote, pi.ecn, + {buf.data(), static_cast(nwrite)}); +} + +std::expected Client::on_stream_close(int64_t stream_id, + uint64_t app_error_code) { + if (!config.quiet) { + std::println(stderr, "QUIC stream {:#x} closed", stream_id); + } + + if (auto rv = proto_codec_->on_stream_close(stream_id, app_error_code); !rv) { + return rv; + } + + if (!ngtcp2_conn_is_local_stream2(conn_, stream_id)) { + // TODO We might later add bidi stream extension here. + if (!ngtcp2_is_bidi_stream(stream_id)) { + ngtcp2_conn_extend_max_streams_uni(conn_, 1); + } + } + + auto it = streams_.find(stream_id); + if (it != std::ranges::end(streams_)) { + ++nstreams_closed_; + + streams_.erase(it); + } + + return {}; +} + +std::expected Client::on_stream_reset(int64_t stream_id) { + return proto_codec_->on_stream_reset(stream_id); +} + +std::expected Client::on_stream_stop_sending(int64_t stream_id) { + return proto_codec_->on_stream_stop_sending(stream_id); +} + +std::expected Client::make_stream_early() { + if (auto rv = setup_codec(); !rv) { + return rv; + } + + on_extend_max_streams(); + + return {}; +} + +void Client::on_extend_max_streams() { + int64_t stream_id; + + if ((config.delay_stream && !handshake_confirmed_) || + ev_is_active(&delay_stream_timer_)) { + return; + } + + for (; nstreams_done_ < config.nstreams; ++nstreams_done_) { + if (auto rv = ngtcp2_conn_open_bidi_stream(conn_, &stream_id, nullptr); + rv != 0) { + assert(NGTCP2_ERR_STREAM_ID_BLOCKED == rv); + break; + } + + auto stream = std::make_unique( + config.requests[nstreams_done_ % config.requests.size()], stream_id); + + if (!proto_codec_->submit_request(stream.get())) { + break; + } + + if (!config.download.empty()) { + stream->open_file(stream->req.path); + } + + if (auto [_, rv] = streams_.try_emplace(stream_id, std::move(stream)); + !rv) { + assert(0); + } + } +} + +std::expected +Client::recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data) { + return proto_codec_->recv_stream_data(flags, stream_id, data); +} + +std::expected Client::acked_stream_data_offset(int64_t stream_id, + uint64_t datalen) { + return proto_codec_->acked_stream_data_offset(stream_id, datalen); +} + +std::expected +Client::select_preferred_address(Address &selected_addr, + const ngtcp2_preferred_addr *paddr) { + auto path = ngtcp2_conn_get_path2(conn_); + + switch (path->local.addr->sa_family) { + case AF_INET: + if (!paddr->ipv4_present) { + return std::unexpected{Error::INTERNAL}; + } + selected_addr.skaddr.emplace(paddr->ipv4); + break; + case AF_INET6: + if (!paddr->ipv6_present) { + return std::unexpected{Error::INTERNAL}; + } + selected_addr.skaddr.emplace(paddr->ipv6); + break; + default: + return std::unexpected{Error::INTERNAL}; + } + + if (!config.quiet) { + char host[NI_MAXHOST], service[NI_MAXSERV]; + if (auto rv = getnameinfo(selected_addr.as_sockaddr(), selected_addr.size(), + host, sizeof(host), service, sizeof(service), + NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::println(stderr, "getnameinfo: {}", gai_strerror(rv)); + return std::unexpected{Error::LIBC}; + } + + std::println(stderr, "selected server preferred_address is [{}]:{}", host, + service); + } + + return {}; +} + +std::expected Client::setup_codec() { + return proto_codec_->setup_codec(); +} + +const std::vector &Client::get_offered_versions() const { + return offered_versions_; +} + +bool Client::get_early_data() const { return early_data_; } + +Stream *Client::find_stream(int64_t stream_id) const { + auto it = streams_.find(stream_id); + if (it == std::ranges::end(streams_)) { + return nullptr; + } + + return (*it).second.get(); +} + +namespace { +std::expected run(Client &c, const char *addr, const char *port, + TLSClientContext &tls_ctx) { + Address remote_addr, local_addr; + + auto maybe_fd = create_sock(remote_addr, addr, port); + if (!maybe_fd) { + return std::unexpected{maybe_fd.error()}; + } + + auto fd = *maybe_fd; + +#ifdef HAVE_LINUX_RTNETLINK_H + auto maybe_ia = get_local_addr(remote_addr); + if (!maybe_ia) { + std::println(stderr, "Could not get local address"); + close(fd); + return std::unexpected{maybe_ia.error()}; + } + + if (auto rv = bind_addr(local_addr, fd, *maybe_ia, remote_addr.family()); + !rv) { + close(fd); + return rv; + } +#else // !defined(HAVE_LINUX_RTNETLINK_H) + if (auto rv = connect_sock(local_addr, fd, remote_addr); !rv) { + close(fd); + return rv; + } +#endif // !defined(HAVE_LINUX_RTNETLINK_H) + + if (auto rv = c.init(fd, local_addr, remote_addr, addr, port, tls_ctx); !rv) { + return rv; + } + + // TODO Do we need this ? + if (auto rv = c.on_write(); !rv) { + return rv; + } + + ev_run(EV_DEFAULT, 0); + + return {}; +} +} // namespace + +namespace { +std::expected parse_uri(std::string_view uri) { + urlparse_url u; + + if (urlparse_parse_url(uri.data(), uri.size(), /* is_connect = */ 0, &u) != + 0) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + if (!(u.field_set & (1 << URLPARSE_SCHEMA)) || + !(u.field_set & (1 << URLPARSE_HOST))) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + Request req; + + req.scheme = util::get_string(uri, u, URLPARSE_SCHEMA); + + auto host = std::string(util::get_string(uri, u, URLPARSE_HOST)); + if (util::numeric_host(host.c_str(), AF_INET6)) { + req.authority = '['; + req.authority += host; + req.authority += ']'; + } else { + req.authority = std::move(host); + } + + if (u.field_set & (1 << URLPARSE_PORT)) { + req.authority += ':'; + req.authority += util::get_string(uri, u, URLPARSE_PORT); + } + + if (u.field_set & (1 << URLPARSE_PATH)) { + req.path = util::get_string(uri, u, URLPARSE_PATH); + } else { + req.path = "/"; + } + + if (u.field_set & (1 << URLPARSE_QUERY)) { + req.path += '?'; + req.path += util::get_string(uri, u, URLPARSE_QUERY); + } + + return req; +} +} // namespace + +namespace { +std::expected parse_requests(char **argv, size_t argvlen) { + for (size_t i = 0; i < argvlen; ++i) { + auto uri = std::string_view{argv[i]}; + auto maybe_req = parse_uri(uri); + if (!maybe_req) { + std::println(stderr, "Could not parse URI: {}", uri); + return std::unexpected{maybe_req.error()}; + } + config.requests.emplace_back(std::move(*maybe_req)); + } + return {}; +} +} // namespace + +std::ofstream keylog_file; + +namespace { +const char *prog = "client"; +} // namespace + +namespace { +void print_usage(FILE *out) { + std::println(out, "Usage: {} [OPTIONS] [...]", prog); +} +} // namespace + +namespace { +void print_help() { + print_usage(stdout); + + Config config; + + std::cout << R"( + Remote server host (DNS name or IP address). In case of + DNS name, it will be sent in TLS SNI extension. + Remote server port + Remote URI +Options: + -t, --tx-loss=

+ The probability of losing outgoing packets.

must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + -r, --rx-loss=

+ The probability of losing incoming packets.

must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + -d, --data= + Read data from , and send them as STREAM data. + -n, --nstreams= + The number of requests. s are used in the order of + appearance in the command-line. If the number of + list is less than , list is wrapped. It + defaults to 0 which means the number of specified. + -v, --version= + Specify QUIC version to use in hex string. If the given + version is not supported by libngtcp2, client will use + QUIC v1 long packet types. Instead of specifying hex + string, there are special aliases available: "v1" + indicates QUIC v1, and "v2" indicates QUIC v2. + Default: )" + << std::hex << "0x" << config.version << std::dec << R"( + --preferred-versions=[[,]...] + Specify QUIC versions in hex string in the order of + preference. Client chooses one of those versions if + client received Version Negotiation packet from server. + These versions must be supported by libngtcp2. Instead + of specifying hex string, there are special aliases + available: "v1" indicates QUIC v1, and "v2" indicates + QUIC v2. + --available-versions=[[,]...] + Specify QUIC versions in hex string that are sent in + available_versions field of version_information + transport parameter. This list can include a version + which is not supported by libngtcp2. Instead of + specifying hex string, there are special aliases + available: "v1" indicates QUIC v1, and "v2" indicates + QUIC v2. + -q, --quiet Suppress debug output. + -s, --show-secret + Print out secrets unless --quiet is used. + --timeout= + Specify idle timeout. + Default: )" + << util::format_duration(config.timeout) << R"( + --ciphers= + Specify the cipher suite list to enable. + Default: )" + << config.ciphers << R"( + --groups= + Specify the supported groups. + Default: )" + << config.groups << R"( + --session-file= + Read/write TLS session from/to . To resume a + session, the previous session must be supplied with this + option. + --tp-file= + Read/write QUIC transport parameters from/to . To + send 0-RTT data, the transport parameters received from + the previous session must be supplied with this option. + --dcid= + Specify initial DCID. is hex string. After + decoded as binary, it should be at least 8 bytes and at + most 20 bytes long. + --scid= + Specify source connection ID. is hex string. If + an empty string is given, zero length connection ID is + assumed. + --change-local-addr= + Client changes local address when elapse + after handshake completes. + --nat-rebinding + When used with --change-local-addr, simulate NAT + rebinding. In other words, client changes local + address, but it does not start path validation. + --key-update= + Client initiates key update when elapse after + handshake completes. + -m, --http-method= + Specify HTTP method. Default: )" + << config.http_method << R"( + --delay-stream= + Delay sending STREAM data in 1-RTT for after + handshake completes. + --no-preferred-addr + Do not try to use preferred address offered by server. + --key= + The path to client private key PEM file. + --cert= + The path to client certificate PEM file. + --download= + The path to the directory to save a downloaded content. + It is undefined if 2 concurrent requests write to the + same file. If a request path does not contain a path + component usable as a file name, it defaults to + "index.html". + --no-quic-dump + Disables printing QUIC STREAM and CRYPTO frame data out. + --no-http-dump + Disables printing HTTP response body out. + --qlog-file= + The path to write qlog. This option and --qlog-dir are + mutually exclusive. + --qlog-dir= + Path to the directory where qlog file is stored. The + file name of each qlog is the Source Connection ID of + client. This option and --qlog-file are mutually + exclusive. + --max-data= + The initial connection-level flow control window. + Default: )" + << util::format_uint_iec(config.max_data) << R"( + --max-stream-data-bidi-local= + The initial stream-level flow control window for a + bidirectional stream that the local endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_local) << R"( + --max-stream-data-bidi-remote= + The initial stream-level flow control window for a + bidirectional stream that the remote endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_remote) << R"( + --max-stream-data-uni= + The initial stream-level flow control window for a + unidirectional stream. + Default: )" + << util::format_uint_iec(config.max_stream_data_uni) << R"( + --max-streams-bidi= + The number of the concurrent bidirectional streams that + the remote endpoint initiates. + Default: )" + << config.max_streams_bidi << R"( + --max-streams-uni= + The number of the concurrent unidirectional streams that + the remote endpoint initiates. + Default: )" + << config.max_streams_uni << R"( + --exit-on-first-stream-close + Exit when a first client initiated HTTP stream is + closed. + --exit-on-all-streams-close + Exit when all client initiated HTTP streams are closed. + --wait-for-ticket + Wait for a ticket to be received before exiting on + --exit-on-first-stream-close or + --exit-on-all-streams-close. --session-file must be + specified. + --disable-early-data + Disable early data. + --cc=(cubic|reno|bbr) + The name of congestion controller algorithm. + Default: )" + << util::strccalgo(config.cc_algo) << R"( + --token-file= + Read/write token from/to . Token is obtained from + NEW_TOKEN frame from server. + --sni= + Send in TLS SNI, overriding the DNS name + specified in . + --initial-rtt= + Set an initial RTT. + Default: )" + << util::format_duration(config.initial_rtt) << R"( + --max-window= + Maximum connection-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_window) << R"( + --max-stream-window= + Maximum stream-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_stream_window) << R"( + --max-udp-payload-size= + Override maximum UDP payload size that client transmits. + With this option, client assumes that a path supports + byte of UDP datagram payload, without performing + Path MTU Discovery. + --handshake-timeout= + Set the QUIC handshake timeout. It defaults to no + timeout. + --no-pmtud Disables Path MTU Discovery. + --ack-thresh= + The minimum number of the received ACK eliciting packets + that triggers immediate acknowledgement. + Default: )" + << config.ack_thresh << R"( + --initial-pkt-num= + The initial packet number that is used for each packet + number space. It must be in range [0, (1 << 31) - 1], + inclusive. By default, the initial packet number is + chosen randomly. + --pmtud-probes=[[,]...] + Specify UDP datagram payload sizes to probe in Path MTU + Discovery. must be strictly larger than 1200. + --ech-config-list-file= + Read/write ECHConfigList from/to . ECH is only + attempted if an underlying TLS stack supports it. If + the handshake fails with ech_required alert, ECH retry + configs, if provided by server, will be written to + . + --no-gso Disables GSO. + --show-stat Print the connection statistics when the connection is + closed. + --gso-burst= + The maximum number of packets to aggregate for GSO. If + GSO is disabled, this is the maximum number of packets + to send per an event loop in a single connection. It + defaults to 0, which means it is not limited by the + configuration. + -h, --help Display this help and exit. + +--- + + The argument is an integer and an optional unit (e.g., 10K is + 10 * 1024). Units are K, M and G (powers of 1024). + + The argument is an integer and an optional unit (e.g., 1s + is 1 second and 500ms is 500 milliseconds). Units are h, m, s, ms, + us, or ns (hours, minutes, seconds, milliseconds, microseconds, and + nanoseconds respectively). If a unit is omitted, a second is used + as unit. + + The argument is an hex string which must start with "0x" + (e.g., 0x00000001).)" + << std::endl; +} +} // namespace + +int main(int argc, char **argv) { + char *data_path = nullptr; + const char *private_key_file = nullptr; + const char *cert_file = nullptr; + + if (argc) { + prog = basename(argv[0]); + } + + for (;;) { + static int flag = 0; + static constexpr option long_opts[] = { + {"help", no_argument, nullptr, 'h'}, + {"tx-loss", required_argument, nullptr, 't'}, + {"rx-loss", required_argument, nullptr, 'r'}, + {"data", required_argument, nullptr, 'd'}, + {"http-method", required_argument, nullptr, 'm'}, + {"nstreams", required_argument, nullptr, 'n'}, + {"version", required_argument, nullptr, 'v'}, + {"quiet", no_argument, nullptr, 'q'}, + {"show-secret", no_argument, nullptr, 's'}, + {"ciphers", required_argument, &flag, 1}, + {"groups", required_argument, &flag, 2}, + {"timeout", required_argument, &flag, 3}, + {"session-file", required_argument, &flag, 4}, + {"tp-file", required_argument, &flag, 5}, + {"dcid", required_argument, &flag, 6}, + {"change-local-addr", required_argument, &flag, 7}, + {"key-update", required_argument, &flag, 8}, + {"nat-rebinding", no_argument, &flag, 9}, + {"delay-stream", required_argument, &flag, 10}, + {"no-preferred-addr", no_argument, &flag, 11}, + {"key", required_argument, &flag, 12}, + {"cert", required_argument, &flag, 13}, + {"download", required_argument, &flag, 14}, + {"no-quic-dump", no_argument, &flag, 15}, + {"no-http-dump", no_argument, &flag, 16}, + {"qlog-file", required_argument, &flag, 17}, + {"max-data", required_argument, &flag, 18}, + {"max-stream-data-bidi-local", required_argument, &flag, 19}, + {"max-stream-data-bidi-remote", required_argument, &flag, 20}, + {"max-stream-data-uni", required_argument, &flag, 21}, + {"max-streams-bidi", required_argument, &flag, 22}, + {"max-streams-uni", required_argument, &flag, 23}, + {"exit-on-first-stream-close", no_argument, &flag, 24}, + {"disable-early-data", no_argument, &flag, 25}, + {"qlog-dir", required_argument, &flag, 26}, + {"cc", required_argument, &flag, 27}, + {"exit-on-all-streams-close", no_argument, &flag, 28}, + {"token-file", required_argument, &flag, 29}, + {"sni", required_argument, &flag, 30}, + {"initial-rtt", required_argument, &flag, 31}, + {"max-window", required_argument, &flag, 32}, + {"max-stream-window", required_argument, &flag, 33}, + {"scid", required_argument, &flag, 34}, + {"max-udp-payload-size", required_argument, &flag, 35}, + {"handshake-timeout", required_argument, &flag, 36}, + {"available-versions", required_argument, &flag, 37}, + {"no-pmtud", no_argument, &flag, 38}, + {"preferred-versions", required_argument, &flag, 39}, + {"ack-thresh", required_argument, &flag, 40}, + {"wait-for-ticket", no_argument, &flag, 41}, + {"initial-pkt-num", required_argument, &flag, 42}, + {"pmtud-probes", required_argument, &flag, 43}, + {"ech-config-list-file", required_argument, &flag, 44}, + {"no-gso", no_argument, &flag, 45}, + {"show-stat", no_argument, &flag, 46}, + {"gso-burst", required_argument, &flag, 47}, + {}, + }; + + auto optidx = 0; + auto c = getopt_long(argc, argv, "d:him:n:qr:st:v:", long_opts, &optidx); + if (c == -1) { + break; + } + switch (c) { + case 'd': + // --data + data_path = optarg; + break; + case 'h': + // --help + print_help(); + exit(EXIT_SUCCESS); + case 'm': + // --http-method + config.http_method = optarg; + break; + case 'n': + // --streams + if (auto n = util::parse_uint(optarg); !n) { + std::println(stderr, "streams: invalid argument"); + exit(EXIT_FAILURE); + } else if (*n > NGTCP2_MAX_VARINT) { + std::println(stderr, "streams: must not exceed {}", NGTCP2_MAX_VARINT); + exit(EXIT_FAILURE); + } else { + config.nstreams = *n; + } + break; + case 'q': + // --quiet + config.quiet = true; + break; + case 'r': + // --rx-loss + config.rx_loss_prob = strtod(optarg, nullptr); + break; + case 's': + // --show-secret + config.show_secret = true; + break; + case 't': + // --tx-loss + config.tx_loss_prob = strtod(optarg, nullptr); + break; + case 'v': { + // --version + if (optarg == "v1"sv) { + config.version = NGTCP2_PROTO_VER_V1; + break; + } + if (optarg == "v2"sv) { + config.version = NGTCP2_PROTO_VER_V2; + break; + } + auto rv = util::parse_version(optarg); + if (!rv) { + std::println(stderr, "version: invalid version {}", optarg); + exit(EXIT_FAILURE); + } + config.version = *rv; + break; + } + case '?': + print_usage(stderr); + exit(EXIT_FAILURE); + case 0: + switch (flag) { + case 1: + // --ciphers + if (util::crypto_default_ciphers()[0] == '\0') { + std::println(stderr, "ciphers: not supported"); + exit(EXIT_FAILURE); + } + config.ciphers = optarg; + break; + case 2: + // --groups + config.groups = optarg; + break; + case 3: + // --timeout + if (auto t = util::parse_duration(optarg); !t) { + std::println(stderr, "timeout: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.timeout = *t; + } + break; + case 4: + // --session-file + config.session_file = optarg; + break; + case 5: + // --tp-file + config.tp_file = optarg; + break; + case 6: { + // --dcid + auto hexcid = std::string_view{optarg}; + if (hexcid.size() < NGTCP2_MIN_INITIAL_DCIDLEN * 2 || + hexcid.size() > NGTCP2_MAX_CIDLEN * 2) { + std::println(stderr, "dcid: wrong length"); + exit(EXIT_FAILURE); + } + + if (!util::is_hex_string(hexcid)) { + std::println(stderr, "dcid: not hex string"); + exit(EXIT_FAILURE); + } + + auto dcid = util::decode_hex(hexcid); + ngtcp2_cid_init(&config.dcid, + reinterpret_cast(dcid.c_str()), + dcid.size()); + break; + } + case 7: + // --change-local-addr + if (auto t = util::parse_duration(optarg); !t) { + std::println(stderr, "change-local-addr: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.change_local_addr = *t; + } + break; + case 8: + // --key-update + if (auto t = util::parse_duration(optarg); !t) { + std::println(stderr, "key-update: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.key_update = *t; + } + break; + case 9: + // --nat-rebinding + config.nat_rebinding = true; + break; + case 10: + // --delay-stream + if (auto t = util::parse_duration(optarg); !t) { + std::println(stderr, "delay-stream: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.delay_stream = *t; + } + break; + case 11: + // --no-preferred-addr + config.no_preferred_addr = true; + break; + case 12: + // --key + private_key_file = optarg; + break; + case 13: + // --cert + cert_file = optarg; + break; + case 14: + // --download + config.download = optarg; + break; + case 15: + // --no-quic-dump + config.no_quic_dump = true; + break; + case 16: + // --no-http-dump + config.no_http_dump = true; + break; + case 17: + // --qlog-file + config.qlog_file = optarg; + break; + case 18: + // --max-data + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-data: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_data = *n; + } + break; + case 19: + // --max-stream-data-bidi-local + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-stream-data-bidi-local: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_local = *n; + } + break; + case 20: + // --max-stream-data-bidi-remote + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-stream-data-bidi-remote: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_remote = *n; + } + break; + case 21: + // --max-stream-data-uni + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-stream-data-uni: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_stream_data_uni = *n; + } + break; + case 22: + // --max-streams-bidi + if (auto n = util::parse_uint(optarg); !n) { + std::println(stderr, "max-streams-bidi: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_streams_bidi = *n; + } + break; + case 23: + // --max-streams-uni + if (auto n = util::parse_uint(optarg); !n) { + std::println(stderr, "max-streams-uni: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_streams_uni = *n; + } + break; + case 24: + // --exit-on-first-stream-close + config.exit_on_first_stream_close = true; + break; + case 25: + // --disable-early-data + config.disable_early_data = true; + break; + case 26: + // --qlog-dir + config.qlog_dir = optarg; + break; + case 27: { + // --cc + auto cc = std::string_view{optarg}; + if (cc == "cubic"sv) { + config.cc_algo = NGTCP2_CC_ALGO_CUBIC; + break; + } + if (cc == "reno"sv) { + config.cc_algo = NGTCP2_CC_ALGO_RENO; + break; + } + if (cc == "bbr"sv) { + config.cc_algo = NGTCP2_CC_ALGO_BBR; + break; + } + std::println(stderr, "cc: specify cubic, reno, or bbr"); + exit(EXIT_FAILURE); + } + case 28: + // --exit-on-all-streams-close + config.exit_on_all_streams_close = true; + break; + case 29: + // --token-file + config.token_file = optarg; + break; + case 30: + // --sni + config.sni = optarg; + break; + case 31: + // --initial-rtt + if (auto t = util::parse_duration(optarg); !t) { + std::println(stderr, "initial-rtt: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.initial_rtt = *t; + } + break; + case 32: + // --max-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-window: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_window = *n; + } + break; + case 33: + // --max-stream-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-stream-window: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_stream_window = *n; + } + break; + case 34: { + // --scid + auto hexcid = std::string_view{optarg}; + if (hexcid.size() > NGTCP2_MAX_CIDLEN * 2) { + std::println(stderr, "scid: wrong length"); + exit(EXIT_FAILURE); + } + + if (!util::is_hex_string(hexcid)) { + std::println(stderr, "scid: not hex string"); + exit(EXIT_FAILURE); + } + + auto scid = util::decode_hex(hexcid); + ngtcp2_cid_init(&config.scid, + reinterpret_cast(scid.c_str()), + scid.size()); + config.scid_present = true; + break; + } + case 35: + // --max-udp-payload-size + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-udp-payload-size: invalid argument"); + exit(EXIT_FAILURE); + } else if (*n > NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE) { + std::println(stderr, "max-udp-payload-size: must not exceed {}", + NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE); + exit(EXIT_FAILURE); + } else if (*n == 0) { + std::println(stderr, "max-udp-payload-size: must not be 0"); + } else { + config.max_udp_payload_size = *n; + } + break; + case 36: + // --handshake-timeout + if (auto t = util::parse_duration(optarg); !t) { + std::println(stderr, "handshake-timeout: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.handshake_timeout = *t; + } + break; + case 37: + // --available-versions + if (strlen(optarg) == 0) { + config.available_versions.resize(0); + + break; + } + + config.available_versions = + util::split_str(optarg) | std::ranges::views::transform([](auto &&k) { + if (k == "v1"sv) { + return NGTCP2_PROTO_VER_V1; + } + + if (k == "v2"sv) { + return NGTCP2_PROTO_VER_V2; + } + + auto rv = util::parse_version(k); + if (!rv) { + std::println(stderr, "available-versions: invalid version {}", k); + exit(EXIT_FAILURE); + } + + return *rv; + }) | + std::ranges::to(); + + break; + case 38: + // --no-pmtud + config.no_pmtud = true; + break; + case 39: + // --preferred-versions + if (strlen(optarg) == 0) { + config.preferred_versions.resize(0); + + break; + } + + config.preferred_versions = + util::split_str(optarg) | std::ranges::views::transform([](auto &&k) { + if (k == "v1"sv) { + return NGTCP2_PROTO_VER_V1; + } + + if (k == "v2"sv) { + return NGTCP2_PROTO_VER_V2; + } + + auto rv = util::parse_version(k); + if (!rv) { + std::println(stderr, "preferred-versions: invalid version {}", k); + exit(EXIT_FAILURE); + } + + if (!ngtcp2_is_supported_version(*rv)) { + std::println(stderr, "preferred-versions: unsupported version {}", + k); + exit(EXIT_FAILURE); + } + + return *rv; + }) | + std::ranges::to(); + + if (config.preferred_versions.size() > max_preferred_versionslen) { + std::println(stderr, "preferred-versions: too many versions > {}", + max_preferred_versionslen); + exit(EXIT_FAILURE); + } + + break; + case 40: + // --ack-thresh + if (auto n = util::parse_uint(optarg); !n) { + std::println(stderr, "ack-thresh: invalid argument"); + exit(EXIT_FAILURE); + } else if (*n > 100) { + std::println(stderr, "ack-thresh: must not exceed 100"); + exit(EXIT_FAILURE); + } else { + config.ack_thresh = *n; + } + break; + case 41: + // --wait-for-ticket + config.wait_for_ticket = true; + break; + case 42: + // --initial-pkt-num + if (auto n = util::parse_uint(optarg); !n) { + std::println(stderr, "initial-pkt-num: invalid argument"); + exit(EXIT_FAILURE); + } else if (*n > INT32_MAX) { + std::println(stderr, + "initial-pkt-num: must not exceed (1 << 31) - 1"); + exit(EXIT_FAILURE); + } else { + config.initial_pkt_num = static_cast(*n); + } + break; + case 43: + // --pmtud-probes + if (strlen(optarg) == 0) { + config.pmtud_probes.resize(0); + + break; + } + + config.pmtud_probes = + util::split_str(optarg) | std::ranges::views::transform([](auto &&s) { + auto n = util::parse_uint_iec(s); + if (!n) { + std::println(stderr, "pmtud-probes: invalid argument"); + exit(EXIT_FAILURE); + } + + if (*n <= NGTCP2_MAX_UDP_PAYLOAD_SIZE || + *n > NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE) { + std::println( + stderr, "pmtud-probes: must be in range [{}, {}], inclusive.", + NGTCP2_MAX_UDP_PAYLOAD_SIZE + 1, + NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE); + exit(EXIT_FAILURE); + } + + return static_cast(*n); + }) | + std::ranges::to(); + + break; + case 44: + // --ech-config-list-file + config.ech_config_list_file = optarg; + break; + case 45: + // --no-gso + config.no_gso = true; + break; + case 46: + // --show-stat + config.show_stat = true; + break; + case 47: { + // --gso-burst + auto n = util::parse_uint(optarg); + if (!n) { + std::println(stderr, "gso-burst: invalid argument"); + exit(EXIT_FAILURE); + } + + if (*n > 64) { + std::println(stderr, + "gso-burst: must be in range [0, 64], inclusive."); + exit(EXIT_FAILURE); + } + + config.gso_burst = static_cast(*n); + + break; + } + } + break; + default: + break; + } + } + + if (argc - optind < 2) { + std::println(stderr, "Too few arguments"); + print_usage(stderr); + exit(EXIT_FAILURE); + } + + if (!config.qlog_file.empty() && !config.qlog_dir.empty()) { + std::println(stderr, "qlog-file and qlog-dir are mutually exclusive"); + exit(EXIT_FAILURE); + } + + if (config.exit_on_first_stream_close && config.exit_on_all_streams_close) { + std::println(stderr, "exit-on-first-stream-close and " + "exit-on-all-streams-close are mutually exclusive"); + exit(EXIT_FAILURE); + } + + if (config.wait_for_ticket && config.session_file.empty()) { + std::println(stderr, "wait-for-ticket: session-file must be specified"); + exit(EXIT_FAILURE); + } + + if (data_path) { + auto fd = open(data_path, O_RDONLY); + if (fd == -1) { + std::println(stderr, "data: Could not open file {}: {}", data_path, + strerror(errno)); + exit(EXIT_FAILURE); + } + struct stat st; + if (fstat(fd, &st) != 0) { + std::println(stderr, "data: Could not stat file {}: {}", data_path, + strerror(errno)); + exit(EXIT_FAILURE); + } + config.fd = fd; + config.datalen = static_cast(st.st_size); + if (config.datalen) { + auto addr = mmap(nullptr, config.datalen, PROT_READ, MAP_SHARED, fd, 0); + if (addr == MAP_FAILED) { + std::println(stderr, "data: Could not mmap file {}: {}", data_path, + strerror(errno)); + exit(EXIT_FAILURE); + } + config.data = static_cast(addr); + } + } + + if (!config.ech_config_list_file.empty()) { + auto ech_config = util::read_file(config.ech_config_list_file); + if (!ech_config) { + std::println(stderr, + "ech-config-list-file: Could not read ECHConfigList"); + } else { + config.ech_config_list = std::move(*ech_config); + } + } + + auto addr = argv[optind++]; + auto port = argv[optind++]; + + if (!parse_requests(&argv[optind], static_cast(argc - optind))) { + exit(EXIT_FAILURE); + } + + if (!ngtcp2_is_reserved_version(config.version)) { + if (!config.preferred_versions.empty() && + std::ranges::find(config.preferred_versions, config.version) == + std::ranges::end(config.preferred_versions)) { + std::println(stderr, "preferred-version: must include version {:#x}", + config.version); + exit(EXIT_FAILURE); + } + + if (!config.available_versions.empty() && + std::ranges::find(config.available_versions, config.version) == + std::ranges::end(config.available_versions)) { + std::println(stderr, "available-versions: must include version {:#x}", + config.version); + exit(EXIT_FAILURE); + } + } + + if (config.nstreams == 0) { + config.nstreams = config.requests.size(); + } + + TLSClientContext tls_ctx; + if (!tls_ctx.init(private_key_file, cert_file)) { + exit(EXIT_FAILURE); + } + + auto ev_loop_d = defer([] { ev_loop_destroy(EV_DEFAULT); }); + + auto keylog_filename = getenv("SSLKEYLOGFILE"); + if (keylog_filename) { + keylog_file.open(keylog_filename, std::ios_base::app); + if (keylog_file) { + tls_ctx.enable_keylog(); + } + } + + if (!util::generate_secure_random(config.static_secret)) { + std::println(stderr, "Unable to generate static secret"); + exit(EXIT_FAILURE); + } + + auto client_chosen_version = config.version; + + for (;;) { + Client c(EV_DEFAULT, client_chosen_version, config.version); + + if (!run(c, addr, port, tls_ctx)) { + exit(EXIT_FAILURE); + } + + if (config.preferred_versions.empty()) { + break; + } + + auto &offered_versions = c.get_offered_versions(); + if (offered_versions.empty()) { + break; + } + + client_chosen_version = ngtcp2_select_version( + config.preferred_versions.data(), config.preferred_versions.size(), + offered_versions.data(), offered_versions.size()); + + if (client_chosen_version == 0) { + std::println(stderr, "Unable to select a version"); + exit(EXIT_FAILURE); + } + + if (!config.quiet) { + std::println(stderr, "Client selected version {:#x}", + client_chosen_version); + } + } + + return EXIT_SUCCESS; +} diff --git a/deps/ngtcp2/ngtcp2/examples/client.h b/deps/ngtcp2/ngtcp2/examples/client.h new file mode 100644 index 00000000000000..f88f3e46664e02 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/client.h @@ -0,0 +1,216 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef CLIENT_H +#define CLIENT_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "client_base.h" +#include "tls_client_context.h" +#include "tls_client_session.h" +#include "network.h" +#include "shared.h" +#include "template.h" + +#ifdef WITH_EXAMPLE_HTTP3_PROTO_CODEC +# include "http3_client_proto_codec.h" +#endif // WITH_EXAMPLE_HTTP3_PROTO_CODEC + +#ifdef WITH_EXAMPLE_HQ_PROTO_CODEC +# include "hq_client_proto_codec.h" +#endif // WITH_EXAMPLE_HQ_PROTO_CODEC + +using namespace ngtcp2; + +struct Stream { + Stream(const Request &req, int64_t stream_id); + ~Stream(); + + std::expected open_file(std::string_view path); + + Request req; + int64_t stream_id; + int fd{-1}; +#ifdef WITH_EXAMPLE_HQ_PROTO_CODEC + std::string rawreqbuf; + std::span reqbuf; +#endif // WITH_EXAMPLE_HQ_PROTO_CODEC +}; + +class Client; + +struct Endpoint { + Address addr; + ev_io rev; + Client *client{}; + int fd{}; +}; + +class Client : public ClientBase { +public: + Client(struct ev_loop *loop, uint32_t client_chosen_version, + uint32_t original_version); + ~Client(); + + std::expected init(int fd, const Address &local_addr, + const Address &remote_addr, const char *addr, + const char *port, TLSClientContext &tls_ctx); + void disconnect(); + + std::expected on_read(const Endpoint &ep); + std::expected on_write(); + std::expected write_streams(); + std::expected feed_data(const Endpoint &ep, const sockaddr *sa, + socklen_t salen, + const ngtcp2_pkt_info *pi, + std::span data); + std::expected handle_expiry(); + void update_timer(); + std::expected handshake_completed(); + void handshake_confirmed(); + void recv_version_negotiation(const uint32_t *sv, size_t nsv); + + std::expected send_packet(const Endpoint &ep, + const ngtcp2_addr &remote_addr, + unsigned int ecn, + std::span data); + std::span + send_packet(const Endpoint &ep, const ngtcp2_addr &remote_addr, + unsigned int ecn, std::span data, size_t gso_size); + std::expected + send_packet_or_blocked(const ngtcp2_path &path, unsigned int ecn, + std::span data, size_t gso_size); + std::expected on_stream_close(int64_t stream_id, + uint64_t app_error_code); + void on_extend_max_streams(); + std::expected handle_error(); + std::expected make_stream_early(); + std::expected change_local_addr(); + void start_change_local_addr_timer(); + std::expected + update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, const uint8_t *current_tx_secret, + size_t secretlen); + std::expected initiate_key_update(); + void start_key_update_timer(); + void start_delay_stream_timer(); + + std::expected + select_preferred_address(Address &selected_addr, + const ngtcp2_preferred_addr *paddr); + + std::expected endpoint_for(const Address &remote_addr); + + void set_remote_addr(const ngtcp2_addr &remote_addr); + + std::expected setup_codec(); + std::expected recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data); + std::expected acked_stream_data_offset(int64_t stream_id, + uint64_t datalen); + std::expected on_stream_reset(int64_t stream_id); + std::expected on_stream_stop_sending(int64_t stream_id); + std::expected extend_max_stream_data(int64_t stream_id, + uint64_t max_data); + + void on_send_blocked(const ngtcp2_path &path, unsigned int ecn, + std::span data, size_t gso_size); + void start_wev_endpoint(const Endpoint &ep); + void send_blocked_packet(); + ngtcp2_ssize write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, uint8_t *dest, + size_t destlen, ngtcp2_tstamp ts); + + const std::vector &get_offered_versions() const; + + bool get_early_data() const; + void early_data_rejected(); + + bool should_exit() const; + + Stream *find_stream(int64_t stream_id) const; + +private: + std::vector endpoints_; + Address remote_addr_; + ev_io wev_; + ev_timer timer_; + ev_timer change_local_addr_timer_; + ev_timer key_update_timer_; + ev_timer delay_stream_timer_; + ev_signal sigintev_; + struct ev_loop *loop_; + std::unordered_map> streams_; + std::vector offered_versions_; + std::unique_ptr proto_codec_; + // addr_ is the server host address. + const char *addr_{}; + // port_ is the server port. + const char *port_{}; + // nstreams_done_ is the number of streams opened. + size_t nstreams_done_{}; + // nstreams_closed_ is the number of streams get closed. + size_t nstreams_closed_{}; + // nkey_update_ is the number of key update occurred. + size_t nkey_update_{}; + uint32_t client_chosen_version_; + uint32_t original_version_; + // early_data_ is true if client attempts to do 0RTT data transfer. + bool early_data_{}; + // handshake_confirmed_ gets true after handshake has been + // confirmed. + bool handshake_confirmed_{}; + bool no_gso_; + + struct { + bool send_blocked; + // blocked field is effective only when send_blocked is true. + struct { + const Endpoint *endpoint; + Address remote_addr; + unsigned int ecn; + std::span data; + size_t gso_size; + } blocked; + } tx_{}; + std::array txbuf_; +}; + +#endif // !defined(CLIENT_H) diff --git a/deps/ngtcp2/ngtcp2/examples/client_base.cc b/deps/ngtcp2/ngtcp2/examples/client_base.cc new file mode 100644 index 00000000000000..0f3eb2c9553f63 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/client_base.cc @@ -0,0 +1,78 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "client_base.h" + +#include +#include +#include + +#include "debug.h" +#include "template.h" +#include "util.h" + +using namespace ngtcp2; +using namespace std::literals; + +extern Config config; + +static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { + auto c = static_cast(conn_ref->user_data); + return c->conn(); +} + +ClientBase::ClientBase() : conn_ref_{get_conn, this} { + ngtcp2_ccerr_default(&last_error_); +} + +ClientBase::~ClientBase() { + if (conn_) { + if (config.show_stat) { + debug::print_conn_info(conn_); + } + + ngtcp2_conn_del(conn_); + } + + if (qlog_) { + fclose(qlog_); + } +} + +ngtcp2_conn *ClientBase::conn() const { return conn_; } + +void qlog_write_cb(void *user_data, uint32_t flags, const void *data, + size_t datalen) { + auto c = static_cast(user_data); + c->write_qlog(data, datalen); +} + +void ClientBase::write_qlog(const void *data, size_t datalen) { + assert(qlog_); + fwrite(data, 1, datalen, qlog_); +} + +ngtcp2_crypto_conn_ref *ClientBase::conn_ref() { return &conn_ref_; } + +void ClientBase::ticket_received() { ticket_received_ = true; } diff --git a/deps/ngtcp2/ngtcp2/examples/client_base.h b/deps/ngtcp2/ngtcp2/examples/client_base.h new file mode 100644 index 00000000000000..88e296cd2b2469 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/client_base.h @@ -0,0 +1,236 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef CLIENT_BASE_H +#define CLIENT_BASE_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include +#include +#include +#include +#include + +#include + +#include "tls_client_session.h" +#include "network.h" +#include "shared.h" +#include "util.h" + +using namespace ngtcp2; + +struct Request { + std::string_view scheme; + std::string authority; + std::string path; +}; + +struct Config { + ngtcp2_cid dcid{}; + ngtcp2_cid scid{}; + bool scid_present{}; + // tx_loss_prob is probability of losing outgoing packet. + double tx_loss_prob{}; + // rx_loss_prob is probability of losing incoming packet. + double rx_loss_prob{}; + // fd is a file descriptor to read input for streams. + int fd{-1}; + // ciphers is the list of enabled ciphers. + const char *ciphers{util::crypto_default_ciphers()}; + // groups is the list of supported groups. + const char *groups{util::crypto_default_groups()}; + // nstreams is the number of streams to open. + size_t nstreams{}; + // data is the pointer to memory region which maps file denoted by + // fd. + uint8_t *data{}; + // datalen is the length of file denoted by fd. + size_t datalen{}; + // version is a QUIC version to use. + uint32_t version{NGTCP2_PROTO_VER_V1}; + // quiet suppresses the output normally shown except for the error + // messages. + bool quiet{}; + // timeout is an idle timeout for QUIC connection. + ngtcp2_duration timeout{30 * NGTCP2_SECONDS}; + // session_file is a path to a file to write, and read TLS session. + std::filesystem::path session_file; + // tp_file is a path to a file to write, and read QUIC transport + // parameters. + std::filesystem::path tp_file; + // show_secret is true if transport secrets should be printed out. + bool show_secret{}; + // change_local_addr is the duration after which client changes + // local address. + ngtcp2_duration change_local_addr{}; + // key_update is the duration after which client initiates key + // update. + ngtcp2_duration key_update{}; + // delay_stream is the duration after which client sends the first + // 1-RTT stream. + ngtcp2_duration delay_stream{}; + // nat_rebinding is true if simulated NAT rebinding is enabled. + bool nat_rebinding{}; + // no_preferred_addr is true if client do not follow preferred + // address offered by server. + bool no_preferred_addr{}; + std::string_view http_method{"GET"sv}; + // download is a path to a directory where a downloaded file is + // saved. If it is empty, no file is saved. + std::filesystem::path download; + // requests contains URIs to request. + std::vector requests; + // no_quic_dump is true if hexdump of QUIC STREAM and CRYPTO data + // should be disabled. + bool no_quic_dump{}; + // no_http_dump is true if hexdump of HTTP response body should be + // disabled. + bool no_http_dump{}; + // qlog_file is the path to write qlog. + std::filesystem::path qlog_file; + // qlog_dir is the path to directory where qlog is stored. qlog_dir + // and qlog_file are mutually exclusive. + std::filesystem::path qlog_dir; + // max_data is the initial connection-level flow control window. + uint64_t max_data{24_m}; + // max_stream_data_bidi_local is the initial stream-level flow + // control window for a bidirectional stream that the local endpoint + // initiates. + uint64_t max_stream_data_bidi_local{16_m}; + // max_stream_data_bidi_remote is the initial stream-level flow + // control window for a bidirectional stream that the remote + // endpoint initiates. + uint64_t max_stream_data_bidi_remote{}; + // max_stream_data_uni is the initial stream-level flow control + // window for a unidirectional stream. + uint64_t max_stream_data_uni{16_m}; + // max_streams_bidi is the number of the concurrent bidirectional + // streams. + uint64_t max_streams_bidi{}; + // max_streams_uni is the number of the concurrent unidirectional + // streams. + uint64_t max_streams_uni{100}; + // max_window is the maximum connection-level flow control window + // size if auto-tuning is enabled. + uint64_t max_window{}; + // max_stream_window is the maximum stream-level flow control window + // size if auto-tuning is enabled. + uint64_t max_stream_window{}; + // exit_on_first_stream_close is the flag that if it is true, client + // exits when a first HTTP stream gets closed. It is not + // necessarily the same time when the underlying QUIC stream closes + // due to the QPACK synchronization. + bool exit_on_first_stream_close{}; + // exit_on_all_streams_close is the flag that if it is true, client + // exits when all HTTP streams get closed. + bool exit_on_all_streams_close{}; + // disable_early_data disables early data. + bool disable_early_data{}; + // static_secret is used to derive keying materials for Stateless + // Retry token. + std::array static_secret; + // cc_algo is the congestion controller algorithm. + ngtcp2_cc_algo cc_algo{NGTCP2_CC_ALGO_CUBIC}; + // token_file is a path to file to read or write token from + // NEW_TOKEN frame. + std::filesystem::path token_file; + // sni is the value sent in TLS SNI, overriding DNS name of the + // remote host. + std::string_view sni; + // initial_rtt is an initial RTT. + ngtcp2_duration initial_rtt{NGTCP2_DEFAULT_INITIAL_RTT}; + // max_udp_payload_size is the maximum UDP payload size that client + // transmits. + size_t max_udp_payload_size{}; + // handshake_timeout is the period of time before giving up QUIC + // connection establishment. + ngtcp2_duration handshake_timeout{UINT64_MAX}; + // preferred_versions includes QUIC versions in the order of + // preference. Client uses this field to select a version from the + // version set offered in Version Negotiation packet. + std::vector preferred_versions; + // available_versions includes QUIC versions that are sent in + // available_versions field of version_information + // transport_parameter. + std::vector available_versions; + // no_pmtud disables Path MTU Discovery. + bool no_pmtud{}; + // ack_thresh is the minimum number of the received ACK eliciting + // packets that triggers immediate acknowledgement. + size_t ack_thresh{2}; + // wait_for_ticket, if true, waits for a ticket to be received + // before exiting on exit_on_first_stream_close or + // exit_on_all_streams_close. + bool wait_for_ticket{}; + // initial_pkt_num is the initial packet number for each packet + // number space. If it is set to UINT32_MAX, it is chosen randomly. + uint32_t initial_pkt_num{UINT32_MAX}; + // pmtud_probes is the array of UDP datagram payload size to probes. + std::vector pmtud_probes; + // ech_config_list contains ECHConfigList. + std::vector ech_config_list; + // ech_config_list_file is a path to a file to read and write + // ECHConfigList. + std::filesystem::path ech_config_list_file; + // no_gso disables GSO. + bool no_gso{}; + // show_stat, if true, displays the connection statistics when the + // connection is closed. + bool show_stat{}; + // gso_burst is the number of packets to aggregate in GSO. 0 means + // it is not limited by the configuration. + size_t gso_burst{}; +}; + +class ClientBase { +public: + ClientBase(); + ~ClientBase(); + + ngtcp2_conn *conn() const; + + void write_qlog(const void *data, size_t datalen); + + ngtcp2_crypto_conn_ref *conn_ref(); + + void ticket_received(); + +protected: + ngtcp2_crypto_conn_ref conn_ref_; + TLSClientSession tls_session_; + FILE *qlog_{}; + ngtcp2_conn *conn_{}; + ngtcp2_ccerr last_error_; + bool ticket_received_{}; +}; + +void qlog_write_cb(void *user_data, uint32_t flags, const void *data, + size_t datalen); + +#endif // !defined(CLIENT_BASE_H) diff --git a/deps/ngtcp2/ngtcp2/examples/debug.cc b/deps/ngtcp2/ngtcp2/examples/debug.cc new file mode 100644 index 00000000000000..495f42cd3cf3d1 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/debug.cc @@ -0,0 +1,338 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "debug.h" + +#include + +#include +#include +#include + +#include "util.h" + +using namespace std::literals; + +namespace ngtcp2 { + +namespace debug { + +namespace { +auto randgen = util::make_mt19937(); +} // namespace + +namespace { +auto *outfile = stderr; +} // namespace + +int handshake_completed(ngtcp2_conn *conn, void *user_data) { + std::println(outfile, "QUIC handshake has completed"); + return 0; +} + +int handshake_confirmed(ngtcp2_conn *conn, void *user_data) { + std::println(outfile, "QUIC handshake has been confirmed"); + return 0; +} + +bool packet_lost(double prob) { + auto p = std::uniform_real_distribution<>(0, 1)(randgen); + return p < prob; +} + +void print_crypto_data(ngtcp2_encryption_level encryption_level, + std::span data) { + const char *encryption_level_str; + switch (encryption_level) { + case NGTCP2_ENCRYPTION_LEVEL_INITIAL: + encryption_level_str = "Initial"; + break; + case NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE: + encryption_level_str = "Handshake"; + break; + case NGTCP2_ENCRYPTION_LEVEL_1RTT: + encryption_level_str = "1-RTT"; + break; + default: + assert(0); + abort(); + } + std::println(outfile, "Ordered CRYPTO data in {} crypto level", + encryption_level_str); + util::hexdump(outfile, data); +} + +void print_stream_data(int64_t stream_id, std::span data) { + std::println(outfile, "Ordered STREAM data stream_id={:#x}", stream_id); + util::hexdump(outfile, data); +} + +void print_initial_secret(std::span data) { + std::println(outfile, "initial_secret={}", util::format_hex(data)); +} + +void print_client_in_secret(std::span data) { + std::println(outfile, "client_in_secret={}", util::format_hex(data)); +} + +void print_server_in_secret(std::span data) { + std::println(outfile, "server_in_secret={}", util::format_hex(data)); +} + +void print_handshake_secret(std::span data) { + std::println(outfile, "handshake_secret={}", util::format_hex(data)); +} + +void print_client_hs_secret(std::span data) { + std::println(outfile, "client_hs_secret={}", util::format_hex(data)); +} + +void print_server_hs_secret(std::span data) { + std::println(outfile, "server_hs_secret={}", util::format_hex(data)); +} + +void print_client_0rtt_secret(std::span data) { + std::println(outfile, "client_0rtt_secret={}", util::format_hex(data)); +} + +void print_client_1rtt_secret(std::span data) { + std::println(outfile, "client_1rtt_secret={}", util::format_hex(data)); +} + +void print_server_1rtt_secret(std::span data) { + std::println(outfile, "server_1rtt_secret={}", util::format_hex(data)); +} + +void print_client_pp_key(std::span data) { + std::println(outfile, "+ client_pp_key={}", util::format_hex(data)); +} + +void print_server_pp_key(std::span data) { + std::println(outfile, "+ server_pp_key={}", util::format_hex(data)); +} + +void print_client_pp_iv(std::span data) { + std::println(outfile, "+ client_pp_iv={}", util::format_hex(data)); +} + +void print_server_pp_iv(std::span data) { + std::println(outfile, "+ server_pp_iv={}", util::format_hex(data)); +} + +void print_client_pp_hp(std::span data) { + std::println(outfile, "+ client_pp_hp={}", util::format_hex(data)); +} + +void print_server_pp_hp(std::span data) { + std::println(outfile, "+ server_pp_hp={}", util::format_hex(data)); +} + +void print_secrets(std::span secret, + std::span key, std::span iv, + std::span hp) { + std::println(stderr, R"(+ secret={} ++ key={} ++ iv={} ++ hp={})", + util::format_hex(secret), util::format_hex(key), + util::format_hex(iv), util::format_hex(hp)); +} + +void print_secrets(std::span secret, + std::span key, std::span iv) { + std::println(stderr, R"(+ secret={} ++ key={} ++ iv={})", + util::format_hex(secret), util::format_hex(key), + util::format_hex(iv)); +} + +void print_hp_mask(std::span mask, + std::span sample) { + std::println(outfile, "mask={} sample={}", util::format_hex(mask), + util::format_hex(sample)); +} + +void log_write(void *user_data, char *msg, size_t len) { + msg[len++] = '\n'; + + while (write(fileno(stderr), msg, len) == -1 && errno == EINTR) + ; +} + +void path_validation(const ngtcp2_path *path, + ngtcp2_path_validation_result res) { + auto local_addr = util::straddr( + reinterpret_cast(path->local.addr), path->local.addrlen); + auto remote_addr = util::straddr( + reinterpret_cast(path->remote.addr), path->remote.addrlen); + + std::println( + stderr, "Path validation against path {{local:{}, remote:{}}} {}", + local_addr, remote_addr, + res == NGTCP2_PATH_VALIDATION_RESULT_SUCCESS ? "succeeded" : "failed"); +} + +void print_http_begin_request_headers(int64_t stream_id) { + std::println(outfile, "http: stream {:#x} request headers started", + stream_id); +} + +void print_http_begin_response_headers(int64_t stream_id) { + std::println(outfile, "http: stream {:#x} response headers started", + stream_id); +} + +namespace { +void print_header(std::span name, std::span value, + uint8_t flags) { + std::println(outfile, "[{}: {}]{}", as_string_view(name), + as_string_view(value), + (flags & NGHTTP3_NV_FLAG_NEVER_INDEX) ? "(sensitive)" : ""); +} +} // namespace + +namespace { +void print_header(const nghttp3_rcbuf *name, const nghttp3_rcbuf *value, + uint8_t flags) { + auto namebuf = nghttp3_rcbuf_get_buf(name); + auto valuebuf = nghttp3_rcbuf_get_buf(value); + print_header({namebuf.base, namebuf.len}, {valuebuf.base, valuebuf.len}, + flags); +} +} // namespace + +namespace { +void print_header(const nghttp3_nv &nv) { + print_header({nv.name, nv.namelen}, {nv.value, nv.valuelen}, nv.flags); +} +} // namespace + +void print_http_header(int64_t stream_id, const nghttp3_rcbuf *name, + const nghttp3_rcbuf *value, uint8_t flags) { + std::print(outfile, "http: stream {:#x} ", stream_id); + print_header(name, value, flags); +} + +void print_http_end_headers(int64_t stream_id) { + std::println(outfile, "http: stream {:#x} headers ended", stream_id); +} + +void print_http_data(int64_t stream_id, std::span data) { + std::println(outfile, "http: stream {:#x} body {} bytes", stream_id, + data.size()); + util::hexdump(outfile, data); +} + +void print_http_begin_trailers(int64_t stream_id) { + std::println(outfile, "http: stream {:#x} trailers started", stream_id); +} + +void print_http_end_trailers(int64_t stream_id) { + std::println(outfile, "http: stream {:#x} trailers ended", stream_id); +} + +void print_http_request_headers(int64_t stream_id, const nghttp3_nv *nva, + size_t nvlen) { + std::println(outfile, "http: stream {:#x} submit request headers", stream_id); + for (size_t i = 0; i < nvlen; ++i) { + auto &nv = nva[i]; + print_header(nv); + } +} + +void print_http_response_headers(int64_t stream_id, const nghttp3_nv *nva, + size_t nvlen) { + std::println(outfile, "http: stream {:#x} submit response headers", + stream_id); + for (size_t i = 0; i < nvlen; ++i) { + auto &nv = nva[i]; + print_header(nv); + } +} + +void print_http_settings(const nghttp3_proto_settings *settings) { + std::println(outfile, R"(http: remote settings +http: SETTINGS_MAX_FIELD_SECTION_SIZE={} +http: SETTINGS_QPACK_MAX_TABLE_CAPACITY={} +http: SETTINGS_QPACK_BLOCKED_STREAMS={} +http: SETTINGS_ENABLE_CONNECT_PROTOCOL={} +http: SETTINGS_H3_DATAGRAM={})", + settings->max_field_section_size, + settings->qpack_max_dtable_capacity, + settings->qpack_blocked_streams, + settings->enable_connect_protocol, settings->h3_datagram); +} + +void print_http_origin(const uint8_t *origin, size_t originlen) { + std::println(outfile, "http: origin [{}]", + as_string_view(std::span{origin, originlen})); +} + +void print_http_end_origin() { std::println(outfile, "http: origin ended"); } + +std::string_view secret_title(ngtcp2_encryption_level level) { + switch (level) { + case NGTCP2_ENCRYPTION_LEVEL_0RTT: + return "early_traffic"sv; + case NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE: + return "handshake_traffic"sv; + case NGTCP2_ENCRYPTION_LEVEL_1RTT: + return "application_traffic"sv; + default: + assert(0); + abort(); + } +} + +void print_conn_info(ngtcp2_conn *conn) { + ngtcp2_conn_info cinfo; + + ngtcp2_conn_get_conn_info2(conn, &cinfo); + + std::println( + R"(# Connection Statistics (see ngtcp2_conn_info for details) +min_rtt={} +smoothed_rtt={} +rttvar={} +cwnd={} +ssthresh={} +pkt_sent={} +bytes_sent={} +pkt_recv={} +bytes_recv={} +pkt_lost={} +bytes_lost={} +ping_recv={} +pkt_discarded={})", + util::format_durationf(cinfo.min_rtt), + util::format_durationf(cinfo.smoothed_rtt), + util::format_durationf(cinfo.rttvar), cinfo.cwnd, cinfo.ssthresh, + cinfo.pkt_sent, cinfo.bytes_sent, cinfo.pkt_recv, cinfo.bytes_recv, + cinfo.pkt_lost, cinfo.bytes_lost, cinfo.ping_recv, cinfo.pkt_discarded); +} + +} // namespace debug + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/debug.h b/deps/ngtcp2/ngtcp2/examples/debug.h new file mode 100644 index 00000000000000..f880833f8369a0 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/debug.h @@ -0,0 +1,133 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef DEBUG_H +#define DEBUG_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#ifndef __STDC_FORMAT_MACROS +// For travis and PRIu64 +# define __STDC_FORMAT_MACROS +#endif // !defined(__STDC_FORMAT_MACROS) + +#include +#include +#include + +#include +#include + +namespace ngtcp2 { + +namespace debug { + +int handshake_completed(ngtcp2_conn *conn, void *user_data); + +int handshake_confirmed(ngtcp2_conn *conn, void *user_data); + +bool packet_lost(double prob); + +void print_crypto_data(ngtcp2_encryption_level encryption_level, + std::span data); + +void print_stream_data(int64_t stream_id, std::span data); + +void print_initial_secret(std::span data); + +void print_client_in_secret(std::span data); +void print_server_in_secret(std::span data); + +void print_handshake_secret(std::span data); + +void print_client_hs_secret(std::span data); +void print_server_hs_secret(std::span data); + +void print_client_0rtt_secret(std::span data); + +void print_client_1rtt_secret(std::span data); +void print_server_1rtt_secret(std::span data); + +void print_client_pp_key(std::span data); +void print_server_pp_key(std::span data); + +void print_client_pp_iv(std::span data); +void print_server_pp_iv(std::span data); + +void print_client_pp_hp(std::span data); +void print_server_pp_hp(std::span data); + +void print_secrets(std::span secret, + std::span key, std::span iv, + std::span hp); + +void print_secrets(std::span secret, + std::span key, std::span iv); + +void print_hp_mask(std::span mask, + std::span sample); + +void log_write(void *user_data, char *msg, size_t len); + +void path_validation(const ngtcp2_path *path, + ngtcp2_path_validation_result res); + +void print_http_begin_request_headers(int64_t stream_id); + +void print_http_begin_response_headers(int64_t stream_id); + +void print_http_header(int64_t stream_id, const nghttp3_rcbuf *name, + const nghttp3_rcbuf *value, uint8_t flags); + +void print_http_end_headers(int64_t stream_id); + +void print_http_data(int64_t stream_id, std::span data); + +void print_http_begin_trailers(int64_t stream_id); + +void print_http_end_trailers(int64_t stream_id); + +void print_http_request_headers(int64_t stream_id, const nghttp3_nv *nva, + size_t nvlen); + +void print_http_response_headers(int64_t stream_id, const nghttp3_nv *nva, + size_t nvlen); + +void print_http_settings(const nghttp3_proto_settings *settings); + +void print_http_origin(const uint8_t *origin, size_t originlen); + +void print_http_end_origin(); + +std::string_view secret_title(ngtcp2_encryption_level level); + +void print_conn_info(ngtcp2_conn *conn); + +} // namespace debug + +} // namespace ngtcp2 + +#endif // !defined(DEBUG_H) diff --git a/deps/ngtcp2/ngtcp2/examples/examplestest.cc b/deps/ngtcp2/ngtcp2/examples/examplestest.cc new file mode 100644 index 00000000000000..9e0fbed9d6b2d3 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/examplestest.cc @@ -0,0 +1,55 @@ +/* + * ngtcp2 + * + * Copyright (c) 2018 ngtcp2 contributors + * Copyright (c) 2013 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include "munit.h" + +// include test cases' include files here +#include "util_test.h" +#include "siphash_test.h" +#ifdef WITH_EXAMPLE_WOLFSSL +# include "sim_test.h" +#endif // defined(WITH_EXAMPLE_WOLFSSL) + +int main(int argc, char *argv[]) { + const MunitSuite suites[] = { + ngtcp2::util_suite, + ngtcp2::siphash_suite, +#ifdef WITH_EXAMPLE_WOLFSSL + ngtcp2::sim_suite, +#endif // defined(WITH_EXAMPLE_WOLFSSL) + {}, + }; + const MunitSuite suite = { + .prefix = "", + .suites = suites, + .iterations = 1, + }; + + return munit_suite_main(&suite, nullptr, argc, argv); +} diff --git a/deps/ngtcp2/ngtcp2/examples/gtlssimpleclient.c b/deps/ngtcp2/ngtcp2/examples/gtlssimpleclient.c new file mode 100644 index 00000000000000..955132c3aa84fd --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/gtlssimpleclient.c @@ -0,0 +1,698 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021-2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include +#endif /* defined(HAVE_CONFIG_H) */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include + +#define REMOTE_HOST "127.0.0.1" +#define REMOTE_PORT "4433" +#define ALPN "hq-interop" +#define MESSAGE "GET /\r\n" + +/* + * Example 1: Handshake with www.google.com + * + * #define REMOTE_HOST "www.google.com" + * #define REMOTE_PORT "443" + * #define ALPN "h3" + * + * and undefine MESSAGE macro. + */ + +static uint64_t timestamp(void) { + struct timespec tp; + + if (clock_gettime(CLOCK_MONOTONIC, &tp) != 0) { + fprintf(stderr, "clock_gettime: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + + return (uint64_t)tp.tv_sec * NGTCP2_SECONDS + (uint64_t)tp.tv_nsec; +} + +static int create_sock(struct sockaddr *addr, socklen_t *paddrlen, + const char *host, const char *port) { + struct addrinfo hints = {0}; + struct addrinfo *res, *rp; + int rv; + int fd = -1; + + hints.ai_flags = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + + rv = getaddrinfo(host, port, &hints, &res); + if (rv != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); + return -1; + } + + for (rp = res; rp; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) { + continue; + } + + break; + } + + if (fd == -1) { + goto end; + } + + *paddrlen = rp->ai_addrlen; + memcpy(addr, rp->ai_addr, rp->ai_addrlen); + +end: + freeaddrinfo(res); + + return fd; +} + +static int connect_sock(struct sockaddr *local_addr, socklen_t *plocal_addrlen, + int fd, const struct sockaddr *remote_addr, + size_t remote_addrlen) { + socklen_t len; + + if (connect(fd, remote_addr, (socklen_t)remote_addrlen) != 0) { + fprintf(stderr, "connect: %s\n", strerror(errno)); + return -1; + } + + len = *plocal_addrlen; + + if (getsockname(fd, local_addr, &len) == -1) { + fprintf(stderr, "getsockname: %s\n", strerror(errno)); + return -1; + } + + *plocal_addrlen = len; + + return 0; +} + +struct client { + ngtcp2_crypto_conn_ref conn_ref; + int fd; + struct sockaddr_storage local_addr; + socklen_t local_addrlen; + gnutls_certificate_credentials_t cred; + gnutls_session_t session; + ngtcp2_conn *conn; + + struct { + int64_t stream_id; + const uint8_t *data; + size_t datalen; + size_t nwrite; + } stream; + + ngtcp2_ccerr last_error; + + ev_io rev; + ev_timer timer; +}; + +static int hook_func(gnutls_session_t session, unsigned int htype, + unsigned when, unsigned int incoming, + const gnutls_datum_t *msg) { + (void)session; + (void)htype; + (void)when; + (void)incoming; + (void)msg; + /* we could save session data here */ + + return 0; +} + +static int numeric_host_family(const char *hostname, int family) { + uint8_t dst[sizeof(struct in6_addr)]; + return inet_pton(family, hostname, dst) == 1; +} + +static int numeric_host(const char *hostname) { + return numeric_host_family(hostname, AF_INET) || + numeric_host_family(hostname, AF_INET6); +} + +static const char priority[] = + "NORMAL:-VERS-ALL:+VERS-TLS1.3:-CIPHER-ALL:+AES-128-GCM:+AES-256-GCM:" + "+CHACHA20-POLY1305:+AES-128-CCM:-GROUP-ALL:+GROUP-SECP256R1:+GROUP-X25519:" + "+GROUP-SECP384R1:" + "+GROUP-SECP521R1:%DISABLE_TLS13_COMPAT_MODE"; + +static const gnutls_datum_t alpn = {(uint8_t *)ALPN, sizeof(ALPN) - 1}; + +static int client_gnutls_init(struct client *c) { + int rv = gnutls_certificate_allocate_credentials(&c->cred); + + if (rv == 0) + rv = gnutls_certificate_set_x509_system_trust(c->cred); + if (rv < 0) { + fprintf(stderr, "cred init failed: %d: %s\n", rv, gnutls_strerror(rv)); + return -1; + } + + rv = gnutls_init(&c->session, GNUTLS_CLIENT | GNUTLS_ENABLE_EARLY_DATA | + GNUTLS_NO_END_OF_EARLY_DATA); + if (rv != 0) { + fprintf(stderr, "gnutls_init: %s\n", gnutls_strerror(rv)); + return -1; + } + + if (ngtcp2_crypto_gnutls_configure_client_session(c->session) != 0) { + fprintf(stderr, "ngtcp2_crypto_gnutls_configure_client_session failed\n"); + return -1; + } + + rv = gnutls_priority_set_direct(c->session, priority, NULL); + if (rv != 0) { + fprintf(stderr, "gnutls_priority_set_direct: %s\n", gnutls_strerror(rv)); + return -1; + } + + gnutls_handshake_set_hook_function(c->session, GNUTLS_HANDSHAKE_ANY, + GNUTLS_HOOK_POST, hook_func); + + gnutls_session_set_ptr(c->session, &c->conn_ref); + + rv = gnutls_credentials_set(c->session, GNUTLS_CRD_CERTIFICATE, c->cred); + + if (rv != 0) { + fprintf(stderr, "gnutls_credentials_set: %s\n", gnutls_strerror(rv)); + return -1; + } + + gnutls_alpn_set_protocols(c->session, &alpn, 1, GNUTLS_ALPN_MANDATORY); + + if (!numeric_host(REMOTE_HOST)) { + gnutls_server_name_set(c->session, GNUTLS_NAME_DNS, REMOTE_HOST, + strlen(REMOTE_HOST)); + } else { + gnutls_server_name_set(c->session, GNUTLS_NAME_DNS, "localhost", + strlen("localhost")); + } + + return 0; +} + +static void rand_cb(uint8_t *dest, size_t destlen, + const ngtcp2_rand_ctx *rand_ctx) { + (void)rand_ctx; + + (void)gnutls_rnd(GNUTLS_RND_RANDOM, dest, destlen); +} + +static int get_new_connection_id_cb(ngtcp2_conn *conn, ngtcp2_cid *cid, + ngtcp2_stateless_reset_token *token, + size_t cidlen, void *user_data) { + (void)conn; + (void)user_data; + + if (gnutls_rnd(GNUTLS_RND_RANDOM, cid->data, cidlen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + + if (gnutls_rnd(GNUTLS_RND_RANDOM, token->data, sizeof(token->data)) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int extend_max_local_streams_bidi(ngtcp2_conn *conn, + uint64_t max_streams, + void *user_data) { +#ifdef MESSAGE + struct client *c = user_data; + int rv; + int64_t stream_id; + (void)max_streams; + + if (c->stream.stream_id != -1) { + return 0; + } + + rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL); + if (rv != 0) { + return 0; + } + + c->stream.stream_id = stream_id; + c->stream.data = (const uint8_t *)MESSAGE; + c->stream.datalen = sizeof(MESSAGE) - 1; + + return 0; +#else /* !defined(MESSAGE) */ + (void)conn; + (void)max_streams; + (void)user_data; + + return 0; +#endif /* !defined(MESSAGE) */ +} + +static void log_printf(void *user_data, const char *fmt, ...) { + va_list ap; + (void)user_data; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + fprintf(stderr, "\n"); +} + +static int client_quic_init(struct client *c, + const struct sockaddr *remote_addr, + socklen_t remote_addrlen, + const struct sockaddr *local_addr, + socklen_t local_addrlen) { + ngtcp2_path path = { + .local = + { + .addr = (struct sockaddr *)local_addr, + .addrlen = local_addrlen, + }, + .remote = + { + .addr = (struct sockaddr *)remote_addr, + .addrlen = remote_addrlen, + }, + }; + ngtcp2_callbacks callbacks = { + .client_initial = ngtcp2_crypto_client_initial_cb, + .recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb, + .encrypt = ngtcp2_crypto_encrypt_cb, + .decrypt = ngtcp2_crypto_decrypt_cb, + .hp_mask = ngtcp2_crypto_hp_mask_cb, + .recv_retry = ngtcp2_crypto_recv_retry_cb, + .extend_max_local_streams_bidi = extend_max_local_streams_bidi, + .rand = rand_cb, + .update_key = ngtcp2_crypto_update_key_cb, + .delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb, + .delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + .version_negotiation = ngtcp2_crypto_version_negotiation_cb, + .get_new_connection_id2 = get_new_connection_id_cb, + .get_path_challenge_data2 = ngtcp2_crypto_get_path_challenge_data2_cb, + }; + ngtcp2_cid dcid, scid; + ngtcp2_settings settings; + ngtcp2_transport_params params; + int rv; + + dcid.datalen = NGTCP2_MIN_INITIAL_DCIDLEN; + if (gnutls_rnd(GNUTLS_RND_RANDOM, dcid.data, dcid.datalen) != 0) { + fprintf(stderr, "gnutls_rnd failed\n"); + return -1; + } + + scid.datalen = 8; + if (gnutls_rnd(GNUTLS_RND_RANDOM, scid.data, scid.datalen) != 0) { + fprintf(stderr, "gnutls_rnd failed\n"); + return -1; + } + + ngtcp2_settings_default(&settings); + + settings.initial_ts = timestamp(); + settings.log_printf = log_printf; + + ngtcp2_transport_params_default(¶ms); + + params.initial_max_streams_uni = 3; + params.initial_max_stream_data_bidi_local = 128 * 1024; + params.initial_max_data = 1024 * 1024; + + rv = + ngtcp2_conn_client_new(&c->conn, &dcid, &scid, &path, NGTCP2_PROTO_VER_V1, + &callbacks, &settings, ¶ms, NULL, c); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_client_new: %s\n", ngtcp2_strerror(rv)); + return -1; + } + + ngtcp2_conn_set_tls_native_handle(c->conn, c->session); + + return 0; +} + +static int client_read(struct client *c) { + uint8_t buf[65536]; + struct sockaddr_storage addr; + struct iovec iov = { + .iov_base = buf, + .iov_len = sizeof(buf), + }; + struct msghdr msg = {0}; + ssize_t nread; + ngtcp2_path path; + ngtcp2_pkt_info pi = {0}; + int rv; + + msg.msg_name = &addr; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + for (;;) { + msg.msg_namelen = sizeof(addr); + + nread = recvmsg(c->fd, &msg, MSG_DONTWAIT); + + if (nread == -1) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + fprintf(stderr, "recvmsg: %s\n", strerror(errno)); + } + + break; + } + + path.local.addrlen = c->local_addrlen; + path.local.addr = (struct sockaddr *)&c->local_addr; + path.remote.addrlen = msg.msg_namelen; + path.remote.addr = msg.msg_name; + + rv = ngtcp2_conn_read_pkt(c->conn, &path, &pi, buf, (size_t)nread, + timestamp()); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_read_pkt: %s\n", ngtcp2_strerror(rv)); + if (!c->last_error.error_code) { + if (rv == NGTCP2_ERR_CRYPTO) { + ngtcp2_ccerr_set_tls_alert( + &c->last_error, ngtcp2_conn_get_tls_alert2(c->conn), NULL, 0); + } else { + ngtcp2_ccerr_set_liberr(&c->last_error, rv, NULL, 0); + } + } + return -1; + } + } + + return 0; +} + +static int client_send_packet(struct client *c, const uint8_t *data, + size_t datalen) { + struct iovec iov = { + .iov_base = (uint8_t *)data, + .iov_len = datalen, + }; + struct msghdr msg = {0}; + ssize_t nwrite; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + do { + nwrite = sendmsg(c->fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + fprintf(stderr, "sendmsg: %s\n", strerror(errno)); + + return -1; + } + + return 0; +} + +static size_t client_get_message(struct client *c, int64_t *pstream_id, + int *pfin, ngtcp2_vec *datav, + size_t datavcnt) { + if (datavcnt == 0) { + return 0; + } + + if (c->stream.stream_id != -1 && c->stream.nwrite < c->stream.datalen) { + *pstream_id = c->stream.stream_id; + *pfin = 1; + datav->base = (uint8_t *)c->stream.data + c->stream.nwrite; + datav->len = c->stream.datalen - c->stream.nwrite; + return 1; + } + + *pstream_id = -1; + *pfin = 0; + datav->base = NULL; + datav->len = 0; + + return 0; +} + +static int client_write_streams(struct client *c) { + ngtcp2_tstamp ts = timestamp(); + ngtcp2_pkt_info pi; + ngtcp2_ssize nwrite; + uint8_t buf[1452]; + ngtcp2_path_storage ps; + ngtcp2_vec datav; + size_t datavcnt; + int64_t stream_id; + ngtcp2_ssize wdatalen; + uint32_t flags; + int fin; + + ngtcp2_path_storage_zero(&ps); + + for (;;) { + datavcnt = client_get_message(c, &stream_id, &fin, &datav, 1); + + flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + if (fin) { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + nwrite = ngtcp2_conn_writev_stream(c->conn, &ps.path, &pi, buf, sizeof(buf), + &wdatalen, flags, stream_id, &datav, + datavcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_WRITE_MORE: + c->stream.nwrite += (size_t)wdatalen; + continue; + default: + fprintf(stderr, "ngtcp2_conn_writev_stream: %s\n", + ngtcp2_strerror((int)nwrite)); + ngtcp2_ccerr_set_liberr(&c->last_error, (int)nwrite, NULL, 0); + return -1; + } + } + + if (nwrite == 0) { + return 0; + } + + if (wdatalen > 0) { + c->stream.nwrite += (size_t)wdatalen; + } + + if (client_send_packet(c, buf, (size_t)nwrite) != 0) { + break; + } + } + + return 0; +} + +static int client_write(struct client *c) { + ngtcp2_tstamp expiry, now; + ev_tstamp t; + + if (client_write_streams(c) != 0) { + return -1; + } + + expiry = ngtcp2_conn_get_expiry2(c->conn); + now = timestamp(); + + t = expiry < now ? 1e-9 : (ev_tstamp)(expiry - now) / NGTCP2_SECONDS; + + c->timer.repeat = t; + ev_timer_again(EV_DEFAULT, &c->timer); + + return 0; +} + +static int client_handle_expiry(struct client *c) { + int rv = ngtcp2_conn_handle_expiry(c->conn, timestamp()); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_handle_expiry: %s\n", ngtcp2_strerror(rv)); + return -1; + } + + return 0; +} + +static void client_close(struct client *c) { + ngtcp2_ssize nwrite; + ngtcp2_pkt_info pi; + ngtcp2_path_storage ps; + uint8_t buf[1280]; + + if (ngtcp2_conn_in_closing_period2(c->conn) || + ngtcp2_conn_in_draining_period2(c->conn)) { + goto fin; + } + + ngtcp2_path_storage_zero(&ps); + + nwrite = ngtcp2_conn_write_connection_close( + c->conn, &ps.path, &pi, buf, sizeof(buf), &c->last_error, timestamp()); + if (nwrite < 0) { + fprintf(stderr, "ngtcp2_conn_write_connection_close: %s\n", + ngtcp2_strerror((int)nwrite)); + goto fin; + } + + client_send_packet(c, buf, (size_t)nwrite); + +fin: + ev_break(EV_DEFAULT, EVBREAK_ALL); +} + +static void read_cb(struct ev_loop *loop, ev_io *w, int revents) { + struct client *c = w->data; + (void)loop; + (void)revents; + + if (client_read(c) != 0) { + client_close(c); + return; + } + + if (client_write(c) != 0) { + client_close(c); + } +} + +static void timer_cb(struct ev_loop *loop, ev_timer *w, int revents) { + struct client *c = w->data; + (void)loop; + (void)revents; + + if (client_handle_expiry(c) != 0) { + client_close(c); + return; + } + + if (client_write(c) != 0) { + client_close(c); + } +} + +static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { + struct client *c = conn_ref->user_data; + return c->conn; +} + +static int client_init(struct client *c) { + struct sockaddr_storage remote_addr, local_addr; + socklen_t remote_addrlen, local_addrlen = sizeof(local_addr); + + *c = (struct client){0}; + + ngtcp2_ccerr_default(&c->last_error); + + c->fd = create_sock((struct sockaddr *)&remote_addr, &remote_addrlen, + REMOTE_HOST, REMOTE_PORT); + if (c->fd == -1) { + return -1; + } + + if (connect_sock((struct sockaddr *)&local_addr, &local_addrlen, c->fd, + (struct sockaddr *)&remote_addr, remote_addrlen) != 0) { + return -1; + } + + memcpy(&c->local_addr, &local_addr, sizeof(c->local_addr)); + c->local_addrlen = local_addrlen; + + if (client_gnutls_init(c) != 0) { + return -1; + } + + if (client_quic_init(c, (struct sockaddr *)&remote_addr, remote_addrlen, + (struct sockaddr *)&local_addr, local_addrlen) != 0) { + return -1; + } + + c->stream.stream_id = -1; + + c->conn_ref.get_conn = get_conn; + c->conn_ref.user_data = c; + + ev_io_init(&c->rev, read_cb, c->fd, EV_READ); + c->rev.data = c; + ev_io_start(EV_DEFAULT, &c->rev); + + ev_timer_init(&c->timer, timer_cb, 0., 0.); + c->timer.data = c; + + return 0; +} + +static void client_free(struct client *c) { + ngtcp2_conn_del(c->conn); + gnutls_deinit(c->session); + gnutls_certificate_free_credentials(c->cred); +} + +int main(void) { + struct client c; + + if (client_init(&c) != 0) { + exit(EXIT_FAILURE); + } + + if (client_write(&c) != 0) { + exit(EXIT_FAILURE); + } + + ev_run(EV_DEFAULT, 0); + + client_free(&c); + + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/examples/hq_client_proto_codec.cc b/deps/ngtcp2/ngtcp2/examples/hq_client_proto_codec.cc new file mode 100644 index 00000000000000..9537f862dad64e --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/hq_client_proto_codec.cc @@ -0,0 +1,192 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "hq_client_proto_codec.h" + +#include + +#include "client.h" +#include "debug.h" + +extern Config config; + +namespace ngtcp2 { + +bool StreamIDLess::operator()(const Stream *lhs, const Stream *rhs) const { + return lhs->stream_id < rhs->stream_id; +} + +ProtoCodec::ProtoCodec(Client *c, ngtcp2_ccerr &last_error) + : client_{c}, conn_{client_->conn()}, last_error_{last_error} {} + +std::expected +ProtoCodec::recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data) { + auto stream = client_->find_stream(stream_id); + if (!stream) { + return {}; + } + + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, data.size()); + ngtcp2_conn_extend_max_offset(conn_, data.size()); + + if (stream->fd == -1) { + return {}; + } + + ssize_t nwrite; + + for (; !data.empty();) { + do { + nwrite = write(stream->fd, data.data(), data.size()); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite < 0) { + std::println(stderr, "Could not write data to file: {}", strerror(errno)); + + return {}; + } + + data = data.subspan(static_cast(nwrite)); + } + + return {}; +} + +std::expected +ProtoCodec::on_stream_close(int64_t stream_id, uint64_t app_error_code) { + if (!ngtcp2_is_bidi_stream(stream_id)) { + return {}; + } + + if (!config.quiet) { + std::println(stderr, "HTTP stream {:#x} closed with error code {:#x}", + stream_id, app_error_code); + } + + return {}; +} + +std::expected +ProtoCodec::extend_max_stream_data(int64_t stream_id, uint64_t max_data) { + auto stream = client_->find_stream(stream_id); + if (!stream) { + return {}; + } + + if (!stream->reqbuf.empty()) { + sendq_.emplace(stream); + } + + return {}; +} + +ngtcp2_ssize ProtoCodec::write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, + uint8_t *dest, size_t destlen, + ngtcp2_tstamp ts) { + ngtcp2_vec vec; + + for (;;) { + int64_t stream_id = -1; + size_t vcnt = 0; + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + Stream *stream = nullptr; + + if (!sendq_.empty() && ngtcp2_conn_get_max_data_left2(conn_)) { + stream = *std::ranges::begin(sendq_); + + stream_id = stream->stream_id; + vec.base = const_cast(stream->reqbuf.data()); + vec.len = stream->reqbuf.size(); + vcnt = 1; + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + ngtcp2_ssize ndatalen; + + auto nwrite = + ngtcp2_conn_writev_stream(conn_, path, pi, dest, destlen, &ndatalen, + flags, stream_id, &vec, vcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + case NGTCP2_ERR_STREAM_SHUT_WR: + assert(ndatalen == -1); + sendq_.erase(std::ranges::begin(sendq_)); + continue; + case NGTCP2_ERR_WRITE_MORE: + assert(ndatalen >= 0); + stream->reqbuf = stream->reqbuf.subspan(as_unsigned(ndatalen)); + if (stream->reqbuf.empty()) { + sendq_.erase(std::ranges::begin(sendq_)); + } + continue; + } + + assert(ndatalen == -1); + + std::println(stderr, "ngtcp2_conn_writev_stream: {}", + ngtcp2_strerror(static_cast(nwrite))); + ngtcp2_ccerr_set_liberr(&last_error_, static_cast(nwrite), nullptr, + 0); + + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (ndatalen >= 0) { + stream->reqbuf = stream->reqbuf.subspan(as_unsigned(ndatalen)); + if (stream->reqbuf.empty()) { + sendq_.erase(std::ranges::begin(sendq_)); + } + } + + return nwrite; + } +} + +std::expected ProtoCodec::submit_request(Stream *stream) { + const auto &req = stream->req; + + stream->rawreqbuf = config.http_method; + stream->rawreqbuf += ' '; + stream->rawreqbuf += req.path; + stream->rawreqbuf += "\r\n"; + + stream->reqbuf = as_uint8_span(std::span{stream->rawreqbuf}); + + if (!config.quiet) { + auto nva = std::to_array({ + util::make_nv_nn(":method", config.http_method), + util::make_nv_nn(":path", req.path), + }); + debug::print_http_request_headers(stream->stream_id, nva.data(), + nva.size()); + } + + sendq_.emplace(stream); + + return {}; +} + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/hq_client_proto_codec.h b/deps/ngtcp2/ngtcp2/examples/hq_client_proto_codec.h new file mode 100644 index 00000000000000..224b8f2a7214bc --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/hq_client_proto_codec.h @@ -0,0 +1,95 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef HQ_CLIENT_PROTO_CODEC_H +#define HQ_CLIENT_PROTO_CODEC_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include +#include + +#include + +#include "shared.h" +#include "client_base.h" + +struct Stream; +class Client; + +namespace ngtcp2 { + +struct StreamIDLess { + bool operator()(const Stream *lhs, const Stream *rhs) const; +}; + +class ProtoCodec { +public: + ProtoCodec(Client *handler, ngtcp2_ccerr &last_error); + + std::expected recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data); + + std::expected acked_stream_data_offset(int64_t stream_id, + uint64_t datalen) { + return {}; + } + + std::expected extend_max_stream_data(int64_t stream_id, + uint64_t max_data); + + void early_data_rejected() {} + + std::expected on_stream_close(int64_t stream_id, + uint64_t app_error_code); + + std::expected on_stream_reset(int64_t stream_id) { return {}; } + + std::expected on_stream_stop_sending(int64_t stream_id) { + return {}; + } + + std::expected submit_request(Stream *stream); + + ngtcp2_ssize write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, uint8_t *dest, + size_t destlen, ngtcp2_tstamp ts); + + std::expected setup_codec() { return {}; } + + static constexpr auto protocol = AppProtocol::HQ; + static constexpr auto no_error = 0; + +private: + Client *client_; + ngtcp2_conn *conn_; + ngtcp2_ccerr &last_error_; + std::set sendq_; +}; + +} // namespace ngtcp2 + +#endif // !defined(HQ_CLIENT_PROTO_CODEC_H) diff --git a/deps/ngtcp2/ngtcp2/examples/hq_server_proto_codec.cc b/deps/ngtcp2/ngtcp2/examples/hq_server_proto_codec.cc new file mode 100644 index 00000000000000..8490ca24b0eb5e --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/hq_server_proto_codec.cc @@ -0,0 +1,261 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "hq_server_proto_codec.h" + +#include "server.h" +#include "debug.h" + +extern Config config; + +namespace ngtcp2 { + +bool StreamIDLess::operator()(const Stream *lhs, const Stream *rhs) const { + return lhs->stream_id < rhs->stream_id; +} + +ProtoCodec::ProtoCodec(Handler *h, ngtcp2_ccerr &last_error) + : handler_{h}, conn_{handler_->conn()}, last_error_{last_error} {} + +namespace { +int on_msg_begin(http_parser *htp) { + auto s = static_cast(htp->data); + if (s->eos) { + return -1; + } + return 0; +} +} // namespace + +namespace { +int on_url_cb(http_parser *htp, const char *data, size_t datalen) { + auto s = static_cast(htp->data); + s->uri.append(data, datalen); + return 0; +} +} // namespace + +namespace { +int on_msg_complete(http_parser *htp) { + auto s = static_cast(htp->data); + s->eos = true; + s->start_response(); + return 0; +} +} // namespace + +constexpr auto htp_settings = http_parser_settings{ + .on_message_begin = on_msg_begin, + .on_url = on_url_cb, + .on_message_complete = on_msg_complete, +}; + +std::expected +ProtoCodec::recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_stream_data(stream_id, data); + } + + auto stream = handler_->find_stream(stream_id); + if (!stream) { + return {}; + } + + if (!stream->eos) { + auto nread = http_parser_execute( + &stream->htp, &htp_settings, reinterpret_cast(data.data()), + data.size()); + if (nread != data.size()) { + if (auto rv = ngtcp2_conn_shutdown_stream(conn_, 0, stream_id, + /* app error code */ 1); + rv != 0) { + std::println(stderr, "ngtcp2_conn_shutdown_stream: {}", + ngtcp2_strerror(rv)); + ngtcp2_ccerr_set_liberr(&last_error_, NGTCP2_ERR_INTERNAL, nullptr, 0); + return std::unexpected{Error::QUIC}; + } + } + } + + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, data.size()); + ngtcp2_conn_extend_max_offset(conn_, data.size()); + + return {}; +} + +std::expected +ProtoCodec::extend_max_stream_data(int64_t stream_id, uint64_t max_data) { + auto stream = handler_->find_stream(stream_id); + if (!stream) { + return {}; + } + + if (!stream->resp_data.empty()) { + sendq_.emplace(stream); + } + + return {}; +} + +ngtcp2_ssize ProtoCodec::write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, + uint8_t *dest, size_t destlen, + ngtcp2_tstamp ts) { + ngtcp2_vec vec; + + for (;;) { + int64_t stream_id = -1; + size_t vcnt = 0; + uint32_t flags = + NGTCP2_WRITE_STREAM_FLAG_MORE | NGTCP2_WRITE_STREAM_FLAG_PADDING; + Stream *stream = nullptr; + + if (!sendq_.empty() && ngtcp2_conn_get_max_data_left2(conn_)) { + stream = *std::ranges::begin(sendq_); + + stream_id = stream->stream_id; + vec.base = const_cast(stream->resp_data.data()); + vec.len = stream->resp_data.size(); + vcnt = 1; + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + ngtcp2_ssize ndatalen; + + auto nwrite = + ngtcp2_conn_writev_stream(conn_, path, pi, dest, destlen, &ndatalen, + flags, stream_id, &vec, vcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + case NGTCP2_ERR_STREAM_SHUT_WR: + assert(ndatalen == -1); + sendq_.erase(std::ranges::begin(sendq_)); + continue; + case NGTCP2_ERR_WRITE_MORE: + assert(ndatalen >= 0); + stream->resp_data = stream->resp_data.subspan(as_unsigned(ndatalen)); + if (stream->resp_data.empty()) { + sendq_.erase(std::ranges::begin(sendq_)); + } + continue; + } + + assert(ndatalen == -1); + + std::println(stderr, "ngtcp2_conn_writev_stream: {}", + ngtcp2_strerror(static_cast(nwrite))); + ngtcp2_ccerr_set_liberr(&last_error_, static_cast(nwrite), nullptr, + 0); + + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (ndatalen >= 0) { + stream->resp_data = stream->resp_data.subspan(as_unsigned(ndatalen)); + if (stream->resp_data.empty()) { + sendq_.erase(std::ranges::begin(sendq_)); + } + } + + return nwrite; + } +} + +std::expected +ProtoCodec::on_stream_close(int64_t stream_id, uint64_t app_error_code) { + auto stream = handler_->find_stream(stream_id); + if (!stream) { + return {}; + } + + sendq_.erase(stream); + + if (!config.quiet) { + std::println(stderr, "HTTP stream {:#x} closed with error code {:#x}", + stream_id, app_error_code); + } + + return {}; +} + +std::expected +ProtoCodec::send_status_response(Stream *stream, unsigned int status_code) { + stream->status_resp_body = make_status_body(status_code); + stream->resp_data = as_uint8_span(std::span{stream->status_resp_body}); + + sendq_.emplace(stream); + + handler_->shutdown_read(stream->stream_id, 0); + + return {}; +} + +std::expected ProtoCodec::start_response(Stream *stream) { + if (stream->uri.empty()) { + send_status_response(stream, 400); + return {}; + } + + auto maybe_req = stream->request_path(); + if (!maybe_req) { + send_status_response(stream, 400); + return {}; + } + + const auto &req = *maybe_req; + + auto path = config.htdocs; + path /= std::filesystem::path{req.path}.relative_path(); + + auto maybe_fe = stream->open_file(path); + if (!maybe_fe) { + send_status_response(stream, 404); + return {}; + } + + const auto &fe = *maybe_fe; + + if (fe.flags & FILE_ENTRY_TYPE_DIR) { + send_status_response(stream, 404); + return {}; + } + + stream->map_file(fe); + + if (!config.quiet) { + auto nva = std::to_array({ + util::make_nv_nn(":status", "200"), + }); + + debug::print_http_response_headers(stream->stream_id, nva.data(), + nva.size()); + } + + sendq_.emplace(stream); + + return {}; +} + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/hq_server_proto_codec.h b/deps/ngtcp2/ngtcp2/examples/hq_server_proto_codec.h new file mode 100644 index 00000000000000..a60d616e3da254 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/hq_server_proto_codec.h @@ -0,0 +1,98 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef HQ_SERVER_PROTO_CODEC_H +#define HQ_SERVER_PROTO_CODEC_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include +#include + +#include +#include + +#include "shared.h" +#include "server_base.h" + +struct Stream; +class Handler; + +namespace ngtcp2 { + +struct StreamIDLess { + bool operator()(const Stream *lhs, const Stream *rhs) const; +}; + +class ProtoCodec { +public: + ProtoCodec(Handler *handler, ngtcp2_ccerr &last_error); + + std::expected acked_stream_data_offset(int64_t stream_id, + uint64_t datalen) { + return {}; + } + + std::expected on_stream_reset(int64_t stream_id) { return {}; } + + std::expected on_stream_stop_sending(int64_t stream_id) { + return {}; + } + + void extend_max_remote_streams_bidi(uint64_t max_streams) {} + + std::expected extend_max_stream_data(int64_t stream_id, + uint64_t max_data); + + std::expected on_app_tx_ready() { return {}; } + + ngtcp2_ssize write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, uint8_t *dest, + size_t destlen, ngtcp2_tstamp ts); + + std::expected recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data); + + std::expected on_stream_close(int64_t stream_id, + uint64_t app_error_code); + + std::expected start_response(Stream *stream); + + static constexpr auto protocol = AppProtocol::HQ; + +private: + std::expected send_status_response(Stream *stream, + unsigned int status_code); + + Handler *handler_; + ngtcp2_conn *conn_; + ngtcp2_ccerr &last_error_; + std::set sendq_; +}; + +} // namespace ngtcp2 + +#endif // !defined(HQ_SERVER_PROTO_CODEC_H) diff --git a/deps/ngtcp2/ngtcp2/examples/http.cc b/deps/ngtcp2/ngtcp2/examples/http.cc new file mode 100644 index 00000000000000..9467a184dfe967 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/http.cc @@ -0,0 +1,140 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * Copyright (c) 2012 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "http.h" + +using namespace std::literals; + +namespace ngtcp2 { + +namespace http { + +std::string_view get_reason_phrase(unsigned int status_code) { + switch (status_code) { + case 100: + return "Continue"sv; + case 101: + return "Switching Protocols"sv; + case 200: + return "OK"sv; + case 201: + return "Created"sv; + case 202: + return "Accepted"sv; + case 203: + return "Non-Authoritative Information"sv; + case 204: + return "No Content"sv; + case 205: + return "Reset Content"sv; + case 206: + return "Partial Content"sv; + case 300: + return "Multiple Choices"sv; + case 301: + return "Moved Permanently"sv; + case 302: + return "Found"sv; + case 303: + return "See Other"sv; + case 304: + return "Not Modified"sv; + case 305: + return "Use Proxy"sv; + // case 306: return "(Unused)"sv; + case 307: + return "Temporary Redirect"sv; + case 308: + return "Permanent Redirect"sv; + case 400: + return "Bad Request"sv; + case 401: + return "Unauthorized"sv; + case 402: + return "Payment Required"sv; + case 403: + return "Forbidden"sv; + case 404: + return "Not Found"sv; + case 405: + return "Method Not Allowed"sv; + case 406: + return "Not Acceptable"sv; + case 407: + return "Proxy Authentication Required"sv; + case 408: + return "Request Timeout"sv; + case 409: + return "Conflict"sv; + case 410: + return "Gone"sv; + case 411: + return "Length Required"sv; + case 412: + return "Precondition Failed"sv; + case 413: + return "Payload Too Large"sv; + case 414: + return "URI Too Long"sv; + case 415: + return "Unsupported Media Type"sv; + case 416: + return "Requested Range Not Satisfiable"sv; + case 417: + return "Expectation Failed"sv; + case 421: + return "Misdirected Request"sv; + case 426: + return "Upgrade Required"sv; + case 428: + return "Precondition Required"sv; + case 429: + return "Too Many Requests"sv; + case 431: + return "Request Header Fields Too Large"sv; + case 451: + return "Unavailable For Legal Reasons"sv; + case 500: + return "Internal Server Error"sv; + case 501: + return "Not Implemented"sv; + case 502: + return "Bad Gateway"sv; + case 503: + return "Service Unavailable"sv; + case 504: + return "Gateway Timeout"sv; + case 505: + return "HTTP Version Not Supported"sv; + case 511: + return "Network Authentication Required"sv; + default: + return ""sv; + } +} + +} // namespace http + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/http.h b/deps/ngtcp2/ngtcp2/examples/http.h new file mode 100644 index 00000000000000..789ba8678db95e --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/http.h @@ -0,0 +1,44 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef HTTP_H +#define HTTP_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +namespace ngtcp2 { + +namespace http { + +std::string_view get_reason_phrase(unsigned int status_code); + +} // namespace http + +} // namespace ngtcp2 + +#endif // !defined(HTTP_H) diff --git a/deps/ngtcp2/ngtcp2/examples/http3_client_proto_codec.cc b/deps/ngtcp2/ngtcp2/examples/http3_client_proto_codec.cc new file mode 100644 index 00000000000000..82e33acffb1914 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/http3_client_proto_codec.cc @@ -0,0 +1,606 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "http3_client_proto_codec.h" + +#include + +#include "client.h" +#include "debug.h" + +extern Config config; + +namespace ngtcp2 { + +ProtoCodec::ProtoCodec(Client *c, ngtcp2_ccerr &last_error) + : client_{c}, conn_{client_->conn()}, last_error_{last_error} {} + +ProtoCodec::~ProtoCodec() { + if (httpconn_) { + nghttp3_conn_del(httpconn_); + } +} + +std::expected +ProtoCodec::recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data) { + auto nconsumed = nghttp3_conn_read_stream2( + httpconn_, stream_id, data.data(), data.size(), + flags & NGTCP2_STREAM_DATA_FLAG_FIN, ngtcp2_conn_get_timestamp(conn_)); + if (nconsumed < 0) { + std::println(stderr, "nghttp3_conn_read_stream2: {}", + nghttp3_strerror(static_cast(nconsumed))); + ngtcp2_ccerr_set_application_error( + &last_error_, + nghttp3_err_infer_quic_app_error_code(static_cast(nconsumed)), + nullptr, 0); + return std::unexpected{Error::HTTP3}; + } + + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, + static_cast(nconsumed)); + ngtcp2_conn_extend_max_offset(conn_, static_cast(nconsumed)); + + return {}; +} + +std::expected +ProtoCodec::acked_stream_data_offset(int64_t stream_id, uint64_t datalen) { + if (auto rv = nghttp3_conn_add_ack_offset(httpconn_, stream_id, datalen); + rv != 0) { + std::println(stderr, "nghttp3_conn_add_ack_offset: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + return {}; +} + +std::expected +ProtoCodec::extend_max_stream_data(int64_t stream_id, uint64_t max_data) { + if (auto rv = nghttp3_conn_unblock_stream(httpconn_, stream_id); rv != 0) { + std::println(stderr, "nghttp3_conn_unblock_stream: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + return {}; +} + +void ProtoCodec::early_data_rejected() { + nghttp3_conn_del(httpconn_); + httpconn_ = nullptr; +} + +void ProtoCodec::http_stream_close(int64_t stream_id, uint64_t app_error_code) { + if (!ngtcp2_is_bidi_stream(stream_id)) { + return; + } + + if (!config.quiet) { + std::println(stderr, "HTTP stream {:#x} closed with error code {:#x}", + stream_id, app_error_code); + } +} + +std::expected +ProtoCodec::on_stream_close(int64_t stream_id, uint64_t app_error_code) { + if (!httpconn_) { + return {}; + } + + if (app_error_code == 0) { + app_error_code = NGHTTP3_H3_NO_ERROR; + } + + if (auto rv = nghttp3_conn_close_stream(httpconn_, stream_id, app_error_code); + rv != 0) { + if (rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + std::println(stderr, "nghttp3_conn_close_stream: {}", + nghttp3_strerror(rv)); + ngtcp2_ccerr_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, 0); + return std::unexpected{Error::HTTP3}; + } + + return {}; + } + + http_stream_close(stream_id, app_error_code); + + return {}; +} + +std::expected ProtoCodec::on_stream_reset(int64_t stream_id) { + if (!httpconn_) { + return {}; + } + + if (auto rv = nghttp3_conn_shutdown_stream_read(httpconn_, stream_id); + rv != 0) { + std::println(stderr, "nghttp3_conn_shutdown_stream_read: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + return {}; +} + +std::expected +ProtoCodec::on_stream_stop_sending(int64_t stream_id) { + if (!httpconn_) { + return {}; + } + + if (auto rv = nghttp3_conn_shutdown_stream_read(httpconn_, stream_id); + rv != 0) { + std::println(stderr, "nghttp3_conn_shutdown_stream_read: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + return {}; +} + +namespace { +nghttp3_ssize read_data(nghttp3_conn *conn, int64_t stream_id, nghttp3_vec *vec, + size_t veccnt, uint32_t *pflags, void *user_data, + void *stream_user_data) { + vec[0].base = config.data; + vec[0].len = config.datalen; + *pflags |= NGHTTP3_DATA_FLAG_EOF; + + return 1; +} +} // namespace + +std::expected ProtoCodec::submit_request(const Stream *stream) { + std::string content_length_str; + + const auto &req = stream->req; + + std::array nva{ + util::make_nv_nn(":method", config.http_method), + util::make_nv_nn(":scheme", req.scheme), + util::make_nv_nn(":authority", req.authority), + util::make_nv_nn(":path", req.path), + util::make_nv_nn("user-agent", "nghttp3/ngtcp2 client"), + }; + size_t nvlen = 5; + if (config.fd != -1) { + content_length_str = util::format_uint(config.datalen); + nva[nvlen++] = util::make_nv_nc("content-length", content_length_str); + } + + if (!config.quiet) { + debug::print_http_request_headers(stream->stream_id, nva.data(), nvlen); + } + + nghttp3_data_reader dr{ + .read_data = read_data, + }; + + if (auto rv = nghttp3_conn_submit_request( + httpconn_, stream->stream_id, nva.data(), nvlen, + config.fd == -1 ? nullptr : &dr, nullptr); + rv != 0) { + std::println(stderr, "nghttp3_conn_submit_request: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + return {}; +} + +ngtcp2_ssize ProtoCodec::write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, + uint8_t *dest, size_t destlen, + ngtcp2_tstamp ts) { + std::array vec; + + for (;;) { + int64_t stream_id = -1; + int fin = 0; + nghttp3_ssize sveccnt = 0; + + if (httpconn_ && ngtcp2_conn_get_max_data_left2(conn_)) { + sveccnt = nghttp3_conn_writev_stream(httpconn_, &stream_id, &fin, + vec.data(), vec.size()); + if (sveccnt < 0) { + std::println(stderr, "nghttp3_conn_writev_stream: {}", + nghttp3_strerror(static_cast(sveccnt))); + ngtcp2_ccerr_set_application_error( + &last_error_, + nghttp3_err_infer_quic_app_error_code(static_cast(sveccnt)), + nullptr, 0); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + } + + ngtcp2_ssize ndatalen; + auto v = vec.data(); + auto vcnt = static_cast(sveccnt); + + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + if (fin) { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + auto nwrite = ngtcp2_conn_writev_stream( + conn_, path, pi, dest, destlen, &ndatalen, flags, stream_id, + reinterpret_cast(v), vcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + assert(ndatalen == -1); + nghttp3_conn_block_stream(httpconn_, stream_id); + continue; + case NGTCP2_ERR_STREAM_SHUT_WR: + assert(ndatalen == -1); + nghttp3_conn_shutdown_stream_write(httpconn_, stream_id); + continue; + case NGTCP2_ERR_WRITE_MORE: + assert(ndatalen >= 0); + if (auto rv = nghttp3_conn_add_write_offset(httpconn_, stream_id, + as_unsigned(ndatalen)); + rv != 0) { + std::println(stderr, "nghttp3_conn_add_write_offset: {}", + nghttp3_strerror(rv)); + ngtcp2_ccerr_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, + 0); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + continue; + } + + assert(ndatalen == -1); + + std::println(stderr, "ngtcp2_conn_writev_stream: {}", + ngtcp2_strerror(static_cast(nwrite))); + ngtcp2_ccerr_set_liberr(&last_error_, static_cast(nwrite), nullptr, + 0); + + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (ndatalen >= 0) { + if (auto rv = nghttp3_conn_add_write_offset(httpconn_, stream_id, + as_unsigned(ndatalen)); + rv != 0) { + std::println(stderr, "nghttp3_conn_add_write_offset: {}", + nghttp3_strerror(rv)); + ngtcp2_ccerr_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, 0); + + return NGTCP2_ERR_CALLBACK_FAILURE; + } + } + + return nwrite; + } +} + +namespace { +int http_recv_data(nghttp3_conn *conn, int64_t stream_id, const uint8_t *data, + size_t datalen, void *user_data, void *stream_user_data) { + if (!config.quiet && !config.no_http_dump) { + debug::print_http_data(stream_id, {data, datalen}); + } + auto pc = static_cast(user_data); + pc->http_consume(stream_id, datalen); + pc->http_write_data(stream_id, {data, datalen}); + return 0; +} +} // namespace + +namespace { +int http_deferred_consume(nghttp3_conn *conn, int64_t stream_id, + size_t nconsumed, void *user_data, + void *stream_user_data) { + auto pc = static_cast(user_data); + pc->http_consume(stream_id, nconsumed); + return 0; +} +} // namespace + +void ProtoCodec::http_consume(int64_t stream_id, size_t nconsumed) { + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, nconsumed); + ngtcp2_conn_extend_max_offset(conn_, nconsumed); +} + +void ProtoCodec::http_write_data(int64_t stream_id, + std::span data) { + auto stream = client_->find_stream(stream_id); + if (!stream) { + return; + } + + if (stream->fd == -1) { + return; + } + + ssize_t nwrite; + + for (; !data.empty();) { + do { + nwrite = write(stream->fd, data.data(), data.size()); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite < 0) { + std::println(stderr, "Could not write data to file: {}", strerror(errno)); + + return; + } + + data = data.subspan(static_cast(nwrite)); + } +} + +namespace { +int http_begin_headers(nghttp3_conn *conn, int64_t stream_id, void *user_data, + void *stream_user_data) { + if (!config.quiet) { + debug::print_http_begin_response_headers(stream_id); + } + return 0; +} +} // namespace + +namespace { +int http_recv_header(nghttp3_conn *conn, int64_t stream_id, int32_t token, + nghttp3_rcbuf *name, nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_header(stream_id, name, value, flags); + } + return 0; +} +} // namespace + +namespace { +int http_end_headers(nghttp3_conn *conn, int64_t stream_id, int fin, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_end_headers(stream_id); + } + return 0; +} +} // namespace + +namespace { +int http_begin_trailers(nghttp3_conn *conn, int64_t stream_id, void *user_data, + void *stream_user_data) { + if (!config.quiet) { + debug::print_http_begin_trailers(stream_id); + } + return 0; +} +} // namespace + +namespace { +int http_recv_trailer(nghttp3_conn *conn, int64_t stream_id, int32_t token, + nghttp3_rcbuf *name, nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_header(stream_id, name, value, flags); + } + return 0; +} +} // namespace + +namespace { +int http_end_trailers(nghttp3_conn *conn, int64_t stream_id, int fin, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_end_trailers(stream_id); + } + return 0; +} +} // namespace + +namespace { +int http_stop_sending(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto pc = static_cast(user_data); + if (!pc->stop_sending(stream_id, app_error_code)) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +std::expected ProtoCodec::stop_sending(int64_t stream_id, + uint64_t app_error_code) { + if (auto rv = + ngtcp2_conn_shutdown_stream_read(conn_, 0, stream_id, app_error_code); + rv != 0) { + std::println(stderr, "ngtcp2_conn_shutdown_stream_read: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + return {}; +} + +namespace { +int http_reset_stream(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto pc = static_cast(user_data); + if (!pc->reset_stream(stream_id, app_error_code)) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +std::expected ProtoCodec::reset_stream(int64_t stream_id, + uint64_t app_error_code) { + if (auto rv = + ngtcp2_conn_shutdown_stream_write(conn_, 0, stream_id, app_error_code); + rv != 0) { + std::println(stderr, "ngtcp2_conn_shutdown_stream_write: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + return {}; +} + +namespace { +void rand_bytes(uint8_t *dest, size_t destlen) { + if (!util::generate_secure_random({dest, destlen})) { + assert(0); + abort(); + } +} +} // namespace + +namespace { +int http_recv_settings(nghttp3_conn *conn, + const nghttp3_proto_settings *settings, + void *conn_user_data) { + if (!config.quiet) { + debug::print_http_settings(settings); + } + + return 0; +} +} // namespace + +namespace { +int http_recv_origin(nghttp3_conn *conn, const uint8_t *origin, + size_t originlen, void *conn_user_data) { + if (!config.quiet) { + debug::print_http_origin(origin, originlen); + } + + return 0; +} +} // namespace + +namespace { +int http_end_origin(nghttp3_conn *conn, void *conn_user_data) { + if (!config.quiet) { + debug::print_http_end_origin(); + } + + return 0; +} +} // namespace + +std::expected ProtoCodec::setup_codec() { + if (httpconn_) { + return {}; + } + + if (ngtcp2_conn_get_streams_uni_left2(conn_) < 3) { + std::println(stderr, + "peer does not allow at least 3 unidirectional streams."); + return std::unexpected{Error::QUIC}; + } + + static constexpr auto callbacks = nghttp3_callbacks{ + .recv_data = ::http_recv_data, + .deferred_consume = ::http_deferred_consume, + .begin_headers = ::http_begin_headers, + .recv_header = ::http_recv_header, + .end_headers = ::http_end_headers, + .begin_trailers = ::http_begin_trailers, + .recv_trailer = ::http_recv_trailer, + .end_trailers = ::http_end_trailers, + .stop_sending = ::http_stop_sending, + .reset_stream = ::http_reset_stream, + .recv_origin = ::http_recv_origin, + .end_origin = ::http_end_origin, + .rand = rand_bytes, + .recv_settings2 = ::http_recv_settings, + }; + nghttp3_settings settings; + nghttp3_settings_default(&settings); + settings.qpack_max_dtable_capacity = 4_k; + settings.qpack_blocked_streams = 100; + + auto mem = nghttp3_mem_default(); + + if (auto rv = + nghttp3_conn_client_new(&httpconn_, &callbacks, &settings, mem, this); + rv != 0) { + std::println(stderr, "nghttp3_conn_client_new: {}", nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + int64_t ctrl_stream_id; + + if (auto rv = ngtcp2_conn_open_uni_stream(conn_, &ctrl_stream_id, nullptr); + rv != 0) { + std::println(stderr, "ngtcp2_conn_open_uni_stream: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + if (auto rv = nghttp3_conn_bind_control_stream(httpconn_, ctrl_stream_id); + rv != 0) { + std::println(stderr, "nghttp3_conn_bind_control_stream: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + if (!config.quiet) { + std::println(stderr, "http: control stream={:#x}", ctrl_stream_id); + } + + int64_t qpack_enc_stream_id, qpack_dec_stream_id; + + if (auto rv = + ngtcp2_conn_open_uni_stream(conn_, &qpack_enc_stream_id, nullptr); + rv != 0) { + std::println(stderr, "ngtcp2_conn_open_uni_stream: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + if (auto rv = + ngtcp2_conn_open_uni_stream(conn_, &qpack_dec_stream_id, nullptr); + rv != 0) { + std::println(stderr, "ngtcp2_conn_open_uni_stream: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + if (auto rv = nghttp3_conn_bind_qpack_streams(httpconn_, qpack_enc_stream_id, + qpack_dec_stream_id); + rv != 0) { + std::println(stderr, "nghttp3_conn_bind_qpack_streams: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + if (!config.quiet) { + std::println(stderr, "http: QPACK streams encoder={:#x} decoder={:#x}", + qpack_enc_stream_id, qpack_dec_stream_id); + } + + return {}; +} + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/http3_client_proto_codec.h b/deps/ngtcp2/ngtcp2/examples/http3_client_proto_codec.h new file mode 100644 index 00000000000000..2655e1bf8072d5 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/http3_client_proto_codec.h @@ -0,0 +1,102 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef HTTP3_CLIENT_PROTO_CODEC_H +#define HTTP3_CLIENT_PROTO_CODEC_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include + +#include +#include + +#include "shared.h" +#include "client_base.h" + +struct Stream; +class Client; + +namespace ngtcp2 { + +class ProtoCodec { +public: + ProtoCodec(Client *handler, ngtcp2_ccerr &last_error); + ~ProtoCodec(); + + std::expected recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data); + + std::expected acked_stream_data_offset(int64_t stream_id, + uint64_t datalen); + + std::expected extend_max_stream_data(int64_t stream_id, + uint64_t max_data); + + void early_data_rejected(); + + std::expected on_stream_close(int64_t stream_id, + uint64_t app_error_code); + + std::expected on_stream_reset(int64_t stream_id); + + std::expected on_stream_stop_sending(int64_t stream_id); + + std::expected submit_request(const Stream *stream); + + ngtcp2_ssize write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, uint8_t *dest, + size_t destlen, ngtcp2_tstamp ts); + + std::expected setup_codec(); + + // The following functions are made public so that they can be + // called from nghttp3 callback functions. + std::expected stop_sending(int64_t stream_id, + uint64_t app_error_code); + + std::expected reset_stream(int64_t stream_id, + uint64_t app_error_code); + + void http_consume(int64_t stream_id, size_t nconsumed); + + void http_write_data(int64_t stream_id, std::span data); + + static constexpr auto protocol = AppProtocol::H3; + static constexpr auto no_error = NGHTTP3_H3_NO_ERROR; + +private: + void http_stream_close(int64_t stream_id, uint64_t app_error_code); + + Client *client_; + ngtcp2_conn *conn_; + ngtcp2_ccerr &last_error_; + nghttp3_conn *httpconn_{}; +}; + +} // namespace ngtcp2 + +#endif // !defined(HTTP3_CLIENT_PROTO_CODEC_H) diff --git a/deps/ngtcp2/ngtcp2/examples/http3_server_proto_codec.cc b/deps/ngtcp2/ngtcp2/examples/http3_server_proto_codec.cc new file mode 100644 index 00000000000000..1a39177e32a6d1 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/http3_server_proto_codec.cc @@ -0,0 +1,844 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "http3_server_proto_codec.h" + +#include "server.h" +#include "debug.h" + +extern Config config; + +namespace ngtcp2 { + +ProtoCodec::ProtoCodec(Handler *h, ngtcp2_ccerr &last_error) + : handler_{h}, conn_{handler_->conn()}, last_error_{last_error} {} + +ProtoCodec::~ProtoCodec() { + if (httpconn_) { + nghttp3_conn_del(httpconn_); + } +} + +std::expected +ProtoCodec::acked_stream_data_offset(int64_t stream_id, uint64_t datalen) { + if (!httpconn_) { + return {}; + } + + if (auto rv = nghttp3_conn_add_ack_offset(httpconn_, stream_id, datalen); + rv != 0) { + std::println(stderr, "nghttp3_conn_add_ack_offset: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + return {}; +} + +std::expected ProtoCodec::on_stream_reset(int64_t stream_id) { + if (!httpconn_) { + return {}; + } + + if (auto rv = nghttp3_conn_shutdown_stream_read(httpconn_, stream_id); + rv != 0) { + std::println(stderr, "nghttp3_conn_shutdown_stream_read: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + return {}; +} + +std::expected +ProtoCodec::on_stream_stop_sending(int64_t stream_id) { + if (!httpconn_) { + return {}; + } + + if (auto rv = nghttp3_conn_shutdown_stream_read(httpconn_, stream_id); + rv != 0) { + std::println(stderr, "nghttp3_conn_shutdown_stream_read: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + return {}; +} + +void ProtoCodec::extend_max_remote_streams_bidi(uint64_t max_streams) { + if (!httpconn_) { + return; + } + + nghttp3_conn_set_max_client_streams_bidi(httpconn_, max_streams); +} + +std::expected +ProtoCodec::extend_max_stream_data(int64_t stream_id, uint64_t max_data) { + if (auto rv = nghttp3_conn_unblock_stream(httpconn_, stream_id); rv != 0) { + std::println(stderr, "nghttp3_conn_unblock_stream: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + return {}; +} + +std::expected ProtoCodec::on_app_tx_ready() { + return setup_httpconn(); +} + +ngtcp2_ssize ProtoCodec::write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, + uint8_t *dest, size_t destlen, + ngtcp2_tstamp ts) { + std::array vec; + + for (;;) { + int64_t stream_id = -1; + int fin = 0; + nghttp3_ssize sveccnt = 0; + + if (httpconn_ && ngtcp2_conn_get_max_data_left2(conn_)) { + sveccnt = nghttp3_conn_writev_stream(httpconn_, &stream_id, &fin, + vec.data(), vec.size()); + if (sveccnt < 0) { + std::println(stderr, "nghttp3_conn_writev_stream: {}", + nghttp3_strerror(static_cast(sveccnt))); + ngtcp2_ccerr_set_application_error( + &last_error_, + nghttp3_err_infer_quic_app_error_code(static_cast(sveccnt)), + nullptr, 0); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + } + + ngtcp2_ssize ndatalen; + auto v = vec.data(); + auto vcnt = static_cast(sveccnt); + + uint32_t flags = + NGTCP2_WRITE_STREAM_FLAG_MORE | NGTCP2_WRITE_STREAM_FLAG_PADDING; + if (fin) { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + auto nwrite = ngtcp2_conn_writev_stream( + conn_, path, pi, dest, destlen, &ndatalen, flags, stream_id, + reinterpret_cast(v), vcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + assert(ndatalen == -1); + nghttp3_conn_block_stream(httpconn_, stream_id); + continue; + case NGTCP2_ERR_STREAM_SHUT_WR: + assert(ndatalen == -1); + nghttp3_conn_shutdown_stream_write(httpconn_, stream_id); + continue; + case NGTCP2_ERR_WRITE_MORE: + assert(ndatalen >= 0); + if (auto rv = nghttp3_conn_add_write_offset(httpconn_, stream_id, + as_unsigned(ndatalen)); + rv != 0) { + std::println(stderr, "nghttp3_conn_add_write_offset: {}", + nghttp3_strerror(rv)); + ngtcp2_ccerr_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, + 0); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + continue; + } + + assert(ndatalen == -1); + + std::println(stderr, "ngtcp2_conn_writev_stream: {}", + ngtcp2_strerror(static_cast(nwrite))); + ngtcp2_ccerr_set_liberr(&last_error_, static_cast(nwrite), nullptr, + 0); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (ndatalen >= 0) { + if (auto rv = nghttp3_conn_add_write_offset(httpconn_, stream_id, + as_unsigned(ndatalen)); + rv != 0) { + std::println(stderr, "nghttp3_conn_add_write_offset: {}", + nghttp3_strerror(rv)); + ngtcp2_ccerr_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, 0); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + } + + return nwrite; + } +} + +std::expected +ProtoCodec::recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_stream_data(stream_id, data); + } + + if (!httpconn_) { + return {}; + } + + auto nconsumed = nghttp3_conn_read_stream2( + httpconn_, stream_id, data.data(), data.size(), + flags & NGTCP2_STREAM_DATA_FLAG_FIN, ngtcp2_conn_get_timestamp(conn_)); + if (nconsumed < 0) { + std::println(stderr, "nghttp3_conn_read_stream2: {}", + nghttp3_strerror(static_cast(nconsumed))); + ngtcp2_ccerr_set_application_error( + &last_error_, + nghttp3_err_infer_quic_app_error_code(static_cast(nconsumed)), + nullptr, 0); + return std::unexpected{Error::HTTP3}; + } + + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, + static_cast(nconsumed)); + ngtcp2_conn_extend_max_offset(conn_, static_cast(nconsumed)); + + return {}; +} + +std::expected +ProtoCodec::on_stream_close(int64_t stream_id, uint64_t app_error_code) { + if (!httpconn_) { + return {}; + } + + if (app_error_code == 0) { + app_error_code = NGHTTP3_H3_NO_ERROR; + } + + if (auto rv = nghttp3_conn_close_stream(httpconn_, stream_id, app_error_code); + rv != 0) { + if (rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + std::println(stderr, "nghttp3_conn_close_stream: {}", + nghttp3_strerror(rv)); + ngtcp2_ccerr_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, 0); + return std::unexpected{Error::HTTP3}; + } + + return {}; + } + + http_stream_close(stream_id, app_error_code); + + return {}; +} + +namespace { +int http_acked_stream_data(nghttp3_conn *conn, int64_t stream_id, + uint64_t datalen, void *user_data, + void *stream_user_data) { + auto pc = static_cast(user_data); + auto stream = static_cast(stream_user_data); + pc->http_acked_stream_data(stream, datalen); + return 0; +} +} // namespace + +void ProtoCodec::http_acked_stream_data(Stream *stream, uint64_t datalen) { + stream->http_acked_stream_data(datalen); +} + +namespace { +int http_recv_data(nghttp3_conn *conn, int64_t stream_id, const uint8_t *data, + size_t datalen, void *user_data, void *stream_user_data) { + if (!config.quiet && !config.no_http_dump) { + debug::print_http_data(stream_id, {data, datalen}); + } + auto pc = static_cast(user_data); + pc->http_consume(stream_id, datalen); + return 0; +} +} // namespace + +namespace { +int http_deferred_consume(nghttp3_conn *conn, int64_t stream_id, + size_t nconsumed, void *user_data, + void *stream_user_data) { + auto pc = static_cast(user_data); + pc->http_consume(stream_id, nconsumed); + return 0; +} +} // namespace + +void ProtoCodec::http_consume(int64_t stream_id, size_t nconsumed) { + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, nconsumed); + ngtcp2_conn_extend_max_offset(conn_, nconsumed); +} + +namespace { +int http_begin_request_headers(nghttp3_conn *conn, int64_t stream_id, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_begin_request_headers(stream_id); + } + + auto pc = static_cast(user_data); + pc->http_begin_request_headers(stream_id); + return 0; +} +} // namespace + +void ProtoCodec::http_begin_request_headers(int64_t stream_id) { + auto stream = handler_->find_stream(stream_id); + if (!stream) { + return; + } + + nghttp3_conn_set_stream_user_data(httpconn_, stream_id, stream); +} + +namespace { +int http_recv_request_header(nghttp3_conn *conn, int64_t stream_id, + int32_t token, nghttp3_rcbuf *name, + nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_header(stream_id, name, value, flags); + } + + auto pc = static_cast(user_data); + auto stream = static_cast(stream_user_data); + pc->http_recv_request_header(stream, token, name, value); + return 0; +} +} // namespace + +void ProtoCodec::http_recv_request_header(Stream *stream, int32_t token, + nghttp3_rcbuf *name, + nghttp3_rcbuf *value) { + auto v = nghttp3_rcbuf_get_buf(value); + + switch (token) { + case NGHTTP3_QPACK_TOKEN__PATH: + stream->uri = std::string{v.base, v.base + v.len}; + break; + case NGHTTP3_QPACK_TOKEN__METHOD: + stream->method = std::string{v.base, v.base + v.len}; + break; + case NGHTTP3_QPACK_TOKEN__AUTHORITY: + stream->authority = std::string{v.base, v.base + v.len}; + break; + } +} + +namespace { +int http_end_request_headers(nghttp3_conn *conn, int64_t stream_id, int fin, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_end_headers(stream_id); + } + + auto pc = static_cast(user_data); + auto stream = static_cast(stream_user_data); + if (!pc->http_end_request_headers(stream)) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +std::expected +ProtoCodec::http_end_request_headers(Stream *stream) { + if (config.early_response) { + if (auto rv = start_response(stream); !rv) { + return rv; + } + + handler_->shutdown_read(stream->stream_id, NGHTTP3_H3_NO_ERROR); + } + return {}; +} + +namespace { +int http_stop_sending(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto pc = static_cast(user_data); + if (!pc->http_stop_sending(stream_id, app_error_code)) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +std::expected +ProtoCodec::http_stop_sending(int64_t stream_id, uint64_t app_error_code) { + if (auto rv = + ngtcp2_conn_shutdown_stream_read(conn_, 0, stream_id, app_error_code); + rv != 0) { + std::println(stderr, "ngtcp2_conn_shutdown_stream_read: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + return {}; +} + +namespace { +int http_end_stream(nghttp3_conn *conn, int64_t stream_id, void *user_data, + void *stream_user_data) { + auto pc = static_cast(user_data); + auto stream = static_cast(stream_user_data); + if (!pc->http_end_stream(stream)) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +std::expected ProtoCodec::http_end_stream(Stream *stream) { + if (!config.early_response) { + return start_response(stream); + } + return {}; +} + +namespace { +int http_reset_stream(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto pc = static_cast(user_data); + if (!pc->http_reset_stream(stream_id, app_error_code)) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +std::expected +ProtoCodec::http_reset_stream(int64_t stream_id, uint64_t app_error_code) { + if (auto rv = + ngtcp2_conn_shutdown_stream_write(conn_, 0, stream_id, app_error_code); + rv != 0) { + std::println(stderr, "ngtcp2_conn_shutdown_stream_write: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + return {}; +} + +namespace { +void rand_bytes(uint8_t *dest, size_t destlen) { + if (!util::generate_secure_random({dest, destlen})) { + assert(0); + abort(); + } +} +} // namespace + +namespace { +int http_recv_settings(nghttp3_conn *conn, + const nghttp3_proto_settings *settings, + void *conn_user_data) { + if (!config.quiet) { + debug::print_http_settings(settings); + } + + return 0; +} +} // namespace + +namespace { +nghttp3_ssize read_data(nghttp3_conn *conn, int64_t stream_id, nghttp3_vec *vec, + size_t veccnt, uint32_t *pflags, void *user_data, + void *stream_user_data) { + auto stream = static_cast(stream_user_data); + + vec[0].base = const_cast(stream->resp_data.data()); + vec[0].len = stream->resp_data.size(); + *pflags |= NGHTTP3_DATA_FLAG_EOF; + if (config.send_trailers) { + *pflags |= NGHTTP3_DATA_FLAG_NO_END_STREAM; + } + + return 1; +} +} // namespace + +auto dyn_buf = std::make_unique>(); + +namespace { +nghttp3_ssize dyn_read_data(nghttp3_conn *conn, int64_t stream_id, + nghttp3_vec *vec, size_t veccnt, uint32_t *pflags, + void *user_data, void *stream_user_data) { + auto stream = static_cast(stream_user_data); + + auto len = + std::min(dyn_buf->size(), static_cast(stream->dyndataleft)); + + vec[0].base = dyn_buf->data(); + vec[0].len = len; + + stream->dynbuflen += len; + stream->dyndataleft -= len; + + if (stream->dyndataleft == 0) { + *pflags |= NGHTTP3_DATA_FLAG_EOF; + if (config.send_trailers) { + *pflags |= NGHTTP3_DATA_FLAG_NO_END_STREAM; + auto stream_id_str = util::format_uint(as_unsigned(stream_id)); + auto trailers = std::to_array({ + util::make_nv_nc("x-ngtcp2-stream-id"sv, stream_id_str), + }); + + if (auto rv = nghttp3_conn_submit_trailers( + conn, stream_id, trailers.data(), trailers.size()); + rv != 0) { + std::println(stderr, "nghttp3_conn_submit_trailers: {}", + nghttp3_strerror(rv)); + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + } + } + + return 1; +} +} // namespace + +std::expected ProtoCodec::start_response(Stream *stream) { + // TODO This should be handled by nghttp3 + if (stream->uri.empty() || stream->method.empty()) { + return send_status_response(stream, 400); + } + + auto maybe_req = stream->request_path(); + if (!maybe_req) { + return send_status_response(stream, 400); + } + + const auto &req = *maybe_req; + + uint64_t content_length; + nghttp3_data_reader dr{}; + auto content_type = "text/plain"sv; + + auto maybe_dyn_len = stream->find_dyn_length(req.path); + if (!maybe_dyn_len) { + auto path = config.htdocs; + path /= std::filesystem::path{req.path}.relative_path(); + + auto maybe_fe = stream->open_file(path); + if (!maybe_fe) { + return send_status_response(stream, 404); + } + + const auto &fe = *maybe_fe; + + if (fe.flags & FILE_ENTRY_TYPE_DIR) { + return send_redirect_response(stream, 308, req.path + '/'); + } + + content_length = fe.len; + + if (stream->method != "HEAD") { + stream->map_file(fe); + } + + dr.read_data = read_data; + + auto ext = path.extension(); + if (!ext.empty() && ext != ".") { + auto it = config.mime_types.find(ext.native()); + if (it != std::ranges::end(config.mime_types)) { + content_type = (*it).second; + } + } + } else { + content_length = *maybe_dyn_len; + stream->dynresp = true; + dr.read_data = dyn_read_data; + + if (stream->method != "HEAD") { + stream->dyndataleft = content_length; + } + + content_type = "application/octet-stream"sv; + } + + auto content_length_str = util::format_uint(content_length); + + std::array nva{ + util::make_nv_nn(":status"sv, "200"sv), + util::make_nv_nn("server"sv, NGTCP2_SERVER), + util::make_nv_nn("content-type"sv, content_type), + util::make_nv_nc("content-length"sv, content_length_str), + }; + + size_t nvlen = 4; + + std::string prival; + + if (req.pri.urgency != -1 || req.pri.inc != -1) { + nghttp3_pri pri; + + if (auto rv = + nghttp3_conn_get_stream_priority(httpconn_, &pri, stream->stream_id); + rv != 0) { + std::println(stderr, "nghttp3_conn_get_stream_priority: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + if (req.pri.urgency != -1) { + pri.urgency = as_unsigned(req.pri.urgency); + } + if (req.pri.inc != -1) { + pri.inc = static_cast(req.pri.inc); + } + + if (auto rv = nghttp3_conn_set_server_stream_priority( + httpconn_, stream->stream_id, &pri); + rv != 0) { + std::println(stderr, "nghttp3_conn_set_server_stream_priority: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + prival = "u="; + prival += static_cast(pri.urgency + '0'); + prival += ",i"; + if (!pri.inc) { + prival += "=?0"; + } + + nva[nvlen++] = util::make_nv_nc("priority"sv, prival); + } + + if (!config.quiet) { + debug::print_http_response_headers(stream->stream_id, nva.data(), nvlen); + } + + if (auto rv = nghttp3_conn_submit_response(httpconn_, stream->stream_id, + nva.data(), nvlen, &dr); + rv != 0) { + std::println(stderr, "nghttp3_conn_submit_response: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + if (config.send_trailers && !maybe_dyn_len) { + auto stream_id_str = util::format_uint(as_unsigned(stream->stream_id)); + auto trailers = std::to_array({ + util::make_nv_nc("x-ngtcp2-stream-id"sv, stream_id_str), + }); + + if (auto rv = nghttp3_conn_submit_trailers( + httpconn_, stream->stream_id, trailers.data(), trailers.size()); + rv != 0) { + std::println(stderr, "nghttp3_conn_submit_trailers: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + } + + return {}; +} + +std::expected +ProtoCodec::send_status_response(Stream *stream, unsigned int status_code, + std::vector extra_headers) { + stream->status_resp_body = make_status_body(status_code); + + auto status_code_str = util::format_uint(status_code); + auto content_length_str = util::format_uint(stream->status_resp_body.size()); + + std::vector nva(4 + extra_headers.size()); + nva[0] = util::make_nv_nc(":status"sv, status_code_str); + nva[1] = util::make_nv_nn("server"sv, NGTCP2_SERVER); + nva[2] = util::make_nv_nn("content-type"sv, "text/html; charset=utf-8"); + nva[3] = util::make_nv_nc("content-length"sv, content_length_str); + for (size_t i = 0; i < extra_headers.size(); ++i) { + auto &hdr = extra_headers[i]; + auto &nv = nva[4 + i]; + nv = util::make_nv_cc(hdr.name, hdr.value); + } + + stream->resp_data = as_uint8_span(std::span{stream->status_resp_body}); + + nghttp3_data_reader dr{ + .read_data = read_data, + }; + + if (auto rv = nghttp3_conn_submit_response(httpconn_, stream->stream_id, + nva.data(), nva.size(), &dr); + rv != 0) { + std::println(stderr, "nghttp3_conn_submit_response: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + if (config.send_trailers) { + auto stream_id_str = util::format_uint(as_unsigned(stream->stream_id)); + auto trailers = std::to_array({ + util::make_nv_nc("x-ngtcp2-stream-id"sv, stream_id_str), + }); + + if (auto rv = nghttp3_conn_submit_trailers( + httpconn_, stream->stream_id, trailers.data(), trailers.size()); + rv != 0) { + std::println(stderr, "nghttp3_conn_submit_trailers: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + } + + handler_->shutdown_read(stream->stream_id, NGHTTP3_H3_NO_ERROR); + + return {}; +} + +std::expected +ProtoCodec::send_redirect_response(Stream *stream, unsigned int status_code, + std::string_view path) { + return send_status_response(stream, status_code, {{"location", path}}); +} + +std::expected ProtoCodec::setup_httpconn() { + if (httpconn_) { + return {}; + } + + if (ngtcp2_conn_get_streams_uni_left2(conn_) < 3) { + std::println(stderr, + "peer does not allow at least 3 unidirectional streams."); + return std::unexpected{Error::QUIC}; + } + + static constexpr auto callbacks = nghttp3_callbacks{ + .acked_stream_data = ::http_acked_stream_data, + .recv_data = ::http_recv_data, + .deferred_consume = ::http_deferred_consume, + .begin_headers = ::http_begin_request_headers, + .recv_header = ::http_recv_request_header, + .end_headers = ::http_end_request_headers, + .stop_sending = ::http_stop_sending, + .end_stream = ::http_end_stream, + .reset_stream = ::http_reset_stream, + .rand = rand_bytes, + .recv_settings2 = ::http_recv_settings, + }; + nghttp3_settings settings; + nghttp3_settings_default(&settings); + settings.qpack_max_dtable_capacity = 4096; + settings.qpack_blocked_streams = 100; + + nghttp3_vec origin_list; + + if (config.origin_list) { + origin_list.base = config.origin_list->data(); + origin_list.len = config.origin_list->size(); + + settings.origin_list = &origin_list; + } + + auto mem = nghttp3_mem_default(); + + if (auto rv = + nghttp3_conn_server_new(&httpconn_, &callbacks, &settings, mem, this); + rv != 0) { + std::println(stderr, "nghttp3_conn_server_new: {}", nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + auto params = ngtcp2_conn_get_local_transport_params2(conn_); + + nghttp3_conn_set_max_client_streams_bidi(httpconn_, + params->initial_max_streams_bidi); + + int64_t ctrl_stream_id; + + if (auto rv = ngtcp2_conn_open_uni_stream(conn_, &ctrl_stream_id, nullptr); + rv != 0) { + std::println(stderr, "ngtcp2_conn_open_uni_stream: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + if (auto rv = nghttp3_conn_bind_control_stream(httpconn_, ctrl_stream_id); + rv != 0) { + std::println(stderr, "nghttp3_conn_bind_control_stream: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + if (!config.quiet) { + std::println(stderr, "http: control stream={:#x}", ctrl_stream_id); + } + + int64_t qpack_enc_stream_id, qpack_dec_stream_id; + + if (auto rv = + ngtcp2_conn_open_uni_stream(conn_, &qpack_enc_stream_id, nullptr); + rv != 0) { + std::println(stderr, "ngtcp2_conn_open_uni_stream: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + if (auto rv = + ngtcp2_conn_open_uni_stream(conn_, &qpack_dec_stream_id, nullptr); + rv != 0) { + std::println(stderr, "ngtcp2_conn_open_uni_stream: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + if (auto rv = nghttp3_conn_bind_qpack_streams(httpconn_, qpack_enc_stream_id, + qpack_dec_stream_id); + rv != 0) { + std::println(stderr, "nghttp3_conn_bind_qpack_streams: {}", + nghttp3_strerror(rv)); + return std::unexpected{Error::HTTP3}; + } + + if (!config.quiet) { + std::println(stderr, "http: QPACK streams encoder={:#x} decoder={:#x}", + qpack_enc_stream_id, qpack_dec_stream_id); + } + + return {}; +} + +void ProtoCodec::http_stream_close(int64_t stream_id, uint64_t app_error_code) { + if (!ngtcp2_is_bidi_stream(stream_id)) { + return; + } + + if (!config.quiet) { + std::println(stderr, "HTTP stream {:#x} closed with error code {:#x}", + stream_id, app_error_code); + } +} + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/http3_server_proto_codec.h b/deps/ngtcp2/ngtcp2/examples/http3_server_proto_codec.h new file mode 100644 index 00000000000000..08b16b79b7eaee --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/http3_server_proto_codec.h @@ -0,0 +1,120 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef HTTP3_SERVER_PROTO_CODEC_H +#define HTTP3_SERVER_PROTO_CODEC_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include + +#include +#include + +#include "shared.h" +#include "server_base.h" + +struct Stream; +class Handler; + +namespace ngtcp2 { + +class ProtoCodec { +public: + ProtoCodec(Handler *handler, ngtcp2_ccerr &last_error); + ~ProtoCodec(); + + std::expected acked_stream_data_offset(int64_t stream_id, + uint64_t datalen); + + std::expected on_stream_reset(int64_t stream_id); + + std::expected on_stream_stop_sending(int64_t stream_id); + + void extend_max_remote_streams_bidi(uint64_t max_streams); + + std::expected extend_max_stream_data(int64_t stream_id, + uint64_t max_data); + + std::expected on_app_tx_ready(); + + ngtcp2_ssize write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, uint8_t *dest, + size_t destlen, ngtcp2_tstamp ts); + + std::expected recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data); + + std::expected on_stream_close(int64_t stream_id, + uint64_t app_error_code); + + std::expected start_response(Stream *stream); + + // The following functions are made public so that they can be + // called from nghttp3 callback functions. + void http_acked_stream_data(Stream *stream, uint64_t datalen); + + void http_consume(int64_t stream_id, size_t nconsumed); + + void http_begin_request_headers(int64_t stream_id); + + void http_recv_request_header(Stream *stream, int32_t token, + nghttp3_rcbuf *name, nghttp3_rcbuf *value); + + std::expected http_end_request_headers(Stream *stream); + + std::expected http_stop_sending(int64_t stream_id, + uint64_t app_error_code); + + std::expected http_end_stream(Stream *stream); + + std::expected http_reset_stream(int64_t stream_id, + uint64_t app_error_code); + + static constexpr auto protocol = AppProtocol::H3; + +private: + std::expected + send_status_response(Stream *stream, unsigned int status_code, + std::vector extra_headers = {}); + + std::expected send_redirect_response(Stream *stream, + unsigned int status_code, + std::string_view path); + + std::expected setup_httpconn(); + + void http_stream_close(int64_t stream_id, uint64_t app_error_code); + + Handler *handler_; + ngtcp2_conn *conn_; + ngtcp2_ccerr &last_error_; + nghttp3_conn *httpconn_{}; +}; + +} // namespace ngtcp2 + +#endif // !defined(HTTP3_SERVER_PROTO_CODEC_H) diff --git a/deps/ngtcp2/ngtcp2/examples/network.h b/deps/ngtcp2/ngtcp2/examples/network.h new file mode 100644 index 00000000000000..5698312845f3c7 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/network.h @@ -0,0 +1,92 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * Copyright (c) 2016 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NETWORK_H +#define NETWORK_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#ifdef HAVE_SYS_SOCKET_H +# include +#endif // defined(HAVE_SYS_SOCKET_H) +#include +#ifdef HAVE_NETINET_IN_H +# include +#endif // defined(HAVE_NETINET_IN_H) +#ifdef HAVE_ARPA_INET_H +# include +#endif // defined(HAVE_ARPA_INET_H) + +#include +#include + +#include + +namespace ngtcp2 { + +enum network_error { + NETWORK_ERR_OK = 0, + NETWORK_ERR_SEND_BLOCKED = -11, + NETWORK_ERR_CLOSE_WAIT = -12, + NETWORK_ERR_RETRY = -13, + NETWORK_ERR_DROP_CONN = -14, +}; + +using InAddr = std::variant; + +using Sockaddr = std::variant; + +struct Address { + // as_sockaddr returns the pointer to the stored address casted to + // const sockaddr *. + [[nodiscard]] const sockaddr *as_sockaddr() const; + [[nodiscard]] sockaddr *as_sockaddr(); + // family returns the address family. + [[nodiscard]] int family() const; + // port returns the port. + [[nodiscard]] uint16_t port() const; + // port sets |port| to this address. + void port(uint16_t port); + // set stores |sa| to this address. The address family is + // determined by |sa|->sa_family, and |sa| must point to the memory + // that contains valid object which is either sockaddr_in or + // sockaddr_in6. + void set(const sockaddr *sa); + // size returns the size of the stored address. + [[nodiscard]] socklen_t size() const; + // empty returns true if this address does not contain any + // meaningful address. + [[nodiscard]] bool empty() const; + + Sockaddr skaddr; + uint32_t ifindex{}; +}; + +} // namespace ngtcp2 + +#endif // !defined(NETWORK_H) diff --git a/deps/ngtcp2/ngtcp2/examples/server.cc b/deps/ngtcp2/ngtcp2/examples/server.cc new file mode 100644 index 00000000000000..20f4a04d21504a --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/server.cc @@ -0,0 +1,3235 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "server.h" +#include "network.h" +#include "debug.h" +#include "util.h" +#include "shared.h" +#include "http.h" +#include "template.h" + +using namespace ngtcp2; +using namespace std::literals; + +constexpr auto NGTCP2_SV_SCIDLEN = 18UZ; + +constexpr auto max_preferred_versionslen = 4UZ; + +namespace { +auto randgen = util::make_mt19937(); +} // namespace + +Config config; + +Stream::Stream(int64_t stream_id, Handler *handler) + : stream_id{stream_id}, handler{handler} { +#ifdef WITH_EXAMPLE_HQ_PROTO_CODEC + htp.data = this; + http_parser_init(&htp, HTTP_REQUEST); +#endif // WITH_EXAMPLE_HQ_PROTO_CODEC +} + +std::string make_status_body(unsigned int status_code) { + auto status_string = util::format_uint(status_code); + auto reason_phrase = http::get_reason_phrase(status_code); + + std::string body; + body = ""; + body += status_string; + body += ' '; + body += reason_phrase; + body += "

"; + body += status_string; + body += ' '; + body += reason_phrase; + body += "


"; + body += NGTCP2_SERVER; + body += " at port "; + body += util::format_uint(config.port); + body += "
"; + body += ""; + return body; +} + +std::expected Stream::start_response() { + return handler->start_response(this); +} + +std::expected Stream::request_path() { + urlparse_url u; + Request req{ + .pri{ + .urgency = -1, + .inc = -1, + }, + }; + auto is_connect = method == "CONNECT"; + + if (auto rv = urlparse_parse_url(uri.data(), uri.size(), is_connect, &u); + rv != 0) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + if (u.field_set & (1 << URLPARSE_PATH)) { + req.path = util::get_string(uri, u, URLPARSE_PATH); + if (req.path.find('%') != std::string::npos) { + req.path = util::percent_decode(req.path); + } + + assert(!req.path.empty()); + + if (req.path[0] != '/') { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + if (req.path.back() == '/') { + req.path += "index.html"; + } + + auto maybe_norm_path = util::normalize_path(req.path); + if (!maybe_norm_path) { + return std::unexpected{maybe_norm_path.error()}; + } + + req.path = std::move(*maybe_norm_path); + } else { + req.path = "/index.html"; + } + + if (u.field_set & (1 << URLPARSE_QUERY)) { + static constexpr auto urgency_prefix = "u="sv; + static constexpr auto inc_prefix = "i="sv; + auto q = util::get_string(uri, u, URLPARSE_QUERY); + for (auto p = std::ranges::begin(q); p != std::ranges::end(q);) { + if (util::istarts_with(std::string_view{p, std::ranges::end(q)}, + urgency_prefix)) { + auto urgency_start = p + urgency_prefix.size(); + auto urgency_end = + std::ranges::find(urgency_start, std::ranges::end(q), '&'); + if (urgency_start + 1 == urgency_end && '0' <= *urgency_start && + *urgency_start <= '7') { + req.pri.urgency = *urgency_start - '0'; + } + if (urgency_end == std::ranges::end(q)) { + break; + } + p = urgency_end + 1; + continue; + } + if (util::istarts_with(std::string_view{p, std::ranges::end(q)}, + inc_prefix)) { + auto inc_start = p + inc_prefix.size(); + auto inc_end = std::ranges::find(inc_start, std::ranges::end(q), '&'); + if (inc_start + 1 == inc_end && + (*inc_start == '0' || *inc_start == '1')) { + req.pri.inc = *inc_start - '0'; + } + if (inc_end == std::ranges::end(q)) { + break; + } + p = inc_end + 1; + continue; + } + + p = std::ranges::find(p, std::ranges::end(q), '&'); + if (p == std::ranges::end(q)) { + break; + } + ++p; + } + } + return req; +} + +namespace { +std::unordered_map file_cache; +} // namespace + +std::expected +Stream::open_file(const std::filesystem::path &path) { + auto it = file_cache.find(path.native()); + if (it != std::ranges::end(file_cache)) { + return (*it).second; + } + + auto fd = open(path.c_str(), O_RDONLY); + if (fd == -1) { + return std::unexpected{Error::SYSCALL}; + } + + struct stat st{}; + if (fstat(fd, &st) != 0) { + close(fd); + return std::unexpected{Error::SYSCALL}; + } + + FileEntry fe; + if (st.st_mode & S_IFDIR) { + fe.flags |= FILE_ENTRY_TYPE_DIR; + fe.fd = -1; + close(fd); + } else { + fe.fd = fd; + fe.len = static_cast(st.st_size); + if (fe.len) { + fe.map = mmap(nullptr, fe.len, PROT_READ, MAP_SHARED, fd, 0); + if (fe.map == MAP_FAILED) { + std::println(stderr, "mmap: {}", strerror(errno)); + close(fd); + return std::unexpected{Error::SYSCALL}; + } + } + } + + file_cache.emplace(path.native(), fe); + + return fe; +} + +void Stream::map_file(const FileEntry &fe) { + resp_data = {static_cast(fe.map), fe.len}; +} + +std::expected Stream::find_dyn_length(std::string_view path) { + assert(path[0] == '/'); + + if (path.size() == 1) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + uint64_t n = 0; + + for (auto it = std::ranges::begin(path) + 1; it != std::ranges::end(path); + ++it) { + if (*it < '0' || '9' < *it) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + auto d = static_cast(*it - '0'); + if (n > (((1ULL << 62) - 1) - d) / 10) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + n = n * 10 + d; + if (n > config.max_dyn_length) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + } + + return n; +} + +void Stream::http_acked_stream_data(uint64_t datalen) { + if (!dynresp) { + return; + } + + assert(dynbuflen >= datalen); + + dynbuflen -= datalen; +} + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto h = static_cast(w->data); + auto s = h->server(); + + if (auto rv = h->on_write(); !rv && rv.error() != Error::CLOSE_WAIT) { + s->remove(h); + } +} +} // namespace + +namespace { +void close_waitcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto h = static_cast(w->data); + auto s = h->server(); + auto conn = h->conn(); + + if (ngtcp2_conn_in_closing_period2(conn)) { + if (!config.quiet) { + std::println(stderr, "Closing Period is over"); + } + + s->remove(h); + return; + } + if (ngtcp2_conn_in_draining_period2(conn)) { + if (!config.quiet) { + std::println(stderr, "Draining Period is over"); + } + + s->remove(h); + return; + } + + assert(0); +} +} // namespace + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto h = static_cast(w->data); + auto s = h->server(); + + if (!config.quiet) { + std::println(stderr, "Timer expired"); + } + + if (auto rv = h->handle_expiry(); !rv) { + if (rv.error() == Error::CLOSE_WAIT) { + ev_timer_stop(loop, w); + } else { + s->remove(h); + } + + return; + } + + h->signal_write(); +} +} // namespace + +Handler::Handler(struct ev_loop *loop, Server *server) + : loop_{loop}, + server_{server}, + no_gso_{ +#ifdef UDP_SEGMENT + config.no_gso +#else // !defined(UDP_SEGMENT) + true +#endif // !defined(UDP_SEGMENT) + } { + ev_io_init(&wev_, writecb, 0, EV_WRITE); + wev_.data = this; + ev_timer_init(&timer_, timeoutcb, 0., 0.); + timer_.data = this; +} + +Handler::~Handler() { + if (!config.quiet) { + std::println(stderr, "{} Closing QUIC connection", scid_); + } + + ev_timer_stop(loop_, &timer_); + ev_io_stop(loop_, &wev_); + + if (qlog_) { + fclose(qlog_); + } +} + +namespace { +int handshake_completed(ngtcp2_conn *conn, void *user_data) { + auto h = static_cast(user_data); + + if (!config.quiet) { + debug::handshake_completed(conn, user_data); + } + + if (!h->handshake_completed()) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +std::expected Handler::handshake_completed() { + if (!config.quiet) { + std::println(stderr, "Negotiated cipher suite is {}", + tls_session_.get_cipher_name()); + if (auto group = tls_session_.get_negotiated_group(); !group.empty()) { + std::println(stderr, "Negotiated group is {}", group); + } + std::println(stderr, "Negotiated ALPN is {}", + tls_session_.get_selected_alpn()); + } + + if (!tls_session_.send_session_ticket()) { + std::println(stderr, "Unable to send session ticket"); + } + + std::array token; + + auto path = ngtcp2_conn_get_path2(conn_); + auto t = util::system_clock_now(); + + auto tokenlen = ngtcp2_crypto_generate_regular_token( + token.data(), config.static_secret.data(), config.static_secret.size(), + path->remote.addr, path->remote.addrlen, t); + if (tokenlen < 0) { + std::println(stderr, "Unable to generate token"); + + return std::unexpected{Error::QUIC}; + } + + if (auto rv = ngtcp2_conn_submit_new_token(conn_, token.data(), + as_unsigned(tokenlen)); + rv != 0) { + std::println(stderr, "ngtcp2_conn_submit_new_token: {}", + ngtcp2_strerror(rv)); + + return std::unexpected{Error::QUIC}; + } + + return {}; +} + +namespace { +int do_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, + const ngtcp2_crypto_cipher_ctx *hp_ctx, const uint8_t *sample) { + if (ngtcp2_crypto_hp_mask(dest, hp, hp_ctx, sample) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (!config.quiet && config.show_secret) { + debug::print_hp_mask({dest, NGTCP2_HP_MASKLEN}, + {sample, NGTCP2_HP_SAMPLELEN}); + } + + return 0; +} +} // namespace + +namespace { +int recv_crypto_data(ngtcp2_conn *conn, + ngtcp2_encryption_level encryption_level, uint64_t offset, + const uint8_t *data, size_t datalen, void *user_data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_crypto_data(encryption_level, {data, datalen}); + } + + return ngtcp2_crypto_recv_crypto_data_cb(conn, encryption_level, offset, data, + datalen, user_data); +} +} // namespace + +namespace { +int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data, void *stream_user_data) { + auto h = static_cast(user_data); + + if (!h->recv_stream_data(flags, stream_id, {data, datalen})) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int acked_stream_data_offset(ngtcp2_conn *conn, int64_t stream_id, + uint64_t offset, uint64_t datalen, void *user_data, + void *stream_user_data) { + auto h = static_cast(user_data); + if (!h->acked_stream_data_offset(stream_id, datalen)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +std::expected Handler::acked_stream_data_offset(int64_t stream_id, + uint64_t datalen) { + return proto_codec_->acked_stream_data_offset(stream_id, datalen); +} + +namespace { +int stream_open(ngtcp2_conn *conn, int64_t stream_id, void *user_data) { + auto h = static_cast(user_data); + h->on_stream_open(stream_id); + return 0; +} +} // namespace + +void Handler::on_stream_open(int64_t stream_id) { + if (!ngtcp2_is_bidi_stream(stream_id)) { + return; + } + + assert(!streams_.contains(stream_id)); + + streams_.emplace(stream_id, std::make_unique(stream_id, this)); +} + +Stream *Handler::find_stream(int64_t stream_id) const { + auto it = streams_.find(stream_id); + if (it == std::ranges::end(streams_)) { + return nullptr; + } + + return (*it).second.get(); +} + +namespace { +int stream_close(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto h = static_cast(user_data); + if (!h->on_stream_close(stream_id, app_error_code)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +namespace { +int stream_reset(ngtcp2_conn *conn, int64_t stream_id, uint64_t final_size, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto h = static_cast(user_data); + if (!h->on_stream_reset(stream_id)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +std::expected Handler::on_stream_reset(int64_t stream_id) { + return proto_codec_->on_stream_reset(stream_id); +} + +namespace { +int stream_stop_sending(ngtcp2_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto h = static_cast(user_data); + if (!h->on_stream_stop_sending(stream_id)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +std::expected Handler::on_stream_stop_sending(int64_t stream_id) { + return proto_codec_->on_stream_stop_sending(stream_id); +} + +namespace { +void rand(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx *rand_ctx) { + if (!util::generate_secure_random({dest, destlen})) { + assert(0); + abort(); + } +} +} // namespace + +namespace { +int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, + ngtcp2_stateless_reset_token *token, size_t cidlen, + void *user_data) { + if (!util::generate_secure_random({cid->data, cidlen})) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + if (ngtcp2_crypto_generate_stateless_reset_token( + token->data, config.static_secret.data(), config.static_secret.size(), + cid) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + auto h = static_cast(user_data); + h->server()->associate_cid(cid, h); + + return 0; +} +} // namespace + +namespace { +int remove_connection_id(ngtcp2_conn *conn, const ngtcp2_cid *cid, + void *user_data) { + auto h = static_cast(user_data); + h->server()->dissociate_cid(cid); + return 0; +} +} // namespace + +namespace { +int update_key(ngtcp2_conn *conn, uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen, + void *user_data) { + auto h = static_cast(user_data); + if (!h->update_key(rx_secret, tx_secret, rx_aead_ctx, rx_iv, tx_aead_ctx, + tx_iv, current_rx_secret, current_tx_secret, secretlen)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +namespace { +int path_validation(ngtcp2_conn *conn, uint32_t flags, const ngtcp2_path *path, + const ngtcp2_path *old_path, + ngtcp2_path_validation_result res, void *user_data) { + if (!config.quiet) { + debug::path_validation(path, res); + } + + if (res != NGTCP2_PATH_VALIDATION_RESULT_SUCCESS || + !(flags & NGTCP2_PATH_VALIDATION_FLAG_NEW_TOKEN)) { + return 0; + } + + std::array token; + auto t = util::system_clock_now(); + + auto tokenlen = ngtcp2_crypto_generate_regular_token( + token.data(), config.static_secret.data(), config.static_secret.size(), + path->remote.addr, path->remote.addrlen, t); + if (tokenlen < 0) { + std::println(stderr, "Unable to generate token"); + + return 0; + } + + if (auto rv = + ngtcp2_conn_submit_new_token(conn, token.data(), as_unsigned(tokenlen)); + rv != 0) { + std::println(stderr, "ngtcp2_conn_submit_new_token: {}", + ngtcp2_strerror(rv)); + + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int extend_max_remote_streams_bidi(ngtcp2_conn *conn, uint64_t max_streams, + void *user_data) { + auto h = static_cast(user_data); + h->extend_max_remote_streams_bidi(max_streams); + return 0; +} +} // namespace + +void Handler::extend_max_remote_streams_bidi(uint64_t max_streams) { + proto_codec_->extend_max_remote_streams_bidi(max_streams); +} + +namespace { +int extend_max_stream_data(ngtcp2_conn *conn, int64_t stream_id, + uint64_t max_data, void *user_data, + void *stream_user_data) { + auto h = static_cast(user_data); + if (!h->extend_max_stream_data(stream_id, max_data)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +std::expected Handler::extend_max_stream_data(int64_t stream_id, + uint64_t max_data) { + return proto_codec_->extend_max_stream_data(stream_id, max_data); +} + +namespace { +int recv_tx_key(ngtcp2_conn *conn, ngtcp2_encryption_level level, + void *user_data) { + if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) { + return 0; + } + + auto h = static_cast(user_data); + if (!h->on_app_tx_ready()) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +std::expected Handler::on_app_tx_ready() { + return proto_codec_->on_app_tx_ready(); +} + +std::expected Handler::start_response(Stream *stream) { + return proto_codec_->start_response(stream); +} + +namespace { +void write_qlog(void *user_data, uint32_t flags, const void *data, + size_t datalen) { + auto h = static_cast(user_data); + h->write_qlog(data, datalen); +} +} // namespace + +void Handler::write_qlog(const void *data, size_t datalen) { + assert(qlog_); + fwrite(data, 1, datalen, qlog_); +} + +std::expected +Handler::init(const Endpoint &ep, const Address &local_addr, + const Address &remote_addr, const ngtcp2_cid *dcid, + const ngtcp2_cid *scid, const ngtcp2_cid *ocid, + std::span token, ngtcp2_token_type token_type, + uint32_t version, TLSServerContext &tls_ctx) { + static constexpr auto callbacks = ngtcp2_callbacks{ + .recv_client_initial = ngtcp2_crypto_recv_client_initial_cb, + .recv_crypto_data = ::recv_crypto_data, + .handshake_completed = ::handshake_completed, + .encrypt = ngtcp2_crypto_encrypt_cb, + .decrypt = ngtcp2_crypto_decrypt_cb, + .hp_mask = do_hp_mask, + .recv_stream_data = ::recv_stream_data, + .acked_stream_data_offset = ::acked_stream_data_offset, + .stream_open = stream_open, + .stream_close = stream_close, + .rand = rand, + .remove_connection_id = remove_connection_id, + .update_key = ::update_key, + .path_validation = path_validation, + .stream_reset = ::stream_reset, + .extend_max_remote_streams_bidi = ::extend_max_remote_streams_bidi, + .extend_max_stream_data = ::extend_max_stream_data, + .delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb, + .delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + .stream_stop_sending = stream_stop_sending, + .version_negotiation = ngtcp2_crypto_version_negotiation_cb, + .recv_tx_key = ::recv_tx_key, + .get_new_connection_id2 = get_new_connection_id, + .get_path_challenge_data2 = ngtcp2_crypto_get_path_challenge_data2_cb, + }; + + scid_.datalen = NGTCP2_SV_SCIDLEN; + if (auto rv = util::generate_secure_random({scid_.data, scid_.datalen}); + !rv) { + std::println(stderr, "Could not generate connection ID"); + return rv; + } + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.log_write = config.quiet ? nullptr : debug::log_write; + settings.initial_ts = util::timestamp(); + settings.token = token.data(); + settings.tokenlen = token.size(); + settings.token_type = token_type; + settings.cc_algo = config.cc_algo; + settings.initial_rtt = config.initial_rtt; + settings.max_window = config.max_window; + settings.max_stream_window = config.max_stream_window; + settings.handshake_timeout = config.handshake_timeout; + settings.no_pmtud = config.no_pmtud; + settings.ack_thresh = config.ack_thresh; + if (config.max_udp_payload_size) { + settings.max_tx_udp_payload_size = config.max_udp_payload_size; + settings.no_tx_udp_payload_size_shaping = 1; + } + if (!config.qlog_dir.empty()) { + auto path = config.qlog_dir; + path /= util::format_hex(scid_.data, as_signed(scid_.datalen)); + path += ".sqlog"; + qlog_ = fopen(path.c_str(), "w"); + if (qlog_ == nullptr) { + std::println(stderr, "Could not open qlog file {}: {}", path.native(), + strerror(errno)); + return std::unexpected{Error::IO}; + } + settings.qlog_write = ::write_qlog; + } + if (!config.preferred_versions.empty()) { + settings.preferred_versions = config.preferred_versions.data(); + settings.preferred_versionslen = config.preferred_versions.size(); + } + if (!config.available_versions.empty()) { + settings.available_versions = config.available_versions.data(); + settings.available_versionslen = config.available_versions.size(); + } + if (config.initial_pkt_num == UINT32_MAX) { + auto dis = std::uniform_int_distribution(0, INT32_MAX); + settings.initial_pkt_num = dis(randgen); + } else { + settings.initial_pkt_num = config.initial_pkt_num; + } + + if (!config.pmtud_probes.empty()) { + settings.pmtud_probes = config.pmtud_probes.data(); + settings.pmtud_probeslen = config.pmtud_probes.size(); + + if (!config.max_udp_payload_size) { + settings.max_tx_udp_payload_size = + *std::ranges::max_element(config.pmtud_probes); + } + } + + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + params.initial_max_stream_data_bidi_local = config.max_stream_data_bidi_local; + params.initial_max_stream_data_bidi_remote = + config.max_stream_data_bidi_remote; + params.initial_max_stream_data_uni = config.max_stream_data_uni; + params.initial_max_data = config.max_data; + params.initial_max_streams_bidi = config.max_streams_bidi; + params.initial_max_streams_uni = config.max_streams_uni; + params.max_idle_timeout = config.timeout; + params.stateless_reset_token_present = 1; + params.active_connection_id_limit = 7; + params.grease_quic_bit = 1; + + if (ocid) { + params.original_dcid = *ocid; + params.retry_scid = *scid; + params.retry_scid_present = 1; + } else { + params.original_dcid = *scid; + } + + params.original_dcid_present = 1; + + if (ngtcp2_crypto_generate_stateless_reset_token( + params.stateless_reset_token, config.static_secret.data(), + config.static_secret.size(), &scid_) != 0) { + return std::unexpected{Error::QUIC}; + } + + if (!config.preferred_ipv4_addr.empty() || + !config.preferred_ipv6_addr.empty()) { + params.preferred_addr_present = 1; + + if (!config.preferred_ipv4_addr.empty()) { + params.preferred_addr.ipv4 = + std::get(config.preferred_ipv4_addr.skaddr); + params.preferred_addr.ipv4_present = 1; + } + + if (!config.preferred_ipv6_addr.empty()) { + params.preferred_addr.ipv6 = + std::get(config.preferred_ipv6_addr.skaddr); + params.preferred_addr.ipv6_present = 1; + } + + if (auto rv = util::generate_secure_random( + params.preferred_addr.stateless_reset_token); + !rv) { + std::println( + stderr, "Could not generate preferred address stateless reset token"); + return rv; + } + + params.preferred_addr.cid.datalen = NGTCP2_SV_SCIDLEN; + if (auto rv = util::generate_secure_random( + {params.preferred_addr.cid.data, params.preferred_addr.cid.datalen}); + !rv) { + std::println(stderr, + "Could not generate preferred address connection ID"); + return rv; + } + } + + auto path = ngtcp2_path{ + .local = as_ngtcp2_addr(local_addr), + .remote = as_ngtcp2_addr(remote_addr), + .user_data = const_cast(&ep), + }; + if (auto rv = + ngtcp2_conn_server_new(&conn_, dcid, &scid_, &path, version, &callbacks, + &settings, ¶ms, nullptr, this); + rv != 0) { + std::println(stderr, "ngtcp2_conn_server_new: {}", ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + proto_codec_ = std::make_unique(this, last_error_); + + if (auto rv = tls_session_.init(tls_ctx, this); !rv) { + return rv; + } + + tls_session_.enable_keylog(); + + ngtcp2_conn_set_tls_native_handle(conn_, tls_session_.get_native_handle()); + + ev_io_set(&wev_, ep.fd, EV_WRITE); + + return {}; +} + +std::expected Handler::feed_data(const Endpoint &ep, + const Address &local_addr, + const Address &remote_addr, + const ngtcp2_pkt_info *pi, + std::span data) { + auto path = ngtcp2_path{ + .local = as_ngtcp2_addr(local_addr), + .remote = as_ngtcp2_addr(remote_addr), + .user_data = const_cast(&ep), + }; + + if (auto rv = ngtcp2_conn_read_pkt(conn_, &path, pi, data.data(), data.size(), + util::timestamp()); + rv != 0) { + std::println(stderr, "ngtcp2_conn_read_pkt: {}", ngtcp2_strerror(rv)); + switch (rv) { + case NGTCP2_ERR_DRAINING: + start_draining_period(); + return std::unexpected{Error::CLOSE_WAIT}; + case NGTCP2_ERR_RETRY: + return std::unexpected{Error::RETRY_CONN}; + case NGTCP2_ERR_DROP_CONN: + return std::unexpected{Error::DROP_CONN}; + case NGTCP2_ERR_CRYPTO: + if (!last_error_.error_code) { + ngtcp2_ccerr_set_tls_alert( + &last_error_, ngtcp2_conn_get_tls_alert2(conn_), nullptr, 0); + } + break; + default: + if (!last_error_.error_code) { + ngtcp2_ccerr_set_liberr(&last_error_, rv, nullptr, 0); + } + } + return handle_error(); + } + + return {}; +} + +std::expected Handler::on_read(const Endpoint &ep, + const Address &local_addr, + const Address &remote_addr, + const ngtcp2_pkt_info *pi, + std::span data) { + if (auto rv = feed_data(ep, local_addr, remote_addr, pi, data); !rv) { + return rv; + } + + update_timer(); + + return {}; +} + +std::expected Handler::handle_expiry() { + auto now = util::timestamp(); + if (auto rv = ngtcp2_conn_handle_expiry(conn_, now); rv != 0) { + std::println(stderr, "ngtcp2_conn_handle_expiry: {}", ngtcp2_strerror(rv)); + ngtcp2_ccerr_set_liberr(&last_error_, rv, nullptr, 0); + return handle_error(); + } + + return {}; +} + +std::expected Handler::on_write() { + if (ngtcp2_conn_in_closing_period2(conn_) || + ngtcp2_conn_in_draining_period2(conn_)) { + return {}; + } + + if (tx_.send_blocked) { + send_blocked_packet(); + + if (tx_.send_blocked) { + return {}; + } + } + + ev_io_stop(loop_, &wev_); + + if (auto rv = write_streams(); !rv) { + return rv; + } + + update_timer(); + + return {}; +} + +namespace { +ngtcp2_ssize write_pkt(ngtcp2_conn *conn, ngtcp2_path *path, + ngtcp2_pkt_info *pi, uint8_t *dest, size_t destlen, + ngtcp2_tstamp ts, void *user_data) { + auto h = static_cast(user_data); + + return h->write_pkt(path, pi, dest, destlen, ts); +} +} // namespace + +ngtcp2_ssize Handler::write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, + uint8_t *dest, size_t destlen, + ngtcp2_tstamp ts) { + return proto_codec_->write_pkt(path, pi, dest, destlen, ts); +} + +std::expected Handler::write_streams() { + ngtcp2_path_storage ps; + ngtcp2_pkt_info pi; + size_t gso_size; + auto ts = util::timestamp(); + auto txbuf = std::span{txbuf_}; + auto buflen = util::clamp_buffer_size(conn_, txbuf.size(), config.gso_burst); + + ngtcp2_path_storage_zero(&ps); + + auto nwrite = ngtcp2_conn_write_aggregate_pkt2( + conn_, &ps.path, &pi, txbuf.data(), buflen, &gso_size, ::write_pkt, + config.gso_burst, ts); + if (nwrite < 0) { + return handle_error(); + } + + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + + if (nwrite == 0) { + return {}; + } + + send_packet(ps.path, pi.ecn, txbuf.first(static_cast(nwrite)), + gso_size); + + return {}; +} + +std::expected Handler::send_packet(const ngtcp2_path &path, + unsigned int ecn, + std::span data, + size_t gso_size) { + auto &ep = *static_cast(path.user_data); + auto rest = server_->send_packet(ep, no_gso_, path.local, path.remote, ecn, + data, gso_size); + if (!rest.empty()) { + on_send_blocked(path, ecn, rest, gso_size); + + start_wev_endpoint(ep); + + return std::unexpected{Error::SEND_BLOCKED}; + } + + return {}; +} + +void Handler::on_send_blocked(const ngtcp2_path &path, unsigned int ecn, + std::span data, size_t gso_size) { + assert(!tx_.send_blocked); + assert(gso_size); + + tx_.send_blocked = true; + + auto &p = tx_.blocked; + + p.local_addr.set(path.local.addr); + p.remote_addr.set(path.remote.addr); + + p.endpoint = static_cast(path.user_data); + p.ecn = ecn; + p.data = data; + p.gso_size = gso_size; +} + +void Handler::start_wev_endpoint(const Endpoint &ep) { + // We do not close ep.fd, so we can expect that each Endpoint has + // unique fd. + if (ep.fd != wev_.fd) { + if (ev_is_active(&wev_)) { + ev_io_stop(loop_, &wev_); + } + + ev_io_set(&wev_, ep.fd, EV_WRITE); + } + + ev_io_start(loop_, &wev_); +} + +void Handler::send_blocked_packet() { + assert(tx_.send_blocked); + + auto &p = tx_.blocked; + + auto rest = server_->send_packet( + *p.endpoint, no_gso_, as_ngtcp2_addr(p.local_addr), + as_ngtcp2_addr(p.remote_addr), p.ecn, p.data, p.gso_size); + if (!rest.empty()) { + p.data = rest; + + start_wev_endpoint(*p.endpoint); + + return; + } + + tx_.send_blocked = false; +} + +void Handler::signal_write() { ev_io_start(loop_, &wev_); } + +void Handler::start_draining_period() { + ev_io_stop(loop_, &wev_); + + ev_set_cb(&timer_, close_waitcb); + timer_.repeat = + static_cast(ngtcp2_conn_get_pto2(conn_)) / NGTCP2_SECONDS * 3; + ev_timer_again(loop_, &timer_); + + if (!config.quiet) { + std::println(stderr, "Draining period has started ({:.9f} seconds)", + timer_.repeat); + } +} + +std::expected Handler::start_closing_period() { + if (!conn_ || ngtcp2_conn_in_closing_period2(conn_) || + ngtcp2_conn_in_draining_period2(conn_)) { + return {}; + } + + ev_io_stop(loop_, &wev_); + + ev_set_cb(&timer_, close_waitcb); + timer_.repeat = + static_cast(ngtcp2_conn_get_pto2(conn_)) / NGTCP2_SECONDS * 3; + ev_timer_again(loop_, &timer_); + + if (!config.quiet) { + std::println(stderr, "Closing period has started ({:.9f} seconds)", + timer_.repeat); + } + + conn_closebuf_ = std::make_unique(NGTCP2_MAX_UDP_PAYLOAD_SIZE); + + ngtcp2_path_storage ps; + + ngtcp2_path_storage_zero(&ps); + + ngtcp2_pkt_info pi; + auto n = ngtcp2_conn_write_connection_close( + conn_, &ps.path, &pi, conn_closebuf_->wpos(), conn_closebuf_->left(), + &last_error_, util::timestamp()); + if (n < 0) { + std::println(stderr, "ngtcp2_conn_write_connection_close: {}", + ngtcp2_strerror(static_cast(n))); + return std::unexpected{Error::QUIC}; + } + + if (n == 0) { + return {}; + } + + conn_closebuf_->push(as_unsigned(n)); + + return {}; +} + +std::expected Handler::handle_error() { + if (last_error_.type == NGTCP2_CCERR_TYPE_IDLE_CLOSE) { + return std::unexpected{Error::INTERNAL}; + } + + if (auto rv = start_closing_period(); !rv) { + return rv; + } + + if (ngtcp2_conn_in_draining_period2(conn_)) { + return std::unexpected{Error::CLOSE_WAIT}; + } + + if (auto rv = send_conn_close(); !rv) { + return rv; + } + + return std::unexpected{Error::CLOSE_WAIT}; +} + +std::expected Handler::send_conn_close() { + if (!config.quiet) { + std::println(stderr, "Closing Period: TX CONNECTION_CLOSE"); + } + + assert(conn_closebuf_ && conn_closebuf_->size()); + assert(conn_); + assert(!ngtcp2_conn_in_draining_period2(conn_)); + + auto path = ngtcp2_conn_get_path2(conn_); + + return server_->send_packet(*static_cast(path->user_data), + path->local, path->remote, + /* ecn = */ 0, conn_closebuf_->data()); +} + +std::expected +Handler::send_conn_close(const Endpoint &ep, const Address &local_addr, + const Address &remote_addr, const ngtcp2_pkt_info *pi, + std::span data) { + assert(conn_closebuf_ && conn_closebuf_->size()); + + close_wait_.bytes_recv += data.size(); + ++close_wait_.num_pkts_recv; + + if (close_wait_.num_pkts_recv < close_wait_.next_pkts_recv || + close_wait_.bytes_recv * 3 < + close_wait_.bytes_sent + conn_closebuf_->size()) { + return {}; + } + + if (auto rv = server_->send_packet(ep, as_ngtcp2_addr(local_addr), + as_ngtcp2_addr(remote_addr), + /* ecn = */ 0, conn_closebuf_->data()); + !rv) { + return rv; + } + + close_wait_.bytes_sent += conn_closebuf_->size(); + close_wait_.next_pkts_recv *= 2; + + return {}; +} + +void Handler::update_timer() { + auto expiry = ngtcp2_conn_get_expiry2(conn_); + auto now = util::timestamp(); + + if (expiry <= now) { + if (!config.quiet) { + auto t = static_cast(now - expiry) / NGTCP2_SECONDS; + std::println(stderr, "Timer has already expired: {:.9f}s", t); + } + + ev_feed_event(loop_, &timer_, EV_TIMER); + + return; + } + + auto t = static_cast(expiry - now) / NGTCP2_SECONDS; + if (!config.quiet) { + std::println(stderr, "Set timer={:.9f}s", t); + } + timer_.repeat = t; + ev_timer_again(loop_, &timer_); +} + +std::expected +Handler::recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_stream_data(stream_id, data); + } + + return proto_codec_->recv_stream_data(flags, stream_id, data); +} + +std::expected +Handler::update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen) { + auto crypto_ctx = ngtcp2_conn_get_crypto_ctx2(conn_); + auto aead = &crypto_ctx->aead; + auto keylen = ngtcp2_crypto_aead_keylen(aead); + auto ivlen = ngtcp2_crypto_packet_protection_ivlen(aead); + + ++nkey_update_; + + std::array rx_key, tx_key; + + if (ngtcp2_crypto_update_key(conn_, rx_secret, tx_secret, rx_aead_ctx, + rx_key.data(), rx_iv, tx_aead_ctx, tx_key.data(), + tx_iv, current_rx_secret, current_tx_secret, + secretlen) != 0) { + return std::unexpected{Error::QUIC}; + } + + if (!config.quiet && config.show_secret) { + std::println(stderr, "application_traffic rx secret {}", nkey_update_); + debug::print_secrets({rx_secret, secretlen}, {rx_key.data(), keylen}, + {rx_iv, ivlen}); + std::println(stderr, "application_traffic tx secret {}", nkey_update_); + debug::print_secrets({tx_secret, secretlen}, {tx_key.data(), keylen}, + {tx_iv, ivlen}); + } + + return {}; +} + +Server *Handler::server() const { return server_; } + +std::expected Handler::on_stream_close(int64_t stream_id, + uint64_t app_error_code) { + if (!config.quiet) { + std::println(stderr, "QUIC stream {:#x} closed", stream_id); + } + + if (auto rv = proto_codec_->on_stream_close(stream_id, app_error_code); !rv) { + return rv; + } + + if (!ngtcp2_conn_is_local_stream2(conn_, stream_id)) { + if (ngtcp2_is_bidi_stream(stream_id)) { + ngtcp2_conn_extend_max_streams_bidi(conn_, 1); + } + + // TODO We might later add uni stream extension here. + } + + auto it = streams_.find(stream_id); + if (it != std::ranges::end(streams_)) { + streams_.erase(it); + } + + return {}; +} + +void Handler::shutdown_read(int64_t stream_id, uint64_t app_error_code) { + ngtcp2_conn_shutdown_stream_read(conn_, 0, stream_id, app_error_code); +} + +namespace { +void sreadcb(struct ev_loop *loop, ev_io *w, int revents) { + auto ep = static_cast(w->data); + + ep->server->on_read(*ep); +} +} // namespace + +namespace { +void siginthandler(struct ev_loop *loop, ev_signal *watcher, int revents) { + ev_break(loop, EVBREAK_ALL); +} +} // namespace + +Server::Server(struct ev_loop *loop, TLSServerContext &tls_ctx) + : loop_{loop}, tls_ctx_{tls_ctx} { + ev_signal_init(&sigintev_, siginthandler, SIGINT); + + ev_timer_init( + &stateless_reset_regen_timer_, + [](struct ev_loop *loop, ev_timer *w, int revents) { + auto server = static_cast(w->data); + + server->on_stateless_reset_regen(); + }, + 0., 1.); + stateless_reset_regen_timer_.data = this; +} + +Server::~Server() { + disconnect(); + close(); +} + +void Server::disconnect() { + config.tx_loss_prob = 0; + + for (auto &ep : endpoints_) { + ev_io_stop(loop_, &ep.rev); + } + + ev_timer_stop(loop_, &stateless_reset_regen_timer_); + ev_signal_stop(loop_, &sigintev_); + + while (!handlers_.empty()) { + auto it = std::ranges::begin(handlers_); + auto &h = (*it).second; + + h->handle_error(); + + remove(h); + } +} + +void Server::close() { + for (auto &ep : endpoints_) { + ::close(ep.fd); + } + + endpoints_.clear(); +} + +namespace { +std::expected create_sock(Address &local_addr, const char *addr, + const char *port, int family) { + addrinfo hints{ + .ai_flags = AI_PASSIVE, + .ai_family = family, + .ai_socktype = SOCK_DGRAM, + }; + addrinfo *res, *rp; + int val = 1; + + if (strcmp(addr, "*") == 0) { + addr = nullptr; + } + + if (auto rv = getaddrinfo(addr, port, &hints, &res); rv != 0) { + std::println(stderr, "getaddrinfo: {}", gai_strerror(rv)); + return std::unexpected{Error::LIBC}; + } + + auto res_d = defer([res] { freeaddrinfo(res); }); + + int fd = -1; + + for (rp = res; rp; rp = rp->ai_next) { + auto maybe_fd = util::create_nonblock_socket(rp->ai_family, rp->ai_socktype, + rp->ai_protocol); + if (!maybe_fd) { + continue; + } + + fd = *maybe_fd; + + if (rp->ai_family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, + static_cast(sizeof(val))) == -1) { + close(fd); + continue; + } + + if (setsockopt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &val, + static_cast(sizeof(val))) == -1) { + close(fd); + continue; + } + } else if (setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &val, + static_cast(sizeof(val))) == -1) { + close(fd); + continue; + } + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, + static_cast(sizeof(val))) == -1) { + close(fd); + continue; + } + + fd_set_recv_ecn(fd, rp->ai_family); + fd_set_ip_mtu_discover(fd, rp->ai_family); + fd_set_ip_dontfrag(fd, family); + fd_set_udp_gro(fd); + + if (bind(fd, rp->ai_addr, rp->ai_addrlen) != -1) { + break; + } + + close(fd); + } + + if (!rp) { + std::println(stderr, "Could not bind"); + return std::unexpected{Error::SYSCALL}; + } + + sockaddr_storage ss; + socklen_t len = sizeof(ss); + if (getsockname(fd, reinterpret_cast(&ss), &len) == -1) { + std::println(stderr, "getsockname: {}", strerror(errno)); + close(fd); + return std::unexpected{Error::SYSCALL}; + } + + local_addr.set(reinterpret_cast(&ss)); + + return fd; +} + +} // namespace + +namespace { +std::expected add_endpoint(std::vector &endpoints, + const char *addr, const char *port, + int af) { + Address dest; + auto maybe_fd = create_sock(dest, addr, port, af); + if (!maybe_fd) { + return std::unexpected{maybe_fd.error()}; + } + + endpoints.emplace_back(); + auto &ep = endpoints.back(); + ep.addr = dest; + ep.fd = *maybe_fd; + ev_io_init(&ep.rev, sreadcb, 0, EV_READ); + ev_set_priority(&ep.rev, EV_MAXPRI); + + return {}; +} +} // namespace + +namespace { +std::expected add_endpoint(std::vector &endpoints, + const Address &addr) { + auto family = addr.family(); + + auto maybe_fd = util::create_nonblock_socket(family, SOCK_DGRAM, 0); + if (!maybe_fd) { + std::println(stderr, "socket: {}", strerror(errno)); + return std::unexpected{maybe_fd.error()}; + } + + auto fd = *maybe_fd; + + int val = 1; + if (family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, + static_cast(sizeof(val))) == -1) { + std::println(stderr, "setsockopt: {}", strerror(errno)); + close(fd); + return std::unexpected{Error::SYSCALL}; + } + + if (setsockopt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &val, + static_cast(sizeof(val))) == -1) { + std::println(stderr, "setsockopt: {}", strerror(errno)); + close(fd); + return std::unexpected{Error::SYSCALL}; + } + } else if (setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &val, + static_cast(sizeof(val))) == -1) { + std::println(stderr, "setsockopt: {}", strerror(errno)); + close(fd); + return std::unexpected{Error::SYSCALL}; + } + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, + static_cast(sizeof(val))) == -1) { + close(fd); + return std::unexpected{Error::SYSCALL}; + } + + fd_set_recv_ecn(fd, family); + fd_set_ip_mtu_discover(fd, family); + fd_set_ip_dontfrag(fd, family); + fd_set_udp_gro(fd); + + if (bind(fd, addr.as_sockaddr(), addr.size()) == -1) { + std::println(stderr, "bind: {}", strerror(errno)); + close(fd); + return std::unexpected{Error::SYSCALL}; + } + + endpoints.emplace_back(Endpoint{}); + auto &ep = endpoints.back(); + ep.addr = addr; + ep.fd = fd; + ev_io_init(&ep.rev, sreadcb, 0, EV_READ); + ev_set_priority(&ep.rev, EV_MAXPRI); + + return {}; +} +} // namespace + +std::expected Server::init(const char *addr, const char *port) { + endpoints_.reserve(4); + + auto ready = false; + auto error = Error::INTERNAL; + + if (!util::numeric_host(addr, AF_INET6)) { + if (auto rv = add_endpoint(endpoints_, addr, port, AF_INET); !rv) { + error = rv.error(); + } else { + ready = true; + } + } + if (!util::numeric_host(addr, AF_INET)) { + if (auto rv = add_endpoint(endpoints_, addr, port, AF_INET6); !rv) { + error = rv.error(); + } else { + ready = true; + } + } + if (!ready) { + return std::unexpected{error}; + } + + if (!config.preferred_ipv4_addr.empty()) { + if (auto rv = add_endpoint(endpoints_, config.preferred_ipv4_addr); !rv) { + return rv; + } + } + + if (!config.preferred_ipv6_addr.empty()) { + if (auto rv = add_endpoint(endpoints_, config.preferred_ipv6_addr); !rv) { + return rv; + } + } + + for (auto &ep : endpoints_) { + ep.server = this; + ep.rev.data = &ep; + + ev_io_set(&ep.rev, ep.fd, EV_READ); + + ev_io_start(loop_, &ep.rev); + } + + ev_signal_start(loop_, &sigintev_); + + return {}; +} + +void Server::on_read(const Endpoint &ep) { + sockaddr_storage ss; + std::array buf; + size_t pktcnt = 0; + ngtcp2_pkt_info pi; + + iovec msg_iov{ + .iov_base = buf.data(), + .iov_len = buf.size(), + }; + + uint8_t msg_ctrl[CMSG_SPACE(sizeof(int)) + CMSG_SPACE(sizeof(in6_pktinfo)) + + CMSG_SPACE(sizeof(int))]; + + msghdr msg{ + .msg_name = &ss, + .msg_iov = &msg_iov, + .msg_iovlen = 1, + .msg_control = msg_ctrl, + }; + + auto start = util::timestamp(); + + for (; pktcnt < MAX_RECV_PKTS;) { + if (util::recv_pkt_time_threshold_exceeded( + config.cc_algo == NGTCP2_CC_ALGO_BBR, start, pktcnt)) { + return; + } + + msg.msg_namelen = sizeof(ss); + msg.msg_controllen = sizeof(msg_ctrl); + + auto nread = recvmsg(ep.fd, &msg, 0); + if (nread == -1) { + if (!(errno == EAGAIN || errno == ENOTCONN)) { + std::println(stderr, "recvmsg: {}", strerror(errno)); + } + return; + } + + // Packets less than 21 bytes never be a valid QUIC packet. + if (nread < 21) { + ++pktcnt; + + continue; + } + + Address remote_addr; + remote_addr.set(reinterpret_cast(&ss)); + + if (util::prohibited_port(remote_addr.port())) { + ++pktcnt; + + continue; + } + + pi.ecn = msghdr_get_ecn(&msg, ss.ss_family); + auto local_addr = msghdr_get_local_addr(&msg, ss.ss_family); + if (!local_addr) { + ++pktcnt; + std::println(stderr, "Unable to obtain local address"); + continue; + } + + auto gso_size = msghdr_get_udp_gro(&msg); + if (gso_size == 0) { + gso_size = static_cast(nread); + } + + local_addr->port(ep.addr.port()); + + auto data = std::span{buf.data(), static_cast(nread)}; + + for (; !data.empty();) { + auto datalen = std::min(data.size(), gso_size); + + ++pktcnt; + + if (!config.quiet) { + std::array ifname; + std::println( + stderr, + "Received packet: local={} remote={} if={} ecn={:#x} {} bytes", + util::straddr(*local_addr), util::straddr(remote_addr), + if_indextoname(local_addr->ifindex, ifname.data()), pi.ecn, datalen); + } + + // Packets less than 21 bytes never be a valid QUIC packet. + if (datalen < 21) { + break; + } + + if (debug::packet_lost(config.rx_loss_prob)) { + if (!config.quiet) { + std::println(stderr, "** Simulated incoming packet loss **"); + } + } else { + read_pkt(ep, *local_addr, remote_addr, &pi, {data.data(), datalen}); + } + + data = data.subspan(datalen); + } + } +} + +void Server::read_pkt(const Endpoint &ep, const Address &local_addr, + const Address &remote_addr, const ngtcp2_pkt_info *pi, + std::span data) { + ngtcp2_version_cid vc; + + switch (auto rv = ngtcp2_pkt_decode_version_cid(&vc, data.data(), data.size(), + NGTCP2_SV_SCIDLEN); + rv) { + case 0: + break; + case NGTCP2_ERR_VERSION_NEGOTIATION: + send_version_negotiation(vc.version, {vc.scid, vc.scidlen}, + {vc.dcid, vc.dcidlen}, ep, local_addr, + remote_addr); + return; + default: + std::println(stderr, + "Could not decode version and CID from QUIC packet header: {}", + ngtcp2_strerror(rv)); + return; + } + + auto dcid_key = util::make_cid_key({vc.dcid, vc.dcidlen}); + + auto handler_it = handlers_.find(dcid_key); + if (handler_it == std::ranges::end(handlers_)) { + ngtcp2_pkt_hd hd; + + if (auto rv = ngtcp2_accept(&hd, data.data(), data.size()); rv != 0) { + if (!config.quiet) { + std::println(stderr, "Unexpected packet received: length={}", + data.size()); + } + + if (!(data[0] & 0x80) && data.size() >= NGTCP2_SV_SCIDLEN + 21) { + send_stateless_reset(data.size(), {vc.dcid, vc.dcidlen}, ep, local_addr, + remote_addr); + } + + return; + } + + ngtcp2_cid ocid; + ngtcp2_cid *pocid = nullptr; + ngtcp2_token_type token_type = NGTCP2_TOKEN_TYPE_UNKNOWN; + + assert(hd.type == NGTCP2_PKT_INITIAL); + + if (config.validate_addr || hd.tokenlen) { + std::println(stderr, "Perform stateless address validation"); + if (hd.tokenlen == 0) { + send_retry(&hd, ep, local_addr, remote_addr, data.size() * 3); + return; + } + + if (hd.token[0] != NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY2 && + hd.dcid.datalen < NGTCP2_MIN_INITIAL_DCIDLEN) { + send_stateless_connection_close(&hd, ep, local_addr, remote_addr); + return; + } + + switch (hd.token[0]) { + case NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY2: + if (auto rv = verify_retry_token(&ocid, &hd, remote_addr); !rv) { + if (rv.error() != Error::UNREADABLE_TOKEN || config.validate_addr) { + send_stateless_connection_close(&hd, ep, local_addr, remote_addr); + + return; + } + + hd.token = nullptr; + hd.tokenlen = 0; + } else { + pocid = &ocid; + token_type = NGTCP2_TOKEN_TYPE_RETRY; + } + + break; + case NGTCP2_CRYPTO_TOKEN_MAGIC_REGULAR: + if (!verify_token(&hd, remote_addr)) { + if (config.validate_addr) { + send_retry(&hd, ep, local_addr, remote_addr, data.size() * 3); + return; + } + + hd.token = nullptr; + hd.tokenlen = 0; + } else { + token_type = NGTCP2_TOKEN_TYPE_NEW_TOKEN; + } + break; + default: + if (!config.quiet) { + std::println(stderr, "Ignore unrecognized token"); + } + if (config.validate_addr) { + send_retry(&hd, ep, local_addr, remote_addr, data.size() * 3); + return; + } + + hd.token = nullptr; + hd.tokenlen = 0; + break; + } + } + + auto h = std::make_unique(loop_, this); + if (!h->init(ep, local_addr, remote_addr, &hd.scid, &hd.dcid, pocid, + {hd.token, hd.tokenlen}, token_type, hd.version, tls_ctx_)) { + return; + } + + if (auto rv = h->on_read(ep, local_addr, remote_addr, pi, data); !rv) { + if (rv.error() == Error::RETRY_CONN) { + send_retry(&hd, ep, local_addr, remote_addr, data.size() * 3); + } + + return; + } + + if (!h->on_write()) { + return; + } + + std::array scids; + auto conn = h->conn(); + + auto num_scid = ngtcp2_conn_get_scid2(conn, nullptr); + + assert(num_scid <= scids.size()); + + ngtcp2_conn_get_scid2(conn, scids.data()); + + for (size_t i = 0; i < num_scid; ++i) { + associate_cid(&scids[i], h.get()); + } + + handlers_.emplace(dcid_key, h.release()); + + return; + } + + auto h = (*handler_it).second; + auto conn = h->conn(); + if (ngtcp2_conn_in_closing_period2(conn)) { + if (!h->send_conn_close(ep, local_addr, remote_addr, pi, data)) { + remove(h); + } + return; + } + if (ngtcp2_conn_in_draining_period2(conn)) { + return; + } + + if (auto rv = h->on_read(ep, local_addr, remote_addr, pi, data); !rv) { + if (rv.error() != Error::CLOSE_WAIT) { + remove(h); + } + return; + } + + h->signal_write(); +} + +namespace { +uint32_t generate_reserved_version(const Address &addr, uint32_t version) { + uint32_t h = 0x811C9DC5U; + const uint8_t *p = reinterpret_cast(addr.as_sockaddr()); + const uint8_t *ep = p + addr.size(); + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193U; + } + version = htonl(version); + p = (const uint8_t *)&version; + ep = p + sizeof(version); + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193U; + } + h &= 0xF0F0F0F0U; + h |= 0x0A0A0A0AU; + return h; +} +} // namespace + +std::expected Server::send_version_negotiation( + uint32_t version, std::span dcid, + std::span scid, const Endpoint &ep, const Address &local_addr, + const Address &remote_addr) { + Buffer buf{NGTCP2_MAX_UDP_PAYLOAD_SIZE}; + std::array sv; + + auto p = std::ranges::begin(sv); + + *p++ = generate_reserved_version(remote_addr, version); + + if (config.preferred_versions.empty()) { + *p++ = NGTCP2_PROTO_VER_V1; + } else { + for (auto v : config.preferred_versions) { + *p++ = v; + } + } + + auto nwrite = ngtcp2_pkt_write_version_negotiation( + buf.wpos(), buf.left(), std::uniform_int_distribution()(randgen), + dcid.data(), dcid.size(), scid.data(), scid.size(), sv.data(), + as_unsigned(p - std::ranges::begin(sv))); + if (nwrite < 0) { + std::println(stderr, "ngtcp2_pkt_write_version_negotiation: {}", + ngtcp2_strerror(static_cast(nwrite))); + return std::unexpected{Error::QUIC}; + } + + buf.push(as_unsigned(nwrite)); + + return send_packet(ep, as_ngtcp2_addr(local_addr), + as_ngtcp2_addr(remote_addr), + /* ecn = */ 0, buf.data()); +} + +std::expected Server::send_retry(const ngtcp2_pkt_hd *chd, + const Endpoint &ep, + const Address &local_addr, + const Address &remote_addr, + size_t max_pktlen) { + std::array host; + std::array port; + + if (auto rv = getnameinfo(remote_addr.as_sockaddr(), remote_addr.size(), + host.data(), host.size(), port.data(), port.size(), + NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::println(stderr, "getnameinfo: {}", gai_strerror(rv)); + return std::unexpected{Error::LIBC}; + } + + if (!config.quiet) { + std::println(stderr, "Sending Retry packet to [{}]:{}", host.data(), + port.data()); + } + + ngtcp2_cid scid; + + scid.datalen = NGTCP2_SV_SCIDLEN; + if (auto rv = util::generate_secure_random({scid.data, scid.datalen}); !rv) { + return rv; + } + + std::array tokenbuf; + + auto t = util::system_clock_now(); + + auto tokenlen = ngtcp2_crypto_generate_retry_token2( + tokenbuf.data(), config.static_secret.data(), config.static_secret.size(), + chd->version, remote_addr.as_sockaddr(), remote_addr.size(), &scid, + &chd->dcid, t); + if (tokenlen < 0) { + return std::unexpected{Error::QUIC}; + } + + auto token = std::span{tokenbuf}.first(as_unsigned(tokenlen)); + + if (!config.quiet) { + std::println(stderr, "Generated address validation token:"); + util::hexdump(stderr, token); + } + + Buffer buf{ + std::min(static_cast(NGTCP2_MAX_UDP_PAYLOAD_SIZE), max_pktlen)}; + + auto nwrite = + ngtcp2_crypto_write_retry(buf.wpos(), buf.left(), chd->version, &chd->scid, + &scid, &chd->dcid, token.data(), token.size()); + if (nwrite < 0) { + std::println(stderr, "ngtcp2_crypto_write_retry failed"); + return std::unexpected{Error::QUIC}; + } + + buf.push(as_unsigned(nwrite)); + + return send_packet(ep, as_ngtcp2_addr(local_addr), + as_ngtcp2_addr(remote_addr), + /* ecn = */ 0, buf.data()); +} + +std::expected Server::send_stateless_connection_close( + const ngtcp2_pkt_hd *chd, const Endpoint &ep, const Address &local_addr, + const Address &remote_addr) { + Buffer buf{NGTCP2_MAX_UDP_PAYLOAD_SIZE}; + + auto nwrite = ngtcp2_crypto_write_connection_close( + buf.wpos(), buf.left(), chd->version, &chd->scid, &chd->dcid, + NGTCP2_INVALID_TOKEN, nullptr, 0); + if (nwrite < 0) { + std::println(stderr, "ngtcp2_crypto_write_connection_close failed"); + return std::unexpected{Error::QUIC}; + } + + buf.push(as_unsigned(nwrite)); + + return send_packet(ep, as_ngtcp2_addr(local_addr), + as_ngtcp2_addr(remote_addr), + /* ecn = */ 0, buf.data()); +} + +std::expected +Server::send_stateless_reset(size_t pktlen, std::span dcid, + const Endpoint &ep, const Address &local_addr, + const Address &remote_addr) { + if (stateless_reset_bucket_ == 0) { + return {}; + } + + --stateless_reset_bucket_; + + if (!ev_is_active(&stateless_reset_regen_timer_)) { + ev_timer_again(loop_, &stateless_reset_regen_timer_); + } + + ngtcp2_cid cid; + + ngtcp2_cid_init(&cid, dcid.data(), dcid.size()); + + ngtcp2_stateless_reset_token token; + + if (ngtcp2_crypto_generate_stateless_reset_token( + token.data, config.static_secret.data(), config.static_secret.size(), + &cid) != 0) { + return std::unexpected{Error::QUIC}; + } + + // SCID + minimum expansion - NGTCP2_STATELESS_RESET_TOKENLEN + constexpr size_t max_rand_byteslen = + NGTCP2_MAX_CIDLEN + 22 - NGTCP2_STATELESS_RESET_TOKENLEN; + + size_t rand_byteslen; + + if (pktlen <= 43) { + // As per + // https://datatracker.ietf.org/doc/html/rfc9000#section-10.3 + rand_byteslen = pktlen - NGTCP2_STATELESS_RESET_TOKENLEN - 1; + } else { + rand_byteslen = max_rand_byteslen; + } + + std::array rand_bytes; + + if (auto rv = + util::generate_secure_random({rand_bytes.data(), rand_byteslen}); + !rv) { + return rv; + } + + Buffer buf{NGTCP2_MAX_UDP_PAYLOAD_SIZE}; + + auto nwrite = ngtcp2_pkt_write_stateless_reset2( + buf.wpos(), buf.left(), &token, rand_bytes.data(), rand_byteslen); + if (nwrite < 0) { + std::println(stderr, "ngtcp2_pkt_write_stateless_reset2: {}", + ngtcp2_strerror(static_cast(nwrite))); + + return std::unexpected{Error::QUIC}; + } + + buf.push(as_unsigned(nwrite)); + + return send_packet(ep, as_ngtcp2_addr(local_addr), + as_ngtcp2_addr(remote_addr), + /* ecn = */ 0, buf.data()); +} + +std::expected +Server::verify_retry_token(ngtcp2_cid *ocid, const ngtcp2_pkt_hd *hd, + const Address &remote_addr) { + int rv; + + if (!config.quiet) { + std::array host; + std::array port; + + if (auto rv = getnameinfo(remote_addr.as_sockaddr(), remote_addr.size(), + host.data(), host.size(), port.data(), + port.size(), NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::println(stderr, "getnameinfo: {}", gai_strerror(rv)); + return std::unexpected{Error::LIBC}; + } + + std::println(stderr, "Verifying Retry token from [{}]:{}", host.data(), + port.data()); + util::hexdump(stderr, {hd->token, hd->tokenlen}); + } + + auto t = util::system_clock_now(); + + rv = ngtcp2_crypto_verify_retry_token2( + ocid, hd->token, hd->tokenlen, config.static_secret.data(), + config.static_secret.size(), hd->version, remote_addr.as_sockaddr(), + remote_addr.size(), &hd->dcid, 10 * NGTCP2_SECONDS, t); + switch (rv) { + case 0: + break; + case NGTCP2_CRYPTO_ERR_VERIFY_TOKEN: + std::println(stderr, "Could not verify Retry token"); + + return std::unexpected{Error::QUIC}; + default: + std::println(stderr, + "Could not read Retry token. Continue without the token"); + + return std::unexpected{Error::UNREADABLE_TOKEN}; + } + + if (!config.quiet) { + std::println(stderr, "Token was successfully validated"); + } + + return {}; +} + +std::expected Server::verify_token(const ngtcp2_pkt_hd *hd, + const Address &remote_addr) { + std::array host; + std::array port; + + if (auto rv = getnameinfo(remote_addr.as_sockaddr(), remote_addr.size(), + host.data(), host.size(), port.data(), port.size(), + NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::println(stderr, "getnameinfo: {}", gai_strerror(rv)); + return std::unexpected{Error::LIBC}; + } + + if (!config.quiet) { + std::println(stderr, "Verifying token from [{}]:{}", host.data(), + port.data()); + util::hexdump(stderr, {hd->token, hd->tokenlen}); + } + + auto t = util::system_clock_now(); + + if (ngtcp2_crypto_verify_regular_token( + hd->token, hd->tokenlen, config.static_secret.data(), + config.static_secret.size(), remote_addr.as_sockaddr(), + remote_addr.size(), 3600 * NGTCP2_SECONDS, t) != 0) { + std::println(stderr, "Could not verify token"); + + return std::unexpected{Error::QUIC}; + } + + if (!config.quiet) { + std::println(stderr, "Token was successfully validated"); + } + + return {}; +} + +std::expected Server::send_packet(const Endpoint &ep, + const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, + unsigned int ecn, + std::span data) { + auto no_gso = false; + auto rest = + send_packet(ep, no_gso, local_addr, remote_addr, ecn, data, data.size()); + if (!rest.empty()) { + return std::unexpected{Error::SEND_BLOCKED}; + } + + return {}; +} + +std::span Server::send_packet(const Endpoint &ep, bool &no_gso, + const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, + unsigned int ecn, + std::span data, + size_t gso_size) { + assert(gso_size); + + if (debug::packet_lost(config.tx_loss_prob)) { + if (!config.quiet) { + std::println(stderr, "** Simulated outgoing packet loss **"); + } + return {}; + } + + if (no_gso && data.size() > gso_size) { + for (; !data.empty();) { + auto len = std::min(gso_size, data.size()); + + auto rest = send_packet(ep, no_gso, local_addr, remote_addr, ecn, + data.first(len), len); + if (!rest.empty()) { + assert(rest.size() == len); + + return data; + } + + data = data.subspan(len); + } + + return {}; + } + + iovec msg_iov{ + .iov_base = const_cast(data.data()), + .iov_len = data.size(), + }; + + uint8_t msg_ctrl[CMSG_SPACE(sizeof(int)) + CMSG_SPACE(sizeof(uint16_t)) + + CMSG_SPACE(sizeof(in6_pktinfo))]{}; + + msghdr msg{ + .msg_name = const_cast(remote_addr.addr), + .msg_namelen = remote_addr.addrlen, + .msg_iov = &msg_iov, + .msg_iovlen = 1, + .msg_control = msg_ctrl, + .msg_controllen = sizeof(msg_ctrl), + }; + + size_t controllen = 0; + + auto cm = CMSG_FIRSTHDR(&msg); + + switch (local_addr.addr->sa_family) { + case AF_INET: { + controllen += CMSG_SPACE(sizeof(in_pktinfo)); + cm->cmsg_level = IPPROTO_IP; + cm->cmsg_type = IP_PKTINFO; + cm->cmsg_len = CMSG_LEN(sizeof(in_pktinfo)); + auto addrin = reinterpret_cast(local_addr.addr); + in_pktinfo pktinfo{ + .ipi_spec_dst = addrin->sin_addr, + }; + memcpy(CMSG_DATA(cm), &pktinfo, sizeof(pktinfo)); + + break; + } + case AF_INET6: { + controllen += CMSG_SPACE(sizeof(in6_pktinfo)); + cm->cmsg_level = IPPROTO_IPV6; + cm->cmsg_type = IPV6_PKTINFO; + cm->cmsg_len = CMSG_LEN(sizeof(in6_pktinfo)); + auto addrin = reinterpret_cast(local_addr.addr); + in6_pktinfo pktinfo{ + .ipi6_addr = addrin->sin6_addr, + }; + memcpy(CMSG_DATA(cm), &pktinfo, sizeof(pktinfo)); + + break; + } + default: + assert(0); + } + +#ifdef UDP_SEGMENT + if (data.size() > gso_size) { + controllen += CMSG_SPACE(sizeof(uint16_t)); + cm = CMSG_NXTHDR(&msg, cm); + cm->cmsg_level = SOL_UDP; + cm->cmsg_type = UDP_SEGMENT; + cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); + auto n = static_cast(gso_size); + memcpy(CMSG_DATA(cm), &n, sizeof(n)); + } +#endif // defined(UDP_SEGMENT) + + controllen += CMSG_SPACE(sizeof(int)); + cm = CMSG_NXTHDR(&msg, cm); + cm->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(cm), &ecn, sizeof(ecn)); + + switch (local_addr.addr->sa_family) { + case AF_INET: + cm->cmsg_level = IPPROTO_IP; + cm->cmsg_type = IP_TOS; + + break; + case AF_INET6: + cm->cmsg_level = IPPROTO_IPV6; + cm->cmsg_type = IPV6_TCLASS; + + break; + default: + assert(0); + } + + msg.msg_controllen = +#ifndef __APPLE__ + controllen +#else // defined(__APPLE__) + static_cast(controllen) +#endif // defined(__APPLE__) + ; + + ssize_t nwrite = 0; + + do { + nwrite = sendmsg(ep.fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + switch (errno) { + case EAGAIN: +#if EAGAIN != EWOULDBLOCK + case EWOULDBLOCK: +#endif // EAGAIN != EWOULDBLOCK + return data; +#ifdef UDP_SEGMENT + case EIO: + if (data.size() > gso_size) { + // GSO failure; send each packet in a separate sendmsg call. + std::println(stderr, "sendmsg: disabling GSO due to {}", + strerror(errno)); + + no_gso = true; + + return send_packet(ep, no_gso, local_addr, remote_addr, ecn, data, + gso_size); + } + break; +#endif // defined(UDP_SEGMENT) + } + + std::println(stderr, "sendmsg: {}", strerror(errno)); + // TODO We have packet which is expected to fail to send (e.g., + // path validation to old path). + return {}; + } + + if (!config.quiet) { + std::println(stderr, "Sent packet: local={} remote={} ecn={:#x} {} bytes", + util::straddr(local_addr.addr, local_addr.addrlen), + util::straddr(remote_addr.addr, remote_addr.addrlen), ecn, + nwrite); + } + + return {}; +} + +void Server::associate_cid(const ngtcp2_cid *cid, Handler *h) { + handlers_.emplace(*cid, h); +} + +void Server::dissociate_cid(const ngtcp2_cid *cid) { handlers_.erase(*cid); } + +void Server::remove(const Handler *h) { + auto conn = h->conn(); + + dissociate_cid(ngtcp2_conn_get_client_initial_dcid2(conn)); + + std::vector cids(ngtcp2_conn_get_scid2(conn, nullptr)); + ngtcp2_conn_get_scid2(conn, cids.data()); + + for (auto &cid : cids) { + dissociate_cid(&cid); + } + + delete h; +} + +void Server::on_stateless_reset_regen() { + assert(stateless_reset_bucket_ < NGTCP2_STATELESS_RESET_BURST); + + if (++stateless_reset_bucket_ == NGTCP2_STATELESS_RESET_BURST) { + ev_timer_stop(loop_, &stateless_reset_regen_timer_); + } +} + +namespace { +std::expected parse_host_port(int af, + std::string_view host_port) { + if (host_port.empty()) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + auto first = std::ranges::begin(host_port); + auto last = std::ranges::end(host_port); + + std::string_view hostv; + + if (*first == '[') { + ++first; + + auto it = std::ranges::find(first, last, ']'); + if (it == last) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + hostv = std::string_view{first, it}; + first = it + 1; + + if (first == last || *first != ':') { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + } else { + auto it = std::ranges::find(first, last, ':'); + if (it == last) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + hostv = std::string_view{first, it}; + first = it; + } + + if (++first == last) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + std::array host; + *std::ranges::copy(hostv, std::ranges::begin(host)).out = '\0'; + + addrinfo hints{ + .ai_family = af, + .ai_socktype = SOCK_DGRAM, + }; + addrinfo *res; + auto svc = first; + + if (auto rv = getaddrinfo(host.data(), svc, &hints, &res); rv != 0) { + std::println(stderr, "getaddrinfo: [{}]:{}: {}", host.data(), svc, + gai_strerror(rv)); + return std::unexpected{Error::LIBC}; + } + + Address dest; + dest.set(res->ai_addr); + + freeaddrinfo(res); + + return dest; +} +} // namespace + +namespace { +const char *prog = "server"; +} // namespace + +namespace { +void print_usage(FILE *out) { + std::println( + out, + "Usage: {} [OPTIONS] ", + prog); +} +} // namespace + +namespace { +void print_help() { + print_usage(stdout); + + Config config; + + std::cout << R"( + Address to listen to. '*' binds to any address. + Port + + Path to private key file + + Path to certificate file +Options: + -t, --tx-loss=

+ The probability of losing outgoing packets.

must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + -r, --rx-loss=

+ The probability of losing incoming packets.

must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + --ciphers= + Specify the cipher suite list to enable. + Default: )" + << config.ciphers << R"( + --groups= + Specify the supported groups. + Default: )" + << config.groups << R"( + -d, --htdocs= + Specify document root. If this option is not specified, + the document root is the current working directory. + -q, --quiet Suppress debug output. + -s, --show-secret + Print out secrets unless --quiet is used. + --timeout= + Specify idle timeout. + Default: )" + << util::format_duration(config.timeout) << R"( + -V, --validate-addr + Perform address validation. + --preferred-ipv4-addr=: + Specify preferred IPv4 address and port. + --preferred-ipv6-addr=: + Specify preferred IPv6 address and port. A numeric IPv6 + address must be enclosed by '[' and ']' (e.g., + [::1]:8443) + --mime-types-file= + Path to file that contains MIME media types and the + extensions. + Default: )" + << config.mime_types_file.native() << R"( + --early-response + Start sending response when it receives HTTP header + fields without waiting for request body. If HTTP + response data is written before receiving request body, + STOP_SENDING is sent. + --verify-client + Request a client certificate. At the moment, we just + request a certificate and no verification is done. + --qlog-dir= + Path to the directory where qlog file is stored. The + file name of each qlog is the Source Connection ID of + server. + --no-quic-dump + Disables printing QUIC STREAM and CRYPTO frame data out. + --no-http-dump + Disables printing HTTP response body out. + --max-data= + The initial connection-level flow control window. + Default: )" + << util::format_uint_iec(config.max_data) << R"( + --max-stream-data-bidi-local= + The initial stream-level flow control window for a + bidirectional stream that the local endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_local) << R"( + --max-stream-data-bidi-remote= + The initial stream-level flow control window for a + bidirectional stream that the remote endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_remote) << R"( + --max-stream-data-uni= + The initial stream-level flow control window for a + unidirectional stream. + Default: )" + << util::format_uint_iec(config.max_stream_data_uni) << R"( + --max-streams-bidi= + The number of the concurrent bidirectional streams that + the remote endpoint initiates. + Default: )" + << config.max_streams_bidi << R"( + --max-streams-uni= + The number of the concurrent unidirectional streams that + the remote endpoint initiates. + Default: )" + << config.max_streams_uni << R"( + --max-dyn-length= + The maximum length of a dynamically generated content. + Default: )" + << util::format_uint_iec(config.max_dyn_length) << R"( + --cc=(cubic|reno|bbr) + The name of congestion controller algorithm. + Default: )" + << util::strccalgo(config.cc_algo) << R"( + --initial-rtt= + Set an initial RTT. + Default: )" + << util::format_duration(config.initial_rtt) << R"( + --max-udp-payload-size= + Override maximum UDP payload size that server transmits. + With this option, server assumes that a path supports + byte of UDP datagram payload, without performing + Path MTU Discovery. + --send-trailers + Send trailer fields. + --max-window= + Maximum connection-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_window) << R"( + --max-stream-window= + Maximum stream-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_stream_window) << R"( + --handshake-timeout= + Set the QUIC handshake timeout. It defaults to no + timeout. + --preferred-versions=[[,]...] + Specify QUIC versions in hex string in the order of + preference. Server negotiates one of those versions if + client initially selects a less preferred version. + These versions must be supported by libngtcp2. Instead + of specifying hex string, there are special aliases + available: "v1" indicates QUIC v1, and "v2" indicates + QUIC v2. + --available-versions=[[,]...] + Specify QUIC versions in hex string that are sent in + available_versions field of version_information + transport parameter. This list can include a version + which is not supported by libngtcp2. Instead of + specifying hex string, there are special aliases + available: "v1" indicates QUIC v1, and "v2" indicates + QUIC v2. + --no-pmtud Disables Path MTU Discovery. + --ack-thresh= + The minimum number of the received ACK eliciting packets + that triggers immediate acknowledgement. + Default: )" + << config.ack_thresh << R"( + --initial-pkt-num= + The initial packet number that is used for each packet + number space. It must be in range [0, (1 << 31) - 1], + inclusive. By default, the initial packet number is + chosen randomly. + --pmtud-probes=[[,]...] + Specify UDP datagram payload sizes to probe in Path MTU + Discovery. must be strictly larger than 1200. + --ech-config-file= + Read private key and ECHConfig from . The file + denoted by must contain private key and + ECHConfigList as described in RFC 9934. ECH + configuration is only applied if an underlying TLS stack + supports it. + --origin= + Specify the origin to send in ORIGIN frame. Repeat to + add multiple origins. + --no-gso Disables GSO. + --show-stat Print the connection statistics when the connection is + closed. + --gso-burst= + The maximum number of packets to aggregate for GSO. If + GSO is disabled, this is the maximum number of packets + to send per an event loop in a single connection. It + defaults to 0, which means it is not limited by the + configuration. + -h, --help Display this help and exit. + +--- + + The argument is an integer and an optional unit (e.g., 10K is + 10 * 1024). Units are K, M and G (powers of 1024). + + The argument is an integer and an optional unit (e.g., 1s + is 1 second and 500ms is 500 milliseconds). Units are h, m, s, ms, + us, or ns (hours, minutes, seconds, milliseconds, microseconds, and + nanoseconds respectively). If a unit is omitted, a second is used + as unit. + + The argument is an hex string which must start with "0x" + (e.g., 0x00000001).)" + << std::endl; +} +} // namespace + +std::ofstream keylog_file; + +int main(int argc, char **argv) { + if (argc) { + prog = basename(argv[0]); + } + + std::filesystem::path ech_config_file; + + for (;;) { + static int flag = 0; + static constexpr option long_opts[] = { + {"help", no_argument, nullptr, 'h'}, + {"tx-loss", required_argument, nullptr, 't'}, + {"rx-loss", required_argument, nullptr, 'r'}, + {"htdocs", required_argument, nullptr, 'd'}, + {"quiet", no_argument, nullptr, 'q'}, + {"show-secret", no_argument, nullptr, 's'}, + {"validate-addr", no_argument, nullptr, 'V'}, + {"ciphers", required_argument, &flag, 1}, + {"groups", required_argument, &flag, 2}, + {"timeout", required_argument, &flag, 3}, + {"preferred-ipv4-addr", required_argument, &flag, 4}, + {"preferred-ipv6-addr", required_argument, &flag, 5}, + {"mime-types-file", required_argument, &flag, 6}, + {"early-response", no_argument, &flag, 7}, + {"verify-client", no_argument, &flag, 8}, + {"qlog-dir", required_argument, &flag, 9}, + {"no-quic-dump", no_argument, &flag, 10}, + {"no-http-dump", no_argument, &flag, 11}, + {"max-data", required_argument, &flag, 12}, + {"max-stream-data-bidi-local", required_argument, &flag, 13}, + {"max-stream-data-bidi-remote", required_argument, &flag, 14}, + {"max-stream-data-uni", required_argument, &flag, 15}, + {"max-streams-bidi", required_argument, &flag, 16}, + {"max-streams-uni", required_argument, &flag, 17}, + {"max-dyn-length", required_argument, &flag, 18}, + {"cc", required_argument, &flag, 19}, + {"initial-rtt", required_argument, &flag, 20}, + {"max-udp-payload-size", required_argument, &flag, 21}, + {"send-trailers", no_argument, &flag, 22}, + {"max-window", required_argument, &flag, 23}, + {"max-stream-window", required_argument, &flag, 24}, + {"handshake-timeout", required_argument, &flag, 26}, + {"preferred-versions", required_argument, &flag, 27}, + {"available-versions", required_argument, &flag, 28}, + {"no-pmtud", no_argument, &flag, 29}, + {"ack-thresh", required_argument, &flag, 30}, + {"initial-pkt-num", required_argument, &flag, 31}, + {"pmtud-probes", required_argument, &flag, 32}, + {"ech-config-file", required_argument, &flag, 33}, + {"origin", required_argument, &flag, 34}, + {"no-gso", no_argument, &flag, 35}, + {"show-stat", no_argument, &flag, 36}, + {"gso-burst", required_argument, &flag, 37}, + {}, + }; + + auto optidx = 0; + auto c = getopt_long(argc, argv, "d:hqr:st:V", long_opts, &optidx); + if (c == -1) { + break; + } + switch (c) { + case 'd': { + // --htdocs + auto path = realpath(optarg, nullptr); + if (path == nullptr) { + std::println(stderr, "path: invalid path {}", optarg); + exit(EXIT_FAILURE); + } + config.htdocs = path; + free(path); + break; + } + case 'h': + // --help + print_help(); + exit(EXIT_SUCCESS); + case 'q': + // --quiet + config.quiet = true; + break; + case 'r': + // --rx-loss + config.rx_loss_prob = strtod(optarg, nullptr); + break; + case 's': + // --show-secret + config.show_secret = true; + break; + case 't': + // --tx-loss + config.tx_loss_prob = strtod(optarg, nullptr); + break; + case 'V': + // --validate-addr + config.validate_addr = true; + break; + case '?': + print_usage(stderr); + exit(EXIT_FAILURE); + case 0: + switch (flag) { + case 1: + // --ciphers + if (util::crypto_default_ciphers()[0] == '\0') { + std::println(stderr, "ciphers: not supported"); + exit(EXIT_FAILURE); + } + config.ciphers = optarg; + break; + case 2: + // --groups + config.groups = optarg; + break; + case 3: + // --timeout + if (auto t = util::parse_duration(optarg); !t) { + std::println(stderr, "timeout: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.timeout = *t; + } + break; + case 4: { + // --preferred-ipv4-addr + auto maybe_addr = parse_host_port(AF_INET, optarg); + if (!maybe_addr) { + std::println(stderr, "preferred-ipv4-addr: could not use {}", optarg); + exit(EXIT_FAILURE); + } + + config.preferred_ipv4_addr = *maybe_addr; + + break; + } + case 5: { + // --preferred-ipv6-addr + auto maybe_addr = parse_host_port(AF_INET6, optarg); + if (!maybe_addr) { + std::println(stderr, "preferred-ipv6-addr: could not use {}", optarg); + exit(EXIT_FAILURE); + } + + config.preferred_ipv6_addr = *maybe_addr; + + break; + } + case 6: + // --mime-types-file + config.mime_types_file = optarg; + break; + case 7: + // --early-response + config.early_response = true; + break; + case 8: + // --verify-client + config.verify_client = true; + break; + case 9: + // --qlog-dir + config.qlog_dir = optarg; + break; + case 10: + // --no-quic-dump + config.no_quic_dump = true; + break; + case 11: + // --no-http-dump + config.no_http_dump = true; + break; + case 12: + // --max-data + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-data: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_data = *n; + } + break; + case 13: + // --max-stream-data-bidi-local + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-stream-data-bidi-local: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_local = *n; + } + break; + case 14: + // --max-stream-data-bidi-remote + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-stream-data-bidi-remote: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_remote = *n; + } + break; + case 15: + // --max-stream-data-uni + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-stream-data-uni: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_stream_data_uni = *n; + } + break; + case 16: + // --max-streams-bidi + if (auto n = util::parse_uint(optarg); !n) { + std::println(stderr, "max-streams-bidi: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_streams_bidi = *n; + } + break; + case 17: + // --max-streams-uni + if (auto n = util::parse_uint(optarg); !n) { + std::println(stderr, "max-streams-uni: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_streams_uni = *n; + } + break; + case 18: + // --max-dyn-length + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-dyn-length: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_dyn_length = *n; + } + break; + case 19: { + // --cc + auto cc = std::string_view{optarg}; + if (cc == "cubic"sv) { + config.cc_algo = NGTCP2_CC_ALGO_CUBIC; + break; + } + if (cc == "reno"sv) { + config.cc_algo = NGTCP2_CC_ALGO_RENO; + break; + } + if (cc == "bbr"sv) { + config.cc_algo = NGTCP2_CC_ALGO_BBR; + break; + } + std::println(stderr, "cc: specify cubic, reno, or bbr"); + exit(EXIT_FAILURE); + } + case 20: + // --initial-rtt + if (auto t = util::parse_duration(optarg); !t) { + std::println(stderr, "initial-rtt: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.initial_rtt = *t; + } + break; + case 21: + // --max-udp-payload-size + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-udp-payload-size: invalid argument"); + exit(EXIT_FAILURE); + } else if (*n > NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE) { + std::println(stderr, "max-udp-payload-size: must not exceed {}", + NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE); + exit(EXIT_FAILURE); + } else { + config.max_udp_payload_size = *n; + } + break; + case 22: + // --send-trailers + config.send_trailers = true; + break; + case 23: + // --max-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-window: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_window = *n; + } + break; + case 24: + // --max-stream-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::println(stderr, "max-stream-window: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.max_stream_window = *n; + } + break; + case 26: + // --handshake-timeout + if (auto t = util::parse_duration(optarg); !t) { + std::println(stderr, "handshake-timeout: invalid argument"); + exit(EXIT_FAILURE); + } else { + config.handshake_timeout = *t; + } + break; + case 27: + // --preferred-versions + if (strlen(optarg) == 0) { + config.preferred_versions.resize(0); + + break; + } + + config.preferred_versions = + util::split_str(optarg) | std::ranges::views::transform([](auto &&k) { + if (k == "v1"sv) { + return NGTCP2_PROTO_VER_V1; + } + + if (k == "v2"sv) { + return NGTCP2_PROTO_VER_V2; + } + + auto rv = util::parse_version(k); + if (!rv) { + std::println(stderr, "preferred-versions: invalid version {}", k); + exit(EXIT_FAILURE); + } + + if (!ngtcp2_is_supported_version(*rv)) { + std::println(stderr, "preferred-versions: unsupported version {}", + k); + exit(EXIT_FAILURE); + } + + return *rv; + }) | + std::ranges::to(); + + if (config.preferred_versions.size() > max_preferred_versionslen) { + std::println(stderr, "preferred-versions: too many versions > {}", + max_preferred_versionslen); + exit(EXIT_FAILURE); + } + + break; + case 28: + // --available-versions + if (strlen(optarg) == 0) { + config.available_versions.resize(0); + + break; + } + + config.available_versions = + util::split_str(optarg) | std::ranges::views::transform([](auto &&k) { + if (k == "v1"sv) { + return NGTCP2_PROTO_VER_V1; + } + + if (k == "v2"sv) { + return NGTCP2_PROTO_VER_V2; + } + + auto rv = util::parse_version(k); + if (!rv) { + std::println(stderr, "available-versions: invalid version {}", k); + exit(EXIT_FAILURE); + } + + return *rv; + }) | + std::ranges::to(); + + break; + case 29: + // --no-pmtud + config.no_pmtud = true; + break; + case 30: + // --ack-thresh + if (auto n = util::parse_uint(optarg); !n) { + std::println(stderr, "ack-thresh: invalid argument"); + exit(EXIT_FAILURE); + } else if (*n > 100) { + std::println(stderr, "ack-thresh: must not exceed 100"); + exit(EXIT_FAILURE); + } else { + config.ack_thresh = *n; + } + break; + case 31: + // --initial-pkt-num + if (auto n = util::parse_uint(optarg); !n) { + std::println(stderr, "initial-pkt-num: invalid argument"); + exit(EXIT_FAILURE); + } else if (*n > INT32_MAX) { + std::println(stderr, + "initial-pkt-num: must not exceed (1 << 31) - 1"); + exit(EXIT_FAILURE); + } else { + config.initial_pkt_num = static_cast(*n); + } + break; + case 32: + // --pmtud-probes + if (strlen(optarg) == 0) { + config.pmtud_probes.resize(0); + + break; + } + + config.pmtud_probes = + util::split_str(optarg) | std::ranges::views::transform([](auto &&s) { + auto n = util::parse_uint_iec(s); + if (!n) { + std::println(stderr, "pmtud-probes: invalid argument"); + exit(EXIT_FAILURE); + } + + if (*n <= NGTCP2_MAX_UDP_PAYLOAD_SIZE || + *n > NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE) { + std::println( + stderr, "pmtud-probes: must be in range [{}, {}], inclusive.", + NGTCP2_MAX_UDP_PAYLOAD_SIZE + 1, + NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE); + exit(EXIT_FAILURE); + } + + return static_cast(*n); + }) | + std::ranges::to(); + + break; + case 33: + // --ech-config-file + ech_config_file = optarg; + break; + case 34: { + // --origin + auto origin = std::string_view{optarg}; + + if (auto max = std::numeric_limits::max(); + max < origin.size()) { + std::println(stderr, "origin: must be less than or equal to {}", max); + exit(EXIT_FAILURE); + } + + if (!config.origin_list) { + config.origin_list = std::vector(); + } + + config.origin_list->push_back(static_cast(origin.size() >> 8)); + config.origin_list->push_back(origin.size() & 0xFF); + std::ranges::copy(origin, std::back_inserter(*config.origin_list)); + + break; + } + case 35: + // --no-gso + config.no_gso = true; + break; + case 36: + // --show-stat + config.show_stat = true; + break; + case 37: { + // --gso-burst + auto n = util::parse_uint(optarg); + if (!n) { + std::println(stderr, "gso-burst: invalid argument"); + exit(EXIT_FAILURE); + } + + if (*n > 64) { + std::println(stderr, + "gso-burst: must be in range [0, 64], inclusive."); + exit(EXIT_FAILURE); + } + + config.gso_burst = static_cast(*n); + + break; + } + } + break; + default: + break; + } + } + + if (argc - optind < 4) { + std::println(stderr, "Too few arguments"); + print_usage(stderr); + exit(EXIT_FAILURE); + } + + auto addr = argv[optind++]; + auto port = argv[optind++]; + auto private_key_file = argv[optind++]; + auto cert_file = argv[optind++]; + + if (auto n = util::parse_uint(port); !n) { + std::println(stderr, "port: invalid port number"); + exit(EXIT_FAILURE); + } else if (*n > 65535) { + std::println(stderr, "port: must not exceed 65535"); + exit(EXIT_FAILURE); + } else { + config.port = static_cast(*n); + } + + if (auto mt = util::read_mime_types(config.mime_types_file); !mt) { + std::println(stderr, + "mime-types-file: Could not read MIME media types file {}", + config.mime_types_file.native()); + } else { + config.mime_types = std::move(*mt); + } + + if (!ech_config_file.empty()) { + auto ech_config = util::read_ech_server_config(ech_config_file); + if (!ech_config) { + std::println(stderr, + "ech-config-file: Could not read private key and ECHConfig"); + exit(EXIT_FAILURE); + } + + config.ech_config = std::move(*ech_config); + } + + TLSServerContext tls_ctx; + + if (!tls_ctx.init(private_key_file, cert_file, ProtoCodec::protocol)) { + exit(EXIT_FAILURE); + } + + std::println(stderr, "Using document root {}", config.htdocs.native()); + + auto ev_loop_d = defer([] { ev_loop_destroy(EV_DEFAULT); }); + + auto keylog_filename = getenv("SSLKEYLOGFILE"); + if (keylog_filename) { + keylog_file.open(keylog_filename, std::ios_base::app); + if (keylog_file) { + tls_ctx.enable_keylog(); + } + } + + if (!util::generate_secure_random(config.static_secret)) { + std::println(stderr, "Unable to generate static secret"); + exit(EXIT_FAILURE); + } + + Server s(EV_DEFAULT, tls_ctx); + if (!s.init(addr, port)) { + exit(EXIT_FAILURE); + } + + ev_run(EV_DEFAULT, 0); + + s.disconnect(); + s.close(); + + return EXIT_SUCCESS; +} diff --git a/deps/ngtcp2/ngtcp2/examples/server.h b/deps/ngtcp2/ngtcp2/examples/server.h new file mode 100644 index 00000000000000..ea3162a47846f0 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/server.h @@ -0,0 +1,317 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SERVER_H +#define SERVER_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "server_base.h" +#include "tls_server_context.h" +#include "network.h" +#include "shared.h" +#include "util.h" + +#ifdef WITH_EXAMPLE_HTTP3_PROTO_CODEC +# include "http3_server_proto_codec.h" +#endif // WITH_EXAMPLE_HTTP3_PROTO_CODEC + +#ifdef WITH_EXAMPLE_HQ_PROTO_CODEC +# include + +# include "hq_server_proto_codec.h" +#endif // WITH_EXAMPLE_HQ_PROTO_CODEC + +using namespace ngtcp2; + +class Handler; + +enum FileEntryFlag { + FILE_ENTRY_TYPE_DIR = 0x1, +}; + +struct FileEntry { + uint64_t len{}; + void *map{}; + int fd{}; + uint8_t flags{}; +}; + +std::string make_status_body(unsigned int status_code); + +struct Request { + std::string path; + struct { + int32_t urgency; + int inc; + } pri{}; +}; + +struct Stream { + Stream(int64_t stream_id, Handler *handler); + + std::expected start_response(); + std::expected open_file(const std::filesystem::path &path); + void map_file(const FileEntry &fe); + std::expected + send_status_response(ProtoCodec *pc, unsigned int status_code, + const std::vector &extra_headers = {}); + std::expected send_redirect_response(ProtoCodec *pc, + unsigned int status_code, + std::string_view path); + std::expected find_dyn_length(std::string_view path); + void http_acked_stream_data(uint64_t datalen); + std::expected request_path(); + + int64_t stream_id; + Handler *handler; + // uri is request uri/path. + std::string uri; + std::string method; + std::string authority; + std::string status_resp_body; + // resp_data is a pointer to the response data. It might be the + // memory which maps file denoted by fd, or status_resp_body. + std::span resp_data; + // dynresp is true if dynamic data response is enabled. + bool dynresp{}; + // dyndataleft is the number of dynamic data left to send. + uint64_t dyndataleft{}; + // dynbuflen is the number of bytes in-flight. + uint64_t dynbuflen{}; +#ifdef WITH_EXAMPLE_HQ_PROTO_CODEC + http_parser htp; + // eos gets true when one HTTP request message is seen. + bool eos{}; +#endif // WITH_EXAMPLE_HQ_PROTO_CODEC +}; + +class Server; + +// Endpoint is a local endpoint. +struct Endpoint { + Address addr; + ev_io rev; + Server *server{}; + int fd{}; +}; + +class Handler : public HandlerBase { +public: + Handler(struct ev_loop *loop, Server *server); + ~Handler(); + + std::expected + init(const Endpoint &ep, const Address &local_addr, + const Address &remote_addr, const ngtcp2_cid *dcid, + const ngtcp2_cid *scid, const ngtcp2_cid *ocid, + std::span token, ngtcp2_token_type token_type, + uint32_t version, TLSServerContext &tls_ctx); + + std::expected on_read(const Endpoint &ep, + const Address &local_addr, + const Address &remote_addr, + const ngtcp2_pkt_info *pi, + std::span data); + std::expected on_write(); + std::expected write_streams(); + std::expected feed_data(const Endpoint &ep, + const Address &local_addr, + const Address &remote_addr, + const ngtcp2_pkt_info *pi, + std::span data); + void update_timer(); + std::expected handle_expiry(); + void signal_write(); + std::expected handshake_completed(); + + Server *server() const; + std::expected recv_stream_data(uint32_t flags, int64_t stream_id, + std::span data); + std::expected acked_stream_data_offset(int64_t stream_id, + uint64_t datalen); + uint32_t version() const; + void on_stream_open(int64_t stream_id); + std::expected on_stream_close(int64_t stream_id, + uint64_t app_error_code); + void start_draining_period(); + std::expected start_closing_period(); + std::expected handle_error(); + std::expected send_conn_close(); + std::expected send_conn_close(const Endpoint &ep, + const Address &local_addr, + const Address &remote_addr, + const ngtcp2_pkt_info *pi, + std::span data); + + std::expected + update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, const uint8_t *current_tx_secret, + size_t secretlen); + + void extend_max_remote_streams_bidi(uint64_t max_streams); + Stream *find_stream(int64_t stream_id) const; + std::expected on_stream_reset(int64_t stream_id); + std::expected on_stream_stop_sending(int64_t stream_id); + std::expected extend_max_stream_data(int64_t stream_id, + uint64_t max_data); + void shutdown_read(int64_t stream_id, uint64_t app_error_code); + + void write_qlog(const void *data, size_t datalen); + + void on_send_blocked(const ngtcp2_path &path, unsigned int ecn, + std::span data, size_t gso_size); + void start_wev_endpoint(const Endpoint &ep); + std::expected send_packet(const ngtcp2_path &path, + unsigned int ecn, + std::span data, + size_t gso_size); + void send_blocked_packet(); + + ngtcp2_ssize write_pkt(ngtcp2_path *path, ngtcp2_pkt_info *pi, uint8_t *dest, + size_t destlen, ngtcp2_tstamp ts); + + std::expected on_app_tx_ready(); + + std::expected start_response(Stream *stream); + +private: + struct ev_loop *loop_; + Server *server_; + ev_io wev_; + ev_timer timer_; + FILE *qlog_{}; + ngtcp2_cid scid_{}; + std::unique_ptr proto_codec_; + std::unordered_map> streams_; + // conn_closebuf_ contains a packet which contains CONNECTION_CLOSE. + // This packet is repeatedly sent as a response to the incoming + // packet in draining period. + std::unique_ptr conn_closebuf_; + // nkey_update_ is the number of key update occurred. + size_t nkey_update_{}; + bool no_gso_; + struct { + size_t bytes_recv; + size_t bytes_sent; + size_t num_pkts_recv; + size_t next_pkts_recv = 1; + } close_wait_{}; + + struct { + bool send_blocked; + // blocked field is effective only when send_blocked is true. + struct { + const Endpoint *endpoint; + Address local_addr; + Address remote_addr; + unsigned int ecn; + std::span data; + size_t gso_size; + } blocked; + } tx_{}; + std::array txbuf_; +}; + +class Server { +public: + Server(struct ev_loop *loop, TLSServerContext &tls_ctx); + ~Server(); + + std::expected init(const char *addr, const char *port); + void disconnect(); + void close(); + + void on_read(const Endpoint &ep); + void read_pkt(const Endpoint &ep, const Address &local_addr, + const Address &remote_addr, const ngtcp2_pkt_info *pi, + std::span data); + std::expected + send_version_negotiation(uint32_t version, std::span dcid, + std::span scid, const Endpoint &ep, + const Address &local_addr, + const Address &remote_addr); + std::expected send_retry(const ngtcp2_pkt_hd *chd, + const Endpoint &ep, + const Address &local_addr, + const Address &remote_addr, + size_t max_pktlen); + std::expected + send_stateless_connection_close(const ngtcp2_pkt_hd *chd, const Endpoint &ep, + const Address &local_addr, + const Address &remote_addr); + std::expected send_stateless_reset(size_t pktlen, + std::span dcid, + const Endpoint &ep, + const Address &local_addr, + const Address &remote_addr); + std::expected verify_retry_token(ngtcp2_cid *ocid, + const ngtcp2_pkt_hd *hd, + const Address &remote_addr); + std::expected verify_token(const ngtcp2_pkt_hd *hd, + const Address &remote_addr); + std::expected send_packet(const Endpoint &ep, + const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, + unsigned int ecn, + std::span data); + std::span + send_packet(const Endpoint &ep, bool &no_gso, const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, unsigned int ecn, + std::span data, size_t gso_size); + void remove(const Handler *h); + + void associate_cid(const ngtcp2_cid *cid, Handler *h); + void dissociate_cid(const ngtcp2_cid *cid); + + void on_stateless_reset_regen(); + +private: + std::unordered_map handlers_; + struct ev_loop *loop_; + std::vector endpoints_; + TLSServerContext &tls_ctx_; + ev_signal sigintev_; + ev_timer stateless_reset_regen_timer_; + size_t stateless_reset_bucket_{NGTCP2_STATELESS_RESET_BURST}; +}; + +#endif // !defined(SERVER_H) diff --git a/deps/ngtcp2/ngtcp2/examples/server_base.cc b/deps/ngtcp2/ngtcp2/examples/server_base.cc new file mode 100644 index 00000000000000..0e439299cfe56c --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/server_base.cc @@ -0,0 +1,61 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "server_base.h" + +#include +#include + +#include "debug.h" + +using namespace ngtcp2; + +extern Config config; + +Buffer::Buffer(const uint8_t *data, size_t datalen) + : buf{data, data + datalen}, begin{buf.data()}, tail{begin + datalen} {} +Buffer::Buffer(size_t datalen) : buf(datalen), begin{buf.data()}, tail{begin} {} + +static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { + auto h = static_cast(conn_ref->user_data); + return h->conn(); +} + +HandlerBase::HandlerBase() : conn_ref_{get_conn, this} { + ngtcp2_ccerr_default(&last_error_); +} + +HandlerBase::~HandlerBase() { + if (conn_) { + if (config.show_stat) { + debug::print_conn_info(conn_); + } + + ngtcp2_conn_del(conn_); + } +} + +ngtcp2_conn *HandlerBase::conn() const { return conn_; } + +ngtcp2_crypto_conn_ref *HandlerBase::conn_ref() { return &conn_ref_; } diff --git a/deps/ngtcp2/ngtcp2/examples/server_base.h b/deps/ngtcp2/ngtcp2/examples/server_base.h new file mode 100644 index 00000000000000..9438d11f8bd82f --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/server_base.h @@ -0,0 +1,225 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SERVER_BASE_H +#define SERVER_BASE_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "tls_server_session.h" +#include "network.h" +#include "shared.h" +#include "template.h" +#include "util.h" + +using namespace ngtcp2; + +struct Config { + Address preferred_ipv4_addr; + Address preferred_ipv6_addr; + // tx_loss_prob is probability of losing outgoing packet. + double tx_loss_prob{}; + // rx_loss_prob is probability of losing incoming packet. + double rx_loss_prob{}; + // ciphers is the list of enabled ciphers. + const char *ciphers{util::crypto_default_ciphers()}; + // groups is the list of supported groups. + const char *groups{util::crypto_default_groups()}; + // htdocs is a root directory to serve documents. + std::filesystem::path htdocs{util::realpath(".")}; + // mime_types_file is a path to "MIME media types and the + // extensions" file. Ubuntu mime-support package includes it in + // /etc/mime/types. + std::filesystem::path mime_types_file{"/etc/mime.types"sv}; + // mime_types maps file extension to MIME media type. + std::unordered_map mime_types; + // port is the port number which server listens on for incoming + // connections. + uint16_t port{}; + // quiet suppresses the output normally shown except for the error + // messages. + bool quiet{}; + // timeout is an idle timeout for QUIC connection. + ngtcp2_duration timeout{30 * NGTCP2_SECONDS}; + // show_secret is true if transport secrets should be printed out. + bool show_secret{}; + // validate_addr is true if server requires address validation. + bool validate_addr{}; + // early_response is true if server starts sending response when it + // receives HTTP header fields without waiting for request body. If + // HTTP response data is written before receiving request body, + // STOP_SENDING is sent. + bool early_response{}; + // verify_client is true if server verifies client with X.509 + // certificate based authentication. + bool verify_client{}; + // qlog_dir is the path to directory where qlog is stored. + std::filesystem::path qlog_dir; + // no_quic_dump is true if hexdump of QUIC STREAM and CRYPTO data + // should be disabled. + bool no_quic_dump{}; + // no_http_dump is true if hexdump of HTTP response body should be + // disabled. + bool no_http_dump{}; + // max_data is the initial connection-level flow control window. + uint64_t max_data{1_m}; + // max_stream_data_bidi_local is the initial stream-level flow + // control window for a bidirectional stream that the local endpoint + // initiates. + uint64_t max_stream_data_bidi_local{}; + // max_stream_data_bidi_remote is the initial stream-level flow + // control window for a bidirectional stream that the remote + // endpoint initiates. + uint64_t max_stream_data_bidi_remote{256_k}; + // max_stream_data_uni is the initial stream-level flow control + // window for a unidirectional stream. + uint64_t max_stream_data_uni{256_k}; + // max_streams_bidi is the number of the concurrent bidirectional + // streams. + uint64_t max_streams_bidi{100}; + // max_streams_uni is the number of the concurrent unidirectional + // streams. + uint64_t max_streams_uni{3}; + // max_window is the maximum connection-level flow control window + // size if auto-tuning is enabled. + uint64_t max_window{6_m}; + // max_stream_window is the maximum stream-level flow control window + // size if auto-tuning is enabled. + uint64_t max_stream_window{6_m}; + // max_dyn_length is the maximum length of dynamically generated + // response. + uint64_t max_dyn_length{20_m}; + // static_secret is used to derive keying materials for Retry and + // Stateless Retry token. + std::array static_secret; + // cc_algo is the congestion controller algorithm. + ngtcp2_cc_algo cc_algo{NGTCP2_CC_ALGO_CUBIC}; + // initial_rtt is an initial RTT. + ngtcp2_duration initial_rtt{NGTCP2_DEFAULT_INITIAL_RTT}; + // max_udp_payload_size is the maximum UDP payload size that server + // transmits. + size_t max_udp_payload_size{}; + // send_trailers controls whether server sends trailer fields or + // not. + bool send_trailers{}; + // handshake_timeout is the period of time before giving up QUIC + // connection establishment. + ngtcp2_duration handshake_timeout{UINT64_MAX}; + // preferred_versions includes QUIC versions in the order of + // preference. Server negotiates one of those versions if a client + // initially selects a less preferred version. + std::vector preferred_versions; + // available_versions includes QUIC versions that are sent in + // available_versions field of version_information + // transport_parameter. + std::vector available_versions; + // no_pmtud disables Path MTU Discovery. + bool no_pmtud{}; + // ack_thresh is the minimum number of the received ACK eliciting + // packets that triggers immediate acknowledgement. + size_t ack_thresh{2}; + // initial_pkt_num is the initial packet number for each packet + // number space. If it is set to UINT32_MAX, it is chosen randomly. + uint32_t initial_pkt_num{UINT32_MAX}; + // pmtud_probes is the array of UDP datagram payload size to probes. + std::vector pmtud_probes; + // ech_config contains server-side ECH configuration. + util::ECHServerConfig ech_config{}; + // origin_list contains a payload of ORIGIN frame. + std::optional> origin_list; + // no_gso disables GSO. + bool no_gso{}; + // show_stat, if true, displays the connection statistics when the + // connection is closed. + bool show_stat{}; + // gso_burst is the number of packets to aggregate in GSO. 0 means + // it is not limited by the configuration. + size_t gso_burst{}; +}; + +struct HTTPHeader { + HTTPHeader(std::string_view name, std::string_view value) + : name{name}, value{value} {} + + std::string_view name; + std::string_view value; +}; + +inline constexpr auto NGTCP2_STATELESS_RESET_BURST = 100UZ; + +struct Buffer { + Buffer(const uint8_t *data, size_t datalen); + explicit Buffer(size_t datalen); + + size_t size() const { return as_unsigned(tail - begin); } + size_t left() const { return as_unsigned(buf.data() + buf.size() - tail); } + uint8_t *const wpos() { return tail; } + std::span data() const { return {begin, size()}; } + void push(size_t len) { tail += len; } + void reset() { tail = begin; } + + std::vector buf; + // begin points to the beginning of the buffer. This might point to + // buf.data() if a buffer space is allocated by this object. It is + // also allowed to point to the external shared buffer. + uint8_t *begin; + // tail points to the position of the buffer where write should + // occur. + uint8_t *tail; +}; + +inline constexpr auto NGTCP2_SERVER = "ngtcp2 server"sv; + +class HandlerBase { +public: + HandlerBase(); + ~HandlerBase(); + + ngtcp2_conn *conn() const; + + TLSServerSession *get_session() { return &tls_session_; } + + ngtcp2_crypto_conn_ref *conn_ref(); + +protected: + ngtcp2_crypto_conn_ref conn_ref_; + TLSServerSession tls_session_; + ngtcp2_conn *conn_{}; + ngtcp2_ccerr last_error_; +}; + +#endif // !defined(SERVER_BASE_H) diff --git a/deps/ngtcp2/ngtcp2/examples/shared.cc b/deps/ngtcp2/ngtcp2/examples/shared.cc new file mode 100644 index 00000000000000..ef78394f04af72 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/shared.cc @@ -0,0 +1,645 @@ +/* + * ngtcp2 + * + * Copyright (c) 2019 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shared.h" + +#include + +#include +#include + +#include +#ifdef HAVE_NETINET_IN_H +# include +#endif // defined(HAVE_NETINET_IN_H) +#ifdef HAVE_NETINET_UDP_H +# include +#endif // defined(HAVE_NETINET_UDP_H) +#ifdef HAVE_NETINET_IP_H +# include +#endif // defined(HAVE_NETINET_IP_H) +#ifdef HAVE_ASM_TYPES_H +# include +#endif // defined(HAVE_ASM_TYPES_H) +#ifdef HAVE_LINUX_NETLINK_H +# include +#endif // defined(HAVE_LINUX_NETLINK_H) +#ifdef HAVE_LINUX_RTNETLINK_H +# include +#endif // defined(HAVE_LINUX_RTNETLINK_H) + +#include "template.h" + +namespace ngtcp2 { + +uint8_t msghdr_get_ecn(msghdr *msg, int family) { + switch (family) { + case AF_INET: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IP && +#ifdef __APPLE__ + cmsg->cmsg_type == IP_RECVTOS +#else // !defined(__APPLE__) + cmsg->cmsg_type == IP_TOS +#endif // !defined(__APPLE__) + && cmsg->cmsg_len) { + return *reinterpret_cast(CMSG_DATA(cmsg)) & IPTOS_ECN_MASK; + } + } + break; + case AF_INET6: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_TCLASS && + cmsg->cmsg_len) { + unsigned int tos; + + memcpy(&tos, CMSG_DATA(cmsg), sizeof(int)); + + return tos & IPTOS_ECN_MASK; + } + } + break; + } + + return 0; +} + +void fd_set_recv_ecn(int fd, int family) { + unsigned int tos = 1; + switch (family) { + case AF_INET: + if (setsockopt(fd, IPPROTO_IP, IP_RECVTOS, &tos, + static_cast(sizeof(tos))) == -1) { + std::println(stderr, "setsockopt: {}", strerror(errno)); + } + break; + case AF_INET6: + if (setsockopt(fd, IPPROTO_IPV6, IPV6_RECVTCLASS, &tos, + static_cast(sizeof(tos))) == -1) { + std::println(stderr, "setsockopt: {}", strerror(errno)); + } + break; + } +} + +void fd_set_ip_mtu_discover(int fd, int family) { +#if defined(IP_MTU_DISCOVER) && defined(IPV6_MTU_DISCOVER) + int val; + + switch (family) { + case AF_INET: + val = IP_PMTUDISC_PROBE; + if (setsockopt(fd, IPPROTO_IP, IP_MTU_DISCOVER, &val, + static_cast(sizeof(val))) == -1) { + std::println(stderr, "setsockopt: IP_MTU_DISCOVER: {}", strerror(errno)); + } + break; + case AF_INET6: + val = IPV6_PMTUDISC_PROBE; + if (setsockopt(fd, IPPROTO_IPV6, IPV6_MTU_DISCOVER, &val, + static_cast(sizeof(val))) == -1) { + std::println(stderr, "setsockopt: IPV6_MTU_DISCOVER: {}", + strerror(errno)); + } + break; + } +#endif // defined(IP_MTU_DISCOVER) && defined(IPV6_MTU_DISCOVER) +} + +void fd_set_ip_dontfrag(int fd, int family) { +#if defined(IP_DONTFRAG) && defined(IPV6_DONTFRAG) + int val = 1; + + switch (family) { + case AF_INET: + if (setsockopt(fd, IPPROTO_IP, IP_DONTFRAG, &val, + static_cast(sizeof(val))) == -1) { + std::println(stderr, "setsockopt: IP_DONTFRAG: {}", strerror(errno)); + } + break; + case AF_INET6: + if (setsockopt(fd, IPPROTO_IPV6, IPV6_DONTFRAG, &val, + static_cast(sizeof(val))) == -1) { + std::println(stderr, "setsockopt: IPV6_DONTFRAG: {}", strerror(errno)); + } + break; + } +#endif // defined(IP_DONTFRAG) && defined(IPV6_DONTFRAG) +} + +void fd_set_udp_gro(int fd) { +#ifdef UDP_GRO + int val = 1; + + if (setsockopt(fd, IPPROTO_UDP, UDP_GRO, &val, + static_cast(sizeof(val))) == -1) { + std::println(stderr, "setsockopt: UDP_GRO: {}", strerror(errno)); + } +#endif // defined(UDP_GRO) +} + +std::expected msghdr_get_local_addr(msghdr *msg, int family) { + switch (family) { + case AF_INET: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) { + in_pktinfo pktinfo; + memcpy(&pktinfo, CMSG_DATA(cmsg), sizeof(pktinfo)); + Address res{ + .ifindex = static_cast(pktinfo.ipi_ifindex), + }; + auto &sa = res.skaddr.emplace(); + sa.sin_family = AF_INET; + sa.sin_addr = pktinfo.ipi_addr; + return res; + } + } + return std::unexpected{Error::NOT_FOUND}; + case AF_INET6: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_PKTINFO) { + in6_pktinfo pktinfo; + memcpy(&pktinfo, CMSG_DATA(cmsg), sizeof(pktinfo)); + Address res{ + .ifindex = static_cast(pktinfo.ipi6_ifindex), + }; + auto &sa = res.skaddr.emplace(); + sa.sin6_family = AF_INET6; + sa.sin6_addr = pktinfo.ipi6_addr; + return res; + } + } + return std::unexpected{Error::NOT_FOUND}; + } + return std::unexpected{Error::INVALID_ARGUMENT}; +} + +size_t msghdr_get_udp_gro(msghdr *msg) { + int gso_size = 0; + +#ifdef UDP_GRO + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == SOL_UDP && cmsg->cmsg_type == UDP_GRO) { + memcpy(&gso_size, CMSG_DATA(cmsg), sizeof(gso_size)); + + break; + } + } +#endif // defined(UDP_GRO) + + return static_cast(gso_size); +} + +#ifdef HAVE_LINUX_RTNETLINK_H + +struct nlmsg { + nlmsghdr hdr; + rtmsg msg; + rtattr dst; + uint8_t dst_addr[sizeof(sockaddr_storage)]; +}; + +namespace { +std::expected send_netlink_msg(int fd, const Address &remote_addr, + uint32_t seq) { + nlmsg nlmsg{ + .hdr{ + .nlmsg_type = RTM_GETROUTE, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, + .nlmsg_seq = seq, + }, + .msg{ + .rtm_family = static_cast(remote_addr.family()), + .rtm_protocol = RTPROT_KERNEL, + }, + .dst{ + .rta_type = RTA_DST, + }, + }; + + std::visit( + [&nlmsg](auto &&arg) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + nlmsg.dst.rta_len = RTA_LENGTH(sizeof(arg.sin_addr)); + memcpy(RTA_DATA(&nlmsg.dst), &arg.sin_addr, sizeof(arg.sin_addr)); + return; + } + + if constexpr (std::is_same_v) { + nlmsg.dst.rta_len = RTA_LENGTH(sizeof(arg.sin6_addr)); + memcpy(RTA_DATA(&nlmsg.dst), &arg.sin6_addr, sizeof(arg.sin6_addr)); + return; + } + + assert(0); + abort(); + }, + remote_addr.skaddr); + + nlmsg.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(nlmsg.msg) + nlmsg.dst.rta_len); + + sockaddr_nl sa{ + .nl_family = AF_NETLINK, + }; + + iovec iov{ + .iov_base = &nlmsg, + .iov_len = nlmsg.hdr.nlmsg_len, + }; + msghdr msg{ + .msg_name = &sa, + .msg_namelen = sizeof(sa), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + + ssize_t nwrite; + + do { + nwrite = sendmsg(fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + std::println(stderr, "sendmsg: Could not write netlink message: {}", + strerror(errno)); + return std::unexpected{Error::SYSCALL}; + } + + return {}; +} +} // namespace + +namespace { +std::expected recv_netlink_msg(int fd, uint32_t seq) { + std::array buf; + iovec iov = { + .iov_base = buf.data(), + .iov_len = buf.size(), + }; + sockaddr_nl sa{}; + msghdr msg{ + .msg_name = &sa, + .msg_namelen = sizeof(sa), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + ssize_t nread; + InAddr ia; + + do { + nread = recvmsg(fd, &msg, 0); + } while (nread == -1 && errno == EINTR); + + if (nread == -1) { + std::println(stderr, "recvmsg: Could not receive netlink message: {}", + strerror(errno)); + return std::unexpected{Error::SYSCALL}; + } + + for (auto hdr = reinterpret_cast(buf.data()); + NLMSG_OK(hdr, nread); hdr = NLMSG_NEXT(hdr, nread)) { + if (seq != hdr->nlmsg_seq) { + std::println(stderr, + "netlink: unexpected sequence number {} while expecting {}", + hdr->nlmsg_seq, seq); + return std::unexpected{Error::INTERNAL}; + } + + if (hdr->nlmsg_flags & NLM_F_MULTI) { + std::println(stderr, "netlink: unexpected NLM_F_MULTI flag set"); + return std::unexpected{Error::INTERNAL}; + } + + switch (hdr->nlmsg_type) { + case NLMSG_DONE: + std::println(stderr, "netlink: unexpected NLMSG_DONE"); + return std::unexpected{Error::INTERNAL}; + case NLMSG_NOOP: + continue; + case NLMSG_ERROR: + std::println(stderr, "netlink: {}", + strerror(-static_cast(NLMSG_DATA(hdr))->error)); + return std::unexpected{Error::INTERNAL}; + } + + auto attrlen = hdr->nlmsg_len - NLMSG_SPACE(sizeof(rtmsg)); + + for (auto rta = reinterpret_cast( + static_cast(NLMSG_DATA(hdr)) + sizeof(rtmsg)); + RTA_OK(rta, attrlen); rta = RTA_NEXT(rta, attrlen)) { + if (rta->rta_type != RTA_PREFSRC) { + continue; + } + + switch (static_cast(NLMSG_DATA(hdr))->rtm_family) { + case AF_INET: { + constexpr auto in_addrlen = sizeof(in_addr); + if (RTA_LENGTH(in_addrlen) != rta->rta_len) { + return std::unexpected{Error::INTERNAL}; + } + + in_addr addr; + memcpy(&addr, RTA_DATA(rta), in_addrlen); + + ia.emplace(addr); + + break; + } + case AF_INET6: { + constexpr auto in_addrlen = sizeof(in6_addr); + if (RTA_LENGTH(in_addrlen) != rta->rta_len) { + return std::unexpected{Error::INTERNAL}; + } + + in6_addr addr; + memcpy(&addr, RTA_DATA(rta), in_addrlen); + + ia.emplace(addr); + + break; + } + default: + assert(0); + abort(); + } + + break; + } + } + + if (in_addr_empty(ia)) { + return std::unexpected{Error::NOT_FOUND}; + } + + // Read ACK + sa = {}; + msg = {}; + + msg.msg_name = &sa; + msg.msg_namelen = sizeof(sa); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + int error = -1; + + do { + nread = recvmsg(fd, &msg, 0); + } while (nread == -1 && errno == EINTR); + + if (nread == -1) { + std::println(stderr, "recvmsg: Could not receive netlink message: {}", + strerror(errno)); + return std::unexpected{Error::SYSCALL}; + } + + error = -1; + + for (auto hdr = reinterpret_cast(buf.data()); + NLMSG_OK(hdr, nread); hdr = NLMSG_NEXT(hdr, nread)) { + if (seq != hdr->nlmsg_seq) { + std::println(stderr, + "netlink: unexpected sequence number {} while expecting {}", + hdr->nlmsg_seq, seq); + return std::unexpected{Error::INTERNAL}; + } + + if (hdr->nlmsg_flags & NLM_F_MULTI) { + std::println(stderr, "netlink: unexpected NLM_F_MULTI flag set"); + return std::unexpected{Error::INTERNAL}; + } + + switch (hdr->nlmsg_type) { + case NLMSG_DONE: + std::println(stderr, "netlink: unexpected NLMSG_DONE"); + return std::unexpected{Error::INTERNAL}; + case NLMSG_NOOP: + continue; + case NLMSG_ERROR: + error = -static_cast(NLMSG_DATA(hdr))->error; + if (error == 0) { + break; + } + + std::println(stderr, "netlink: {}", strerror(error)); + + return std::unexpected{Error::INTERNAL}; + } + } + + if (error != 0) { + return std::unexpected{Error::INTERNAL}; + } + + return ia; +} +} // namespace + +std::expected get_local_addr(const Address &remote_addr) { + sockaddr_nl sa{ + .nl_family = AF_NETLINK, + }; + + auto fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (fd == -1) { + std::println(stderr, "socket: Could not create netlink socket: {}", + strerror(errno)); + return std::unexpected{Error::SYSCALL}; + } + + auto fd_d = defer([fd] { close(fd); }); + + if (bind(fd, reinterpret_cast(&sa), sizeof(sa)) == -1) { + std::println(stderr, "bind: Could not bind netlink socket: {}", + strerror(errno)); + return std::unexpected{Error::SYSCALL}; + } + + uint32_t seq = 1; + + if (auto rv = send_netlink_msg(fd, remote_addr, seq); !rv) { + return std::unexpected{rv.error()}; + } + + return recv_netlink_msg(fd, seq); +} + +#endif // defined(HAVE_LINUX_NETLINK_H) + +bool addreq(const Address &addr, const InAddr &ia) { + return std::visit( + [&ia](auto &&arg) -> bool { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + auto rhs = std::get_if(&ia); + + return rhs && memcmp(&arg.sin_addr, rhs, sizeof(*rhs)) == 0; + } + + if constexpr (std::is_same_v) { + auto rhs = std::get_if(&ia); + + return rhs && memcmp(&arg.sin6_addr, rhs, sizeof(*rhs)) == 0; + } + + assert(0); + abort(); + }, + addr.skaddr); +} + +const void *in_addr_get_ptr(const InAddr &ia) { + return std::visit( + [](auto &&arg) { + if constexpr (std::is_same_v, + std::monostate>) { + assert(0); + abort(); + } + + return reinterpret_cast(&arg); + }, + ia); +} + +bool in_addr_empty(const InAddr &ia) { + return std::holds_alternative(ia); +} + +const sockaddr *as_sockaddr(const Sockaddr &skaddr) { + return std::visit( + [](auto &&arg) { + if constexpr (std::is_same_v, + std::monostate>) { + assert(0); + abort(); + } + + return reinterpret_cast(&arg); + }, + skaddr); +} + +sockaddr *as_sockaddr(Sockaddr &skaddr) { + return std::visit( + [](auto &&arg) { + if constexpr (std::is_same_v, + std::monostate>) { + assert(0); + abort(); + } + + return reinterpret_cast(&arg); + }, + skaddr); +} + +int sockaddr_family(const Sockaddr &skaddr) { + return as_sockaddr(skaddr)->sa_family; +} + +uint16_t sockaddr_port(const Sockaddr &skaddr) { + return std::visit( + [](auto &&arg) -> uint16_t { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + return ntohs(arg.sin_port); + } + + if constexpr (std::is_same_v) { + return ntohs(arg.sin6_port); + } + + assert(0); + abort(); + }, + skaddr); +} + +void sockaddr_port(Sockaddr &skaddr, uint16_t port) { + std::visit( + [port](auto &&arg) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + arg.sin_port = htons(port); + return; + } + + if constexpr (std::is_same_v) { + arg.sin6_port = htons(port); + return; + } + + assert(0); + abort(); + }, + skaddr); +} + +void sockaddr_set(Sockaddr &skaddr, const sockaddr *sa) { + switch (sa->sa_family) { + case AF_INET: + skaddr.emplace(*reinterpret_cast(sa)); + return; + case AF_INET6: + skaddr.emplace(*reinterpret_cast(sa)); + return; + default: + assert(0); + abort(); + } +} + +socklen_t sockaddr_size(const Sockaddr &skaddr) { + return std::visit( + [](auto &&arg) { return static_cast(sizeof(arg)); }, skaddr); +} + +bool sockaddr_empty(const Sockaddr &skaddr) { + return std::holds_alternative(skaddr); +} + +const sockaddr *Address::as_sockaddr() const { + return ngtcp2::as_sockaddr(skaddr); +} + +sockaddr *Address::as_sockaddr() { return ngtcp2::as_sockaddr(skaddr); } + +int Address::family() const { return sockaddr_family(skaddr); } + +uint16_t Address::port() const { return sockaddr_port(skaddr); } + +void Address::port(uint16_t port) { sockaddr_port(skaddr, port); } + +void Address::set(const sockaddr *sa) { sockaddr_set(skaddr, sa); } + +socklen_t Address::size() const { return sockaddr_size(skaddr); } + +bool Address::empty() const { return sockaddr_empty(skaddr); } + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/shared.h b/deps/ngtcp2/ngtcp2/examples/shared.h new file mode 100644 index 00000000000000..b9e0e1f0b01261 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/shared.h @@ -0,0 +1,252 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHARED_H +#define SHARED_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include +#include +#include +#include + +#include + +#include "network.h" + +using namespace std::literals; + +namespace ngtcp2 { + +enum class Error { + // the generic errors that are not covered by more specific error + // codes. + INTERNAL, + // function arguments are invalid + INVALID_ARGUMENT, + // integer overflow error + INTEGER_OVERFLOW, + // file I/O error + IO, + // function is not implemented yet + NOT_IMPLEMENTED, + // the operation is not supported + UNSUPPORTED, + // entity is not found (e.g., file not found) + NOT_FOUND, + // crypto related error (e.g., error from TLS stack) + CRYPTO, + // system call error + SYSCALL, + // C library error (e.g., error from getaddrinfo) + LIBC, + // HTTP3 library error (e.g., error from nghttp3 API) + HTTP3, + // QUIC library error (e.g., error from ngtcp2 API) + QUIC, + // sending packet is blocked by kernel + SEND_BLOCKED, + // QUIC connection is in close-wait. + CLOSE_WAIT, + // QUIC connection should be retried. + RETRY_CONN, + // QUIC connection should be dropped. + DROP_CONN, + // Retry token is unreadable, and should be ignored. + UNREADABLE_TOKEN, +}; + +enum class AppProtocol { + H3, + HQ, +}; + +template +consteval std::span span_from_lit(const uint8_t (&s)[N]) { + return {s, N - 1}; +} + +inline constexpr uint8_t RAW_HQ_ALPN[] = "\xAhq-interop"; +inline constexpr auto HQ_ALPN = span_from_lit(RAW_HQ_ALPN); +inline constexpr auto HQ_ALPN_V1 = span_from_lit(RAW_HQ_ALPN); + +inline constexpr uint8_t RAW_H3_ALPN[] = "\x2h3"; +inline constexpr auto H3_ALPN = span_from_lit(RAW_H3_ALPN); +inline constexpr auto H3_ALPN_V1 = span_from_lit(RAW_H3_ALPN); + +inline constexpr uint32_t TLS_ALERT_ECH_REQUIRED = 121; + +inline constexpr auto MAX_RECV_PKTS = 64UZ; + +// msghdr_get_ecn gets ECN bits from |msg|. |family| is the address +// family from which packet is received. +uint8_t msghdr_get_ecn(msghdr *msg, int family); + +// fd_set_recv_ecn sets socket option to |fd| so that it can receive +// ECN bits. +void fd_set_recv_ecn(int fd, int family); + +// fd_set_ip_mtu_discover sets IP(V6)_MTU_DISCOVER socket option to +// |fd|. +void fd_set_ip_mtu_discover(int fd, int family); + +// fd_set_ip_dontfrag sets IP(V6)_DONTFRAG socket option to |fd|. +void fd_set_ip_dontfrag(int fd, int family); + +// fd_set_udp_gro sets UDP_GRO socket option to |fd|. +void fd_set_udp_gro(int fd); + +std::expected msghdr_get_local_addr(msghdr *msg, int family); + +// msghdr_get_udp_gro returns UDP_GRO value from |msg|. If UDP_GRO is +// not found, or UDP_GRO is not supported, this function returns 0. +size_t msghdr_get_udp_gro(msghdr *msg); + +// get_local_addr returns the preferred local address (interface +// address) for a given destination address |remote_addr|. +std::expected get_local_addr(const Address &remote_addr); + +// addreq returns true if |addr| and |ia| contain the same address. +bool addreq(const Address &addr, const InAddr &ia); + +// in_addr_get_ptr returns the pointer to the stored address in |ia|. +// It is undefined if |ia| contains std::monostate. +const void *in_addr_get_ptr(const InAddr &ia); + +// in_addr_empty returns true if |ia| if it does not contain any +// meaningful address. +bool in_addr_empty(const InAddr &ia); + +// as_sockaddr returns the pointer to the stored address casted to +// const sockaddr *. +[[nodiscard]] const sockaddr *as_sockaddr(const Sockaddr &skaddr); +[[nodiscard]] sockaddr *as_sockaddr(Sockaddr &skaddr); + +// sockaddr_family returns the address family. +[[nodiscard]] int sockaddr_family(const Sockaddr &skaddr); + +// sockaddr_port returns the port. +[[nodiscard]] uint16_t sockaddr_port(const Sockaddr &skaddr); + +// sockaddr_port sets |port| to |skaddr|. +void sockaddr_port(Sockaddr &skaddr, uint16_t port); + +// sockaddr_set stores |sa| to |skaddr|. The address family is +// determined by |sa|->sa_family, and |sa| must point to the memory +// that contains valid object which is either sockaddr_in or +// sockaddr_in6. +void sockaddr_set(Sockaddr &skaddr, const sockaddr *sa); + +// sockaddr_size returns the size of the stored address. +[[nodiscard]] socklen_t sockaddr_size(const Sockaddr &skaddr); + +// sockaddr_empty returns true if |skaddr| does not contain any +// meaningful address. +[[nodiscard]] bool sockaddr_empty(const Sockaddr &skaddr); + +[[nodiscard]] inline ngtcp2_addr as_ngtcp2_addr(const Address &addr) { + return { + .addr = const_cast(addr.as_sockaddr()), + .addrlen = addr.size(), + }; +} + +[[nodiscard]] inline ngtcp2_addr as_ngtcp2_addr(Address &addr) { + return { + .addr = addr.as_sockaddr(), + .addrlen = addr.size(), + }; +} + +} // namespace ngtcp2 + +template <> +struct std::formatter : public std::formatter { + template + auto format(ngtcp2::Error e, FormatContext &ctx) const { + auto s = "unknown"sv; + + switch (e) { + case ngtcp2::Error::INTERNAL: + s = "internal"sv; + break; + case ngtcp2::Error::INVALID_ARGUMENT: + s = "invalid argument"sv; + break; + case ngtcp2::Error::INTEGER_OVERFLOW: + s = "integer overflow"sv; + break; + case ngtcp2::Error::IO: + s = "I/O"sv; + break; + case ngtcp2::Error::NOT_IMPLEMENTED: + s = "not implemented"sv; + break; + case ngtcp2::Error::UNSUPPORTED: + s = "unsupported"sv; + break; + case ngtcp2::Error::NOT_FOUND: + s = "not found"sv; + break; + case ngtcp2::Error::CRYPTO: + s = "crypto"sv; + break; + case ngtcp2::Error::SYSCALL: + s = "syscall"sv; + break; + case ngtcp2::Error::LIBC: + s = "libc"sv; + break; + case ngtcp2::Error::HTTP3: + s = "HTTP3"sv; + break; + case ngtcp2::Error::QUIC: + s = "QUIC"sv; + break; + case ngtcp2::Error::SEND_BLOCKED: + s = "send blocked"sv; + break; + case ngtcp2::Error::CLOSE_WAIT: + s = "close wait"sv; + break; + case ngtcp2::Error::RETRY_CONN: + s = "retry connection"sv; + break; + case ngtcp2::Error::DROP_CONN: + s = "drop connection"sv; + break; + case ngtcp2::Error::UNREADABLE_TOKEN: + s = "unreadable token"sv; + break; + } + + return std::formatter::format(s, ctx); + } +}; + +#endif // !defined(SHARED_H) diff --git a/deps/ngtcp2/ngtcp2/examples/sim.cc b/deps/ngtcp2/ngtcp2/examples/sim.cc new file mode 100644 index 00000000000000..2f371092bdcd27 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/sim.cc @@ -0,0 +1,1095 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "sim.h" + +#include +#include +#include + +#include +#include +#include + +#include "ngtcp2/ngtcp2_crypto_wolfssl.h" + +#include "util.h" +#include "debug.h" + +using namespace std::literals; + +namespace ngtcp2 { + +constexpr auto ALPN_LIST = "ngtcp2-sim"sv; +constexpr auto CIDLEN = 10UZ; +constexpr uint8_t SERVER_SECRET[] = "server_secret"; +constexpr std::array static_secret{0xCA, 0xCE, 0xCA, 0xFE}; + +namespace { +std::expected generate_secure_random(std::span data) { + if (wolfSSL_RAND_bytes(data.data(), static_cast(data.size())) != 1) { + return std::unexpected{Error::CRYPTO}; + } + + return {}; +} + +void rand_bytes(uint8_t *dest, size_t destlen, + const ngtcp2_rand_ctx *rand_ctx) { + auto rv = generate_secure_random({dest, destlen}); + (void)rv; + assert(rv); +} + +int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, + ngtcp2_stateless_reset_token *token, size_t cidlen, + void *user_data) { + if (!generate_secure_random({cid->data, cidlen})) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + if (ngtcp2_crypto_generate_stateless_reset_token( + token->data, SERVER_SECRET, sizeof(SERVER_SECRET) - 1, cid) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +uint64_t LinkConfig::compute_expected_goodput(Timestamp::duration rtt) const { + // Assume 80% usage ratio. + uint64_t g = rate * 8 / 10; + + if (loss < 1e-9) { + return g; + } + + constexpr double margin = 0.9; + + return std::min(g, + static_cast(MAX_UDP_PAYLOAD_SIZE * NGTCP2_SECONDS / + static_cast(rtt.count()) / + sqrt(loss) * 8 * margin)); +} + +namespace { +int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data, void *stream_user_data) { + ngtcp2_conn_extend_max_stream_offset(conn, stream_id, datalen); + ngtcp2_conn_extend_max_offset(conn, datalen); + + return 0; +} +} // namespace + +ngtcp2_callbacks default_client_callbacks() { + return ngtcp2_callbacks{ + .client_initial = ngtcp2_crypto_client_initial_cb, + .recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb, + .encrypt = ngtcp2_crypto_encrypt_cb, + .decrypt = ngtcp2_crypto_decrypt_cb, + .hp_mask = ngtcp2_crypto_hp_mask_cb, + .recv_stream_data = recv_stream_data, + .recv_retry = ngtcp2_crypto_recv_retry_cb, + .rand = rand_bytes, + .update_key = ngtcp2_crypto_update_key_cb, + .delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb, + .delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + .version_negotiation = ngtcp2_crypto_version_negotiation_cb, + .get_new_connection_id2 = get_new_connection_id, + .get_path_challenge_data2 = ngtcp2_crypto_get_path_challenge_data2_cb, + }; +} + +ngtcp2_callbacks default_server_callbacks() { + return ngtcp2_callbacks{ + .recv_client_initial = ngtcp2_crypto_recv_client_initial_cb, + .recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb, + .encrypt = ngtcp2_crypto_encrypt_cb, + .decrypt = ngtcp2_crypto_decrypt_cb, + .hp_mask = ngtcp2_crypto_hp_mask_cb, + .recv_stream_data = recv_stream_data, + .rand = rand_bytes, + .update_key = ngtcp2_crypto_update_key_cb, + .delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb, + .delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + .version_negotiation = ngtcp2_crypto_version_negotiation_cb, + .get_new_connection_id2 = get_new_connection_id, + .get_path_challenge_data2 = ngtcp2_crypto_get_path_challenge_data2_cb, + }; +} + +ngtcp2_settings default_client_settings() { + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + + settings.log_write = debug::log_write; + + return settings; +} + +ngtcp2_settings default_server_settings() { + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + + settings.log_write = debug::log_write; + + return settings; +} + +ngtcp2_transport_params default_client_transport_params() { + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + + return params; +} + +ngtcp2_transport_params default_server_transport_params() { + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + + return params; +} + +Sockaddr getaddrinfo(const char *host, const char *svc) { + auto hints = addrinfo{ + .ai_flags = AI_NUMERICHOST | AI_NUMERICSERV, + .ai_family = AF_UNSPEC, + }; + addrinfo *rp; + + auto rv = getaddrinfo(host, svc, &hints, &rp); + (void)rv; + assert(0 == rv); + + Sockaddr skaddr; + sockaddr_set(skaddr, rp->ai_addr); + + freeaddrinfo(rp); + + return skaddr; +} + +ngtcp2_addr default_client_addr() { + static auto skaddr = getaddrinfo("10.0.1.1", "12345"); + + return ngtcp2_addr{ + .addr = as_sockaddr(skaddr), + .addrlen = sockaddr_size(skaddr), + }; +} + +ngtcp2_addr default_server_addr() { + static auto skaddr = getaddrinfo("10.0.2.1", "443"); + + return ngtcp2_addr{ + .addr = as_sockaddr(skaddr), + .addrlen = sockaddr_size(skaddr), + }; +} + +EndpointConfig default_client_endpoint_config() { + return EndpointConfig{ + .callbacks = default_client_callbacks(), + .settings = default_client_settings(), + .params = default_client_transport_params(), + .local_addr = default_client_addr(), + }; +} + +EndpointConfig default_server_endpoint_config() { + return EndpointConfig{ + .server = true, + .callbacks = default_server_callbacks(), + .settings = default_server_settings(), + .params = default_server_transport_params(), + .local_addr = default_server_addr(), + }; +} + +namespace { +ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { + auto ep = static_cast(conn_ref->user_data); + return ep->get_conn(); +} +} // namespace + +Endpoint::Endpoint() + : conn_ref_{ngtcp2::get_conn, this}, channel_{config_.link} {} + +Endpoint::Endpoint(const EndpointConfig &config) + : config_{config}, + conn_ref_{ngtcp2::get_conn, this}, + channel_{config_.link} {} + +Endpoint::Endpoint(Endpoint &&other) noexcept + : config_{std::exchange(other.config_, {})}, + ssl_ctx_{std::exchange(other.ssl_ctx_, nullptr)}, + ssl_{std::exchange(other.ssl_, nullptr)}, + conn_{std::exchange(other.conn_, nullptr)}, + conn_ref_{ngtcp2::get_conn, this}, + channel_{std::exchange(other.channel_, {})}, + initialized_{std::exchange(other.initialized_, false)} { + if (ssl_) { + wolfSSL_set_app_data(ssl_, &conn_ref_); + } +} + +Endpoint::~Endpoint() { reset(); } + +void Endpoint::reset() { + ngtcp2_conn_del(conn_); + conn_ = nullptr; + + if (ssl_) { + wolfSSL_free(ssl_); + ssl_ = nullptr; + } + + if (ssl_ctx_) { + wolfSSL_CTX_free(ssl_ctx_); + ssl_ctx_ = nullptr; + } + + initialized_ = false; +} + +Endpoint &Endpoint::operator=(Endpoint &&other) noexcept { + reset(); + + config_ = std::exchange(other.config_, {}); + ssl_ctx_ = std::exchange(other.ssl_ctx_, nullptr); + ssl_ = std::exchange(other.ssl_, nullptr); + conn_ = std::exchange(other.conn_, nullptr); + conn_ref_ = {ngtcp2::get_conn, this}; + channel_ = std::exchange(other.channel_, {}); + initialized_ = std::exchange(other.initialized_, false); + + if (ssl_) { + wolfSSL_set_app_data(ssl_, &conn_ref_); + } + + return *this; +} + +constexpr auto tls_key = R"(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwEvkGGgXAcRaG7Z8 +gA7C6+W2RsW9gcjV9e5ybr0ikaahRANCAASCo35bDi+Q/q/CzHI1e5QaBrbqbFhW +G20QbVAeMK8l0oC8OGD3PSpZK1HXwALwzhMuwhxDos3ANb5naa5y17fQ +-----END PRIVATE KEY----- +)"sv; + +constexpr auto tls_crt = R"(-----BEGIN CERTIFICATE----- +MIICBzCCAa2gAwIBAgIUd2l6Pce3S0QH3dQC0Q/CjHbmggowCgYIKoZIzj0EAwIw +WTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu +dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1 +MTExNDExNTcwMFoXDTI1MTIxNDExNTcwMFowWTELMAkGA1UEBhMCQVUxEzARBgNV +BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 +ZDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +gqN+Ww4vkP6vwsxyNXuUGga26mxYVhttEG1QHjCvJdKAvDhg9z0qWStR18AC8M4T +LsIcQ6LNwDW+Z2mucte30KNTMFEwHQYDVR0OBBYEFFVgXLoLwzpf6+twP5z8Ujr2 +5mxnMB8GA1UdIwQYMBaAFFVgXLoLwzpf6+twP5z8Ujr25mxnMA8GA1UdEwEB/wQF +MAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAO4tnDNRAcooz62vf2m7vTyDqFCjcaIv +SJ9Gq0lvEXEcAiBwWBNUASBqLaje3hmtgwxcF7EIqqiGo5j8f9Ufgu6SRg== +-----END CERTIFICATE----- +)"sv; + +std::expected +Endpoint::setup_server(std::span original_dcid, + std::span client_scid, uint32_t version, + const ngtcp2_addr *remote_addr, + std::optional token_params) { + ngtcp2_cid scid{ + .datalen = CIDLEN, + }; + + if (auto rv = generate_secure_random({scid.data, scid.datalen}); !rv) { + return rv; + } + + ngtcp2_cid dcid; + ngtcp2_cid_init(&dcid, client_scid.data(), client_scid.size()); + + auto params = config_.params; + auto settings = config_.settings; + + if (token_params) { + params.original_dcid = *token_params->original_dcid; + params.retry_scid = *token_params->retry_scid; + params.retry_scid_present = 1; + + settings.token = token_params->token.data(); + settings.tokenlen = token_params->token.size(); + settings.token_type = NGTCP2_TOKEN_TYPE_RETRY; + } else { + ngtcp2_cid_init(¶ms.original_dcid, original_dcid.data(), + original_dcid.size()); + } + + params.original_dcid_present = 1; + + if (ngtcp2_crypto_generate_stateless_reset_token( + params.stateless_reset_token, SERVER_SECRET, sizeof(SERVER_SECRET) - 1, + &scid) != 0) { + return std::unexpected{Error::QUIC}; + } + + auto path = ngtcp2_path{ + .local = config_.local_addr, + .remote = *remote_addr, + }; + + if (ngtcp2_conn_server_new(&conn_, &dcid, &scid, &path, version, + &config_.callbacks, &settings, ¶ms, nullptr, + config_.user_data) != 0) { + return std::unexpected{Error::QUIC}; + } + + ssl_ctx_ = wolfSSL_CTX_new(wolfTLSv1_3_server_method()); + if (!ssl_ctx_) { + return std::unexpected{Error::CRYPTO}; + } + + if (ngtcp2_crypto_wolfssl_configure_server_context(ssl_ctx_) != 0) { + return std::unexpected{Error::CRYPTO}; + } + + if (wolfSSL_CTX_use_certificate_buffer( + ssl_ctx_, reinterpret_cast(tls_crt.data()), + static_cast(tls_crt.size()), SSL_FILETYPE_PEM) != SSL_SUCCESS) { + return std::unexpected{Error::CRYPTO}; + } + + if (wolfSSL_CTX_use_PrivateKey_buffer( + ssl_ctx_, reinterpret_cast(tls_key.data()), + static_cast(tls_key.size()), SSL_FILETYPE_PEM) != SSL_SUCCESS) { + return std::unexpected{Error::CRYPTO}; + } + + ssl_ = wolfSSL_new(ssl_ctx_); + if (!ssl_) { + return std::unexpected{Error::CRYPTO}; + } + + if (wolfSSL_UseALPN(ssl_, const_cast(ALPN_LIST.data()), + ALPN_LIST.size(), + WOLFSSL_ALPN_FAILED_ON_MISMATCH) != WOLFSSL_SUCCESS) { + return std::unexpected{Error::CRYPTO}; + } + + wolfSSL_set_app_data(ssl_, &conn_ref_); + wolfSSL_set_accept_state(ssl_); + wolfSSL_set_quic_transport_version(ssl_, 0x39); + + ngtcp2_conn_set_tls_native_handle(conn_, ssl_); + + initialized_ = true; + + return {}; +} + +std::expected +Endpoint::setup_client(const ngtcp2_addr *remote_addr) { + ngtcp2_cid dcid{ + .datalen = CIDLEN, + }; + ngtcp2_cid scid{ + .datalen = CIDLEN, + }; + + if (auto rv = generate_secure_random({dcid.data, dcid.datalen}); !rv) { + return rv; + } + + if (auto rv = generate_secure_random({scid.data, scid.datalen}); !rv) { + return rv; + } + + auto path = ngtcp2_path{ + .local = config_.local_addr, + .remote = *remote_addr, + }; + + if (ngtcp2_conn_client_new( + &conn_, &dcid, &scid, &path, NGTCP2_PROTO_VER_V1, &config_.callbacks, + &config_.settings, &config_.params, nullptr, config_.user_data) != 0) { + return std::unexpected{Error::QUIC}; + } + + ssl_ctx_ = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); + if (!ssl_ctx_) { + return std::unexpected{Error::CRYPTO}; + } + + if (ngtcp2_crypto_wolfssl_configure_client_context(ssl_ctx_) != 0) { + return std::unexpected{Error::CRYPTO}; + } + + ssl_ = wolfSSL_new(ssl_ctx_); + if (!ssl_) { + return std::unexpected{Error::CRYPTO}; + } + + if (wolfSSL_UseALPN(ssl_, const_cast(ALPN_LIST.data()), + ALPN_LIST.size(), + WOLFSSL_ALPN_FAILED_ON_MISMATCH) != WOLFSSL_SUCCESS) { + return std::unexpected{Error::CRYPTO}; + } + + wolfSSL_set_app_data(ssl_, &conn_ref_); + wolfSSL_set_connect_state(ssl_); + wolfSSL_set_quic_transport_version(ssl_, 0x39); + + ngtcp2_conn_set_tls_native_handle(conn_, ssl_); + + initialized_ = true; + + return {}; +} + +std::expected Endpoint::on_read(const NetworkPath &path, + std::span pkt, + const Context &ctx) { + auto ts = to_ngtcp2_tstamp(ctx.ts); + auto cpath = to_ngtcp2_path(path); + + auto rv = + ngtcp2_conn_read_pkt(conn_, &cpath, nullptr, pkt.data(), pkt.size(), ts); + if (rv != 0) { + if (rv == NGTCP2_ERR_RETRY) { + assert(ngtcp2_conn_is_server(conn_)); + + reset(); + + ngtcp2_version_cid vcid; + + auto rv = + ngtcp2_pkt_decode_version_cid(&vcid, pkt.data(), pkt.size(), CIDLEN); + if (rv != 0) { + return std::unexpected{Error::QUIC}; + } + + return send_retry(path, vcid); + } + + std::println(stderr, "ngtcp2_conn_read_pkt: {}", ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + ctx.endpoint->get_channel().schedule_timeout(ctx.ts); + + return {}; +} + +std::expected +Endpoint::send_retry(const NetworkPath &path, const ngtcp2_version_cid &vcid) { + ngtcp2_cid dcid, odcid; + + assert(vcid.scidlen <= NGTCP2_MAX_CIDLEN); + assert(vcid.dcidlen <= NGTCP2_MAX_CIDLEN); + + ngtcp2_cid_init(&dcid, vcid.scid, vcid.scidlen); + ngtcp2_cid_init(&odcid, vcid.dcid, vcid.dcidlen); + + ngtcp2_cid scid; + + scid.datalen = CIDLEN; + + if (auto rv = generate_secure_random({scid.data, scid.datalen}); !rv) { + return rv; + } + + std::array token; + + auto cpath = to_ngtcp2_path(path); + + auto tokenlen = ngtcp2_crypto_generate_retry_token2( + token.data(), static_secret.data(), static_secret.size(), vcid.version, + cpath.remote.addr, cpath.remote.addrlen, &scid, &odcid, + to_ngtcp2_tstamp(channel_.get_timestamp())); + if (tokenlen < 0) { + return std::unexpected{Error::QUIC}; + } + + std::array buf; + + auto nwrite = ngtcp2_crypto_write_retry(buf.data(), buf.size(), vcid.version, + &dcid, &scid, &odcid, token.data(), + as_unsigned(tokenlen)); + if (nwrite < 0) { + return std::unexpected{Error::QUIC}; + } + + if (nwrite) { + channel_.send_pkt(path, {buf.data(), as_unsigned(nwrite)}); + } + + return {}; +} + +std::expected Endpoint::on_write(const Context &ctx) { + if (auto rv = config_.on_write(conn_, ctx); !rv) { + return rv; + } + + auto next_expiry_ts = ngtcp2_conn_get_expiry2(conn_); + if (next_expiry_ts == UINT64_MAX) { + return {}; + } + + if (to_ngtcp2_tstamp(Timestamp::max()) < next_expiry_ts) { + return std::unexpected{Error::INTERNAL}; + } + + ctx.endpoint->get_channel().schedule_timeout(to_timestamp(next_expiry_ts)); + + return {}; +} + +std::expected Endpoint::on_timeout(const Context &ctx) { + auto rv = ngtcp2_conn_handle_expiry(conn_, to_ngtcp2_tstamp(ctx.ts)); + if (rv != 0) { + std::println(stderr, "ngtcp2_conn_handle_expiry: {}", ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + return on_write(ctx); +} + +NetworkPath to_network_path(const ngtcp2_path *path) { + NetworkPath res; + + res.local.set(path->local.addr); + res.remote.set(path->remote.addr); + + return res; +} + +ngtcp2_path to_ngtcp2_path(const NetworkPath &path) { + return { + .local = as_ngtcp2_addr(path.local), + .remote = as_ngtcp2_addr(path.remote), + }; +} + +NetworkPath NetworkPath::invert() const { + auto path = *this; + + std::swap(path.local, path.remote); + + return path; +} + +Channel::Channel(const LinkConfig &config) + : link_config_{config}, gen_{link_config_.seed} {} + +Channel::Channel(Channel &&other) noexcept + : link_config_{std::exchange(other.link_config_, {})}, + gen_{std::exchange(other.gen_, {})}, + tx_queue_{std::exchange(other.tx_queue_, {})}, + tx_queue_size_{std::exchange(other.tx_queue_size_, 0)}, + link_free_ts_{std::exchange(other.link_free_ts_, {})}, + queue_{std::exchange(other.queue_, {})}, + timeout_{std::exchange(other.timeout_, {})}, + ts_{std::exchange(other.ts_, {})} {} + +Channel &Channel::operator=(Channel &&other) noexcept { + link_config_ = std::exchange(other.link_config_, {}); + gen_ = std::exchange(other.gen_, {}); + tx_queue_ = std::exchange(other.tx_queue_, {}); + tx_queue_size_ = std::exchange(other.tx_queue_size_, 0); + link_free_ts_ = std::exchange(other.link_free_ts_, {}); + queue_ = std::exchange(other.queue_, {}); + timeout_ = std::exchange(other.timeout_, {}); + ts_ = std::exchange(other.ts_, {}); + + return *this; +} + +void Channel::send_pkt(const NetworkPath &path, std::span pkt) { + auto rate = link_config_.rate / 8; + + if (rate == 0) { + queue_.emplace(Event{ + .ts = ts_ + link_config_.delay, + .type = EVENT_TYPE_PKT, + .path = path, + .pkt = std::vector(std::ranges::begin(pkt), std::ranges::end(pkt)), + }); + + return; + } + + if (link_config_.limit && tx_queue_size_ + pkt.size() > link_config_.limit) { + return; + } + + auto departure_ts = std::max(ts_, link_free_ts_) + + Timestamp::duration{pkt.size() * NGTCP2_SECONDS / rate}; + + if (!decide_pkt_lost()) { + queue_.emplace(Event{ + .ts = departure_ts + link_config_.delay, + .type = EVENT_TYPE_PKT, + .path = path, + .pkt = std::vector(std::ranges::begin(pkt), std::ranges::end(pkt)), + }); + } + + tx_queue_.emplace_back(TxPacket{ + .departure_ts = departure_ts, + .size = pkt.size(), + }); + + tx_queue_size_ += pkt.size(); + link_free_ts_ = departure_ts; +} + +bool Channel::decide_pkt_lost() { + return std::uniform_real_distribution<>(0, 1.0)(gen_) < link_config_.loss; +} + +void Channel::pop_tx_queue() { + size_t n = 0; + + auto it = std::ranges::find_if(tx_queue_, [&n, this](const auto &pkt) { + if (pkt.departure_ts > ts_) { + return true; + } + + n += pkt.size; + + return false; + }); + + assert(tx_queue_size_ >= n); + + tx_queue_size_ -= n; + tx_queue_.erase(std::ranges::begin(tx_queue_), it); +} + +void Channel::schedule_timeout(Timestamp ts) { + timeout_ = std::min(timeout_, ts); +} + +Timestamp Channel::get_next_timestamp() const { + if (queue_.empty()) { + return timeout_; + } + + auto &top = queue_.top(); + + return std::min(timeout_, top.ts); +} + +Event Channel::get_next_event() { + if (!queue_.empty() && queue_.top().ts <= timeout_) { + auto &top = const_cast(queue_.top()); + + auto ev = Event{ + .ts = top.ts, + .type = top.type, + .path = top.path, + .pkt = std::move(top.pkt), + }; + + queue_.pop(); + + return ev; + } + + return Event{ + .ts = std::exchange(timeout_, Timestamp::max()), + .type = EVENT_TYPE_TIMEOUT, + }; +} + +void Channel::run_eventcb(Timestamp ts) { + if (link_config_.eventcb) { + link_config_.eventcb(ts, link_config_); + } +} + +Simulator::Simulator(Endpoint client, Endpoint server) + : client_{std::move(client)}, server_{std::move(server)} {} + +Simulator::Simulator(Simulator &&other) noexcept + : client_{std::exchange(client_, {})}, + server_{std::exchange(server_, {})}, + max_events_{std::exchange(other.max_events_, 0)} {} + +Simulator &Simulator::operator=(Simulator &&other) noexcept { + client_ = std::exchange(other.client_, {}); + server_ = std::exchange(other.server_, {}); + max_events_ = std::exchange(other.max_events_, 0); + + return *this; +} + +std::optional> Simulator::get_next_event() { + auto &client_chan = client_.get_channel(); + auto &server_chan = server_.get_channel(); + + auto client_next_ts = client_chan.get_next_timestamp(); + auto server_next_ts = server_chan.get_next_timestamp(); + + if (client_next_ts == Timestamp::max() && + server_next_ts == Timestamp::max()) { + return {}; + } + + if (client_next_ts <= server_next_ts) { + auto ev = client_chan.get_next_event(); + return {{std::move(ev), client_}}; + } + + auto ev = server_chan.get_next_event(); + + return {{std::move(ev), server_}}; +} + +std::expected Simulator::run() { + if (client_.get_initialized()) { + return std::unexpected{Error::INTERNAL}; + } + + if (auto rv = client_.setup_client(&server_.get_endpoint_config().local_addr); + !rv) { + return rv; + } + + auto ts = Timestamp{}; + auto &client_chan = client_.get_channel(); + auto &server_chan = server_.get_channel(); + + client_chan.schedule_timeout(ts); + + size_t k = 0; + + for (; k < max_events_; ++k) { + auto maybe_event = get_next_event(); + if (!maybe_event) { + break; + } + + auto &[event, ep] = *maybe_event; + + assert(ts <= event.ts); + + ts = event.ts; + + client_chan.set_timestamp(ts); + server_chan.set_timestamp(ts); + + client_chan.pop_tx_queue(); + server_chan.pop_tx_queue(); + + client_chan.run_eventcb(ts); + server_chan.run_eventcb(ts); + + switch (event.type) { + case EVENT_TYPE_TIMEOUT: { + auto ctx = Context{ + .sim = this, + .ts = ts, + .endpoint = &ep, + }; + + if (auto rv = ep.on_timeout(ctx); !rv) { + return rv; + } + + break; + } + case EVENT_TYPE_PKT: + if (auto rv = deliver_pkt(ep, event.path.invert(), event.pkt, ts); !rv) { + return rv; + } + + break; + } + } + + if (k == max_events_) { + return std::unexpected{Error::INTERNAL}; + } + + return {}; +} + +Endpoint &Simulator::get_opposite_endpoint(const Endpoint &ep) { + return &ep == &client_ ? server_ : client_; +} + +std::expected Simulator::deliver_pkt(Endpoint &remote_ep, + const NetworkPath &path, + std::span pkt, + Timestamp ts) { + auto &local_ep = get_opposite_endpoint(remote_ep); + + if (!local_ep.get_initialized() && local_ep.get_endpoint_config().server) { + ngtcp2_version_cid vcid; + + auto rv = + ngtcp2_pkt_decode_version_cid(&vcid, pkt.data(), pkt.size(), CIDLEN); + if (rv != 0) { + return std::unexpected{Error::QUIC}; + } + + ngtcp2_pkt_hd hd; + + if (ngtcp2_accept(&hd, pkt.data(), pkt.size()) != 0) { + return {}; + } + + ngtcp2_cid odcid; + std::optional token_params; + + if (hd.tokenlen) { + auto cpath = to_ngtcp2_path(path); + auto delay = local_ep.get_endpoint_config().link.delay; + auto timeout = delay * 2 * 2; + + auto rv = ngtcp2_crypto_verify_retry_token2( + &odcid, hd.token, hd.tokenlen, static_secret.data(), + static_secret.size(), vcid.version, cpath.remote.addr, + cpath.remote.addrlen, &hd.dcid, to_ngtcp2_duration(timeout), + to_ngtcp2_tstamp(ts)); + if (rv != 0) { + return std::unexpected{Error::QUIC}; + } + + token_params = TokenParams{ + .original_dcid = &odcid, + .retry_scid = &hd.dcid, + .token = {hd.token, hd.tokenlen}, + }; + } + + if (auto rv = local_ep.setup_server( + {vcid.dcid, vcid.dcidlen}, {vcid.scid, vcid.scidlen}, vcid.version, + &remote_ep.get_endpoint_config().local_addr, token_params); + !rv) { + return rv; + } + } + + auto ctx = Context{ + .sim = this, + .ts = ts, + .endpoint = &local_ep, + }; + + return local_ep.on_read(path, pkt, ctx); +} + +void HandshakeApp::configure(EndpointConfig &config) { + auto handshake_confirmed = [](ngtcp2_conn *conn, void *user_data) { + auto app = static_cast(user_data); + + app->handshake_confirmed(); + + return 0; + }; + + if (config.server) { + config.callbacks.handshake_completed = handshake_confirmed; + } else { + config.callbacks.handshake_confirmed = handshake_confirmed; + } + + config.on_write = [](ngtcp2_conn *conn, + const Context &ctx) -> std::expected { + std::array buf; + + auto ts = to_ngtcp2_tstamp(ctx.ts); + + ngtcp2_path_storage ps; + ngtcp2_path_storage_zero(&ps); + + auto nwrite = ngtcp2_conn_write_pkt(conn, &ps.path, nullptr, buf.data(), + buf.size(), ts); + if (nwrite < 0) { + std::println(stderr, "ngtcp2_conn_write_pkt: {}", + ngtcp2_strerror(static_cast(nwrite))); + return std::unexpected{Error::QUIC}; + } + + if (nwrite == 0) { + return {}; + } + + ngtcp2_conn_update_pkt_tx_time(conn, ts); + + ctx.endpoint->get_channel().send_pkt( + to_network_path(&ps.path), {buf.data(), static_cast(nwrite)}); + + return {}; + }; + + config.user_data = this; +} + +UniStreamApp::UniStreamApp(uint64_t max_bytes) : max_bytes_{max_bytes} {} + +namespace { +std::array nulldata; +} // namespace + +void UniStreamApp::configure(EndpointConfig &config) { + config.callbacks.stream_close = [](ngtcp2_conn *conn, uint32_t flags, + int64_t stream_id, uint64_t app_error_code, + void *user_data, void *stream_user_data) { + auto app = static_cast(user_data); + + app->stream_close(conn, stream_id); + + return 0; + }; + + config.callbacks.extend_max_local_streams_uni = + [](ngtcp2_conn *conn, uint64_t max_streams, void *user_data) { + auto app = static_cast(user_data); + + if (!app->extend_max_local_streams_uni(conn)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; + }; + + config.on_write = [this](ngtcp2_conn *conn, const Context &ctx) { + return on_write(conn, ctx); + }; + + config.user_data = this; +} + +uint64_t UniStreamApp::compute_goodput() const { + auto d = get_transmit_duration(); + if (d == Timestamp::duration::zero()) { + return 0; + } + + return static_cast(static_cast(bytes_sent_) * + NGTCP2_SECONDS / static_cast(d.count()) * + 8); +} + +void UniStreamApp::stream_close(ngtcp2_conn *conn, int64_t stream_id) { + if (stream_id_ != stream_id) { + return; + } + + if (is_all_bytes_sent()) { + end_ts_ = to_timestamp(ngtcp2_conn_get_timestamp(conn)); + } +} + +std::expected +UniStreamApp::extend_max_local_streams_uni(ngtcp2_conn *conn) { + if (stream_id_ != -1) { + return {}; + } + + int64_t stream_id; + + auto rv = ngtcp2_conn_open_uni_stream(conn, &stream_id, nullptr); + if (rv != 0) { + std::println(stderr, "ngtcp2_conn_open_uni_stream: {}", + ngtcp2_strerror(rv)); + return std::unexpected{Error::QUIC}; + } + + stream_id_ = stream_id; + start_ts_ = to_timestamp(ngtcp2_conn_get_timestamp(conn)); + + return {}; +} + +std::expected UniStreamApp::on_write(ngtcp2_conn *conn, + const Context &ctx) { + std::array buf; + + int64_t stream_id; + ngtcp2_vec vec; + size_t veccnt; + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_NONE; + + if (stream_id_ != -1 && max_bytes_ > bytes_sent_) { + stream_id = stream_id_; + vec.base = nulldata.data(); + vec.len = static_cast( + std::min(static_cast(buf.size()), max_bytes_ - bytes_sent_)); + veccnt = 1; + + if (bytes_sent_ + vec.len == max_bytes_) { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + } else { + stream_id = -1; + veccnt = 0; + } + + auto ts = to_ngtcp2_tstamp(ctx.ts); + + ngtcp2_path_storage ps; + ngtcp2_path_storage_zero(&ps); + + ngtcp2_ssize ndatalen; + + auto nwrite = + ngtcp2_conn_writev_stream(conn, &ps.path, nullptr, buf.data(), buf.size(), + &ndatalen, flags, stream_id, &vec, veccnt, ts); + if (nwrite < 0) { + if (nwrite == NGTCP2_ERR_STREAM_DATA_BLOCKED) { + return {}; + } + + std::println(stderr, "ngtcp2_conn_writev_stream: {}", + ngtcp2_strerror(static_cast(nwrite))); + + return std::unexpected{Error::QUIC}; + } + + if (nwrite == 0) { + return {}; + } + + if (ndatalen > 0) { + bytes_sent_ += static_cast(ndatalen); + } + + ngtcp2_conn_update_pkt_tx_time(conn, ts); + + ctx.endpoint->get_channel().send_pkt( + to_network_path(&ps.path), {buf.data(), static_cast(nwrite)}); + + return {}; +} + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/sim.h b/deps/ngtcp2/ngtcp2/examples/sim.h new file mode 100644 index 00000000000000..98e72791b6f1b5 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/sim.h @@ -0,0 +1,325 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SIM_H +#define SIM_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "network.h" +#include "shared.h" + +namespace ngtcp2 { +inline constexpr auto MAX_UDP_PAYLOAD_SIZE = 1500UZ; + +using Timestamp = + std::chrono::time_point; + +[[nodiscard]] constexpr ngtcp2_tstamp to_ngtcp2_tstamp(Timestamp ts) { + return static_cast(ts.time_since_epoch().count()); +} + +[[nodiscard]] constexpr ngtcp2_duration +to_ngtcp2_duration(Timestamp::duration d) { + return static_cast(d.count()); +} + +[[nodiscard]] constexpr Timestamp to_timestamp(ngtcp2_tstamp ts) { + return Timestamp{Timestamp::duration{ts}}; +} + +class Simulator; +class Endpoint; + +struct Context { + Simulator *sim; + Timestamp ts; + Endpoint *endpoint; +}; + +[[nodiscard]] constexpr unsigned long long +operator""_kbps(unsigned long long k) noexcept { + return k * 1'000; +} + +[[nodiscard]] constexpr unsigned long long +operator""_mbps(unsigned long long m) noexcept { + return m * 1'000'000; +} + +[[nodiscard]] constexpr unsigned long long +operator""_gbps(unsigned long long g) noexcept { + return g * 1'000'000'000; +} + +struct LinkConfig { + // compute_expected_goodput computes the expected goodput with the + // given |rtt| in bits per second. + uint64_t compute_expected_goodput(Timestamp::duration rtt) const; + + // delay is the one-way link delay. + Timestamp::duration delay; + // rate is the bandwidth of this link measured in bits per second + // (e.g., 10_mbps). + uint64_t rate{}; + // limit is the maximum queue length of the outgoing packet measured + // in bytes. + uint64_t limit{}; + // loss is the probability of losing a packet. + double loss{}; + // seed is a seed value for the random number generator. + std::mt19937::result_type seed{}; + // eventcb is an optional callback that is invoked before processing + // each event. In this callback, LinkConfig can be dynamically + // adjusted to simulate dynamic network conditions. + std::function eventcb; +}; + +struct EndpointConfig { + bool server{}; + ngtcp2_callbacks callbacks{}; + ngtcp2_settings settings{}; + ngtcp2_transport_params params{}; + ngtcp2_addr local_addr{}; + void *user_data{}; + LinkConfig link; + + std::function(ngtcp2_conn *, const Context &)> + on_write; +}; + +ngtcp2_callbacks default_client_callbacks(); + +ngtcp2_callbacks default_server_callbacks(); + +ngtcp2_settings default_client_settings(); + +ngtcp2_settings default_server_settings(); + +ngtcp2_transport_params default_client_transport_params(); + +ngtcp2_transport_params default_server_transport_params(); + +ngtcp2_addr default_client_addr(); + +ngtcp2_addr default_server_addr(); + +EndpointConfig default_client_endpoint_config(); + +EndpointConfig default_server_endpoint_config(); + +struct NetworkPath { + NetworkPath invert() const; + + Address local{}; + Address remote{}; +}; + +NetworkPath to_network_path(const ngtcp2_path *path); + +ngtcp2_path to_ngtcp2_path(const NetworkPath &path); + +enum EventType { + EVENT_TYPE_TIMEOUT, + EVENT_TYPE_PKT, +}; + +struct Event { + Timestamp ts; + EventType type; + + NetworkPath path; + std::vector pkt; +}; + +constexpr bool operator>(const Event &lhs, const Event &rhs) { + return lhs.ts > rhs.ts; +} + +struct TxPacket { + Timestamp departure_ts; + size_t size; +}; + +class Channel { +public: + Channel() = default; + Channel(const LinkConfig &config); + Channel(const Channel &) = delete; + Channel(Channel &&) noexcept; + + Channel &operator=(const Channel &) = delete; + Channel &operator=(Channel &&) noexcept; + + void send_pkt(const NetworkPath &path, std::span pkt); + void schedule_timeout(Timestamp ts); + void set_timestamp(Timestamp ts) { ts_ = ts; } + Timestamp get_timestamp() const { return ts_; } + Timestamp get_next_timestamp() const; + Event get_next_event(); + void pop_tx_queue(); + void run_eventcb(Timestamp ts); + +private: + bool decide_pkt_lost(); + + LinkConfig link_config_; + std::mt19937 gen_; + std::deque tx_queue_; + size_t tx_queue_size_{}; + Timestamp link_free_ts_; + using EventQueue = + std::priority_queue, std::greater>; + EventQueue queue_; + Timestamp timeout_{Timestamp::max()}; + Timestamp ts_{}; +}; + +struct TokenParams { + const ngtcp2_cid *original_dcid; + const ngtcp2_cid *retry_scid; + std::span token; +}; + +class Endpoint { +public: + Endpoint(); + explicit Endpoint(const EndpointConfig &config); + Endpoint(Endpoint &&endpoint) noexcept; + Endpoint(const Endpoint &) = delete; + ~Endpoint(); + + Endpoint &operator=(const Endpoint &) = delete; + Endpoint &operator=(Endpoint &&) noexcept; + + std::expected setup_client(const ngtcp2_addr *remote_addr); + std::expected + setup_server(std::span original_dcid, + std::span client_scid, uint32_t version, + const ngtcp2_addr *remote_addr, + std::optional token_params); + ngtcp2_conn *get_conn() const { return conn_; } + bool get_initialized() const { return initialized_; } + const EndpointConfig &get_endpoint_config() const { return config_; } + std::expected on_read(const NetworkPath &path, + std::span pkt, + const Context &ctx); + std::expected on_write(const Context &ctx); + std::expected on_timeout(const Context &ctx); + Channel &get_channel() { return channel_; } + +private: + void reset(); + std::expected send_retry(const NetworkPath &path, + const ngtcp2_version_cid &vcid); + + EndpointConfig config_; + WOLFSSL_CTX *ssl_ctx_{}; + WOLFSSL *ssl_{}; + ngtcp2_conn *conn_{}; + ngtcp2_crypto_conn_ref conn_ref_{}; + Channel channel_; + bool initialized_{}; +}; + +class Simulator { +public: + Simulator(Endpoint client, Endpoint server); + Simulator(const Simulator &) = delete; + Simulator(Simulator &&) noexcept; + + Simulator &operator=(const Simulator &) = delete; + Simulator &operator=(Simulator &&) noexcept; + + std::expected run(); + void set_max_events(size_t n) { max_events_ = n; } + +private: + Endpoint &get_opposite_endpoint(const Endpoint &ep); + std::optional> get_next_event(); + std::expected deliver_pkt(Endpoint &remote_ep, + const NetworkPath &path, + std::span pkt, + Timestamp ts); + + Endpoint client_; + Endpoint server_; + size_t max_events_{1'000'000}; +}; + +class HandshakeApp { +public: + void configure(EndpointConfig &config); + + bool get_handshake_confirmed() const { return handshake_confirmed_; } + +private: + void handshake_confirmed() { handshake_confirmed_ = true; } + + bool handshake_confirmed_{}; +}; + +class UniStreamApp { +public: + UniStreamApp(uint64_t max_bytes); + + void configure(EndpointConfig &config); + uint64_t get_bytes_sent() const { return bytes_sent_; } + Timestamp::duration get_transmit_duration() const { + return end_ts_ - start_ts_; + } + bool is_all_bytes_sent() const { return bytes_sent_ == max_bytes_; } + // compute_goodput computes goodput in bits per second. + uint64_t compute_goodput() const; + +private: + void stream_close(ngtcp2_conn *conn, int64_t stream_id); + std::expected extend_max_local_streams_uni(ngtcp2_conn *conn); + std::expected on_write(ngtcp2_conn *conn, const Context &ctx); + + int64_t stream_id_{-1}; + uint64_t max_bytes_{}; + uint64_t bytes_sent_{}; + Timestamp start_ts_{Timestamp::max()}; + Timestamp end_ts_{Timestamp::max()}; +}; + +} // namespace ngtcp2 + +#endif // !defined(SIM_H) diff --git a/deps/ngtcp2/ngtcp2/examples/sim_test.cc b/deps/ngtcp2/ngtcp2/examples/sim_test.cc new file mode 100644 index 00000000000000..b7e6d69e36d67f --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/sim_test.cc @@ -0,0 +1,138 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "sim_test.h" + +#include "sim.h" +#include "util.h" + +using namespace std::literals; + +namespace ngtcp2 { + +namespace { +const MunitTest tests[]{ + munit_void_test(test_sim_handshake), + munit_void_test(test_sim_unistream), + munit_test_end(), +}; +} // namespace + +const MunitSuite sim_suite{ + .prefix = "/sim", + .tests = tests, +}; + +void test_sim_handshake(void) { + struct Test { + const char *name; + Timestamp::duration delay; + }; + + auto tests = std::to_array({ + { + .name = "short delay", + .delay = 15ms, + }, + { + .name = "long delay", + .delay = 1h, + }, + }); + + for (auto &t : tests) { + munit_logf(MUNIT_LOG_INFO, "testcase: %s", t.name); + + auto link = LinkConfig{ + .delay = t.delay, + }; + + HandshakeApp clapp; + auto cl = default_client_endpoint_config(); + clapp.configure(cl); + cl.link = link; + + HandshakeApp svapp; + auto sv = default_server_endpoint_config(); + svapp.configure(sv); + sv.link = link; + + auto rv = Simulator{Endpoint(cl), Endpoint(sv)}.run(); + + assert_true(rv.has_value()); + assert_true(clapp.get_handshake_confirmed()); + assert_true(svapp.get_handshake_confirmed()); + } +} + +void test_sim_unistream(void) { + struct Test { + const char *name; + double loss; + }; + + auto tests = std::to_array({ + { + .name = "no loss", + }, + { + .name = "1% loss", + .loss = 0.01, + }, + }); + + for (auto &t : tests) { + munit_logf(MUNIT_LOG_INFO, "testcase: %s", t.name); + + auto link = LinkConfig{ + .delay = 15ms, + .rate = 10_mbps, + .limit = MAX_UDP_PAYLOAD_SIZE * 25, + .loss = t.loss, + .seed = munit_rand_uint32(), + }; + + HandshakeApp clapp; + auto cl = default_client_endpoint_config(); + clapp.configure(cl); + cl.params.initial_max_streams_uni = 1; + cl.params.initial_max_stream_data_uni = 6_m; + cl.params.initial_max_data = 6_m; + cl.link = link; + + UniStreamApp svapp(10_m); + auto sv = default_server_endpoint_config(); + svapp.configure(sv); + sv.link = link; + + auto rv = Simulator{Endpoint(cl), Endpoint(sv)}.run(); + + assert_true(rv.has_value()); + assert_true(svapp.is_all_bytes_sent()); + assert_uint64(link.compute_expected_goodput(link.delay * 2), <=, + svapp.compute_goodput()); + } +} + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/sim_test.h b/deps/ngtcp2/ngtcp2/examples/sim_test.h new file mode 100644 index 00000000000000..ea94520caaa160 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/sim_test.h @@ -0,0 +1,45 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SIM_TEST_H +#define SIM_TEST_H + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif // defined(HAVE_CONFIG_H) + +#define MUNIT_ENABLE_ASSERT_ALIASES + +#include "munitxx.h" + +namespace ngtcp2 { + +extern const MunitSuite sim_suite; + +munit_void_test_decl(test_sim_handshake) +munit_void_test_decl(test_sim_unistream) + +} // namespace ngtcp2 + +#endif // !defined(SIM_TEST_H) diff --git a/deps/ngtcp2/ngtcp2/examples/simpleclient.c b/deps/ngtcp2/ngtcp2/examples/simpleclient.c new file mode 100644 index 00000000000000..65ab3560552b02 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/simpleclient.c @@ -0,0 +1,662 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include +#endif /* defined(HAVE_CONFIG_H) */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include + +#define REMOTE_HOST "127.0.0.1" +#define REMOTE_PORT "4433" +#define ALPN "\xAhq-interop" +#define MESSAGE "GET /\r\n" + +/* + * Example 1: Handshake with www.google.com + * + * #define REMOTE_HOST "www.google.com" + * #define REMOTE_PORT "443" + * #define ALPN "\x2h3" + * + * and undefine MESSAGE macro. + */ + +static uint64_t timestamp(void) { + struct timespec tp; + + if (clock_gettime(CLOCK_MONOTONIC, &tp) != 0) { + fprintf(stderr, "clock_gettime: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + + return (uint64_t)tp.tv_sec * NGTCP2_SECONDS + (uint64_t)tp.tv_nsec; +} + +static int create_sock(struct sockaddr *addr, socklen_t *paddrlen, + const char *host, const char *port) { + struct addrinfo hints = {0}; + struct addrinfo *res, *rp; + int rv; + int fd = -1; + + hints.ai_flags = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + + rv = getaddrinfo(host, port, &hints, &res); + if (rv != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); + return -1; + } + + for (rp = res; rp; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) { + continue; + } + + break; + } + + if (fd == -1) { + goto end; + } + + *paddrlen = rp->ai_addrlen; + memcpy(addr, rp->ai_addr, rp->ai_addrlen); + +end: + freeaddrinfo(res); + + return fd; +} + +static int connect_sock(struct sockaddr *local_addr, socklen_t *plocal_addrlen, + int fd, const struct sockaddr *remote_addr, + size_t remote_addrlen) { + socklen_t len; + + if (connect(fd, remote_addr, (socklen_t)remote_addrlen) != 0) { + fprintf(stderr, "connect: %s\n", strerror(errno)); + return -1; + } + + len = *plocal_addrlen; + + if (getsockname(fd, local_addr, &len) == -1) { + fprintf(stderr, "getsockname: %s\n", strerror(errno)); + return -1; + } + + *plocal_addrlen = len; + + return 0; +} + +struct client { + ngtcp2_crypto_conn_ref conn_ref; + int fd; + struct sockaddr_storage local_addr; + socklen_t local_addrlen; + SSL_CTX *ssl_ctx; + SSL *ssl; + ngtcp2_conn *conn; + + struct { + int64_t stream_id; + const uint8_t *data; + size_t datalen; + size_t nwrite; + } stream; + + ngtcp2_ccerr last_error; + + ev_io rev; + ev_timer timer; +}; + +static int numeric_host_family(const char *hostname, int family) { + uint8_t dst[sizeof(struct in6_addr)]; + return inet_pton(family, hostname, dst) == 1; +} + +static int numeric_host(const char *hostname) { + return numeric_host_family(hostname, AF_INET) || + numeric_host_family(hostname, AF_INET6); +} + +static int client_ssl_init(struct client *c) { + c->ssl_ctx = SSL_CTX_new(TLS_client_method()); + if (!c->ssl_ctx) { + fprintf(stderr, "SSL_CTX_new: %s\n", + ERR_error_string(ERR_get_error(), NULL)); + return -1; + } + + if (ngtcp2_crypto_quictls_configure_client_context(c->ssl_ctx) != 0) { + fprintf(stderr, "ngtcp2_crypto_quictls_configure_client_context failed\n"); + return -1; + } + + c->ssl = SSL_new(c->ssl_ctx); + if (!c->ssl) { + fprintf(stderr, "SSL_new: %s\n", ERR_error_string(ERR_get_error(), NULL)); + return -1; + } + + SSL_set_app_data(c->ssl, &c->conn_ref); + SSL_set_connect_state(c->ssl); + SSL_set_alpn_protos(c->ssl, (const unsigned char *)ALPN, sizeof(ALPN) - 1); + if (!numeric_host(REMOTE_HOST)) { + SSL_set_tlsext_host_name(c->ssl, REMOTE_HOST); + } + + return 0; +} + +static void rand_cb(uint8_t *dest, size_t destlen, + const ngtcp2_rand_ctx *rand_ctx) { + int rv; + (void)rand_ctx; + + rv = RAND_bytes(dest, (int)destlen); + if (rv != 1) { + assert(0); + abort(); + } +} + +static int get_new_connection_id_cb(ngtcp2_conn *conn, ngtcp2_cid *cid, + ngtcp2_stateless_reset_token *token, + size_t cidlen, void *user_data) { + (void)conn; + (void)user_data; + + if (RAND_bytes(cid->data, (int)cidlen) != 1) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + + if (RAND_bytes(token->data, sizeof(token->data)) != 1) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int extend_max_local_streams_bidi(ngtcp2_conn *conn, + uint64_t max_streams, + void *user_data) { +#ifdef MESSAGE + struct client *c = user_data; + int rv; + int64_t stream_id; + (void)max_streams; + + if (c->stream.stream_id != -1) { + return 0; + } + + rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL); + if (rv != 0) { + return 0; + } + + c->stream.stream_id = stream_id; + c->stream.data = (const uint8_t *)MESSAGE; + c->stream.datalen = sizeof(MESSAGE) - 1; + + return 0; +#else /* !defined(MESSAGE) */ + (void)conn; + (void)max_streams; + (void)user_data; + + return 0; +#endif /* !defined(MESSAGE) */ +} + +static void log_printf(void *user_data, const char *fmt, ...) { + va_list ap; + (void)user_data; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + fprintf(stderr, "\n"); +} + +static int client_quic_init(struct client *c, + const struct sockaddr *remote_addr, + socklen_t remote_addrlen, + const struct sockaddr *local_addr, + socklen_t local_addrlen) { + ngtcp2_path path = { + .local = + { + .addr = (struct sockaddr *)local_addr, + .addrlen = local_addrlen, + }, + .remote = + { + .addr = (struct sockaddr *)remote_addr, + .addrlen = remote_addrlen, + }, + }; + ngtcp2_callbacks callbacks = { + .client_initial = ngtcp2_crypto_client_initial_cb, + .recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb, + .encrypt = ngtcp2_crypto_encrypt_cb, + .decrypt = ngtcp2_crypto_decrypt_cb, + .hp_mask = ngtcp2_crypto_hp_mask_cb, + .recv_retry = ngtcp2_crypto_recv_retry_cb, + .extend_max_local_streams_bidi = extend_max_local_streams_bidi, + .rand = rand_cb, + .update_key = ngtcp2_crypto_update_key_cb, + .delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb, + .delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + .version_negotiation = ngtcp2_crypto_version_negotiation_cb, + .get_new_connection_id2 = get_new_connection_id_cb, + .get_path_challenge_data2 = ngtcp2_crypto_get_path_challenge_data2_cb, + }; + ngtcp2_cid dcid, scid; + ngtcp2_settings settings; + ngtcp2_transport_params params; + int rv; + + dcid.datalen = NGTCP2_MIN_INITIAL_DCIDLEN; + if (RAND_bytes(dcid.data, (int)dcid.datalen) != 1) { + fprintf(stderr, "RAND_bytes failed\n"); + return -1; + } + + scid.datalen = 8; + if (RAND_bytes(scid.data, (int)scid.datalen) != 1) { + fprintf(stderr, "RAND_bytes failed\n"); + return -1; + } + + ngtcp2_settings_default(&settings); + + settings.initial_ts = timestamp(); + settings.log_printf = log_printf; + + ngtcp2_transport_params_default(¶ms); + + params.initial_max_streams_uni = 3; + params.initial_max_stream_data_bidi_local = 128 * 1024; + params.initial_max_data = 1024 * 1024; + + rv = + ngtcp2_conn_client_new(&c->conn, &dcid, &scid, &path, NGTCP2_PROTO_VER_V1, + &callbacks, &settings, ¶ms, NULL, c); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_client_new: %s\n", ngtcp2_strerror(rv)); + return -1; + } + + ngtcp2_conn_set_tls_native_handle(c->conn, c->ssl); + + return 0; +} + +static int client_read(struct client *c) { + uint8_t buf[65536]; + struct sockaddr_storage addr; + struct iovec iov = { + .iov_base = buf, + .iov_len = sizeof(buf), + }; + struct msghdr msg = {0}; + ssize_t nread; + ngtcp2_path path; + ngtcp2_pkt_info pi = {0}; + int rv; + + msg.msg_name = &addr; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + for (;;) { + msg.msg_namelen = sizeof(addr); + + nread = recvmsg(c->fd, &msg, MSG_DONTWAIT); + + if (nread == -1) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + fprintf(stderr, "recvmsg: %s\n", strerror(errno)); + } + + break; + } + + path.local.addrlen = c->local_addrlen; + path.local.addr = (struct sockaddr *)&c->local_addr; + path.remote.addrlen = msg.msg_namelen; + path.remote.addr = msg.msg_name; + + rv = ngtcp2_conn_read_pkt(c->conn, &path, &pi, buf, (size_t)nread, + timestamp()); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_read_pkt: %s\n", ngtcp2_strerror(rv)); + if (!c->last_error.error_code) { + if (rv == NGTCP2_ERR_CRYPTO) { + ngtcp2_ccerr_set_tls_alert( + &c->last_error, ngtcp2_conn_get_tls_alert2(c->conn), NULL, 0); + } else { + ngtcp2_ccerr_set_liberr(&c->last_error, rv, NULL, 0); + } + } + return -1; + } + } + + return 0; +} + +static int client_send_packet(struct client *c, const uint8_t *data, + size_t datalen) { + struct iovec iov = { + .iov_base = (uint8_t *)data, + .iov_len = datalen, + }; + struct msghdr msg = {0}; + ssize_t nwrite; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + do { + nwrite = sendmsg(c->fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + fprintf(stderr, "sendmsg: %s\n", strerror(errno)); + + return -1; + } + + return 0; +} + +static size_t client_get_message(struct client *c, int64_t *pstream_id, + int *pfin, ngtcp2_vec *datav, + size_t datavcnt) { + if (datavcnt == 0) { + return 0; + } + + if (c->stream.stream_id != -1 && c->stream.nwrite < c->stream.datalen) { + *pstream_id = c->stream.stream_id; + *pfin = 1; + datav->base = (uint8_t *)c->stream.data + c->stream.nwrite; + datav->len = c->stream.datalen - c->stream.nwrite; + return 1; + } + + *pstream_id = -1; + *pfin = 0; + datav->base = NULL; + datav->len = 0; + + return 0; +} + +static int client_write_streams(struct client *c) { + ngtcp2_tstamp ts = timestamp(); + ngtcp2_pkt_info pi; + ngtcp2_ssize nwrite; + uint8_t buf[1452]; + ngtcp2_path_storage ps; + ngtcp2_vec datav; + size_t datavcnt; + int64_t stream_id; + ngtcp2_ssize wdatalen; + uint32_t flags; + int fin; + + ngtcp2_path_storage_zero(&ps); + + for (;;) { + datavcnt = client_get_message(c, &stream_id, &fin, &datav, 1); + + flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + if (fin) { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + nwrite = ngtcp2_conn_writev_stream(c->conn, &ps.path, &pi, buf, sizeof(buf), + &wdatalen, flags, stream_id, &datav, + datavcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_WRITE_MORE: + c->stream.nwrite += (size_t)wdatalen; + continue; + default: + fprintf(stderr, "ngtcp2_conn_writev_stream: %s\n", + ngtcp2_strerror((int)nwrite)); + ngtcp2_ccerr_set_liberr(&c->last_error, (int)nwrite, NULL, 0); + return -1; + } + } + + if (nwrite == 0) { + return 0; + } + + if (wdatalen > 0) { + c->stream.nwrite += (size_t)wdatalen; + } + + if (client_send_packet(c, buf, (size_t)nwrite) != 0) { + break; + } + } + + return 0; +} + +static int client_write(struct client *c) { + ngtcp2_tstamp expiry, now; + ev_tstamp t; + + if (client_write_streams(c) != 0) { + return -1; + } + + expiry = ngtcp2_conn_get_expiry2(c->conn); + now = timestamp(); + + t = expiry < now ? 1e-9 : (ev_tstamp)(expiry - now) / NGTCP2_SECONDS; + + c->timer.repeat = t; + ev_timer_again(EV_DEFAULT, &c->timer); + + return 0; +} + +static int client_handle_expiry(struct client *c) { + int rv = ngtcp2_conn_handle_expiry(c->conn, timestamp()); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_handle_expiry: %s\n", ngtcp2_strerror(rv)); + return -1; + } + + return 0; +} + +static void client_close(struct client *c) { + ngtcp2_ssize nwrite; + ngtcp2_pkt_info pi; + ngtcp2_path_storage ps; + uint8_t buf[1280]; + + if (ngtcp2_conn_in_closing_period2(c->conn) || + ngtcp2_conn_in_draining_period2(c->conn)) { + goto fin; + } + + ngtcp2_path_storage_zero(&ps); + + nwrite = ngtcp2_conn_write_connection_close( + c->conn, &ps.path, &pi, buf, sizeof(buf), &c->last_error, timestamp()); + if (nwrite < 0) { + fprintf(stderr, "ngtcp2_conn_write_connection_close: %s\n", + ngtcp2_strerror((int)nwrite)); + goto fin; + } + + client_send_packet(c, buf, (size_t)nwrite); + +fin: + ev_break(EV_DEFAULT, EVBREAK_ALL); +} + +static void read_cb(struct ev_loop *loop, ev_io *w, int revents) { + struct client *c = w->data; + (void)loop; + (void)revents; + + if (client_read(c) != 0) { + client_close(c); + return; + } + + if (client_write(c) != 0) { + client_close(c); + } +} + +static void timer_cb(struct ev_loop *loop, ev_timer *w, int revents) { + struct client *c = w->data; + (void)loop; + (void)revents; + + if (client_handle_expiry(c) != 0) { + client_close(c); + return; + } + + if (client_write(c) != 0) { + client_close(c); + } +} + +static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { + struct client *c = conn_ref->user_data; + return c->conn; +} + +static int client_init(struct client *c) { + struct sockaddr_storage remote_addr, local_addr; + socklen_t remote_addrlen, local_addrlen = sizeof(local_addr); + + *c = (struct client){0}; + + ngtcp2_ccerr_default(&c->last_error); + + c->fd = create_sock((struct sockaddr *)&remote_addr, &remote_addrlen, + REMOTE_HOST, REMOTE_PORT); + if (c->fd == -1) { + return -1; + } + + if (connect_sock((struct sockaddr *)&local_addr, &local_addrlen, c->fd, + (struct sockaddr *)&remote_addr, remote_addrlen) != 0) { + return -1; + } + + memcpy(&c->local_addr, &local_addr, sizeof(c->local_addr)); + c->local_addrlen = local_addrlen; + + if (client_ssl_init(c) != 0) { + return -1; + } + + if (client_quic_init(c, (struct sockaddr *)&remote_addr, remote_addrlen, + (struct sockaddr *)&local_addr, local_addrlen) != 0) { + return -1; + } + + c->stream.stream_id = -1; + + c->conn_ref.get_conn = get_conn; + c->conn_ref.user_data = c; + + ev_io_init(&c->rev, read_cb, c->fd, EV_READ); + c->rev.data = c; + ev_io_start(EV_DEFAULT, &c->rev); + + ev_timer_init(&c->timer, timer_cb, 0., 0.); + c->timer.data = c; + + return 0; +} + +static void client_free(struct client *c) { + ngtcp2_conn_del(c->conn); + SSL_free(c->ssl); + SSL_CTX_free(c->ssl_ctx); +} + +int main(void) { + struct client c; + + srandom((unsigned int)timestamp()); + + if (client_init(&c) != 0) { + exit(EXIT_FAILURE); + } + + if (client_write(&c) != 0) { + exit(EXIT_FAILURE); + } + + ev_run(EV_DEFAULT, 0); + + client_free(&c); + + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/examples/siphash.cc b/deps/ngtcp2/ngtcp2/examples/siphash.cc new file mode 100644 index 00000000000000..58cad9d1b775ba --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/siphash.cc @@ -0,0 +1,115 @@ +/* Copyright 2019 The BoringSSL Authors + * + * 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. */ + +/* + * ngtcp2 + * + * Copyright (c) 2025 nghttp2 contributors + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include +#include + +#include "siphash.h" + +namespace { +auto CRYPTO_load_u64_le(std::span in) { + uint64_t v; + + memcpy(&v, in.data(), sizeof(v)); + + if constexpr (std::endian::native == std::endian::big) { + return byteswap(v); + } + + return v; +} +} // namespace + +namespace { +constexpr void siphash_round(uint64_t v[4]) { + v[0] += v[1]; + v[2] += v[3]; + v[1] = std::rotl(v[1], 13); + v[3] = std::rotl(v[3], 16); + v[1] ^= v[0]; + v[3] ^= v[2]; + v[0] = std::rotl(v[0], 32); + v[2] += v[1]; + v[0] += v[3]; + v[1] = std::rotl(v[1], 17); + v[3] = std::rotl(v[3], 21); + v[1] ^= v[2]; + v[3] ^= v[0]; + v[2] = std::rotl(v[2], 32); +} +} // namespace + +uint64_t siphash24(std::span key, + std::span input) { + const auto orig_input_len = input.size(); + uint64_t v[]{ + key[0] ^ UINT64_C(0x736F6D6570736575), + key[1] ^ UINT64_C(0x646F72616E646F6D), + key[0] ^ UINT64_C(0x6C7967656E657261), + key[1] ^ UINT64_C(0x7465646279746573), + }; + + while (input.size() >= sizeof(uint64_t)) { + auto m = CRYPTO_load_u64_le(input.first()); + v[3] ^= m; + siphash_round(v); + siphash_round(v); + v[0] ^= m; + + input = input.subspan(sizeof(uint64_t)); + } + + std::array last_block{}; + std::ranges::copy(input, std::ranges::begin(last_block)); + last_block.back() = orig_input_len & 0xFF; + + auto last_block_word = CRYPTO_load_u64_le(last_block); + v[3] ^= last_block_word; + siphash_round(v); + siphash_round(v); + v[0] ^= last_block_word; + + v[2] ^= 0xFF; + siphash_round(v); + siphash_round(v); + siphash_round(v); + siphash_round(v); + + return v[0] ^ v[1] ^ v[2] ^ v[3]; +} diff --git a/deps/ngtcp2/ngtcp2/examples/siphash.h b/deps/ngtcp2/ngtcp2/examples/siphash.h new file mode 100644 index 00000000000000..8f82c53c79fc05 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/siphash.h @@ -0,0 +1,63 @@ +/* Copyright 2019 The BoringSSL Authors + * + * 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. */ + +/* + * ngtcp2 + * + * Copyright (c) 2025 nghttp2 contributors + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SIPHASH_H +#define SIPHASH_H + +#include +#include +#include +#include +#include + +// SipHash is a fast, secure PRF that is often used for hash tables. + +// siphash24 implements SipHash-2-4. See +// https://131002.net/siphash/siphash.pdf +uint64_t siphash24(std::span key, + std::span input); + +// Define here to be usable in tests. +template T byteswap(T v) { + auto c = std::bit_cast>(v); + std::ranges::reverse(c); + return std::bit_cast(c); +} + +#endif // !defined(SIPHASH_H) diff --git a/deps/ngtcp2/ngtcp2/examples/siphash_test.cc b/deps/ngtcp2/ngtcp2/examples/siphash_test.cc new file mode 100644 index 00000000000000..54f300444efa6e --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/siphash_test.cc @@ -0,0 +1,98 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 nghttp2 contributors + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "siphash_test.h" + +#include +#include +#include + +#include "siphash.h" +#include "siphash_vector.h" + +namespace ngtcp2 { + +namespace { +const MunitTest tests[]{ + munit_void_test(test_siphash), + munit_void_test(test_siphash_vector), + munit_test_end(), +}; +} // namespace + +const MunitSuite siphash_suite{ + .prefix = "/siphash", + .tests = tests, +}; + +void test_siphash(void) { + std::array key_bytes; + std::iota(std::ranges::begin(key_bytes), std::ranges::end(key_bytes), 0); + + std::array key; + memcpy(key.data(), key_bytes.data(), key_bytes.size()); + + if constexpr (std::endian::native == std::endian::big) { + key[0] = byteswap(key[0]); + key[1] = byteswap(key[1]); + } + + std::array input; + std::iota(std::ranges::begin(input), std::ranges::end(input), 0); + + assert_uint64(0xA129CA6149BE45E5ULL, ==, siphash24(key, input)); +} + +void test_siphash_vector(void) { + std::array key_bytes; + std::iota(std::ranges::begin(key_bytes), std::ranges::end(key_bytes), 0); + + std::array key; + memcpy(key.data(), key_bytes.data(), key_bytes.size()); + + if constexpr (std::endian::native == std::endian::big) { + key[0] = byteswap(key[0]); + key[1] = byteswap(key[1]); + } + + std::array in; + + for (size_t i = 0; i < 64; ++i) { + in[i] = static_cast(i); + auto h = siphash24(key, std::span{in}.first(i)); + + uint64_t expect; + + memcpy(&expect, &vectors_sip64[i], sizeof(expect)); + + if constexpr (std::endian::native == std::endian::big) { + expect = byteswap(expect); + } + + assert_uint64(expect, ==, h); + } +} + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/siphash_test.h b/deps/ngtcp2/ngtcp2/examples/siphash_test.h new file mode 100644 index 00000000000000..c356d2e6b00408 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/siphash_test.h @@ -0,0 +1,46 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 nghttp2 contributors + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SIPHASH_TEST_H +#define SIPHASH_TEST_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#define MUNIT_ENABLE_ASSERT_ALIASES + +#include "munit.h" + +namespace ngtcp2 { + +extern const MunitSuite siphash_suite; + +munit_void_test_decl(test_siphash) +munit_void_test_decl(test_siphash_vector) + +} // namespace ngtcp2 + +#endif // !defined(SIPHASH_TEST_H) diff --git a/deps/ngtcp2/ngtcp2/examples/siphash_vector.h b/deps/ngtcp2/ngtcp2/examples/siphash_vector.h new file mode 100644 index 00000000000000..5f4c294cf365cf --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/siphash_vector.h @@ -0,0 +1,645 @@ +// https://github.com/veorq/SipHash/blob/f26d35e964c6290ffe23d9043475ad3129f409e0/vectors.h +#include + +const uint8_t vectors_sip64[64][8] = { + { + 0x31, + 0x0E, + 0x0E, + 0xDD, + 0x47, + 0xDB, + 0x6F, + 0x72, + }, + { + 0xFD, + 0x67, + 0xDC, + 0x93, + 0xC5, + 0x39, + 0xF8, + 0x74, + }, + { + 0x5A, + 0x4F, + 0xA9, + 0xD9, + 0x09, + 0x80, + 0x6C, + 0x0D, + }, + { + 0x2D, + 0x7E, + 0xFB, + 0xD7, + 0x96, + 0x66, + 0x67, + 0x85, + }, + { + 0xB7, + 0x87, + 0x71, + 0x27, + 0xE0, + 0x94, + 0x27, + 0xCF, + }, + { + 0x8D, + 0xA6, + 0x99, + 0xCD, + 0x64, + 0x55, + 0x76, + 0x18, + }, + { + 0xCE, + 0xE3, + 0xFE, + 0x58, + 0x6E, + 0x46, + 0xC9, + 0xCB, + }, + { + 0x37, + 0xD1, + 0x01, + 0x8B, + 0xF5, + 0x00, + 0x02, + 0xAB, + }, + { + 0x62, + 0x24, + 0x93, + 0x9A, + 0x79, + 0xF5, + 0xF5, + 0x93, + }, + { + 0xB0, + 0xE4, + 0xA9, + 0x0B, + 0xDF, + 0x82, + 0x00, + 0x9E, + }, + { + 0xF3, + 0xB9, + 0xDD, + 0x94, + 0xC5, + 0xBB, + 0x5D, + 0x7A, + }, + { + 0xA7, + 0xAD, + 0x6B, + 0x22, + 0x46, + 0x2F, + 0xB3, + 0xF4, + }, + { + 0xFB, + 0xE5, + 0x0E, + 0x86, + 0xBC, + 0x8F, + 0x1E, + 0x75, + }, + { + 0x90, + 0x3D, + 0x84, + 0xC0, + 0x27, + 0x56, + 0xEA, + 0x14, + }, + { + 0xEE, + 0xF2, + 0x7A, + 0x8E, + 0x90, + 0xCA, + 0x23, + 0xF7, + }, + { + 0xE5, + 0x45, + 0xBE, + 0x49, + 0x61, + 0xCA, + 0x29, + 0xA1, + }, + { + 0xDB, + 0x9B, + 0xC2, + 0x57, + 0x7F, + 0xCC, + 0x2A, + 0x3F, + }, + { + 0x94, + 0x47, + 0xBE, + 0x2C, + 0xF5, + 0xE9, + 0x9A, + 0x69, + }, + { + 0x9C, + 0xD3, + 0x8D, + 0x96, + 0xF0, + 0xB3, + 0xC1, + 0x4B, + }, + { + 0xBD, + 0x61, + 0x79, + 0xA7, + 0x1D, + 0xC9, + 0x6D, + 0xBB, + }, + { + 0x98, + 0xEE, + 0xA2, + 0x1A, + 0xF2, + 0x5C, + 0xD6, + 0xBE, + }, + { + 0xC7, + 0x67, + 0x3B, + 0x2E, + 0xB0, + 0xCB, + 0xF2, + 0xD0, + }, + { + 0x88, + 0x3E, + 0xA3, + 0xE3, + 0x95, + 0x67, + 0x53, + 0x93, + }, + { + 0xC8, + 0xCE, + 0x5C, + 0xCD, + 0x8C, + 0x03, + 0x0C, + 0xA8, + }, + { + 0x94, + 0xAF, + 0x49, + 0xF6, + 0xC6, + 0x50, + 0xAD, + 0xB8, + }, + { + 0xEA, + 0xB8, + 0x85, + 0x8A, + 0xDE, + 0x92, + 0xE1, + 0xBC, + }, + { + 0xF3, + 0x15, + 0xBB, + 0x5B, + 0xB8, + 0x35, + 0xD8, + 0x17, + }, + { + 0xAD, + 0xCF, + 0x6B, + 0x07, + 0x63, + 0x61, + 0x2E, + 0x2F, + }, + { + 0xA5, + 0xC9, + 0x1D, + 0xA7, + 0xAC, + 0xAA, + 0x4D, + 0xDE, + }, + { + 0x71, + 0x65, + 0x95, + 0x87, + 0x66, + 0x50, + 0xA2, + 0xA6, + }, + { + 0x28, + 0xEF, + 0x49, + 0x5C, + 0x53, + 0xA3, + 0x87, + 0xAD, + }, + { + 0x42, + 0xC3, + 0x41, + 0xD8, + 0xFA, + 0x92, + 0xD8, + 0x32, + }, + { + 0xCE, + 0x7C, + 0xF2, + 0x72, + 0x2F, + 0x51, + 0x27, + 0x71, + }, + { + 0xE3, + 0x78, + 0x59, + 0xF9, + 0x46, + 0x23, + 0xF3, + 0xA7, + }, + { + 0x38, + 0x12, + 0x05, + 0xBB, + 0x1A, + 0xB0, + 0xE0, + 0x12, + }, + { + 0xAE, + 0x97, + 0xA1, + 0x0F, + 0xD4, + 0x34, + 0xE0, + 0x15, + }, + { + 0xB4, + 0xA3, + 0x15, + 0x08, + 0xBE, + 0xFF, + 0x4D, + 0x31, + }, + { + 0x81, + 0x39, + 0x62, + 0x29, + 0xF0, + 0x90, + 0x79, + 0x02, + }, + { + 0x4D, + 0x0C, + 0xF4, + 0x9E, + 0xE5, + 0xD4, + 0xDC, + 0xCA, + }, + { + 0x5C, + 0x73, + 0x33, + 0x6A, + 0x76, + 0xD8, + 0xBF, + 0x9A, + }, + { + 0xD0, + 0xA7, + 0x04, + 0x53, + 0x6B, + 0xA9, + 0x3E, + 0x0E, + }, + { + 0x92, + 0x59, + 0x58, + 0xFC, + 0xD6, + 0x42, + 0x0C, + 0xAD, + }, + { + 0xA9, + 0x15, + 0xC2, + 0x9B, + 0xC8, + 0x06, + 0x73, + 0x18, + }, + { + 0x95, + 0x2B, + 0x79, + 0xF3, + 0xBC, + 0x0A, + 0xA6, + 0xD4, + }, + { + 0xF2, + 0x1D, + 0xF2, + 0xE4, + 0x1D, + 0x45, + 0x35, + 0xF9, + }, + { + 0x87, + 0x57, + 0x75, + 0x19, + 0x04, + 0x8F, + 0x53, + 0xA9, + }, + { + 0x10, + 0xA5, + 0x6C, + 0xF5, + 0xDF, + 0xCD, + 0x9A, + 0xDB, + }, + { + 0xEB, + 0x75, + 0x09, + 0x5C, + 0xCD, + 0x98, + 0x6C, + 0xD0, + }, + { + 0x51, + 0xA9, + 0xCB, + 0x9E, + 0xCB, + 0xA3, + 0x12, + 0xE6, + }, + { + 0x96, + 0xAF, + 0xAD, + 0xFC, + 0x2C, + 0xE6, + 0x66, + 0xC7, + }, + { + 0x72, + 0xFE, + 0x52, + 0x97, + 0x5A, + 0x43, + 0x64, + 0xEE, + }, + { + 0x5A, + 0x16, + 0x45, + 0xB2, + 0x76, + 0xD5, + 0x92, + 0xA1, + }, + { + 0xB2, + 0x74, + 0xCB, + 0x8E, + 0xBF, + 0x87, + 0x87, + 0x0A, + }, + { + 0x6F, + 0x9B, + 0xB4, + 0x20, + 0x3D, + 0xE7, + 0xB3, + 0x81, + }, + { + 0xEA, + 0xEC, + 0xB2, + 0xA3, + 0x0B, + 0x22, + 0xA8, + 0x7F, + }, + { + 0x99, + 0x24, + 0xA4, + 0x3C, + 0xC1, + 0x31, + 0x57, + 0x24, + }, + { + 0xBD, + 0x83, + 0x8D, + 0x3A, + 0xAF, + 0xBF, + 0x8D, + 0xB7, + }, + { + 0x0B, + 0x1A, + 0x2A, + 0x32, + 0x65, + 0xD5, + 0x1A, + 0xEA, + }, + { + 0x13, + 0x50, + 0x79, + 0xA3, + 0x23, + 0x1C, + 0xE6, + 0x60, + }, + { + 0x93, + 0x2B, + 0x28, + 0x46, + 0xE4, + 0xD7, + 0x06, + 0x66, + }, + { + 0xE1, + 0x91, + 0x5F, + 0x5C, + 0xB1, + 0xEC, + 0xA4, + 0x6C, + }, + { + 0xF3, + 0x25, + 0x96, + 0x5C, + 0xA1, + 0x6D, + 0x62, + 0x9F, + }, + { + 0x57, + 0x5F, + 0xF2, + 0x8E, + 0x60, + 0x38, + 0x1B, + 0xE5, + }, + { + 0x72, + 0x45, + 0x06, + 0xEB, + 0x4C, + 0x32, + 0x8A, + 0x95, + }, +}; diff --git a/deps/ngtcp2/ngtcp2/examples/template.h b/deps/ngtcp2/ngtcp2/examples/template.h new file mode 100644 index 00000000000000..43ebe0b31e4056 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/template.h @@ -0,0 +1,111 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * Copyright (c) 2015 ngttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TEMPLATE_H +#define TEMPLATE_H + +#include +#include +#include +#include + +template +[[nodiscard]] constexpr auto as_unsigned(T n) noexcept { + return static_cast>(n); +} + +template +[[nodiscard]] constexpr auto as_signed(T n) noexcept { + return static_cast>(n); +} + +template +[[nodiscard]] auto as_uint8_span(std::span s) noexcept { + return std::span{ + reinterpret_cast(s.data()), s.size_bytes()}; +} + +template struct Defer { + explicit Defer(F &&f) noexcept(std::is_nothrow_constructible_v) + : f(std::forward(f)) {} + ~Defer() { f(); } + + Defer(Defer &&o) = delete; + Defer(const Defer &) = delete; + Defer &operator=(const Defer &) = delete; + Defer &operator=(Defer &&) = delete; + + F f; +}; + +template [[nodiscard]] Defer> defer(F &&f) { + return Defer>(std::forward(f)); +} + +template constexpr size_t array_size(T (&)[N]) { + return N; +} + +template constexpr size_t str_size(T (&)[N]) { + return N - 1; +} + +// User-defined literals for K, M, and G (powers of 1024) + +constexpr unsigned long long operator""_k(unsigned long long k) { + return k * 1024; +} + +constexpr unsigned long long operator""_m(unsigned long long m) { + return m * 1024 * 1024; +} + +constexpr unsigned long long operator""_g(unsigned long long g) { + return g * 1024 * 1024 * 1024; +} + +template +[[nodiscard]] std::span +as_writable_uint8_span(std::span s) noexcept { + return std::span{ + reinterpret_cast(s.data()), s.size_bytes()}; +} + +template +requires(std::ranges::contiguous_range && std::ranges::sized_range && + std::ranges::borrowed_range && + !std::is_array_v> && + sizeof(std::ranges::range_value_t) == + sizeof(std::string_view::value_type)) +[[nodiscard]] std::string_view as_string_view(R &&r) { + return std::string_view{ + reinterpret_cast(std::ranges::data(r)), + std::ranges::size(r)}; +} + +#endif // !defined(TEMPLATE_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tests/README.rst b/deps/ngtcp2/ngtcp2/examples/tests/README.rst new file mode 100644 index 00000000000000..1c59152311807b --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/README.rst @@ -0,0 +1,60 @@ +Examples Tests +============== + +This is a ``pytest`` suite intended to verify interoperability between +the different example clients and servers built. + +You run it by executing ``pytest`` on top level project dir or in +the examples/tests directory. + +.. code-block:: text + + examples/test> pytest + ngtcp2-examples: [0.9.0-DEV, crypto_libs=['quictls', 'wolfssl']] + ... + +Requirements +------------ + +You need a Python3 (3.8 is probably sufficient), ``pytest`` and the +Python ``cryptography`` module installed. + +Usage +----- + +If you run ``pytest`` without arguments, it will print the test suite +and a ``.`` for every test case passed. Add ``-v`` and all test cases +will be listed in the full name. Adding several ``v`` will increase the +logging level on failure output. + +The name of test cases include the crypto libs of the server and client +used. For example: + +.. code-block:: text + + test_01_handshake.py::TestHandshake::test_01_01_get[quictls-quictls] PASSED [ 16%] + test_01_handshake.py::TestHandshake::test_01_01_get[quictls-wolfssl] PASSED + +Here, ``test_01_01`` is run first with the quictls server and client and then +with the quictls server and wolfSSL client. By default, the test suite runs +all combinations of servers and clients that have been configured in the project. + +To track down problems, you can restrict the test cases that are run by +matching patterns: + +.. code-block:: text + + # only tests with wolfSSL example server + > pytest -v -k 'wolfssl-' + # only tests with wolfSSL example client + > pytest -v -k 'test and -wolfssl' + # tests with a specific combination + > pytest -v -k 'quictls-wolfssl' + + +Analysing +--------- + +To make analysis of a broken test case easier, you best run only that +test case. Use ``pytest -vv`` (or more) to get more verbose logging. +Inspect server and client log files in ``examples/tests/gen``. diff --git a/deps/ngtcp2/ngtcp2/examples/tests/__init__.py b/deps/ngtcp2/ngtcp2/examples/tests/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/deps/ngtcp2/ngtcp2/examples/tests/conftest.py b/deps/ngtcp2/ngtcp2/examples/tests/conftest.py new file mode 100644 index 00000000000000..a566ff028fa549 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/conftest.py @@ -0,0 +1,28 @@ +import logging +import pytest + +from .ngtcp2test import Env + + +@pytest.mark.usefixtures("env") +def pytest_report_header(config): + env = Env() + return [ + f"ngtcp2-examples: [{env.version}, crypto_libs={env.crypto_libs}]", + f"example clients: {env.clients}", + f"example servers: {env.servers}", + ] + + +@pytest.fixture(scope="package") +def env(pytestconfig) -> Env: + console = logging.StreamHandler() + console.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + logging.getLogger('').addHandler(console) + env = Env(pytestconfig=pytestconfig) + level = logging.DEBUG if env.verbose > 0 else logging.INFO + console.setLevel(level) + logging.getLogger('').setLevel(level=level) + env.setup() + + return env diff --git a/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/__init__.py b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/__init__.py new file mode 100644 index 00000000000000..65c61d8529833f --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/__init__.py @@ -0,0 +1,6 @@ +from .env import Env, CryptoLib +from .log import LogFile +from .client import ExampleClient, ClientRun +from .server import ExampleServer, ServerRun +from .certs import Ngtcp2TestCA, Credentials +from .tls import HandShake, HSRecord diff --git a/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/certs.py b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/certs.py new file mode 100644 index 00000000000000..3ab6260ec35849 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/certs.py @@ -0,0 +1,476 @@ +import os +import re +from datetime import timedelta, datetime +from typing import List, Any, Optional + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption, load_pem_private_key +from cryptography.x509 import ExtendedKeyUsageOID, NameOID + + +EC_SUPPORTED = {} +EC_SUPPORTED.update([(curve.name.upper(), curve) for curve in [ + ec.SECP192R1, + ec.SECP224R1, + ec.SECP256R1, + ec.SECP384R1, +]]) + + +def _private_key(key_type): + if isinstance(key_type, str): + key_type = key_type.upper() + m = re.match(r'^(RSA)?(\d+)$', key_type) + if m: + key_type = int(m.group(2)) + + if isinstance(key_type, int): + return rsa.generate_private_key( + public_exponent=65537, + key_size=key_type, + backend=default_backend() + ) + if not isinstance(key_type, ec.EllipticCurve) and key_type in EC_SUPPORTED: + key_type = EC_SUPPORTED[key_type] + return ec.generate_private_key( + curve=key_type, + backend=default_backend() + ) + + +class CertificateSpec: + + def __init__(self, name: str = None, domains: List[str] = None, + email: str = None, + key_type: str = None, single_file: bool = False, + valid_from: timedelta = timedelta(days=-1), + valid_to: timedelta = timedelta(days=89), + client: bool = False, + sub_specs: List['CertificateSpec'] = None): + self._name = name + self.domains = domains + self.client = client + self.email = email + self.key_type = key_type + self.single_file = single_file + self.valid_from = valid_from + self.valid_to = valid_to + self.sub_specs = sub_specs + + @property + def name(self) -> Optional[str]: + if self._name: + return self._name + elif self.domains: + return self.domains[0] + return None + + @property + def type(self) -> Optional[str]: + if self.domains and len(self.domains): + return "server" + elif self.client: + return "client" + elif self.name: + return "ca" + return None + + +class Credentials: + + def __init__(self, name: str, cert: Any, pkey: Any, issuer: 'Credentials' = None): + self._name = name + self._cert = cert + self._pkey = pkey + self._issuer = issuer + self._cert_file = None + self._pkey_file = None + self._store = None + + @property + def name(self) -> str: + return self._name + + @property + def subject(self) -> x509.Name: + return self._cert.subject + + @property + def key_type(self): + if isinstance(self._pkey, RSAPrivateKey): + return f"rsa{self._pkey.key_size}" + elif isinstance(self._pkey, EllipticCurvePrivateKey): + return f"{self._pkey.curve.name}" + else: + raise Exception(f"unknown key type: {self._pkey}") + + @property + def private_key(self) -> Any: + return self._pkey + + @property + def certificate(self) -> Any: + return self._cert + + @property + def cert_pem(self) -> bytes: + return self._cert.public_bytes(Encoding.PEM) + + @property + def pkey_pem(self) -> bytes: + return self._pkey.private_bytes( + Encoding.PEM, + PrivateFormat.TraditionalOpenSSL if self.key_type.startswith('rsa') else PrivateFormat.PKCS8, + NoEncryption()) + + @property + def issuer(self) -> Optional['Credentials']: + return self._issuer + + def set_store(self, store: 'CertStore'): + self._store = store + + def set_files(self, cert_file: str, pkey_file: str = None): + self._cert_file = cert_file + self._pkey_file = pkey_file + + @property + def cert_file(self) -> str: + return self._cert_file + + @property + def pkey_file(self) -> Optional[str]: + return self._pkey_file + + def get_first(self, name) -> Optional['Credentials']: + creds = self._store.get_credentials_for_name(name) if self._store else [] + return creds[0] if len(creds) else None + + def get_credentials_for_name(self, name) -> List['Credentials']: + return self._store.get_credentials_for_name(name) if self._store else [] + + def issue_certs(self, specs: List[CertificateSpec], + chain: List['Credentials'] = None) -> List['Credentials']: + return [self.issue_cert(spec=spec, chain=chain) for spec in specs] + + def issue_cert(self, spec: CertificateSpec, chain: List['Credentials'] = None) -> 'Credentials': + key_type = spec.key_type if spec.key_type else self.key_type + creds = None + if self._store: + creds = self._store.load_credentials( + name=spec.name, key_type=key_type, single_file=spec.single_file, issuer=self) + if creds is None: + creds = Ngtcp2TestCA.create_credentials(spec=spec, issuer=self, key_type=key_type, + valid_from=spec.valid_from, valid_to=spec.valid_to) + if self._store: + self._store.save(creds, single_file=spec.single_file) + if spec.type == "ca": + self._store.save_chain(creds, "ca", with_root=True) + + if spec.sub_specs: + if self._store: + sub_store = CertStore(fpath=os.path.join(self._store.path, creds.name)) + creds.set_store(sub_store) + subchain = chain.copy() if chain else [] + subchain.append(self) + creds.issue_certs(spec.sub_specs, chain=subchain) + return creds + + +class CertStore: + + def __init__(self, fpath: str): + self._store_dir = fpath + if not os.path.exists(self._store_dir): + os.makedirs(self._store_dir) + self._creds_by_name = {} + + @property + def path(self) -> str: + return self._store_dir + + def save(self, creds: Credentials, name: str = None, + chain: List[Credentials] = None, + single_file: bool = False) -> None: + name = name if name is not None else creds.name + cert_file = self.get_cert_file(name=name, key_type=creds.key_type) + pkey_file = self.get_pkey_file(name=name, key_type=creds.key_type) + if single_file: + pkey_file = None + with open(cert_file, "wb") as fd: + fd.write(creds.cert_pem) + if chain: + for c in chain: + fd.write(c.cert_pem) + if pkey_file is None: + fd.write(creds.pkey_pem) + if pkey_file is not None: + with open(pkey_file, "wb") as fd: + fd.write(creds.pkey_pem) + creds.set_files(cert_file, pkey_file) + self._add_credentials(name, creds) + + def save_chain(self, creds: Credentials, infix: str, with_root=False): + name = creds.name + chain = [creds] + while creds.issuer is not None: + creds = creds.issuer + chain.append(creds) + if not with_root and len(chain) > 1: + chain = chain[:-1] + chain_file = os.path.join(self._store_dir, f'{name}-{infix}.pem') + with open(chain_file, "wb") as fd: + for c in chain: + fd.write(c.cert_pem) + + def _add_credentials(self, name: str, creds: Credentials): + if name not in self._creds_by_name: + self._creds_by_name[name] = [] + self._creds_by_name[name].append(creds) + + def get_credentials_for_name(self, name) -> List[Credentials]: + return self._creds_by_name[name] if name in self._creds_by_name else [] + + def get_cert_file(self, name: str, key_type=None) -> str: + key_infix = ".{0}".format(key_type) if key_type is not None else "" + return os.path.join(self._store_dir, f'{name}{key_infix}.cert.pem') + + def get_pkey_file(self, name: str, key_type=None) -> str: + key_infix = ".{0}".format(key_type) if key_type is not None else "" + return os.path.join(self._store_dir, f'{name}{key_infix}.pkey.pem') + + def load_pem_cert(self, fpath: str) -> x509.Certificate: + with open(fpath) as fd: + return x509.load_pem_x509_certificate("".join(fd.readlines()).encode()) + + def load_pem_pkey(self, fpath: str): + with open(fpath) as fd: + return load_pem_private_key("".join(fd.readlines()).encode(), password=None) + + def load_credentials(self, name: str, key_type=None, single_file: bool = False, issuer: Credentials = None): + cert_file = self.get_cert_file(name=name, key_type=key_type) + pkey_file = cert_file if single_file else self.get_pkey_file(name=name, key_type=key_type) + if os.path.isfile(cert_file) and os.path.isfile(pkey_file): + cert = self.load_pem_cert(cert_file) + pkey = self.load_pem_pkey(pkey_file) + creds = Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer) + creds.set_store(self) + creds.set_files(cert_file, pkey_file) + self._add_credentials(name, creds) + return creds + return None + + +class Ngtcp2TestCA: + + @classmethod + def create_root(cls, name: str, store_dir: str, key_type: str = "rsa2048") -> Credentials: + store = CertStore(fpath=store_dir) + creds = store.load_credentials(name="ca", key_type=key_type, issuer=None) + if creds is None: + creds = Ngtcp2TestCA._make_ca_credentials(name=name, key_type=key_type) + store.save(creds, name="ca") + creds.set_store(store) + return creds + + @staticmethod + def create_credentials(spec: CertificateSpec, issuer: Credentials, key_type: Any, + valid_from: timedelta = timedelta(days=-1), + valid_to: timedelta = timedelta(days=89), + ) -> Credentials: + """Create a certificate signed by this CA for the given domains. + :returns: the certificate and private key PEM file paths + """ + if spec.domains and len(spec.domains): + creds = Ngtcp2TestCA._make_server_credentials(name=spec.name, domains=spec.domains, + issuer=issuer, valid_from=valid_from, + valid_to=valid_to, key_type=key_type) + elif spec.client: + creds = Ngtcp2TestCA._make_client_credentials(name=spec.name, issuer=issuer, + email=spec.email, valid_from=valid_from, + valid_to=valid_to, key_type=key_type) + elif spec.name: + creds = Ngtcp2TestCA._make_ca_credentials(name=spec.name, issuer=issuer, + valid_from=valid_from, valid_to=valid_to, + key_type=key_type) + else: + raise Exception(f"unrecognized certificate specification: {spec}") + return creds + + @staticmethod + def _make_x509_name(org_name: str = None, common_name: str = None, parent: x509.Name = None) -> x509.Name: + name_pieces = [] + if org_name: + oid = NameOID.ORGANIZATIONAL_UNIT_NAME if parent else NameOID.ORGANIZATION_NAME + name_pieces.append(x509.NameAttribute(oid, org_name)) + elif common_name: + name_pieces.append(x509.NameAttribute(NameOID.COMMON_NAME, common_name)) + if parent: + name_pieces.extend([rdn for rdn in parent]) + return x509.Name(name_pieces) + + @staticmethod + def _make_csr( + subject: x509.Name, + pkey: Any, + issuer_subject: Optional[Credentials], + valid_from_delta: timedelta = None, + valid_until_delta: timedelta = None + ): + pubkey = pkey.public_key() + issuer_subject = issuer_subject if issuer_subject is not None else subject + + valid_from = datetime.now() + if valid_until_delta is not None: + valid_from += valid_from_delta + valid_until = datetime.now() + if valid_until_delta is not None: + valid_until += valid_until_delta + + return ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer_subject) + .public_key(pubkey) + .not_valid_before(valid_from) + .not_valid_after(valid_until) + .serial_number(x509.random_serial_number()) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(pubkey), + critical=False, + ) + ) + + @staticmethod + def _add_ca_usages(csr: Any) -> Any: + return csr.add_extension( + x509.BasicConstraints(ca=True, path_length=9), + critical=True, + ).add_extension( + x509.KeyUsage( + digital_signature=True, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=True, + crl_sign=True, + encipher_only=False, + decipher_only=False), + critical=True + ).add_extension( + x509.ExtendedKeyUsage([ + ExtendedKeyUsageOID.CLIENT_AUTH, + ExtendedKeyUsageOID.SERVER_AUTH, + ExtendedKeyUsageOID.CODE_SIGNING, + ]), + critical=True + ) + + @staticmethod + def _add_leaf_usages(csr: Any, domains: List[str], issuer: Credentials) -> Any: + return csr.add_extension( + x509.BasicConstraints(ca=False, path_length=None), + critical=True, + ).add_extension( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( + issuer.certificate.extensions.get_extension_for_class( + x509.SubjectKeyIdentifier).value), + critical=False + ).add_extension( + x509.SubjectAlternativeName([x509.DNSName(domain) for domain in domains]), + critical=True, + ).add_extension( + x509.ExtendedKeyUsage([ + ExtendedKeyUsageOID.SERVER_AUTH, + ]), + critical=True + ) + + @staticmethod + def _add_client_usages(csr: Any, issuer: Credentials, rfc82name: str = None) -> Any: + cert = csr.add_extension( + x509.BasicConstraints(ca=False, path_length=None), + critical=True, + ).add_extension( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( + issuer.certificate.extensions.get_extension_for_class( + x509.SubjectKeyIdentifier).value), + critical=False + ) + if rfc82name: + cert.add_extension( + x509.SubjectAlternativeName([x509.RFC822Name(rfc82name)]), + critical=True, + ) + cert.add_extension( + x509.ExtendedKeyUsage([ + ExtendedKeyUsageOID.CLIENT_AUTH, + ]), + critical=True + ) + return cert + + @staticmethod + def _make_ca_credentials(name, key_type: Any, + issuer: Credentials = None, + valid_from: timedelta = timedelta(days=-1), + valid_to: timedelta = timedelta(days=89), + ) -> Credentials: + pkey = _private_key(key_type=key_type) + if issuer is not None: + issuer_subject = issuer.certificate.subject + issuer_key = issuer.private_key + else: + issuer_subject = None + issuer_key = pkey + subject = Ngtcp2TestCA._make_x509_name(org_name=name, parent=issuer.subject if issuer else None) + csr = Ngtcp2TestCA._make_csr(subject=subject, + issuer_subject=issuer_subject, pkey=pkey, + valid_from_delta=valid_from, valid_until_delta=valid_to) + csr = Ngtcp2TestCA._add_ca_usages(csr) + cert = csr.sign(private_key=issuer_key, + algorithm=hashes.SHA256(), + backend=default_backend()) + return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer) + + @staticmethod + def _make_server_credentials(name: str, domains: List[str], issuer: Credentials, + key_type: Any, + valid_from: timedelta = timedelta(days=-1), + valid_to: timedelta = timedelta(days=89), + ) -> Credentials: + name = name + pkey = _private_key(key_type=key_type) + subject = Ngtcp2TestCA._make_x509_name(common_name=name, parent=issuer.subject) + csr = Ngtcp2TestCA._make_csr(subject=subject, + issuer_subject=issuer.certificate.subject, pkey=pkey, + valid_from_delta=valid_from, valid_until_delta=valid_to) + csr = Ngtcp2TestCA._add_leaf_usages(csr, domains=domains, issuer=issuer) + cert = csr.sign(private_key=issuer.private_key, + algorithm=hashes.SHA256(), + backend=default_backend()) + return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer) + + @staticmethod + def _make_client_credentials(name: str, + issuer: Credentials, email: Optional[str], + key_type: Any, + valid_from: timedelta = timedelta(days=-1), + valid_to: timedelta = timedelta(days=89), + ) -> Credentials: + pkey = _private_key(key_type=key_type) + subject = Ngtcp2TestCA._make_x509_name(common_name=name, parent=issuer.subject) + csr = Ngtcp2TestCA._make_csr(subject=subject, + issuer_subject=issuer.certificate.subject, pkey=pkey, + valid_from_delta=valid_from, valid_until_delta=valid_to) + csr = Ngtcp2TestCA._add_client_usages(csr, issuer=issuer, rfc82name=email) + cert = csr.sign(private_key=issuer.private_key, + algorithm=hashes.SHA256(), + backend=default_backend()) + return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer) diff --git a/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/client.py b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/client.py new file mode 100644 index 00000000000000..c22f661a7828c5 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/client.py @@ -0,0 +1,187 @@ +import logging +import os +import re +import subprocess +from typing import List + +import pytest + +from .server import ExampleServer, ServerRun +from .certs import Credentials +from .tls import HandShake, HSRecord +from .env import Env, CryptoLib +from .log import LogFile, HexDumpScanner + + +log = logging.getLogger(__name__) + + +class ClientRun: + + def __init__(self, env: Env, returncode, logfile: LogFile, srun: ServerRun): + self.env = env + self.returncode = returncode + self.logfile = logfile + self.log_lines = logfile.get_recent() + self._data_recs = None + self._hs_recs = None + self._srun = srun + if self.env.verbose > 1: + log.debug(f'read {len(self.log_lines)} lines from {logfile.path}') + + @property + def handshake(self) -> List[HSRecord]: + if self._data_recs is None: + crypto_line = re.compile(r'Ordered CRYPTO data in \S+ crypto level') + scanner = HexDumpScanner(source=self.log_lines, + leading_regex=crypto_line) + self._data_recs = [data for data in scanner] + if self.env.verbose > 1: + log.debug(f'detected {len(self._data_recs)} crypto hexdumps ' + f'in {self.logfile.path}') + if self._hs_recs is None: + self._hs_recs = [hrec for hrec in HandShake(source=self._data_recs, + verbose=self.env.verbose)] + if self.env.verbose > 1: + log.debug(f'detected {len(self._hs_recs)} crypto ' + f'records in {self.logfile.path}') + return self._hs_recs + + @property + def hs_stripe(self) -> str: + return ":".join([hrec.name for hrec in self.handshake]) + + @property + def early_data_rejected(self) -> bool: + for l in self.log_lines: + if re.match(r'^Early data was rejected by server.*', l): + return True + return False + + @property + def server(self) -> ServerRun: + return self._srun + + def norm_exp(self, c_hs, s_hs, allow_hello_retry=True): + if allow_hello_retry and self.hs_stripe.startswith('HelloRetryRequest:'): + c_hs = "HelloRetryRequest:" + c_hs + s_hs = "ClientHello:" + s_hs + return c_hs, s_hs + + def _assert_hs(self, c_hs, s_hs): + if re.match('^'+c_hs, self.hs_stripe) is None: + # what happened? + if self.hs_stripe == '': + # server send nothing + if self.server.hs_stripe == '': + # client send nothing + pytest.fail(f'client did not send a ClientHello"') + else: + # client send sth, but server did not respond + pytest.fail(f'server did not respond to ClientHello: ' + f'{self.server.handshake[0].to_text()}"') + else: + pytest.fail(f'Expected "{c_hs}", got "{self.hs_stripe}"') + assert re.match('^'+s_hs+'$', self.server.hs_stripe) is not None, \ + f'Expected "{s_hs}", got "{self.server.hs_stripe}"\n' + + def assert_non_resume_handshake(self, allow_hello_retry=True): + # for client/server where KEY_SHARE do not match, the hello is retried + c_hs, s_hs = self.norm_exp( + "ServerHello:EncryptedExtensions:(Compressed)?Certificate:CertificateVerify:Finished", + "ClientHello:Finished", allow_hello_retry=allow_hello_retry) + self._assert_hs(c_hs, s_hs) + + def assert_resume_handshake(self): + # for client/server where KEY_SHARE do not match, the hello is retried + c_hs, s_hs = self.norm_exp("ServerHello:EncryptedExtensions:Finished", + "ClientHello:Finished") + self._assert_hs(c_hs, s_hs) + + def assert_verify_null_handshake(self): + c_hs, s_hs = self.norm_exp( + "ServerHello:EncryptedExtensions:CertificateRequest:(Compressed)?Certificate:CertificateVerify:Finished", + "ClientHello:Certificate:Finished") + self._assert_hs(c_hs, s_hs) + + def assert_verify_cert_handshake(self): + c_hs, s_hs = self.norm_exp( + "ServerHello:EncryptedExtensions:CertificateRequest:(Compressed)?Certificate:CertificateVerify:Finished", + "ClientHello:Certificate:CertificateVerify:Finished") + self._assert_hs(c_hs, s_hs) + + +class ExampleClient: + + def __init__(self, env: Env, crypto_lib: str): + self.env = env + self._crypto_lib = crypto_lib + self._path = env.client_path(self._crypto_lib) + self._log_path = f'{self.env.gen_dir}/{self._crypto_lib}-client.log' + self._qlog_path = f'{self.env.gen_dir}/{self._crypto_lib}-client.qlog' + self._session_path = f'{self.env.gen_dir}/{self._crypto_lib}-client.session' + self._tp_path = f'{self.env.gen_dir}/{self._crypto_lib}-client.tp' + self._data_path = f'{self.env.gen_dir}/{self._crypto_lib}-client.data' + + @property + def path(self): + return self._path + + @property + def crypto_lib(self): + return self._crypto_lib + + @property + def uses_cipher_config(self): + return CryptoLib.uses_cipher_config(self.crypto_lib) + + def supports_cipher(self, cipher): + return CryptoLib.supports_cipher(self.crypto_lib, cipher) + + def exists(self): + return os.path.isfile(self.path) + + def clear_session(self): + if os.path.isfile(self._session_path): + os.remove(self._session_path) + if os.path.isfile(self._tp_path): + os.remove(self._tp_path) + + def http_get(self, server: ExampleServer, url: str, extra_args: List[str] = None, + use_session=False, data=None, + credentials: Credentials = None, + ciphers: str = None): + args = [ + self.path, '--exit-on-all-streams-close', + f'--qlog-file={self._qlog_path}' + ] + if use_session: + args.append(f'--session-file={self._session_path}') + args.append(f'--tp-file={self._tp_path}') + if data is not None: + with open(self._data_path, 'w') as fd: + fd.write(data) + args.append(f'--data={self._data_path}') + if ciphers is not None: + ciphers = CryptoLib.adjust_ciphers(self.crypto_lib, ciphers) + args.append(f'--ciphers={ciphers}') + if credentials is not None: + args.append(f'--key={credentials.pkey_file}') + args.append(f'--cert={credentials.cert_file}') + if extra_args is not None: + args.extend(extra_args) + args.extend([ + 'localhost', str(self.env.examples_port), + url + ]) + if os.path.isfile(self._qlog_path): + os.remove(self._qlog_path) + with open(self._log_path, 'w') as log_file: + logfile = LogFile(path=self._log_path) + server.log.advance() + process = subprocess.Popen(args=args, text=True, + stdout=log_file, stderr=log_file) + process.wait() + return ClientRun(env=self.env, returncode=process.returncode, + logfile=logfile, srun=server.get_run()) + diff --git a/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/env.py b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/env.py new file mode 100644 index 00000000000000..9699d552cc5bab --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/env.py @@ -0,0 +1,191 @@ +import logging +import os +from configparser import ConfigParser, ExtendedInterpolation +from typing import Dict, Optional + +from .certs import CertificateSpec, Ngtcp2TestCA, Credentials + +log = logging.getLogger(__name__) + + +class CryptoLib: + + IGNORES_CIPHER_CONFIG = [ + 'picotls', 'boringssl' + ] + UNSUPPORTED_CIPHERS = { + 'wolfssl': [ + 'TLS_AES_128_CCM_SHA256', # no plans to + ], + 'picotls': [ + 'TLS_AES_128_CCM_SHA256', # no plans to + ], + 'boringssl': [ + 'TLS_AES_128_CCM_SHA256', # no plans to + ] + } + GNUTLS_CIPHERS = { + 'TLS_AES_128_GCM_SHA256': 'AES-128-GCM', + 'TLS_AES_256_GCM_SHA384': 'AES-256-GCM', + 'TLS_CHACHA20_POLY1305_SHA256': 'CHACHA20-POLY1305', + 'TLS_AES_128_CCM_SHA256': 'AES-128-CCM', + } + + @classmethod + def uses_cipher_config(cls, crypto_lib): + return crypto_lib not in cls.IGNORES_CIPHER_CONFIG + + @classmethod + def supports_cipher(cls, crypto_lib, cipher): + return crypto_lib not in cls.UNSUPPORTED_CIPHERS or \ + cipher not in cls.UNSUPPORTED_CIPHERS[crypto_lib] + + @classmethod + def adjust_ciphers(cls, crypto_lib, ciphers: str) -> str: + if crypto_lib == 'gnutls': + gciphers = "NORMAL:-VERS-ALL:+VERS-TLS1.3:-CIPHER-ALL" + for cipher in ciphers.split(':'): + gciphers += f':+{cls.GNUTLS_CIPHERS[cipher]}' + return gciphers + return ciphers + + +def init_config_from(conf_path): + if os.path.isfile(conf_path): + config = ConfigParser(interpolation=ExtendedInterpolation()) + config.read(conf_path) + return config + return None + + +TESTS_PATH = os.path.dirname(os.path.dirname(__file__)) +EXAMPLES_PATH = os.path.dirname(TESTS_PATH) +DEF_CONFIG = init_config_from(os.path.join(TESTS_PATH, 'config.ini')) + + +class Env: + + @classmethod + def get_crypto_libs(cls, configurable_ciphers=None): + names = [name for name in DEF_CONFIG['examples'] + if DEF_CONFIG['examples'][name] == 'yes'] + if configurable_ciphers is not None: + names = [n for n in names if CryptoLib.uses_cipher_config(n)] + return names + + def __init__(self, examples_dir=None, tests_dir=None, config=None, + pytestconfig=None): + self._verbose = pytestconfig.option.verbose if pytestconfig is not None else 0 + self._examples_dir = examples_dir if examples_dir is not None else EXAMPLES_PATH + self._tests_dir = examples_dir if tests_dir is not None else TESTS_PATH + self._gen_dir = os.path.join(self._tests_dir, 'gen') + self.config = config if config is not None else DEF_CONFIG + self._version = self.config['ngtcp2']['version'] + self._crypto_libs = [name for name in self.config['examples'] + if self.config['examples'][name] == 'yes'] + self._clients = [self.config['clients'][lib] for lib in self._crypto_libs + if lib in self.config['clients']] + self._servers = [self.config['servers'][lib] for lib in self._crypto_libs + if lib in self.config['servers']] + self._examples_pem = { + 'key': 'xxx', + 'cert': 'xxx', + } + self._htdocs_dir = os.path.join(self._gen_dir, 'htdocs') + self._tld = 'tests.ngtcp2.nghttp2.org' + self._example_domain = f"one.{self._tld}" + self._ca = None + self._cert_specs = [ + CertificateSpec(domains=[self._example_domain], key_type='rsa2048'), + CertificateSpec(name="clientsX", sub_specs=[ + CertificateSpec(name="user1", client=True), + ]), + ] + + def issue_certs(self): + if self._ca is None: + self._ca = Ngtcp2TestCA.create_root(name=self._tld, + store_dir=os.path.join(self.gen_dir, 'ca'), + key_type="rsa2048") + self._ca.issue_certs(self._cert_specs) + + def setup(self): + os.makedirs(self._gen_dir, exist_ok=True) + os.makedirs(self._htdocs_dir, exist_ok=True) + self.issue_certs() + + def get_server_credentials(self) -> Optional[Credentials]: + creds = self.ca.get_credentials_for_name(self._example_domain) + if len(creds) > 0: + return creds[0] + return None + + @property + def verbose(self) -> int: + return self._verbose + + @property + def version(self) -> str: + return self._version + + @property + def gen_dir(self) -> str: + return self._gen_dir + + @property + def ca(self): + return self._ca + + @property + def htdocs_dir(self) -> str: + return self._htdocs_dir + + @property + def example_domain(self) -> str: + return self._example_domain + + @property + def examples_dir(self) -> str: + return self._examples_dir + + @property + def examples_port(self) -> int: + return int(self.config['examples']['port']) + + @property + def examples_pem(self) -> Dict[str, str]: + return self._examples_pem + + @property + def crypto_libs(self): + return self._crypto_libs + + @property + def clients(self): + return self._clients + + @property + def servers(self): + return self._servers + + def client_name(self, crypto_lib): + if crypto_lib in self.config['clients']: + return self.config['clients'][crypto_lib] + return None + + def client_path(self, crypto_lib): + cname = self.client_name(crypto_lib) + if cname is not None: + return os.path.join(self.examples_dir, cname) + return None + + def server_name(self, crypto_lib): + if crypto_lib in self.config['servers']: + return self.config['servers'][crypto_lib] + return None + + def server_path(self, crypto_lib): + sname = self.server_name(crypto_lib) + if sname is not None: + return os.path.join(self.examples_dir, sname) + return None diff --git a/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/log.py b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/log.py new file mode 100644 index 00000000000000..9e8f39945ddb6e --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/log.py @@ -0,0 +1,101 @@ +import binascii +import os +import re +import sys +import time +from datetime import timedelta, datetime +from io import SEEK_END +from typing import List + + +class LogFile: + + def __init__(self, path: str): + self._path = path + self._start_pos = 0 + self._last_pos = self._start_pos + + @property + def path(self) -> str: + return self._path + + def reset(self): + self._start_pos = 0 + self._last_pos = self._start_pos + + def advance(self) -> None: + if os.path.isfile(self._path): + with open(self._path) as fd: + self._start_pos = fd.seek(0, SEEK_END) + + def get_recent(self, advance=True) -> List[str]: + lines = [] + if os.path.isfile(self._path): + with open(self._path) as fd: + fd.seek(self._last_pos, os.SEEK_SET) + for line in fd: + lines.append(line) + if advance: + self._last_pos = fd.tell() + return lines + + def scan_recent(self, pattern: re, timeout=10) -> bool: + if not os.path.isfile(self.path): + return False + with open(self.path) as fd: + end = datetime.now() + timedelta(seconds=timeout) + while True: + fd.seek(self._last_pos, os.SEEK_SET) + for line in fd: + if pattern.match(line): + return True + if datetime.now() > end: + raise TimeoutError(f"pattern not found in error log after {timeout} seconds") + time.sleep(.1) + return False + + +class HexDumpScanner: + + def __init__(self, source, leading_regex=None): + self._source = source + self._leading_regex = leading_regex + + def __iter__(self): + data = b'' + offset = 0 if self._leading_regex is None else -1 + idx = 0 + for l in self._source: + if offset == -1: + pass + elif offset == 0: + # possible start of a hex dump + m = re.match(r'^\s*0+(\s+-)?((\s+[0-9a-f]{2}){1,16})(\s+.*)$', + l, re.IGNORECASE) + if m: + data = binascii.unhexlify(re.sub(r'\s+', '', m.group(2))) + offset = 16 + idx = 1 + continue + else: + # possible continuation of a hexdump + m = re.match(r'^\s*([0-9a-f]+)(\s+-)?((\s+[0-9a-f]{2}){1,16})' + r'(\s+.*)$', l, re.IGNORECASE) + if m: + loffset = int(m.group(1), 16) + if loffset == offset or loffset == idx: + data += binascii.unhexlify(re.sub(r'\s+', '', + m.group(3))) + offset += 16 + idx += 1 + continue + else: + sys.stderr.write(f'wrong offset {loffset}, expected {offset} or {idx}\n') + # not a hexdump line, produce any collected data + if len(data) > 0: + yield data + data = b'' + offset = 0 if self._leading_regex is None \ + or self._leading_regex.match(l) else -1 + if len(data) > 0: + yield data diff --git a/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/server.py b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/server.py new file mode 100644 index 00000000000000..9f4e9a03f8eee0 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/server.py @@ -0,0 +1,137 @@ +import logging +import os +import re +import subprocess +import time +from datetime import datetime, timedelta +from threading import Thread + +from .tls import HandShake +from .env import Env, CryptoLib +from .log import LogFile, HexDumpScanner + + +log = logging.getLogger(__name__) + + +class ServerRun: + + def __init__(self, env: Env, logfile: LogFile): + self.env = env + self._logfile = logfile + self.log_lines = self._logfile.get_recent() + self._data_recs = None + self._hs_recs = None + if self.env.verbose > 1: + log.debug(f'read {len(self.log_lines)} lines from {logfile.path}') + + @property + def handshake(self): + if self._data_recs is None: + self._data_recs = [data for data in HexDumpScanner(source=self.log_lines)] + if self.env.verbose > 1: + log.debug(f'detected {len(self._data_recs)} hexdumps ' + f'in {self._logfile.path}') + if self._hs_recs is None: + self._hs_recs = [hrec for hrec in HandShake(source=self._data_recs, + verbose=self.env.verbose)] + if self.env.verbose > 1: + log.debug(f'detected {len(self._hs_recs)} crypto records ' + f'in {self._logfile.path}') + return self._hs_recs + + @property + def hs_stripe(self): + return ":".join([hrec.name for hrec in self.handshake]) + + +def monitor_proc(env: Env, proc): + _env = env + proc.wait() + + +class ExampleServer: + + def __init__(self, env: Env, crypto_lib: str, verify_client=False): + self.env = env + self._crypto_lib = crypto_lib + self._path = env.server_path(self._crypto_lib) + self._logpath = f'{self.env.gen_dir}/{self._crypto_lib}-server.log' + self._log = LogFile(path=self._logpath) + self._logfile = None + self._process = None + self._verify_client = verify_client + + @property + def path(self): + return self._path + + @property + def crypto_lib(self): + return self._crypto_lib + + @property + def uses_cipher_config(self): + return CryptoLib.uses_cipher_config(self.crypto_lib) + + def supports_cipher(self, cipher): + return CryptoLib.supports_cipher(self.crypto_lib, cipher) + + @property + def log(self): + return self._log + + def exists(self): + return os.path.isfile(self.path) + + def start(self): + if self._process is not None: + return False + creds = self.env.get_server_credentials() + assert creds + args = [ + self.path, + f'--htdocs={self.env.htdocs_dir}', + ] + if self._verify_client: + args.append('--verify-client') + args.extend([ + '*', str(self.env.examples_port), + creds.pkey_file, creds.cert_file + ]) + self._logfile = open(self._logpath, 'w') + self._process = subprocess.Popen(args=args, text=True, + stdout=self._logfile, stderr=self._logfile) + t = Thread(target=monitor_proc, daemon=True, args=(self.env, self._process)) + t.start() + timeout = 5 + end = datetime.now() + timedelta(seconds=timeout) + while True: + if self._process.poll(): + return False + try: + if self.log.scan_recent(pattern=re.compile(r'^Using document root'), timeout=0.5): + break + except TimeoutError: + pass + if datetime.now() > end: + raise TimeoutError(f"pattern not found in error log after {timeout} seconds") + self.log.advance() + return True + + def stop(self): + if self._process: + self._process.terminate() + self._process = None + if self._logfile: + self._logfile.close() + self._logfile = None + return True + + def restart(self): + self.stop() + self._log.reset() + return self.start() + + def get_run(self) -> ServerRun: + return ServerRun(env=self.env, logfile=self.log) diff --git a/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/tls.py b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/tls.py new file mode 100644 index 00000000000000..414b94d38e5fe3 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/ngtcp2test/tls.py @@ -0,0 +1,983 @@ +import binascii +import logging +import sys +from collections.abc import Iterator +from typing import Dict, Any, Iterable + + +log = logging.getLogger(__name__) + + +class ParseError(Exception): + pass + +def _get_int(d, n): + if len(d) < n: + raise ParseError(f'get_int: {n} bytes needed, but data is {d}') + if n == 1: + dlen = d[0] + else: + dlen = int.from_bytes(d[0:n], byteorder='big') + return d[n:], dlen + + +def _get_field(d, dlen): + if dlen > 0: + if len(d) < dlen: + raise ParseError(f'field len={dlen}, but data len={len(d)}') + field = d[0:dlen] + return d[dlen:], field + return d, b'' + + +def _get_len_field(d, n): + d, dlen = _get_int(d, n) + return _get_field(d, dlen) + + +# d are bytes that start with a quic variable length integer +def _get_qint(d): + i = d[0] & 0xc0 + if i == 0: + return d[1:], int(d[0]) + elif i == 0x40: + ndata = bytearray(d[0:2]) + d = d[2:] + ndata[0] = ndata[0] & ~0xc0 + return d, int.from_bytes(ndata, byteorder='big') + elif i == 0x80: + ndata = bytearray(d[0:4]) + d = d[4:] + ndata[0] = ndata[0] & ~0xc0 + return d, int.from_bytes(ndata, byteorder='big') + else: + ndata = bytearray(d[0:8]) + d = d[8:] + ndata[0] = ndata[0] & ~0xc0 + return d, int.from_bytes(ndata, byteorder='big') + + +class TlsSupportedGroups: + NAME_BY_ID = { + 0: 'Reserved', + 1: 'sect163k1', + 2: 'sect163r1', + 3: 'sect163r2', + 4: 'sect193r1', + 5: 'sect193r2', + 6: 'sect233k1', + 7: 'sect233r1', + 8: 'sect239k1', + 9: 'sect283k1', + 10: 'sect283r1', + 11: 'sect409k1', + 12: 'sect409r1', + 13: 'sect571k1', + 14: 'sect571r1', + 15: 'secp160k1', + 16: 'secp160r1', + 17: 'secp160r2', + 18: 'secp192k1', + 19: 'secp192r1', + 20: 'secp224k1', + 21: 'secp224r1', + 22: 'secp256k1', + 23: 'secp256r1', + 24: 'secp384r1', + 25: 'secp521r1', + 26: 'brainpoolP256r1', + 27: 'brainpoolP384r1', + 28: 'brainpoolP512r1', + 29: 'x25519', + 30: 'x448', + 31: 'brainpoolP256r1tls13', + 32: 'brainpoolP384r1tls13', + 33: 'brainpoolP512r1tls13', + 34: 'GC256A', + 35: 'GC256B', + 36: 'GC256C', + 37: 'GC256D', + 38: 'GC512A', + 39: 'GC512B', + 40: 'GC512C', + 41: 'curveSM2', + } + + @classmethod + def name(cls, gid): + if gid in cls.NAME_BY_ID: + return f'{cls.NAME_BY_ID[gid]}(0x{gid:0x})' + return f'0x{gid:0x}' + + +class TlsSignatureScheme: + NAME_BY_ID = { + 0x0201: 'rsa_pkcs1_sha1', + 0x0202: 'Reserved', + 0x0203: 'ecdsa_sha1', + 0x0401: 'rsa_pkcs1_sha256', + 0x0403: 'ecdsa_secp256r1_sha256', + 0x0420: 'rsa_pkcs1_sha256_legacy', + 0x0501: 'rsa_pkcs1_sha384', + 0x0503: 'ecdsa_secp384r1_sha384', + 0x0520: 'rsa_pkcs1_sha384_legacy', + 0x0601: 'rsa_pkcs1_sha512', + 0x0603: 'ecdsa_secp521r1_sha512', + 0x0620: 'rsa_pkcs1_sha512_legacy', + 0x0704: 'eccsi_sha256', + 0x0705: 'iso_ibs1', + 0x0706: 'iso_ibs2', + 0x0707: 'iso_chinese_ibs', + 0x0708: 'sm2sig_sm3', + 0x0709: 'gostr34102012_256a', + 0x070A: 'gostr34102012_256b', + 0x070B: 'gostr34102012_256c', + 0x070C: 'gostr34102012_256d', + 0x070D: 'gostr34102012_512a', + 0x070E: 'gostr34102012_512b', + 0x070F: 'gostr34102012_512c', + 0x0804: 'rsa_pss_rsae_sha256', + 0x0805: 'rsa_pss_rsae_sha384', + 0x0806: 'rsa_pss_rsae_sha512', + 0x0807: 'ed25519', + 0x0808: 'ed448', + 0x0809: 'rsa_pss_pss_sha256', + 0x080A: 'rsa_pss_pss_sha384', + 0x080B: 'rsa_pss_pss_sha512', + 0x081A: 'ecdsa_brainpoolP256r1tls13_sha256', + 0x081B: 'ecdsa_brainpoolP384r1tls13_sha384', + 0x081C: 'ecdsa_brainpoolP512r1tls13_sha512', + } + + @classmethod + def name(cls, gid): + if gid in cls.NAME_BY_ID: + return f'{cls.NAME_BY_ID[gid]}(0x{gid:0x})' + return f'0x{gid:0x}' + + +class TlsCipherSuites: + NAME_BY_ID = { + 0x1301: 'TLS_AES_128_GCM_SHA256', + 0x1302: 'TLS_AES_256_GCM_SHA384', + 0x1303: 'TLS_CHACHA20_POLY1305_SHA256', + 0x1304: 'TLS_AES_128_CCM_SHA256', + 0x1305: 'TLS_AES_128_CCM_8_SHA256', + 0x00ff: 'TLS_EMPTY_RENEGOTIATION_INFO_SCSV', + } + + @classmethod + def name(cls, cid): + if cid in cls.NAME_BY_ID: + return f'{cls.NAME_BY_ID[cid]}(0x{cid:0x})' + return f'Cipher(0x{cid:0x})' + + +class PskKeyExchangeMode: + NAME_BY_ID = { + 0x00: 'psk_ke', + 0x01: 'psk_dhe_ke', + } + + @classmethod + def name(cls, gid): + if gid in cls.NAME_BY_ID: + return f'{cls.NAME_BY_ID[gid]}(0x{gid:0x})' + return f'0x{gid:0x}' + + +class QuicTransportParam: + NAME_BY_ID = { + 0x00: 'original_destination_connection_id', + 0x01: 'max_idle_timeout', + 0x02: 'stateless_reset_token', + 0x03: 'max_udp_payload_size', + 0x04: 'initial_max_data', + 0x05: 'initial_max_stream_data_bidi_local', + 0x06: 'initial_max_stream_data_bidi_remote', + 0x07: 'initial_max_stream_data_uni', + 0x08: 'initial_max_streams_bidi', + 0x09: 'initial_max_streams_uni', + 0x0a: 'ack_delay_exponent', + 0x0b: 'max_ack_delay', + 0x0c: 'disable_active_migration', + 0x0d: 'preferred_address', + 0x0e: 'active_connection_id_limit', + 0x0f: 'initial_source_connection_id', + 0x10: 'retry_source_connection_id', + } + TYPE_BY_ID = { + 0x00: bytes, + 0x01: int, + 0x02: bytes, + 0x03: int, + 0x04: int, + 0x05: int, + 0x06: int, + 0x07: int, + 0x08: int, + 0x09: int, + 0x0a: int, + 0x0b: int, + 0x0c: int, + 0x0d: bytes, + 0x0e: int, + 0x0f: bytes, + 0x10: bytes, + } + + @classmethod + def name(cls, cid): + if cid in cls.NAME_BY_ID: + return f'{cls.NAME_BY_ID[cid]}(0x{cid:0x})' + return f'QuicTP(0x{cid:0x})' + + @classmethod + def is_qint(cls, cid): + if cid in cls.TYPE_BY_ID: + return cls.TYPE_BY_ID[cid] == int + return False + + +class Extension: + + def __init__(self, eid, name, edata, hsid): + self._eid = eid + self._name = name + self._edata = edata + self._hsid = hsid + + @property + def data(self): + return self._edata + + @property + def hsid(self): + return self._hsid + + def to_json(self): + jdata = { + 'id': self._eid, + 'name': self._name, + } + if len(self.data) > 0: + jdata['data'] = binascii.hexlify(self.data).decode() + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + s = f'{ind}{self._name}(0x{self._eid:0x})' + if len(self._edata): + s += f'\n{ind} data({len(self._edata)}): ' \ + f'{binascii.hexlify(self._edata).decode()}' + return s + + +class ExtSupportedGroups(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = edata + self._groups = [] + while len(d) > 0: + d, gid = _get_int(d, 2) + self._groups.append(gid) + + def to_json(self): + jdata = { + 'id': self._eid, + 'name': self._name, + } + if len(self._groups): + jdata['groups'] = [TlsSupportedGroups.name(gid) + for gid in self._groups] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + gnames = [TlsSupportedGroups.name(gid) for gid in self._groups] + s = f'{ind}{self._name}(0x{self._eid:0x}): {", ".join(gnames)}' + return s + + +class ExtKeyShare(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + self._keys = [] + self._group = None + self._pubkey = None + if self.hsid == 2: # ServerHello + # single key share (group, pubkey) + d, self._group = _get_int(d, 2) + d, self._pubkey = _get_len_field(d, 2) + elif self.hsid == 6: # HelloRetryRequest + assert len(d) == 2 + d, self._group = _get_int(d, 2) + else: + # list if key shares (group, pubkey) + d, shares = _get_len_field(d, 2) + while len(shares) > 0: + shares, group = _get_int(shares, 2) + shares, pubkey = _get_len_field(shares, 2) + self._keys.append({ + 'group': TlsSupportedGroups.name(group), + 'pubkey': binascii.hexlify(pubkey).decode() + }) + + def to_json(self): + jdata = super().to_json() + if self._group is not None: + jdata['group'] = TlsSupportedGroups.name(self._group) + if self._pubkey is not None: + jdata['pubkey'] = binascii.hexlify(self._pubkey).decode() + if len(self._keys) > 0: + jdata['keys'] = self._keys + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + s = f'{ind}{self._name}(0x{self._eid:0x})' + if self._group is not None: + s += f'\n{ind} group: {TlsSupportedGroups.name(self._group)}' + if self._pubkey is not None: + s += f'\n{ind} pubkey: {binascii.hexlify(self._pubkey).decode()}' + if len(self._keys) > 0: + for idx, key in enumerate(self._keys): + s += f'\n{ind} {idx}: {key["group"]}, {key["pubkey"]}' + return s + + +class ExtSNI(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + self._indicators = [] + while len(d) > 0: + d, entry = _get_len_field(d, 2) + entry, stype = _get_int(entry, 1) + entry, sname = _get_len_field(entry, 2) + self._indicators.append({ + 'type': stype, + 'name': sname.decode() + }) + + def to_json(self): + jdata = super().to_json() + for i in self._indicators: + if i['type'] == 0: + jdata['host_name'] = i['name'] + else: + jdata[f'0x{i["type"]}'] = i['name'] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + s = f'{ind}{self._name}(0x{self._eid:0x})' + if len(self._indicators) == 1 and self._indicators[0]['type'] == 0: + s += f': {self._indicators[0]["name"]}' + else: + for i in self._indicators: + ikey = 'host_name' if i["type"] == 0 else f'type(0x{i["type"]:0x}' + s += f'\n{ind} {ikey}: {i["name"]}' + return s + + +class ExtALPN(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + d, list_len = _get_int(d, 2) + self._protocols = [] + while len(d) > 0: + d, proto = _get_len_field(d, 1) + self._protocols.append(proto.decode()) + + def to_json(self): + jdata = super().to_json() + if len(self._protocols) == 1: + jdata['alpn'] = self._protocols[0] + else: + jdata['alpn'] = self._protocols + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind}{self._name}(0x{self._eid:0x}): {", ".join(self._protocols)}' + + +class ExtEarlyData(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + self._max_size = None + d = self.data + if hsid == 4: # SessionTicket + assert len(d) == 4, f'expected 4, len is {len(d)} data={d}' + d, self._max_size = _get_int(d, 4) + else: + assert len(d) == 0 + + def to_json(self): + jdata = super().to_json() + if self._max_size is not None: + jdata['max_size'] = self._max_size + return jdata + + +class ExtSignatureAlgorithms(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + d, list_len = _get_int(d, 2) + self._algos = [] + while len(d) > 0: + d, algo = _get_int(d, 2) + self._algos.append(TlsSignatureScheme.name(algo)) + + def to_json(self): + jdata = super().to_json() + if len(self._algos) > 0: + jdata['algorithms'] = self._algos + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind}{self._name}(0x{self._eid:0x}): {", ".join(self._algos)}' + + +class ExtPSKExchangeModes(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + d, list_len = _get_int(d, 1) + self._modes = [] + while len(d) > 0: + d, mode = _get_int(d, 1) + self._modes.append(PskKeyExchangeMode.name(mode)) + + def to_json(self): + jdata = super().to_json() + jdata['modes'] = self._modes + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind}{self._name}(0x{self._eid:0x}): {", ".join(self._modes)}' + + +class ExtPreSharedKey(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + self._kid = None + self._identities = None + self._binders = None + d = self.data + if hsid == 1: # client hello + d, idata = _get_len_field(d, 2) + self._identities = [] + while len(idata): + idata, identity = _get_len_field(idata, 2) + idata, obfs_age = _get_int(idata, 4) + self._identities.append({ + 'id': binascii.hexlify(identity).decode(), + 'age': obfs_age, + }) + d, binders = _get_len_field(d, 2) + self._binders = [] + while len(binders) > 0: + binders, hmac = _get_len_field(binders, 1) + self._binders.append(binascii.hexlify(hmac).decode()) + assert len(d) == 0 + else: + d, self._kid = _get_int(d, 2) + + def to_json(self): + jdata = super().to_json() + if self.hsid == 1: + jdata['identities'] = self._identities + jdata['binders'] = self._binders + else: + jdata['identity'] = self._kid + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + s = f'{ind}{self._name}(0x{self._hsid:0x})' + if self.hsid == 1: + for idx, i in enumerate(self._identities): + s += f'\n{ind} {idx}: {i["id"]} ({i["age"]})' + s += f'\n{ind} binders: {self._binders}' + else: + s += f'\n{ind} identity: {self._kid}' + return s + + +class ExtSupportedVersions(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + self._versions = [] + if hsid == 1: # client hello + d, list_len = _get_int(d, 1) + while len(d) > 0: + d, version = _get_int(d, 2) + self._versions.append(f'0x{version:0x}') + else: + d, version = _get_int(d, 2) + self._versions.append(f'0x{version:0x}') + + def to_json(self): + jdata = super().to_json() + if len(self._versions) == 1: + jdata['version'] = self._versions[0] + else: + jdata['versions'] = self._versions + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind}{self._name}(0x{self._eid:0x}): {", ".join(self._versions)}' + + +class ExtQuicTP(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + self._params = [] + while len(d) > 0: + d, ptype = _get_qint(d) + d, plen = _get_qint(d) + d, pvalue = _get_field(d, plen) + if QuicTransportParam.is_qint(ptype): + _, pvalue = _get_qint(pvalue) + else: + pvalue = binascii.hexlify(pvalue).decode() + self._params.append({ + 'key': QuicTransportParam.name(ptype), + 'value': pvalue, + }) + + def to_json(self): + jdata = super().to_json() + jdata['params'] = self._params + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + s = f'{ind}{self._name}(0x{self._eid:0x})' + for p in self._params: + s += f'\n{ind} {p["key"]}: {p["value"]}' + return s + + +class TlsExtensions: + + EXT_TYPES = [ + (0x00, 'SNI', ExtSNI), + (0x01, 'MAX_FRAGMENT_LENGTH', Extension), + (0x03, 'TRUSTED_CA_KEYS', Extension), + (0x04, 'TRUNCATED_HMAC', Extension), + (0x05, 'OSCP_STATUS_REQUEST', Extension), + (0x0a, 'SUPPORTED_GROUPS', ExtSupportedGroups), + (0x0b, 'EC_POINT_FORMATS', Extension), + (0x0d, 'SIGNATURE_ALGORITHMS', ExtSignatureAlgorithms), + (0x0e, 'USE_SRTP', Extension), + (0x10, 'ALPN', ExtALPN), + (0x11, 'STATUS_REQUEST_V2', Extension), + (0x16, 'ENCRYPT_THEN_MAC', Extension), + (0x17, 'EXTENDED_MASTER_SECRET', Extension), + (0x23, 'SESSION_TICKET', Extension), + (0x29, 'PRE_SHARED_KEY', ExtPreSharedKey), + (0x2a, 'EARLY_DATA', ExtEarlyData), + (0x2b, 'SUPPORTED_VERSIONS', ExtSupportedVersions), + (0x2c, 'COOKIE', Extension), + (0x2d, 'PSK_KEY_EXCHANGE_MODES', ExtPSKExchangeModes), + (0x31, 'POST_HANDSHAKE_AUTH', Extension), + (0x32, 'SIGNATURE_ALGORITHMS_CERT', Extension), + (0x33, 'KEY_SHARE', ExtKeyShare), + (0x39, 'QUIC_TP_PARAMS', ExtQuicTP), + (0xff01, 'RENEGOTIATION_INFO', Extension), + ] + NAME_BY_ID = {} + CLASS_BY_ID = {} + + @classmethod + def init(cls): + for (eid, name, ecls) in cls.EXT_TYPES: + cls.NAME_BY_ID[eid] = name + cls.CLASS_BY_ID[eid] = ecls + + @classmethod + def from_data(cls, hsid, data): + exts = [] + d = data + while len(d): + d, eid = _get_int(d, 2) + d, elen = _get_int(d, 2) + d, edata = _get_field(d, elen) + if eid in cls.NAME_BY_ID: + ename = cls.NAME_BY_ID[eid] + ecls = cls.CLASS_BY_ID[eid] + exts.append(ecls(eid=eid, name=ename, edata=edata, hsid=hsid)) + else: + exts.append(Extension(eid=eid, name=f'(0x{eid:0x})', + edata=edata, hsid=hsid)) + return exts + + +TlsExtensions.init() + + +class HSRecord: + + def __init__(self, hsid: int, name: str, data): + self._hsid = hsid + self._name = name + self._data = data + + @property + def hsid(self): + return self._hsid + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + @property + def data(self): + return self._data + + def __repr__(self): + return f'{self.name}[{binascii.hexlify(self._data).decode()}]' + + def to_json(self) -> Dict[str, Any]: + return { + 'name': self.name, + 'data': binascii.hexlify(self._data).decode(), + } + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind}{self._name}\n'\ + f'{ind} id: 0x{self._hsid:0x}\n'\ + f'{ind} data({len(self._data)}): '\ + f'{binascii.hexlify(self._data).decode()}' + + +class ClientHello(HSRecord): + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, self._version = _get_int(d, 2) + d, self._random = _get_field(d, 32) + d, self._session_id = _get_len_field(d, 1) + self._ciphers = [] + d, ciphers = _get_len_field(d, 2) + while len(ciphers): + ciphers, cipher = _get_int(ciphers, 2) + self._ciphers.append(TlsCipherSuites.name(cipher)) + d, comps = _get_len_field(d, 1) + self._compressions = [int(c) for c in comps] + d, edata = _get_len_field(d, 2) + self._extensions = TlsExtensions.from_data(hsid, edata) + + def to_json(self): + jdata = super().to_json() + jdata['version'] = f'0x{self._version:0x}' + jdata['random'] = f'{binascii.hexlify(self._random).decode()}' + jdata['session_id'] = binascii.hexlify(self._session_id).decode() + jdata['ciphers'] = self._ciphers + jdata['compressions'] = self._compressions + jdata['extensions'] = [ext.to_json() for ext in self._extensions] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return super().to_text(indent=indent) + '\n'\ + f'{ind} version: 0x{self._version:0x}\n'\ + f'{ind} random: {binascii.hexlify(self._random).decode()}\n' \ + f'{ind} session_id: {binascii.hexlify(self._session_id).decode()}\n' \ + f'{ind} ciphers: {", ".join(self._ciphers)}\n'\ + f'{ind} compressions: {self._compressions}\n'\ + f'{ind} extensions: \n' + '\n'.join( + [ext.to_text(indent=indent+4) for ext in self._extensions]) + + +class ServerHello(HSRecord): + + HELLO_RETRY_RANDOM = binascii.unhexlify( + 'CF21AD74E59A6111BE1D8C021E65B891C2A211167ABB8C5E079E09E2C8A8339C' + ) + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, self._version = _get_int(d, 2) + d, self._random = _get_field(d, 32) + if self._random == self.HELLO_RETRY_RANDOM: + self.name = 'HelloRetryRequest' + hsid = 6 + d, self._session_id = _get_len_field(d, 1) + d, cipher = _get_int(d, 2) + self._cipher = TlsCipherSuites.name(cipher) + d, self._compression = _get_int(d, 1) + d, edata = _get_len_field(d, 2) + self._extensions = TlsExtensions.from_data(hsid, edata) + + def to_json(self): + jdata = super().to_json() + jdata['version'] = f'0x{self._version:0x}' + jdata['random'] = f'{binascii.hexlify(self._random).decode()}' + jdata['session_id'] = binascii.hexlify(self._session_id).decode() + jdata['cipher'] = self._cipher + jdata['compression'] = int(self._compression) + jdata['extensions'] = [ext.to_json() for ext in self._extensions] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return super().to_text(indent=indent) + '\n'\ + f'{ind} version: 0x{self._version:0x}\n'\ + f'{ind} random: {binascii.hexlify(self._random).decode()}\n' \ + f'{ind} session_id: {binascii.hexlify(self._session_id).decode()}\n' \ + f'{ind} cipher: {self._cipher}\n'\ + f'{ind} compression: {int(self._compression)}\n'\ + f'{ind} extensions: \n' + '\n'.join( + [ext.to_text(indent=indent+4) for ext in self._extensions]) + + +class EncryptedExtensions(HSRecord): + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, edata = _get_len_field(d, 2) + self._extensions = TlsExtensions.from_data(hsid, edata) + + def to_json(self): + jdata = super().to_json() + jdata['extensions'] = [ext.to_json() for ext in self._extensions] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return super().to_text(indent=indent) + '\n'\ + f'{ind} extensions: \n' + '\n'.join( + [ext.to_text(indent=indent+4) for ext in self._extensions]) + + +class CertificateRequest(HSRecord): + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, self._context = _get_int(d, 1) + d, edata = _get_len_field(d, 2) + self._extensions = TlsExtensions.from_data(hsid, edata) + + def to_json(self): + jdata = super().to_json() + jdata['context'] = self._context + jdata['extensions'] = [ext.to_json() for ext in self._extensions] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return super().to_text(indent=indent) + '\n'\ + f'{ind} context: {self._context}\n'\ + f'{ind} extensions: \n' + '\n'.join( + [ext.to_text(indent=indent+4) for ext in self._extensions]) + + +class Certificate(HSRecord): + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, self._context = _get_int(d, 1) + d, clist = _get_len_field(d, 3) + self._cert_entries = [] + while len(clist) > 0: + clist, cert_data = _get_len_field(clist, 3) + clist, cert_exts = _get_len_field(clist, 2) + exts = TlsExtensions.from_data(hsid, cert_exts) + self._cert_entries.append({ + 'cert': binascii.hexlify(cert_data).decode(), + 'extensions': exts, + }) + + def to_json(self): + jdata = super().to_json() + jdata['context'] = self._context + jdata['certificate_list'] = [{ + 'cert': e['cert'], + 'extensions': [x.to_json() for x in e['extensions']], + } for e in self._cert_entries] + return jdata + + def _enxtry_text(self, e, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind} cert: {e["cert"]}\n'\ + f'{ind} extensions: \n' + '\n'.join( + [x.to_text(indent=indent + 4) for x in e['extensions']]) + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return super().to_text(indent=indent) + '\n'\ + f'{ind} context: {self._context}\n'\ + f'{ind} certificate_list: \n' + '\n'.join( + [self._enxtry_text(e, indent+4) for e in self._cert_entries]) + + +class SessionTicket(HSRecord): + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, self._lifetime = _get_int(d, 4) + d, self._age = _get_int(d, 4) + d, self._nonce = _get_len_field(d, 1) + d, self._ticket = _get_len_field(d, 2) + d, edata = _get_len_field(d, 2) + self._extensions = TlsExtensions.from_data(hsid, edata) + + def to_json(self): + jdata = super().to_json() + jdata['lifetime'] = self._lifetime + jdata['age'] = self._age + jdata['nonce'] = binascii.hexlify(self._nonce).decode() + jdata['ticket'] = binascii.hexlify(self._ticket).decode() + jdata['extensions'] = [ext.to_json() for ext in self._extensions] + return jdata + + +class HSIterator(Iterator): + + def __init__(self, recs): + self._recs = recs + self._index = 0 + + def __iter__(self): + return self + + def __next__(self): + try: + result = self._recs[self._index] + except IndexError: + raise StopIteration + self._index += 1 + return result + + +class HandShake: + REC_TYPES = [ + (1, 'ClientHello', ClientHello), + (2, 'ServerHello', ServerHello), + (3, 'HelloVerifyRequest', HSRecord), + (4, 'SessionTicket', SessionTicket), + (5, 'EndOfEarlyData', HSRecord), + (6, 'HelloRetryRequest', ServerHello), + (8, 'EncryptedExtensions', EncryptedExtensions), + (11, 'Certificate', Certificate), + (12, 'ServerKeyExchange ', HSRecord), + (13, 'CertificateRequest', CertificateRequest), + (14, 'ServerHelloDone', HSRecord), + (15, 'CertificateVerify', HSRecord), + (16, 'ClientKeyExchange', HSRecord), + (20, 'Finished', HSRecord), + (22, 'CertificateStatus', HSRecord), + (24, 'KeyUpdate', HSRecord), + (25, 'CompressedCertificate', HSRecord), + ] + RT_NAME_BY_ID = {} + RT_CLS_BY_ID = {} + + @classmethod + def _parse_rec(cls, data): + d, hsid = _get_int(data, 1) + if hsid not in cls.RT_CLS_BY_ID: + raise ParseError(f'unknown type {hsid}') + d, rec_len = _get_int(d, 3) + if rec_len > len(d): + # incomplete, need more data + return data, None + d, rec_data = _get_field(d, rec_len) + if hsid in cls.RT_CLS_BY_ID: + name = cls.RT_NAME_BY_ID[hsid] + rcls = cls.RT_CLS_BY_ID[hsid] + else: + name = f'CryptoRecord(0x{hsid:0x})' + rcls = HSRecord + return d, rcls(hsid=hsid, name=name, data=rec_data) + + @classmethod + def _parse(cls, source, strict=False, verbose: int = 0): + d = b'' + hsid = 0 + hsrecs = [] + if verbose > 0: + log.debug(f'scanning for handshake records') + blocks = [d for d in source] + while len(blocks) > 0: + try: + total_data = b''.join(blocks) + remain, r = cls._parse_rec(total_data) + if r is None: + # if we could not recognize a record, skip the first + # data block and try again + blocks = blocks[1:] + continue + hsrecs.append(r) + cons_len = len(total_data) - len(remain) + while cons_len > 0 and len(blocks) > 0: + if cons_len >= len(blocks[0]): + cons_len -= len(blocks[0]) + blocks = blocks[1:] + else: + blocks[0] = blocks[0][cons_len:] + cons_len = 0 + if verbose > 2: + log.debug(f'added record: {r.to_text()}') + except ParseError as err: + # if we could not recognize a record, skip the first + # data block and try again + blocks = blocks[1:] + if len(blocks) > 0 and strict: + raise Exception(f'possibly incomplete handshake record ' + f'id={hsid}, from raw={blocks}\n') + return hsrecs + + + + @classmethod + def init(cls): + for (hsid, name, rcls) in cls.REC_TYPES: + cls.RT_NAME_BY_ID[hsid] = name + cls.RT_CLS_BY_ID[hsid] = rcls + + def __init__(self, source: Iterable[bytes], strict: bool = False, + verbose: int = 0): + self._source = source + self._strict = strict + self._verbose = verbose + + def __iter__(self): + return HSIterator(recs=self._parse(self._source, strict=self._strict, + verbose=self._verbose)) + + +HandShake.init() diff --git a/deps/ngtcp2/ngtcp2/examples/tests/test_01_handshake.py b/deps/ngtcp2/ngtcp2/examples/tests/test_01_handshake.py new file mode 100644 index 00000000000000..f1a01d1fec397b --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/test_01_handshake.py @@ -0,0 +1,30 @@ +import pytest + +from .ngtcp2test import ExampleClient +from .ngtcp2test import ExampleServer +from .ngtcp2test import Env + + +@pytest.mark.skipif(condition=len(Env.get_crypto_libs()) == 0, + reason="no crypto lib examples configured") +class TestHandshake: + + @pytest.fixture(scope='class', params=Env.get_crypto_libs()) + def server(self, env, request) -> ExampleServer: + s = ExampleServer(env=env, crypto_lib=request.param) + assert s.exists(), f'server not found: {s.path}' + assert s.start() + yield s + s.stop() + + @pytest.fixture(scope='function', params=Env.get_crypto_libs()) + def client(self, env, request) -> ExampleClient: + client = ExampleClient(env=env, crypto_lib=request.param) + assert client.exists() + yield client + + def test_01_01_get(self, env: Env, server, client): + # run simple GET, no sessions, needs to give full handshake + cr = client.http_get(server, url=f'https://{env.example_domain}/') + assert cr.returncode == 0 + cr.assert_non_resume_handshake() diff --git a/deps/ngtcp2/ngtcp2/examples/tests/test_02_resume.py b/deps/ngtcp2/ngtcp2/examples/tests/test_02_resume.py new file mode 100644 index 00000000000000..3de1344e4b2d0e --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/test_02_resume.py @@ -0,0 +1,46 @@ +import pytest + +from .ngtcp2test import ExampleClient +from .ngtcp2test import ExampleServer +from .ngtcp2test import Env + + +@pytest.mark.skipif(condition=len(Env.get_crypto_libs()) == 0, + reason="no crypto lib examples configured") +class TestResume: + + @pytest.fixture(scope='class', params=Env.get_crypto_libs()) + def server(self, env, request) -> ExampleServer: + s = ExampleServer(env=env, crypto_lib=request.param) + assert s.exists(), f'server not found: {s.path}' + assert s.start() + yield s + s.stop() + + @pytest.fixture(scope='function', params=Env.get_crypto_libs()) + def client(self, env, request) -> ExampleClient: + client = ExampleClient(env=env, crypto_lib=request.param) + assert client.exists() + yield client + + def test_02_01(self, env: Env, server, client): + # run GET with sessions but no early data, cleared first, then reused + client.clear_session() + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, + extra_args=['--disable-early-data']) + assert cr.returncode == 0 + cr.assert_non_resume_handshake() + # Now do this again and we expect a resumption, meaning no certificate + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, + extra_args=['--disable-early-data']) + assert cr.returncode == 0 + cr.assert_resume_handshake() + # restart the server, do it again + server.restart() + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, + extra_args=['--disable-early-data']) + assert cr.returncode == 0 + cr.assert_non_resume_handshake() diff --git a/deps/ngtcp2/ngtcp2/examples/tests/test_03_earlydata.py b/deps/ngtcp2/ngtcp2/examples/tests/test_03_earlydata.py new file mode 100644 index 00000000000000..a0170c31b12899 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/test_03_earlydata.py @@ -0,0 +1,56 @@ +import pytest + +from .ngtcp2test import ExampleClient +from .ngtcp2test import ExampleServer +from .ngtcp2test import Env + + +@pytest.mark.skipif(condition=len(Env.get_crypto_libs()) == 0, + reason="no crypto lib examples configured") +class TestEarlyData: + + @pytest.fixture(scope='class', params=Env.get_crypto_libs()) + def server(self, env, request) -> ExampleServer: + s = ExampleServer(env=env, crypto_lib=request.param) + assert s.exists(), f'server not found: {s.path}' + assert s.start() + yield s + s.stop() + + @pytest.fixture(scope='function', params=Env.get_crypto_libs()) + def client(self, env, request) -> ExampleClient: + client = ExampleClient(env=env, crypto_lib=request.param) + assert client.exists() + yield client + + def test_03_01(self, env: Env, server, client): + # run GET with sessions, cleared first, without a session, early + # data will not even be attempted + client.clear_session() + edata = 'This is the early data. It is not much.' + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, data=edata) + assert cr.returncode == 0 + cr.assert_non_resume_handshake() + # resume session, early data is sent and accepted + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, data=edata) + assert cr.returncode == 0 + cr.assert_resume_handshake() + assert not cr.early_data_rejected + # restart the server, resume, early data is attempted but will not work + server.restart() + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, data=edata) + assert cr.returncode == 0 + assert cr.early_data_rejected + cr.assert_non_resume_handshake() + # restart again, sent data, but not as early data + server.restart() + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, data=edata, + extra_args=['--disable-early-data']) + assert cr.returncode == 0 + # we see no rejection, since it was not used + assert not cr.early_data_rejected + cr.assert_non_resume_handshake() diff --git a/deps/ngtcp2/ngtcp2/examples/tests/test_04_clientcert.py b/deps/ngtcp2/ngtcp2/examples/tests/test_04_clientcert.py new file mode 100644 index 00000000000000..bde1b187fda9d9 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/test_04_clientcert.py @@ -0,0 +1,57 @@ +import pytest + +from .ngtcp2test import ExampleClient +from .ngtcp2test import ExampleServer +from .ngtcp2test import Env + + +@pytest.mark.skipif(condition=len(Env.get_crypto_libs()) == 0, + reason="no crypto lib examples configured") +class TestClientCert: + + @pytest.fixture(scope='class', params=Env.get_crypto_libs()) + def server(self, env, request) -> ExampleServer: + s = ExampleServer(env=env, crypto_lib=request.param, + verify_client=True) + assert s.exists(), f'server not found: {s.path}' + assert s.start() + yield s + s.stop() + + @pytest.fixture(scope='function', params=Env.get_crypto_libs()) + def client(self, env, request) -> ExampleClient: + client = ExampleClient(env=env, crypto_lib=request.param) + assert client.exists() + yield client + + def test_04_01(self, env: Env, server, client): + # run GET with a server requesting a cert, client has none to offer + cr = client.http_get(server, url=f'https://{env.example_domain}/') + assert cr.returncode == 0 + cr.assert_verify_null_handshake() + creqs = [r for r in cr.handshake if r.hsid == 13] # CertificateRequest + assert len(creqs) == 1 + creq = creqs[0].to_json() + certs = [r for r in cr.server.handshake if r.hsid == 11] # Certificate + assert len(certs) == 1 + crec = certs[0].to_json() + assert len(crec['certificate_list']) == 0 + assert creq['context'] == crec['context'] + # TODO: check that GET had no answer + + def test_04_02(self, env: Env, server, client): + # run GET with a server requesting a cert, client has cert to offer + credentials = env.ca.get_first("clientsX") + cr = client.http_get(server, url=f'https://{env.example_domain}/', + credentials=credentials) + assert cr.returncode == 0 + cr.assert_verify_cert_handshake() + creqs = [r for r in cr.handshake if r.hsid == 13] # CertificateRequest + assert len(creqs) == 1 + creq = creqs[0].to_json() + certs = [r for r in cr.server.handshake if r.hsid == 11] # Certificate + assert len(certs) == 1 + crec = certs[0].to_json() + assert len(crec['certificate_list']) == 1 + assert creq['context'] == crec['context'] + # TODO: check that GET indeed gave a response diff --git a/deps/ngtcp2/ngtcp2/examples/tests/test_05_ciphers.py b/deps/ngtcp2/ngtcp2/examples/tests/test_05_ciphers.py new file mode 100644 index 00000000000000..27f326e99dd267 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tests/test_05_ciphers.py @@ -0,0 +1,46 @@ +import sys + +import pytest + +from .ngtcp2test import ExampleClient +from .ngtcp2test import ExampleServer +from .ngtcp2test import Env + + +@pytest.mark.skipif(condition=len(Env.get_crypto_libs()) == 0, + reason="no crypto lib examples configured") +class TestCiphers: + + @pytest.fixture(scope='class', params=Env.get_crypto_libs()) + def server(self, env, request) -> ExampleServer: + s = ExampleServer(env=env, crypto_lib=request.param) + assert s.exists(), f'server not found: {s.path}' + assert s.start() + yield s + s.stop() + + @pytest.fixture(scope='function', + params=Env.get_crypto_libs(configurable_ciphers=True)) + def client(self, env, request) -> ExampleClient: + client = ExampleClient(env=env, crypto_lib=request.param) + assert client.exists() + yield client + + @pytest.mark.parametrize('cipher', [ + 'TLS_AES_128_GCM_SHA256', + 'TLS_AES_256_GCM_SHA384', + 'TLS_CHACHA20_POLY1305_SHA256', + 'TLS_AES_128_CCM_SHA256', + ]) + def test_05_01_get(self, env: Env, server, client, cipher): + if not client.uses_cipher_config: + pytest.skip(f'client {client.crypto_lib} ignores cipher config\n') + # run simple GET, no sessions, needs to give full handshake + if not client.supports_cipher(cipher): + pytest.skip(f'client {client.crypto_lib} does not support {cipher}\n') + if not server.supports_cipher(cipher): + pytest.skip(f'server {server.crypto_lib} does not support {cipher}\n') + cr = client.http_get(server, url=f'https://{env.example_domain}/', + ciphers=cipher) + assert cr.returncode == 0 + cr.assert_non_resume_handshake() diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_context.h b/deps/ngtcp2/ngtcp2/examples/tls_client_context.h new file mode 100644 index 00000000000000..065d37bc3360eb --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_context.h @@ -0,0 +1,56 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_H +#define TLS_CLIENT_CONTEXT_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#ifdef WITH_EXAMPLE_QUICTLS +# include "tls_client_context_quictls.h" +#endif // defined(WITH_EXAMPLE_QUICTLS) + +#ifdef WITH_EXAMPLE_GNUTLS +# include "tls_client_context_gnutls.h" +#endif // defined(WITH_EXAMPLE_GNUTLS) + +#ifdef WITH_EXAMPLE_BORINGSSL +# include "tls_client_context_boringssl.h" +#endif // defined(WITH_EXAMPLE_BORINGSSL) + +#ifdef WITH_EXAMPLE_PICOTLS +# include "tls_client_context_picotls.h" +#endif // defined(WITH_EXAMPLE_PICOTLS) + +#ifdef WITH_EXAMPLE_WOLFSSL +# include "tls_client_context_wolfssl.h" +#endif // defined(WITH_EXAMPLE_WOLFSSL) + +#ifdef WITH_EXAMPLE_OSSL +# include "tls_client_context_ossl.h" +#endif // defined(WITH_EXAMPLE_OSSL) + +#endif // !defined(TLS_CLIENT_CONTEXT_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_context_boringssl.cc b/deps/ngtcp2/ngtcp2/examples/tls_client_context_boringssl.cc new file mode 100644 index 00000000000000..b6f28a5d3c1bd0 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_context_boringssl.cc @@ -0,0 +1,139 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_context_boringssl.h" + +#include +#include + +#include + +#include + +#include "client_base.h" +#include "template.h" +#include "tls_shared_boringssl.h" + +extern Config config; + +TLSClientContext::~TLSClientContext() { + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + } +} + +SSL_CTX *TLSClientContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int new_session_cb(SSL *ssl, SSL_SESSION *session) { + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto c = static_cast(conn_ref->user_data); + + c->ticket_received(); + + auto f = BIO_new_file(config.session_file.c_str(), "w"); + if (f == nullptr) { + std::println(stderr, "Could not write TLS session in {}", + config.session_file.native()); + return 0; + } + + if (!PEM_write_bio_SSL_SESSION(f, session)) { + std::println(stderr, "Unable to write TLS session to file"); + } + + BIO_free(f); + + return 0; +} +} // namespace + +std::expected TLSClientContext::init(const char *private_key_file, + const char *cert_file) { + ssl_ctx_ = SSL_CTX_new(TLS_client_method()); + if (!ssl_ctx_) { + std::println(stderr, "SSL_CTX_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (ngtcp2_crypto_boringssl_configure_client_context(ssl_ctx_) != 0) { + std::println(stderr, + "ngtcp2_crypto_boringssl_configure_client_context failed"); + return std::unexpected{Error::CRYPTO}; + } + + SSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (SSL_CTX_set1_groups_list(ssl_ctx_, config.groups) != 1) { + std::println(stderr, "SSL_CTX_set1_groups_list failed"); + return std::unexpected{Error::CRYPTO}; + } + + if (private_key_file && cert_file) { + if (SSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::println(stderr, "SSL_CTX_use_PrivateKey_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::println(stderr, "SSL_CTX_use_certificate_chain_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + } + + if (!config.session_file.empty()) { + SSL_CTX_set_session_cache_mode(ssl_ctx_, SSL_SESS_CACHE_CLIENT | + SSL_SESS_CACHE_NO_INTERNAL); + SSL_CTX_sess_set_new_cb(ssl_ctx_, new_session_cb); + } + +#ifdef HAVE_LIBBROTLI + if (!SSL_CTX_add_cert_compression_alg( + ssl_ctx_, ngtcp2::tls::CERTIFICATE_COMPRESSION_ALGO_BROTLI, + ngtcp2::tls::cert_compress, ngtcp2::tls::cert_decompress)) { + std::println(stderr, "SSL_CTX_add_cert_compression_alg failed"); + return std::unexpected{Error::CRYPTO}; + } +#endif // defined(HAVE_LIBBROTLI) + + return {}; +} + +extern std::ofstream keylog_file; + +namespace { +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, static_cast(strlen(line))); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +void TLSClientContext::enable_keylog() { + SSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_context_boringssl.h b/deps/ngtcp2/ngtcp2/examples/tls_client_context_boringssl.h new file mode 100644 index 00000000000000..3f3fab08380cde --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_context_boringssl.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_BORINGSSL_H +#define TLS_CLIENT_CONTEXT_BORINGSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext { +public: + TLSClientContext() = default; + ~TLSClientContext(); + + std::expected init(const char *private_key_file, + const char *cert_file); + + SSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + SSL_CTX *ssl_ctx_{}; +}; + +#endif // !defined(TLS_CLIENT_CONTEXT_BORINGSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_context_ossl.cc b/deps/ngtcp2/ngtcp2/examples/tls_client_context_ossl.cc new file mode 100644 index 00000000000000..d7f9f8f58c8abc --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_context_ossl.cc @@ -0,0 +1,146 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_context_ossl.h" + +#include +#include +#include +#include + +#include + +#include + +#include "client_base.h" +#include "template.h" + +namespace { +auto _ = [] { + if (ngtcp2_crypto_ossl_init() != 0) { + assert(0); + abort(); + } + + return 0; +}(); +} // namespace + +extern Config config; + +TLSClientContext::~TLSClientContext() { + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + } +} + +SSL_CTX *TLSClientContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int new_session_cb(SSL *ssl, SSL_SESSION *session) { + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto c = static_cast(conn_ref->user_data); + + c->ticket_received(); + + if (SSL_SESSION_get_max_early_data(session) != + std::numeric_limits::max()) { + std::println(stderr, "max_early_data_size is not 0xffffffff"); + } + auto f = BIO_new_file(config.session_file.c_str(), "w"); + if (f == nullptr) { + std::println(stderr, "Could not write TLS session in {}", + config.session_file.native()); + return 0; + } + + if (!PEM_write_bio_SSL_SESSION(f, session)) { + std::println(stderr, "Unable to write TLS session to file"); + } + + BIO_free(f); + + return 0; +} +} // namespace + +std::expected TLSClientContext::init(const char *private_key_file, + const char *cert_file) { + ssl_ctx_ = SSL_CTX_new(TLS_client_method()); + if (!ssl_ctx_) { + std::println(stderr, "SSL_CTX_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + SSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (SSL_CTX_set_ciphersuites(ssl_ctx_, config.ciphers) != 1) { + std::println(stderr, "SSL_CTX_set_ciphersuites: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_set1_groups_list(ssl_ctx_, config.groups) != 1) { + std::println(stderr, "SSL_CTX_set1_groups_list failed"); + return std::unexpected{Error::CRYPTO}; + } + + if (private_key_file && cert_file) { + if (SSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::println(stderr, "SSL_CTX_use_PrivateKey_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::println(stderr, "SSL_CTX_use_certificate_chain_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + } + + if (!config.session_file.empty()) { + SSL_CTX_set_session_cache_mode(ssl_ctx_, SSL_SESS_CACHE_CLIENT | + SSL_SESS_CACHE_NO_INTERNAL); + SSL_CTX_sess_set_new_cb(ssl_ctx_, new_session_cb); + } + + return {}; +} + +extern std::ofstream keylog_file; + +namespace { +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, static_cast(strlen(line))); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +void TLSClientContext::enable_keylog() { + SSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_context_ossl.h b/deps/ngtcp2/ngtcp2/examples/tls_client_context_ossl.h new file mode 100644 index 00000000000000..fad71595f2579c --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_context_ossl.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_OSSL_H +#define TLS_CLIENT_CONTEXT_OSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext { +public: + TLSClientContext() = default; + ~TLSClientContext(); + + std::expected init(const char *private_key_file, + const char *cert_file); + + SSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + SSL_CTX *ssl_ctx_{}; +}; + +#endif // !defined(TLS_CLIENT_CONTEXT_OSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_context_picotls.cc b/deps/ngtcp2/ngtcp2/examples/tls_client_context_picotls.cc new file mode 100644 index 00000000000000..d5ee705be0a778 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_context_picotls.cc @@ -0,0 +1,170 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_context_picotls.h" + +#include + +#include +#include + +#include "client_base.h" +#include "tls_shared_picotls.h" +#include "template.h" + +extern Config config; + +namespace { +int save_ticket_cb(ptls_save_ticket_t *self, ptls_t *ptls, ptls_iovec_t input) { + auto conn_ref = + static_cast(*ptls_get_data_ptr(ptls)); + auto c = static_cast(conn_ref->user_data); + + c->ticket_received(); + + auto f = BIO_new_file(config.session_file.c_str(), "w"); + if (f == nullptr) { + std::println(stderr, "Could not write TLS session in {}", + config.session_file.native()); + return 0; + } + + if (!PEM_write_bio(f, "PICOTLS SESSION PARAMETERS", "", input.base, + static_cast(input.len))) { + std::println(stderr, "Unable to write TLS session to file"); + } + + BIO_free(f); + + return 0; +} + +ptls_save_ticket_t save_ticket = {save_ticket_cb}; +} // namespace + +namespace { +ptls_key_exchange_algorithm_t *key_exchanges[] = { +#if PTLS_OPENSSL_HAVE_X25519 + &ptls_openssl_x25519, +#endif // PTLS_OPENSSL_X25519 + &ptls_openssl_secp256r1, + &ptls_openssl_secp384r1, + &ptls_openssl_secp521r1, +#if PTLS_OPENSSL_HAVE_X25519MLKEM768 + &ptls_openssl_x25519mlkem768, +#endif // PTLS_OPENSSL_HAVE_X25519MLKEM768 + nullptr, +}; +} // namespace + +namespace { +ptls_cipher_suite_t *cipher_suites[] = { + &ptls_openssl_aes128gcmsha256, + &ptls_openssl_aes256gcmsha384, +#if PTLS_OPENSSL_HAVE_CHACHA20_POLY1305 + &ptls_openssl_chacha20poly1305sha256, +#endif // PTLS_OPENSSL_CHACHA20POLY1305SHA256 + nullptr, +}; +} // namespace + +TLSClientContext::TLSClientContext() + : ctx_{ + .random_bytes = ptls_openssl_random_bytes, + .get_time = &ptls_get_time, + .key_exchanges = key_exchanges, + .cipher_suites = cipher_suites, + .require_dhe_on_psk = 1, + } {} + +TLSClientContext::~TLSClientContext() { + if (sign_cert_.key) { + ptls_openssl_dispose_sign_certificate(&sign_cert_); + } + + for (size_t i = 0; i < ctx_.certificates.count; ++i) { + free(ctx_.certificates.list[i].base); + } + free(ctx_.certificates.list); +} + +ptls_context_t *TLSClientContext::get_native_handle() { return &ctx_; } + +std::expected TLSClientContext::init(const char *private_key_file, + const char *cert_file) { + if (ngtcp2_crypto_picotls_configure_client_context(&ctx_) != 0) { + std::println(stderr, + "ngtcp2_crypto_picotls_configure_client_context failed"); + return std::unexpected{Error::CRYPTO}; + } + + if (!config.session_file.empty()) { + ctx_.save_ticket = &save_ticket; + } + + if (private_key_file && cert_file) { + if (ptls_load_certificates(&ctx_, cert_file) != 0) { + std::println(stderr, "ptls_load_certificates failed"); + return std::unexpected{Error::CRYPTO}; + } + + if (auto rv = load_private_key(private_key_file); !rv) { + return rv; + } + } + + return {}; +} + +std::expected +TLSClientContext::load_private_key(const char *private_key_file) { + auto fp = fopen(private_key_file, "rb"); + if (fp == nullptr) { + std::println(stderr, "Could not open private key file {}: {}", + private_key_file, strerror(errno)); + return std::unexpected{Error::IO}; + } + + auto fp_d = defer([fp] { fclose(fp); }); + + auto pkey = PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr); + if (pkey == nullptr) { + std::println(stderr, "Could not read private key file {}", + private_key_file); + return std::unexpected{Error::IO}; + } + + auto pkey_d = defer([pkey] { EVP_PKEY_free(pkey); }); + + if (ptls_openssl_init_sign_certificate(&sign_cert_, pkey) != 0) { + std::println(stderr, "ptls_openssl_init_sign_certificate failed"); + return std::unexpected{Error::CRYPTO}; + } + + ctx_.sign_certificate = &sign_cert_.super; + + return {}; +} + +void TLSClientContext::enable_keylog() { ctx_.log_event = &log_event; } diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_context_picotls.h b/deps/ngtcp2/ngtcp2/examples/tls_client_context_picotls.h new file mode 100644 index 00000000000000..7f111fd58e0df9 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_context_picotls.h @@ -0,0 +1,58 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_PICOTLS_H +#define TLS_CLIENT_CONTEXT_PICOTLS_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include + +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext { +public: + TLSClientContext(); + ~TLSClientContext(); + + std::expected init(const char *private_key_file, + const char *cert_file); + + ptls_context_t *get_native_handle(); + + void enable_keylog(); + +private: + std::expected load_private_key(const char *private_key_file); + + ptls_context_t ctx_; + ptls_openssl_sign_certificate_t sign_cert_{}; +}; + +#endif // !defined(TLS_CLIENT_CONTEXT_PICOTLS_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_context_quictls.cc b/deps/ngtcp2/ngtcp2/examples/tls_client_context_quictls.cc new file mode 100644 index 00000000000000..b88717708f3aaf --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_context_quictls.cc @@ -0,0 +1,152 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_context_quictls.h" + +#include +#include +#include +#include + +#include + +#include + +#include "client_base.h" +#include "template.h" + +namespace { +auto _ = [] { + if (ngtcp2_crypto_quictls_init() != 0) { + assert(0); + abort(); + } + + return 0; +}(); +} // namespace + +extern Config config; + +TLSClientContext::~TLSClientContext() { + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + } +} + +SSL_CTX *TLSClientContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int new_session_cb(SSL *ssl, SSL_SESSION *session) { + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto c = static_cast(conn_ref->user_data); + + c->ticket_received(); + + if (SSL_SESSION_get_max_early_data(session) != + std::numeric_limits::max()) { + std::println(stderr, "max_early_data_size is not 0xffffffff"); + } + auto f = BIO_new_file(config.session_file.c_str(), "w"); + if (f == nullptr) { + std::println(stderr, "Could not write TLS session in {}", + config.session_file.native()); + return 0; + } + + if (!PEM_write_bio_SSL_SESSION(f, session)) { + std::println(stderr, "Unable to write TLS session to file"); + } + + BIO_free(f); + + return 0; +} +} // namespace + +std::expected TLSClientContext::init(const char *private_key_file, + const char *cert_file) { + ssl_ctx_ = SSL_CTX_new(TLS_client_method()); + if (!ssl_ctx_) { + std::println(stderr, "SSL_CTX_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (ngtcp2_crypto_quictls_configure_client_context(ssl_ctx_) != 0) { + std::println(stderr, + "ngtcp2_crypto_quictls_configure_client_context failed"); + return std::unexpected{Error::CRYPTO}; + } + + SSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (SSL_CTX_set_ciphersuites(ssl_ctx_, config.ciphers) != 1) { + std::println(stderr, "SSL_CTX_set_ciphersuites: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_set1_groups_list(ssl_ctx_, config.groups) != 1) { + std::println(stderr, "SSL_CTX_set1_groups_list failed"); + return std::unexpected{Error::CRYPTO}; + } + + if (private_key_file && cert_file) { + if (SSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::println(stderr, "SSL_CTX_use_PrivateKey_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::println(stderr, "SSL_CTX_use_certificate_chain_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + } + + if (!config.session_file.empty()) { + SSL_CTX_set_session_cache_mode(ssl_ctx_, SSL_SESS_CACHE_CLIENT | + SSL_SESS_CACHE_NO_INTERNAL); + SSL_CTX_sess_set_new_cb(ssl_ctx_, new_session_cb); + } + + return {}; +} + +extern std::ofstream keylog_file; + +namespace { +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, static_cast(strlen(line))); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +void TLSClientContext::enable_keylog() { + SSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_context_quictls.h b/deps/ngtcp2/ngtcp2/examples/tls_client_context_quictls.h new file mode 100644 index 00000000000000..97cb478f75ac4b --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_context_quictls.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_QUICTLS_H +#define TLS_CLIENT_CONTEXT_QUICTLS_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext { +public: + TLSClientContext() = default; + ~TLSClientContext(); + + std::expected init(const char *private_key_file, + const char *cert_file); + + SSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + SSL_CTX *ssl_ctx_{}; +}; + +#endif // !defined(TLS_CLIENT_CONTEXT_QUICTLS_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_context_wolfssl.cc b/deps/ngtcp2/ngtcp2/examples/tls_client_context_wolfssl.cc new file mode 100644 index 00000000000000..8eef8a259deef5 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_context_wolfssl.cc @@ -0,0 +1,173 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_context_wolfssl.h" + +#include +#include +#include + +#include + +#include +#include + +#include "client_base.h" +#include "template.h" + +extern Config config; + +TLSClientContext::~TLSClientContext() { + if (ssl_ctx_) { + wolfSSL_CTX_free(ssl_ctx_); + } +} + +WOLFSSL_CTX *TLSClientContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int new_session_cb(WOLFSSL *ssl, WOLFSSL_SESSION *session) { + auto conn_ref = + static_cast(wolfSSL_get_app_data(ssl)); + auto c = static_cast(conn_ref->user_data); + + c->ticket_received(); + +#ifdef HAVE_SESSION_TICKET + if (wolfSSL_SESSION_get_max_early_data(session) != + std::numeric_limits::max()) { + std::println(stderr, "max_early_data_size is not 0xffffffff"); + } + + unsigned char sbuffer[16 * 1024], *data; + auto sz = wolfSSL_i2d_SSL_SESSION(session, nullptr); + if (sz <= 0) { + std::println(stderr, "Could not export TLS session in {}", + config.session_file.native()); + return 0; + } + if (static_cast(sz) > sizeof(sbuffer)) { + std::println(stderr, "Exported TLS session too large"); + return 0; + } + data = sbuffer; + sz = wolfSSL_i2d_SSL_SESSION(session, &data); + + auto f = wolfSSL_BIO_new_file(config.session_file.c_str(), "w"); + if (f == nullptr) { + std::println(stderr, "Could not write TLS session in {}", + config.session_file.native()); + return 0; + } + + auto f_d = defer([f] { wolfSSL_BIO_free(f); }); + + if (!wolfSSL_PEM_write_bio(f, "WOLFSSL SESSION PARAMETERS", "", sbuffer, + sz)) { + std::println(stderr, "Unable to write TLS session to file"); + return 0; + } +#else // !defined(HAVE_SESSION_TICKET) + std::println(stderr, "TLS session tickets not enabled in wolfSSL"); +#endif // !defined(HAVE_SESSION_TICKET) + return 0; +} +} // namespace + +std::expected TLSClientContext::init(const char *private_key_file, + const char *cert_file) { + ssl_ctx_ = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); + if (!ssl_ctx_) { + std::println(stderr, "wolfSSL_CTX_new: {}", + wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (ngtcp2_crypto_wolfssl_configure_client_context(ssl_ctx_) != 0) { + std::println(stderr, + "ngtcp2_crypto_wolfssl_configure_client_context failed"); + return std::unexpected{Error::CRYPTO}; + } + + if (wolfSSL_CTX_set_default_verify_paths(ssl_ctx_) == + WOLFSSL_NOT_IMPLEMENTED) { + /* hmm, not verifying the server cert for now */ + wolfSSL_CTX_set_verify(ssl_ctx_, WOLFSSL_VERIFY_NONE, 0); + } + + if (wolfSSL_CTX_set_cipher_list(ssl_ctx_, config.ciphers) != + WOLFSSL_SUCCESS) { + std::println(stderr, "wolfSSL_CTX_set_cipher_list: {}", + wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (wolfSSL_CTX_set1_groups_list( + ssl_ctx_, const_cast(config.groups)) != WOLFSSL_SUCCESS) { + std::println(stderr, "wolfSSL_CTX_set1_groups_list({}) failed", + config.groups); + return std::unexpected{Error::CRYPTO}; + } + + if (private_key_file && cert_file) { + if (wolfSSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != WOLFSSL_SUCCESS) { + std::println(stderr, "wolfSSL_CTX_use_PrivateKey_file: {}", + wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (wolfSSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != + WOLFSSL_SUCCESS) { + std::println(stderr, "wolfSSL_CTX_use_certificate_chain_file: {}", + wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + } + + if (!config.session_file.empty()) { + wolfSSL_CTX_UseSessionTicket(ssl_ctx_); + wolfSSL_CTX_sess_set_new_cb(ssl_ctx_, new_session_cb); + } + + return {}; +} + +extern std::ofstream keylog_file; + +#ifdef HAVE_SECRET_CALLBACK +namespace { +void keylog_callback(const WOLFSSL *ssl, const char *line) { + keylog_file.write(line, static_cast(strlen(line))); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace +#endif // defined(HAVE_SECRET_CALLBACK) + +void TLSClientContext::enable_keylog() { +#ifdef HAVE_SECRET_CALLBACK + wolfSSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +#endif // defined(HAVE_SECRET_CALLBACK) +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_context_wolfssl.h b/deps/ngtcp2/ngtcp2/examples/tls_client_context_wolfssl.h new file mode 100644 index 00000000000000..a8a66ed50d0a60 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_context_wolfssl.h @@ -0,0 +1,56 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_WOLFSSL_H +#define TLS_CLIENT_CONTEXT_WOLFSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include +#include + +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext { +public: + TLSClientContext() = default; + ~TLSClientContext(); + + std::expected init(const char *private_key_file, + const char *cert_file); + + WOLFSSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + WOLFSSL_CTX *ssl_ctx_{}; +}; + +#endif // !defined(TLS_CLIENT_CONTEXT_WOLFSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_session.h b/deps/ngtcp2/ngtcp2/examples/tls_client_session.h new file mode 100644 index 00000000000000..39ce2caeeae3f4 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_session.h @@ -0,0 +1,56 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_H +#define TLS_CLIENT_SESSION_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#ifdef WITH_EXAMPLE_QUICTLS +# include "tls_client_session_quictls.h" +#endif // defined(WITH_EXAMPLE_QUICTLS) + +#ifdef WITH_EXAMPLE_GNUTLS +# include "tls_client_session_gnutls.h" +#endif // defined(WITH_EXAMPLE_GNUTLS) + +#ifdef WITH_EXAMPLE_BORINGSSL +# include "tls_client_session_boringssl.h" +#endif // defined(WITH_EXAMPLE_BORINGSSL) + +#ifdef WITH_EXAMPLE_PICOTLS +# include "tls_client_session_picotls.h" +#endif // defined(WITH_EXAMPLE_PICOTLS) + +#ifdef WITH_EXAMPLE_WOLFSSL +# include "tls_client_session_wolfssl.h" +#endif // defined(WITH_EXAMPLE_WOLFSSL) + +#ifdef WITH_EXAMPLE_OSSL +# include "tls_client_session_ossl.h" +#endif // defined(WITH_EXAMPLE_OSSL) + +#endif // !defined(TLS_CLIENT_SESSION_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_session_boringssl.cc b/deps/ngtcp2/ngtcp2/examples/tls_client_session_boringssl.cc new file mode 100644 index 00000000000000..75989b66bbf594 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_session_boringssl.cc @@ -0,0 +1,144 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_session_boringssl.h" + +#include +#include + +#include "tls_client_context_boringssl.h" +#include "client_base.h" +#include "template.h" +#include "util.h" + +extern Config config; + +std::expected +TLSClientSession::init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, const char *remote_addr, + ClientBase *client, uint32_t quic_version, + AppProtocol app_proto) { + early_data_enabled = false; + + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = SSL_new(ssl_ctx); + if (!ssl_) { + std::println(stderr, "SSL_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + SSL_set_app_data(ssl_, client->conn_ref()); + SSL_set_connect_state(ssl_); + + switch (app_proto) { + case AppProtocol::H3: + SSL_set_alpn_protos(ssl_, H3_ALPN.data(), H3_ALPN.size()); + break; + case AppProtocol::HQ: + SSL_set_alpn_protos(ssl_, HQ_ALPN.data(), HQ_ALPN.size()); + break; + } + + if (!config.sni.empty()) { + SSL_set_tlsext_host_name(ssl_, config.sni.data()); + } else if (util::numeric_host(remote_addr)) { + // If remote host is numeric address, just send "localhost" as SNI + // for now. + SSL_set_tlsext_host_name(ssl_, "localhost"); + } else { + SSL_set_tlsext_host_name(ssl_, remote_addr); + } + + if (!config.session_file.empty()) { + auto f = BIO_new_file(config.session_file.c_str(), "r"); + if (f == nullptr) { + std::println(stderr, "Could not read TLS session file {}", + config.session_file.native()); + } else { + auto session = PEM_read_bio_SSL_SESSION(f, nullptr, 0, nullptr); + BIO_free(f); + if (session == nullptr) { + std::println(stderr, "Could not read TLS session file {}", + config.session_file.native()); + } else { + if (!SSL_set_session(ssl_, session)) { + std::println(stderr, "Could not set session"); + } else if (!config.disable_early_data && + SSL_SESSION_early_data_capable(session)) { + early_data_enabled = true; + SSL_set_early_data_enabled(ssl_, 1); + } + SSL_SESSION_free(session); + } + } + } + + if (!config.ech_config_list.empty() && + SSL_set1_ech_config_list(ssl_, config.ech_config_list.data(), + config.ech_config_list.size()) != 1) { + std::println(stderr, "Could not set ECHConfigList: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + return {}; +} + +bool TLSClientSession::get_early_data_accepted() const { + return SSL_early_data_accepted(ssl_); +} + +bool TLSClientSession::get_ech_accepted() const { + return SSL_ech_accepted(ssl_); +} + +std::expected TLSClientSession::write_ech_config_list( + const std::filesystem::path &path) const { + const uint8_t *retry_configs; + size_t retry_configslen; + + SSL_get0_ech_retry_configs(ssl_, &retry_configs, &retry_configslen); + if (retry_configslen == 0) { + std::println(stderr, "No ECH retry configs found"); + return std::unexpected{Error::CRYPTO}; + } + + auto f = std::ofstream(path); + + if (!f) { + return std::unexpected{Error::IO}; + } + + f.write(reinterpret_cast(retry_configs), + static_cast(retry_configslen)); + f.close(); + + if (!f) { + return std::unexpected{Error::IO}; + } + + return {}; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_session_boringssl.h b/deps/ngtcp2/ngtcp2/examples/tls_client_session_boringssl.h new file mode 100644 index 00000000000000..89000c9ff2ee89 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_session_boringssl.h @@ -0,0 +1,57 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_BORINGSSL_H +#define TLS_CLIENT_SESSION_BORINGSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include "tls_session_base_quictls.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext; +class ClientBase; + +class TLSClientSession : public TLSSessionBase { +public: + TLSClientSession() = default; + + std::expected init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, + uint32_t quic_version, AppProtocol app_proto); + + bool get_early_data_accepted() const; + bool get_ech_accepted() const; + std::expected + write_ech_config_list(const std::filesystem::path &path) const; +}; + +#endif // !defined(TLS_CLIENT_SESSION_BORINGSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_session_ossl.cc b/deps/ngtcp2/ngtcp2/examples/tls_client_session_ossl.cc new file mode 100644 index 00000000000000..b15a45fc2a8ee5 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_session_ossl.cc @@ -0,0 +1,116 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_session_ossl.h" + +#include + +#include + +#include "tls_client_context_ossl.h" +#include "client_base.h" +#include "template.h" +#include "util.h" + +extern Config config; + +std::expected +TLSClientSession::init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, const char *remote_addr, + ClientBase *client, uint32_t quic_version, + AppProtocol app_proto) { + early_data_enabled = false; + + auto ssl_ctx = tls_ctx.get_native_handle(); + + auto ssl = SSL_new(ssl_ctx); + if (!ssl) { + std::println(stderr, "SSL_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + ngtcp2_crypto_ossl_ctx_set_ssl(ossl_ctx_, ssl); + + if (ngtcp2_crypto_ossl_configure_client_session(ssl) != 0) { + std::println(stderr, "ngtcp2_crypto_ossl_configure_client_session failed"); + return std::unexpected{Error::CRYPTO}; + } + + SSL_set_app_data(ssl, client->conn_ref()); + SSL_set_connect_state(ssl); + + switch (app_proto) { + case AppProtocol::H3: + SSL_set_alpn_protos(ssl, H3_ALPN.data(), H3_ALPN.size()); + break; + case AppProtocol::HQ: + SSL_set_alpn_protos(ssl, HQ_ALPN.data(), HQ_ALPN.size()); + break; + } + + if (!config.sni.empty()) { + SSL_set_tlsext_host_name(ssl, config.sni.data()); + } else if (util::numeric_host(remote_addr)) { + // If remote host is numeric address, just send "localhost" as SNI + // for now. + SSL_set_tlsext_host_name(ssl, "localhost"); + } else { + SSL_set_tlsext_host_name(ssl, remote_addr); + } + + if (!config.session_file.empty()) { + auto f = BIO_new_file(config.session_file.c_str(), "r"); + if (f == nullptr) { + std::println(stderr, "Could not read TLS session file {}", + config.session_file.native()); + } else { + auto session = PEM_read_bio_SSL_SESSION(f, nullptr, 0, nullptr); + BIO_free(f); + if (session == nullptr) { + std::println(stderr, "Could not read TLS session file {}", + config.session_file.native()); + } else { + if (!SSL_set_session(ssl, session)) { + std::println(stderr, "Could not set session"); + } else if (!config.disable_early_data && + SSL_SESSION_get_max_early_data(session)) { + early_data_enabled = true; + SSL_set_quic_tls_early_data_enabled(ssl, 1); + } + + SSL_SESSION_free(session); + } + } + } + + return {}; +} + +bool TLSClientSession::get_early_data_accepted() const { + auto ssl = ngtcp2_crypto_ossl_ctx_get_ssl(ossl_ctx_); + + // SSL_get_early_data_status works after handshake completes. + return SSL_get_early_data_status(ssl) == SSL_EARLY_DATA_ACCEPTED; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_session_ossl.h b/deps/ngtcp2/ngtcp2/examples/tls_client_session_ossl.h new file mode 100644 index 00000000000000..4476ba4c7beda1 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_session_ossl.h @@ -0,0 +1,59 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_OSSL_H +#define TLS_CLIENT_SESSION_OSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include "tls_session_base_ossl.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext; +class ClientBase; + +class TLSClientSession : public TLSSessionBase { +public: + TLSClientSession() = default; + + std::expected init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, + uint32_t quic_version, AppProtocol app_proto); + + bool get_early_data_accepted() const; + bool get_ech_accepted() const { return false; } + std::expected + write_ech_config_list(const std::filesystem::path &path) const { + return {}; + } +}; + +#endif // !defined(TLS_CLIENT_SESSION_OSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_session_picotls.cc b/deps/ngtcp2/ngtcp2/examples/tls_client_session_picotls.cc new file mode 100644 index 00000000000000..d65d2a096f3a09 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_session_picotls.cc @@ -0,0 +1,167 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_session_picotls.h" + +#include +#include + +#include + +#include +#include + +#include + +#include "tls_client_context_picotls.h" +#include "client_base.h" +#include "template.h" +#include "util.h" + +using namespace std::literals; + +extern Config config; + +TLSClientSession::~TLSClientSession() { + auto &hsprops = cptls_.handshake_properties; + + delete[] hsprops.client.session_ticket.base; +} + +namespace { +auto negotiated_protocols_h3 = std::to_array({ + { + .base = const_cast(&H3_ALPN_V1[1]), + .len = H3_ALPN_V1[0], + }, +}); +} // namespace + +namespace { +auto negotiated_protocols_hq = std::to_array({ + { + .base = const_cast(&HQ_ALPN_V1[1]), + .len = HQ_ALPN_V1[0], + }, +}); +} // namespace + +std::expected +TLSClientSession::init(bool &early_data_enabled, TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, + uint32_t quic_version, AppProtocol app_proto) { + cptls_.ptls = ptls_client_new(tls_ctx.get_native_handle()); + if (!cptls_.ptls) { + std::println(stderr, "ptls_client_new failed"); + return std::unexpected{Error::CRYPTO}; + } + + *ptls_get_data_ptr(cptls_.ptls) = client->conn_ref(); + + auto conn = client->conn(); + auto &hsprops = cptls_.handshake_properties; + + hsprops.additional_extensions = new ptls_raw_extension_t[2]{ + { + .type = UINT16_MAX, + }, + { + .type = UINT16_MAX, + }, + }; + + if (ngtcp2_crypto_picotls_configure_client_session(&cptls_, conn) != 0) { + std::println(stderr, + "ngtcp2_crypto_picotls_configure_client_session failed"); + return std::unexpected{Error::CRYPTO}; + } + + switch (app_proto) { + case AppProtocol::H3: + hsprops.client.negotiated_protocols.list = negotiated_protocols_h3.data(); + hsprops.client.negotiated_protocols.count = negotiated_protocols_h3.size(); + + break; + case AppProtocol::HQ: + hsprops.client.negotiated_protocols.list = negotiated_protocols_hq.data(); + hsprops.client.negotiated_protocols.count = negotiated_protocols_hq.size(); + + break; + } + + if (util::numeric_host(remote_addr)) { + // If remote host is numeric address, just send "localhost" as SNI + // for now. + ptls_set_server_name(cptls_.ptls, "localhost", strlen("localhost")); + } else { + ptls_set_server_name(cptls_.ptls, remote_addr, strlen(remote_addr)); + } + + if (!config.session_file.empty()) { + auto f = BIO_new_file(config.session_file.c_str(), "r"); + if (f == nullptr) { + std::println(stderr, "Could not read TLS session file {}", + config.session_file.native()); + } else { + auto f_d = defer([f] { BIO_free(f); }); + + char *name, *header; + unsigned char *data; + long datalen; + + if (PEM_read_bio(f, &name, &header, &data, &datalen) != 1) { + std::println(stderr, "Could not read TLS session file {}", + config.session_file.native()); + } else { + if ("PICOTLS SESSION PARAMETERS"sv != name) { + std::println(stderr, "TLS session file contains unexpected name: {}", + name); + } else { + hsprops.client.session_ticket.base = + new uint8_t[static_cast(datalen)]; + hsprops.client.session_ticket.len = static_cast(datalen); + std::ranges::copy_n(data, datalen, + hsprops.client.session_ticket.base); + + if (!config.disable_early_data) { + // No easy way to check max_early_data from ticket. We + // need to run ptls_handle_message. + early_data_enabled = true; + } + } + + OPENSSL_free(name); + OPENSSL_free(header); + OPENSSL_free(data); + } + } + } + + return {}; +} + +bool TLSClientSession::get_early_data_accepted() const { + return cptls_.handshake_properties.client.early_data_acceptance == + PTLS_EARLY_DATA_ACCEPTED; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_session_picotls.h b/deps/ngtcp2/ngtcp2/examples/tls_client_session_picotls.h new file mode 100644 index 00000000000000..c5aed12e20f32e --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_session_picotls.h @@ -0,0 +1,60 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_PICOTLS_H +#define TLS_CLIENT_SESSION_PICOTLS_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include "tls_session_base_picotls.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext; +class ClientBase; + +class TLSClientSession : public TLSSessionBase { +public: + TLSClientSession() = default; + ~TLSClientSession(); + + std::expected init(bool &early_data_enabled, + TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, + uint32_t quic_version, AppProtocol app_proto); + + bool get_early_data_accepted() const; + bool get_ech_accepted() const { return false; } + std::expected + write_ech_config_list(const std::filesystem::path &path) const { + return {}; + } +}; + +#endif // !defined(TLS_CLIENT_SESSION_PICOTLS_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_session_quictls.cc b/deps/ngtcp2/ngtcp2/examples/tls_client_session_quictls.cc new file mode 100644 index 00000000000000..b2060568347b8f --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_session_quictls.cc @@ -0,0 +1,109 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_session_quictls.h" + +#include + +#include + +#include "tls_client_context_quictls.h" +#include "client_base.h" +#include "template.h" +#include "util.h" + +extern Config config; + +std::expected +TLSClientSession::init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, const char *remote_addr, + ClientBase *client, uint32_t quic_version, + AppProtocol app_proto) { + early_data_enabled = false; + + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = SSL_new(ssl_ctx); + if (!ssl_) { + std::println(stderr, "SSL_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + SSL_set_app_data(ssl_, client->conn_ref()); + SSL_set_connect_state(ssl_); + + switch (app_proto) { + case AppProtocol::H3: + SSL_set_alpn_protos(ssl_, H3_ALPN.data(), H3_ALPN.size()); + break; + case AppProtocol::HQ: + SSL_set_alpn_protos(ssl_, HQ_ALPN.data(), HQ_ALPN.size()); + break; + } + + if (!config.sni.empty()) { + SSL_set_tlsext_host_name(ssl_, config.sni.data()); + } else if (util::numeric_host(remote_addr)) { + // If remote host is numeric address, just send "localhost" as SNI + // for now. + SSL_set_tlsext_host_name(ssl_, "localhost"); + } else { + SSL_set_tlsext_host_name(ssl_, remote_addr); + } + + if (!config.session_file.empty()) { + auto f = BIO_new_file(config.session_file.c_str(), "r"); + if (f == nullptr) { + std::println(stderr, "Could not read TLS session file {}", + config.session_file.native()); + } else { + auto session = PEM_read_bio_SSL_SESSION(f, nullptr, 0, nullptr); + BIO_free(f); + if (session == nullptr) { + std::println(stderr, "Could not read TLS session file {}", + config.session_file.native()); + } else { + if (!SSL_set_session(ssl_, session)) { + std::println(stderr, "Could not set session"); + } +#ifndef LIBRESSL_VERSION_NUMBER + else if (!config.disable_early_data && + SSL_SESSION_get_max_early_data(session)) { + early_data_enabled = true; + SSL_set_quic_early_data_enabled(ssl_, 1); + } +#endif // !defined(LIBRESSL_VERSION_NUMBER) + SSL_SESSION_free(session); + } + } + } + + return {}; +} + +bool TLSClientSession::get_early_data_accepted() const { + // SSL_get_early_data_status works after handshake completes. + return SSL_get_early_data_status(ssl_) == SSL_EARLY_DATA_ACCEPTED; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_session_quictls.h b/deps/ngtcp2/ngtcp2/examples/tls_client_session_quictls.h new file mode 100644 index 00000000000000..ad309f61840ebe --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_session_quictls.h @@ -0,0 +1,59 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_QUICTLS_H +#define TLS_CLIENT_SESSION_QUICTLS_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include "tls_session_base_quictls.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext; +class ClientBase; + +class TLSClientSession : public TLSSessionBase { +public: + TLSClientSession() = default; + + std::expected init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, + uint32_t quic_version, AppProtocol app_proto); + + bool get_early_data_accepted() const; + bool get_ech_accepted() const { return false; } + std::expected + write_ech_config_list(const std::filesystem::path &path) const { + return {}; + } +}; + +#endif // !defined(TLS_CLIENT_SESSION_QUICTLS_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_session_wolfssl.cc b/deps/ngtcp2/ngtcp2/examples/tls_client_session_wolfssl.cc new file mode 100644 index 00000000000000..16f5ed79769e7b --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_session_wolfssl.cc @@ -0,0 +1,147 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_session_wolfssl.h" + +#include +#include + +#include "tls_client_context_wolfssl.h" +#include "client_base.h" +#include "template.h" +#include "util.h" + +using namespace std::literals; + +extern Config config; + +std::expected +TLSClientSession::init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, const char *remote_addr, + ClientBase *client, uint32_t quic_version, + AppProtocol app_proto) { + early_data_enabled = false; + + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = wolfSSL_new(ssl_ctx); + if (!ssl_) { + std::println(stderr, "wolfSSL_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + wolfSSL_set_app_data(ssl_, client->conn_ref()); + wolfSSL_set_connect_state(ssl_); + + switch (app_proto) { + case AppProtocol::H3: + wolfSSL_set_alpn_protos(ssl_, H3_ALPN.data(), H3_ALPN.size()); + break; + case AppProtocol::HQ: + wolfSSL_set_alpn_protos(ssl_, HQ_ALPN.data(), HQ_ALPN.size()); + break; + } + + if (!config.sni.empty()) { + wolfSSL_UseSNI(ssl_, WOLFSSL_SNI_HOST_NAME, config.sni.data(), + static_cast(config.sni.length())); + } else if (util::numeric_host(remote_addr)) { + // If remote host is numeric address, just send "localhost" as SNI + // for now. + wolfSSL_UseSNI(ssl_, WOLFSSL_SNI_HOST_NAME, "localhost", + sizeof("localhost") - 1); + } else { + wolfSSL_UseSNI(ssl_, WOLFSSL_SNI_HOST_NAME, remote_addr, + static_cast(strlen(remote_addr))); + } + + // Just use QUIC v1 + wolfSSL_set_quic_transport_version(ssl_, 0x39); + + if (!config.session_file.empty()) { +#ifdef HAVE_SESSION_TICKET + auto f = wolfSSL_BIO_new_file(config.session_file.c_str(), "r"); + if (f == nullptr) { + std::println(stderr, "Could not open TLS session file {}", + config.session_file.native()); + } else { + char *name, *header; + unsigned char *data; + const unsigned char *pdata; + long datalen; + WOLFSSL_SESSION *session; + + if (wolfSSL_PEM_read_bio(f, &name, &header, &data, &datalen) != 1) { + std::println(stderr, "Could not read TLS session file {}", + config.session_file.native()); + } else { + if ("WOLFSSL SESSION PARAMETERS"sv != name) { + std::println(stderr, "TLS session file contains unexpected name: {}", + name); + } else { + pdata = data; + session = wolfSSL_d2i_SSL_SESSION(nullptr, &pdata, datalen); + if (session == nullptr) { + std::println(stderr, "Could not parse TLS session from file {}", + config.session_file.native()); + } else { + auto ret = wolfSSL_set_session(ssl_, session); + if (ret != WOLFSSL_SUCCESS) { + std::println(stderr, "Could not install TLS session from file {}", + config.session_file.native()); + } else { + if (!config.disable_early_data && + wolfSSL_SESSION_get_max_early_data(session)) { + early_data_enabled = true; + wolfSSL_set_quic_early_data_enabled(ssl_, 1); + } + } + wolfSSL_SESSION_free(session); + } + } + + wolfSSL_OPENSSL_free(name); + wolfSSL_OPENSSL_free(header); + wolfSSL_OPENSSL_free(data); + } + wolfSSL_BIO_free(f); + } + wolfSSL_UseSessionTicket(ssl_); +#else // !defined(HAVE_SESSION_TICKET) + std::println(stderr, "TLS session im-/export not enabled in wolfSSL"); +#endif // !defined(HAVE_SESSION_TICKET) + } + + return {}; +} + +bool TLSClientSession::get_early_data_accepted() const { + // wolfSSL_get_early_data_status works after handshake completes. +#ifdef WOLFSSL_EARLY_DATA + return wolfSSL_get_early_data_status(ssl_) == SSL_EARLY_DATA_ACCEPTED; +#else // !defined(WOLFSSL_EARLY_DATA) + return 0; +#endif // !defined(WOLFSSL_EARLY_DATA) +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_client_session_wolfssl.h b/deps/ngtcp2/ngtcp2/examples/tls_client_session_wolfssl.h new file mode 100644 index 00000000000000..eb876ebdfcf3af --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_client_session_wolfssl.h @@ -0,0 +1,59 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_WOLFSSL_H +#define TLS_CLIENT_SESSION_WOLFSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include "tls_session_base_wolfssl.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext; +class ClientBase; + +class TLSClientSession : public TLSSessionBase { +public: + TLSClientSession() = default; + + std::expected init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, + uint32_t quic_version, AppProtocol app_proto); + + bool get_early_data_accepted() const; + bool get_ech_accepted() const { return false; } + std::expected + write_ech_config_list(const std::filesystem::path &path) const { + return {}; + } +}; + +#endif // !defined(TLS_CLIENT_SESSION_WOLFSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_context.h b/deps/ngtcp2/ngtcp2/examples/tls_server_context.h new file mode 100644 index 00000000000000..1aa16918c94b6d --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_context.h @@ -0,0 +1,56 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_H +#define TLS_SERVER_CONTEXT_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#ifdef WITH_EXAMPLE_QUICTLS +# include "tls_server_context_quictls.h" +#endif // defined(WITH_EXAMPLE_QUICTLS) + +#ifdef WITH_EXAMPLE_GNUTLS +# include "tls_server_context_gnutls.h" +#endif // defined(WITH_EXAMPLE_GNUTLS) + +#ifdef WITH_EXAMPLE_BORINGSSL +# include "tls_server_context_boringssl.h" +#endif // defined(WITH_EXAMPLE_BORINGSSL) + +#ifdef WITH_EXAMPLE_PICOTLS +# include "tls_server_context_picotls.h" +#endif // defined(WITH_EXAMPLE_PICOTLS) + +#ifdef WITH_EXAMPLE_WOLFSSL +# include "tls_server_context_wolfssl.h" +#endif // defined(WITH_EXAMPLE_WOLFSSL) + +#ifdef WITH_EXAMPLE_OSSL +# include "tls_server_context_ossl.h" +#endif // defined(WITH_EXAMPLE_OSSL) + +#endif // !defined(TLS_SERVER_CONTEXT_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_context_boringssl.cc b/deps/ngtcp2/ngtcp2/examples/tls_server_context_boringssl.cc new file mode 100644 index 00000000000000..64b3bd013ed9d5 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_context_boringssl.cc @@ -0,0 +1,265 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_context_boringssl.h" + +#include +#include +#include + +#include + +#include +#include + +#include "server_base.h" +#include "template.h" +#include "tls_shared_boringssl.h" + +extern Config config; + +TLSServerContext::~TLSServerContext() { + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + } +} + +SSL_CTX *TLSServerContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int alpn_select_proto_h3_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version2(h->conn()); + + switch (version) { + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2: + break; + default: + if (!config.quiet) { + std::println(stderr, "Unexpected quic protocol version: {:#x}", version); + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto s = std::span{in, inlen}; s.size() >= H3_ALPN_V1.size(); + s = s.subspan(s[0] + 1)) { + if (std::ranges::equal(H3_ALPN_V1, s.first(H3_ALPN_V1.size()))) { + *out = &s[1]; + *outlen = s[0]; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::println(stderr, "Client did not present ALPN {}", + as_string_view(H3_ALPN_V1.subspan(1))); + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int alpn_select_proto_hq_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version2(h->conn()); + + switch (version) { + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2: + break; + default: + if (!config.quiet) { + std::println(stderr, "Unexpected quic protocol version: {:#x}", version); + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto s = std::span{in, inlen}; s.size() >= HQ_ALPN_V1.size(); + s = s.subspan(s[0] + 1)) { + if (std::ranges::equal(HQ_ALPN_V1, s.first(HQ_ALPN_V1.size()))) { + *out = &s[1]; + *outlen = s[0]; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::println(stderr, "Client did not present ALPN {}", + as_string_view(HQ_ALPN_V1.subspan(1))); + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int verify_cb(int preverify_ok, X509_STORE_CTX *ctx) { + // We don't verify the client certificate. Just request it for the + // testing purpose. + return 1; +} +} // namespace + +std::expected TLSServerContext::init(const char *private_key_file, + const char *cert_file, + AppProtocol app_proto) { + static constexpr unsigned char sid_ctx[] = "ngtcp2 server"; + + ssl_ctx_ = SSL_CTX_new(TLS_server_method()); + if (!ssl_ctx_) { + std::println(stderr, "SSL_CTX_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + constexpr auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_SINGLE_ECDH_USE | + SSL_OP_CIPHER_SERVER_PREFERENCE; + + SSL_CTX_set_options(ssl_ctx_, ssl_opts); + + if (SSL_CTX_set1_groups_list(ssl_ctx_, config.groups) != 1) { + std::println(stderr, "SSL_CTX_set1_groups_list failed"); + return std::unexpected{Error::CRYPTO}; + } + + SSL_CTX_set_mode(ssl_ctx_, SSL_MODE_RELEASE_BUFFERS); + + if (ngtcp2_crypto_boringssl_configure_server_context(ssl_ctx_) != 0) { + std::println(stderr, + "ngtcp2_crypto_boringssl_configure_server_context failed"); + return std::unexpected{Error::CRYPTO}; + } + + switch (app_proto) { + case AppProtocol::H3: + SSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_h3_cb, nullptr); + break; + case AppProtocol::HQ: + SSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_hq_cb, nullptr); + break; + } + + SSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (SSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::println(stderr, "SSL_CTX_use_PrivateKey_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::println(stderr, "SSL_CTX_use_certificate_chain_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_check_private_key(ssl_ctx_) != 1) { + std::println(stderr, "SSL_CTX_check_private_key: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + SSL_CTX_set_session_id_context(ssl_ctx_, sid_ctx, sizeof(sid_ctx) - 1); + + if (config.verify_client) { + SSL_CTX_set_verify(ssl_ctx_, + SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_cb); + } + +#ifdef HAVE_LIBBROTLI + if (!SSL_CTX_add_cert_compression_alg( + ssl_ctx_, ngtcp2::tls::CERTIFICATE_COMPRESSION_ALGO_BROTLI, + ngtcp2::tls::cert_compress, ngtcp2::tls::cert_decompress)) { + std::println(stderr, "SSL_CTX_add_cert_compression_alg failed"); + return std::unexpected{Error::CRYPTO}; + } +#endif // defined(HAVE_LIBBROTLI) + + if (!config.ech_config.ech_config_list.empty()) { + const auto &echconf = config.ech_config; + + auto pkey = EVP_HPKE_KEY_new(); + + if (EVP_HPKE_KEY_init(pkey, EVP_hpke_x25519_hkdf_sha256(), + echconf.private_key.bytes.data(), + echconf.private_key.bytes.size()) != 1) { + std::println(stderr, "EVP_HPKE_KEY_init failed: {}", + ERR_error_string(ERR_get_error(), nullptr)); + + return std::unexpected{Error::CRYPTO}; + } + + auto pkey_d = defer([pkey] { EVP_HPKE_KEY_free(pkey); }); + + auto keys = SSL_ECH_KEYS_new(); + auto keys_d = defer([keys] { SSL_ECH_KEYS_free(keys); }); + + for (const auto &ech_config : echconf.ech_config_list) { + if (SSL_ECH_KEYS_add(keys, 1, ech_config.data(), ech_config.size(), + pkey) != 1) { + std::println(stderr, "SSL_ECH_KEYS_add failed: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + } + + if (SSL_CTX_set1_ech_keys(ssl_ctx_, keys) != 1) { + std::println(stderr, "SSL_CTX_set1_ech_keys failed: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + } + + return {}; +} + +extern std::ofstream keylog_file; + +namespace { +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, static_cast(strlen(line))); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +void TLSServerContext::enable_keylog() { + SSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_context_boringssl.h b/deps/ngtcp2/ngtcp2/examples/tls_server_context_boringssl.h new file mode 100644 index 00000000000000..c55e24c034fbbb --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_context_boringssl.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_BORINGSSL_H +#define TLS_SERVER_CONTEXT_BORINGSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext { +public: + TLSServerContext() = default; + ~TLSServerContext(); + + std::expected init(const char *private_key_file, + const char *cert_file, AppProtocol app_proto); + + SSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + SSL_CTX *ssl_ctx_{}; +}; + +#endif // !defined(TLS_SERVER_CONTEXT_BORINGSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_context_ossl.cc b/deps/ngtcp2/ngtcp2/examples/tls_server_context_ossl.cc new file mode 100644 index 00000000000000..054db5caaf00b1 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_context_ossl.cc @@ -0,0 +1,305 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_context_ossl.h" + +#include +#include +#include +#include +#include + +#include + +#include + +#include "server_base.h" +#include "template.h" + +namespace { +auto _ = [] { + if (ngtcp2_crypto_ossl_init() != 0) { + assert(0); + abort(); + } + + return 0; +}(); +} // namespace + +extern Config config; + +TLSServerContext::~TLSServerContext() { + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + } +} + +SSL_CTX *TLSServerContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int alpn_select_proto_h3_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version2(h->conn()); + + switch (version) { + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2: + break; + default: + if (!config.quiet) { + std::println(stderr, "Unexpected quic protocol version: {:#x}", version); + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto s = std::span{in, inlen}; s.size() >= H3_ALPN_V1.size(); + s = s.subspan(s[0] + 1)) { + if (std::ranges::equal(H3_ALPN_V1, s.first(H3_ALPN_V1.size()))) { + *out = &s[1]; + *outlen = s[0]; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::println(stderr, "Client did not present ALPN {}", + as_string_view(H3_ALPN_V1.subspan(1))); + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int alpn_select_proto_hq_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version2(h->conn()); + + switch (version) { + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2: + break; + default: + if (!config.quiet) { + std::println(stderr, "Unexpected quic protocol version: {:#x}", version); + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto s = std::span{in, inlen}; s.size() >= HQ_ALPN_V1.size(); + s = s.subspan(s[0] + 1)) { + if (std::ranges::equal(HQ_ALPN_V1, s.first(HQ_ALPN_V1.size()))) { + *out = &s[1]; + *outlen = s[0]; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::println(stderr, "Client did not present ALPN {}", + as_string_view(HQ_ALPN_V1.subspan(1))); + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int verify_cb(int preverify_ok, X509_STORE_CTX *ctx) { + // We don't verify the client certificate. Just request it for the + // testing purpose. + return 1; +} +} // namespace + +namespace { +int gen_ticket_cb(SSL *ssl, void *arg) { + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + auto ver = htonl(ngtcp2_conn_get_negotiated_version2(h->conn())); + + if (!SSL_SESSION_set1_ticket_appdata(SSL_get0_session(ssl), &ver, + sizeof(ver))) { + return 0; + } + + return 1; +} +} // namespace + +namespace { +SSL_TICKET_RETURN decrypt_ticket_cb(SSL *ssl, SSL_SESSION *session, + const unsigned char *keyname, + size_t keynamelen, SSL_TICKET_STATUS status, + void *arg) { + switch (status) { + case SSL_TICKET_EMPTY: + case SSL_TICKET_NO_DECRYPT: + return SSL_TICKET_RETURN_IGNORE_RENEW; + } + + uint8_t *pver; + uint32_t ver; + size_t verlen; + + if (!SSL_SESSION_get0_ticket_appdata( + session, reinterpret_cast(&pver), &verlen) || + verlen != sizeof(ver)) { + switch (status) { + case SSL_TICKET_SUCCESS: + return SSL_TICKET_RETURN_IGNORE; + case SSL_TICKET_SUCCESS_RENEW: + default: + return SSL_TICKET_RETURN_IGNORE_RENEW; + } + } + + memcpy(&ver, pver, sizeof(ver)); + + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + + if (ngtcp2_conn_get_client_chosen_version2(h->conn()) != ntohl(ver)) { + switch (status) { + case SSL_TICKET_SUCCESS: + return SSL_TICKET_RETURN_IGNORE; + case SSL_TICKET_SUCCESS_RENEW: + default: + return SSL_TICKET_RETURN_IGNORE_RENEW; + } + } + + switch (status) { + case SSL_TICKET_SUCCESS: + return SSL_TICKET_RETURN_USE; + case SSL_TICKET_SUCCESS_RENEW: + default: + return SSL_TICKET_RETURN_USE_RENEW; + } +} +} // namespace + +std::expected TLSServerContext::init(const char *private_key_file, + const char *cert_file, + AppProtocol app_proto) { + static constexpr unsigned char sid_ctx[] = "ngtcp2 server"; + + ssl_ctx_ = SSL_CTX_new(TLS_server_method()); + if (!ssl_ctx_) { + std::println(stderr, "SSL_CTX_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + SSL_CTX_set_max_early_data(ssl_ctx_, UINT32_MAX); + + constexpr auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_SINGLE_ECDH_USE | + SSL_OP_CIPHER_SERVER_PREFERENCE | + SSL_OP_NO_ANTI_REPLAY; + + SSL_CTX_set_options(ssl_ctx_, ssl_opts); + + if (SSL_CTX_set_ciphersuites(ssl_ctx_, config.ciphers) != 1) { + std::println(stderr, "SSL_CTX_set_ciphersuites: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_set1_groups_list(ssl_ctx_, config.groups) != 1) { + std::println(stderr, "SSL_CTX_set1_groups_list failed"); + return std::unexpected{Error::CRYPTO}; + } + + SSL_CTX_set_mode(ssl_ctx_, SSL_MODE_RELEASE_BUFFERS); + + switch (app_proto) { + case AppProtocol::H3: + SSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_h3_cb, nullptr); + break; + case AppProtocol::HQ: + SSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_hq_cb, nullptr); + break; + } + + SSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (SSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::println(stderr, "SSL_CTX_use_PrivateKey_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::println(stderr, "SSL_CTX_use_certificate_chain_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_check_private_key(ssl_ctx_) != 1) { + std::println(stderr, "SSL_CTX_check_private_key: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + SSL_CTX_set_session_id_context(ssl_ctx_, sid_ctx, sizeof(sid_ctx) - 1); + + if (config.verify_client) { + SSL_CTX_set_verify(ssl_ctx_, + SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_cb); + } + + SSL_CTX_set_session_ticket_cb(ssl_ctx_, gen_ticket_cb, decrypt_ticket_cb, + nullptr); + + return {}; +} + +extern std::ofstream keylog_file; + +namespace { +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, static_cast(strlen(line))); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +void TLSServerContext::enable_keylog() { + SSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_context_ossl.h b/deps/ngtcp2/ngtcp2/examples/tls_server_context_ossl.h new file mode 100644 index 00000000000000..619f1310fec093 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_context_ossl.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_OSSL_H +#define TLS_SERVER_CONTEXT_OSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext { +public: + TLSServerContext() = default; + ~TLSServerContext(); + + std::expected init(const char *private_key_file, + const char *cert_file, AppProtocol app_proto); + + SSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + SSL_CTX *ssl_ctx_{}; +}; + +#endif // !defined(TLS_SERVER_CONTEXT_OSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_context_picotls.cc b/deps/ngtcp2/ngtcp2/examples/tls_server_context_picotls.cc new file mode 100644 index 00000000000000..597669df7caf9d --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_context_picotls.cc @@ -0,0 +1,376 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_context_picotls.h" + +#include +#include + +#include + +#include +#if OPENSSL_VERSION_NUMBER >= 0x30000000L +# include +#endif // OPENSSL_VERSION_NUMBER >= 0x30000000L + +#include "server_base.h" +#include "tls_shared_picotls.h" +#include "template.h" + +extern Config config; + +namespace { +int on_client_hello_h3_cb(ptls_on_client_hello_t *self, ptls_t *ptls, + ptls_on_client_hello_parameters_t *params) { + auto &negprotos = params->negotiated_protocols; + + for (size_t i = 0; i < negprotos.count; ++i) { + auto &proto = negprotos.list[i]; + if (std::ranges::equal(H3_ALPN_V1.subspan(1), + std::span{proto.base, proto.len})) { + if (ptls_set_negotiated_protocol( + ptls, reinterpret_cast(proto.base), proto.len) != 0) { + return -1; + } + + return 0; + } + } + + return PTLS_ALERT_NO_APPLICATION_PROTOCOL; +} + +ptls_on_client_hello_t on_client_hello_h3 = {on_client_hello_h3_cb}; +} // namespace + +namespace { +int on_client_hello_hq_cb(ptls_on_client_hello_t *self, ptls_t *ptls, + ptls_on_client_hello_parameters_t *params) { + auto &negprotos = params->negotiated_protocols; + + for (size_t i = 0; i < negprotos.count; ++i) { + auto &proto = negprotos.list[i]; + if (std::ranges::equal(HQ_ALPN_V1.subspan(1), + std::span{proto.base, proto.len})) { + if (ptls_set_negotiated_protocol( + ptls, reinterpret_cast(proto.base), proto.len) != 0) { + return -1; + } + + return 0; + } + } + + return PTLS_ALERT_NO_APPLICATION_PROTOCOL; +} + +ptls_on_client_hello_t on_client_hello_hq = {on_client_hello_hq_cb}; +} // namespace + +namespace { +std::span get_ticket_key_name() { + static std::array key_name; + ptls_openssl_random_bytes(key_name.data(), key_name.size()); + return key_name; +} + +std::span get_ticket_key() { + static std::array key; + ptls_openssl_random_bytes(key.data(), key.size()); + return key; +} + +std::span get_ticket_hmac_key() { + static std::array hmac_key; + ptls_openssl_random_bytes(hmac_key.data(), hmac_key.size()); + return hmac_key; +} +} // namespace + +namespace { +int ticket_key_cb(unsigned char *key_name, unsigned char *iv, + EVP_CIPHER_CTX *ctx, +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + EVP_MAC_CTX *hctx, +#else // OPENSSL_VERSION_NUMBER < 0x30000000L + HMAC_CTX *hctx, +#endif // OPENSSL_VERSION_NUMBER < 0x30000000L + int enc) { + static const auto static_key_name = get_ticket_key_name(); + static const auto static_key = get_ticket_key(); + static const auto static_hmac_key = get_ticket_hmac_key(); +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + static const auto ticket_hmac = EVP_MD_fetch(nullptr, "sha256", nullptr); + static const auto aes_256_cbc = + EVP_CIPHER_fetch(nullptr, "AES-256-CBC", nullptr); +#else // OPENSSL_VERSION_NUMBER < 0x30000000L + static const auto ticket_hmac = EVP_sha256(); + static const auto aes_256_cbc = EVP_aes_256_cbc(); +#endif // OPENSSL_VERSION_NUMBER < 0x30000000L + + if (enc) { + ptls_openssl_random_bytes(iv, EVP_MAX_IV_LENGTH); + + std::ranges::copy(static_key_name, key_name); + + if (!EVP_EncryptInit_ex(ctx, aes_256_cbc, nullptr, static_key.data(), iv)) { + return 0; + } +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + auto params = std::to_array({ + OSSL_PARAM_construct_octet_string( + OSSL_MAC_PARAM_KEY, const_cast(static_hmac_key.data()), + static_hmac_key.size()), + OSSL_PARAM_construct_utf8_string( + OSSL_MAC_PARAM_DIGEST, + const_cast(EVP_MD_get0_name(ticket_hmac)), 0), + OSSL_PARAM_construct_end(), + }); + if (!EVP_MAC_CTX_set_params(hctx, params.data())) { + /* TODO Which value should we return on error? */ + return 0; + } +#else // OPENSSL_VERSION_NUMBER < 0x30000000L + if (!HMAC_Init_ex(hctx, static_hmac_key.data(), + static_cast(static_hmac_key.size()), ticket_hmac, + nullptr)) { + return 0; + } +#endif // OPENSSL_VERSION_NUMBER < 0x30000000L + + return 1; + } + + if (!std::ranges::equal(std::span{key_name, static_key_name.size()}, + static_key_name)) { + return 0; + } + + if (!EVP_DecryptInit_ex(ctx, aes_256_cbc, nullptr, static_key.data(), iv)) { + return 0; + } +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + auto params = std::to_array({ + OSSL_PARAM_construct_octet_string( + OSSL_MAC_PARAM_KEY, const_cast(static_hmac_key.data()), + static_hmac_key.size()), + OSSL_PARAM_construct_utf8_string( + OSSL_MAC_PARAM_DIGEST, const_cast(EVP_MD_get0_name(ticket_hmac)), + 0), + OSSL_PARAM_construct_end(), + }); + if (!EVP_MAC_CTX_set_params(hctx, params.data())) { + /* TODO Which value should we return on error? */ + return 0; + } +#else // OPENSSL_VERSION_NUMBER < 0x30000000L + if (!HMAC_Init_ex(hctx, static_hmac_key.data(), + static_cast(static_hmac_key.size()), ticket_hmac, + nullptr)) { + return 0; + } +#endif // OPENSSL_VERSION_NUMBER < 0x30000000L + + return 1; +} +} // namespace + +namespace { +int encrypt_ticket_cb(ptls_encrypt_ticket_t *encrypt_ticket, ptls_t *ptls, + int is_encrypt, ptls_buffer_t *dst, ptls_iovec_t src) { + int rv; + auto conn_ref = + static_cast(*ptls_get_data_ptr(ptls)); + auto conn = conn_ref->get_conn(conn_ref); + uint32_t ver; + + if (is_encrypt) { + ver = htonl(ngtcp2_conn_get_negotiated_version2(conn)); + // TODO Replace std::make_unique with + // std::make_unique_for_overwrite when it is available. + auto buf = std::make_unique(src.len + sizeof(ver)); + auto p = std::ranges::copy_n(src.base, as_signed(src.len), buf.get()).out; + p = std::ranges::copy_n(reinterpret_cast(&ver), sizeof(ver), p) + .out; + + src.base = buf.get(); + src.len = as_unsigned(p - buf.get()); + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + rv = ptls_openssl_encrypt_ticket_evp(dst, src, ticket_key_cb); +#else // OPENSSL_VERSION_NUMBER < 0x30000000L + rv = ptls_openssl_encrypt_ticket(dst, src, ticket_key_cb); +#endif // OPENSSL_VERSION_NUMBER < 0x30000000L + if (rv != 0) { + return -1; + } + + return 0; + } + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + rv = ptls_openssl_decrypt_ticket_evp(dst, src, ticket_key_cb); +#else // OPENSSL_VERSION_NUMBER < 0x30000000L + rv = ptls_openssl_decrypt_ticket(dst, src, ticket_key_cb); +#endif // OPENSSL_VERSION_NUMBER < 0x30000000L + if (rv != 0) { + return -1; + } + + if (dst->off < sizeof(ver)) { + return -1; + } + + memcpy(&ver, dst->base + dst->off - sizeof(ver), sizeof(ver)); + + if (ngtcp2_conn_get_client_chosen_version2(conn) != ntohl(ver)) { + return -1; + } + + dst->off -= sizeof(ver); + + return 0; +} + +ptls_encrypt_ticket_t encrypt_ticket = {encrypt_ticket_cb}; +} // namespace + +namespace { +ptls_key_exchange_algorithm_t *key_exchanges[] = { +#if PTLS_OPENSSL_HAVE_X25519 + &ptls_openssl_x25519, +#endif // PTLS_OPENSSL_X25519 + &ptls_openssl_secp256r1, + &ptls_openssl_secp384r1, + &ptls_openssl_secp521r1, +#if PTLS_OPENSSL_HAVE_X25519MLKEM768 + &ptls_openssl_x25519mlkem768, +#endif // PTLS_OPENSSL_HAVE_X25519MLKEM768 + nullptr, +}; +} // namespace + +namespace { +ptls_cipher_suite_t *cipher_suites[] = { + &ptls_openssl_aes128gcmsha256, + &ptls_openssl_aes256gcmsha384, +#if PTLS_OPENSSL_HAVE_CHACHA20_POLY1305 + &ptls_openssl_chacha20poly1305sha256, +#endif // PTLS_OPENSSL_CHACHA20POLY1305SHA256 + nullptr, +}; +} // namespace + +TLSServerContext::TLSServerContext() + : ctx_{ + .random_bytes = ptls_openssl_random_bytes, + .get_time = &ptls_get_time, + .key_exchanges = key_exchanges, + .cipher_suites = cipher_suites, + .ticket_lifetime = 86400, + .require_dhe_on_psk = 1, + .server_cipher_preference = 1, + .encrypt_ticket = &encrypt_ticket, + } {} + +TLSServerContext::~TLSServerContext() { + if (sign_cert_.key) { + ptls_openssl_dispose_sign_certificate(&sign_cert_); + } + + for (size_t i = 0; i < ctx_.certificates.count; ++i) { + free(ctx_.certificates.list[i].base); + } + free(ctx_.certificates.list); +} + +ptls_context_t *TLSServerContext::get_native_handle() { return &ctx_; } + +std::expected TLSServerContext::init(const char *private_key_file, + const char *cert_file, + AppProtocol app_proto) { + switch (app_proto) { + case AppProtocol::H3: + ctx_.on_client_hello = &on_client_hello_h3; + + break; + case AppProtocol::HQ: + ctx_.on_client_hello = &on_client_hello_hq; + + break; + } + + if (ngtcp2_crypto_picotls_configure_server_context(&ctx_) != 0) { + std::println(stderr, + "ngtcp2_crypto_picotls_configure_server_context failed"); + return std::unexpected{Error::CRYPTO}; + } + + if (ptls_load_certificates(&ctx_, cert_file) != 0) { + std::println(stderr, "ptls_load_certificates failed"); + return std::unexpected{Error::CRYPTO}; + } + + if (auto rv = load_private_key(private_key_file); !rv) { + return rv; + } + + if (config.verify_client) { + ctx_.require_client_authentication = 1; + } + + return {}; +} + +std::expected +TLSServerContext::load_private_key(const char *private_key_file) { + auto fp = fopen(private_key_file, "rb"); + if (fp == nullptr) { + std::println(stderr, "Could not open private key file {}: {}", + private_key_file, strerror(errno)); + return std::unexpected{Error::IO}; + } + + auto fp_d = defer([fp] { fclose(fp); }); + + auto pkey = PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr); + if (pkey == nullptr) { + std::println(stderr, "Could not read private key file {}", + private_key_file); + return std::unexpected{Error::IO}; + } + + auto pkey_d = defer([pkey] { EVP_PKEY_free(pkey); }); + + if (ptls_openssl_init_sign_certificate(&sign_cert_, pkey) != 0) { + std::println(stderr, "ptls_openssl_init_sign_certificate failed"); + return std::unexpected{Error::CRYPTO}; + } + + ctx_.sign_certificate = &sign_cert_.super; + + return {}; +} + +void TLSServerContext::enable_keylog() { ctx_.log_event = &log_event; } diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_context_picotls.h b/deps/ngtcp2/ngtcp2/examples/tls_server_context_picotls.h new file mode 100644 index 00000000000000..862568f96bed48 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_context_picotls.h @@ -0,0 +1,58 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_PICOTLS_H +#define TLS_SERVER_CONTEXT_PICOTLS_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include + +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext { +public: + TLSServerContext(); + ~TLSServerContext(); + + std::expected init(const char *private_key_file, + const char *cert_file, AppProtocol app_proto); + + ptls_context_t *get_native_handle(); + + void enable_keylog(); + +private: + std::expected load_private_key(const char *private_key_file); + + ptls_context_t ctx_; + ptls_openssl_sign_certificate_t sign_cert_{}; +}; + +#endif // !defined(TLS_SERVER_CONTEXT_PICOTLS_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_context_quictls.cc b/deps/ngtcp2/ngtcp2/examples/tls_server_context_quictls.cc new file mode 100644 index 00000000000000..9c93fdc9ac9b57 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_context_quictls.cc @@ -0,0 +1,318 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_context_quictls.h" + +#include +#include +#include +#include +#include + +#include + +#include + +#include "server_base.h" +#include "template.h" + +namespace { +auto _ = [] { + if (ngtcp2_crypto_quictls_init() != 0) { + assert(0); + abort(); + } + + return 0; +}(); +} // namespace + +extern Config config; + +TLSServerContext::~TLSServerContext() { + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + } +} + +SSL_CTX *TLSServerContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int alpn_select_proto_h3_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version2(h->conn()); + + switch (version) { + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2: + break; + default: + if (!config.quiet) { + std::println(stderr, "Unexpected quic protocol version: {:#x}", version); + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto s = std::span{in, inlen}; s.size() >= H3_ALPN_V1.size(); + s = s.subspan(s[0] + 1)) { + if (std::ranges::equal(H3_ALPN_V1, s.first(H3_ALPN_V1.size()))) { + *out = &s[1]; + *outlen = s[0]; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::println(stderr, "Client did not present ALPN {}", + as_string_view(H3_ALPN_V1.subspan(1))); + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int alpn_select_proto_hq_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version2(h->conn()); + + switch (version) { + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2: + break; + default: + if (!config.quiet) { + std::println(stderr, "Unexpected quic protocol version: {:#x}", version); + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto s = std::span{in, inlen}; s.size() >= HQ_ALPN_V1.size(); + s = s.subspan(s[0] + 1)) { + if (std::ranges::equal(HQ_ALPN_V1, s.first(HQ_ALPN_V1.size()))) { + *out = &s[1]; + *outlen = s[0]; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::println(stderr, "Client did not present ALPN {}", + as_string_view(HQ_ALPN_V1.subspan(1))); + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int verify_cb(int preverify_ok, X509_STORE_CTX *ctx) { + // We don't verify the client certificate. Just request it for the + // testing purpose. + return 1; +} +} // namespace + +#ifndef LIBRESSL_VERSION_NUMBER +namespace { +int gen_ticket_cb(SSL *ssl, void *arg) { + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + auto ver = htonl(ngtcp2_conn_get_negotiated_version2(h->conn())); + + if (!SSL_SESSION_set1_ticket_appdata(SSL_get0_session(ssl), &ver, + sizeof(ver))) { + return 0; + } + + return 1; +} +} // namespace + +namespace { +SSL_TICKET_RETURN decrypt_ticket_cb(SSL *ssl, SSL_SESSION *session, + const unsigned char *keyname, + size_t keynamelen, SSL_TICKET_STATUS status, + void *arg) { + switch (status) { + case SSL_TICKET_EMPTY: + case SSL_TICKET_NO_DECRYPT: + return SSL_TICKET_RETURN_IGNORE_RENEW; + } + + uint8_t *pver; + uint32_t ver; + size_t verlen; + + if (!SSL_SESSION_get0_ticket_appdata( + session, reinterpret_cast(&pver), &verlen) || + verlen != sizeof(ver)) { + switch (status) { + case SSL_TICKET_SUCCESS: + return SSL_TICKET_RETURN_IGNORE; + case SSL_TICKET_SUCCESS_RENEW: + default: + return SSL_TICKET_RETURN_IGNORE_RENEW; + } + } + + memcpy(&ver, pver, sizeof(ver)); + + auto conn_ref = static_cast(SSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + + if (ngtcp2_conn_get_client_chosen_version2(h->conn()) != ntohl(ver)) { + switch (status) { + case SSL_TICKET_SUCCESS: + return SSL_TICKET_RETURN_IGNORE; + case SSL_TICKET_SUCCESS_RENEW: + default: + return SSL_TICKET_RETURN_IGNORE_RENEW; + } + } + + switch (status) { + case SSL_TICKET_SUCCESS: + return SSL_TICKET_RETURN_USE; + case SSL_TICKET_SUCCESS_RENEW: + default: + return SSL_TICKET_RETURN_USE_RENEW; + } +} +} // namespace +#endif // !defined(LIBRESSL_VERSION_NUMBER) + +std::expected TLSServerContext::init(const char *private_key_file, + const char *cert_file, + AppProtocol app_proto) { + static constexpr unsigned char sid_ctx[] = "ngtcp2 server"; + + ssl_ctx_ = SSL_CTX_new(TLS_server_method()); + if (!ssl_ctx_) { + std::println(stderr, "SSL_CTX_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (ngtcp2_crypto_quictls_configure_server_context(ssl_ctx_) != 0) { + std::println(stderr, + "ngtcp2_crypto_quictls_configure_server_context failed"); + return std::unexpected{Error::CRYPTO}; + } + + SSL_CTX_set_max_early_data(ssl_ctx_, UINT32_MAX); + + constexpr auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_SINGLE_ECDH_USE | + SSL_OP_CIPHER_SERVER_PREFERENCE +#ifndef LIBRESSL_VERSION_NUMBER + | SSL_OP_NO_ANTI_REPLAY +#endif // !defined(LIBRESSL_VERSION_NUMBER) + ; + + SSL_CTX_set_options(ssl_ctx_, ssl_opts); + + if (SSL_CTX_set_ciphersuites(ssl_ctx_, config.ciphers) != 1) { + std::println(stderr, "SSL_CTX_set_ciphersuites: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_set1_groups_list(ssl_ctx_, config.groups) != 1) { + std::println(stderr, "SSL_CTX_set1_groups_list failed"); + return std::unexpected{Error::CRYPTO}; + } + + SSL_CTX_set_mode(ssl_ctx_, SSL_MODE_RELEASE_BUFFERS); + + switch (app_proto) { + case AppProtocol::H3: + SSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_h3_cb, nullptr); + break; + case AppProtocol::HQ: + SSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_hq_cb, nullptr); + break; + } + + SSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (SSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::println(stderr, "SSL_CTX_use_PrivateKey_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::println(stderr, "SSL_CTX_use_certificate_chain_file: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_CTX_check_private_key(ssl_ctx_) != 1) { + std::println(stderr, "SSL_CTX_check_private_key: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + SSL_CTX_set_session_id_context(ssl_ctx_, sid_ctx, sizeof(sid_ctx) - 1); + + if (config.verify_client) { + SSL_CTX_set_verify(ssl_ctx_, + SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_cb); + } + +#ifndef LIBRESSL_VERSION_NUMBER + SSL_CTX_set_session_ticket_cb(ssl_ctx_, gen_ticket_cb, decrypt_ticket_cb, + nullptr); +#endif // !defined(LIBRESSL_VERSION_NUMBER) + + return {}; +} + +extern std::ofstream keylog_file; + +namespace { +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, static_cast(strlen(line))); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +void TLSServerContext::enable_keylog() { + SSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_context_quictls.h b/deps/ngtcp2/ngtcp2/examples/tls_server_context_quictls.h new file mode 100644 index 00000000000000..ccb485c48ac01a --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_context_quictls.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_QUICTLS_H +#define TLS_SERVER_CONTEXT_QUICTLS_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext { +public: + TLSServerContext() = default; + ~TLSServerContext(); + + std::expected init(const char *private_key_file, + const char *cert_file, AppProtocol app_proto); + + SSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + SSL_CTX *ssl_ctx_{}; +}; + +#endif // !defined(TLS_SERVER_CONTEXT_QUICTLS_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_context_wolfssl.cc b/deps/ngtcp2/ngtcp2/examples/tls_server_context_wolfssl.cc new file mode 100644 index 00000000000000..5245b555c8f88c --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_context_wolfssl.cc @@ -0,0 +1,242 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_context_wolfssl.h" + +#include +#include +#include +#include + +#include + +#include "server_base.h" +#include "template.h" + +extern Config config; + +TLSServerContext::~TLSServerContext() { + if (ssl_ctx_) { + wolfSSL_CTX_free(ssl_ctx_); + } +} + +WOLFSSL_CTX *TLSServerContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int alpn_select_proto_h3_cb(WOLFSSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = + static_cast(wolfSSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version2(h->conn()); + + switch (version) { + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2: + break; + default: + if (!config.quiet) { + std::println(stderr, "Unexpected quic protocol version: {:#x}", version); + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto s = std::span{in, inlen}; s.size() >= H3_ALPN_V1.size(); + s = s.subspan(s[0] + 1)) { + if (std::ranges::equal(H3_ALPN_V1, s.first(H3_ALPN_V1.size()))) { + *out = &s[1]; + *outlen = s[0]; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::println(stderr, "Client did not present ALPN {}", + as_string_view(H3_ALPN_V1.subspan(1))); + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int alpn_select_proto_hq_cb(WOLFSSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = + static_cast(wolfSSL_get_app_data(ssl)); + auto h = static_cast(conn_ref->user_data); + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version2(h->conn()); + + switch (version) { + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2: + break; + default: + if (!config.quiet) { + std::println(stderr, "Unexpected quic protocol version: {:#x}", version); + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto s = std::span{in, inlen}; s.size() >= HQ_ALPN_V1.size(); + s = s.subspan(s[0] + 1)) { + if (std::ranges::equal(HQ_ALPN_V1, s.first(HQ_ALPN_V1.size()))) { + *out = &s[1]; + *outlen = s[0]; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::println(stderr, "Client did not present ALPN {}", + as_string_view(HQ_ALPN_V1.subspan(1))); + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int verify_cb(int preverify_ok, X509_STORE_CTX *ctx) { + // We don't verify the client certificate. Just request it for the + // testing purpose. + return 1; +} +} // namespace + +std::expected TLSServerContext::init(const char *private_key_file, + const char *cert_file, + AppProtocol app_proto) { + static constexpr unsigned char sid_ctx[] = "ngtcp2 server"; + +#ifdef DEBUG_WOLFSSL + if (!config.quiet) { + /*wolfSSL_Debugging_ON();*/ + } +#endif // defined(DEBUG_WOLFSSL) + + ssl_ctx_ = wolfSSL_CTX_new(wolfTLSv1_3_server_method()); + if (!ssl_ctx_) { + std::println(stderr, "wolfSSL_CTX_new: {}", + wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (ngtcp2_crypto_wolfssl_configure_server_context(ssl_ctx_) != 0) { + std::println(stderr, + "ngtcp2_crypto_wolfssl_configure_server_context failed"); + return std::unexpected{Error::CRYPTO}; + } + +#ifdef WOLFSSL_EARLY_DATA + wolfSSL_CTX_set_max_early_data(ssl_ctx_, UINT32_MAX); +#endif // defined(WOLFSSL_EARLY_DATA) + + constexpr auto ssl_opts = + (WOLFSSL_OP_ALL & ~WOLFSSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + WOLFSSL_OP_SINGLE_ECDH_USE | WOLFSSL_OP_CIPHER_SERVER_PREFERENCE; + + wolfSSL_CTX_set_options(ssl_ctx_, ssl_opts); + + if (wolfSSL_CTX_set_cipher_list(ssl_ctx_, config.ciphers) != 1) { + std::println(stderr, "wolfSSL_CTX_set_cipher_list: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (wolfSSL_CTX_set1_groups_list(ssl_ctx_, + const_cast(config.groups)) != 1) { + std::println(stderr, "wolfSSL_CTX_set1_groups_list({}) failed", + config.groups); + return std::unexpected{Error::CRYPTO}; + } + + wolfSSL_CTX_set_mode(ssl_ctx_, SSL_MODE_RELEASE_BUFFERS); + + switch (app_proto) { + case AppProtocol::H3: + wolfSSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_h3_cb, nullptr); + break; + case AppProtocol::HQ: + wolfSSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_hq_cb, nullptr); + break; + } + + wolfSSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (wolfSSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::println(stderr, "wolfSSL_CTX_use_PrivateKey_file: {}", + wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (wolfSSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::println(stderr, "wolfSSL_CTX_use_certificate_chain_file: {}", + wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + if (wolfSSL_CTX_check_private_key(ssl_ctx_) != 1) { + std::println(stderr, "wolfSSL_CTX_check_private_key: {}", + wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + wolfSSL_CTX_set_session_id_context(ssl_ctx_, sid_ctx, sizeof(sid_ctx) - 1); + + if (config.verify_client) { + wolfSSL_CTX_set_verify(ssl_ctx_, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_CLIENT_ONCE | + WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_cb); + } + + return {}; +} + +extern std::ofstream keylog_file; + +#ifdef HAVE_SECRET_CALLBACK +namespace { +void keylog_callback(const WOLFSSL *ssl, const char *line) { + keylog_file.write(line, static_cast(strlen(line))); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace +#endif // defined(HAVE_SECRET_CALLBACK) + +void TLSServerContext::enable_keylog() { +#ifdef HAVE_SECRET_CALLBACK + wolfSSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +#endif // defined(HAVE_SECRET_CALLBACK) +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_context_wolfssl.h b/deps/ngtcp2/ngtcp2/examples/tls_server_context_wolfssl.h new file mode 100644 index 00000000000000..2f235ca4570b47 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_context_wolfssl.h @@ -0,0 +1,55 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_WOLFSSL_H +#define TLS_SERVER_CONTEXT_WOLFSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include + +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext { +public: + TLSServerContext() = default; + ~TLSServerContext(); + + std::expected init(const char *private_key_file, + const char *cert_file, AppProtocol app_proto); + + WOLFSSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + WOLFSSL_CTX *ssl_ctx_{}; +}; + +#endif // !defined(TLS_SERVER_CONTEXT_WOLFSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_session.h b/deps/ngtcp2/ngtcp2/examples/tls_server_session.h new file mode 100644 index 00000000000000..652f76dbdfca95 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_session.h @@ -0,0 +1,56 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_H +#define TLS_SERVER_SESSION_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#ifdef WITH_EXAMPLE_QUICTLS +# include "tls_server_session_quictls.h" +#endif // defined(WITH_EXAMPLE_QUICTLS) + +#ifdef WITH_EXAMPLE_GNUTLS +# include "tls_server_session_gnutls.h" +#endif // defined(WITH_EXAMPLE_GNUTLS) + +#ifdef WITH_EXAMPLE_BORINGSSL +# include "tls_server_session_boringssl.h" +#endif // defined(WITH_EXAMPLE_BORINGSSL) + +#ifdef WITH_EXAMPLE_PICOTLS +# include "tls_server_session_picotls.h" +#endif // defined(WITH_EXAMPLE_PICOTLS) + +#ifdef WITH_EXAMPLE_WOLFSSL +# include "tls_server_session_wolfssl.h" +#endif // defined(WITH_EXAMPLE_WOLFSSL) + +#ifdef WITH_EXAMPLE_OSSL +# include "tls_server_session_ossl.h" +#endif // defined(WITH_EXAMPLE_OSSL) + +#endif // !defined(TLS_SERVER_SESSION_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_session_boringssl.cc b/deps/ngtcp2/ngtcp2/examples/tls_server_session_boringssl.cc new file mode 100644 index 00000000000000..c2f28045496c2b --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_session_boringssl.cc @@ -0,0 +1,78 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_session_boringssl.h" + +#include + +#include + +#include "tls_server_context_boringssl.h" +#include "server_base.h" + +extern Config config; + +std::expected +TLSServerSession::init(const TLSServerContext &tls_ctx, HandlerBase *handler) { + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = SSL_new(ssl_ctx); + if (!ssl_) { + std::println(stderr, "SSL_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + SSL_set_app_data(ssl_, handler->conn_ref()); + SSL_set_accept_state(ssl_); + SSL_set_early_data_enabled(ssl_, 1); + + std::array quic_early_data_ctx; + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + params.initial_max_streams_bidi = config.max_streams_bidi; + params.initial_max_streams_uni = config.max_streams_uni; + params.initial_max_stream_data_bidi_local = config.max_stream_data_bidi_local; + params.initial_max_stream_data_bidi_remote = + config.max_stream_data_bidi_remote; + params.initial_max_stream_data_uni = config.max_stream_data_uni; + params.initial_max_data = config.max_data; + + auto quic_early_data_ctxlen = ngtcp2_transport_params_encode( + quic_early_data_ctx.data(), quic_early_data_ctx.size(), ¶ms); + if (quic_early_data_ctxlen < 0) { + std::println(stderr, "ngtcp2_transport_params_encode: {}", + ngtcp2_strerror(static_cast(quic_early_data_ctxlen))); + return std::unexpected{Error::CRYPTO}; + } + + if (SSL_set_quic_early_data_context(ssl_, quic_early_data_ctx.data(), + as_unsigned(quic_early_data_ctxlen)) != + 1) { + std::println(stderr, "SSL_set_quic_early_data_context failed"); + return std::unexpected{Error::CRYPTO}; + } + + return {}; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_session_boringssl.h b/deps/ngtcp2/ngtcp2/examples/tls_server_session_boringssl.h new file mode 100644 index 00000000000000..3599e9a6c12081 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_session_boringssl.h @@ -0,0 +1,50 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_BORINGSSL_H +#define TLS_SERVER_SESSION_BORINGSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include "tls_session_base_quictls.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext; +class HandlerBase; + +class TLSServerSession : public TLSSessionBase { +public: + TLSServerSession() = default; + + std::expected init(const TLSServerContext &tls_ctx, + HandlerBase *handler); + // ticket is sent automatically. + std::expected send_session_ticket() { return {}; } +}; + +#endif // !defined(TLS_SERVER_SESSION_BORINGSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_session_ossl.cc b/deps/ngtcp2/ngtcp2/examples/tls_server_session_ossl.cc new file mode 100644 index 00000000000000..c0e3a71d1d3179 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_session_ossl.cc @@ -0,0 +1,55 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_session_ossl.h" + +#include + +#include "tls_server_context_ossl.h" +#include "server_base.h" + +std::expected +TLSServerSession::init(const TLSServerContext &tls_ctx, HandlerBase *handler) { + auto ssl_ctx = tls_ctx.get_native_handle(); + + auto ssl = SSL_new(ssl_ctx); + if (!ssl) { + std::println(stderr, "SSL_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + ngtcp2_crypto_ossl_ctx_set_ssl(ossl_ctx_, ssl); + + if (ngtcp2_crypto_ossl_configure_server_session(ssl) != 0) { + std::println(stderr, "ngtcp2_crypto_ossl_configure_server_session failed"); + return std::unexpected{Error::CRYPTO}; + } + + SSL_set_app_data(ssl, handler->conn_ref()); + SSL_set_accept_state(ssl); + SSL_set_quic_tls_early_data_enabled(ssl, 1); + + return {}; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_session_ossl.h b/deps/ngtcp2/ngtcp2/examples/tls_server_session_ossl.h new file mode 100644 index 00000000000000..6f859e25e8d614 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_session_ossl.h @@ -0,0 +1,50 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_OSSL_H +#define TLS_SERVER_SESSION_OSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include "tls_session_base_ossl.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext; +class HandlerBase; + +class TLSServerSession : public TLSSessionBase { +public: + TLSServerSession() = default; + + std::expected init(const TLSServerContext &tls_ctx, + HandlerBase *handler); + // ticket is sent automatically. + std::expected send_session_ticket() { return {}; } +}; + +#endif // !defined(TLS_SERVER_SESSION_OSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_session_picotls.cc b/deps/ngtcp2/ngtcp2/examples/tls_server_session_picotls.cc new file mode 100644 index 00000000000000..6f4dbf568acafa --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_session_picotls.cc @@ -0,0 +1,66 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_session_picotls.h" + +#include + +#include + +#include "tls_server_context_picotls.h" +#include "server_base.h" +#include "util.h" + +using namespace ngtcp2; + +extern Config config; + +std::expected TLSServerSession::init(TLSServerContext &tls_ctx, + HandlerBase *handler) { + cptls_.ptls = ptls_server_new(tls_ctx.get_native_handle()); + if (!cptls_.ptls) { + std::println(stderr, "ptls_server_new failed"); + return std::unexpected{Error::CRYPTO}; + } + + *ptls_get_data_ptr(cptls_.ptls) = handler->conn_ref(); + + cptls_.handshake_properties.additional_extensions = + new ptls_raw_extension_t[2]{ + { + .type = UINT16_MAX, + }, + { + .type = UINT16_MAX, + }, + }; + + if (ngtcp2_crypto_picotls_configure_server_session(&cptls_) != 0) { + std::println(stderr, + "ngtcp2_crypto_picotls_configure_server_session failed"); + return std::unexpected{Error::CRYPTO}; + } + + return {}; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_session_picotls.h b/deps/ngtcp2/ngtcp2/examples/tls_server_session_picotls.h new file mode 100644 index 00000000000000..fb15ee81f4dd47 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_session_picotls.h @@ -0,0 +1,50 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_PICOTLS_H +#define TLS_SERVER_SESSION_PICOTLS_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include "tls_session_base_picotls.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext; +class HandlerBase; + +class TLSServerSession : public TLSSessionBase { +public: + TLSServerSession() = default; + + std::expected init(TLSServerContext &tls_ctx, + HandlerBase *handler); + // ticket is sent automatically. + std::expected send_session_ticket() { return {}; } +}; + +#endif // !defined(TLS_SERVER_SESSION_PICOTLS_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_session_quictls.cc b/deps/ngtcp2/ngtcp2/examples/tls_server_session_quictls.cc new file mode 100644 index 00000000000000..7379f390040488 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_session_quictls.cc @@ -0,0 +1,50 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_session_quictls.h" + +#include + +#include "tls_server_context_quictls.h" +#include "server_base.h" + +std::expected +TLSServerSession::init(const TLSServerContext &tls_ctx, HandlerBase *handler) { + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = SSL_new(ssl_ctx); + if (!ssl_) { + std::println(stderr, "SSL_new: {}", + ERR_error_string(ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + SSL_set_app_data(ssl_, handler->conn_ref()); + SSL_set_accept_state(ssl_); +#ifndef LIBRESSL_VERSION_NUMBER + SSL_set_quic_early_data_enabled(ssl_, 1); +#endif // !defined(LIBRESSL_VERSION_NUMBER) + + return {}; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_session_quictls.h b/deps/ngtcp2/ngtcp2/examples/tls_server_session_quictls.h new file mode 100644 index 00000000000000..a5b493bd6bdf94 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_session_quictls.h @@ -0,0 +1,50 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_QUICTLS_H +#define TLS_SERVER_SESSION_QUICTLS_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include "tls_session_base_quictls.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext; +class HandlerBase; + +class TLSServerSession : public TLSSessionBase { +public: + TLSServerSession() = default; + + std::expected init(const TLSServerContext &tls_ctx, + HandlerBase *handler); + // ticket is sent automatically. + std::expected send_session_ticket() { return {}; } +}; + +#endif // !defined(TLS_SERVER_SESSION_QUICTLS_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_session_wolfssl.cc b/deps/ngtcp2/ngtcp2/examples/tls_server_session_wolfssl.cc new file mode 100644 index 00000000000000..dce9e47aa8fb4e --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_session_wolfssl.cc @@ -0,0 +1,50 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_session_wolfssl.h" + +#include "tls_server_context_wolfssl.h" +#include "server_base.h" + +std::expected +TLSServerSession::init(const TLSServerContext &tls_ctx, HandlerBase *handler) { + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = wolfSSL_new(ssl_ctx); + if (!ssl_) { + std::println(stderr, "wolfSSL_new: {}", + wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr)); + return std::unexpected{Error::CRYPTO}; + } + + wolfSSL_set_app_data(ssl_, handler->conn_ref()); + wolfSSL_set_accept_state(ssl_); +#ifdef WOLFSSL_EARLY_DATA + wolfSSL_set_quic_early_data_enabled(ssl_, 1); +#endif // defined(WOLFSSL_EARLY_DATA) + // Just use QUIC v1 + wolfSSL_set_quic_transport_version(ssl_, 0x39); + + return {}; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_server_session_wolfssl.h b/deps/ngtcp2/ngtcp2/examples/tls_server_session_wolfssl.h new file mode 100644 index 00000000000000..8d5f4bcc45eed8 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_server_session_wolfssl.h @@ -0,0 +1,50 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_WOLFSSL_H +#define TLS_SERVER_SESSION_WOLFSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include "tls_session_base_wolfssl.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext; +class HandlerBase; + +class TLSServerSession : public TLSSessionBase { +public: + TLSServerSession() = default; + + std::expected init(const TLSServerContext &tls_ctx, + HandlerBase *handler); + // ticket is sent automatically. + std::expected send_session_ticket() { return {}; } +}; + +#endif // !defined(TLS_SERVER_SESSION_WOLFSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_session_base_ossl.cc b/deps/ngtcp2/ngtcp2/examples/tls_session_base_ossl.cc new file mode 100644 index 00000000000000..cc9edda47c7df8 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_session_base_ossl.cc @@ -0,0 +1,77 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_session_base_ossl.h" + +#include + +#include "util.h" +#include "template.h" + +using namespace ngtcp2; +using namespace std::literals; + +TLSSessionBase::TLSSessionBase() { + ngtcp2_crypto_ossl_ctx_new(&ossl_ctx_, nullptr); +} + +TLSSessionBase::~TLSSessionBase() { + auto ssl = ngtcp2_crypto_ossl_ctx_get_ssl(ossl_ctx_); + + if (ssl) { + SSL_set_app_data(ssl, nullptr); + SSL_free(ssl); + } + + ngtcp2_crypto_ossl_ctx_del(ossl_ctx_); +} + +ngtcp2_crypto_ossl_ctx *TLSSessionBase::get_native_handle() const { + return ossl_ctx_; +} + +std::string TLSSessionBase::get_cipher_name() const { + return SSL_get_cipher_name(ngtcp2_crypto_ossl_ctx_get_ssl(ossl_ctx_)); +} + +std::string_view TLSSessionBase::get_negotiated_group() const { + auto ssl = ngtcp2_crypto_ossl_ctx_get_ssl(ossl_ctx_); + auto name = SSL_get0_group_name(ssl); + + if (!name) { + return ""sv; + } + + return name; +} + +std::string TLSSessionBase::get_selected_alpn() const { + auto ssl = ngtcp2_crypto_ossl_ctx_get_ssl(ossl_ctx_); + const unsigned char *alpn = nullptr; + unsigned int alpnlen; + + SSL_get0_alpn_selected(ssl, &alpn, &alpnlen); + + return std::string{alpn, alpn + alpnlen}; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_session_base_ossl.h b/deps/ngtcp2/ngtcp2/examples/tls_session_base_ossl.h new file mode 100644 index 00000000000000..d4de743c8776ef --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_session_base_ossl.h @@ -0,0 +1,56 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SESSION_BASE_OSSL_H +#define TLS_SESSION_BASE_OSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include + +#include + +#include + +class TLSSessionBase { +public: + TLSSessionBase(); + ~TLSSessionBase(); + + ngtcp2_crypto_ossl_ctx *get_native_handle() const; + + std::string get_cipher_name() const; + std::string_view get_negotiated_group() const; + std::string get_selected_alpn() const; + // Keylog is enabled per SSL_CTX. + void enable_keylog() {} + +protected: + ngtcp2_crypto_ossl_ctx *ossl_ctx_; +}; + +#endif // !defined(TLS_SESSION_BASE_OSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_session_base_picotls.cc b/deps/ngtcp2/ngtcp2/examples/tls_session_base_picotls.cc new file mode 100644 index 00000000000000..b8413b41ed0cf8 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_session_base_picotls.cc @@ -0,0 +1,56 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_session_base_picotls.h" + +TLSSessionBase::TLSSessionBase() { ngtcp2_crypto_picotls_ctx_init(&cptls_); } + +TLSSessionBase::~TLSSessionBase() { + ngtcp2_crypto_picotls_deconfigure_session(&cptls_); + + delete[] cptls_.handshake_properties.additional_extensions; + + if (cptls_.ptls) { + ptls_free(cptls_.ptls); + } +} + +ngtcp2_crypto_picotls_ctx *TLSSessionBase::get_native_handle() { + return &cptls_; +} + +std::string TLSSessionBase::get_cipher_name() const { + auto cs = ptls_get_cipher(cptls_.ptls); + return cs->aead->name; +} + +std::string TLSSessionBase::get_selected_alpn() const { + auto alpn = ptls_get_negotiated_protocol(cptls_.ptls); + + if (!alpn) { + return {}; + } + + return alpn; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_session_base_picotls.h b/deps/ngtcp2/ngtcp2/examples/tls_session_base_picotls.h new file mode 100644 index 00000000000000..a191b7df3ec3a1 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_session_base_picotls.h @@ -0,0 +1,60 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SESSION_BASE_PICOTLS_H +#define TLS_SESSION_BASE_PICOTLS_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include + +#include + +#include + +class TLSSessionBase { +public: + TLSSessionBase(); + ~TLSSessionBase(); + + ngtcp2_crypto_picotls_ctx *get_native_handle(); + + std::string get_cipher_name() const; + std::string_view get_negotiated_group() const { + using namespace std::literals; + + return ""sv; + } + std::string get_selected_alpn() const; + // TODO make keylog work with picotls + void enable_keylog() {} + +protected: + ngtcp2_crypto_picotls_ctx cptls_; +}; + +#endif // !defined(TLS_SESSION_BASE_PICOTLS_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_session_base_quictls.cc b/deps/ngtcp2/ngtcp2/examples/tls_session_base_quictls.cc new file mode 100644 index 00000000000000..8ef5027af6fde2 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_session_base_quictls.cc @@ -0,0 +1,99 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_session_base_quictls.h" + +#include + +#include "util.h" +#include "template.h" + +using namespace ngtcp2; +using namespace std::literals; + +TLSSessionBase::~TLSSessionBase() { + if (ssl_) { + SSL_free(ssl_); + } +} + +SSL *TLSSessionBase::get_native_handle() const { return ssl_; } + +std::string TLSSessionBase::get_cipher_name() const { + return SSL_get_cipher_name(ssl_); +} + +std::string_view TLSSessionBase::get_negotiated_group() const { +#ifdef WITH_EXAMPLE_BORINGSSL + return SSL_get_group_name(SSL_get_group_id(ssl_)); +#elif OPENSSL_VERSION_NUMBER >= 0x30000000L + auto name = + SSL_group_to_name(ssl_, static_cast(SSL_get_negotiated_group(ssl_))); + if (!name) { + return ""sv; + } + + return name; +#elif defined(LIBRESSL_VERSION_NUMBER) + return ""sv; +#else // !(defined(WITH_EXAMPLE_BORINGSSL) || + // OPENSSL_VERSION_NUMBER >= 0x30000000L || + // defined(LIBRESSL_VERSION_NUMBER)) + EVP_PKEY *key; + + if (!SSL_get_tmp_key(ssl_, &key)) { + return ""sv; + } + + auto key_del = defer([key] { EVP_PKEY_free(key); }); + + auto nid = EVP_PKEY_id(key); + if (nid == EVP_PKEY_EC) { + auto ec = EVP_PKEY_get0_EC_KEY(key); + + nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec)); + } + + auto name = EC_curve_nid2nist(nid); + if (!name) { + name = OBJ_nid2sn(nid); + if (!name) { + return ""sv; + } + } + + return name; +#endif // !(defined(WITH_EXAMPLE_BORINGSSL) || + // OPENSSL_VERSION_NUMBER >= 0x30000000L || + // defined(LIBRESSL_VERSION_NUMBER)) +} + +std::string TLSSessionBase::get_selected_alpn() const { + const unsigned char *alpn = nullptr; + unsigned int alpnlen; + + SSL_get0_alpn_selected(ssl_, &alpn, &alpnlen); + + return std::string{alpn, alpn + alpnlen}; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_session_base_quictls.h b/deps/ngtcp2/ngtcp2/examples/tls_session_base_quictls.h new file mode 100644 index 00000000000000..4c022a2422dc8f --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_session_base_quictls.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SESSION_BASE_QUICTLS_H +#define TLS_SESSION_BASE_QUICTLS_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include + +#include + +class TLSSessionBase { +public: + TLSSessionBase() = default; + ~TLSSessionBase(); + + SSL *get_native_handle() const; + + std::string get_cipher_name() const; + std::string_view get_negotiated_group() const; + std::string get_selected_alpn() const; + // Keylog is enabled per SSL_CTX. + void enable_keylog() {} + +protected: + SSL *ssl_{}; +}; + +#endif // !defined(TLS_SESSION_BASE_QUICTLS_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_session_base_wolfssl.cc b/deps/ngtcp2/ngtcp2/examples/tls_session_base_wolfssl.cc new file mode 100644 index 00000000000000..ce5c2ebe9edbca --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_session_base_wolfssl.cc @@ -0,0 +1,63 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_session_base_wolfssl.h" + +#include + +#include "util.h" + +using namespace ngtcp2; + +TLSSessionBase::~TLSSessionBase() { + if (ssl_) { + wolfSSL_free(ssl_); + } +} + +WOLFSSL *TLSSessionBase::get_native_handle() const { return ssl_; } + +std::string TLSSessionBase::get_cipher_name() const { + return wolfSSL_get_cipher_name(ssl_); +} + +std::string_view TLSSessionBase::get_negotiated_group() const { + using namespace std::literals; + + auto name = wolfSSL_get_curve_name(ssl_); + if (!name) { + return ""sv; + } + + return name; +} + +std::string TLSSessionBase::get_selected_alpn() const { + char *alpn = nullptr; + unsigned short alpnlen; + + wolfSSL_ALPN_GetProtocol(ssl_, &alpn, &alpnlen); + + return std::string{alpn, alpn + alpnlen}; +} diff --git a/deps/ngtcp2/ngtcp2/examples/tls_session_base_wolfssl.h b/deps/ngtcp2/ngtcp2/examples/tls_session_base_wolfssl.h new file mode 100644 index 00000000000000..b3942bc8e78d27 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_session_base_wolfssl.h @@ -0,0 +1,56 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SESSION_BASE_WOLFSSL_H +#define TLS_SESSION_BASE_WOLFSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include +#include + +#include +#include +#include + +class TLSSessionBase { +public: + TLSSessionBase() = default; + ~TLSSessionBase(); + + WOLFSSL *get_native_handle() const; + + std::string get_cipher_name() const; + std::string_view get_negotiated_group() const; + std::string get_selected_alpn() const; + // Keylog is enabled per SSL_CTX. + void enable_keylog() {} + +protected: + WOLFSSL *ssl_{}; +}; + +#endif // !defined(TLS_SESSION_BASE_WOLFSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_shared_boringssl.cc b/deps/ngtcp2/ngtcp2/examples/tls_shared_boringssl.cc new file mode 100644 index 00000000000000..eaf673e5a79c68 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_shared_boringssl.cc @@ -0,0 +1,89 @@ +/* + * ngtcp2 + * + * Copyright (c) 2024 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_shared_boringssl.h" + +#ifdef HAVE_LIBBROTLI +# include +# include +#endif // defined(HAVE_LIBBROTLI) + +namespace ngtcp2 { + +namespace tls { + +#ifdef HAVE_LIBBROTLI +int cert_compress(SSL *ssl, CBB *out, const uint8_t *in, size_t in_len) { + uint8_t *dest; + + auto compressed_size = BrotliEncoderMaxCompressedSize(in_len); + if (compressed_size == 0) { + return 0; + } + + if (!CBB_reserve(out, &dest, compressed_size)) { + return 0; + } + + if (BrotliEncoderCompress(BROTLI_MAX_QUALITY, BROTLI_DEFAULT_WINDOW, + BROTLI_MODE_GENERIC, in_len, in, &compressed_size, + dest) != BROTLI_TRUE) { + return 0; + } + + if (!CBB_did_write(out, compressed_size)) { + return 0; + } + + return 1; +} + +int cert_decompress(SSL *ssl, CRYPTO_BUFFER **out, size_t uncompressed_len, + const uint8_t *in, size_t in_len) { + uint8_t *dest; + auto buf = CRYPTO_BUFFER_alloc(&dest, uncompressed_len); + auto len = uncompressed_len; + + if (BrotliDecoderDecompress(in_len, in, &len, dest) != + BROTLI_DECODER_RESULT_SUCCESS) { + CRYPTO_BUFFER_free(buf); + + return 0; + } + + if (uncompressed_len != len) { + CRYPTO_BUFFER_free(buf); + + return 0; + } + + *out = buf; + + return 1; +} +#endif // defined(HAVE_LIBBROTLI) + +} // namespace tls + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/tls_shared_boringssl.h b/deps/ngtcp2/ngtcp2/examples/tls_shared_boringssl.h new file mode 100644 index 00000000000000..c08712d1794828 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_shared_boringssl.h @@ -0,0 +1,51 @@ +/* + * ngtcp2 + * + * Copyright (c) 2024 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SHARED_BORINGSSL_H +#define TLS_SHARED_BORINGSSL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +namespace ngtcp2 { + +namespace tls { + +inline constexpr uint16_t CERTIFICATE_COMPRESSION_ALGO_BROTLI = 2; + +#ifdef HAVE_LIBBROTLI +int cert_compress(SSL *ssl, CBB *out, const uint8_t *in, size_t in_len); + +int cert_decompress(SSL *ssl, CRYPTO_BUFFER **out, size_t uncompressed_len, + const uint8_t *in, size_t in_len); +#endif // defined(HAVE_LIBBROTLI) + +} // namespace tls + +} // namespace ngtcp2 + +#endif // !defined(TLS_SHARED_BORINGSSL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/tls_shared_picotls.cc b/deps/ngtcp2/ngtcp2/examples/tls_shared_picotls.cc new file mode 100644 index 00000000000000..fa379dc8558659 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_shared_picotls.cc @@ -0,0 +1,59 @@ +/* + * ngtcp2 + * + * Copyright (c) 2023 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_shared_picotls.h" + +#include +#include +#include + +extern std::ofstream keylog_file; + +namespace { +void log_event_cb(ptls_log_event_t *self, ptls_t *ptls, const char *type, + const char *fmt, ...) { + char buf[128]; + va_list ap; + + va_start(ap, fmt); + auto len = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + if (len < 0 || static_cast(len) >= sizeof(buf)) { + return; + } + + char randhex[PTLS_HELLO_RANDOM_SIZE * 2 + 1]; + + ptls_hexdump(randhex, ptls_get_client_random(ptls).base, + PTLS_HELLO_RANDOM_SIZE); + + keylog_file << type << ' ' << randhex << ' '; + keylog_file.write(buf, len); + keylog_file << '\n'; + keylog_file.flush(); +} +} // namespace + +ptls_log_event_t log_event = {log_event_cb}; diff --git a/deps/ngtcp2/ngtcp2/examples/tls_shared_picotls.h b/deps/ngtcp2/ngtcp2/examples/tls_shared_picotls.h new file mode 100644 index 00000000000000..023d771d054246 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/tls_shared_picotls.h @@ -0,0 +1,36 @@ +/* + * ngtcp2 + * + * Copyright (c) 2023 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SHARED_PICOTLS_H +#define TLS_SHARED_PICOTLS_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +extern ptls_log_event_t log_event; + +#endif // !defined(TLS_SHARED_PICOTLS_H) diff --git a/deps/ngtcp2/ngtcp2/examples/util.cc b/deps/ngtcp2/ngtcp2/examples/util.cc new file mode 100644 index 00000000000000..8f6d7c7668f074 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/util.cc @@ -0,0 +1,922 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * Copyright (c) 2012 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "util.h" + +#ifdef HAVE_ARPA_INET_H +# include +#endif // defined(HAVE_ARPA_INET_H) +#ifdef HAVE_NETINET_IN_H +# include +#endif // defined(HAVE_NETINET_IN_H) +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "template.h" + +using namespace std::literals; + +namespace ngtcp2 { + +namespace util { + +std::expected +read_hpke_private_key_pem(const std::filesystem::path &path); + +std::expected, Error> +read_pem(const std::filesystem::path &path, std::string_view name, + std::string_view type); + +std::expected write_pem(const std::filesystem::path &path, + std::string_view name, + std::string_view type, + std::span data); + +std::string decode_hex(std::string_view s) { + assert(s.size() % 2 == 0); + std::string res(s.size() / 2, '0'); + auto p = std::ranges::begin(res); + for (auto it = std::ranges::begin(s); it != std::ranges::end(s); it += 2) { + *p++ = static_cast((hex_to_uint(*it) << 4) | hex_to_uint(*(it + 1))); + } + return res; +} + +namespace { +// format_fraction2 formats |n| as fraction part of integer. |n| is +// considered as fraction, and its precision is 3 digits. The last +// digit is ignored. The precision of the resulting fraction is 2 +// digits. +std::string format_fraction2(uint32_t n) { + n /= 10; + + if (n < 10) { + return {'.', '0', static_cast('0' + n)}; + } + return {'.', static_cast('0' + n / 10), + static_cast('0' + (n % 10))}; +} +} // namespace + +namespace { +// round2even rounds the last digit of |n| so that the n / 10 becomes +// even. +uint64_t round2even(uint64_t n) { + if (n % 10 == 5) { + if ((n / 10) & 1) { + n += 10; + } + } else { + n += 5; + } + return n; +} +} // namespace + +std::string format_durationf(uint64_t ns) { + static constexpr std::string_view units[] = {"us"sv, "ms"sv, "s"sv}; + if (ns < 1000) { + return format_uint(ns) + "ns"; + } + auto unit = 0; + if (ns < 1000000) { + // do nothing + } else if (ns < 1000000000) { + ns /= 1000; + unit = 1; + } else { + ns /= 1000000; + unit = 2; + } + + ns = round2even(ns); + + if (ns / 1000 >= 1000 && unit < 2) { + ns /= 1000; + ++unit; + } + + auto res = format_uint(ns / 1000); + res += format_fraction2(static_cast(ns % 1000)); + res += units[unit]; + + return res; +} + +std::mt19937 make_mt19937() { + std::random_device rd; + return std::mt19937(rd()); +} + +ngtcp2_tstamp timestamp() { + return static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); +} + +ngtcp2_tstamp system_clock_now() { + return static_cast( + std::chrono::floor( + std::chrono::system_clock::now().time_since_epoch()) + .count()); +} + +bool numeric_host(const char *hostname) { + return numeric_host(hostname, AF_INET) || numeric_host(hostname, AF_INET6); +} + +bool numeric_host(const char *hostname, int family) { + int rv; + std::array dst; + + rv = inet_pton(family, hostname, dst.data()); + + return rv == 1; +} + +namespace { +uint8_t *hexdump_addr(uint8_t *dest, size_t addr) { + // Lower 32 bits are displayed. + return format_hex(static_cast(addr), dest); +} +} // namespace + +namespace { +uint8_t *hexdump_ascii(uint8_t *dest, std::span data) { + *dest++ = '|'; + + for (auto c : data) { + if (0x20 <= c && c <= 0x7E) { + *dest++ = c; + } else { + *dest++ = '.'; + } + } + + *dest++ = '|'; + + return dest; +} +} // namespace + +namespace { +uint8_t *hexdump8(uint8_t *dest, std::span data) { + for (auto c : data) { + dest = format_hex(c, dest); + *dest++ = ' '; + } + + for (auto i = data.size(); i < 8; ++i) { + *dest++ = ' '; + *dest++ = ' '; + *dest++ = ' '; + } + + return dest; +} +} // namespace + +namespace { +uint8_t *hexdump16(uint8_t *dest, std::span data) { + if (data.size() > 8) { + dest = hexdump8(dest, {data.data(), 8}); + *dest++ = ' '; + dest = hexdump8(dest, data.subspan(8)); + *dest++ = ' '; + } else { + dest = hexdump8(dest, data); + *dest++ = ' '; + dest = hexdump8(dest, {}); + *dest++ = ' '; + } + + return dest; +} +} // namespace + +namespace { +uint8_t *hexdump_line(uint8_t *dest, std::span data, + size_t addr) { + dest = hexdump_addr(dest, addr); + *dest++ = ' '; + *dest++ = ' '; + + dest = hexdump16(dest, data); + + return hexdump_ascii(dest, data); +} +} // namespace + +namespace { +std::expected hexdump_write(int fd, + std::span data) { + ssize_t nwrite; + + for (; + (nwrite = write(fd, data.data(), data.size())) == -1 && errno == EINTR;) + ; + if (nwrite == -1) { + return std::unexpected{Error::IO}; + } + + return {}; +} +} // namespace + +std::expected hexdump(FILE *out, std::span data) { + if (data.empty()) { + return {}; + } + + // min_space is the additional minimum space that the buffer must + // accept, which is the size of a single full line output + one + // repeat line marker ("*\n"). If the remaining buffer size is less + // than that, flush the buffer and reset. + constexpr auto min_space = 79UZ + 2UZ; + + auto fd = fileno(out); + std::array buf; + auto input = data; + auto last = buf.data(); + auto repeated = false; + std::span s, last_s{}; + + for (; !input.empty(); input = input.subspan(s.size())) { + s = input; + + if (s.size() >= 16) { + s = s.first(16); + + if (std::ranges::equal(last_s, s)) { + if (repeated) { + continue; + } + + repeated = true; + + *last++ = '*'; + *last++ = '\n'; + + continue; + } + + repeated = false; + } + + last = hexdump_line(last, s, as_unsigned(s.data() - data.data())); + *last++ = '\n'; + last_s = s; + + auto len = static_cast(last - buf.data()); + if (len + min_space > buf.size()) { + if (auto rv = hexdump_write(fd, {buf.data(), len}); !rv) { + return rv; + } + + last = buf.data(); + } + } + + last = hexdump_addr(last, data.size()); + *last++ = '\n'; + + auto len = static_cast(last - buf.data()); + if (len) { + return hexdump_write(fd, {buf.data(), len}); + } + + return {}; +} + +ngtcp2_cid make_cid_key(std::span cid) { + assert(cid.size() <= NGTCP2_MAX_CIDLEN); + + ngtcp2_cid res; + + std::ranges::copy(cid, std::ranges::begin(res.data)); + res.datalen = cid.size(); + + return res; +} + +std::string straddr(const sockaddr *sa, socklen_t salen) { + std::array host; + std::array port; + + auto rv = getnameinfo(sa, salen, host.data(), host.size(), port.data(), + port.size(), NI_NUMERICHOST | NI_NUMERICSERV); + if (rv != 0) { + std::println(stderr, "getnameinfo: {}", gai_strerror(rv)); + return ""; + } + std::string res = "["; + res.append(host.data(), strlen(host.data())); + res += "]:"; + res.append(port.data(), strlen(port.data())); + return res; +} + +std::string straddr(const Address &addr) { + return straddr(addr.as_sockaddr(), addr.size()); +} + +bool prohibited_port(uint16_t port) { + switch (port) { + case 1900: + case 5353: + case 11211: + case 20800: + case 27015: + return true; + default: + return port < 1024; + } +} + +std::string_view strccalgo(ngtcp2_cc_algo cc_algo) { + switch (cc_algo) { + case NGTCP2_CC_ALGO_RENO: + return "reno"sv; + case NGTCP2_CC_ALGO_CUBIC: + return "cubic"sv; + case NGTCP2_CC_ALGO_BBR: + return "bbr"sv; + default: + assert(0); + abort(); + } +} + +namespace { +constexpr bool rws(char c) { return c == '\t' || c == ' '; } +} // namespace + +std::expected, Error> +read_mime_types(const std::filesystem::path &filename) { + std::ifstream f(filename); + if (!f) { + return std::unexpected{Error::IO}; + } + + std::unordered_map dest; + + std::string line; + while (std::getline(f, line)) { + if (line.empty() || line[0] == '#') { + continue; + } + + auto p = std::ranges::find_if(line, rws); + if (p == std::ranges::begin(line) || p == std::ranges::end(line)) { + continue; + } + + auto media_type = std::string{std::ranges::begin(line), p}; + for (;;) { + auto ext = std::ranges::find_if_not(p, std::ranges::end(line), rws); + if (ext == std::ranges::end(line)) { + break; + } + + p = std::ranges::find_if(ext, std::ranges::end(line), rws); + auto key = "."s; + key += std::string{ext, p}; + dest.emplace(key, media_type); + } + } + + return dest; +} + +std::string format_duration(ngtcp2_duration n) { + if (n >= 3600 * NGTCP2_SECONDS && (n % (3600 * NGTCP2_SECONDS)) == 0) { + return format_uint(n / (3600 * NGTCP2_SECONDS)) + 'h'; + } + if (n >= 60 * NGTCP2_SECONDS && (n % (60 * NGTCP2_SECONDS)) == 0) { + return format_uint(n / (60 * NGTCP2_SECONDS)) + 'm'; + } + if (n >= NGTCP2_SECONDS && (n % NGTCP2_SECONDS) == 0) { + return format_uint(n / NGTCP2_SECONDS) + 's'; + } + if (n >= NGTCP2_MILLISECONDS && (n % NGTCP2_MILLISECONDS) == 0) { + return format_uint(n / NGTCP2_MILLISECONDS) + "ms"; + } + if (n >= NGTCP2_MICROSECONDS && (n % NGTCP2_MICROSECONDS) == 0) { + return format_uint(n / NGTCP2_MICROSECONDS) + "us"; + } + return format_uint(n) + "ns"; +} + +namespace { +std::expected, Error> +parse_uint_internal(std::string_view s) { + uint64_t res = 0; + + if (s.empty()) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + for (size_t i = 0; i < s.size(); ++i) { + auto c = s[i]; + if (!is_digit(c)) { + if (i == 0) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + return {{res, i}}; + } + + auto d = static_cast(c - '0'); + if (res > (std::numeric_limits::max() - d) / 10) { + return std::unexpected{Error::INTEGER_OVERFLOW}; + } + + res *= 10; + res += d; + } + + return {{res, s.size()}}; +} +} // namespace + +std::expected parse_uint(std::string_view s) { + auto o = parse_uint_internal(s); + if (!o) { + return std::unexpected{o.error()}; + } + auto [res, idx] = *o; + if (idx != s.size()) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + return res; +} + +std::expected parse_uint_iec(std::string_view s) { + auto o = parse_uint_internal(s); + if (!o) { + return std::unexpected{o.error()}; + } + auto [res, idx] = *o; + if (idx == s.size()) { + return res; + } + if (idx + 1 != s.size()) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + uint64_t m; + switch (s[idx]) { + case 'G': + case 'g': + m = 1 << 30; + break; + case 'M': + case 'm': + m = 1 << 20; + break; + case 'K': + case 'k': + m = 1 << 10; + break; + default: + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + if (res > std::numeric_limits::max() / m) { + return std::unexpected{Error::INTEGER_OVERFLOW}; + } + + return res * m; +} + +std::expected parse_duration(std::string_view s) { + auto o = parse_uint_internal(s); + if (!o) { + return std::unexpected{o.error()}; + } + auto [res, idx] = *o; + if (idx == s.size()) { + return res * NGTCP2_SECONDS; + } + + uint64_t m; + if (idx + 1 == s.size()) { + switch (s[idx]) { + case 'H': + case 'h': + m = 3600 * NGTCP2_SECONDS; + break; + case 'M': + case 'm': + m = 60 * NGTCP2_SECONDS; + break; + case 'S': + case 's': + m = NGTCP2_SECONDS; + break; + default: + return std::unexpected{Error::INVALID_ARGUMENT}; + } + } else if (idx + 2 == s.size() && (s[idx + 1] == 's' || s[idx + 1] == 'S')) { + switch (s[idx]) { + case 'M': + case 'm': + m = NGTCP2_MILLISECONDS; + break; + case 'U': + case 'u': + m = NGTCP2_MICROSECONDS; + break; + case 'N': + case 'n': + return res; + default: + return std::unexpected{Error::INVALID_ARGUMENT}; + } + } else { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + if (res > std::numeric_limits::max() / m) { + return std::unexpected{Error::INTEGER_OVERFLOW}; + } + + return res * m; +} + +namespace { +template InputIt eat_file(InputIt first, InputIt last) { + if (first == last) { + *first++ = '/'; + return first; + } + + if (*(last - 1) == '/') { + return last; + } + + auto p = last; + for (; p != first && *(p - 1) != '/'; --p) + ; + if (p == first) { + // this should not happened in normal case, where we expect path + // starts with '/' + *first++ = '/'; + return first; + } + + return p; +} +} // namespace + +namespace { +template InputIt eat_dir(InputIt first, InputIt last) { + auto p = eat_file(first, last); + + --p; + + assert(*p == '/'); + + return eat_file(first, p); +} +} // namespace + +std::expected normalize_path(std::string_view path) { + constexpr auto max_path = 1024UZ; + + if (path.size() > max_path) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + assert(path.size() > 0); + assert(path[0] == '/'); + + std::array res; + auto p = res.data(); + + auto first = std::ranges::begin(path); + auto last = std::ranges::end(path); + + *p++ = '/'; + ++first; + for (; first != last && *first == '/'; ++first) + ; + + for (; first != last;) { + if (*first == '.') { + if (first + 1 == last) { + break; + } + if (*(first + 1) == '/') { + first += 2; + continue; + } + if (*(first + 1) == '.') { + if (first + 2 == last) { + p = eat_dir(res.data(), p); + break; + } + if (*(first + 2) == '/') { + p = eat_dir(res.data(), p); + first += 3; + continue; + } + } + } + if (*(p - 1) != '/') { + p = eat_file(res.data(), p); + } + auto slash = std::ranges::find(first, last, '/'); + if (slash == last) { + p = std::ranges::copy(first, last, p).out; + break; + } + p = std::ranges::copy(first, slash + 1, p).out; + first = slash + 1; + for (; first != last && *first == '/'; ++first) + ; + } + return std::string{res.data(), p}; +} + +std::expected make_socket_nonblocking(int fd) { + int rv; + int flags; + + while ((flags = fcntl(fd, F_GETFL, 0)) == -1 && errno == EINTR) + ; + if (flags == -1) { + return std::unexpected{Error::SYSCALL}; + } + + while ((rv = fcntl(fd, F_SETFL, flags | O_NONBLOCK)) == -1 && errno == EINTR) + ; + + if (rv == -1) { + return std::unexpected{Error::SYSCALL}; + } + + return {}; +} + +std::expected create_nonblock_socket(int domain, int type, + int protocol) { +#ifdef SOCK_NONBLOCK + auto fd = socket(domain, type | SOCK_NONBLOCK, protocol); + if (fd == -1) { + return std::unexpected{Error::SYSCALL}; + } +#else // !defined(SOCK_NONBLOCK) + auto fd = socket(domain, type, protocol); + if (fd == -1) { + return std::unexpected{Error::SYSCALL}; + } + + make_socket_nonblocking(fd); +#endif // !defined(SOCK_NONBLOCK) + + return fd; +} + +std::expected parse_version(std::string_view s) { + if (!util::istarts_with(s, "0x"sv)) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + auto k = s.substr(2); + auto k_last = k.data() + k.size(); + uint32_t v; + auto rv = std::from_chars(k.data(), k_last, v, 16); + if (rv.ptr != k_last || rv.ec != std::errc{}) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + return v; +} + +std::expected, Error> +read_token(const std::filesystem::path &path) { + return read_pem(path, "token"sv, "QUIC TOKEN"sv); +} + +std::expected write_token(const std::filesystem::path &path, + std::span token) { + return write_pem(path, "token"sv, "QUIC TOKEN"sv, token); +} + +std::expected, Error> +read_transport_params(const std::filesystem::path &path) { + return read_pem(path, "transport parameters"sv, + "QUIC TRANSPORT PARAMETERS"sv); +} + +std::expected +write_transport_params(const std::filesystem::path &path, + std::span data) { + return write_pem(path, "transport parameters"sv, + "QUIC TRANSPORT PARAMETERS"sv, data); +} + +std::string percent_decode(std::string_view s) { + std::string result; + + result.resize_and_overwrite(s.size(), [s](auto p, auto len) { + auto head = p; + + for (auto first = std::ranges::begin(s), last = std::ranges::end(s); + first != last; ++first) { + if (*first != '%') { + *p++ = *first; + continue; + } + + if (first + 1 != last && first + 2 != last && + is_hex_digit(*(first + 1)) && is_hex_digit(*(first + 2))) { + *p++ = static_cast((hex_to_uint(*(first + 1)) << 4) + + hex_to_uint(*(first + 2))); + first += 2; + continue; + } + + *p++ = *first; + } + + return p - head; + }); + + return result; +} + +std::expected, Error> +read_file(const std::filesystem::path &path) { + auto fd = open(path.c_str(), O_RDONLY); + if (fd == -1) { + return std::unexpected{Error::IO}; + } + + auto fd_d = defer([fd] { close(fd); }); + + auto size = lseek(fd, 0, SEEK_END); + if (size == static_cast(-1)) { + return std::unexpected{Error::IO}; + } + + auto addr = + mmap(nullptr, static_cast(size), PROT_READ, MAP_SHARED, fd, 0); + if (addr == MAP_FAILED) { + return std::unexpected{Error::IO}; + } + + auto addr_d = + defer([addr, size] { munmap(addr, static_cast(size)); }); + + auto p = static_cast(addr); + + return {{p, p + size}}; +} + +size_t clamp_buffer_size(ngtcp2_conn *conn, size_t buflen, size_t gso_burst) { + return std::min(gso_burst == 0 + ? ngtcp2_conn_get_send_quantum2(conn) + : ngtcp2_conn_get_path_max_tx_udp_payload_size2(conn) * + gso_burst, + buflen); +} + +bool recv_pkt_time_threshold_exceeded(bool time_sensitive, ngtcp2_tstamp start, + size_t pktcnt) { + return time_sensitive && pktcnt && + util::timestamp() - start >= NGTCP2_MILLISECONDS; +} + +std::expected +read_ech_server_config(const std::filesystem::path &path) { + auto pkey = read_hpke_private_key_pem(path); + if (!pkey) { + return std::unexpected{pkey.error()}; + } + + auto maybe_ech_config_list = read_pem(path, "ECH config"sv, "ECHCONFIG"sv); + if (!maybe_ech_config_list) { + return std::unexpected{maybe_ech_config_list.error()}; + } + + auto ech_config_list = std::span{*maybe_ech_config_list}; + if (ech_config_list.size() < 2) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + auto data = ech_config_list.subspan(2); + + if (auto len = + static_cast((ech_config_list[0] << 8) + ech_config_list[1]); + len != data.size()) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + std::vector> ech_configs; + + for (; !data.empty();) { + // version and length, each 2 bytes + if (data.size() < 4) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + auto version = (data[0] << 8) + data[1]; + + auto conflen = static_cast(4 + (data[2] << 8) + data[3]); + if (data.size() < conflen) { + return std::unexpected{Error::INVALID_ARGUMENT}; + } + + if (version == 0xFE0D) { + auto conf = data.first(conflen); + ech_configs.emplace_back(std::ranges::begin(conf), + std::ranges::end(conf)); + } else { + std::println(stderr, "Skipping the unsupported ECH version {:#x}", + version); + } + + data = data.subspan(conflen); + } + + return ECHServerConfig{ + .private_key = std::move(*pkey), + .ech_config_list = std::move(ech_configs), + }; +} + +std::span generate_siphash_key() { + static auto key = [] { + std::array key; + + if (!generate_secure_random(as_writable_uint8_span(std::span{key}))) { + assert(0); + abort(); + } + + return key; + }(); + + ++key[0]; + + return key; +} + +std::filesystem::path realpath(const std::filesystem::path &path) { + std::error_code ec; + + auto abspath = std::filesystem::canonical(path, ec); + if (ec) { + std::println(stderr, "Could not get canonical path for {}: {}", + path.native(), ec.message()); + abort(); + } + + return abspath; +} + +} // namespace util + +std::ostream &operator<<(std::ostream &os, const ngtcp2_cid &cid) { + return os << "0x" << util::format_hex(cid.data, as_signed(cid.datalen)); +} + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/util.h b/deps/ngtcp2/ngtcp2/examples/util.h new file mode 100644 index 00000000000000..1fc855470dcf78 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/util.h @@ -0,0 +1,640 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * Copyright (c) 2012 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef UTIL_H +#define UTIL_H + +#ifdef HAVE_CONFIG_H +# include +#endif // defined(HAVE_CONFIG_H) + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "network.h" +#include "siphash.h" +#include "template.h" +#include "shared.h" + +namespace ngtcp2 { + +namespace util { + +inline nghttp3_nv make_nv(std::string_view name, std::string_view value, + uint8_t flags) { + return nghttp3_nv{ + reinterpret_cast(const_cast(std::ranges::data(name))), + reinterpret_cast(const_cast(std::ranges::data(value))), + name.size(), + value.size(), + flags, + }; +} + +inline nghttp3_nv make_nv_cc(std::string_view name, std::string_view value) { + return make_nv(name, value, NGHTTP3_NV_FLAG_NONE); +} + +inline nghttp3_nv make_nv_nc(std::string_view name, std::string_view value) { + return make_nv(name, value, NGHTTP3_NV_FLAG_NO_COPY_NAME); +} + +inline nghttp3_nv make_nv_nn(std::string_view name, std::string_view value) { + return make_nv(name, value, + NGHTTP3_NV_FLAG_NO_COPY_NAME | NGHTTP3_NV_FLAG_NO_COPY_VALUE); +} + +inline constexpr char LOWER_XDIGITS[] = "0123456789abcdef"; + +template +requires(std::indirectly_writable) +constexpr O format_hex_uint8(uint8_t b, O result) { +#ifdef __GNUC__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wsign-conversion" +#endif // __GNUC__ + *result++ = LOWER_XDIGITS[b >> 4]; + *result++ = LOWER_XDIGITS[b & 0xF]; +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif // __GNUC__ + + return result; +} + +// format_hex converts a range [|first|, |last|) in hex format, and +// stores the result in another range, beginning at |result|. It +// returns an output iterator to the element past the last element +// stored. +template +requires(std::indirectly_writable && + sizeof(std::iter_value_t) == sizeof(uint8_t)) +constexpr O format_hex(I first, I last, O result) { + for (; first != last; ++first) { + result = format_hex_uint8(static_cast(*first), result); + } + + return result; +} + +// format_hex converts a range [|first|, |first| + |n|) in hex format, +// and stores the result in another range, beginning at |result|. It +// returns an output iterator to the element past the last element +// stored. +template +requires(std::indirectly_writable && + sizeof(std::iter_value_t) == sizeof(uint8_t)) +constexpr O format_hex(I first, std::iter_difference_t n, O result) { + return format_hex(first, std::ranges::next(first, n), std::move(result)); +} + +// format_hex converts a range [|first|, |first| + |n|) in hex format, +// and returns it. +template +requires(sizeof(std::iter_value_t) == sizeof(uint8_t)) +constexpr std::string format_hex(I first, std::iter_difference_t n) { + if (n <= 0) { + return {}; + } + + std::string res; + + res.resize_and_overwrite(as_unsigned(n * 2), [first = std::move(first), + n](auto p, auto len) mutable { + format_hex(std::move(first), n, p); + + return len; + }); + + return res; +} + +// format_hex converts |R| in hex format, and stores the result in +// another range, beginning at |result|. It returns an output +// iterator to the element past the last element stored. +template +requires(std::indirectly_writable && + !std::is_array_v> && + sizeof(std::ranges::range_value_t) == sizeof(uint8_t)) +constexpr O format_hex(R &&r, O result) { + return format_hex(std::ranges::begin(r), std::ranges::end(r), + std::move(result)); +} + +// format_hex converts |R| in hex format, and returns the result. +template +requires(!std::is_array_v> && + sizeof(std::ranges::range_value_t) == sizeof(uint8_t)) +constexpr std::string format_hex(R &&r) { + std::string res; + + res.resize_and_overwrite(as_unsigned(std::ranges::distance(r) * 2), + [&r](auto p, auto len) { + format_hex(r, p); + + return len; + }); + + return res; +} + +// format_hex converts |n| in hex format, and stores the result in +// another range, beginning at |result|. It returns an output +// iterator to the element past the last element stored. +template +requires(std::indirectly_writable) +constexpr O format_hex(T n, O result) { + if constexpr (sizeof(n) == 1) { + return format_hex_uint8(n, result); + } + + if constexpr (std::endian::native == std::endian::little) { + auto end = reinterpret_cast(&n); + auto p = end + sizeof(n); + + for (; p != end; --p) { + result = format_hex_uint8(*(p - 1), result); + } + } else { + auto p = reinterpret_cast(&n); + auto end = p + sizeof(n); + + for (; p != end; ++p) { + result = format_hex_uint8(*p, result); + } + } + + return result; +} + +// format_hex converts |n| in hex format, and returns it. +template constexpr std::string format_hex(T n) { + std::string res; + + res.resize_and_overwrite(sizeof(n) * 2, [n](auto p, auto len) { + format_hex(n, p); + + return len; + }); + + return res; +} + +std::string decode_hex(std::string_view s); + +// format_durationf formats |ns| in human readable manner. |ns| must +// be nanoseconds resolution. This function uses the largest unit so +// that the integral part is strictly more than zero, and the +// precision is at most 2 digits. For example, 1234 is formatted as +// "1.23us". The largest unit is seconds. +std::string format_durationf(uint64_t ns); + +std::mt19937 make_mt19937(); + +ngtcp2_tstamp timestamp(); + +// system_clock_now returns the current timestamp of system clock. +ngtcp2_tstamp system_clock_now(); + +bool numeric_host(const char *hostname); + +bool numeric_host(const char *hostname, int family); + +// hexdump dumps |data| in the format similar to hexdump(1) with -C +// option. +std::expected hexdump(FILE *out, std::span data); + +template +std::expected hexdump(FILE *out, std::span data) { + return hexdump(out, as_uint8_span(data)); +} + +inline constexpr auto lowcase_tbl = [] { + std::array tbl; + + for (size_t i = 0; i < 256; ++i) { + if ('A' <= i && i <= 'Z') { + tbl[i] = static_cast(i - 'A' + 'a'); + } else { + tbl[i] = static_cast(i); + } + } + + return tbl; +}(); + +constexpr char lowcase(char c) noexcept { + return lowcase_tbl[static_cast(c)]; +} + +struct CaseCmp { + constexpr bool operator()(char lhs, char rhs) const noexcept { + return lowcase(lhs) == lowcase(rhs); + } +}; + +// istarts_with returns true if |s| starts with |prefix|. Comparison +// is performed in case-insensitive manner. +constexpr bool istarts_with(std::string_view s, std::string_view prefix) { + return s.size() >= prefix.size() && + std::ranges::equal(s.substr(0, prefix.size()), prefix, CaseCmp()); +} + +// make_cid_key returns the key for |cid|. +std::string_view make_cid_key(const ngtcp2_cid *cid); +ngtcp2_cid make_cid_key(std::span cid); + +// straddr stringifies |sa| of length |salen| in a format "[IP]:PORT". +std::string straddr(const sockaddr *sa, socklen_t salen); + +// straddr stringifies |addr| in a format "[IP]:PORT". +std::string straddr(const Address &addr); + +// prohibited_port returns true if |port| is prohibited as a client +// port. +bool prohibited_port(uint16_t port); + +// strccalgo stringifies |cc_algo|. +std::string_view strccalgo(ngtcp2_cc_algo cc_algo); + +// read_mime_types reads "MIME media types and the extensions" file +// denoted by |filename| and returns the mapping of extension to MIME +// media type. The key contains the leading "." (e.g., .txt). +std::expected, Error> +read_mime_types(const std::filesystem::path &filename); + +inline constexpr auto count_digit_tbl = [] { + std::array::digits10> tbl; + + uint64_t x = 1; + + for (size_t i = 0; i < tbl.size(); ++i) { + x *= 10; + tbl[i] = x - 1; + } + + return tbl; +}(); + +// count_digit returns the minimum number of digits to represent |x| +// in base 10. +// +// credit: +// https://lemire.me/blog/2025/01/07/counting-the-digits-of-64-bit-integers/ +template constexpr size_t count_digit(T x) { + auto y = static_cast(19 * (std::numeric_limits::digits - 1 - + std::countl_zero(static_cast(x | 1))) >> + 6); + + y += x > count_digit_tbl[y]; + + return y + 1; +} + +inline constexpr auto utos_digits = [] { + std::array a; + + for (size_t i = 0; i < 100; ++i) { + a[i * 2] = '0' + static_cast(i / 10); + a[i * 2 + 1] = '0' + static_cast(i % 10); + } + + return a; +}(); + +struct UIntFormatter { + template + requires(std::indirectly_writable) + constexpr O operator()(T n, O result) { + using result_type = std::iter_value_t; + + if (n < 10) { + *result++ = static_cast('0' + static_cast(n)); + return result; + } + + if (n < 100) { + return std::ranges::copy_n(utos_digits.data() + n * 2, 2, result).out; + } + + std::ranges::advance(result, as_signed(count_digit(n))); + + auto p = result; + + for (; n >= 100; n /= 100) { + std::ranges::advance(p, -2); + std::ranges::copy_n(utos_digits.data() + (n % 100) * 2, 2, p); + } + + if (n < 10) { + *--p = static_cast('0' + static_cast(n)); + return result; + } + + std::ranges::advance(p, -2); + std::ranges::copy_n(utos_digits.data() + n * 2, 2, p); + + return result; + } +}; + +template +requires(std::indirectly_writable) +constexpr O utos(T n, O result) { + return UIntFormatter{}(std::move(n), std::move(result)); +} + +// format_uint converts |n| into string. +template constexpr std::string format_uint(T n) { + using namespace std::literals; + + if (n == 0) { + return "0"s; + } + + std::string res; + + res.resize_and_overwrite(count_digit(n), [n](auto p, auto len) { + utos(n, p); + + return len; + }); + + return res; +} + +// format_uint_iec converts |n| into string with the IEC unit (either +// "G", "M", or "K"). It chooses the largest unit which does not drop +// precision. +template std::string format_uint_iec(T n) { + if (n >= (1 << 30) && (n & ((1 << 30) - 1)) == 0) { + return format_uint(n / (1 << 30)) + 'G'; + } + if (n >= (1 << 20) && (n & ((1 << 20) - 1)) == 0) { + return format_uint(n / (1 << 20)) + 'M'; + } + if (n >= (1 << 10) && (n & ((1 << 10) - 1)) == 0) { + return format_uint(n / (1 << 10)) + 'K'; + } + return format_uint(n); +} + +// format_duration converts |n| into string with the unit in either +// "h" (hours), "m" (minutes), "s" (seconds), "ms" (milliseconds), +// "us" (microseconds) or "ns" (nanoseconds). It chooses the largest +// unit which does not drop precision. |n| is in nanosecond +// resolution. +std::string format_duration(ngtcp2_duration n); + +// parse_uint parses |s| as 64-bit unsigned integer. If it cannot +// parse |s|, the return value does not contain a value. +std::expected parse_uint(std::string_view s); + +// parse_uint_iec parses |s| as 64-bit unsigned integer. It accepts +// IEC unit letter (either "G", "M", or "K") in |s|. If it cannot +// parse |s|, the return value does not contain a value. +std::expected parse_uint_iec(std::string_view s); + +// parse_duration parses |s| as 64-bit unsigned integer. It accepts a +// unit (either "h", "m", "s", "ms", "us", or "ns") in |s|. If no +// unit is present, the unit "s" is assumed. If it cannot parse |s|, +// the return value does not contain a value. +std::expected parse_duration(std::string_view s); + +// generate_secure_random generates a cryptographically secure pseudo +// random data of |data|. +std::expected generate_secure_random(std::span data); + +// normalize_path removes ".." by consuming a previous path component. +// It also removes ".". It assumes that |path| starts with "/". If +// it cannot consume a previous path component, it just removes "..". +std::expected normalize_path(std::string_view path); + +template Pred> +consteval auto pred_tbl_gen256(Pred pred) { + std::array tbl; + + for (size_t i = 0; i < tbl.size(); ++i) { + tbl[i] = pred(i); + } + + return tbl; +} + +consteval auto digit_pred(size_t i) noexcept { return '0' <= i && i <= '9'; } + +inline constexpr auto is_digit_tbl = pred_tbl_gen256(digit_pred); + +constexpr bool is_digit(char c) noexcept { + return is_digit_tbl[static_cast(c)]; +} + +consteval auto hex_digit_pred(size_t i) noexcept { + return digit_pred(i) || ('A' <= i && i <= 'F') || ('a' <= i && i <= 'f'); +} + +inline constexpr auto is_hex_digit_tbl = pred_tbl_gen256(hex_digit_pred); + +constexpr bool is_hex_digit(char c) noexcept { + return is_hex_digit_tbl[static_cast(c)]; +} + +// is_hex_string returns true if the length of |s| is even, and |s| +// does not contain a character other than [0-9A-Fa-f]. It returns +// false otherwise. +template +requires(!std::is_array_v>) +constexpr bool is_hex_string(R &&r) { + return !(std::ranges::size(r) & 1) && std::ranges::all_of(r, is_hex_digit); +} + +inline constexpr auto hex_to_uint_tbl = [] { + std::array tbl; + + std::ranges::fill(tbl, 256); + + for (char i = '0'; i <= '9'; ++i) { + tbl[static_cast(i)] = static_cast(i - '0'); + } + + for (char i = 'A'; i <= 'F'; ++i) { + tbl[static_cast(i)] = static_cast(i - 'A' + 10); + } + + for (char i = 'a'; i <= 'f'; ++i) { + tbl[static_cast(i)] = static_cast(i - 'a' + 10); + } + + return tbl; +}(); + +// hex_to_uint returns integer corresponding to hex notation |c|. If +// is_hex_digit(c) is false, it returns 256. +constexpr uint32_t hex_to_uint(char c) noexcept { + return hex_to_uint_tbl[static_cast(c)]; +} + +std::string percent_decode(std::string_view s); + +std::expected make_socket_nonblocking(int fd); + +std::expected create_nonblock_socket(int domain, int type, + int protocol); + +std::expected, Error> +read_token(const std::filesystem::path &path); +std::expected write_token(const std::filesystem::path &path, + std::span token); + +std::expected, Error> +read_transport_params(const std::filesystem::path &path); +std::expected +write_transport_params(const std::filesystem::path &path, + std::span data); + +const char *crypto_default_ciphers(); + +const char *crypto_default_groups(); + +// split_str parses delimited strings in |s| and returns substrings +// delimited by |delim|. The any white spaces around substring are +// treated as a part of substring. If |s| is an empty string, this +// function returns an empty view. +inline auto split_str(std::string_view s, char delim = ',') { + return s | std::ranges::views::split(delim) | + std::ranges::views::transform( + [](auto &&r) { return std::string_view{r}; }); +} + +// parse_version parses |s| to get 4 byte QUIC version. |s| must be a +// hex string and must start with "0x" (e.g., 0x00000001). +std::expected parse_version(std::string_view s); + +// read_file reads a file denoted by |path| and returns its content. +std::expected, Error> +read_file(const std::filesystem::path &path); + +size_t clamp_buffer_size(ngtcp2_conn *conn, size_t buflen, size_t gso_burst); + +bool recv_pkt_time_threshold_exceeded(bool time_sensitive, ngtcp2_tstamp start, + size_t pktcnt); + +enum HPKEPrivateKeyType : uint16_t { + HPKE_DHKEM_X25519_HKDF_SHA256 = 0x0020, +}; + +// HPKEPrivateKey contains HPKE private key. +struct HPKEPrivateKey { + // type is HPKE private key type. + HPKEPrivateKeyType type; + // bytes contains raw private key. + std::vector bytes; +}; + +// ECHServerConfig is a server-side ECH configuration. +struct ECHServerConfig { + // private_key contains a private key used for decrypting encrypted + // Client Hello. + HPKEPrivateKey private_key; + // ech_config_list contains list of a serialized ECHConfig. + std::vector> ech_config_list; +}; + +// read_ech_server_config reads server-side ECH configuration from a +// file denoted by |path|. +std::expected +read_ech_server_config(const std::filesystem::path &path); + +std::span generate_siphash_key(); + +// get_string returns a URL component specified by |f| of |uri|. This +// function assumes that u.field_set & (1 << f) is nonzero. +constexpr std::string_view +get_string(std::string_view uri, const urlparse_url &u, urlparse_url_fields f) { + assert(u.field_set & (1 << f)); + + auto p = &u.field_data[f]; + return {uri.data() + p->off, p->len}; +} + +// realpath returns the canonicalized absolute path to |path|. +std::filesystem::path realpath(const std::filesystem::path &path); + +} // namespace util + +std::ostream &operator<<(std::ostream &os, const ngtcp2_cid &cid); + +} // namespace ngtcp2 + +namespace std { +template <> struct hash { + hash() { + std::ranges::copy(ngtcp2::util::generate_siphash_key(), + std::ranges::begin(key)); + } + + std::size_t operator()(const ngtcp2_cid &cid) const noexcept { + return static_cast(siphash24(key, {cid.data, cid.datalen})); + } + + std::array key; +}; +} // namespace std + +inline bool operator==(const ngtcp2_cid &lhs, const ngtcp2_cid &rhs) { + return ngtcp2_cid_eq(&lhs, &rhs); +} + +template <> +struct std::formatter : public std::formatter { + template + auto format(const ngtcp2_cid &cid, FormatContext &ctx) const { + std::array buf; + buf[0] = '0'; + buf[1] = 'x'; + + auto end = ngtcp2::util::format_hex(std::span{cid.data, cid.datalen}, + std::ranges::begin(buf) + 2); + + return std::formatter::format( + {std::ranges::begin(buf), end}, ctx); + } +}; + +#endif // !defined(UTIL_H) diff --git a/deps/ngtcp2/ngtcp2/examples/util_openssl.cc b/deps/ngtcp2/ngtcp2/examples/util_openssl.cc new file mode 100644 index 00000000000000..f07dd01654e036 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/util_openssl.cc @@ -0,0 +1,178 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "util.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "template.h" + +namespace ngtcp2 { + +namespace util { + +std::expected generate_secure_random(std::span data) { +#ifdef WITH_EXAMPLE_BORINGSSL + using size_type = size_t; +#else // !defined(WITH_EXAMPLE_BORINGSSL) + using size_type = int; +#endif // !defined(WITH_EXAMPLE_BORINGSSL) + + if (RAND_bytes(data.data(), static_cast(data.size())) != 1) { + return std::unexpected{Error::CRYPTO}; + } + + return {}; +} + +std::expected +read_hpke_private_key_pem(const std::filesystem::path &path) { + auto f = BIO_new_file(path.c_str(), "r"); + if (f == nullptr) { + std::println(stderr, "Could not open file {}", path.native()); + return std::unexpected{Error::IO}; + } + + auto f_d = defer([f] { BIO_free(f); }); + + EVP_PKEY *pkey; + + if (PEM_read_bio_PrivateKey(f, &pkey, nullptr, nullptr) == nullptr) { + return std::unexpected{Error::IO}; + } + + auto pkey_d = defer([pkey] { EVP_PKEY_free(pkey); }); + + HPKEPrivateKey res; + + switch (EVP_PKEY_id(pkey)) { + case EVP_PKEY_X25519: { + res.type = HPKE_DHKEM_X25519_HKDF_SHA256; + + size_t len; + + EVP_PKEY_get_raw_private_key(pkey, nullptr, &len); + + res.bytes.resize(len); + + EVP_PKEY_get_raw_private_key(pkey, &res.bytes[0], &len); + + break; + } + default: + return std::unexpected{Error::UNSUPPORTED}; + } + + return res; +} + +std::expected, Error> +read_pem(const std::filesystem::path &path, std::string_view name, + std::string_view type) { + auto f = BIO_new_file(path.c_str(), "r"); + if (f == nullptr) { + std::println(stderr, "Could not open {} file {}", name, path.native()); + return std::unexpected{Error::IO}; + } + + auto f_d = defer([f] { BIO_free(f); }); + + for (;;) { + char *pem_type, *header; + unsigned char *data; + long datalen; + + if (PEM_read_bio(f, &pem_type, &header, &data, &datalen) != 1) { + std::println(stderr, "Could not read {} file {}", name, path.native()); + return std::unexpected{Error::IO}; + } + + auto pem_d = defer([pem_type, header, data] { + OPENSSL_free(pem_type); + OPENSSL_free(header); + OPENSSL_free(data); + }); + + if (type != pem_type) { + continue; + } + + return {{data, data + datalen}}; + } +} + +std::expected write_pem(const std::filesystem::path &path, + std::string_view name, + std::string_view type, + std::span data) { + auto f = BIO_new_file(path.c_str(), "w"); + if (f == nullptr) { + std::println(stderr, "Could not write {} in {}", name, path.native()); + return std::unexpected{Error::IO}; + } + + PEM_write_bio(f, type.data(), "", data.data(), + static_cast(data.size())); + BIO_free(f); + + return {}; +} + +const char *crypto_default_ciphers() { +#if defined(WITH_EXAMPLE_QUICTLS) || defined(WITH_EXAMPLE_OSSL) + return "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_" + "SHA256" +# ifndef LIBRESSL_VERSION_NUMBER + ":TLS_AES_128_CCM_SHA256" +# endif // !defined(LIBRESSL_VERSION_NUMBER) + ; +#else // !(defined(WITH_EXAMPLE_QUICTLS) && defined(WITH_EXAMPLE_OSSL)) + return ""; +#endif // !(defined(WITH_EXAMPLE_QUICTLS) && defined(WITH_EXAMPLE_OSSL)) +} + +const char *crypto_default_groups() { + return "X25519:P-256:P-384:P-521" +#if defined(WITH_EXAMPLE_BORINGSSL) || defined(WITH_EXAMPLE_OSSL) || \ + defined(LIBRESSL_VERSION_NUMBER) + ":X25519MLKEM768" +#endif // defined(WITH_EXAMPLE_BORINGSSL) || defined(WITH_EXAMPLE_OSSL) || + // defined(LIBRESSL_VERSION_NUMBER) + ; +} + +} // namespace util + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/util_test.cc b/deps/ngtcp2/ngtcp2/examples/util_test.cc new file mode 100644 index 00000000000000..46934221d0a7d4 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/util_test.cc @@ -0,0 +1,625 @@ +/* + * ngtcp2 + * + * Copyright (c) 2018 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "util_test.h" + +#include +#include +#include +#include +#include + +#include "util.h" + +using namespace std::literals; + +namespace ngtcp2 { + +namespace { +const MunitTest tests[]{ + munit_void_test(test_util_format_durationf), + munit_void_test(test_util_format_uint), + munit_void_test(test_util_format_uint_iec), + munit_void_test(test_util_format_duration), + munit_void_test(test_util_parse_uint), + munit_void_test(test_util_parse_uint_iec), + munit_void_test(test_util_parse_duration), + munit_void_test(test_util_normalize_path), + munit_void_test(test_util_hexdump), + munit_void_test(test_util_format_hex), + munit_void_test(test_util_decode_hex), + munit_void_test(test_util_is_hex_string), + munit_void_test(test_util_split_str), + munit_test_end(), +}; +} // namespace + +const MunitSuite util_suite{ + .prefix = "/util", + .tests = tests, +}; + +namespace util { +std::expected +read_hpke_private_key_pem(const std::filesystem::path &path) { + return std::unexpected{Error::NOT_IMPLEMENTED}; +} +} // namespace util + +namespace util { +std::expected, Error> +read_pem(const std::filesystem::path &path, std::string_view name, + std::string_view type) { + return std::unexpected{Error::NOT_IMPLEMENTED}; +} +} // namespace util + +namespace util { +std::expected write_pem(const std::filesystem::path &path, + std::string_view name, + std::string_view type, + std::span data) { + return std::unexpected{Error::NOT_IMPLEMENTED}; +} +} // namespace util + +namespace util { +std::expected generate_secure_random(std::span data) { + return std::unexpected{Error::NOT_IMPLEMENTED}; +} +} // namespace util + +void test_util_format_durationf() { + assert_stdstring_equal("0ns", util::format_durationf(0)); + assert_stdstring_equal("999ns", util::format_durationf(999)); + assert_stdstring_equal("1.00us", util::format_durationf(1000)); + assert_stdstring_equal("1.00us", util::format_durationf(1004)); + assert_stdstring_equal("1.00us", util::format_durationf(1005)); + assert_stdstring_equal("1.02us", util::format_durationf(1015)); + assert_stdstring_equal("2.00us", util::format_durationf(1999)); + assert_stdstring_equal("1.00ms", util::format_durationf(999999)); + assert_stdstring_equal("3.50ms", util::format_durationf(3500111)); + assert_stdstring_equal("9999.99s", util::format_durationf(9999990000000ULL)); +} + +void test_util_format_uint() { + assert_stdstring_equal("0"s, util::format_uint(0U)); + assert_stdstring_equal("18446744073709551615"s, + util::format_uint(18446744073709551615ULL)); +} + +void test_util_format_uint_iec() { + assert_stdstring_equal("0"s, util::format_uint_iec(0U)); + assert_stdstring_equal("1023"s, util::format_uint_iec((1U << 10) - 1)); + assert_stdstring_equal("1K"s, util::format_uint_iec(1U << 10)); + assert_stdstring_equal("1M"s, util::format_uint_iec(1U << 20)); + assert_stdstring_equal("1G"s, util::format_uint_iec(1U << 30)); + assert_stdstring_equal( + "18446744073709551615"s, + util::format_uint_iec(std::numeric_limits::max())); + assert_stdstring_equal("1025K"s, + util::format_uint_iec((1U << 20) + (1U << 10))); +} + +void test_util_format_duration() { + assert_stdstring_equal("0ns", util::format_duration(0)); + assert_stdstring_equal("999ns", util::format_duration(999)); + assert_stdstring_equal("1us", util::format_duration(1000)); + assert_stdstring_equal("1ms", util::format_duration(1000000)); + assert_stdstring_equal("1s", util::format_duration(1000000000)); + assert_stdstring_equal("1m", util::format_duration(60000000000ULL)); + assert_stdstring_equal("1h", util::format_duration(3600000000000ULL)); + assert_stdstring_equal( + "18446744073709551615ns", + util::format_duration(std::numeric_limits::max())); + assert_stdstring_equal("61s", util::format_duration(61000000000ULL)); +} + +void test_util_parse_uint() { + { + auto res = util::parse_uint("0"); + assert_true(res.has_value()); + assert_uint64(0, ==, *res); + } + { + auto res = util::parse_uint("1"); + assert_true(res.has_value()); + assert_uint64(1, ==, *res); + } + { + auto res = util::parse_uint("18446744073709551615"); + assert_true(res.has_value()); + assert_uint64(18446744073709551615ULL, ==, *res); + } + { + auto res = util::parse_uint("18446744073709551616"); + assert_false(res.has_value()); + } + { + auto res = util::parse_uint("a"); + assert_false(res.has_value()); + } + { + auto res = util::parse_uint("1a"); + assert_false(res.has_value()); + } + { + auto res = util::parse_uint(""); + assert_false(res.has_value()); + } +} + +void test_util_parse_uint_iec() { + { + auto res = util::parse_uint_iec("0"); + assert_true(res.has_value()); + assert_uint64(0, ==, *res); + } + { + auto res = util::parse_uint_iec("1023"); + assert_true(res.has_value()); + assert_uint64(1023, ==, *res); + } + { + auto res = util::parse_uint_iec("1K"); + assert_true(res.has_value()); + assert_uint64(1 << 10, ==, *res); + } + { + auto res = util::parse_uint_iec("1M"); + assert_true(res.has_value()); + assert_uint64(1 << 20, ==, *res); + } + { + auto res = util::parse_uint_iec("1G"); + assert_true(res.has_value()); + assert_uint64(1 << 30, ==, *res); + } + { + auto res = util::parse_uint_iec("11G"); + assert_true(res.has_value()); + assert_uint64((1ULL << 30) * 11, ==, *res); + } + { + auto res = util::parse_uint_iec("18446744073709551616"); + assert_false(res.has_value()); + } + { + auto res = util::parse_uint_iec("1x"); + assert_false(res.has_value()); + } + { + auto res = util::parse_uint_iec("1Gx"); + assert_false(res.has_value()); + } + { + auto res = util::parse_uint_iec("G"); + assert_false(res.has_value()); + } + { + auto res = util::parse_uint_iec(""); + assert_false(res.has_value()); + } +} + +void test_util_parse_duration() { + { + auto res = util::parse_duration("0"); + assert_true(res.has_value()); + assert_uint64(0, ==, *res); + } + { + auto res = util::parse_duration("1"); + assert_true(res.has_value()); + assert_uint64(NGTCP2_SECONDS, ==, *res); + } + { + auto res = util::parse_duration("0ns"); + assert_true(res.has_value()); + assert_uint64(0, ==, *res); + } + { + auto res = util::parse_duration("1ns"); + assert_true(res.has_value()); + assert_uint64(1, ==, *res); + } + { + auto res = util::parse_duration("1us"); + assert_true(res.has_value()); + assert_uint64(NGTCP2_MICROSECONDS, ==, *res); + } + { + auto res = util::parse_duration("1ms"); + assert_true(res.has_value()); + assert_uint64(NGTCP2_MILLISECONDS, ==, *res); + } + { + auto res = util::parse_duration("1s"); + assert_true(res.has_value()); + assert_uint64(NGTCP2_SECONDS, ==, *res); + } + { + auto res = util::parse_duration("1m"); + assert_true(res.has_value()); + assert_uint64(60 * NGTCP2_SECONDS, ==, *res); + } + { + auto res = util::parse_duration("1h"); + assert_true(res.has_value()); + assert_uint64(3600 * NGTCP2_SECONDS, ==, *res); + } + { + auto res = util::parse_duration("2h"); + assert_true(res.has_value()); + assert_uint64(2 * 3600 * NGTCP2_SECONDS, ==, *res); + } + { + auto res = util::parse_duration("18446744073709551616"); + assert_false(res.has_value()); + } + { + auto res = util::parse_duration("1x"); + assert_false(res.has_value()); + } + { + auto res = util::parse_duration("1mx"); + assert_false(res.has_value()); + } + { + auto res = util::parse_duration("1mxy"); + assert_false(res.has_value()); + } + { + auto res = util::parse_duration("s"); + assert_false(res.has_value()); + } + { + auto res = util::parse_duration("ms"); + assert_false(res.has_value()); + } + { + auto res = util::parse_duration(""); + assert_false(res.has_value()); + } +} + +void test_util_normalize_path() { + { + auto rv = util::normalize_path("/"); + + assert_true(rv.has_value()); + assert_stdstring_equal("/", rv.value()); + } + + { + auto rv = util::normalize_path("//"); + + assert_true(rv.has_value()); + assert_stdstring_equal("/", rv.value()); + } + + { + auto rv = util::normalize_path("/foo"); + + assert_true(rv.has_value()); + assert_stdstring_equal("/foo", rv.value()); + } + + { + auto rv = util::normalize_path("/foo/bar/"); + + assert_true(rv.has_value()); + assert_stdstring_equal("/foo/bar/", rv.value()); + } + + { + auto rv = util::normalize_path("/foo/abc/../bar/"); + + assert_true(rv.has_value()); + assert_stdstring_equal("/foo/bar/", rv.value()); + } + + { + auto rv = util::normalize_path("/../foo/abc/../bar/"); + + assert_true(rv.has_value()); + assert_stdstring_equal("/foo/bar/", rv.value()); + } + + { + auto rv = util::normalize_path("/./foo/././abc///.././bar/./"); + + assert_true(rv.has_value()); + assert_stdstring_equal("/foo/bar/", rv.value()); + } + + { + auto rv = util::normalize_path("/foo/."); + + assert_true(rv.has_value()); + assert_stdstring_equal("/foo/", rv.value()); + } + + { + auto rv = util::normalize_path("/foo/./bar"); + + assert_true(rv.has_value()); + assert_stdstring_equal("/foo/bar", rv.value()); + } + + { + auto rv = util::normalize_path("/foo/./../bar"); + + assert_true(rv.has_value()); + assert_stdstring_equal("/bar", rv.value()); + } + + { + auto rv = util::normalize_path("/../../bar"); + + assert_true(rv.has_value()); + assert_stdstring_equal("/bar", rv.value()); + } + + { + auto rv = util::normalize_path(std::string(1024, '/')); + + assert_true(rv.has_value()); + assert_stdstring_equal("/", rv.value()); + } + + { + auto rv = util::normalize_path(std::string(1025, '/')); + + assert_false(rv); + } + + { + auto rv = util::normalize_path("/.."); + + assert_true(rv.has_value()); + assert_stdstring_equal("/", rv.value()); + } + + { + auto rv = util::normalize_path("/../../index.html"); + + assert_true(rv.has_value()); + assert_stdstring_equal("/index.html", rv.value()); + } +} + +void test_util_hexdump() { + char buf[4096]; + + struct hexdump_testdata { + const char *title; + std::string_view data; + std::string_view dump; + }; + + auto tests = std::to_array({ + { + .title = "Empty data", + .data = ""sv, + .dump = ""sv, + }, + { + .title = "1 byte", + .data = "0"sv, + .dump = "00000000 30 " + "|0|\n" + "00000001\n"sv, + }, + { + .title = "8 bytes", + .data = "01234567"sv, + .dump = "00000000 30 31 32 33 34 35 36 37 " + "|01234567|\n" + "00000008\n"sv, + }, + { + .title = "9 bytes", + .data = "012345678"sv, + .dump = "00000000 30 31 32 33 34 35 36 37 38 " + "|012345678|\n" + "00000009\n"sv, + }, + { + .title = "15 bytes", + .data = "0123456789abcde"sv, + .dump = "00000000 30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 " + "|0123456789abcde|\n" + "0000000f\n"sv, + }, + { + .title = "16 bytes", + .data = "0123456789abcdef"sv, + .dump = "00000000 30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66 " + "|0123456789abcdef|\n" + "00000010\n"sv, + }, + { + .title = "17 bytes", + .data = "0123456789abcdefg"sv, + .dump = "00000000 30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66 " + "|0123456789abcdef|\n" + "00000010 67 " + "|g|\n" + "00000011\n"sv, + }, + { + .title = "Non-printables", + .data = "\0\a\b\t\n\v\f\r\x7F"sv, + .dump = "00000000 00 07 08 09 0a 0b 0c 0d 7f " + "|.........|\n" + "00000009\n"sv, + }, + { + .title = "Multiple lines", + .data = "alpha bravo charlie delta echo foxtrot golf"sv, + .dump = "00000000 61 6c 70 68 61 20 62 72 61 76 6f 20 63 68 61 72 " + "|alpha bravo char|\n" + "00000010 6c 69 65 20 64 65 6c 74 61 20 65 63 68 6f 20 66 " + "|lie delta echo f|\n" + "00000020 6f 78 74 72 6f 74 20 67 6f 6c 66 " + "|oxtrot golf|\n" + "0000002b\n"sv, + }, + { + .title = "Repeated lines", + .data = "000000000000000100000000000000010000000000000001000000000000" + "00020000" + "0000000000020000000000000003"sv, + .dump = "00000000 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 31 " + "|0000000000000001|\n" + "*\n" + "00000030 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 32 " + "|0000000000000002|\n" + "*\n" + "00000050 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 33 " + "|0000000000000003|\n" + "00000060\n"sv, + }, + { + .title = "Ends with the repeated line", + .data = "" + "000000000000000100000000000000010000000000000001000000000000" + "00020000000000000002"sv, + .dump = "00000000 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 31 " + "|0000000000000001|\n" + "*\n" + "00000030 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 32 " + "|0000000000000002|\n" + "*\n" + "00000050\n"sv, + }, + }); + + for (auto &t : tests) { + munit_log(MUNIT_LOG_INFO, t.title); + + auto f = tmpfile(); + auto rv = util::hexdump(f, std::span{t.data}); + + assert_true(rv.has_value()); + + fseek(f, 0, SEEK_SET); + auto nread = fread(buf, 1, sizeof(buf), f); + buf[nread] = '\0'; + + assert_stdsv_equal(t.dump, buf); + + fclose(f); + } +} + +void test_util_format_hex() { + auto a = std::to_array({0xDE, 0xAD, 0xBE, 0xEF}); + + assert_stdstring_equal("deadbeef"s, util::format_hex(a)); + assert_stdstring_equal("deadbeef"s, util::format_hex(0xDEADBEEF)); + assert_stdstring_equal("beef"s, util::format_hex(a.data() + 2, 2)); + + std::array buf; + + assert_stdsv_equal( + "00"sv, (std::string_view{std::ranges::begin(buf), + util::format_hex(static_cast(0U), + std::ranges::begin(buf))})); + assert_stdsv_equal( + "ec"sv, (std::string_view{std::ranges::begin(buf), + util::format_hex(static_cast(0xECU), + std::ranges::begin(buf))})); + assert_stdsv_equal( + "00000000"sv, + (std::string_view{std::ranges::begin(buf), + util::format_hex(0U, std::ranges::begin(buf))})); + assert_stdsv_equal( + "0000ab01"sv, + (std::string_view{std::ranges::begin(buf), + util::format_hex(0xAB01U, std::ranges::begin(buf))})); + assert_stdsv_equal( + "deadbeefbaadf00d"sv, + (std::string_view{ + std::ranges::begin(buf), + util::format_hex(0xDEADBEEFBAADF00DU, std::ranges::begin(buf))})); + assert_stdsv_equal( + "ffffffffffffffff"sv, + (std::string_view{std::ranges::begin(buf), + util::format_hex(std::numeric_limits::max(), + std::ranges::begin(buf))})); + + std::vector char_vec; + util::format_hex(a, std::back_inserter(char_vec)); + + assert_stdsv_equal("deadbeef"sv, + (std::string_view{std::ranges::begin(char_vec), + std::ranges::end(char_vec)})); + + std::vector uint8_vec; + util::format_hex(a, std::back_inserter(uint8_vec)); + + assert_stdsv_equal( + "deadbeef"sv, + (std::string_view{reinterpret_cast(uint8_vec.data()), + uint8_vec.size()})); +} + +void test_util_decode_hex() { + assert_stdstring_equal("\xDE\xAD\xBE\xEF"s, util::decode_hex("deadbeef"sv)); + assert_stdstring_equal(""s, util::decode_hex(""sv)); +} + +void test_util_is_hex_string() { + assert_true(util::is_hex_string(""sv)); + assert_true(util::is_hex_string("0123456789abcdef"sv)); + assert_true(util::is_hex_string("0123456789ABCDEF"sv)); + assert_false(util::is_hex_string("0123456789ABCDEF9"sv)); + assert_false(util::is_hex_string("aaa"sv)); + assert_true(util::is_hex_string("aa"sv)); + assert_false(util::is_hex_string("a"sv)); + assert_false(util::is_hex_string("zzz"sv)); + assert_false(util::is_hex_string("zz"sv)); + assert_false(util::is_hex_string("z"sv)); +} + +void test_util_split_str() { + assert_true((std::vector{"alpha"sv, "bravo"sv, "charlie"sv} == + (util::split_str("alpha,bravo,charlie"sv) | + std::ranges::to()))); + assert_true((std::vector{"alpha"sv, "bravo"sv, "charlie"sv} == + (util::split_str("alpha bravo charlie"sv, ' ') | + std::ranges::to()))); + assert_true((std::vector{} == + (util::split_str(""sv, ' ') | std::ranges::to()))); + assert_true((std::vector{""sv, ""sv} == + (util::split_str(","sv) | std::ranges::to()))); + assert_true( + (std::vector{""sv, "alpha"sv, ""sv, ""sv, "bravo"sv, "charlie"sv, ""sv, + ""sv} == (util::split_str(" alpha bravo charlie "sv, ' ') | + std::ranges::to()))); +} + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/examples/util_test.h b/deps/ngtcp2/ngtcp2/examples/util_test.h new file mode 100644 index 00000000000000..3546bcfc0ce6db --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/util_test.h @@ -0,0 +1,56 @@ +/* + * ngtcp2 + * + * Copyright (c) 2018 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef UTIL_TEST_H +#define UTIL_TEST_H + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif // defined(HAVE_CONFIG_H) + +#define MUNIT_ENABLE_ASSERT_ALIASES + +#include "munitxx.h" + +namespace ngtcp2 { + +extern const MunitSuite util_suite; + +munit_void_test_decl(test_util_format_durationf) +munit_void_test_decl(test_util_format_uint) +munit_void_test_decl(test_util_format_uint_iec) +munit_void_test_decl(test_util_format_duration) +munit_void_test_decl(test_util_parse_uint) +munit_void_test_decl(test_util_parse_uint_iec) +munit_void_test_decl(test_util_parse_duration) +munit_void_test_decl(test_util_normalize_path) +munit_void_test_decl(test_util_hexdump) +munit_void_test_decl(test_util_format_hex) +munit_void_test_decl(test_util_decode_hex) +munit_void_test_decl(test_util_is_hex_string) +munit_void_test_decl(test_util_split_str) + +} // namespace ngtcp2 + +#endif // !defined(UTIL_TEST_H) diff --git a/deps/ngtcp2/ngtcp2/examples/util_wolfssl.cc b/deps/ngtcp2/ngtcp2/examples/util_wolfssl.cc new file mode 100644 index 00000000000000..904067b101edd4 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/examples/util_wolfssl.cc @@ -0,0 +1,129 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "util.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "template.h" + +namespace ngtcp2 { + +namespace util { + +std::expected generate_secure_random(std::span data) { + if (wolfSSL_RAND_bytes(data.data(), static_cast(data.size())) != 1) { + return std::unexpected{Error::CRYPTO}; + } + + return {}; +} + +std::expected +read_hpke_private_key_pem(const std::filesystem::path &path) { + return std::unexpected{Error::NOT_IMPLEMENTED}; +} + +std::expected, Error> +read_pem(const std::filesystem::path &path, std::string_view name, + std::string_view type) { + auto f = wolfSSL_BIO_new_file(path.c_str(), "r"); + if (f == nullptr) { + std::println(stderr, "Could not open {} file {}", name, path.native()); + return std::unexpected{Error::IO}; + } + + auto f_d = defer([f] { wolfSSL_BIO_free(f); }); + + char *pem_type, *header; + unsigned char *data; + long datalen; + + if (wolfSSL_PEM_read_bio(f, &pem_type, &header, &data, &datalen) != 1) { + std::println(stderr, "Could not read {} file {}", name, path.native()); + return std::unexpected{Error::IO}; + } + + auto pem_d = defer([pem_type, header, data] { + wolfSSL_OPENSSL_free(pem_type); + wolfSSL_OPENSSL_free(header); + wolfSSL_OPENSSL_free(data); + }); + + if (type != pem_type) { + std::println(stderr, "{} file {} contains unexpected type", name, + path.native()); + return std::unexpected{Error::IO}; + } + + return {{data, data + datalen}}; +} + +std::expected write_pem(const std::filesystem::path &path, + std::string_view name, + std::string_view type, + std::span data) { + auto f = wolfSSL_BIO_new_file(path.c_str(), "w"); + if (f == nullptr) { + std::println(stderr, "Could not write {} to {}", name, path.native()); + return std::unexpected{Error::IO}; + } + + wolfSSL_PEM_write_bio(f, type.data(), "", data.data(), + static_cast(data.size())); + wolfSSL_BIO_free(f); + + return {}; +} + +const char *crypto_default_ciphers() { + return "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_" + "SHA256:TLS_AES_128_CCM_SHA256"; +} + +const char *crypto_default_groups() { + return "X25519:P-256:P-384:P-521" +#ifdef WOLFSSL_HAVE_MLKEM +# if LIBWOLFSSL_VERSION_HEX < 0x05008004 + ":X25519_ML_KEM_768" +# else // LIBWOLFSSL_VERSION_HEX >= 0x05008004 + ":X25519MLKEM768" +# endif // LIBWOLFSSL_VERSION_HEX >= 0x05008004 +#endif // WOLFSSL_HAVE_MLKEM + ; +} + +} // namespace util + +} // namespace ngtcp2 diff --git a/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/ngtcp2.h b/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/ngtcp2.h index d58a6f1240d4a4..278b30ca07bf18 100644 --- a/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/ngtcp2.h +++ b/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/ngtcp2.h @@ -262,7 +262,7 @@ typedef struct ngtcp2_mem { * * :macro:`NGTCP2_PROTO_VER_V1` is the QUIC version 1. */ -#define NGTCP2_PROTO_VER_V1 ((uint32_t)0x00000001u) +#define NGTCP2_PROTO_VER_V1 ((uint32_t)0x00000001U) /** * @macro @@ -270,21 +270,29 @@ typedef struct ngtcp2_mem { * :macro:`NGTCP2_PROTO_VER_V2` is the QUIC version 2. See * :rfc:`9369`. */ -#define NGTCP2_PROTO_VER_V2 ((uint32_t)0x6b3343cfu) +#define NGTCP2_PROTO_VER_V2 ((uint32_t)0x6B3343CFU) /** * @macro * + * .. warning:: + * + * .. version-deprecated:: 1.1.0 + * * :macro:`NGTCP2_PROTO_VER_MAX` is the highest QUIC version that this - * library supports. Deprecated since v1.1.0. + * library supports. */ #define NGTCP2_PROTO_VER_MAX NGTCP2_PROTO_VER_V1 /** * @macro * + * .. warning:: + * + * .. version-deprecated:: 1.1.0 + * * :macro:`NGTCP2_PROTO_VER_MIN` is the lowest QUIC version that this - * library supports. Deprecated since v1.1.0. + * library supports. */ #define NGTCP2_PROTO_VER_MIN NGTCP2_PROTO_VER_V1 @@ -294,7 +302,7 @@ typedef struct ngtcp2_mem { * :macro:`NGTCP2_RESERVED_VERSION_MASK` is the bit mask of reserved * version. */ -#define NGTCP2_RESERVED_VERSION_MASK 0x0a0a0a0au +#define NGTCP2_RESERVED_VERSION_MASK 0x0A0A0A0AU /** * @macrosection @@ -306,14 +314,30 @@ typedef struct ngtcp2_mem { * @macro * * :macro:`NGTCP2_MAX_UDP_PAYLOAD_SIZE` is the default maximum UDP - * datagram payload size that the local endpoint transmits. + * datagram payload size that the local endpoint transmits without + * Path MTU Discovery (PMTUD) or the custom settings (see + * :member:`ngtcp2_settings.max_tx_udp_payload_size` and + * :member:`ngtcp2_settings.no_tx_udp_payload_size_shaping`). */ #define NGTCP2_MAX_UDP_PAYLOAD_SIZE 1200 /** * @macro * - * :macro:`NGTCP2_MAX_PMTUD_UDP_PAYLOAD_SIZE` is the maximum UDP + * :macro:`NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE` is the maximum UDP datagram + * payload size that this library can output. + */ +#define NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE 65527 + +/** + * @macro + * + * .. warning:: + * + * .. version-deprecated:: 1.17.0 + * Path MTU Discovery is not capped to this value anymore. + * + * :macro:`NGTCP2_MAX_PMTUD_UDP_PAYLOAD_SIZE` was the maximum UDP * datagram payload size that Path MTU Discovery can discover. */ #define NGTCP2_MAX_PMTUD_UDP_PAYLOAD_SIZE 1452 @@ -363,7 +387,7 @@ typedef struct ngtcp2_mem { * integrity tag of Retry packet. It is used for QUIC v1. */ #define NGTCP2_RETRY_KEY_V1 \ - "\xbe\x0c\x69\x0b\x9f\x66\x57\x5a\x1d\x76\x6b\x54\xe3\x68\xc8\x4e" + "\xBE\x0C\x69\x0B\x9F\x66\x57\x5A\x1D\x76\x6B\x54\xE3\x68\xC8\x4E" /** * @macro @@ -371,7 +395,7 @@ typedef struct ngtcp2_mem { * :macro:`NGTCP2_RETRY_NONCE_V1` is nonce used when generating * integrity tag of Retry packet. It is used for QUIC v1. */ -#define NGTCP2_RETRY_NONCE_V1 "\x46\x15\x99\xd3\x5d\x63\x2b\xf2\x23\x98\x25\xbb" +#define NGTCP2_RETRY_NONCE_V1 "\x46\x15\x99\xD3\x5D\x63\x2B\xF2\x23\x98\x25\xBB" /** * @macro @@ -381,7 +405,7 @@ typedef struct ngtcp2_mem { * :rfc:`9369`. */ #define NGTCP2_RETRY_KEY_V2 \ - "\x8f\xb4\xb0\x1b\x56\xac\x48\xe2\x60\xfb\xcb\xce\xad\x7c\xcc\x92" + "\x8F\xB4\xB0\x1B\x56\xAC\x48\xE2\x60\xFB\xCB\xCE\xAD\x7C\xCC\x92" /** * @macro @@ -390,7 +414,7 @@ typedef struct ngtcp2_mem { * integrity tag of Retry packet. It is used for QUIC v2. See * :rfc:`9369`. */ -#define NGTCP2_RETRY_NONCE_V2 "\xd8\x69\x69\xbc\x2d\x7c\x6d\x99\x90\xef\xb0\x4a" +#define NGTCP2_RETRY_NONCE_V2 "\xD8\x69\x69\xBC\x2D\x7C\x6D\x99\x90\xEF\xB0\x4A" /** * @macro @@ -798,7 +822,7 @@ typedef struct NGTCP2_ALIGN(8) ngtcp2_pkt_info { * * :macro:`NGTCP2_PKT_FLAG_NONE` indicates no flag set. */ -#define NGTCP2_PKT_FLAG_NONE 0x00u +#define NGTCP2_PKT_FLAG_NONE 0x00U /** * @macro @@ -806,7 +830,7 @@ typedef struct NGTCP2_ALIGN(8) ngtcp2_pkt_info { * :macro:`NGTCP2_PKT_FLAG_LONG_FORM` indicates the Long header packet * header. */ -#define NGTCP2_PKT_FLAG_LONG_FORM 0x01u +#define NGTCP2_PKT_FLAG_LONG_FORM 0x01U /** * @macro @@ -814,14 +838,14 @@ typedef struct NGTCP2_ALIGN(8) ngtcp2_pkt_info { * :macro:`NGTCP2_PKT_FLAG_FIXED_BIT_CLEAR` indicates that Fixed Bit * (aka QUIC bit) is not set. */ -#define NGTCP2_PKT_FLAG_FIXED_BIT_CLEAR 0x02u +#define NGTCP2_PKT_FLAG_FIXED_BIT_CLEAR 0x02U /** * @macro * * :macro:`NGTCP2_PKT_FLAG_KEY_PHASE` indicates Key Phase bit set. */ -#define NGTCP2_PKT_FLAG_KEY_PHASE 0x04u +#define NGTCP2_PKT_FLAG_KEY_PHASE 0x04U /** * @enum @@ -873,7 +897,7 @@ typedef enum ngtcp2_pkt_type { * * :macro:`NGTCP2_NO_ERROR` is QUIC transport error code ``NO_ERROR``. */ -#define NGTCP2_NO_ERROR 0x0u +#define NGTCP2_NO_ERROR 0x0U /** * @macro @@ -881,7 +905,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_INTERNAL_ERROR` is QUIC transport error code * ``INTERNAL_ERROR``. */ -#define NGTCP2_INTERNAL_ERROR 0x1u +#define NGTCP2_INTERNAL_ERROR 0x1U /** * @macro @@ -889,7 +913,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_CONNECTION_REFUSED` is QUIC transport error code * ``CONNECTION_REFUSED``. */ -#define NGTCP2_CONNECTION_REFUSED 0x2u +#define NGTCP2_CONNECTION_REFUSED 0x2U /** * @macro @@ -897,7 +921,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_FLOW_CONTROL_ERROR` is QUIC transport error code * ``FLOW_CONTROL_ERROR``. */ -#define NGTCP2_FLOW_CONTROL_ERROR 0x3u +#define NGTCP2_FLOW_CONTROL_ERROR 0x3U /** * @macro @@ -905,7 +929,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_STREAM_LIMIT_ERROR` is QUIC transport error code * ``STREAM_LIMIT_ERROR``. */ -#define NGTCP2_STREAM_LIMIT_ERROR 0x4u +#define NGTCP2_STREAM_LIMIT_ERROR 0x4U /** * @macro @@ -913,7 +937,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_STREAM_STATE_ERROR` is QUIC transport error code * ``STREAM_STATE_ERROR``. */ -#define NGTCP2_STREAM_STATE_ERROR 0x5u +#define NGTCP2_STREAM_STATE_ERROR 0x5U /** * @macro @@ -921,7 +945,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_FINAL_SIZE_ERROR` is QUIC transport error code * ``FINAL_SIZE_ERROR``. */ -#define NGTCP2_FINAL_SIZE_ERROR 0x6u +#define NGTCP2_FINAL_SIZE_ERROR 0x6U /** * @macro @@ -929,7 +953,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_FRAME_ENCODING_ERROR` is QUIC transport error code * ``FRAME_ENCODING_ERROR``. */ -#define NGTCP2_FRAME_ENCODING_ERROR 0x7u +#define NGTCP2_FRAME_ENCODING_ERROR 0x7U /** * @macro @@ -937,7 +961,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_TRANSPORT_PARAMETER_ERROR` is QUIC transport error * code ``TRANSPORT_PARAMETER_ERROR``. */ -#define NGTCP2_TRANSPORT_PARAMETER_ERROR 0x8u +#define NGTCP2_TRANSPORT_PARAMETER_ERROR 0x8U /** * @macro @@ -945,7 +969,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_CONNECTION_ID_LIMIT_ERROR` is QUIC transport error * code ``CONNECTION_ID_LIMIT_ERROR``. */ -#define NGTCP2_CONNECTION_ID_LIMIT_ERROR 0x9u +#define NGTCP2_CONNECTION_ID_LIMIT_ERROR 0x9U /** * @macro @@ -953,7 +977,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_PROTOCOL_VIOLATION` is QUIC transport error code * ``PROTOCOL_VIOLATION``. */ -#define NGTCP2_PROTOCOL_VIOLATION 0xau +#define NGTCP2_PROTOCOL_VIOLATION 0xAU /** * @macro @@ -961,7 +985,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_INVALID_TOKEN` is QUIC transport error code * ``INVALID_TOKEN``. */ -#define NGTCP2_INVALID_TOKEN 0xbu +#define NGTCP2_INVALID_TOKEN 0xBU /** * @macro @@ -969,7 +993,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_APPLICATION_ERROR` is QUIC transport error code * ``APPLICATION_ERROR``. */ -#define NGTCP2_APPLICATION_ERROR 0xcu +#define NGTCP2_APPLICATION_ERROR 0xCU /** * @macro @@ -977,7 +1001,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_CRYPTO_BUFFER_EXCEEDED` is QUIC transport error code * ``CRYPTO_BUFFER_EXCEEDED``. */ -#define NGTCP2_CRYPTO_BUFFER_EXCEEDED 0xdu +#define NGTCP2_CRYPTO_BUFFER_EXCEEDED 0xDU /** * @macro @@ -985,7 +1009,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_KEY_UPDATE_ERROR` is QUIC transport error code * ``KEY_UPDATE_ERROR``. */ -#define NGTCP2_KEY_UPDATE_ERROR 0xeu +#define NGTCP2_KEY_UPDATE_ERROR 0xEU /** * @macro @@ -993,7 +1017,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_AEAD_LIMIT_REACHED` is QUIC transport error code * ``AEAD_LIMIT_REACHED``. */ -#define NGTCP2_AEAD_LIMIT_REACHED 0xfu +#define NGTCP2_AEAD_LIMIT_REACHED 0xFU /** * @macro @@ -1001,7 +1025,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_NO_VIABLE_PATH` is QUIC transport error code * ``NO_VIABLE_PATH``. */ -#define NGTCP2_NO_VIABLE_PATH 0x10u +#define NGTCP2_NO_VIABLE_PATH 0x10U /** * @macro @@ -1009,7 +1033,7 @@ typedef enum ngtcp2_pkt_type { * :macro:`NGTCP2_CRYPTO_ERROR` is QUIC transport error code * ``CRYPTO_ERROR``. */ -#define NGTCP2_CRYPTO_ERROR 0x100u +#define NGTCP2_CRYPTO_ERROR 0x100U /** * @macro @@ -1172,6 +1196,11 @@ typedef struct ngtcp2_pkt_hd { /** * @struct * + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use :type:`ngtcp2_pkt_stateless_reset2` instead. + * * :type:`ngtcp2_pkt_stateless_reset` represents Stateless Reset. */ typedef struct ngtcp2_pkt_stateless_reset { @@ -1190,6 +1219,40 @@ typedef struct ngtcp2_pkt_stateless_reset { size_t randlen; } ngtcp2_pkt_stateless_reset; +/** + * @struct + * + * :type:`ngtcp2_stateless_reset_token` stores stateless reset token. + * + * .. version-added:: 1.22.0 + */ +typedef struct ngtcp2_stateless_reset_token { + uint8_t data[NGTCP2_STATELESS_RESET_TOKENLEN]; +} ngtcp2_stateless_reset_token; + +/** + * @struct + * + * :type:`ngtcp2_pkt_stateless_reset2` represents Stateless Reset. + * + * .. version-added:: 1.22.0 + */ +typedef struct ngtcp2_pkt_stateless_reset2 { + /** + * :member:`token` contains stateless reset token. + */ + ngtcp2_stateless_reset_token token; + /** + * :member:`rand` points a buffer which contains random bytes + * section. + */ + const uint8_t *rand; + /** + * :member:`randlen` is the number of random bytes. + */ + size_t randlen; +} ngtcp2_pkt_stateless_reset2; + /** * @macrosection * @@ -1236,7 +1299,7 @@ typedef struct ngtcp2_pkt_stateless_reset { * :macro:`NGTCP2_TLSEXT_QUIC_TRANSPORT_PARAMETERS_V1` is TLS * extension type of quic_transport_parameters. */ -#define NGTCP2_TLSEXT_QUIC_TRANSPORT_PARAMETERS_V1 0x39u +#define NGTCP2_TLSEXT_QUIC_TRANSPORT_PARAMETERS_V1 0x39U #ifdef NGTCP2_USE_GENERIC_SOCKADDR # ifndef NGTCP2_AF_INET @@ -1267,7 +1330,7 @@ typedef struct ngtcp2_sockaddr_in { } ngtcp2_sockaddr_in; typedef struct ngtcp2_in6_addr { - uint8_t in6_addr[16]; + uint8_t s6_addr[16]; } ngtcp2_in6_addr; typedef struct ngtcp2_sockaddr_in6 { @@ -1299,6 +1362,14 @@ typedef struct sockaddr ngtcp2_sockaddr; * the generic struct sockaddr_in defined in ngtcp2.h. */ typedef struct sockaddr_in ngtcp2_sockaddr_in; +/** + * @typedef + * + * :type:`ngtcp2_in_addr` is typedefed to struct in_addr. If + * :macro:`NGTCP2_USE_GENERIC_SOCKADDR` is defined, it is typedefed to + * the generic struct in_addr defined in ngtcp2.h. + */ +typedef struct in_addr ngtcp2_in_addr; /** * @typedef * @@ -1307,6 +1378,14 @@ typedef struct sockaddr_in ngtcp2_sockaddr_in; * to the generic struct sockaddr_in6 defined in ngtcp2.h. */ typedef struct sockaddr_in6 ngtcp2_sockaddr_in6; +/** + * @typedef + * + * :type:`ngtcp2_in6_addr` is typedefed to struct in6_addr. If + * :macro:`NGTCP2_USE_GENERIC_SOCKADDR` is defined, it is typedefed to + * the generic struct in6_addr defined in ngtcp2.h. + */ +typedef struct in6_addr ngtcp2_in6_addr; /** * @typedef * @@ -1561,7 +1640,8 @@ typedef struct ngtcp2_transport_params { } ngtcp2_transport_params; #define NGTCP2_CONN_INFO_V1 1 -#define NGTCP2_CONN_INFO_VERSION NGTCP2_CONN_INFO_V1 +#define NGTCP2_CONN_INFO_V2 2 +#define NGTCP2_CONN_INFO_VERSION NGTCP2_CONN_INFO_V2 /** * @struct @@ -1600,6 +1680,61 @@ typedef struct ngtcp2_conn_info { * packets which have not been acknowledged. */ uint64_t bytes_in_flight; + /* The following fields have been added since + NGTCP2_CONN_INFO_V2. */ + /** + * :member:`pkt_sent` is the number of QUIC packets sent. + * + * .. version-added:: 1.16.0 + */ + uint64_t pkt_sent; + /** + * :member:`bytes_sent` is the number of bytes (the sum of QUIC + * packet length) sent. + * + * .. version-added:: 1.16.0 + */ + uint64_t bytes_sent; + /** + * :member:`pkt_recv` is the number of QUIC packets received, + * excluding discarded ones. + * + * .. version-added:: 1.16.0 + */ + uint64_t pkt_recv; + /** + * :member:`bytes_recv` is the number of bytes (the sum of QUIC + * packet length) received, excluding discarded ones. + * + * .. version-added:: 1.16.0 + */ + uint64_t bytes_recv; + /** + * :member:`pkt_lost` is the number of QUIC packets that are + * considered lost, excluding PMTUD packets. + * + * .. version-added:: 1.16.0 + */ + uint64_t pkt_lost; + /** + * :member:`bytes_lost` is the number of bytes (the sum of QUIC + * packet length) lost, excluding PMTUD packets. + * + * .. version-added:: 1.16.0 + */ + uint64_t bytes_lost; + /** + * :member:`ping_recv` is the number of PING frames received. + * + * .. version-added:: 1.16.0 + */ + uint64_t ping_recv; + /** + * :member:`pkt_discarded` is the number of QUIC packets discarded. + * + * .. version-added:: 1.16.0 + */ + uint64_t pkt_discarded; } ngtcp2_conn_info; /** @@ -1625,12 +1760,32 @@ typedef enum ngtcp2_cc_algo { /** * @functypedef * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use :type:`ngtcp2_log_write` instead. + * * :type:`ngtcp2_printf` is a callback function for logging. * |user_data| is the same object passed to `ngtcp2_conn_client_new` * or `ngtcp2_conn_server_new`. */ typedef void (*ngtcp2_printf)(void *user_data, const char *format, ...); +/** + * @functypedef + * + * :type:`ngtcp2_log_write` is a callback function for logging. + * |user_data| is the same object passed to `ngtcp2_conn_client_new` + * or `ngtcp2_conn_server_new`. The caller guarantees that the memory + * region [|msg|, |msg| + |len|], inclusive, are writable, and + * |msg|[|len|] == '\0'. If application needs to emit a single line + * with a line terminator, one can do msg[len] = '\n', and write |len| + * + 1 bytes from |msg|. + * + * .. version-added:: 1.23.0 + */ +typedef void (*ngtcp2_log_write)(void *user_data, char *msg, size_t len); + /** * @macrosection * @@ -1642,14 +1797,14 @@ typedef void (*ngtcp2_printf)(void *user_data, const char *format, ...); * * :macro:`NGTCP2_QLOG_WRITE_FLAG_NONE` indicates no flag set. */ -#define NGTCP2_QLOG_WRITE_FLAG_NONE 0x00u +#define NGTCP2_QLOG_WRITE_FLAG_NONE 0x00U /** * @macro * * :macro:`NGTCP2_QLOG_WRITE_FLAG_FIN` indicates that this is the * final call to :type:`ngtcp2_qlog_write` in the current connection. */ -#define NGTCP2_QLOG_WRITE_FLAG_FIN 0x01u +#define NGTCP2_QLOG_WRITE_FLAG_FIN 0x01U /** * @struct @@ -1705,7 +1860,8 @@ typedef enum ngtcp2_token_type { #define NGTCP2_SETTINGS_V1 1 #define NGTCP2_SETTINGS_V2 2 #define NGTCP2_SETTINGS_V3 3 -#define NGTCP2_SETTINGS_VERSION NGTCP2_SETTINGS_V3 +#define NGTCP2_SETTINGS_V4 4 +#define NGTCP2_SETTINGS_VERSION NGTCP2_SETTINGS_V4 /** * @struct @@ -1732,6 +1888,11 @@ typedef struct ngtcp2_settings { */ ngtcp2_duration initial_rtt; /** + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use :member:`log_write` instead. + * * :member:`log_printf` is a function that the library uses to write * logs. ``NULL`` means no logging output. It is nothing to do * with qlog. @@ -1739,7 +1900,9 @@ typedef struct ngtcp2_settings { ngtcp2_printf log_printf; /** * :member:`max_tx_udp_payload_size` is the maximum size of UDP - * datagram payload that the local endpoint transmits. + * datagram payload that the local endpoint transmits. This must be + * larger than or equal to :macro:`NGTCP2_MAX_UDP_PAYLOAD_SIZE`, and + * less then or equal to :macro:`NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE`. */ size_t max_tx_udp_payload_size; /** @@ -1803,8 +1966,8 @@ typedef struct ngtcp2_settings { uint64_t max_stream_window; /** * :member:`ack_thresh` is the minimum number of the received ACK - * eliciting packets that trigger the immediate acknowledgement from - * the local endpoint. + * eliciting packets that triggers the immediate acknowledgement + * from the local endpoint. */ size_t ack_thresh; /** @@ -1904,18 +2067,21 @@ typedef struct ngtcp2_settings { /** * :member:`pmtud_probes` is the array of UDP datagram payload size * to probe during Path MTU Discovery. The discovery is done in the - * order appeared in this array. The size must be strictly larger - * than 1200, otherwise the behavior is undefined. The maximum - * value in this array should be set to - * :member:`max_tx_udp_payload_size`. If this field is not set, the - * predefined PMTUD probes are made. This field has been available - * since v1.4.0. + * order appeared in this array. The payload size must be strictly + * larger than :macro:`NGTCP2_MAX_UDP_PAYLOAD_SIZE`, and less than + * or equal to :macro:`NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE`. Otherwise + * the behavior is undefined. The maximum value in this array + * should be set to :member:`max_tx_udp_payload_size`. If this + * field is not set, the predefined PMTUD probes are made. + * + * .. version-added:: 1.4.0 */ const uint16_t *pmtud_probes; /** * :member:`pmtud_probeslen` is the number of elements that are - * contained in the array pointed by :member:`pmtud_probes`. This - * field has been available since v1.4.0. + * contained in the array pointed by :member:`pmtud_probes`. + * + * .. version-added:: 1.4.0 */ size_t pmtud_probeslen; /* The following fields have been added since NGTCP2_SETTINGS_V3. */ @@ -1925,16 +2091,31 @@ typedef struct ngtcp2_settings { * activity from a remote endpoint. If detected, certain amount of * tokens are consumed. If no tokens are available to consume, the * connection is closed. The rate of token generation is specified - * by :member:`glitch_ratelim_rate`. This field has been available - * since v1.15.0. + * by :member:`glitch_ratelim_rate`. + * + * .. version-added:: 1.15.0 */ uint64_t glitch_ratelim_burst; /** * :member:`glitch_ratelim_rate` is the number of tokens generated * per second. See :member:`glitch_ratelim_burst` for "glitch" rate - * limiter. This field has been available since v1.15.0. + * limiter. + * + * .. version-added:: 1.15.0 */ uint64_t glitch_ratelim_rate; + /* The following fields have been added since NGTCP2_SETTINGS_V4. */ + /** + * :member:`log_write` is the callback function when a single log + * message is emitted. If both :member:`log_write` and + * :member:`log_printf` are specified, the former has precedence. + * If both :member:`log_write` and :member:`log_printf` are + * ``NULL``, logging is disabled. For qlog, see + * :member:`qlog_write`. + * + * .. version-added:: 1.23.0 + */ + ngtcp2_log_write log_write; } ngtcp2_settings; /** @@ -2360,6 +2541,11 @@ NGTCP2_EXTERN ngtcp2_ssize ngtcp2_pkt_decode_hd_short(ngtcp2_pkt_hd *dest, /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use `ngtcp2_pkt_write_stateless_reset2` instead. + * * `ngtcp2_pkt_write_stateless_reset` writes Stateless Reset packet in * the buffer pointed by |dest| whose length is |destlen|. * |stateless_reset_token| is a pointer to the Stateless Reset Token, @@ -2385,6 +2571,34 @@ NGTCP2_EXTERN ngtcp2_ssize ngtcp2_pkt_write_stateless_reset( uint8_t *dest, size_t destlen, const uint8_t *stateless_reset_token, const uint8_t *rand, size_t randlen); +/** + * @function + * + * `ngtcp2_pkt_write_stateless_reset2` writes Stateless Reset packet + * in the buffer pointed by |dest| whose length is |destlen|. |token| + * must store the Stateless Reset Token. |rand| specifies the random + * octets preceding Stateless Reset Token. The length of |rand| is + * specified by |randlen| which must be at least + * :macro:`NGTCP2_MIN_STATELESS_RESET_RANDLEN` bytes long. + * + * If |randlen| is too long to write them all in the buffer, |rand| is + * written to the buffer as much as possible, and is truncated. + * + * This function returns the number of bytes written to the buffer, or + * one of the following negative error codes: + * + * :macro:`NGTCP2_ERR_NOBUF` + * Buffer is too small. + * :macro:`NGTCP2_ERR_INVALID_ARGUMENT` + * |randlen| is strictly less than + * :macro:`NGTCP2_MIN_STATELESS_RESET_RANDLEN`. + * + * .. version-added:: 1.22.0 + */ +NGTCP2_EXTERN ngtcp2_ssize ngtcp2_pkt_write_stateless_reset2( + uint8_t *dest, size_t destlen, const ngtcp2_stateless_reset_token *token, + const uint8_t *rand, size_t randlen); + /** * @function * @@ -2680,7 +2894,7 @@ typedef int (*ngtcp2_hp_mask)(uint8_t *dest, const ngtcp2_crypto_cipher *hp, * * :macro:`NGTCP2_STREAM_DATA_FLAG_NONE` indicates no flag set. */ -#define NGTCP2_STREAM_DATA_FLAG_NONE 0x00u +#define NGTCP2_STREAM_DATA_FLAG_NONE 0x00U /** * @macro @@ -2688,7 +2902,7 @@ typedef int (*ngtcp2_hp_mask)(uint8_t *dest, const ngtcp2_crypto_cipher *hp, * :macro:`NGTCP2_STREAM_DATA_FLAG_FIN` indicates that this chunk of * data is final piece of an incoming stream. */ -#define NGTCP2_STREAM_DATA_FLAG_FIN 0x01u +#define NGTCP2_STREAM_DATA_FLAG_FIN 0x01U /** * @macro @@ -2697,7 +2911,7 @@ typedef int (*ngtcp2_hp_mask)(uint8_t *dest, const ngtcp2_crypto_cipher *hp, * data contains data received in 0-RTT packet, and the handshake has * not completed yet, which means that the data might be replayed. */ -#define NGTCP2_STREAM_DATA_FLAG_0RTT 0x02u +#define NGTCP2_STREAM_DATA_FLAG_0RTT 0x02U /** * @functypedef @@ -2752,7 +2966,7 @@ typedef int (*ngtcp2_stream_open)(ngtcp2_conn *conn, int64_t stream_id, * * :macro:`NGTCP2_STREAM_CLOSE_FLAG_NONE` indicates no flag set. */ -#define NGTCP2_STREAM_CLOSE_FLAG_NONE 0x00u +#define NGTCP2_STREAM_CLOSE_FLAG_NONE 0x00U /** * @macro @@ -2760,7 +2974,7 @@ typedef int (*ngtcp2_stream_open)(ngtcp2_conn *conn, int64_t stream_id, * :macro:`NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET` indicates that * app_error_code parameter is set. */ -#define NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET 0x01u +#define NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET 0x01U /** * @functypedef @@ -2834,6 +3048,11 @@ typedef int (*ngtcp2_acked_stream_data_offset)( /** * @functypedef * + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use :type:`ngtcp2_recv_stateless_reset2` instead. + * * :type:`ngtcp2_recv_stateless_reset` is a callback function which is * called when Stateless Reset packet is received. The stateless * reset details are given in |sr|. @@ -2893,6 +3112,11 @@ typedef void (*ngtcp2_rand)(uint8_t *dest, size_t destlen, /** * @functypedef * + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use :type:`ngtcp2_get_new_connection_id2` instead. + * * :type:`ngtcp2_get_new_connection_id` is a callback function to ask * an application for new connection ID. Application must generate * new unused connection ID with the exact |cidlen| bytes, and store @@ -2969,7 +3193,7 @@ typedef int (*ngtcp2_update_key)( * * :macro:`NGTCP2_PATH_VALIDATION_FLAG_NONE` indicates no flag set. */ -#define NGTCP2_PATH_VALIDATION_FLAG_NONE 0x00u +#define NGTCP2_PATH_VALIDATION_FLAG_NONE 0x00U /** * @macro @@ -2978,7 +3202,7 @@ typedef int (*ngtcp2_update_key)( * validation involving server preferred address. This flag is only * set for client. */ -#define NGTCP2_PATH_VALIDATION_FLAG_PREFERRED_ADDR 0x01u +#define NGTCP2_PATH_VALIDATION_FLAG_PREFERRED_ADDR 0x01U /** * @macro @@ -2987,7 +3211,7 @@ typedef int (*ngtcp2_update_key)( * server should send NEW_TOKEN frame for the new remote address. * This flag is only set for server. */ -#define NGTCP2_PATH_VALIDATION_FLAG_NEW_TOKEN 0x02u +#define NGTCP2_PATH_VALIDATION_FLAG_NEW_TOKEN 0x02U /** * @functypedef @@ -3049,7 +3273,7 @@ typedef int (*ngtcp2_path_validation)(ngtcp2_conn *conn, uint32_t flags, * address, leave :member:`dest->local ` * unmodified, or copy the value of :member:`local * ` field of the current network path obtained - * from `ngtcp2_conn_get_path()`. Both :member:`dest->local.addr + * from `ngtcp2_conn_get_path2()`. Both :member:`dest->local.addr * ` and :member:`dest->remote.addr * ` point to buffers which are at least * sizeof(:type:`ngtcp2_sockaddr_union`) bytes long, respectively. If @@ -3089,6 +3313,11 @@ typedef enum ngtcp2_connection_id_status_type { /** * @functypedef * + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use :type:`ngtcp2_connection_id_status2` instead. + * * :type:`ngtcp2_connection_id_status` is a callback function which is * called when the status of Destination Connection ID changes. * @@ -3157,7 +3386,7 @@ typedef void (*ngtcp2_delete_crypto_cipher_ctx)( * * :macro:`NGTCP2_DATAGRAM_FLAG_NONE` indicates no flag set. */ -#define NGTCP2_DATAGRAM_FLAG_NONE 0x00u +#define NGTCP2_DATAGRAM_FLAG_NONE 0x00U /** * @macro @@ -3166,7 +3395,7 @@ typedef void (*ngtcp2_delete_crypto_cipher_ctx)( * received in 0-RTT packet, and the handshake has not completed yet, * which means that the data might be replayed. */ -#define NGTCP2_DATAGRAM_FLAG_0RTT 0x01u +#define NGTCP2_DATAGRAM_FLAG_0RTT 0x01U /** * @functypedef @@ -3220,6 +3449,11 @@ typedef int (*ngtcp2_lost_datagram)(ngtcp2_conn *conn, uint64_t dgram_id, /** * @functypedef * + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use :type:`ngtcp2_get_path_challenge_data2` instead. + * * :type:`ngtcp2_get_path_challenge_data` is a callback function to * ask an application for new data that is sent in PATH_CHALLENGE * frame. Application must generate new unpredictable, exactly @@ -3300,9 +3534,99 @@ typedef int (*ngtcp2_recv_key)(ngtcp2_conn *conn, ngtcp2_encryption_level level, typedef int (*ngtcp2_tls_early_data_rejected)(ngtcp2_conn *conn, void *user_data); +/** + * @functypedef + * + * :type:`ngtcp2_recv_stateless_reset2` is a callback function which + * is called when Stateless Reset packet is received. The stateless + * reset details are given in |sr|. + * + * The implementation of this callback should return 0 if it succeeds. + * Returning :macro:`NGTCP2_ERR_CALLBACK_FAILURE` makes the library + * call return immediately. + * + * .. version-added:: 1.22.0 + */ +typedef int (*ngtcp2_recv_stateless_reset2)( + ngtcp2_conn *conn, const ngtcp2_pkt_stateless_reset2 *sr, void *user_data); + +/** + * @functypedef + * + * :type:`ngtcp2_get_new_connection_id2` is a callback function to ask + * an application for new connection ID. Application must generate + * new unused connection ID with the exact |cidlen| bytes, and store + * it in |cid|. It also has to generate a stateless reset token, and + * store it in |token|. + * + * The callback function must return 0 if it succeeds. Returning + * :macro:`NGTCP2_ERR_CALLBACK_FAILURE` makes the library call return + * immediately. + * + * .. version-added:: 1.22.0 + */ +typedef int (*ngtcp2_get_new_connection_id2)( + ngtcp2_conn *conn, ngtcp2_cid *cid, ngtcp2_stateless_reset_token *token, + size_t cidlen, void *user_data); + +/** + * @functypedef + * + * :type:`ngtcp2_connection_id_status2` is a callback function which + * is called when the status of Destination Connection ID changes. + * + * |token| is the associated stateless reset token, and it is ``NULL`` + * if no token is present. + * + * |type| is the one of the value defined in + * :type:`ngtcp2_connection_id_status_type`. The new value might be + * added in the future release. + * + * The callback function must return 0 if it succeeds. Returning + * :macro:`NGTCP2_ERR_CALLBACK_FAILURE` makes the library call return + * immediately. + * + * .. version-added:: 1.22.0 + */ +typedef int (*ngtcp2_connection_id_status2)( + ngtcp2_conn *conn, ngtcp2_connection_id_status_type type, uint64_t seq, + const ngtcp2_cid *cid, const ngtcp2_stateless_reset_token *token, + void *user_data); + +/** + * @struct + * + * :type:`ngtcp2_path_challenge_data` stores path challenge data. + * + * .. version-added:: 1.22.0 + */ +typedef struct ngtcp2_path_challenge_data { + uint8_t data[NGTCP2_PATH_CHALLENGE_DATALEN]; +} ngtcp2_path_challenge_data; + +/** + * @functypedef + * + * :type:`ngtcp2_get_path_challenge_data2` is a callback function to + * ask an application for new data that is sent in PATH_CHALLENGE + * frame. Application must generate new unpredictable, exactly + * :macro:`NGTCP2_PATH_CHALLENGE_DATALEN` bytes of random data, and + * store them into |data|. + * + * The callback function must return 0 if it succeeds. Returning + * :macro:`NGTCP2_ERR_CALLBACK_FAILURE` makes the library call return + * immediately. + * + * .. version-added:: 1.22.0 + */ +typedef int (*ngtcp2_get_path_challenge_data2)(ngtcp2_conn *conn, + ngtcp2_path_challenge_data *data, + void *user_data); + #define NGTCP2_CALLBACKS_V1 1 #define NGTCP2_CALLBACKS_V2 2 -#define NGTCP2_CALLBACKS_VERSION NGTCP2_CALLBACKS_V2 +#define NGTCP2_CALLBACKS_V3 3 +#define NGTCP2_CALLBACKS_VERSION NGTCP2_CALLBACKS_V3 /** * @struct @@ -3386,6 +3710,14 @@ typedef struct ngtcp2_callbacks { */ ngtcp2_stream_close stream_close; /** + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use :member:`recv_stateless_reset2` instead. If both + * :member:`recv_stateless_reset` and + * :member:`recv_stateless_reset2` are set, the latter has the + * precedence. + * * :member:`recv_stateless_reset` is a callback function which is * invoked when Stateless Reset packet is received. This callback * function is optional. @@ -3418,9 +3750,18 @@ typedef struct ngtcp2_callbacks { */ ngtcp2_rand rand; /** + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use :member:`get_new_connection_id2` instead. If both + * :member:`get_new_connection_id` and + * :member:`get_new_connection_id2` are set, the latter has the + * precedence. + * * :member:`get_new_connection_id` is a callback function which is - * invoked when the library needs new connection ID. This callback - * function must be specified. + * invoked when the library needs new connection ID. Either this + * callback function or :member:`get_new_connection_id2` must be + * specified. */ ngtcp2_get_new_connection_id get_new_connection_id; /** @@ -3477,6 +3818,13 @@ typedef struct ngtcp2_callbacks { */ ngtcp2_extend_max_stream_data extend_max_stream_data; /** + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use :member:`dcid_status2` instead. If both + * :member:`dcid_status` and :member:`dcid_status2` are set, the + * latter has the precedence. + * * :member:`dcid_status` is a callback function which is invoked * when the new Destination Connection ID is activated, or the * activated Destination Connection ID is now deactivated. This @@ -3528,6 +3876,11 @@ typedef struct ngtcp2_callbacks { */ ngtcp2_lost_datagram lost_datagram; /** + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use :member:`get_path_challenge_data2` instead. + * * :member:`get_path_challenge_data` is a callback function which is * invoked when the library needs new data sent along with * PATH_CHALLENGE frame. This callback must be specified. @@ -3567,13 +3920,50 @@ typedef struct ngtcp2_callbacks { * is only used by client. */ ngtcp2_tls_early_data_rejected tls_early_data_rejected; - /* The following fields have been added since NGTCP2_CALLBACKS_V2. */ + /* The following fields have been added since + NGTCP2_CALLBACKS_V2. */ /** * :member:`begin_path_validation` is a callback function which is - * invoked when a path validation has started. This field is - * available since v1.14.0. + * invoked when a path validation has started. + * + * .. version-added:: 1.14.0 */ ngtcp2_begin_path_validation begin_path_validation; + /* The following fields have been added since + NGTCP2_CALLBACKS_V3. */ + /** + * :member:`recv_stateless_reset2` is a callback function which is + * invoked when Stateless Reset packet is received. This callback + * function is optional. + * + * .. version-added:: 1.22.0 + */ + ngtcp2_recv_stateless_reset2 recv_stateless_reset2; + /** + * :member:`get_new_connection_id2` is a callback function which is + * invoked when the library needs new connection ID. This callback + * function must be specified. + * + * .. version-added:: 1.22.0 + */ + ngtcp2_get_new_connection_id2 get_new_connection_id2; + /** + * :member:`dcid_status2` is a callback function which is invoked + * when the new Destination Connection ID is activated, or the + * activated Destination Connection ID is now deactivated. This + * callback function is optional. + * + * .. version-added:: 1.22.0 + */ + ngtcp2_connection_id_status2 dcid_status2; + /** + * :member:`get_path_challenge_data2` is a callback function which + * is invoked when the library needs new data sent along with + * PATH_CHALLENGE frame. This callback must be specified. + * + * .. version-added:: 1.22.0 + */ + ngtcp2_get_path_challenge_data2 get_path_challenge_data2; } ngtcp2_callbacks; /** @@ -3765,7 +4155,7 @@ NGTCP2_EXTERN void ngtcp2_conn_del(ngtcp2_conn *conn); * `ngtcp2_conn_write_connection_close` makes a connection enter * this state. * :macro:`NGTCP2_ERR_CRYPTO` - * An error happened in TLS stack. `ngtcp2_conn_get_tls_alert` + * An error happened in TLS stack. `ngtcp2_conn_get_tls_alert2` * returns TLS alert if set. * * If any other negative error is returned, call @@ -3789,6 +4179,21 @@ NGTCP2_EXTERN ngtcp2_ssize ngtcp2_conn_write_pkt_versioned( ngtcp2_conn *conn, ngtcp2_path *path, int pkt_info_version, ngtcp2_pkt_info *pi, uint8_t *dest, size_t destlen, ngtcp2_tstamp ts); +/** + * @function + * + * `ngtcp2_conn_continue_handshake` resumes handshake interrupted by + * TLS stack routine (e.g., private key operation offloading, + * certificate lookup, etc). + * + * This function returns 0 if it succeeds. In general, this function + * returns the same set of error codes from `ngtcp2_conn_read_pkt`. + * + * .. version-added:: 1.22.0 + */ +NGTCP2_EXTERN int ngtcp2_conn_continue_handshake(ngtcp2_conn *conn, + ngtcp2_tstamp ts); + /** * @function * @@ -3802,11 +4207,24 @@ NGTCP2_EXTERN void ngtcp2_conn_tls_handshake_completed(ngtcp2_conn *conn); /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_handshake_completed2` instead. + * * `ngtcp2_conn_get_handshake_completed` returns nonzero if QUIC * handshake has completed. */ NGTCP2_EXTERN int ngtcp2_conn_get_handshake_completed(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_get_handshake_completed2` returns nonzero if QUIC + * handshake has completed. + */ +NGTCP2_EXTERN int ngtcp2_conn_get_handshake_completed2(const ngtcp2_conn *conn); + /** * @function * @@ -4057,38 +4475,70 @@ NGTCP2_EXTERN int ngtcp2_conn_initiate_key_update(ngtcp2_conn *conn, * can set the error code (e.g., * :macro:`NGTCP2_ERR_MALFORMED_TRANSPORT_PARAM`) using this function. * - * See also `ngtcp2_conn_get_tls_error`. + * See also `ngtcp2_conn_get_tls_error2`. */ NGTCP2_EXTERN void ngtcp2_conn_set_tls_error(ngtcp2_conn *conn, int liberr); /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_tls_error2` instead. + * * `ngtcp2_conn_get_tls_error` returns the value set by * `ngtcp2_conn_set_tls_error`. If no value is set, this function * returns 0. */ NGTCP2_EXTERN int ngtcp2_conn_get_tls_error(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_get_tls_error2` returns the value set by + * `ngtcp2_conn_set_tls_error`. If no value is set, this function + * returns 0. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN int ngtcp2_conn_get_tls_error2(const ngtcp2_conn *conn); + /** * @function * * `ngtcp2_conn_set_tls_alert` sets a TLS alert |alert| generated by a * TLS stack of a local endpoint to |conn|. * - * See also `ngtcp2_conn_get_tls_alert`. + * See also `ngtcp2_conn_get_tls_alert2`. */ NGTCP2_EXTERN void ngtcp2_conn_set_tls_alert(ngtcp2_conn *conn, uint8_t alert); /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_tls_alert2` instead. + * * `ngtcp2_conn_get_tls_alert` returns the value set by * `ngtcp2_conn_set_tls_alert`. If no value is set, this function * returns 0. */ NGTCP2_EXTERN uint8_t ngtcp2_conn_get_tls_alert(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_get_tls_alert2` returns the value set by + * `ngtcp2_conn_set_tls_alert`. If no value is set, this function + * returns 0. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN uint8_t ngtcp2_conn_get_tls_alert2(const ngtcp2_conn *conn); + /** * @function * @@ -4105,30 +4555,73 @@ NGTCP2_EXTERN void ngtcp2_conn_set_keep_alive_timeout(ngtcp2_conn *conn, /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_expiry2` instead. + * * `ngtcp2_conn_get_expiry` returns the next expiry time. It returns * ``UINT64_MAX`` if there is no next expiry. * - * Call `ngtcp2_conn_handle_expiry` and then - * `ngtcp2_conn_writev_stream` (or `ngtcp2_conn_writev_datagram`) when - * the expiry time has passed. + * Call `ngtcp2_conn_handle_expiry` when the expiry time has passed. */ NGTCP2_EXTERN ngtcp2_tstamp ngtcp2_conn_get_expiry(ngtcp2_conn *conn); /** * @function * - * `ngtcp2_conn_handle_expiry` handles expired timer. + * `ngtcp2_conn_get_expiry2` returns the next expiry time. It returns + * ``UINT64_MAX`` if there is no next expiry. + * + * Call `ngtcp2_conn_handle_expiry` when the expiry time has passed. + * + * .. version-added:: 1.23.0 */ -NGTCP2_EXTERN int ngtcp2_conn_handle_expiry(ngtcp2_conn *conn, - ngtcp2_tstamp ts); +NGTCP2_EXTERN ngtcp2_tstamp ngtcp2_conn_get_expiry2(const ngtcp2_conn *conn); /** * @function * - * `ngtcp2_conn_get_pto` returns Probe Timeout (PTO). - */ + * `ngtcp2_conn_handle_expiry` handles expired timer. + * + * If it returns :macro:`NGTCP2_ERR_IDLE_CLOSE`, it means that an idle + * timer has fired for this particular connection. In this case, drop + * the connection without calling + * `ngtcp2_conn_write_connection_close`. If it returns any of the + * other negative error codes, close the connection by sending the + * terminal packet produced by `ngtcp2_conn_write_connection_close`. + * Otherwise, schedule `ngtcp2_conn_writev_stream` call. An + * application may call any number of additional + * `ngtcp2_conn_read_pkt` and `ngtcp2_conn_handle_expiry` before + * calling `ngtcp2_conn_writev_stream`. After calling + * `ngtcp2_conn_writev_stream`, new expiry is set. The application + * should call `ngtcp2_conn_get_expiry2` to get a new deadline and set + * the timer. + */ +NGTCP2_EXTERN int ngtcp2_conn_handle_expiry(ngtcp2_conn *conn, + ngtcp2_tstamp ts); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_pto2` instead. + * + * `ngtcp2_conn_get_pto` returns Probe Timeout (PTO). + */ NGTCP2_EXTERN ngtcp2_duration ngtcp2_conn_get_pto(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_get_pto2` returns Probe Timeout (PTO). + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN ngtcp2_duration ngtcp2_conn_get_pto2(const ngtcp2_conn *conn); + /** * @function * @@ -4156,6 +4649,11 @@ NGTCP2_EXTERN int ngtcp2_conn_decode_and_set_remote_transport_params( /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_remote_transport_params2` instead. + * * `ngtcp2_conn_get_remote_transport_params` returns a pointer to the * remote QUIC transport parameters. If no remote transport * parameters are set, it returns NULL. @@ -4166,6 +4664,23 @@ ngtcp2_conn_get_remote_transport_params(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_remote_transport_params2` returns a pointer to the + * remote QUIC transport parameters. If no remote transport + * parameters are set, it returns NULL. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN const ngtcp2_transport_params * +ngtcp2_conn_get_remote_transport_params2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_encode_0rtt_transport_params2` instead. + * * `ngtcp2_conn_encode_0rtt_transport_params` encodes the QUIC * transport parameters that are used for 0-RTT data in the buffer * pointed by |dest| of length |destlen|. It includes at least the @@ -4203,6 +4718,48 @@ ngtcp2_ssize ngtcp2_conn_encode_0rtt_transport_params(ngtcp2_conn *conn, uint8_t *dest, size_t destlen); +/** + * @function + * + * `ngtcp2_conn_encode_0rtt_transport_params2` encodes the QUIC + * transport parameters that are used for 0-RTT data in the buffer + * pointed by |dest| of length |destlen|. It includes at least the + * following fields: + * + * - :member:`ngtcp2_transport_params.initial_max_streams_bidi` + * - :member:`ngtcp2_transport_params.initial_max_streams_uni` + * - :member:`ngtcp2_transport_params.initial_max_stream_data_bidi_local` + * - :member:`ngtcp2_transport_params.initial_max_stream_data_bidi_remote` + * - :member:`ngtcp2_transport_params.initial_max_stream_data_uni` + * - :member:`ngtcp2_transport_params.initial_max_data` + * - :member:`ngtcp2_transport_params.active_connection_id_limit` + * - :member:`ngtcp2_transport_params.max_datagram_frame_size` + * + * If |conn| is initialized as server, the following additional fields + * are also included: + * + * - :member:`ngtcp2_transport_params.max_idle_timeout` + * - :member:`ngtcp2_transport_params.max_udp_payload_size` + * - :member:`ngtcp2_transport_params.disable_active_migration` + * + * If |conn| is initialized as client, these parameters are + * synthesized from the remote transport parameters received from + * server. Otherwise, they are the local transport parameters that + * are set by the local endpoint. + * + * This function returns the number of bytes written, or one of the + * following negative error codes: + * + * :macro:`NGTCP2_ERR_NOBUF` + * Buffer is too small. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN +ngtcp2_ssize ngtcp2_conn_encode_0rtt_transport_params2(const ngtcp2_conn *conn, + uint8_t *dest, + size_t destlen); + /** * @function * @@ -4262,6 +4819,11 @@ NGTCP2_EXTERN int ngtcp2_conn_set_local_transport_params_versioned( /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_local_transport_params2` instead. + * * `ngtcp2_conn_get_local_transport_params` returns a pointer to the * local QUIC transport parameters. */ @@ -4271,6 +4833,22 @@ ngtcp2_conn_get_local_transport_params(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_local_transport_params2` returns a pointer to the + * local QUIC transport parameters. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN const ngtcp2_transport_params * +ngtcp2_conn_get_local_transport_params2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_encode_local_transport_params2` instead. + * * `ngtcp2_conn_encode_local_transport_params` encodes the local QUIC * transport parameters in |dest| of length |destlen|. * @@ -4283,6 +4861,23 @@ ngtcp2_conn_get_local_transport_params(ngtcp2_conn *conn); NGTCP2_EXTERN ngtcp2_ssize ngtcp2_conn_encode_local_transport_params( ngtcp2_conn *conn, uint8_t *dest, size_t destlen); +/** + * @function + * + * `ngtcp2_conn_encode_local_transport_params2` encodes the local QUIC + * transport parameters in |dest| of length |destlen|. + * + * This function returns the number of bytes written, or one of the + * following negative error codes: + * + * :macro:`NGTCP2_ERR_NOBUF` + * Buffer is too small. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN ngtcp2_ssize ngtcp2_conn_encode_local_transport_params2( + const ngtcp2_conn *conn, uint8_t *dest, size_t destlen); + /** * @function * @@ -4436,7 +5031,7 @@ NGTCP2_EXTERN int ngtcp2_conn_shutdown_stream_read(ngtcp2_conn *conn, * * :macro:`NGTCP2_WRITE_STREAM_FLAG_NONE` indicates no flag set. */ -#define NGTCP2_WRITE_STREAM_FLAG_NONE 0x00u +#define NGTCP2_WRITE_STREAM_FLAG_NONE 0x00U /** * @macro @@ -4444,7 +5039,7 @@ NGTCP2_EXTERN int ngtcp2_conn_shutdown_stream_read(ngtcp2_conn *conn, * :macro:`NGTCP2_WRITE_STREAM_FLAG_MORE` indicates that more data may * come, and should be coalesced into the same packet if possible. */ -#define NGTCP2_WRITE_STREAM_FLAG_MORE 0x01u +#define NGTCP2_WRITE_STREAM_FLAG_MORE 0x01U /** * @macro @@ -4452,7 +5047,7 @@ NGTCP2_EXTERN int ngtcp2_conn_shutdown_stream_read(ngtcp2_conn *conn, * :macro:`NGTCP2_WRITE_STREAM_FLAG_FIN` indicates that a passed data * is the final part of a stream. */ -#define NGTCP2_WRITE_STREAM_FLAG_FIN 0x02u +#define NGTCP2_WRITE_STREAM_FLAG_FIN 0x02U /** * @macro @@ -4463,7 +5058,7 @@ NGTCP2_EXTERN int ngtcp2_conn_shutdown_stream_read(ngtcp2_conn *conn, * finalizing it. PATH_CHALLENGE, PATH_RESPONSE, CONNECTION_CLOSE * only packets and PMTUD packets are excluded. */ -#define NGTCP2_WRITE_STREAM_FLAG_PADDING 0x04u +#define NGTCP2_WRITE_STREAM_FLAG_PADDING 0x04U /** * @function @@ -4648,7 +5243,7 @@ NGTCP2_EXTERN ngtcp2_ssize ngtcp2_conn_writev_stream_versioned( * * :macro:`NGTCP2_WRITE_DATAGRAM_FLAG_NONE` indicates no flag set. */ -#define NGTCP2_WRITE_DATAGRAM_FLAG_NONE 0x00u +#define NGTCP2_WRITE_DATAGRAM_FLAG_NONE 0x00U /** * @macro @@ -4656,7 +5251,7 @@ NGTCP2_EXTERN ngtcp2_ssize ngtcp2_conn_writev_stream_versioned( * :macro:`NGTCP2_WRITE_DATAGRAM_FLAG_MORE` indicates that more data * may come, and should be coalesced into the same packet if possible. */ -#define NGTCP2_WRITE_DATAGRAM_FLAG_MORE 0x01u +#define NGTCP2_WRITE_DATAGRAM_FLAG_MORE 0x01U /** * @macro @@ -4667,7 +5262,7 @@ NGTCP2_EXTERN ngtcp2_ssize ngtcp2_conn_writev_stream_versioned( * finalizing it. PATH_CHALLENGE, PATH_RESPONSE, CONNECTION_CLOSE * only packets and PMTUD packets are excluded. */ -#define NGTCP2_WRITE_DATAGRAM_FLAG_PADDING 0x02u +#define NGTCP2_WRITE_DATAGRAM_FLAG_PADDING 0x02U /** * @function @@ -4787,6 +5382,11 @@ NGTCP2_EXTERN ngtcp2_ssize ngtcp2_conn_writev_datagram_versioned( /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_in_closing_period2` instead. + * * `ngtcp2_conn_in_closing_period` returns nonzero if |conn| is in the * closing period. */ @@ -4795,11 +5395,36 @@ NGTCP2_EXTERN int ngtcp2_conn_in_closing_period(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_in_closing_period2` returns nonzero if |conn| is in + * the closing period. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN int ngtcp2_conn_in_closing_period2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_in_draining_period2` instead. + * * `ngtcp2_conn_in_draining_period` returns nonzero if |conn| is in * the draining period. */ NGTCP2_EXTERN int ngtcp2_conn_in_draining_period(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_in_draining_period2` returns nonzero if |conn| is in + * the draining period. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN int ngtcp2_conn_in_draining_period2(const ngtcp2_conn *conn); + /** * @function * @@ -4866,6 +5491,11 @@ NGTCP2_EXTERN void ngtcp2_conn_extend_max_streams_uni(ngtcp2_conn *conn, /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_dcid2` instead. + * * `ngtcp2_conn_get_dcid` returns the non-NULL pointer to the current * Destination Connection ID. If no Destination Connection ID is * present, the return value is not ``NULL``, and its :member:`datalen @@ -4876,6 +5506,23 @@ NGTCP2_EXTERN const ngtcp2_cid *ngtcp2_conn_get_dcid(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_dcid2` returns the non-NULL pointer to the current + * Destination Connection ID. If no Destination Connection ID is + * present, the return value is not ``NULL``, and its :member:`datalen + * ` field is 0. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN const ngtcp2_cid *ngtcp2_conn_get_dcid2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_client_initial_dcid2` instead. + * * `ngtcp2_conn_get_client_initial_dcid` returns the non-NULL pointer * to the Destination Connection ID that client sent in its Initial * packet. If the Destination Connection ID is not present, the @@ -4888,7 +5535,26 @@ ngtcp2_conn_get_client_initial_dcid(ngtcp2_conn *conn); /** * @function * - * `ngtcp2_conn_get_scid` writes the all Source Connection IDs which a + * `ngtcp2_conn_get_client_initial_dcid2` returns the non-NULL pointer + * to the Destination Connection ID that client sent in its Initial + * packet. If the Destination Connection ID is not present, the + * return value is not ``NULL``, and its :member:`datalen + * ` field is 0. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN const ngtcp2_cid * +ngtcp2_conn_get_client_initial_dcid2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_scid2` instead. + * + * `ngtcp2_conn_get_scid` writes all Source Connection IDs which a * local endpoint has provided to a remote endpoint, and are not * retired in |dest|. If |dest| is NULL, this function does not write * anything, and returns the number of Source Connection IDs that @@ -4899,9 +5565,31 @@ ngtcp2_conn_get_client_initial_dcid(ngtcp2_conn *conn); */ NGTCP2_EXTERN size_t ngtcp2_conn_get_scid(ngtcp2_conn *conn, ngtcp2_cid *dest); +/** + * @function + * + * `ngtcp2_conn_get_scid2` writes all Source Connection IDs which a + * local endpoint has provided to a remote endpoint, and are not + * retired in |dest|. If |dest| is NULL, this function does not write + * anything, and returns the number of Source Connection IDs that + * would otherwise be written to the provided buffer. The buffer + * pointed by |dest| must have sizeof(:type:`ngtcp2_cid`) * n bytes + * available, where n is the return value of `ngtcp2_conn_get_scid2` + * with |dest| == NULL. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN size_t ngtcp2_conn_get_scid2(const ngtcp2_conn *conn, + ngtcp2_cid *dest); + /** * @struct * + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use :type:`ngtcp2_cid_token2` instead. + * * :type:`ngtcp2_cid_token` is the convenient struct to store * Connection ID, its associated path, and stateless reset token. */ @@ -4934,7 +5622,12 @@ typedef struct ngtcp2_cid_token { /** * @function * - * `ngtcp2_conn_get_active_dcid` writes the all active Destination + * .. warning:: + * + * .. version-deprecated:: 1.22.0 + * Use `ngtcp2_conn_get_active_dcid3` instead. + * + * `ngtcp2_conn_get_active_dcid` writes all active Destination * Connection IDs and their tokens to |dest|. Before handshake * completes, this function returns 0. If |dest| is NULL, this * function does not write anything, and returns the number of @@ -4947,9 +5640,89 @@ typedef struct ngtcp2_cid_token { NGTCP2_EXTERN size_t ngtcp2_conn_get_active_dcid(ngtcp2_conn *conn, ngtcp2_cid_token *dest); +/** + * @struct + * + * :type:`ngtcp2_cid_token2` is the convenient struct to store + * Connection ID, its associated path, and stateless reset token. + * + * .. version-added:: 1.22.0 + */ +typedef struct ngtcp2_cid_token2 { + /** + * :member:`seq` is the sequence number of this Connection ID. + */ + uint64_t seq; + /** + * :member:`cid` is Connection ID. + */ + ngtcp2_cid cid; + /** + * :member:`ps` is the path which this Connection ID is associated + * with. + */ + ngtcp2_path_storage ps; + /** + * :member:`token` is the stateless reset token for this Connection + * ID. + */ + ngtcp2_stateless_reset_token token; + /** + * :member:`token_present` is nonzero if token contains stateless + * reset token. + */ + uint8_t token_present; +} ngtcp2_cid_token2; + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_active_dcid3` instead. + * + * `ngtcp2_conn_get_active_dcid2` writes all active Destination + * Connection IDs and their tokens to |dest|. Before handshake + * completes, this function returns 0. If |dest| is NULL, this + * function does not write anything, and returns the number of + * Destination Connection IDs that would otherwise be written to the + * provided buffer. The buffer pointed by |dest| must have + * sizeof(:type:`ngtcp2_cid_token2`) * n bytes available, where n is + * the return value of `ngtcp2_conn_get_active_dcid2` with |dest| == + * NULL. + * + * .. version-added:: 1.22.0 + */ +NGTCP2_EXTERN size_t ngtcp2_conn_get_active_dcid2(ngtcp2_conn *conn, + ngtcp2_cid_token2 *dest); + /** * @function * + * `ngtcp2_conn_get_active_dcid3` writes all active Destination + * Connection IDs and their tokens to |dest|. Before handshake + * completes, this function returns 0. If |dest| is NULL, this + * function does not write anything, and returns the number of + * Destination Connection IDs that would otherwise be written to the + * provided buffer. The buffer pointed by |dest| must have + * sizeof(:type:`ngtcp2_cid_token2`) * n bytes available, where n is + * the return value of `ngtcp2_conn_get_active_dcid3` with |dest| == + * NULL. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN size_t ngtcp2_conn_get_active_dcid3(const ngtcp2_conn *conn, + ngtcp2_cid_token2 *dest); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_client_chosen_version2` instead. + * * `ngtcp2_conn_get_client_chosen_version` returns the client chosen * version. */ @@ -4958,6 +5731,22 @@ NGTCP2_EXTERN uint32_t ngtcp2_conn_get_client_chosen_version(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_client_chosen_version2` returns the client chosen + * version. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN uint32_t +ngtcp2_conn_get_client_chosen_version2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_negotiated_version2` instead. + * * `ngtcp2_conn_get_negotiated_version` returns the negotiated * version. * @@ -4965,6 +5754,19 @@ NGTCP2_EXTERN uint32_t ngtcp2_conn_get_client_chosen_version(ngtcp2_conn *conn); */ NGTCP2_EXTERN uint32_t ngtcp2_conn_get_negotiated_version(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_get_negotiated_version2` returns the negotiated + * version. + * + * Until the version is negotiated, this function returns 0. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN uint32_t +ngtcp2_conn_get_negotiated_version2(const ngtcp2_conn *conn); + /** * @function * @@ -4993,6 +5795,11 @@ NGTCP2_EXTERN int ngtcp2_conn_tls_early_data_rejected(ngtcp2_conn *conn); /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_tls_early_data_rejected2`. + * * `ngtcp2_conn_get_tls_early_data_rejected` returns nonzero if * `ngtcp2_conn_tls_early_data_rejected` has been called. */ @@ -5001,6 +5808,22 @@ NGTCP2_EXTERN int ngtcp2_conn_get_tls_early_data_rejected(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_tls_early_data_rejected2` returns nonzero if + * `ngtcp2_conn_tls_early_data_rejected` has been called. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN int +ngtcp2_conn_get_tls_early_data_rejected2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_conn_info2` instead. + * * `ngtcp2_conn_get_conn_info` assigns connection statistics data to * |*cinfo|. */ @@ -5008,6 +5831,17 @@ NGTCP2_EXTERN void ngtcp2_conn_get_conn_info_versioned(ngtcp2_conn *conn, int conn_info_version, ngtcp2_conn_info *cinfo); +/** + * @function + * + * `ngtcp2_conn_get_conn_info2` assigns connection statistics data to + * |*cinfo|. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN void ngtcp2_conn_get_conn_info2_versioned( + const ngtcp2_conn *conn, int conn_info_version, ngtcp2_conn_info *cinfo); + /** * @function * @@ -5065,6 +5899,11 @@ NGTCP2_EXTERN void ngtcp2_conn_set_path_user_data(ngtcp2_conn *conn, /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_path2` instead. + * * `ngtcp2_conn_get_path` returns the current path. */ NGTCP2_EXTERN const ngtcp2_path *ngtcp2_conn_get_path(ngtcp2_conn *conn); @@ -5072,6 +5911,20 @@ NGTCP2_EXTERN const ngtcp2_path *ngtcp2_conn_get_path(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_path2` returns the current path. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN const ngtcp2_path *ngtcp2_conn_get_path2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_max_tx_udp_payload_size2` instead. + * * `ngtcp2_conn_get_max_tx_udp_payload_size` returns the maximum UDP * payload size that this local endpoint would send. This is the * value of :member:`ngtcp2_settings.max_tx_udp_payload_size` that is @@ -5082,6 +5935,24 @@ NGTCP2_EXTERN size_t ngtcp2_conn_get_max_tx_udp_payload_size(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_max_tx_udp_payload_size2` returns the maximum UDP + * payload size that this local endpoint would send. This is the + * value of :member:`ngtcp2_settings.max_tx_udp_payload_size` that is + * passed to `ngtcp2_conn_client_new` or `ngtcp2_conn_server_new`. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN size_t +ngtcp2_conn_get_max_tx_udp_payload_size2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_path_max_tx_udp_payload_size2` instead. + * * `ngtcp2_conn_get_path_max_tx_udp_payload_size` returns the maximum * UDP payload size for the current path. If * :member:`ngtcp2_settings.no_tx_udp_payload_size_shaping` is set to @@ -5092,6 +5963,21 @@ NGTCP2_EXTERN size_t ngtcp2_conn_get_max_tx_udp_payload_size(ngtcp2_conn *conn); NGTCP2_EXTERN size_t ngtcp2_conn_get_path_max_tx_udp_payload_size(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_get_path_max_tx_udp_payload_size2` returns the maximum + * UDP payload size for the current path. If + * :member:`ngtcp2_settings.no_tx_udp_payload_size_shaping` is set to + * nonzero, this function is equivalent to + * `ngtcp2_conn_get_max_tx_udp_payload_size2`. Otherwise, it returns + * the maximum UDP payload size that is probed for the current path. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN size_t +ngtcp2_conn_get_path_max_tx_udp_payload_size2(const ngtcp2_conn *conn); + /** * @function * @@ -5148,6 +6034,11 @@ NGTCP2_EXTERN int ngtcp2_conn_initiate_migration(ngtcp2_conn *conn, /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_max_data_left2` instead. + * * `ngtcp2_conn_get_max_data_left` returns the number of bytes that * this local endpoint can send in this connection without violating * connection-level flow control. @@ -5157,6 +6048,22 @@ NGTCP2_EXTERN uint64_t ngtcp2_conn_get_max_data_left(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_max_data_left2` returns the number of bytes that + * this local endpoint can send in this connection without violating + * connection-level flow control. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN uint64_t ngtcp2_conn_get_max_data_left2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_max_stream_data_left2` instead. + * * `ngtcp2_conn_get_max_stream_data_left` returns the number of bytes * that this local endpoint can send to a stream identified by * |stream_id| without violating stream-level flow control. If no @@ -5168,6 +6075,24 @@ NGTCP2_EXTERN uint64_t ngtcp2_conn_get_max_stream_data_left(ngtcp2_conn *conn, /** * @function * + * `ngtcp2_conn_get_max_stream_data_left2` returns the number of bytes + * that this local endpoint can send to a stream identified by + * |stream_id| without violating stream-level flow control. If no + * such stream is found, this function returns 0. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN uint64_t ngtcp2_conn_get_max_stream_data_left2( + const ngtcp2_conn *conn, int64_t stream_id); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_streams_bidi_left2` instead. + * * `ngtcp2_conn_get_streams_bidi_left` returns the number of * bidirectional streams which the local endpoint can open without * violating stream concurrency limit. @@ -5177,6 +6102,23 @@ NGTCP2_EXTERN uint64_t ngtcp2_conn_get_streams_bidi_left(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_streams_bidi_left2` returns the number of + * bidirectional streams which the local endpoint can open without + * violating stream concurrency limit. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN uint64_t +ngtcp2_conn_get_streams_bidi_left2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_streams_uni_left2` instead. + * * `ngtcp2_conn_get_streams_uni_left` returns the number of * unidirectional streams which the local endpoint can open without * violating stream concurrency limit. @@ -5186,12 +6128,40 @@ NGTCP2_EXTERN uint64_t ngtcp2_conn_get_streams_uni_left(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_streams_uni_left2` returns the number of + * unidirectional streams which the local endpoint can open without + * violating stream concurrency limit. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN uint64_t +ngtcp2_conn_get_streams_uni_left2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_cwnd_left2` instead. + * * `ngtcp2_conn_get_cwnd_left` returns the cwnd minus the number of * bytes in flight on the current path. If the former is smaller than * the latter, this function returns 0. */ NGTCP2_EXTERN uint64_t ngtcp2_conn_get_cwnd_left(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_get_cwnd_left2` returns the cwnd minus the number of + * bytes in flight on the current path. If the former is smaller than + * the latter, this function returns 0. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN uint64_t ngtcp2_conn_get_cwnd_left2(const ngtcp2_conn *conn); + /** * @function * @@ -5207,12 +6177,28 @@ ngtcp2_conn_set_initial_crypto_ctx(ngtcp2_conn *conn, /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_initial_crypto_ctx2` instead. + * * `ngtcp2_conn_get_initial_crypto_ctx` returns * :type:`ngtcp2_crypto_ctx` object for Initial packet encryption. */ NGTCP2_EXTERN const ngtcp2_crypto_ctx * ngtcp2_conn_get_initial_crypto_ctx(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_get_initial_crypto_ctx2` returns + * :type:`ngtcp2_crypto_ctx` object for Initial packet encryption. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN const ngtcp2_crypto_ctx * +ngtcp2_conn_get_initial_crypto_ctx2(const ngtcp2_conn *conn); + /** * @function * @@ -5227,12 +6213,28 @@ NGTCP2_EXTERN void ngtcp2_conn_set_crypto_ctx(ngtcp2_conn *conn, /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_crypto_ctx2` instead. + * * `ngtcp2_conn_get_crypto_ctx` returns :type:`ngtcp2_crypto_ctx` * object for Handshake/1-RTT packet encryption. */ NGTCP2_EXTERN const ngtcp2_crypto_ctx * ngtcp2_conn_get_crypto_ctx(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_get_crypto_ctx2` returns :type:`ngtcp2_crypto_ctx` + * object for Handshake/1-RTT packet encryption. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN const ngtcp2_crypto_ctx * +ngtcp2_conn_get_crypto_ctx2(const ngtcp2_conn *conn); + /** * @function * @@ -5248,6 +6250,11 @@ ngtcp2_conn_set_0rtt_crypto_ctx(ngtcp2_conn *conn, /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_0rtt_crypto_ctx2` instead. + * * `ngtcp2_conn_get_0rtt_crypto_ctx` returns :type:`ngtcp2_crypto_ctx` * object for 0-RTT packet encryption. */ @@ -5257,11 +6264,37 @@ ngtcp2_conn_get_0rtt_crypto_ctx(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_0rtt_crypto_ctx2` returns + * :type:`ngtcp2_crypto_ctx` object for 0-RTT packet encryption. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN const ngtcp2_crypto_ctx * +ngtcp2_conn_get_0rtt_crypto_ctx2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_tls_native_handle2` instead. + * * `ngtcp2_conn_get_tls_native_handle` returns TLS native handle set * by `ngtcp2_conn_set_tls_native_handle`. */ NGTCP2_EXTERN void *ngtcp2_conn_get_tls_native_handle(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_get_tls_native_handle2` returns TLS native handle set + * by `ngtcp2_conn_set_tls_native_handle`. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN void *ngtcp2_conn_get_tls_native_handle2(const ngtcp2_conn *conn); + /** * @function * @@ -5477,7 +6510,7 @@ NGTCP2_EXTERN void ngtcp2_ccerr_set_application_error(ngtcp2_ccerr *ccerr, * @function * * `ngtcp2_conn_write_connection_close` writes a packet which contains - * CONNECTION_CLOSE frame(s) (type 0x1c or 0x1d) in the buffer pointed + * CONNECTION_CLOSE frame(s) (type 0x1C or 0x1D) in the buffer pointed * by |dest| whose capacity is |destlen|. * * For client, |destlen| should be at least @@ -5494,16 +6527,18 @@ NGTCP2_EXTERN void ngtcp2_ccerr_set_application_error(ngtcp2_ccerr *ccerr, * * If :member:`ccerr->type ` == * :enum:`ngtcp2_ccerr_type.NGTCP2_CCERR_TYPE_TRANSPORT`, this - * function sends CONNECTION_CLOSE (type 0x1c) frame. If + * function sends CONNECTION_CLOSE (type 0x1C) frame. If * :member:`ccerr->type ` == * :enum:`ngtcp2_ccerr_type.NGTCP2_CCERR_TYPE_APPLICATION`, it sends - * CONNECTION_CLOSE (type 0x1d) frame. Otherwise, it does not produce + * CONNECTION_CLOSE (type 0x1D) frame. Otherwise, it does not produce * any data, and returns 0. * * |destlen| could be shorten by some factors (e.g., server side * amplification limit). This function returns * :macro:`NGTCP2_ERR_NOBUF` if the resulting buffer is too small even - * if the given buffer has enough space. + * if the given buffer has enough space. This can happen if sending a + * packet would exceed a transmission limit (e.g., for amplification + * attack protection). * * This function must not be called from inside the callback * functions. @@ -5518,7 +6553,8 @@ NGTCP2_EXTERN void ngtcp2_ccerr_set_application_error(ngtcp2_ccerr *ccerr, * :macro:`NGTCP2_ERR_NOMEM` * Out of memory * :macro:`NGTCP2_ERR_NOBUF` - * Buffer is too small + * Buffer is too small or packet would exceed the transmission + * limit (e.g., for amplification attack protection). * :macro:`NGTCP2_ERR_INVALID_STATE` * The current state does not allow sending CONNECTION_CLOSE * frame. @@ -5535,6 +6571,11 @@ NGTCP2_EXTERN ngtcp2_ssize ngtcp2_conn_write_connection_close_versioned( /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_ccerr2` instead. + * * `ngtcp2_conn_get_ccerr` returns the received connection close * error. If no connection error is received, it returns * :type:`ngtcp2_ccerr` that is initialized by `ngtcp2_ccerr_default`. @@ -5544,6 +6585,23 @@ NGTCP2_EXTERN const ngtcp2_ccerr *ngtcp2_conn_get_ccerr(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_ccerr2` returns the received connection close + * error. If no connection error is received, it returns + * :type:`ngtcp2_ccerr` that is initialized by `ngtcp2_ccerr_default`. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN const ngtcp2_ccerr * +ngtcp2_conn_get_ccerr2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_is_local_stream2` instead. + * * `ngtcp2_conn_is_local_stream` returns nonzero if |stream_id| * denotes a locally initiated stream. */ @@ -5553,6 +6611,22 @@ NGTCP2_EXTERN int ngtcp2_conn_is_local_stream(ngtcp2_conn *conn, /** * @function * + * `ngtcp2_conn_is_local_stream2` returns nonzero if |stream_id| + * denotes a locally initiated stream. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN int ngtcp2_conn_is_local_stream2(const ngtcp2_conn *conn, + int64_t stream_id); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_is_server2` instead. + * * `ngtcp2_conn_is_server` returns nonzero if |conn| is initialized as * server. */ @@ -5561,11 +6635,37 @@ NGTCP2_EXTERN int ngtcp2_conn_is_server(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_is_server2` returns nonzero if |conn| is initialized + * as server. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN int ngtcp2_conn_is_server2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_after_retry2` instead. + * * `ngtcp2_conn_after_retry` returns nonzero if |conn| as a client has * received Retry packet from server, and successfully validated it. */ NGTCP2_EXTERN int ngtcp2_conn_after_retry(ngtcp2_conn *conn); +/** + * @function + * + * `ngtcp2_conn_after_retry2` returns nonzero if |conn| as a client + * has received Retry packet from server, and successfully validated + * it. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN int ngtcp2_conn_after_retry2(const ngtcp2_conn *conn); + /** * @function * @@ -5582,6 +6682,51 @@ NGTCP2_EXTERN int ngtcp2_conn_set_stream_user_data(ngtcp2_conn *conn, int64_t stream_id, void *stream_user_data); +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_stream_user_data2` instead. + * + * `ngtcp2_conn_get_stream_user_data` returns stream_user_data + * associated to the stream identified by |stream_id|. If the stream + * is not found, or no stream data is associated to the stream, this + * function returns NULL. + * + * The stream_user_data can be associated to the stream by one of the + * following functions: + * + * - `ngtcp2_conn_open_bidi_stream` + * - `ngtcp2_conn_open_uni_stream` + * - `ngtcp2_conn_set_stream_user_data` + * + * .. version-added:: 1.17.0 + */ +NGTCP2_EXTERN void *ngtcp2_conn_get_stream_user_data(ngtcp2_conn *conn, + int64_t stream_id); + +/** + * @function + * + * `ngtcp2_conn_get_stream_user_data2` returns stream_user_data + * associated to the stream identified by |stream_id|. If the stream + * is not found, or no stream data is associated to the stream, this + * function returns NULL. + * + * The stream_user_data can be associated to the stream by one of the + * following functions: + * + * - `ngtcp2_conn_open_bidi_stream` + * - `ngtcp2_conn_open_uni_stream` + * - `ngtcp2_conn_set_stream_user_data` + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN void *ngtcp2_conn_get_stream_user_data2(const ngtcp2_conn *conn, + int64_t stream_id); + /** * @function * @@ -5598,6 +6743,11 @@ NGTCP2_EXTERN void ngtcp2_conn_update_pkt_tx_time(ngtcp2_conn *conn, /** * @function * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_send_quantum2` instead. + * * `ngtcp2_conn_get_send_quantum` returns the maximum number of bytes * that can be sent in one go without packet spacing. */ @@ -5606,6 +6756,21 @@ NGTCP2_EXTERN size_t ngtcp2_conn_get_send_quantum(ngtcp2_conn *conn); /** * @function * + * `ngtcp2_conn_get_send_quantum2` returns the maximum number of bytes + * that can be sent in one go without packet spacing. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN size_t ngtcp2_conn_get_send_quantum2(const ngtcp2_conn *conn); + +/** + * @function + * + * .. warning:: + * + * .. version-deprecated:: 1.23.0 + * Use `ngtcp2_conn_get_stream_loss_count2` instead. + * * `ngtcp2_conn_get_stream_loss_count` returns the number of packets * that contain STREAM frame for a stream identified by |stream_id| * and are declared to be lost. The number may include the spurious @@ -5615,6 +6780,20 @@ NGTCP2_EXTERN size_t ngtcp2_conn_get_send_quantum(ngtcp2_conn *conn); NGTCP2_EXTERN size_t ngtcp2_conn_get_stream_loss_count(ngtcp2_conn *conn, int64_t stream_id); +/** + * @function + * + * `ngtcp2_conn_get_stream_loss_count2` returns the number of packets + * that contain STREAM frame for a stream identified by |stream_id| + * and are declared to be lost. The number may include the spurious + * losses. If no stream identified by |stream_id| is found, this + * function returns 0. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN size_t ngtcp2_conn_get_stream_loss_count2(const ngtcp2_conn *conn, + int64_t stream_id); + /** * @functypedef * @@ -5643,7 +6822,7 @@ NGTCP2_EXTERN size_t ngtcp2_conn_get_stream_loss_count(ngtcp2_conn *conn, * :macro:`NGTCP2_WRITE_DATAGRAM_FLAG_PADDING` if * `ngtcp2_conn_writev_datagram` is used) is recommended. * - * This callback function has been available since v1.15.0. + * .. version-added:: 1.15.0 */ typedef ngtcp2_ssize (*ngtcp2_write_pkt)(ngtcp2_conn *conn, ngtcp2_path *path, ngtcp2_pkt_info *pi, uint8_t *dest, @@ -5658,34 +6837,77 @@ typedef ngtcp2_ssize (*ngtcp2_write_pkt)(ngtcp2_conn *conn, ngtcp2_path *path, * sent at once in GSO. This function returns the number of bytes * written to the buffer pointed by |buf| of length |buflen|. * |buflen| must be at least - * `ngtcp2_conn_get_path_max_tx_udp_payload_size(conn) - * ` bytes long. It is + * `ngtcp2_conn_get_path_max_tx_udp_payload_size2(conn) + * ` bytes long. It is * recommended to pass the buffer at least - * `ngtcp2_conn_get_max_tx_udp_payload_size(conn) - * ` bytes in order to send a - * PMTUD packet. This function only writes multiple packets if the - * first packet is `ngtcp2_conn_get_path_max_tx_udp_payload_size(conn) - * ` bytes long. The + * `ngtcp2_conn_get_max_tx_udp_payload_size2(conn) + * ` bytes in order to send + * a PMTUD packet. This function only writes multiple packets if the + * first packet is + * `ngtcp2_conn_get_path_max_tx_udp_payload_size2(conn) + * ` bytes long. The * application can adjust the length of the buffer to limit the number - * of packets to aggregate. If this function returns positive - * integer, all packets share the same :type:`ngtcp2_path` and - * :type:`ngtcp2_pkt_info` values, and they are assigned to the - * objects pointed by |path| and |pi| respectively. The length of all - * packets other than the last packet is assigned to |*pgsolen|. The - * length of last packet is equal to or less than |*pgsolen|. - * |write_pkt| must write a single packet. After all packets are - * written, this function calls `ngtcp2_conn_update_pkt_tx_time`. + * of packets to aggregate (or use `ngtcp2_conn_write_aggregate_pkt2` + * to control the number of packets to write directly). If this + * function returns positive integer, all packets share the same + * :type:`ngtcp2_path` and :type:`ngtcp2_pkt_info` values, and they + * are assigned to the objects pointed by |path| and |pi| + * respectively. The length of all packets other than the last packet + * is assigned to |*pgsolen|. The length of last packet is equal to + * or less than |*pgsolen|. |write_pkt| must write a single packet. + * After all packets are written, this function calls + * `ngtcp2_conn_update_pkt_tx_time`. + * + * This function is equivalent to call + * `ngtcp2_conn_write_aggregate_pkt2` with |buflen| = min(|buflen|, + * `ngtcp2_conn_get_send_quantum2(conn) + * `) and |num_pkts| = 0 followed by + * `ngtcp2_conn_update_pkt_tx_time(conn) + * `. * * This function returns the number of bytes written to the buffer, or * a negative error code returned by |write_pkt|. * - * This function has been available since v1.15.0. + * .. version-added:: 1.15.0 */ NGTCP2_EXTERN ngtcp2_ssize ngtcp2_conn_write_aggregate_pkt_versioned( ngtcp2_conn *conn, ngtcp2_path *path, int pkt_info_version, ngtcp2_pkt_info *pi, uint8_t *buf, size_t buflen, size_t *pgsolen, ngtcp2_write_pkt write_pkt, ngtcp2_tstamp ts); +/** + * @function + * + * `ngtcp2_conn_write_aggregate_pkt2` behaves like + * `ngtcp2_conn_write_aggregate_pkt`, but it accepts |num_pkts| to + * specify the maximum number of packets to write. If |num_pkts| is + * 0, this function writes packets as much as possible. The actual + * number of packets to write is determined by the connection state + * (e.g., the congestion controller, data available to send) and the + * length of packet produced. It also does not clamp |buflen|, and + * does not call `ngtcp2_conn_update_pkt_tx_time`. + * + * This function offers more flexibility and optimization chances to + * an application. It can experiment different GSO buffer size + * strategy and number of GSO writes per event loop. + * + * .. version-added:: 1.17.0 + */ +NGTCP2_EXTERN ngtcp2_ssize ngtcp2_conn_write_aggregate_pkt2_versioned( + ngtcp2_conn *conn, ngtcp2_path *path, int pkt_info_version, + ngtcp2_pkt_info *pi, uint8_t *buf, size_t buflen, size_t *pgsolen, + ngtcp2_write_pkt write_pkt, size_t num_pkts, ngtcp2_tstamp ts); + +/** + * @function + * + * `ngtcp2_conn_get_timestamp` returns the latest timestamp that is + * known to |conn|. + * + * .. version-added:: 1.16.0 + */ +NGTCP2_EXTERN ngtcp2_tstamp ngtcp2_conn_get_timestamp(const ngtcp2_conn *conn); + /** * @function * @@ -5778,9 +7000,9 @@ NGTCP2_EXTERN void ngtcp2_path_storage_zero(ngtcp2_path_storage *ps); * * :member:`handshake_timeout ` = * ``UINT64_MAX`` * * :member:`glitch_ratelim_burst - * ` = 1000 + * ` = 10000 * * :member:`glitch_ratelim_rate - * ` = 33 + * ` = 330 */ NGTCP2_EXTERN void ngtcp2_settings_default_versioned(int settings_version, ngtcp2_settings *settings); @@ -5928,6 +7150,25 @@ NGTCP2_EXTERN uint32_t ngtcp2_select_version(const uint32_t *preferred_versions, const uint32_t *offered_versions, size_t offered_versionslen); +/** + * @function + * + * `ngtcp2_secure_clear` writes |len| bytes of zeros into the buffer + * pointed by |data|. It does that by avoiding compiler + * optimizations. If the following functions are available, one of + * them is used: + * + * - ``SecureZeroMemory`` + * - ``explicit_bzero`` + * - ``memset_s`` + * + * Otherwise, it uses volatile function pointer to ``memset`` to fill + * zeros. + * + * .. version-added:: 1.23.0 + */ +NGTCP2_EXTERN void ngtcp2_secure_clear(void *data, size_t len); + /* * Versioned function wrappers */ @@ -6072,6 +7313,15 @@ NGTCP2_EXTERN uint32_t ngtcp2_select_version(const uint32_t *preferred_versions, #define ngtcp2_conn_get_conn_info(CONN, CINFO) \ ngtcp2_conn_get_conn_info_versioned((CONN), NGTCP2_CONN_INFO_VERSION, (CINFO)) +/* + * `ngtcp2_conn_get_conn_info2` is a wrapper around + * `ngtcp2_conn_get_conn_info2_versioned` to set the correct struct + * version. + */ +#define ngtcp2_conn_get_conn_info2(CONN, CINFO) \ + ngtcp2_conn_get_conn_info2_versioned((CONN), NGTCP2_CONN_INFO_VERSION, \ + (CINFO)) + /* * `ngtcp2_conn_write_aggregate_pkt` is a wrapper around * `ngtcp2_conn_write_aggregate_pkt_versioned` to set the correct @@ -6083,6 +7333,17 @@ NGTCP2_EXTERN uint32_t ngtcp2_select_version(const uint32_t *preferred_versions, (CONN), (PATH), NGTCP2_PKT_INFO_VERSION, (PI), (BUF), (BUFLEN), (PGSOLEN), \ (WRITE_PKT), (TS)) +/* + * `ngtcp2_conn_write_aggregate_pkt2` is a wrapper around + * `ngtcp2_conn_write_aggregate_pkt2_versioned` to set the correct + * struct version. + */ +#define ngtcp2_conn_write_aggregate_pkt2(CONN, PATH, PI, BUF, BUFLEN, PGSOLEN, \ + WRITE_PKT, NUM_PKTS, TS) \ + ngtcp2_conn_write_aggregate_pkt2_versioned( \ + (CONN), (PATH), NGTCP2_PKT_INFO_VERSION, (PI), (BUF), (BUFLEN), (PGSOLEN), \ + (WRITE_PKT), (NUM_PKTS), (TS)) + /* * `ngtcp2_settings_default` is a wrapper around * `ngtcp2_settings_default_versioned` to set the correct struct diff --git a/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/version.h b/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/version.h index e70a095ceb1300..7a47cdaba0bdb9 100644 --- a/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/version.h +++ b/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/version.h @@ -36,7 +36,7 @@ * * Version number of the ngtcp2 library release. */ -#define NGTCP2_VERSION "1.15.1" +#define NGTCP2_VERSION "1.23.0" /** * @macro @@ -46,6 +46,6 @@ * number, 8 bits for minor and 8 bits for patch. Version 1.2.3 * becomes 0x010203. */ -#define NGTCP2_VERSION_NUM 0x010f01 +#define NGTCP2_VERSION_NUM 0x011700 #endif /* !defined(NGTCP2_VERSION_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.c index 59bc621ef46b28..2f194ab13105a3 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.c @@ -34,9 +34,11 @@ ngtcp2_objalloc_def(acktr_entry, ngtcp2_acktr_entry, oplent) static void acktr_entry_init(ngtcp2_acktr_entry *ent, int64_t pkt_num, ngtcp2_tstamp tstamp) { - ent->pkt_num = pkt_num; - ent->len = 1; - ent->tstamp = tstamp; + *ent = (ngtcp2_acktr_entry){ + .pkt_num = pkt_num, + .len = 1, + .tstamp = tstamp, + }; } int ngtcp2_acktr_entry_objalloc_new(ngtcp2_acktr_entry **ent, int64_t pkt_num, @@ -219,8 +221,10 @@ ngtcp2_acktr_ack_entry *ngtcp2_acktr_add_ack(ngtcp2_acktr *acktr, int64_t largest_ack) { ngtcp2_acktr_ack_entry *ent = ngtcp2_ringbuf_push_front(&acktr->acks.rb); - ent->largest_ack = largest_ack; - ent->pkt_num = pkt_num; + *ent = (ngtcp2_acktr_ack_entry){ + .largest_ack = largest_ack, + .pkt_num = pkt_num, + }; return ent; } @@ -333,16 +337,14 @@ void ngtcp2_acktr_immediate_ack(ngtcp2_acktr *acktr) { acktr->flags |= NGTCP2_ACKTR_FLAG_IMMEDIATE_ACK; } -ngtcp2_frame *ngtcp2_acktr_create_ack_frame(ngtcp2_acktr *acktr, - ngtcp2_frame *fr, uint8_t type, - ngtcp2_tstamp ts, - ngtcp2_duration ack_delay, - uint64_t ack_delay_exponent) { +int ngtcp2_acktr_create_ack_frame(ngtcp2_acktr *acktr, ngtcp2_ack *ack, + uint8_t type, ngtcp2_tstamp ts, + ngtcp2_duration ack_delay, + uint64_t ack_delay_exponent) { int64_t last_pkt_num; ngtcp2_ack_range *range; ngtcp2_ksl_it it; ngtcp2_acktr_entry *rpkt; - ngtcp2_ack *ack = &fr->ack; ngtcp2_tstamp largest_ack_ts; size_t num_acks; @@ -351,13 +353,13 @@ ngtcp2_frame *ngtcp2_acktr_create_ack_frame(ngtcp2_acktr *acktr, } if (!ngtcp2_acktr_require_active_ack(acktr, ack_delay, ts)) { - return NULL; + return -1; } it = ngtcp2_acktr_get(acktr); if (ngtcp2_ksl_it_end(&it)) { ngtcp2_acktr_commit_ack(acktr); - return NULL; + return -1; } num_acks = ngtcp2_ksl_len(&acktr->ents); @@ -408,7 +410,7 @@ ngtcp2_frame *ngtcp2_acktr_create_ack_frame(ngtcp2_acktr *acktr, ack->ack_delay = 0; } - num_acks = ngtcp2_min_size(num_acks, NGTCP2_MAX_ACK_RANGES); + num_acks = ngtcp2_min(num_acks, NGTCP2_MAX_ACK_RANGES); for (; ack->rangecnt < num_acks; ngtcp2_ksl_it_next(&it)) { rpkt = ngtcp2_ksl_it_get(&it); @@ -420,7 +422,7 @@ ngtcp2_frame *ngtcp2_acktr_create_ack_frame(ngtcp2_acktr *acktr, last_pkt_num = rpkt->pkt_num - (int64_t)(rpkt->len - 1); } - return fr; + return 0; } void ngtcp2_acktr_increase_ecn_counts(ngtcp2_acktr *acktr, diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.h index cf75a774db3e82..026c2cabe8733b 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.h @@ -97,16 +97,16 @@ typedef struct ngtcp2_acktr_ack_entry { } ngtcp2_acktr_ack_entry; /* NGTCP2_ACKTR_FLAG_NONE indicates that no flag set. */ -#define NGTCP2_ACKTR_FLAG_NONE 0x00u +#define NGTCP2_ACKTR_FLAG_NONE 0x00U /* NGTCP2_ACKTR_FLAG_IMMEDIATE_ACK indicates that immediate acknowledgement is required. */ -#define NGTCP2_ACKTR_FLAG_IMMEDIATE_ACK 0x01u +#define NGTCP2_ACKTR_FLAG_IMMEDIATE_ACK 0x01U /* NGTCP2_ACKTR_FLAG_ACTIVE_ACK indicates that there are pending protected packet to be acknowledged. */ -#define NGTCP2_ACKTR_FLAG_ACTIVE_ACK 0x02u +#define NGTCP2_ACKTR_FLAG_ACTIVE_ACK 0x02U /* NGTCP2_ACKTR_FLAG_CANCEL_TIMER is set when ACK delay timer is expired and canceled. */ -#define NGTCP2_ACKTR_FLAG_CANCEL_TIMER 0x0100u +#define NGTCP2_ACKTR_FLAG_CANCEL_TIMER 0x0100U ngtcp2_static_ringbuf_def(acks, 32, sizeof(ngtcp2_acktr_ack_entry)) @@ -120,8 +120,6 @@ typedef struct ngtcp2_acktr { packet number. */ ngtcp2_ksl ents; ngtcp2_log *log; - /* flags is bitwise OR of zero, or more of NGTCP2_ACKTR_FLAG_*. */ - uint16_t flags; /* first_unacked_ts is timestamp when ngtcp2_acktr_entry is added first time after the last outgoing ACK frame. */ ngtcp2_tstamp first_unacked_ts; @@ -148,6 +146,9 @@ typedef struct ngtcp2_acktr { uint64_t ce; } ack; } ecn; + + /* flags is bitwise OR of zero, or more of NGTCP2_ACKTR_FLAG_*. */ + uint16_t flags; } ngtcp2_acktr; /* @@ -235,19 +236,18 @@ void ngtcp2_acktr_immediate_ack(ngtcp2_acktr *acktr); /* * ngtcp2_acktr_create_ack_frame creates ACK frame in the object - * pointed by |fr|, and returns |fr| if there are any received packets - * to acknowledge. If there are no packets to acknowledge, this - * function returns NULL. fr->ack.ranges must be able to contain at + * pointed by |ack|, and returns 0 if it successfully creates ACK + * frame in |ack|. If there are no packets to acknowledge, this + * function returns -1. |ack|->ranges must be able to contain at * least NGTCP2_MAX_ACK_RANGES elements. * * Call ngtcp2_acktr_commit_ack after a created ACK frame is * successfully serialized into a packet. */ -ngtcp2_frame *ngtcp2_acktr_create_ack_frame(ngtcp2_acktr *acktr, - ngtcp2_frame *fr, uint8_t type, - ngtcp2_tstamp ts, - ngtcp2_duration ack_delay, - uint64_t ack_delay_exponent); +int ngtcp2_acktr_create_ack_frame(ngtcp2_acktr *acktr, ngtcp2_ack *ack, + uint8_t type, ngtcp2_tstamp ts, + ngtcp2_duration ack_delay, + uint64_t ack_delay_exponent); /* * ngtcp2_acktr_increase_ecn_counts increases ECN counts from |pi|. diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_addr.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_addr.c index 1fb273d494e8e2..58694e3836e42e 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_addr.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_addr.c @@ -31,8 +31,11 @@ ngtcp2_addr *ngtcp2_addr_init(ngtcp2_addr *dest, const ngtcp2_sockaddr *addr, ngtcp2_socklen addrlen) { - dest->addrlen = addrlen; - dest->addr = (ngtcp2_sockaddr *)addr; + *dest = (ngtcp2_addr){ + .addr = (ngtcp2_sockaddr *)addr, + .addrlen = addrlen, + }; + return dest; } @@ -58,14 +61,12 @@ int ngtcp2_sockaddr_eq(const ngtcp2_sockaddr *a, const ngtcp2_sockaddr *b) { switch (a->sa_family) { case NGTCP2_AF_INET: { - const ngtcp2_sockaddr_in *ai = (const ngtcp2_sockaddr_in *)(void *)a, - *bi = (const ngtcp2_sockaddr_in *)(void *)b; + const ngtcp2_sockaddr_in *ai = (void *)a, *bi = (void *)b; return ai->sin_port == bi->sin_port && memcmp(&ai->sin_addr, &bi->sin_addr, sizeof(ai->sin_addr)) == 0; } case NGTCP2_AF_INET6: { - const ngtcp2_sockaddr_in6 *ai = (const ngtcp2_sockaddr_in6 *)(void *)a, - *bi = (const ngtcp2_sockaddr_in6 *)(void *)b; + const ngtcp2_sockaddr_in6 *ai = (void *)a, *bi = (void *)b; return ai->sin6_port == bi->sin6_port && memcmp(&ai->sin6_addr, &bi->sin6_addr, sizeof(ai->sin6_addr)) == 0; } @@ -89,8 +90,7 @@ uint32_t ngtcp2_addr_cmp(const ngtcp2_addr *aa, const ngtcp2_addr *bb) { switch (a->sa_family) { case NGTCP2_AF_INET: { - const ngtcp2_sockaddr_in *ai = (const ngtcp2_sockaddr_in *)(void *)a, - *bi = (const ngtcp2_sockaddr_in *)(void *)b; + const ngtcp2_sockaddr_in *ai = (void *)a, *bi = (void *)b; if (memcmp(&ai->sin_addr, &bi->sin_addr, sizeof(ai->sin_addr))) { flags |= NGTCP2_ADDR_CMP_FLAG_ADDR; } @@ -100,8 +100,7 @@ uint32_t ngtcp2_addr_cmp(const ngtcp2_addr *aa, const ngtcp2_addr *bb) { return flags; } case NGTCP2_AF_INET6: { - const ngtcp2_sockaddr_in6 *ai = (const ngtcp2_sockaddr_in6 *)(void *)a, - *bi = (const ngtcp2_sockaddr_in6 *)(void *)b; + const ngtcp2_sockaddr_in6 *ai = (void *)a, *bi = (void *)b; if (memcmp(&ai->sin6_addr, &bi->sin6_addr, sizeof(ai->sin6_addr))) { flags |= NGTCP2_ADDR_CMP_FLAG_ADDR; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_addr.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_addr.h index c2224f85cd9c11..6314b1afc81270 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_addr.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_addr.h @@ -46,14 +46,14 @@ void ngtcp2_addr_copy(ngtcp2_addr *dest, const ngtcp2_addr *src); int ngtcp2_addr_eq(const ngtcp2_addr *a, const ngtcp2_addr *b); /* NGTCP2_ADDR_CMP_FLAG_NONE indicates that no flag set. */ -#define NGTCP2_ADDR_CMP_FLAG_NONE 0x0u +#define NGTCP2_ADDR_CMP_FLAG_NONE 0x0U /* NGTCP2_ADDR_CMP_FLAG_ADDR indicates IP addresses do not match. */ -#define NGTCP2_ADDR_CMP_FLAG_ADDR 0x1u +#define NGTCP2_ADDR_CMP_FLAG_ADDR 0x1U /* NGTCP2_ADDR_CMP_FLAG_PORT indicates ports do not match. */ -#define NGTCP2_ADDR_CMP_FLAG_PORT 0x2u +#define NGTCP2_ADDR_CMP_FLAG_PORT 0x2U /* NGTCP2_ADDR_CMP_FLAG_FAMILY indicates address families do not match. */ -#define NGTCP2_ADDR_CMP_FLAG_FAMILY 0x4u +#define NGTCP2_ADDR_CMP_FLAG_FAMILY 0x4U /* * ngtcp2_addr_cmp compares address and port between |a| and |b|, and diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_balloc.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_balloc.c index 4a6797689fcc01..ea72eb8314325b 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_balloc.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_balloc.c @@ -30,7 +30,7 @@ void ngtcp2_balloc_init(ngtcp2_balloc *balloc, size_t blklen, const ngtcp2_mem *mem) { - assert((blklen & 0xfu) == 0); + assert((blklen & 0xFU) == 0); balloc->mem = mem; balloc->blklen = blklen; @@ -66,25 +66,25 @@ int ngtcp2_balloc_get(ngtcp2_balloc *balloc, void **pbuf, size_t n) { if (ngtcp2_buf_left(&balloc->buf) < n) { p = ngtcp2_mem_malloc(balloc->mem, - sizeof(ngtcp2_memblock_hd) + 0x8u + balloc->blklen); + sizeof(ngtcp2_memblock_hd) + 0x8U + balloc->blklen); if (p == NULL) { return NGTCP2_ERR_NOMEM; } - hd = (ngtcp2_memblock_hd *)(void *)p; + hd = (void *)p; hd->next = balloc->head; balloc->head = hd; ngtcp2_buf_init( &balloc->buf, - (uint8_t *)(((uintptr_t)p + sizeof(ngtcp2_memblock_hd) + 0xfu) & - ~(uintptr_t)0xfu), + (uint8_t *)(((uintptr_t)p + sizeof(ngtcp2_memblock_hd) + 0xFU) & + ~(uintptr_t)0xFU), balloc->blklen); } - assert(((uintptr_t)balloc->buf.last & 0xfu) == 0); + assert(((uintptr_t)balloc->buf.last & 0xFU) == 0); *pbuf = balloc->buf.last; - balloc->buf.last += (n + 0xfu) & ~(uintptr_t)0xfu; + balloc->buf.last += (n + 0xFU) & ~(size_t)0xFU; return 0; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.c index 44be1e189b8835..cdeb29bb506523 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.c @@ -62,6 +62,8 @@ #define NGTCP2_BBR_PACING_MARGIN_PERCENT 1 +#define NGTCP2_BBR_MAX_DRAIN_ROUNDS 3 + static void bbr_on_init(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, ngtcp2_tstamp initial_ts); @@ -76,13 +78,15 @@ static void bbr_init_round_counting(ngtcp2_cc_bbr *bbr); static void bbr_reset_full_bw(ngtcp2_cc_bbr *bbr); -static void bbr_init_pacing_rate(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); +static void bbr_init_pacing_rate(const ngtcp2_cc_bbr *bbr, + ngtcp2_conn_stat *cstat); -static void bbr_set_pacing_rate_with_gain(ngtcp2_cc_bbr *bbr, +static void bbr_set_pacing_rate_with_gain(const ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, uint64_t pacing_gain_h); -static void bbr_set_pacing_rate(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); +static void bbr_set_pacing_rate(const ngtcp2_cc_bbr *bbr, + ngtcp2_conn_stat *cstat); static void bbr_enter_startup(ngtcp2_cc_bbr *bbr); @@ -100,47 +104,47 @@ static void bbr_update_control_parameters(ngtcp2_cc_bbr *cc, ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack); -static void bbr_update_on_loss(ngtcp2_cc_bbr *cc, ngtcp2_conn_stat *cstat, - const ngtcp2_cc_pkt *pkt, ngtcp2_tstamp ts); - static void bbr_update_latest_delivery_signals(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat); + const ngtcp2_conn_stat *cstat); static void bbr_advance_latest_delivery_signals(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat); + const ngtcp2_conn_stat *cstat); static void bbr_update_congestion_signals(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack); -static void bbr_adapt_lower_bounds_from_congestion(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat); +static void +bbr_adapt_lower_bounds_from_congestion(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat); -static void bbr_init_lower_bounds(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); +static void bbr_init_lower_bounds(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat); static void bbr_loss_lower_bounds(ngtcp2_cc_bbr *bbr); static void bbr_bound_bw_for_model(ngtcp2_cc_bbr *bbr); -static void bbr_update_max_bw(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, +static void bbr_update_max_bw(ngtcp2_cc_bbr *bbr, const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack); static void bbr_update_round(ngtcp2_cc_bbr *bbr, const ngtcp2_cc_ack *ack); static void bbr_start_round(ngtcp2_cc_bbr *bbr); -static int bbr_is_in_probe_bw_state(ngtcp2_cc_bbr *bbr); +static int bbr_is_in_probe_bw_state(const ngtcp2_cc_bbr *bbr); -static int bbr_is_probing_bw(ngtcp2_cc_bbr *bbr); +static int bbr_is_probing_bw(const ngtcp2_cc_bbr *bbr); static void bbr_update_ack_aggregation(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack, ngtcp2_tstamp ts); static void bbr_enter_drain(ngtcp2_cc_bbr *bbr); -static void bbr_check_drain_done(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, +static void bbr_check_drain_done(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat, ngtcp2_tstamp ts); static void bbr_enter_probe_bw(ngtcp2_cc_bbr *bbr, ngtcp2_tstamp ts); @@ -151,59 +155,70 @@ static void bbr_start_probe_bw_cruise(ngtcp2_cc_bbr *bbr); static void bbr_start_probe_bw_refill(ngtcp2_cc_bbr *bbr); -static void bbr_start_probe_bw_up(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); +static void bbr_start_probe_bw_up(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat); static void bbr_update_probe_bw_cycle_phase(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack, ngtcp2_tstamp ts); -static int bbr_is_time_to_cruise(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); +static int bbr_is_time_to_cruise(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat); -static int bbr_is_time_to_go_down(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); +static int bbr_is_time_to_go_down(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat); -static int bbr_has_elapsed_in_phase(ngtcp2_cc_bbr *bbr, +static int bbr_has_elapsed_in_phase(const ngtcp2_cc_bbr *bbr, ngtcp2_duration interval, ngtcp2_tstamp ts); -static uint64_t bbr_inflight_with_headroom(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat); +static uint64_t bbr_inflight_with_headroom(const ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat); static void bbr_raise_inflight_longterm_slope(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat); + const ngtcp2_conn_stat *cstat); static void bbr_probe_inflight_longterm_upward(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack); static void bbr_adapt_longterm_model(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack); -static int bbr_is_time_to_probe_bw(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, +static int bbr_is_time_to_probe_bw(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat, ngtcp2_tstamp ts); static void bbr_pick_probe_wait(ngtcp2_cc_bbr *bbr); -static int bbr_is_reno_coexistence_probe_time(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat); +static int bbr_is_reno_coexistence_probe_time(const ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat); -static uint64_t bbr_target_inflight(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat); +static uint64_t bbr_target_inflight(const ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat); -static int bbr_is_inflight_too_high(ngtcp2_cc_bbr *bbr); +static int bbr_is_inflight_too_high(const ngtcp2_cc_bbr *bbr, + const ngtcp2_rs *rs); static void bbr_handle_inflight_too_high(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, - ngtcp2_tstamp ts); + const ngtcp2_conn_stat *cstat, + const ngtcp2_rs *rs, ngtcp2_tstamp ts); static void bbr_note_loss(ngtcp2_cc_bbr *bbr); -static void bbr_handle_lost_packet(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, +static void bbr_save_state_upon_loss(ngtcp2_cc_bbr *bbr); + +static void bbr_handle_spurious_loss_detection(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat); + +static void bbr_handle_lost_packet(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_pkt *pkt, ngtcp2_tstamp ts); -static uint64_t -bbr_inflight_longterm_from_lost_packet(ngtcp2_cc_bbr *bbr, - const ngtcp2_cc_pkt *pkt); +static uint64_t bbr_inflight_at_loss(const ngtcp2_cc_bbr *bbr, + const ngtcp2_cc_pkt *pkt, + const ngtcp2_rs *rs); static void bbr_update_min_rtt(ngtcp2_cc_bbr *bbr, const ngtcp2_cc_ack *ack, ngtcp2_tstamp ts); @@ -220,7 +235,7 @@ static void bbr_check_probe_rtt_done(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, ngtcp2_tstamp ts); static void bbr_mark_connection_app_limited(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat); + const ngtcp2_conn_stat *cstat); static void bbr_exit_probe_rtt(ngtcp2_cc_bbr *bbr, ngtcp2_tstamp ts); @@ -231,27 +246,28 @@ static void bbr_handle_restart_from_idle(ngtcp2_cc_bbr *bbr, static uint64_t bbr_bdp_multiple(ngtcp2_cc_bbr *bbr, uint64_t gain_h); static uint64_t bbr_quantization_budget(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, uint64_t inflight); -static uint64_t bbr_inflight(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, +static uint64_t bbr_inflight(ngtcp2_cc_bbr *bbr, const ngtcp2_conn_stat *cstat, uint64_t gain_h); static void bbr_update_max_inflight(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat); + const ngtcp2_conn_stat *cstat); static void bbr_update_offload_budget(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat); + const ngtcp2_conn_stat *cstat); static uint64_t min_pipe_cwnd(size_t max_udp_payload_size); static void bbr_advance_max_bw_filter(ngtcp2_cc_bbr *bbr); -static void bbr_save_cwnd(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); +static void bbr_save_cwnd(ngtcp2_cc_bbr *bbr, const ngtcp2_conn_stat *cstat); -static void bbr_restore_cwnd(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); +static void bbr_restore_cwnd(const ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); -static uint64_t bbr_probe_rtt_cwnd(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); +static uint64_t bbr_probe_rtt_cwnd(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat); static void bbr_bound_cwnd_for_probe_rtt(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); @@ -259,10 +275,11 @@ static void bbr_bound_cwnd_for_probe_rtt(ngtcp2_cc_bbr *bbr, static void bbr_set_cwnd(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack); -static void bbr_bound_cwnd_for_model(ngtcp2_cc_bbr *bbr, +static void bbr_bound_cwnd_for_model(const ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); -static void bbr_set_send_quantum(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat); +static void bbr_set_send_quantum(const ngtcp2_cc_bbr *bbr, + ngtcp2_conn_stat *cstat); static int in_congestion_recovery(const ngtcp2_conn_stat *cstat, ngtcp2_tstamp sent_time); @@ -334,9 +351,13 @@ static void bbr_on_init(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, bbr->max_inflight = 0; - bbr->congestion_recovery_start_ts = UINT64_MAX; - bbr->bdp = 0; + bbr->drain_start_round = 0; + + bbr->undo_state = 0; + bbr->undo_bw_shortterm = 0; + bbr->undo_inflight_shortterm = 0; + bbr->undo_inflight_longterm = 0; } static void bbr_reset_congestion_signals(ngtcp2_cc_bbr *bbr) { @@ -363,7 +384,7 @@ static void bbr_reset_full_bw(ngtcp2_cc_bbr *bbr) { } static void bbr_check_full_bw_reached(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { + const ngtcp2_conn_stat *cstat) { if (bbr->full_bw_now || !bbr->round_start || bbr->rst->rs.is_app_limited) { return; } @@ -384,32 +405,34 @@ static void bbr_check_full_bw_reached(ngtcp2_cc_bbr *bbr, bbr->full_bw_reached = 1; - ngtcp2_log_info(bbr->cc.log, NGTCP2_LOG_EVENT_CCA, - "bbr reached full bandwidth, full_bw=%" PRIu64, bbr->full_bw); + ngtcp2_log_infof(bbr->cc.log, NGTCP2_LOG_EVENT_CCA, + "bbr reached full bandwidth, full_bw=", bbr->full_bw); } static void bbr_check_startup_high_loss(ngtcp2_cc_bbr *bbr) { if (bbr->full_bw_reached || bbr->loss_events_in_round <= 6 || (bbr->in_loss_recovery && bbr->round_count <= bbr->round_count_at_recovery) || - !bbr_is_inflight_too_high(bbr)) { + !bbr_is_inflight_too_high(bbr, &bbr->rst->rs)) { return; } bbr->full_bw_reached = 1; - bbr->inflight_longterm = ngtcp2_max_uint64( - bbr_bdp_multiple(bbr, bbr->cwnd_gain_h), bbr->inflight_latest); + bbr->inflight_longterm = + ngtcp2_max(bbr_bdp_multiple(bbr, bbr->cwnd_gain_h), bbr->inflight_latest); } -static void bbr_init_pacing_rate(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { +static void bbr_init_pacing_rate(const ngtcp2_cc_bbr *bbr, + ngtcp2_conn_stat *cstat) { cstat->pacing_interval_m = - ((cstat->first_rtt_sample_ts == UINT64_MAX ? NGTCP2_MILLISECONDS - : cstat->smoothed_rtt) - << 10) * - 100 / NGTCP2_BBR_STARTUP_PACING_GAIN_H / bbr->initial_cwnd; + ngtcp2_max(((cstat->first_rtt_sample_ts == UINT64_MAX ? NGTCP2_MILLISECONDS + : cstat->smoothed_rtt) + << 10) * + 100 / NGTCP2_BBR_STARTUP_PACING_GAIN_H / bbr->initial_cwnd, + 1); } -static void bbr_set_pacing_rate_with_gain(ngtcp2_cc_bbr *bbr, +static void bbr_set_pacing_rate_with_gain(const ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, uint64_t pacing_gain_h) { uint64_t interval_m; @@ -420,14 +443,15 @@ static void bbr_set_pacing_rate_with_gain(ngtcp2_cc_bbr *bbr, interval_m = (NGTCP2_SECONDS << 10) * 100 * 100 / pacing_gain_h / bbr->bw / (100 - NGTCP2_BBR_PACING_MARGIN_PERCENT); - interval_m = ngtcp2_max_uint64(interval_m, 1); + interval_m = ngtcp2_max(interval_m, 1); if (bbr->full_bw_reached || interval_m < cstat->pacing_interval_m) { cstat->pacing_interval_m = interval_m; } } -static void bbr_set_pacing_rate(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { +static void bbr_set_pacing_rate(const ngtcp2_cc_bbr *bbr, + ngtcp2_conn_stat *cstat) { bbr_set_pacing_rate_with_gain(bbr, cstat, bbr->pacing_gain_h); } @@ -483,17 +507,12 @@ static void bbr_update_control_parameters(ngtcp2_cc_bbr *bbr, bbr_set_cwnd(bbr, cstat, ack); } -static void bbr_update_on_loss(ngtcp2_cc_bbr *cc, ngtcp2_conn_stat *cstat, - const ngtcp2_cc_pkt *pkt, ngtcp2_tstamp ts) { - bbr_handle_lost_packet(cc, cstat, pkt, ts); -} - static void bbr_update_latest_delivery_signals(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { + const ngtcp2_conn_stat *cstat) { bbr->loss_round_start = 0; - bbr->bw_latest = ngtcp2_max_uint64(bbr->bw_latest, cstat->delivery_rate_sec); + bbr->bw_latest = ngtcp2_max(bbr->bw_latest, cstat->delivery_rate_sec); bbr->inflight_latest = - ngtcp2_max_uint64(bbr->inflight_latest, bbr->rst->rs.delivered); + ngtcp2_max(bbr->inflight_latest, bbr->rst->rs.delivered); if (bbr->rst->rs.prior_delivered >= bbr->loss_round_delivered) { bbr->loss_round_delivered = bbr->rst->delivered; @@ -502,7 +521,7 @@ static void bbr_update_latest_delivery_signals(ngtcp2_cc_bbr *bbr, } static void bbr_advance_latest_delivery_signals(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { + const ngtcp2_conn_stat *cstat) { if (bbr->loss_round_start) { bbr->bw_latest = cstat->delivery_rate_sec; bbr->inflight_latest = bbr->rst->rs.delivered; @@ -510,7 +529,7 @@ static void bbr_advance_latest_delivery_signals(ngtcp2_cc_bbr *bbr, } static void bbr_update_congestion_signals(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack) { bbr_update_max_bw(bbr, cstat, ack); @@ -528,8 +547,9 @@ static void bbr_update_congestion_signals(ngtcp2_cc_bbr *bbr, bbr->loss_in_round = 0; } -static void bbr_adapt_lower_bounds_from_congestion(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { +static void +bbr_adapt_lower_bounds_from_congestion(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat) { if (bbr_is_probing_bw(bbr)) { return; } @@ -540,7 +560,8 @@ static void bbr_adapt_lower_bounds_from_congestion(ngtcp2_cc_bbr *bbr, } } -static void bbr_init_lower_bounds(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { +static void bbr_init_lower_bounds(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat) { if (bbr->bw_shortterm == UINT64_MAX) { bbr->bw_shortterm = bbr->max_bw; } @@ -551,23 +572,24 @@ static void bbr_init_lower_bounds(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { } static void bbr_loss_lower_bounds(ngtcp2_cc_bbr *bbr) { - bbr->bw_shortterm = ngtcp2_max_uint64( - bbr->bw_latest, - bbr->bw_shortterm * NGTCP2_BBR_BETA_NUMER / NGTCP2_BBR_BETA_DENOM); - bbr->inflight_shortterm = ngtcp2_max_uint64( + bbr->bw_shortterm = + ngtcp2_max(bbr->bw_latest, bbr->bw_shortterm * NGTCP2_BBR_BETA_NUMER / + NGTCP2_BBR_BETA_DENOM); + bbr->inflight_shortterm = ngtcp2_max( bbr->inflight_latest, bbr->inflight_shortterm * NGTCP2_BBR_BETA_NUMER / NGTCP2_BBR_BETA_DENOM); } static void bbr_bound_bw_for_model(ngtcp2_cc_bbr *bbr) { - bbr->bw = ngtcp2_min_uint64(bbr->max_bw, bbr->bw_shortterm); + bbr->bw = ngtcp2_min(bbr->max_bw, bbr->bw_shortterm); } -static void bbr_update_max_bw(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, +static void bbr_update_max_bw(ngtcp2_cc_bbr *bbr, const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack) { bbr_update_round(bbr, ack); - if (cstat->delivery_rate_sec >= bbr->max_bw || !bbr->rst->rs.is_app_limited) { + if (cstat->delivery_rate_sec && (cstat->delivery_rate_sec >= bbr->max_bw || + !bbr->rst->rs.is_app_limited)) { ngtcp2_window_filter_update(&bbr->max_bw_filter, cstat->delivery_rate_sec, bbr->cycle_count); @@ -598,7 +620,7 @@ static void bbr_start_round(ngtcp2_cc_bbr *bbr) { bbr->next_round_delivered = bbr->rst->delivered; } -static int bbr_is_in_probe_bw_state(ngtcp2_cc_bbr *bbr) { +static int bbr_is_in_probe_bw_state(const ngtcp2_cc_bbr *bbr) { switch (bbr->state) { case NGTCP2_BBR_STATE_PROBE_BW_DOWN: case NGTCP2_BBR_STATE_PROBE_BW_CRUISE: @@ -610,7 +632,7 @@ static int bbr_is_in_probe_bw_state(ngtcp2_cc_bbr *bbr) { } } -static int bbr_is_probing_bw(ngtcp2_cc_bbr *bbr) { +static int bbr_is_probing_bw(const ngtcp2_cc_bbr *bbr) { switch (bbr->state) { case NGTCP2_BBR_STATE_STARTUP: case NGTCP2_BBR_STATE_PROBE_BW_REFILL: @@ -622,7 +644,7 @@ static int bbr_is_probing_bw(ngtcp2_cc_bbr *bbr) { } static void bbr_update_ack_aggregation(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack, ngtcp2_tstamp ts) { ngtcp2_duration interval = ts - bbr->extra_acked_interval_start; @@ -641,7 +663,7 @@ static void bbr_update_ack_aggregation(ngtcp2_cc_bbr *bbr, extra = 0; } else { extra = bbr->extra_acked_delivered - expected_delivered; - extra = ngtcp2_min_uint64(extra, cstat->cwnd); + extra = ngtcp2_min(extra, cstat->cwnd); } if (bbr->full_bw_reached) { @@ -662,12 +684,16 @@ static void bbr_enter_drain(ngtcp2_cc_bbr *bbr) { bbr->state = NGTCP2_BBR_STATE_DRAIN; bbr->pacing_gain_h = NGTCP2_BBR_DRAIN_PACING_GAIN_H; bbr->cwnd_gain_h = NGTCP2_BBR_DEFAULT_CWND_GAIN_H; + bbr->drain_start_round = bbr->round_count; } -static void bbr_check_drain_done(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, +static void bbr_check_drain_done(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat, ngtcp2_tstamp ts) { if (bbr->state == NGTCP2_BBR_STATE_DRAIN && - cstat->bytes_in_flight <= bbr_inflight(bbr, cstat, 100)) { + (cstat->bytes_in_flight <= bbr_inflight(bbr, cstat, 100) || + bbr->round_count > + bbr->drain_start_round + NGTCP2_BBR_MAX_DRAIN_ROUNDS)) { bbr_enter_probe_bw(bbr, ts); } } @@ -721,7 +747,8 @@ static void bbr_start_probe_bw_refill(ngtcp2_cc_bbr *bbr) { bbr->cwnd_gain_h = NGTCP2_BBR_DEFAULT_CWND_GAIN_H; } -static void bbr_start_probe_bw_up(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { +static void bbr_start_probe_bw_up(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat) { ngtcp2_log_info(bbr->cc.log, NGTCP2_LOG_EVENT_CCA, "bbr start ProbeBW_UP"); bbr->ack_phase = NGTCP2_BBR_ACK_PHASE_ACKS_PROBE_STARTING; @@ -738,7 +765,7 @@ static void bbr_start_probe_bw_up(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { } static void bbr_update_probe_bw_cycle_phase(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack, ngtcp2_tstamp ts) { if (!bbr->full_bw_reached) { @@ -786,14 +813,16 @@ static void bbr_update_probe_bw_cycle_phase(ngtcp2_cc_bbr *bbr, } } -static int bbr_is_time_to_cruise(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { - uint64_t inflight = ngtcp2_min_uint64(bbr_inflight_with_headroom(bbr, cstat), - bbr_inflight(bbr, cstat, 100)); +static int bbr_is_time_to_cruise(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat) { + uint64_t inflight = ngtcp2_min(bbr_inflight_with_headroom(bbr, cstat), + bbr_inflight(bbr, cstat, 100)); return cstat->bytes_in_flight <= inflight; } -static int bbr_is_time_to_go_down(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { +static int bbr_is_time_to_go_down(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat) { if (bbr->rst->is_cwnd_limited && cstat->cwnd >= bbr->inflight_longterm) { bbr_reset_full_bw(bbr); bbr->full_bw = cstat->delivery_rate_sec; @@ -804,44 +833,43 @@ static int bbr_is_time_to_go_down(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { return bbr->full_bw_now; } -static int bbr_has_elapsed_in_phase(ngtcp2_cc_bbr *bbr, +static int bbr_has_elapsed_in_phase(const ngtcp2_cc_bbr *bbr, ngtcp2_duration interval, ngtcp2_tstamp ts) { return ts > bbr->cycle_stamp + interval; } -static uint64_t bbr_inflight_with_headroom(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { +static uint64_t bbr_inflight_with_headroom(const ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat) { uint64_t headroom; uint64_t mpcwnd; if (bbr->inflight_longterm == UINT64_MAX) { return UINT64_MAX; } - headroom = - ngtcp2_max_uint64(cstat->max_tx_udp_payload_size, - bbr->inflight_longterm * NGTCP2_BBR_HEADROOM_NUMER / - NGTCP2_BBR_HEADROOM_DENOM); + headroom = ngtcp2_max((uint64_t)cstat->max_tx_udp_payload_size, + bbr->inflight_longterm * NGTCP2_BBR_HEADROOM_NUMER / + NGTCP2_BBR_HEADROOM_DENOM); mpcwnd = min_pipe_cwnd(cstat->max_tx_udp_payload_size); if (bbr->inflight_longterm > headroom) { - return ngtcp2_max_uint64(bbr->inflight_longterm - headroom, mpcwnd); + return ngtcp2_max(bbr->inflight_longterm - headroom, mpcwnd); } return mpcwnd; } static void bbr_raise_inflight_longterm_slope(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { + const ngtcp2_conn_stat *cstat) { uint64_t growth_this_round = cstat->max_tx_udp_payload_size << bbr->bw_probe_up_rounds; - bbr->bw_probe_up_rounds = ngtcp2_min_size(bbr->bw_probe_up_rounds + 1, 30); - bbr->probe_up_cnt = ngtcp2_max_uint64(cstat->cwnd / growth_this_round, 1); + bbr->bw_probe_up_rounds = ngtcp2_min(bbr->bw_probe_up_rounds + 1, 30); + bbr->probe_up_cnt = ngtcp2_max(cstat->cwnd / growth_this_round, 1); } static void bbr_probe_inflight_longterm_upward(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack) { uint64_t delta; @@ -865,7 +893,7 @@ static void bbr_probe_inflight_longterm_upward(ngtcp2_cc_bbr *bbr, } static void bbr_adapt_longterm_model(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack) { if (bbr->ack_phase == NGTCP2_BBR_ACK_PHASE_ACKS_PROBE_STARTING && bbr->round_start) { @@ -879,7 +907,7 @@ static void bbr_adapt_longterm_model(ngtcp2_cc_bbr *bbr, } } - if (!bbr_is_inflight_too_high(bbr)) { + if (!bbr_is_inflight_too_high(bbr, &bbr->rst->rs)) { if (bbr->inflight_longterm == UINT64_MAX) { return; } @@ -894,7 +922,8 @@ static void bbr_adapt_longterm_model(ngtcp2_cc_bbr *bbr, } } -static int bbr_is_time_to_probe_bw(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, +static int bbr_is_time_to_probe_bw(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat, ngtcp2_tstamp ts) { if (bbr_has_elapsed_in_phase(bbr, bbr->bw_probe_wait, ts) || bbr_is_reno_coexistence_probe_time(bbr, cstat)) { @@ -912,34 +941,34 @@ static void bbr_pick_probe_wait(ngtcp2_cc_bbr *bbr) { 2 * NGTCP2_SECONDS + ngtcp2_pcg32_rand_n(bbr->pcg, NGTCP2_SECONDS + 1); } -static int bbr_is_reno_coexistence_probe_time(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { +static int bbr_is_reno_coexistence_probe_time(const ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat) { uint64_t reno_rounds = bbr_target_inflight(bbr, cstat) / cstat->max_tx_udp_payload_size; - return bbr->rounds_since_bw_probe >= ngtcp2_min_uint64(reno_rounds, 63); + return bbr->rounds_since_bw_probe >= ngtcp2_min(reno_rounds, 63); } -static uint64_t bbr_target_inflight(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { - return ngtcp2_min_uint64(bbr->bdp, cstat->cwnd); +static uint64_t bbr_target_inflight(const ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat) { + return ngtcp2_min(bbr->bdp, cstat->cwnd); } -static int bbr_is_inflight_too_high(ngtcp2_cc_bbr *bbr) { - const ngtcp2_rs *rs = &bbr->rst->rs; +static int bbr_is_inflight_too_high(const ngtcp2_cc_bbr *bbr, + const ngtcp2_rs *rs) { + (void)bbr; return rs->lost * NGTCP2_BBR_LOSS_THRESH_DENOM > rs->tx_in_flight * NGTCP2_BBR_LOSS_THRESH_NUMER; } static void bbr_handle_inflight_too_high(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, + const ngtcp2_rs *rs, ngtcp2_tstamp ts) { - const ngtcp2_rs *rs = &bbr->rst->rs; - bbr->bw_probe_samples = 0; if (!rs->is_app_limited) { - bbr->inflight_longterm = ngtcp2_max_uint64( + bbr->inflight_longterm = ngtcp2_max( rs->tx_in_flight, bbr_target_inflight(bbr, cstat) * NGTCP2_BBR_BETA_NUMER / NGTCP2_BBR_BETA_DENOM); } @@ -952,14 +981,50 @@ static void bbr_handle_inflight_too_high(ngtcp2_cc_bbr *bbr, static void bbr_note_loss(ngtcp2_cc_bbr *bbr) { if (!bbr->loss_in_round) { bbr->loss_round_delivered = bbr->rst->delivered; + bbr_save_state_upon_loss(bbr); } bbr->loss_in_round = 1; } -static void bbr_handle_lost_packet(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, +static void bbr_save_state_upon_loss(ngtcp2_cc_bbr *bbr) { + bbr->undo_state = bbr->state; + bbr->undo_bw_shortterm = bbr->bw_shortterm; + bbr->undo_inflight_shortterm = bbr->inflight_shortterm; + bbr->undo_inflight_longterm = bbr->inflight_longterm; +} + +static void bbr_handle_spurious_loss_detection(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat) { + bbr->loss_in_round = 0; + + bbr_reset_full_bw(bbr); + + bbr->bw_shortterm = ngtcp2_max(bbr->bw_shortterm, bbr->undo_bw_shortterm); + bbr->inflight_shortterm = + ngtcp2_max(bbr->inflight_shortterm, bbr->undo_inflight_shortterm); + bbr->inflight_longterm = + ngtcp2_max(bbr->inflight_longterm, bbr->undo_inflight_longterm); + + if (bbr->state != NGTCP2_BBR_STATE_PROBE_RTT && + bbr->state != bbr->undo_state) { + switch (bbr->undo_state) { + case NGTCP2_BBR_STATE_STARTUP: + bbr_enter_startup(bbr); + break; + case NGTCP2_BBR_STATE_PROBE_BW_UP: + bbr_start_probe_bw_up(bbr, cstat); + break; + default: + break; + } + } +} + +static void bbr_handle_lost_packet(ngtcp2_cc_bbr *bbr, + const ngtcp2_conn_stat *cstat, const ngtcp2_cc_pkt *pkt, ngtcp2_tstamp ts) { - ngtcp2_rs *rs = &bbr->rst->rs; + ngtcp2_rs rs = {0}; bbr_note_loss(bbr); @@ -967,22 +1032,21 @@ static void bbr_handle_lost_packet(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, return; } - rs->tx_in_flight = pkt->tx_in_flight; + rs.tx_in_flight = pkt->tx_in_flight; assert(bbr->rst->lost >= pkt->lost); - rs->lost = bbr->rst->lost - pkt->lost; - rs->is_app_limited = pkt->is_app_limited; + rs.lost = bbr->rst->lost - pkt->lost; + rs.is_app_limited = pkt->is_app_limited; - if (bbr_is_inflight_too_high(bbr)) { - rs->tx_in_flight = bbr_inflight_longterm_from_lost_packet(bbr, pkt); + if (bbr_is_inflight_too_high(bbr, &rs)) { + rs.tx_in_flight = bbr_inflight_at_loss(bbr, pkt, &rs); - bbr_handle_inflight_too_high(bbr, cstat, ts); + bbr_handle_inflight_too_high(bbr, cstat, &rs, ts); } } -static uint64_t -bbr_inflight_longterm_from_lost_packet(ngtcp2_cc_bbr *bbr, - const ngtcp2_cc_pkt *pkt) { - ngtcp2_rs *rs = &bbr->rst->rs; +static uint64_t bbr_inflight_at_loss(const ngtcp2_cc_bbr *bbr, + const ngtcp2_cc_pkt *pkt, + const ngtcp2_rs *rs) { uint64_t inflight_prev, lost_prev, lost_prefix; (void)bbr; @@ -1025,8 +1089,8 @@ static void bbr_update_min_rtt(ngtcp2_cc_bbr *bbr, const ngtcp2_cc_ack *ack, bbr->min_rtt = bbr->probe_rtt_min_delay; bbr->min_rtt_stamp = bbr->probe_rtt_min_stamp; - ngtcp2_log_info(bbr->cc.log, NGTCP2_LOG_EVENT_CCA, - "bbr update min_rtt=%" PRIu64, bbr->min_rtt); + ngtcp2_log_infof(bbr->cc.log, NGTCP2_LOG_EVENT_CCA, + "bbr update min_rtt=", bbr->min_rtt); } } @@ -1097,14 +1161,9 @@ static void bbr_check_probe_rtt_done(ngtcp2_cc_bbr *bbr, } static void bbr_mark_connection_app_limited(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { - uint64_t app_limited = bbr->rst->delivered + cstat->bytes_in_flight; - - if (app_limited) { - bbr->rst->app_limited = app_limited; - } else { - bbr->rst->app_limited = cstat->max_tx_udp_payload_size; - } + const ngtcp2_conn_stat *cstat) { + bbr->rst->app_limited = + ngtcp2_max(bbr->rst->delivered + cstat->bytes_in_flight, 1); } static void bbr_exit_probe_rtt(ngtcp2_cc_bbr *bbr, ngtcp2_tstamp ts) { @@ -1140,7 +1199,7 @@ static uint64_t bbr_bdp_multiple(ngtcp2_cc_bbr *bbr, uint64_t gain_h) { return bbr->initial_cwnd; } - bbr->bdp = ngtcp2_max_uint64(bbr->bw * bbr->min_rtt / NGTCP2_SECONDS, 1); + bbr->bdp = ngtcp2_max(bbr->bw * bbr->min_rtt / NGTCP2_SECONDS, 1); return (uint64_t)(bbr->bdp * gain_h / 100); } @@ -1150,13 +1209,13 @@ static uint64_t min_pipe_cwnd(size_t max_udp_payload_size) { } static uint64_t bbr_quantization_budget(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat, + const ngtcp2_conn_stat *cstat, uint64_t inflight) { bbr_update_offload_budget(bbr, cstat); - inflight = ngtcp2_max_uint64(inflight, bbr->offload_budget); + inflight = ngtcp2_max(inflight, bbr->offload_budget); inflight = - ngtcp2_max_uint64(inflight, min_pipe_cwnd(cstat->max_tx_udp_payload_size)); + ngtcp2_max(inflight, min_pipe_cwnd(cstat->max_tx_udp_payload_size)); if (bbr->state == NGTCP2_BBR_STATE_PROBE_BW_UP) { inflight += 2 * cstat->max_tx_udp_payload_size; @@ -1165,7 +1224,7 @@ static uint64_t bbr_quantization_budget(ngtcp2_cc_bbr *bbr, return inflight; } -static uint64_t bbr_inflight(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, +static uint64_t bbr_inflight(ngtcp2_cc_bbr *bbr, const ngtcp2_conn_stat *cstat, uint64_t gain_h) { uint64_t inflight = bbr_bdp_multiple(bbr, gain_h); @@ -1173,7 +1232,7 @@ static uint64_t bbr_inflight(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, } static void bbr_update_max_inflight(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { + const ngtcp2_conn_stat *cstat) { uint64_t inflight; inflight = bbr_bdp_multiple(bbr, bbr->cwnd_gain_h) + bbr->extra_acked; @@ -1181,7 +1240,7 @@ static void bbr_update_max_inflight(ngtcp2_cc_bbr *bbr, } static void bbr_update_offload_budget(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { + const ngtcp2_conn_stat *cstat) { bbr->offload_budget = 3 * cstat->send_quantum; } @@ -1189,33 +1248,33 @@ static void bbr_advance_max_bw_filter(ngtcp2_cc_bbr *bbr) { ++bbr->cycle_count; } -static void bbr_save_cwnd(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { +static void bbr_save_cwnd(ngtcp2_cc_bbr *bbr, const ngtcp2_conn_stat *cstat) { if (!bbr->in_loss_recovery && bbr->state != NGTCP2_BBR_STATE_PROBE_RTT) { bbr->prior_cwnd = cstat->cwnd; return; } - bbr->prior_cwnd = ngtcp2_max_uint64(bbr->prior_cwnd, cstat->cwnd); + bbr->prior_cwnd = ngtcp2_max(bbr->prior_cwnd, cstat->cwnd); } -static void bbr_restore_cwnd(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { - cstat->cwnd = ngtcp2_max_uint64(cstat->cwnd, bbr->prior_cwnd); +static void bbr_restore_cwnd(const ngtcp2_cc_bbr *bbr, + ngtcp2_conn_stat *cstat) { + cstat->cwnd = ngtcp2_max(cstat->cwnd, bbr->prior_cwnd); } static uint64_t bbr_probe_rtt_cwnd(ngtcp2_cc_bbr *bbr, - ngtcp2_conn_stat *cstat) { + const ngtcp2_conn_stat *cstat) { uint64_t probe_rtt_cwnd = bbr_bdp_multiple(bbr, NGTCP2_BBR_PROBE_RTT_CWND_GAIN_H); uint64_t mpcwnd = min_pipe_cwnd(cstat->max_tx_udp_payload_size); - return ngtcp2_max_uint64(probe_rtt_cwnd, mpcwnd); + return ngtcp2_max(probe_rtt_cwnd, mpcwnd); } static void bbr_bound_cwnd_for_probe_rtt(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { if (bbr->state == NGTCP2_BBR_STATE_PROBE_RTT) { - cstat->cwnd = - ngtcp2_min_uint64(cstat->cwnd, bbr_probe_rtt_cwnd(bbr, cstat)); + cstat->cwnd = ngtcp2_min(cstat->cwnd, bbr_probe_rtt_cwnd(bbr, cstat)); } } @@ -1227,20 +1286,20 @@ static void bbr_set_cwnd(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, if (bbr->full_bw_reached) { cstat->cwnd += ack->bytes_delivered; - cstat->cwnd = ngtcp2_min_uint64(cstat->cwnd, bbr->max_inflight); + cstat->cwnd = ngtcp2_min(cstat->cwnd, bbr->max_inflight); } else if (cstat->cwnd < bbr->max_inflight || bbr->rst->delivered < bbr->initial_cwnd) { cstat->cwnd += ack->bytes_delivered; } mpcwnd = min_pipe_cwnd(cstat->max_tx_udp_payload_size); - cstat->cwnd = ngtcp2_max_uint64(cstat->cwnd, mpcwnd); + cstat->cwnd = ngtcp2_max(cstat->cwnd, mpcwnd); bbr_bound_cwnd_for_probe_rtt(bbr, cstat); bbr_bound_cwnd_for_model(bbr, cstat); } -static void bbr_bound_cwnd_for_model(ngtcp2_cc_bbr *bbr, +static void bbr_bound_cwnd_for_model(const ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { uint64_t cap = UINT64_MAX; uint64_t mpcwnd = min_pipe_cwnd(cstat->max_tx_udp_payload_size); @@ -1253,22 +1312,22 @@ static void bbr_bound_cwnd_for_model(ngtcp2_cc_bbr *bbr, cap = bbr_inflight_with_headroom(bbr, cstat); } - cap = ngtcp2_min_uint64(cap, bbr->inflight_shortterm); - cap = ngtcp2_max_uint64(cap, mpcwnd); + cap = ngtcp2_min(cap, bbr->inflight_shortterm); + cap = ngtcp2_max(cap, mpcwnd); - cstat->cwnd = ngtcp2_min_uint64(cstat->cwnd, cap); + cstat->cwnd = ngtcp2_min(cstat->cwnd, cap); } -static void bbr_set_send_quantum(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat) { +static void bbr_set_send_quantum(const ngtcp2_cc_bbr *bbr, + ngtcp2_conn_stat *cstat) { size_t send_quantum = 64 * 1024; (void)bbr; - send_quantum = - ngtcp2_min_size(send_quantum, (size_t)((NGTCP2_MILLISECONDS << 10) / - cstat->pacing_interval_m)); + send_quantum = ngtcp2_min(send_quantum, (size_t)((NGTCP2_MILLISECONDS << 10) / + cstat->pacing_interval_m)); cstat->send_quantum = - ngtcp2_max_size(send_quantum, 2 * cstat->max_tx_udp_payload_size); + ngtcp2_max(send_quantum, 2 * cstat->max_tx_udp_payload_size); } static int in_congestion_recovery(const ngtcp2_conn_stat *cstat, @@ -1279,28 +1338,15 @@ static int in_congestion_recovery(const ngtcp2_conn_stat *cstat, static void bbr_handle_recovery(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack) { - if (bbr->in_loss_recovery) { - if (ack->largest_pkt_sent_ts != UINT64_MAX && - !in_congestion_recovery(cstat, ack->largest_pkt_sent_ts)) { - bbr->in_loss_recovery = 0; - bbr->round_count_at_recovery = UINT64_MAX; - bbr_restore_cwnd(bbr, cstat); - } - + if (!bbr->in_loss_recovery) { return; } - if (bbr->congestion_recovery_start_ts != UINT64_MAX) { - bbr->in_loss_recovery = 1; - bbr->round_count_at_recovery = - bbr->round_start ? bbr->round_count : bbr->round_count + 1; - bbr_save_cwnd(bbr, cstat); - cstat->cwnd = - cstat->bytes_in_flight + - ngtcp2_max_uint64(ack->bytes_delivered, cstat->max_tx_udp_payload_size); - - cstat->congestion_recovery_start_ts = bbr->congestion_recovery_start_ts; - bbr->congestion_recovery_start_ts = UINT64_MAX; + if (ack->largest_pkt_sent_ts != UINT64_MAX && + !in_congestion_recovery(cstat, ack->largest_pkt_sent_ts)) { + bbr->in_loss_recovery = 0; + bbr->round_count_at_recovery = UINT64_MAX; + bbr_restore_cwnd(bbr, cstat); } } @@ -1308,22 +1354,26 @@ static void bbr_cc_on_pkt_lost(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, const ngtcp2_cc_pkt *pkt, ngtcp2_tstamp ts) { ngtcp2_cc_bbr *bbr = ngtcp2_struct_of(cc, ngtcp2_cc_bbr, cc); - bbr_update_on_loss(bbr, cstat, pkt, ts); + bbr_handle_lost_packet(bbr, cstat, pkt, ts); } static void bbr_cc_congestion_event(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, - ngtcp2_tstamp sent_ts, uint64_t bytes_lost, + ngtcp2_tstamp sent_ts, + const ngtcp2_cc_ack *ack, ngtcp2_tstamp ts) { ngtcp2_cc_bbr *bbr = ngtcp2_struct_of(cc, ngtcp2_cc_bbr, cc); - (void)bytes_lost; + (void)ack; - if (bbr->in_loss_recovery || - bbr->congestion_recovery_start_ts != UINT64_MAX || - in_congestion_recovery(cstat, sent_ts)) { + if (bbr->in_loss_recovery || in_congestion_recovery(cstat, sent_ts)) { return; } - bbr->congestion_recovery_start_ts = ts; + bbr->in_loss_recovery = 1; + bbr->round_count_at_recovery = + bbr->round_start ? bbr->round_count : bbr->round_count + 1; + bbr_save_cwnd(bbr, cstat); + + cstat->congestion_recovery_start_ts = ts; } static void bbr_cc_on_spurious_congestion(ngtcp2_cc *cc, @@ -1332,14 +1382,13 @@ static void bbr_cc_on_spurious_congestion(ngtcp2_cc *cc, ngtcp2_cc_bbr *bbr = ngtcp2_struct_of(cc, ngtcp2_cc_bbr, cc); (void)ts; - bbr->congestion_recovery_start_ts = UINT64_MAX; cstat->congestion_recovery_start_ts = UINT64_MAX; - if (bbr->in_loss_recovery) { - bbr->in_loss_recovery = 0; - bbr->round_count_at_recovery = UINT64_MAX; - bbr_restore_cwnd(bbr, cstat); - } + bbr->in_loss_recovery = 0; + bbr->round_count_at_recovery = UINT64_MAX; + + bbr_restore_cwnd(bbr, cstat); + bbr_handle_spurious_loss_detection(bbr, cstat); } static void bbr_cc_on_persistent_congestion(ngtcp2_cc *cc, @@ -1349,14 +1398,13 @@ static void bbr_cc_on_persistent_congestion(ngtcp2_cc *cc, (void)ts; cstat->congestion_recovery_start_ts = UINT64_MAX; - bbr->congestion_recovery_start_ts = UINT64_MAX; bbr->in_loss_recovery = 0; bbr->round_count_at_recovery = UINT64_MAX; bbr_save_cwnd(bbr, cstat); cstat->cwnd = cstat->bytes_in_flight + cstat->max_tx_udp_payload_size; - cstat->cwnd = ngtcp2_max_uint64( - cstat->cwnd, min_pipe_cwnd(cstat->max_tx_udp_payload_size)); + cstat->cwnd = + ngtcp2_max(cstat->cwnd, min_pipe_cwnd(cstat->max_tx_udp_payload_size)); } static void bbr_cc_on_ack_recv(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, @@ -1364,6 +1412,11 @@ static void bbr_cc_on_ack_recv(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, ngtcp2_cc_bbr *bbr = ngtcp2_struct_of(cc, ngtcp2_cc_bbr, cc); bbr_handle_recovery(bbr, cstat, ack); + + if (ack->bytes_delivered == 0) { + return; + } + bbr_update_on_ack(bbr, cstat, ack, ts); } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.h index 0499924f58264f..81384e6d9f5cda 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.h @@ -75,8 +75,8 @@ typedef struct ngtcp2_cc_bbr { ngtcp2_tstamp min_rtt_stamp; ngtcp2_tstamp probe_rtt_done_stamp; int probe_rtt_round_done; - uint64_t prior_cwnd; int idle_restart; + uint64_t prior_cwnd; ngtcp2_tstamp extra_acked_interval_start; uint64_t extra_acked_delivered; @@ -91,8 +91,8 @@ typedef struct ngtcp2_cc_bbr { /* Round counting */ uint64_t next_round_delivered; - int round_start; uint64_t round_count; + int round_start; /* Full pipe */ uint64_t full_bw; @@ -106,7 +106,12 @@ typedef struct ngtcp2_cc_bbr { ngtcp2_bbr_state state; uint64_t cwnd_gain_h; - int loss_round_start; + /* Backup for spurious losses */ + ngtcp2_bbr_state undo_state; + uint64_t undo_bw_shortterm; + uint64_t undo_inflight_shortterm; + uint64_t undo_inflight_longterm; + uint64_t loss_round_delivered; uint64_t rounds_since_bw_probe; uint64_t max_bw; @@ -120,18 +125,19 @@ typedef struct ngtcp2_cc_bbr { ngtcp2_tstamp cycle_stamp; ngtcp2_bbr_ack_phase ack_phase; ngtcp2_duration bw_probe_wait; - int bw_probe_samples; size_t bw_probe_up_rounds; uint64_t bw_probe_up_acks; uint64_t inflight_longterm; - int probe_rtt_expired; ngtcp2_duration probe_rtt_min_delay; ngtcp2_tstamp probe_rtt_min_stamp; - int in_loss_recovery; uint64_t round_count_at_recovery; uint64_t max_inflight; - ngtcp2_tstamp congestion_recovery_start_ts; uint64_t bdp; + uint64_t drain_start_round; + int loss_round_start; + int bw_probe_samples; + int probe_rtt_expired; + int in_loss_recovery; } ngtcp2_cc_bbr; void ngtcp2_cc_bbr_init(ngtcp2_cc_bbr *bbr, ngtcp2_log *log, diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.c index bf4273f816a810..083a766281b299 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.c @@ -26,8 +26,12 @@ #include "ngtcp2_mem.h" void ngtcp2_buf_init(ngtcp2_buf *buf, uint8_t *begin, size_t len) { - buf->begin = buf->pos = buf->last = begin; - buf->end = begin + len; + *buf = (ngtcp2_buf){ + .begin = begin, + .end = begin + len, + .pos = begin, + .last = begin, + }; } void ngtcp2_buf_reset(ngtcp2_buf *buf) { buf->pos = buf->last = buf->begin; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_callbacks.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_callbacks.c index 7e77186772b762..1d65d93d566944 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_callbacks.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_callbacks.c @@ -42,7 +42,7 @@ const ngtcp2_callbacks *ngtcp2_callbacks_convert_to_latest( return src; } - memset(dest, 0, sizeof(*dest)); + *dest = (ngtcp2_callbacks){0}; callbacks_copy(dest, src, callbacks_version); @@ -63,6 +63,9 @@ size_t ngtcp2_callbackslen_version(int callbacks_version) { switch (callbacks_version) { case NGTCP2_CALLBACKS_VERSION: return sizeof(callbacks); + case NGTCP2_CALLBACKS_V2: + return offsetof(ngtcp2_callbacks, begin_path_validation) + + sizeof(callbacks.begin_path_validation); case NGTCP2_CALLBACKS_V1: return offsetof(ngtcp2_callbacks, tls_early_data_rejected) + sizeof(callbacks.tls_early_data_rejected); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_cc.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_cc.c index c16953802afd2a..2d3c65dbf38581 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_cc.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_cc.c @@ -36,9 +36,41 @@ #include "ngtcp2_unreachable.h" uint64_t ngtcp2_cc_compute_initcwnd(size_t max_udp_payload_size) { - uint64_t n = 2 * max_udp_payload_size; - n = ngtcp2_max_uint64(n, 14720); - return ngtcp2_min_uint64(10 * max_udp_payload_size, n); + size_t n = ngtcp2_max(2 * max_udp_payload_size, 14720); + return ngtcp2_min(10 * max_udp_payload_size, n); +} + +/* 1.25 is the under-utilization avoidance factor described in + https://datatracker.ietf.org/doc/html/rfc9002#section-7.7 */ +#define NGTCP2_CC_PACING_GAIN_H 125 + +static void init_pacing_rate(ngtcp2_conn_stat *cstat) { + assert(cstat->cwnd); + + cstat->pacing_interval_m = ngtcp2_max((NGTCP2_MILLISECONDS << 10) * 100 / + NGTCP2_CC_PACING_GAIN_H / cstat->cwnd, + 1); + cstat->send_quantum = 10 * cstat->max_tx_udp_payload_size; +} + +static void set_pacing_rate(ngtcp2_conn_stat *cstat) { + size_t send_quantum = 64 * 1024; + + assert(cstat->cwnd); + + cstat->pacing_interval_m = + ((cstat->first_rtt_sample_ts == UINT64_MAX ? NGTCP2_MILLISECONDS + : cstat->smoothed_rtt) + << 10) * + 100 / NGTCP2_CC_PACING_GAIN_H / cstat->cwnd; + + cstat->pacing_interval_m = ngtcp2_max(cstat->pacing_interval_m, 1); + + send_quantum = ngtcp2_min(send_quantum, (size_t)((NGTCP2_MILLISECONDS << 10) / + cstat->pacing_interval_m)); + + cstat->send_quantum = + ngtcp2_max(send_quantum, 10 * cstat->max_tx_udp_payload_size); } ngtcp2_cc_pkt *ngtcp2_cc_pkt_init(ngtcp2_cc_pkt *pkt, int64_t pkt_num, @@ -56,9 +88,14 @@ ngtcp2_cc_pkt *ngtcp2_cc_pkt_init(ngtcp2_cc_pkt *pkt, int64_t pkt_num, return pkt; } -static void reno_cc_reset(ngtcp2_cc_reno *reno) { reno->pending_add = 0; } +static void reno_cc_reset(ngtcp2_cc_reno *reno, ngtcp2_conn_stat *cstat) { + reno->pending_add = 0; -void ngtcp2_cc_reno_init(ngtcp2_cc_reno *reno, ngtcp2_log *log) { + init_pacing_rate(cstat); +} + +void ngtcp2_cc_reno_init(ngtcp2_cc_reno *reno, ngtcp2_log *log, + ngtcp2_conn_stat *cstat) { *reno = (ngtcp2_cc_reno){ .cc = { @@ -70,7 +107,7 @@ void ngtcp2_cc_reno_init(ngtcp2_cc_reno *reno, ngtcp2_log *log) { }, }; - reno_cc_reset(reno); + reno_cc_reset(reno, cstat); } static int in_congestion_recovery(const ngtcp2_conn_stat *cstat, @@ -92,9 +129,11 @@ void ngtcp2_cc_reno_cc_on_pkt_acked(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, if (cstat->cwnd < cstat->ssthresh) { cstat->cwnd += pkt->pktlen; - ngtcp2_log_info(reno->cc.log, NGTCP2_LOG_EVENT_CCA, - "pkn=%" PRId64 " acked, slow start cwnd=%" PRIu64, - pkt->pkt_num, cstat->cwnd); + + set_pacing_rate(cstat); + + ngtcp2_log_infof(reno->cc.log, NGTCP2_LOG_EVENT_CCA, "pkn=", pkt->pkt_num, + " acked, slow start cwnd=", cstat->cwnd); return; } @@ -102,14 +141,17 @@ void ngtcp2_cc_reno_cc_on_pkt_acked(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, reno->pending_add = m % cstat->cwnd; cstat->cwnd += m / cstat->cwnd; + + set_pacing_rate(cstat); } void ngtcp2_cc_reno_cc_congestion_event(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, ngtcp2_tstamp sent_ts, - uint64_t bytes_lost, ngtcp2_tstamp ts) { + const ngtcp2_cc_ack *ack, + ngtcp2_tstamp ts) { ngtcp2_cc_reno *reno = ngtcp2_struct_of(cc, ngtcp2_cc_reno, cc); uint64_t min_cwnd; - (void)bytes_lost; + (void)ack; if (in_congestion_recovery(cstat, sent_ts)) { return; @@ -118,14 +160,15 @@ void ngtcp2_cc_reno_cc_congestion_event(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, cstat->congestion_recovery_start_ts = ts; cstat->cwnd >>= NGTCP2_LOSS_REDUCTION_FACTOR_BITS; min_cwnd = 2 * cstat->max_tx_udp_payload_size; - cstat->cwnd = ngtcp2_max_uint64(cstat->cwnd, min_cwnd); + cstat->cwnd = ngtcp2_max(cstat->cwnd, min_cwnd); cstat->ssthresh = cstat->cwnd; reno->pending_add = 0; - ngtcp2_log_info(reno->cc.log, NGTCP2_LOG_EVENT_CCA, - "reduce cwnd because of packet loss cwnd=%" PRIu64, - cstat->cwnd); + set_pacing_rate(cstat); + + ngtcp2_log_infof(reno->cc.log, NGTCP2_LOG_EVENT_CCA, + "reduce cwnd because of packet loss cwnd=", cstat->cwnd); } void ngtcp2_cc_reno_cc_on_persistent_congestion(ngtcp2_cc *cc, @@ -136,15 +179,16 @@ void ngtcp2_cc_reno_cc_on_persistent_congestion(ngtcp2_cc *cc, cstat->cwnd = 2 * cstat->max_tx_udp_payload_size; cstat->congestion_recovery_start_ts = UINT64_MAX; + + set_pacing_rate(cstat); } void ngtcp2_cc_reno_cc_reset(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, ngtcp2_tstamp ts) { ngtcp2_cc_reno *reno = ngtcp2_struct_of(cc, ngtcp2_cc_reno, cc); - (void)cstat; (void)ts; - reno_cc_reset(reno); + reno_cc_reset(reno, cstat); } static void cubic_vars_reset(ngtcp2_cubic_vars *v) { @@ -156,11 +200,11 @@ static void cubic_vars_reset(ngtcp2_cubic_vars *v) { v->app_limited_start_ts = UINT64_MAX; v->app_limited_duration = 0; - v->pending_bytes_delivered = 0; - v->pending_est_bytes_delivered = 0; + v->pending_bytes_acked = 0; + v->pending_est_bytes_acked = 0; } -static void cubic_cc_reset(ngtcp2_cc_cubic *cubic) { +static void cubic_cc_reset(ngtcp2_cc_cubic *cubic, ngtcp2_conn_stat *cstat) { cubic_vars_reset(&cubic->current); cubic_vars_reset(&cubic->undo.v); cubic->undo.cwnd = 0; @@ -174,10 +218,12 @@ static void cubic_cc_reset(ngtcp2_cc_cubic *cubic) { cubic->hs.css_round = 0; cubic->next_round_delivered = 0; + + init_pacing_rate(cstat); } void ngtcp2_cc_cubic_init(ngtcp2_cc_cubic *cubic, ngtcp2_log *log, - ngtcp2_rst *rst) { + ngtcp2_conn_stat *cstat, ngtcp2_rst *rst) { *cubic = (ngtcp2_cc_cubic){ .cc = { @@ -191,7 +237,7 @@ void ngtcp2_cc_cubic_init(ngtcp2_cc_cubic *cubic, ngtcp2_log *log, .rst = rst, }; - cubic_cc_reset(cubic); + cubic_cc_reset(cubic, cstat); } uint64_t ngtcp2_cbrt(uint64_t n) { @@ -211,7 +257,6 @@ uint64_t ngtcp2_cbrt(uint64_t n) { y <<= 1; b = 3 * y * (y + 1) + 1; if (n >= b) { - n -= b; y++; } @@ -242,7 +287,7 @@ static uint64_t cubic_cc_compute_w_cubic(ngtcp2_cc_cubic *cubic, time_delta_m = tx_m - cubic->current.k_m; } - time_delta_m = ngtcp2_min_uint64(time_delta_m, 3600 << 10); + time_delta_m = ngtcp2_min(time_delta_m, 3600 << 10); delta = ((((time_delta_m * time_delta_m) >> 10) * time_delta_m) >> 10) * cstat->max_tx_udp_payload_size * 4 / 10; @@ -266,24 +311,26 @@ void ngtcp2_cc_cubic_cc_on_ack_recv(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, ngtcp2_cc_cubic *cubic = ngtcp2_struct_of(cc, ngtcp2_cc_cubic, cc); uint64_t w_cubic, w_cubic_next; uint64_t target, m; + uint64_t bytes_acked; ngtcp2_duration rtt_thresh; int round_start; int is_app_limited = cubic->rst->rs.is_app_limited && !cubic->rst->is_cwnd_limited; - if (in_congestion_recovery(cstat, ack->largest_pkt_sent_ts)) { + if (ack->bytes_delivered == 0 || + in_congestion_recovery(cstat, ack->largest_pkt_sent_ts)) { return; } - if (cstat->cwnd < cstat->ssthresh) { - /* slow-start */ - round_start = ack->pkt_delivered >= cubic->next_round_delivered; - if (round_start) { - cubic->next_round_delivered = cubic->rst->delivered; + round_start = ack->pkt_delivered >= cubic->next_round_delivered; + if (round_start) { + cubic->next_round_delivered = cubic->rst->delivered; - cubic->rst->is_cwnd_limited = 0; - } + cubic->rst->is_cwnd_limited = 0; + } + if (cstat->cwnd < cstat->ssthresh) { + /* slow-start */ if (!is_app_limited) { if (cubic->hs.css_round) { cstat->cwnd += ack->bytes_delivered / NGTCP2_HS_CSS_GROWTH_DIVISOR; @@ -291,9 +338,11 @@ void ngtcp2_cc_cubic_cc_on_ack_recv(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, cstat->cwnd += ack->bytes_delivered; } - ngtcp2_log_info(cubic->cc.log, NGTCP2_LOG_EVENT_CCA, - "%" PRIu64 " bytes acked, slow start cwnd=%" PRIu64, - ack->bytes_delivered, cstat->cwnd); + set_pacing_rate(cstat); + + ngtcp2_log_infof(cubic->cc.log, NGTCP2_LOG_EVENT_CCA, + ack->bytes_delivered, + " bytes acked, slow start cwnd=", cstat->cwnd); } if (round_start) { @@ -307,7 +356,7 @@ void ngtcp2_cc_cubic_cc_on_ack_recv(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, } cubic->hs.current_round_min_rtt = - ngtcp2_min_uint64(cubic->hs.current_round_min_rtt, ack->rtt); + ngtcp2_min(cubic->hs.current_round_min_rtt, ack->rtt); ++cubic->hs.rtt_sample_count; if (cubic->hs.css_round) { @@ -334,11 +383,10 @@ void ngtcp2_cc_cubic_cc_on_ack_recv(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, if (cubic->hs.rtt_sample_count >= NGTCP2_HS_N_RTT_SAMPLE && cubic->hs.current_round_min_rtt != UINT64_MAX && cubic->hs.last_round_min_rtt != UINT64_MAX) { - rtt_thresh = - ngtcp2_max_uint64(NGTCP2_HS_MIN_RTT_THRESH, - ngtcp2_min_uint64(cubic->hs.last_round_min_rtt / - NGTCP2_HS_MIN_RTT_DIVISOR, - NGTCP2_HS_MAX_RTT_THRESH)); + rtt_thresh = ngtcp2_max( + NGTCP2_HS_MIN_RTT_THRESH, + ngtcp2_min(cubic->hs.last_round_min_rtt / NGTCP2_HS_MIN_RTT_DIVISOR, + NGTCP2_HS_MAX_RTT_THRESH)); if (cubic->hs.current_round_min_rtt >= cubic->hs.last_round_min_rtt + rtt_thresh) { @@ -379,35 +427,45 @@ void ngtcp2_cc_cubic_cc_on_ack_recv(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, target = w_cubic_next; } - m = ack->bytes_delivered * cstat->max_tx_udp_payload_size + - cubic->current.pending_est_bytes_delivered; - cubic->current.pending_est_bytes_delivered = m % cstat->cwnd; + bytes_acked = ack->bytes_delivered * cstat->max_tx_udp_payload_size; + m = (bytes_acked + cubic->current.pending_est_bytes_acked) / cstat->cwnd; + + cubic->current.pending_est_bytes_acked += bytes_acked; + cubic->current.pending_est_bytes_acked -= m * cstat->cwnd; + + assert(cubic->current.pending_est_bytes_acked < cstat->cwnd); if (cubic->current.w_est < cubic->current.cwnd_prior) { - cubic->current.w_est += m * 9 / 17 / cstat->cwnd; + cubic->current.w_est += m * 9 / 17; } else { - cubic->current.w_est += m / cstat->cwnd; + cubic->current.w_est += m; } if (cubic->current.w_est > w_cubic) { cstat->cwnd = cubic->current.w_est; } else { - m = (target - cstat->cwnd) * cstat->max_tx_udp_payload_size + - cubic->current.pending_bytes_delivered; - cubic->current.pending_bytes_delivered = m % cstat->cwnd; - cstat->cwnd += m / cstat->cwnd; + bytes_acked = (target - cstat->cwnd) * cstat->max_tx_udp_payload_size; + m = (bytes_acked + cubic->current.pending_bytes_acked) / cstat->cwnd; + + cubic->current.pending_bytes_acked += bytes_acked; + cubic->current.pending_bytes_acked -= m * cstat->cwnd; + + assert(cubic->current.pending_bytes_acked < cstat->cwnd); + + cstat->cwnd += m; } - ngtcp2_log_info(cubic->cc.log, NGTCP2_LOG_EVENT_CCA, - "%" PRIu64 " bytes acked, cubic-ca cwnd=%" PRIu64 - " k_m=%" PRIu64 " target=%" PRIu64 " w_est=%" PRIu64, - ack->bytes_delivered, cstat->cwnd, cubic->current.k_m, target, - cubic->current.w_est); + set_pacing_rate(cstat); + + ngtcp2_log_infof(cubic->cc.log, NGTCP2_LOG_EVENT_CCA, ack->bytes_delivered, + " bytes acked, cubic-ca cwnd=", cstat->cwnd, + " k_m=", cubic->current.k_m, " target=", target, + " w_est=", cubic->current.w_est); } void ngtcp2_cc_cubic_cc_congestion_event(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, ngtcp2_tstamp sent_ts, - uint64_t bytes_lost, + const ngtcp2_cc_ack *ack, ngtcp2_tstamp ts) { ngtcp2_cc_cubic *cubic = ngtcp2_struct_of(cc, ngtcp2_cc_cubic, cc); uint64_t flight_size; @@ -428,8 +486,8 @@ void ngtcp2_cc_cubic_cc_congestion_event(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, cubic->current.epoch_start = ts; cubic->current.app_limited_start_ts = UINT64_MAX; cubic->current.app_limited_duration = 0; - cubic->current.pending_bytes_delivered = 0; - cubic->current.pending_est_bytes_delivered = 0; + cubic->current.pending_bytes_acked = 0; + cubic->current.pending_est_bytes_acked = 0; if (cstat->cwnd < cubic->current.w_max) { cubic->current.w_max = cstat->cwnd * 17 / 20; @@ -438,19 +496,18 @@ void ngtcp2_cc_cubic_cc_congestion_event(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, } cubic->current.w_max = - ngtcp2_max_uint64(cubic->current.w_max, 2 * cstat->max_tx_udp_payload_size); + ngtcp2_max(cubic->current.w_max, 2 * cstat->max_tx_udp_payload_size); cstat->ssthresh = cstat->cwnd * 7 / 10; if (cubic->rst->rs.delivered * 2 < cstat->cwnd) { - flight_size = cstat->bytes_in_flight + bytes_lost; - cstat->ssthresh = ngtcp2_min_uint64( - cstat->ssthresh, - ngtcp2_max_uint64(cubic->rst->rs.delivered, flight_size)); + flight_size = cstat->bytes_in_flight + ack->bytes_lost; + cstat->ssthresh = ngtcp2_min( + cstat->ssthresh, ngtcp2_max(cubic->rst->rs.delivered, flight_size)); } cstat->ssthresh = - ngtcp2_max_uint64(cstat->ssthresh, 2 * cstat->max_tx_udp_payload_size); + ngtcp2_max(cstat->ssthresh, 2 * cstat->max_tx_udp_payload_size); cubic->current.cwnd_prior = cstat->cwnd; cstat->cwnd = cstat->ssthresh; @@ -464,9 +521,10 @@ void ngtcp2_cc_cubic_cc_congestion_event(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, cubic->current.k_m = ngtcp2_cbrt((cwnd_delta << 30) * 10 / 4 / cstat->max_tx_udp_payload_size); - ngtcp2_log_info(cubic->cc.log, NGTCP2_LOG_EVENT_CCA, - "reduce cwnd because of packet loss cwnd=%" PRIu64, - cstat->cwnd); + set_pacing_rate(cstat); + + ngtcp2_log_infof(cubic->cc.log, NGTCP2_LOG_EVENT_CCA, + "reduce cwnd because of packet loss cwnd=", cstat->cwnd); } void ngtcp2_cc_cubic_cc_on_spurious_congestion(ngtcp2_cc *cc, @@ -482,10 +540,12 @@ void ngtcp2_cc_cubic_cc_on_spurious_congestion(ngtcp2_cc *cc, cstat->cwnd = cubic->undo.cwnd; cstat->ssthresh = cubic->undo.ssthresh; - ngtcp2_log_info(cubic->cc.log, NGTCP2_LOG_EVENT_CCA, - "spurious congestion is detected and congestion state is " - "restored cwnd=%" PRIu64, - cstat->cwnd); + set_pacing_rate(cstat); + + ngtcp2_log_infof(cubic->cc.log, NGTCP2_LOG_EVENT_CCA, + "spurious congestion is detected and congestion state is " + "restored cwnd=", + cstat->cwnd); } cubic_vars_reset(&cubic->undo.v); @@ -499,17 +559,18 @@ void ngtcp2_cc_cubic_cc_on_persistent_congestion(ngtcp2_cc *cc, ngtcp2_cc_cubic *cubic = ngtcp2_struct_of(cc, ngtcp2_cc_cubic, cc); (void)ts; - cubic_cc_reset(cubic); + cubic_cc_reset(cubic, cstat); cstat->cwnd = 2 * cstat->max_tx_udp_payload_size; cstat->congestion_recovery_start_ts = UINT64_MAX; + + set_pacing_rate(cstat); } void ngtcp2_cc_cubic_cc_reset(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, ngtcp2_tstamp ts) { ngtcp2_cc_cubic *cubic = ngtcp2_struct_of(cc, ngtcp2_cc_cubic, cc); - (void)cstat; (void)ts; - cubic_cc_reset(cubic); + cubic_cc_reset(cubic, cstat); } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_cc.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_cc.h index e4e19e1aa6b945..a19e90b1d0db47 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_cc.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_cc.h @@ -88,11 +88,6 @@ typedef struct ngtcp2_cc_pkt { * acknowledged and lost bytes. */ typedef struct ngtcp2_cc_ack { - /** - * :member:`prior_bytes_in_flight` is the in-flight bytes before - * processing this ACK. - */ - uint64_t prior_bytes_in_flight; /** * :member:`bytes_delivered` is the number of bytes acknowledged. */ @@ -143,13 +138,16 @@ typedef void (*ngtcp2_cc_on_pkt_lost)(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, * @functypedef * * :type:`ngtcp2_cc_congestion_event` is a callback function which is - * called when congestion event happens (e.g., when packet is lost). - * |bytes_lost| is the number of bytes lost in this congestion event. + * called when congestion event happens (e.g., when packet is lost or + * due to ECN). |ack| contains information after ACK processing. + * This callback may be called from non-ACK processing context. In + * that case, the information only taken from |ack| processing has + * default values, like 0 or UINT64_MAX; */ typedef void (*ngtcp2_cc_congestion_event)(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, ngtcp2_tstamp sent_ts, - uint64_t bytes_lost, + const ngtcp2_cc_ack *ack, ngtcp2_tstamp ts); /** @@ -186,20 +184,12 @@ typedef void (*ngtcp2_cc_on_ack_recv)(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, * @functypedef * * :type:`ngtcp2_cc_on_pkt_sent` is a callback function which is - * called when an ack-eliciting packet is sent. + * called when an ack-eliciting packet is sent. The lost, + * tx_in_flight, and is_app_limited fields in |pkt| are set to 0. */ typedef void (*ngtcp2_cc_on_pkt_sent)(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, const ngtcp2_cc_pkt *pkt); -/** - * @functypedef - * - * :type:`ngtcp2_cc_new_rtt_sample` is a callback function which is - * called when new RTT sample is obtained. - */ -typedef void (*ngtcp2_cc_new_rtt_sample)(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, - ngtcp2_tstamp ts); - /** * @functypedef * @@ -209,28 +199,6 @@ typedef void (*ngtcp2_cc_new_rtt_sample)(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, typedef void (*ngtcp2_cc_reset)(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, ngtcp2_tstamp ts); -/** - * @enum - * - * :type:`ngtcp2_cc_event_type` defines congestion control events. - */ -typedef enum ngtcp2_cc_event_type { - /** - * :enum:`NGTCP2_CC_EVENT_TX_START` occurs when ack-eliciting packet - * is sent and no other ack-eliciting packet is present. - */ - NGTCP2_CC_EVENT_TYPE_TX_START -} ngtcp2_cc_event_type; - -/** - * @functypedef - * - * :type:`ngtcp2_cc_event` is a callback function which is called when - * a specific event happens. - */ -typedef void (*ngtcp2_cc_event)(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, - ngtcp2_cc_event_type event, ngtcp2_tstamp ts); - /** * @struct * @@ -254,7 +222,7 @@ typedef struct ngtcp2_cc { ngtcp2_cc_on_pkt_lost on_pkt_lost; /** * :member:`congestion_event` is a callback function which is called - * when congestion event happens (.e.g, packet is lost). + * when congestion event happens (e.g., packet is lost). */ ngtcp2_cc_congestion_event congestion_event; /** @@ -277,21 +245,11 @@ typedef struct ngtcp2_cc { * ack-eliciting packet is sent. */ ngtcp2_cc_on_pkt_sent on_pkt_sent; - /** - * :member:`new_rtt_sample` is a callback function which is called - * when new RTT sample is obtained. - */ - ngtcp2_cc_new_rtt_sample new_rtt_sample; /** * :member:`reset` is a callback function which is called when * congestion control state must be reset. */ ngtcp2_cc_reset reset; - /** - * :member:`event` is a callback function which is called when a - * specific event happens. - */ - ngtcp2_cc_event event; } ngtcp2_cc; /* @@ -310,14 +268,16 @@ typedef struct ngtcp2_cc_reno { uint64_t pending_add; } ngtcp2_cc_reno; -void ngtcp2_cc_reno_init(ngtcp2_cc_reno *reno, ngtcp2_log *log); +void ngtcp2_cc_reno_init(ngtcp2_cc_reno *reno, ngtcp2_log *log, + ngtcp2_conn_stat *cstat); void ngtcp2_cc_reno_cc_on_pkt_acked(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, const ngtcp2_cc_pkt *pkt, ngtcp2_tstamp ts); void ngtcp2_cc_reno_cc_congestion_event(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, ngtcp2_tstamp sent_ts, - uint64_t bytes_lost, ngtcp2_tstamp ts); + const ngtcp2_cc_ack *ack, + ngtcp2_tstamp ts); void ngtcp2_cc_reno_cc_on_persistent_congestion(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, @@ -340,8 +300,8 @@ typedef struct ngtcp2_cubic_vars { /* app_limited_duration is the cumulative duration where a connection is under app limited when ACK is received. */ ngtcp2_duration app_limited_duration; - uint64_t pending_bytes_delivered; - uint64_t pending_est_bytes_delivered; + uint64_t pending_bytes_acked; + uint64_t pending_est_bytes_acked; } ngtcp2_cubic_vars; /* ngtcp2_cc_cubic is CUBIC congestion controller. */ @@ -371,14 +331,15 @@ typedef struct ngtcp2_cc_cubic { } ngtcp2_cc_cubic; void ngtcp2_cc_cubic_init(ngtcp2_cc_cubic *cc, ngtcp2_log *log, - ngtcp2_rst *rst); + ngtcp2_conn_stat *cstat, ngtcp2_rst *rst); void ngtcp2_cc_cubic_cc_on_ack_recv(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, const ngtcp2_cc_ack *ack, ngtcp2_tstamp ts); void ngtcp2_cc_cubic_cc_congestion_event(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, ngtcp2_tstamp sent_ts, - uint64_t bytes_lost, ngtcp2_tstamp ts); + const ngtcp2_cc_ack *ack, + ngtcp2_tstamp ts); void ngtcp2_cc_cubic_cc_on_spurious_congestion(ngtcp2_cc *ccx, ngtcp2_conn_stat *cstat, diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_cid.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_cid.c index acbee78aaf435f..4eba2aaca5d316 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_cid.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_cid.c @@ -29,8 +29,9 @@ #include "ngtcp2_path.h" #include "ngtcp2_str.h" +#include "ngtcp2_pkt.h" -void ngtcp2_cid_zero(ngtcp2_cid *cid) { memset(cid, 0, sizeof(*cid)); } +void ngtcp2_cid_zero(ngtcp2_cid *cid) { *cid = (ngtcp2_cid){0}; } void ngtcp2_cid_init(ngtcp2_cid *cid, const uint8_t *data, size_t datalen) { assert(datalen <= NGTCP2_MAX_CIDLEN); @@ -58,11 +59,13 @@ int ngtcp2_cid_less(const ngtcp2_cid *lhs, const ngtcp2_cid *rhs) { int ngtcp2_cid_empty(const ngtcp2_cid *cid) { return cid->datalen == 0; } void ngtcp2_scid_init(ngtcp2_scid *scid, uint64_t seq, const ngtcp2_cid *cid) { - scid->pe.index = NGTCP2_PQ_BAD_INDEX; - scid->seq = seq; - scid->cid = *cid; - scid->retired_ts = UINT64_MAX; - scid->flags = NGTCP2_SCID_FLAG_NONE; + *scid = (ngtcp2_scid){ + .pe.index = NGTCP2_PQ_BAD_INDEX, + .seq = seq, + .cid = *cid, + .retired_ts = UINT64_MAX, + .flags = NGTCP2_SCID_FLAG_NONE, + }; } void ngtcp2_scid_copy(ngtcp2_scid *dest, const ngtcp2_scid *src) { @@ -72,12 +75,12 @@ void ngtcp2_scid_copy(ngtcp2_scid *dest, const ngtcp2_scid *src) { } void ngtcp2_dcid_init(ngtcp2_dcid *dcid, uint64_t seq, const ngtcp2_cid *cid, - const uint8_t *token) { + const ngtcp2_stateless_reset_token *token) { dcid->seq = seq; dcid->cid = *cid; if (token) { - memcpy(dcid->token, token, NGTCP2_STATELESS_RESET_TOKENLEN); + dcid->token = *token; dcid->flags = NGTCP2_DCID_FLAG_TOKEN_PRESENT; } else { dcid->flags = NGTCP2_DCID_FLAG_NONE; @@ -91,11 +94,12 @@ void ngtcp2_dcid_init(ngtcp2_dcid *dcid, uint64_t seq, const ngtcp2_cid *cid, dcid->max_udp_payload_size = NGTCP2_MAX_UDP_PAYLOAD_SIZE; } -void ngtcp2_dcid_set_token(ngtcp2_dcid *dcid, const uint8_t *token) { +void ngtcp2_dcid_set_token(ngtcp2_dcid *dcid, + const ngtcp2_stateless_reset_token *token) { assert(token); dcid->flags |= NGTCP2_DCID_FLAG_TOKEN_PRESENT; - memcpy(dcid->token, token, NGTCP2_STATELESS_RESET_TOKENLEN); + dcid->token = *token; } void ngtcp2_dcid_set_path(ngtcp2_dcid *dcid, const ngtcp2_path *path) { @@ -104,7 +108,7 @@ void ngtcp2_dcid_set_path(ngtcp2_dcid *dcid, const ngtcp2_path *path) { void ngtcp2_dcid_copy(ngtcp2_dcid *dest, const ngtcp2_dcid *src) { ngtcp2_dcid_init(dest, src->seq, &src->cid, - (src->flags & NGTCP2_DCID_FLAG_TOKEN_PRESENT) ? src->token + (src->flags & NGTCP2_DCID_FLAG_TOKEN_PRESENT) ? &src->token : NULL); ngtcp2_path_copy(&dest->ps.path, &src->ps.path); dest->retired_ts = src->retired_ts; @@ -121,18 +125,19 @@ void ngtcp2_dcid_copy_cid_token(ngtcp2_dcid *dest, const ngtcp2_dcid *src) { if (src->flags & NGTCP2_DCID_FLAG_TOKEN_PRESENT) { dest->flags |= NGTCP2_DCID_FLAG_TOKEN_PRESENT; - memcpy(dest->token, src->token, NGTCP2_STATELESS_RESET_TOKENLEN); + dest->token = src->token; } else if (dest->flags & NGTCP2_DCID_FLAG_TOKEN_PRESENT) { dest->flags &= (uint8_t)~NGTCP2_DCID_FLAG_TOKEN_PRESENT; } } int ngtcp2_dcid_verify_uniqueness(const ngtcp2_dcid *dcid, uint64_t seq, - const ngtcp2_cid *cid, const uint8_t *token) { + const ngtcp2_cid *cid, + const ngtcp2_stateless_reset_token *token) { if (dcid->seq == seq) { return ngtcp2_cid_eq(&dcid->cid, cid) && (dcid->flags & NGTCP2_DCID_FLAG_TOKEN_PRESENT) && - memcmp(dcid->token, token, NGTCP2_STATELESS_RESET_TOKENLEN) == 0 + ngtcp2_stateless_reset_token_eq(&dcid->token, token) ? 0 : NGTCP2_ERR_PROTO; } @@ -140,12 +145,12 @@ int ngtcp2_dcid_verify_uniqueness(const ngtcp2_dcid *dcid, uint64_t seq, return !ngtcp2_cid_eq(&dcid->cid, cid) ? 0 : NGTCP2_ERR_PROTO; } -int ngtcp2_dcid_verify_stateless_reset_token(const ngtcp2_dcid *dcid, - const ngtcp2_path *path, - const uint8_t *token) { +int ngtcp2_dcid_verify_stateless_reset_token( + const ngtcp2_dcid *dcid, const ngtcp2_path *path, + const ngtcp2_stateless_reset_token *token) { return ngtcp2_path_eq(&dcid->ps.path, path) && (dcid->flags & NGTCP2_DCID_FLAG_TOKEN_PRESENT) && - ngtcp2_cmemeq(dcid->token, token, NGTCP2_STATELESS_RESET_TOKENLEN) + ngtcp2_cmemeq(dcid->token.data, token->data, sizeof(token->data)) ? 0 : NGTCP2_ERR_INVALID_ARGUMENT; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_cid.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_cid.h index 9321cfb64e6daf..f4956b0bac0ebe 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_cid.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_cid.h @@ -35,13 +35,13 @@ #include "ngtcp2_path.h" /* NGTCP2_SCID_FLAG_NONE indicates that no flag is set. */ -#define NGTCP2_SCID_FLAG_NONE 0x00u +#define NGTCP2_SCID_FLAG_NONE 0x00U /* NGTCP2_SCID_FLAG_USED indicates that a local endpoint observed that a remote endpoint uses this particular Connection ID. */ -#define NGTCP2_SCID_FLAG_USED 0x01u +#define NGTCP2_SCID_FLAG_USED 0x01U /* NGTCP2_SCID_FLAG_RETIRED indicates that this particular Connection ID is retired. */ -#define NGTCP2_SCID_FLAG_RETIRED 0x02u +#define NGTCP2_SCID_FLAG_RETIRED 0x02U typedef struct ngtcp2_scid { ngtcp2_pq_entry pe; @@ -57,13 +57,13 @@ typedef struct ngtcp2_scid { } ngtcp2_scid; /* NGTCP2_DCID_FLAG_NONE indicates that no flag is set. */ -#define NGTCP2_DCID_FLAG_NONE 0x00u +#define NGTCP2_DCID_FLAG_NONE 0x00U /* NGTCP2_DCID_FLAG_PATH_VALIDATED indicates that an associated path has been validated. */ -#define NGTCP2_DCID_FLAG_PATH_VALIDATED 0x01u +#define NGTCP2_DCID_FLAG_PATH_VALIDATED 0x01U /* NGTCP2_DCID_FLAG_TOKEN_PRESENT indicates that a stateless reset token is set in token field. */ -#define NGTCP2_DCID_FLAG_TOKEN_PRESENT 0x02u +#define NGTCP2_DCID_FLAG_TOKEN_PRESENT 0x02U typedef struct ngtcp2_dcid { /* seq is the sequence number associated to the Connection ID. */ @@ -93,7 +93,7 @@ typedef struct ngtcp2_dcid { /* token is a stateless reset token received along with this Connection ID. The stateless reset token is tied to the connection, not to the particular Connection ID. */ - uint8_t token[NGTCP2_STATELESS_RESET_TOKENLEN]; + ngtcp2_stateless_reset_token token; } ngtcp2_dcid; /* ngtcp2_cid_zero makes |cid| zero-length. */ @@ -123,17 +123,18 @@ void ngtcp2_scid_copy(ngtcp2_scid *dest, const ngtcp2_scid *src); /* * ngtcp2_dcid_init initializes |dcid| with the given parameters. If - * |token| is NULL, the function fills dcid->token with 0. |token| - * must be NGTCP2_STATELESS_RESET_TOKENLEN bytes long. + * |token| is not NULL, the function sets + * NGTCP2_DCID_FLAG_TOKEN_PRESENT flag. */ void ngtcp2_dcid_init(ngtcp2_dcid *dcid, uint64_t seq, const ngtcp2_cid *cid, - const uint8_t *token); + const ngtcp2_stateless_reset_token *token); /* * ngtcp2_dcid_set_token sets |token| to |dcid|. |token| must not be - * NULL, and must be NGTCP2_STATELESS_RESET_TOKENLEN bytes long. + * NULL. */ -void ngtcp2_dcid_set_token(ngtcp2_dcid *dcid, const uint8_t *token); +void ngtcp2_dcid_set_token(ngtcp2_dcid *dcid, + const ngtcp2_stateless_reset_token *token); /* * ngtcp2_dcid_set_path sets |path| to |dcid|. It sets @@ -160,7 +161,8 @@ void ngtcp2_dcid_copy_cid_token(ngtcp2_dcid *dest, const ngtcp2_dcid *src); * |token|) tuple against |dcid|. */ int ngtcp2_dcid_verify_uniqueness(const ngtcp2_dcid *dcid, uint64_t seq, - const ngtcp2_cid *cid, const uint8_t *token); + const ngtcp2_cid *cid, + const ngtcp2_stateless_reset_token *token); /* * ngtcp2_dcid_verify_stateless_reset_token verifies stateless reset @@ -172,9 +174,9 @@ int ngtcp2_dcid_verify_uniqueness(const ngtcp2_dcid *dcid, uint64_t seq, * NGTCP2_ERR_INVALID_ARGUMENT * Tokens do not match; or |dcid| does not contain a token. */ -int ngtcp2_dcid_verify_stateless_reset_token(const ngtcp2_dcid *dcid, - const ngtcp2_path *path, - const uint8_t *token); +int ngtcp2_dcid_verify_stateless_reset_token( + const ngtcp2_dcid *dcid, const ngtcp2_path *path, + const ngtcp2_stateless_reset_token *token); /* TODO It might be performance win if we store congestion state in this entry, and restore it when migrate back to this path. */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.c index d97ba6a71b2529..28bbde1233e1e4 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.c @@ -42,6 +42,7 @@ #include "ngtcp2_callbacks.h" #include "ngtcp2_tstamp.h" #include "ngtcp2_frame_chain.h" +#include "ngtcp2_conn_info.h" /* NGTCP2_FLOW_WINDOW_RTT_FACTOR is the factor of RTT when flow control window auto-tuning is triggered. */ @@ -57,13 +58,23 @@ are not processed. */ #define NGTCP2_MAX_ACK_PER_PKT 1 +/* NGTCP2_MAX_INITIAL_CRYPTO_OFFSET is the maximum offset of CRYPTO + data at Initial encryption level. */ +#define NGTCP2_MAX_INITIAL_CRYPTO_OFFSET (16 * 1024) +/* NGTCP2_MAX_HANDSHAKE_CRYPTO_OFFSET is the maximum offset of CRYPTO + data at Handshake encryption level. */ +#define NGTCP2_MAX_HANDSHAKE_CRYPTO_OFFSET (64 * 1024) +/* NGTCP2_MAX_1RTT_CRYPTO_OFFSET is the maximum offset of CRYPTO data + at 1RTT encryption level. */ +#define NGTCP2_MAX_1RTT_CRYPTO_OFFSET (256 * 1024) + ngtcp2_objalloc_def(strm, ngtcp2_strm, oplent) /* * conn_local_stream returns nonzero if |stream_id| indicates that it * is the stream initiated by local endpoint. */ -static int conn_local_stream(ngtcp2_conn *conn, int64_t stream_id) { +static int conn_local_stream(const ngtcp2_conn *conn, int64_t stream_id) { return (uint8_t)(stream_id & 1) == conn->server; } @@ -85,7 +96,7 @@ static void conn_update_timestamp(ngtcp2_conn *conn, ngtcp2_tstamp ts) { * conn_is_tls_handshake_completed returns nonzero if TLS handshake * has completed and 1 RTT keys are available. */ -static int conn_is_tls_handshake_completed(ngtcp2_conn *conn) { +static int conn_is_tls_handshake_completed(const ngtcp2_conn *conn) { return (conn->flags & NGTCP2_CONN_FLAG_TLS_HANDSHAKE_COMPLETED) && conn->pktns.crypto.rx.ckm && conn->pktns.crypto.tx.ckm; } @@ -254,13 +265,20 @@ static int conn_call_extend_max_local_streams_uni(ngtcp2_conn *conn, } static int conn_call_get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, - uint8_t *token, size_t cidlen) { + ngtcp2_stateless_reset_token *token, + size_t cidlen) { int rv; - assert(conn->callbacks.get_new_connection_id); + if (conn->callbacks.get_new_connection_id2) { + rv = conn->callbacks.get_new_connection_id2(conn, cid, token, cidlen, + conn->user_data); + } else { + assert(conn->callbacks.get_new_connection_id); + + rv = conn->callbacks.get_new_connection_id(conn, cid, token->data, cidlen, + conn->user_data); + } - rv = conn->callbacks.get_new_connection_id(conn, cid, token, cidlen, - conn->user_data); if (rv != 0) { return NGTCP2_ERR_CALLBACK_FAILURE; } @@ -423,14 +441,20 @@ static int conn_call_dcid_status(ngtcp2_conn *conn, const ngtcp2_dcid *dcid) { int rv; - if (!conn->callbacks.dcid_status) { + if (conn->callbacks.dcid_status2) { + rv = conn->callbacks.dcid_status2( + conn, type, dcid->seq, &dcid->cid, + (dcid->flags & NGTCP2_DCID_FLAG_TOKEN_PRESENT) ? &dcid->token : NULL, + conn->user_data); + } else if (conn->callbacks.dcid_status) { + rv = conn->callbacks.dcid_status( + conn, type, dcid->seq, &dcid->cid, + (dcid->flags & NGTCP2_DCID_FLAG_TOKEN_PRESENT) ? dcid->token.data : NULL, + conn->user_data); + } else { return 0; } - rv = conn->callbacks.dcid_status( - conn, type, dcid->seq, &dcid->cid, - (dcid->flags & NGTCP2_DCID_FLAG_TOKEN_PRESENT) ? dcid->token : NULL, - conn->user_data); if (rv != 0) { return NGTCP2_ERR_CALLBACK_FAILURE; } @@ -503,12 +527,19 @@ static int conn_call_client_initial(ngtcp2_conn *conn) { return 0; } -static int conn_call_get_path_challenge_data(ngtcp2_conn *conn, uint8_t *data) { +static int conn_call_get_path_challenge_data(ngtcp2_conn *conn, + ngtcp2_path_challenge_data *data) { int rv; - assert(conn->callbacks.get_path_challenge_data); + if (conn->callbacks.get_path_challenge_data2) { + rv = conn->callbacks.get_path_challenge_data2(conn, data, conn->user_data); + } else { + assert(conn->callbacks.get_path_challenge_data); + + rv = conn->callbacks.get_path_challenge_data(conn, data->data, + conn->user_data); + } - rv = conn->callbacks.get_path_challenge_data(conn, data, conn->user_data); if (rv != 0) { return NGTCP2_ERR_CALLBACK_FAILURE; } @@ -549,14 +580,24 @@ static int conn_call_recv_retry(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd) { static int conn_call_recv_stateless_reset(ngtcp2_conn *conn, - const ngtcp2_pkt_stateless_reset *sr) { + const ngtcp2_pkt_stateless_reset2 *sr) { int rv; + ngtcp2_pkt_stateless_reset legacy_sr; - if (!conn->callbacks.recv_stateless_reset) { + if (conn->callbacks.recv_stateless_reset2) { + rv = conn->callbacks.recv_stateless_reset2(conn, sr, conn->user_data); + } else if (conn->callbacks.recv_stateless_reset) { + memcpy(legacy_sr.stateless_reset_token, sr->token.data, + sizeof(legacy_sr.stateless_reset_token)); + legacy_sr.rand = sr->rand; + legacy_sr.randlen = sr->randlen; + + rv = + conn->callbacks.recv_stateless_reset(conn, &legacy_sr, conn->user_data); + } else { return 0; } - rv = conn->callbacks.recv_stateless_reset(conn, sr, conn->user_data); if (rv != 0) { return NGTCP2_ERR_CALLBACK_FAILURE; } @@ -696,8 +737,10 @@ static int conn_call_recv_tx_key(ngtcp2_conn *conn, return 0; } -// pktns_init initializes |pktns|. It assumes that the object pointed -// by |pktns| is zero-cleared. +/* + * pktns_init initializes |pktns|. It assumes that the object pointed + * by |pktns| is zero-cleared. + */ static void pktns_init(ngtcp2_pktns *pktns, ngtcp2_pktns_id pktns_id, ngtcp2_rst *rst, ngtcp2_cc *cc, int64_t initial_pkt_num, ngtcp2_log *log, ngtcp2_qlog *qlog, @@ -821,7 +864,7 @@ static void conn_reset_conn_stat_cc(ngtcp2_conn *conn, cstat->pto_count = 0; cstat->loss_detection_timer = UINT64_MAX; cstat->max_tx_udp_payload_size = - ngtcp2_conn_get_path_max_tx_udp_payload_size(conn); + ngtcp2_conn_get_path_max_tx_udp_payload_size2(conn); cstat->cwnd = ngtcp2_cc_compute_initcwnd(cstat->max_tx_udp_payload_size); cstat->ssthresh = UINT64_MAX; cstat->congestion_recovery_start_ts = UINT64_MAX; @@ -837,8 +880,8 @@ static void conn_reset_conn_stat_cc(ngtcp2_conn *conn, */ static void reset_conn_stat_recovery(ngtcp2_conn_stat *cstat) { /* Initializes them with UINT64_MAX. */ - memset(cstat->loss_time, 0xff, sizeof(cstat->loss_time)); - memset(cstat->last_tx_pkt_ts, 0xff, sizeof(cstat->last_tx_pkt_ts)); + memset(cstat->loss_time, 0xFF, sizeof(cstat->loss_time)); + memset(cstat->last_tx_pkt_ts, 0xFF, sizeof(cstat->last_tx_pkt_ts)); } /* @@ -865,15 +908,15 @@ static void delete_scid(ngtcp2_ksl *scids, const ngtcp2_mem *mem) { static ngtcp2_duration compute_pto(ngtcp2_duration smoothed_rtt, ngtcp2_duration rttvar, ngtcp2_duration max_ack_delay) { - ngtcp2_duration var = ngtcp2_max_uint64(4 * rttvar, NGTCP2_GRANULARITY); + ngtcp2_duration var = ngtcp2_max(4 * rttvar, NGTCP2_GRANULARITY); return smoothed_rtt + var + max_ack_delay; } /* * conn_compute_initial_pto computes PTO using the initial RTT. */ -static ngtcp2_duration conn_compute_initial_pto(ngtcp2_conn *conn, - ngtcp2_pktns *pktns) { +static ngtcp2_duration conn_compute_initial_pto(const ngtcp2_conn *conn, + const ngtcp2_pktns *pktns) { ngtcp2_duration initial_rtt = conn->local.settings.initial_rtt; ngtcp2_duration max_ack_delay; @@ -886,12 +929,9 @@ static ngtcp2_duration conn_compute_initial_pto(ngtcp2_conn *conn, return compute_pto(initial_rtt, initial_rtt / 2, max_ack_delay); } -/* - * conn_compute_pto computes the current PTO. - */ -static ngtcp2_duration conn_compute_pto(ngtcp2_conn *conn, - ngtcp2_pktns *pktns) { - ngtcp2_conn_stat *cstat = &conn->cstat; +ngtcp2_duration ngtcp2_conn_compute_pto(const ngtcp2_conn *conn, + const ngtcp2_pktns *pktns) { + const ngtcp2_conn_stat *cstat = &conn->cstat; ngtcp2_duration max_ack_delay; if (pktns->id == NGTCP2_PKTNS_ID_APPLICATION && @@ -903,28 +943,23 @@ static ngtcp2_duration conn_compute_pto(ngtcp2_conn *conn, return compute_pto(cstat->smoothed_rtt, cstat->rttvar, max_ack_delay); } -ngtcp2_duration ngtcp2_conn_compute_pto(ngtcp2_conn *conn, - ngtcp2_pktns *pktns) { - return conn_compute_pto(conn, pktns); -} - /* * conn_compute_pv_timeout_pto returns path validation timeout using * the given |pto|. */ -static ngtcp2_duration conn_compute_pv_timeout_pto(ngtcp2_conn *conn, +static ngtcp2_duration conn_compute_pv_timeout_pto(const ngtcp2_conn *conn, ngtcp2_duration pto) { ngtcp2_duration initial_pto = conn_compute_initial_pto(conn, &conn->pktns); - return 3 * ngtcp2_max_uint64(pto, initial_pto); + return 3 * ngtcp2_max(pto, initial_pto); } /* * conn_compute_pv_timeout returns path validation timeout. */ -static ngtcp2_duration conn_compute_pv_timeout(ngtcp2_conn *conn) { - return conn_compute_pv_timeout_pto(conn, - conn_compute_pto(conn, &conn->pktns)); +static ngtcp2_duration conn_compute_pv_timeout(const ngtcp2_conn *conn) { + return conn_compute_pv_timeout_pto( + conn, ngtcp2_conn_compute_pto(conn, &conn->pktns)); } static void conn_handle_tx_ecn(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, @@ -959,7 +994,7 @@ static void conn_handle_tx_ecn(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, conn->tx.ecn.validation_start_ts = ts; } else if (ts - conn->tx.ecn.validation_start_ts >= - 3 * conn_compute_pto(conn, pktns)) { + 3 * ngtcp2_conn_compute_pto(conn, pktns)) { conn->tx.ecn.state = NGTCP2_ECN_STATE_UNKNOWN; break; } @@ -1069,13 +1104,13 @@ static void conn_update_skip_pkt(ngtcp2_conn *conn, ngtcp2_pktns *pktns) { conn->callbacks.rand(&r, 1, &conn->local.settings.rand_ctx); - if (1ll << pktns->tx.skip_pkt.exponent > + if (1LL << pktns->tx.skip_pkt.exponent > (NGTCP2_MAX_PKT_NUM - min_gap) / ((int64_t)r + 1)) { pktns->tx.skip_pkt.next_pkt_num = INT64_MAX; return; } - gap = ((int64_t)r + 1) * (1ll << pktns->tx.skip_pkt.exponent++) + min_gap; + gap = ((int64_t)r + 1) * (1LL << pktns->tx.skip_pkt.exponent++) + min_gap; if (pktns->tx.last_pkt_num > NGTCP2_MAX_PKT_NUM - gap) { pktns->tx.skip_pkt.next_pkt_num = INT64_MAX; @@ -1084,8 +1119,8 @@ static void conn_update_skip_pkt(ngtcp2_conn *conn, ngtcp2_pktns *pktns) { pktns->tx.skip_pkt.next_pkt_num = pktns->tx.last_pkt_num + gap; - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, "next skip pkn=%" PRId64, - pktns->tx.skip_pkt.next_pkt_num); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "next skip pkn=", pktns->tx.skip_pkt.next_pkt_num); } static int conn_handle_skip_pkt(ngtcp2_conn *conn, ngtcp2_pktns *pktns, @@ -1126,11 +1161,11 @@ static int conn_handle_skip_pkt(ngtcp2_conn *conn, ngtcp2_pktns *pktns, } static size_t buflen_align(size_t buflen) { - return (buflen + 0x7) & (size_t)~0x7; + return (buflen + 0x7U) & ~(size_t)0x7U; } static void *buf_align(void *buf) { - return (void *)((uintptr_t)((uint8_t *)buf + 0x7) & (uintptr_t)~0x7); + return (void *)((uintptr_t)((uint8_t *)buf + 0x7U) & ~(uintptr_t)0x7U); } static void *buf_advance(void *buf, size_t n) { return (uint8_t *)buf + n; } @@ -1145,7 +1180,7 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, const ngtcp2_mem *mem, void *user_data, int server) { int rv; ngtcp2_scid *scident; - void *buf, *tokenbuf; + void *buf, *tokenbuf, *logbuf; size_t buflen; uint8_t fixed_bit_byte; size_t i; @@ -1165,8 +1200,8 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, assert(settings->max_window <= NGTCP2_MAX_VARINT); assert(settings->max_stream_window <= NGTCP2_MAX_VARINT); - assert(settings->max_tx_udp_payload_size); - assert(settings->max_tx_udp_payload_size <= NGTCP2_HARD_MAX_UDP_PAYLOAD_SIZE); + assert(settings->max_tx_udp_payload_size >= NGTCP2_MAX_UDP_PAYLOAD_SIZE); + assert(settings->max_tx_udp_payload_size <= NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE); assert(settings->initial_pkt_num <= INT32_MAX); assert(settings->initial_rtt); assert(params->active_connection_id_limit >= @@ -1193,15 +1228,17 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, assert(callbacks->hp_mask); assert(server || callbacks->recv_retry); assert(callbacks->rand); - assert(callbacks->get_new_connection_id); + assert(callbacks->get_new_connection_id2 || callbacks->get_new_connection_id); assert(callbacks->update_key); assert(callbacks->delete_crypto_aead_ctx); assert(callbacks->delete_crypto_cipher_ctx); - assert(callbacks->get_path_challenge_data); + assert(callbacks->get_path_challenge_data2 || + callbacks->get_path_challenge_data); assert(!server || !ngtcp2_is_reserved_version(client_chosen_version)); for (i = 0; i < settings->pmtud_probeslen; ++i) { assert(settings->pmtud_probes[i] > NGTCP2_MAX_UDP_PAYLOAD_SIZE); + assert(settings->pmtud_probes[i] <= NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE); } if (mem == NULL) { @@ -1240,6 +1277,11 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, buflen += sizeof(client_chosen_version); } + if (settings->log_write || settings->log_printf) { + buflen = buflen_align(buflen); + buflen += NGTCP2_LOG_BUFLEN; + } + buf = ngtcp2_mem_calloc(mem, 1, buflen); if (buf == NULL) { return NGTCP2_ERR_NOMEM; @@ -1277,8 +1319,18 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, ngtcp2_static_ringbuf_path_challenge_init(&(*pconn)->rx.path_challenge); - ngtcp2_log_init(&(*pconn)->log, scid, settings->log_printf, - settings->initial_ts, user_data); + if (settings->log_write || settings->log_printf) { + buf = buf_align(buf); + logbuf = (char *)buf; + buf = buf_advance(buf, NGTCP2_LOG_BUFLEN); + } else { + logbuf = NULL; + } + + ngtcp2_log_init(&(*pconn)->log, scid, settings->log_write, + settings->log_printf, logbuf, settings->initial_ts, + user_data); + ngtcp2_qlog_init(&(*pconn)->qlog, settings->qlog_write, settings->initial_ts, user_data); if ((*pconn)->qlog.write) { @@ -1334,11 +1386,12 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, switch (settings->cc_algo) { case NGTCP2_CC_ALGO_RENO: - ngtcp2_cc_reno_init(&(*pconn)->reno, &(*pconn)->log); + ngtcp2_cc_reno_init(&(*pconn)->reno, &(*pconn)->log, &(*pconn)->cstat); break; case NGTCP2_CC_ALGO_CUBIC: - ngtcp2_cc_cubic_init(&(*pconn)->cubic, &(*pconn)->log, &(*pconn)->rst); + ngtcp2_cc_cubic_init(&(*pconn)->cubic, &(*pconn)->log, &(*pconn)->cstat, + &(*pconn)->rst); break; case NGTCP2_CC_ALGO_BBR: @@ -1522,6 +1575,8 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, fail_token: ngtcp2_mem_free(mem, *pconn); + *pconn = NULL; + return rv; } @@ -1549,6 +1604,8 @@ int ngtcp2_conn_client_new_versioned( rv = ngtcp2_conn_commit_local_transport_params(*pconn); if (rv != 0) { ngtcp2_conn_del(*pconn); + *pconn = NULL; + return rv; } @@ -1588,9 +1645,10 @@ int ngtcp2_conn_server_new_versioned( * the given stream. Both connection and stream level flow control * credits are considered. */ -static uint64_t conn_fc_credits(ngtcp2_conn *conn, ngtcp2_strm *strm) { - return ngtcp2_min_uint64(strm->tx.max_offset - strm->tx.offset, - conn->tx.max_offset - conn->tx.offset); +static uint64_t conn_fc_credits(const ngtcp2_conn *conn, + const ngtcp2_strm *strm) { + return ngtcp2_min(strm->tx.max_offset - strm->tx.offset, + conn->tx.max_offset - conn->tx.offset); } /* @@ -1598,10 +1656,11 @@ static uint64_t conn_fc_credits(ngtcp2_conn *conn, ngtcp2_strm *strm) { * sent to the given stream. |len| might be shorted because of * available flow control credits. */ -static uint64_t conn_enforce_flow_control(ngtcp2_conn *conn, ngtcp2_strm *strm, +static uint64_t conn_enforce_flow_control(const ngtcp2_conn *conn, + const ngtcp2_strm *strm, uint64_t len) { uint64_t fc_credits = conn_fc_credits(conn, strm); - return ngtcp2_min_uint64(len, fc_credits); + return ngtcp2_min(len, fc_credits); } static int delete_strms_each(void *data, void *ptr) { @@ -1740,10 +1799,10 @@ void ngtcp2_conn_del(ngtcp2_conn *conn) { * conn_compute_ack_delay computes ACK delay for outgoing protected * ACK. */ -static ngtcp2_duration conn_compute_ack_delay(ngtcp2_conn *conn) { - return ngtcp2_min_uint64( +static ngtcp2_duration conn_compute_ack_delay(const ngtcp2_conn *conn) { + return ngtcp2_min( conn->local.transport_params.max_ack_delay, - ngtcp2_max_uint64(conn->cstat.smoothed_rtt / 8, NGTCP2_NANOSECONDS)); + ngtcp2_max(conn->cstat.smoothed_rtt / 8, NGTCP2_NANOSECONDS)); } /* @@ -1806,8 +1865,18 @@ static int conn_ppe_write_frame(ngtcp2_conn *conn, ngtcp2_ppe *ppe, static int conn_on_pkt_sent(ngtcp2_conn *conn, ngtcp2_pktns *pktns, ngtcp2_rtb_entry *ent) { ngtcp2_rtb *rtb = &pktns->rtb; + ngtcp2_cc_pkt cc_pkt; int rv; + if ((ent->flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING) && + conn->cc.on_pkt_sent) { + conn->cc.on_pkt_sent( + &conn->cc, &conn->cstat, + ngtcp2_cc_pkt_init(&cc_pkt, ent->hd.pkt_num, ent->pktlen, pktns->id, + ent->ts, /* lost = */ 0, + /* tx_in_flight = */ 0, /* is_app_limited = */ 0)); + } + /* This function implements OnPacketSent, but it handles only non-ACK-only packet. */ rv = ngtcp2_rtb_add(rtb, ent, &conn->cstat); @@ -1830,9 +1899,9 @@ static int conn_on_pkt_sent(ngtcp2_conn *conn, ngtcp2_pktns *pktns, * number. It returns the number of bytes to encode the packet * number. */ -static size_t pktns_select_pkt_numlen(ngtcp2_pktns *pktns) { +static size_t pktns_select_pkt_numlen(const ngtcp2_pktns *pktns) { int64_t pkt_num = pktns->tx.last_pkt_num + 1; - ngtcp2_rtb *rtb = &pktns->rtb; + const ngtcp2_rtb *rtb = &pktns->rtb; int64_t n = pkt_num - rtb->largest_acked_tx_pkt_num; if (NGTCP2_MAX_PKT_NUM / 2 < n) { @@ -1841,13 +1910,13 @@ static size_t pktns_select_pkt_numlen(ngtcp2_pktns *pktns) { n = n * 2 - 1; - if (n > 0xffffff) { + if (n > 0xFFFFFF) { return 4; } - if (n > 0xffff) { + if (n > 0xFFFF) { return 3; } - if (n > 0xff) { + if (n > 0xFF) { return 2; } return 1; @@ -1857,14 +1926,14 @@ static size_t pktns_select_pkt_numlen(ngtcp2_pktns *pktns) { * conn_cwnd_is_zero returns nonzero if the number of bytes the local * endpoint can sent at this time is zero. */ -static int conn_cwnd_is_zero(ngtcp2_conn *conn) { +static int conn_cwnd_is_zero(const ngtcp2_conn *conn) { uint64_t bytes_in_flight = conn->cstat.bytes_in_flight; uint64_t cwnd = conn->cstat.cwnd; if (bytes_in_flight >= cwnd) { - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_LDC, - "cwnd limited bytes_in_flight=%lu cwnd=%lu", - bytes_in_flight, cwnd); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_LDC, + "cwnd limited bytes_in_flight=", bytes_in_flight, + " cwnd=", cwnd); } return bytes_in_flight >= cwnd; @@ -1898,7 +1967,7 @@ static uint64_t conn_retry_early_payloadlen(ngtcp2_conn *conn) { /* Take the min because in conn_should_pad_pkt we take max in order to deal with unbreakable DATAGRAM. */ - return ngtcp2_min_uint64(len, NGTCP2_MIN_COALESCED_PAYLOADLEN); + return ngtcp2_min(len, NGTCP2_MIN_COALESCED_PAYLOADLEN); } return 0; @@ -1955,15 +2024,25 @@ static int conn_verify_dcid(ngtcp2_conn *conn, int *pnew_cid_used, return 0; } +static int conn_can_send_next_pkt(const ngtcp2_conn *conn, size_t left, + uint64_t min_payloadlen) { + /* TODO the next packet type should be taken into account */ + return left >= + /* TODO Assuming that pkt_num is encoded in 1 byte. */ + NGTCP2_MIN_LONG_HEADERLEN + conn->dcid.current.cid.datalen + + conn->oscid.datalen + NGTCP2_PKT_LENGTHLEN - 1 + min_payloadlen + + NGTCP2_MAX_AEAD_OVERHEAD; +} + /* * conn_should_pad_pkt returns nonzero if the packet should be padded. * |type| is the type of packet. |left| is the space left in packet * buffer. |write_datalen| is the number of bytes which will be sent * in the next, coalesced 0-RTT packet. */ -static int conn_should_pad_pkt(ngtcp2_conn *conn, uint8_t type, size_t left, - uint64_t write_datalen, int ack_eliciting, - int require_padding) { +static int conn_should_pad_pkt(const ngtcp2_conn *conn, uint8_t type, + size_t left, uint64_t write_datalen, + int ack_eliciting, int require_padding) { uint64_t min_payloadlen; if (type == NGTCP2_PKT_INITIAL) { @@ -1996,31 +2075,35 @@ static int conn_should_pad_pkt(ngtcp2_conn *conn, uint8_t type, size_t left, PADDING in that packet. Take maximum in case that write_datalen includes DATAGRAM which cannot be split. */ min_payloadlen = - ngtcp2_max_uint64(write_datalen, NGTCP2_MIN_COALESCED_PAYLOADLEN); + ngtcp2_max(write_datalen, NGTCP2_MIN_COALESCED_PAYLOADLEN); } else { return 1; } } - } else { - assert(type == NGTCP2_PKT_HANDSHAKE); - if (!require_padding) { - return 0; - } + return !conn_can_send_next_pkt(conn, left, min_payloadlen); + } - if (!conn->pktns.crypto.tx.ckm) { - return 1; - } + assert(type == NGTCP2_PKT_HANDSHAKE); - min_payloadlen = NGTCP2_MIN_COALESCED_PAYLOADLEN; + if (!require_padding) { + /* If we have 1RTT key, pad this Handshake packet so that the next + 1RTT packet can be squeezed into the same GSO buffer. */ + return conn->pktns.crypto.tx.ckm && + !conn_can_send_next_pkt(conn, left, NGTCP2_MIN_COALESCED_PAYLOADLEN); } - /* TODO the next packet type should be taken into account */ - return left < - /* TODO Assuming that pkt_num is encoded in 1 byte. */ - NGTCP2_MIN_LONG_HEADERLEN + conn->dcid.current.cid.datalen + - conn->oscid.datalen + NGTCP2_PKT_LENGTHLEN - 1 + min_payloadlen + - NGTCP2_MAX_AEAD_OVERHEAD; + if (!conn->pktns.crypto.tx.ckm) { + return 1; + } + + /* We might send Handshake packet even if exceeding CWND. In that + case, we do not write non-probe 1RTT packet. */ + if (conn_cwnd_is_zero(conn) && conn->pktns.rtb.probe_pkt_left == 0) { + return 1; + } + + return !conn_can_send_next_pkt(conn, left, NGTCP2_MIN_COALESCED_PAYLOADLEN); } static void conn_restart_timer_on_write(ngtcp2_conn *conn, ngtcp2_tstamp ts) { @@ -2036,7 +2119,7 @@ static void conn_restart_timer_on_read(ngtcp2_conn *conn, ngtcp2_tstamp ts) { /* * conn_keep_alive_enabled returns nonzero if keep-alive is enabled. */ -static int conn_keep_alive_enabled(ngtcp2_conn *conn) { +static int conn_keep_alive_enabled(const ngtcp2_conn *conn) { return conn->keep_alive.last_ts != UINT64_MAX && conn->keep_alive.timeout != UINT64_MAX; } @@ -2045,7 +2128,7 @@ static int conn_keep_alive_enabled(ngtcp2_conn *conn) { * conn_keep_alive_expired returns nonzero if keep-alive timer has * expired. */ -static int conn_keep_alive_expired(ngtcp2_conn *conn, ngtcp2_tstamp ts) { +static int conn_keep_alive_expired(const ngtcp2_conn *conn, ngtcp2_tstamp ts) { return ngtcp2_tstamp_elapsed(conn->keep_alive.last_ts, conn->keep_alive.timeout, ts); } @@ -2053,7 +2136,7 @@ static int conn_keep_alive_expired(ngtcp2_conn *conn, ngtcp2_tstamp ts) { /* * conn_keep_alive_expiry returns the expiry time of keep-alive timer. */ -static ngtcp2_tstamp conn_keep_alive_expiry(ngtcp2_conn *conn) { +static ngtcp2_tstamp conn_keep_alive_expiry(const ngtcp2_conn *conn) { if ((conn->flags & NGTCP2_CONN_FLAG_KEEP_ALIVE_CANCELLED) || !(conn->flags & NGTCP2_CONN_FLAG_HANDSHAKE_COMPLETED) || !conn_keep_alive_enabled(conn) || @@ -2127,7 +2210,7 @@ static int conn_pacing_pkt_tx_allowed(ngtcp2_conn *conn, ngtcp2_tstamp ts) { return 1; } -static uint8_t conn_pkt_flags(ngtcp2_conn *conn) { +static uint8_t conn_pkt_flags(const ngtcp2_conn *conn) { if (conn->remote.transport_params && conn->remote.transport_params->grease_quic_bit && (conn->flags & NGTCP2_CONN_FLAG_CLEAR_FIXED_BIT)) { @@ -2137,11 +2220,11 @@ static uint8_t conn_pkt_flags(ngtcp2_conn *conn) { return NGTCP2_PKT_FLAG_NONE; } -static uint8_t conn_pkt_flags_long(ngtcp2_conn *conn) { +static uint8_t conn_pkt_flags_long(const ngtcp2_conn *conn) { return NGTCP2_PKT_FLAG_LONG_FORM | conn_pkt_flags(conn); } -static uint8_t conn_pkt_flags_short(ngtcp2_conn *conn) { +static uint8_t conn_pkt_flags_short(const ngtcp2_conn *conn) { return (uint8_t)(conn_pkt_flags(conn) | ((conn->pktns.crypto.tx.ckm->flags & NGTCP2_CRYPTO_KM_FLAG_KEY_PHASE_ONE) ? NGTCP2_PKT_FLAG_KEY_PHASE @@ -2149,18 +2232,14 @@ static uint8_t conn_pkt_flags_short(ngtcp2_conn *conn) { } /* - * conn_cut_crypto_frame splits (*pfrc)->fr.stream by removing - * |removed_data| from (*pfrc)->fr.stream.data[0]. - * (*pfrc)->fr.stream.data[0] must contain |removed_data|, and - * (*pfrc)->fr.stream.datacnt >= 1. New ngtcp2_frame_chain object - * that contains |removed_data| is created, and pushed to - * |crypto_strm| via ngtcp2_strm_streamfrq_push. Because - * (*pfrc)->fr.stream.datacnt cannot be changed, if it is not 1, new - * ngtcp2_frame_chain object is created to contain the data before - * |removed_data|. Then *pfrc is deleted, and the newly created - * object is assigned to *pfrc instead. If there are data following - * the removed part of data, new ngtcp2_frame_chain object is created - * for it, and (*pfrc)->next points to the object. + * conn_cut_crypto_frame splits frc->fr.stream by removing + * |removed_data| from frc->fr.stream.data[0]. frc->fr.stream.data[0] + * must contain |removed_data|, and frc->fr.stream.datacnt >= 1. New + * ngtcp2_frame_chain object that contains |removed_data| is created, + * and pushed to |crypto_strm| via ngtcp2_strm_streamfrq_push. If + * there are data following the removed part of data, new + * ngtcp2_frame_chain object is created for it, and frc->next points + * to the object. * * This function returns 0 if it succeeds, or one of the following * negative error codes: @@ -2168,13 +2247,13 @@ static uint8_t conn_pkt_flags_short(ngtcp2_conn *conn) { * NGTCP2_ERR_NOMEM * Out of memory */ -static int conn_cut_crypto_frame(ngtcp2_conn *conn, ngtcp2_frame_chain **pfrc, +static int conn_cut_crypto_frame(ngtcp2_conn *conn, ngtcp2_frame_chain *frc, ngtcp2_strm *crypto_strm, const ngtcp2_vec *removed_data) { - ngtcp2_vec *data = (*pfrc)->fr.stream.data; - size_t datacnt = (*pfrc)->fr.stream.datacnt; + ngtcp2_vec *data = frc->fr.stream.data; + size_t datacnt = frc->fr.stream.datacnt; size_t ndatacnt; - ngtcp2_frame_chain *left_frc, *right_frc = NULL, *removed_frc; + ngtcp2_frame_chain *right_frc = NULL, *removed_frc; size_t offset; int rv; @@ -2192,7 +2271,10 @@ static int conn_cut_crypto_frame(ngtcp2_conn *conn, ngtcp2_frame_chain **pfrc, /* ngtcp2_frame_chain for the removed data */ removed_frc->fr.stream.type = NGTCP2_FRAME_CRYPTO; - removed_frc->fr.stream.offset = (*pfrc)->fr.stream.offset + offset; + removed_frc->fr.stream.flags = 0; + removed_frc->fr.stream.fin = 0; + removed_frc->fr.stream.stream_id = 0; + removed_frc->fr.stream.offset = frc->fr.stream.offset + offset; removed_frc->fr.stream.datacnt = 1; removed_frc->fr.stream.data[0] = (ngtcp2_vec){ .base = data->base + offset, @@ -2221,6 +2303,9 @@ static int conn_cut_crypto_frame(ngtcp2_conn *conn, ngtcp2_frame_chain **pfrc, } right_frc->fr.stream.type = NGTCP2_FRAME_CRYPTO; + right_frc->fr.stream.flags = 0; + right_frc->fr.stream.fin = 0; + right_frc->fr.stream.stream_id = 0; right_frc->fr.stream.offset = removed_frc->fr.stream.offset + removed_frc->fr.stream.data->len; right_frc->fr.stream.datacnt = 0; @@ -2231,32 +2316,9 @@ static int conn_cut_crypto_frame(ngtcp2_conn *conn, ngtcp2_frame_chain **pfrc, assert(1 == datacnt); } - /* We cannot change (*pfrc)->fr.stream.datacnt. If it changes, - create new ngtcp2_frame_chain. */ - if ((*pfrc)->fr.stream.datacnt == 1) { - (*pfrc)->fr.stream.data[0].len = offset; - (*pfrc)->next = right_frc; - return 0; - } - - rv = ngtcp2_frame_chain_stream_datacnt_objalloc_new( - &left_frc, 1, &conn->frc_objalloc, conn->mem); - if (rv != 0) { - ngtcp2_frame_chain_objalloc_del(right_frc, &conn->frc_objalloc, conn->mem); - return rv; - } - - left_frc->fr.stream.type = NGTCP2_FRAME_CRYPTO; - left_frc->fr.stream.offset = (*pfrc)->fr.stream.offset; - left_frc->fr.stream.datacnt = 1; - left_frc->fr.stream.data[0] = (ngtcp2_vec){ - .base = data[0].base, - .len = offset, - }; - left_frc->next = right_frc; - - ngtcp2_frame_chain_objalloc_del(*pfrc, &conn->frc_objalloc, conn->mem); - *pfrc = left_frc; + frc->fr.stream.datacnt = 1; + frc->fr.stream.data[0].len = offset; + frc->next = right_frc; return 0; } @@ -2340,7 +2402,7 @@ conn_crumble_initial_crypto(ngtcp2_conn *conn, ngtcp2_frame_chain **pfrc, datacnt = ngtcp2_pkt_remove_vec_partial( &removed_data, data, datacnt, offsets, &conn->pcg, &server_name); - rv = conn_cut_crypto_frame(conn, pfrc, crypto_strm, &removed_data); + rv = conn_cut_crypto_frame(conn, *pfrc, crypto_strm, &removed_data); if (rv != 0) { ngtcp2_frame_chain_objalloc_del(*pfrc, &conn->frc_objalloc, conn->mem); return rv; @@ -2372,7 +2434,16 @@ conn_crumble_initial_crypto(ngtcp2_conn *conn, ngtcp2_frame_chain **pfrc, return (ngtcp2_ssize)datacnt; } -static size_t conn_min_pktlen(ngtcp2_conn *conn); +static size_t conn_dgram_padding(const ngtcp2_conn *conn, ngtcp2_ppe *ppe) { + if (conn->local.settings.no_tx_udp_payload_size_shaping) { + return ngtcp2_ppe_dgram_padding_size( + ppe, conn->local.settings.max_tx_udp_payload_size); + } + + return ngtcp2_ppe_dgram_padding(ppe); +} + +static size_t conn_min_pktlen(const ngtcp2_conn *conn); /* * conn_write_handshake_pkt writes handshake packet in the buffer @@ -2402,8 +2473,8 @@ conn_write_handshake_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, uint8_t *dest, ngtcp2_pkt_hd hd; ngtcp2_frame_chain *frq = NULL, **pfrc = &frq; ngtcp2_frame_chain *nfrc; - ngtcp2_max_frame mfr; - ngtcp2_frame *ackfr = NULL, lfr; + ngtcp2_ack_range ack_ranges[NGTCP2_MAX_ACK_RANGES]; + ngtcp2_frame lfr; ngtcp2_ssize spktlen; ngtcp2_crypto_cc cc; ngtcp2_rtb_entry *rtbent; @@ -2474,20 +2545,16 @@ conn_write_handshake_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, uint8_t *dest, return 0; } - if (!ngtcp2_ppe_ensure_hp_sample(&ppe)) { - return 0; - } - - ackfr = ngtcp2_acktr_create_ack_frame(&pktns->acktr, &mfr.fr, type, ts, - /* ack_delay = */ 0, - NGTCP2_DEFAULT_ACK_DELAY_EXPONENT); - if (ackfr) { - rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, ackfr); + lfr.ack.ranges = ack_ranges; + if (ngtcp2_acktr_create_ack_frame(&pktns->acktr, &lfr.ack, type, ts, + /* ack_delay = */ 0, + NGTCP2_DEFAULT_ACK_DELAY_EXPONENT) == 0) { + rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, &lfr); if (rv != 0) { assert(NGTCP2_ERR_NOBUF == rv); } else { ngtcp2_acktr_commit_ack(&pktns->acktr); - ngtcp2_acktr_add_ack(&pktns->acktr, hd.pkt_num, ackfr->ack.largest_ack); + ngtcp2_acktr_add_ack(&pktns->acktr, hd.pkt_num, lfr.ack.largest_ack); pkt_empty = 0; } } @@ -2545,7 +2612,7 @@ conn_write_handshake_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, uint8_t *dest, .type = NGTCP2_FRAME_CRYPTO, .offset = offsets[i], .datacnt = 1, - .data[0] = data[i], + .data = &data[i], }; } @@ -2595,7 +2662,9 @@ conn_write_handshake_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, uint8_t *dest, if (!(rtb_entry_flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING) && pktns->rtb.num_retransmittable && pktns->rtb.probe_pkt_left) { - num_reclaimed = ngtcp2_rtb_reclaim_on_pto(&pktns->rtb, conn, pktns, 1); + num_reclaimed = ngtcp2_rtb_reclaim_on_pto( + &pktns->rtb, conn, pktns, + !conn->server && type == NGTCP2_PKT_INITIAL ? 2 : 1); if (num_reclaimed < 0) { ngtcp2_frame_chain_list_objalloc_del(frq, &conn->frc_objalloc, conn->mem); @@ -2622,7 +2691,7 @@ conn_write_handshake_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, uint8_t *dest, if (!(rtb_entry_flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING) && pktns->rtb.probe_pkt_left) { - lfr.type = NGTCP2_FRAME_PING; + lfr.ping.type = NGTCP2_FRAME_PING; rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, &lfr); if (rv != 0) { @@ -2638,7 +2707,7 @@ conn_write_handshake_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, uint8_t *dest, if (!(rtb_entry_flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING)) { if (ngtcp2_tstamp_elapsed(pktns->tx.non_ack_pkt_start_ts, conn->cstat.smoothed_rtt, ts)) { - lfr.type = NGTCP2_FRAME_PING; + lfr.ping.type = NGTCP2_FRAME_PING; rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, &lfr); if (rv != 0) { @@ -2667,12 +2736,12 @@ conn_write_handshake_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, uint8_t *dest, conn, type, ngtcp2_ppe_left(&ppe), write_datalen, (rtb_entry_flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING) != 0, require_padding)) { - lfr.type = NGTCP2_FRAME_PADDING; - lfr.padding.len = ngtcp2_ppe_dgram_padding(&ppe); + lfr.padding.type = NGTCP2_FRAME_PADDING; + lfr.padding.len = conn_dgram_padding(conn, &ppe); } else if (pkt_empty) { return 0; } else { - lfr.type = NGTCP2_FRAME_PADDING; + lfr.padding.type = NGTCP2_FRAME_PADDING; lfr.padding.len = ngtcp2_ppe_padding_size(&ppe, conn_min_pktlen(conn)); min_padded = 1; } @@ -2735,6 +2804,9 @@ conn_write_handshake_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, uint8_t *dest, conn->tx.pacing.pktlen += (size_t)spktlen; + ++conn->cstat.pkt_sent; + conn->cstat.bytes_sent += (uint64_t)spktlen; + ngtcp2_qlog_metrics_updated(&conn->qlog, &conn->cstat); ++pktns->tx.last_pkt_num; @@ -2758,12 +2830,12 @@ conn_write_handshake_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, uint8_t *dest, static ngtcp2_ssize conn_write_ack_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, uint8_t *dest, size_t destlen, uint8_t type, ngtcp2_tstamp ts) { - ngtcp2_frame *ackfr; ngtcp2_pktns *pktns; ngtcp2_duration ack_delay; uint64_t ack_delay_exponent; ngtcp2_ssize spktlen; - ngtcp2_max_frame mfr; + ngtcp2_ack_range ack_ranges[NGTCP2_MAX_ACK_RANGES]; + ngtcp2_frame fr; assert(!(conn->flags & NGTCP2_CONN_FLAG_PPE_PENDING)); @@ -2792,15 +2864,15 @@ static ngtcp2_ssize conn_write_ack_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, return 0; } - ackfr = ngtcp2_acktr_create_ack_frame(&pktns->acktr, &mfr.fr, type, ts, - ack_delay, ack_delay_exponent); - if (!ackfr) { + fr.ack.ranges = ack_ranges; + if (ngtcp2_acktr_create_ack_frame(&pktns->acktr, &fr.ack, type, ts, ack_delay, + ack_delay_exponent) != 0) { return 0; } spktlen = ngtcp2_conn_write_single_frame_pkt( conn, pi, dest, destlen, type, NGTCP2_WRITE_PKT_FLAG_NONE, - &conn->dcid.current.cid, ackfr, NGTCP2_RTB_ENTRY_FLAG_NONE, NULL, ts); + &conn->dcid.current.cid, &fr, NGTCP2_RTB_ENTRY_FLAG_NONE, NULL, ts); if (spktlen <= 0) { return spktlen; @@ -2873,7 +2945,7 @@ static void conn_discard_early_key(ngtcp2_conn *conn) { conn_call_delete_crypto_aead_ctx(conn, &conn->early.ckm->aead_ctx); conn_call_delete_crypto_cipher_ctx(conn, &conn->early.hp_ctx); - memset(&conn->early.hp_ctx, 0, sizeof(conn->early.hp_ctx)); + conn->early.hp_ctx = (ngtcp2_crypto_cipher_ctx){0}; ngtcp2_crypto_km_del(conn->early.ckm, conn->mem); conn->early.ckm = NULL; @@ -2960,7 +3032,7 @@ static ngtcp2_ssize conn_write_client_initial(ngtcp2_conn *conn, * dcid_tx_left returns the maximum number of bytes that server is * allowed to send to an unvalidated path associated to |dcid|. */ -static uint64_t dcid_tx_left(ngtcp2_dcid *dcid) { +static uint64_t dcid_tx_left(const ngtcp2_dcid *dcid) { if (dcid->flags & NGTCP2_DCID_FLAG_PATH_VALIDATED) { return SIZE_MAX; } @@ -2976,7 +3048,8 @@ static uint64_t dcid_tx_left(ngtcp2_dcid *dcid) { * conn_server_tx_left returns the maximum number of bytes that server * is allowed to send to an unvalidated path. */ -static uint64_t conn_server_tx_left(ngtcp2_conn *conn, ngtcp2_dcid *dcid) { +static uint64_t conn_server_tx_left(const ngtcp2_conn *conn, + const ngtcp2_dcid *dcid) { assert(conn->server); /* If pv->dcid has the current path, use conn->dcid.current. This @@ -3063,21 +3136,16 @@ static ngtcp2_ssize conn_write_handshake_pkts(ngtcp2_conn *conn, dest += nwrite; destlen -= (size_t)nwrite; - /* If initial packet size is at least - NGTCP2_MAX_UDP_PAYLOAD_SIZE, no extra padding is needed in a - subsequent packet. */ - if (nwrite < NGTCP2_MAX_UDP_PAYLOAD_SIZE) { - if (conn->server) { - it = ngtcp2_rtb_head(&conn->in_pktns->rtb); - if (!ngtcp2_ksl_it_end(&it)) { - rtbent = ngtcp2_ksl_it_get(&it); - if (rtbent->flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING) { - wflags |= NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING; - } + if (conn->server) { + it = ngtcp2_rtb_head(&conn->in_pktns->rtb); + if (!ngtcp2_ksl_it_end(&it)) { + rtbent = ngtcp2_ksl_it_get(&it); + if (rtbent->flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING) { + wflags |= NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING; } - } else { - wflags |= NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING; } + } else { + wflags |= NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING; } } } @@ -3127,8 +3195,8 @@ static uint64_t conn_initial_stream_rx_offset(ngtcp2_conn *conn, * conn_should_send_max_stream_data returns nonzero if MAX_STREAM_DATA * frame should be send for |strm|. */ -static int conn_should_send_max_stream_data(ngtcp2_conn *conn, - ngtcp2_strm *strm) { +static int conn_should_send_max_stream_data(const ngtcp2_conn *conn, + const ngtcp2_strm *strm) { uint64_t inc = strm->rx.unsent_max_offset - strm->rx.max_offset; (void)conn; @@ -3139,7 +3207,7 @@ static int conn_should_send_max_stream_data(ngtcp2_conn *conn, * conn_should_send_max_data returns nonzero if MAX_DATA frame should * be sent. */ -static int conn_should_send_max_data(ngtcp2_conn *conn) { +static int conn_should_send_max_data(const ngtcp2_conn *conn) { uint64_t inc = conn->rx.unsent_max_offset - conn->rx.max_offset; return conn->rx.window < 4 * inc; @@ -3150,8 +3218,8 @@ static int conn_should_send_max_data(ngtcp2_conn *conn) { * additional connection ID the local endpoint has to provide to the * remote endpoint. */ -static size_t conn_required_num_new_connection_id(ngtcp2_conn *conn) { - uint64_t n; +static size_t conn_required_num_new_connection_id(const ngtcp2_conn *conn) { + size_t n; size_t len = ngtcp2_ksl_len(&conn->scid.set); size_t lim; @@ -3172,12 +3240,13 @@ static size_t conn_required_num_new_connection_id(ngtcp2_conn *conn) { /* len includes retired CID. We don't provide extra CID if doing so exceeds NGTCP2_MAX_SCID_POOL_SIZE. */ - n = conn->remote.transport_params->active_connection_id_limit + - conn->scid.num_retired; - - n = ngtcp2_min_uint64(NGTCP2_MAX_SCID_POOL_SIZE, n) - len; + n = (size_t)ngtcp2_min( + NGTCP2_MAX_SCID_POOL_SIZE, + conn->remote.transport_params->active_connection_id_limit + + conn->scid.num_retired) - + len; - return (size_t)ngtcp2_min_uint64(lim, n); + return ngtcp2_min(lim, n); } /* @@ -3195,58 +3264,59 @@ static size_t conn_required_num_new_connection_id(ngtcp2_conn *conn) { static int conn_enqueue_new_connection_id(ngtcp2_conn *conn) { size_t i, need = conn_required_num_new_connection_id(conn); size_t cidlen = conn->oscid.datalen; - ngtcp2_cid cid; - uint64_t seq; int rv; - uint8_t token[NGTCP2_STATELESS_RESET_TOKENLEN]; ngtcp2_frame_chain *nfrc; ngtcp2_pktns *pktns = &conn->pktns; ngtcp2_scid *scid; ngtcp2_ksl_it it; for (i = 0; i < need; ++i) { - rv = conn_call_get_new_connection_id(conn, &cid, token, cidlen); + rv = ngtcp2_frame_chain_objalloc_new(&nfrc, &conn->frc_objalloc); if (rv != 0) { return rv; } - if (cid.datalen != cidlen) { - return NGTCP2_ERR_CALLBACK_FAILURE; + nfrc->fr.new_connection_id.type = NGTCP2_FRAME_NEW_CONNECTION_ID; + nfrc->fr.new_connection_id.seq = ++conn->scid.last_seq; + nfrc->fr.new_connection_id.retire_prior_to = 0; + + rv = conn_call_get_new_connection_id(conn, &nfrc->fr.new_connection_id.cid, + &nfrc->fr.new_connection_id.token, + cidlen); + if (rv != 0) { + goto fail; + } + + if (nfrc->fr.new_connection_id.cid.datalen != cidlen) { + rv = NGTCP2_ERR_CALLBACK_FAILURE; + goto fail; } /* Assert uniqueness */ - it = ngtcp2_ksl_lower_bound(&conn->scid.set, &cid); + it = + ngtcp2_ksl_lower_bound(&conn->scid.set, &nfrc->fr.new_connection_id.cid); if (!ngtcp2_ksl_it_end(&it) && - ngtcp2_cid_eq(ngtcp2_ksl_it_key(&it), &cid)) { - return NGTCP2_ERR_CALLBACK_FAILURE; + ngtcp2_cid_eq(ngtcp2_ksl_it_key(&it), + &nfrc->fr.new_connection_id.cid)) { + rv = NGTCP2_ERR_CALLBACK_FAILURE; + goto fail; } - seq = ++conn->scid.last_seq; - scid = ngtcp2_mem_malloc(conn->mem, sizeof(*scid)); if (scid == NULL) { - return NGTCP2_ERR_NOMEM; + rv = NGTCP2_ERR_NOMEM; + goto fail; } - ngtcp2_scid_init(scid, seq, &cid); + ngtcp2_scid_init(scid, nfrc->fr.new_connection_id.seq, + &nfrc->fr.new_connection_id.cid); rv = ngtcp2_ksl_insert(&conn->scid.set, NULL, &scid->cid, scid); if (rv != 0) { ngtcp2_mem_free(conn->mem, scid); - return rv; + goto fail; } - rv = ngtcp2_frame_chain_objalloc_new(&nfrc, &conn->frc_objalloc); - if (rv != 0) { - return rv; - } - - nfrc->fr.type = NGTCP2_FRAME_NEW_CONNECTION_ID; - nfrc->fr.new_connection_id.seq = seq; - nfrc->fr.new_connection_id.retire_prior_to = 0; - nfrc->fr.new_connection_id.cid = cid; - memcpy(nfrc->fr.new_connection_id.stateless_reset_token, token, - sizeof(token)); nfrc->next = pktns->tx.frq; pktns->tx.frq = nfrc; @@ -3256,6 +3326,11 @@ static int conn_enqueue_new_connection_id(ngtcp2_conn *conn) { } return 0; + +fail: + ngtcp2_frame_chain_objalloc_del(nfrc, &conn->frc_objalloc, conn->mem); + + return rv; } static int dcidtr_on_deactivate(const ngtcp2_dcid *dcid, void *user_data) { @@ -3319,7 +3394,7 @@ static int conn_remove_retired_connection_id(ngtcp2_conn *conn, * sends. It may underestimate the length because this does not take * into account header protection sample. */ -static size_t conn_min_pktlen(ngtcp2_conn *conn) { +static size_t conn_min_pktlen(const ngtcp2_conn *conn) { return conn->oscid.datalen + NGTCP2_MIN_PKT_EXPANDLEN; } @@ -3346,13 +3421,11 @@ static void conn_handle_unconfirmed_key_update_from_remote(ngtcp2_conn *conn, ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CRY, "key update confirmed"); } -static uint64_t conn_tx_strmq_first_cycle(ngtcp2_conn *conn); - /* * strm_should_send_stream_data_blocked returns nonzero if * STREAM_DATA_BLOCKED frame should be sent to |strm|. */ -static int strm_should_send_stream_data_blocked(ngtcp2_strm *strm) { +static int strm_should_send_stream_data_blocked(const ngtcp2_strm *strm) { return strm->tx.offset == strm->tx.max_offset && strm->tx.last_blocked_offset != strm->tx.max_offset; } @@ -3361,7 +3434,7 @@ static int strm_should_send_stream_data_blocked(ngtcp2_strm *strm) { * conn_should_send_data_blocked returns nonzero if DATA_BLOCKED frame * should be sent. */ -static int conn_should_send_data_blocked(ngtcp2_conn *conn) { +static int conn_should_send_data_blocked(const ngtcp2_conn *conn) { return conn->tx.offset == conn->tx.max_offset && conn->tx.last_blocked_offset != conn->tx.max_offset; } @@ -3415,8 +3488,8 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, ngtcp2_crypto_cc *cc = &conn->pkt.cc; ngtcp2_ppe *ppe = &conn->pkt.ppe; ngtcp2_pkt_hd *hd = &conn->pkt.hd; - ngtcp2_max_frame mfr; - ngtcp2_frame *ackfr = NULL, lfr; + ngtcp2_ack_range ack_ranges[NGTCP2_MAX_ACK_RANGES]; + ngtcp2_frame lfr; ngtcp2_ssize nwrite; ngtcp2_frame_chain **pfrc, *nfrc, *frc; ngtcp2_rtb_entry *ent; @@ -3442,11 +3515,9 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, size_t min_pktlen = conn_min_pktlen(conn); int min_padded = 0; int padded = 0; - ngtcp2_cc_pkt cc_pkt; uint64_t crypto_offset; uint64_t stream_offset; ngtcp2_ssize num_reclaimed; - int fin; uint64_t target_max_data; ngtcp2_conn_stat *cstat = &conn->cstat; uint64_t delta; @@ -3559,8 +3630,10 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, conn->tx.last_max_data_ts = ts; - nfrc->fr.type = NGTCP2_FRAME_MAX_DATA; - nfrc->fr.max_data.max_data = conn->rx.unsent_max_offset + delta; + nfrc->fr.max_data = (ngtcp2_max_data){ + .type = NGTCP2_FRAME_MAX_DATA, + .max_data = conn->rx.unsent_max_offset + delta, + }; nfrc->next = pktns->tx.frq; pktns->tx.frq = nfrc; @@ -3585,19 +3658,16 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, return 0; } - if (!ngtcp2_ppe_ensure_hp_sample(ppe)) { - return 0; - } - if (ngtcp2_ringbuf_len(&conn->rx.path_challenge.rb)) { pcent = ngtcp2_ringbuf_get(&conn->rx.path_challenge.rb, 0); /* PATH_RESPONSE is bound to the path that the corresponding PATH_CHALLENGE is received. */ if (ngtcp2_path_eq(&conn->dcid.current.ps.path, &pcent->ps.path)) { - lfr.type = NGTCP2_FRAME_PATH_RESPONSE; - memcpy(lfr.path_response.data, pcent->data, - sizeof(lfr.path_response.data)); + lfr.path_response = (ngtcp2_path_response){ + .type = NGTCP2_FRAME_PATH_RESPONSE, + .data = pcent->data, + }; rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &lfr); if (rv != 0) { @@ -3616,7 +3686,7 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, An endpoint that receives a PATH_CHALLENGE on an active path SHOULD send a non-probing packet in response. */ - lfr.type = NGTCP2_FRAME_PING; + lfr.ping.type = NGTCP2_FRAME_PING; rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &lfr); if (rv != 0) { assert(NGTCP2_ERR_NOBUF == rv); @@ -3625,20 +3695,19 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, } } - ackfr = ngtcp2_acktr_create_ack_frame( - &pktns->acktr, &mfr.fr, type, ts, conn_compute_ack_delay(conn), - conn->local.transport_params.ack_delay_exponent); - if (ackfr) { - rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, ackfr); + lfr.ack.ranges = ack_ranges; + if (ngtcp2_acktr_create_ack_frame( + &pktns->acktr, &lfr.ack, type, ts, conn_compute_ack_delay(conn), + conn->local.transport_params.ack_delay_exponent) == 0) { + rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &lfr); if (rv != 0) { assert(NGTCP2_ERR_NOBUF == rv); } else { ngtcp2_acktr_commit_ack(&pktns->acktr); - ngtcp2_acktr_add_ack(&pktns->acktr, hd->pkt_num, - ackfr->ack.largest_ack); + ngtcp2_acktr_add_ack(&pktns->acktr, hd->pkt_num, lfr.ack.largest_ack); assert(NGTCP2_PKT_1RTT == type); - conn_handle_unconfirmed_key_update_from_remote( - conn, ackfr->ack.largest_ack, ts); + conn_handle_unconfirmed_key_update_from_remote(conn, + lfr.ack.largest_ack, ts); pkt_empty = 0; } } @@ -3653,7 +3722,7 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, continue; } - switch ((*pfrc)->fr.type) { + switch ((*pfrc)->fr.hd.type) { case NGTCP2_FRAME_RESET_STREAM: strm = ngtcp2_conn_find_stream(conn, (*pfrc)->fr.reset_stream.stream_id); @@ -3803,11 +3872,12 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, return rv; } - nfrc->fr.type = NGTCP2_FRAME_RESET_STREAM; - nfrc->fr.reset_stream.stream_id = strm->stream_id; - nfrc->fr.reset_stream.app_error_code = - strm->tx.reset_stream_app_error_code; - nfrc->fr.reset_stream.final_size = strm->tx.offset; + nfrc->fr.reset_stream = (ngtcp2_reset_stream){ + .type = NGTCP2_FRAME_RESET_STREAM, + .stream_id = strm->stream_id, + .app_error_code = strm->tx.reset_stream_app_error_code, + .final_size = strm->tx.offset, + }; *pfrc = nfrc; strm->flags &= ~NGTCP2_STRM_FLAG_SEND_RESET_STREAM; @@ -3846,10 +3916,11 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, return rv; } - nfrc->fr.type = NGTCP2_FRAME_STOP_SENDING; - nfrc->fr.stop_sending.stream_id = strm->stream_id; - nfrc->fr.stop_sending.app_error_code = - strm->tx.stop_sending_app_error_code; + nfrc->fr.stop_sending = (ngtcp2_stop_sending){ + .type = NGTCP2_FRAME_STOP_SENDING, + .stream_id = strm->stream_id, + .app_error_code = strm->tx.stop_sending_app_error_code, + }; *pfrc = nfrc; strm->flags &= ~NGTCP2_STRM_FLAG_SEND_STOP_SENDING; @@ -3877,9 +3948,11 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, return rv; } - nfrc->fr.type = NGTCP2_FRAME_STREAM_DATA_BLOCKED; - nfrc->fr.stream_data_blocked.stream_id = strm->stream_id; - nfrc->fr.stream_data_blocked.offset = strm->tx.max_offset; + nfrc->fr.stream_data_blocked = (ngtcp2_stream_data_blocked){ + .type = NGTCP2_FRAME_STREAM_DATA_BLOCKED, + .stream_id = strm->stream_id, + .offset = strm->tx.max_offset, + }; *pfrc = nfrc; strm->tx.last_blocked_offset = strm->tx.max_offset; @@ -3931,10 +4004,11 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, strm->tx.last_max_stream_data_ts = ts; - nfrc->fr.type = NGTCP2_FRAME_MAX_STREAM_DATA; - nfrc->fr.max_stream_data.stream_id = strm->stream_id; - nfrc->fr.max_stream_data.max_stream_data = - strm->rx.unsent_max_offset + delta; + nfrc->fr.max_stream_data = (ngtcp2_max_stream_data){ + .type = NGTCP2_FRAME_MAX_STREAM_DATA, + .stream_id = strm->stream_id, + .max_stream_data = strm->rx.unsent_max_offset + delta, + }; *pfrc = nfrc; strm->rx.max_offset = strm->rx.unsent_max_offset = @@ -4030,8 +4104,10 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, assert(ngtcp2_err_is_fatal(rv)); return rv; } - nfrc->fr.type = NGTCP2_FRAME_MAX_STREAMS_BIDI; - nfrc->fr.max_streams.max_streams = conn->remote.bidi.unsent_max_streams; + nfrc->fr.max_streams = (ngtcp2_max_streams){ + .type = NGTCP2_FRAME_MAX_STREAMS_BIDI, + .max_streams = conn->remote.bidi.unsent_max_streams, + }; *pfrc = nfrc; conn->remote.bidi.max_streams = conn->remote.bidi.unsent_max_streams; @@ -4062,8 +4138,10 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, assert(ngtcp2_err_is_fatal(rv)); return rv; } - nfrc->fr.type = NGTCP2_FRAME_MAX_STREAMS_UNI; - nfrc->fr.max_streams.max_streams = conn->remote.uni.unsent_max_streams; + nfrc->fr.max_streams = (ngtcp2_max_streams){ + .type = NGTCP2_FRAME_MAX_STREAMS_UNI, + .max_streams = conn->remote.uni.unsent_max_streams, + }; *pfrc = nfrc; conn->remote.uni.max_streams = conn->remote.uni.unsent_max_streams; @@ -4136,14 +4214,14 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, nfrc->fr.stream.type = NGTCP2_FRAME_STREAM; nfrc->fr.stream.flags = 0; + nfrc->fr.stream.fin = 0; nfrc->fr.stream.stream_id = vmsg->stream.strm->stream_id; nfrc->fr.stream.offset = vmsg->stream.strm->tx.offset; nfrc->fr.stream.datacnt = datacnt; ngtcp2_vec_copy(nfrc->fr.stream.data, data, datacnt); - fin = (vmsg->stream.flags & NGTCP2_WRITE_STREAM_FLAG_FIN) && - ndatalen == datalen; - nfrc->fr.stream.fin = (uint8_t)fin; + nfrc->fr.stream.fin = (vmsg->stream.flags & NGTCP2_WRITE_STREAM_FLAG_FIN) && + ndatalen == datalen; rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &nfrc->fr); if (rv != 0) { @@ -4162,7 +4240,7 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, conn->tx.offset += ndatalen; vmsg->stream.strm->flags |= NGTCP2_STRM_FLAG_ANY_SENT; - if (fin) { + if (nfrc->fr.stream.fin) { ngtcp2_strm_shutdown(vmsg->stream.strm, NGTCP2_STRM_FLAG_SHUT_WR); } @@ -4185,8 +4263,10 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, return rv; } - nfrc->fr.type = NGTCP2_FRAME_DATA_BLOCKED; - nfrc->fr.data_blocked.offset = conn->tx.offset; + nfrc->fr.data_blocked = (ngtcp2_data_blocked){ + .type = NGTCP2_FRAME_DATA_BLOCKED, + .offset = conn->tx.offset, + }; rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &nfrc->fr); if (rv != 0) { @@ -4217,9 +4297,11 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, return rv; } - nfrc->fr.type = NGTCP2_FRAME_STREAM_DATA_BLOCKED; - nfrc->fr.stream_data_blocked.stream_id = strm->stream_id; - nfrc->fr.stream_data_blocked.offset = strm->tx.max_offset; + nfrc->fr.stream_data_blocked = (ngtcp2_stream_data_blocked){ + .type = NGTCP2_FRAME_STREAM_DATA_BLOCKED, + .stream_id = strm->stream_id, + .offset = strm->tx.max_offset, + }; rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &nfrc->fr); if (rv != 0) { @@ -4229,7 +4311,7 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, ngtcp2_frame_chain_objalloc_del(nfrc, &conn->frc_objalloc, conn->mem); if (!ngtcp2_strm_is_tx_queued(strm)) { - strm->cycle = conn_tx_strmq_first_cycle(conn); + strm->cycle = ngtcp2_conn_tx_strmq_first_cycle(conn); rv = ngtcp2_conn_tx_strmq_push(conn, strm); if (rv != 0) { return rv; @@ -4299,7 +4381,7 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, if (pkt_empty) { if (*pfrc == NULL && rv == 0 && stream_blocked && (write_more || !require_padding) && - ngtcp2_conn_get_max_data_left(conn)) { + ngtcp2_conn_get_max_data_left2(conn)) { if (write_more) { conn->pkt.pfrc = pfrc; conn->pkt.pkt_empty = pkt_empty; @@ -4340,7 +4422,7 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, break; } - if (*pfrc == NULL && ngtcp2_conn_get_max_data_left(conn) && + if (*pfrc == NULL && ngtcp2_conn_get_max_data_left2(conn) && stream_blocked) { return NGTCP2_ERR_STREAM_DATA_BLOCKED; } @@ -4362,7 +4444,7 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, if (ngtcp2_tstamp_elapsed(pktns->tx.non_ack_pkt_start_ts, cstat->smoothed_rtt, ts) || keep_alive_expired || conn->pktns.rtb.probe_pkt_left) { - lfr.type = NGTCP2_FRAME_PING; + lfr.ping.type = NGTCP2_FRAME_PING; rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &lfr); if (rv != 0) { @@ -4377,7 +4459,6 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, rtb_entry_flags |= NGTCP2_RTB_ENTRY_FLAG_PTO_ELICITING; } pktns->tx.non_ack_pkt_start_ts = UINT64_MAX; - pkt_empty = 0; } } else if (pktns->tx.non_ack_pkt_start_ts == UINT64_MAX) { pktns->tx.non_ack_pkt_start_ts = ts; @@ -4392,7 +4473,7 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, (rtb_entry_flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING)) { lfr.padding.len = ngtcp2_ppe_padding_size(ppe, destlen); } else if (require_padding) { - lfr.padding.len = ngtcp2_ppe_dgram_padding(ppe); + lfr.padding.len = conn_dgram_padding(conn, ppe); } else { lfr.padding.len = ngtcp2_ppe_padding_size(ppe, min_pktlen); min_padded = 1; @@ -4403,7 +4484,7 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, (rtb_entry_flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING)) { padded = 1; } - lfr.type = NGTCP2_FRAME_PADDING; + lfr.padding.type = NGTCP2_FRAME_PADDING; ngtcp2_log_tx_fr(&conn->log, hd, &lfr); ngtcp2_qlog_write_frame(&conn->qlog, &lfr); } @@ -4439,12 +4520,6 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, *pfrc = NULL; } - if ((rtb_entry_flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING) && - pktns->rtb.num_ack_eliciting == 0 && conn->cc.event) { - conn->cc.event(&conn->cc, &conn->cstat, NGTCP2_CC_EVENT_TYPE_TX_START, - ts); - } - rv = conn_on_pkt_sent(conn, pktns, ent); if (rv != 0) { assert(ngtcp2_err_is_fatal(rv)); @@ -4453,18 +4528,9 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, return rv; } - if (rtb_entry_flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING) { - if (conn->cc.on_pkt_sent) { - conn->cc.on_pkt_sent( - &conn->cc, &conn->cstat, - ngtcp2_cc_pkt_init(&cc_pkt, hd->pkt_num, (size_t)nwrite, - NGTCP2_PKTNS_ID_APPLICATION, ts, ent->rst.lost, - ent->rst.tx_in_flight, ent->rst.is_app_limited)); - } - - if (conn->flags & NGTCP2_CONN_FLAG_RESTART_IDLE_TIMER_ON_WRITE) { - conn_restart_timer_on_write(conn, ts); - } + if ((rtb_entry_flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING) && + (conn->flags & NGTCP2_CONN_FLAG_RESTART_IDLE_TIMER_ON_WRITE)) { + conn_restart_timer_on_write(conn, ts); } } else if (pi && conn->tx.ecn.state == NGTCP2_ECN_STATE_CAPABLE) { conn_handle_tx_ecn(conn, pi, NULL, pktns, hd, ts); @@ -4476,8 +4542,8 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, (rtb_entry_flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING)) { --pktns->rtb.probe_pkt_left; - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, "probe pkt size=%td", - nwrite); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "probe pkt size=", nwrite); } conn_update_keep_alive_last_ts(conn, ts); @@ -4486,6 +4552,9 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, conn->tx.pacing.pktlen += (size_t)nwrite; + ++conn->cstat.pkt_sent; + conn->cstat.bytes_sent += (uint64_t)nwrite; + ngtcp2_qlog_metrics_updated(&conn->qlog, &conn->cstat); ++pktns->tx.last_pkt_num; @@ -4571,10 +4640,6 @@ ngtcp2_ssize ngtcp2_conn_write_single_frame_pkt( return 0; } - if (!ngtcp2_ppe_ensure_hp_sample(&ppe)) { - return 0; - } - ngtcp2_log_tx_pkt_hd(&conn->log, &hd); ngtcp2_qlog_pkt_sent_start(&conn->qlog); @@ -4584,17 +4649,17 @@ ngtcp2_ssize ngtcp2_conn_write_single_frame_pkt( return 0; } - lfr.type = NGTCP2_FRAME_PADDING; + lfr.padding.type = NGTCP2_FRAME_PADDING; if (flags & NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING_FULL) { lfr.padding.len = ngtcp2_ppe_dgram_padding_size(&ppe, destlen); } else if (flags & NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING) { - lfr.padding.len = ngtcp2_ppe_dgram_padding(&ppe); + lfr.padding.len = conn_dgram_padding(conn, &ppe); } else { - switch (fr->type) { + switch (fr->hd.type) { case NGTCP2_FRAME_PATH_CHALLENGE: case NGTCP2_FRAME_PATH_RESPONSE: if (!conn->server || destlen >= NGTCP2_MAX_UDP_PAYLOAD_SIZE) { - lfr.padding.len = ngtcp2_ppe_dgram_padding(&ppe); + lfr.padding.len = conn_dgram_padding(conn, &ppe); } else { lfr.padding.len = 0; } @@ -4621,7 +4686,7 @@ ngtcp2_ssize ngtcp2_conn_write_single_frame_pkt( ngtcp2_qlog_pkt_sent_end(&conn->qlog, &hd, (size_t)nwrite); /* Do this when we are sure that there is no error. */ - switch (fr->type) { + switch (fr->hd.type) { case NGTCP2_FRAME_ACK: case NGTCP2_FRAME_ACK_ECN: ngtcp2_acktr_commit_ack(&pktns->acktr); @@ -4636,6 +4701,13 @@ ngtcp2_ssize ngtcp2_conn_write_single_frame_pkt( padded = 0; } + break; + case NGTCP2_FRAME_CONNECTION_CLOSE: + case NGTCP2_FRAME_CONNECTION_CLOSE_APP: + /* Clear padded so that we never store the terminal packet in + ngtcp2_rtb. */ + padded = 0; + break; } @@ -4669,8 +4741,8 @@ ngtcp2_ssize ngtcp2_conn_write_single_frame_pkt( ngtcp2_path_eq(&conn->dcid.current.ps.path, path)) { --pktns->rtb.probe_pkt_left; - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, "probe pkt size=%td", - nwrite); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "probe pkt size=", nwrite); } } } else if (pi && conn->tx.ecn.state == NGTCP2_ECN_STATE_CAPABLE) { @@ -4682,7 +4754,7 @@ ngtcp2_ssize ngtcp2_conn_write_single_frame_pkt( } if (!padded) { - switch (fr->type) { + switch (fr->hd.type) { case NGTCP2_FRAME_ACK: case NGTCP2_FRAME_ACK_ECN: break; @@ -4693,6 +4765,9 @@ ngtcp2_ssize ngtcp2_conn_write_single_frame_pkt( conn->tx.pacing.pktlen += (size_t)nwrite; } + ++conn->cstat.pkt_sent; + conn->cstat.bytes_sent += (uint64_t)nwrite; + ngtcp2_qlog_metrics_updated(&conn->qlog, &conn->cstat); ++pktns->tx.last_pkt_num; @@ -4728,9 +4803,9 @@ static void conn_process_early_rtb(ngtcp2_conn *conn) { * handshake packets the local endpoint has to send, including new * packets and lost ones. */ -static int conn_handshake_remnants_left(ngtcp2_conn *conn) { - ngtcp2_pktns *in_pktns = conn->in_pktns; - ngtcp2_pktns *hs_pktns = conn->hs_pktns; +static int conn_handshake_remnants_left(const ngtcp2_conn *conn) { + const ngtcp2_pktns *in_pktns = conn->in_pktns; + const ngtcp2_pktns *hs_pktns = conn->hs_pktns; return !conn_is_tls_handshake_completed(conn) || (in_pktns && (in_pktns->rtb.num_pto_eliciting || @@ -4759,8 +4834,10 @@ static int conn_enqueue_retire_connection_id(ngtcp2_conn *conn, uint64_t seq) { return rv; } - nfrc->fr.type = NGTCP2_FRAME_RETIRE_CONNECTION_ID; - nfrc->fr.retire_connection_id.seq = seq; + nfrc->fr.retire_connection_id = (ngtcp2_retire_connection_id){ + .type = NGTCP2_FRAME_RETIRE_CONNECTION_ID, + .seq = seq, + }; nfrc->next = pktns->tx.frq; pktns->tx.frq = nfrc; @@ -4846,9 +4923,9 @@ static int conn_start_pmtud(ngtcp2_conn *conn) { assert(conn->remote.transport_params->max_udp_payload_size >= NGTCP2_MAX_UDP_PAYLOAD_SIZE); - hard_max_udp_payload_size = (size_t)ngtcp2_min_uint64( - conn->remote.transport_params->max_udp_payload_size, - (uint64_t)conn->local.settings.max_tx_udp_payload_size); + hard_max_udp_payload_size = + (size_t)ngtcp2_min(conn->remote.transport_params->max_udp_payload_size, + (uint64_t)conn->local.settings.max_tx_udp_payload_size); rv = ngtcp2_pmtud_new(&conn->pmtud, conn->dcid.current.max_udp_payload_size, @@ -4899,10 +4976,10 @@ static ngtcp2_ssize conn_write_pmtud_probe(ngtcp2_conn *conn, return 0; } - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, - "sending PMTUD probe packet len=%zu", probelen); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "sending PMTUD probe packet len=", probelen); - lfr.type = NGTCP2_FRAME_PING; + lfr.ping.type = NGTCP2_FRAME_PING; nwrite = ngtcp2_conn_write_single_frame_pkt( conn, pi, dest, probelen, NGTCP2_PKT_1RTT, @@ -4916,8 +4993,8 @@ static ngtcp2_ssize conn_write_pmtud_probe(ngtcp2_conn *conn, assert(nwrite); - ngtcp2_pmtud_probe_sent(conn->pmtud, conn_compute_pto(conn, &conn->pktns), - ts); + ngtcp2_pmtud_probe_sent(conn->pmtud, + ngtcp2_conn_compute_pto(conn, &conn->pktns), ts); return nwrite; } @@ -4995,26 +5072,27 @@ static int conn_abort_pv(ngtcp2_conn *conn, ngtcp2_tstamp ts) { return conn_stop_pv(conn, ts); } -static size_t conn_shape_udp_payload(ngtcp2_conn *conn, const ngtcp2_dcid *dcid, +static size_t conn_shape_udp_payload(const ngtcp2_conn *conn, + const ngtcp2_dcid *dcid, size_t payloadlen) { if (conn->remote.transport_params && conn->remote.transport_params->max_udp_payload_size) { assert(conn->remote.transport_params->max_udp_payload_size >= NGTCP2_MAX_UDP_PAYLOAD_SIZE); - payloadlen = (size_t)ngtcp2_min_uint64( - (uint64_t)payloadlen, - conn->remote.transport_params->max_udp_payload_size); + payloadlen = + (size_t)ngtcp2_min((uint64_t)payloadlen, + conn->remote.transport_params->max_udp_payload_size); } payloadlen = - ngtcp2_min_size(payloadlen, conn->local.settings.max_tx_udp_payload_size); + ngtcp2_min(payloadlen, conn->local.settings.max_tx_udp_payload_size); if (conn->local.settings.no_tx_udp_payload_size_shaping) { return payloadlen; } - return ngtcp2_min_size(payloadlen, dcid->max_udp_payload_size); + return ngtcp2_min(payloadlen, dcid->max_udp_payload_size); } static void conn_reset_congestion_state(ngtcp2_conn *conn, ngtcp2_tstamp ts); @@ -5100,24 +5178,24 @@ static ngtcp2_ssize conn_write_path_challenge(ngtcp2_conn *conn, return 0; } - rv = conn_call_get_path_challenge_data(conn, lfr.path_challenge.data); + rv = conn_call_get_path_challenge_data(conn, &lfr.path_challenge.data); if (rv != 0) { return rv; } - lfr.type = NGTCP2_FRAME_PATH_CHALLENGE; + lfr.path_challenge.type = NGTCP2_FRAME_PATH_CHALLENGE; initial_pto = conn_compute_initial_pto(conn, &conn->pktns); - timeout = conn_compute_pto(conn, &conn->pktns); - timeout = ngtcp2_max_uint64(timeout, initial_pto); + timeout = ngtcp2_conn_compute_pto(conn, &conn->pktns); + timeout = ngtcp2_max(timeout, initial_pto); expiry = ts + timeout * (1ULL << pv->round); - destlen = ngtcp2_min_size(destlen, NGTCP2_MAX_UDP_PAYLOAD_SIZE); + destlen = ngtcp2_min(destlen, NGTCP2_MAX_UDP_PAYLOAD_SIZE); if (conn->server) { if (!(pv->dcid.flags & NGTCP2_DCID_FLAG_PATH_VALIDATED)) { tx_left = conn_server_tx_left(conn, &pv->dcid); - destlen = (size_t)ngtcp2_min_uint64((uint64_t)destlen, tx_left); + destlen = (size_t)ngtcp2_min((uint64_t)destlen, tx_left); if (destlen == 0) { return 0; } @@ -5132,7 +5210,7 @@ static ngtcp2_ssize conn_write_path_challenge(ngtcp2_conn *conn, flags = NGTCP2_PV_ENTRY_FLAG_NONE; } - ngtcp2_pv_add_entry(pv, lfr.path_challenge.data, expiry, flags, ts); + ngtcp2_pv_add_entry(pv, &lfr.path_challenge.data, expiry, flags, ts); nwrite = ngtcp2_conn_write_single_frame_pkt( conn, pi, dest, destlen, NGTCP2_PKT_1RTT, NGTCP2_WRITE_PKT_FLAG_NONE, @@ -5228,18 +5306,20 @@ static ngtcp2_ssize conn_write_path_response(ngtcp2_conn *conn, } } - destlen = ngtcp2_min_size(destlen, NGTCP2_MAX_UDP_PAYLOAD_SIZE); + destlen = ngtcp2_min(destlen, NGTCP2_MAX_UDP_PAYLOAD_SIZE); if (conn->server && !(dcid->flags & NGTCP2_DCID_FLAG_PATH_VALIDATED)) { tx_left = conn_server_tx_left(conn, dcid); - destlen = (size_t)ngtcp2_min_uint64((uint64_t)destlen, tx_left); + destlen = (size_t)ngtcp2_min((uint64_t)destlen, tx_left); if (destlen == 0) { return 0; } } - lfr.type = NGTCP2_FRAME_PATH_RESPONSE; - memcpy(lfr.path_response.data, pcent->data, sizeof(lfr.path_response.data)); + lfr.path_response = (ngtcp2_path_response){ + .type = NGTCP2_FRAME_PATH_RESPONSE, + .data = pcent->data, + }; nwrite = ngtcp2_conn_write_single_frame_pkt( conn, pi, dest, destlen, NGTCP2_PKT_1RTT, NGTCP2_WRITE_PKT_FLAG_NONE, @@ -5348,18 +5428,7 @@ static int conn_on_version_negotiation(ngtcp2_conn *conn, return rv; } -static uint64_t conn_tx_strmq_first_cycle(ngtcp2_conn *conn) { - ngtcp2_strm *strm; - - if (ngtcp2_pq_empty(&conn->tx.strmq)) { - return 0; - } - - strm = ngtcp2_struct_of(ngtcp2_pq_top(&conn->tx.strmq), ngtcp2_strm, pe); - return strm->cycle; -} - -uint64_t ngtcp2_conn_tx_strmq_first_cycle(ngtcp2_conn *conn) { +uint64_t ngtcp2_conn_tx_strmq_first_cycle(const ngtcp2_conn *conn) { ngtcp2_strm *strm; if (ngtcp2_pq_empty(&conn->tx.strmq)) { @@ -5396,11 +5465,10 @@ static int conn_on_retry(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd, ngtcp2_pktns *in_pktns = conn->in_pktns; ngtcp2_rtb *rtb = &conn->pktns.rtb; ngtcp2_rtb *in_rtb; - uint8_t cidbuf[sizeof(retry.odcid.data) * 2 + 1]; uint8_t *token; - if (!in_pktns || conn->flags & NGTCP2_CONN_FLAG_RECV_RETRY) { - return 0; + if (!in_pktns || (conn->flags & NGTCP2_CONN_FLAG_RECV_RETRY)) { + return NGTCP2_ERR_DISCARD_PKT; } in_rtb = &in_pktns->rtb; @@ -5421,9 +5489,7 @@ static int conn_on_retry(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd, return rv; } - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_PKT, "odcid=0x%s", - (const char *)ngtcp2_encode_hex(cidbuf, retry.odcid.data, - retry.odcid.datalen)); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_PKT, "odcid=0x", &retry.odcid); if (retry.tokenlen == 0) { return NGTCP2_ERR_PROTO; @@ -5517,21 +5583,17 @@ static int conn_recv_ack(ngtcp2_conn *conn, ngtcp2_pktns *pktns, ngtcp2_ack *fr, num_acked = ngtcp2_rtb_recv_ack(&pktns->rtb, fr, &conn->cstat, conn, pktns, pkt_ts, ts); - if (num_acked < 0) { + if (num_acked <= 0) { return (int)num_acked; } - if (num_acked == 0) { - return 0; - } - pktns->rtb.probe_pkt_left = 0; if (cstat->pto_count && (conn->server || (conn->flags & NGTCP2_CONN_FLAG_SERVER_ADDR_VERIFIED))) { /* Reset PTO count but no less than 2 to avoid frequent probe packet transmission. */ - cstat->pto_count = ngtcp2_min_size(cstat->pto_count, 2); + cstat->pto_count = ngtcp2_min(cstat->pto_count, 2); } ngtcp2_conn_set_loss_detection_timer(conn, ts); @@ -5675,7 +5737,7 @@ static int conn_recv_max_stream_data(ngtcp2_conn *conn, * conn_recv_max_data processes received MAX_DATA frame |fr|. */ static void conn_recv_max_data(ngtcp2_conn *conn, const ngtcp2_max_data *fr) { - conn->tx.max_offset = ngtcp2_max_uint64(conn->tx.max_offset, fr->max_data); + conn->tx.max_offset = ngtcp2_max(conn->tx.max_offset, fr->max_data); } /* @@ -5860,9 +5922,9 @@ decrypt_hp(ngtcp2_pkt_hd *hd, uint8_t *dest, const ngtcp2_crypto_cipher *hp, } if (hd->flags & NGTCP2_PKT_FLAG_LONG_FORM) { - dest[0] = (uint8_t)(dest[0] ^ (mask[0] & 0x0f)); + dest[0] = (uint8_t)(dest[0] ^ (mask[0] & 0x0FU)); } else { - dest[0] = (uint8_t)(dest[0] ^ (mask[0] & 0x1f)); + dest[0] = (uint8_t)(dest[0] ^ (mask[0] & 0x1FU)); if (dest[0] & NGTCP2_SHORT_KEY_PHASE_BIT) { hd->flags |= NGTCP2_PKT_FLAG_KEY_PHASE; } @@ -5956,7 +6018,7 @@ static int conn_recv_connection_close(ngtcp2_conn *conn, } } - ccerr->reasonlen = ngtcp2_min_size(fr->reasonlen, NGTCP2_CCERR_MAX_REASONLEN); + ccerr->reasonlen = ngtcp2_min(fr->reasonlen, NGTCP2_CCERR_MAX_REASONLEN); ngtcp2_cpymem((uint8_t *)ccerr->reason, fr->reason, ccerr->reasonlen); return 0; @@ -5977,7 +6039,7 @@ static void conn_recv_path_challenge(ngtcp2_conn *conn, const ngtcp2_path *path, } ent = ngtcp2_ringbuf_push_front(&conn->rx.path_challenge.rb); - ngtcp2_path_challenge_entry_init(ent, path, fr->data); + ngtcp2_path_challenge_entry_init(ent, path, &fr->data); } /* @@ -6047,7 +6109,7 @@ static int conn_recv_path_response(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd, return 0; } - rv = ngtcp2_pv_validate(pv, &ent_flags, fr->data); + rv = ngtcp2_pv_validate(pv, &ent_flags, &fr->data); if (rv != 0) { assert(!ngtcp2_err_is_fatal(rv)); @@ -6150,7 +6212,8 @@ static int conn_recv_path_response(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd, * pktns_pkt_num_is_duplicate returns nonzero if |pkt_num| is * duplicated packet number. */ -static int pktns_pkt_num_is_duplicate(ngtcp2_pktns *pktns, int64_t pkt_num) { +static int pktns_pkt_num_is_duplicate(const ngtcp2_pktns *pktns, + int64_t pkt_num) { return ngtcp2_gaptr_is_pushed(&pktns->rx.pngap, (uint64_t)pkt_num, 1); } @@ -6266,7 +6329,8 @@ static int vneg_available_versions_includes(const uint8_t *available_versions, * NGTCP2_ERR_INVALID_ARGUMENT * Clearing fixed bit is not permitted. */ -static int conn_verify_fixed_bit(ngtcp2_conn *conn, ngtcp2_pkt_hd *hd) { +static int conn_verify_fixed_bit(const ngtcp2_conn *conn, + const ngtcp2_pkt_hd *hd) { if (!(hd->flags & NGTCP2_PKT_FLAG_FIXED_BIT_CLEAR)) { return 0; } @@ -6351,8 +6415,8 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, ngtcp2_tstamp ts) { ngtcp2_ssize nread; ngtcp2_pkt_hd hd; - ngtcp2_max_frame mfr; - ngtcp2_frame *fr = &mfr.fr; + ngtcp2_frame_decoder frd; + ngtcp2_frame fr; int rv; int require_ack = 0; size_t hdpktlen; @@ -6383,8 +6447,8 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, return 0; } - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, - "buffering 1RTT packet len=%zu", pktlen); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "buffering 1RTT packet len=", pktlen); rv = conn_buffer_pkt(conn, &conn->pktns, path, pi, pkt, pktlen, dgramlen, ts); @@ -6531,8 +6595,8 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, } /* Buffer re-ordered 0-RTT packet. */ - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, - "buffering 0-RTT packet len=%zu", pktlen); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "buffering 0-RTT packet len=", pktlen); rv = conn_buffer_pkt(conn, conn->in_pktns, path, pi, pkt, pktlen, dgramlen, ts); @@ -6554,11 +6618,11 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, if (conn->server) { if (dgramlen < NGTCP2_MAX_UDP_PAYLOAD_SIZE) { - ngtcp2_log_info( + ngtcp2_log_infof( &conn->log, NGTCP2_LOG_EVENT_PKT, "Initial packet was ignored because it is included in UDP datagram " - "less than %zu bytes: %zu bytes", - NGTCP2_MAX_UDP_PAYLOAD_SIZE, dgramlen); + "less than " stringify(NGTCP2_MAX_UDP_PAYLOAD_SIZE) " bytes: ", + dgramlen, " bytes"); return NGTCP2_ERR_DISCARD_PKT; } if (conn->local.settings.tokenlen) { @@ -6637,8 +6701,8 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, "Handshake packet at this point is unexpected and discarded"); return (ngtcp2_ssize)pktlen; } - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, - "buffering Handshake packet len=%zu", pktlen); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "buffering Handshake packet len=", pktlen); rv = conn_buffer_pkt(conn, conn->hs_pktns, path, pi, pkt, pktlen, dgramlen, ts); @@ -6692,8 +6756,8 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, hd.pkt_num = ngtcp2_pkt_adjust_pkt_num(pktns->acktr.max_pkt_num, hd.pkt_num, hd.pkt_numlen); if (hd.pkt_num > NGTCP2_MAX_PKT_NUM) { - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_PKT, - "pkn=%" PRId64 " is greater than maximum pkn", hd.pkt_num); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_PKT, "pkn=", hd.pkt_num, + " is greater than maximum pkn"); return NGTCP2_ERR_DISCARD_PKT; } @@ -6740,9 +6804,9 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, !conn->negotiated_version) { conn->negotiated_version = hd.version; - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, - "the negotiated version is 0x%08x", - conn->negotiated_version); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "the negotiated version is 0x", + hexw(conn->negotiated_version, 8)); } payload = conn->crypto.decrypt_buf.base; @@ -6798,7 +6862,7 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, ngtcp2_qlog_pkt_received_start(&conn->qlog); for (; payloadlen;) { - nread = ngtcp2_pkt_decode_frame(fr, payload, payloadlen); + nread = ngtcp2_frame_decoder_decode(&frd, &fr, payload, payloadlen); if (nread < 0) { return nread; } @@ -6806,14 +6870,14 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, payload += nread; payloadlen -= (size_t)nread; - switch (fr->type) { + switch (fr.hd.type) { case NGTCP2_FRAME_ACK: case NGTCP2_FRAME_ACK_ECN: - fr->ack.ack_delay = 0; - fr->ack.ack_delay_unscaled = 0; + fr.ack.ack_delay = 0; + fr.ack.ack_delay_unscaled = 0; rv = - ngtcp2_pkt_validate_ack(&fr->ack, conn->local.settings.initial_pkt_num); + ngtcp2_pkt_validate_ack(&fr.ack, conn->local.settings.initial_pkt_num); if (rv != 0) { return rv; } @@ -6821,9 +6885,9 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, break; } - ngtcp2_log_rx_fr(&conn->log, &hd, fr); + ngtcp2_log_rx_fr(&conn->log, &hd, &fr); - switch (fr->type) { + switch (fr.hd.type) { case NGTCP2_FRAME_ACK: case NGTCP2_FRAME_ACK_ECN: if (num_ack_processed >= NGTCP2_MAX_ACK_PER_PKT) { @@ -6832,7 +6896,7 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, if (!conn->server && hd.type == NGTCP2_PKT_HANDSHAKE) { conn->flags |= NGTCP2_CONN_FLAG_SERVER_ADDR_VERIFIED; } - rv = conn_recv_ack(conn, pktns, &fr->ack, pkt_ts, ts); + rv = conn_recv_ack(conn, pktns, &fr.ack, pkt_ts, ts); if (rv != 0) { return rv; } @@ -6842,34 +6906,35 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, break; case NGTCP2_FRAME_CRYPTO: if (!conn->server && !conn->negotiated_version && - ngtcp2_vec_len(fr->stream.data, fr->stream.datacnt)) { + ngtcp2_vec_len(fr.stream.data, fr.stream.datacnt)) { conn->negotiated_version = hd.version; - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, - "the negotiated version is 0x%08x", - conn->negotiated_version); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "the negotiated version is 0x", + hexw(conn->negotiated_version, 8)); } - rv = conn_recv_crypto(conn, encryption_level, crypto, &fr->stream, ts); + rv = conn_recv_crypto(conn, encryption_level, crypto, &fr.stream, ts); if (rv != 0) { return rv; } require_ack = 1; break; case NGTCP2_FRAME_CONNECTION_CLOSE: - rv = conn_recv_connection_close(conn, &fr->connection_close); + rv = conn_recv_connection_close(conn, &fr.connection_close); if (rv != 0) { return rv; } break; case NGTCP2_FRAME_PING: + ++conn->cstat.ping_recv; require_ack = 1; break; default: return NGTCP2_ERR_PROTO; } - ngtcp2_qlog_write_frame(&conn->qlog, fr); + ngtcp2_qlog_write_frame(&conn->qlog, &fr); } if (hd.type == NGTCP2_PKT_HANDSHAKE) { @@ -6929,6 +6994,10 @@ static ngtcp2_ssize conn_recv_handshake_cpkt(ngtcp2_conn *conn, const uint8_t *origpkt = pkt; uint32_t version; + if (pktlen == 0) { + return 0; + } + if (ngtcp2_path_eq(&conn->dcid.current.ps.path, path)) { conn->dcid.current.bytes_recv += dgramlen; } @@ -6956,6 +7025,8 @@ static ngtcp2_ssize conn_recv_handshake_cpkt(ngtcp2_conn *conn, return nread; } + ++conn->cstat.pkt_discarded; + /* If server discards first Initial, then drop connection state. This is because SCID in packet might be corrupted and the current connection state might wrongly discard @@ -6973,11 +7044,15 @@ static ngtcp2_ssize conn_recv_handshake_cpkt(ngtcp2_conn *conn, unrecoverable, therefore drop connection. */ return nread; } + + ++conn->cstat.pkt_discarded; + return (ngtcp2_ssize)dgramlen; } } if (nread == NGTCP2_ERR_DISCARD_PKT) { + ++conn->cstat.pkt_discarded; return (ngtcp2_ssize)dgramlen; } @@ -6993,8 +7068,11 @@ static ngtcp2_ssize conn_recv_handshake_cpkt(ngtcp2_conn *conn, pkt += nread; pktlen -= (size_t)nread; - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_PKT, - "read packet %td left %zu", nread, pktlen); + ++conn->cstat.pkt_recv; + conn->cstat.bytes_recv += (uint64_t)nread; + + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_PKT, "read packet ", nread, + " left ", pktlen); } return (ngtcp2_ssize)dgramlen; @@ -7006,6 +7084,7 @@ int ngtcp2_conn_init_stream(ngtcp2_conn *conn, ngtcp2_strm *strm, uint64_t max_rx_offset; uint64_t max_tx_offset; int local_stream = conn_local_stream(conn, stream_id); + int bidi = bidi_stream(stream_id); assert(conn->remote.transport_params); @@ -7033,6 +7112,15 @@ int ngtcp2_conn_init_stream(ngtcp2_conn *conn, ngtcp2_strm *strm, max_tx_offset, stream_user_data, &conn->frc_objalloc, conn->mem); + if (!bidi) { + if (local_stream) { + ngtcp2_strm_shutdown(strm, NGTCP2_STRM_FLAG_SHUT_RD); + } else { + ngtcp2_strm_shutdown(strm, NGTCP2_STRM_FLAG_SHUT_WR); + strm->flags |= NGTCP2_STRM_FLAG_FIN_ACKED; + } + } + rv = ngtcp2_map_insert(&conn->strms, (ngtcp2_map_key_type)strm->stream_id, strm); if (rv != 0) { @@ -7150,6 +7238,7 @@ static int conn_recv_crypto(ngtcp2_conn *conn, uint64_t rx_offset; int rv; ngtcp2_ssize nwrite; + size_t max_offset; if (fr->datacnt == 0) { if (encryption_level != NGTCP2_ENCRYPTION_LEVEL_INITIAL && @@ -7166,6 +7255,27 @@ static int conn_recv_crypto(ngtcp2_conn *conn, return NGTCP2_ERR_FRAME_ENCODING; } + /* Apply the absolute upper bound against CRYPTO data offset because + some TLS stacks do not care much about the number of bytes they + store. */ + switch (encryption_level) { + case NGTCP2_ENCRYPTION_LEVEL_INITIAL: + max_offset = NGTCP2_MAX_INITIAL_CRYPTO_OFFSET; + break; + case NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE: + max_offset = NGTCP2_MAX_HANDSHAKE_CRYPTO_OFFSET; + break; + case NGTCP2_ENCRYPTION_LEVEL_1RTT: + max_offset = NGTCP2_MAX_1RTT_CRYPTO_OFFSET; + break; + default: + ngtcp2_unreachable(); + } + + if (fr_end_offset > max_offset) { + return NGTCP2_ERR_CRYPTO_BUFFER_EXCEEDED; + } + rx_offset = ngtcp2_strm_rx_offset(crypto); if (fr_end_offset <= rx_offset) { @@ -7195,8 +7305,7 @@ static int conn_recv_crypto(ngtcp2_conn *conn, return 0; } - crypto->rx.last_offset = - ngtcp2_max_uint64(crypto->rx.last_offset, fr_end_offset); + crypto->rx.last_offset = ngtcp2_max(crypto->rx.last_offset, fr_end_offset); /* TODO Before dispatching incoming data to TLS stack, make sure that previous data in previous encryption level has been @@ -7249,7 +7358,7 @@ static int conn_recv_crypto(ngtcp2_conn *conn, * conn_max_data_violated returns nonzero if receiving |datalen| * violates connection flow control on local endpoint. */ -static int conn_max_data_violated(ngtcp2_conn *conn, uint64_t datalen) { +static int conn_max_data_violated(const ngtcp2_conn *conn, uint64_t datalen) { return conn->rx.max_offset - conn->rx.offset < datalen; } @@ -7359,11 +7468,6 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr, new_strm = 1; - if (!bidi) { - ngtcp2_strm_shutdown(strm, NGTCP2_STRM_FLAG_SHUT_WR); - strm->flags |= NGTCP2_STRM_FLAG_FIN_ACKED; - } - rv = conn_call_stream_open(conn, strm); if (rv != 0) { return rv; @@ -7426,8 +7530,7 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr, return NGTCP2_ERR_FINAL_SIZE; } - strm->rx.last_offset = - ngtcp2_max_uint64(strm->rx.last_offset, fr_end_offset); + strm->rx.last_offset = ngtcp2_max(strm->rx.last_offset, fr_end_offset); if (fr_end_offset <= rx_offset) { if (!new_strm && ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { @@ -7521,7 +7624,7 @@ static int conn_reset_stream(ngtcp2_conn *conn, ngtcp2_strm *strm, return 0; } - strm->cycle = conn_tx_strmq_first_cycle(conn); + strm->cycle = ngtcp2_conn_tx_strmq_first_cycle(conn); return ngtcp2_conn_tx_strmq_push(conn, strm); } @@ -7545,7 +7648,7 @@ static int conn_stop_sending(ngtcp2_conn *conn, ngtcp2_strm *strm, return 0; } - strm->cycle = conn_tx_strmq_first_cycle(conn); + strm->cycle = ngtcp2_conn_tx_strmq_first_cycle(conn); return ngtcp2_conn_tx_strmq_push(conn, strm); } @@ -7855,9 +7958,8 @@ static int conn_recv_stop_sending(ngtcp2_conn *conn, */ static int check_stateless_reset(const ngtcp2_dcid *dcid, const ngtcp2_path *path, - const ngtcp2_pkt_stateless_reset *sr) { - return ngtcp2_dcid_verify_stateless_reset_token( - dcid, path, sr->stateless_reset_token) == 0; + const ngtcp2_pkt_stateless_reset2 *sr) { + return ngtcp2_dcid_verify_stateless_reset_token(dcid, path, &sr->token) == 0; } /* @@ -7881,7 +7983,7 @@ static int conn_on_stateless_reset(ngtcp2_conn *conn, const ngtcp2_path *path, const uint8_t *payload, size_t payloadlen) { int rv; ngtcp2_pv *pv = conn->pv; - ngtcp2_pkt_stateless_reset sr; + ngtcp2_pkt_stateless_reset2 sr; rv = ngtcp2_pkt_decode_stateless_reset(&sr, payload, payloadlen); if (rv != 0) { @@ -7892,8 +7994,7 @@ static int conn_on_stateless_reset(ngtcp2_conn *conn, const ngtcp2_path *path, (!pv || (!check_stateless_reset(&pv->dcid, path, &sr) && (!(pv->flags & NGTCP2_PV_FLAG_FALLBACK_PRESENT) || !check_stateless_reset(&pv->fallback_dcid, path, &sr))))) { - rv = ngtcp2_dcidtr_verify_stateless_reset(&conn->dcid.dtr, path, - sr.stateless_reset_token); + rv = ngtcp2_dcidtr_verify_stateless_reset(&conn->dcid.dtr, path, &sr.token); if (rv != 0) { return rv; } @@ -7928,7 +8029,7 @@ static int conn_recv_max_streams(ngtcp2_conn *conn, return NGTCP2_ERR_FRAME_ENCODING; } - n = ngtcp2_min_uint64(fr->max_streams, NGTCP2_MAX_STREAMS); + n = ngtcp2_min(fr->max_streams, NGTCP2_MAX_STREAMS); if (fr->type == NGTCP2_FRAME_MAX_STREAMS_BIDI) { if (conn->local.bidi.max_streams < n) { @@ -7973,7 +8074,7 @@ static int conn_recv_new_connection_id(ngtcp2_conn *conn, } rv = ngtcp2_dcid_verify_uniqueness(&conn->dcid.current, fr->seq, &fr->cid, - fr->stateless_reset_token); + &fr->token); if (rv != 0) { return rv; } @@ -7982,8 +8083,8 @@ static int conn_recv_new_connection_id(ngtcp2_conn *conn, } if (pv) { - rv = ngtcp2_dcid_verify_uniqueness(&pv->dcid, fr->seq, &fr->cid, - fr->stateless_reset_token); + rv = + ngtcp2_dcid_verify_uniqueness(&pv->dcid, fr->seq, &fr->cid, &fr->token); if (rv != 0) { return rv; } @@ -7992,8 +8093,8 @@ static int conn_recv_new_connection_id(ngtcp2_conn *conn, } } - rv = ngtcp2_dcidtr_verify_token_uniqueness( - &conn->dcid.dtr, &found, fr->seq, &fr->cid, fr->stateless_reset_token); + rv = ngtcp2_dcidtr_verify_token_uniqueness(&conn->dcid.dtr, &found, fr->seq, + &fr->cid, &fr->token); if (rv != 0) { return rv; } @@ -8071,8 +8172,7 @@ static int conn_recv_new_connection_id(ngtcp2_conn *conn, return 0; } - ngtcp2_dcidtr_push_unused(&conn->dcid.dtr, fr->seq, &fr->cid, - fr->stateless_reset_token); + ngtcp2_dcidtr_push_unused(&conn->dcid.dtr, fr->seq, &fr->cid, &fr->token); return 0; } @@ -8372,11 +8472,6 @@ static int conn_recv_stream_data_blocked(ngtcp2_conn *conn, return rv; } - if (!bidi) { - ngtcp2_strm_shutdown(strm, NGTCP2_STRM_FLAG_SHUT_WR); - strm->flags |= NGTCP2_STRM_FLAG_FIN_ACKED; - } - rv = conn_call_stream_open(conn, strm); if (rv != 0) { return rv; @@ -8560,8 +8655,9 @@ static int conn_recv_datagram(ngtcp2_conn *conn, ngtcp2_datagram *fr) { * conn_key_phase_changed returns nonzero if |hd| indicates that the * key phase has unexpected value. */ -static int conn_key_phase_changed(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd) { - ngtcp2_pktns *pktns = &conn->pktns; +static int conn_key_phase_changed(const ngtcp2_conn *conn, + const ngtcp2_pkt_hd *hd) { + const ngtcp2_pktns *pktns = &conn->pktns; return !(pktns->crypto.rx.ckm->flags & NGTCP2_CRYPTO_KM_FLAG_KEY_PHASE_ONE) ^ !(hd->flags & NGTCP2_PKT_FLAG_KEY_PHASE); @@ -8575,7 +8671,7 @@ static int conn_initiate_key_update(ngtcp2_conn *conn, ngtcp2_tstamp ts); static int conn_prepare_key_update(ngtcp2_conn *conn, ngtcp2_tstamp ts) { int rv; ngtcp2_tstamp confirmed_ts = conn->crypto.key_update.confirmed_ts; - ngtcp2_duration pto = conn_compute_pto(conn, &conn->pktns); + ngtcp2_duration pto = ngtcp2_conn_compute_pto(conn, &conn->pktns); ngtcp2_pktns *pktns = &conn->pktns; ngtcp2_crypto_km *rx_ckm = pktns->crypto.rx.ckm; ngtcp2_crypto_km *tx_ckm = pktns->crypto.tx.ckm; @@ -8684,9 +8780,9 @@ static void conn_rotate_keys(ngtcp2_conn *conn, int64_t pkt_num, * conn_path_validation_in_progress returns nonzero if path validation * against |path| is underway. */ -static int conn_path_validation_in_progress(ngtcp2_conn *conn, +static int conn_path_validation_in_progress(const ngtcp2_conn *conn, const ngtcp2_path *path) { - ngtcp2_pv *pv = conn->pv; + const ngtcp2_pv *pv = conn->pv; return pv && ngtcp2_path_eq(&pv->dcid.ps.path, path); } @@ -8713,7 +8809,6 @@ static int conn_recv_non_probing_pkt_on_new_path(ngtcp2_conn *conn, ngtcp2_pv *pv = NULL; int rv; ngtcp2_duration pto; - int require_new_cid; int local_addr_eq; int pref_addr_migration; uint32_t remote_addr_cmp; @@ -8771,9 +8866,6 @@ static int conn_recv_non_probing_pkt_on_new_path(ngtcp2_conn *conn, * continue to use the current connection ID with the new remote * address while still sending from the same local address. */ - require_new_cid = conn->dcid.current.cid.datalen && - ((new_cid_used && remote_addr_cmp) || !local_addr_eq); - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, "non-probing packet was received from new remote address"); @@ -8781,8 +8873,6 @@ static int conn_recv_non_probing_pkt_on_new_path(ngtcp2_conn *conn, ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, "Found DCID which has already been bound to the new path"); - require_new_cid = 0; - if (dcid.cid.datalen) { rv = conn_call_activate_dcid(conn, &dcid); if (rv != 0) { @@ -8790,7 +8880,9 @@ static int conn_recv_non_probing_pkt_on_new_path(ngtcp2_conn *conn, } } } else { - if (require_new_cid) { + if (conn->dcid.current.cid.datalen && + ((new_cid_used && remote_addr_cmp) || !local_addr_eq)) { + /* New DCID is required. */ if (ngtcp2_dcidtr_unused_empty(&conn->dcid.dtr)) { return NGTCP2_ERR_CONN_ID_BLOCKED; } @@ -8817,7 +8909,7 @@ static int conn_recv_non_probing_pkt_on_new_path(ngtcp2_conn *conn, validated_path = ngtcp2_conn_find_path_history(conn, path, ts); if (!validated_path) { - pto = conn_compute_pto(conn, &conn->pktns); + pto = ngtcp2_conn_compute_pto(conn, &conn->pktns); rv = ngtcp2_pv_new(&pv, &dcid, conn_compute_pv_timeout_pto(conn, pto), NGTCP2_PV_FLAG_NONE, &conn->log, conn->mem); @@ -8955,8 +9047,8 @@ conn_recv_delayed_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_pkt_info *pi, const uint8_t *payload, size_t payloadlen, ngtcp2_tstamp pkt_ts, ngtcp2_tstamp ts) { ngtcp2_ssize nread; - ngtcp2_max_frame mfr; - ngtcp2_frame *fr = &mfr.fr; + ngtcp2_frame_decoder frd; + ngtcp2_frame fr; int rv; int require_ack = 0; ngtcp2_pktns *pktns; @@ -8974,7 +9066,7 @@ conn_recv_delayed_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_pkt_info *pi, ngtcp2_qlog_pkt_received_start(&conn->qlog); for (; payloadlen;) { - nread = ngtcp2_pkt_decode_frame(fr, payload, payloadlen); + nread = ngtcp2_frame_decoder_decode(&frd, &fr, payload, payloadlen); if (nread < 0) { return (int)nread; } @@ -8982,14 +9074,14 @@ conn_recv_delayed_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_pkt_info *pi, payload += nread; payloadlen -= (size_t)nread; - switch (fr->type) { + switch (fr.hd.type) { case NGTCP2_FRAME_ACK: case NGTCP2_FRAME_ACK_ECN: - fr->ack.ack_delay = 0; - fr->ack.ack_delay_unscaled = 0; + fr.ack.ack_delay = 0; + fr.ack.ack_delay_unscaled = 0; rv = - ngtcp2_pkt_validate_ack(&fr->ack, conn->local.settings.initial_pkt_num); + ngtcp2_pkt_validate_ack(&fr.ack, conn->local.settings.initial_pkt_num); if (rv != 0) { return rv; } @@ -8997,9 +9089,9 @@ conn_recv_delayed_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_pkt_info *pi, break; } - ngtcp2_log_rx_fr(&conn->log, hd, fr); + ngtcp2_log_rx_fr(&conn->log, hd, &fr); - switch (fr->type) { + switch (fr.hd.type) { case NGTCP2_FRAME_ACK: case NGTCP2_FRAME_ACK_ECN: if (num_ack_processed >= NGTCP2_MAX_ACK_PER_PKT) { @@ -9008,7 +9100,7 @@ conn_recv_delayed_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_pkt_info *pi, if (!conn->server) { conn->flags |= NGTCP2_CONN_FLAG_SERVER_ADDR_VERIFIED; } - rv = conn_recv_ack(conn, pktns, &fr->ack, pkt_ts, ts); + rv = conn_recv_ack(conn, pktns, &fr.ack, pkt_ts, ts); if (rv != 0) { return rv; } @@ -9017,20 +9109,22 @@ conn_recv_delayed_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_pkt_info *pi, case NGTCP2_FRAME_PADDING: break; case NGTCP2_FRAME_CONNECTION_CLOSE: - rv = conn_recv_connection_close(conn, &fr->connection_close); + rv = conn_recv_connection_close(conn, &fr.connection_close); if (rv != 0) { return rv; } break; - case NGTCP2_FRAME_CRYPTO: case NGTCP2_FRAME_PING: + ++conn->cstat.ping_recv; + /* fall through */ + case NGTCP2_FRAME_CRYPTO: require_ack = 1; break; default: return NGTCP2_ERR_PROTO; } - ngtcp2_qlog_write_frame(&conn->qlog, fr); + ngtcp2_qlog_write_frame(&conn->qlog, &fr); } ngtcp2_qlog_pkt_received_end(&conn->qlog, hd, pktlen); @@ -9098,8 +9192,8 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, const uint8_t *payload; size_t payloadlen; ngtcp2_ssize nread, nwrite; - ngtcp2_max_frame mfr; - ngtcp2_frame *fr = &mfr.fr; + ngtcp2_frame_decoder frd; + ngtcp2_frame fr; int require_ack = 0; ngtcp2_crypto_aead *aead; ngtcp2_crypto_cipher *hp; @@ -9193,8 +9287,8 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, break; default: ngtcp2_log_rx_pkt_hd(&conn->log, &hd); - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_PKT, - "packet type 0x%02x was ignored", hd.type); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_PKT, "packet type 0x", + hex(hd.type), " was ignored"); return (ngtcp2_ssize)pktlen; } } else { @@ -9241,8 +9335,8 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, hd.pkt_num = ngtcp2_pkt_adjust_pkt_num(pktns->acktr.max_pkt_num, hd.pkt_num, hd.pkt_numlen); if (hd.pkt_num > NGTCP2_MAX_PKT_NUM) { - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_PKT, - "pkn=%" PRId64 " is greater than maximum pkn", hd.pkt_num); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_PKT, "pkn=", hd.pkt_num, + " is greater than maximum pkn"); return NGTCP2_ERR_DISCARD_PKT; } @@ -9253,12 +9347,10 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, conn->rx.preferred_addr.pkt_num < hd.pkt_num && ngtcp2_sockaddr_eq((const ngtcp2_sockaddr *)&conn->hs_local_addr, path->local.addr)) { - ngtcp2_log_info( - &conn->log, NGTCP2_LOG_EVENT_PKT, - "pkt=%" PRId64 + ngtcp2_log_infof( + &conn->log, NGTCP2_LOG_EVENT_PKT, "pkt=", hd.pkt_num, " is discarded because it was received on handshake local " - "address after preferred address migration", - hd.pkt_num); + "address after preferred address migration"); return NGTCP2_ERR_DISCARD_PKT; } @@ -9405,7 +9497,7 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, ngtcp2_qlog_pkt_received_start(&conn->qlog); for (; payloadlen;) { - nread = ngtcp2_pkt_decode_frame(fr, payload, payloadlen); + nread = ngtcp2_frame_decoder_decode(&frd, &fr, payload, payloadlen); if (nread < 0) { return nread; } @@ -9413,7 +9505,7 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, payload += nread; payloadlen -= (size_t)nread; - switch (fr->type) { + switch (fr.hd.type) { case NGTCP2_FRAME_ACK: case NGTCP2_FRAME_ACK_ECN: if ((hd.flags & NGTCP2_PKT_FLAG_LONG_FORM) && @@ -9422,10 +9514,10 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, } assert(conn->remote.transport_params); assign_recved_ack_delay_unscaled( - &fr->ack, conn->remote.transport_params->ack_delay_exponent); + &fr.ack, conn->remote.transport_params->ack_delay_exponent); rv = - ngtcp2_pkt_validate_ack(&fr->ack, conn->local.settings.initial_pkt_num); + ngtcp2_pkt_validate_ack(&fr.ack, conn->local.settings.initial_pkt_num); if (rv != 0) { return rv; } @@ -9433,10 +9525,10 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, break; } - ngtcp2_log_rx_fr(&conn->log, &hd, fr); + ngtcp2_log_rx_fr(&conn->log, &hd, &fr); if (hd.type == NGTCP2_PKT_0RTT) { - switch (fr->type) { + switch (fr.hd.type) { case NGTCP2_FRAME_PADDING: case NGTCP2_FRAME_PING: case NGTCP2_FRAME_RESET_STREAM: @@ -9462,7 +9554,7 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, } } - switch (fr->type) { + switch (fr.hd.type) { case NGTCP2_FRAME_ACK: case NGTCP2_FRAME_ACK_ECN: case NGTCP2_FRAME_PADDING: @@ -9473,7 +9565,7 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, require_ack = 1; } - switch (fr->type) { + switch (fr.hd.type) { case NGTCP2_FRAME_ACK: case NGTCP2_FRAME_ACK_ECN: if (num_ack_processed >= NGTCP2_MAX_ACK_PER_PKT) { @@ -9482,7 +9574,7 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, if (!conn->server) { conn->flags |= NGTCP2_CONN_FLAG_SERVER_ADDR_VERIFIED; } - rv = conn_recv_ack(conn, pktns, &fr->ack, pkt_ts, ts); + rv = conn_recv_ack(conn, pktns, &fr.ack, pkt_ts, ts); if (rv != 0) { return rv; } @@ -9490,7 +9582,7 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, ++num_ack_processed; break; case NGTCP2_FRAME_STREAM: - rv = conn_recv_stream(conn, &fr->stream, ts); + rv = conn_recv_stream(conn, &fr.stream, ts); if (rv != 0) { return rv; } @@ -9498,40 +9590,40 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, break; case NGTCP2_FRAME_CRYPTO: rv = conn_recv_crypto(conn, NGTCP2_ENCRYPTION_LEVEL_1RTT, - &pktns->crypto.strm, &fr->stream, ts); + &pktns->crypto.strm, &fr.stream, ts); if (rv != 0) { return rv; } non_probing_pkt = 1; break; case NGTCP2_FRAME_RESET_STREAM: - rv = conn_recv_reset_stream(conn, &fr->reset_stream, ts); + rv = conn_recv_reset_stream(conn, &fr.reset_stream, ts); if (rv != 0) { return rv; } non_probing_pkt = 1; break; case NGTCP2_FRAME_STOP_SENDING: - rv = conn_recv_stop_sending(conn, &fr->stop_sending, ts); + rv = conn_recv_stop_sending(conn, &fr.stop_sending, ts); if (rv != 0) { return rv; } non_probing_pkt = 1; break; case NGTCP2_FRAME_MAX_STREAM_DATA: - rv = conn_recv_max_stream_data(conn, &fr->max_stream_data, ts); + rv = conn_recv_max_stream_data(conn, &fr.max_stream_data, ts); if (rv != 0) { return rv; } non_probing_pkt = 1; break; case NGTCP2_FRAME_MAX_DATA: - conn_recv_max_data(conn, &fr->max_data); + conn_recv_max_data(conn, &fr.max_data); non_probing_pkt = 1; break; case NGTCP2_FRAME_MAX_STREAMS_BIDI: case NGTCP2_FRAME_MAX_STREAMS_UNI: - rv = conn_recv_max_streams(conn, &fr->max_streams); + rv = conn_recv_max_streams(conn, &fr.max_streams); if (rv != 0) { return rv; } @@ -9539,41 +9631,42 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, break; case NGTCP2_FRAME_CONNECTION_CLOSE: case NGTCP2_FRAME_CONNECTION_CLOSE_APP: - rv = conn_recv_connection_close(conn, &fr->connection_close); + rv = conn_recv_connection_close(conn, &fr.connection_close); if (rv != 0) { return rv; } break; case NGTCP2_FRAME_PING: + ++conn->cstat.ping_recv; non_probing_pkt = 1; break; case NGTCP2_FRAME_PATH_CHALLENGE: - conn_recv_path_challenge(conn, path, &fr->path_challenge); + conn_recv_path_challenge(conn, path, &fr.path_challenge); path_challenge_recved = 1; break; case NGTCP2_FRAME_PATH_RESPONSE: - rv = conn_recv_path_response(conn, &hd, &fr->path_response, ts); + rv = conn_recv_path_response(conn, &hd, &fr.path_response, ts); if (rv != 0) { return rv; } break; case NGTCP2_FRAME_NEW_CONNECTION_ID: - rv = conn_recv_new_connection_id(conn, &fr->new_connection_id); + rv = conn_recv_new_connection_id(conn, &fr.new_connection_id); if (rv != 0) { return rv; } recv_ncid = 1; break; case NGTCP2_FRAME_RETIRE_CONNECTION_ID: - rv = conn_recv_retire_connection_id(conn, &hd, &fr->retire_connection_id, - ts); + rv = + conn_recv_retire_connection_id(conn, &hd, &fr.retire_connection_id, ts); if (rv != 0) { return rv; } non_probing_pkt = 1; break; case NGTCP2_FRAME_NEW_TOKEN: - rv = conn_recv_new_token(conn, &fr->new_token); + rv = conn_recv_new_token(conn, &fr.new_token); if (rv != 0) { return rv; } @@ -9587,28 +9680,28 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, non_probing_pkt = 1; break; case NGTCP2_FRAME_STREAMS_BLOCKED_BIDI: - rv = conn_recv_streams_blocked_bidi(conn, &fr->streams_blocked); + rv = conn_recv_streams_blocked_bidi(conn, &fr.streams_blocked); if (rv != 0) { return rv; } non_probing_pkt = 1; break; case NGTCP2_FRAME_STREAMS_BLOCKED_UNI: - rv = conn_recv_streams_blocked_uni(conn, &fr->streams_blocked); + rv = conn_recv_streams_blocked_uni(conn, &fr.streams_blocked); if (rv != 0) { return rv; } non_probing_pkt = 1; break; case NGTCP2_FRAME_STREAM_DATA_BLOCKED: - rv = conn_recv_stream_data_blocked(conn, &fr->stream_data_blocked); + rv = conn_recv_stream_data_blocked(conn, &fr.stream_data_blocked); if (rv != 0) { return rv; } non_probing_pkt = 1; break; case NGTCP2_FRAME_DATA_BLOCKED: - rv = conn_recv_data_blocked(conn, &fr->data_blocked); + rv = conn_recv_data_blocked(conn, &fr.data_blocked); if (rv != 0) { return rv; } @@ -9620,7 +9713,7 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, conn->local.transport_params.max_datagram_frame_size) { return NGTCP2_ERR_PROTO; } - rv = conn_recv_datagram(conn, &fr->datagram); + rv = conn_recv_datagram(conn, &fr.datagram); if (rv != 0) { return rv; } @@ -9628,7 +9721,7 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, break; } - ngtcp2_qlog_write_frame(&conn->qlog, fr); + ngtcp2_qlog_write_frame(&conn->qlog, &fr); } ngtcp2_qlog_pkt_received_end(&conn->qlog, &hd, pktlen); @@ -9743,6 +9836,7 @@ static int conn_process_buffered_protected_pkt(ngtcp2_conn *conn, *ppc = next; if (nread < 0) { if (nread == NGTCP2_ERR_DISCARD_PKT) { + ++conn->cstat.pkt_discarded; continue; } return (int)nread; @@ -9777,6 +9871,7 @@ static int conn_process_buffered_handshake_pkt(ngtcp2_conn *conn, *ppc = next; if (nread < 0) { if (nread == NGTCP2_ERR_DISCARD_PKT) { + ++conn->cstat.pkt_discarded; continue; } return (int)nread; @@ -9922,6 +10017,7 @@ static int conn_recv_cpkt(ngtcp2_conn *conn, const ngtcp2_path *path, } } if (nread == NGTCP2_ERR_DISCARD_PKT) { + ++conn->cstat.pkt_discarded; return 0; } return (int)nread; @@ -9931,8 +10027,11 @@ static int conn_recv_cpkt(ngtcp2_conn *conn, const ngtcp2_path *path, pkt += nread; pktlen -= (size_t)nread; - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_PKT, - "read packet %td left %zu", nread, pktlen); + ++conn->cstat.pkt_recv; + conn->cstat.bytes_recv += (uint64_t)nread; + + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_PKT, "read packet ", nread, + " left ", pktlen); } return 0; @@ -9954,7 +10053,7 @@ static int conn_enqueue_handshake_done(ngtcp2_conn *conn) { return rv; } - nfrc->fr.type = NGTCP2_FRAME_HANDSHAKE_DONE; + nfrc->fr.handshake_done.type = NGTCP2_FRAME_HANDSHAKE_DONE; nfrc->next = pktns->tx.frq; pktns->tx.frq = nfrc; @@ -10104,9 +10203,8 @@ static ngtcp2_ssize conn_read_handshake(ngtcp2_conn *conn, if ((size_t)nread < pktlen) { /* We have 1RTT packet and application rx key, but the handshake has not completed yet. */ - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, - "buffering 1RTT packet len=%zu", - pktlen - (size_t)nread); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "buffering 1RTT packet len=", pktlen - (size_t)nread); rv = conn_buffer_pkt(conn, &conn->pktns, path, pi, pkt + nread, pktlen - (size_t)nread, pktlen, ts); @@ -10185,8 +10283,8 @@ int ngtcp2_conn_read_pkt_versioned(ngtcp2_conn *conn, const ngtcp2_path *path, conn_update_timestamp(conn, ts); - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, "recv packet len=%zu", - pktlen); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "recv packet len=", pktlen); if (pktlen == 0) { return 0; @@ -10198,6 +10296,8 @@ int ngtcp2_conn_read_pkt_versioned(ngtcp2_conn *conn, const ngtcp2_path *path, !ngtcp2_dcidtr_check_path_retired(&conn->dcid.dtr, path)) { ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, "ignore packet from unknown path"); + ++conn->cstat.pkt_discarded; + return 0; } @@ -10271,13 +10371,56 @@ int ngtcp2_conn_read_pkt_versioned(ngtcp2_conn *conn, const ngtcp2_path *path, return conn_recv_cpkt(conn, path, pi, pkt, pktlen, ts); } +int ngtcp2_conn_continue_handshake(ngtcp2_conn *conn, ngtcp2_tstamp ts) { + int rv; + ngtcp2_encryption_level encryption_level; + uint64_t offset; + + conn_update_timestamp(conn, ts); + + switch (conn->state) { + case NGTCP2_CS_CLIENT_INITIAL: + case NGTCP2_CS_CLIENT_WAIT_HANDSHAKE: + case NGTCP2_CS_SERVER_INITIAL: + case NGTCP2_CS_SERVER_WAIT_HANDSHAKE: + /* Most of the handshake interruption happens in Initial + encryption level, but this might not be the case depending on + the TLS stack and its functionality and where interruption + occurs. After all, we do not need to support all kinds of + interruptions. */ + if (conn->in_pktns) { + encryption_level = NGTCP2_ENCRYPTION_LEVEL_INITIAL; + offset = ngtcp2_strm_rx_offset(&conn->in_pktns->crypto.strm); + } else if (conn->hs_pktns) { + encryption_level = NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE; + offset = ngtcp2_strm_rx_offset(&conn->hs_pktns->crypto.strm); + } else { + return 0; + } + + rv = conn_call_recv_crypto_data(conn, encryption_level, offset, NULL, 0); + if (rv != 0) { + return rv; + } + + return (int)conn_read_handshake(conn, /* path = */ NULL, /* pi = */ NULL, + /* pkt = */ NULL, 0, ts); + case NGTCP2_CS_CLOSING: + return NGTCP2_ERR_CLOSING; + case NGTCP2_CS_DRAINING: + return NGTCP2_ERR_DRAINING; + default: + return 0; + } +} + /* * conn_check_pkt_num_exhausted returns nonzero if packet number is * exhausted in at least one of packet number space. */ -static int conn_check_pkt_num_exhausted(ngtcp2_conn *conn) { - ngtcp2_pktns *in_pktns = conn->in_pktns; - ngtcp2_pktns *hs_pktns = conn->hs_pktns; +static int conn_check_pkt_num_exhausted(const ngtcp2_conn *conn) { + const ngtcp2_pktns *in_pktns = conn->in_pktns; + const ngtcp2_pktns *hs_pktns = conn->hs_pktns; return (in_pktns && in_pktns->tx.last_pkt_num == NGTCP2_MAX_PKT_NUM) || (hs_pktns && hs_pktns->tx.last_pkt_num == NGTCP2_MAX_PKT_NUM) || @@ -10301,7 +10444,7 @@ conn_retransmit_retry_early(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, * packets to be sent for Initial or Handshake packet number space * left. */ -static int conn_handshake_probe_left(ngtcp2_conn *conn) { +static int conn_handshake_probe_left(const ngtcp2_conn *conn) { return (conn->in_pktns && conn->in_pktns->rtb.probe_pkt_left) || conn->hs_pktns->rtb.probe_pkt_left; } @@ -10312,7 +10455,8 @@ static int conn_handshake_probe_left(ngtcp2_conn *conn) { * are not reduced. This function is only used by client and should * only be called when early data is accepted by server. */ -static int conn_validate_early_transport_params_limits(ngtcp2_conn *conn) { +static int +conn_validate_early_transport_params_limits(const ngtcp2_conn *conn) { const ngtcp2_transport_params *params = conn->remote.transport_params; assert(!conn->server); @@ -10372,6 +10516,7 @@ static ngtcp2_ssize conn_write_handshake(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, size_t origlen = destlen; uint64_t pending_early_datalen; ngtcp2_preferred_addr *paddr; + ngtcp2_stateless_reset_token token; switch (conn->state) { case NGTCP2_CS_CLIENT_INITIAL: @@ -10499,8 +10644,8 @@ static ngtcp2_ssize conn_write_handshake(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, assert(!ngtcp2_dcidtr_unused_full(&conn->dcid.dtr)); paddr = &conn->remote.transport_params->preferred_addr; - ngtcp2_dcidtr_push_unused(&conn->dcid.dtr, 1, &paddr->cid, - paddr->stateless_reset_token); + memcpy(token.data, paddr->stateless_reset_token, sizeof(token.data)); + ngtcp2_dcidtr_push_unused(&conn->dcid.dtr, 1, &paddr->cid, &token); rv = ngtcp2_gaptr_push(&conn->dcid.seqgap, 1, 1); if (rv != 0) { @@ -10508,12 +10653,12 @@ static ngtcp2_ssize conn_write_handshake(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, } } - if (conn->remote.transport_params->stateless_reset_token_present) { - assert(conn->dcid.current.seq == 0); + if (conn->remote.transport_params->stateless_reset_token_present && + conn->dcid.current.seq == 0) { assert(!(conn->dcid.current.flags & NGTCP2_DCID_FLAG_TOKEN_PRESENT)); - ngtcp2_dcid_set_token( - &conn->dcid.current, - conn->remote.transport_params->stateless_reset_token); + memcpy(token.data, conn->remote.transport_params->stateless_reset_token, + sizeof(token.data)); + ngtcp2_dcid_set_token(&conn->dcid.current, &token); } rv = conn_call_activate_dcid(conn, &conn->dcid.current); @@ -10552,8 +10697,6 @@ static ngtcp2_ssize conn_write_handshake(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, } res += nwrite; - dest += nwrite; - destlen -= (size_t)nwrite; } if (res == 0) { @@ -10563,8 +10706,6 @@ static ngtcp2_ssize conn_write_handshake(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, } res += nwrite; - dest += nwrite; - origlen -= (size_t)nwrite; } return res; @@ -10623,8 +10764,8 @@ conn_client_write_handshake(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, datalen = ngtcp2_vec_len(vmsg->stream.data, vmsg->stream.datacnt); send_stream = conn_retry_early_payloadlen(conn) == 0; if (send_stream) { - write_datalen = ngtcp2_min_uint64(datalen + NGTCP2_STREAM_OVERHEAD, - NGTCP2_MIN_COALESCED_PAYLOADLEN); + write_datalen = ngtcp2_min(datalen + NGTCP2_STREAM_OVERHEAD, + NGTCP2_MIN_COALESCED_PAYLOADLEN); if (vmsg->stream.flags & NGTCP2_WRITE_STREAM_FLAG_MORE) { wflags |= NGTCP2_WRITE_PKT_FLAG_MORE; @@ -10716,6 +10857,10 @@ void ngtcp2_conn_tls_handshake_completed(ngtcp2_conn *conn) { } int ngtcp2_conn_get_handshake_completed(ngtcp2_conn *conn) { + return ngtcp2_conn_get_handshake_completed2(conn); +} + +int ngtcp2_conn_get_handshake_completed2(const ngtcp2_conn *conn) { return conn_is_tls_handshake_completed(conn) && (conn->flags & NGTCP2_CONN_FLAG_HANDSHAKE_COMPLETED); } @@ -10800,6 +10945,9 @@ int ngtcp2_conn_install_initial_key( rv = ngtcp2_crypto_km_new(&pktns->crypto.tx.ckm, NULL, 0, NULL, tx_iv, ivlen, conn->mem); if (rv != 0) { + ngtcp2_crypto_km_del(pktns->crypto.rx.ckm, conn->mem); + pktns->crypto.rx.ckm = NULL; + return rv; } @@ -10850,6 +10998,9 @@ int ngtcp2_conn_install_vneg_initial_key( rv = ngtcp2_crypto_km_new(&conn->vneg.tx.ckm, NULL, 0, NULL, tx_iv, ivlen, conn->mem); if (rv != 0) { + ngtcp2_crypto_km_del(conn->vneg.rx.ckm, conn->mem); + conn->vneg.rx.ckm = NULL; + return rv; } @@ -10887,8 +11038,7 @@ int ngtcp2_conn_install_rx_handshake_key( if (rv != 0) { ngtcp2_crypto_km_del(pktns->crypto.rx.ckm, conn->mem); pktns->crypto.rx.ckm = NULL; - - memset(&pktns->crypto.rx.hp_ctx, 0, sizeof(pktns->crypto.rx.hp_ctx)); + pktns->crypto.rx.hp_ctx = (ngtcp2_crypto_cipher_ctx){0}; return rv; } @@ -10918,21 +11068,26 @@ int ngtcp2_conn_install_tx_handshake_key( if (conn->server) { rv = ngtcp2_conn_commit_local_transport_params(conn); if (rv != 0) { - return rv; + goto fail; } } rv = conn_call_recv_tx_key(conn, NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE); if (rv != 0) { - ngtcp2_crypto_km_del(pktns->crypto.tx.ckm, conn->mem); - pktns->crypto.tx.ckm = NULL; - - memset(&pktns->crypto.tx.hp_ctx, 0, sizeof(pktns->crypto.tx.hp_ctx)); - - return rv; + goto fail; } return 0; + +fail: + /* If this function fails, aead_ctx and hp_ctx are still owned by + the caller. Delete the install key to remove the any reference + to them. */ + ngtcp2_crypto_km_del(pktns->crypto.tx.ckm, conn->mem); + pktns->crypto.tx.ckm = NULL; + pktns->crypto.tx.hp_ctx = (ngtcp2_crypto_cipher_ctx){0}; + + return rv; } int ngtcp2_conn_install_0rtt_key(ngtcp2_conn *conn, @@ -10963,8 +11118,7 @@ int ngtcp2_conn_install_0rtt_key(ngtcp2_conn *conn, if (rv != 0) { ngtcp2_crypto_km_del(conn->early.ckm, conn->mem); conn->early.ckm = NULL; - - memset(&conn->early.hp_ctx, 0, sizeof(conn->early.hp_ctx)); + conn->early.hp_ctx = (ngtcp2_crypto_cipher_ctx){0}; return rv; } @@ -11011,8 +11165,7 @@ int ngtcp2_conn_install_rx_key(ngtcp2_conn *conn, const uint8_t *secret, if (rv != 0) { ngtcp2_crypto_km_del(pktns->crypto.rx.ckm, conn->mem); pktns->crypto.rx.ckm = NULL; - - memset(&pktns->crypto.rx.hp_ctx, 0, sizeof(pktns->crypto.rx.hp_ctx)); + pktns->crypto.rx.hp_ctx = (ngtcp2_crypto_cipher_ctx){0}; return rv; } @@ -11057,8 +11210,7 @@ int ngtcp2_conn_install_tx_key(ngtcp2_conn *conn, const uint8_t *secret, if (rv != 0) { ngtcp2_crypto_km_del(pktns->crypto.tx.ckm, conn->mem); pktns->crypto.tx.ckm = NULL; - - memset(&pktns->crypto.tx.hp_ctx, 0, sizeof(pktns->crypto.tx.hp_ctx)); + pktns->crypto.tx.hp_ctx = (ngtcp2_crypto_cipher_ctx){0}; return rv; } @@ -11068,7 +11220,7 @@ int ngtcp2_conn_install_tx_key(ngtcp2_conn *conn, const uint8_t *secret, static int conn_initiate_key_update(ngtcp2_conn *conn, ngtcp2_tstamp ts) { ngtcp2_tstamp confirmed_ts = conn->crypto.key_update.confirmed_ts; - ngtcp2_duration pto = conn_compute_pto(conn, &conn->pktns); + ngtcp2_duration pto = ngtcp2_conn_compute_pto(conn, &conn->pktns); assert(conn->state == NGTCP2_CS_POST_HANDSHAKE); @@ -11091,13 +11243,13 @@ int ngtcp2_conn_initiate_key_update(ngtcp2_conn *conn, ngtcp2_tstamp ts) { return conn_initiate_key_update(conn, ts); } -ngtcp2_tstamp ngtcp2_conn_loss_detection_expiry(ngtcp2_conn *conn) { +ngtcp2_tstamp ngtcp2_conn_loss_detection_expiry(const ngtcp2_conn *conn) { return conn->cstat.loss_detection_timer; } -ngtcp2_tstamp ngtcp2_conn_internal_expiry(ngtcp2_conn *conn) { +ngtcp2_tstamp ngtcp2_conn_internal_expiry(const ngtcp2_conn *conn) { ngtcp2_tstamp res = UINT64_MAX, ts; - ngtcp2_duration pto = conn_compute_pto(conn, &conn->pktns); + ngtcp2_duration pto = ngtcp2_conn_compute_pto(conn, &conn->pktns); ngtcp2_scid *scid; if (conn->pv) { @@ -11105,38 +11257,38 @@ ngtcp2_tstamp ngtcp2_conn_internal_expiry(ngtcp2_conn *conn) { } if (conn->pmtud) { - res = ngtcp2_min_uint64(res, conn->pmtud->expiry); + res = ngtcp2_min(res, conn->pmtud->expiry); } if (!ngtcp2_pq_empty(&conn->scid.used)) { scid = ngtcp2_struct_of(ngtcp2_pq_top(&conn->scid.used), ngtcp2_scid, pe); if (scid->retired_ts != UINT64_MAX) { - res = ngtcp2_min_uint64(res, scid->retired_ts + pto); + res = ngtcp2_min(res, scid->retired_ts + pto); } } ts = ngtcp2_dcidtr_earliest_retired_ts(&conn->dcid.dtr); if (ts != UINT64_MAX) { - res = ngtcp2_min_uint64(res, ts + pto); + res = ngtcp2_min(res, ts + pto); } if (conn->dcid.current.cid.datalen) { ts = ngtcp2_dcidtr_earliest_bound_ts(&conn->dcid.dtr); if (ts != UINT64_MAX) { - res = ngtcp2_min_uint64(res, ts + 3 * pto); + res = ngtcp2_min(res, ts + 3 * pto); } } if (conn->server && conn->early.ckm && conn->early.discard_started_ts != UINT64_MAX) { - res = ngtcp2_min_uint64(res, conn->early.discard_started_ts + 3 * pto); + res = ngtcp2_min(res, conn->early.discard_started_ts + 3 * pto); } return res; } -ngtcp2_tstamp ngtcp2_conn_ack_delay_expiry(ngtcp2_conn *conn) { - ngtcp2_acktr *acktr = &conn->pktns.acktr; +ngtcp2_tstamp ngtcp2_conn_ack_delay_expiry(const ngtcp2_conn *conn) { + const ngtcp2_acktr *acktr = &conn->pktns.acktr; if (!(acktr->flags & NGTCP2_ACKTR_FLAG_CANCEL_TIMER) && acktr->first_unacked_ts != UINT64_MAX) { @@ -11145,7 +11297,7 @@ ngtcp2_tstamp ngtcp2_conn_ack_delay_expiry(ngtcp2_conn *conn) { return UINT64_MAX; } -static ngtcp2_tstamp conn_handshake_expiry(ngtcp2_conn *conn) { +static ngtcp2_tstamp conn_handshake_expiry(const ngtcp2_conn *conn) { if (conn_is_tls_handshake_completed(conn) || conn->local.settings.handshake_timeout == UINT64_MAX || conn->local.settings.initial_ts >= @@ -11158,14 +11310,18 @@ static ngtcp2_tstamp conn_handshake_expiry(ngtcp2_conn *conn) { } ngtcp2_tstamp ngtcp2_conn_get_expiry(ngtcp2_conn *conn) { - ngtcp2_tstamp res = ngtcp2_min_uint64(ngtcp2_conn_loss_detection_expiry(conn), - ngtcp2_conn_ack_delay_expiry(conn)); - res = ngtcp2_min_uint64(res, ngtcp2_conn_internal_expiry(conn)); - res = ngtcp2_min_uint64(res, ngtcp2_conn_lost_pkt_expiry(conn)); - res = ngtcp2_min_uint64(res, conn_keep_alive_expiry(conn)); - res = ngtcp2_min_uint64(res, conn_handshake_expiry(conn)); - res = ngtcp2_min_uint64(res, ngtcp2_conn_get_idle_expiry(conn)); - return ngtcp2_min_uint64(res, conn->tx.pacing.next_ts); + return ngtcp2_conn_get_expiry2(conn); +} + +ngtcp2_tstamp ngtcp2_conn_get_expiry2(const ngtcp2_conn *conn) { + ngtcp2_tstamp res = ngtcp2_min(ngtcp2_conn_loss_detection_expiry(conn), + ngtcp2_conn_ack_delay_expiry(conn)); + res = ngtcp2_min(res, ngtcp2_conn_internal_expiry(conn)); + res = ngtcp2_min(res, ngtcp2_conn_lost_pkt_expiry(conn)); + res = ngtcp2_min(res, conn_keep_alive_expiry(conn)); + res = ngtcp2_min(res, conn_handshake_expiry(conn)); + res = ngtcp2_min(res, ngtcp2_conn_get_idle_expiry(conn)); + return ngtcp2_min(res, conn->tx.pacing.next_ts); } int ngtcp2_conn_handle_expiry(ngtcp2_conn *conn, ngtcp2_tstamp ts) { @@ -11174,7 +11330,7 @@ int ngtcp2_conn_handle_expiry(ngtcp2_conn *conn, ngtcp2_tstamp ts) { conn_update_timestamp(conn, ts); - pto = conn_compute_pto(conn, &conn->pktns); + pto = ngtcp2_conn_compute_pto(conn, &conn->pktns); assert(!(conn->flags & NGTCP2_CONN_FLAG_PPE_PENDING)); @@ -11257,29 +11413,29 @@ void ngtcp2_conn_cancel_expired_ack_delay_timer(ngtcp2_conn *conn, acktr_cancel_expired_ack_delay_timer(&conn->pktns.acktr, ack_delay, ts); } -ngtcp2_tstamp ngtcp2_conn_lost_pkt_expiry(ngtcp2_conn *conn) { +ngtcp2_tstamp ngtcp2_conn_lost_pkt_expiry(const ngtcp2_conn *conn) { ngtcp2_tstamp res = UINT64_MAX, ts; if (conn->in_pktns) { ts = ngtcp2_rtb_lost_pkt_ts(&conn->in_pktns->rtb); if (ts != UINT64_MAX) { - ts += conn_compute_pto(conn, conn->in_pktns) * 3; - res = ngtcp2_min_uint64(res, ts); + ts += ngtcp2_conn_compute_pto(conn, conn->in_pktns) * 3; + res = ngtcp2_min(res, ts); } } if (conn->hs_pktns) { ts = ngtcp2_rtb_lost_pkt_ts(&conn->hs_pktns->rtb); if (ts != UINT64_MAX) { - ts += conn_compute_pto(conn, conn->hs_pktns) * 3; - res = ngtcp2_min_uint64(res, ts); + ts += ngtcp2_conn_compute_pto(conn, conn->hs_pktns) * 3; + res = ngtcp2_min(res, ts); } } ts = ngtcp2_rtb_lost_pkt_ts(&conn->pktns.rtb); if (ts != UINT64_MAX) { - ts += conn_compute_pto(conn, &conn->pktns) * 3; - res = ngtcp2_min_uint64(res, ts); + ts += ngtcp2_conn_compute_pto(conn, &conn->pktns) * 3; + res = ngtcp2_min(res, ts); } return res; @@ -11289,14 +11445,14 @@ void ngtcp2_conn_remove_lost_pkt(ngtcp2_conn *conn, ngtcp2_tstamp ts) { ngtcp2_duration timeout; if (conn->in_pktns) { - timeout = conn_compute_pto(conn, conn->in_pktns) * 3; + timeout = ngtcp2_conn_compute_pto(conn, conn->in_pktns) * 3; ngtcp2_rtb_remove_expired_lost_pkt(&conn->in_pktns->rtb, timeout, ts); } if (conn->hs_pktns) { - timeout = conn_compute_pto(conn, conn->hs_pktns) * 3; + timeout = ngtcp2_conn_compute_pto(conn, conn->hs_pktns) * 3; ngtcp2_rtb_remove_expired_lost_pkt(&conn->hs_pktns->rtb, timeout, ts); } - timeout = conn_compute_pto(conn, &conn->pktns) * 3; + timeout = ngtcp2_conn_compute_pto(conn, &conn->pktns) * 3; ngtcp2_rtb_remove_expired_lost_pkt(&conn->pktns.rtb, timeout, ts); } @@ -11353,7 +11509,7 @@ static uint32_t select_preferred_version(const uint32_t *preferred_versions, * Validation against version negotiation parameters failed. */ static int -conn_client_validate_transport_params(ngtcp2_conn *conn, +conn_client_validate_transport_params(const ngtcp2_conn *conn, const ngtcp2_transport_params *params) { if (!params->original_dcid_present) { return NGTCP2_ERR_REQUIRED_TRANSPORT_PARAM; @@ -11516,9 +11672,9 @@ int ngtcp2_conn_set_remote_transport_params( conn->local.transport_params.version_info.chosen_version = conn->negotiated_version; - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, - "the negotiated version is 0x%08x", - conn->negotiated_version); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_CON, + "the negotiated version is 0x", + hexw(conn->negotiated_version, 8)); } else { rv = conn_client_validate_transport_params(conn, params); if (rv != 0) { @@ -11574,6 +11730,11 @@ int ngtcp2_conn_decode_and_set_remote_transport_params(ngtcp2_conn *conn, const ngtcp2_transport_params * ngtcp2_conn_get_remote_transport_params(ngtcp2_conn *conn) { + return ngtcp2_conn_get_remote_transport_params2(conn); +} + +const ngtcp2_transport_params * +ngtcp2_conn_get_remote_transport_params2(const ngtcp2_conn *conn) { if (conn->remote.pending_transport_params) { return conn->remote.pending_transport_params; } @@ -11584,7 +11745,14 @@ ngtcp2_conn_get_remote_transport_params(ngtcp2_conn *conn) { ngtcp2_ssize ngtcp2_conn_encode_0rtt_transport_params(ngtcp2_conn *conn, uint8_t *dest, size_t destlen) { - ngtcp2_transport_params params, *src; + return ngtcp2_conn_encode_0rtt_transport_params2(conn, dest, destlen); +} + +ngtcp2_ssize ngtcp2_conn_encode_0rtt_transport_params2(const ngtcp2_conn *conn, + uint8_t *dest, + size_t destlen) { + ngtcp2_transport_params params; + const ngtcp2_transport_params *src; if (conn->server) { src = &conn->local.transport_params; @@ -11657,34 +11825,31 @@ int ngtcp2_conn_set_0rtt_remote_transport_params( p->initial_max_data = params->initial_max_data; /* we might hit garbage, then set the sane default. */ p->active_connection_id_limit = - ngtcp2_max_uint64(NGTCP2_DEFAULT_ACTIVE_CONNECTION_ID_LIMIT, - params->active_connection_id_limit); + ngtcp2_max(NGTCP2_DEFAULT_ACTIVE_CONNECTION_ID_LIMIT, + params->active_connection_id_limit); p->max_datagram_frame_size = params->max_datagram_frame_size; /* we might hit garbage, then set the sane default. */ if (params->max_udp_payload_size) { - p->max_udp_payload_size = ngtcp2_max_uint64(NGTCP2_MAX_UDP_PAYLOAD_SIZE, - params->max_udp_payload_size); + p->max_udp_payload_size = + ngtcp2_max(NGTCP2_MAX_UDP_PAYLOAD_SIZE, params->max_udp_payload_size); } /* These parameters are treated specially. If server accepts early data, it must not set values for these parameters that are smaller than these remembered values. */ - conn->early.transport_params.initial_max_streams_bidi = - params->initial_max_streams_bidi; - conn->early.transport_params.initial_max_streams_uni = - params->initial_max_streams_uni; - conn->early.transport_params.initial_max_stream_data_bidi_local = - params->initial_max_stream_data_bidi_local; - conn->early.transport_params.initial_max_stream_data_bidi_remote = - params->initial_max_stream_data_bidi_remote; - conn->early.transport_params.initial_max_stream_data_uni = - params->initial_max_stream_data_uni; - conn->early.transport_params.initial_max_data = params->initial_max_data; - conn->early.transport_params.active_connection_id_limit = - params->active_connection_id_limit; - conn->early.transport_params.max_datagram_frame_size = - params->max_datagram_frame_size; + conn->early.transport_params = (ngtcp2_early_transport_params){ + .initial_max_streams_bidi = params->initial_max_streams_bidi, + .initial_max_streams_uni = params->initial_max_streams_uni, + .initial_max_stream_data_bidi_local = + params->initial_max_stream_data_bidi_local, + .initial_max_stream_data_bidi_remote = + params->initial_max_stream_data_bidi_remote, + .initial_max_stream_data_uni = params->initial_max_stream_data_uni, + .initial_max_data = params->initial_max_data, + .active_connection_id_limit = params->active_connection_id_limit, + .max_datagram_frame_size = params->max_datagram_frame_size, + }; conn_sync_stream_id_limit(conn); @@ -11768,12 +11933,23 @@ int ngtcp2_conn_commit_local_transport_params(ngtcp2_conn *conn) { const ngtcp2_transport_params * ngtcp2_conn_get_local_transport_params(ngtcp2_conn *conn) { + return ngtcp2_conn_get_local_transport_params2(conn); +} + +const ngtcp2_transport_params * +ngtcp2_conn_get_local_transport_params2(const ngtcp2_conn *conn) { return &conn->local.transport_params; } ngtcp2_ssize ngtcp2_conn_encode_local_transport_params(ngtcp2_conn *conn, uint8_t *dest, size_t destlen) { + return ngtcp2_conn_encode_local_transport_params2(conn, dest, destlen); +} + +ngtcp2_ssize ngtcp2_conn_encode_local_transport_params2(const ngtcp2_conn *conn, + uint8_t *dest, + size_t destlen) { return ngtcp2_transport_params_encode(dest, destlen, &conn->local.transport_params); } @@ -11783,7 +11959,7 @@ int ngtcp2_conn_open_bidi_stream(ngtcp2_conn *conn, int64_t *pstream_id, int rv; ngtcp2_strm *strm; - if (ngtcp2_conn_get_streams_bidi_left(conn) == 0) { + if (ngtcp2_conn_get_streams_bidi_left2(conn) == 0) { return NGTCP2_ERR_STREAM_ID_BLOCKED; } @@ -11810,7 +11986,7 @@ int ngtcp2_conn_open_uni_stream(ngtcp2_conn *conn, int64_t *pstream_id, int rv; ngtcp2_strm *strm; - if (ngtcp2_conn_get_streams_uni_left(conn) == 0) { + if (ngtcp2_conn_get_streams_uni_left2(conn) == 0) { return NGTCP2_ERR_STREAM_ID_BLOCKED; } @@ -11825,15 +12001,14 @@ int ngtcp2_conn_open_uni_stream(ngtcp2_conn *conn, int64_t *pstream_id, ngtcp2_objalloc_strm_release(&conn->strm_objalloc, strm); return rv; } - ngtcp2_strm_shutdown(strm, NGTCP2_STRM_FLAG_SHUT_RD); - *pstream_id = conn->local.uni.next_stream_id; conn->local.uni.next_stream_id += 4; return 0; } -ngtcp2_strm *ngtcp2_conn_find_stream(ngtcp2_conn *conn, int64_t stream_id) { +ngtcp2_strm *ngtcp2_conn_find_stream(const ngtcp2_conn *conn, + int64_t stream_id) { return ngtcp2_map_find(&conn->strms, (uint64_t)stream_id); } @@ -11874,6 +12049,8 @@ conn_write_vmsg_wrapper(ngtcp2_conn *conn, ngtcp2_path *path, return nwrite; } + assert((size_t)nwrite <= destlen); + if (cstat->bytes_in_flight >= cstat->cwnd) { conn->rst.is_cwnd_limited = 1; } else if ((cstat->cwnd >= cstat->ssthresh || @@ -11884,11 +12061,8 @@ conn_write_vmsg_wrapper(ngtcp2_conn *conn, ngtcp2_path *path, packet is produced, if it is set, we are sure that we are not app-limited. */ !(conn->flags & NGTCP2_CONN_FLAG_AGGREGATE_PKTS)) { - conn->rst.app_limited = conn->rst.delivered + cstat->bytes_in_flight; - - if (conn->rst.app_limited == 0) { - conn->rst.app_limited = cstat->max_tx_udp_payload_size; - } + conn->rst.app_limited = + ngtcp2_max(conn->rst.delivered + cstat->bytes_in_flight, 1); } return nwrite; @@ -11932,12 +12106,17 @@ ngtcp2_ssize ngtcp2_conn_writev_stream_versioned( return NGTCP2_ERR_INVALID_ARGUMENT; } - vmsg.type = NGTCP2_VMSG_TYPE_STREAM; - vmsg.stream.strm = strm; - vmsg.stream.flags = flags; - vmsg.stream.data = datav; - vmsg.stream.datacnt = datavcnt; - vmsg.stream.pdatalen = pdatalen; + vmsg = (ngtcp2_vmsg){ + .type = NGTCP2_VMSG_TYPE_STREAM, + .stream = + { + .strm = strm, + .data = datav, + .datacnt = datavcnt, + .pdatalen = pdatalen, + .flags = flags, + }, + }; pvmsg = &vmsg; } @@ -12010,12 +12189,17 @@ ngtcp2_ssize ngtcp2_conn_writev_datagram_versioned( return NGTCP2_ERR_INVALID_ARGUMENT; } - vmsg.type = NGTCP2_VMSG_TYPE_DATAGRAM; - vmsg.datagram.dgram_id = dgram_id; - vmsg.datagram.flags = flags; - vmsg.datagram.data = datav; - vmsg.datagram.datacnt = datavcnt; - vmsg.datagram.paccepted = paccepted; + vmsg = (ngtcp2_vmsg){ + .type = NGTCP2_VMSG_TYPE_DATAGRAM, + .datagram = + { + .data = datav, + .datacnt = datavcnt, + .dgram_id = dgram_id, + .paccepted = paccepted, + .flags = flags, + }, + }; if (flags & NGTCP2_WRITE_DATAGRAM_FLAG_PADDING) { wflags = NGTCP2_WRITE_PKT_FLAG_PADDING_IF_NOT_EMPTY; @@ -12071,27 +12255,29 @@ ngtcp2_ssize ngtcp2_conn_write_vmsg(ngtcp2_conn *conn, ngtcp2_path *path, conn_client_write_handshake(conn, pi, dest, destlen, wflags, vmsg, ts); /* We might be unable to write a packet because of depletion of congestion window budget, perhaps due to packet loss that - shrinks the window drastically. */ - if (nwrite <= 0) { + shrinks the window drastically. Then continue if we are in + post-handshake. There, we might be able to write packets + exceeding CWND to avoid deadlock. */ + if (nwrite < 0) { return nwrite; } if (conn->state != NGTCP2_CS_POST_HANDSHAKE) { return nwrite; } - assert(nwrite); - assert(dest[0] & NGTCP2_HEADER_FORM_BIT); - assert(conn->negotiated_version); + if (nwrite) { + assert(dest[0] & NGTCP2_HEADER_FORM_BIT); + assert(conn->negotiated_version); - if (nwrite < NGTCP2_MAX_UDP_PAYLOAD_SIZE && - ngtcp2_pkt_get_type_long(conn->negotiated_version, dest[0]) == + if (ngtcp2_pkt_get_type_long(conn->negotiated_version, dest[0]) == NGTCP2_PKT_INITIAL) { - wflags |= NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING; - } + wflags |= NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING; + } - res = nwrite; - dest += nwrite; - destlen -= (size_t)nwrite; + res = nwrite; + dest += nwrite; + destlen -= (size_t)nwrite; + } /* Break here so that we can coalesces 1RTT packet. */ break; case NGTCP2_CS_SERVER_INITIAL: @@ -12105,7 +12291,7 @@ ngtcp2_ssize ngtcp2_conn_write_vmsg(ngtcp2_conn *conn, ngtcp2_path *path, return 0; } - origlen = (size_t)ngtcp2_min_uint64((uint64_t)origlen, server_tx_left); + origlen = (size_t)ngtcp2_min((uint64_t)origlen, server_tx_left); } return conn_write_handshake_ack_pkts(conn, pi, dest, origlen, ts); @@ -12125,7 +12311,7 @@ ngtcp2_ssize ngtcp2_conn_write_vmsg(ngtcp2_conn *conn, ngtcp2_path *path, return 0; } - destlen = (size_t)ngtcp2_min_uint64((uint64_t)destlen, server_tx_left); + destlen = (size_t)ngtcp2_min((uint64_t)destlen, server_tx_left); } if (conn->in_pktns) { @@ -12146,7 +12332,7 @@ ngtcp2_ssize ngtcp2_conn_write_vmsg(ngtcp2_conn *conn, ngtcp2_path *path, dest += nwrite; destlen -= (size_t)nwrite; - if (res < NGTCP2_MAX_UDP_PAYLOAD_SIZE && conn->in_pktns && nwrite > 0) { + if (conn->in_pktns && nwrite > 0) { it = ngtcp2_rtb_head(&conn->in_pktns->rtb); if (!ngtcp2_ksl_it_end(&it)) { rtbent = ngtcp2_ksl_it_get(&it); @@ -12172,7 +12358,7 @@ ngtcp2_ssize ngtcp2_conn_write_vmsg(ngtcp2_conn *conn, ngtcp2_path *path, return 0; } - origlen = (size_t)ngtcp2_min_uint64((uint64_t)origlen, server_tx_left); + origlen = (size_t)ngtcp2_min((uint64_t)origlen, server_tx_left); } return conn_write_ack_pkt(conn, pi, dest, origlen, NGTCP2_PKT_1RTT, ts); @@ -12261,8 +12447,8 @@ ngtcp2_ssize ngtcp2_conn_write_vmsg(ngtcp2_conn *conn, ngtcp2_path *path, if (conn->server && !(conn->dcid.current.flags & NGTCP2_DCID_FLAG_PATH_VALIDATED)) { server_tx_left = conn_server_tx_left(conn, &conn->dcid.current); - origlen = (size_t)ngtcp2_min_uint64((uint64_t)origlen, server_tx_left); - destlen = (size_t)ngtcp2_min_uint64((uint64_t)destlen, server_tx_left); + origlen = (size_t)ngtcp2_min((uint64_t)origlen, server_tx_left); + destlen = (size_t)ngtcp2_min((uint64_t)destlen, server_tx_left); if (server_tx_left == 0 && conn->cstat.loss_detection_timer != UINT64_MAX) { @@ -12293,9 +12479,22 @@ ngtcp2_ssize ngtcp2_conn_write_vmsg(ngtcp2_conn *conn, ngtcp2_path *path, return nwrite; } if (nwrite > 0) { + /* This makes 1RTT packet padded. If 1RTT packet is not going + to be sent, packet is already padded. */ + if (ngtcp2_pkt_get_type_long(conn->negotiated_version, dest[0]) == + NGTCP2_PKT_INITIAL) { + wflags |= NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING; + } + res = nwrite; dest += nwrite; destlen -= (size_t)nwrite; + + /* We only exceed CWND to avoid deadlock. Do no write 1RTT + packet if CWND is depleted. */ + if (conn_cwnd_is_zero(conn) && conn->pktns.rtb.probe_pkt_left == 0) { + return res; + } } else if (destlen == 0) { res = conn_write_handshake_ack_pkts(conn, pi, dest, origlen, ts); if (res) { @@ -12306,9 +12505,9 @@ ngtcp2_ssize ngtcp2_conn_write_vmsg(ngtcp2_conn *conn, ngtcp2_path *path, } if (conn->pktns.rtb.probe_pkt_left) { - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, - "transmit probe pkt left=%zu", - conn->pktns.rtb.probe_pkt_left); + ngtcp2_log_infof( + &conn->log, NGTCP2_LOG_EVENT_CON, + "transmit probe pkt left=", conn->pktns.rtb.probe_pkt_left); nwrite = conn_write_pkt(conn, pi, dest, destlen, (size_t)res, vmsg, NGTCP2_PKT_1RTT, wflags, ts); @@ -12362,11 +12561,12 @@ conn_write_connection_close(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, ngtcp2_frame fr; uint8_t flags = NGTCP2_WRITE_PKT_FLAG_NONE; - fr.type = NGTCP2_FRAME_CONNECTION_CLOSE; - fr.connection_close.error_code = error_code; - fr.connection_close.frame_type = 0; - fr.connection_close.reasonlen = reasonlen; - fr.connection_close.reason = (uint8_t *)reason; + fr.connection_close = (ngtcp2_connection_close){ + .type = NGTCP2_FRAME_CONNECTION_CLOSE, + .error_code = error_code, + .reasonlen = reasonlen, + .reason = (uint8_t *)reason, + }; if (!(conn->flags & NGTCP2_CONN_FLAG_HANDSHAKE_CONFIRMED) && pkt_type != NGTCP2_PKT_INITIAL) { @@ -12456,7 +12656,7 @@ ngtcp2_ssize ngtcp2_conn_write_connection_close_pkt( if (conn->server) { server_tx_left = conn_server_tx_left(conn, &conn->dcid.current); - destlen = (size_t)ngtcp2_min_uint64((uint64_t)destlen, server_tx_left); + destlen = (size_t)ngtcp2_min((uint64_t)destlen, server_tx_left); } if (conn->state == NGTCP2_CS_POST_HANDSHAKE || @@ -12518,7 +12718,7 @@ ngtcp2_ssize ngtcp2_conn_write_application_close_pkt( if (conn->server) { server_tx_left = conn_server_tx_left(conn, &conn->dcid.current); - destlen = (size_t)ngtcp2_min_uint64((uint64_t)destlen, server_tx_left); + destlen = (size_t)ngtcp2_min((uint64_t)destlen, server_tx_left); } if (!(conn->flags & NGTCP2_CONN_FLAG_HANDSHAKE_CONFIRMED)) { @@ -12536,16 +12736,17 @@ ngtcp2_ssize ngtcp2_conn_write_application_close_pkt( if (conn->state != NGTCP2_CS_POST_HANDSHAKE && (!conn->server || !conn->pktns.crypto.tx.ckm)) { - return res; + goto fin; } assert(conn->pktns.crypto.tx.ckm); - fr.type = NGTCP2_FRAME_CONNECTION_CLOSE_APP; - fr.connection_close.error_code = app_error_code; - fr.connection_close.frame_type = 0; - fr.connection_close.reasonlen = reasonlen; - fr.connection_close.reason = (uint8_t *)reason; + fr.connection_close = (ngtcp2_connection_close){ + .type = NGTCP2_FRAME_CONNECTION_CLOSE_APP, + .error_code = app_error_code, + .reasonlen = reasonlen, + .reason = (uint8_t *)reason, + }; nwrite = ngtcp2_conn_write_single_frame_pkt( conn, pi, dest, destlen, NGTCP2_PKT_1RTT, NGTCP2_WRITE_PKT_FLAG_NONE, @@ -12561,6 +12762,7 @@ ngtcp2_ssize ngtcp2_conn_write_application_close_pkt( return NGTCP2_ERR_NOBUF; } +fin: conn->state = NGTCP2_CS_CLOSING; return res; @@ -12569,11 +12771,12 @@ ngtcp2_ssize ngtcp2_conn_write_application_close_pkt( static void ccerr_init(ngtcp2_ccerr *ccerr, ngtcp2_ccerr_type type, uint64_t error_code, const uint8_t *reason, size_t reasonlen) { - ccerr->type = type; - ccerr->error_code = error_code; - ccerr->frame_type = 0; - ccerr->reason = (uint8_t *)reason; - ccerr->reasonlen = reasonlen; + *ccerr = (ngtcp2_ccerr){ + .type = type, + .error_code = error_code, + .reason = (uint8_t *)reason, + .reasonlen = reasonlen, + }; } void ngtcp2_ccerr_default(ngtcp2_ccerr *ccerr) { @@ -12608,7 +12811,7 @@ void ngtcp2_ccerr_set_liberr(ngtcp2_ccerr *ccerr, int liberr, reasonlen); return; - }; + } ngtcp2_ccerr_set_transport_error( ccerr, ngtcp2_err_infer_quic_transport_error_code(liberr), reason, @@ -12652,10 +12855,18 @@ ngtcp2_ssize ngtcp2_conn_write_connection_close_versioned( } int ngtcp2_conn_in_closing_period(ngtcp2_conn *conn) { + return ngtcp2_conn_in_closing_period2(conn); +} + +int ngtcp2_conn_in_closing_period2(const ngtcp2_conn *conn) { return conn->state == NGTCP2_CS_CLOSING; } int ngtcp2_conn_in_draining_period(ngtcp2_conn *conn) { + return ngtcp2_conn_in_draining_period2(conn); +} + +int ngtcp2_conn_in_draining_period2(const ngtcp2_conn *conn) { return conn->state == NGTCP2_CS_DRAINING; } @@ -12855,7 +13066,7 @@ static int conn_extend_max_stream_offset(ngtcp2_conn *conn, ngtcp2_strm *strm, top = ngtcp2_conn_tx_strmq_top(conn); strm->cycle = top->cycle; } - strm->cycle = conn_tx_strmq_first_cycle(conn); + strm->cycle = ngtcp2_conn_tx_strmq_first_cycle(conn); return ngtcp2_conn_tx_strmq_push(conn, strm); } @@ -12897,18 +13108,35 @@ void ngtcp2_conn_extend_max_streams_uni(ngtcp2_conn *conn, size_t n) { } const ngtcp2_cid *ngtcp2_conn_get_dcid(ngtcp2_conn *conn) { + return ngtcp2_conn_get_dcid2(conn); +} + +const ngtcp2_cid *ngtcp2_conn_get_dcid2(const ngtcp2_conn *conn) { return &conn->dcid.current.cid; } const ngtcp2_cid *ngtcp2_conn_get_client_initial_dcid(ngtcp2_conn *conn) { + return ngtcp2_conn_get_client_initial_dcid2(conn); +} + +const ngtcp2_cid * +ngtcp2_conn_get_client_initial_dcid2(const ngtcp2_conn *conn) { return &conn->rcid; } uint32_t ngtcp2_conn_get_client_chosen_version(ngtcp2_conn *conn) { + return ngtcp2_conn_get_client_chosen_version2(conn); +} + +uint32_t ngtcp2_conn_get_client_chosen_version2(const ngtcp2_conn *conn) { return conn->client_chosen_version; } uint32_t ngtcp2_conn_get_negotiated_version(ngtcp2_conn *conn) { + return ngtcp2_conn_get_negotiated_version2(conn); +} + +uint32_t ngtcp2_conn_get_negotiated_version2(const ngtcp2_conn *conn) { return conn->negotiated_version; } @@ -12986,11 +13214,15 @@ int ngtcp2_conn_tls_early_data_rejected(ngtcp2_conn *conn) { } int ngtcp2_conn_get_tls_early_data_rejected(ngtcp2_conn *conn) { + return ngtcp2_conn_get_tls_early_data_rejected2(conn); +} + +int ngtcp2_conn_get_tls_early_data_rejected2(const ngtcp2_conn *conn) { return (conn->flags & NGTCP2_CONN_FLAG_EARLY_DATA_REJECTED) != 0; } -int ngtcp2_conn_update_rtt(ngtcp2_conn *conn, ngtcp2_duration rtt, - ngtcp2_duration ack_delay, ngtcp2_tstamp ts) { +void ngtcp2_conn_update_rtt(ngtcp2_conn *conn, ngtcp2_duration rtt, + ngtcp2_duration ack_delay, ngtcp2_tstamp ts) { ngtcp2_conn_stat *cstat = &conn->cstat; assert(rtt > 0); @@ -13005,23 +13237,23 @@ int ngtcp2_conn_update_rtt(ngtcp2_conn *conn, ngtcp2_duration rtt, if (conn->flags & NGTCP2_CONN_FLAG_HANDSHAKE_CONFIRMED) { assert(conn->remote.transport_params); - ack_delay = ngtcp2_min_uint64( - ack_delay, conn->remote.transport_params->max_ack_delay); + ack_delay = + ngtcp2_min(ack_delay, conn->remote.transport_params->max_ack_delay); } else if (ack_delay > 0 && rtt >= cstat->min_rtt && rtt < cstat->min_rtt + ack_delay) { /* Ignore RTT sample if adjusting ack_delay causes the sample less than min_rtt before handshake confirmation. */ - ngtcp2_log_info( + ngtcp2_log_infof( &conn->log, NGTCP2_LOG_EVENT_LDC, - "ignore rtt sample because ack_delay is too large latest_rtt=%" PRIu64 - " min_rtt=%" PRIu64 " ack_delay=%" PRIu64, - rtt / NGTCP2_MILLISECONDS, cstat->min_rtt / NGTCP2_MILLISECONDS, - ack_delay / NGTCP2_MILLISECONDS); - return NGTCP2_ERR_INVALID_ARGUMENT; + "ignore rtt sample because ack_delay is too large latest_rtt=", + rtt / NGTCP2_MILLISECONDS, + " min_rtt=", cstat->min_rtt / NGTCP2_MILLISECONDS, + " ack_delay=", ack_delay / NGTCP2_MILLISECONDS); + return; } cstat->latest_rtt = rtt; - cstat->min_rtt = ngtcp2_min_uint64(cstat->min_rtt, rtt); + cstat->min_rtt = ngtcp2_min(cstat->min_rtt, rtt); if (rtt >= cstat->min_rtt + ack_delay) { rtt -= ack_delay; @@ -13034,31 +13266,24 @@ int ngtcp2_conn_update_rtt(ngtcp2_conn *conn, ngtcp2_duration rtt, cstat->smoothed_rtt = (cstat->smoothed_rtt * 7 + rtt) / 8; } - ngtcp2_log_info( - &conn->log, NGTCP2_LOG_EVENT_LDC, - "latest_rtt=%" PRIu64 " min_rtt=%" PRIu64 " smoothed_rtt=%" PRIu64 - " rttvar=%" PRIu64 " ack_delay=%" PRIu64, - cstat->latest_rtt / NGTCP2_MILLISECONDS, - cstat->min_rtt / NGTCP2_MILLISECONDS, - cstat->smoothed_rtt / NGTCP2_MILLISECONDS, - cstat->rttvar / NGTCP2_MILLISECONDS, ack_delay / NGTCP2_MILLISECONDS); - - return 0; + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_LDC, + "latest_rtt=", cstat->latest_rtt / NGTCP2_MILLISECONDS, + " min_rtt=", cstat->min_rtt / NGTCP2_MILLISECONDS, + " smoothed_rtt=", cstat->smoothed_rtt / NGTCP2_MILLISECONDS, + " rttvar=", cstat->rttvar / NGTCP2_MILLISECONDS, + " ack_delay=", ack_delay / NGTCP2_MILLISECONDS); } void ngtcp2_conn_get_conn_info_versioned(ngtcp2_conn *conn, int conn_info_version, ngtcp2_conn_info *cinfo) { - const ngtcp2_conn_stat *cstat = &conn->cstat; - (void)conn_info_version; + ngtcp2_conn_get_conn_info2_versioned(conn, conn_info_version, cinfo); +} - cinfo->latest_rtt = cstat->latest_rtt; - cinfo->min_rtt = cstat->min_rtt; - cinfo->smoothed_rtt = cstat->smoothed_rtt; - cinfo->rttvar = cstat->rttvar; - cinfo->cwnd = cstat->cwnd; - cinfo->ssthresh = cstat->ssthresh; - cinfo->bytes_in_flight = cstat->bytes_in_flight; +void ngtcp2_conn_get_conn_info2_versioned(const ngtcp2_conn *conn, + int conn_info_version, + ngtcp2_conn_info *cinfo) { + ngtcp2_conn_info_init_versioned(conn_info_version, cinfo, &conn->cstat); } static void conn_get_loss_time_and_pktns(ngtcp2_conn *conn, @@ -13088,13 +13313,14 @@ static void conn_get_loss_time_and_pktns(ngtcp2_conn *conn, } } -static ngtcp2_tstamp conn_get_earliest_pto_expiry(ngtcp2_conn *conn, +static ngtcp2_tstamp conn_get_earliest_pto_expiry(const ngtcp2_conn *conn, ngtcp2_tstamp ts) { - ngtcp2_pktns *const ns[] = {conn->in_pktns, conn->hs_pktns, &conn->pktns}; + const ngtcp2_pktns *const ns[] = {conn->in_pktns, conn->hs_pktns, + &conn->pktns}; size_t i; ngtcp2_tstamp earliest_ts = UINT64_MAX, t; - ngtcp2_conn_stat *cstat = &conn->cstat; - ngtcp2_tstamp *times = cstat->last_tx_pkt_ts; + const ngtcp2_conn_stat *cstat = &conn->cstat; + const ngtcp2_tstamp *times = cstat->last_tx_pkt_ts; ngtcp2_duration duration = compute_pto(cstat->smoothed_rtt, cstat->rttvar, /* max_ack_delay = */ 0) * (1ULL << cstat->pto_count); @@ -13140,9 +13366,9 @@ void ngtcp2_conn_set_loss_detection_timer(ngtcp2_conn *conn, ngtcp2_tstamp ts) { if (earliest_loss_time != UINT64_MAX) { cstat->loss_detection_timer = earliest_loss_time; - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_LDC, - "loss_detection_timer=%" PRIu64 " nonzero crypto loss time", - cstat->loss_detection_timer); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_LDC, + "loss_detection_timer=", cstat->loss_detection_timer, + " nonzero crypto loss time"); return; } @@ -13166,9 +13392,9 @@ void ngtcp2_conn_set_loss_detection_timer(ngtcp2_conn *conn, ngtcp2_tstamp ts) { timeout = cstat->loss_detection_timer > ts ? cstat->loss_detection_timer - ts : 0; - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_LDC, - "loss_detection_timer=%" PRIu64 " timeout=%" PRIu64, - cstat->loss_detection_timer, timeout / NGTCP2_MILLISECONDS); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_LDC, + "loss_detection_timer=", cstat->loss_detection_timer, + " timeout=", timeout / NGTCP2_MILLISECONDS); } void ngtcp2_conn_cancel_loss_detection_timer(ngtcp2_conn *conn) { @@ -13240,8 +13466,8 @@ int ngtcp2_conn_on_loss_detection_timer(ngtcp2_conn *conn, ngtcp2_tstamp ts) { ++cstat->pto_count; - ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_LDC, "pto_count=%zu", - cstat->pto_count); + ngtcp2_log_infof(&conn->log, NGTCP2_LOG_EVENT_LDC, + "pto_count=", cstat->pto_count); ngtcp2_conn_set_loss_detection_timer(conn, ts); @@ -13264,8 +13490,7 @@ static int conn_buffer_crypto_data(ngtcp2_conn *conn, const uint8_t **pdata, } if (!*pbufchain) { - rv = ngtcp2_buf_chain_new(pbufchain, ngtcp2_max_size(1024, datalen), - conn->mem); + rv = ngtcp2_buf_chain_new(pbufchain, ngtcp2_max(1024, datalen), conn->mem); if (rv != 0) { return rv; } @@ -13282,7 +13507,6 @@ int ngtcp2_conn_submit_crypto_data(ngtcp2_conn *conn, const uint8_t *data, const size_t datalen) { ngtcp2_pktns *pktns; ngtcp2_frame_chain *frc; - ngtcp2_stream *fr; int rv; if (datalen == 0) { @@ -13310,21 +13534,22 @@ int ngtcp2_conn_submit_crypto_data(ngtcp2_conn *conn, return rv; } - rv = ngtcp2_frame_chain_objalloc_new(&frc, &conn->frc_objalloc); + rv = ngtcp2_frame_chain_stream_datacnt_objalloc_new( + &frc, 1, &conn->frc_objalloc, conn->mem); if (rv != 0) { return rv; } - fr = &frc->fr.stream; - - fr->type = NGTCP2_FRAME_CRYPTO; - fr->flags = 0; - fr->fin = 0; - fr->stream_id = 0; - fr->offset = pktns->crypto.tx.offset; - fr->datacnt = 1; - fr->data[0].len = datalen; - fr->data[0].base = (uint8_t *)data; + frc->fr.stream.type = NGTCP2_FRAME_CRYPTO; + frc->fr.stream.flags = 0; + frc->fr.stream.fin = 0; + frc->fr.stream.stream_id = 0; + frc->fr.stream.offset = pktns->crypto.tx.offset; + frc->fr.stream.datacnt = 1; + frc->fr.stream.data[0] = (ngtcp2_vec){ + .base = (uint8_t *)data, + .len = datalen, + }; rv = ngtcp2_strm_streamfrq_push(&pktns->crypto.strm, frc); if (rv != 0) { @@ -13375,7 +13600,7 @@ int ngtcp2_conn_tx_strmq_push(ngtcp2_conn *conn, ngtcp2_strm *strm) { return ngtcp2_pq_push(&conn->tx.strmq, &strm->pe); } -static int conn_has_uncommitted_preferred_addr_cid(ngtcp2_conn *conn) { +static int conn_has_uncommitted_preferred_addr_cid(const ngtcp2_conn *conn) { return conn->server && !(conn->flags & NGTCP2_CONN_FLAG_LOCAL_TRANSPORT_PARAMS_COMMITTED) && conn->oscid.datalen && @@ -13383,6 +13608,10 @@ static int conn_has_uncommitted_preferred_addr_cid(ngtcp2_conn *conn) { } size_t ngtcp2_conn_get_scid(ngtcp2_conn *conn, ngtcp2_cid *dest) { + return ngtcp2_conn_get_scid2(conn, dest); +} + +size_t ngtcp2_conn_get_scid2(const ngtcp2_conn *conn, ngtcp2_cid *dest) { ngtcp2_cid *origdest = dest; ngtcp2_ksl_it it; ngtcp2_scid *scid; @@ -13405,7 +13634,7 @@ size_t ngtcp2_conn_get_scid(ngtcp2_conn *conn, ngtcp2_cid *dest) { return (size_t)(dest - origdest); } -static size_t conn_get_num_active_dcid(ngtcp2_conn *conn) { +static size_t conn_get_num_active_dcid(const ngtcp2_conn *conn) { size_t n = 1; /* for conn->dcid.current */ ngtcp2_pv *pv = conn->pv; @@ -13429,21 +13658,52 @@ static size_t conn_get_num_active_dcid(ngtcp2_conn *conn) { return n; } -static void copy_dcid_to_cid_token(ngtcp2_cid_token *dest, +size_t ngtcp2_conn_get_active_dcid(ngtcp2_conn *conn, ngtcp2_cid_token *dest) { + ngtcp2_cid_token2 cid_tokens[/* current */ 1 + /* pv */ 2 + + NGTCP2_DCIDTR_MAX_RETIRED_DCID_SIZE]; + size_t n, i; + + if (!dest) { + return ngtcp2_conn_get_active_dcid3(conn, NULL); + } + + n = ngtcp2_conn_get_active_dcid3(conn, cid_tokens); + + for (i = 0; i < n; ++i) { + dest[i].seq = cid_tokens[i].seq; + dest[i].cid = cid_tokens[i].cid; + ngtcp2_path_storage_init2(&dest[i].ps, &cid_tokens[i].ps.path); + dest[i].token_present = cid_tokens[i].token_present; + + if (dest[i].token_present) { + memcpy(dest[i].token, cid_tokens[i].token.data, sizeof(dest[i].token)); + } + } + + return n; +} + +static void copy_dcid_to_cid_token(ngtcp2_cid_token2 *dest, const ngtcp2_dcid *src) { dest->seq = src->seq; dest->cid = src->cid; ngtcp2_path_storage_init2(&dest->ps, &src->ps.path); if ((dest->token_present = (src->flags & NGTCP2_DCID_FLAG_TOKEN_PRESENT) != 0)) { - memcpy(dest->token, src->token, NGTCP2_STATELESS_RESET_TOKENLEN); + dest->token = src->token; } } -size_t ngtcp2_conn_get_active_dcid(ngtcp2_conn *conn, ngtcp2_cid_token *dest) { - ngtcp2_pv *pv = conn->pv; - ngtcp2_cid_token *orig = dest; - ngtcp2_dcid *dcid; +size_t ngtcp2_conn_get_active_dcid2(ngtcp2_conn *conn, + ngtcp2_cid_token2 *dest) { + return ngtcp2_conn_get_active_dcid3(conn, dest); +} + +size_t ngtcp2_conn_get_active_dcid3(const ngtcp2_conn *conn, + ngtcp2_cid_token2 *dest) { + const ngtcp2_pv *pv = conn->pv; + ngtcp2_cid_token2 *orig = dest; + const ngtcp2_dcid *dcid; size_t len, i; if (!(conn->flags & NGTCP2_CONN_FLAG_HANDSHAKE_COMPLETED)) { @@ -13497,16 +13757,28 @@ void ngtcp2_conn_set_path_user_data(ngtcp2_conn *conn, void *path_user_data) { } const ngtcp2_path *ngtcp2_conn_get_path(ngtcp2_conn *conn) { + return ngtcp2_conn_get_path2(conn); +} + +const ngtcp2_path *ngtcp2_conn_get_path2(const ngtcp2_conn *conn) { return &conn->dcid.current.ps.path; } size_t ngtcp2_conn_get_max_tx_udp_payload_size(ngtcp2_conn *conn) { + return ngtcp2_conn_get_max_tx_udp_payload_size2(conn); +} + +size_t ngtcp2_conn_get_max_tx_udp_payload_size2(const ngtcp2_conn *conn) { return conn->local.settings.max_tx_udp_payload_size; } size_t ngtcp2_conn_get_path_max_tx_udp_payload_size(ngtcp2_conn *conn) { + return ngtcp2_conn_get_path_max_tx_udp_payload_size2(conn); +} + +size_t ngtcp2_conn_get_path_max_tx_udp_payload_size2(const ngtcp2_conn *conn) { if (conn->local.settings.no_tx_udp_payload_size_shaping) { - return ngtcp2_conn_get_max_tx_udp_payload_size(conn); + return ngtcp2_conn_get_max_tx_udp_payload_size2(conn); } return conn->dcid.current.max_udp_payload_size; @@ -13653,12 +13925,21 @@ int ngtcp2_conn_initiate_migration(ngtcp2_conn *conn, const ngtcp2_path *path, } uint64_t ngtcp2_conn_get_max_data_left(ngtcp2_conn *conn) { + return ngtcp2_conn_get_max_data_left2(conn); +} + +uint64_t ngtcp2_conn_get_max_data_left2(const ngtcp2_conn *conn) { return conn->tx.max_offset - conn->tx.offset; } uint64_t ngtcp2_conn_get_max_stream_data_left(ngtcp2_conn *conn, int64_t stream_id) { - ngtcp2_strm *strm = ngtcp2_conn_find_stream(conn, stream_id); + return ngtcp2_conn_get_max_stream_data_left2(conn, stream_id); +} + +uint64_t ngtcp2_conn_get_max_stream_data_left2(const ngtcp2_conn *conn, + int64_t stream_id) { + const ngtcp2_strm *strm = ngtcp2_conn_find_stream(conn, stream_id); if (strm == NULL) { return 0; @@ -13668,6 +13949,10 @@ uint64_t ngtcp2_conn_get_max_stream_data_left(ngtcp2_conn *conn, } uint64_t ngtcp2_conn_get_streams_bidi_left(ngtcp2_conn *conn) { + return ngtcp2_conn_get_streams_bidi_left2(conn); +} + +uint64_t ngtcp2_conn_get_streams_bidi_left2(const ngtcp2_conn *conn) { uint64_t n = ngtcp2_ord_stream_id(conn->local.bidi.next_stream_id); return n > conn->local.bidi.max_streams @@ -13676,6 +13961,10 @@ uint64_t ngtcp2_conn_get_streams_bidi_left(ngtcp2_conn *conn) { } uint64_t ngtcp2_conn_get_streams_uni_left(ngtcp2_conn *conn) { + return ngtcp2_conn_get_streams_uni_left2(conn); +} + +uint64_t ngtcp2_conn_get_streams_uni_left2(const ngtcp2_conn *conn) { uint64_t n = ngtcp2_ord_stream_id(conn->local.uni.next_stream_id); return n > conn->local.uni.max_streams ? 0 @@ -13683,6 +13972,10 @@ uint64_t ngtcp2_conn_get_streams_uni_left(ngtcp2_conn *conn) { } uint64_t ngtcp2_conn_get_cwnd_left(ngtcp2_conn *conn) { + return ngtcp2_conn_get_cwnd_left2(conn); +} + +uint64_t ngtcp2_conn_get_cwnd_left2(const ngtcp2_conn *conn) { uint64_t bytes_in_flight = conn->cstat.bytes_in_flight; uint64_t cwnd = conn->cstat.cwnd; @@ -13693,7 +13986,7 @@ uint64_t ngtcp2_conn_get_cwnd_left(ngtcp2_conn *conn) { return 0; } -ngtcp2_tstamp ngtcp2_conn_get_idle_expiry(ngtcp2_conn *conn) { +ngtcp2_tstamp ngtcp2_conn_get_idle_expiry(const ngtcp2_conn *conn) { ngtcp2_duration trpto; ngtcp2_duration idle_timeout; @@ -13714,11 +14007,11 @@ ngtcp2_tstamp ngtcp2_conn_get_idle_expiry(ngtcp2_conn *conn) { return UINT64_MAX; } - trpto = 3 * conn_compute_pto(conn, conn_is_tls_handshake_completed(conn) - ? &conn->pktns - : conn->hs_pktns); + trpto = 3 * ngtcp2_conn_compute_pto( + conn, conn_is_tls_handshake_completed(conn) ? &conn->pktns + : conn->hs_pktns); - idle_timeout = ngtcp2_max_uint64(idle_timeout, trpto); + idle_timeout = ngtcp2_max(idle_timeout, trpto); if (conn->idle_ts >= UINT64_MAX - idle_timeout) { return UINT64_MAX; @@ -13728,9 +14021,13 @@ ngtcp2_tstamp ngtcp2_conn_get_idle_expiry(ngtcp2_conn *conn) { } ngtcp2_duration ngtcp2_conn_get_pto(ngtcp2_conn *conn) { - return conn_compute_pto(conn, conn_is_tls_handshake_completed(conn) - ? &conn->pktns - : conn->hs_pktns); + return ngtcp2_conn_get_pto2(conn); +} + +ngtcp2_duration ngtcp2_conn_get_pto2(const ngtcp2_conn *conn) { + return ngtcp2_conn_compute_pto(conn, conn_is_tls_handshake_completed(conn) + ? &conn->pktns + : conn->hs_pktns); } void ngtcp2_conn_set_initial_crypto_ctx(ngtcp2_conn *conn, @@ -13740,6 +14037,11 @@ void ngtcp2_conn_set_initial_crypto_ctx(ngtcp2_conn *conn, } const ngtcp2_crypto_ctx *ngtcp2_conn_get_initial_crypto_ctx(ngtcp2_conn *conn) { + return ngtcp2_conn_get_initial_crypto_ctx2(conn); +} + +const ngtcp2_crypto_ctx * +ngtcp2_conn_get_initial_crypto_ctx2(const ngtcp2_conn *conn) { assert(conn->in_pktns); return &conn->in_pktns->crypto.ctx; } @@ -13761,6 +14063,10 @@ void ngtcp2_conn_set_crypto_ctx(ngtcp2_conn *conn, } const ngtcp2_crypto_ctx *ngtcp2_conn_get_crypto_ctx(ngtcp2_conn *conn) { + return ngtcp2_conn_get_crypto_ctx2(conn); +} + +const ngtcp2_crypto_ctx *ngtcp2_conn_get_crypto_ctx2(const ngtcp2_conn *conn) { return &conn->pktns.crypto.ctx; } @@ -13770,10 +14076,19 @@ void ngtcp2_conn_set_0rtt_crypto_ctx(ngtcp2_conn *conn, } const ngtcp2_crypto_ctx *ngtcp2_conn_get_0rtt_crypto_ctx(ngtcp2_conn *conn) { + return ngtcp2_conn_get_0rtt_crypto_ctx2(conn); +} + +const ngtcp2_crypto_ctx * +ngtcp2_conn_get_0rtt_crypto_ctx2(const ngtcp2_conn *conn) { return &conn->early.ctx; } void *ngtcp2_conn_get_tls_native_handle(ngtcp2_conn *conn) { + return ngtcp2_conn_get_tls_native_handle2(conn); +} + +void *ngtcp2_conn_get_tls_native_handle2(const ngtcp2_conn *conn) { return conn->crypto.tls_native_handle; } @@ -13783,6 +14098,10 @@ void ngtcp2_conn_set_tls_native_handle(ngtcp2_conn *conn, } const ngtcp2_ccerr *ngtcp2_conn_get_ccerr(ngtcp2_conn *conn) { + return ngtcp2_conn_get_ccerr2(conn); +} + +const ngtcp2_ccerr *ngtcp2_conn_get_ccerr2(const ngtcp2_conn *conn) { return &conn->rx.ccerr; } @@ -13791,6 +14110,10 @@ void ngtcp2_conn_set_tls_error(ngtcp2_conn *conn, int liberr) { } int ngtcp2_conn_get_tls_error(ngtcp2_conn *conn) { + return ngtcp2_conn_get_tls_error2(conn); +} + +int ngtcp2_conn_get_tls_error2(const ngtcp2_conn *conn) { return conn->crypto.tls_error; } @@ -13799,16 +14122,32 @@ void ngtcp2_conn_set_tls_alert(ngtcp2_conn *conn, uint8_t alert) { } uint8_t ngtcp2_conn_get_tls_alert(ngtcp2_conn *conn) { + return ngtcp2_conn_get_tls_alert2(conn); +} + +uint8_t ngtcp2_conn_get_tls_alert2(const ngtcp2_conn *conn) { return conn->crypto.tls_alert; } int ngtcp2_conn_is_local_stream(ngtcp2_conn *conn, int64_t stream_id) { + return ngtcp2_conn_is_local_stream2(conn, stream_id); +} + +int ngtcp2_conn_is_local_stream2(const ngtcp2_conn *conn, int64_t stream_id) { return conn_local_stream(conn, stream_id); } -int ngtcp2_conn_is_server(ngtcp2_conn *conn) { return conn->server; } +int ngtcp2_conn_is_server(ngtcp2_conn *conn) { + return ngtcp2_conn_is_server2(conn); +} + +int ngtcp2_conn_is_server2(const ngtcp2_conn *conn) { return conn->server; } int ngtcp2_conn_after_retry(ngtcp2_conn *conn) { + return ngtcp2_conn_after_retry2(conn); +} + +int ngtcp2_conn_after_retry2(const ngtcp2_conn *conn) { return (conn->flags & NGTCP2_CONN_FLAG_RECV_RETRY) != 0; } @@ -13825,8 +14164,22 @@ int ngtcp2_conn_set_stream_user_data(ngtcp2_conn *conn, int64_t stream_id, return 0; } +void *ngtcp2_conn_get_stream_user_data(ngtcp2_conn *conn, int64_t stream_id) { + return ngtcp2_conn_get_stream_user_data2(conn, stream_id); +} + +void *ngtcp2_conn_get_stream_user_data2(const ngtcp2_conn *conn, + int64_t stream_id) { + const ngtcp2_strm *strm = ngtcp2_conn_find_stream(conn, stream_id); + + if (strm == NULL) { + return NULL; + } + + return strm->stream_user_data; +} + void ngtcp2_conn_update_pkt_tx_time(ngtcp2_conn *conn, ngtcp2_tstamp ts) { - uint64_t pacing_interval_m; ngtcp2_duration wait, d; conn_update_timestamp(conn, ts); @@ -13835,22 +14188,11 @@ void ngtcp2_conn_update_pkt_tx_time(ngtcp2_conn *conn, ngtcp2_tstamp ts) { return; } - if (conn->cstat.pacing_interval_m) { - pacing_interval_m = conn->cstat.pacing_interval_m; - } else { - /* 1.25 is the under-utilization avoidance factor described in - https://datatracker.ietf.org/doc/html/rfc9002#section-7.7 */ - pacing_interval_m = ((conn->cstat.first_rtt_sample_ts == UINT64_MAX - ? NGTCP2_MILLISECONDS - : conn->cstat.smoothed_rtt) - << 10) * - 100 / 125 / conn->cstat.cwnd; - pacing_interval_m = ngtcp2_max_uint64(pacing_interval_m, 1); - } - - wait = (ngtcp2_duration)((conn->tx.pacing.pktlen * pacing_interval_m) >> 10); + wait = (ngtcp2_duration)((conn->tx.pacing.pktlen * + conn->cstat.pacing_interval_m) >> + 10); - d = ngtcp2_min_uint64(wait / 2, conn->tx.pacing.compensation); + d = ngtcp2_min(wait / 2, conn->tx.pacing.compensation); wait -= d; conn->tx.pacing.compensation -= d; @@ -13859,11 +14201,20 @@ void ngtcp2_conn_update_pkt_tx_time(ngtcp2_conn *conn, ngtcp2_tstamp ts) { } size_t ngtcp2_conn_get_send_quantum(ngtcp2_conn *conn) { + return ngtcp2_conn_get_send_quantum2(conn); +} + +size_t ngtcp2_conn_get_send_quantum2(const ngtcp2_conn *conn) { return conn->cstat.send_quantum; } size_t ngtcp2_conn_get_stream_loss_count(ngtcp2_conn *conn, int64_t stream_id) { - ngtcp2_strm *strm = ngtcp2_conn_find_stream(conn, stream_id); + return ngtcp2_conn_get_stream_loss_count2(conn, stream_id); +} + +size_t ngtcp2_conn_get_stream_loss_count2(const ngtcp2_conn *conn, + int64_t stream_id) { + const ngtcp2_strm *strm = ngtcp2_conn_find_stream(conn, stream_id); if (strm == NULL) { return 0; @@ -13886,9 +14237,28 @@ ngtcp2_ssize ngtcp2_conn_write_aggregate_pkt_versioned( ngtcp2_conn *conn, ngtcp2_path *path, int pkt_info_version, ngtcp2_pkt_info *pi, uint8_t *buf, size_t buflen, size_t *pgsolen, ngtcp2_write_pkt write_pkt, ngtcp2_tstamp ts) { - size_t max_udp_payloadlen = ngtcp2_conn_get_max_tx_udp_payload_size(conn); + ngtcp2_ssize nwrite; + + buflen = ngtcp2_min(buflen, ngtcp2_conn_get_send_quantum2(conn)); + + nwrite = ngtcp2_conn_write_aggregate_pkt2_versioned( + conn, path, pkt_info_version, pi, buf, buflen, pgsolen, write_pkt, 0, ts); + if (nwrite < 0) { + return nwrite; + } + + ngtcp2_conn_update_pkt_tx_time(conn, ts); + + return nwrite; +} + +ngtcp2_ssize ngtcp2_conn_write_aggregate_pkt2_versioned( + ngtcp2_conn *conn, ngtcp2_path *path, int pkt_info_version, + ngtcp2_pkt_info *pi, uint8_t *buf, size_t buflen, size_t *pgsolen, + ngtcp2_write_pkt write_pkt, size_t num_pkts, ngtcp2_tstamp ts) { + size_t max_udp_payloadlen = ngtcp2_conn_get_max_tx_udp_payload_size2(conn); size_t path_max_udp_payloadlen = - ngtcp2_conn_get_path_max_tx_udp_payload_size(conn); + ngtcp2_conn_get_path_max_tx_udp_payload_size2(conn); ngtcp2_ssize nwrite; uint8_t *wbuf = buf; size_t wbuflen; @@ -13900,9 +14270,9 @@ ngtcp2_ssize ngtcp2_conn_write_aggregate_pkt_versioned( assert(buflen >= path_max_udp_payloadlen); - buflen = - ngtcp2_min_size(buflen, ngtcp2_max_size(ngtcp2_conn_get_send_quantum(conn), - path_max_udp_payloadlen)); + if (num_pkts == 0) { + num_pkts = SIZE_MAX; + } for (;;) { ecn_state = conn->tx.ecn.state; @@ -13924,13 +14294,16 @@ ngtcp2_ssize ngtcp2_conn_write_aggregate_pkt_versioned( wbuf += nwrite; buflen -= (size_t)nwrite; + --num_pkts; + if (first_pkt) { assert(!(conn->flags & NGTCP2_CONN_FLAG_AGGREGATE_PKTS)); *pgsolen = (size_t)nwrite; if ((size_t)nwrite != path_max_udp_payloadlen || - buflen < path_max_udp_payloadlen || ecn_state != conn->tx.ecn.state) { + buflen < path_max_udp_payloadlen || ecn_state != conn->tx.ecn.state || + num_pkts == 0) { nwrite = wbuf - buf; break; } @@ -13954,7 +14327,7 @@ ngtcp2_ssize ngtcp2_conn_write_aggregate_pkt_versioned( } if (buflen < path_max_udp_payloadlen || (size_t)nwrite < *pgsolen || - ecn_state != conn->tx.ecn.state) { + ecn_state != conn->tx.ecn.state || num_pkts == 0) { nwrite = wbuf - buf; break; } @@ -13962,15 +14335,17 @@ ngtcp2_ssize ngtcp2_conn_write_aggregate_pkt_versioned( conn->flags &= ~NGTCP2_CONN_FLAG_AGGREGATE_PKTS; - ngtcp2_conn_update_pkt_tx_time(conn, ts); - return nwrite; } +ngtcp2_tstamp ngtcp2_conn_get_timestamp(const ngtcp2_conn *conn) { + return conn->log.last_ts; +} + const ngtcp2_path_history_entry * -ngtcp2_conn_find_path_history(ngtcp2_conn *conn, const ngtcp2_path *path, +ngtcp2_conn_find_path_history(const ngtcp2_conn *conn, const ngtcp2_path *path, ngtcp2_tstamp ts) { - ngtcp2_ringbuf *rb = &conn->path_history.rb; + const ngtcp2_ringbuf *rb = &conn->path_history.rb; size_t i, len = ngtcp2_ringbuf_len(rb); ngtcp2_path_history_entry *ent; @@ -13990,9 +14365,9 @@ ngtcp2_conn_find_path_history(ngtcp2_conn *conn, const ngtcp2_path *path, void ngtcp2_path_challenge_entry_init(ngtcp2_path_challenge_entry *pcent, const ngtcp2_path *path, - const uint8_t *data) { + const ngtcp2_path_challenge_data *data) { ngtcp2_path_storage_init2(&pcent->ps, path); - memcpy(pcent->data, data, sizeof(pcent->data)); + pcent->data = *data; } /* The functions prefixed with ngtcp2_pkt_ are usually put inside @@ -14037,10 +14412,6 @@ ngtcp2_ssize ngtcp2_pkt_write_connection_close( return rv; } - if (!ngtcp2_ppe_ensure_hp_sample(&ppe)) { - return NGTCP2_ERR_NOBUF; - } - fr.connection_close = (ngtcp2_connection_close){ .type = NGTCP2_FRAME_CONNECTION_CLOSE, .error_code = error_code, diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.h index 2d607d379fc310..548c296e8ae1d0 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.h @@ -77,10 +77,6 @@ typedef enum { unreceived data. */ #define NGTCP2_MAX_REORDERED_CRYPTO_DATA 65536 -/* NGTCP2_MAX_RETRIES is the number of Retry packet which client can - accept. */ -#define NGTCP2_MAX_RETRIES 3 - /* NGTCP2_MAX_SCID_POOL_SIZE is the maximum number of source connection ID the local endpoint provides to the remote endpoint. The chosen value was described in old draft. Now a remote endpoint @@ -98,48 +94,35 @@ typedef enum { #define NGTCP2_CCERR_MAX_REASONLEN 1024 /* NGTCP2_WRITE_PKT_FLAG_NONE indicates that no flag is set. */ -#define NGTCP2_WRITE_PKT_FLAG_NONE 0x00u +#define NGTCP2_WRITE_PKT_FLAG_NONE 0x00U /* NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING indicates that packet other than Initial packet should be padded so that UDP datagram payload is at least NGTCP2_MAX_UDP_PAYLOAD_SIZE bytes. Initial packet might be padded based on QUIC requirement regardless of this flag. */ -#define NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING 0x01u +#define NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING 0x01U /* NGTCP2_WRITE_PKT_FLAG_MORE indicates that more frames might come and it should be encoded into the current packet. */ -#define NGTCP2_WRITE_PKT_FLAG_MORE 0x02u +#define NGTCP2_WRITE_PKT_FLAG_MORE 0x02U /* NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING_FULL is just like NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING, but it requests to add padding to the full UDP datagram payload size. */ -#define NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING_FULL 0x04u +#define NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING_FULL 0x04U /* NGTCP2_WRITE_PKT_FLAG_PADDING_IF_NOT_EMPTY adds padding to the QUIC packet as much as possible if the packet is not empty. */ -#define NGTCP2_WRITE_PKT_FLAG_PADDING_IF_NOT_EMPTY 0x08u - -/* - * ngtcp2_max_frame is defined so that it covers the largest ACK - * frame. - */ -typedef union ngtcp2_max_frame { - ngtcp2_frame fr; - struct { - ngtcp2_ack ack; - /* ack includes 1 ngtcp2_ack_range. */ - ngtcp2_ack_range ranges[NGTCP2_MAX_ACK_RANGES - 1]; - } ackfr; -} ngtcp2_max_frame; +#define NGTCP2_WRITE_PKT_FLAG_PADDING_IF_NOT_EMPTY 0x08U typedef struct ngtcp2_path_challenge_entry { ngtcp2_path_storage ps; - uint8_t data[8]; + ngtcp2_path_challenge_data data; } ngtcp2_path_challenge_entry; void ngtcp2_path_challenge_entry_init(ngtcp2_path_challenge_entry *pcent, const ngtcp2_path *path, - const uint8_t *data); + const ngtcp2_path_challenge_data *data); /* NGTCP2_CONN_FLAG_NONE indicates that no flag is set. */ -#define NGTCP2_CONN_FLAG_NONE 0x00u +#define NGTCP2_CONN_FLAG_NONE 0x00U /* NGTCP2_CONN_FLAG_TLS_HANDSHAKE_COMPLETED is set when TLS stack declares that TLS handshake has completed. The condition of this declaration varies between TLS implementations and this flag does @@ -147,69 +130,69 @@ void ngtcp2_path_challenge_entry_init(ngtcp2_path_challenge_entry *pcent, implementations declare TLS handshake completion as server when they write off Server Finished and before deriving application rx secret. */ -#define NGTCP2_CONN_FLAG_TLS_HANDSHAKE_COMPLETED 0x01u +#define NGTCP2_CONN_FLAG_TLS_HANDSHAKE_COMPLETED 0x01U /* NGTCP2_CONN_FLAG_INITIAL_PKT_PROCESSED is set when the first Initial packet has successfully been processed. */ -#define NGTCP2_CONN_FLAG_INITIAL_PKT_PROCESSED 0x02u +#define NGTCP2_CONN_FLAG_INITIAL_PKT_PROCESSED 0x02U /* NGTCP2_CONN_FLAG_TRANSPORT_PARAM_RECVED is set if transport parameters are received. */ -#define NGTCP2_CONN_FLAG_TRANSPORT_PARAM_RECVED 0x04u +#define NGTCP2_CONN_FLAG_TRANSPORT_PARAM_RECVED 0x04U /* NGTCP2_CONN_FLAG_LOCAL_TRANSPORT_PARAMS_COMMITTED is set when a local transport parameters are applied. */ -#define NGTCP2_CONN_FLAG_LOCAL_TRANSPORT_PARAMS_COMMITTED 0x08u +#define NGTCP2_CONN_FLAG_LOCAL_TRANSPORT_PARAMS_COMMITTED 0x08U /* NGTCP2_CONN_FLAG_RECV_RETRY is set when a client receives Retry packet. */ -#define NGTCP2_CONN_FLAG_RECV_RETRY 0x10u +#define NGTCP2_CONN_FLAG_RECV_RETRY 0x10U /* NGTCP2_CONN_FLAG_EARLY_DATA_REJECTED is set when 0-RTT packet is rejected by a peer. */ -#define NGTCP2_CONN_FLAG_EARLY_DATA_REJECTED 0x20u +#define NGTCP2_CONN_FLAG_EARLY_DATA_REJECTED 0x20U /* NGTCP2_CONN_FLAG_KEEP_ALIVE_CANCELLED is set when the expired keep-alive timer has been cancelled. */ -#define NGTCP2_CONN_FLAG_KEEP_ALIVE_CANCELLED 0x40u +#define NGTCP2_CONN_FLAG_KEEP_ALIVE_CANCELLED 0x40U /* NGTCP2_CONN_FLAG_HANDSHAKE_CONFIRMED is set when an endpoint confirmed completion of handshake. */ -#define NGTCP2_CONN_FLAG_HANDSHAKE_CONFIRMED 0x80u +#define NGTCP2_CONN_FLAG_HANDSHAKE_CONFIRMED 0x80U /* NGTCP2_CONN_FLAG_HANDSHAKE_COMPLETED is set when the library transitions its state to "post handshake". */ -#define NGTCP2_CONN_FLAG_HANDSHAKE_COMPLETED 0x0100u +#define NGTCP2_CONN_FLAG_HANDSHAKE_COMPLETED 0x0100U /* NGTCP2_CONN_FLAG_HANDSHAKE_EARLY_RETRANSMIT is set when the early handshake retransmission has done when server receives overlapping Initial crypto data. */ -#define NGTCP2_CONN_FLAG_HANDSHAKE_EARLY_RETRANSMIT 0x0200u +#define NGTCP2_CONN_FLAG_HANDSHAKE_EARLY_RETRANSMIT 0x0200U /* NGTCP2_CONN_FLAG_CLEAR_FIXED_BIT indicates that the local endpoint sends a QUIC packet without Fixed Bit set if a remote endpoint supports Greasing QUIC Bit extension. */ -#define NGTCP2_CONN_FLAG_CLEAR_FIXED_BIT 0x0400u +#define NGTCP2_CONN_FLAG_CLEAR_FIXED_BIT 0x0400U /* NGTCP2_CONN_FLAG_KEY_UPDATE_NOT_CONFIRMED is set when key update is not confirmed by the local endpoint. That is, it has not received ACK frame which acknowledges packet which is encrypted with new key. */ -#define NGTCP2_CONN_FLAG_KEY_UPDATE_NOT_CONFIRMED 0x0800u +#define NGTCP2_CONN_FLAG_KEY_UPDATE_NOT_CONFIRMED 0x0800U /* NGTCP2_CONN_FLAG_PPE_PENDING is set when NGTCP2_WRITE_STREAM_FLAG_MORE is used and the intermediate state of ngtcp2_ppe is stored in pkt struct of ngtcp2_conn. */ -#define NGTCP2_CONN_FLAG_PPE_PENDING 0x1000u +#define NGTCP2_CONN_FLAG_PPE_PENDING 0x1000U /* NGTCP2_CONN_FLAG_RESTART_IDLE_TIMER_ON_WRITE is set when idle timer should be restarted on next write. */ -#define NGTCP2_CONN_FLAG_RESTART_IDLE_TIMER_ON_WRITE 0x2000u +#define NGTCP2_CONN_FLAG_RESTART_IDLE_TIMER_ON_WRITE 0x2000U /* NGTCP2_CONN_FLAG_SERVER_ADDR_VERIFIED indicates that server as peer verified client address. This flag is only used by client. */ -#define NGTCP2_CONN_FLAG_SERVER_ADDR_VERIFIED 0x4000u +#define NGTCP2_CONN_FLAG_SERVER_ADDR_VERIFIED 0x4000U /* NGTCP2_CONN_FLAG_EARLY_KEY_INSTALLED indicates that an early key is installed. conn->early.ckm cannot be used for this purpose because it might be discarded when a certain condition is met. */ -#define NGTCP2_CONN_FLAG_EARLY_KEY_INSTALLED 0x8000u +#define NGTCP2_CONN_FLAG_EARLY_KEY_INSTALLED 0x8000U /* NGTCP2_CONN_FLAG_KEY_UPDATE_INITIATOR is set when the local endpoint has initiated key update. */ -#define NGTCP2_CONN_FLAG_KEY_UPDATE_INITIATOR 0x10000u +#define NGTCP2_CONN_FLAG_KEY_UPDATE_INITIATOR 0x10000U /* NGTCP2_CONN_FLAG_AGGREGATE_PKTS is set when ngtcp2_conn_writev_stream is called inside the callback invoked by ngtcp2_conn_write_aggregate_pkt. */ -#define NGTCP2_CONN_FLAG_AGGREGATE_PKTS 0x20000u +#define NGTCP2_CONN_FLAG_AGGREGATE_PKTS 0x20000U /* NGTCP2_CONN_FLAG_CRUMBLE_INITIAL_CRYPTO, if set, crumbles an Initial CRYPTO frame into pieces as a countermeasure against Deep Packet Inspection. */ -#define NGTCP2_CONN_FLAG_CRUMBLE_INITIAL_CRYPTO 0x40000u +#define NGTCP2_CONN_FLAG_CRUMBLE_INITIAL_CRYPTO 0x40000U typedef struct ngtcp2_pktns { struct { @@ -404,14 +387,14 @@ struct ngtcp2_conn { ngtcp2_tstamp last_max_data_ts; struct { - /* state is the state of ECN validation */ - ngtcp2_ecn_state state; /* validation_start_ts is the timestamp when ECN validation is started. It is UINT64_MAX if it has not started yet. */ ngtcp2_tstamp validation_start_ts; /* dgram_sent is the number of UDP datagram sent during ECN validation period. */ size_t dgram_sent; + /* state is the state of ECN validation */ + ngtcp2_ecn_state state; } ecn; struct { @@ -562,13 +545,13 @@ struct ngtcp2_conn { /* retry_aead_ctx is AEAD cipher context to verify Retry packet integrity. It is used by client only. */ ngtcp2_crypto_aead_ctx retry_aead_ctx; + /* decryption_failure_count is the number of received packets that + fail authentication. */ + uint64_t decryption_failure_count; /* tls_error is TLS related error. */ int tls_error; /* tls_alert is TLS alert generated by the local endpoint. */ uint8_t tls_alert; - /* decryption_failure_count is the number of received packets that - fail authentication. */ - uint64_t decryption_failure_count; } crypto; /* pkt contains the packet intermediate construction data to support @@ -578,13 +561,13 @@ struct ngtcp2_conn { ngtcp2_pkt_hd hd; ngtcp2_ppe ppe; ngtcp2_frame_chain **pfrc; + ngtcp2_ssize hs_spktlen; int pkt_empty; int hd_logged; + int require_padding; /* flags is bitwise OR of zero or more of NGTCP2_RTB_ENTRY_FLAG_*. */ uint16_t rtb_entry_flags; - ngtcp2_ssize hs_spktlen; - int require_padding; } pkt; struct { @@ -640,7 +623,6 @@ struct ngtcp2_conn { ngtcp2_log log; ngtcp2_qlog qlog; ngtcp2_rst rst; - ngtcp2_cc_algo cc_algo; union { ngtcp2_cc cc; ngtcp2_cc_reno reno; @@ -667,6 +649,7 @@ struct ngtcp2_conn { uint32_t negotiated_version; /* flags is bitwise OR of zero or more of NGTCP2_CONN_FLAG_*. */ uint32_t flags; + ngtcp2_cc_algo cc_algo; int server; }; @@ -678,9 +661,6 @@ typedef enum ngtcp2_vmsg_type { typedef struct ngtcp2_vmsg_stream { /* strm is a stream that data is sent to. */ ngtcp2_strm *strm; - /* flags is bitwise OR of zero or more of - NGTCP2_WRITE_STREAM_FLAG_*. */ - uint32_t flags; /* data is the pointer to ngtcp2_vec array which contains the stream data to send. */ const ngtcp2_vec *data; @@ -689,6 +669,9 @@ typedef struct ngtcp2_vmsg_stream { /* pdatalen is the pointer to the variable which the number of bytes written is assigned to if pdatalen is not NULL. */ ngtcp2_ssize *pdatalen; + /* flags is bitwise OR of zero or more of + NGTCP2_WRITE_STREAM_FLAG_*. */ + uint32_t flags; } ngtcp2_vmsg_stream; typedef struct ngtcp2_vmsg_datagram { @@ -699,12 +682,12 @@ typedef struct ngtcp2_vmsg_datagram { size_t datacnt; /* dgram_id is an opaque identifier chosen by an application. */ uint64_t dgram_id; - /* flags is bitwise OR of zero or more of - NGTCP2_WRITE_DATAGRAM_FLAG_*. */ - uint32_t flags; /* paccepted is the pointer to the variable which, if it is not NULL, is assigned nonzero if data is written to a packet. */ int *paccepted; + /* flags is bitwise OR of zero or more of + NGTCP2_WRITE_DATAGRAM_FLAG_*. */ + uint32_t flags; } ngtcp2_vmsg_datagram; typedef struct ngtcp2_vmsg { @@ -719,7 +702,8 @@ typedef struct ngtcp2_vmsg { * ngtcp2_conn_find_stream returns a stream whose stream ID is * |stream_id|. If no such stream is found, it returns NULL. */ -ngtcp2_strm *ngtcp2_conn_find_stream(ngtcp2_conn *conn, int64_t stream_id); +ngtcp2_strm *ngtcp2_conn_find_stream(const ngtcp2_conn *conn, + int64_t stream_id); /* * conn_init_stream initializes |strm|. Its stream ID is |stream_id|. @@ -772,15 +756,9 @@ int ngtcp2_conn_close_stream_if_shut_rdwr(ngtcp2_conn *conn, ngtcp2_strm *strm); * ack_delay included in ACK frame. |ack_delay| is actually tainted * (sent by peer), so don't assume that |ack_delay| is always smaller * than, or equals to |rtt|. - * - * This function returns 0 if it succeeds, or one of the following - * negative error codes: - * - * NGTCP2_ERR_INVALID_ARGUMENT - * RTT sample is ignored. */ -int ngtcp2_conn_update_rtt(ngtcp2_conn *conn, ngtcp2_duration rtt, - ngtcp2_duration ack_delay, ngtcp2_tstamp ts); +void ngtcp2_conn_update_rtt(ngtcp2_conn *conn, ngtcp2_duration rtt, + ngtcp2_duration ack_delay, ngtcp2_tstamp ts); void ngtcp2_conn_set_loss_detection_timer(ngtcp2_conn *conn, ngtcp2_tstamp ts); @@ -827,7 +805,7 @@ int ngtcp2_conn_tx_strmq_push(ngtcp2_conn *conn, ngtcp2_strm *strm); * ngtcp2_conn_internal_expiry returns the minimum expiry time among * all timers in |conn|. */ -ngtcp2_tstamp ngtcp2_conn_internal_expiry(ngtcp2_conn *conn); +ngtcp2_tstamp ngtcp2_conn_internal_expiry(const ngtcp2_conn *conn); ngtcp2_ssize ngtcp2_conn_write_vmsg(ngtcp2_conn *conn, ngtcp2_path *path, int pkt_info_version, ngtcp2_pkt_info *pi, @@ -876,14 +854,14 @@ int ngtcp2_conn_commit_local_transport_params(ngtcp2_conn *conn); * ngtcp2_conn_lost_pkt_expiry returns the earliest expiry time of * lost packet. */ -ngtcp2_tstamp ngtcp2_conn_lost_pkt_expiry(ngtcp2_conn *conn); +ngtcp2_tstamp ngtcp2_conn_lost_pkt_expiry(const ngtcp2_conn *conn); /* * ngtcp2_conn_remove_lost_pkt removes the expired lost packet. */ void ngtcp2_conn_remove_lost_pkt(ngtcp2_conn *conn, ngtcp2_tstamp ts); -uint64_t ngtcp2_conn_tx_strmq_first_cycle(ngtcp2_conn *conn); +uint64_t ngtcp2_conn_tx_strmq_first_cycle(const ngtcp2_conn *conn); /** * @function @@ -894,7 +872,7 @@ uint64_t ngtcp2_conn_tx_strmq_first_cycle(ngtcp2_conn *conn); * `ngtcp2_conn_write_pkt` (or `ngtcp2_conn_writev_stream`) when it * expires. It returns UINT64_MAX if there is no expiry. */ -ngtcp2_tstamp ngtcp2_conn_ack_delay_expiry(ngtcp2_conn *conn); +ngtcp2_tstamp ngtcp2_conn_ack_delay_expiry(const ngtcp2_conn *conn); /** * @function @@ -915,7 +893,7 @@ void ngtcp2_conn_cancel_expired_ack_delay_timer(ngtcp2_conn *conn, * (or `ngtcp2_conn_writev_stream`) when it expires. It returns * UINT64_MAX if loss detection timer is not armed. */ -ngtcp2_tstamp ngtcp2_conn_loss_detection_expiry(ngtcp2_conn *conn); +ngtcp2_tstamp ngtcp2_conn_loss_detection_expiry(const ngtcp2_conn *conn); /** * @function @@ -924,34 +902,13 @@ ngtcp2_tstamp ngtcp2_conn_loss_detection_expiry(ngtcp2_conn *conn); * should be closed if it continues to be idle. If idle timeout is * disabled, this function returns ``UINT64_MAX``. */ -ngtcp2_tstamp ngtcp2_conn_get_idle_expiry(ngtcp2_conn *conn); - -ngtcp2_duration ngtcp2_conn_compute_pto(ngtcp2_conn *conn, ngtcp2_pktns *pktns); - -/* - * ngtcp2_conn_track_retired_dcid_seq tracks the sequence number |seq| - * of unacknowledged retiring Destination Connection ID. - * - * This function returns 0 if it succeeds, or one of the following - * negative error codes: - * - * NGTCP2_ERR_CONNECTION_ID_LIMIT - * The number of unacknowledged retirement exceeds the limit. - */ -int ngtcp2_conn_track_retired_dcid_seq(ngtcp2_conn *conn, uint64_t seq); - -/* - * ngtcp2_conn_untrack_retired_dcid_seq deletes the sequence number - * |seq| of unacknowledged retiring Destination Connection ID. It is - * fine if such sequence number is not found. - */ -void ngtcp2_conn_untrack_retired_dcid_seq(ngtcp2_conn *conn, uint64_t seq); +ngtcp2_tstamp ngtcp2_conn_get_idle_expiry(const ngtcp2_conn *conn); /* - * ngtcp2_conn_check_retired_dcid_tracked returns nonzero if |seq| has - * already been tracked. + * ngtcp2_conn_compute_pto computes the current PTO. */ -int ngtcp2_conn_check_retired_dcid_tracked(ngtcp2_conn *conn, uint64_t seq); +ngtcp2_duration ngtcp2_conn_compute_pto(const ngtcp2_conn *conn, + const ngtcp2_pktns *pktns); /* * ngtcp2_conn_server_negotiate_version negotiates QUIC version. It @@ -966,7 +923,7 @@ ngtcp2_conn_server_negotiate_version(ngtcp2_conn *conn, * @function * * `ngtcp2_conn_write_connection_close_pkt` writes a packet which - * contains a CONNECTION_CLOSE frame (type 0x1c) in the buffer pointed + * contains a CONNECTION_CLOSE frame (type 0x1C) in the buffer pointed * by |dest| whose capacity is |datalen|. * * If |path| is not ``NULL``, this function stores the network path @@ -1008,7 +965,7 @@ ngtcp2_ssize ngtcp2_conn_write_connection_close_pkt( * @function * * `ngtcp2_conn_write_application_close_pkt` writes a packet which - * contains a CONNECTION_CLOSE frame (type 0x1d) in the buffer pointed + * contains a CONNECTION_CLOSE frame (type 0x1D) in the buffer pointed * by |dest| whose capacity is |datalen|. * * If |path| is not ``NULL``, this function stores the network path @@ -1021,7 +978,7 @@ ngtcp2_ssize ngtcp2_conn_write_connection_close_pkt( * if it succeeds. The metadata includes ECN markings. * * If handshake has not been confirmed yet, CONNECTION_CLOSE (type - * 0x1c) with error code :macro:`NGTCP2_APPLICATION_ERROR` is written + * 0x1C) with error code :macro:`NGTCP2_APPLICATION_ERROR` is written * instead. * * This function must not be called from inside the callback @@ -1130,7 +1087,7 @@ void ngtcp2_conn_add_path_history(ngtcp2_conn *conn, const ngtcp2_dcid *dcid, ngtcp2_tstamp ts); const ngtcp2_path_history_entry * -ngtcp2_conn_find_path_history(ngtcp2_conn *conn, const ngtcp2_path *path, +ngtcp2_conn_find_path_history(const ngtcp2_conn *conn, const ngtcp2_path *path, ngtcp2_tstamp ts); #endif /* !defined(NGTCP2_CONN_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn_info.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn_info.c new file mode 100644 index 00000000000000..84bb8820f50d65 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn_info.c @@ -0,0 +1,50 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "ngtcp2_conn_info.h" +#include "ngtcp2_conn_stat.h" + +void ngtcp2_conn_info_init_versioned(int conn_info_version, + ngtcp2_conn_info *cinfo, + const ngtcp2_conn_stat *cstat) { + cinfo->latest_rtt = cstat->latest_rtt; + cinfo->min_rtt = cstat->min_rtt; + cinfo->smoothed_rtt = cstat->smoothed_rtt; + cinfo->rttvar = cstat->rttvar; + cinfo->cwnd = cstat->cwnd; + cinfo->ssthresh = cstat->ssthresh; + cinfo->bytes_in_flight = cstat->bytes_in_flight; + + switch (conn_info_version) { + case NGTCP2_CONN_INFO_V2: + cinfo->pkt_sent = cstat->pkt_sent; + cinfo->bytes_sent = cstat->bytes_sent; + cinfo->pkt_recv = cstat->pkt_recv; + cinfo->bytes_recv = cstat->bytes_recv; + cinfo->pkt_lost = cstat->pkt_lost; + cinfo->bytes_lost = cstat->bytes_lost; + cinfo->ping_recv = cstat->ping_recv; + cinfo->pkt_discarded = cstat->pkt_discarded; + } +} diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn_info.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn_info.h new file mode 100644 index 00000000000000..161309df304314 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn_info.h @@ -0,0 +1,45 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGTCP2_CONN_INFO_H +#define NGTCP2_CONN_INFO_H + +#ifdef HAVE_CONFIG_H +# include +#endif /* defined(HAVE_CONFIG_H) */ + +#include + +typedef struct ngtcp2_conn_stat ngtcp2_conn_stat; + +/* + * ngtcp2_conn_info_init_versioned initializes |cinfo| of version + * |conn_info_version| from |cstat|. This function only fills the + * fields of |cinfo| that are available in the specified version. + */ +void ngtcp2_conn_info_init_versioned(int conn_info_version, + ngtcp2_conn_info *cinfo, + const ngtcp2_conn_stat *cstat); + +#endif /* !defined(NGTCP2_CONN_INFO_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn_stat.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn_stat.h index be041b9086081d..b5fea473910951 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn_stat.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn_stat.h @@ -31,6 +31,8 @@ #include +#include "ngtcp2_pktns_id.h" + /** * @struct * @@ -128,6 +130,43 @@ typedef struct ngtcp2_conn_stat { * scheduled and transmitted together. */ size_t send_quantum; + /* + * pkt_sent is the number of QUIC packets sent. + */ + uint64_t pkt_sent; + /* + * bytes_sent is the number of bytes (the sum of QUIC packet length) + * sent. + */ + uint64_t bytes_sent; + /* + * pkt_recv is the number of QUIC packets received, excluding + * discarded ones. + */ + uint64_t pkt_recv; + /* + * bytes_recv is the number of bytes (the sum of QUIC packet length) + * received, excluding discarded ones. + */ + uint64_t bytes_recv; + /* + * pkt_lost is the number of QUIC packets that are considered lost, + * excluding PMTUD packets. + */ + uint64_t pkt_lost; + /* + * bytes_lost is the number of bytes (the sum of QUIC packet length) + * lost, excluding PMTUD packets. + */ + uint64_t bytes_lost; + /* + * ping_recv is the number of PING frames received. + */ + uint64_t ping_recv; + /* + * pkt_discarded is the number of QUIC packets discarded. + */ + uint64_t pkt_discarded; } ngtcp2_conn_stat; #endif /* !defined(NGTCP2_CONN_STAT_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conv.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conv.c index 6528011cc0edf4..42651cddbbaa9c 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conv.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conv.c @@ -62,34 +62,34 @@ const uint8_t *ngtcp2_get_uint16(uint16_t *dest, const uint8_t *p) { return p + sizeof(*dest); } -static const uint8_t *get_uvarint(uint64_t *dest, const uint8_t *p) { - union { - uint8_t n8; - uint16_t n16; - uint32_t n32; - uint64_t n64; - } n; +const uint8_t *ngtcp2_get_uvarint(uint64_t *dest, const uint8_t *p) { + uint16_t n16; + uint32_t n32; + uint64_t n64; switch (*p >> 6) { case 0: *dest = *p++; return p; case 1: - memcpy(&n, p, 2); - n.n8 &= 0x3f; - *dest = ngtcp2_ntohs(n.n16); + memcpy(&n16, p, 2); + n16 = ngtcp2_ntohs(n16); + n16 &= 0x3FFFU; + *dest = n16; return p + 2; case 2: - memcpy(&n, p, 4); - n.n8 &= 0x3f; - *dest = ngtcp2_ntohl(n.n32); + memcpy(&n32, p, 4); + n32 = ngtcp2_ntohl(n32); + n32 &= 0x3FFFFFFFU; + *dest = n32; return p + 4; case 3: - memcpy(&n, p, 8); - n.n8 &= 0x3f; - *dest = ngtcp2_ntohl64(n.n64); + memcpy(&n64, p, 8); + n64 = ngtcp2_ntohl64(n64); + n64 &= 0x3FFFFFFFFFFFFFFFU; + *dest = n64; return p + 8; default: @@ -97,14 +97,6 @@ static const uint8_t *get_uvarint(uint64_t *dest, const uint8_t *p) { } } -const uint8_t *ngtcp2_get_uvarint(uint64_t *dest, const uint8_t *p) { - return get_uvarint(dest, p); -} - -const uint8_t *ngtcp2_get_varint(int64_t *dest, const uint8_t *p) { - return get_uvarint((uint64_t *)dest, p); -} - int64_t ngtcp2_get_pkt_num(const uint8_t *p, size_t pkt_numlen) { uint32_t l; uint16_t s; @@ -158,17 +150,17 @@ uint8_t *ngtcp2_put_uvarint(uint8_t *p, uint64_t n) { } if (n < 16384) { rv = ngtcp2_put_uint16be(p, (uint16_t)n); - *p |= 0x40; + *p |= 0x40U; return rv; } if (n < 1073741824) { rv = ngtcp2_put_uint32be(p, (uint32_t)n); - *p |= 0x80; + *p |= 0x80U; return rv; } assert(n < 4611686018427387904ULL); rv = ngtcp2_put_uint64be(p, n); - *p |= 0xc0; + *p |= 0xC0U; return rv; } @@ -178,7 +170,7 @@ uint8_t *ngtcp2_put_uvarint30(uint8_t *p, uint32_t n) { assert(n < 1073741824); rv = ngtcp2_put_uint32be(p, n); - *p |= 0x80; + *p |= 0x80U; return rv; } @@ -200,7 +192,7 @@ uint8_t *ngtcp2_put_pkt_num(uint8_t *p, int64_t pkt_num, size_t len) { } size_t ngtcp2_get_uvarintlen(const uint8_t *p) { - return (size_t)(1u << (*p >> 6)); + return (size_t)(1U << (*p >> 6)); } size_t ngtcp2_put_uvarintlen(uint64_t n) { diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conv.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conv.h index ad924683b8dc10..7e7b9f465c5118 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conv.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conv.h @@ -80,7 +80,15 @@ const uint8_t *ngtcp2_get_uvarint(uint64_t *dest, const uint8_t *p); * lost in this cast, because the variable-length integer is 62 * bits. It returns |p| plus the number of bytes read from |p|. */ -const uint8_t *ngtcp2_get_varint(int64_t *dest, const uint8_t *p); +static inline const uint8_t *ngtcp2_get_varint(int64_t *dest, + const uint8_t *p) { + uint64_t n; + + p = ngtcp2_get_uvarint(&n, p); + *dest = (int64_t)n; + + return p; +} /* * ngtcp2_get_pkt_num reads encoded packet number from |p|. The diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_crypto.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_crypto.c index 1f74e8c58397b1..f435870ba47d18 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_crypto.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_crypto.c @@ -28,6 +28,7 @@ #include #include "ngtcp2_net.h" +#include "ngtcp2_str.h" int ngtcp2_crypto_km_new(ngtcp2_crypto_km **pckm, const uint8_t *secret, size_t secretlen, @@ -65,15 +66,21 @@ int ngtcp2_crypto_km_nocopy_new(ngtcp2_crypto_km **pckm, size_t secretlen, } p = (uint8_t *)(*pckm) + sizeof(ngtcp2_crypto_km); - (*pckm)->secret.base = p; - (*pckm)->secret.len = secretlen; - p += secretlen; - (*pckm)->iv.base = p; - (*pckm)->iv.len = ivlen; - (*pckm)->aead_ctx.native_handle = NULL; - (*pckm)->pkt_num = -1; - (*pckm)->use_count = 0; - (*pckm)->flags = NGTCP2_CRYPTO_KM_FLAG_NONE; + + **pckm = (ngtcp2_crypto_km){ + .secret = + { + .base = p, + .len = secretlen, + }, + .iv = + { + .base = p + secretlen, + .len = ivlen, + }, + .pkt_num = -1, + .flags = NGTCP2_CRYPTO_KM_FLAG_NONE, + }; return 0; } @@ -84,13 +91,7 @@ void ngtcp2_crypto_km_del(ngtcp2_crypto_km *ckm, const ngtcp2_mem *mem) { } if (ckm->secret.len) { -#ifdef WIN32 - SecureZeroMemory(ckm->secret.base, ckm->secret.len); -#elif defined(HAVE_EXPLICIT_BZERO) - explicit_bzero(ckm->secret.base, ckm->secret.len); -#elif defined(HAVE_MEMSET_S) - memset_s(ckm->secret.base, ckm->secret.len, 0, ckm->secret.len); -#endif /* defined(HAVE_MEMSET_S) */ + ngtcp2_secure_clear(ckm->secret.base, ckm->secret.len); } ngtcp2_mem_free(mem, ckm); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_crypto.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_crypto.h index ca6d494846f324..c1e786b5fc7b36 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_crypto.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_crypto.h @@ -42,10 +42,10 @@ #define NGTCP2_MAX_AEAD_OVERHEAD 16 /* NGTCP2_CRYPTO_KM_FLAG_NONE indicates that no flag is set. */ -#define NGTCP2_CRYPTO_KM_FLAG_NONE 0x00u +#define NGTCP2_CRYPTO_KM_FLAG_NONE 0x00U /* NGTCP2_CRYPTO_KM_FLAG_KEY_PHASE_ONE is set if key phase bit is set. */ -#define NGTCP2_CRYPTO_KM_FLAG_KEY_PHASE_ONE 0x01u +#define NGTCP2_CRYPTO_KM_FLAG_KEY_PHASE_ONE 0x01U typedef struct ngtcp2_crypto_km { ngtcp2_vec secret; diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_dcidtr.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_dcidtr.c index 170b9f803cccec..0360ba82cbd896 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_dcidtr.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_dcidtr.c @@ -157,9 +157,9 @@ int ngtcp2_dcidtr_bind_dcid(ngtcp2_dcidtr *dtr, ngtcp2_dcid **pdest, return 0; } -int ngtcp2_dcidtr_verify_stateless_reset(const ngtcp2_dcidtr *dtr, - const ngtcp2_path *path, - const uint8_t *token) { +int ngtcp2_dcidtr_verify_stateless_reset( + const ngtcp2_dcidtr *dtr, const ngtcp2_path *path, + const ngtcp2_stateless_reset_token *token) { const ngtcp2_dcid *dcid; const ngtcp2_ringbuf *rb = &dtr->bound.rb; size_t i, len = ngtcp2_ringbuf_len(rb); @@ -176,7 +176,7 @@ int ngtcp2_dcidtr_verify_stateless_reset(const ngtcp2_dcidtr *dtr, static int verify_token_uniqueness(const ngtcp2_ringbuf *rb, int *pfound, uint64_t seq, const ngtcp2_cid *cid, - const uint8_t *token) { + const ngtcp2_stateless_reset_token *token) { const ngtcp2_dcid *dcid; size_t i, len = ngtcp2_ringbuf_len(rb); int rv; @@ -196,9 +196,9 @@ static int verify_token_uniqueness(const ngtcp2_ringbuf *rb, int *pfound, return 0; } -int ngtcp2_dcidtr_verify_token_uniqueness(const ngtcp2_dcidtr *dtr, int *pfound, - uint64_t seq, const ngtcp2_cid *cid, - const uint8_t *token) { +int ngtcp2_dcidtr_verify_token_uniqueness( + const ngtcp2_dcidtr *dtr, int *pfound, uint64_t seq, const ngtcp2_cid *cid, + const ngtcp2_stateless_reset_token *token) { int rv; rv = verify_token_uniqueness(&dtr->bound.rb, pfound, seq, cid, token); @@ -386,7 +386,7 @@ ngtcp2_tstamp ngtcp2_dcidtr_earliest_bound_ts(const ngtcp2_dcidtr *dtr) { assert(dcid->cid.datalen); assert(dcid->bound_ts != UINT64_MAX); - res = ngtcp2_min_uint64(res, dcid->bound_ts); + res = ngtcp2_min(res, dcid->bound_ts); } return res; @@ -406,7 +406,8 @@ ngtcp2_tstamp ngtcp2_dcidtr_earliest_retired_ts(const ngtcp2_dcidtr *dtr) { } void ngtcp2_dcidtr_push_unused(ngtcp2_dcidtr *dtr, uint64_t seq, - const ngtcp2_cid *cid, const uint8_t *token) { + const ngtcp2_cid *cid, + const ngtcp2_stateless_reset_token *token) { ngtcp2_dcid *dcid = ngtcp2_ringbuf_push_back(&dtr->unused.rb); ngtcp2_dcid_init(dcid, seq, cid, token); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_dcidtr.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_dcidtr.h index 63043427bc0900..945ed12bae145f 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_dcidtr.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_dcidtr.h @@ -154,9 +154,9 @@ int ngtcp2_dcidtr_bind_dcid(ngtcp2_dcidtr *dtr, ngtcp2_dcid **pdest, * There is no Destination Connection ID that matches the given * |path| and |token|. */ -int ngtcp2_dcidtr_verify_stateless_reset(const ngtcp2_dcidtr *dtr, - const ngtcp2_path *path, - const uint8_t *token); +int ngtcp2_dcidtr_verify_stateless_reset( + const ngtcp2_dcidtr *dtr, const ngtcp2_path *path, + const ngtcp2_stateless_reset_token *token); /* * ngtcp2_dcidtr_verify_token_uniqueness verifies that the uniqueness @@ -180,9 +180,9 @@ int ngtcp2_dcidtr_verify_stateless_reset(const ngtcp2_dcidtr *dtr, * The given combination of values does not satisfy the above * conditions. */ -int ngtcp2_dcidtr_verify_token_uniqueness(const ngtcp2_dcidtr *dtr, int *pfound, - uint64_t seq, const ngtcp2_cid *cid, - const uint8_t *token); +int ngtcp2_dcidtr_verify_token_uniqueness( + const ngtcp2_dcidtr *dtr, int *pfound, uint64_t seq, const ngtcp2_cid *cid, + const ngtcp2_stateless_reset_token *token); /* * ngtcp2_dcidtr_retire_inactive_dcid_prior_to retires inactive @@ -273,7 +273,8 @@ ngtcp2_tstamp ngtcp2_dcidtr_earliest_retired_ts(const ngtcp2_dcidtr *dtr); * buffer space is full, the earliest ngtcp2_dcid is removed. */ void ngtcp2_dcidtr_push_unused(ngtcp2_dcidtr *dtr, uint64_t seq, - const ngtcp2_cid *cid, const uint8_t *token); + const ngtcp2_cid *cid, + const ngtcp2_stateless_reset_token *token); /* * ngtcp2_dcidtr_pop_unused_cid_token removes an unused Destination diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_fmt.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_fmt.c new file mode 100644 index 00000000000000..63899f7e03932b --- /dev/null +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_fmt.c @@ -0,0 +1,112 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "ngtcp2_fmt.h" + +#include + +#include "ngtcp2_str.h" + +char *ngtcp2_fmt_write_int64(char *dest, int64_t n) { + if (n < 0) { + *dest++ = '-'; + return ngtcp2_fmt_write_uint64(dest, 0 - (uint64_t)n); + } + + return ngtcp2_fmt_write_uint64(dest, (uint64_t)n); +} + +char *ngtcp2_fmt_write_uint64(char *dest, uint64_t n) { + return (char *)ngtcp2_encode_uint((uint8_t *)dest, n); +} + +char *ngtcp2_fmt_write_char(char *dest, char c) { + *dest++ = c; + + return dest; +} + +char *ngtcp2_fmt_write_str(char *dest, const char *s) { + return ngtcp2_cpymem(dest, s, strlen(s)); +} + +static char *fill_zero(char *dest, size_t len, size_t width) { + if (len < width) { + return (char *)ngtcp2_setmem((uint8_t *)dest, '0', width - len); + } + + return dest; +} + +char *ngtcp2_fmt_write_uint64w(char *dest, ngtcp2_fmt_uint64w f) { + size_t len = ngtcp2_encode_uintlen(f.n); + + return (char *)ngtcp2_encode_uint((uint8_t *)fill_zero(dest, len, f.width), + f.n); +} + +char *ngtcp2_fmt_write_hex(char *dest, ngtcp2_fmt_hex f) { + return (char *)ngtcp2_encode_uint_hex((uint8_t *)dest, f.n); +} + +char *ngtcp2_fmt_write_hexw(char *dest, ngtcp2_fmt_hexw f) { + size_t len = ngtcp2_encode_uint_hexlen(f.n); + + return (char *)ngtcp2_encode_uint_hex( + (uint8_t *)fill_zero(dest, len, f.width), f.n); +} + +char *ngtcp2_fmt_write_cid(char *dest, const ngtcp2_cid *cid) { + return (char *)ngtcp2_encode_hex((uint8_t *)dest, cid->data, cid->datalen); +} + +char *ngtcp2_fmt_write_stateless_reset_token( + char *dest, const ngtcp2_stateless_reset_token *token) { + return (char *)ngtcp2_encode_hex((uint8_t *)dest, token->data, + sizeof(token->data)); +} + +char * +ngtcp2_fmt_write_path_challenge_data(char *dest, + const ngtcp2_path_challenge_data *data) { + return (char *)ngtcp2_encode_hex((uint8_t *)dest, data->data, + sizeof(data->data)); +} + +char *ngtcp2_fmt_write_bhex(char *dest, ngtcp2_fmt_bhex f) { + return (char *)ngtcp2_encode_hex((uint8_t *)dest, f.data, f.len); +} + +char *ngtcp2_fmt_write_in_addr(char *dest, const ngtcp2_in_addr *addr) { + return (char *)ngtcp2_encode_ipv4((uint8_t *)dest, addr); +} + +char *ngtcp2_fmt_write_in6_addr(char *dest, const ngtcp2_in6_addr *addr) { + return (char *)ngtcp2_encode_ipv6((uint8_t *)dest, addr); +} + +char *ngtcp2_fmt_write_printable_ascii(char *dest, + const ngtcp2_fmt_printable_ascii f) { + return (char *)ngtcp2_encode_printable_ascii((uint8_t *)dest, f.data, f.len); +} diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_fmt.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_fmt.h new file mode 100644 index 00000000000000..9d01ffc80b903f --- /dev/null +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_fmt.h @@ -0,0 +1,1211 @@ +/* + * ngtcp2 + * + * Copyright (c) 2026 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGTCP2_FMT_H +#define NGTCP2_FMT_H + +#ifdef HAVE_CONFIG_H +# include +#endif /* defined(HAVE_CONFIG_H) */ + +#include + +typedef struct ngtcp2_fmt_hex { + uint64_t n; +} ngtcp2_fmt_hex; + +static inline ngtcp2_fmt_hex ngtcp2_fmt_hex_init(uint64_t n) { + return (ngtcp2_fmt_hex){ + .n = n, + }; +} + +static inline ngtcp2_fmt_hex ngtcp2_fmt_hex_signed_init(int64_t n) { + return ngtcp2_fmt_hex_init((uint64_t)n); +} + +static inline ngtcp2_fmt_hex ngtcp2_fmt_hex_long_int_init(long int n) { + return ngtcp2_fmt_hex_init((unsigned long int)n); +} + +static inline ngtcp2_fmt_hex ngtcp2_fmt_hex_int_init(int n) { + return ngtcp2_fmt_hex_init((unsigned int)n); +} + +static inline ngtcp2_fmt_hex ngtcp2_fmt_hex_short_int_init(short int n) { + return ngtcp2_fmt_hex_init((unsigned short int)n); +} + +static inline ngtcp2_fmt_hex ngtcp2_fmt_hex_signed_char_init(signed char n) { + return ngtcp2_fmt_hex_init((unsigned char)n); +} + +static inline ngtcp2_fmt_hex ngtcp2_fmt_hex_char_init(char n) { + return ngtcp2_fmt_hex_init((unsigned char)n); +} + +/* hex formats integral |T| in unsigned hexadecimal notation. It + drops the leading zeros. */ +#define hex(T) \ + _Generic((T), \ + long long int: ngtcp2_fmt_hex_signed_init, \ + long int: ngtcp2_fmt_hex_long_int_init, \ + int: ngtcp2_fmt_hex_int_init, \ + short int: ngtcp2_fmt_hex_short_int_init, \ + signed char: ngtcp2_fmt_hex_signed_char_init, \ + char: ngtcp2_fmt_hex_char_init, \ + unsigned long long int: ngtcp2_fmt_hex_init, \ + unsigned long int: ngtcp2_fmt_hex_init, \ + unsigned int: ngtcp2_fmt_hex_init, \ + unsigned short int: ngtcp2_fmt_hex_init, \ + unsigned char: ngtcp2_fmt_hex_init)((T)) + +typedef struct ngtcp2_fmt_hexw { + uint64_t n; + size_t width; +} ngtcp2_fmt_hexw; + +static inline ngtcp2_fmt_hexw ngtcp2_fmt_hexw_init(uint64_t n, size_t width) { + return (ngtcp2_fmt_hexw){ + .n = n, + .width = width, + }; +} + +static inline ngtcp2_fmt_hexw ngtcp2_fmt_hexw_signed_init(int64_t n, + size_t width) { + return ngtcp2_fmt_hexw_init((uint64_t)n, width); +} + +static inline ngtcp2_fmt_hexw ngtcp2_fmt_hexw_long_int_init(long int n, + size_t width) { + return ngtcp2_fmt_hexw_init((unsigned long int)n, width); +} + +static inline ngtcp2_fmt_hexw ngtcp2_fmt_hexw_int_init(int n, size_t width) { + return ngtcp2_fmt_hexw_init((unsigned int)n, width); +} + +static inline ngtcp2_fmt_hexw ngtcp2_fmt_hexw_short_int_init(short int n, + size_t width) { + return ngtcp2_fmt_hexw_init((unsigned short int)n, width); +} + +static inline ngtcp2_fmt_hexw ngtcp2_fmt_hexw_signed_char_init(signed char n, + size_t width) { + return ngtcp2_fmt_hexw_init((unsigned char)n, width); +} + +static inline ngtcp2_fmt_hexw ngtcp2_fmt_hexw_char_init(char n, size_t width) { + return ngtcp2_fmt_hexw_init((unsigned char)n, width); +} + +/* hex formats integral |T| in unsigned hexadecimal notation. If the + produced value has fewer characters than |WIDTH|, it will be padded + with 0 on the left. */ +#define hexw(T, WIDTH) \ + _Generic((T), \ + long long int: ngtcp2_fmt_hexw_signed_init, \ + long int: ngtcp2_fmt_hexw_long_int_init, \ + int: ngtcp2_fmt_hexw_int_init, \ + short int: ngtcp2_fmt_hexw_short_int_init, \ + signed char: ngtcp2_fmt_hexw_signed_char_init, \ + char: ngtcp2_fmt_hexw_char_init, \ + unsigned long long int: ngtcp2_fmt_hexw_init, \ + unsigned long int: ngtcp2_fmt_hexw_init, \ + unsigned int: ngtcp2_fmt_hexw_init, \ + unsigned short int: ngtcp2_fmt_hexw_init, \ + unsigned char: ngtcp2_fmt_hexw_init)((T), (WIDTH)) + +typedef struct ngtcp2_fmt_uint64w { + uint64_t n; + size_t width; +} ngtcp2_fmt_uint64w; + +/* uintw formats integral |n|. If the produced value has fewer + characters than |width|, it will be padded with 0 on the left. */ +static inline ngtcp2_fmt_uint64w uintw(uint64_t n, size_t width) { + return (ngtcp2_fmt_uint64w){ + .n = n, + .width = width, + }; +} + +typedef struct ngtcp2_fmt_bhex { + const uint8_t *data; + size_t len; +} ngtcp2_fmt_bhex; + +/* bhex formats the binary data [|data|, |data| + |len|) in + hexadecimal notation. */ +static inline ngtcp2_fmt_bhex bhex(const uint8_t *data, size_t len) { + return (ngtcp2_fmt_bhex){ + .data = data, + .len = len, + }; +} + +/* lbhex formats the binary data [|B|, |B| + sizeof(|B|)) in + hexadecimal notation. To make it work, |B| must be an array that + is not decayed to the pointer. */ +#define lbhex(B) bhex((B), sizeof(B)) + +typedef struct ngtcp2_fmt_printable_ascii { + const uint8_t *data; + size_t len; +} ngtcp2_fmt_printable_ascii; + +/* ascii formats the binary data [|data|, |data| + |len|) in such a + way that the printable ASCII characters are copied as is, and the + other characters are converted to '.'. */ +static inline ngtcp2_fmt_printable_ascii ascii(const uint8_t *data, + size_t len) { + return (ngtcp2_fmt_printable_ascii){ + .data = data, + .len = len, + }; +} + +#define ngtcp2_fmt_stringify(M) #M + +/* stringify converts macro |M| to string literal.*/ +#define stringify(M) ngtcp2_fmt_stringify(M) + +char *ngtcp2_fmt_write_int64(char *dest, int64_t n); +char *ngtcp2_fmt_write_uint64(char *dest, uint64_t n); +char *ngtcp2_fmt_write_char(char *dest, char c); +char *ngtcp2_fmt_write_str(char *dest, const char *s); +char *ngtcp2_fmt_write_uint64w(char *dest, ngtcp2_fmt_uint64w f); +char *ngtcp2_fmt_write_hex(char *dest, ngtcp2_fmt_hex f); +char *ngtcp2_fmt_write_hexw(char *dest, ngtcp2_fmt_hexw f); +char *ngtcp2_fmt_write_cid(char *dest, const ngtcp2_cid *cid); +char *ngtcp2_fmt_write_stateless_reset_token( + char *dest, const ngtcp2_stateless_reset_token *token); +char * +ngtcp2_fmt_write_path_challenge_data(char *dest, + const ngtcp2_path_challenge_data *data); +char *ngtcp2_fmt_write_bhex(char *dest, ngtcp2_fmt_bhex f); +char *ngtcp2_fmt_write_in_addr(char *dest, const ngtcp2_in_addr *addr); +char *ngtcp2_fmt_write_in6_addr(char *dest, const ngtcp2_in6_addr *addr); +char *ngtcp2_fmt_write_printable_ascii(char *dest, + const ngtcp2_fmt_printable_ascii f); + +#define NGTCP2_FMT_WRITE_TYPE(DEST, T) \ + _Generic((T), \ + long long int: ngtcp2_fmt_write_int64, \ + long int: ngtcp2_fmt_write_int64, \ + int: ngtcp2_fmt_write_int64, \ + short int: ngtcp2_fmt_write_int64, \ + signed char: ngtcp2_fmt_write_int64, \ + char: ngtcp2_fmt_write_char, \ + unsigned long long int: ngtcp2_fmt_write_uint64, \ + unsigned long int: ngtcp2_fmt_write_uint64, \ + unsigned int: ngtcp2_fmt_write_uint64, \ + unsigned short int: ngtcp2_fmt_write_uint64, \ + unsigned char: ngtcp2_fmt_write_uint64, \ + char *: ngtcp2_fmt_write_str, \ + const char *: ngtcp2_fmt_write_str, \ + ngtcp2_cid *: ngtcp2_fmt_write_cid, \ + const ngtcp2_cid *: ngtcp2_fmt_write_cid, \ + ngtcp2_stateless_reset_token *: ngtcp2_fmt_write_stateless_reset_token, \ + const ngtcp2_stateless_reset_token \ + *: ngtcp2_fmt_write_stateless_reset_token, \ + ngtcp2_path_challenge_data *: ngtcp2_fmt_write_path_challenge_data, \ + const ngtcp2_path_challenge_data *: ngtcp2_fmt_write_path_challenge_data, \ + ngtcp2_in_addr *: ngtcp2_fmt_write_in_addr, \ + const ngtcp2_in_addr *: ngtcp2_fmt_write_in_addr, \ + ngtcp2_in6_addr *: ngtcp2_fmt_write_in6_addr, \ + const ngtcp2_in6_addr *: ngtcp2_fmt_write_in6_addr, \ + ngtcp2_fmt_uint64w: ngtcp2_fmt_write_uint64w, \ + ngtcp2_fmt_hex: ngtcp2_fmt_write_hex, \ + ngtcp2_fmt_hexw: ngtcp2_fmt_write_hexw, \ + ngtcp2_fmt_bhex: ngtcp2_fmt_write_bhex, \ + ngtcp2_fmt_printable_ascii: ngtcp2_fmt_write_printable_ascii)((DEST), (T)) + +/* ngtcp2_fmt_format formats arguments and writes them into the buffer + pointed by |BUF|. It also writes the terminal NUL byte. The + function assumes that the buffer is the large enough. It assigns + the number of bytes written, excluding the terminal NUL, to + |*PNWRITE|. */ + +/* Generated by fmtgen.py */ +#define NGTCP2_FMT_SELECT_WRITE_PACK( \ + _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, \ + _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, \ + _33, _34, _35, _36, _37, _38, _39, _40, PACK, ...) \ + PACK + +#define ngtcp2_fmt_format(BUF, PNWRITE, ...) \ + do { \ + char *fmt_destp = (char *)(BUF); \ + NGTCP2_FMT_SELECT_WRITE_PACK( \ + __VA_ARGS__, NGTCP2_FMT_WRITE_PACK_40, NGTCP2_FMT_WRITE_PACK_39, \ + NGTCP2_FMT_WRITE_PACK_38, NGTCP2_FMT_WRITE_PACK_37, \ + NGTCP2_FMT_WRITE_PACK_36, NGTCP2_FMT_WRITE_PACK_35, \ + NGTCP2_FMT_WRITE_PACK_34, NGTCP2_FMT_WRITE_PACK_33, \ + NGTCP2_FMT_WRITE_PACK_32, NGTCP2_FMT_WRITE_PACK_31, \ + NGTCP2_FMT_WRITE_PACK_30, NGTCP2_FMT_WRITE_PACK_29, \ + NGTCP2_FMT_WRITE_PACK_28, NGTCP2_FMT_WRITE_PACK_27, \ + NGTCP2_FMT_WRITE_PACK_26, NGTCP2_FMT_WRITE_PACK_25, \ + NGTCP2_FMT_WRITE_PACK_24, NGTCP2_FMT_WRITE_PACK_23, \ + NGTCP2_FMT_WRITE_PACK_22, NGTCP2_FMT_WRITE_PACK_21, \ + NGTCP2_FMT_WRITE_PACK_20, NGTCP2_FMT_WRITE_PACK_19, \ + NGTCP2_FMT_WRITE_PACK_18, NGTCP2_FMT_WRITE_PACK_17, \ + NGTCP2_FMT_WRITE_PACK_16, NGTCP2_FMT_WRITE_PACK_15, \ + NGTCP2_FMT_WRITE_PACK_14, NGTCP2_FMT_WRITE_PACK_13, \ + NGTCP2_FMT_WRITE_PACK_12, NGTCP2_FMT_WRITE_PACK_11, \ + NGTCP2_FMT_WRITE_PACK_10, NGTCP2_FMT_WRITE_PACK_9, \ + NGTCP2_FMT_WRITE_PACK_8, NGTCP2_FMT_WRITE_PACK_7, \ + NGTCP2_FMT_WRITE_PACK_6, NGTCP2_FMT_WRITE_PACK_5, \ + NGTCP2_FMT_WRITE_PACK_4, NGTCP2_FMT_WRITE_PACK_3, \ + NGTCP2_FMT_WRITE_PACK_2, \ + NGTCP2_FMT_WRITE_PACK_1)(fmt_destp, __VA_ARGS__); \ + *fmt_destp = '\0'; \ + *(PNWRITE) = (size_t)(fmt_destp - (char *)(BUF)); \ + } while (0) + +#define NGTCP2_FMT_WRITE_PACK_1(DEST, _1) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)) +#define NGTCP2_FMT_WRITE_PACK_2(DEST, _1, _2) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)) +#define NGTCP2_FMT_WRITE_PACK_3(DEST, _1, _2, _3) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)) +#define NGTCP2_FMT_WRITE_PACK_4(DEST, _1, _2, _3, _4) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)) +#define NGTCP2_FMT_WRITE_PACK_5(DEST, _1, _2, _3, _4, _5) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)) +#define NGTCP2_FMT_WRITE_PACK_6(DEST, _1, _2, _3, _4, _5, _6) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)) +#define NGTCP2_FMT_WRITE_PACK_7(DEST, _1, _2, _3, _4, _5, _6, _7) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)) +#define NGTCP2_FMT_WRITE_PACK_8(DEST, _1, _2, _3, _4, _5, _6, _7, _8) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)) +#define NGTCP2_FMT_WRITE_PACK_9(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)) +#define NGTCP2_FMT_WRITE_PACK_10(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)) +#define NGTCP2_FMT_WRITE_PACK_11(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)) +#define NGTCP2_FMT_WRITE_PACK_12(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)) +#define NGTCP2_FMT_WRITE_PACK_13(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)) +#define NGTCP2_FMT_WRITE_PACK_14(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)) +#define NGTCP2_FMT_WRITE_PACK_15(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)) +#define NGTCP2_FMT_WRITE_PACK_16(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)) +#define NGTCP2_FMT_WRITE_PACK_17(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)) +#define NGTCP2_FMT_WRITE_PACK_18(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)) +#define NGTCP2_FMT_WRITE_PACK_19(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)) +#define NGTCP2_FMT_WRITE_PACK_20(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)) +#define NGTCP2_FMT_WRITE_PACK_21(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)) +#define NGTCP2_FMT_WRITE_PACK_22(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21, _22) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)) +#define NGTCP2_FMT_WRITE_PACK_23(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21, _22, _23) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)) +#define NGTCP2_FMT_WRITE_PACK_24(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21, _22, _23, _24) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)) +#define NGTCP2_FMT_WRITE_PACK_25(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21, _22, _23, _24, _25) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)) +#define NGTCP2_FMT_WRITE_PACK_26(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21, _22, _23, _24, _25, _26) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)) +#define NGTCP2_FMT_WRITE_PACK_27(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21, _22, _23, _24, _25, _26, _27) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)) +#define NGTCP2_FMT_WRITE_PACK_28( \ + DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, \ + _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)) +#define NGTCP2_FMT_WRITE_PACK_29( \ + DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, \ + _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)) +#define NGTCP2_FMT_WRITE_PACK_30( \ + DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, \ + _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_30)) +#define NGTCP2_FMT_WRITE_PACK_31( \ + DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, \ + _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_30)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_31)) +#define NGTCP2_FMT_WRITE_PACK_32(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21, _22, _23, _24, _25, _26, _27, \ + _28, _29, _30, _31, _32) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_30)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_31)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_32)) +#define NGTCP2_FMT_WRITE_PACK_33(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21, _22, _23, _24, _25, _26, _27, \ + _28, _29, _30, _31, _32, _33) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_30)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_31)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_32)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_33)) +#define NGTCP2_FMT_WRITE_PACK_34(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21, _22, _23, _24, _25, _26, _27, \ + _28, _29, _30, _31, _32, _33, _34) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_30)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_31)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_32)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_33)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_34)) +#define NGTCP2_FMT_WRITE_PACK_35(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21, _22, _23, _24, _25, _26, _27, \ + _28, _29, _30, _31, _32, _33, _34, _35) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_30)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_31)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_32)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_33)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_34)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_35)) +#define NGTCP2_FMT_WRITE_PACK_36(DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, \ + _19, _20, _21, _22, _23, _24, _25, _26, _27, \ + _28, _29, _30, _31, _32, _33, _34, _35, _36) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_30)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_31)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_32)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_33)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_34)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_35)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_36)) +#define NGTCP2_FMT_WRITE_PACK_37( \ + DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, \ + _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, \ + _32, _33, _34, _35, _36, _37) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_30)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_31)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_32)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_33)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_34)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_35)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_36)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_37)) +#define NGTCP2_FMT_WRITE_PACK_38( \ + DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, \ + _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, \ + _32, _33, _34, _35, _36, _37, _38) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_30)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_31)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_32)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_33)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_34)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_35)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_36)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_37)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_38)) +#define NGTCP2_FMT_WRITE_PACK_39( \ + DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, \ + _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, \ + _32, _33, _34, _35, _36, _37, _38, _39) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_30)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_31)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_32)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_33)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_34)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_35)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_36)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_37)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_38)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_39)) +#define NGTCP2_FMT_WRITE_PACK_40( \ + DEST, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, \ + _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, \ + _32, _33, _34, _35, _36, _37, _38, _39, _40) \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_1)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_2)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_3)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_4)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_5)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_6)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_7)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_8)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_9)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_10)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_11)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_12)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_13)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_14)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_15)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_16)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_17)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_18)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_19)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_20)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_21)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_22)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_23)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_24)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_25)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_26)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_27)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_28)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_29)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_30)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_31)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_32)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_33)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_34)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_35)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_36)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_37)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_38)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_39)); \ + (DEST) = NGTCP2_FMT_WRITE_TYPE((DEST), (_40)) + +#endif /* !defined(NGTCP2_FMT_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.c index 0f6b06a788d174..ec0839b7a4d126 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.c @@ -36,7 +36,7 @@ int ngtcp2_frame_chain_objalloc_new(ngtcp2_frame_chain **pfrc, return NGTCP2_ERR_NOMEM; } - ngtcp2_frame_chain_init(*pfrc); + ngtcp2_frame_chain_init(*pfrc, NGTCP2_FRAME_CHAIN_FLAG_NONE); return 0; } @@ -48,7 +48,7 @@ int ngtcp2_frame_chain_extralen_new(ngtcp2_frame_chain **pfrc, size_t extralen, return NGTCP2_ERR_NOMEM; } - ngtcp2_frame_chain_init(*pfrc); + ngtcp2_frame_chain_init(*pfrc, NGTCP2_FRAME_CHAIN_FLAG_MALLOC); return 0; } @@ -57,14 +57,26 @@ int ngtcp2_frame_chain_stream_datacnt_objalloc_new(ngtcp2_frame_chain **pfrc, size_t datacnt, ngtcp2_objalloc *objalloc, const ngtcp2_mem *mem) { + int rv; + if (datacnt > NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES) { - return ngtcp2_frame_chain_extralen_new(pfrc, - sizeof(ngtcp2_vec) * (datacnt - 1) - - NGTCP2_FRAME_CHAIN_STREAM_AVAIL, - mem); + rv = ngtcp2_frame_chain_extralen_new( + pfrc, + sizeof(ngtcp2_vec) * (datacnt - NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES), + mem); + } else { + rv = ngtcp2_frame_chain_objalloc_new(pfrc, objalloc); + } + + if (rv != 0) { + return rv; } - return ngtcp2_frame_chain_objalloc_new(pfrc, objalloc); + (*pfrc)->fr.stream.data = + (ngtcp2_vec *)(void *)((uint8_t *)*pfrc + + offsetof(ngtcp2_frame_chain, buf)); + + return 0; } int ngtcp2_frame_chain_new_token_objalloc_new(ngtcp2_frame_chain **pfrc, @@ -87,9 +99,9 @@ int ngtcp2_frame_chain_new_token_objalloc_new(ngtcp2_frame_chain **pfrc, } fr = &(*pfrc)->fr; - fr->type = NGTCP2_FRAME_NEW_TOKEN; + fr->new_token.type = NGTCP2_FRAME_NEW_TOKEN; - p = (uint8_t *)fr + sizeof(ngtcp2_new_token); + p = (uint8_t *)*pfrc + offsetof(ngtcp2_frame_chain, buf); memcpy(p, token, tokenlen); fr->new_token.token = p; @@ -98,21 +110,6 @@ int ngtcp2_frame_chain_new_token_objalloc_new(ngtcp2_frame_chain **pfrc, return 0; } -void ngtcp2_frame_chain_del(ngtcp2_frame_chain *frc, const ngtcp2_mem *mem) { - ngtcp2_frame_chain_binder *binder; - - if (frc == NULL) { - return; - } - - binder = frc->binder; - if (binder && --binder->refcount == 0) { - ngtcp2_mem_free(mem, binder); - } - - ngtcp2_mem_free(mem, frc); -} - void ngtcp2_frame_chain_objalloc_del(ngtcp2_frame_chain *frc, ngtcp2_objalloc *objalloc, const ngtcp2_mem *mem) { @@ -122,39 +119,23 @@ void ngtcp2_frame_chain_objalloc_del(ngtcp2_frame_chain *frc, return; } - switch (frc->fr.type) { - case NGTCP2_FRAME_CRYPTO: - case NGTCP2_FRAME_STREAM: - if (frc->fr.stream.datacnt > NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES) { - ngtcp2_frame_chain_del(frc, mem); - - return; - } - - break; - case NGTCP2_FRAME_NEW_TOKEN: - if (frc->fr.new_token.tokenlen > NGTCP2_FRAME_CHAIN_NEW_TOKEN_THRES) { - ngtcp2_frame_chain_del(frc, mem); - - return; - } - - break; - } - binder = frc->binder; if (binder && --binder->refcount == 0) { ngtcp2_mem_free(mem, binder); } - frc->binder = NULL; + if (frc->flags & NGTCP2_FRAME_CHAIN_FLAG_MALLOC) { + ngtcp2_mem_free(mem, frc); + return; + } ngtcp2_objalloc_frame_chain_release(objalloc, frc); } -void ngtcp2_frame_chain_init(ngtcp2_frame_chain *frc) { +void ngtcp2_frame_chain_init(ngtcp2_frame_chain *frc, uint32_t flags) { frc->next = NULL; frc->binder = NULL; + frc->flags = flags; } void ngtcp2_frame_chain_list_objalloc_del(ngtcp2_frame_chain *frc, diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.h index 01f07cfa4d95ed..4b3c300060a45b 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.h @@ -36,10 +36,10 @@ /* NGTCP2_FRAME_CHAIN_BINDER_FLAG_NONE indicates that no flag is set. */ -#define NGTCP2_FRAME_CHAIN_BINDER_FLAG_NONE 0x00u +#define NGTCP2_FRAME_CHAIN_BINDER_FLAG_NONE 0x00U /* NGTCP2_FRAME_CHAIN_BINDER_FLAG_ACK indicates that an information which a frame carries has been acknowledged. */ -#define NGTCP2_FRAME_CHAIN_BINDER_FLAG_ACK 0x01u +#define NGTCP2_FRAME_CHAIN_BINDER_FLAG_ACK 0x01U /* * ngtcp2_frame_chain_binder binds 2 or more of ngtcp2_frame_chain to @@ -57,6 +57,25 @@ typedef struct ngtcp2_frame_chain_binder { int ngtcp2_frame_chain_binder_new(ngtcp2_frame_chain_binder **pbinder, const ngtcp2_mem *mem); +/* NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES is the number of datacnt + that changes allocation method. If datacnt is more than this + value, ngtcp2_frame_chain is allocated without ngtcp2_objalloc. + Otherwise, it is allocated using ngtcp2_objalloc. */ +#define NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES 4 + +/* NGTCP2_FRAME_CHAIN_NEW_TOKEN_THRES is the length of a token that + changes allocation method. If the length is more than this value, + ngtcp2_frame_chain is allocated without ngtcp2_objalloc. + Otherwise, it is allocated using ngtcp2_objalloc. */ +#define NGTCP2_FRAME_CHAIN_NEW_TOKEN_THRES \ + (NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES * sizeof(ngtcp2_vec)) + +/* NGTCP2_FRAME_CHAIN_FLAG_NONE indicates no flag is set. */ +#define NGTCP2_FRAME_CHAIN_FLAG_NONE 0x0U +/* NGTCP2_FRAME_CHAIN_FLAG_MALLOC indicates that ngtcp2_frame_chain is + allocated by ngtcp2_mem_malloc. */ +#define NGTCP2_FRAME_CHAIN_FLAG_MALLOC 0x1U + typedef struct ngtcp2_frame_chain ngtcp2_frame_chain; /* @@ -67,7 +86,9 @@ struct ngtcp2_frame_chain { struct { ngtcp2_frame_chain *next; ngtcp2_frame_chain_binder *binder; + uint32_t flags; ngtcp2_frame fr; + uint8_t buf[sizeof(ngtcp2_vec) * NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES]; }; ngtcp2_opl_entry oplent; @@ -104,26 +125,6 @@ int ngtcp2_frame_chain_objalloc_new(ngtcp2_frame_chain **pfrc, int ngtcp2_frame_chain_extralen_new(ngtcp2_frame_chain **pfrc, size_t extralen, const ngtcp2_mem *mem); -/* NGTCP2_FRAME_CHAIN_STREAM_AVAIL is the number of additional bytes - available after ngtcp2_stream when it is embedded in - ngtcp2_frame. */ -#define NGTCP2_FRAME_CHAIN_STREAM_AVAIL \ - (sizeof(ngtcp2_frame) - sizeof(ngtcp2_stream)) - -/* NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES is the number of datacnt - that changes allocation method. If datacnt is more than this - value, ngtcp2_frame_chain is allocated without ngtcp2_objalloc. - Otherwise, it is allocated using ngtcp2_objalloc. */ -#define NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES \ - (NGTCP2_FRAME_CHAIN_STREAM_AVAIL / sizeof(ngtcp2_vec) + 1) - -/* NGTCP2_FRAME_CHAIN_NEW_TOKEN_THRES is the length of a token that - changes allocation method. If the length is more than this value, - ngtcp2_frame_chain is allocated without ngtcp2_objalloc. - Otherwise, it is allocated using ngtcp2_objalloc. */ -#define NGTCP2_FRAME_CHAIN_NEW_TOKEN_THRES \ - (sizeof(ngtcp2_frame) - sizeof(ngtcp2_new_token)) - /* * ngtcp2_frame_chain_stream_datacnt_objalloc_new allocates enough * data to store additional |datacnt| - 1 ngtcp2_vec object after @@ -131,8 +132,6 @@ int ngtcp2_frame_chain_extralen_new(ngtcp2_frame_chain **pfrc, size_t extralen, * words, |datacnt| <= NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES, * ngtcp2_frame_chain_objalloc_new is called internally. Otherwise, * ngtcp2_frame_chain_extralen_new is used and objalloc is not used. - * Therefore, it is important to call ngtcp2_frame_chain_objalloc_del - * without changing datacnt field. */ int ngtcp2_frame_chain_stream_datacnt_objalloc_new(ngtcp2_frame_chain **pfrc, size_t datacnt, @@ -153,24 +152,19 @@ int ngtcp2_frame_chain_new_token_objalloc_new(ngtcp2_frame_chain **pfrc, const ngtcp2_mem *mem); /* - * ngtcp2_frame_chain_del deallocates |frc|. It also deallocates the - * memory pointed by |frc|. - */ -void ngtcp2_frame_chain_del(ngtcp2_frame_chain *frc, const ngtcp2_mem *mem); - -/* - * ngtcp2_frame_chain_objalloc_del adds |frc| to |objalloc| for reuse. - * It might just delete |frc| depending on the frame type and the size - * of |frc|. + * ngtcp2_frame_chain_objalloc_del adds |frc| to |objalloc| for reuse + * if NGTCP2_FRAME_CHAIN_FLAG_MALLOC is not set in |frc|->flags. + * Otherwise, it deletes |frc|. */ void ngtcp2_frame_chain_objalloc_del(ngtcp2_frame_chain *frc, ngtcp2_objalloc *objalloc, const ngtcp2_mem *mem); /* - * ngtcp2_frame_chain_init initializes |frc|. + * ngtcp2_frame_chain_init initializes |frc|. |flags| is bitwise-OR + * of zero or more of NGTCP2_FRAME_CHAIN_FLAG_*. */ -void ngtcp2_frame_chain_init(ngtcp2_frame_chain *frc); +void ngtcp2_frame_chain_init(ngtcp2_frame_chain *frc, uint32_t flags); /* * ngtcp2_frame_chain_list_objalloc_del adds all ngtcp2_frame_chain diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_gaptr.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_gaptr.c index d04b9634c20528..a2ec5ffa376acd 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_gaptr.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_gaptr.c @@ -35,11 +35,11 @@ void ngtcp2_gaptr_init(ngtcp2_gaptr *gaptr, const ngtcp2_mem *mem) { } static int gaptr_gap_init(ngtcp2_gaptr *gaptr) { - ngtcp2_range range = { + static const ngtcp2_range end = { .end = UINT64_MAX, }; - return ngtcp2_ksl_insert(&gaptr->gap, NULL, &range, NULL); + return ngtcp2_ksl_insert(&gaptr->gap, NULL, &end, NULL); } void ngtcp2_gaptr_free(ngtcp2_gaptr *gaptr) { @@ -116,10 +116,6 @@ uint64_t ngtcp2_gaptr_first_gap_offset(const ngtcp2_gaptr *gaptr) { ngtcp2_range ngtcp2_gaptr_get_first_gap_after(const ngtcp2_gaptr *gaptr, uint64_t offset) { - ngtcp2_range q = { - .begin = offset, - .end = offset + 1, - }; ngtcp2_ksl_it it; if (ngtcp2_ksl_len(&gaptr->gap) == 0) { @@ -129,7 +125,11 @@ ngtcp2_range ngtcp2_gaptr_get_first_gap_after(const ngtcp2_gaptr *gaptr, return r; } - it = ngtcp2_ksl_lower_bound_search(&gaptr->gap, &q, + it = ngtcp2_ksl_lower_bound_search(&gaptr->gap, + &(ngtcp2_range){ + .begin = offset, + .end = offset + 1, + }, ngtcp2_ksl_range_exclusive_search); assert(!ngtcp2_ksl_it_end(&it)); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ksl.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ksl.c index 22c131a1ac677c..b88985234e56ca 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ksl.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ksl.c @@ -31,71 +31,77 @@ #include "ngtcp2_macro.h" #include "ngtcp2_mem.h" -#include "ngtcp2_range.h" static ngtcp2_ksl_blk null_blk; ngtcp2_objalloc_def(ksl_blk, ngtcp2_ksl_blk, oplent) -static size_t ksl_nodelen(size_t keylen) { - assert(keylen >= sizeof(uint64_t)); - - return (sizeof(ngtcp2_ksl_node) + keylen - sizeof(uint64_t) + 0x7u) & - ~(uintptr_t)0x7u; -} - -static size_t ksl_blklen(size_t nodelen) { - return sizeof(ngtcp2_ksl_blk) + nodelen * NGTCP2_KSL_MAX_NBLK - - sizeof(uint64_t); +static size_t ksl_blklen(size_t aligned_keylen) { + return sizeof(ngtcp2_ksl_blk) + NGTCP2_KSL_MAX_NBLK * aligned_keylen; } /* - * ksl_node_set_key sets |key| to |node|. + * ksl_set_nth_key sets |key| to |n|th node under |blk|. */ -static void ksl_node_set_key(ngtcp2_ksl *ksl, ngtcp2_ksl_node *node, - const void *key) { - memcpy(node->key, key, ksl->keylen); +static void ksl_set_nth_key(const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, + size_t n, const ngtcp2_ksl_key *key) { + memcpy(blk->keys + n * ksl->aligned_keylen, key, ksl->keylen); } void ngtcp2_ksl_init(ngtcp2_ksl *ksl, ngtcp2_ksl_compar compar, ngtcp2_ksl_search search, size_t keylen, const ngtcp2_mem *mem) { - size_t nodelen = ksl_nodelen(keylen); + size_t aligned_keylen; + + assert(keylen >= sizeof(uint64_t)); + + aligned_keylen = (keylen + 0x7U) & ~(size_t)0x7U; + + assert(aligned_keylen <= UINT16_MAX); - ngtcp2_objalloc_init(&ksl->blkalloc, - (ksl_blklen(nodelen) + 0xfu) & ~(uintptr_t)0xfu, mem); + ngtcp2_objalloc_init( + &ksl->blkalloc, (ksl_blklen(aligned_keylen) + 0xFU) & ~(size_t)0xFU, mem); - ksl->head = NULL; + ksl->root = NULL; ksl->front = ksl->back = NULL; ksl->compar = compar; ksl->search = search; ksl->n = 0; ksl->keylen = keylen; - ksl->nodelen = nodelen; + ksl->aligned_keylen = aligned_keylen; } static ngtcp2_ksl_blk *ksl_blk_objalloc_new(ngtcp2_ksl *ksl) { - return ngtcp2_objalloc_ksl_blk_len_get(&ksl->blkalloc, - ksl_blklen(ksl->nodelen)); + ngtcp2_ksl_blk *blk = ngtcp2_objalloc_ksl_blk_len_get( + &ksl->blkalloc, ksl_blklen(ksl->aligned_keylen)); + + if (!blk) { + return NULL; + } + + blk->keys = (uint8_t *)blk + sizeof(*blk); + blk->aligned_keylen = (uint16_t)ksl->aligned_keylen; + + return blk; } static void ksl_blk_objalloc_del(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk) { ngtcp2_objalloc_ksl_blk_release(&ksl->blkalloc, blk); } -static int ksl_head_init(ngtcp2_ksl *ksl) { - ngtcp2_ksl_blk *head = ksl_blk_objalloc_new(ksl); +static int ksl_root_init(ngtcp2_ksl *ksl) { + ngtcp2_ksl_blk *root = ksl_blk_objalloc_new(ksl); - if (!head) { + if (!root) { return NGTCP2_ERR_NOMEM; } - head->next = head->prev = NULL; - head->n = 0; - head->leaf = 1; + root->next = root->prev = NULL; + root->n = 0; + root->leaf = 1; - ksl->head = head; - ksl->front = ksl->back = head; + ksl->root = root; + ksl->front = ksl->back = root; return 0; } @@ -109,7 +115,7 @@ static void ksl_free_blk(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk) { if (!blk->leaf) { for (i = 0; i < blk->n; ++i) { - ksl_free_blk(ksl, ngtcp2_ksl_nth_node(ksl, blk, i)->blk); + ksl_free_blk(ksl, blk->nodes[i].blk); } } @@ -118,12 +124,12 @@ static void ksl_free_blk(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk) { #endif /* defined(NOMEMPOOL) */ void ngtcp2_ksl_free(ngtcp2_ksl *ksl) { - if (!ksl || !ksl->head) { + if (!ksl || !ksl->root) { return; } #ifdef NOMEMPOOL - ksl_free_blk(ksl, ksl->head); + ksl_free_blk(ksl, ksl->root); #endif /* defined(NOMEMPOOL) */ ngtcp2_objalloc_free(&ksl->blkalloc); @@ -160,8 +166,10 @@ static ngtcp2_ksl_blk *ksl_split_blk(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk) { rblk->n = blk->n / 2; blk->n -= rblk->n; - memcpy(rblk->nodes, blk->nodes + ksl->nodelen * blk->n, - ksl->nodelen * rblk->n); + memcpy(rblk->nodes, blk->nodes + blk->n, rblk->n * sizeof(ngtcp2_ksl_node)); + + memcpy(rblk->keys, blk->keys + blk->n * ksl->aligned_keylen, + rblk->n * ksl->aligned_keylen); assert(blk->n >= NGTCP2_KSL_MIN_NBLK); assert(rblk->n >= NGTCP2_KSL_MIN_NBLK); @@ -181,32 +189,31 @@ static ngtcp2_ksl_blk *ksl_split_blk(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk) { * Out of memory. */ static int ksl_split_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { - ngtcp2_ksl_node *node; - ngtcp2_ksl_blk *lblk = ngtcp2_ksl_nth_node(ksl, blk, i)->blk, *rblk; + ngtcp2_ksl_blk *lblk = blk->nodes[i].blk, *rblk; rblk = ksl_split_blk(ksl, lblk); if (rblk == NULL) { return NGTCP2_ERR_NOMEM; } - memmove(blk->nodes + (i + 2) * ksl->nodelen, - blk->nodes + (i + 1) * ksl->nodelen, - ksl->nodelen * (blk->n - (i + 1))); + memmove(blk->nodes + (i + 2), blk->nodes + (i + 1), + (blk->n - (i + 1)) * sizeof(ngtcp2_ksl_node)); + + memmove(blk->keys + (i + 1) * ksl->aligned_keylen, + blk->keys + i * ksl->aligned_keylen, + (blk->n - i) * ksl->aligned_keylen); - node = ngtcp2_ksl_nth_node(ksl, blk, i + 1); - node->blk = rblk; + blk->nodes[i + 1].blk = rblk; ++blk->n; - ksl_node_set_key(ksl, node, ngtcp2_ksl_nth_node(ksl, rblk, rblk->n - 1)->key); - node = ngtcp2_ksl_nth_node(ksl, blk, i); - ksl_node_set_key(ksl, node, ngtcp2_ksl_nth_node(ksl, lblk, lblk->n - 1)->key); + ksl_set_nth_key(ksl, blk, i, ngtcp2_ksl_blk_nth_key(lblk, lblk->n - 1)); return 0; } /* - * ksl_split_head splits a head (root) block. It increases the height - * of skip list by 1. + * ksl_split_root splits a root block. It increases the height of + * skip list by 1. * * It returns 0 if it succeeds, or one of the following negative error * codes: @@ -214,37 +221,34 @@ static int ksl_split_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { * NGTCP2_ERR_NOMEM * Out of memory. */ -static int ksl_split_head(ngtcp2_ksl *ksl) { - ngtcp2_ksl_blk *rblk = NULL, *lblk, *nhead = NULL; - ngtcp2_ksl_node *node; +static int ksl_split_root(ngtcp2_ksl *ksl) { + ngtcp2_ksl_blk *rblk = NULL, *lblk, *nroot = NULL; - rblk = ksl_split_blk(ksl, ksl->head); + rblk = ksl_split_blk(ksl, ksl->root); if (rblk == NULL) { return NGTCP2_ERR_NOMEM; } - lblk = ksl->head; + lblk = ksl->root; - nhead = ksl_blk_objalloc_new(ksl); + nroot = ksl_blk_objalloc_new(ksl); - if (nhead == NULL) { + if (nroot == NULL) { ksl_blk_objalloc_del(ksl, rblk); return NGTCP2_ERR_NOMEM; } - nhead->next = nhead->prev = NULL; - nhead->n = 2; - nhead->leaf = 0; + nroot->next = nroot->prev = NULL; + nroot->n = 2; + nroot->leaf = 0; - node = ngtcp2_ksl_nth_node(ksl, nhead, 0); - ksl_node_set_key(ksl, node, ngtcp2_ksl_nth_node(ksl, lblk, lblk->n - 1)->key); - node->blk = lblk; + ksl_set_nth_key(ksl, nroot, 0, ngtcp2_ksl_blk_nth_key(lblk, lblk->n - 1)); + nroot->nodes[0].blk = lblk; - node = ngtcp2_ksl_nth_node(ksl, nhead, 1); - ksl_node_set_key(ksl, node, ngtcp2_ksl_nth_node(ksl, rblk, rblk->n - 1)->key); - node->blk = rblk; + ksl_set_nth_key(ksl, nroot, 1, ngtcp2_ksl_blk_nth_key(rblk, rblk->n - 1)); + nroot->nodes[1].blk = rblk; - ksl->head = nhead; + ksl->root = nroot; return 0; } @@ -257,16 +261,17 @@ static int ksl_split_head(ngtcp2_ksl *ksl) { */ static void ksl_insert_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i, const ngtcp2_ksl_key *key, void *data) { - ngtcp2_ksl_node *node; - assert(blk->n < NGTCP2_KSL_MAX_NBLK); - memmove(blk->nodes + (i + 1) * ksl->nodelen, blk->nodes + i * ksl->nodelen, - ksl->nodelen * (blk->n - i)); + memmove(blk->nodes + (i + 1), blk->nodes + i, + (blk->n - i) * sizeof(ngtcp2_ksl_node)); + + memmove(blk->keys + (i + 1) * ksl->aligned_keylen, + blk->keys + i * ksl->aligned_keylen, + (blk->n - i) * ksl->aligned_keylen); - node = ngtcp2_ksl_nth_node(ksl, blk, i); - ksl_node_set_key(ksl, node, key); - node->data = data; + ksl_set_nth_key(ksl, blk, i, key); + blk->nodes[i].data = data; ++blk->n; } @@ -278,28 +283,27 @@ int ngtcp2_ksl_insert(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, size_t i; int rv; - if (!ksl->head) { - rv = ksl_head_init(ksl); + if (!ksl->root) { + rv = ksl_root_init(ksl); if (rv != 0) { return rv; } } - if (ksl->head->n == NGTCP2_KSL_MAX_NBLK) { - rv = ksl_split_head(ksl); + if (ksl->root->n == NGTCP2_KSL_MAX_NBLK) { + rv = ksl_split_root(ksl); if (rv != 0) { return rv; } } - blk = ksl->head; + blk = ksl->root; for (;;) { i = ksl->search(ksl, blk, key); if (blk->leaf) { - if (i < blk->n && - !ksl->compar(key, ngtcp2_ksl_nth_node(ksl, blk, i)->key)) { + if (i < blk->n && !ksl->compar(key, ngtcp2_ksl_blk_nth_key(blk, i))) { if (it) { *it = ngtcp2_ksl_end(ksl); } @@ -311,7 +315,7 @@ int ngtcp2_ksl_insert(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, ++ksl->n; if (it) { - ngtcp2_ksl_it_init(it, ksl, blk, i); + ngtcp2_ksl_it_init(it, blk, i); } return 0; @@ -320,17 +324,17 @@ int ngtcp2_ksl_insert(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, if (i == blk->n) { /* This insertion extends the largest key in this subtree. */ for (; !blk->leaf;) { - node = ngtcp2_ksl_nth_node(ksl, blk, blk->n - 1); + node = &blk->nodes[blk->n - 1]; if (node->blk->n == NGTCP2_KSL_MAX_NBLK) { rv = ksl_split_node(ksl, blk, blk->n - 1); if (rv != 0) { return rv; } - node = ngtcp2_ksl_nth_node(ksl, blk, blk->n - 1); + node = &blk->nodes[blk->n - 1]; } - ksl_node_set_key(ksl, node, key); + ksl_set_nth_key(ksl, blk, blk->n - 1, key); blk = node->blk; } @@ -338,13 +342,13 @@ int ngtcp2_ksl_insert(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, ++ksl->n; if (it) { - ngtcp2_ksl_it_init(it, ksl, blk, blk->n - 1); + ngtcp2_ksl_it_init(it, blk, blk->n - 1); } return 0; } - node = ngtcp2_ksl_nth_node(ksl, blk, i); + node = &blk->nodes[i]; if (node->blk->n == NGTCP2_KSL_MAX_NBLK) { rv = ksl_split_node(ksl, blk, i); @@ -352,12 +356,8 @@ int ngtcp2_ksl_insert(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, return rv; } - if (ksl->compar((ngtcp2_ksl_key *)node->key, key)) { - node = ngtcp2_ksl_nth_node(ksl, blk, i + 1); - - if (ksl->compar((ngtcp2_ksl_key *)node->key, key)) { - ksl_node_set_key(ksl, node, key); - } + if (ksl->compar(ngtcp2_ksl_blk_nth_key(blk, i), key)) { + node = &blk->nodes[i + 1]; } } @@ -370,8 +370,12 @@ int ngtcp2_ksl_insert(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, * |i|. */ static void ksl_remove_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { - memmove(blk->nodes + i * ksl->nodelen, blk->nodes + (i + 1) * ksl->nodelen, - ksl->nodelen * (blk->n - (i + 1))); + memmove(blk->nodes + i, blk->nodes + (i + 1), + (blk->n - (i + 1)) * sizeof(ngtcp2_ksl_node)); + + memmove(blk->keys + i * ksl->aligned_keylen, + blk->keys + (i + 1) * ksl->aligned_keylen, + (blk->n - (i + 1)) * ksl->aligned_keylen); --blk->n; } @@ -380,9 +384,9 @@ static void ksl_remove_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { * ksl_merge_node merges 2 nodes which are the nodes at the index of * |i| and |i + 1|. * - * If |blk| is the head (root) block and it contains just 2 nodes - * before merging nodes, the merged block becomes head block, which - * decreases the height of |ksl| by 1. + * If |blk| is the root block and it contains just 2 nodes before + * merging nodes, the merged block becomes root block, which decreases + * the height of |ksl| by 1. * * This function returns the pointer to the merged block. */ @@ -393,15 +397,17 @@ static ngtcp2_ksl_blk *ksl_merge_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, assert(i + 1 < blk->n); - lnode = ngtcp2_ksl_nth_node(ksl, blk, i); + lnode = &blk->nodes[i]; lblk = lnode->blk; - rblk = ngtcp2_ksl_nth_node(ksl, blk, i + 1)->blk; + rblk = blk->nodes[i + 1].blk; - assert(lblk->n + rblk->n < NGTCP2_KSL_MAX_NBLK); + assert(lblk->n + rblk->n <= NGTCP2_KSL_MAX_NBLK); - memcpy(lblk->nodes + ksl->nodelen * lblk->n, rblk->nodes, - ksl->nodelen * rblk->n); + memcpy(lblk->nodes + lblk->n, rblk->nodes, rblk->n * sizeof(ngtcp2_ksl_node)); + + memcpy(lblk->keys + lblk->n * ksl->aligned_keylen, rblk->keys, + rblk->n * ksl->aligned_keylen); lblk->n += rblk->n; lblk->next = rblk->next; @@ -414,13 +420,12 @@ static ngtcp2_ksl_blk *ksl_merge_node(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, ksl_blk_objalloc_del(ksl, rblk); - if (ksl->head == blk && blk->n == 2) { - ksl_blk_objalloc_del(ksl, ksl->head); - ksl->head = lblk; + if (ksl->root == blk && blk->n == 2) { + ksl_blk_objalloc_del(ksl, ksl->root); + ksl->root = lblk; } else { ksl_remove_node(ksl, blk, i + 1); - ksl_node_set_key(ksl, lnode, - ngtcp2_ksl_nth_node(ksl, lblk, lblk->n - 1)->key); + ksl_set_nth_key(ksl, blk, i, ngtcp2_ksl_blk_nth_key(lblk, lblk->n - 1)); } return lblk; @@ -438,8 +443,8 @@ static void ksl_shift_left(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { assert(i > 0); - lnode = ngtcp2_ksl_nth_node(ksl, blk, i - 1); - rnode = ngtcp2_ksl_nth_node(ksl, blk, i); + lnode = &blk->nodes[i - 1]; + rnode = &blk->nodes[i]; lblk = lnode->blk; rblk = rnode->blk; @@ -453,15 +458,20 @@ static void ksl_shift_left(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { assert(lblk->n <= NGTCP2_KSL_MAX_NBLK - n); assert(rblk->n >= NGTCP2_KSL_MIN_NBLK + n); - memcpy(lblk->nodes + ksl->nodelen * lblk->n, rblk->nodes, ksl->nodelen * n); + memcpy(lblk->nodes + lblk->n, rblk->nodes, n * sizeof(ngtcp2_ksl_node)); + + memcpy(lblk->keys + lblk->n * ksl->aligned_keylen, rblk->keys, + n * ksl->aligned_keylen); lblk->n += (uint32_t)n; rblk->n -= (uint32_t)n; - ksl_node_set_key(ksl, lnode, - ngtcp2_ksl_nth_node(ksl, lblk, lblk->n - 1)->key); + ksl_set_nth_key(ksl, blk, i - 1, ngtcp2_ksl_blk_nth_key(lblk, lblk->n - 1)); + + memmove(rblk->nodes, rblk->nodes + n, rblk->n * sizeof(ngtcp2_ksl_node)); - memmove(rblk->nodes, rblk->nodes + ksl->nodelen * n, ksl->nodelen * rblk->n); + memmove(rblk->keys, rblk->keys + n * ksl->aligned_keylen, + rblk->n * ksl->aligned_keylen); } /* @@ -476,8 +486,8 @@ static void ksl_shift_right(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { assert(i < blk->n - 1); - lnode = ngtcp2_ksl_nth_node(ksl, blk, i); - rnode = ngtcp2_ksl_nth_node(ksl, blk, i + 1); + lnode = &blk->nodes[i]; + rnode = &blk->nodes[i + 1]; lblk = lnode->blk; rblk = rnode->blk; @@ -491,15 +501,20 @@ static void ksl_shift_right(ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t i) { assert(lblk->n >= NGTCP2_KSL_MIN_NBLK + n); assert(rblk->n <= NGTCP2_KSL_MAX_NBLK - n); - memmove(rblk->nodes + ksl->nodelen * n, rblk->nodes, ksl->nodelen * rblk->n); + memmove(rblk->nodes + n, rblk->nodes, rblk->n * sizeof(ngtcp2_ksl_node)); + + memmove(rblk->keys + n * ksl->aligned_keylen, rblk->keys, + rblk->n * ksl->aligned_keylen); rblk->n += (uint32_t)n; lblk->n -= (uint32_t)n; - memcpy(rblk->nodes, lblk->nodes + ksl->nodelen * lblk->n, ksl->nodelen * n); + memcpy(rblk->nodes, lblk->nodes + lblk->n, n * sizeof(ngtcp2_ksl_node)); + + memcpy(rblk->keys, lblk->keys + lblk->n * ksl->aligned_keylen, + n * ksl->aligned_keylen); - ksl_node_set_key(ksl, lnode, - ngtcp2_ksl_nth_node(ksl, lblk, lblk->n - 1)->key); + ksl_set_nth_key(ksl, blk, i, ngtcp2_ksl_blk_nth_key(lblk, lblk->n - 1)); } /* @@ -516,9 +531,9 @@ int ngtcp2_ksl_remove_hint(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, const ngtcp2_ksl_key *key) { ngtcp2_ksl_blk *blk = hint->blk; - assert(ksl->head); + assert(ksl->root); - if (blk->n <= NGTCP2_KSL_MIN_NBLK) { + if (blk != ksl->root && blk->n == NGTCP2_KSL_MIN_NBLK) { return ngtcp2_ksl_remove(ksl, it, key); } @@ -528,9 +543,9 @@ int ngtcp2_ksl_remove_hint(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, if (it) { if (hint->i == blk->n && blk->next) { - ngtcp2_ksl_it_init(it, ksl, blk->next, 0); + ngtcp2_ksl_it_init(it, blk->next, 0); } else { - ngtcp2_ksl_it_init(it, ksl, blk, hint->i); + ngtcp2_ksl_it_init(it, blk, hint->i); } } @@ -539,7 +554,7 @@ int ngtcp2_ksl_remove_hint(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, int ngtcp2_ksl_remove(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, const ngtcp2_ksl_key *key) { - ngtcp2_ksl_blk *blk = ksl->head; + ngtcp2_ksl_blk *blk = ksl->root; ngtcp2_ksl_node *node; size_t i; @@ -548,8 +563,8 @@ int ngtcp2_ksl_remove(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, } if (!blk->leaf && blk->n == 2 && - ngtcp2_ksl_nth_node(ksl, blk, 0)->blk->n == NGTCP2_KSL_MIN_NBLK && - ngtcp2_ksl_nth_node(ksl, blk, 1)->blk->n == NGTCP2_KSL_MIN_NBLK) { + blk->nodes[0].blk->n == NGTCP2_KSL_MIN_NBLK && + blk->nodes[1].blk->n == NGTCP2_KSL_MIN_NBLK) { blk = ksl_merge_node(ksl, blk, 0); } @@ -565,7 +580,7 @@ int ngtcp2_ksl_remove(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, } if (blk->leaf) { - if (ksl->compar(key, ngtcp2_ksl_nth_node(ksl, blk, i)->key)) { + if (ksl->compar(key, ngtcp2_ksl_blk_nth_key(blk, i))) { if (it) { *it = ngtcp2_ksl_end(ksl); } @@ -578,16 +593,16 @@ int ngtcp2_ksl_remove(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, if (it) { if (blk->n == i && blk->next) { - ngtcp2_ksl_it_init(it, ksl, blk->next, 0); + ngtcp2_ksl_it_init(it, blk->next, 0); } else { - ngtcp2_ksl_it_init(it, ksl, blk, i); + ngtcp2_ksl_it_init(it, blk, i); } } return 0; } - node = ngtcp2_ksl_nth_node(ksl, blk, i); + node = &blk->nodes[i]; if (node->blk->n > NGTCP2_KSL_MIN_NBLK) { blk = node->blk; @@ -596,16 +611,14 @@ int ngtcp2_ksl_remove(ngtcp2_ksl *ksl, ngtcp2_ksl_it *it, assert(node->blk->n == NGTCP2_KSL_MIN_NBLK); - if (i + 1 < blk->n && - ngtcp2_ksl_nth_node(ksl, blk, i + 1)->blk->n > NGTCP2_KSL_MIN_NBLK) { + if (i + 1 < blk->n && blk->nodes[i + 1].blk->n > NGTCP2_KSL_MIN_NBLK) { ksl_shift_left(ksl, blk, i + 1); blk = node->blk; continue; } - if (i > 0 && - ngtcp2_ksl_nth_node(ksl, blk, i - 1)->blk->n > NGTCP2_KSL_MIN_NBLK) { + if (i > 0 && blk->nodes[i - 1].blk->n > NGTCP2_KSL_MIN_NBLK) { ksl_shift_right(ksl, blk, i - 1); blk = node->blk; @@ -631,12 +644,12 @@ ngtcp2_ksl_it ngtcp2_ksl_lower_bound(const ngtcp2_ksl *ksl, ngtcp2_ksl_it ngtcp2_ksl_lower_bound_search(const ngtcp2_ksl *ksl, const ngtcp2_ksl_key *key, ngtcp2_ksl_search search) { - ngtcp2_ksl_blk *blk = ksl->head; + ngtcp2_ksl_blk *blk = ksl->root; ngtcp2_ksl_it it; size_t i; if (!blk) { - ngtcp2_ksl_it_init(&it, ksl, &null_blk, 0); + ngtcp2_ksl_it_init(&it, &null_blk, 0); return it; } @@ -649,7 +662,7 @@ ngtcp2_ksl_it ngtcp2_ksl_lower_bound_search(const ngtcp2_ksl *ksl, i = 0; } - ngtcp2_ksl_it_init(&it, ksl, blk, i); + ngtcp2_ksl_it_init(&it, blk, i); return it; } @@ -657,7 +670,7 @@ ngtcp2_ksl_it ngtcp2_ksl_lower_bound_search(const ngtcp2_ksl *ksl, if (i == blk->n) { /* This happens if descendant has smaller key. Fast forward to find last node in this subtree. */ - for (; !blk->leaf; blk = ngtcp2_ksl_nth_node(ksl, blk, blk->n - 1)->blk) + for (; !blk->leaf; blk = blk->nodes[blk->n - 1].blk) ; if (blk->next) { @@ -667,39 +680,41 @@ ngtcp2_ksl_it ngtcp2_ksl_lower_bound_search(const ngtcp2_ksl *ksl, i = blk->n; } - ngtcp2_ksl_it_init(&it, ksl, blk, i); + ngtcp2_ksl_it_init(&it, blk, i); return it; } - blk = ngtcp2_ksl_nth_node(ksl, blk, i)->blk; + blk = blk->nodes[i].blk; } } void ngtcp2_ksl_update_key(ngtcp2_ksl *ksl, const ngtcp2_ksl_key *old_key, const ngtcp2_ksl_key *new_key) { - ngtcp2_ksl_blk *blk = ksl->head; + ngtcp2_ksl_blk *blk = ksl->root; ngtcp2_ksl_node *node; + const ngtcp2_ksl_key *node_key; size_t i; - assert(ksl->head); + assert(ksl->root); for (;;) { i = ksl->search(ksl, blk, old_key); assert(i < blk->n); - node = ngtcp2_ksl_nth_node(ksl, blk, i); + node = &blk->nodes[i]; + node_key = ngtcp2_ksl_blk_nth_key(blk, i); if (blk->leaf) { - assert(key_equal(ksl->compar, (ngtcp2_ksl_key *)node->key, old_key)); - ksl_node_set_key(ksl, node, new_key); + assert(key_equal(ksl->compar, node_key, old_key)); + ksl_set_nth_key(ksl, blk, i, new_key); return; } - if (key_equal(ksl->compar, (ngtcp2_ksl_key *)node->key, old_key) || - ksl->compar((ngtcp2_ksl_key *)node->key, new_key)) { - ksl_node_set_key(ksl, node, new_key); + if (key_equal(ksl->compar, node_key, old_key) || + ksl->compar(node_key, new_key)) { + ksl_set_nth_key(ksl, blk, i, new_key); } blk = node->blk; @@ -709,15 +724,15 @@ void ngtcp2_ksl_update_key(ngtcp2_ksl *ksl, const ngtcp2_ksl_key *old_key, size_t ngtcp2_ksl_len(const ngtcp2_ksl *ksl) { return ksl->n; } void ngtcp2_ksl_clear(ngtcp2_ksl *ksl) { - if (!ksl->head) { + if (!ksl->root) { return; } #ifdef NOMEMPOOL - ksl_free_blk(ksl, ksl->head); + ksl_free_blk(ksl, ksl->root); #endif /* defined(NOMEMPOOL) */ - ksl->front = ksl->back = ksl->head = NULL; + ksl->front = ksl->back = ksl->root = NULL; ksl->n = 0; ngtcp2_objalloc_clear(&ksl->blkalloc); @@ -727,14 +742,12 @@ void ngtcp2_ksl_clear(ngtcp2_ksl *ksl) { static void ksl_print(const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, size_t level) { size_t i; - ngtcp2_ksl_node *node; fprintf(stderr, "LV=%zu n=%u\n", level, blk->n); if (blk->leaf) { for (i = 0; i < blk->n; ++i) { - node = ngtcp2_ksl_nth_node(ksl, blk, i); - fprintf(stderr, " %" PRId64, *(int64_t *)(void *)node->key); + fprintf(stderr, " %" PRId64, *(int64_t *)ngtcp2_ksl_blk_nth_key(blk, i)); } fprintf(stderr, "\n"); @@ -743,26 +756,26 @@ static void ksl_print(const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, } for (i = 0; i < blk->n; ++i) { - ksl_print(ksl, ngtcp2_ksl_nth_node(ksl, blk, i)->blk, level + 1); + ksl_print(ksl, blk->nodes[i].blk, level + 1); } } void ngtcp2_ksl_print(const ngtcp2_ksl *ksl) { - if (!ksl->head) { + if (!ksl->root) { return; } - ksl_print(ksl, ksl->head, 0); + ksl_print(ksl, ksl->root, 0); } #endif /* !defined(WIN32) */ ngtcp2_ksl_it ngtcp2_ksl_begin(const ngtcp2_ksl *ksl) { ngtcp2_ksl_it it; - if (ksl->head) { - ngtcp2_ksl_it_init(&it, ksl, ksl->front, 0); + if (ksl->root) { + ngtcp2_ksl_it_init(&it, ksl->front, 0); } else { - ngtcp2_ksl_it_init(&it, ksl, &null_blk, 0); + ngtcp2_ksl_it_init(&it, &null_blk, 0); } return it; @@ -771,18 +784,16 @@ ngtcp2_ksl_it ngtcp2_ksl_begin(const ngtcp2_ksl *ksl) { ngtcp2_ksl_it ngtcp2_ksl_end(const ngtcp2_ksl *ksl) { ngtcp2_ksl_it it; - if (ksl->head) { - ngtcp2_ksl_it_init(&it, ksl, ksl->back, ksl->back->n); + if (ksl->root) { + ngtcp2_ksl_it_init(&it, ksl->back, ksl->back->n); } else { - ngtcp2_ksl_it_init(&it, ksl, &null_blk, 0); + ngtcp2_ksl_it_init(&it, &null_blk, 0); } return it; } -void ngtcp2_ksl_it_init(ngtcp2_ksl_it *it, const ngtcp2_ksl *ksl, - ngtcp2_ksl_blk *blk, size_t i) { - it->ksl = ksl; +void ngtcp2_ksl_it_init(ngtcp2_ksl_it *it, ngtcp2_ksl_blk *blk, size_t i) { it->blk = blk; it->i = i; } @@ -802,12 +813,6 @@ int ngtcp2_ksl_it_begin(const ngtcp2_ksl_it *it) { return it->i == 0 && it->blk->prev == NULL; } -int ngtcp2_ksl_range_compar(const ngtcp2_ksl_key *lhs, - const ngtcp2_ksl_key *rhs) { - const ngtcp2_range *a = lhs, *b = rhs; - return a->begin < b->begin; -} - ngtcp2_ksl_search_def(range, ngtcp2_ksl_range_compar) size_t ngtcp2_ksl_range_search(const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, @@ -815,13 +820,6 @@ size_t ngtcp2_ksl_range_search(const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, return ksl_range_search(ksl, blk, key); } -int ngtcp2_ksl_range_exclusive_compar(const ngtcp2_ksl_key *lhs, - const ngtcp2_ksl_key *rhs) { - const ngtcp2_range *a = lhs, *b = rhs; - return a->begin < b->begin && !(ngtcp2_max_uint64(a->begin, b->begin) < - ngtcp2_min_uint64(a->end, b->end)); -} - ngtcp2_ksl_search_def(range_exclusive, ngtcp2_ksl_range_exclusive_compar) size_t ngtcp2_ksl_range_exclusive_search(const ngtcp2_ksl *ksl, @@ -830,11 +828,6 @@ size_t ngtcp2_ksl_range_exclusive_search(const ngtcp2_ksl *ksl, return ksl_range_exclusive_search(ksl, blk, key); } -int ngtcp2_ksl_uint64_less(const ngtcp2_ksl_key *lhs, - const ngtcp2_ksl_key *rhs) { - return *(uint64_t *)lhs < *(uint64_t *)rhs; -} - ngtcp2_ksl_search_def(uint64_less, ngtcp2_ksl_uint64_less) size_t ngtcp2_ksl_uint64_less_search(const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, @@ -842,11 +835,6 @@ size_t ngtcp2_ksl_uint64_less_search(const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, return ksl_uint64_less_search(ksl, blk, key); } -int ngtcp2_ksl_int64_greater(const ngtcp2_ksl_key *lhs, - const ngtcp2_ksl_key *rhs) { - return *(int64_t *)lhs > *(int64_t *)rhs; -} - ngtcp2_ksl_search_def(int64_greater, ngtcp2_ksl_int64_greater) size_t ngtcp2_ksl_int64_greater_search(const ngtcp2_ksl *ksl, diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ksl.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ksl.h index d5c3f61d2acc7b..9ef201a8dbb95d 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ksl.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ksl.h @@ -34,14 +34,15 @@ #include #include "ngtcp2_objalloc.h" +#include "ngtcp2_range.h" #define NGTCP2_KSL_DEGR 16 /* NGTCP2_KSL_MAX_NBLK is the maximum number of nodes which a single block can contain. */ -#define NGTCP2_KSL_MAX_NBLK (2 * NGTCP2_KSL_DEGR - 1) +#define NGTCP2_KSL_MAX_NBLK (2 * NGTCP2_KSL_DEGR) /* NGTCP2_KSL_MIN_NBLK is the minimum number of nodes which a single block other than root must contain. */ -#define NGTCP2_KSL_MIN_NBLK (NGTCP2_KSL_DEGR - 1) +#define NGTCP2_KSL_MIN_NBLK NGTCP2_KSL_DEGR /* * ngtcp2_ksl_key represents key in ngtcp2_ksl. @@ -55,22 +56,13 @@ typedef struct ngtcp2_ksl_blk ngtcp2_ksl_blk; /* * ngtcp2_ksl_node is a node which contains either ngtcp2_ksl_blk or * opaque data. If a node is an internal node, it contains - * ngtcp2_ksl_blk. Otherwise, it has data. The key is stored at the - * location starting at key. + * ngtcp2_ksl_blk. Otherwise, it has data. */ struct ngtcp2_ksl_node { union { ngtcp2_ksl_blk *blk; void *data; }; - union { - uint64_t align; - /* key is a buffer to include key associated to this node. - Because the length of key is unknown until ngtcp2_ksl_init is - called, the actual buffer will be allocated after this - field. */ - uint8_t key[1]; - }; }; /* @@ -84,19 +76,19 @@ struct ngtcp2_ksl_blk { /* prev points to the previous block if leaf field is nonzero. */ ngtcp2_ksl_blk *prev; + ngtcp2_ksl_node nodes[NGTCP2_KSL_MAX_NBLK]; + /* keys is a pointer to the buffer to include + NGTCP2_KSL_MAX_NBLK keys. Because the length of key is + unknown until ngtcp2_ksl_init is called, the actual buffer + will be allocated after this object. */ + uint8_t *keys; /* n is the number of nodes this object contains in nodes. */ uint32_t n; + /* aligned_keylen is the length of the single key including + alignment. */ + uint16_t aligned_keylen; /* leaf is nonzero if this block contains leaf nodes. */ - uint32_t leaf; - union { - uint64_t align; - /* nodes is a buffer to contain NGTCP2_KSL_MAX_NBLK - ngtcp2_ksl_node objects. Because ngtcp2_ksl_node object is - allocated along with the additional variable length key - storage, the size of buffer is unknown until ngtcp2_ksl_init is - called. */ - uint8_t nodes[1]; - }; + uint8_t leaf; }; ngtcp2_opl_entry oplent; @@ -131,11 +123,10 @@ typedef size_t (*ngtcp2_ksl_search)(const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, static size_t ksl_##NAME##_search( \ const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, const ngtcp2_ksl_key *key) { \ size_t i; \ - ngtcp2_ksl_node *node; \ + uint8_t *node_key; \ \ - for (i = 0, node = (ngtcp2_ksl_node *)(void *)blk->nodes; \ - i < blk->n && COMPAR((ngtcp2_ksl_key *)node->key, key); ++i, \ - node = (ngtcp2_ksl_node *)(void *)((uint8_t *)node + ksl->nodelen)) \ + for (i = 0, node_key = blk->keys; i < blk->n && COMPAR(node_key, key); \ + ++i, node_key += ksl->aligned_keylen) \ ; \ \ return i; \ @@ -147,7 +138,6 @@ typedef struct ngtcp2_ksl_it ngtcp2_ksl_it; * ngtcp2_ksl_it is a bidirectional iterator to iterate nodes. */ struct ngtcp2_ksl_it { - const ngtcp2_ksl *ksl; ngtcp2_ksl_blk *blk; size_t i; }; @@ -157,8 +147,8 @@ struct ngtcp2_ksl_it { */ struct ngtcp2_ksl { ngtcp2_objalloc blkalloc; - /* head points to the root block. */ - ngtcp2_ksl_blk *head; + /* root points to the root block. */ + ngtcp2_ksl_blk *root; /* front points to the first leaf block. */ ngtcp2_ksl_blk *front; /* back points to the last leaf block. */ @@ -169,9 +159,7 @@ struct ngtcp2_ksl { size_t n; /* keylen is the size of key */ size_t keylen; - /* nodelen is the actual size of ngtcp2_ksl_node including key - storage. */ - size_t nodelen; + size_t aligned_keylen; }; /* @@ -290,12 +278,11 @@ size_t ngtcp2_ksl_len(const ngtcp2_ksl *ksl); void ngtcp2_ksl_clear(ngtcp2_ksl *ksl); /* - * ngtcp2_ksl_nth_node returns the |n|th node under |blk|. + * ngtcp2_ksl_blk_nth_key returns the |n|th key under |blk|. */ -static inline ngtcp2_ksl_node *ngtcp2_ksl_nth_node(const ngtcp2_ksl *ksl, - const ngtcp2_ksl_blk *blk, - size_t n) { - return (ngtcp2_ksl_node *)(void *)(blk->nodes + ksl->nodelen * n); +static inline const ngtcp2_ksl_key * +ngtcp2_ksl_blk_nth_key(const ngtcp2_ksl_blk *blk, size_t n) { + return blk->keys + n * blk->aligned_keylen; } #ifndef WIN32 @@ -310,8 +297,7 @@ void ngtcp2_ksl_print(const ngtcp2_ksl *ksl); /* * ngtcp2_ksl_it_init initializes |it|. */ -void ngtcp2_ksl_it_init(ngtcp2_ksl_it *it, const ngtcp2_ksl *ksl, - ngtcp2_ksl_blk *blk, size_t i); +void ngtcp2_ksl_it_init(ngtcp2_ksl_it *it, ngtcp2_ksl_blk *blk, size_t i); /* * ngtcp2_ksl_it_get returns the data associated to the node which @@ -319,7 +305,7 @@ void ngtcp2_ksl_it_init(ngtcp2_ksl_it *it, const ngtcp2_ksl *ksl, * ngtcp2_ksl_it_end(it) returns nonzero. */ static inline void *ngtcp2_ksl_it_get(const ngtcp2_ksl_it *it) { - return ngtcp2_ksl_nth_node(it->ksl, it->blk, it->i)->data; + return it->blk->nodes[it->i].data; } /* @@ -361,8 +347,8 @@ int ngtcp2_ksl_it_begin(const ngtcp2_ksl_it *it); * It is undefined to call this function when ngtcp2_ksl_it_end(it) * returns nonzero. */ -static inline ngtcp2_ksl_key *ngtcp2_ksl_it_key(const ngtcp2_ksl_it *it) { - return (ngtcp2_ksl_key *)ngtcp2_ksl_nth_node(it->ksl, it->blk, it->i)->key; +static inline const ngtcp2_ksl_key *ngtcp2_ksl_it_key(const ngtcp2_ksl_it *it) { + return ngtcp2_ksl_blk_nth_key(it->blk, it->i); } /* @@ -371,8 +357,12 @@ static inline ngtcp2_ksl_key *ngtcp2_ksl_it_key(const ngtcp2_ksl_it *it) { * returns nonzero if ((const ngtcp2_range *)lhs)->begin < ((const * ngtcp2_range *)rhs)->begin. */ -int ngtcp2_ksl_range_compar(const ngtcp2_ksl_key *lhs, - const ngtcp2_ksl_key *rhs); +static inline int ngtcp2_ksl_range_compar(const ngtcp2_ksl_key *lhs, + const ngtcp2_ksl_key *rhs) { + const ngtcp2_range *a = (const ngtcp2_range *)lhs, + *b = (const ngtcp2_range *)rhs; + return a->begin < b->begin; +} /* * ngtcp2_ksl_range_search is an implementation of ngtcp2_ksl_search @@ -388,8 +378,13 @@ size_t ngtcp2_ksl_range_search(const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, * *)lhs)->begin < ((const ngtcp2_range *)rhs)->begin, and the 2 * ranges do not intersect. */ -int ngtcp2_ksl_range_exclusive_compar(const ngtcp2_ksl_key *lhs, - const ngtcp2_ksl_key *rhs); +static inline int ngtcp2_ksl_range_exclusive_compar(const ngtcp2_ksl_key *lhs, + const ngtcp2_ksl_key *rhs) { + const ngtcp2_range *a = (const ngtcp2_range *)lhs, + *b = (const ngtcp2_range *)rhs; + return a->begin < b->begin && + !(ngtcp2_max(a->begin, b->begin) < ngtcp2_min(a->end, b->end)); +} /* * ngtcp2_ksl_range_exclusive_search is an implementation of @@ -404,8 +399,10 @@ size_t ngtcp2_ksl_range_exclusive_search(const ngtcp2_ksl *ksl, * |lhs| and |rhs| must point to uint64_t objects, and the function * returns nonzero if *(uint64_t *)|lhs| < *(uint64_t *)|rhs|. */ -int ngtcp2_ksl_uint64_less(const ngtcp2_ksl_key *lhs, - const ngtcp2_ksl_key *rhs); +static inline int ngtcp2_ksl_uint64_less(const ngtcp2_ksl_key *lhs, + const ngtcp2_ksl_key *rhs) { + return *(const uint64_t *)lhs < *(const uint64_t *)rhs; +} /* * ngtcp2_ksl_uint64_less_search is an implementation of @@ -419,8 +416,10 @@ size_t ngtcp2_ksl_uint64_less_search(const ngtcp2_ksl *ksl, ngtcp2_ksl_blk *blk, * |lhs| and |rhs| must point to int64_t objects, and the function * returns nonzero if *(int64_t *)|lhs| > *(int64_t *)|rhs|. */ -int ngtcp2_ksl_int64_greater(const ngtcp2_ksl_key *lhs, - const ngtcp2_ksl_key *rhs); +static inline int ngtcp2_ksl_int64_greater(const ngtcp2_ksl_key *lhs, + const ngtcp2_ksl_key *rhs) { + return *(const int64_t *)lhs > *(const int64_t *)rhs; +} /* * ngtcp2_ksl_int64_greater_search is an implementation of diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_log.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_log.c index e006c22e4e7ff0..51a9d70d13c47d 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_log.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_log.c @@ -39,17 +39,19 @@ #include "ngtcp2_net.h" void ngtcp2_log_init(ngtcp2_log *log, const ngtcp2_cid *scid, - ngtcp2_printf log_printf, ngtcp2_tstamp ts, - void *user_data) { + ngtcp2_log_write log_write, ngtcp2_printf log_printf, + char *buf, ngtcp2_tstamp ts, void *user_data) { if (scid) { - ngtcp2_encode_hex(log->scid, scid->data, scid->datalen); + *ngtcp2_encode_hex((uint8_t *)log->scid, scid->data, scid->datalen) = '\0'; } else { log->scid[0] = '\0'; } + log->log_write = log_write; log->log_printf = log_printf; - log->events = 0xff; + log->events = 0xFF; log->ts = log->last_ts = ts; log->user_data = user_data; + log->buf = buf; } /* @@ -90,23 +92,9 @@ void ngtcp2_log_init(ngtcp2_log *log, const ngtcp2_cid *scid, * Frame type in hex string. */ -#define NGTCP2_LOG_BUFLEN 4096 +#define NGTCP2_LOG_TP "remote transport_parameters" -/* TODO Split second and remaining fraction with comma */ -#define NGTCP2_LOG_HD "I%08" PRIu64 " 0x%s %s" -#define NGTCP2_LOG_PKT NGTCP2_LOG_HD " %s %" PRId64 " %s" -#define NGTCP2_LOG_TP NGTCP2_LOG_HD " remote transport_parameters" - -#define NGTCP2_LOG_FRM_HD_FIELDS(DIR) \ - timestamp_cast(log->last_ts - log->ts), (const char *)log->scid, "frm", \ - (DIR), hd->pkt_num, strpkttype(hd) - -#define NGTCP2_LOG_PKT_HD_FIELDS(DIR) \ - timestamp_cast(log->last_ts - log->ts), (const char *)log->scid, "pkt", \ - (DIR), hd->pkt_num, strpkttype(hd) - -#define NGTCP2_LOG_TP_HD_FIELDS \ - timestamp_cast(log->last_ts - log->ts), (const char *)log->scid, "cry" +#define NGTCP2_LOG_PKT(DIR, HD) (DIR), " ", (HD)->pkt_num, " ", strpkttype((HD)) static const char *strerrorcode(uint64_t error_code) { switch (error_code) { @@ -140,10 +128,14 @@ static const char *strerrorcode(uint64_t error_code) { return "CRYPTO_BUFFER_EXCEEDED"; case NGTCP2_KEY_UPDATE_ERROR: return "KEY_UPDATE_ERROR"; + case NGTCP2_AEAD_LIMIT_REACHED: + return "AEAD_LIMIT_REACHED"; + case NGTCP2_NO_VIABLE_PATH: + return "NO_VIABLE_PATH"; case NGTCP2_VERSION_NEGOTIATION_ERROR: return "VERSION_NEGOTIATION_ERROR"; default: - if (0x100u <= error_code && error_code <= 0x1ffu) { + if (0x100U <= error_code && error_code <= 0x1FFU) { return "CRYPTO_ERROR"; } return "(unknown)"; @@ -155,80 +147,41 @@ static const char *strapperrorcode(uint64_t app_error_code) { return "(unknown)"; } -static const char *strpkttype_long(uint8_t type) { - switch (type) { +static const char *strpkttype(const ngtcp2_pkt_hd *hd) { + switch (hd->type) { case NGTCP2_PKT_INITIAL: return "Initial"; - case NGTCP2_PKT_RETRY: - return "Retry"; - case NGTCP2_PKT_HANDSHAKE: - return "Handshake"; case NGTCP2_PKT_0RTT: return "0RTT"; - default: - return "(unknown)"; - } -} - -static const char *strpkttype(const ngtcp2_pkt_hd *hd) { - if (hd->flags & NGTCP2_PKT_FLAG_LONG_FORM) { - return strpkttype_long(hd->type); - } - - switch (hd->type) { + case NGTCP2_PKT_HANDSHAKE: + return "Handshake"; + case NGTCP2_PKT_RETRY: + return "Retry"; + case NGTCP2_PKT_1RTT: + return "1RTT"; case NGTCP2_PKT_VERSION_NEGOTIATION: return "VN"; case NGTCP2_PKT_STATELESS_RESET: return "SR"; - case NGTCP2_PKT_1RTT: - return "1RTT"; default: return "(unknown)"; } } static const char *strpkttype_type_flags(uint8_t type, uint8_t flags) { - ngtcp2_pkt_hd hd = {0}; - - hd.type = type; - hd.flags = flags; - - return strpkttype(&hd); -} - -static const char *strevent(ngtcp2_log_event ev) { - switch (ev) { - case NGTCP2_LOG_EVENT_CON: - return "con"; - case NGTCP2_LOG_EVENT_PKT: - return "pkt"; - case NGTCP2_LOG_EVENT_FRM: - return "frm"; - case NGTCP2_LOG_EVENT_LDC: - return "ldc"; - case NGTCP2_LOG_EVENT_CRY: - return "cry"; - case NGTCP2_LOG_EVENT_PTV: - return "ptv"; - case NGTCP2_LOG_EVENT_CCA: - return "cca"; - case NGTCP2_LOG_EVENT_NONE: - default: - return "non"; - } + return strpkttype(&(ngtcp2_pkt_hd){ + .type = type, + .flags = flags, + }); } -static uint64_t timestamp_cast(uint64_t ns) { return ns / NGTCP2_MILLISECONDS; } - static void log_fr_stream(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_stream *fr, const char *dir) { - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " STREAM(0x%02" PRIx64 ") id=0x%" PRIx64 - " fin=%d offset=%" PRIu64 " len=%" PRIu64 " uni=%d"), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type | fr->flags, fr->stream_id, fr->fin, - fr->offset, ngtcp2_vec_len(fr->data, fr->datacnt), - (fr->stream_id & 0x2) != 0); + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), " STREAM(0x", + hex(fr->type | fr->flags), ") id=0x", hex(fr->stream_id), " fin=", fr->fin, + " offset=", fr->offset, " len=", ngtcp2_vec_len(fr->data, fr->datacnt), + " uni=", (fr->stream_id & 0x2) != 0); } static void log_fr_ack(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, @@ -236,250 +189,204 @@ static void log_fr_ack(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, int64_t largest_ack, min_ack; size_t i; - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " ACK(0x%02" PRIx64 ") largest_ack=%" PRId64 - " ack_delay=%" PRIu64 "(%" PRIu64 ") ack_range_count=%zu"), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->largest_ack, - fr->ack_delay_unscaled / NGTCP2_MILLISECONDS, fr->ack_delay, fr->rangecnt); + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), " ACK(0x", + hex(fr->type), ") largest_ack=", fr->largest_ack, + " ack_delay=", fr->ack_delay_unscaled / NGTCP2_MILLISECONDS, "(", + fr->ack_delay, ") ack_range_count=", fr->rangecnt); largest_ack = fr->largest_ack; min_ack = fr->largest_ack - (int64_t)fr->first_ack_range; - log->log_printf(log->user_data, - (NGTCP2_LOG_PKT " ACK(0x%02" PRIx64 ") range=[%" PRId64 - "..%" PRId64 "] len=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, largest_ack, min_ack, - fr->first_ack_range); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " ACK(0x", hex(fr->type), ") range=[", largest_ack, "..", + min_ack, "] len=", fr->first_ack_range); for (i = 0; i < fr->rangecnt; ++i) { const ngtcp2_ack_range *range = &fr->ranges[i]; largest_ack = min_ack - (int64_t)range->gap - 2; min_ack = largest_ack - (int64_t)range->len; - log->log_printf(log->user_data, - (NGTCP2_LOG_PKT " ACK(0x%02" PRIx64 ") range=[%" PRId64 - "..%" PRId64 "] gap=%" PRIu64 - " len=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, largest_ack, - min_ack, range->gap, range->len); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " ACK(0x", hex(fr->type), ") range=[", largest_ack, + "..", min_ack, "] gap=", range->gap, + " len=", range->len); } if (fr->type == NGTCP2_FRAME_ACK_ECN) { - log->log_printf(log->user_data, - (NGTCP2_LOG_PKT " ACK(0x%02" PRIx64 ") ect0=%" PRIu64 - " ect1=%" PRIu64 " ce=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->ecn.ect0, - fr->ecn.ect1, fr->ecn.ce); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " ACK(0x", hex(fr->type), ") ect0=", fr->ecn.ect0, + " ect1=", fr->ecn.ect1, " ce=", fr->ecn.ce); } } static void log_fr_padding(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_padding *fr, const char *dir) { - log->log_printf(log->user_data, - (NGTCP2_LOG_PKT " PADDING(0x%02" PRIx64 ") len=%zu"), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->len); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " PADDING(0x", hex(fr->type), ") len=", fr->len); } static void log_fr_reset_stream(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_reset_stream *fr, const char *dir) { - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " RESET_STREAM(0x%02" PRIx64 ") id=0x%" PRIx64 - " app_error_code=%s(0x%" PRIx64 ") final_size=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->stream_id, - strapperrorcode(fr->app_error_code), fr->app_error_code, fr->final_size); + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), " RESET_STREAM(0x", + hex(fr->type), ") id=0x", hex(fr->stream_id), + " app_error_code=", strapperrorcode(fr->app_error_code), "(0x", + hex(fr->app_error_code), ") final_size=", fr->final_size); } static void log_fr_connection_close(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_connection_close *fr, const char *dir) { - char reason[256]; - size_t reasonlen = ngtcp2_min_size(sizeof(reason) - 1, fr->reasonlen); + size_t reasonlen = ngtcp2_min(64, fr->reasonlen); - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " CONNECTION_CLOSE(0x%02" PRIx64 - ") error_code=%s(0x%" PRIx64 ") " - "frame_type=%" PRIx64 " reason_len=%zu reason=[%s]"), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, - fr->type == NGTCP2_FRAME_CONNECTION_CLOSE ? strerrorcode(fr->error_code) - : strapperrorcode(fr->error_code), - fr->error_code, fr->frame_type, fr->reasonlen, - ngtcp2_encode_printable_ascii(reason, fr->reason, reasonlen)); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " CONNECTION_CLOSE(0x", hex(fr->type), ") error_code=", + fr->type == NGTCP2_FRAME_CONNECTION_CLOSE + ? strerrorcode(fr->error_code) + : strapperrorcode(fr->error_code), + "(0x", hex(fr->error_code), ") frame_type=0x", + hex(fr->frame_type), " reason_len=", fr->reasonlen, + " reason=[", ascii(fr->reason, reasonlen), "]"); } static void log_fr_max_data(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_max_data *fr, const char *dir) { - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " MAX_DATA(0x%02" PRIx64 ") max_data=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->max_data); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " MAX_DATA(0x", hex(fr->type), + ") max_data=", fr->max_data); } static void log_fr_max_stream_data(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_max_stream_data *fr, const char *dir) { - log->log_printf(log->user_data, - (NGTCP2_LOG_PKT " MAX_STREAM_DATA(0x%02" PRIx64 - ") id=0x%" PRIx64 - " max_stream_data=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->stream_id, - fr->max_stream_data); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " MAX_STREAM_DATA(0x", hex(fr->type), ") id=0x", + hex(fr->stream_id), + " max_stream_data=", fr->max_stream_data); } static void log_fr_max_streams(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_max_streams *fr, const char *dir) { - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " MAX_STREAMS(0x%02" PRIx64 ") max_streams=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->max_streams); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " MAX_STREAMS(0x", hex(fr->type), + ") max_streams=", fr->max_streams); } static void log_fr_ping(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_ping *fr, const char *dir) { - log->log_printf(log->user_data, (NGTCP2_LOG_PKT " PING(0x%02" PRIx64 ")"), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " PING(0x", hex(fr->type), ")"); } static void log_fr_data_blocked(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_data_blocked *fr, const char *dir) { - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " DATA_BLOCKED(0x%02" PRIx64 ") offset=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->offset); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " DATA_BLOCKED(0x", hex(fr->type), + ") offset=", fr->offset); } static void log_fr_stream_data_blocked(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_stream_data_blocked *fr, const char *dir) { - log->log_printf(log->user_data, - (NGTCP2_LOG_PKT " STREAM_DATA_BLOCKED(0x%02" PRIx64 - ") id=0x%" PRIx64 " offset=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->stream_id, - fr->offset); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " STREAM_DATA_BLOCKED(0x", hex(fr->type), ") id=0x", + hex(fr->stream_id), " offset=", fr->offset); } static void log_fr_streams_blocked(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_streams_blocked *fr, const char *dir) { - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " STREAMS_BLOCKED(0x%02" PRIx64 ") max_streams=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->max_streams); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " STREAMS_BLOCKED(0x", hex(fr->type), + ") max_streams=", fr->max_streams); } static void log_fr_new_connection_id(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_new_connection_id *fr, const char *dir) { - uint8_t buf[sizeof(fr->stateless_reset_token) * 2 + 1]; - uint8_t cid[sizeof(fr->cid.data) * 2 + 1]; - - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " NEW_CONNECTION_ID(0x%02" PRIx64 ") seq=%" PRIu64 - " cid=0x%s retire_prior_to=%" PRIu64 - " stateless_reset_token=0x%s"), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->seq, - (const char *)ngtcp2_encode_hex(cid, fr->cid.data, fr->cid.datalen), - fr->retire_prior_to, - (const char *)ngtcp2_encode_hex(buf, fr->stateless_reset_token, - sizeof(fr->stateless_reset_token))); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " NEW_CONNECTION_ID(0x", hex(fr->type), + ") seq=", fr->seq, " cid=0x", &fr->cid, + " retire_prior_to=", fr->retire_prior_to, + " stateless_reset_token=0x", &fr->token); } static void log_fr_stop_sending(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_stop_sending *fr, const char *dir) { - log->log_printf(log->user_data, - (NGTCP2_LOG_PKT " STOP_SENDING(0x%02" PRIx64 ") id=0x%" PRIx64 - " app_error_code=%s(0x%" PRIx64 ")"), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->stream_id, - strapperrorcode(fr->app_error_code), fr->app_error_code); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " STOP_SENDING(0x", hex(fr->type), ") id=0x", + hex(fr->stream_id), + " app_error_code=", strapperrorcode(fr->app_error_code), + "(0x", hex(fr->app_error_code), ")"); } static void log_fr_path_challenge(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_path_challenge *fr, const char *dir) { - uint8_t buf[sizeof(fr->data) * 2 + 1]; - - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " PATH_CHALLENGE(0x%02" PRIx64 ") data=0x%s"), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, - (const char *)ngtcp2_encode_hex(buf, fr->data, sizeof(fr->data))); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " PATH_CHALLENGE(0x", hex(fr->type), ") data=0x", + &fr->data); } static void log_fr_path_response(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_path_response *fr, const char *dir) { - uint8_t buf[sizeof(fr->data) * 2 + 1]; - - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " PATH_RESPONSE(0x%02" PRIx64 ") data=0x%s"), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, - (const char *)ngtcp2_encode_hex(buf, fr->data, sizeof(fr->data))); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " PATH_RESPONSE(0x", hex(fr->type), ") data=0x", + &fr->data); } static void log_fr_crypto(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_stream *fr, const char *dir) { - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " CRYPTO(0x%02" PRIx64 ") offset=%" PRIu64 " len=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->offset, - ngtcp2_vec_len(fr->data, fr->datacnt)); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " CRYPTO(0x", hex(fr->type), ") offset=", fr->offset, + " len=", ngtcp2_vec_len(fr->data, fr->datacnt)); } +#define NGTCP2_LOG_MAX_TOKENLEN 64 + static void log_fr_new_token(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_new_token *fr, const char *dir) { - /* Show at most first 64 bytes of token. If token is longer than 64 - bytes, log first 64 bytes and then append "*" */ - uint8_t buf[128 + 1 + 1]; - uint8_t *p; - - if (fr->tokenlen > 64) { - p = ngtcp2_encode_hex(buf, fr->token, 64); - p[128] = '*'; - p[129] = '\0'; - } else { - p = ngtcp2_encode_hex(buf, fr->token, fr->tokenlen); - } - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " NEW_TOKEN(0x%02" PRIx64 ") token=0x%s len=%zu"), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, (const char *)p, fr->tokenlen); + /* Show at most first NGTCP2_LOG_MAX_TOKENLEN bytes of token. If + token is longer than that, log first those bytes and then append + "*" */ + size_t tokenlen = ngtcp2_min(fr->tokenlen, NGTCP2_LOG_MAX_TOKENLEN); + + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), " NEW_TOKEN(0x", + hex(fr->type), ") token=0x", bhex(fr->token, tokenlen), + fr->tokenlen > NGTCP2_LOG_MAX_TOKENLEN ? "*" : "", " len=", fr->tokenlen); } static void log_fr_retire_connection_id(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_retire_connection_id *fr, const char *dir) { - log->log_printf( - log->user_data, - (NGTCP2_LOG_PKT " RETIRE_CONNECTION_ID(0x%02" PRIx64 ") seq=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, fr->seq); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " RETIRE_CONNECTION_ID(0x", hex(fr->type), + ") seq=", fr->seq); } static void log_fr_handshake_done(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_handshake_done *fr, const char *dir) { - log->log_printf(log->user_data, - (NGTCP2_LOG_PKT " HANDSHAKE_DONE(0x%02" PRIx64 ")"), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " HANDSHAKE_DONE(0x", hex(fr->type), ")"); } static void log_fr_datagram(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_datagram *fr, const char *dir) { - log->log_printf(log->user_data, - (NGTCP2_LOG_PKT " DATAGRAM(0x%02" PRIx64 ") len=%" PRIu64), - NGTCP2_LOG_FRM_HD_FIELDS(dir), fr->type, - ngtcp2_vec_len(fr->data, fr->datacnt)); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_FRM, NGTCP2_LOG_PKT(dir, hd), + " DATAGRAM(0x", hex(fr->type), + ") len=", ngtcp2_vec_len(fr->data, fr->datacnt)); } static void log_fr(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_frame *fr, const char *dir) { - switch (fr->type) { + switch (fr->hd.type) { case NGTCP2_FRAME_STREAM: log_fr_stream(log, hd, &fr->stream, dir); break; @@ -555,7 +462,8 @@ static void log_fr(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, void ngtcp2_log_rx_fr(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_frame *fr) { - if (!log->log_printf || !(log->events & NGTCP2_LOG_EVENT_FRM)) { + if ((!log->log_write && !log->log_printf) || + !(log->events & NGTCP2_LOG_EVENT_FRM)) { return; } @@ -564,7 +472,8 @@ void ngtcp2_log_rx_fr(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, void ngtcp2_log_tx_fr(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_frame *fr) { - if (!log->log_printf || !(log->events & NGTCP2_LOG_EVENT_FRM)) { + if ((!log->log_write && !log->log_printf) || + !(log->events & NGTCP2_LOG_EVENT_FRM)) { return; } @@ -575,174 +484,149 @@ void ngtcp2_log_rx_vn(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const uint32_t *sv, size_t nsv) { size_t i; - if (!log->log_printf || !(log->events & NGTCP2_LOG_EVENT_PKT)) { + if ((!log->log_write && !log->log_printf) || + !(log->events & NGTCP2_LOG_EVENT_PKT)) { return; } for (i = 0; i < nsv; ++i) { - log->log_printf(log->user_data, (NGTCP2_LOG_PKT " v=0x%08x"), - NGTCP2_LOG_PKT_HD_FIELDS("rx"), sv[i]); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_PKT, NGTCP2_LOG_PKT("rx", hd), + " v=0x", hexw(sv[i], 8)); } } -void ngtcp2_log_rx_sr(ngtcp2_log *log, const ngtcp2_pkt_stateless_reset *sr) { - uint8_t buf[sizeof(sr->stateless_reset_token) * 2 + 1]; - ngtcp2_pkt_hd shd; - ngtcp2_pkt_hd *hd = &shd; +void ngtcp2_log_rx_sr(ngtcp2_log *log, const ngtcp2_pkt_stateless_reset2 *sr) { + ngtcp2_pkt_hd hd; - if (!log->log_printf || !(log->events & NGTCP2_LOG_EVENT_PKT)) { + if ((!log->log_write && !log->log_printf) || + !(log->events & NGTCP2_LOG_EVENT_PKT)) { return; } - shd = (ngtcp2_pkt_hd){ + hd = (ngtcp2_pkt_hd){ .type = NGTCP2_PKT_STATELESS_RESET, }; - log->log_printf( - log->user_data, (NGTCP2_LOG_PKT " token=0x%s randlen=%zu"), - NGTCP2_LOG_PKT_HD_FIELDS("rx"), - (const char *)ngtcp2_encode_hex(buf, sr->stateless_reset_token, - sizeof(sr->stateless_reset_token)), - sr->randlen); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_PKT, NGTCP2_LOG_PKT("rx", &hd), + " token=0x", &sr->token, " randlen=", sr->randlen); } void ngtcp2_log_remote_tp(ngtcp2_log *log, const ngtcp2_transport_params *params) { - uint8_t token[NGTCP2_STATELESS_RESET_TOKENLEN * 2 + 1]; - uint8_t addr[16 * 2 + 7 + 1]; - uint8_t cid[NGTCP2_MAX_CIDLEN * 2 + 1]; size_t i; const ngtcp2_sockaddr_in *sa_in; const ngtcp2_sockaddr_in6 *sa_in6; const uint8_t *p; uint32_t version; - if (!log->log_printf || !(log->events & NGTCP2_LOG_EVENT_CRY)) { + if ((!log->log_write && !log->log_printf) || + !(log->events & NGTCP2_LOG_EVENT_CRY)) { return; } if (params->stateless_reset_token_present) { - log->log_printf( - log->user_data, (NGTCP2_LOG_TP " stateless_reset_token=0x%s"), - NGTCP2_LOG_TP_HD_FIELDS, - (const char *)ngtcp2_encode_hex(token, params->stateless_reset_token, - sizeof(params->stateless_reset_token))); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " stateless_reset_token=0x", + lbhex(params->stateless_reset_token)); } if (params->preferred_addr_present) { if (params->preferred_addr.ipv4_present) { sa_in = ¶ms->preferred_addr.ipv4; - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " preferred_address.ipv4_addr=%s"), - NGTCP2_LOG_TP_HD_FIELDS, - (const char *)ngtcp2_encode_ipv4( - addr, (const uint8_t *)&sa_in->sin_addr)); - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " preferred_address.ipv4_port=%u"), - NGTCP2_LOG_TP_HD_FIELDS, ngtcp2_ntohs(sa_in->sin_port)); + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " preferred_address.ipv4_addr=", &sa_in->sin_addr); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " preferred_address.ipv4_port=", + ngtcp2_ntohs(sa_in->sin_port)); } if (params->preferred_addr.ipv6_present) { sa_in6 = ¶ms->preferred_addr.ipv6; - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " preferred_address.ipv6_addr=%s"), - NGTCP2_LOG_TP_HD_FIELDS, - (const char *)ngtcp2_encode_ipv6( - addr, (const uint8_t *)&sa_in6->sin6_addr)); - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " preferred_address.ipv6_port=%u"), - NGTCP2_LOG_TP_HD_FIELDS, ngtcp2_ntohs(sa_in6->sin6_port)); + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " preferred_address.ipv6_addr=", &sa_in6->sin6_addr); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " preferred_address.ipv6_port=", + ngtcp2_ntohs(sa_in6->sin6_port)); } - log->log_printf( - log->user_data, (NGTCP2_LOG_TP " preferred_address.cid=0x%s"), - NGTCP2_LOG_TP_HD_FIELDS, - (const char *)ngtcp2_encode_hex(cid, params->preferred_addr.cid.data, - params->preferred_addr.cid.datalen)); - log->log_printf( - log->user_data, - (NGTCP2_LOG_TP " preferred_address.stateless_reset_token=0x%s"), - NGTCP2_LOG_TP_HD_FIELDS, - (const char *)ngtcp2_encode_hex( - token, params->preferred_addr.stateless_reset_token, - sizeof(params->preferred_addr.stateless_reset_token))); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " preferred_address.cid=0x", + ¶ms->preferred_addr.cid); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP + " preferred_address.stateless_reset_token=0x", + lbhex(params->preferred_addr.stateless_reset_token)); } if (params->original_dcid_present) { - log->log_printf( - log->user_data, - (NGTCP2_LOG_TP " original_destination_connection_id=0x%s"), - NGTCP2_LOG_TP_HD_FIELDS, - (const char *)ngtcp2_encode_hex(cid, params->original_dcid.data, - params->original_dcid.datalen)); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " original_destination_connection_id=0x", + ¶ms->original_dcid); } if (params->retry_scid_present) { - log->log_printf( - log->user_data, (NGTCP2_LOG_TP " retry_source_connection_id=0x%s"), - NGTCP2_LOG_TP_HD_FIELDS, - (const char *)ngtcp2_encode_hex(cid, params->retry_scid.data, - params->retry_scid.datalen)); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " retry_source_connection_id=0x", + ¶ms->retry_scid); } if (params->initial_scid_present) { - log->log_printf( - log->user_data, (NGTCP2_LOG_TP " initial_source_connection_id=0x%s"), - NGTCP2_LOG_TP_HD_FIELDS, - (const char *)ngtcp2_encode_hex(cid, params->initial_scid.data, - params->initial_scid.datalen)); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " initial_source_connection_id=0x", + ¶ms->initial_scid); } - log->log_printf( - log->user_data, - (NGTCP2_LOG_TP " initial_max_stream_data_bidi_local=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, params->initial_max_stream_data_bidi_local); - log->log_printf( - log->user_data, - (NGTCP2_LOG_TP " initial_max_stream_data_bidi_remote=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, params->initial_max_stream_data_bidi_remote); - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " initial_max_stream_data_uni=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, params->initial_max_stream_data_uni); - log->log_printf(log->user_data, (NGTCP2_LOG_TP " initial_max_data=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, params->initial_max_data); - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " initial_max_streams_bidi=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, params->initial_max_streams_bidi); - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " initial_max_streams_uni=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, params->initial_max_streams_uni); - log->log_printf(log->user_data, (NGTCP2_LOG_TP " max_idle_timeout=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, - params->max_idle_timeout / NGTCP2_MILLISECONDS); - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " max_udp_payload_size=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, params->max_udp_payload_size); - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " ack_delay_exponent=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, params->ack_delay_exponent); - log->log_printf(log->user_data, (NGTCP2_LOG_TP " max_ack_delay=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, - params->max_ack_delay / NGTCP2_MILLISECONDS); - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " active_connection_id_limit=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, params->active_connection_id_limit); - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " disable_active_migration=%d"), - NGTCP2_LOG_TP_HD_FIELDS, params->disable_active_migration); - log->log_printf(log->user_data, - (NGTCP2_LOG_TP " max_datagram_frame_size=%" PRIu64), - NGTCP2_LOG_TP_HD_FIELDS, params->max_datagram_frame_size); - log->log_printf(log->user_data, (NGTCP2_LOG_TP " grease_quic_bit=%d"), - NGTCP2_LOG_TP_HD_FIELDS, params->grease_quic_bit); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " initial_max_stream_data_bidi_local=", + params->initial_max_stream_data_bidi_local); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " initial_max_stream_data_bidi_remote=", + params->initial_max_stream_data_bidi_remote); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " initial_max_stream_data_uni=", + params->initial_max_stream_data_uni); + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " initial_max_data=", params->initial_max_data); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " initial_max_streams_bidi=", + params->initial_max_streams_bidi); + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " initial_max_streams_uni=", params->initial_max_streams_uni); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " max_idle_timeout=", + params->max_idle_timeout / NGTCP2_MILLISECONDS); + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " max_udp_payload_size=", params->max_udp_payload_size); + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " ack_delay_exponent=", params->ack_delay_exponent); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " max_ack_delay=", + params->max_ack_delay / NGTCP2_MILLISECONDS); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " active_connection_id_limit=", + params->active_connection_id_limit); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " disable_active_migration=", + params->disable_active_migration); + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " max_datagram_frame_size=", params->max_datagram_frame_size); + ngtcp2_log_infof_raw( + log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " grease_quic_bit=", params->grease_quic_bit); if (params->version_info_present) { - log->log_printf( - log->user_data, - (NGTCP2_LOG_TP " version_information.chosen_version=0x%08x"), - NGTCP2_LOG_TP_HD_FIELDS, params->version_info.chosen_version); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP " version_information.chosen_version=0x", + hexw(params->version_info.chosen_version, 8)); assert(!(params->version_info.available_versionslen & 0x3)); @@ -751,48 +635,42 @@ void ngtcp2_log_remote_tp(ngtcp2_log *log, i += sizeof(uint32_t)) { p = ngtcp2_get_uint32be(&version, p); - log->log_printf( - log->user_data, - (NGTCP2_LOG_TP " version_information.available_versions[%zu]=0x%08x"), - NGTCP2_LOG_TP_HD_FIELDS, i >> 2, version); + ngtcp2_log_infof_raw(log, NGTCP2_LOG_EVENT_CRY, + NGTCP2_LOG_TP + " version_information.available_versions[", + i >> 2, "]=0x", hexw(version, 8)); } } } void ngtcp2_log_pkt_lost(ngtcp2_log *log, int64_t pkt_num, uint8_t type, uint8_t flags, ngtcp2_tstamp sent_ts) { - if (!log->log_printf || !(log->events & NGTCP2_LOG_EVENT_LDC)) { + if ((!log->log_write && !log->log_printf) || + !(log->events & NGTCP2_LOG_EVENT_LDC)) { return; } - ngtcp2_log_info(log, NGTCP2_LOG_EVENT_LDC, - "pkn=%" PRId64 " lost type=%s sent_ts=%" PRIu64, pkt_num, - strpkttype_type_flags(type, flags), sent_ts); + ngtcp2_log_infof(log, NGTCP2_LOG_EVENT_LDC, "pkn=", pkt_num, + " lost type=", strpkttype_type_flags(type, flags), + " sent_ts=", sent_ts); } static void log_pkt_hd(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const char *dir) { - uint8_t dcid[sizeof(hd->dcid.data) * 2 + 1]; - uint8_t scid[sizeof(hd->scid.data) * 2 + 1]; - - if (!log->log_printf || !(log->events & NGTCP2_LOG_EVENT_PKT)) { + if ((!log->log_write && !log->log_printf) || + !(log->events & NGTCP2_LOG_EVENT_PKT)) { return; } if (hd->type == NGTCP2_PKT_1RTT) { - ngtcp2_log_info( - log, NGTCP2_LOG_EVENT_PKT, "%s pkn=%" PRId64 " dcid=0x%s type=%s k=%d", - dir, hd->pkt_num, - (const char *)ngtcp2_encode_hex(dcid, hd->dcid.data, hd->dcid.datalen), - strpkttype(hd), (hd->flags & NGTCP2_PKT_FLAG_KEY_PHASE) != 0); + ngtcp2_log_infof(log, NGTCP2_LOG_EVENT_PKT, dir, " pkn=", hd->pkt_num, + " dcid=0x", &hd->dcid, " type=", strpkttype(hd), + " k=", (hd->flags & NGTCP2_PKT_FLAG_KEY_PHASE) != 0); } else { - ngtcp2_log_info( - log, NGTCP2_LOG_EVENT_PKT, - "%s pkn=%" PRId64 " dcid=0x%s scid=0x%s version=0x%08x type=%s len=%zu", - dir, hd->pkt_num, - (const char *)ngtcp2_encode_hex(dcid, hd->dcid.data, hd->dcid.datalen), - (const char *)ngtcp2_encode_hex(scid, hd->scid.data, hd->scid.datalen), - hd->version, strpkttype(hd), hd->len); + ngtcp2_log_infof(log, NGTCP2_LOG_EVENT_PKT, dir, " pkn=", hd->pkt_num, + " dcid=0x", &hd->dcid, " scid=0x", &hd->scid, + " version=0x", hexw(hd->version, 8), + " type=", strpkttype(hd), " len=", hd->len); } } @@ -804,31 +682,6 @@ void ngtcp2_log_tx_pkt_hd(ngtcp2_log *log, const ngtcp2_pkt_hd *hd) { log_pkt_hd(log, hd, "tx"); } -void ngtcp2_log_info(ngtcp2_log *log, ngtcp2_log_event ev, const char *fmt, - ...) { - va_list ap; - int n; - char buf[NGTCP2_LOG_BUFLEN]; - - if (!log->log_printf || !(log->events & ev)) { - return; - } - - va_start(ap, fmt); - n = vsnprintf(buf, sizeof(buf), fmt, ap); - va_end(ap); - - if (n < 0 || (size_t)n >= sizeof(buf)) { - return; - } - - log->log_printf(log->user_data, (NGTCP2_LOG_HD " %s"), - timestamp_cast(log->last_ts - log->ts), log->scid, - strevent(ev), buf); -} - -void ngtcp2_log_tx_cancel(ngtcp2_log *log, const ngtcp2_pkt_hd *hd) { - ngtcp2_log_info(log, NGTCP2_LOG_EVENT_PKT, - "cancel tx pkn=%" PRId64 " type=%s", hd->pkt_num, - strpkttype(hd)); +uint64_t ngtcp2_log_timestamp(const ngtcp2_log *log) { + return (log->last_ts - log->ts) / NGTCP2_MILLISECONDS; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_log.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_log.h index 13fb81a72e1d51..c014cea1984af4 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_log.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_log.h @@ -32,8 +32,12 @@ #include #include "ngtcp2_pkt.h" +#include "ngtcp2_fmt.h" + +#define NGTCP2_LOG_BUFLEN 1024 typedef struct ngtcp2_log { + ngtcp2_log_write log_write; /* log_printf is a sink to write log. NULL means no logging output. */ ngtcp2_printf log_printf; @@ -49,7 +53,8 @@ typedef struct ngtcp2_log { log_pritnf. */ void *user_data; /* scid is SCID encoded as NULL-terminated hex string. */ - uint8_t scid[NGTCP2_MAX_CIDLEN * 2 + 1]; + char scid[NGTCP2_MAX_CIDLEN * 2 + 1]; + char *buf; } ngtcp2_log; /** @@ -96,8 +101,8 @@ typedef enum ngtcp2_log_event { } ngtcp2_log_event; void ngtcp2_log_init(ngtcp2_log *log, const ngtcp2_cid *scid, - ngtcp2_printf log_printf, ngtcp2_tstamp ts, - void *user_data); + ngtcp2_log_write log_write, ngtcp2_printf log_printf, + char *buf, ngtcp2_tstamp ts, void *user_data); void ngtcp2_log_rx_fr(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const ngtcp2_frame *fr); @@ -107,7 +112,7 @@ void ngtcp2_log_tx_fr(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, void ngtcp2_log_rx_vn(ngtcp2_log *log, const ngtcp2_pkt_hd *hd, const uint32_t *sv, size_t nsv); -void ngtcp2_log_rx_sr(ngtcp2_log *log, const ngtcp2_pkt_stateless_reset *sr); +void ngtcp2_log_rx_sr(ngtcp2_log *log, const ngtcp2_pkt_stateless_reset2 *sr); void ngtcp2_log_remote_tp(ngtcp2_log *log, const ngtcp2_transport_params *params); @@ -119,14 +124,57 @@ void ngtcp2_log_rx_pkt_hd(ngtcp2_log *log, const ngtcp2_pkt_hd *hd); void ngtcp2_log_tx_pkt_hd(ngtcp2_log *log, const ngtcp2_pkt_hd *hd); -void ngtcp2_log_tx_cancel(ngtcp2_log *log, const ngtcp2_pkt_hd *hd); - -/** - * @function - * - * `ngtcp2_log_info` writes info level log. - */ -void ngtcp2_log_info(ngtcp2_log *log, ngtcp2_log_event ev, const char *fmt, - ...); +uint64_t ngtcp2_log_timestamp(const ngtcp2_log *log); + +static inline const char *ngtcp2_log_event_str(ngtcp2_log_event ev) { + switch (ev) { + case NGTCP2_LOG_EVENT_CON: + return "con"; + case NGTCP2_LOG_EVENT_PKT: + return "pkt"; + case NGTCP2_LOG_EVENT_FRM: + return "frm"; + case NGTCP2_LOG_EVENT_LDC: + return "ldc"; + case NGTCP2_LOG_EVENT_CRY: + return "cry"; + case NGTCP2_LOG_EVENT_PTV: + return "ptv"; + case NGTCP2_LOG_EVENT_CCA: + return "cca"; + case NGTCP2_LOG_EVENT_NONE: + default: + return "non"; + } +} + +#define NGTCP2_LOG_HD(LOG, EV) \ + "I", uintw(ngtcp2_log_timestamp(LOG), 8), " 0x", (LOG)->scid, " ", \ + ngtcp2_log_event_str(EV), " " + +#define ngtcp2_log_infof_raw(LOG, EV, ...) \ + do { \ + size_t log_nwrite; \ + \ + ngtcp2_fmt_format((LOG)->buf, &log_nwrite, NGTCP2_LOG_HD((LOG), (EV)), \ + __VA_ARGS__); \ + if ((LOG)->log_write) { \ + (LOG)->log_write((LOG)->user_data, (LOG)->buf, log_nwrite); \ + } else { \ + (LOG)->log_printf((LOG)->user_data, "%s", (LOG)->buf); \ + } \ + } while (0) + +#define ngtcp2_log_infof(LOG, EV, ...) \ + do { \ + if ((!(LOG)->log_write && !(LOG)->log_printf) || \ + !((LOG)->events & (EV))) { \ + break; \ + } \ + \ + ngtcp2_log_infof_raw((LOG), (EV), __VA_ARGS__); \ + } while (0) + +#define ngtcp2_log_info(LOG, EV, ARG) ngtcp2_log_infof((LOG), (EV), (ARG)) #endif /* !defined(NGTCP2_LOG_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_macro.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_macro.h index dfe5e0aed220f8..ea98c9e1276114 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_macro.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_macro.h @@ -52,30 +52,91 @@ */ #define ngtcp2_arraylen(A) (sizeof(A) / sizeof(A[0])) +/* + * ngtcp2_strlen_lit returns the length of string literal |S|. This + * macro assumes |S| is NULL-terminated string literal. It must not + * be used with pointers. + */ +#define ngtcp2_strlen_lit(S) (sizeof(S) - 1) + #define ngtcp2_max_def(SUFFIX, T) \ static inline T ngtcp2_max_##SUFFIX(T a, T b) { return a < b ? b : a; } -ngtcp2_max_def(int8, int8_t) -ngtcp2_max_def(int16, int16_t) -ngtcp2_max_def(int32, int32_t) -ngtcp2_max_def(int64, int64_t) -ngtcp2_max_def(uint8, uint8_t) -ngtcp2_max_def(uint16, uint16_t) -ngtcp2_max_def(uint32, uint32_t) -ngtcp2_max_def(uint64, uint64_t) -ngtcp2_max_def(size, size_t) +ngtcp2_max_def(long_long_int, long long int) +ngtcp2_max_def(long_int, long int) +ngtcp2_max_def(int, int) +ngtcp2_max_def(short_int, short int) +ngtcp2_max_def(signed_char, signed char) +ngtcp2_max_def(char, char) +ngtcp2_max_def(unsigned_long_long_int, unsigned long long int) +ngtcp2_max_def(unsigned_long_int, unsigned long int) +ngtcp2_max_def(unsigned_int, unsigned int) +ngtcp2_max_def(unsigned_short_int, unsigned short int) +ngtcp2_max_def(unsigned_char, unsigned char) + +#define ngtcp2_max(A, B) \ + _Generic((A), \ + long long int: ngtcp2_max_long_long_int, \ + long int: ngtcp2_max_long_int, \ + int: _Generic((B), \ + long long int: ngtcp2_max_long_long_int, \ + long int: ngtcp2_max_long_int, \ + int: ngtcp2_max_int, \ + short int: ngtcp2_max_short_int, \ + signed char: ngtcp2_max_signed_char, \ + char: ngtcp2_max_char, \ + unsigned long long int: ngtcp2_max_unsigned_long_long_int, \ + unsigned long int: ngtcp2_max_unsigned_long_int, \ + unsigned int: ngtcp2_max_unsigned_int, \ + unsigned short int: ngtcp2_max_unsigned_short_int, \ + unsigned char: ngtcp2_max_unsigned_char), \ + short int: ngtcp2_max_short_int, \ + signed char: ngtcp2_max_signed_char, \ + char: ngtcp2_max_char, \ + unsigned long long int: ngtcp2_max_unsigned_long_long_int, \ + unsigned long int: ngtcp2_max_unsigned_long_int, \ + unsigned int: ngtcp2_max_unsigned_int, \ + unsigned short int: ngtcp2_max_unsigned_short_int, \ + unsigned char: ngtcp2_max_unsigned_char)((A), (B)) #define ngtcp2_min_def(SUFFIX, T) \ static inline T ngtcp2_min_##SUFFIX(T a, T b) { return a < b ? a : b; } -ngtcp2_min_def(int8, int8_t) -ngtcp2_min_def(int16, int16_t) -ngtcp2_min_def(int32, int32_t) -ngtcp2_min_def(int64, int64_t) -ngtcp2_min_def(uint8, uint8_t) -ngtcp2_min_def(uint16, uint16_t) -ngtcp2_min_def(uint32, uint32_t) -ngtcp2_min_def(uint64, uint64_t) -ngtcp2_min_def(size, size_t) +ngtcp2_min_def(long_long_int, long long int) +ngtcp2_min_def(long_int, long int) +ngtcp2_min_def(int, int) +ngtcp2_min_def(short_int, short int) +ngtcp2_min_def(signed_char, signed char) +ngtcp2_min_def(char, char) +ngtcp2_min_def(unsigned_long_long_int, unsigned long long int) +ngtcp2_min_def(unsigned_long_int, unsigned long int) +ngtcp2_min_def(unsigned_int, unsigned int) +ngtcp2_min_def(unsigned_short_int, unsigned short int) +ngtcp2_min_def(unsigned_char, unsigned char) + +#define ngtcp2_min(A, B) \ + _Generic((A), \ + long long int: ngtcp2_min_long_long_int, \ + long int: ngtcp2_min_long_int, \ + int: _Generic((B), \ + long long int: ngtcp2_min_long_long_int, \ + long int: ngtcp2_min_long_int, \ + int: ngtcp2_min_int, \ + short int: ngtcp2_min_short_int, \ + signed char: ngtcp2_min_signed_char, \ + char: ngtcp2_min_char, \ + unsigned long long int: ngtcp2_min_unsigned_long_long_int, \ + unsigned long int: ngtcp2_min_unsigned_long_int, \ + unsigned int: ngtcp2_min_unsigned_int, \ + unsigned short int: ngtcp2_min_unsigned_short_int, \ + unsigned char: ngtcp2_min_unsigned_char), \ + short int: ngtcp2_min_short_int, \ + signed char: ngtcp2_min_signed_char, \ + char: ngtcp2_min_char, \ + unsigned long long int: ngtcp2_min_unsigned_long_long_int, \ + unsigned long int: ngtcp2_min_unsigned_long_int, \ + unsigned int: ngtcp2_min_unsigned_int, \ + unsigned short int: ngtcp2_min_unsigned_short_int, \ + unsigned char: ngtcp2_min_unsigned_char)((A), (B)) #endif /* !defined(NGTCP2_MACRO_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_map.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_map.c index 246c779f2908ca..3ad4b9f1ef0591 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_map.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_map.c @@ -34,11 +34,10 @@ #define NGTCP2_INITIAL_HASHBITS 4 void ngtcp2_map_init(ngtcp2_map *map, uint64_t seed, const ngtcp2_mem *mem) { - map->mem = mem; - map->hashbits = 0; - map->table = NULL; - map->seed = seed; - map->size = 0; + *map = (ngtcp2_map){ + .mem = mem, + .seed = seed, + }; } void ngtcp2_map_free(ngtcp2_map *map) { @@ -46,30 +45,27 @@ void ngtcp2_map_free(ngtcp2_map *map) { return; } - ngtcp2_mem_free(map->mem, map->table); + ngtcp2_mem_free(map->mem, map->keys); } int ngtcp2_map_each(const ngtcp2_map *map, int (*func)(void *data, void *ptr), void *ptr) { int rv; size_t i; - ngtcp2_map_bucket *bkt; size_t tablelen; if (map->size == 0) { return 0; } - tablelen = 1u << map->hashbits; + tablelen = (size_t)1 << map->hashbits; for (i = 0; i < tablelen; ++i) { - bkt = &map->table[i]; - - if (bkt->data == NULL) { + if (map->psl[i] == 0) { continue; } - rv = func(bkt->data, ptr); + rv = func(map->data[i], ptr); if (rv != 0) { return rv; } @@ -78,176 +74,228 @@ int ngtcp2_map_each(const ngtcp2_map *map, int (*func)(void *data, void *ptr), return 0; } -static size_t map_hash(const ngtcp2_map *map, ngtcp2_map_key_type key) { - /* hasher from - https://github.com/rust-lang/rustc-hash/blob/dc5c33f1283de2da64d8d7a06401d91aded03ad4/src/lib.rs - We do not perform finalization here because we use top bits - anyway. */ - key += map->seed; - key *= 0xf1357aea2e62a9c5ull; - return (size_t)((key * 11400714819323198485llu) >> (64 - map->hashbits)); -} - -static void map_bucket_swap(ngtcp2_map_bucket *a, ngtcp2_map_bucket *b) { - ngtcp2_map_bucket c = *a; +/* Hasher from + https://github.com/rust-lang/rustc-hash/blob/dc5c33f1283de2da64d8d7a06401d91aded03ad4/src/lib.rs + to maximize the output's sensitivity to all input bits. */ +#define NGTCP2_MAP_HASHER 0xF1357AEA2E62A9C5ULL +/* 64-bit Fibonacci hashing constant, Golden Ratio constant, to get + the high bits with the good distribution. */ +#define NGTCP2_MAP_FIBO 0x9E3779B97F4A7C15ULL - *a = *b; - *b = c; +static size_t map_index(const ngtcp2_map *map, ngtcp2_map_key_type key) { + key += map->seed; + key *= NGTCP2_MAP_HASHER; + return (size_t)((key * NGTCP2_MAP_FIBO) >> (64 - map->hashbits)); } #ifndef WIN32 void ngtcp2_map_print_distance(const ngtcp2_map *map) { size_t i; size_t idx; - ngtcp2_map_bucket *bkt; size_t tablelen; if (map->size == 0) { return; } - tablelen = 1u << map->hashbits; + tablelen = (size_t)1 << map->hashbits; for (i = 0; i < tablelen; ++i) { - bkt = &map->table[i]; - - if (bkt->data == NULL) { + if (map->psl[i] == 0) { fprintf(stderr, "@%zu \n", i); continue; } - idx = map_hash(map, bkt->key); - fprintf(stderr, "@%zu hash=%zu key=%" PRIu64 " base=%zu distance=%u\n", i, - map_hash(map, bkt->key), bkt->key, idx, bkt->psl); + idx = map_index(map, map->keys[i]); + fprintf(stderr, "@%zu key=%" PRIu64 " base=%zu distance=%u\n", i, + map->keys[i], idx, map->psl[i] - 1); } } #endif /* !defined(WIN32) */ -static int map_insert(ngtcp2_map *map, ngtcp2_map_key_type key, void *data) { - size_t idx = map_hash(map, key); - ngtcp2_map_bucket b = { - .key = key, - .data = data, - }; - ngtcp2_map_bucket *bkt; - size_t mask = (1u << map->hashbits) - 1; +static void map_set_entry(ngtcp2_map *map, size_t idx, ngtcp2_map_key_type key, + void *data, size_t psl) { + map->keys[idx] = key; + map->data[idx] = data; + map->psl[idx] = (uint8_t)psl; +} + +#define NGTCP2_SWAP(TYPE, A, B) \ + do { \ + TYPE t = (TYPE) * (A); \ + \ + *(A) = *(B); \ + *(B) = t; \ + } while (0) + +/* + * map_insert inserts |key| and |data| to |map|, and returns the index + * where the pair is stored if it succeeds. Otherwise, it returns one + * of the following negative error codes: + * + * NGTCP2_ERR_INVALID_ARGUMENT + * The another data associated to |key| is already present. + */ +static ngtcp2_ssize map_insert(ngtcp2_map *map, ngtcp2_map_key_type key, + void *data) { + size_t idx = map_index(map, key); + size_t mask = ((size_t)1 << map->hashbits) - 1; + size_t psl = 1; + size_t kpsl; for (;;) { - bkt = &map->table[idx]; + kpsl = map->psl[idx]; - if (bkt->data == NULL) { - *bkt = b; + if (kpsl == 0) { + map_set_entry(map, idx, key, data, psl); ++map->size; - return 0; + + return (ngtcp2_ssize)idx; } - if (b.psl > bkt->psl) { - map_bucket_swap(bkt, &b); - } else if (bkt->key == key) { - /* TODO This check is just a waste after first swap or if this - function is called from map_resize. That said, there is no - difference with or without this conditional in performance - wise. */ + if (psl > kpsl) { + NGTCP2_SWAP(ngtcp2_map_key_type, &key, &map->keys[idx]); + NGTCP2_SWAP(void *, &data, &map->data[idx]); + NGTCP2_SWAP(uint8_t, &psl, &map->psl[idx]); + } else if (map->keys[idx] == key) { + /* This check ensures that no duplicate keys are inserted. But + it is just a waste after first swap or if this function is + called from map_resize. That said, there is no difference + with or without this conditional in performance wise. */ return NGTCP2_ERR_INVALID_ARGUMENT; } - ++b.psl; + ++psl; idx = (idx + 1) & mask; } } +/* NGTCP2_MAP_MAX_HASHBITS is the maximum number of bits used for hash + table. The theoretical limit of the maximum number of keys that + can be stored is 1 << NGTCP2_MAP_MAX_HASHBITS. */ +#define NGTCP2_MAP_MAX_HASHBITS (sizeof(size_t) * 8 - 1) + static int map_resize(ngtcp2_map *map, size_t new_hashbits) { size_t i; - ngtcp2_map_bucket *bkt; size_t tablelen; - int rv; + ngtcp2_ssize idx; ngtcp2_map new_map = { - .table = ngtcp2_mem_calloc(map->mem, 1u << new_hashbits, - sizeof(ngtcp2_map_bucket)), .mem = map->mem, .seed = map->seed, .hashbits = new_hashbits, }; - (void)rv; + void *buf; + (void)idx; - if (new_map.table == NULL) { + if (new_hashbits > NGTCP2_MAP_MAX_HASHBITS) { return NGTCP2_ERR_NOMEM; } + tablelen = (size_t)1 << new_hashbits; + + buf = ngtcp2_mem_calloc(map->mem, tablelen, + sizeof(ngtcp2_map_key_type) + sizeof(void *) + + sizeof(uint8_t)); + if (buf == NULL) { + return NGTCP2_ERR_NOMEM; + } + + new_map.keys = buf; + new_map.data = + (void *)((uint8_t *)new_map.keys + tablelen * sizeof(ngtcp2_map_key_type)); + new_map.psl = (uint8_t *)new_map.data + tablelen * sizeof(void *); + if (map->size) { - tablelen = 1u << map->hashbits; + tablelen = (size_t)1 << map->hashbits; for (i = 0; i < tablelen; ++i) { - bkt = &map->table[i]; - if (bkt->data == NULL) { + if (map->psl[i] == 0) { continue; } - rv = map_insert(&new_map, bkt->key, bkt->data); + idx = map_insert(&new_map, map->keys[i], map->data[i]); - assert(0 == rv); + /* map_insert must not fail because all keys are unique during + resize. */ + assert(idx >= 0); } } - ngtcp2_mem_free(map->mem, map->table); - map->table = new_map.table; + ngtcp2_mem_free(map->mem, map->keys); + map->keys = new_map.keys; + map->data = new_map.data; + map->psl = new_map.psl; map->hashbits = new_hashbits; return 0; } +/* NGTCP2_MAX_PSL_RESIZE_THRESH is the maximum psl threshold. If + reached, resize the table. */ +#define NGTCP2_MAX_PSL_RESIZE_THRESH 128 + int ngtcp2_map_insert(ngtcp2_map *map, ngtcp2_map_key_type key, void *data) { int rv; + size_t tablelen; + ngtcp2_ssize idx; assert(data); - /* Load factor is 7/8 */ - /* Under the very initial condition, that is map->size == 0 and - map->hashbits == 0, 8 > 7 still holds nicely. */ - if ((map->size + 1) * 8 > (1u << map->hashbits) * 7) { - if (map->hashbits) { - rv = map_resize(map, map->hashbits + 1); - if (rv != 0) { - return rv; - } - } else { - rv = map_resize(map, NGTCP2_INITIAL_HASHBITS); - if (rv != 0) { - return rv; - } + /* tablelen is incorrect if map->hashbits == 0 which leads to + tablelen = 1, but it is only used to check the load factor, and + it works in this special case. */ + tablelen = (size_t)1 << map->hashbits; + + /* Load factor is 7 / 8. Because tablelen is power of 2, (tablelen + - (tablelen >> 3)) computes tablelen * 7 / 8. */ + if (map->size + 1 >= (tablelen - (tablelen >> 3))) { + rv = map_resize(map, map->hashbits ? map->hashbits + 1 + : NGTCP2_INITIAL_HASHBITS); + if (rv != 0) { + return rv; } + + idx = map_insert(map, key, data); + if (idx < 0) { + return (int)idx; + } + + return 0; } - rv = map_insert(map, key, data); - if (rv != 0) { - return rv; + idx = map_insert(map, key, data); + if (idx < 0) { + return (int)idx; } - return 0; + /* Resize if psl reaches really large value which is almost + improbable, but just in case. */ + if (map->psl[idx] - 1 < NGTCP2_MAX_PSL_RESIZE_THRESH) { + return 0; + } + + return map_resize(map, map->hashbits + 1); } void *ngtcp2_map_find(const ngtcp2_map *map, ngtcp2_map_key_type key) { size_t idx; - ngtcp2_map_bucket *bkt; - size_t psl = 0; + size_t psl = 1; size_t mask; if (map->size == 0) { return NULL; } - idx = map_hash(map, key); - mask = (1u << map->hashbits) - 1; + idx = map_index(map, key); + mask = ((size_t)1 << map->hashbits) - 1; for (;;) { - bkt = &map->table[idx]; - - if (bkt->data == NULL || psl > bkt->psl) { + if (psl > map->psl[idx]) { return NULL; } - if (bkt->key == key) { - return bkt->data; + if (map->keys[idx] == key) { + return map->data[idx]; } ++psl; @@ -257,38 +305,36 @@ void *ngtcp2_map_find(const ngtcp2_map *map, ngtcp2_map_key_type key) { int ngtcp2_map_remove(ngtcp2_map *map, ngtcp2_map_key_type key) { size_t idx; - ngtcp2_map_bucket *b, *bkt; - size_t psl = 0; + size_t dest; + size_t psl = 1, kpsl; size_t mask; if (map->size == 0) { return NGTCP2_ERR_INVALID_ARGUMENT; } - idx = map_hash(map, key); - mask = (1u << map->hashbits) - 1; + idx = map_index(map, key); + mask = ((size_t)1 << map->hashbits) - 1; for (;;) { - bkt = &map->table[idx]; - - if (bkt->data == NULL || psl > bkt->psl) { + if (psl > map->psl[idx]) { return NGTCP2_ERR_INVALID_ARGUMENT; } - if (bkt->key == key) { - b = bkt; + if (map->keys[idx] == key) { + dest = idx; idx = (idx + 1) & mask; for (;;) { - bkt = &map->table[idx]; - if (bkt->data == NULL || bkt->psl == 0) { - b->data = NULL; + kpsl = map->psl[idx]; + if (kpsl <= 1) { + map->psl[dest] = 0; break; } - --bkt->psl; - *b = *bkt; - b = bkt; + map_set_entry(map, dest, map->keys[idx], map->data[idx], kpsl - 1); + + dest = idx; idx = (idx + 1) & mask; } @@ -308,7 +354,7 @@ void ngtcp2_map_clear(ngtcp2_map *map) { return; } - memset(map->table, 0, sizeof(*map->table) * (1u << map->hashbits)); + memset(map->psl, 0, sizeof(*map->psl) * ((size_t)1 << map->hashbits)); map->size = 0; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_map.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_map.h index 0c1a76552aa4c4..1afe3167e65b85 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_map.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_map.h @@ -38,14 +38,13 @@ typedef uint64_t ngtcp2_map_key_type; -typedef struct ngtcp2_map_bucket { - uint32_t psl; - ngtcp2_map_key_type key; - void *data; -} ngtcp2_map_bucket; - typedef struct ngtcp2_map { - ngtcp2_map_bucket *table; + ngtcp2_map_key_type *keys; + void **data; + /* psl is the Probe Sequence Length. 0 has special meaning that the + element is not stored at i-th position if psl[i] == 0. Because + of this, the actual psl value is psl[i] - 1 if psl[i] > 0. */ + uint8_t *psl; const ngtcp2_mem *mem; uint64_t seed; size_t size; diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_net.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_net.h index 103a2fb2d80714..5fdeb98532053b 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_net.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_net.h @@ -67,11 +67,11 @@ # define ngtcp2_htonl64(N) (N) # else /* !defined(WORDS_BIGENDIAN) */ # if HAVE_DECL_BSWAP_64 -# define ngtcp2_bswap64 bswap_64 +# define ngtcp2_bswap64(N) bswap_64(N) # elif defined(WIN32) -# define ngtcp2_bswap64 _byteswap_uint64 +# define ngtcp2_bswap64(N) _byteswap_uint64(N) # elif defined(__APPLE__) -# define ngtcp2_bswap64 OSSwapInt64 +# define ngtcp2_bswap64(N) OSSwapInt64(N) # else /* !(HAVE_DECL_BSWAP_64 || defined(WIN32) || defined(__APPLE__)) */ # define ngtcp2_bswap64(N) \ ((uint64_t)(ngtcp2_ntohl((uint32_t)(N))) << 32 | \ @@ -83,59 +83,19 @@ #endif /* !HAVE_DECL_BE64TOH */ #ifdef WIN32 -/* Windows requires ws2_32 library for ntonl family functions. We - define inline functions for those function so that we don't have - dependency on that lib. */ - -# ifdef _MSC_VER -# define STIN static __inline -# else /* !defined(_MSC_VER) */ -# define STIN static inline -# endif /* !defined(_MSC_VER) */ - -STIN uint32_t ngtcp2_htonl(uint32_t hostlong) { - uint32_t res; - unsigned char *p = (unsigned char *)&res; - *p++ = (unsigned char)(hostlong >> 24); - *p++ = (hostlong >> 16) & 0xffu; - *p++ = (hostlong >> 8) & 0xffu; - *p = hostlong & 0xffu; - return res; -} - -STIN uint16_t ngtcp2_htons(uint16_t hostshort) { - uint16_t res; - unsigned char *p = (unsigned char *)&res; - *p++ = (unsigned char)(hostshort >> 8); - *p = hostshort & 0xffu; - return res; -} - -STIN uint32_t ngtcp2_ntohl(uint32_t netlong) { - uint32_t res; - unsigned char *p = (unsigned char *)&netlong; - res = (uint32_t)(*p++ << 24); - res += (uint32_t)(*p++ << 16); - res += (uint32_t)(*p++ << 8); - res += *p; - return res; -} - -STIN uint16_t ngtcp2_ntohs(uint16_t netshort) { - uint16_t res; - unsigned char *p = (unsigned char *)&netshort; - res = (uint16_t)(*p++ << 8); - res += *p; - return res; -} - +/* Windows requires ws2_32 library for ntonl family of functions. + Instead of using them, use _byteswap_* functions. This is fine + because all platforms that can run Windows these days are little + endian. */ +# define ngtcp2_htonl(N) _byteswap_ulong(N) +# define ngtcp2_htons(N) _byteswap_ushort(N) +# define ngtcp2_ntohl(N) _byteswap_ulong(N) +# define ngtcp2_ntohs(N) _byteswap_ushort(N) #else /* !defined(WIN32) */ - -# define ngtcp2_htonl htonl -# define ngtcp2_htons htons -# define ngtcp2_ntohl ntohl -# define ngtcp2_ntohs ntohs - +# define ngtcp2_htonl(N) htonl(N) +# define ngtcp2_htons(N) htons(N) +# define ngtcp2_ntohl(N) ntohl(N) +# define ngtcp2_ntohs(N) ntohs(N) #endif /* !defined(WIN32) */ #endif /* !defined(NGTCP2_NET_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_objalloc.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_objalloc.h index cf23de7b2b7f20..38eabb323ba72b 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_objalloc.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_objalloc.h @@ -69,7 +69,7 @@ void ngtcp2_objalloc_clear(ngtcp2_objalloc *objalloc); inline static void ngtcp2_objalloc_##NAME##_init( \ ngtcp2_objalloc *objalloc, size_t nmemb, const ngtcp2_mem *mem) { \ ngtcp2_objalloc_init( \ - objalloc, ((sizeof(TYPE) + 0xfu) & ~(uintptr_t)0xfu) * nmemb, mem); \ + objalloc, ((sizeof(TYPE) + 0xFU) & ~(size_t)0xFU) * nmemb, mem); \ } \ \ TYPE *ngtcp2_objalloc_##NAME##_get(ngtcp2_objalloc *objalloc); \ @@ -123,7 +123,7 @@ void ngtcp2_objalloc_clear(ngtcp2_objalloc *objalloc); inline static void ngtcp2_objalloc_##NAME##_init( \ ngtcp2_objalloc *objalloc, size_t nmemb, const ngtcp2_mem *mem) { \ ngtcp2_objalloc_init( \ - objalloc, ((sizeof(TYPE) + 0xfu) & ~(uintptr_t)0xfu) * nmemb, mem); \ + objalloc, ((sizeof(TYPE) + 0xFU) & ~(size_t)0xFU) * nmemb, mem); \ } \ \ inline static TYPE *ngtcp2_objalloc_##NAME##_get( \ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_path.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_path.c index 83238730033537..c9636a8db9a90f 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_path.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_path.c @@ -28,12 +28,6 @@ #include "ngtcp2_addr.h" -void ngtcp2_path_init(ngtcp2_path *path, const ngtcp2_addr *local, - const ngtcp2_addr *remote) { - path->local = *local; - path->remote = *remote; -} - void ngtcp2_path_copy(ngtcp2_path *dest, const ngtcp2_path *src) { ngtcp2_addr_copy(&dest->local, &src->local); ngtcp2_addr_copy(&dest->remote, &src->remote); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_path.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_path.h index a708378db32fbb..9d4205cee2279f 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_path.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_path.h @@ -31,14 +31,6 @@ #include -/* - * ngtcp2_path_init initializes |path| with the given addresses. Note - * that the buffer pointed by local->addr and remote->addr are not - * copied. Their pointer values are assigned instead. - */ -void ngtcp2_path_init(ngtcp2_path *path, const ngtcp2_addr *local, - const ngtcp2_addr *remote); - /* * ngtcp2_path_storage_init2 initializes |ps| using |path| as initial * data. diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pcg.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pcg.c index 9d0eb57e0db940..ddaf94a779ab36 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pcg.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pcg.c @@ -66,12 +66,12 @@ void ngtcp2_pcg32_init(ngtcp2_pcg32 *pcg, uint64_t seed) { } static uint32_t pcg_rotr_32(uint32_t value, unsigned int rot) { - return (value >> rot) | (value << ((-rot) & 31)); + return (value >> rot) | (value << ((32 - rot) & 31)); } static uint32_t pcg_output_xsh_rr_64_32(uint64_t state) { - return pcg_rotr_32((uint32_t)(((state >> 18u) ^ state) >> 27u), - (unsigned int)(state >> 59u)); + return pcg_rotr_32((uint32_t)(((state >> 18U) ^ state) >> 27U), + (unsigned int)(state >> 59U)); } uint32_t ngtcp2_pcg32_rand(ngtcp2_pcg32 *pcg) { diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.c index d63dc932e1bfb4..d146145843b73f 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.c @@ -408,8 +408,9 @@ ngtcp2_ssize ngtcp2_pkt_encode_hd_long(uint8_t *out, size_t outlen, p = out; *p = (uint8_t)(NGTCP2_HEADER_FORM_BIT | - (ngtcp2_pkt_versioned_type(hd->version, hd->type) << 4) | - (uint8_t)(hd->pkt_numlen - 1)); + (uint32_t)(ngtcp2_pkt_versioned_type(hd->version, hd->type) + << 4) | + (uint32_t)(hd->pkt_numlen - 1)); if (!(hd->flags & NGTCP2_PKT_FLAG_FIXED_BIT_CLEAR)) { *p |= NGTCP2_FIXED_BIT_MASK; @@ -482,8 +483,10 @@ ngtcp2_ssize ngtcp2_pkt_encode_hd_short(uint8_t *out, size_t outlen, return (ngtcp2_ssize)len; } -ngtcp2_ssize ngtcp2_pkt_decode_frame(ngtcp2_frame *dest, const uint8_t *payload, - size_t payloadlen) { +ngtcp2_ssize ngtcp2_frame_decoder_decode(ngtcp2_frame_decoder *frd, + ngtcp2_frame *dest, + const uint8_t *payload, + size_t payloadlen) { uint8_t type; if (payloadlen == 0) { @@ -532,6 +535,7 @@ ngtcp2_ssize ngtcp2_pkt_decode_frame(ngtcp2_frame *dest, const uint8_t *payload, payloadlen); case NGTCP2_FRAME_ACK: case NGTCP2_FRAME_ACK_ECN: + dest->ack.ranges = frd->buf.ack_ranges; return ngtcp2_pkt_decode_ack_frame(&dest->ack, payload, payloadlen); case NGTCP2_FRAME_PATH_CHALLENGE: return ngtcp2_pkt_decode_path_challenge_frame(&dest->path_challenge, @@ -540,6 +544,7 @@ ngtcp2_ssize ngtcp2_pkt_decode_frame(ngtcp2_frame *dest, const uint8_t *payload, return ngtcp2_pkt_decode_path_response_frame(&dest->path_response, payload, payloadlen); case NGTCP2_FRAME_CRYPTO: + dest->stream.data = &frd->buf.data; return ngtcp2_pkt_decode_crypto_frame(&dest->stream, payload, payloadlen); case NGTCP2_FRAME_NEW_TOKEN: return ngtcp2_pkt_decode_new_token_frame(&dest->new_token, payload, @@ -552,14 +557,16 @@ ngtcp2_ssize ngtcp2_pkt_decode_frame(ngtcp2_frame *dest, const uint8_t *payload, payload, payloadlen); case NGTCP2_FRAME_DATAGRAM: case NGTCP2_FRAME_DATAGRAM_LEN: + dest->datagram.data = &frd->buf.data; return ngtcp2_pkt_decode_datagram_frame(&dest->datagram, payload, payloadlen); default: if ((type & ~(NGTCP2_FRAME_STREAM - 1)) == NGTCP2_FRAME_STREAM) { + dest->stream.data = &frd->buf.data; return ngtcp2_pkt_decode_stream_frame(&dest->stream, payload, payloadlen); } - /* For frame types > 0xff, use ngtcp2_get_uvarintlen and + /* For frame types > 0xFF, use ngtcp2_get_uvarintlen and ngtcp2_get_uvarint to get a frame type, and then switch over it. Verify that payloadlen >= ngtcp2_get_uvarintlen(payload) before calling ngtcp2_get_uvarint(payload). */ @@ -769,7 +776,7 @@ ngtcp2_ssize ngtcp2_pkt_decode_ack_frame(ngtcp2_ack *dest, } /* TODO We might not decode all ranges. It could be very large. */ - max_rangecnt = ngtcp2_min_size(NGTCP2_MAX_ACK_RANGES, rangecnt); + max_rangecnt = ngtcp2_min(NGTCP2_MAX_ACK_RANGES, rangecnt); p = payload + 1; @@ -918,7 +925,7 @@ ngtcp2_ssize ngtcp2_pkt_decode_connection_close_frame( return NGTCP2_ERR_FRAME_ENCODING; } - p = ngtcp2_get_uvarint(&vi, p); + ngtcp2_get_uvarint(&vi, p); if (payloadlen - len < vi) { return NGTCP2_ERR_FRAME_ENCODING; } @@ -1053,6 +1060,8 @@ ngtcp2_ssize ngtcp2_pkt_decode_ping_frame(ngtcp2_ping *dest, (void)payload; (void)payloadlen; + assert(payloadlen > 0); + dest->type = NGTCP2_FRAME_PING; return 1; } @@ -1200,8 +1209,7 @@ ngtcp2_ssize ngtcp2_pkt_decode_new_connection_id_frame( ++p; ngtcp2_cid_init(&dest->cid, p, cil); p += cil; - p = ngtcp2_get_bytes(dest->stateless_reset_token, p, - NGTCP2_STATELESS_RESET_TOKENLEN); + p = ngtcp2_get_bytes(dest->token.data, p, sizeof(dest->token.data)); assert((size_t)(p - payload) == len); @@ -1260,7 +1268,7 @@ ngtcp2_ssize ngtcp2_pkt_decode_path_challenge_frame(ngtcp2_path_challenge *dest, p = payload + 1; dest->type = NGTCP2_FRAME_PATH_CHALLENGE; - ngtcp2_cpymem(dest->data, p, sizeof(dest->data)); + ngtcp2_cpymem(dest->data.data, p, sizeof(dest->data.data)); p += sizeof(dest->data); assert((size_t)(p - payload) == len); @@ -1281,7 +1289,7 @@ ngtcp2_ssize ngtcp2_pkt_decode_path_response_frame(ngtcp2_path_response *dest, p = payload + 1; dest->type = NGTCP2_FRAME_PATH_RESPONSE; - ngtcp2_cpymem(dest->data, p, sizeof(dest->data)); + ngtcp2_cpymem(dest->data.data, p, sizeof(dest->data.data)); p += sizeof(dest->data); assert((size_t)(p - payload) == len); @@ -1321,7 +1329,7 @@ ngtcp2_ssize ngtcp2_pkt_decode_crypto_frame(ngtcp2_stream *dest, return NGTCP2_ERR_FRAME_ENCODING; } - p = ngtcp2_get_uvarint(&vi, p); + ngtcp2_get_uvarint(&vi, p); if (payloadlen - len < vi) { return NGTCP2_ERR_FRAME_ENCODING; } @@ -1428,6 +1436,8 @@ ngtcp2_ssize ngtcp2_pkt_decode_handshake_done_frame(ngtcp2_handshake_done *dest, (void)payload; (void)payloadlen; + assert(payloadlen > 0); + dest->type = NGTCP2_FRAME_HANDSHAKE_DONE; return 1; } @@ -1442,9 +1452,7 @@ ngtcp2_ssize ngtcp2_pkt_decode_datagram_frame(ngtcp2_datagram *dest, size_t n; uint64_t vi; - if (payloadlen < len) { - return NGTCP2_ERR_FRAME_ENCODING; - } + assert(payloadlen > 0); type = payload[0]; @@ -1483,16 +1491,13 @@ ngtcp2_ssize ngtcp2_pkt_decode_datagram_frame(ngtcp2_datagram *dest, dest->type = type; - if (datalen == 0) { - dest->datacnt = 0; - dest->data = NULL; - } else { + if (datalen) { + dest->data[0].len = datalen; + dest->data[0].base = (uint8_t *)p; dest->datacnt = 1; - dest->data = dest->rdata; - dest->rdata[0].len = datalen; - - dest->rdata[0].base = (uint8_t *)p; p += datalen; + } else { + dest->datacnt = 0; } assert((size_t)(p - payload) == len); @@ -1502,7 +1507,7 @@ ngtcp2_ssize ngtcp2_pkt_decode_datagram_frame(ngtcp2_datagram *dest, ngtcp2_ssize ngtcp2_pkt_encode_frame(uint8_t *out, size_t outlen, ngtcp2_frame *fr) { - switch (fr->type) { + switch (fr->hd.type) { case NGTCP2_FRAME_STREAM: return ngtcp2_pkt_encode_stream_frame(out, outlen, &fr->stream); case NGTCP2_FRAME_ACK: @@ -1878,7 +1883,7 @@ ngtcp2_pkt_encode_new_connection_id_frame(uint8_t *out, size_t outlen, const ngtcp2_new_connection_id *fr) { size_t len = 1 + ngtcp2_put_uvarintlen(fr->seq) + ngtcp2_put_uvarintlen(fr->retire_prior_to) + 1 + - fr->cid.datalen + NGTCP2_STATELESS_RESET_TOKENLEN; + fr->cid.datalen + sizeof(fr->token.data); uint8_t *p; if (outlen < len) { @@ -1892,8 +1897,7 @@ ngtcp2_pkt_encode_new_connection_id_frame(uint8_t *out, size_t outlen, p = ngtcp2_put_uvarint(p, fr->retire_prior_to); *p++ = (uint8_t)fr->cid.datalen; p = ngtcp2_cpymem(p, fr->cid.data, fr->cid.datalen); - p = ngtcp2_cpymem(p, fr->stateless_reset_token, - NGTCP2_STATELESS_RESET_TOKENLEN); + p = ngtcp2_cpymem(p, fr->token.data, sizeof(fr->token.data)); assert((size_t)(p - out) == len); @@ -1935,7 +1939,7 @@ ngtcp2_pkt_encode_path_challenge_frame(uint8_t *out, size_t outlen, p = out; *p++ = NGTCP2_FRAME_PATH_CHALLENGE; - p = ngtcp2_cpymem(p, fr->data, sizeof(fr->data)); + p = ngtcp2_cpymem(p, fr->data.data, sizeof(fr->data.data)); assert((size_t)(p - out) == len); @@ -1955,7 +1959,7 @@ ngtcp2_pkt_encode_path_response_frame(uint8_t *out, size_t outlen, p = out; *p++ = NGTCP2_FRAME_PATH_RESPONSE; - p = ngtcp2_cpymem(p, fr->data, sizeof(fr->data)); + p = ngtcp2_cpymem(p, fr->data.data, sizeof(fr->data.data)); assert((size_t)(p - out) == len); @@ -2111,7 +2115,7 @@ ngtcp2_ssize ngtcp2_pkt_write_version_negotiation( p = dest; - *p++ = 0xc0 | unused_random; + *p++ = 0xC0U | unused_random; p = ngtcp2_put_uint32be(p, 0); *p++ = (uint8_t)dcidlen; @@ -2148,20 +2152,20 @@ size_t ngtcp2_pkt_decode_version_negotiation(uint32_t *dest, return payloadlen / sizeof(uint32_t); } -int ngtcp2_pkt_decode_stateless_reset(ngtcp2_pkt_stateless_reset *sr, +int ngtcp2_pkt_decode_stateless_reset(ngtcp2_pkt_stateless_reset2 *sr, const uint8_t *payload, size_t payloadlen) { const uint8_t *p = payload; if (payloadlen < - NGTCP2_MIN_STATELESS_RESET_RANDLEN + NGTCP2_STATELESS_RESET_TOKENLEN) { + NGTCP2_MIN_STATELESS_RESET_RANDLEN + sizeof(sr->token.data)) { return NGTCP2_ERR_INVALID_ARGUMENT; } sr->rand = p; - sr->randlen = payloadlen - NGTCP2_STATELESS_RESET_TOKENLEN; + sr->randlen = payloadlen - sizeof(sr->token.data); p += sr->randlen; - memcpy(sr->stateless_reset_token, p, NGTCP2_STATELESS_RESET_TOKENLEN); + memcpy(sr->token.data, p, sizeof(sr->token.data)); return 0; } @@ -2183,22 +2187,22 @@ int ngtcp2_pkt_decode_retry(ngtcp2_pkt_retry *dest, const uint8_t *payload, int64_t ngtcp2_pkt_adjust_pkt_num(int64_t max_pkt_num, int64_t pkt_num, size_t pkt_numlen) { - int64_t expected = max_pkt_num + 1; - int64_t win = (int64_t)1 << (pkt_numlen * 8); - int64_t hwin = win / 2; - int64_t mask = win - 1; - int64_t cand = (expected & ~mask) | pkt_num; + uint64_t expected = (uint64_t)max_pkt_num + 1; + uint64_t win = 1ULL << (pkt_numlen * 8); + uint64_t hwin = win / 2; + uint64_t mask = win - 1; + uint64_t cand = (expected & ~mask) | (uint64_t)pkt_num; - if (cand <= expected - hwin) { - assert(cand <= (int64_t)NGTCP2_MAX_VARINT - win); - return cand + win; + if (cand + hwin <= expected) { + assert(cand <= NGTCP2_MAX_VARINT - win); + return (int64_t)(cand + win); } if (cand > expected + hwin && cand >= win) { - return cand - win; + return (int64_t)(cand - win); } - return cand; + return (int64_t)cand; } int ngtcp2_pkt_validate_ack(const ngtcp2_ack *fr, int64_t min_pkt_num) { @@ -2240,10 +2244,21 @@ ngtcp2_ssize ngtcp2_pkt_write_stateless_reset(uint8_t *dest, size_t destlen, const uint8_t *stateless_reset_token, const uint8_t *rand, size_t randlen) { + ngtcp2_stateless_reset_token token; + + memcpy(token.data, stateless_reset_token, sizeof(token.data)); + + return ngtcp2_pkt_write_stateless_reset2(dest, destlen, &token, rand, + randlen); +} + +ngtcp2_ssize +ngtcp2_pkt_write_stateless_reset2(uint8_t *dest, size_t destlen, + const ngtcp2_stateless_reset_token *token, + const uint8_t *rand, size_t randlen) { uint8_t *p; - if (destlen < - NGTCP2_MIN_STATELESS_RESET_RANDLEN + NGTCP2_STATELESS_RESET_TOKENLEN) { + if (destlen < NGTCP2_MIN_STATELESS_RESET_RANDLEN + sizeof(token->data)) { return NGTCP2_ERR_NOBUF; } @@ -2253,11 +2268,11 @@ ngtcp2_pkt_write_stateless_reset(uint8_t *dest, size_t destlen, p = dest; - randlen = ngtcp2_min_size(destlen - NGTCP2_STATELESS_RESET_TOKENLEN, randlen); + randlen = ngtcp2_min(destlen - sizeof(token->data), randlen); p = ngtcp2_cpymem(p, rand, randlen); - p = ngtcp2_cpymem(p, stateless_reset_token, NGTCP2_STATELESS_RESET_TOKENLEN); - *dest = (uint8_t)((*dest & 0x7fu) | 0x40u); + p = ngtcp2_cpymem(p, token->data, sizeof(token->data)); + *dest = (uint8_t)((*dest & 0x3FU) | 0x40U); return p - dest; } @@ -2301,11 +2316,11 @@ ngtcp2_ssize ngtcp2_pkt_write_retry( case NGTCP2_PROTO_VER_V1: default: nonce = (const uint8_t *)NGTCP2_RETRY_NONCE_V1; - noncelen = sizeof(NGTCP2_RETRY_NONCE_V1) - 1; + noncelen = ngtcp2_strlen_lit(NGTCP2_RETRY_NONCE_V1); break; case NGTCP2_PROTO_VER_V2: nonce = (const uint8_t *)NGTCP2_RETRY_NONCE_V2; - noncelen = sizeof(NGTCP2_RETRY_NONCE_V2) - 1; + noncelen = ngtcp2_strlen_lit(NGTCP2_RETRY_NONCE_V2); break; } @@ -2351,7 +2366,7 @@ ngtcp2_ssize ngtcp2_pkt_encode_pseudo_retry( return NGTCP2_ERR_NOBUF; } - *p &= 0xf0; + *p &= 0xF0U; *p |= unused; p += nwrite; @@ -2391,11 +2406,11 @@ int ngtcp2_pkt_verify_retry_tag(uint32_t version, const ngtcp2_pkt_retry *retry, case NGTCP2_PROTO_VER_V1: default: nonce = (const uint8_t *)NGTCP2_RETRY_NONCE_V1; - noncelen = sizeof(NGTCP2_RETRY_NONCE_V1) - 1; + noncelen = ngtcp2_strlen_lit(NGTCP2_RETRY_NONCE_V1); break; case NGTCP2_PROTO_VER_V2: nonce = (const uint8_t *)NGTCP2_RETRY_NONCE_V2; - noncelen = sizeof(NGTCP2_RETRY_NONCE_V2) - 1; + noncelen = ngtcp2_strlen_lit(NGTCP2_RETRY_NONCE_V2); break; } @@ -2425,22 +2440,22 @@ size_t ngtcp2_pkt_stream_max_datalen(int64_t stream_id, uint64_t offset, left -= n; if (left > 8 + 1073741823 && len > 1073741823) { - len = ngtcp2_min_uint64(len, 4611686018427387903lu); - return (size_t)ngtcp2_min_uint64(len, (uint64_t)(left - 8)); + len = ngtcp2_min(len, 4611686018427387903UL); + return (size_t)ngtcp2_min(len, (uint64_t)(left - 8)); } if (left > 4 + 16383 && len > 16383) { - len = ngtcp2_min_uint64(len, 1073741823); - return (size_t)ngtcp2_min_uint64(len, (uint64_t)(left - 4)); + len = ngtcp2_min(len, 1073741823); + return (size_t)ngtcp2_min(len, (uint64_t)(left - 4)); } if (left > 2 + 63 && len > 63) { - len = ngtcp2_min_uint64(len, 16383); - return (size_t)ngtcp2_min_uint64(len, (uint64_t)(left - 2)); + len = ngtcp2_min(len, 16383); + return (size_t)ngtcp2_min(len, (uint64_t)(left - 2)); } - len = ngtcp2_min_uint64(len, 63); - return (size_t)ngtcp2_min_uint64(len, (uint64_t)(left - 1)); + len = ngtcp2_min(len, 63); + return (size_t)ngtcp2_min(len, (uint64_t)(left - 1)); } size_t ngtcp2_pkt_crypto_max_datalen(uint64_t offset, size_t len, size_t left) { @@ -2456,23 +2471,23 @@ size_t ngtcp2_pkt_crypto_max_datalen(uint64_t offset, size_t len, size_t left) { if (left > 8 + 1073741823 && len > 1073741823) { #if SIZE_MAX == UINT64_MAX - len = ngtcp2_min_size(len, 4611686018427387903lu); + len = ngtcp2_min(len, 4611686018427387903UL); #endif /* SIZE_MAX == UINT64_MAX */ - return ngtcp2_min_size(len, left - 8); + return ngtcp2_min(len, left - 8); } if (left > 4 + 16383 && len > 16383) { - len = ngtcp2_min_size(len, 1073741823); - return ngtcp2_min_size(len, left - 4); + len = ngtcp2_min(len, 1073741823); + return ngtcp2_min(len, left - 4); } if (left > 2 + 63 && len > 63) { - len = ngtcp2_min_size(len, 16383); - return ngtcp2_min_size(len, left - 2); + len = ngtcp2_min(len, 16383); + return ngtcp2_min(len, left - 2); } - len = ngtcp2_min_size(len, 63); - return ngtcp2_min_size(len, left - 1); + len = ngtcp2_min(len, 63); + return ngtcp2_min(len, left - 1); } size_t ngtcp2_pkt_datagram_framelen(size_t len) { @@ -2855,7 +2870,7 @@ size_t ngtcp2_pkt_remove_vec_partial(ngtcp2_vec *removed_data, ngtcp2_vec *data, } len = 1 + ngtcp2_pcg32_rand_n( - pcg, (uint32_t)ngtcp2_min_size(30, removed_data->len - 1)); + pcg, (uint32_t)ngtcp2_min(30, removed_data->len - 1)); assert(len < removed_data->len); ngtcp2_vec_split_at(&data[datacnt], removed_data, len); @@ -2864,3 +2879,8 @@ size_t ngtcp2_pkt_remove_vec_partial(ngtcp2_vec *removed_data, ngtcp2_vec *data, return datacnt + 1; } + +int ngtcp2_stateless_reset_token_eq(const ngtcp2_stateless_reset_token *a, + const ngtcp2_stateless_reset_token *b) { + return memcmp(a->data, b->data, sizeof(a->data)) == 0; +} diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.h index ed358dc48d5cae..b6c818d491a596 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.h @@ -32,20 +32,20 @@ #include /* QUIC header macros */ -#define NGTCP2_HEADER_FORM_BIT 0x80 -#define NGTCP2_FIXED_BIT_MASK 0x40 -#define NGTCP2_PKT_NUMLEN_MASK 0x03 +#define NGTCP2_HEADER_FORM_BIT 0x80U +#define NGTCP2_FIXED_BIT_MASK 0x40U +#define NGTCP2_PKT_NUMLEN_MASK 0x03U /* Long header specific macros */ -#define NGTCP2_LONG_TYPE_MASK 0x30 -#define NGTCP2_LONG_RESERVED_BIT_MASK 0x0c +#define NGTCP2_LONG_TYPE_MASK 0x30U +#define NGTCP2_LONG_RESERVED_BIT_MASK 0x0CU /* Short header specific macros */ -#define NGTCP2_SHORT_RESERVED_BIT_MASK 0x18 -#define NGTCP2_SHORT_KEY_PHASE_BIT 0x04 +#define NGTCP2_SHORT_RESERVED_BIT_MASK 0x18U +#define NGTCP2_SHORT_KEY_PHASE_BIT 0x04U /* NGTCP2_SR_TYPE is a Type field of Stateless Reset. */ -#define NGTCP2_SR_TYPE 0x1f +#define NGTCP2_SR_TYPE 0x1FU /* NGTCP2_MIN_LONG_HEADERLEN is the minimum length of long header. That is (1|1|TT|RR|PP)<1> + VERSION<4> + DCIL<1> + SCIL<1> + @@ -53,9 +53,9 @@ #define NGTCP2_MIN_LONG_HEADERLEN (1 + 4 + 1 + 1 + 1 + 1) /* STREAM frame specific macros */ -#define NGTCP2_STREAM_FIN_BIT 0x01 -#define NGTCP2_STREAM_LEN_BIT 0x02 -#define NGTCP2_STREAM_OFF_BIT 0x04 +#define NGTCP2_STREAM_FIN_BIT 0x01U +#define NGTCP2_STREAM_LEN_BIT 0x02U +#define NGTCP2_STREAM_OFF_BIT 0x04U /* NGTCP2_MIN_QUIC_PKTLEN is the minimum length of a valid QUIC packet. */ @@ -78,23 +78,23 @@ /* NGTCP2_MAX_SERVER_STREAM_ID_BIDI is the maximum bidirectional server stream ID. */ -#define NGTCP2_MAX_SERVER_STREAM_ID_BIDI ((int64_t)0x3ffffffffffffffdll) +#define NGTCP2_MAX_SERVER_STREAM_ID_BIDI ((int64_t)0x3FFFFFFFFFFFFFFDLL) /* NGTCP2_MAX_CLIENT_STREAM_ID_BIDI is the maximum bidirectional client stream ID. */ -#define NGTCP2_MAX_CLIENT_STREAM_ID_BIDI ((int64_t)0x3ffffffffffffffcll) +#define NGTCP2_MAX_CLIENT_STREAM_ID_BIDI ((int64_t)0x3FFFFFFFFFFFFFFCLL) /* NGTCP2_MAX_SERVER_STREAM_ID_UNI is the maximum unidirectional server stream ID. */ -#define NGTCP2_MAX_SERVER_STREAM_ID_UNI ((int64_t)0x3fffffffffffffffll) +#define NGTCP2_MAX_SERVER_STREAM_ID_UNI ((int64_t)0x3FFFFFFFFFFFFFFFLL) /* NGTCP2_MAX_CLIENT_STREAM_ID_UNI is the maximum unidirectional client stream ID. */ -#define NGTCP2_MAX_CLIENT_STREAM_ID_UNI ((int64_t)0x3ffffffffffffffell) +#define NGTCP2_MAX_CLIENT_STREAM_ID_UNI ((int64_t)0x3FFFFFFFFFFFFFFELL) /* NGTCP2_MAX_NUM_ACK_RANGES is the maximum number of Additional ACK ranges which this library can create, or decode. */ #define NGTCP2_MAX_ACK_RANGES 32 /* NGTCP2_MAX_PKT_NUM is the maximum packet number. */ -#define NGTCP2_MAX_PKT_NUM ((int64_t)((1ll << 62) - 1)) +#define NGTCP2_MAX_PKT_NUM ((int64_t)((1LL << 62) - 1)) /* NGTCP2_MIN_PKT_EXPANDLEN is the minimum packet size expansion to hide/trigger Stateless Reset. */ @@ -103,39 +103,35 @@ /* NGTCP2_RETRY_TAGLEN is the length of Retry packet integrity tag. */ #define NGTCP2_RETRY_TAGLEN 16 -/* NGTCP2_HARD_MAX_UDP_PAYLOAD_SIZE is the maximum UDP datagram - payload size that this library can write. */ -#define NGTCP2_HARD_MAX_UDP_PAYLOAD_SIZE ((1 << 24) - 1) - /* NGTCP2_PKT_LENGTHLEN is the number of bytes that is occupied by Length field in Long packet header. */ #define NGTCP2_PKT_LENGTHLEN 4 /* NGTCP2_PKT_TYPE_INITIAL_V1 is Initial long header packet type for QUIC v1. */ -#define NGTCP2_PKT_TYPE_INITIAL_V1 0x0 +#define NGTCP2_PKT_TYPE_INITIAL_V1 0x0U /* NGTCP2_PKT_TYPE_0RTT_V1 is 0RTT long header packet type for QUIC v1. */ -#define NGTCP2_PKT_TYPE_0RTT_V1 0x1 +#define NGTCP2_PKT_TYPE_0RTT_V1 0x1U /* NGTCP2_PKT_TYPE_HANDSHAKE_V1 is Handshake long header packet type for QUIC v1. */ -#define NGTCP2_PKT_TYPE_HANDSHAKE_V1 0x2 +#define NGTCP2_PKT_TYPE_HANDSHAKE_V1 0x2U /* NGTCP2_PKT_TYPE_RETRY_V1 is Retry long header packet type for QUIC v1. */ -#define NGTCP2_PKT_TYPE_RETRY_V1 0x3 +#define NGTCP2_PKT_TYPE_RETRY_V1 0x3U /* NGTCP2_PKT_TYPE_INITIAL_V2 is Initial long header packet type for QUIC v2. */ -#define NGTCP2_PKT_TYPE_INITIAL_V2 0x1 +#define NGTCP2_PKT_TYPE_INITIAL_V2 0x1U /* NGTCP2_PKT_TYPE_0RTT_V2 is 0RTT long header packet type for QUIC v2. */ -#define NGTCP2_PKT_TYPE_0RTT_V2 0x2 +#define NGTCP2_PKT_TYPE_0RTT_V2 0x2U /* NGTCP2_PKT_TYPE_HANDSHAKE_V2 is Handshake long header packet type for QUIC v2. */ -#define NGTCP2_PKT_TYPE_HANDSHAKE_V2 0x3 +#define NGTCP2_PKT_TYPE_HANDSHAKE_V2 0x3U /* NGTCP2_PKT_TYPE_RETRY_V2 is Retry long header packet type for QUIC v2. */ -#define NGTCP2_PKT_TYPE_RETRY_V2 0x0 +#define NGTCP2_PKT_TYPE_RETRY_V2 0x0U /* NGTCP2_MIN_STREAM_DATALEN is the minimum length of STREAM frame to avoid too small frame. It is not always enforced for various @@ -157,32 +153,36 @@ typedef struct ngtcp2_pkt_retry { uint8_t tag[NGTCP2_RETRY_TAGLEN]; } ngtcp2_pkt_retry; -#define NGTCP2_FRAME_PADDING 0x00 -#define NGTCP2_FRAME_PING 0x01 -#define NGTCP2_FRAME_ACK 0x02 -#define NGTCP2_FRAME_ACK_ECN 0x03 -#define NGTCP2_FRAME_RESET_STREAM 0x04 -#define NGTCP2_FRAME_STOP_SENDING 0x05 -#define NGTCP2_FRAME_CRYPTO 0x06 -#define NGTCP2_FRAME_NEW_TOKEN 0x07 -#define NGTCP2_FRAME_STREAM 0x08 -#define NGTCP2_FRAME_MAX_DATA 0x10 -#define NGTCP2_FRAME_MAX_STREAM_DATA 0x11 -#define NGTCP2_FRAME_MAX_STREAMS_BIDI 0x12 -#define NGTCP2_FRAME_MAX_STREAMS_UNI 0x13 -#define NGTCP2_FRAME_DATA_BLOCKED 0x14 -#define NGTCP2_FRAME_STREAM_DATA_BLOCKED 0x15 -#define NGTCP2_FRAME_STREAMS_BLOCKED_BIDI 0x16 -#define NGTCP2_FRAME_STREAMS_BLOCKED_UNI 0x17 -#define NGTCP2_FRAME_NEW_CONNECTION_ID 0x18 -#define NGTCP2_FRAME_RETIRE_CONNECTION_ID 0x19 -#define NGTCP2_FRAME_PATH_CHALLENGE 0x1a -#define NGTCP2_FRAME_PATH_RESPONSE 0x1b -#define NGTCP2_FRAME_CONNECTION_CLOSE 0x1c -#define NGTCP2_FRAME_CONNECTION_CLOSE_APP 0x1d -#define NGTCP2_FRAME_HANDSHAKE_DONE 0x1e -#define NGTCP2_FRAME_DATAGRAM 0x30 -#define NGTCP2_FRAME_DATAGRAM_LEN 0x31 +#define NGTCP2_FRAME_PADDING 0x00U +#define NGTCP2_FRAME_PING 0x01U +#define NGTCP2_FRAME_ACK 0x02U +#define NGTCP2_FRAME_ACK_ECN 0x03U +#define NGTCP2_FRAME_RESET_STREAM 0x04U +#define NGTCP2_FRAME_STOP_SENDING 0x05U +#define NGTCP2_FRAME_CRYPTO 0x06U +#define NGTCP2_FRAME_NEW_TOKEN 0x07U +#define NGTCP2_FRAME_STREAM 0x08U +#define NGTCP2_FRAME_MAX_DATA 0x10U +#define NGTCP2_FRAME_MAX_STREAM_DATA 0x11U +#define NGTCP2_FRAME_MAX_STREAMS_BIDI 0x12U +#define NGTCP2_FRAME_MAX_STREAMS_UNI 0x13U +#define NGTCP2_FRAME_DATA_BLOCKED 0x14U +#define NGTCP2_FRAME_STREAM_DATA_BLOCKED 0x15U +#define NGTCP2_FRAME_STREAMS_BLOCKED_BIDI 0x16U +#define NGTCP2_FRAME_STREAMS_BLOCKED_UNI 0x17U +#define NGTCP2_FRAME_NEW_CONNECTION_ID 0x18U +#define NGTCP2_FRAME_RETIRE_CONNECTION_ID 0x19U +#define NGTCP2_FRAME_PATH_CHALLENGE 0x1AU +#define NGTCP2_FRAME_PATH_RESPONSE 0x1BU +#define NGTCP2_FRAME_CONNECTION_CLOSE 0x1CU +#define NGTCP2_FRAME_CONNECTION_CLOSE_APP 0x1DU +#define NGTCP2_FRAME_HANDSHAKE_DONE 0x1EU +#define NGTCP2_FRAME_DATAGRAM 0x30U +#define NGTCP2_FRAME_DATAGRAM_LEN 0x31U + +typedef struct ngtcp2_frame_hd { + uint64_t type; +} ngtcp2_frame_hd; /* ngtcp2_stream represents STREAM and CRYPTO frames. */ typedef struct ngtcp2_stream { @@ -195,7 +195,7 @@ typedef struct ngtcp2_stream { uint8_t flags; /* CRYPTO frame does not include this field, and must set it to 0. */ - uint8_t fin; + int fin; /* CRYPTO frame does not include this field, and must set it to 0. */ int64_t stream_id; @@ -204,8 +204,9 @@ typedef struct ngtcp2_stream { the length of data is 1 in this definition, the library may allocate extra bytes to hold more elements. */ size_t datacnt; - /* data is the array of ngtcp2_vec which references data. */ - ngtcp2_vec data[1]; + /* data points to ngtcp2_vec array which references data. If + datacnt == 0, this field may be NULL. */ + ngtcp2_vec *data; } ngtcp2_stream; typedef struct ngtcp2_ack_range { @@ -229,7 +230,7 @@ typedef struct ngtcp2_ack { } ecn; uint64_t first_ack_range; size_t rangecnt; - ngtcp2_ack_range ranges[1]; + ngtcp2_ack_range *ranges; } ngtcp2_ack; typedef struct ngtcp2_padding { @@ -299,7 +300,7 @@ typedef struct ngtcp2_new_connection_id { uint64_t seq; uint64_t retire_prior_to; ngtcp2_cid cid; - uint8_t stateless_reset_token[NGTCP2_STATELESS_RESET_TOKENLEN]; + ngtcp2_stateless_reset_token token; } ngtcp2_new_connection_id; typedef struct ngtcp2_stop_sending { @@ -310,12 +311,12 @@ typedef struct ngtcp2_stop_sending { typedef struct ngtcp2_path_challenge { uint64_t type; - uint8_t data[NGTCP2_PATH_CHALLENGE_DATALEN]; + ngtcp2_path_challenge_data data; } ngtcp2_path_challenge; typedef struct ngtcp2_path_response { uint64_t type; - uint8_t data[NGTCP2_PATH_CHALLENGE_DATALEN]; + ngtcp2_path_challenge_data data; } ngtcp2_path_response; typedef struct ngtcp2_new_token { @@ -339,17 +340,13 @@ typedef struct ngtcp2_datagram { uint64_t dgram_id; /* datacnt is the number of elements that data contains. */ size_t datacnt; - /* data is a pointer to ngtcp2_vec array that stores data. */ + /* data is a pointer to ngtcp2_vec array that stores data. If + datacnt == 0, this field may be NULL.*/ ngtcp2_vec *data; - /* rdata is conveniently embedded to ngtcp2_datagram, so that data - field can just point to the address of this field to store a - single vector which is the case when DATAGRAM is received from a - remote endpoint. */ - ngtcp2_vec rdata[1]; } ngtcp2_datagram; typedef union ngtcp2_frame { - uint64_t type; + ngtcp2_frame_hd hd; ngtcp2_stream stream; ngtcp2_ack ack; ngtcp2_padding padding; @@ -370,11 +367,6 @@ typedef union ngtcp2_frame { ngtcp2_retire_connection_id retire_connection_id; ngtcp2_handshake_done handshake_done; ngtcp2_datagram datagram; - /* Extend ngtcp2_frame so that ngtcp2_stream has at least additional - 3 ngtcp2_vec, totaling 4 slots, which can store HEADERS header, - HEADERS payload, DATA header, and DATA payload in the standard - sized ngtcp2_frame_chain. */ - uint8_t pad[sizeof(ngtcp2_stream) + sizeof(ngtcp2_vec) * 3]; } ngtcp2_frame; typedef struct ngtcp2_pkt_chain ngtcp2_pkt_chain; @@ -454,10 +446,22 @@ ngtcp2_ssize ngtcp2_pkt_encode_hd_long(uint8_t *out, size_t outlen, ngtcp2_ssize ngtcp2_pkt_encode_hd_short(uint8_t *out, size_t outlen, const ngtcp2_pkt_hd *hd); +/* + * ngtcp2_frame_decoder is QUIC frame decoder. For frames that + * require the external buffers (e.g., ngtcp2_stream and ngtcp2_ack), + * it provides those buffers on demand. + */ +typedef struct ngtcp2_frame_decoder { + union { + ngtcp2_vec data; + ngtcp2_ack_range ack_ranges[NGTCP2_MAX_ACK_RANGES]; + } buf; +} ngtcp2_frame_decoder; + /** * @function * - * `ngtcp2_pkt_decode_frame` decodes a QUIC frame from the buffer + * `ngtcp2_frame_decoder_decode` decodes a QUIC frame from the buffer * pointed by |payload| whose length is |payloadlen|. * * This function returns the number of bytes read to decode a single @@ -467,8 +471,10 @@ ngtcp2_ssize ngtcp2_pkt_encode_hd_short(uint8_t *out, size_t outlen, * Frame is badly formatted; or frame type is unknown; or * |payloadlen| is 0. */ -ngtcp2_ssize ngtcp2_pkt_decode_frame(ngtcp2_frame *dest, const uint8_t *payload, - size_t payloadlen); +ngtcp2_ssize ngtcp2_frame_decoder_decode(ngtcp2_frame_decoder *frd, + ngtcp2_frame *dest, + const uint8_t *payload, + size_t payloadlen); /** * @function @@ -508,7 +514,7 @@ size_t ngtcp2_pkt_decode_version_negotiation(uint32_t *dest, * NGTCP2_ERR_INVALID_ARGUMENT * Payloadlen is too short. */ -int ngtcp2_pkt_decode_stateless_reset(ngtcp2_pkt_stateless_reset *sr, +int ngtcp2_pkt_decode_stateless_reset(ngtcp2_pkt_stateless_reset2 *sr, const uint8_t *payload, size_t payloadlen); @@ -1317,4 +1323,11 @@ size_t ngtcp2_pkt_remove_vec_partial(ngtcp2_vec *removed_data, ngtcp2_vec *data, size_t datacnt, uint64_t *offsets, ngtcp2_pcg32 *pcg, const ngtcp2_vec *part); +/* + * ngtcp2_stateless_reset_token_eq returns nonzero if |a| and |b| + * share the same token. + */ +int ngtcp2_stateless_reset_token_eq(const ngtcp2_stateless_reset_token *a, + const ngtcp2_stateless_reset_token *b); + #endif /* !defined(NGTCP2_PKT_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pmtud.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pmtud.c index ebd113f6746217..3827cba9afe910 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pmtud.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pmtud.c @@ -134,7 +134,7 @@ static void pmtud_next_probe(ngtcp2_pmtud *pmtud) { void ngtcp2_pmtud_probe_success(ngtcp2_pmtud *pmtud, size_t payloadlen) { pmtud->max_udp_payload_size = - ngtcp2_max_size(pmtud->max_udp_payload_size, payloadlen); + ngtcp2_max(pmtud->max_udp_payload_size, payloadlen); assert(pmtud->mtu_idx < pmtud->probeslen); @@ -156,8 +156,8 @@ void ngtcp2_pmtud_handle_expiry(ngtcp2_pmtud *pmtud, ngtcp2_tstamp ts) { return; } - pmtud->min_fail_udp_payload_size = ngtcp2_min_size( - pmtud->min_fail_udp_payload_size, pmtud->probes[pmtud->mtu_idx]); + pmtud->min_fail_udp_payload_size = + ngtcp2_min(pmtud->min_fail_udp_payload_size, pmtud->probes[pmtud->mtu_idx]); pmtud_next_probe(pmtud); } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ppe.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ppe.c index 4d193125ae8e89..81bc3296de85ae 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ppe.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ppe.c @@ -44,6 +44,14 @@ void ngtcp2_ppe_init(ngtcp2_ppe *ppe, uint8_t *out, size_t outlen, ppe->cc = cc; } +/* + * ppe_sample_offset returns the offset to sample for packet number + * encryption. + */ +static size_t ppe_sample_offset(ngtcp2_ppe *ppe) { + return ppe->pkt_num_offset + 4; +} + int ngtcp2_ppe_encode_hd(ngtcp2_ppe *ppe, const ngtcp2_pkt_hd *hd) { ngtcp2_ssize rv; ngtcp2_buf *buf = &ppe->buf; @@ -78,6 +86,10 @@ int ngtcp2_ppe_encode_hd(ngtcp2_ppe *ppe, const ngtcp2_pkt_hd *hd) { buf->last += rv; + if (ngtcp2_buf_cap(buf) < ppe_sample_offset(ppe) + NGTCP2_HP_SAMPLELEN) { + return NGTCP2_ERR_NOBUF; + } + ppe->pkt_numlen = hd->pkt_numlen; ppe->hdlen = (size_t)rv; ppe->pkt_num = hd->pkt_num; @@ -105,14 +117,6 @@ int ngtcp2_ppe_encode_frame(ngtcp2_ppe *ppe, ngtcp2_frame *fr) { return 0; } -/* - * ppe_sample_offset returns the offset to sample for packet number - * encryption. - */ -static size_t ppe_sample_offset(ngtcp2_ppe *ppe) { - return ppe->pkt_num_offset + 4; -} - ngtcp2_ssize ngtcp2_ppe_final(ngtcp2_ppe *ppe, const uint8_t **ppkt) { ngtcp2_buf *buf = &ppe->buf; ngtcp2_crypto_cc *cc = ppe->cc; @@ -154,9 +158,9 @@ ngtcp2_ssize ngtcp2_ppe_final(ngtcp2_ppe *ppe, const uint8_t **ppkt) { p = buf->begin; if (*p & NGTCP2_HEADER_FORM_BIT) { - *p = (uint8_t)(*p ^ (mask[0] & 0x0f)); + *p = (uint8_t)(*p ^ (mask[0] & 0x0FU)); } else { - *p = (uint8_t)(*p ^ (mask[0] & 0x1f)); + *p = (uint8_t)(*p ^ (mask[0] & 0x1FU)); } p = buf->begin + ppe->pkt_num_offset; @@ -187,21 +191,21 @@ size_t ngtcp2_ppe_padding_size(ngtcp2_ppe *ppe, size_t n) { ngtcp2_buf *buf = &ppe->buf; size_t pktlen = ngtcp2_buf_len(buf) + cc->aead.max_overhead; size_t len = 0; - size_t max_samplelen; + size_t min_pktlen; - n = ngtcp2_min_size(n, ngtcp2_buf_cap(buf)); + n = ngtcp2_min(n, ngtcp2_buf_cap(buf)); if (pktlen < n) { len = n - pktlen; } /* Ensure header protection sample */ - max_samplelen = - ngtcp2_buf_len(buf) + cc->aead.max_overhead - ppe_sample_offset(ppe); - - if (max_samplelen < NGTCP2_HP_SAMPLELEN) { - len = ngtcp2_max_size(len, NGTCP2_HP_SAMPLELEN - max_samplelen); + min_pktlen = ppe_sample_offset(ppe) + NGTCP2_HP_SAMPLELEN; + if (pktlen < min_pktlen) { + len = ngtcp2_max(len, min_pktlen - pktlen); } + /* ngtcp2_ppe_encode_hd ensures that the buffer has enough capacity + for the padding required for header protection sample. */ assert(ngtcp2_buf_left(buf) >= len + cc->aead.max_overhead); if (len == 0) { @@ -220,24 +224,34 @@ size_t ngtcp2_ppe_dgram_padding(ngtcp2_ppe *ppe) { size_t ngtcp2_ppe_dgram_padding_size(ngtcp2_ppe *ppe, size_t n) { ngtcp2_crypto_cc *cc = ppe->cc; ngtcp2_buf *buf = &ppe->buf; - size_t dgramlen = - ppe->dgram_offset + ngtcp2_buf_len(buf) + cc->aead.max_overhead; + size_t pktlen = ngtcp2_buf_len(buf) + cc->aead.max_overhead; + size_t dgramlen = ppe->dgram_offset + pktlen; size_t len; + size_t min_pktlen; - n = ngtcp2_min_size(n, ppe->dgram_offset + ngtcp2_buf_cap(buf)); + n = ngtcp2_min(n, ppe->dgram_offset + ngtcp2_buf_cap(buf)); - if (dgramlen >= n) { + if (dgramlen < n) { + len = n - dgramlen; + } else { + len = 0; + } + + /* Ensure header protection sample */ + min_pktlen = ppe_sample_offset(ppe) + NGTCP2_HP_SAMPLELEN; + if (pktlen < min_pktlen) { + len = ngtcp2_max(len, min_pktlen - pktlen); + } + + /* ngtcp2_ppe_encode_hd ensures that the buffer has enough capacity + for the padding required for header protection sample. */ + assert(ngtcp2_buf_left(buf) >= len + cc->aead.max_overhead); + + if (len == 0) { return 0; } - len = n - dgramlen; buf->last = ngtcp2_setmem(buf->last, 0, len); return len; } - -int ngtcp2_ppe_ensure_hp_sample(ngtcp2_ppe *ppe) { - ngtcp2_buf *buf = &ppe->buf; - - return ngtcp2_buf_left(buf) >= (4 - ppe->pkt_numlen) + NGTCP2_HP_SAMPLELEN; -} diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ppe.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ppe.h index 9874b36800540d..ba9edc698ad20a 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ppe.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ppe.h @@ -125,7 +125,9 @@ size_t ngtcp2_ppe_dgram_padding(ngtcp2_ppe *ppe); * of a UDP datagram payload is at least |n| bytes long. If it is * unable to add PADDING in that way, this function still adds PADDING * frame as much as possible. This function should be called just - * before calling ngtcp2_ppe_final(). + * before calling ngtcp2_ppe_final(). This function also ensures that + * the packet has enough space for header protection sample by + * possibly adding extra padding more than |n|. * * This function returns the number of bytes added as padding. */ @@ -138,20 +140,11 @@ size_t ngtcp2_ppe_dgram_padding_size(ngtcp2_ppe *ppe, size_t n); * PADDING at least |n| bytes, this function still adds PADDING frames * as much as possible. This function also adds PADDING frames so * that the minimum padding requirement of header protection is met. - * Those padding may be larger than |n| bytes. It is recommended to - * make sure that ngtcp2_ppe_ensure_hp_sample succeeds after writing - * QUIC packet header. This function should be called just before - * calling ngtcp2_ppe_final(). + * Those padding may be larger than |n| bytes. This function should + * be called just before calling ngtcp2_ppe_final(). * * This function returns the number of bytes added as padding. */ size_t ngtcp2_ppe_padding_size(ngtcp2_ppe *ppe, size_t n); -/* - * ngtcp2_ppe_ensure_hp_sample returns nonzero if the buffer has - * enough space for header protection sample. This should be called - * right after packet header is written. - */ -int ngtcp2_ppe_ensure_hp_sample(ngtcp2_ppe *ppe); - #endif /* !defined(NGTCP2_PPE_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pq.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pq.c index 162bed00156755..87d261dd750bd5 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pq.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pq.c @@ -30,11 +30,10 @@ #include "ngtcp2_macro.h" void ngtcp2_pq_init(ngtcp2_pq *pq, ngtcp2_pq_less less, const ngtcp2_mem *mem) { - pq->q = NULL; - pq->mem = mem; - pq->length = 0; - pq->capacity = 0; - pq->less = less; + *pq = (ngtcp2_pq){ + .mem = mem, + .less = less, + }; } void ngtcp2_pq_free(ngtcp2_pq *pq) { @@ -74,7 +73,7 @@ int ngtcp2_pq_push(ngtcp2_pq *pq, ngtcp2_pq_entry *item) { void *nq; size_t ncapacity; - ncapacity = ngtcp2_max_size(4, pq->capacity * 2); + ncapacity = ngtcp2_max(4, pq->capacity * 2); nq = ngtcp2_mem_realloc(pq->mem, pq->q, ncapacity * sizeof(ngtcp2_pq_entry *)); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pv.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pv.c index 471f84c76440fe..12b81eb80332e1 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pv.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pv.c @@ -31,12 +31,16 @@ #include "ngtcp2_log.h" #include "ngtcp2_macro.h" #include "ngtcp2_addr.h" +#include "ngtcp2_str.h" -void ngtcp2_pv_entry_init(ngtcp2_pv_entry *pvent, const uint8_t *data, +void ngtcp2_pv_entry_init(ngtcp2_pv_entry *pvent, + const ngtcp2_path_challenge_data *data, ngtcp2_tstamp expiry, uint8_t flags) { - memcpy(pvent->data, data, sizeof(pvent->data)); - pvent->expiry = expiry; - pvent->flags = flags; + *pvent = (ngtcp2_pv_entry){ + .expiry = expiry, + .flags = flags, + .data = *data, + }; } int ngtcp2_pv_new(ngtcp2_pv **ppv, const ngtcp2_dcid *dcid, @@ -70,7 +74,7 @@ void ngtcp2_pv_del(ngtcp2_pv *pv) { ngtcp2_mem_free(pv->mem, pv); } -void ngtcp2_pv_add_entry(ngtcp2_pv *pv, const uint8_t *data, +void ngtcp2_pv_add_entry(ngtcp2_pv *pv, const ngtcp2_path_challenge_data *data, ngtcp2_tstamp expiry, uint8_t flags, ngtcp2_tstamp ts) { ngtcp2_pv_entry *ent; @@ -88,7 +92,8 @@ void ngtcp2_pv_add_entry(ngtcp2_pv *pv, const uint8_t *data, --pv->probe_pkt_left; } -int ngtcp2_pv_validate(ngtcp2_pv *pv, uint8_t *pflags, const uint8_t *data) { +int ngtcp2_pv_validate(ngtcp2_pv *pv, uint8_t *pflags, + const ngtcp2_path_challenge_data *data) { size_t len = ngtcp2_ringbuf_len(&pv->ents.rb); size_t i; ngtcp2_pv_entry *ent; @@ -99,7 +104,7 @@ int ngtcp2_pv_validate(ngtcp2_pv *pv, uint8_t *pflags, const uint8_t *data) { for (i = 0; i < len; ++i) { ent = ngtcp2_ringbuf_get(&pv->ents.rb, i); - if (memcmp(ent->data, data, sizeof(ent->data)) == 0) { + if (ngtcp2_cmemeq(ent->data.data, data->data, sizeof(ent->data.data))) { *pflags = ent->flags; ngtcp2_log_info(pv->log, NGTCP2_LOG_EVENT_PTV, "path has been validated"); return 0; @@ -143,7 +148,7 @@ int ngtcp2_pv_validation_timed_out(ngtcp2_pv *pv, ngtcp2_tstamp ts) { ent = ngtcp2_ringbuf_get(&pv->ents.rb, ngtcp2_ringbuf_len(&pv->ents.rb) - 1); t = pv->started_ts + pv->timeout; - t = ngtcp2_max_uint64(t, ent->expiry); + t = ngtcp2_max(t, ent->expiry); return t <= ts; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pv.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pv.h index 2d07e41648db2e..28bdf722b2047f 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pv.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pv.h @@ -46,10 +46,10 @@ typedef struct ngtcp2_log ngtcp2_log; typedef struct ngtcp2_frame_chain ngtcp2_frame_chain; /* NGTCP2_PV_ENTRY_FLAG_NONE indicates that no flag is set. */ -#define NGTCP2_PV_ENTRY_FLAG_NONE 0x00u +#define NGTCP2_PV_ENTRY_FLAG_NONE 0x00U /* NGTCP2_PV_ENTRY_FLAG_UNDERSIZED indicates that UDP datagram which contains PATH_CHALLENGE is undersized (< 1200 bytes) */ -#define NGTCP2_PV_ENTRY_FLAG_UNDERSIZED 0x01u +#define NGTCP2_PV_ENTRY_FLAG_UNDERSIZED 0x01U typedef struct ngtcp2_pv_entry { /* expiry is the timestamp when this PATH_CHALLENGE expires. */ @@ -57,30 +57,31 @@ typedef struct ngtcp2_pv_entry { /* flags is zero or more of NGTCP2_PV_ENTRY_FLAG_*. */ uint8_t flags; /* data is a byte string included in PATH_CHALLENGE. */ - uint8_t data[8]; + ngtcp2_path_challenge_data data; } ngtcp2_pv_entry; -void ngtcp2_pv_entry_init(ngtcp2_pv_entry *pvent, const uint8_t *data, +void ngtcp2_pv_entry_init(ngtcp2_pv_entry *pvent, + const ngtcp2_path_challenge_data *data, ngtcp2_tstamp expiry, uint8_t flags); /* NGTCP2_PV_FLAG_NONE indicates no flag is set. */ -#define NGTCP2_PV_FLAG_NONE 0x00u +#define NGTCP2_PV_FLAG_NONE 0x00U /* NGTCP2_PV_FLAG_DONT_CARE indicates that the outcome of path validation should be ignored entirely. */ -#define NGTCP2_PV_FLAG_DONT_CARE 0x01u +#define NGTCP2_PV_FLAG_DONT_CARE 0x01U /* NGTCP2_PV_FLAG_CANCEL_TIMER indicates that the expiry timer is cancelled. */ -#define NGTCP2_PV_FLAG_CANCEL_TIMER 0x02u +#define NGTCP2_PV_FLAG_CANCEL_TIMER 0x02U /* NGTCP2_PV_FLAG_FALLBACK_PRESENT indicates that a fallback Destination Connection ID and PTO are available in ngtcp2_pv. If path validation fails, then fallback to them. If path validation succeeds, the fallback Destination Connection ID is retired if it is not zero length, and does not equal to the current Destination Connection ID. */ -#define NGTCP2_PV_FLAG_FALLBACK_PRESENT 0x04u +#define NGTCP2_PV_FLAG_FALLBACK_PRESENT 0x04U /* NGTCP2_PV_FLAG_PREFERRED_ADDR indicates that client is migrating to server's preferred address. This flag is only used by client. */ -#define NGTCP2_PV_FLAG_PREFERRED_ADDR 0x10u +#define NGTCP2_PV_FLAG_PREFERRED_ADDR 0x10U typedef struct ngtcp2_pv ngtcp2_pv; @@ -141,7 +142,7 @@ void ngtcp2_pv_del(ngtcp2_pv *pv); * ngtcp2_pv_add_entry adds new entry with |data|. |expiry| is the * expiry time of the entry. */ -void ngtcp2_pv_add_entry(ngtcp2_pv *pv, const uint8_t *data, +void ngtcp2_pv_add_entry(ngtcp2_pv *pv, const ngtcp2_path_challenge_data *data, ngtcp2_tstamp expiry, uint8_t flags, ngtcp2_tstamp ts); /* @@ -164,7 +165,8 @@ int ngtcp2_pv_full(ngtcp2_pv *pv); * NGTCP2_ERR_INVALID_ARGUMENT * |pv| does not have an entry which has |data| and |path| */ -int ngtcp2_pv_validate(ngtcp2_pv *pv, uint8_t *pflags, const uint8_t *data); +int ngtcp2_pv_validate(ngtcp2_pv *pv, uint8_t *pflags, + const ngtcp2_path_challenge_data *data); /* * ngtcp2_pv_handle_entry_expiry checks expiry of existing entries. diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_qlog.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_qlog.c index c0f920746a4dff..9f233b6cd83819 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_qlog.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_qlog.c @@ -40,7 +40,7 @@ void ngtcp2_qlog_init(ngtcp2_qlog *qlog, ngtcp2_qlog_write write, qlog->user_data = user_data; } -#define write_verbatim(DEST, S) ngtcp2_cpymem((DEST), (S), sizeof(S) - 1) +#define write_verbatim(DEST, S) ngtcp2_cpymem((DEST), (S), ngtcp2_strlen_lit(S)) static uint8_t *write_string_impl(uint8_t *p, const uint8_t *data, size_t datalen) { @@ -53,17 +53,11 @@ static uint8_t *write_string_impl(uint8_t *p, const uint8_t *data, } #define write_string(DEST, S) \ - write_string_impl((DEST), (const uint8_t *)(S), sizeof(S) - 1) - -#define NGTCP2_LOWER_XDIGITS "0123456789abcdef" + write_string_impl((DEST), (const uint8_t *)(S), ngtcp2_strlen_lit(S)) static uint8_t *write_hex(uint8_t *p, const uint8_t *data, size_t datalen) { - const uint8_t *b = data, *end = data + datalen; *p++ = '"'; - for (; b != end; ++b) { - *p++ = (uint8_t)NGTCP2_LOWER_XDIGITS[*b >> 4]; - *p++ = (uint8_t)NGTCP2_LOWER_XDIGITS[*b & 0xf]; - } + p = ngtcp2_encode_hex(p, data, datalen); *p++ = '"'; return p; } @@ -72,38 +66,19 @@ static uint8_t *write_cid(uint8_t *p, const ngtcp2_cid *cid) { return write_hex(p, cid->data, cid->datalen); } -static uint8_t *write_number(uint8_t *p, uint64_t n) { - size_t nlen = 0; - uint64_t t; - uint8_t *res; - - if (n == 0) { - *p++ = '0'; - return p; - } - for (t = n; t; t /= 10, ++nlen) - ; - p += nlen; - res = p; - for (; n; n /= 10) { - *--p = (uint8_t)((n % 10) + '0'); - } - return res; -} - static uint8_t *write_tstamp(uint8_t *p, ngtcp2_tstamp ts) { - return write_number(p, ts / NGTCP2_MILLISECONDS); + return ngtcp2_encode_uint(p, ts / NGTCP2_MILLISECONDS); } static uint8_t *write_duration(uint8_t *p, ngtcp2_duration duration) { - return write_number(p, duration / NGTCP2_MILLISECONDS); + return ngtcp2_encode_uint(p, duration / NGTCP2_MILLISECONDS); } static uint8_t *write_bool(uint8_t *p, int b) { if (b) { - return ngtcp2_cpymem(p, "true", sizeof("true") - 1); + return ngtcp2_cpymem(p, "true", ngtcp2_strlen_lit("true")); } - return ngtcp2_cpymem(p, "false", sizeof("false") - 1); + return ngtcp2_cpymem(p, "false", ngtcp2_strlen_lit("false")); } static uint8_t *write_pair_impl(uint8_t *p, const uint8_t *name, size_t namelen, @@ -114,7 +89,8 @@ static uint8_t *write_pair_impl(uint8_t *p, const uint8_t *name, size_t namelen, } #define write_pair(DEST, NAME, VALUE) \ - write_pair_impl((DEST), (const uint8_t *)(NAME), sizeof(NAME) - 1, (VALUE)) + write_pair_impl((DEST), (const uint8_t *)(NAME), ngtcp2_strlen_lit(NAME), \ + (VALUE)) static uint8_t *write_pair_hex_impl(uint8_t *p, const uint8_t *name, size_t namelen, const uint8_t *value, @@ -125,19 +101,19 @@ static uint8_t *write_pair_hex_impl(uint8_t *p, const uint8_t *name, } #define write_pair_hex(DEST, NAME, VALUE, VALUELEN) \ - write_pair_hex_impl((DEST), (const uint8_t *)(NAME), sizeof(NAME) - 1, \ - (VALUE), (VALUELEN)) + write_pair_hex_impl((DEST), (const uint8_t *)(NAME), \ + ngtcp2_strlen_lit(NAME), (VALUE), (VALUELEN)) static uint8_t *write_pair_number_impl(uint8_t *p, const uint8_t *name, size_t namelen, uint64_t value) { p = write_string_impl(p, name, namelen); *p++ = ':'; - return write_number(p, value); + return ngtcp2_encode_uint(p, value); } #define write_pair_number(DEST, NAME, VALUE) \ - write_pair_number_impl((DEST), (const uint8_t *)(NAME), sizeof(NAME) - 1, \ - (VALUE)) + write_pair_number_impl((DEST), (const uint8_t *)(NAME), \ + ngtcp2_strlen_lit(NAME), (VALUE)) static uint8_t *write_pair_duration_impl(uint8_t *p, const uint8_t *name, size_t namelen, @@ -148,8 +124,8 @@ static uint8_t *write_pair_duration_impl(uint8_t *p, const uint8_t *name, } #define write_pair_duration(DEST, NAME, VALUE) \ - write_pair_duration_impl((DEST), (const uint8_t *)(NAME), sizeof(NAME) - 1, \ - (VALUE)) + write_pair_duration_impl((DEST), (const uint8_t *)(NAME), \ + ngtcp2_strlen_lit(NAME), (VALUE)) static uint8_t *write_pair_tstamp_impl(uint8_t *p, const uint8_t *name, size_t namelen, ngtcp2_tstamp ts) { @@ -159,8 +135,8 @@ static uint8_t *write_pair_tstamp_impl(uint8_t *p, const uint8_t *name, } #define write_pair_tstamp(DEST, NAME, VALUE) \ - write_pair_tstamp_impl((DEST), (const uint8_t *)(NAME), sizeof(NAME) - 1, \ - (VALUE)) + write_pair_tstamp_impl((DEST), (const uint8_t *)(NAME), \ + ngtcp2_strlen_lit(NAME), (VALUE)) static uint8_t *write_pair_bool_impl(uint8_t *p, const uint8_t *name, size_t namelen, int b) { @@ -170,8 +146,8 @@ static uint8_t *write_pair_bool_impl(uint8_t *p, const uint8_t *name, } #define write_pair_bool(DEST, NAME, VALUE) \ - write_pair_bool_impl((DEST), (const uint8_t *)(NAME), sizeof(NAME) - 1, \ - (VALUE)) + write_pair_bool_impl((DEST), (const uint8_t *)(NAME), \ + ngtcp2_strlen_lit(NAME), (VALUE)) static uint8_t *write_pair_cid_impl(uint8_t *p, const uint8_t *name, size_t namelen, const ngtcp2_cid *cid) { @@ -181,10 +157,10 @@ static uint8_t *write_pair_cid_impl(uint8_t *p, const uint8_t *name, } #define write_pair_cid(DEST, NAME, VALUE) \ - write_pair_cid_impl((DEST), (const uint8_t *)(NAME), sizeof(NAME) - 1, \ - (VALUE)) + write_pair_cid_impl((DEST), (const uint8_t *)(NAME), \ + ngtcp2_strlen_lit(NAME), (VALUE)) -#define ngtcp2_make_vec_lit(S) {(uint8_t *)(S), sizeof((S)) - 1} +#define ngtcp2_make_vec_lit(S) {(uint8_t *)(S), ngtcp2_strlen_lit((S))} static uint8_t *write_common_fields(uint8_t *p, const ngtcp2_cid *odcid) { p = write_verbatim( @@ -218,7 +194,7 @@ void ngtcp2_qlog_start(ngtcp2_qlog *qlog, const ngtcp2_cid *odcid, int server) { } p = write_verbatim( - p, "\x1e{\"qlog_format\":\"JSON-SEQ\",\"qlog_version\":\"0.3\","); + p, "\x1E{\"qlog_format\":\"JSON-SEQ\",\"qlog_version\":\"0.3\","); p = write_trace(p, server, odcid); p = write_verbatim(p, "}\n"); @@ -227,49 +203,41 @@ void ngtcp2_qlog_start(ngtcp2_qlog *qlog, const ngtcp2_cid *odcid, int server) { } void ngtcp2_qlog_end(ngtcp2_qlog *qlog) { - uint8_t buf[1] = {0}; - if (!qlog->write) { return; } - qlog->write(qlog->user_data, NGTCP2_QLOG_WRITE_FLAG_FIN, &buf, 0); + qlog->write(qlog->user_data, NGTCP2_QLOG_WRITE_FLAG_FIN, "", 0); } -static ngtcp2_vec vec_pkt_type_initial = ngtcp2_make_vec_lit("initial"); -static ngtcp2_vec vec_pkt_type_handshake = ngtcp2_make_vec_lit("handshake"); -static ngtcp2_vec vec_pkt_type_0rtt = ngtcp2_make_vec_lit("0RTT"); -static ngtcp2_vec vec_pkt_type_1rtt = ngtcp2_make_vec_lit("1RTT"); -static ngtcp2_vec vec_pkt_type_retry = ngtcp2_make_vec_lit("retry"); -static ngtcp2_vec vec_pkt_type_version_negotiation = +static const ngtcp2_vec vec_pkt_type_initial = ngtcp2_make_vec_lit("initial"); +static const ngtcp2_vec vec_pkt_type_handshake = + ngtcp2_make_vec_lit("handshake"); +static const ngtcp2_vec vec_pkt_type_0rtt = ngtcp2_make_vec_lit("0RTT"); +static const ngtcp2_vec vec_pkt_type_1rtt = ngtcp2_make_vec_lit("1RTT"); +static const ngtcp2_vec vec_pkt_type_retry = ngtcp2_make_vec_lit("retry"); +static const ngtcp2_vec vec_pkt_type_version_negotiation = ngtcp2_make_vec_lit("version_negotiation"); -static ngtcp2_vec vec_pkt_type_stateless_reset = +static const ngtcp2_vec vec_pkt_type_stateless_reset = ngtcp2_make_vec_lit("stateless_reset"); -static ngtcp2_vec vec_pkt_type_unknown = ngtcp2_make_vec_lit("unknown"); +static const ngtcp2_vec vec_pkt_type_unknown = ngtcp2_make_vec_lit("unknown"); static const ngtcp2_vec *qlog_pkt_type(const ngtcp2_pkt_hd *hd) { - if (hd->flags & NGTCP2_PKT_FLAG_LONG_FORM) { - switch (hd->type) { - case NGTCP2_PKT_INITIAL: - return &vec_pkt_type_initial; - case NGTCP2_PKT_HANDSHAKE: - return &vec_pkt_type_handshake; - case NGTCP2_PKT_0RTT: - return &vec_pkt_type_0rtt; - case NGTCP2_PKT_RETRY: - return &vec_pkt_type_retry; - default: - return &vec_pkt_type_unknown; - } - } - switch (hd->type) { + case NGTCP2_PKT_INITIAL: + return &vec_pkt_type_initial; + case NGTCP2_PKT_0RTT: + return &vec_pkt_type_0rtt; + case NGTCP2_PKT_HANDSHAKE: + return &vec_pkt_type_handshake; + case NGTCP2_PKT_RETRY: + return &vec_pkt_type_retry; + case NGTCP2_PKT_1RTT: + return &vec_pkt_type_1rtt; case NGTCP2_PKT_VERSION_NEGOTIATION: return &vec_pkt_type_version_negotiation; case NGTCP2_PKT_STATELESS_RESET: return &vec_pkt_type_stateless_reset; - case NGTCP2_PKT_1RTT: - return &vec_pkt_type_1rtt; default: return &vec_pkt_type_unknown; } @@ -340,10 +308,10 @@ static uint8_t *write_ack_frame(uint8_t *p, const ngtcp2_ack *fr) { min_ack = fr->largest_ack - (int64_t)fr->first_ack_range; *p++ = '['; - p = write_number(p, (uint64_t)min_ack); + p = ngtcp2_encode_uint(p, (uint64_t)min_ack); if (largest_ack != min_ack) { *p++ = ','; - p = write_number(p, (uint64_t)largest_ack); + p = ngtcp2_encode_uint(p, (uint64_t)largest_ack); } *p++ = ']'; @@ -353,10 +321,10 @@ static uint8_t *write_ack_frame(uint8_t *p, const ngtcp2_ack *fr) { min_ack = largest_ack - (int64_t)range->len; *p++ = ','; *p++ = '['; - p = write_number(p, (uint64_t)min_ack); + p = ngtcp2_encode_uint(p, (uint64_t)min_ack); if (largest_ack != min_ack) { *p++ = ','; - p = write_number(p, (uint64_t)largest_ack); + p = ngtcp2_encode_uint(p, (uint64_t)largest_ack); } *p++ = ']'; } @@ -579,8 +547,7 @@ write_new_connection_id_frame(uint8_t *p, const ngtcp2_new_connection_id *fr) { *p++ = ','; p = write_pair_cid(p, "connection_id", &fr->cid); p = write_verbatim(p, ",\"stateless_reset_token\":{"); - p = write_pair_hex(p, "data", fr->stateless_reset_token, - sizeof(fr->stateless_reset_token)); + p = write_pair_hex(p, "data", fr->token.data, sizeof(fr->token.data)); *p++ = '}'; *p++ = '}'; @@ -610,7 +577,7 @@ static uint8_t *write_path_challenge_frame(uint8_t *p, #define NGTCP2_QLOG_PATH_CHALLENGE_FRAME_OVERHEAD 57 p = write_verbatim(p, "{\"frame_type\":\"path_challenge\","); - p = write_pair_hex(p, "data", fr->data, sizeof(fr->data)); + p = write_pair_hex(p, "data", fr->data.data, sizeof(fr->data.data)); *p++ = '}'; return p; @@ -624,7 +591,7 @@ static uint8_t *write_path_response_frame(uint8_t *p, #define NGTCP2_QLOG_PATH_RESPONSE_FRAME_OVERHEAD 56 p = write_verbatim(p, "{\"frame_type\":\"path_response\","); - p = write_pair_hex(p, "data", fr->data, sizeof(fr->data)); + p = write_pair_hex(p, "data", fr->data.data, sizeof(fr->data.data)); *p++ = '}'; return p; @@ -694,7 +661,7 @@ static void qlog_pkt_write_start(ngtcp2_qlog *qlog, int sent) { ngtcp2_buf_reset(&qlog->buf); p = qlog->buf.last; - *p++ = '\x1e'; + *p++ = '\x1E'; *p++ = '{'; p = qlog_write_time(qlog, p); p = write_verbatim(p, ",\"name\":"); @@ -738,7 +705,7 @@ static void qlog_pkt_write_end(ngtcp2_qlog *qlog, const ngtcp2_pkt_hd *hd, p = write_verbatim(p, "],\"header\":"); p = write_pkt_hd(p, hd); p = write_verbatim(p, ",\"raw\":{\"length\":"); - p = write_number(p, pktlen); + p = ngtcp2_encode_uint(p, pktlen); p = write_verbatim(p, "}}}\n"); qlog->buf.last = p; @@ -754,7 +721,7 @@ void ngtcp2_qlog_write_frame(ngtcp2_qlog *qlog, const ngtcp2_frame *fr) { return; } - switch (fr->type) { + switch (fr->hd.type) { case NGTCP2_FRAME_PADDING: if (ngtcp2_buf_left(&qlog->buf) < NGTCP2_QLOG_PADDING_FRAME_OVERHEAD + 1) { return; @@ -771,7 +738,7 @@ void ngtcp2_qlog_write_frame(ngtcp2_qlog *qlog, const ngtcp2_frame *fr) { case NGTCP2_FRAME_ACK_ECN: if (ngtcp2_buf_left(&qlog->buf) < NGTCP2_QLOG_ACK_FRAME_BASE_OVERHEAD + - (size_t)(fr->type == NGTCP2_FRAME_ACK_ECN + (size_t)(fr->ack.type == NGTCP2_FRAME_ACK_ECN ? NGTCP2_QLOG_ACK_FRAME_ECN_OVERHEAD : 0) + NGTCP2_QLOG_ACK_FRAME_RANGE_OVERHEAD * (1 + fr->ack.rangecnt) + 1) { @@ -935,7 +902,7 @@ void ngtcp2_qlog_pkt_sent_end(ngtcp2_qlog *qlog, const ngtcp2_pkt_hd *hd, void ngtcp2_qlog_parameters_set_transport_params( ngtcp2_qlog *qlog, const ngtcp2_transport_params *params, int server, ngtcp2_qlog_side side) { - uint8_t buf[1024]; + uint8_t buf[2048]; uint8_t *p = buf; const ngtcp2_preferred_addr *paddr; const ngtcp2_sockaddr_in *sa_in; @@ -945,7 +912,7 @@ void ngtcp2_qlog_parameters_set_transport_params( return; } - *p++ = '\x1e'; + *p++ = '\x1E'; *p++ = '{'; p = qlog_write_time(qlog, p); p = write_verbatim( @@ -1061,7 +1028,7 @@ void ngtcp2_qlog_metrics_updated(ngtcp2_qlog *qlog, return; } - *p++ = '\x1e'; + *p++ = '\x1E'; *p++ = '{'; p = qlog_write_time(qlog, p); p = write_verbatim(p, ",\"name\":\"recovery:metrics_updated\",\"data\":{"); @@ -1095,23 +1062,22 @@ void ngtcp2_qlog_metrics_updated(ngtcp2_qlog *qlog, void ngtcp2_qlog_pkt_lost(ngtcp2_qlog *qlog, ngtcp2_rtb_entry *ent) { uint8_t buf[256]; uint8_t *p = buf; - ngtcp2_pkt_hd hd = {0}; if (!qlog->write) { return; } - *p++ = '\x1e'; + *p++ = '\x1E'; *p++ = '{'; p = qlog_write_time(qlog, p); p = write_verbatim( p, ",\"name\":\"recovery:packet_lost\",\"data\":{\"header\":"); - hd.type = ent->hd.type; - hd.flags = ent->hd.flags; - hd.pkt_num = ent->hd.pkt_num; - - p = write_pkt_hd(p, &hd); + p = write_pkt_hd(p, &(ngtcp2_pkt_hd){ + .pkt_num = ent->hd.pkt_num, + .type = ent->hd.type, + .flags = ent->hd.flags, + }); p = write_verbatim(p, "}}\n"); qlog->write(qlog->user_data, NGTCP2_QLOG_WRITE_FLAG_NONE, buf, @@ -1129,7 +1095,7 @@ void ngtcp2_qlog_retry_pkt_received(ngtcp2_qlog *qlog, const ngtcp2_pkt_hd *hd, ngtcp2_buf_init(&buf, rawbuf, sizeof(rawbuf)); - *buf.last++ = '\x1e'; + *buf.last++ = '\x1E'; *buf.last++ = '{'; buf.last = qlog_write_time(qlog, buf.last); buf.last = write_verbatim( @@ -1151,26 +1117,26 @@ void ngtcp2_qlog_retry_pkt_received(ngtcp2_qlog *qlog, const ngtcp2_pkt_hd *hd, } void ngtcp2_qlog_stateless_reset_pkt_received( - ngtcp2_qlog *qlog, const ngtcp2_pkt_stateless_reset *sr) { + ngtcp2_qlog *qlog, const ngtcp2_pkt_stateless_reset2 *sr) { uint8_t buf[256]; uint8_t *p = buf; - ngtcp2_pkt_hd hd = {0}; + static const ngtcp2_pkt_hd hd = { + .type = NGTCP2_PKT_STATELESS_RESET, + }; if (!qlog->write) { return; } - hd.type = NGTCP2_PKT_STATELESS_RESET; - - *p++ = '\x1e'; + *p++ = '\x1E'; *p++ = '{'; p = qlog_write_time(qlog, p); p = write_verbatim( p, ",\"name\":\"transport:packet_received\",\"data\":{\"header\":"); p = write_pkt_hd(p, &hd); *p++ = ','; - p = write_pair_hex(p, "stateless_reset_token", sr->stateless_reset_token, - NGTCP2_STATELESS_RESET_TOKENLEN); + p = write_pair_hex(p, "stateless_reset_token", sr->token.data, + sizeof(sr->token.data)); p = write_verbatim(p, "}}\n"); qlog->write(qlog->user_data, NGTCP2_QLOG_WRITE_FLAG_NONE, buf, @@ -1192,7 +1158,7 @@ void ngtcp2_qlog_version_negotiation_pkt_received(ngtcp2_qlog *qlog, ngtcp2_buf_init(&buf, rawbuf, sizeof(rawbuf)); - *buf.last++ = '\x1e'; + *buf.last++ = '\x1E'; *buf.last++ = '{'; buf.last = qlog_write_time(qlog, buf.last); buf.last = write_verbatim( @@ -1201,8 +1167,8 @@ void ngtcp2_qlog_version_negotiation_pkt_received(ngtcp2_qlog *qlog, buf.last = write_verbatim(buf.last, ",\"supported_versions\":["); if (nsv) { - if (ngtcp2_buf_left(&buf) < - (sizeof("\"xxxxxxxx\",") - 1) * nsv - 1 + sizeof("]}}\n") - 1) { + if (ngtcp2_buf_left(&buf) < ngtcp2_strlen_lit("\"xxxxxxxx\",") * nsv - 1 + + ngtcp2_strlen_lit("]}}\n")) { return; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_qlog.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_qlog.h index d2a5f1038c0f42..17668a50233f4d 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_qlog.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_qlog.h @@ -147,7 +147,7 @@ void ngtcp2_qlog_retry_pkt_received(ngtcp2_qlog *qlog, const ngtcp2_pkt_hd *hd, * event for a received Stateless Reset packet. */ void ngtcp2_qlog_stateless_reset_pkt_received( - ngtcp2_qlog *qlog, const ngtcp2_pkt_stateless_reset *sr); + ngtcp2_qlog *qlog, const ngtcp2_pkt_stateless_reset2 *sr); /* * ngtcp2_qlog_version_negotiation_pkt_received writes packet_received diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_range.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_range.c index e89891532936e8..8cb43830fb3d8b 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_range.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_range.c @@ -26,18 +26,22 @@ #include "ngtcp2_macro.h" void ngtcp2_range_init(ngtcp2_range *r, uint64_t begin, uint64_t end) { - r->begin = begin; - r->end = end; + *r = (ngtcp2_range){ + .begin = begin, + .end = end, + }; } ngtcp2_range ngtcp2_range_intersect(const ngtcp2_range *a, const ngtcp2_range *b) { - ngtcp2_range r = {0}; - uint64_t begin = ngtcp2_max_uint64(a->begin, b->begin); - uint64_t end = ngtcp2_min_uint64(a->end, b->end); + ngtcp2_range r; + uint64_t begin = ngtcp2_max(a->begin, b->begin); + uint64_t end = ngtcp2_min(a->end, b->end); if (begin < end) { ngtcp2_range_init(&r, begin, end); + } else { + r = (ngtcp2_range){0}; } return r; @@ -52,10 +56,14 @@ int ngtcp2_range_eq(const ngtcp2_range *a, const ngtcp2_range *b) { void ngtcp2_range_cut(ngtcp2_range *left, ngtcp2_range *right, const ngtcp2_range *a, const ngtcp2_range *b) { /* Assume that b is included in a */ - left->begin = a->begin; - left->end = b->begin; - right->begin = b->end; - right->end = a->end; + *left = (ngtcp2_range){ + .begin = a->begin, + .end = b->begin, + }; + *right = (ngtcp2_range){ + .begin = b->end, + .end = a->end, + }; } int ngtcp2_range_not_after(const ngtcp2_range *a, const ngtcp2_range *b) { diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.c index efedc3daa7600f..d13ed9a14aa4bd 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.c @@ -41,7 +41,7 @@ void ngtcp2_ratelim_init(ngtcp2_ratelim *rlim, uint64_t burst, uint64_t rate, /* ratelim_update updates rlim->tokens with the current |ts|. */ static void ratelim_update(ngtcp2_ratelim *rlim, ngtcp2_tstamp ts) { - uint64_t d, gain; + uint64_t d, gain, gps; assert(ts >= rlim->ts); @@ -52,16 +52,23 @@ static void ratelim_update(ngtcp2_ratelim *rlim, ngtcp2_tstamp ts) { d = ts - rlim->ts; rlim->ts = ts; - gain = rlim->rate * d + rlim->carry; + if (rlim->rate > (UINT64_MAX - rlim->carry) / d) { + gain = UINT64_MAX; + } else { + gain = rlim->rate * d + rlim->carry; + } - rlim->tokens += gain / NGTCP2_SECONDS; + gps = gain / NGTCP2_SECONDS; - if (rlim->tokens < rlim->burst) { + if (gps < rlim->burst && rlim->tokens < rlim->burst - gps) { + rlim->tokens += gps; rlim->carry = gain % NGTCP2_SECONDS; - } else { - rlim->tokens = rlim->burst; - rlim->carry = 0; + + return; } + + rlim->tokens = rlim->burst; + rlim->carry = 0; } int ngtcp2_ratelim_drain(ngtcp2_ratelim *rlim, uint64_t n, ngtcp2_tstamp ts) { diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.h index 14485c562d4e32..231f5d1b0182e9 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.h @@ -28,7 +28,7 @@ #ifdef HAVE_CONFIG_H # include -#endif /* HAVE_CONFIG_H */ +#endif /* defined(HAVE_CONFIG_H) */ #include diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ringbuf.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ringbuf.c index 353afca4d48fb5..40c25f2a3d1cbc 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ringbuf.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ringbuf.c @@ -32,18 +32,8 @@ #include "ngtcp2_macro.h" #ifndef NDEBUG -static int ispow2(size_t n) { -# if defined(_MSC_VER) && !defined(__clang__) && \ - (defined(_M_ARM) || (defined(_M_ARM64) && _MSC_VER < 1941)) - return n && !(n & (n - 1)); -# elif defined(WIN32) - return 1 == __popcnt((unsigned int)n); -# else /* !((defined(_MSC_VER) && !defined(__clang__) && (defined(_M_ARM) || \ - (defined(_M_ARM64) && _MSC_VER < 1941))) || defined(WIN32)) */ - return 1 == __builtin_popcount((unsigned int)n); -# endif /* !((defined(_MSC_VER) && !defined(__clang__) && (defined(_M_ARM) || \ - (defined(_M_ARM64) && _MSC_VER < 1941))) || defined(WIN32)) */ -} +/* Power-of-two test; simple portable bit trick. */ +static int ispow2(size_t n) { return n && !(n & (n - 1)); } #endif /* !defined(NDEBUG) */ int ngtcp2_ringbuf_init(ngtcp2_ringbuf *rb, size_t nmemb, size_t size, diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.c index ef1938ea6326ca..e45351be2b55aa 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.c @@ -36,8 +36,13 @@ int ngtcp2_rob_gap_new(ngtcp2_rob_gap **pg, uint64_t begin, uint64_t end, return NGTCP2_ERR_NOMEM; } - (*pg)->range.begin = begin; - (*pg)->range.end = end; + **pg = (ngtcp2_rob_gap){ + .range = + { + .begin = begin, + .end = end, + }, + }; return 0; } @@ -53,9 +58,14 @@ int ngtcp2_rob_data_new(ngtcp2_rob_data **pd, uint64_t offset, size_t chunk, return NGTCP2_ERR_NOMEM; } - (*pd)->range.begin = offset; - (*pd)->range.end = offset + chunk; - (*pd)->begin = (uint8_t *)(*pd) + sizeof(ngtcp2_rob_data); + **pd = (ngtcp2_rob_data){ + .range = + { + .begin = offset, + .end = offset + chunk, + }, + .begin = (uint8_t *)(*pd) + sizeof(ngtcp2_rob_data), + }; return 0; } @@ -151,8 +161,7 @@ static int rob_write_data(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, } } - n = (size_t)ngtcp2_min_uint64((uint64_t)len, - d->range.begin + rob->chunk - offset); + n = (size_t)ngtcp2_min((uint64_t)len, d->range.begin + rob->chunk - offset); memcpy(d->begin + (offset - d->range.begin), data, n); offset += n; data += n; @@ -254,9 +263,11 @@ void ngtcp2_rob_remove_prefix(ngtcp2_rob *rob, uint64_t offset) { } if (offset < g->range.end) { - ngtcp2_range r = {offset, g->range.end}; - - ngtcp2_ksl_update_key(&rob->gapksl, &g->range, &r); + ngtcp2_ksl_update_key(&rob->gapksl, &g->range, + &(ngtcp2_range){ + .begin = offset, + .end = g->range.end, + }); g->range.begin = offset; break; @@ -305,9 +316,8 @@ size_t ngtcp2_rob_data_at(const ngtcp2_rob *rob, const uint8_t **pdest, *pdest = d->begin + (offset - d->range.begin); - return ( - size_t)(ngtcp2_min_uint64(g->range.begin, d->range.begin + rob->chunk) - - offset); + return (size_t)(ngtcp2_min(g->range.begin, d->range.begin + rob->chunk) - + offset); } void ngtcp2_rob_pop(ngtcp2_rob *rob, uint64_t offset, size_t len) { diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rst.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rst.c index 6f44c8ac8de673..7792ebe41ba67f 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rst.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rst.c @@ -32,20 +32,15 @@ #include "ngtcp2_conn_stat.h" void ngtcp2_rs_init(ngtcp2_rs *rs) { - rs->interval = UINT64_MAX; - rs->delivered = 0; - rs->prior_delivered = 0; - rs->prior_ts = UINT64_MAX; - rs->tx_in_flight = 0; - rs->lost = 0; - rs->send_elapsed = 0; - rs->ack_elapsed = 0; - rs->last_end_seq = -1; - rs->is_app_limited = 0; + *rs = (ngtcp2_rs){ + .interval = UINT64_MAX, + .prior_ts = UINT64_MAX, + .last_acked_pkt_id = -1, + }; } void ngtcp2_rst_init(ngtcp2_rst *rst) { - rst->last_seq = -1; + rst->pkt_id = -1; ngtcp2_rst_reset(rst); } @@ -59,18 +54,29 @@ void ngtcp2_rst_reset(ngtcp2_rst *rst) { rst->lost = 0; } +void ngtcp2_rst_reset_rate_sample(ngtcp2_rst *rst, ngtcp2_conn_stat *cstat) { + ngtcp2_rs *rs = &rst->rs; + + rs->interval = UINT64_MAX; + rs->prior_ts = UINT64_MAX; + + cstat->delivery_rate_sec = 0; +} + void ngtcp2_rst_on_pkt_sent(ngtcp2_rst *rst, ngtcp2_rtb_entry *ent, const ngtcp2_conn_stat *cstat) { - if (cstat->bytes_in_flight == 0) { + /* cstat->bytes_in_flight includes ent->pktlen. If they are the + same, there is no in-flight packets. */ + if (cstat->bytes_in_flight == ent->pktlen) { rst->first_sent_ts = rst->delivered_ts = ent->ts; } ent->rst.first_sent_ts = rst->first_sent_ts; ent->rst.delivered_ts = rst->delivered_ts; ent->rst.delivered = rst->delivered; ent->rst.is_app_limited = rst->app_limited != 0; - ent->rst.tx_in_flight = cstat->bytes_in_flight + ent->pktlen; + ent->rst.tx_in_flight = cstat->bytes_in_flight; ent->rst.lost = rst->lost; - ent->rst.end_seq = ++rst->last_seq; + ent->rst.pkt_id = ++rst->pkt_id; } void ngtcp2_rst_on_ack_recv(ngtcp2_rst *rst, ngtcp2_conn_stat *cstat) { @@ -84,12 +90,11 @@ void ngtcp2_rst_on_ack_recv(ngtcp2_rst *rst, ngtcp2_conn_stat *cstat) { return; } - rs->interval = ngtcp2_max_uint64(rs->send_elapsed, rs->ack_elapsed); + rs->interval = ngtcp2_max(rs->send_elapsed, rs->ack_elapsed); rs->delivered = rst->delivered - rs->prior_delivered; if (rs->interval < cstat->min_rtt) { - rs->interval = UINT64_MAX; return; } @@ -101,7 +106,7 @@ void ngtcp2_rst_on_ack_recv(ngtcp2_rst *rst, ngtcp2_conn_stat *cstat) { } static int is_newest_pkt(const ngtcp2_rtb_entry *ent, const ngtcp2_rs *rs) { - return ent->rst.end_seq > rs->last_end_seq; + return ent->rst.pkt_id > rs->last_acked_pkt_id; } void ngtcp2_rst_update_rate_sample(ngtcp2_rst *rst, const ngtcp2_rtb_entry *ent, @@ -117,7 +122,7 @@ void ngtcp2_rst_update_rate_sample(ngtcp2_rst *rst, const ngtcp2_rtb_entry *ent, rs->is_app_limited = ent->rst.is_app_limited; rs->send_elapsed = ent->ts - ent->rst.first_sent_ts; rs->ack_elapsed = rst->delivered_ts - ent->rst.delivered_ts; - rs->last_end_seq = ent->rst.end_seq; + rs->last_acked_pkt_id = ent->rst.pkt_id; rst->first_sent_ts = ent->ts; } } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rst.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rst.h index 1bb624de79b53f..86346f49295add 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rst.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rst.h @@ -50,7 +50,7 @@ typedef struct ngtcp2_rs { uint64_t lost; ngtcp2_duration send_elapsed; ngtcp2_duration ack_elapsed; - int64_t last_end_seq; + int64_t last_acked_pkt_id; int is_app_limited; } ngtcp2_rs; @@ -67,11 +67,11 @@ typedef struct ngtcp2_rst { ngtcp2_tstamp first_sent_ts; uint64_t app_limited; uint64_t lost; - /* last_seq is the sequence number of packets across all packet - number spaces. If we would adopt single packet number sequence - across all packet number spaces, we can replace this with a - packet number. */ - int64_t last_seq; + /* pkt_id is the identifier of packets across all packet number + spaces. If we would adopt single packet number sequence across + all packet number spaces, we can replace this with a packet + number. */ + int64_t pkt_id; int is_cwnd_limited; } ngtcp2_rst; @@ -79,6 +79,8 @@ void ngtcp2_rst_init(ngtcp2_rst *rst); void ngtcp2_rst_reset(ngtcp2_rst *rst); +void ngtcp2_rst_reset_rate_sample(ngtcp2_rst *rst, ngtcp2_conn_stat *cstat); + void ngtcp2_rst_on_pkt_sent(ngtcp2_rst *rst, ngtcp2_rtb_entry *ent, const ngtcp2_conn_stat *cstat); void ngtcp2_rst_on_ack_recv(ngtcp2_rst *rst, ngtcp2_conn_stat *cstat); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.c index 7df1c197db79d7..e8d05a3feadce9 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.c @@ -128,13 +128,12 @@ void ngtcp2_rtb_free(ngtcp2_rtb *rtb) { static void rtb_on_add(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent, ngtcp2_conn_stat *cstat) { - ngtcp2_rst_on_pkt_sent(rtb->rst, ent, cstat); - assert(rtb->cc_pkt_num <= ent->hd.pkt_num); cstat->bytes_in_flight += ent->pktlen; rtb->cc_bytes_in_flight += ent->pktlen; + ngtcp2_rst_on_pkt_sent(rtb->rst, ent, cstat); ngtcp2_rst_update_app_limited(rtb->rst, cstat); if (ent->flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING) { @@ -201,10 +200,10 @@ static size_t rtb_on_remove(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent, } /* NGTCP2_RECLAIM_FLAG_NONE indicates that no flag is set. */ -#define NGTCP2_RECLAIM_FLAG_NONE 0x00u +#define NGTCP2_RECLAIM_FLAG_NONE 0x00U /* NGTCP2_RECLAIM_FLAG_ON_LOSS indicates that frames are reclaimed because of the packet loss.*/ -#define NGTCP2_RECLAIM_FLAG_ON_LOSS 0x01u +#define NGTCP2_RECLAIM_FLAG_ON_LOSS 0x01U /* * rtb_reclaim_frame copies and queues frames included in |ent| for @@ -234,7 +233,7 @@ static ngtcp2_ssize rtb_reclaim_frame(ngtcp2_rtb *rtb, uint8_t flags, continue; } - switch (frc->fr.type) { + switch (frc->fr.hd.type) { case NGTCP2_FRAME_STREAM: strm = ngtcp2_conn_find_stream(conn, fr->stream.stream_id); if (strm == NULL || (strm->flags & NGTCP2_STRM_FLAG_RESET_STREAM)) { @@ -272,7 +271,12 @@ static ngtcp2_ssize rtb_reclaim_frame(ngtcp2_rtb *rtb, uint8_t flags, return rv; } - nfrc->fr = *fr; + nfrc->fr.stream.type = fr->stream.type; + nfrc->fr.stream.flags = fr->stream.flags; + nfrc->fr.stream.fin = fr->stream.fin; + nfrc->fr.stream.stream_id = fr->stream.stream_id; + nfrc->fr.stream.offset = fr->stream.offset; + nfrc->fr.stream.datacnt = fr->stream.datacnt; ngtcp2_vec_copy(nfrc->fr.stream.data, fr->stream.data, fr->stream.datacnt); @@ -315,7 +319,12 @@ static ngtcp2_ssize rtb_reclaim_frame(ngtcp2_rtb *rtb, uint8_t flags, return rv; } - nfrc->fr = *fr; + nfrc->fr.stream.type = fr->stream.type; + nfrc->fr.stream.flags = 0; + nfrc->fr.stream.fin = 0; + nfrc->fr.stream.stream_id = 0; + nfrc->fr.stream.offset = fr->stream.offset; + nfrc->fr.stream.datacnt = fr->stream.datacnt; ngtcp2_vec_copy(nfrc->fr.stream.data, fr->stream.data, fr->stream.datacnt); @@ -416,7 +425,7 @@ static int conn_process_lost_datagram(ngtcp2_conn *conn, int rv; for (frc = ent->frc; frc; frc = frc->next) { - switch (frc->fr.type) { + switch (frc->fr.hd.type) { case NGTCP2_FRAME_DATAGRAM: case NGTCP2_FRAME_DATAGRAM_LEN: assert(conn->callbacks.lost_datagram); @@ -454,23 +463,27 @@ static int rtb_on_pkt_lost(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent, if (ent->flags & (NGTCP2_RTB_ENTRY_FLAG_PMTUD_PROBE | NGTCP2_RTB_ENTRY_FLAG_SKIP)) { ++rtb->num_lost_ignore_pkts; - } else if (ent->hd.pkt_num >= rtb->cc_pkt_num) { - rtb->rst->lost += ent->pktlen; - - if (rtb->cc->on_pkt_lost) { - cc->on_pkt_lost(cc, cstat, - ngtcp2_cc_pkt_init(&pkt, ent->hd.pkt_num, ent->pktlen, - pktns->id, ent->ts, ent->rst.lost, - ent->rst.tx_in_flight, - ent->rst.is_app_limited), - ts); + } else { + ++cstat->pkt_lost; + cstat->bytes_lost += ent->pktlen; + + if (ent->hd.pkt_num >= rtb->cc_pkt_num) { + rtb->rst->lost += ent->pktlen; + + if (rtb->cc->on_pkt_lost) { + cc->on_pkt_lost(cc, cstat, + ngtcp2_cc_pkt_init(&pkt, ent->hd.pkt_num, ent->pktlen, + pktns->id, ent->ts, ent->rst.lost, + ent->rst.tx_in_flight, + ent->rst.is_app_limited), + ts); + } } } if (ent->flags & NGTCP2_RTB_ENTRY_FLAG_PTO_RECLAIMED) { - ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_LDC, - "pkn=%" PRId64 " has already been reclaimed on PTO", - ent->hd.pkt_num); + ngtcp2_log_infof(rtb->log, NGTCP2_LOG_EVENT_LDC, "pkn=", ent->hd.pkt_num, + " has already been reclaimed on PTO"); assert(!(ent->flags & NGTCP2_RTB_ENTRY_FLAG_LOST_RETRANSMITTED)); assert(UINT64_MAX == ent->lost_ts); } else { @@ -527,9 +540,10 @@ static void rtb_remove(ngtcp2_rtb *rtb, ngtcp2_ksl_it *it, int rv; (void)rv; + rtb_on_remove(rtb, ent, cstat); + rv = ngtcp2_ksl_remove_hint(&rtb->ents, it, it, &ent->hd.pkt_num); assert(0 == rv); - rtb_on_remove(rtb, ent, cstat); assert(ent->next == NULL); @@ -581,7 +595,7 @@ static int process_acked_pkt(ngtcp2_rtb_entry *ent, ngtcp2_conn *conn, if (conn->dcid.current.max_udp_payload_size < ent->pktlen) { conn->dcid.current.max_udp_payload_size = ent->pktlen; conn->cstat.max_tx_udp_payload_size = - ngtcp2_conn_get_path_max_tx_udp_payload_size(conn); + ngtcp2_conn_get_path_max_tx_udp_payload_size2(conn); } if (ngtcp2_pmtud_finished(conn->pmtud)) { @@ -598,7 +612,7 @@ static int process_acked_pkt(ngtcp2_rtb_entry *ent, ngtcp2_conn *conn, frc->binder->flags |= NGTCP2_FRAME_CHAIN_BINDER_FLAG_ACK; } - switch (frc->fr.type) { + switch (frc->fr.hd.type) { case NGTCP2_FRAME_STREAM: strm = ngtcp2_conn_find_stream(conn, frc->fr.stream.stream_id); if (strm == NULL) { @@ -733,8 +747,7 @@ static void rtb_on_pkt_acked(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent, static void conn_verify_ecn(ngtcp2_conn *conn, ngtcp2_pktns *pktns, ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, const ngtcp2_ack *fr, size_t ecn_acked, - ngtcp2_tstamp largest_pkt_sent_ts, - ngtcp2_tstamp ts) { + const ngtcp2_cc_ack *cc_ack, ngtcp2_tstamp ts) { if (conn->tx.ecn.state == NGTCP2_ECN_STATE_FAILED) { return; } @@ -761,9 +774,9 @@ static void conn_verify_ecn(ngtcp2_conn *conn, ngtcp2_pktns *pktns, } if (fr->type == NGTCP2_FRAME_ACK_ECN) { - if (cc->congestion_event && largest_pkt_sent_ts != UINT64_MAX && + if (cc->congestion_event && cc_ack->largest_pkt_sent_ts != UINT64_MAX && fr->ecn.ce > pktns->acktr.ecn.ack.ce) { - cc->congestion_event(cc, cstat, largest_pkt_sent_ts, 0, ts); + cc->congestion_event(cc, cstat, cc_ack->largest_pkt_sent_ts, cc_ack, ts); } pktns->acktr.ecn.ack.ect0 = fr->ecn.ect0; @@ -772,7 +785,7 @@ static void conn_verify_ecn(ngtcp2_conn *conn, ngtcp2_pktns *pktns, } } -static int rtb_detect_lost_pkt(ngtcp2_rtb *rtb, uint64_t *ppkt_lost, +static int rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_cc_ack *cc_ack, ngtcp2_conn *conn, ngtcp2_pktns *pktns, ngtcp2_conn_stat *cstat, ngtcp2_tstamp ts); @@ -786,19 +799,18 @@ ngtcp2_ssize ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, int rv; ngtcp2_ksl_it it; size_t num_acked = 0; - ngtcp2_tstamp largest_pkt_sent_ts = UINT64_MAX; int64_t pkt_num; ngtcp2_cc *cc = rtb->cc; ngtcp2_rtb_entry *acked_ent = NULL; int ack_eliciting_pkt_acked = 0; size_t ecn_acked = 0; int verify_ecn = 0; - ngtcp2_cc_ack cc_ack = {0}; + ngtcp2_cc_ack cc_ack = { + .largest_pkt_sent_ts = UINT64_MAX, + .rtt = UINT64_MAX, + }; size_t num_lost_pkts = rtb->num_lost_pkts - rtb->num_lost_ignore_pkts; - cc_ack.prior_bytes_in_flight = cstat->bytes_in_flight; - cc_ack.rtt = UINT64_MAX; - if (conn && (conn->flags & NGTCP2_CONN_FLAG_KEY_UPDATE_NOT_CONFIRMED) && (conn->flags & NGTCP2_CONN_FLAG_KEY_UPDATE_INITIATOR) && largest_ack >= conn->pktns.crypto.tx.ckm->pkt_num) { @@ -814,12 +826,17 @@ ngtcp2_ssize ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, verify_ecn = 1; } + ngtcp2_rst_reset_rate_sample(rtb->rst, cstat); + /* Assume that ngtcp2_pkt_validate_ack(fr) returns 0 */ it = ngtcp2_ksl_lower_bound(&rtb->ents, &largest_ack); if (ngtcp2_ksl_it_end(&it)) { if (conn && verify_ecn) { - conn_verify_ecn(conn, pktns, rtb->cc, cstat, fr, ecn_acked, - largest_pkt_sent_ts, ts); + conn_verify_ecn(conn, pktns, rtb->cc, cstat, fr, ecn_acked, &cc_ack, ts); + } + + if (cc->on_ack_recv) { + cc->on_ack_recv(cc, cstat, &cc_ack, ts); } return 0; @@ -843,8 +860,8 @@ ngtcp2_ssize ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, goto fail; } - if (largest_ack == pkt_num) { - largest_pkt_sent_ts = ent->ts; + if (rtb->largest_acked_tx_pkt_num == pkt_num) { + cc_ack.largest_pkt_sent_ts = ent->ts; } if (ent->flags & NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING) { @@ -884,15 +901,11 @@ ngtcp2_ssize ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, } } - if (largest_pkt_sent_ts != UINT64_MAX && ack_eliciting_pkt_acked) { + if (cc_ack.largest_pkt_sent_ts != UINT64_MAX && ack_eliciting_pkt_acked) { cc_ack.rtt = - ngtcp2_max_uint64(pkt_ts - largest_pkt_sent_ts, NGTCP2_NANOSECONDS); + ngtcp2_max(pkt_ts - cc_ack.largest_pkt_sent_ts, NGTCP2_NANOSECONDS); - rv = ngtcp2_conn_update_rtt(conn, cc_ack.rtt, fr->ack_delay_unscaled, ts); - if (rv == 0 && cc->new_rtt_sample && - rtb->largest_acked_tx_pkt_num >= rtb->cc_pkt_num) { - cc->new_rtt_sample(cc, cstat, ts); - } + ngtcp2_conn_update_rtt(conn, cc_ack.rtt, fr->ack_delay_unscaled, ts); } if (conn) { @@ -922,11 +935,6 @@ ngtcp2_ssize ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, ngtcp2_rtb_entry_objalloc_del(ent, rtb->rtb_entry_objalloc, rtb->frc_objalloc, rtb->mem); } - - if (verify_ecn) { - conn_verify_ecn(conn, pktns, rtb->cc, cstat, fr, ecn_acked, - largest_pkt_sent_ts, ts); - } } else { /* For unit tests */ for (ent = acked_ent; ent; ent = acked_ent) { @@ -947,15 +955,18 @@ ngtcp2_ssize ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, ngtcp2_rst_on_ack_recv(rtb->rst, cstat); if (conn) { - rv = rtb_detect_lost_pkt(rtb, &cc_ack.bytes_lost, conn, pktns, cstat, ts); + rv = rtb_detect_lost_pkt(rtb, &cc_ack, conn, pktns, cstat, ts); if (rv != 0) { return rv; } } } - cc_ack.largest_pkt_sent_ts = largest_pkt_sent_ts; - if (num_acked && cc->on_ack_recv) { + if (conn && verify_ecn) { + conn_verify_ecn(conn, pktns, rtb->cc, cstat, fr, ecn_acked, &cc_ack, ts); + } + + if (cc->on_ack_recv) { cc->on_ack_recv(cc, cstat, &cc_ack, ts); } @@ -987,7 +998,7 @@ static int rtb_pkt_lost(ngtcp2_rtb *rtb, ngtcp2_conn_stat *cstat, if (loss_time == UINT64_MAX) { loss_time = ent->ts + loss_delay; } else { - loss_time = ngtcp2_min_uint64(loss_time, ent->ts + loss_delay); + loss_time = ngtcp2_min(loss_time, ent->ts + loss_delay); } cstat->loss_time[pktns->id] = loss_time; @@ -1001,8 +1012,8 @@ static int rtb_pkt_lost(ngtcp2_rtb *rtb, ngtcp2_conn_stat *cstat, static ngtcp2_duration compute_pkt_loss_delay(const ngtcp2_conn_stat *cstat) { /* 9/8 is kTimeThreshold */ ngtcp2_duration loss_delay = - ngtcp2_max_uint64(cstat->latest_rtt, cstat->smoothed_rtt) * 9 / 8; - return ngtcp2_max_uint64(loss_delay, NGTCP2_GRANULARITY); + ngtcp2_max(cstat->latest_rtt, cstat->smoothed_rtt) * 9 / 8; + return ngtcp2_max(loss_delay, NGTCP2_GRANULARITY); } /* @@ -1021,7 +1032,11 @@ static int conn_all_ecn_pkt_lost(ngtcp2_conn *conn) { pktns->tx.ecn.validation_pkt_sent == pktns->tx.ecn.validation_pkt_lost; } -static int rtb_detect_lost_pkt(ngtcp2_rtb *rtb, uint64_t *ppkt_lost, +/* + * This function assigns the number of bytes lost to + * |cc_ack|->bytes_lost if any. + */ +static int rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_cc_ack *cc_ack, ngtcp2_conn *conn, ngtcp2_pktns *pktns, ngtcp2_conn_stat *cstat, ngtcp2_tstamp ts) { ngtcp2_rtb_entry *ent; @@ -1040,8 +1055,8 @@ static int rtb_detect_lost_pkt(ngtcp2_rtb *rtb, uint64_t *ppkt_lost, uint64_t bytes_lost = 0; ngtcp2_duration max_ack_delay; - pkt_thres = ngtcp2_max_uint64(pkt_thres, NGTCP2_PKT_THRESHOLD); - pkt_thres = ngtcp2_min_uint64(pkt_thres, 256); + pkt_thres = ngtcp2_max(pkt_thres, NGTCP2_PKT_THRESHOLD); + pkt_thres = ngtcp2_min(pkt_thres, 256); cstat->loss_time[pktns->id] = UINT64_MAX; loss_delay = compute_pkt_loss_delay(cstat); @@ -1066,12 +1081,11 @@ static int rtb_detect_lost_pkt(ngtcp2_rtb *rtb, uint64_t *ppkt_lost, congestion_period = (cstat->smoothed_rtt + - ngtcp2_max_uint64(4 * cstat->rttvar, NGTCP2_GRANULARITY) + - max_ack_delay) * + ngtcp2_max(4 * cstat->rttvar, NGTCP2_GRANULARITY) + max_ack_delay) * NGTCP2_PERSISTENT_CONGESTION_THRESHOLD; - start_ts = ngtcp2_max_uint64(conn->handshake_confirmed_ts, - cstat->first_rtt_sample_ts); + start_ts = + ngtcp2_max(conn->handshake_confirmed_ts, cstat->first_rtt_sample_ts); for (; !ngtcp2_ksl_it_end(&it); ngtcp2_ksl_it_next(&it)) { ent = ngtcp2_ksl_it_get(&it); @@ -1139,8 +1153,10 @@ static int rtb_detect_lost_pkt(ngtcp2_rtb *rtb, uint64_t *ppkt_lost, break; } + cc_ack->bytes_lost = bytes_lost; + if (cc->congestion_event) { - cc->congestion_event(cc, cstat, latest_ts, bytes_lost, ts); + cc->congestion_event(cc, cstat, latest_ts, cc_ack, ts); } loss_window = latest_ts - oldest_ts; @@ -1153,10 +1169,9 @@ static int rtb_detect_lost_pkt(ngtcp2_rtb *rtb, uint64_t *ppkt_lost, */ if (pktns->id == NGTCP2_PKTNS_ID_APPLICATION && loss_window && loss_window >= congestion_period) { - ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_LDC, - "persistent congestion loss_window=%" PRIu64 - " congestion_period=%" PRIu64, - loss_window, congestion_period); + ngtcp2_log_infof(rtb->log, NGTCP2_LOG_EVENT_LDC, + "persistent congestion loss_window=", loss_window, + " congestion_period=", congestion_period); /* Reset min_rtt, srtt, and rttvar here. Next new RTT sample will be used to recalculate these values. */ @@ -1176,18 +1191,18 @@ static int rtb_detect_lost_pkt(ngtcp2_rtb *rtb, uint64_t *ppkt_lost, ngtcp2_rtb_remove_excessive_lost_pkt(rtb, (size_t)pkt_thres); - if (ppkt_lost) { - *ppkt_lost = bytes_lost; - } - return 0; } int ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_conn *conn, ngtcp2_pktns *pktns, ngtcp2_conn_stat *cstat, ngtcp2_tstamp ts) { - return rtb_detect_lost_pkt(rtb, /* ppkt_lost = */ NULL, conn, pktns, cstat, - ts); + return rtb_detect_lost_pkt(rtb, + &(ngtcp2_cc_ack){ + .largest_pkt_sent_ts = UINT64_MAX, + .rtt = UINT64_MAX, + }, + conn, pktns, cstat, ts); } void ngtcp2_rtb_remove_excessive_lost_pkt(ngtcp2_rtb *rtb, size_t n) { @@ -1203,8 +1218,8 @@ void ngtcp2_rtb_remove_excessive_lost_pkt(ngtcp2_rtb *rtb, size_t n) { assert(ent->flags & NGTCP2_RTB_ENTRY_FLAG_LOST_RETRANSMITTED); - ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_LDC, - "removing stale lost pkn=%" PRId64, ent->hd.pkt_num); + ngtcp2_log_infof(rtb->log, NGTCP2_LOG_EVENT_LDC, + "removing stale lost pkn=", ent->hd.pkt_num); --rtb->num_lost_pkts; @@ -1240,13 +1255,14 @@ void ngtcp2_rtb_remove_expired_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_ksl_it_prev(&it); ent = ngtcp2_ksl_it_get(&it); - if (!(ent->flags & NGTCP2_RTB_ENTRY_FLAG_LOST_RETRANSMITTED) || - ts - ent->lost_ts < timeout) { + assert(ent->flags & NGTCP2_RTB_ENTRY_FLAG_LOST_RETRANSMITTED); + + if (ts - ent->lost_ts < timeout) { return; } - ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_LDC, - "removing stale lost pkn=%" PRId64, ent->hd.pkt_num); + ngtcp2_log_infof(rtb->log, NGTCP2_LOG_EVENT_LDC, + "removing stale lost pkn=", ent->hd.pkt_num); --rtb->num_lost_pkts; @@ -1290,7 +1306,7 @@ static int rtb_reclaim_frame_on_retry(ngtcp2_rtb *rtb, ngtcp2_conn *conn, int rv; for (; *pfrc;) { - switch ((*pfrc)->fr.type) { + switch ((*pfrc)->fr.hd.type) { case NGTCP2_FRAME_STREAM: frc = *pfrc; @@ -1398,15 +1414,21 @@ int ngtcp2_rtb_reclaim_on_retry(ngtcp2_rtb *rtb, ngtcp2_conn *conn, assert(!(ent->flags & NGTCP2_RTB_ENTRY_FLAG_PMTUD_PROBE)); if (ent->flags & NGTCP2_RTB_ENTRY_FLAG_PTO_RECLAIMED) { - ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_LDC, - "pkn=%" PRId64 " has already been reclaimed on PTO", - ent->hd.pkt_num); + ngtcp2_log_infof(rtb->log, NGTCP2_LOG_EVENT_LDC, "pkn=", ent->hd.pkt_num, + " has already been reclaimed on PTO"); + + ngtcp2_rtb_entry_objalloc_del(ent, rtb->rtb_entry_objalloc, + rtb->frc_objalloc, rtb->mem); + continue; } if (!(ent->flags & NGTCP2_RTB_ENTRY_FLAG_RETRANSMITTABLE) && (!(ent->flags & NGTCP2_RTB_ENTRY_FLAG_DATAGRAM) || !conn->callbacks.lost_datagram)) { + ngtcp2_rtb_entry_objalloc_del(ent, rtb->rtb_entry_objalloc, + rtb->frc_objalloc, rtb->mem); + continue; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.h index 14684a458a60e6..664498be962803 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.h @@ -48,41 +48,41 @@ typedef struct ngtcp2_conn_stat ngtcp2_conn_stat; typedef struct ngtcp2_frame_chain ngtcp2_frame_chain; /* NGTCP2_RTB_ENTRY_FLAG_NONE indicates that no flag is set. */ -#define NGTCP2_RTB_ENTRY_FLAG_NONE 0x00u +#define NGTCP2_RTB_ENTRY_FLAG_NONE 0x00U /* NGTCP2_RTB_ENTRY_FLAG_PROBE indicates that the entry includes a probe packet. */ -#define NGTCP2_RTB_ENTRY_FLAG_PROBE 0x01u +#define NGTCP2_RTB_ENTRY_FLAG_PROBE 0x01U /* NGTCP2_RTB_ENTRY_FLAG_RETRANSMITTABLE indicates that the entry includes a frame which must be retransmitted until it is acknowledged. In most cases, this flag is used along with NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING and NGTCP2_RTB_ENTRY_FLAG_PTO_ELICITING. */ -#define NGTCP2_RTB_ENTRY_FLAG_RETRANSMITTABLE 0x02u +#define NGTCP2_RTB_ENTRY_FLAG_RETRANSMITTABLE 0x02U /* NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING indicates that the entry elicits acknowledgement. */ -#define NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING 0x04u +#define NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING 0x04U /* NGTCP2_RTB_ENTRY_FLAG_PTO_RECLAIMED indicates that the packet has been reclaimed on PTO. It is not marked lost yet and still consumes congestion window. */ -#define NGTCP2_RTB_ENTRY_FLAG_PTO_RECLAIMED 0x08u +#define NGTCP2_RTB_ENTRY_FLAG_PTO_RECLAIMED 0x08U /* NGTCP2_RTB_ENTRY_FLAG_LOST_RETRANSMITTED indicates that the entry has been marked lost and, optionally, scheduled to retransmit. */ -#define NGTCP2_RTB_ENTRY_FLAG_LOST_RETRANSMITTED 0x10u +#define NGTCP2_RTB_ENTRY_FLAG_LOST_RETRANSMITTED 0x10U /* NGTCP2_RTB_ENTRY_FLAG_ECN indicates that the entry is included in a UDP datagram with ECN marking. */ -#define NGTCP2_RTB_ENTRY_FLAG_ECN 0x20u +#define NGTCP2_RTB_ENTRY_FLAG_ECN 0x20U /* NGTCP2_RTB_ENTRY_FLAG_DATAGRAM indicates that the entry includes DATAGRAM frame. */ -#define NGTCP2_RTB_ENTRY_FLAG_DATAGRAM 0x40u +#define NGTCP2_RTB_ENTRY_FLAG_DATAGRAM 0x40U /* NGTCP2_RTB_ENTRY_FLAG_PMTUD_PROBE indicates that the entry includes a PMTUD probe packet. */ -#define NGTCP2_RTB_ENTRY_FLAG_PMTUD_PROBE 0x80u +#define NGTCP2_RTB_ENTRY_FLAG_PMTUD_PROBE 0x80U /* NGTCP2_RTB_ENTRY_FLAG_PTO_ELICITING indicates that the entry includes a packet which elicits PTO probe packets. */ -#define NGTCP2_RTB_ENTRY_FLAG_PTO_ELICITING 0x100u +#define NGTCP2_RTB_ENTRY_FLAG_PTO_ELICITING 0x100U /* NGTCP2_RTB_ENTRY_FLAG_SKIP indicates that the entry has the skipped packet number. */ -#define NGTCP2_RTB_ENTRY_FLAG_SKIP 0x200u +#define NGTCP2_RTB_ENTRY_FLAG_SKIP 0x200U typedef struct ngtcp2_rtb_entry ngtcp2_rtb_entry; @@ -115,7 +115,7 @@ struct ngtcp2_rtb_entry { ngtcp2_tstamp first_sent_ts; uint64_t tx_in_flight; uint64_t lost; - int64_t end_seq; + int64_t pkt_id; int is_app_limited; } rst; /* flags is bitwise-OR of zero or more of diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.c index f774504282e6b1..31c52b4d957199 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.c @@ -37,6 +37,7 @@ void ngtcp2_settings_default_versioned(int settings_version, switch (settings_version) { case NGTCP2_SETTINGS_VERSION: + case NGTCP2_SETTINGS_V3: settings->glitch_ratelim_burst = NGTCP2_DEFAULT_GLITCH_RATELIM_BURST; settings->glitch_ratelim_rate = NGTCP2_DEFAULT_GLITCH_RATELIM_RATE; /* fall through */ @@ -86,6 +87,9 @@ size_t ngtcp2_settingslen_version(int settings_version) { switch (settings_version) { case NGTCP2_SETTINGS_VERSION: return sizeof(settings); + case NGTCP2_SETTINGS_V3: + return offsetof(ngtcp2_settings, glitch_ratelim_rate) + + sizeof(settings.glitch_ratelim_rate); case NGTCP2_SETTINGS_V2: return offsetof(ngtcp2_settings, pmtud_probeslen) + sizeof(settings.pmtud_probeslen); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.h index caa0fb58c58b13..b40b6c4a89525b 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.h @@ -33,10 +33,10 @@ /* NGTCP2_DEFAULT_GLITCH_RATELIM_BURST is the maximum number of tokens in glitch rate limiter. It is also the initial value. */ -#define NGTCP2_DEFAULT_GLITCH_RATELIM_BURST 1000 +#define NGTCP2_DEFAULT_GLITCH_RATELIM_BURST 10000 /* NGTCP2_DEFAULT_GLITCH_RATELIM_RATE is the rate of tokens generated per second for glitch rate limiter. */ -#define NGTCP2_DEFAULT_GLITCH_RATELIM_RATE 33 +#define NGTCP2_DEFAULT_GLITCH_RATELIM_RATE 330 /* * ngtcp2_settings_convert_to_latest converts |src| of version diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_str.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_str.c index a61636d188fae5..dd5ef5a1af6f6c 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_str.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_str.c @@ -25,8 +25,10 @@ #include "ngtcp2_str.h" #include +#include #include "ngtcp2_macro.h" +#include "ngtcp2_unreachable.h" void *ngtcp2_cpymem(void *dest, const void *src, size_t n) { memcpy(dest, src, n); @@ -38,6 +40,22 @@ uint8_t *ngtcp2_setmem(uint8_t *dest, uint8_t b, size_t n) { return dest + n; } +void ngtcp2_secure_clear(void *data, size_t len) { +#ifdef WIN32 + SecureZeroMemory(data, len); +#elif defined(HAVE_EXPLICIT_BZERO) + explicit_bzero(data, len); +#elif defined(HAVE_MEMSET_S) + memset_s(data, len, 0, len); +#else /* !defined(WIN32) && !defined(HAVE_EXPLICIT_BZERO) && \ + !defined(HAVE_MEMSET_S) */ + static void *(*volatile memset_ptr)(void *, int, size_t) = memset; + + memset_ptr(data, 0, len); +#endif /* !defined(WIN32) && !defined(HAVE_EXPLICIT_BZERO) && \ + !defined(HAVE_MEMSET_S) */ +} + const void *ngtcp2_get_bytes(void *dest, const void *src, size_t n) { memcpy(dest, src, n); return (uint8_t *)src + n; @@ -47,76 +65,101 @@ const void *ngtcp2_get_bytes(void *dest, const void *src, size_t n) { uint8_t *ngtcp2_encode_hex(uint8_t *dest, const uint8_t *data, size_t len) { size_t i; - uint8_t *p = dest; for (i = 0; i < len; ++i) { - *p++ = (uint8_t)LOWER_XDIGITS[data[i] >> 4]; - *p++ = (uint8_t)LOWER_XDIGITS[data[i] & 0xf]; + *dest++ = (uint8_t)LOWER_XDIGITS[data[i] >> 4]; + *dest++ = (uint8_t)LOWER_XDIGITS[data[i] & 0xFU]; } - *p = '\0'; - return dest; } -char *ngtcp2_encode_printable_ascii(char *dest, const uint8_t *data, - size_t len) { +size_t ngtcp2_encode_uint_hexlen(uint64_t n) { size_t i; - char *p = dest; - uint8_t c; + uint8_t d; - for (i = 0; i < len; ++i) { - c = data[i]; - if (0x20 <= c && c <= 0x7e) { - *p++ = (char)c; - } else { - *p++ = '.'; - } + if (n == 0) { + return 1; } - *p = '\0'; + for (i = 0; i < sizeof(n); ++i) { + d = (uint8_t)(n >> (sizeof(n) - 1 - i) * 8); + if (!d) { + continue; + } - return dest; + if (d >> 4) { + return (sizeof(n) - i) * 2; + } + + return (sizeof(n) - i) * 2 - 1; + } + + ngtcp2_unreachable(); } -/* - * write_uint writes |n| to the buffer pointed by |p| in decimal - * representation. It returns |p| plus the number of bytes written. - * The function assumes that the buffer has enough capacity to contain - * a string. - */ -static uint8_t *write_uint(uint8_t *p, uint64_t n) { - size_t nlen = 0; - uint64_t t; - uint8_t *res; +uint8_t *ngtcp2_encode_uint_hex(uint8_t *dest, uint64_t n) { + size_t i; + uint8_t d; if (n == 0) { - *p++ = '0'; - return p; + *dest++ = '0'; + + return dest; } - for (t = n; t; t /= 10, ++nlen) - ; - p += nlen; - res = p; - for (; n; n /= 10) { - *--p = (uint8_t)((n % 10) + '0'); + + for (i = 0; i < sizeof(n); ++i) { + d = (uint8_t)(n >> (sizeof(n) - 1 - i) * 8); + if (d) { + if (d >> 4) { + *dest++ = (uint8_t)LOWER_XDIGITS[d >> 4]; + } + + *dest++ = (uint8_t)LOWER_XDIGITS[d & 0xFU]; + ++i; + + break; + } + } + + for (; i < sizeof(n); ++i) { + d = (uint8_t)(n >> (sizeof(n) - 1 - i) * 8); + + *dest++ = (uint8_t)LOWER_XDIGITS[d >> 4]; + *dest++ = (uint8_t)LOWER_XDIGITS[d & 0xFU]; } - return res; + + return dest; } -uint8_t *ngtcp2_encode_ipv4(uint8_t *dest, const uint8_t *addr) { +uint8_t *ngtcp2_encode_printable_ascii(uint8_t *dest, const uint8_t *data, + size_t len) { size_t i; - uint8_t *p = dest; + uint8_t c; - p = write_uint(p, addr[0]); + for (i = 0; i < len; ++i) { + c = data[i]; + if (0x20 <= c && c <= 0x7E) { + *dest++ = c; + } else { + *dest++ = '.'; + } + } + + return dest; +} + +uint8_t *ngtcp2_encode_ipv4(uint8_t *dest, const ngtcp2_in_addr *addr) { + size_t i; + const uint8_t *in = (const uint8_t *)addr; + + dest = ngtcp2_encode_uint(dest, in[0]); for (i = 1; i < 4; ++i) { - *p++ = '.'; - p = write_uint(p, addr[i]); + *dest++ = '.'; + dest = ngtcp2_encode_uint(dest, in[i]); } - *p = '\0'; - return dest; } @@ -136,7 +179,7 @@ static uint8_t *write_hex_zsup(uint8_t *dest, const uint8_t *data, size_t len) { break; } - d &= 0xf; + d &= 0xFU; if (d) { *p++ = (uint8_t)LOWER_XDIGITS[d]; @@ -153,22 +196,22 @@ static uint8_t *write_hex_zsup(uint8_t *dest, const uint8_t *data, size_t len) { for (; i < len; ++i) { d = data[i]; *p++ = (uint8_t)LOWER_XDIGITS[d >> 4]; - *p++ = (uint8_t)LOWER_XDIGITS[d & 0xf]; + *p++ = (uint8_t)LOWER_XDIGITS[d & 0xFU]; } return p; } -uint8_t *ngtcp2_encode_ipv6(uint8_t *dest, const uint8_t *addr) { +uint8_t *ngtcp2_encode_ipv6(uint8_t *dest, const ngtcp2_in6_addr *addr) { uint16_t blks[8]; size_t i; size_t zlen, zoff; size_t max_zlen = 0, max_zoff = 8; - uint8_t *p = dest; + const uint8_t *in = (const uint8_t *)addr; for (i = 0; i < 16; i += sizeof(uint16_t)) { /* Copy in network byte order. */ - memcpy(&blks[i / sizeof(uint16_t)], addr + i, sizeof(uint16_t)); + memcpy(&blks[i / sizeof(uint16_t)], in + i, sizeof(uint16_t)); } for (i = 0; i < 8;) { @@ -195,29 +238,29 @@ uint8_t *ngtcp2_encode_ipv6(uint8_t *dest, const uint8_t *addr) { } if (max_zoff != 0) { - p = write_hex_zsup(p, (const uint8_t *)blks, sizeof(uint16_t)); + dest = write_hex_zsup(dest, (const uint8_t *)blks, sizeof(uint16_t)); for (i = 1; i < max_zoff; ++i) { - *p++ = ':'; - p = write_hex_zsup(p, (const uint8_t *)(blks + i), sizeof(uint16_t)); + *dest++ = ':'; + dest = + write_hex_zsup(dest, (const uint8_t *)(blks + i), sizeof(uint16_t)); } } if (max_zoff != 8) { - *p++ = ':'; + *dest++ = ':'; if (max_zoff + max_zlen == 8) { - *p++ = ':'; + *dest++ = ':'; } else { for (i = max_zoff + max_zlen; i < 8; ++i) { - *p++ = ':'; - p = write_hex_zsup(p, (const uint8_t *)(blks + i), sizeof(uint16_t)); + *dest++ = ':'; + dest = + write_hex_zsup(dest, (const uint8_t *)(blks + i), sizeof(uint16_t)); } } } - *p = '\0'; - return dest; } @@ -231,3 +274,126 @@ int ngtcp2_cmemeq(const uint8_t *a, const uint8_t *b, size_t n) { return rv == 0; } + +/* countl_zero counts the number of leading zeros in |x|. It is + undefined if |x| is 0. */ +static int countl_zero(uint64_t x) { +#ifdef __GNUC__ + return __builtin_clzll(x); +#else /* !defined(__GNUC__) */ + /* This is the same implementation of Go's LeadingZeros64 in + math/bits package. */ + static const uint8_t len8tab[] = { + 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + }; + int n = 0; + + if (x >= 1ULL << 32) { + x >>= 32; + n += 32; + } + + if (x >= 1 << 16) { + x >>= 16; + n += 16; + } + + if (x >= 1 << 8) { + x >>= 8; + n += 8; + } + + return 64 - (n + len8tab[x]); +#endif /* !defined(__GNUC__) */ +} + +/* + * count_digit returns the minimum number of digits to represent |x| + * in base 10. + * + * credit: + * https://lemire.me/blog/2025/01/07/counting-the-digits-of-64-bit-integers/ + */ +static size_t count_digit(uint64_t x) { + static const uint64_t count_digit_tbl[] = { + 9ULL, + 99ULL, + 999ULL, + 9999ULL, + 99999ULL, + 999999ULL, + 9999999ULL, + 99999999ULL, + 999999999ULL, + 9999999999ULL, + 99999999999ULL, + 999999999999ULL, + 9999999999999ULL, + 99999999999999ULL, + 999999999999999ULL, + 9999999999999999ULL, + 99999999999999999ULL, + 999999999999999999ULL, + 9999999999999999999ULL, + }; + size_t y = (size_t)(19 * (63 - countl_zero(x | 1)) >> 6); + + y += x > count_digit_tbl[y]; + + return y + 1; +} + +size_t ngtcp2_encode_uintlen(uint64_t n) { return count_digit(n); } + +uint8_t *ngtcp2_encode_uint(uint8_t *dest, uint64_t n) { + static const uint8_t uint_digits[] = + "00010203040506070809101112131415161718192021222324252627282930313233343536" + "37383940414243444546474849505152535455565758596061626364656667686970717273" + "7475767778798081828384858687888990919293949596979899"; + uint8_t *p; + const uint8_t *tp; + + if (n < 10) { + *dest++ = (uint8_t)('0' + n); + return dest; + } + + if (n < 100) { + tp = &uint_digits[n * 2]; + *dest++ = *tp++; + *dest++ = *tp; + return dest; + } + + dest += count_digit(n); + p = dest; + + for (; n >= 100; n /= 100) { + p -= 2; + tp = &uint_digits[(n % 100) * 2]; + p[0] = *tp++; + p[1] = *tp; + } + + if (n < 10) { + *--p = (uint8_t)('0' + n); + return dest; + } + + p -= 2; + tp = &uint_digits[n * 2]; + p[0] = *tp++; + p[1] = *tp; + + return dest; +} diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_str.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_str.h index f970c153e805a8..abda53e46d528e 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_str.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_str.h @@ -46,43 +46,54 @@ uint8_t *ngtcp2_setmem(uint8_t *dest, uint8_t b, size_t n); const void *ngtcp2_get_bytes(void *dest, const void *src, size_t n); /* - * ngtcp2_encode_hex encodes |data| of length |len| in hex string. It - * writes additional NULL bytes at the end of the buffer. The buffer - * pointed by |dest| must have at least |len| * 2 + 1 bytes space. - * This function returns |dest|. + * ngtcp2_encode_hex encodes |data| of length |len| in hex string. + * The buffer pointed by |dest| must have at least |len| * 2 bytes + * space. This function returns |dest| + |len| * 2. */ uint8_t *ngtcp2_encode_hex(uint8_t *dest, const uint8_t *data, size_t len); +/* + * ngtcp2_encode_uint_hexlen returns the number of bytes + * ngtcp2_encode_uint_hex produces when |n| is given. + */ +size_t ngtcp2_encode_uint_hexlen(uint64_t n); + +/* + * ngtcp2_encode_uint_hex encodes |n| in hex string. It omits the + * leading zeros (e.g., 1fb). The buffer pointed by |dest| must have + * at least ngtcp2_encode_uint_hexlen(|n|) bytes. This function + * returns |dest| + the number of bytes written. + */ +uint8_t *ngtcp2_encode_uint_hex(uint8_t *dest, uint64_t n); + /* * ngtcp2_encode_ipv4 encodes binary form IPv4 address stored in * |addr| to human readable text form in the buffer pointed by |dest|. - * The capacity of buffer must have enough length to store a text form - * plus a terminating NULL byte. The resulting text form ends with - * NULL byte. The function returns |dest|. + * The capacity of buffer must have enough length to store a text + * form. The function returns |dest| + the number of bytes written. */ -uint8_t *ngtcp2_encode_ipv4(uint8_t *dest, const uint8_t *addr); +uint8_t *ngtcp2_encode_ipv4(uint8_t *dest, const ngtcp2_in_addr *addr); /* * ngtcp2_encode_ipv6 encodes binary form IPv6 address stored in * |addr| to human readable text form in the buffer pointed by |dest|. - * The capacity of buffer must have enough length to store a text form - * plus a terminating NULL byte. The resulting text form ends with - * NULL byte. The function produces the canonical form of IPv6 text + * The capacity of buffer must have enough length to store a text + * form. The function produces the canonical form of IPv6 text * representation described in * https://tools.ietf.org/html/rfc5952#section-4. The function - * returns |dest|. + * returns |dest| + the number of bytes written. */ -uint8_t *ngtcp2_encode_ipv6(uint8_t *dest, const uint8_t *addr); +uint8_t *ngtcp2_encode_ipv6(uint8_t *dest, const ngtcp2_in6_addr *addr); /* * ngtcp2_encode_printable_ascii encodes |data| of length |len| in * |dest| in the following manner: printable ascii characters are - * copied as is. The other characters are converted to ".". It - * writes additional NULL bytes at the end of the buffer. |dest| must - * have at least |len| + 1 bytes. This function returns |dest|. + * copied as is. The other characters are converted to ".". |dest| + * must have at least |len|. This function returns |dest| + the + * number of bytes written. */ -char *ngtcp2_encode_printable_ascii(char *dest, const uint8_t *data, - size_t len); +uint8_t *ngtcp2_encode_printable_ascii(uint8_t *dest, const uint8_t *data, + size_t len); /* * ngtcp2_cmemeq returns nonzero if the first |n| bytes of the buffers @@ -91,4 +102,18 @@ char *ngtcp2_encode_printable_ascii(char *dest, const uint8_t *data, */ int ngtcp2_cmemeq(const uint8_t *a, const uint8_t *b, size_t n); +/* + * ngtcp2_encode_uintlen returns the number of bytes + * ngtcp2_encode_uint produces when |n| is given. + */ +size_t ngtcp2_encode_uintlen(uint64_t n); + +/* + * ngtcp2_encode_uint encodes |n| as a decimal integer to the buffer + * pointed by |dest|. This function assumes that the buffer contains + * the sufficient capacity to write the number. This function returns + * the pointer to the buffer past the last byte written. + */ +uint8_t *ngtcp2_encode_uint(uint8_t *dest, uint64_t n); + #endif /* !defined(NGTCP2_STR_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.c index faa417713224d5..70aa8933a3b83f 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.c @@ -36,30 +36,27 @@ void ngtcp2_strm_init(ngtcp2_strm *strm, int64_t stream_id, uint32_t flags, uint64_t max_rx_offset, uint64_t max_tx_offset, void *stream_user_data, ngtcp2_objalloc *frc_objalloc, const ngtcp2_mem *mem) { - strm->pe.index = NGTCP2_PQ_BAD_INDEX; - strm->cycle = 0; - strm->frc_objalloc = frc_objalloc; - strm->tx.acked_offset = NULL; - strm->tx.cont_acked_offset = 0; - strm->tx.streamfrq = NULL; - strm->tx.offset = 0; - strm->tx.max_offset = max_tx_offset; - strm->tx.last_blocked_offset = UINT64_MAX; - strm->tx.last_max_stream_data_ts = UINT64_MAX; - strm->tx.loss_count = 0; - strm->tx.last_lost_pkt_num = -1; - strm->tx.stop_sending_app_error_code = 0; - strm->tx.reset_stream_app_error_code = 0; - strm->rx.rob = NULL; - strm->rx.cont_offset = 0; - strm->rx.last_offset = 0; - strm->rx.max_offset = strm->rx.unsent_max_offset = strm->rx.window = - max_rx_offset; - strm->mem = mem; - strm->stream_id = stream_id; - strm->stream_user_data = stream_user_data; - strm->flags = flags; - strm->app_error_code = 0; + *strm = (ngtcp2_strm){ + .pe.index = NGTCP2_PQ_BAD_INDEX, + .frc_objalloc = frc_objalloc, + .tx = + { + .max_offset = max_tx_offset, + .last_blocked_offset = UINT64_MAX, + .last_max_stream_data_ts = UINT64_MAX, + .last_lost_pkt_num = -1, + }, + .rx = + { + .max_offset = max_rx_offset, + .unsent_max_offset = max_rx_offset, + .window = max_rx_offset, + }, + .mem = mem, + .stream_id = stream_id, + .stream_user_data = stream_user_data, + .flags = flags, + }; } void ngtcp2_strm_free(ngtcp2_strm *strm) { @@ -120,7 +117,7 @@ uint64_t ngtcp2_strm_rx_offset(const ngtcp2_strm *strm) { /* strm_rob_heavily_fragmented returns nonzero if the number of gaps in |rob| exceeds the limit. */ static int strm_rob_heavily_fragmented(const ngtcp2_rob *rob) { - return ngtcp2_ksl_len(&rob->gapksl) >= 1000; + return ngtcp2_ksl_len(&rob->gapksl) >= 4000; } ngtcp2_ssize ngtcp2_strm_recv_reordering(ngtcp2_strm *strm, const uint8_t *data, @@ -193,8 +190,8 @@ static int strm_streamfrq_init(ngtcp2_strm *strm) { int ngtcp2_strm_streamfrq_push(ngtcp2_strm *strm, ngtcp2_frame_chain *frc) { int rv; - assert(frc->fr.type == NGTCP2_FRAME_STREAM || - frc->fr.type == NGTCP2_FRAME_CRYPTO); + assert(frc->fr.hd.type == NGTCP2_FRAME_STREAM || + frc->fr.hd.type == NGTCP2_FRAME_CRYPTO); assert(frc->next == NULL); if (strm->tx.streamfrq == NULL) { @@ -202,7 +199,7 @@ int ngtcp2_strm_streamfrq_push(ngtcp2_strm *strm, ngtcp2_frame_chain *frc) { if (rv != 0) { return rv; } - } else if (ngtcp2_ksl_len(strm->tx.streamfrq) >= 1000) { + } else if (ngtcp2_ksl_len(strm->tx.streamfrq) >= 8000) { return NGTCP2_ERR_INTERNAL; } @@ -308,8 +305,7 @@ static int strm_streamfrq_unacked_pop(ngtcp2_strm *strm, fr->offset = offset + base_offset; fr->datacnt = end_idx - idx; - fr->data[0].base += base_offset; - fr->data[0].len -= (size_t)base_offset; + ngtcp2_vec_drop(&fr->data[0], (size_t)base_offset); *pfrc = frc; @@ -335,8 +331,7 @@ static int strm_streamfrq_unacked_pop(ngtcp2_strm *strm, nfr->stream_id = fr->stream_id; nfr->offset = end_offset + end_base_offset; nfr->datacnt = fr->datacnt - end_idx; - nfr->data[0].base += end_base_offset; - nfr->data[0].len -= (size_t)end_base_offset; + ngtcp2_vec_drop(&nfr->data[0], (size_t)end_base_offset); rv = ngtcp2_ksl_insert(strm->tx.streamfrq, NULL, &nfr->offset, nfrc); if (rv != 0) { @@ -364,8 +359,7 @@ static int strm_streamfrq_unacked_pop(ngtcp2_strm *strm, fr->data[fr->datacnt - 1].len = (size_t)end_base_offset; } - fr->data[0].base += base_offset; - fr->data[0].len -= (size_t)base_offset; + ngtcp2_vec_drop(&fr->data[0], (size_t)base_offset); *pfrc = frc; @@ -378,13 +372,12 @@ static int strm_streamfrq_unacked_pop(ngtcp2_strm *strm, int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc, size_t left) { ngtcp2_stream *fr, *nfr; - ngtcp2_frame_chain *frc, *nfrc, *sfrc; + ngtcp2_frame_chain *frc, *nfrc; int rv; size_t nmerged; uint64_t datalen; - ngtcp2_vec a[NGTCP2_MAX_STREAM_DATACNT]; - ngtcp2_vec b[NGTCP2_MAX_STREAM_DATACNT]; - size_t acnt, bcnt; + ngtcp2_vec data[NGTCP2_MAX_STREAM_DATACNT]; + size_t datacnt; uint64_t unacked_offset; if (strm->tx.streamfrq == NULL || ngtcp2_ksl_len(strm->tx.streamfrq) == 0) { @@ -424,17 +417,15 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc, } if (datalen > left) { - ngtcp2_vec_copy(a, fr->data, fr->datacnt); - acnt = fr->datacnt; - - bcnt = 0; - ngtcp2_vec_split(b, &bcnt, a, &acnt, left, NGTCP2_MAX_STREAM_DATACNT); + datacnt = 0; + ngtcp2_vec_split(data, &datacnt, fr->data, &fr->datacnt, left, + NGTCP2_MAX_STREAM_DATACNT); - assert(acnt > 0); - assert(bcnt > 0); + assert(fr->datacnt > 0); + assert(datacnt > 0); rv = ngtcp2_frame_chain_stream_datacnt_objalloc_new( - &nfrc, bcnt, strm->frc_objalloc, strm->mem); + &nfrc, datacnt, strm->frc_objalloc, strm->mem); if (rv != 0) { assert(ngtcp2_err_is_fatal(rv)); ngtcp2_frame_chain_objalloc_del(frc, strm->frc_objalloc, strm->mem); @@ -447,8 +438,8 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc, nfr->fin = fr->fin; nfr->stream_id = fr->stream_id; nfr->offset = fr->offset + left; - nfr->datacnt = bcnt; - ngtcp2_vec_copy(nfr->data, b, bcnt); + nfr->datacnt = datacnt; + ngtcp2_vec_copy(nfr->data, data, datacnt); rv = ngtcp2_ksl_insert(strm->tx.streamfrq, NULL, &nfr->offset, nfrc); if (rv != 0) { @@ -459,31 +450,17 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc, return rv; } - rv = ngtcp2_frame_chain_stream_datacnt_objalloc_new( - &nfrc, acnt, strm->frc_objalloc, strm->mem); - if (rv != 0) { - assert(ngtcp2_err_is_fatal(rv)); - ngtcp2_frame_chain_objalloc_del(frc, strm->frc_objalloc, strm->mem); - return rv; - } - - nfr = &nfrc->fr.stream; - *nfr = *fr; - nfr->fin = 0; - nfr->datacnt = acnt; - ngtcp2_vec_copy(nfr->data, a, acnt); - - ngtcp2_frame_chain_objalloc_del(frc, strm->frc_objalloc, strm->mem); + fr->fin = 0; - *pfrc = nfrc; + *pfrc = frc; return 0; } left -= (size_t)datalen; - ngtcp2_vec_copy(a, fr->data, fr->datacnt); - acnt = fr->datacnt; + ngtcp2_vec_copy(data, fr->data, fr->datacnt); + datacnt = fr->datacnt; for (; left && ngtcp2_ksl_len(strm->tx.streamfrq);) { unacked_offset = ngtcp2_strm_streamfrq_unacked_offset(strm); @@ -510,9 +487,7 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc, break; } - bcnt = nfr->datacnt; - - nmerged = ngtcp2_vec_merge(a, &acnt, nfr->data, &bcnt, left, + nmerged = ngtcp2_vec_merge(data, &datacnt, nfr->data, &nfr->datacnt, left, NGTCP2_MAX_STREAM_DATACNT); if (nmerged == 0) { rv = ngtcp2_ksl_insert(strm->tx.streamfrq, NULL, &nfr->offset, nfrc); @@ -530,54 +505,27 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc, datalen += nmerged; left -= nmerged; - if (bcnt == 0) { + if (nfr->datacnt == 0) { fr->fin = nfr->fin; ngtcp2_frame_chain_objalloc_del(nfrc, strm->frc_objalloc, strm->mem); continue; } - if (nfr->datacnt <= NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES || - bcnt > NGTCP2_FRAME_CHAIN_STREAM_DATACNT_THRES) { - nfr->offset += nmerged; - nfr->datacnt = bcnt; - - rv = ngtcp2_ksl_insert(strm->tx.streamfrq, NULL, &nfr->offset, nfrc); - if (rv != 0) { - ngtcp2_frame_chain_objalloc_del(nfrc, strm->frc_objalloc, strm->mem); - ngtcp2_frame_chain_objalloc_del(frc, strm->frc_objalloc, strm->mem); - return rv; - } - } else { - rv = ngtcp2_frame_chain_stream_datacnt_objalloc_new( - &sfrc, bcnt, strm->frc_objalloc, strm->mem); - if (rv != 0) { - ngtcp2_frame_chain_objalloc_del(nfrc, strm->frc_objalloc, strm->mem); - ngtcp2_frame_chain_objalloc_del(frc, strm->frc_objalloc, strm->mem); - return rv; - } - - sfrc->fr.stream = nfrc->fr.stream; - sfrc->fr.stream.offset += nmerged; - sfrc->fr.stream.datacnt = bcnt; - ngtcp2_vec_copy(sfrc->fr.stream.data, nfrc->fr.stream.data, bcnt); + nfr->offset += nmerged; + rv = ngtcp2_ksl_insert(strm->tx.streamfrq, NULL, &nfr->offset, nfrc); + if (rv != 0) { ngtcp2_frame_chain_objalloc_del(nfrc, strm->frc_objalloc, strm->mem); - - rv = ngtcp2_ksl_insert(strm->tx.streamfrq, NULL, &sfrc->fr.stream.offset, - sfrc); - if (rv != 0) { - ngtcp2_frame_chain_objalloc_del(sfrc, strm->frc_objalloc, strm->mem); - ngtcp2_frame_chain_objalloc_del(frc, strm->frc_objalloc, strm->mem); - return rv; - } + ngtcp2_frame_chain_objalloc_del(frc, strm->frc_objalloc, strm->mem); + return rv; } break; } - if (acnt == fr->datacnt) { - if (acnt > 0) { - fr->data[acnt - 1] = a[acnt - 1]; + if (datacnt == fr->datacnt) { + if (datacnt > 0) { + fr->data[datacnt - 1] = data[datacnt - 1]; } *pfrc = frc; @@ -585,19 +533,23 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc, return 0; } - assert(acnt > fr->datacnt); + assert(datacnt > fr->datacnt); rv = ngtcp2_frame_chain_stream_datacnt_objalloc_new( - &nfrc, acnt, strm->frc_objalloc, strm->mem); + &nfrc, datacnt, strm->frc_objalloc, strm->mem); if (rv != 0) { ngtcp2_frame_chain_objalloc_del(frc, strm->frc_objalloc, strm->mem); return rv; } nfr = &nfrc->fr.stream; - *nfr = *fr; - nfr->datacnt = acnt; - ngtcp2_vec_copy(nfr->data, a, acnt); + nfr->type = fr->type; + nfr->flags = fr->flags; + nfr->fin = fr->fin; + nfr->stream_id = fr->stream_id; + nfr->offset = fr->offset; + nfr->datacnt = datacnt; + ngtcp2_vec_copy(nfr->data, data, datacnt); ngtcp2_frame_chain_objalloc_del(frc, strm->frc_objalloc, strm->mem); @@ -694,12 +646,11 @@ int ngtcp2_strm_is_all_tx_data_fin_acked(const ngtcp2_strm *strm) { ngtcp2_range ngtcp2_strm_get_unacked_range_after(const ngtcp2_strm *strm, uint64_t offset) { - ngtcp2_range gap; - if (strm->tx.acked_offset == NULL) { - gap.begin = strm->tx.cont_acked_offset; - gap.end = UINT64_MAX; - return gap; + return (ngtcp2_range){ + .begin = strm->tx.cont_acked_offset, + .end = UINT64_MAX, + }; } return ngtcp2_gaptr_get_first_gap_after(strm->tx.acked_offset, offset); @@ -754,7 +705,7 @@ int ngtcp2_strm_ack_data(ngtcp2_strm *strm, uint64_t offset, uint64_t len) { return rv; } - if (ngtcp2_ksl_len(&strm->tx.acked_offset->gap) >= 1000) { + if (ngtcp2_ksl_len(&strm->tx.acked_offset->gap) >= 4000) { return NGTCP2_ERR_INTERNAL; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.h index 1a1e8fd3b7d5c4..0d5d8a63a62c62 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.h @@ -41,54 +41,54 @@ typedef struct ngtcp2_frame_chain ngtcp2_frame_chain; /* NGTCP2_STRM_FLAG_NONE indicates that no flag is set. */ -#define NGTCP2_STRM_FLAG_NONE 0x00u +#define NGTCP2_STRM_FLAG_NONE 0x00U /* NGTCP2_STRM_FLAG_SHUT_RD indicates that further reception of stream data is not allowed. */ -#define NGTCP2_STRM_FLAG_SHUT_RD 0x01u +#define NGTCP2_STRM_FLAG_SHUT_RD 0x01U /* NGTCP2_STRM_FLAG_SHUT_WR indicates that further transmission of stream data is not allowed. */ -#define NGTCP2_STRM_FLAG_SHUT_WR 0x02u +#define NGTCP2_STRM_FLAG_SHUT_WR 0x02U #define NGTCP2_STRM_FLAG_SHUT_RDWR \ (NGTCP2_STRM_FLAG_SHUT_RD | NGTCP2_STRM_FLAG_SHUT_WR) /* NGTCP2_STRM_FLAG_RESET_STREAM indicates that RESET_STREAM is sent from the local endpoint. In this case, NGTCP2_STRM_FLAG_SHUT_WR is also set. */ -#define NGTCP2_STRM_FLAG_RESET_STREAM 0x04u +#define NGTCP2_STRM_FLAG_RESET_STREAM 0x04U /* NGTCP2_STRM_FLAG_RESET_STREAM_RECVED indicates that RESET_STREAM is received from the remote endpoint. In this case, NGTCP2_STRM_FLAG_SHUT_RD is also set. */ -#define NGTCP2_STRM_FLAG_RESET_STREAM_RECVED 0x08u +#define NGTCP2_STRM_FLAG_RESET_STREAM_RECVED 0x08U /* NGTCP2_STRM_FLAG_STOP_SENDING indicates that STOP_SENDING is sent from the local endpoint. */ -#define NGTCP2_STRM_FLAG_STOP_SENDING 0x10u +#define NGTCP2_STRM_FLAG_STOP_SENDING 0x10U /* NGTCP2_STRM_FLAG_RESET_STREAM_ACKED indicates that the outgoing RESET_STREAM is acknowledged by peer. */ -#define NGTCP2_STRM_FLAG_RESET_STREAM_ACKED 0x20u +#define NGTCP2_STRM_FLAG_RESET_STREAM_ACKED 0x20U /* NGTCP2_STRM_FLAG_FIN_ACKED indicates that a STREAM with FIN bit set is acknowledged by a remote endpoint. */ -#define NGTCP2_STRM_FLAG_FIN_ACKED 0x40u +#define NGTCP2_STRM_FLAG_FIN_ACKED 0x40U /* NGTCP2_STRM_FLAG_ANY_ACKED indicates that any portion of stream data, including 0 length segment, is acknowledged. */ -#define NGTCP2_STRM_FLAG_ANY_ACKED 0x80u +#define NGTCP2_STRM_FLAG_ANY_ACKED 0x80U /* NGTCP2_STRM_FLAG_APP_ERROR_CODE_SET indicates that app_error_code field is set. This resolves the ambiguity that the initial app_error_code value 0 might be a proper application error code. In this case, without this flag, we are unable to distinguish assigned value from unassigned one. */ -#define NGTCP2_STRM_FLAG_APP_ERROR_CODE_SET 0x100u +#define NGTCP2_STRM_FLAG_APP_ERROR_CODE_SET 0x100U /* NGTCP2_STRM_FLAG_SEND_STOP_SENDING is set when STOP_SENDING frame should be sent. */ -#define NGTCP2_STRM_FLAG_SEND_STOP_SENDING 0x200u +#define NGTCP2_STRM_FLAG_SEND_STOP_SENDING 0x200U /* NGTCP2_STRM_FLAG_SEND_RESET_STREAM is set when RESET_STREAM frame should be sent. */ -#define NGTCP2_STRM_FLAG_SEND_RESET_STREAM 0x400u +#define NGTCP2_STRM_FLAG_SEND_RESET_STREAM 0x400U /* NGTCP2_STRM_FLAG_STOP_SENDING_RECVED indicates that STOP_SENDING is received from the remote endpoint. In this case, NGTCP2_STRM_FLAG_SHUT_WR is also set. */ -#define NGTCP2_STRM_FLAG_STOP_SENDING_RECVED 0x800u +#define NGTCP2_STRM_FLAG_STOP_SENDING_RECVED 0x800U /* NGTCP2_STRM_FLAG_ANY_SENT indicates that any STREAM frame, including empty one, has been sent. */ -#define NGTCP2_STRM_FLAG_ANY_SENT 0x1000u +#define NGTCP2_STRM_FLAG_ANY_SENT 0x1000U typedef struct ngtcp2_strm ngtcp2_strm; @@ -171,14 +171,14 @@ struct ngtcp2_strm { const ngtcp2_mem *mem; int64_t stream_id; void *stream_user_data; - /* flags is bit-wise OR of zero or more of NGTCP2_STRM_FLAG_*. */ - uint32_t flags; /* app_error_code is an error code the local endpoint sent in RESET_STREAM or STOP_SENDING, or received from a remote endpoint in RESET_STREAM or STOP_SENDING. First application error code is chosen and when set, NGTCP2_STRM_FLAG_APP_ERROR_CODE_SET flag is set in flags field. */ uint64_t app_error_code; + /* flags is bit-wise OR of zero or more of NGTCP2_STRM_FLAG_*. */ + uint32_t flags; }; ngtcp2_opl_entry oplent; diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_transport_params.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_transport_params.c index d652ece4e07a88..f28c4f57b15967 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_transport_params.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_transport_params.c @@ -150,8 +150,8 @@ ngtcp2_ssize ngtcp2_transport_params_encode_versioned( if (params->stateless_reset_token_present) { len += ngtcp2_put_uvarintlen(NGTCP2_TRANSPORT_PARAM_STATELESS_RESET_TOKEN) + - ngtcp2_put_uvarintlen(NGTCP2_STATELESS_RESET_TOKENLEN) + - NGTCP2_STATELESS_RESET_TOKENLEN; + ngtcp2_put_uvarintlen(sizeof(params->stateless_reset_token)) + + sizeof(params->stateless_reset_token); } if (params->preferred_addr_present) { @@ -160,7 +160,7 @@ ngtcp2_ssize ngtcp2_transport_params_encode_versioned( preferred_addrlen = 4 /* ipv4Address */ + 2 /* ipv4Port */ + 16 /* ipv6Address */ + 2 /* ipv6Port */ + 1 + params->preferred_addr.cid.datalen /* CID */ + - NGTCP2_STATELESS_RESET_TOKENLEN; + sizeof(params->preferred_addr.stateless_reset_token); len += ngtcp2_put_uvarintlen(NGTCP2_TRANSPORT_PARAM_PREFERRED_ADDRESS) + ngtcp2_put_uvarintlen(preferred_addrlen) + preferred_addrlen; } @@ -586,6 +586,9 @@ int ngtcp2_transport_params_decode_versioned(int transport_params_version, if (decode_varint_param(¶ms->max_idle_timeout, &p, end) != 0) { return NGTCP2_ERR_MALFORMED_TRANSPORT_PARAM; } + if (params->max_idle_timeout > UINT64_MAX / NGTCP2_MILLISECONDS) { + params->max_idle_timeout = UINT64_MAX; + } params->max_idle_timeout *= NGTCP2_MILLISECONDS; break; case NGTCP2_TRANSPORT_PARAM_MAX_UDP_PAYLOAD_SIZE: @@ -626,7 +629,8 @@ int ngtcp2_transport_params_decode_versioned(int transport_params_version, } len = 4 /* ipv4Address */ + 2 /* ipv4Port */ + 16 /* ipv6Address */ + 2 /* ipv6Port */ - + 1 /* cid length */ + NGTCP2_STATELESS_RESET_TOKENLEN; + + 1 /* cid length */ + + sizeof(params->preferred_addr.stateless_reset_token); if (valuelen < len) { return NGTCP2_ERR_MALFORMED_TRANSPORT_PARAM; } @@ -732,7 +736,7 @@ int ngtcp2_transport_params_decode_versioned(int transport_params_version, if ((size_t)(end - p) < valuelen) { return NGTCP2_ERR_MALFORMED_TRANSPORT_PARAM; } - if (valuelen < sizeof(uint32_t) || (valuelen & 0x3)) { + if (valuelen < sizeof(uint32_t) || (valuelen & 0x3U)) { return NGTCP2_ERR_MALFORMED_TRANSPORT_PARAM; } p = ngtcp2_get_uint32be(¶ms->version_info.chosen_version, p); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_transport_params.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_transport_params.h index c077f06a9dd717..0ea85edb0a4dc6 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_transport_params.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_transport_params.h @@ -35,28 +35,28 @@ parameter ID. */ typedef uint64_t ngtcp2_transport_param_id; -#define NGTCP2_TRANSPORT_PARAM_ORIGINAL_DESTINATION_CONNECTION_ID 0x00 -#define NGTCP2_TRANSPORT_PARAM_MAX_IDLE_TIMEOUT 0x01 -#define NGTCP2_TRANSPORT_PARAM_STATELESS_RESET_TOKEN 0x02 -#define NGTCP2_TRANSPORT_PARAM_MAX_UDP_PAYLOAD_SIZE 0x03 -#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_DATA 0x04 -#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL 0x05 -#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE 0x06 -#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_STREAM_DATA_UNI 0x07 -#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_STREAMS_BIDI 0x08 -#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_STREAMS_UNI 0x09 -#define NGTCP2_TRANSPORT_PARAM_ACK_DELAY_EXPONENT 0x0a -#define NGTCP2_TRANSPORT_PARAM_MAX_ACK_DELAY 0x0b -#define NGTCP2_TRANSPORT_PARAM_DISABLE_ACTIVE_MIGRATION 0x0c -#define NGTCP2_TRANSPORT_PARAM_PREFERRED_ADDRESS 0x0d -#define NGTCP2_TRANSPORT_PARAM_ACTIVE_CONNECTION_ID_LIMIT 0x0e -#define NGTCP2_TRANSPORT_PARAM_INITIAL_SOURCE_CONNECTION_ID 0x0f -#define NGTCP2_TRANSPORT_PARAM_RETRY_SOURCE_CONNECTION_ID 0x10 +#define NGTCP2_TRANSPORT_PARAM_ORIGINAL_DESTINATION_CONNECTION_ID 0x00U +#define NGTCP2_TRANSPORT_PARAM_MAX_IDLE_TIMEOUT 0x01U +#define NGTCP2_TRANSPORT_PARAM_STATELESS_RESET_TOKEN 0x02U +#define NGTCP2_TRANSPORT_PARAM_MAX_UDP_PAYLOAD_SIZE 0x03U +#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_DATA 0x04U +#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL 0x05U +#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE 0x06U +#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_STREAM_DATA_UNI 0x07U +#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_STREAMS_BIDI 0x08U +#define NGTCP2_TRANSPORT_PARAM_INITIAL_MAX_STREAMS_UNI 0x09U +#define NGTCP2_TRANSPORT_PARAM_ACK_DELAY_EXPONENT 0x0AU +#define NGTCP2_TRANSPORT_PARAM_MAX_ACK_DELAY 0x0BU +#define NGTCP2_TRANSPORT_PARAM_DISABLE_ACTIVE_MIGRATION 0x0CU +#define NGTCP2_TRANSPORT_PARAM_PREFERRED_ADDRESS 0x0DU +#define NGTCP2_TRANSPORT_PARAM_ACTIVE_CONNECTION_ID_LIMIT 0x0EU +#define NGTCP2_TRANSPORT_PARAM_INITIAL_SOURCE_CONNECTION_ID 0x0FU +#define NGTCP2_TRANSPORT_PARAM_RETRY_SOURCE_CONNECTION_ID 0x10U /* https://datatracker.ietf.org/doc/html/rfc9221 */ -#define NGTCP2_TRANSPORT_PARAM_MAX_DATAGRAM_FRAME_SIZE 0x20 -#define NGTCP2_TRANSPORT_PARAM_GREASE_QUIC_BIT 0x2ab2 +#define NGTCP2_TRANSPORT_PARAM_MAX_DATAGRAM_FRAME_SIZE 0x20U +#define NGTCP2_TRANSPORT_PARAM_GREASE_QUIC_BIT 0x2AB2U /* https://datatracker.ietf.org/doc/html/rfc9368 */ -#define NGTCP2_TRANSPORT_PARAM_VERSION_INFORMATION 0x11 +#define NGTCP2_TRANSPORT_PARAM_VERSION_INFORMATION 0x11U /* NGTCP2_MAX_STREAMS is the maximum number of streams. */ #define NGTCP2_MAX_STREAMS (1LL << 60) diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.c index ada027b9095f4e..f94c6218d53d41 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.c @@ -30,8 +30,11 @@ #include "ngtcp2_str.h" ngtcp2_vec *ngtcp2_vec_init(ngtcp2_vec *vec, const uint8_t *base, size_t len) { - vec->base = (uint8_t *)base; - vec->len = len; + *vec = (ngtcp2_vec){ + .base = (uint8_t *)base, + .len = len, + }; + return vec; } @@ -102,8 +105,7 @@ ngtcp2_ssize ngtcp2_vec_split(ngtcp2_vec *dst, size_t *pdstcnt, ngtcp2_vec *src, memcpy(dst, src + i, sizeof(ngtcp2_vec) * nmove); } - dst[0].len -= left; - dst[0].base += left; + ngtcp2_vec_drop(&dst[0], left); src[i].len = left; if (nmove == 0) { @@ -136,8 +138,7 @@ size_t ngtcp2_vec_merge(ngtcp2_vec *dst, size_t *pdstcnt, ngtcp2_vec *src, a->len = left; a->base = b->base; - b->len -= left; - b->base += left; + ngtcp2_vec_drop(b, left); return left; } @@ -158,13 +159,14 @@ size_t ngtcp2_vec_merge(ngtcp2_vec *dst, size_t *pdstcnt, ngtcp2_vec *src, } else if (*pdstcnt == maxcnt) { break; } else { - dst[*pdstcnt].len = left; - dst[*pdstcnt].base = b->base; + dst[*pdstcnt] = (ngtcp2_vec){ + .base = b->base, + .len = left, + }; ++*pdstcnt; } - b->len -= left; - b->base += left; + ngtcp2_vec_drop(b, left); left = 0; break; @@ -199,8 +201,10 @@ size_t ngtcp2_vec_copy_at_most(ngtcp2_vec *dst, size_t dstcnt, } if (src[i].len > left) { - dst[j].base = src[i].base; - dst[j].len = left; + dst[j] = (ngtcp2_vec){ + .base = src[i].base, + .len = left, + }; return j + 1; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.h index af9b4d644539cf..fe6a47a029ad8d 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.h @@ -110,4 +110,14 @@ static inline uint8_t *ngtcp2_vec_end(const ngtcp2_vec *v) { return v->base + v->len; } +/* + * ngtcp2_vec_drop removes the first |n| bytes from |v| by adjusting + * its base and len fields. This function assumes |v|->len > 0 && + * |v|->len >= n. + */ +static inline void ngtcp2_vec_drop(ngtcp2_vec *v, size_t n) { + v->base += n; + v->len -= n; +} + #endif /* !defined(NGTCP2_VEC_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_window_filter.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_window_filter.c index 39f3d408a741ba..707cd570799e46 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_window_filter.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_window_filter.c @@ -39,7 +39,7 @@ void ngtcp2_window_filter_init(ngtcp2_window_filter *wf, uint64_t window_length) { wf->window_length = window_length; - memset(wf->estimates, 0xff, sizeof(wf->estimates)); + memset(wf->estimates, 0xFF, sizeof(wf->estimates)); } void ngtcp2_window_filter_update(ngtcp2_window_filter *wf, uint64_t new_sample, diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/.mailmap b/deps/ngtcp2/ngtcp2/third-party/http-parser/.mailmap new file mode 100644 index 00000000000000..278d1412637240 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/.mailmap @@ -0,0 +1,8 @@ +# update AUTHORS with: +# git log --all --reverse --format='%aN <%aE>' | perl -ne 'BEGIN{print "# Authors ordered by first contribution.\n"} print unless $h{$_}; $h{$_} = 1' > AUTHORS +Ryan Dahl +Salman Haq +Simon Zimmermann +Thomas LE ROUX LE ROUX Thomas +Thomas LE ROUX Thomas LE ROUX +Fedor Indutny diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/.travis.yml b/deps/ngtcp2/ngtcp2/third-party/http-parser/.travis.yml new file mode 100644 index 00000000000000..4b038e6e62d638 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/.travis.yml @@ -0,0 +1,13 @@ +language: c + +compiler: + - clang + - gcc + +script: + - "make" + +notifications: + email: false + irc: + - "irc.freenode.net#node-ci" diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/AUTHORS b/deps/ngtcp2/ngtcp2/third-party/http-parser/AUTHORS new file mode 100644 index 00000000000000..5323b685caefb9 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/AUTHORS @@ -0,0 +1,68 @@ +# Authors ordered by first contribution. +Ryan Dahl +Jeremy Hinegardner +Sergey Shepelev +Joe Damato +tomika +Phoenix Sol +Cliff Frey +Ewen Cheslack-Postava +Santiago Gala +Tim Becker +Jeff Terrace +Ben Noordhuis +Nathan Rajlich +Mark Nottingham +Aman Gupta +Tim Becker +Sean Cunningham +Peter Griess +Salman Haq +Cliff Frey +Jon Kolb +Fouad Mardini +Paul Querna +Felix Geisendörfer +koichik +Andre Caron +Ivo Raisr +James McLaughlin +David Gwynne +Thomas LE ROUX +Randy Rizun +Andre Louis Caron +Simon Zimmermann +Erik Dubbelboer +Martell Malone +Bertrand Paquet +BogDan Vatra +Peter Faiman +Corey Richardson +Tóth Tamás +Cam Swords +Chris Dickinson +Uli Köhler +Charlie Somerville +Patrik Stutz +Fedor Indutny +runner +Alexis Campailla +David Wragg +Vinnie Falco +Alex Butum +Rex Feng +Alex Kocharin +Mark Koopman +Helge Heß +Alexis La Goutte +George Miroshnykov +Maciej Małecki +Marc O'Morain +Jeff Pinner +Timothy J Fontaine +Akagi201 +Romain Giraud +Jay Satiro +Arne Steen +Kjell Schubert +Olivier Mengué diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/LICENSE-MIT b/deps/ngtcp2/ngtcp2/third-party/http-parser/LICENSE-MIT new file mode 100644 index 00000000000000..58010b388945f9 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/LICENSE-MIT @@ -0,0 +1,23 @@ +http_parser.c is based on src/http/ngx_http_parse.c from NGINX copyright +Igor Sysoev. + +Additional changes are licensed under the same terms as NGINX and +copyright Joyent, Inc. and other Node contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/README.md b/deps/ngtcp2/ngtcp2/third-party/http-parser/README.md new file mode 100644 index 00000000000000..439b30998d43dd --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/README.md @@ -0,0 +1,246 @@ +HTTP Parser +=========== + +[![Build Status](https://api.travis-ci.org/nodejs/http-parser.svg?branch=master)](https://travis-ci.org/nodejs/http-parser) + +This is a parser for HTTP messages written in C. It parses both requests and +responses. The parser is designed to be used in performance HTTP +applications. It does not make any syscalls nor allocations, it does not +buffer data, it can be interrupted at anytime. Depending on your +architecture, it only requires about 40 bytes of data per message +stream (in a web server that is per connection). + +Features: + + * No dependencies + * Handles persistent streams (keep-alive). + * Decodes chunked encoding. + * Upgrade support + * Defends against buffer overflow attacks. + +The parser extracts the following information from HTTP messages: + + * Header fields and values + * Content-Length + * Request method + * Response status code + * Transfer-Encoding + * HTTP version + * Request URL + * Message body + + +Usage +----- + +One `http_parser` object is used per TCP connection. Initialize the struct +using `http_parser_init()` and set the callbacks. That might look something +like this for a request parser: +```c +http_parser_settings settings; +settings.on_url = my_url_callback; +settings.on_header_field = my_header_field_callback; +/* ... */ + +http_parser *parser = malloc(sizeof(http_parser)); +http_parser_init(parser, HTTP_REQUEST); +parser->data = my_socket; +``` + +When data is received on the socket execute the parser and check for errors. + +```c +size_t len = 80*1024, nparsed; +char buf[len]; +ssize_t recved; + +recved = recv(fd, buf, len, 0); + +if (recved < 0) { + /* Handle error. */ +} + +/* Start up / continue the parser. + * Note we pass recved==0 to signal that EOF has been received. + */ +nparsed = http_parser_execute(parser, &settings, buf, recved); + +if (parser->upgrade) { + /* handle new protocol */ +} else if (nparsed != recved) { + /* Handle error. Usually just close the connection. */ +} +``` + +HTTP needs to know where the end of the stream is. For example, sometimes +servers send responses without Content-Length and expect the client to +consume input (for the body) until EOF. To tell http_parser about EOF, give +`0` as the fourth parameter to `http_parser_execute()`. Callbacks and errors +can still be encountered during an EOF, so one must still be prepared +to receive them. + +Scalar valued message information such as `status_code`, `method`, and the +HTTP version are stored in the parser structure. This data is only +temporally stored in `http_parser` and gets reset on each new message. If +this information is needed later, copy it out of the structure during the +`headers_complete` callback. + +The parser decodes the transfer-encoding for both requests and responses +transparently. That is, a chunked encoding is decoded before being sent to +the on_body callback. + + +The Special Problem of Upgrade +------------------------------ + +HTTP supports upgrading the connection to a different protocol. An +increasingly common example of this is the WebSocket protocol which sends +a request like + + GET /demo HTTP/1.1 + Upgrade: WebSocket + Connection: Upgrade + Host: example.com + Origin: http://example.com + WebSocket-Protocol: sample + +followed by non-HTTP data. + +(See [RFC6455](https://tools.ietf.org/html/rfc6455) for more information the +WebSocket protocol.) + +To support this, the parser will treat this as a normal HTTP message without a +body, issuing both on_headers_complete and on_message_complete callbacks. However +http_parser_execute() will stop parsing at the end of the headers and return. + +The user is expected to check if `parser->upgrade` has been set to 1 after +`http_parser_execute()` returns. Non-HTTP data begins at the buffer supplied +offset by the return value of `http_parser_execute()`. + + +Callbacks +--------- + +During the `http_parser_execute()` call, the callbacks set in +`http_parser_settings` will be executed. The parser maintains state and +never looks behind, so buffering the data is not necessary. If you need to +save certain data for later usage, you can do that from the callbacks. + +There are two types of callbacks: + +* notification `typedef int (*http_cb) (http_parser*);` + Callbacks: on_message_begin, on_headers_complete, on_message_complete. +* data `typedef int (*http_data_cb) (http_parser*, const char *at, size_t length);` + Callbacks: (requests only) on_url, + (common) on_header_field, on_header_value, on_body; + +Callbacks must return 0 on success. Returning a non-zero value indicates +error to the parser, making it exit immediately. + +For cases where it is necessary to pass local information to/from a callback, +the `http_parser` object's `data` field can be used. +An example of such a case is when using threads to handle a socket connection, +parse a request, and then give a response over that socket. By instantiation +of a thread-local struct containing relevant data (e.g. accepted socket, +allocated memory for callbacks to write into, etc), a parser's callbacks are +able to communicate data between the scope of the thread and the scope of the +callback in a threadsafe manner. This allows http-parser to be used in +multi-threaded contexts. + +Example: +```c + typedef struct { + socket_t sock; + void* buffer; + int buf_len; + } custom_data_t; + + +int my_url_callback(http_parser* parser, const char *at, size_t length) { + /* access to thread local custom_data_t struct. + Use this access save parsed data for later use into thread local + buffer, or communicate over socket + */ + parser->data; + ... + return 0; +} + +... + +void http_parser_thread(socket_t sock) { + int nparsed = 0; + /* allocate memory for user data */ + custom_data_t *my_data = malloc(sizeof(custom_data_t)); + + /* some information for use by callbacks. + * achieves thread -> callback information flow */ + my_data->sock = sock; + + /* instantiate a thread-local parser */ + http_parser *parser = malloc(sizeof(http_parser)); + http_parser_init(parser, HTTP_REQUEST); /* initialise parser */ + /* this custom data reference is accessible through the reference to the + parser supplied to callback functions */ + parser->data = my_data; + + http_parser_settings settings; /* set up callbacks */ + settings.on_url = my_url_callback; + + /* execute parser */ + nparsed = http_parser_execute(parser, &settings, buf, recved); + + ... + /* parsed information copied from callback. + can now perform action on data copied into thread-local memory from callbacks. + achieves callback -> thread information flow */ + my_data->buffer; + ... +} + +``` + +In case you parse HTTP message in chunks (i.e. `read()` request line +from socket, parse, read half headers, parse, etc) your data callbacks +may be called more than once. Http-parser guarantees that data pointer is only +valid for the lifetime of callback. You can also `read()` into a heap allocated +buffer to avoid copying memory around if this fits your application. + +Reading headers may be a tricky task if you read/parse headers partially. +Basically, you need to remember whether last header callback was field or value +and apply the following logic: + + (on_header_field and on_header_value shortened to on_h_*) + ------------------------ ------------ -------------------------------------------- + | State (prev. callback) | Callback | Description/action | + ------------------------ ------------ -------------------------------------------- + | nothing (first call) | on_h_field | Allocate new buffer and copy callback data | + | | | into it | + ------------------------ ------------ -------------------------------------------- + | value | on_h_field | New header started. | + | | | Copy current name,value buffers to headers | + | | | list and allocate new buffer for new name | + ------------------------ ------------ -------------------------------------------- + | field | on_h_field | Previous name continues. Reallocate name | + | | | buffer and append callback data to it | + ------------------------ ------------ -------------------------------------------- + | field | on_h_value | Value for current header started. Allocate | + | | | new buffer and copy callback data to it | + ------------------------ ------------ -------------------------------------------- + | value | on_h_value | Value continues. Reallocate value buffer | + | | | and append callback data to it | + ------------------------ ------------ -------------------------------------------- + + +Parsing URLs +------------ + +A simplistic zero-copy URL parser is provided as `http_parser_parse_url()`. +Users of this library may wish to use it to parse URLs constructed from +consecutive `on_url` callbacks. + +See examples of reading in headers: + +* [partial example](http://gist.github.com/155877) in C +* [from http-parser tests](http://github.com/joyent/http-parser/blob/37a0ff8/test.c#L403) in C +* [from Node library](http://github.com/joyent/node/blob/842eaf4/src/http.js#L284) in Javascript diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/bench.c b/deps/ngtcp2/ngtcp2/third-party/http-parser/bench.c new file mode 100644 index 00000000000000..5b452fa1cdb6e6 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/bench.c @@ -0,0 +1,111 @@ +/* Copyright Fedor Indutny. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include "http_parser.h" +#include +#include +#include +#include + +static const char data[] = + "POST /joyent/http-parser HTTP/1.1\r\n" + "Host: github.com\r\n" + "DNT: 1\r\n" + "Accept-Encoding: gzip, deflate, sdch\r\n" + "Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4\r\n" + "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/39.0.2171.65 Safari/537.36\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9," + "image/webp,*/*;q=0.8\r\n" + "Referer: https://github.com/joyent/http-parser\r\n" + "Connection: keep-alive\r\n" + "Transfer-Encoding: chunked\r\n" + "Cache-Control: max-age=0\r\n\r\nb\r\nhello world\r\n0\r\n\r\n"; +static const size_t data_len = sizeof(data) - 1; + +static int on_info(http_parser* p) { + return 0; +} + + +static int on_data(http_parser* p, const char *at, size_t length) { + return 0; +} + +static http_parser_settings settings = { + .on_message_begin = on_info, + .on_headers_complete = on_info, + .on_message_complete = on_info, + .on_header_field = on_data, + .on_header_value = on_data, + .on_url = on_data, + .on_status = on_data, + .on_body = on_data +}; + +int bench(int iter_count, int silent) { + struct http_parser parser; + int i; + int err; + struct timeval start; + struct timeval end; + float rps; + + if (!silent) { + err = gettimeofday(&start, NULL); + assert(err == 0); + } + + for (i = 0; i < iter_count; i++) { + size_t parsed; + http_parser_init(&parser, HTTP_REQUEST); + + parsed = http_parser_execute(&parser, &settings, data, data_len); + assert(parsed == data_len); + } + + if (!silent) { + err = gettimeofday(&end, NULL); + assert(err == 0); + + fprintf(stdout, "Benchmark result:\n"); + + rps = (float) (end.tv_sec - start.tv_sec) + + (end.tv_usec - start.tv_usec) * 1e-6f; + fprintf(stdout, "Took %f seconds to run\n", rps); + + rps = (float) iter_count / rps; + fprintf(stdout, "%f req/sec\n", rps); + fflush(stdout); + } + + return 0; +} + +int main(int argc, char** argv) { + if (argc == 2 && strcmp(argv[1], "infinite") == 0) { + for (;;) + bench(5000000, 1); + return 0; + } else { + return bench(5000000, 0); + } +} diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/contrib/parsertrace.c b/deps/ngtcp2/ngtcp2/third-party/http-parser/contrib/parsertrace.c new file mode 100644 index 00000000000000..e7153680f467de --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/contrib/parsertrace.c @@ -0,0 +1,160 @@ +/* Based on src/http/ngx_http_parse.c from NGINX copyright Igor Sysoev + * + * Additional changes are licensed under the same terms as NGINX and + * copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* Dump what the parser finds to stdout as it happen */ + +#include "http_parser.h" +#include +#include +#include + +int on_message_begin(http_parser* _) { + (void)_; + printf("\n***MESSAGE BEGIN***\n\n"); + return 0; +} + +int on_headers_complete(http_parser* _) { + (void)_; + printf("\n***HEADERS COMPLETE***\n\n"); + return 0; +} + +int on_message_complete(http_parser* _) { + (void)_; + printf("\n***MESSAGE COMPLETE***\n\n"); + return 0; +} + +int on_url(http_parser* _, const char* at, size_t length) { + (void)_; + printf("Url: %.*s\n", (int)length, at); + return 0; +} + +int on_header_field(http_parser* _, const char* at, size_t length) { + (void)_; + printf("Header field: %.*s\n", (int)length, at); + return 0; +} + +int on_header_value(http_parser* _, const char* at, size_t length) { + (void)_; + printf("Header value: %.*s\n", (int)length, at); + return 0; +} + +int on_body(http_parser* _, const char* at, size_t length) { + (void)_; + printf("Body: %.*s\n", (int)length, at); + return 0; +} + +void usage(const char* name) { + fprintf(stderr, + "Usage: %s $type $filename\n" + " type: -x, where x is one of {r,b,q}\n" + " parses file as a Response, reQuest, or Both\n", + name); + exit(EXIT_FAILURE); +} + +int main(int argc, char* argv[]) { + enum http_parser_type file_type; + + if (argc != 3) { + usage(argv[0]); + } + + char* type = argv[1]; + if (type[0] != '-') { + usage(argv[0]); + } + + switch (type[1]) { + /* in the case of "-", type[1] will be NUL */ + case 'r': + file_type = HTTP_RESPONSE; + break; + case 'q': + file_type = HTTP_REQUEST; + break; + case 'b': + file_type = HTTP_BOTH; + break; + default: + usage(argv[0]); + } + + char* filename = argv[2]; + FILE* file = fopen(filename, "r"); + if (file == NULL) { + perror("fopen"); + goto fail; + } + + fseek(file, 0, SEEK_END); + long file_length = ftell(file); + if (file_length == -1) { + perror("ftell"); + goto fail; + } + fseek(file, 0, SEEK_SET); + + char* data = malloc(file_length); + if (fread(data, 1, file_length, file) != (size_t)file_length) { + fprintf(stderr, "couldn't read entire file\n"); + free(data); + goto fail; + } + + http_parser_settings settings; + memset(&settings, 0, sizeof(settings)); + settings.on_message_begin = on_message_begin; + settings.on_url = on_url; + settings.on_header_field = on_header_field; + settings.on_header_value = on_header_value; + settings.on_headers_complete = on_headers_complete; + settings.on_body = on_body; + settings.on_message_complete = on_message_complete; + + http_parser parser; + http_parser_init(&parser, file_type); + size_t nparsed = http_parser_execute(&parser, &settings, data, file_length); + free(data); + + if (nparsed != (size_t)file_length) { + fprintf(stderr, + "Error: %s (%s)\n", + http_errno_description(HTTP_PARSER_ERRNO(&parser)), + http_errno_name(HTTP_PARSER_ERRNO(&parser))); + goto fail; + } + + return EXIT_SUCCESS; + +fail: + fclose(file); + return EXIT_FAILURE; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/contrib/url_parser.c b/deps/ngtcp2/ngtcp2/third-party/http-parser/contrib/url_parser.c new file mode 100644 index 00000000000000..f235bed9e483dc --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/contrib/url_parser.c @@ -0,0 +1,47 @@ +#include "http_parser.h" +#include +#include + +void +dump_url (const char *url, const struct http_parser_url *u) +{ + unsigned int i; + + printf("\tfield_set: 0x%x, port: %u\n", u->field_set, u->port); + for (i = 0; i < UF_MAX; i++) { + if ((u->field_set & (1 << i)) == 0) { + printf("\tfield_data[%u]: unset\n", i); + continue; + } + + printf("\tfield_data[%u]: off: %u, len: %u, part: %.*s\n", + i, + u->field_data[i].off, + u->field_data[i].len, + u->field_data[i].len, + url + u->field_data[i].off); + } +} + +int main(int argc, char ** argv) { + struct http_parser_url u; + int len, connect, result; + + if (argc != 3) { + printf("Syntax : %s connect|get url\n", argv[0]); + return 1; + } + len = strlen(argv[2]); + connect = strcmp("connect", argv[1]) == 0 ? 1 : 0; + printf("Parsing %s, connect %d\n", argv[2], connect); + + http_parser_url_init(&u); + result = http_parser_parse_url(argv[2], len, connect, &u); + if (result != 0) { + printf("Parse error : %d\n", result); + return result; + } + printf("Parse ok, result : \n"); + dump_url(argv[2], &u); + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/http_parser.c b/deps/ngtcp2/ngtcp2/third-party/http-parser/http_parser.c new file mode 100644 index 00000000000000..0cc16cb69423ee --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/http_parser.c @@ -0,0 +1,2419 @@ +/* Based on src/http/ngx_http_parse.c from NGINX copyright Igor Sysoev + * + * Additional changes are licensed under the same terms as NGINX and + * copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include "http_parser.h" +#include +#include +#include +#include +#include +#include + +#ifndef ULLONG_MAX +# define ULLONG_MAX ((uint64_t) -1) /* 2^64-1 */ +#endif + +#ifndef MIN +# define MIN(a,b) ((a) < (b) ? (a) : (b)) +#endif + +#ifndef ARRAY_SIZE +# define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#endif + +#ifndef BIT_AT +# define BIT_AT(a, i) \ + (!!((unsigned int) (a)[(unsigned int) (i) >> 3] & \ + (1 << ((unsigned int) (i) & 7)))) +#endif + +#ifndef ELEM_AT +# define ELEM_AT(a, i, v) ((unsigned int) (i) < ARRAY_SIZE(a) ? (a)[(i)] : (v)) +#endif + +#define SET_ERRNO(e) \ +do { \ + parser->http_errno = (e); \ +} while(0) + +#define CURRENT_STATE() p_state +#define UPDATE_STATE(V) p_state = (enum state) (V); +#define RETURN(V) \ +do { \ + parser->state = CURRENT_STATE(); \ + return (V); \ +} while (0); +#define REEXECUTE() \ + goto reexecute; \ + + +#ifdef __GNUC__ +# define LIKELY(X) __builtin_expect(!!(X), 1) +# define UNLIKELY(X) __builtin_expect(!!(X), 0) +#else +# define LIKELY(X) (X) +# define UNLIKELY(X) (X) +#endif + + +/* Run the notify callback FOR, returning ER if it fails */ +#define CALLBACK_NOTIFY_(FOR, ER) \ +do { \ + assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ + \ + if (LIKELY(settings->on_##FOR)) { \ + parser->state = CURRENT_STATE(); \ + if (UNLIKELY(0 != settings->on_##FOR(parser))) { \ + SET_ERRNO(HPE_CB_##FOR); \ + } \ + UPDATE_STATE(parser->state); \ + \ + /* We either errored above or got paused; get out */ \ + if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \ + return (ER); \ + } \ + } \ +} while (0) + +/* Run the notify callback FOR and consume the current byte */ +#define CALLBACK_NOTIFY(FOR) CALLBACK_NOTIFY_(FOR, p - data + 1) + +/* Run the notify callback FOR and don't consume the current byte */ +#define CALLBACK_NOTIFY_NOADVANCE(FOR) CALLBACK_NOTIFY_(FOR, p - data) + +/* Run data callback FOR with LEN bytes, returning ER if it fails */ +#define CALLBACK_DATA_(FOR, LEN, ER) \ +do { \ + assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ + \ + if (FOR##_mark) { \ + if (LIKELY(settings->on_##FOR)) { \ + parser->state = CURRENT_STATE(); \ + if (UNLIKELY(0 != \ + settings->on_##FOR(parser, FOR##_mark, (LEN)))) { \ + SET_ERRNO(HPE_CB_##FOR); \ + } \ + UPDATE_STATE(parser->state); \ + \ + /* We either errored above or got paused; get out */ \ + if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \ + return (ER); \ + } \ + } \ + FOR##_mark = NULL; \ + } \ +} while (0) + +/* Run the data callback FOR and consume the current byte */ +#define CALLBACK_DATA(FOR) \ + CALLBACK_DATA_(FOR, p - FOR##_mark, p - data + 1) + +/* Run the data callback FOR and don't consume the current byte */ +#define CALLBACK_DATA_NOADVANCE(FOR) \ + CALLBACK_DATA_(FOR, p - FOR##_mark, p - data) + +/* Set the mark FOR; non-destructive if mark is already set */ +#define MARK(FOR) \ +do { \ + if (!FOR##_mark) { \ + FOR##_mark = p; \ + } \ +} while (0) + +/* Don't allow the total size of the HTTP headers (including the status + * line) to exceed HTTP_MAX_HEADER_SIZE. This check is here to protect + * embedders against denial-of-service attacks where the attacker feeds + * us a never-ending header that the embedder keeps buffering. + * + * This check is arguably the responsibility of embedders but we're doing + * it on the embedder's behalf because most won't bother and this way we + * make the web a little safer. HTTP_MAX_HEADER_SIZE is still far bigger + * than any reasonable request or response so this should never affect + * day-to-day operation. + */ +#define COUNT_HEADER_SIZE(V) \ +do { \ + parser->nread += (V); \ + if (UNLIKELY(parser->nread > (HTTP_MAX_HEADER_SIZE))) { \ + SET_ERRNO(HPE_HEADER_OVERFLOW); \ + goto error; \ + } \ +} while (0) + + +#define PROXY_CONNECTION "proxy-connection" +#define CONNECTION "connection" +#define CONTENT_LENGTH "content-length" +#define TRANSFER_ENCODING "transfer-encoding" +#define UPGRADE "upgrade" +#define CHUNKED "chunked" +#define KEEP_ALIVE "keep-alive" +#define CLOSE "close" + + +static const char *method_strings[] = + { +#define XX(num, name, string) #string, + HTTP_METHOD_MAP(XX) +#undef XX + }; + + +/* Tokens as defined by rfc 2616. Also lowercases them. + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + */ +static const char tokens[256] = { +/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ + 0, '!', 0, '#', '$', '%', '&', '\'', +/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ + 0, 0, '*', '+', 0, '-', '.', 0, +/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ + '0', '1', '2', '3', '4', '5', '6', '7', +/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ + '8', '9', 0, 0, 0, 0, 0, 0, +/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ + 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', +/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', +/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', +/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ + 'x', 'y', 'z', 0, 0, 0, '^', '_', +/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ + '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', +/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', +/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', +/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ + 'x', 'y', 'z', 0, '|', 0, '~', 0 }; + + +static const int8_t unhex[256] = + {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + , 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1,-1,-1,-1,-1 + ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + }; + + +#if HTTP_PARSER_STRICT +# define T(v) 0 +#else +# define T(v) v +#endif + + +static const uint8_t normal_url_char[32] = { +/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ + 0 | T(2) | 0 | 0 | T(16) | 0 | 0 | 0, +/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ + 0 | 2 | 4 | 0 | 16 | 32 | 64 | 128, +/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, +/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, }; + +#undef T + +enum state + { s_dead = 1 /* important that this is > 0 */ + + , s_start_req_or_res + , s_res_or_resp_H + , s_start_res + , s_res_H + , s_res_HT + , s_res_HTT + , s_res_HTTP + , s_res_http_major + , s_res_http_dot + , s_res_http_minor + , s_res_http_end + , s_res_first_status_code + , s_res_status_code + , s_res_status_start + , s_res_status + , s_res_line_almost_done + + , s_start_req + + , s_req_method + , s_req_spaces_before_url + , s_req_schema + , s_req_schema_slash + , s_req_schema_slash_slash + , s_req_server_start + , s_req_server + , s_req_server_with_at + , s_req_path + , s_req_query_string_start + , s_req_query_string + , s_req_fragment_start + , s_req_fragment + , s_req_http_start + , s_req_http_H + , s_req_http_HT + , s_req_http_HTT + , s_req_http_HTTP + , s_req_http_major + , s_req_http_dot + , s_req_http_minor + , s_req_http_end + , s_req_line_almost_done + + , s_header_field_start + , s_header_field + , s_header_value_discard_ws + , s_header_value_discard_ws_almost_done + , s_header_value_discard_lws + , s_header_value_start + , s_header_value + , s_header_value_lws + + , s_header_almost_done + + , s_chunk_size_start + , s_chunk_size + , s_chunk_parameters + , s_chunk_size_almost_done + + , s_headers_almost_done + , s_headers_done + + /* Important: 's_headers_done' must be the last 'header' state. All + * states beyond this must be 'body' states. It is used for overflow + * checking. See the PARSING_HEADER() macro. + */ + + , s_chunk_data + , s_chunk_data_almost_done + , s_chunk_data_done + + , s_body_identity + , s_body_identity_eof + + , s_message_done + }; + + +#define PARSING_HEADER(state) (state <= s_headers_done) + + +enum header_states + { h_general = 0 + , h_C + , h_CO + , h_CON + + , h_matching_connection + , h_matching_proxy_connection + , h_matching_content_length + , h_matching_transfer_encoding + , h_matching_upgrade + + , h_connection + , h_content_length + , h_transfer_encoding + , h_upgrade + + , h_matching_transfer_encoding_chunked + , h_matching_connection_token_start + , h_matching_connection_keep_alive + , h_matching_connection_close + , h_matching_connection_upgrade + , h_matching_connection_token + + , h_transfer_encoding_chunked + , h_connection_keep_alive + , h_connection_close + , h_connection_upgrade + }; + +enum http_host_state + { + s_http_host_dead = 1 + , s_http_userinfo_start + , s_http_userinfo + , s_http_host_start + , s_http_host_v6_start + , s_http_host + , s_http_host_v6 + , s_http_host_v6_end + , s_http_host_v6_zone_start + , s_http_host_v6_zone + , s_http_host_port_start + , s_http_host_port +}; + +/* Macros for character classes; depends on strict-mode */ +#define CR '\r' +#define LF '\n' +#define LOWER(c) (unsigned char)(c | 0x20) +#define IS_ALPHA(c) (LOWER(c) >= 'a' && LOWER(c) <= 'z') +#define IS_NUM(c) ((c) >= '0' && (c) <= '9') +#define IS_ALPHANUM(c) (IS_ALPHA(c) || IS_NUM(c)) +#define IS_HEX(c) (IS_NUM(c) || (LOWER(c) >= 'a' && LOWER(c) <= 'f')) +#define IS_MARK(c) ((c) == '-' || (c) == '_' || (c) == '.' || \ + (c) == '!' || (c) == '~' || (c) == '*' || (c) == '\'' || (c) == '(' || \ + (c) == ')') +#define IS_USERINFO_CHAR(c) (IS_ALPHANUM(c) || IS_MARK(c) || (c) == '%' || \ + (c) == ';' || (c) == ':' || (c) == '&' || (c) == '=' || (c) == '+' || \ + (c) == '$' || (c) == ',') + +#define STRICT_TOKEN(c) (tokens[(unsigned char)c]) + +#if HTTP_PARSER_STRICT +#define TOKEN(c) (tokens[(unsigned char)c]) +#define IS_URL_CHAR(c) (BIT_AT(normal_url_char, (unsigned char)c)) +#define IS_HOST_CHAR(c) (IS_ALPHANUM(c) || (c) == '.' || (c) == '-') +#else +#define TOKEN(c) ((c == ' ') ? ' ' : tokens[(unsigned char)c]) +#define IS_URL_CHAR(c) \ + (BIT_AT(normal_url_char, (unsigned char)c) || ((c) & 0x80)) +#define IS_HOST_CHAR(c) \ + (IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_') +#endif + +/** + * Verify that a char is a valid visible (printable) US-ASCII + * character or %x80-FF + **/ +#define IS_HEADER_CHAR(ch) \ + (ch == CR || ch == LF || ch == 9 || ((unsigned char)ch > 31 && ch != 127)) + +#define start_state (parser->type == HTTP_REQUEST ? s_start_req : s_start_res) + + +#if HTTP_PARSER_STRICT +# define STRICT_CHECK(cond) \ +do { \ + if (cond) { \ + SET_ERRNO(HPE_STRICT); \ + goto error; \ + } \ +} while (0) +# define NEW_MESSAGE() (http_should_keep_alive(parser) ? start_state : s_dead) +#else +# define STRICT_CHECK(cond) +# define NEW_MESSAGE() start_state +#endif + + +/* Map errno values to strings for human-readable output */ +#define HTTP_STRERROR_GEN(n, s) { "HPE_" #n, s }, +static struct { + const char *name; + const char *description; +} http_strerror_tab[] = { + HTTP_ERRNO_MAP(HTTP_STRERROR_GEN) +}; +#undef HTTP_STRERROR_GEN + +int http_message_needs_eof(const http_parser *parser); + +/* Our URL parser. + * + * This is designed to be shared by http_parser_execute() for URL validation, + * hence it has a state transition + byte-for-byte interface. In addition, it + * is meant to be embedded in http_parser_parse_url(), which does the dirty + * work of turning state transitions URL components for its API. + * + * This function should only be invoked with non-space characters. It is + * assumed that the caller cares about (and can detect) the transition between + * URL and non-URL states by looking for these. + */ +static enum state +parse_url_char(enum state s, const char ch) +{ + if (ch == ' ' || ch == '\r' || ch == '\n') { + return s_dead; + } + +#if HTTP_PARSER_STRICT + if (ch == '\t' || ch == '\f') { + return s_dead; + } +#endif + + switch (s) { + case s_req_spaces_before_url: + /* Proxied requests are followed by scheme of an absolute URI (alpha). + * All methods except CONNECT are followed by '/' or '*'. + */ + + if (ch == '/' || ch == '*') { + return s_req_path; + } + + if (IS_ALPHA(ch)) { + return s_req_schema; + } + + break; + + case s_req_schema: + if (IS_ALPHA(ch)) { + return s; + } + + if (ch == ':') { + return s_req_schema_slash; + } + + break; + + case s_req_schema_slash: + if (ch == '/') { + return s_req_schema_slash_slash; + } + + break; + + case s_req_schema_slash_slash: + if (ch == '/') { + return s_req_server_start; + } + + break; + + case s_req_server_with_at: + if (ch == '@') { + return s_dead; + } + + /* FALLTHROUGH */ + case s_req_server_start: + case s_req_server: + if (ch == '/') { + return s_req_path; + } + + if (ch == '?') { + return s_req_query_string_start; + } + + if (ch == '@') { + return s_req_server_with_at; + } + + if (IS_USERINFO_CHAR(ch) || ch == '[' || ch == ']') { + return s_req_server; + } + + break; + + case s_req_path: + if (IS_URL_CHAR(ch)) { + return s; + } + + switch (ch) { + case '?': + return s_req_query_string_start; + + case '#': + return s_req_fragment_start; + } + + break; + + case s_req_query_string_start: + case s_req_query_string: + if (IS_URL_CHAR(ch)) { + return s_req_query_string; + } + + switch (ch) { + case '?': + /* allow extra '?' in query string */ + return s_req_query_string; + + case '#': + return s_req_fragment_start; + } + + break; + + case s_req_fragment_start: + if (IS_URL_CHAR(ch)) { + return s_req_fragment; + } + + switch (ch) { + case '?': + return s_req_fragment; + + case '#': + return s; + } + + break; + + case s_req_fragment: + if (IS_URL_CHAR(ch)) { + return s; + } + + switch (ch) { + case '?': + case '#': + return s; + } + + break; + + default: + break; + } + + /* We should never fall out of the switch above unless there's an error */ + return s_dead; +} + +size_t http_parser_execute (http_parser *parser, + const http_parser_settings *settings, + const char *data, + size_t len) +{ + char c, ch; + int8_t unhex_val; + const char *p = data; + const char *header_field_mark = 0; + const char *header_value_mark = 0; + const char *url_mark = 0; + const char *body_mark = 0; + const char *status_mark = 0; + enum state p_state = (enum state) parser->state; + const unsigned int lenient = parser->lenient_http_headers; + + /* We're in an error state. Don't bother doing anything. */ + if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { + return 0; + } + + if (len == 0) { + switch (CURRENT_STATE()) { + case s_body_identity_eof: + /* Use of CALLBACK_NOTIFY() here would erroneously return 1 byte read if + * we got paused. + */ + CALLBACK_NOTIFY_NOADVANCE(message_complete); + return 0; + + case s_dead: + case s_start_req_or_res: + case s_start_res: + case s_start_req: + return 0; + + default: + SET_ERRNO(HPE_INVALID_EOF_STATE); + return 1; + } + } + + + if (CURRENT_STATE() == s_header_field) + header_field_mark = data; + if (CURRENT_STATE() == s_header_value) + header_value_mark = data; + switch (CURRENT_STATE()) { + case s_req_path: + case s_req_schema: + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + case s_req_server: + case s_req_server_with_at: + case s_req_query_string_start: + case s_req_query_string: + case s_req_fragment_start: + case s_req_fragment: + url_mark = data; + break; + case s_res_status: + status_mark = data; + break; + default: + break; + } + + for (p=data; p != data + len; p++) { + ch = *p; + + if (PARSING_HEADER(CURRENT_STATE())) + COUNT_HEADER_SIZE(1); + +reexecute: + switch (CURRENT_STATE()) { + + case s_dead: + /* this state is used after a 'Connection: close' message + * the parser will error out if it reads another message + */ + if (LIKELY(ch == CR || ch == LF)) + break; + + SET_ERRNO(HPE_CLOSED_CONNECTION); + goto error; + + case s_start_req_or_res: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->content_length = ULLONG_MAX; + + if (ch == 'H') { + UPDATE_STATE(s_res_or_resp_H); + + CALLBACK_NOTIFY(message_begin); + } else { + parser->type = HTTP_REQUEST; + UPDATE_STATE(s_start_req); + REEXECUTE(); + } + + break; + } + + case s_res_or_resp_H: + if (ch == 'T') { + parser->type = HTTP_RESPONSE; + UPDATE_STATE(s_res_HT); + } else { + if (UNLIKELY(ch != 'E')) { + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + + parser->type = HTTP_REQUEST; + parser->method = HTTP_HEAD; + parser->index = 2; + UPDATE_STATE(s_req_method); + } + break; + + case s_start_res: + { + parser->flags = 0; + parser->content_length = ULLONG_MAX; + + switch (ch) { + case 'H': + UPDATE_STATE(s_res_H); + break; + + case CR: + case LF: + break; + + default: + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + + CALLBACK_NOTIFY(message_begin); + break; + } + + case s_res_H: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_res_HT); + break; + + case s_res_HT: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_res_HTT); + break; + + case s_res_HTT: + STRICT_CHECK(ch != 'P'); + UPDATE_STATE(s_res_HTTP); + break; + + case s_res_HTTP: + STRICT_CHECK(ch != '/'); + UPDATE_STATE(s_res_http_major); + break; + + case s_res_http_major: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_major = ch - '0'; + UPDATE_STATE(s_res_http_dot); + break; + + case s_res_http_dot: + { + if (UNLIKELY(ch != '.')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_res_http_minor); + break; + } + + case s_res_http_minor: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_minor = ch - '0'; + UPDATE_STATE(s_res_http_end); + break; + + case s_res_http_end: + { + if (UNLIKELY(ch != ' ')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_res_first_status_code); + break; + } + + case s_res_first_status_code: + { + if (!IS_NUM(ch)) { + if (ch == ' ') { + break; + } + + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + parser->status_code = ch - '0'; + UPDATE_STATE(s_res_status_code); + break; + } + + case s_res_status_code: + { + if (!IS_NUM(ch)) { + switch (ch) { + case ' ': + UPDATE_STATE(s_res_status_start); + break; + case CR: + case LF: + UPDATE_STATE(s_res_status_start); + REEXECUTE(); + break; + default: + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + break; + } + + parser->status_code *= 10; + parser->status_code += ch - '0'; + + if (UNLIKELY(parser->status_code > 999)) { + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + + break; + } + + case s_res_status_start: + { + MARK(status); + UPDATE_STATE(s_res_status); + parser->index = 0; + + if (ch == CR || ch == LF) + REEXECUTE(); + + break; + } + + case s_res_status: + if (ch == CR) { + UPDATE_STATE(s_res_line_almost_done); + CALLBACK_DATA(status); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_field_start); + CALLBACK_DATA(status); + break; + } + + break; + + case s_res_line_almost_done: + STRICT_CHECK(ch != LF); + UPDATE_STATE(s_header_field_start); + break; + + case s_start_req: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->content_length = ULLONG_MAX; + + if (UNLIKELY(!IS_ALPHA(ch))) { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + parser->method = (enum http_method) 0; + parser->index = 1; + switch (ch) { + case 'A': parser->method = HTTP_ACL; break; + case 'B': parser->method = HTTP_BIND; break; + case 'C': parser->method = HTTP_CONNECT; /* or COPY, CHECKOUT */ break; + case 'D': parser->method = HTTP_DELETE; break; + case 'G': parser->method = HTTP_GET; break; + case 'H': parser->method = HTTP_HEAD; break; + case 'L': parser->method = HTTP_LOCK; /* or LINK */ break; + case 'M': parser->method = HTTP_MKCOL; /* or MOVE, MKACTIVITY, MERGE, M-SEARCH, MKCALENDAR */ break; + case 'N': parser->method = HTTP_NOTIFY; break; + case 'O': parser->method = HTTP_OPTIONS; break; + case 'P': parser->method = HTTP_POST; + /* or PROPFIND|PROPPATCH|PUT|PATCH|PURGE */ + break; + case 'R': parser->method = HTTP_REPORT; /* or REBIND */ break; + case 'S': parser->method = HTTP_SUBSCRIBE; /* or SEARCH */ break; + case 'T': parser->method = HTTP_TRACE; break; + case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE, UNBIND, UNLINK */ break; + default: + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + UPDATE_STATE(s_req_method); + + CALLBACK_NOTIFY(message_begin); + + break; + } + + case s_req_method: + { + const char *matcher; + if (UNLIKELY(ch == '\0')) { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + matcher = method_strings[parser->method]; + if (ch == ' ' && matcher[parser->index] == '\0') { + UPDATE_STATE(s_req_spaces_before_url); + } else if (ch == matcher[parser->index]) { + ; /* nada */ + } else if ((ch >= 'A' && ch <= 'Z') || ch == '-') { + + switch (parser->method << 16 | parser->index << 8 | ch) { +#define XX(meth, pos, ch, new_meth) \ + case (HTTP_##meth << 16 | pos << 8 | ch): \ + parser->method = HTTP_##new_meth; break; + + XX(POST, 1, 'U', PUT) + XX(POST, 1, 'A', PATCH) + XX(POST, 1, 'R', PROPFIND) + XX(PUT, 2, 'R', PURGE) + XX(CONNECT, 1, 'H', CHECKOUT) + XX(CONNECT, 2, 'P', COPY) + XX(MKCOL, 1, 'O', MOVE) + XX(MKCOL, 1, 'E', MERGE) + XX(MKCOL, 1, '-', MSEARCH) + XX(MKCOL, 2, 'A', MKACTIVITY) + XX(MKCOL, 3, 'A', MKCALENDAR) + XX(SUBSCRIBE, 1, 'E', SEARCH) + XX(REPORT, 2, 'B', REBIND) + XX(PROPFIND, 4, 'P', PROPPATCH) + XX(LOCK, 1, 'I', LINK) + XX(UNLOCK, 2, 'S', UNSUBSCRIBE) + XX(UNLOCK, 2, 'B', UNBIND) + XX(UNLOCK, 3, 'I', UNLINK) +#undef XX + default: + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + } else { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + ++parser->index; + break; + } + + case s_req_spaces_before_url: + { + if (ch == ' ') break; + + MARK(url); + if (parser->method == HTTP_CONNECT) { + UPDATE_STATE(s_req_server_start); + } + + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + + break; + } + + case s_req_schema: + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + { + switch (ch) { + /* No whitespace allowed here */ + case ' ': + case CR: + case LF: + SET_ERRNO(HPE_INVALID_URL); + goto error; + default: + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + } + + break; + } + + case s_req_server: + case s_req_server_with_at: + case s_req_path: + case s_req_query_string_start: + case s_req_query_string: + case s_req_fragment_start: + case s_req_fragment: + { + switch (ch) { + case ' ': + UPDATE_STATE(s_req_http_start); + CALLBACK_DATA(url); + break; + case CR: + case LF: + parser->http_major = 0; + parser->http_minor = 9; + CALLBACK_DATA(url); + if (ch == CR) { + UPDATE_STATE(s_req_line_almost_done); + } else { + UPDATE_STATE(s_headers_almost_done); + REEXECUTE(); + } + break; + default: + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + } + break; + } + + case s_req_http_start: + switch (ch) { + case 'H': + UPDATE_STATE(s_req_http_H); + break; + case ' ': + break; + default: + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + break; + + case s_req_http_H: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_req_http_HT); + break; + + case s_req_http_HT: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_req_http_HTT); + break; + + case s_req_http_HTT: + STRICT_CHECK(ch != 'P'); + UPDATE_STATE(s_req_http_HTTP); + break; + + case s_req_http_HTTP: + STRICT_CHECK(ch != '/'); + UPDATE_STATE(s_req_http_major); + break; + + case s_req_http_major: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_major = ch - '0'; + UPDATE_STATE(s_req_http_dot); + break; + + case s_req_http_dot: + { + if (UNLIKELY(ch != '.')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_req_http_minor); + break; + } + + case s_req_http_minor: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_minor = ch - '0'; + UPDATE_STATE(s_req_http_end); + break; + + case s_req_http_end: + { + if (ch == CR) { + UPDATE_STATE(s_req_line_almost_done); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_field_start); + break; + } + + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + break; + } + + /* end of request line */ + case s_req_line_almost_done: + { + if (UNLIKELY(ch != LF)) { + SET_ERRNO(HPE_LF_EXPECTED); + goto error; + } + + if (parser->http_major == 0) { + UPDATE_STATE(s_headers_almost_done); + REEXECUTE(); + } else { + UPDATE_STATE(s_header_field_start); + } + break; + } + + case s_header_field_start: + { + if (ch == CR) { + UPDATE_STATE(s_headers_almost_done); + break; + } + + if (ch == LF) { + /* they might be just sending \n instead of \r\n so this would be + * the second \n to denote the end of headers*/ + UPDATE_STATE(s_headers_almost_done); + REEXECUTE(); + } + + c = TOKEN(ch); + + if (UNLIKELY(!c)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + MARK(header_field); + + parser->index = 0; + UPDATE_STATE(s_header_field); + + switch (c) { + case 'c': + parser->header_state = h_C; + break; + + case 'p': + parser->header_state = h_matching_proxy_connection; + break; + + case 't': + parser->header_state = h_matching_transfer_encoding; + break; + + case 'u': + parser->header_state = h_matching_upgrade; + break; + + default: + parser->header_state = h_general; + break; + } + break; + } + + case s_header_field: + { + const char* start = p; + for (; p != data + len; p++) { + ch = *p; + c = TOKEN(ch); + + if (!c) + break; + + switch (parser->header_state) { + case h_general: + break; + + case h_C: + parser->index++; + parser->header_state = (c == 'o' ? h_CO : h_general); + break; + + case h_CO: + parser->index++; + parser->header_state = (c == 'n' ? h_CON : h_general); + break; + + case h_CON: + parser->index++; + switch (c) { + case 'n': + parser->header_state = h_matching_connection; + break; + case 't': + parser->header_state = h_matching_content_length; + break; + default: + parser->header_state = h_general; + break; + } + break; + + /* connection */ + + case h_matching_connection: + parser->index++; + if (parser->index > sizeof(CONNECTION)-1 + || c != CONNECTION[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(CONNECTION)-2) { + parser->header_state = h_connection; + } + break; + + /* proxy-connection */ + + case h_matching_proxy_connection: + parser->index++; + if (parser->index > sizeof(PROXY_CONNECTION)-1 + || c != PROXY_CONNECTION[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(PROXY_CONNECTION)-2) { + parser->header_state = h_connection; + } + break; + + /* content-length */ + + case h_matching_content_length: + parser->index++; + if (parser->index > sizeof(CONTENT_LENGTH)-1 + || c != CONTENT_LENGTH[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(CONTENT_LENGTH)-2) { + parser->header_state = h_content_length; + } + break; + + /* transfer-encoding */ + + case h_matching_transfer_encoding: + parser->index++; + if (parser->index > sizeof(TRANSFER_ENCODING)-1 + || c != TRANSFER_ENCODING[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(TRANSFER_ENCODING)-2) { + parser->header_state = h_transfer_encoding; + } + break; + + /* upgrade */ + + case h_matching_upgrade: + parser->index++; + if (parser->index > sizeof(UPGRADE)-1 + || c != UPGRADE[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(UPGRADE)-2) { + parser->header_state = h_upgrade; + } + break; + + case h_connection: + case h_content_length: + case h_transfer_encoding: + case h_upgrade: + if (ch != ' ') parser->header_state = h_general; + break; + + default: + assert(0 && "Unknown header_state"); + break; + } + } + + COUNT_HEADER_SIZE(p - start); + + if (p == data + len) { + --p; + break; + } + + if (ch == ':') { + UPDATE_STATE(s_header_value_discard_ws); + CALLBACK_DATA(header_field); + break; + } + + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + case s_header_value_discard_ws: + if (ch == ' ' || ch == '\t') break; + + if (ch == CR) { + UPDATE_STATE(s_header_value_discard_ws_almost_done); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_value_discard_lws); + break; + } + + /* FALLTHROUGH */ + + case s_header_value_start: + { + MARK(header_value); + + UPDATE_STATE(s_header_value); + parser->index = 0; + + c = LOWER(ch); + + switch (parser->header_state) { + case h_upgrade: + parser->flags |= F_UPGRADE; + parser->header_state = h_general; + break; + + case h_transfer_encoding: + /* looking for 'Transfer-Encoding: chunked' */ + if ('c' == c) { + parser->header_state = h_matching_transfer_encoding_chunked; + } else { + parser->header_state = h_general; + } + break; + + case h_content_length: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + } + + if (parser->flags & F_CONTENTLENGTH) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + + parser->flags |= F_CONTENTLENGTH; + parser->content_length = ch - '0'; + break; + + case h_connection: + /* looking for 'Connection: keep-alive' */ + if (c == 'k') { + parser->header_state = h_matching_connection_keep_alive; + /* looking for 'Connection: close' */ + } else if (c == 'c') { + parser->header_state = h_matching_connection_close; + } else if (c == 'u') { + parser->header_state = h_matching_connection_upgrade; + } else { + parser->header_state = h_matching_connection_token; + } + break; + + /* Multi-value `Connection` header */ + case h_matching_connection_token_start: + break; + + default: + parser->header_state = h_general; + break; + } + break; + } + + case s_header_value: + { + const char* start = p; + enum header_states h_state = (enum header_states) parser->header_state; + for (; p != data + len; p++) { + ch = *p; + if (ch == CR) { + UPDATE_STATE(s_header_almost_done); + parser->header_state = h_state; + CALLBACK_DATA(header_value); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_almost_done); + COUNT_HEADER_SIZE(p - start); + parser->header_state = h_state; + CALLBACK_DATA_NOADVANCE(header_value); + REEXECUTE(); + } + + if (!lenient && !IS_HEADER_CHAR(ch)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + c = LOWER(ch); + + switch (h_state) { + case h_general: + { + const char* p_cr; + const char* p_lf; + size_t limit = data + len - p; + + limit = MIN(limit, HTTP_MAX_HEADER_SIZE); + + p_cr = (const char*) memchr(p, CR, limit); + p_lf = (const char*) memchr(p, LF, limit); + if (p_cr != NULL) { + if (p_lf != NULL && p_cr >= p_lf) + p = p_lf; + else + p = p_cr; + } else if (UNLIKELY(p_lf != NULL)) { + p = p_lf; + } else { + p = data + len; + } + --p; + + break; + } + + case h_connection: + case h_transfer_encoding: + assert(0 && "Shouldn't get here."); + break; + + case h_content_length: + { + uint64_t t; + + if (ch == ' ') break; + + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + } + + t = parser->content_length; + t *= 10; + t += ch - '0'; + + /* Overflow? Test against a conservative limit for simplicity. */ + if (UNLIKELY((ULLONG_MAX - 10) / 10 < parser->content_length)) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + } + + parser->content_length = t; + break; + } + + /* Transfer-Encoding: chunked */ + case h_matching_transfer_encoding_chunked: + parser->index++; + if (parser->index > sizeof(CHUNKED)-1 + || c != CHUNKED[parser->index]) { + h_state = h_general; + } else if (parser->index == sizeof(CHUNKED)-2) { + h_state = h_transfer_encoding_chunked; + } + break; + + case h_matching_connection_token_start: + /* looking for 'Connection: keep-alive' */ + if (c == 'k') { + h_state = h_matching_connection_keep_alive; + /* looking for 'Connection: close' */ + } else if (c == 'c') { + h_state = h_matching_connection_close; + } else if (c == 'u') { + h_state = h_matching_connection_upgrade; + } else if (STRICT_TOKEN(c)) { + h_state = h_matching_connection_token; + } else if (c == ' ' || c == '\t') { + /* Skip lws */ + } else { + h_state = h_general; + } + break; + + /* looking for 'Connection: keep-alive' */ + case h_matching_connection_keep_alive: + parser->index++; + if (parser->index > sizeof(KEEP_ALIVE)-1 + || c != KEEP_ALIVE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(KEEP_ALIVE)-2) { + h_state = h_connection_keep_alive; + } + break; + + /* looking for 'Connection: close' */ + case h_matching_connection_close: + parser->index++; + if (parser->index > sizeof(CLOSE)-1 || c != CLOSE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(CLOSE)-2) { + h_state = h_connection_close; + } + break; + + /* looking for 'Connection: upgrade' */ + case h_matching_connection_upgrade: + parser->index++; + if (parser->index > sizeof(UPGRADE) - 1 || + c != UPGRADE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(UPGRADE)-2) { + h_state = h_connection_upgrade; + } + break; + + case h_matching_connection_token: + if (ch == ',') { + h_state = h_matching_connection_token_start; + parser->index = 0; + } + break; + + case h_transfer_encoding_chunked: + if (ch != ' ') h_state = h_general; + break; + + case h_connection_keep_alive: + case h_connection_close: + case h_connection_upgrade: + if (ch == ',') { + if (h_state == h_connection_keep_alive) { + parser->flags |= F_CONNECTION_KEEP_ALIVE; + } else if (h_state == h_connection_close) { + parser->flags |= F_CONNECTION_CLOSE; + } else if (h_state == h_connection_upgrade) { + parser->flags |= F_CONNECTION_UPGRADE; + } + h_state = h_matching_connection_token_start; + parser->index = 0; + } else if (ch != ' ') { + h_state = h_matching_connection_token; + } + break; + + default: + UPDATE_STATE(s_header_value); + h_state = h_general; + break; + } + } + parser->header_state = h_state; + + COUNT_HEADER_SIZE(p - start); + + if (p == data + len) + --p; + break; + } + + case s_header_almost_done: + { + if (UNLIKELY(ch != LF)) { + SET_ERRNO(HPE_LF_EXPECTED); + goto error; + } + + UPDATE_STATE(s_header_value_lws); + break; + } + + case s_header_value_lws: + { + if (ch == ' ' || ch == '\t') { + UPDATE_STATE(s_header_value_start); + REEXECUTE(); + } + + /* finished the header */ + switch (parser->header_state) { + case h_connection_keep_alive: + parser->flags |= F_CONNECTION_KEEP_ALIVE; + break; + case h_connection_close: + parser->flags |= F_CONNECTION_CLOSE; + break; + case h_transfer_encoding_chunked: + parser->flags |= F_CHUNKED; + break; + case h_connection_upgrade: + parser->flags |= F_CONNECTION_UPGRADE; + break; + default: + break; + } + + UPDATE_STATE(s_header_field_start); + REEXECUTE(); + } + + case s_header_value_discard_ws_almost_done: + { + STRICT_CHECK(ch != LF); + UPDATE_STATE(s_header_value_discard_lws); + break; + } + + case s_header_value_discard_lws: + { + if (ch == ' ' || ch == '\t') { + UPDATE_STATE(s_header_value_discard_ws); + break; + } else { + switch (parser->header_state) { + case h_connection_keep_alive: + parser->flags |= F_CONNECTION_KEEP_ALIVE; + break; + case h_connection_close: + parser->flags |= F_CONNECTION_CLOSE; + break; + case h_connection_upgrade: + parser->flags |= F_CONNECTION_UPGRADE; + break; + case h_transfer_encoding_chunked: + parser->flags |= F_CHUNKED; + break; + default: + break; + } + + /* header value was empty */ + MARK(header_value); + UPDATE_STATE(s_header_field_start); + CALLBACK_DATA_NOADVANCE(header_value); + REEXECUTE(); + } + } + + case s_headers_almost_done: + { + STRICT_CHECK(ch != LF); + + if (parser->flags & F_TRAILING) { + /* End of a chunked request */ + UPDATE_STATE(s_message_done); + CALLBACK_NOTIFY_NOADVANCE(chunk_complete); + REEXECUTE(); + } + + /* Cannot use chunked encoding and a content-length header together + per the HTTP specification. */ + if ((parser->flags & F_CHUNKED) && + (parser->flags & F_CONTENTLENGTH)) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + + UPDATE_STATE(s_headers_done); + + /* Set this here so that on_headers_complete() callbacks can see it */ + if ((parser->flags & F_UPGRADE) && + (parser->flags & F_CONNECTION_UPGRADE)) { + /* For responses, "Upgrade: foo" and "Connection: upgrade" are + * mandatory only when it is a 101 Switching Protocols response, + * otherwise it is purely informational, to announce support. + */ + parser->upgrade = + (parser->type == HTTP_REQUEST || parser->status_code == 101); + } else { + parser->upgrade = (parser->method == HTTP_CONNECT); + } + + /* Here we call the headers_complete callback. This is somewhat + * different than other callbacks because if the user returns 1, we + * will interpret that as saying that this message has no body. This + * is needed for the annoying case of recieving a response to a HEAD + * request. + * + * We'd like to use CALLBACK_NOTIFY_NOADVANCE() here but we cannot, so + * we have to simulate it by handling a change in errno below. + */ + if (settings->on_headers_complete) { + switch (settings->on_headers_complete(parser)) { + case 0: + break; + + case 2: + parser->upgrade = 1; + + /* FALLTHROUGH */ + case 1: + parser->flags |= F_SKIPBODY; + break; + + default: + SET_ERRNO(HPE_CB_headers_complete); + RETURN(p - data); /* Error */ + } + } + + if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { + RETURN(p - data); + } + + REEXECUTE(); + } + + case s_headers_done: + { + int hasBody; + STRICT_CHECK(ch != LF); + + parser->nread = 0; + + hasBody = parser->flags & F_CHUNKED || + (parser->content_length > 0 && parser->content_length != ULLONG_MAX); + if (parser->upgrade && (parser->method == HTTP_CONNECT || + (parser->flags & F_SKIPBODY) || !hasBody)) { + /* Exit, the rest of the message is in a different protocol. */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + RETURN((p - data) + 1); + } + + if (parser->flags & F_SKIPBODY) { + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else if (parser->flags & F_CHUNKED) { + /* chunked encoding - ignore Content-Length header */ + UPDATE_STATE(s_chunk_size_start); + } else { + if (parser->content_length == 0) { + /* Content-Length header given but zero: Content-Length: 0\r\n */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else if (parser->content_length != ULLONG_MAX) { + /* Content-Length header given and non-zero */ + UPDATE_STATE(s_body_identity); + } else { + if (!http_message_needs_eof(parser)) { + /* Assume content-length 0 - read the next */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else { + /* Read body until EOF */ + UPDATE_STATE(s_body_identity_eof); + } + } + } + + break; + } + + case s_body_identity: + { + uint64_t to_read = MIN(parser->content_length, + (uint64_t) ((data + len) - p)); + + assert(parser->content_length != 0 + && parser->content_length != ULLONG_MAX); + + /* The difference between advancing content_length and p is because + * the latter will automaticaly advance on the next loop iteration. + * Further, if content_length ends up at 0, we want to see the last + * byte again for our message complete callback. + */ + MARK(body); + parser->content_length -= to_read; + p += to_read - 1; + + if (parser->content_length == 0) { + UPDATE_STATE(s_message_done); + + /* Mimic CALLBACK_DATA_NOADVANCE() but with one extra byte. + * + * The alternative to doing this is to wait for the next byte to + * trigger the data callback, just as in every other case. The + * problem with this is that this makes it difficult for the test + * harness to distinguish between complete-on-EOF and + * complete-on-length. It's not clear that this distinction is + * important for applications, but let's keep it for now. + */ + CALLBACK_DATA_(body, p - body_mark + 1, p - data); + REEXECUTE(); + } + + break; + } + + /* read until EOF */ + case s_body_identity_eof: + MARK(body); + p = data + len - 1; + + break; + + case s_message_done: + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + if (parser->upgrade) { + /* Exit, the rest of the message is in a different protocol. */ + RETURN((p - data) + 1); + } + break; + + case s_chunk_size_start: + { + assert(parser->nread == 1); + assert(parser->flags & F_CHUNKED); + + unhex_val = unhex[(unsigned char)ch]; + if (UNLIKELY(unhex_val == -1)) { + SET_ERRNO(HPE_INVALID_CHUNK_SIZE); + goto error; + } + + parser->content_length = unhex_val; + UPDATE_STATE(s_chunk_size); + break; + } + + case s_chunk_size: + { + uint64_t t; + + assert(parser->flags & F_CHUNKED); + + if (ch == CR) { + UPDATE_STATE(s_chunk_size_almost_done); + break; + } + + unhex_val = unhex[(unsigned char)ch]; + + if (unhex_val == -1) { + if (ch == ';' || ch == ' ') { + UPDATE_STATE(s_chunk_parameters); + break; + } + + SET_ERRNO(HPE_INVALID_CHUNK_SIZE); + goto error; + } + + t = parser->content_length; + t *= 16; + t += unhex_val; + + /* Overflow? Test against a conservative limit for simplicity. */ + if (UNLIKELY((ULLONG_MAX - 16) / 16 < parser->content_length)) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + } + + parser->content_length = t; + break; + } + + case s_chunk_parameters: + { + assert(parser->flags & F_CHUNKED); + /* just ignore this shit. TODO check for overflow */ + if (ch == CR) { + UPDATE_STATE(s_chunk_size_almost_done); + break; + } + break; + } + + case s_chunk_size_almost_done: + { + assert(parser->flags & F_CHUNKED); + STRICT_CHECK(ch != LF); + + parser->nread = 0; + + if (parser->content_length == 0) { + parser->flags |= F_TRAILING; + UPDATE_STATE(s_header_field_start); + } else { + UPDATE_STATE(s_chunk_data); + } + CALLBACK_NOTIFY(chunk_header); + break; + } + + case s_chunk_data: + { + uint64_t to_read = MIN(parser->content_length, + (uint64_t) ((data + len) - p)); + + assert(parser->flags & F_CHUNKED); + assert(parser->content_length != 0 + && parser->content_length != ULLONG_MAX); + + /* See the explanation in s_body_identity for why the content + * length and data pointers are managed this way. + */ + MARK(body); + parser->content_length -= to_read; + p += to_read - 1; + + if (parser->content_length == 0) { + UPDATE_STATE(s_chunk_data_almost_done); + } + + break; + } + + case s_chunk_data_almost_done: + assert(parser->flags & F_CHUNKED); + assert(parser->content_length == 0); + STRICT_CHECK(ch != CR); + UPDATE_STATE(s_chunk_data_done); + CALLBACK_DATA(body); + break; + + case s_chunk_data_done: + assert(parser->flags & F_CHUNKED); + STRICT_CHECK(ch != LF); + parser->nread = 0; + UPDATE_STATE(s_chunk_size_start); + CALLBACK_NOTIFY(chunk_complete); + break; + + default: + assert(0 && "unhandled state"); + SET_ERRNO(HPE_INVALID_INTERNAL_STATE); + goto error; + } + } + + /* Run callbacks for any marks that we have leftover after we ran our of + * bytes. There should be at most one of these set, so it's OK to invoke + * them in series (unset marks will not result in callbacks). + * + * We use the NOADVANCE() variety of callbacks here because 'p' has already + * overflowed 'data' and this allows us to correct for the off-by-one that + * we'd otherwise have (since CALLBACK_DATA() is meant to be run with a 'p' + * value that's in-bounds). + */ + + assert(((header_field_mark ? 1 : 0) + + (header_value_mark ? 1 : 0) + + (url_mark ? 1 : 0) + + (body_mark ? 1 : 0) + + (status_mark ? 1 : 0)) <= 1); + + CALLBACK_DATA_NOADVANCE(header_field); + CALLBACK_DATA_NOADVANCE(header_value); + CALLBACK_DATA_NOADVANCE(url); + CALLBACK_DATA_NOADVANCE(body); + CALLBACK_DATA_NOADVANCE(status); + + RETURN(len); + +error: + if (HTTP_PARSER_ERRNO(parser) == HPE_OK) { + SET_ERRNO(HPE_UNKNOWN); + } + + RETURN(p - data); +} + + +/* Does the parser need to see an EOF to find the end of the message? */ +int +http_message_needs_eof (const http_parser *parser) +{ + if (parser->type == HTTP_REQUEST) { + return 0; + } + + /* See RFC 2616 section 4.4 */ + if (parser->status_code / 100 == 1 || /* 1xx e.g. Continue */ + parser->status_code == 204 || /* No Content */ + parser->status_code == 304 || /* Not Modified */ + parser->flags & F_SKIPBODY) { /* response to a HEAD request */ + return 0; + } + + if ((parser->flags & F_CHUNKED) || parser->content_length != ULLONG_MAX) { + return 0; + } + + return 1; +} + + +int +http_should_keep_alive (const http_parser *parser) +{ + if (parser->http_major > 0 && parser->http_minor > 0) { + /* HTTP/1.1 */ + if (parser->flags & F_CONNECTION_CLOSE) { + return 0; + } + } else { + /* HTTP/1.0 or earlier */ + if (!(parser->flags & F_CONNECTION_KEEP_ALIVE)) { + return 0; + } + } + + return !http_message_needs_eof(parser); +} + + +const char * +http_method_str (enum http_method m) +{ + return ELEM_AT(method_strings, m, ""); +} + + +void +http_parser_init (http_parser *parser, enum http_parser_type t) +{ + void *data = parser->data; /* preserve application data */ + memset(parser, 0, sizeof(*parser)); + parser->data = data; + parser->type = t; + parser->state = (t == HTTP_REQUEST ? s_start_req : (t == HTTP_RESPONSE ? s_start_res : s_start_req_or_res)); + parser->http_errno = HPE_OK; +} + +void +http_parser_settings_init(http_parser_settings *settings) +{ + memset(settings, 0, sizeof(*settings)); +} + +const char * +http_errno_name(enum http_errno err) { + assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); + return http_strerror_tab[err].name; +} + +const char * +http_errno_description(enum http_errno err) { + assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); + return http_strerror_tab[err].description; +} + +static enum http_host_state +http_parse_host_char(enum http_host_state s, const char ch) { + switch(s) { + case s_http_userinfo: + case s_http_userinfo_start: + if (ch == '@') { + return s_http_host_start; + } + + if (IS_USERINFO_CHAR(ch)) { + return s_http_userinfo; + } + break; + + case s_http_host_start: + if (ch == '[') { + return s_http_host_v6_start; + } + + if (IS_HOST_CHAR(ch)) { + return s_http_host; + } + + break; + + case s_http_host: + if (IS_HOST_CHAR(ch)) { + return s_http_host; + } + + /* FALLTHROUGH */ + case s_http_host_v6_end: + if (ch == ':') { + return s_http_host_port_start; + } + + break; + + case s_http_host_v6: + if (ch == ']') { + return s_http_host_v6_end; + } + + /* FALLTHROUGH */ + case s_http_host_v6_start: + if (IS_HEX(ch) || ch == ':' || ch == '.') { + return s_http_host_v6; + } + + if (s == s_http_host_v6 && ch == '%') { + return s_http_host_v6_zone_start; + } + break; + + case s_http_host_v6_zone: + if (ch == ']') { + return s_http_host_v6_end; + } + + /* FALLTHROUGH */ + case s_http_host_v6_zone_start: + /* RFC 6874 Zone ID consists of 1*( unreserved / pct-encoded) */ + if (IS_ALPHANUM(ch) || ch == '%' || ch == '.' || ch == '-' || ch == '_' || + ch == '~') { + return s_http_host_v6_zone; + } + break; + + case s_http_host_port: + case s_http_host_port_start: + if (IS_NUM(ch)) { + return s_http_host_port; + } + + break; + + default: + break; + } + return s_http_host_dead; +} + +static int +http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { + enum http_host_state s; + + const char *p; + size_t buflen = u->field_data[UF_HOST].off + u->field_data[UF_HOST].len; + + assert(u->field_set & (1 << UF_HOST)); + + u->field_data[UF_HOST].len = 0; + + s = found_at ? s_http_userinfo_start : s_http_host_start; + + for (p = buf + u->field_data[UF_HOST].off; p < buf + buflen; p++) { + enum http_host_state new_s = http_parse_host_char(s, *p); + + if (new_s == s_http_host_dead) { + return 1; + } + + switch(new_s) { + case s_http_host: + if (s != s_http_host) { + u->field_data[UF_HOST].off = p - buf; + } + u->field_data[UF_HOST].len++; + break; + + case s_http_host_v6: + if (s != s_http_host_v6) { + u->field_data[UF_HOST].off = p - buf; + } + u->field_data[UF_HOST].len++; + break; + + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: + u->field_data[UF_HOST].len++; + break; + + case s_http_host_port: + if (s != s_http_host_port) { + u->field_data[UF_PORT].off = p - buf; + u->field_data[UF_PORT].len = 0; + u->field_set |= (1 << UF_PORT); + } + u->field_data[UF_PORT].len++; + break; + + case s_http_userinfo: + if (s != s_http_userinfo) { + u->field_data[UF_USERINFO].off = p - buf ; + u->field_data[UF_USERINFO].len = 0; + u->field_set |= (1 << UF_USERINFO); + } + u->field_data[UF_USERINFO].len++; + break; + + default: + break; + } + s = new_s; + } + + /* Make sure we don't end somewhere unexpected */ + switch (s) { + case s_http_host_start: + case s_http_host_v6_start: + case s_http_host_v6: + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: + case s_http_host_port_start: + case s_http_userinfo: + case s_http_userinfo_start: + return 1; + default: + break; + } + + return 0; +} + +void +http_parser_url_init(struct http_parser_url *u) { + memset(u, 0, sizeof(*u)); +} + +int +http_parser_parse_url(const char *buf, size_t buflen, int is_connect, + struct http_parser_url *u) +{ + enum state s; + const char *p; + enum http_parser_url_fields uf, old_uf; + int found_at = 0; + + u->port = u->field_set = 0; + s = is_connect ? s_req_server_start : s_req_spaces_before_url; + old_uf = UF_MAX; + + for (p = buf; p < buf + buflen; p++) { + s = parse_url_char(s, *p); + + /* Figure out the next field that we're operating on */ + switch (s) { + case s_dead: + return 1; + + /* Skip delimeters */ + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + case s_req_query_string_start: + case s_req_fragment_start: + continue; + + case s_req_schema: + uf = UF_SCHEMA; + break; + + case s_req_server_with_at: + found_at = 1; + + /* FALLTHROUGH */ + case s_req_server: + uf = UF_HOST; + break; + + case s_req_path: + uf = UF_PATH; + break; + + case s_req_query_string: + uf = UF_QUERY; + break; + + case s_req_fragment: + uf = UF_FRAGMENT; + break; + + default: + assert(!"Unexpected state"); + return 1; + } + + /* Nothing's changed; soldier on */ + if (uf == old_uf) { + u->field_data[uf].len++; + continue; + } + + u->field_data[uf].off = p - buf; + u->field_data[uf].len = 1; + + u->field_set |= (1 << uf); + old_uf = uf; + } + + /* host must be present if there is a schema */ + /* parsing http:///toto will fail */ + if ((u->field_set & (1 << UF_SCHEMA)) && + (u->field_set & (1 << UF_HOST)) == 0) { + return 1; + } + + if (u->field_set & (1 << UF_HOST)) { + if (http_parse_host(buf, u, found_at) != 0) { + return 1; + } + } + + /* CONNECT requests can only contain "hostname:port" */ + if (is_connect && u->field_set != ((1 << UF_HOST)|(1 << UF_PORT))) { + return 1; + } + + if (u->field_set & (1 << UF_PORT)) { + /* Don't bother with endp; we've already validated the string */ + unsigned long v = strtoul(buf + u->field_data[UF_PORT].off, NULL, 10); + + /* Ports have a max value of 2^16 */ + if (v > 0xffff) { + return 1; + } + + u->port = (uint16_t) v; + } + + return 0; +} + +void +http_parser_pause(http_parser *parser, int paused) { + /* Users should only be pausing/unpausing a parser that is not in an error + * state. In non-debug builds, there's not much that we can do about this + * other than ignore it. + */ + if (HTTP_PARSER_ERRNO(parser) == HPE_OK || + HTTP_PARSER_ERRNO(parser) == HPE_PAUSED) { + SET_ERRNO((paused) ? HPE_PAUSED : HPE_OK); + } else { + assert(0 && "Attempting to pause parser in error state"); + } +} + +int +http_body_is_final(const struct http_parser *parser) { + return parser->state == s_message_done; +} + +unsigned long +http_parser_version(void) { + return HTTP_PARSER_VERSION_MAJOR * 0x10000 | + HTTP_PARSER_VERSION_MINOR * 0x00100 | + HTTP_PARSER_VERSION_PATCH * 0x00001; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/http_parser.gyp b/deps/ngtcp2/ngtcp2/third-party/http-parser/http_parser.gyp new file mode 100644 index 00000000000000..ef34ecaeaeab45 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/http_parser.gyp @@ -0,0 +1,111 @@ +# This file is used with the GYP meta build system. +# http://code.google.com/p/gyp/ +# To build try this: +# svn co http://gyp.googlecode.com/svn/trunk gyp +# ./gyp/gyp -f make --depth=`pwd` http_parser.gyp +# ./out/Debug/test +{ + 'target_defaults': { + 'default_configuration': 'Debug', + 'configurations': { + # TODO: hoist these out and put them somewhere common, because + # RuntimeLibrary MUST MATCH across the entire project + 'Debug': { + 'defines': [ 'DEBUG', '_DEBUG' ], + 'cflags': [ '-Wall', '-Wextra', '-O0', '-g', '-ftrapv' ], + 'msvs_settings': { + 'VCCLCompilerTool': { + 'RuntimeLibrary': 1, # static debug + }, + }, + }, + 'Release': { + 'defines': [ 'NDEBUG' ], + 'cflags': [ '-Wall', '-Wextra', '-O3' ], + 'msvs_settings': { + 'VCCLCompilerTool': { + 'RuntimeLibrary': 0, # static release + }, + }, + } + }, + 'msvs_settings': { + 'VCCLCompilerTool': { + }, + 'VCLibrarianTool': { + }, + 'VCLinkerTool': { + 'GenerateDebugInformation': 'true', + }, + }, + 'conditions': [ + ['OS == "win"', { + 'defines': [ + 'WIN32' + ], + }] + ], + }, + + 'targets': [ + { + 'target_name': 'http_parser', + 'type': 'static_library', + 'include_dirs': [ '.' ], + 'direct_dependent_settings': { + 'defines': [ 'HTTP_PARSER_STRICT=0' ], + 'include_dirs': [ '.' ], + }, + 'defines': [ 'HTTP_PARSER_STRICT=0' ], + 'sources': [ './http_parser.c', ], + 'conditions': [ + ['OS=="win"', { + 'msvs_settings': { + 'VCCLCompilerTool': { + # Compile as C++. http_parser.c is actually C99, but C++ is + # close enough in this case. + 'CompileAs': 2, + }, + }, + }] + ], + }, + + { + 'target_name': 'http_parser_strict', + 'type': 'static_library', + 'include_dirs': [ '.' ], + 'direct_dependent_settings': { + 'defines': [ 'HTTP_PARSER_STRICT=1' ], + 'include_dirs': [ '.' ], + }, + 'defines': [ 'HTTP_PARSER_STRICT=1' ], + 'sources': [ './http_parser.c', ], + 'conditions': [ + ['OS=="win"', { + 'msvs_settings': { + 'VCCLCompilerTool': { + # Compile as C++. http_parser.c is actually C99, but C++ is + # close enough in this case. + 'CompileAs': 2, + }, + }, + }] + ], + }, + + { + 'target_name': 'test-nonstrict', + 'type': 'executable', + 'dependencies': [ 'http_parser' ], + 'sources': [ 'test.c' ] + }, + + { + 'target_name': 'test-strict', + 'type': 'executable', + 'dependencies': [ 'http_parser_strict' ], + 'sources': [ 'test.c' ] + } + ] +} diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/http_parser.h b/deps/ngtcp2/ngtcp2/third-party/http-parser/http_parser.h new file mode 100644 index 00000000000000..fd2a564148bec1 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/http_parser.h @@ -0,0 +1,431 @@ +/* Copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#ifndef http_parser_h +#define http_parser_h +#ifdef __cplusplus +extern "C" { +#endif + +/* Also update SONAME in the Makefile whenever you change these. */ +#define HTTP_PARSER_VERSION_MAJOR 2 +#define HTTP_PARSER_VERSION_MINOR 7 +#define HTTP_PARSER_VERSION_PATCH 1 + +#include +#if defined(_WIN32) && !defined(__MINGW32__) && \ + (!defined(_MSC_VER) || _MSC_VER<1600) && !defined(__WINE__) +#include +typedef __int8 int8_t; +typedef unsigned __int8 uint8_t; +typedef __int16 int16_t; +typedef unsigned __int16 uint16_t; +typedef __int32 int32_t; +typedef unsigned __int32 uint32_t; +typedef __int64 int64_t; +typedef unsigned __int64 uint64_t; +#else +#include +#endif + +/* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run + * faster + */ +#ifndef HTTP_PARSER_STRICT +# define HTTP_PARSER_STRICT 1 +#endif + +/* Maximium header size allowed. If the macro is not defined + * before including this header then the default is used. To + * change the maximum header size, define the macro in the build + * environment (e.g. -DHTTP_MAX_HEADER_SIZE=). To remove + * the effective limit on the size of the header, define the macro + * to a very large number (e.g. -DHTTP_MAX_HEADER_SIZE=0x7fffffff) + */ +#ifndef HTTP_MAX_HEADER_SIZE +# define HTTP_MAX_HEADER_SIZE (80*1024) +#endif + +typedef struct http_parser http_parser; +typedef struct http_parser_settings http_parser_settings; + + +/* Callbacks should return non-zero to indicate an error. The parser will + * then halt execution. + * + * The one exception is on_headers_complete. In a HTTP_RESPONSE parser + * returning '1' from on_headers_complete will tell the parser that it + * should not expect a body. This is used when receiving a response to a + * HEAD request which may contain 'Content-Length' or 'Transfer-Encoding: + * chunked' headers that indicate the presence of a body. + * + * Returning `2` from on_headers_complete will tell parser that it should not + * expect neither a body nor any futher responses on this connection. This is + * useful for handling responses to a CONNECT request which may not contain + * `Upgrade` or `Connection: upgrade` headers. + * + * http_data_cb does not return data chunks. It will be called arbitrarily + * many times for each string. E.G. you might get 10 callbacks for "on_url" + * each providing just a few characters more data. + */ +typedef int (*http_data_cb) (http_parser*, const char *at, size_t length); +typedef int (*http_cb) (http_parser*); + + +/* Status Codes */ +#define HTTP_STATUS_MAP(XX) \ + XX(100, CONTINUE, Continue) \ + XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \ + XX(102, PROCESSING, Processing) \ + XX(200, OK, OK) \ + XX(201, CREATED, Created) \ + XX(202, ACCEPTED, Accepted) \ + XX(203, NON_AUTHORITATIVE_INFORMATION, Non-Authoritative Information) \ + XX(204, NO_CONTENT, No Content) \ + XX(205, RESET_CONTENT, Reset Content) \ + XX(206, PARTIAL_CONTENT, Partial Content) \ + XX(207, MULTI_STATUS, Multi-Status) \ + XX(208, ALREADY_REPORTED, Already Reported) \ + XX(226, IM_USED, IM Used) \ + XX(300, MULTIPLE_CHOICES, Multiple Choices) \ + XX(301, MOVED_PERMANENTLY, Moved Permanently) \ + XX(302, FOUND, Found) \ + XX(303, SEE_OTHER, See Other) \ + XX(304, NOT_MODIFIED, Not Modified) \ + XX(305, USE_PROXY, Use Proxy) \ + XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \ + XX(308, PERMANENT_REDIRECT, Permanent Redirect) \ + XX(400, BAD_REQUEST, Bad Request) \ + XX(401, UNAUTHORIZED, Unauthorized) \ + XX(402, PAYMENT_REQUIRED, Payment Required) \ + XX(403, FORBIDDEN, Forbidden) \ + XX(404, NOT_FOUND, Not Found) \ + XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \ + XX(406, NOT_ACCEPTABLE, Not Acceptable) \ + XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \ + XX(408, REQUEST_TIMEOUT, Request Timeout) \ + XX(409, CONFLICT, Conflict) \ + XX(410, GONE, Gone) \ + XX(411, LENGTH_REQUIRED, Length Required) \ + XX(412, PRECONDITION_FAILED, Precondition Failed) \ + XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \ + XX(414, URI_TOO_LONG, URI Too Long) \ + XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \ + XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \ + XX(417, EXPECTATION_FAILED, Expectation Failed) \ + XX(421, MISDIRECTED_REQUEST, Misdirected Request) \ + XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \ + XX(423, LOCKED, Locked) \ + XX(424, FAILED_DEPENDENCY, Failed Dependency) \ + XX(426, UPGRADE_REQUIRED, Upgrade Required) \ + XX(428, PRECONDITION_REQUIRED, Precondition Required) \ + XX(429, TOO_MANY_REQUESTS, Too Many Requests) \ + XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \ + XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \ + XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \ + XX(501, NOT_IMPLEMENTED, Not Implemented) \ + XX(502, BAD_GATEWAY, Bad Gateway) \ + XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \ + XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \ + XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \ + XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \ + XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \ + XX(508, LOOP_DETECTED, Loop Detected) \ + XX(510, NOT_EXTENDED, Not Extended) \ + XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) \ + +enum http_status + { +#define XX(num, name, string) HTTP_STATUS_##name = num, + HTTP_STATUS_MAP(XX) +#undef XX + }; + + +/* Request Methods */ +#define HTTP_METHOD_MAP(XX) \ + XX(0, DELETE, DELETE) \ + XX(1, GET, GET) \ + XX(2, HEAD, HEAD) \ + XX(3, POST, POST) \ + XX(4, PUT, PUT) \ + /* pathological */ \ + XX(5, CONNECT, CONNECT) \ + XX(6, OPTIONS, OPTIONS) \ + XX(7, TRACE, TRACE) \ + /* WebDAV */ \ + XX(8, COPY, COPY) \ + XX(9, LOCK, LOCK) \ + XX(10, MKCOL, MKCOL) \ + XX(11, MOVE, MOVE) \ + XX(12, PROPFIND, PROPFIND) \ + XX(13, PROPPATCH, PROPPATCH) \ + XX(14, SEARCH, SEARCH) \ + XX(15, UNLOCK, UNLOCK) \ + XX(16, BIND, BIND) \ + XX(17, REBIND, REBIND) \ + XX(18, UNBIND, UNBIND) \ + XX(19, ACL, ACL) \ + /* subversion */ \ + XX(20, REPORT, REPORT) \ + XX(21, MKACTIVITY, MKACTIVITY) \ + XX(22, CHECKOUT, CHECKOUT) \ + XX(23, MERGE, MERGE) \ + /* upnp */ \ + XX(24, MSEARCH, M-SEARCH) \ + XX(25, NOTIFY, NOTIFY) \ + XX(26, SUBSCRIBE, SUBSCRIBE) \ + XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \ + /* RFC-5789 */ \ + XX(28, PATCH, PATCH) \ + XX(29, PURGE, PURGE) \ + /* CalDAV */ \ + XX(30, MKCALENDAR, MKCALENDAR) \ + /* RFC-2068, section 19.6.1.2 */ \ + XX(31, LINK, LINK) \ + XX(32, UNLINK, UNLINK) \ + +enum http_method + { +#define XX(num, name, string) HTTP_##name = num, + HTTP_METHOD_MAP(XX) +#undef XX + }; + + +enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH }; + + +/* Flag values for http_parser.flags field */ +enum flags + { F_CHUNKED = 1 << 0 + , F_CONNECTION_KEEP_ALIVE = 1 << 1 + , F_CONNECTION_CLOSE = 1 << 2 + , F_CONNECTION_UPGRADE = 1 << 3 + , F_TRAILING = 1 << 4 + , F_UPGRADE = 1 << 5 + , F_SKIPBODY = 1 << 6 + , F_CONTENTLENGTH = 1 << 7 + }; + + +/* Map for errno-related constants + * + * The provided argument should be a macro that takes 2 arguments. + */ +#define HTTP_ERRNO_MAP(XX) \ + /* No error */ \ + XX(OK, "success") \ + \ + /* Callback-related errors */ \ + XX(CB_message_begin, "the on_message_begin callback failed") \ + XX(CB_url, "the on_url callback failed") \ + XX(CB_header_field, "the on_header_field callback failed") \ + XX(CB_header_value, "the on_header_value callback failed") \ + XX(CB_headers_complete, "the on_headers_complete callback failed") \ + XX(CB_body, "the on_body callback failed") \ + XX(CB_message_complete, "the on_message_complete callback failed") \ + XX(CB_status, "the on_status callback failed") \ + XX(CB_chunk_header, "the on_chunk_header callback failed") \ + XX(CB_chunk_complete, "the on_chunk_complete callback failed") \ + \ + /* Parsing-related errors */ \ + XX(INVALID_EOF_STATE, "stream ended at an unexpected time") \ + XX(HEADER_OVERFLOW, \ + "too many header bytes seen; overflow detected") \ + XX(CLOSED_CONNECTION, \ + "data received after completed connection: close message") \ + XX(INVALID_VERSION, "invalid HTTP version") \ + XX(INVALID_STATUS, "invalid HTTP status code") \ + XX(INVALID_METHOD, "invalid HTTP method") \ + XX(INVALID_URL, "invalid URL") \ + XX(INVALID_HOST, "invalid host") \ + XX(INVALID_PORT, "invalid port") \ + XX(INVALID_PATH, "invalid path") \ + XX(INVALID_QUERY_STRING, "invalid query string") \ + XX(INVALID_FRAGMENT, "invalid fragment") \ + XX(LF_EXPECTED, "LF character expected") \ + XX(INVALID_HEADER_TOKEN, "invalid character in header") \ + XX(INVALID_CONTENT_LENGTH, \ + "invalid character in content-length header") \ + XX(UNEXPECTED_CONTENT_LENGTH, \ + "unexpected content-length header") \ + XX(INVALID_CHUNK_SIZE, \ + "invalid character in chunk size header") \ + XX(INVALID_CONSTANT, "invalid constant string") \ + XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\ + XX(STRICT, "strict mode assertion failed") \ + XX(PAUSED, "parser is paused") \ + XX(UNKNOWN, "an unknown error occurred") + + +/* Define HPE_* values for each errno value above */ +#define HTTP_ERRNO_GEN(n, s) HPE_##n, +enum http_errno { + HTTP_ERRNO_MAP(HTTP_ERRNO_GEN) +}; +#undef HTTP_ERRNO_GEN + + +/* Get an http_errno value from an http_parser */ +#define HTTP_PARSER_ERRNO(p) ((enum http_errno) (p)->http_errno) + + +struct http_parser { + /** PRIVATE **/ + unsigned int type : 2; /* enum http_parser_type */ + unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */ + unsigned int state : 7; /* enum state from http_parser.c */ + unsigned int header_state : 7; /* enum header_state from http_parser.c */ + unsigned int index : 7; /* index into current matcher */ + unsigned int lenient_http_headers : 1; + + uint32_t nread; /* # bytes read in various scenarios */ + uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */ + + /** READ-ONLY **/ + unsigned short http_major; + unsigned short http_minor; + unsigned int status_code : 16; /* responses only */ + unsigned int method : 8; /* requests only */ + unsigned int http_errno : 7; + + /* 1 = Upgrade header was present and the parser has exited because of that. + * 0 = No upgrade header present. + * Should be checked when http_parser_execute() returns in addition to + * error checking. + */ + unsigned int upgrade : 1; + + /** PUBLIC **/ + void *data; /* A pointer to get hook to the "connection" or "socket" object */ +}; + + +struct http_parser_settings { + http_cb on_message_begin; + http_data_cb on_url; + http_data_cb on_status; + http_data_cb on_header_field; + http_data_cb on_header_value; + http_cb on_headers_complete; + http_data_cb on_body; + http_cb on_message_complete; + /* When on_chunk_header is called, the current chunk length is stored + * in parser->content_length. + */ + http_cb on_chunk_header; + http_cb on_chunk_complete; +}; + + +enum http_parser_url_fields + { UF_SCHEMA = 0 + , UF_HOST = 1 + , UF_PORT = 2 + , UF_PATH = 3 + , UF_QUERY = 4 + , UF_FRAGMENT = 5 + , UF_USERINFO = 6 + , UF_MAX = 7 + }; + + +/* Result structure for http_parser_parse_url(). + * + * Callers should index into field_data[] with UF_* values iff field_set + * has the relevant (1 << UF_*) bit set. As a courtesy to clients (and + * because we probably have padding left over), we convert any port to + * a uint16_t. + */ +struct http_parser_url { + uint16_t field_set; /* Bitmask of (1 << UF_*) values */ + uint16_t port; /* Converted UF_PORT string */ + + struct { + uint16_t off; /* Offset into buffer in which field starts */ + uint16_t len; /* Length of run in buffer */ + } field_data[UF_MAX]; +}; + + +/* Returns the library version. Bits 16-23 contain the major version number, + * bits 8-15 the minor version number and bits 0-7 the patch level. + * Usage example: + * + * unsigned long version = http_parser_version(); + * unsigned major = (version >> 16) & 255; + * unsigned minor = (version >> 8) & 255; + * unsigned patch = version & 255; + * printf("http_parser v%u.%u.%u\n", major, minor, patch); + */ +unsigned long http_parser_version(void); + +void http_parser_init(http_parser *parser, enum http_parser_type type); + + +/* Initialize http_parser_settings members to 0 + */ +void http_parser_settings_init(http_parser_settings *settings); + + +/* Executes the parser. Returns number of parsed bytes. Sets + * `parser->http_errno` on error. */ +size_t http_parser_execute(http_parser *parser, + const http_parser_settings *settings, + const char *data, + size_t len); + + +/* If http_should_keep_alive() in the on_headers_complete or + * on_message_complete callback returns 0, then this should be + * the last message on the connection. + * If you are the server, respond with the "Connection: close" header. + * If you are the client, close the connection. + */ +int http_should_keep_alive(const http_parser *parser); + +/* Returns a string version of the HTTP method. */ +const char *http_method_str(enum http_method m); + +/* Return a string name of the given error */ +const char *http_errno_name(enum http_errno err); + +/* Return a string description of the given error */ +const char *http_errno_description(enum http_errno err); + +/* Initialize all http_parser_url members to 0 */ +void http_parser_url_init(struct http_parser_url *u); + +/* Parse a URL; return nonzero on failure */ +int http_parser_parse_url(const char *buf, size_t buflen, + int is_connect, + struct http_parser_url *u); + +/* Pause or un-pause the parser; a nonzero value pauses */ +void http_parser_pause(http_parser *parser, int paused); + +/* Checks if this is the final chunk of the body. */ +int http_body_is_final(const http_parser *parser); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/deps/ngtcp2/ngtcp2/third-party/http-parser/test.c b/deps/ngtcp2/ngtcp2/third-party/http-parser/test.c new file mode 100644 index 00000000000000..ae637d1925d713 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/http-parser/test.c @@ -0,0 +1,4410 @@ +/* Copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include "http_parser.h" +#include +#include +#include +#include +#include + +#if defined(__APPLE__) +# undef strlcat +# undef strlncpy +# undef strlcpy +#endif /* defined(__APPLE__) */ + +#undef TRUE +#define TRUE 1 +#undef FALSE +#define FALSE 0 + +#define MAX_HEADERS 13 +#define MAX_ELEMENT_SIZE 2048 +#define MAX_CHUNKS 16 + +#define MIN(a,b) ((a) < (b) ? (a) : (b)) + +static http_parser *parser; + +struct message { + const char *name; // for debugging purposes + const char *raw; + enum http_parser_type type; + enum http_method method; + int status_code; + char response_status[MAX_ELEMENT_SIZE]; + char request_path[MAX_ELEMENT_SIZE]; + char request_url[MAX_ELEMENT_SIZE]; + char fragment[MAX_ELEMENT_SIZE]; + char query_string[MAX_ELEMENT_SIZE]; + char body[MAX_ELEMENT_SIZE]; + size_t body_size; + const char *host; + const char *userinfo; + uint16_t port; + int num_headers; + enum { NONE=0, FIELD, VALUE } last_header_element; + char headers [MAX_HEADERS][2][MAX_ELEMENT_SIZE]; + int should_keep_alive; + + int num_chunks; + int num_chunks_complete; + int chunk_lengths[MAX_CHUNKS]; + + const char *upgrade; // upgraded body + + unsigned short http_major; + unsigned short http_minor; + + int message_begin_cb_called; + int headers_complete_cb_called; + int message_complete_cb_called; + int status_cb_called; + int message_complete_on_eof; + int body_is_final; +}; + +static int currently_parsing_eof; + +static struct message messages[5]; +static int num_messages; +static http_parser_settings *current_pause_parser; + +/* * R E Q U E S T S * */ +const struct message requests[] = +#define CURL_GET 0 +{ {.name= "curl get" + ,.type= HTTP_REQUEST + ,.raw= "GET /test HTTP/1.1\r\n" + "User-Agent: curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1\r\n" + "Host: 0.0.0.0=5000\r\n" + "Accept: */*\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/test" + ,.request_url= "/test" + ,.num_headers= 3 + ,.headers= + { { "User-Agent", "curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1" } + , { "Host", "0.0.0.0=5000" } + , { "Accept", "*/*" } + } + ,.body= "" + } + +#define FIREFOX_GET 1 +, {.name= "firefox get" + ,.type= HTTP_REQUEST + ,.raw= "GET /favicon.ico HTTP/1.1\r\n" + "Host: 0.0.0.0=5000\r\n" + "User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "Accept-Language: en-us,en;q=0.5\r\n" + "Accept-Encoding: gzip,deflate\r\n" + "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + "Keep-Alive: 300\r\n" + "Connection: keep-alive\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/favicon.ico" + ,.request_url= "/favicon.ico" + ,.num_headers= 8 + ,.headers= + { { "Host", "0.0.0.0=5000" } + , { "User-Agent", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0" } + , { "Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } + , { "Accept-Language", "en-us,en;q=0.5" } + , { "Accept-Encoding", "gzip,deflate" } + , { "Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7" } + , { "Keep-Alive", "300" } + , { "Connection", "keep-alive" } + } + ,.body= "" + } + +#define DUMBFUCK 2 +, {.name= "dumbfuck" + ,.type= HTTP_REQUEST + ,.raw= "GET /dumbfuck HTTP/1.1\r\n" + "aaaaaaaaaaaaa:++++++++++\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/dumbfuck" + ,.request_url= "/dumbfuck" + ,.num_headers= 1 + ,.headers= + { { "aaaaaaaaaaaaa", "++++++++++" } + } + ,.body= "" + } + +#define FRAGMENT_IN_URI 3 +, {.name= "fragment in url" + ,.type= HTTP_REQUEST + ,.raw= "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "page=1" + ,.fragment= "posts-17408" + ,.request_path= "/forums/1/topics/2375" + /* XXX request url does include fragment? */ + ,.request_url= "/forums/1/topics/2375?page=1#posts-17408" + ,.num_headers= 0 + ,.body= "" + } + +#define GET_NO_HEADERS_NO_BODY 4 +, {.name= "get no headers no body" + ,.type= HTTP_REQUEST + ,.raw= "GET /get_no_headers_no_body/world HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE /* would need Connection: close */ + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/get_no_headers_no_body/world" + ,.request_url= "/get_no_headers_no_body/world" + ,.num_headers= 0 + ,.body= "" + } + +#define GET_ONE_HEADER_NO_BODY 5 +, {.name= "get one header no body" + ,.type= HTTP_REQUEST + ,.raw= "GET /get_one_header_no_body HTTP/1.1\r\n" + "Accept: */*\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE /* would need Connection: close */ + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/get_one_header_no_body" + ,.request_url= "/get_one_header_no_body" + ,.num_headers= 1 + ,.headers= + { { "Accept" , "*/*" } + } + ,.body= "" + } + +#define GET_FUNKY_CONTENT_LENGTH 6 +, {.name= "get funky content length body hello" + ,.type= HTTP_REQUEST + ,.raw= "GET /get_funky_content_length_body_hello HTTP/1.0\r\n" + "conTENT-Length: 5\r\n" + "\r\n" + "HELLO" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/get_funky_content_length_body_hello" + ,.request_url= "/get_funky_content_length_body_hello" + ,.num_headers= 1 + ,.headers= + { { "conTENT-Length" , "5" } + } + ,.body= "HELLO" + } + +#define POST_IDENTITY_BODY_WORLD 7 +, {.name= "post identity body world" + ,.type= HTTP_REQUEST + ,.raw= "POST /post_identity_body_world?q=search#hey HTTP/1.1\r\n" + "Accept: */*\r\n" + "Transfer-Encoding: identity\r\n" + "Content-Length: 5\r\n" + "\r\n" + "World" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "q=search" + ,.fragment= "hey" + ,.request_path= "/post_identity_body_world" + ,.request_url= "/post_identity_body_world?q=search#hey" + ,.num_headers= 3 + ,.headers= + { { "Accept", "*/*" } + , { "Transfer-Encoding", "identity" } + , { "Content-Length", "5" } + } + ,.body= "World" + } + +#define POST_CHUNKED_ALL_YOUR_BASE 8 +, {.name= "post - chunked body: all your base are belong to us" + ,.type= HTTP_REQUEST + ,.raw= "POST /post_chunked_all_your_base HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "1e\r\nall your base are belong to us\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/post_chunked_all_your_base" + ,.request_url= "/post_chunked_all_your_base" + ,.num_headers= 1 + ,.headers= + { { "Transfer-Encoding" , "chunked" } + } + ,.body= "all your base are belong to us" + ,.num_chunks_complete= 2 + ,.chunk_lengths= { 0x1e } + } + +#define TWO_CHUNKS_MULT_ZERO_END 9 +, {.name= "two chunks ; triple zero ending" + ,.type= HTTP_REQUEST + ,.raw= "POST /two_chunks_mult_zero_end HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "6\r\n world\r\n" + "000\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/two_chunks_mult_zero_end" + ,.request_url= "/two_chunks_mult_zero_end" + ,.num_headers= 1 + ,.headers= + { { "Transfer-Encoding", "chunked" } + } + ,.body= "hello world" + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 5, 6 } + } + +#define CHUNKED_W_TRAILING_HEADERS 10 +, {.name= "chunked with trailing headers. blech." + ,.type= HTTP_REQUEST + ,.raw= "POST /chunked_w_trailing_headers HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "6\r\n world\r\n" + "0\r\n" + "Vary: *\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/chunked_w_trailing_headers" + ,.request_url= "/chunked_w_trailing_headers" + ,.num_headers= 3 + ,.headers= + { { "Transfer-Encoding", "chunked" } + , { "Vary", "*" } + , { "Content-Type", "text/plain" } + } + ,.body= "hello world" + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 5, 6 } + } + +#define CHUNKED_W_BULLSHIT_AFTER_LENGTH 11 +, {.name= "with bullshit after the length" + ,.type= HTTP_REQUEST + ,.raw= "POST /chunked_w_bullshit_after_length HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5; ihatew3;whatthefuck=aretheseparametersfor\r\nhello\r\n" + "6; blahblah; blah\r\n world\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/chunked_w_bullshit_after_length" + ,.request_url= "/chunked_w_bullshit_after_length" + ,.num_headers= 1 + ,.headers= + { { "Transfer-Encoding", "chunked" } + } + ,.body= "hello world" + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 5, 6 } + } + +#define WITH_QUOTES 12 +, {.name= "with quotes" + ,.type= HTTP_REQUEST + ,.raw= "GET /with_\"stupid\"_quotes?foo=\"bar\" HTTP/1.1\r\n\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "foo=\"bar\"" + ,.fragment= "" + ,.request_path= "/with_\"stupid\"_quotes" + ,.request_url= "/with_\"stupid\"_quotes?foo=\"bar\"" + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define APACHEBENCH_GET 13 +/* The server receiving this request SHOULD NOT wait for EOF + * to know that content-length == 0. + * How to represent this in a unit test? message_complete_on_eof + * Compare with NO_CONTENT_LENGTH_RESPONSE. + */ +, {.name = "apachebench get" + ,.type= HTTP_REQUEST + ,.raw= "GET /test HTTP/1.0\r\n" + "Host: 0.0.0.0:5000\r\n" + "User-Agent: ApacheBench/2.3\r\n" + "Accept: */*\r\n\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/test" + ,.request_url= "/test" + ,.num_headers= 3 + ,.headers= { { "Host", "0.0.0.0:5000" } + , { "User-Agent", "ApacheBench/2.3" } + , { "Accept", "*/*" } + } + ,.body= "" + } + +#define QUERY_URL_WITH_QUESTION_MARK_GET 14 +/* Some clients include '?' characters in query strings. + */ +, {.name = "query url with question mark" + ,.type= HTTP_REQUEST + ,.raw= "GET /test.cgi?foo=bar?baz HTTP/1.1\r\n\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "foo=bar?baz" + ,.fragment= "" + ,.request_path= "/test.cgi" + ,.request_url= "/test.cgi?foo=bar?baz" + ,.num_headers= 0 + ,.headers= {} + ,.body= "" + } + +#define PREFIX_NEWLINE_GET 15 +/* Some clients, especially after a POST in a keep-alive connection, + * will send an extra CRLF before the next request + */ +, {.name = "newline prefix get" + ,.type= HTTP_REQUEST + ,.raw= "\r\nGET /test HTTP/1.1\r\n\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/test" + ,.request_url= "/test" + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define UPGRADE_REQUEST 16 +, {.name = "upgrade request" + ,.type= HTTP_REQUEST + ,.raw= "GET /demo HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\n" + "Sec-WebSocket-Protocol: sample\r\n" + "Upgrade: WebSocket\r\n" + "Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\n" + "Origin: http://example.com\r\n" + "\r\n" + "Hot diggity dogg" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/demo" + ,.request_url= "/demo" + ,.num_headers= 7 + ,.upgrade="Hot diggity dogg" + ,.headers= { { "Host", "example.com" } + , { "Connection", "Upgrade" } + , { "Sec-WebSocket-Key2", "12998 5 Y3 1 .P00" } + , { "Sec-WebSocket-Protocol", "sample" } + , { "Upgrade", "WebSocket" } + , { "Sec-WebSocket-Key1", "4 @1 46546xW%0l 1 5" } + , { "Origin", "http://example.com" } + } + ,.body= "" + } + +#define CONNECT_REQUEST 17 +, {.name = "connect request" + ,.type= HTTP_REQUEST + ,.raw= "CONNECT 0-home0.netscape.com:443 HTTP/1.0\r\n" + "User-agent: Mozilla/1.1N\r\n" + "Proxy-authorization: basic aGVsbG86d29ybGQ=\r\n" + "\r\n" + "some data\r\n" + "and yet even more data" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_CONNECT + ,.query_string= "" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "0-home0.netscape.com:443" + ,.num_headers= 2 + ,.upgrade="some data\r\nand yet even more data" + ,.headers= { { "User-agent", "Mozilla/1.1N" } + , { "Proxy-authorization", "basic aGVsbG86d29ybGQ=" } + } + ,.body= "" + } + +#define REPORT_REQ 18 +, {.name= "report request" + ,.type= HTTP_REQUEST + ,.raw= "REPORT /test HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_REPORT + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/test" + ,.request_url= "/test" + ,.num_headers= 0 + ,.headers= {} + ,.body= "" + } + +#define NO_HTTP_VERSION 19 +, {.name= "request with no http version" + ,.type= HTTP_REQUEST + ,.raw= "GET /\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 0 + ,.http_minor= 9 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.num_headers= 0 + ,.headers= {} + ,.body= "" + } + +#define MSEARCH_REQ 20 +, {.name= "m-search request" + ,.type= HTTP_REQUEST + ,.raw= "M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "MAN: \"ssdp:discover\"\r\n" + "ST: \"ssdp:all\"\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_MSEARCH + ,.query_string= "" + ,.fragment= "" + ,.request_path= "*" + ,.request_url= "*" + ,.num_headers= 3 + ,.headers= { { "HOST", "239.255.255.250:1900" } + , { "MAN", "\"ssdp:discover\"" } + , { "ST", "\"ssdp:all\"" } + } + ,.body= "" + } + +#define LINE_FOLDING_IN_HEADER 21 +, {.name= "line folding in header value" + ,.type= HTTP_REQUEST + ,.raw= "GET / HTTP/1.1\r\n" + "Line1: abc\r\n" + "\tdef\r\n" + " ghi\r\n" + "\t\tjkl\r\n" + " mno \r\n" + "\t \tqrs\r\n" + "Line2: \t line2\t\r\n" + "Line3:\r\n" + " line3\r\n" + "Line4: \r\n" + " \r\n" + "Connection:\r\n" + " close\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.num_headers= 5 + ,.headers= { { "Line1", "abc\tdef ghi\t\tjkl mno \t \tqrs" } + , { "Line2", "line2\t" } + , { "Line3", "line3" } + , { "Line4", "" } + , { "Connection", "close" }, + } + ,.body= "" + } + + +#define QUERY_TERMINATED_HOST 22 +, {.name= "host terminated by a query string" + ,.type= HTTP_REQUEST + ,.raw= "GET http://hypnotoad.org?hail=all HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "hail=all" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "http://hypnotoad.org?hail=all" + ,.host= "hypnotoad.org" + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define QUERY_TERMINATED_HOSTPORT 23 +, {.name= "host:port terminated by a query string" + ,.type= HTTP_REQUEST + ,.raw= "GET http://hypnotoad.org:1234?hail=all HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "hail=all" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "http://hypnotoad.org:1234?hail=all" + ,.host= "hypnotoad.org" + ,.port= 1234 + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define SPACE_TERMINATED_HOSTPORT 24 +, {.name= "host:port terminated by a space" + ,.type= HTTP_REQUEST + ,.raw= "GET http://hypnotoad.org:1234 HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "http://hypnotoad.org:1234" + ,.host= "hypnotoad.org" + ,.port= 1234 + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define PATCH_REQ 25 +, {.name = "PATCH request" + ,.type= HTTP_REQUEST + ,.raw= "PATCH /file.txt HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Type: application/example\r\n" + "If-Match: \"e0023aa4e\"\r\n" + "Content-Length: 10\r\n" + "\r\n" + "cccccccccc" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_PATCH + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/file.txt" + ,.request_url= "/file.txt" + ,.num_headers= 4 + ,.headers= { { "Host", "www.example.com" } + , { "Content-Type", "application/example" } + , { "If-Match", "\"e0023aa4e\"" } + , { "Content-Length", "10" } + } + ,.body= "cccccccccc" + } + +#define CONNECT_CAPS_REQUEST 26 +, {.name = "connect caps request" + ,.type= HTTP_REQUEST + ,.raw= "CONNECT HOME0.NETSCAPE.COM:443 HTTP/1.0\r\n" + "User-agent: Mozilla/1.1N\r\n" + "Proxy-authorization: basic aGVsbG86d29ybGQ=\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_CONNECT + ,.query_string= "" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "HOME0.NETSCAPE.COM:443" + ,.num_headers= 2 + ,.upgrade="" + ,.headers= { { "User-agent", "Mozilla/1.1N" } + , { "Proxy-authorization", "basic aGVsbG86d29ybGQ=" } + } + ,.body= "" + } + +#if !HTTP_PARSER_STRICT +#define UTF8_PATH_REQ 27 +, {.name= "utf-8 path request" + ,.type= HTTP_REQUEST + ,.raw= "GET /δ¶/δt/pope?q=1#narf HTTP/1.1\r\n" + "Host: github.com\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "q=1" + ,.fragment= "narf" + ,.request_path= "/δ¶/δt/pope" + ,.request_url= "/δ¶/δt/pope?q=1#narf" + ,.num_headers= 1 + ,.headers= { {"Host", "github.com" } + } + ,.body= "" + } + +#define HOSTNAME_UNDERSCORE 28 +, {.name = "hostname underscore" + ,.type= HTTP_REQUEST + ,.raw= "CONNECT home_0.netscape.com:443 HTTP/1.0\r\n" + "User-agent: Mozilla/1.1N\r\n" + "Proxy-authorization: basic aGVsbG86d29ybGQ=\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_CONNECT + ,.query_string= "" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "home_0.netscape.com:443" + ,.num_headers= 2 + ,.upgrade="" + ,.headers= { { "User-agent", "Mozilla/1.1N" } + , { "Proxy-authorization", "basic aGVsbG86d29ybGQ=" } + } + ,.body= "" + } +#endif /* !HTTP_PARSER_STRICT */ + +/* see https://github.com/ry/http-parser/issues/47 */ +#define EAT_TRAILING_CRLF_NO_CONNECTION_CLOSE 29 +, {.name = "eat CRLF between requests, no \"Connection: close\" header" + ,.raw= "POST / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: 4\r\n" + "\r\n" + "q=42\r\n" /* note the trailing CRLF */ + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.num_headers= 3 + ,.upgrade= 0 + ,.headers= { { "Host", "www.example.com" } + , { "Content-Type", "application/x-www-form-urlencoded" } + , { "Content-Length", "4" } + } + ,.body= "q=42" + } + +/* see https://github.com/ry/http-parser/issues/47 */ +#define EAT_TRAILING_CRLF_WITH_CONNECTION_CLOSE 30 +, {.name = "eat CRLF between requests even if \"Connection: close\" is set" + ,.raw= "POST / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: 4\r\n" + "Connection: close\r\n" + "\r\n" + "q=42\r\n" /* note the trailing CRLF */ + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE /* input buffer isn't empty when on_message_complete is called */ + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.num_headers= 4 + ,.upgrade= 0 + ,.headers= { { "Host", "www.example.com" } + , { "Content-Type", "application/x-www-form-urlencoded" } + , { "Content-Length", "4" } + , { "Connection", "close" } + } + ,.body= "q=42" + } + +#define PURGE_REQ 31 +, {.name = "PURGE request" + ,.type= HTTP_REQUEST + ,.raw= "PURGE /file.txt HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_PURGE + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/file.txt" + ,.request_url= "/file.txt" + ,.num_headers= 1 + ,.headers= { { "Host", "www.example.com" } } + ,.body= "" + } + +#define SEARCH_REQ 32 +, {.name = "SEARCH request" + ,.type= HTTP_REQUEST + ,.raw= "SEARCH / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_SEARCH + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.num_headers= 1 + ,.headers= { { "Host", "www.example.com" } } + ,.body= "" + } + +#define PROXY_WITH_BASIC_AUTH 33 +, {.name= "host:port and basic_auth" + ,.type= HTTP_REQUEST + ,.raw= "GET http://a%12:b!&*$@hypnotoad.org:1234/toto HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.fragment= "" + ,.request_path= "/toto" + ,.request_url= "http://a%12:b!&*$@hypnotoad.org:1234/toto" + ,.host= "hypnotoad.org" + ,.userinfo= "a%12:b!&*$" + ,.port= 1234 + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define LINE_FOLDING_IN_HEADER_WITH_LF 34 +, {.name= "line folding in header value" + ,.type= HTTP_REQUEST + ,.raw= "GET / HTTP/1.1\n" + "Line1: abc\n" + "\tdef\n" + " ghi\n" + "\t\tjkl\n" + " mno \n" + "\t \tqrs\n" + "Line2: \t line2\t\n" + "Line3:\n" + " line3\n" + "Line4: \n" + " \n" + "Connection:\n" + " close\n" + "\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.num_headers= 5 + ,.headers= { { "Line1", "abc\tdef ghi\t\tjkl mno \t \tqrs" } + , { "Line2", "line2\t" } + , { "Line3", "line3" } + , { "Line4", "" } + , { "Connection", "close" }, + } + ,.body= "" + } + +#define CONNECTION_MULTI 35 +, {.name = "multiple connection header values with folding" + ,.type= HTTP_REQUEST + ,.raw= "GET /demo HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: Something,\r\n" + " Upgrade, ,Keep-Alive\r\n" + "Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\n" + "Sec-WebSocket-Protocol: sample\r\n" + "Upgrade: WebSocket\r\n" + "Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\n" + "Origin: http://example.com\r\n" + "\r\n" + "Hot diggity dogg" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/demo" + ,.request_url= "/demo" + ,.num_headers= 7 + ,.upgrade="Hot diggity dogg" + ,.headers= { { "Host", "example.com" } + , { "Connection", "Something, Upgrade, ,Keep-Alive" } + , { "Sec-WebSocket-Key2", "12998 5 Y3 1 .P00" } + , { "Sec-WebSocket-Protocol", "sample" } + , { "Upgrade", "WebSocket" } + , { "Sec-WebSocket-Key1", "4 @1 46546xW%0l 1 5" } + , { "Origin", "http://example.com" } + } + ,.body= "" + } + +#define CONNECTION_MULTI_LWS 36 +, {.name = "multiple connection header values with folding and lws" + ,.type= HTTP_REQUEST + ,.raw= "GET /demo HTTP/1.1\r\n" + "Connection: keep-alive, upgrade\r\n" + "Upgrade: WebSocket\r\n" + "\r\n" + "Hot diggity dogg" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/demo" + ,.request_url= "/demo" + ,.num_headers= 2 + ,.upgrade="Hot diggity dogg" + ,.headers= { { "Connection", "keep-alive, upgrade" } + , { "Upgrade", "WebSocket" } + } + ,.body= "" + } + +#define CONNECTION_MULTI_LWS_CRLF 37 +, {.name = "multiple connection header values with folding and lws" + ,.type= HTTP_REQUEST + ,.raw= "GET /demo HTTP/1.1\r\n" + "Connection: keep-alive, \r\n upgrade\r\n" + "Upgrade: WebSocket\r\n" + "\r\n" + "Hot diggity dogg" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/demo" + ,.request_url= "/demo" + ,.num_headers= 2 + ,.upgrade="Hot diggity dogg" + ,.headers= { { "Connection", "keep-alive, upgrade" } + , { "Upgrade", "WebSocket" } + } + ,.body= "" + } + +#define UPGRADE_POST_REQUEST 38 +, {.name = "upgrade post request" + ,.type= HTTP_REQUEST + ,.raw= "POST /demo HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: Upgrade\r\n" + "Upgrade: HTTP/2.0\r\n" + "Content-Length: 15\r\n" + "\r\n" + "sweet post body" + "Hot diggity dogg" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.request_path= "/demo" + ,.request_url= "/demo" + ,.num_headers= 4 + ,.upgrade="Hot diggity dogg" + ,.headers= { { "Host", "example.com" } + , { "Connection", "Upgrade" } + , { "Upgrade", "HTTP/2.0" } + , { "Content-Length", "15" } + } + ,.body= "sweet post body" + } + +#define CONNECT_WITH_BODY_REQUEST 39 +, {.name = "connect with body request" + ,.type= HTTP_REQUEST + ,.raw= "CONNECT foo.bar.com:443 HTTP/1.0\r\n" + "User-agent: Mozilla/1.1N\r\n" + "Proxy-authorization: basic aGVsbG86d29ybGQ=\r\n" + "Content-Length: 10\r\n" + "\r\n" + "blarfcicle" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_CONNECT + ,.request_url= "foo.bar.com:443" + ,.num_headers= 3 + ,.upgrade="blarfcicle" + ,.headers= { { "User-agent", "Mozilla/1.1N" } + , { "Proxy-authorization", "basic aGVsbG86d29ybGQ=" } + , { "Content-Length", "10" } + } + ,.body= "" + } + +/* Examples from the Internet draft for LINK/UNLINK methods: + * https://tools.ietf.org/id/draft-snell-link-method-01.html#rfc.section.5 + */ + +#define LINK_REQUEST 40 +, {.name = "link request" + ,.type= HTTP_REQUEST + ,.raw= "LINK /images/my_dog.jpg HTTP/1.1\r\n" + "Host: example.com\r\n" + "Link: ; rel=\"tag\"\r\n" + "Link: ; rel=\"tag\"\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_LINK + ,.request_path= "/images/my_dog.jpg" + ,.request_url= "/images/my_dog.jpg" + ,.query_string= "" + ,.fragment= "" + ,.num_headers= 3 + ,.headers= { { "Host", "example.com" } + , { "Link", "; rel=\"tag\"" } + , { "Link", "; rel=\"tag\"" } + } + ,.body= "" + } + +#define UNLINK_REQUEST 41 +, {.name = "unlink request" + ,.type= HTTP_REQUEST + ,.raw= "UNLINK /images/my_dog.jpg HTTP/1.1\r\n" + "Host: example.com\r\n" + "Link: ; rel=\"tag\"\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_UNLINK + ,.request_path= "/images/my_dog.jpg" + ,.request_url= "/images/my_dog.jpg" + ,.query_string= "" + ,.fragment= "" + ,.num_headers= 2 + ,.headers= { { "Host", "example.com" } + , { "Link", "; rel=\"tag\"" } + } + ,.body= "" + } + +, {.name= NULL } /* sentinel */ +}; + +/* * R E S P O N S E S * */ +const struct message responses[] = +#define GOOGLE_301 0 +{ {.name= "google 301" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 301 Moved Permanently\r\n" + "Location: http://www.google.com/\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "Date: Sun, 26 Apr 2009 11:11:49 GMT\r\n" + "Expires: Tue, 26 May 2009 11:11:49 GMT\r\n" + "X-$PrototypeBI-Version: 1.6.0.3\r\n" /* $ char in header field */ + "Cache-Control: public, max-age=2592000\r\n" + "Server: gws\r\n" + "Content-Length: 219 \r\n" + "\r\n" + "\n" + "301 Moved\n" + "

301 Moved

\n" + "The document has moved\n" + "here.\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 301 + ,.response_status= "Moved Permanently" + ,.num_headers= 8 + ,.headers= + { { "Location", "http://www.google.com/" } + , { "Content-Type", "text/html; charset=UTF-8" } + , { "Date", "Sun, 26 Apr 2009 11:11:49 GMT" } + , { "Expires", "Tue, 26 May 2009 11:11:49 GMT" } + , { "X-$PrototypeBI-Version", "1.6.0.3" } + , { "Cache-Control", "public, max-age=2592000" } + , { "Server", "gws" } + , { "Content-Length", "219 " } + } + ,.body= "\n" + "301 Moved\n" + "

301 Moved

\n" + "The document has moved\n" + "here.\r\n" + "\r\n" + } + +#define NO_CONTENT_LENGTH_RESPONSE 1 +/* The client should wait for the server's EOF. That is, when content-length + * is not specified, and "Connection: close", the end of body is specified + * by the EOF. + * Compare with APACHEBENCH_GET + */ +, {.name= "no content-length response" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Date: Tue, 04 Aug 2009 07:59:32 GMT\r\n" + "Server: Apache\r\n" + "X-Powered-By: Servlet/2.5 JSP/2.1\r\n" + "Content-Type: text/xml; charset=utf-8\r\n" + "Connection: close\r\n" + "\r\n" + "\n" + "\n" + " \n" + " \n" + " SOAP-ENV:Client\n" + " Client Error\n" + " \n" + " \n" + "" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 5 + ,.headers= + { { "Date", "Tue, 04 Aug 2009 07:59:32 GMT" } + , { "Server", "Apache" } + , { "X-Powered-By", "Servlet/2.5 JSP/2.1" } + , { "Content-Type", "text/xml; charset=utf-8" } + , { "Connection", "close" } + } + ,.body= "\n" + "\n" + " \n" + " \n" + " SOAP-ENV:Client\n" + " Client Error\n" + " \n" + " \n" + "" + } + +#define NO_HEADERS_NO_BODY_404 2 +, {.name= "404 no headers no body" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 404 Not Found\r\n\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 404 + ,.response_status= "Not Found" + ,.num_headers= 0 + ,.headers= {} + ,.body_size= 0 + ,.body= "" + } + +#define NO_REASON_PHRASE 3 +, {.name= "301 no response phrase" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 301\r\n\r\n" + ,.should_keep_alive = FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 301 + ,.response_status= "" + ,.num_headers= 0 + ,.headers= {} + ,.body= "" + } + +#define TRAILING_SPACE_ON_CHUNKED_BODY 4 +, {.name="200 trailing space on chunked body" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "25 \r\n" + "This is the data in the first chunk\r\n" + "\r\n" + "1C\r\n" + "and this is the second one\r\n" + "\r\n" + "0 \r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 2 + ,.headers= + { {"Content-Type", "text/plain" } + , {"Transfer-Encoding", "chunked" } + } + ,.body_size = 37+28 + ,.body = + "This is the data in the first chunk\r\n" + "and this is the second one\r\n" + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 0x25, 0x1c } + } + +#define NO_CARRIAGE_RET 5 +, {.name="no carriage ret" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\n" + "Content-Type: text/html; charset=utf-8\n" + "Connection: close\n" + "\n" + "these headers are from http://news.ycombinator.com/" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 2 + ,.headers= + { {"Content-Type", "text/html; charset=utf-8" } + , {"Connection", "close" } + } + ,.body= "these headers are from http://news.ycombinator.com/" + } + +#define PROXY_CONNECTION 6 +, {.name="proxy connection" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "Content-Length: 11\r\n" + "Proxy-Connection: close\r\n" + "Date: Thu, 31 Dec 2009 20:55:48 +0000\r\n" + "\r\n" + "hello world" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 4 + ,.headers= + { {"Content-Type", "text/html; charset=UTF-8" } + , {"Content-Length", "11" } + , {"Proxy-Connection", "close" } + , {"Date", "Thu, 31 Dec 2009 20:55:48 +0000"} + } + ,.body= "hello world" + } + +#define UNDERSTORE_HEADER_KEY 7 + // shown by + // curl -o /dev/null -v "http://ad.doubleclick.net/pfadx/DARTSHELLCONFIGXML;dcmt=text/xml;" +, {.name="underscore header key" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Server: DCLK-AdSvr\r\n" + "Content-Type: text/xml\r\n" + "Content-Length: 0\r\n" + "DCLK_imp: v7;x;114750856;0-0;0;17820020;0/0;21603567/21621457/1;;~okv=;dcmt=text/xml;;~cs=o\r\n\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 4 + ,.headers= + { {"Server", "DCLK-AdSvr" } + , {"Content-Type", "text/xml" } + , {"Content-Length", "0" } + , {"DCLK_imp", "v7;x;114750856;0-0;0;17820020;0/0;21603567/21621457/1;;~okv=;dcmt=text/xml;;~cs=o" } + } + ,.body= "" + } + +#define BONJOUR_MADAME_FR 8 +/* The client should not merge two headers fields when the first one doesn't + * have a value. + */ +, {.name= "bonjourmadame.fr" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.0 301 Moved Permanently\r\n" + "Date: Thu, 03 Jun 2010 09:56:32 GMT\r\n" + "Server: Apache/2.2.3 (Red Hat)\r\n" + "Cache-Control: public\r\n" + "Pragma: \r\n" + "Location: http://www.bonjourmadame.fr/\r\n" + "Vary: Accept-Encoding\r\n" + "Content-Length: 0\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "Connection: keep-alive\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.status_code= 301 + ,.response_status= "Moved Permanently" + ,.num_headers= 9 + ,.headers= + { { "Date", "Thu, 03 Jun 2010 09:56:32 GMT" } + , { "Server", "Apache/2.2.3 (Red Hat)" } + , { "Cache-Control", "public" } + , { "Pragma", "" } + , { "Location", "http://www.bonjourmadame.fr/" } + , { "Vary", "Accept-Encoding" } + , { "Content-Length", "0" } + , { "Content-Type", "text/html; charset=UTF-8" } + , { "Connection", "keep-alive" } + } + ,.body= "" + } + +#define RES_FIELD_UNDERSCORE 9 +/* Should handle spaces in header fields */ +, {.name= "field underscore" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Date: Tue, 28 Sep 2010 01:14:13 GMT\r\n" + "Server: Apache\r\n" + "Cache-Control: no-cache, must-revalidate\r\n" + "Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n" + ".et-Cookie: PlaxoCS=1274804622353690521; path=/; domain=.plaxo.com\r\n" + "Vary: Accept-Encoding\r\n" + "_eep-Alive: timeout=45\r\n" /* semantic value ignored */ + "_onnection: Keep-Alive\r\n" /* semantic value ignored */ + "Transfer-Encoding: chunked\r\n" + "Content-Type: text/html\r\n" + "Connection: close\r\n" + "\r\n" + "0\r\n\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 11 + ,.headers= + { { "Date", "Tue, 28 Sep 2010 01:14:13 GMT" } + , { "Server", "Apache" } + , { "Cache-Control", "no-cache, must-revalidate" } + , { "Expires", "Mon, 26 Jul 1997 05:00:00 GMT" } + , { ".et-Cookie", "PlaxoCS=1274804622353690521; path=/; domain=.plaxo.com" } + , { "Vary", "Accept-Encoding" } + , { "_eep-Alive", "timeout=45" } + , { "_onnection", "Keep-Alive" } + , { "Transfer-Encoding", "chunked" } + , { "Content-Type", "text/html" } + , { "Connection", "close" } + } + ,.body= "" + ,.num_chunks_complete= 1 + ,.chunk_lengths= {} + } + +#define NON_ASCII_IN_STATUS_LINE 10 +/* Should handle non-ASCII in status line */ +, {.name= "non-ASCII in status line" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 500 Oriëntatieprobleem\r\n" + "Date: Fri, 5 Nov 2010 23:07:12 GMT+2\r\n" + "Content-Length: 0\r\n" + "Connection: close\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 500 + ,.response_status= "Oriëntatieprobleem" + ,.num_headers= 3 + ,.headers= + { { "Date", "Fri, 5 Nov 2010 23:07:12 GMT+2" } + , { "Content-Length", "0" } + , { "Connection", "close" } + } + ,.body= "" + } + +#define HTTP_VERSION_0_9 11 +/* Should handle HTTP/0.9 */ +, {.name= "http version 0.9" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/0.9 200 OK\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 0 + ,.http_minor= 9 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 0 + ,.headers= + {} + ,.body= "" + } + +#define NO_CONTENT_LENGTH_NO_TRANSFER_ENCODING_RESPONSE 12 +/* The client should wait for the server's EOF. That is, when neither + * content-length nor transfer-encoding is specified, the end of body + * is specified by the EOF. + */ +, {.name= "neither content-length nor transfer-encoding response" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + "hello world" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 1 + ,.headers= + { { "Content-Type", "text/plain" } + } + ,.body= "hello world" + } + +#define NO_BODY_HTTP10_KA_200 13 +, {.name= "HTTP/1.0 with keep-alive and EOF-terminated 200 status" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.0 200 OK\r\n" + "Connection: keep-alive\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 0 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 1 + ,.headers= + { { "Connection", "keep-alive" } + } + ,.body_size= 0 + ,.body= "" + } + +#define NO_BODY_HTTP10_KA_204 14 +, {.name= "HTTP/1.0 with keep-alive and a 204 status" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.0 204 No content\r\n" + "Connection: keep-alive\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.status_code= 204 + ,.response_status= "No content" + ,.num_headers= 1 + ,.headers= + { { "Connection", "keep-alive" } + } + ,.body_size= 0 + ,.body= "" + } + +#define NO_BODY_HTTP11_KA_200 15 +, {.name= "HTTP/1.1 with an EOF-terminated 200 status" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 0 + ,.headers={} + ,.body_size= 0 + ,.body= "" + } + +#define NO_BODY_HTTP11_KA_204 16 +, {.name= "HTTP/1.1 with a 204 status" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 204 No content\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 204 + ,.response_status= "No content" + ,.num_headers= 0 + ,.headers={} + ,.body_size= 0 + ,.body= "" + } + +#define NO_BODY_HTTP11_NOKA_204 17 +, {.name= "HTTP/1.1 with a 204 status and keep-alive disabled" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 204 No content\r\n" + "Connection: close\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 204 + ,.response_status= "No content" + ,.num_headers= 1 + ,.headers= + { { "Connection", "close" } + } + ,.body_size= 0 + ,.body= "" + } + +#define NO_BODY_HTTP11_KA_CHUNKED_200 18 +, {.name= "HTTP/1.1 with chunked endocing and a 200 response" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 1 + ,.headers= + { { "Transfer-Encoding", "chunked" } + } + ,.body_size= 0 + ,.body= "" + ,.num_chunks_complete= 1 + } + +#if !HTTP_PARSER_STRICT +#define SPACE_IN_FIELD_RES 19 +/* Should handle spaces in header fields */ +, {.name= "field space" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Server: Microsoft-IIS/6.0\r\n" + "X-Powered-By: ASP.NET\r\n" + "en-US Content-Type: text/xml\r\n" /* this is the problem */ + "Content-Type: text/xml\r\n" + "Content-Length: 16\r\n" + "Date: Fri, 23 Jul 2010 18:45:38 GMT\r\n" + "Connection: keep-alive\r\n" + "\r\n" + "hello" /* fake body */ + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 7 + ,.headers= + { { "Server", "Microsoft-IIS/6.0" } + , { "X-Powered-By", "ASP.NET" } + , { "en-US Content-Type", "text/xml" } + , { "Content-Type", "text/xml" } + , { "Content-Length", "16" } + , { "Date", "Fri, 23 Jul 2010 18:45:38 GMT" } + , { "Connection", "keep-alive" } + } + ,.body= "hello" + } +#endif /* !HTTP_PARSER_STRICT */ + +#define AMAZON_COM 20 +, {.name= "amazon.com" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 301 MovedPermanently\r\n" + "Date: Wed, 15 May 2013 17:06:33 GMT\r\n" + "Server: Server\r\n" + "x-amz-id-1: 0GPHKXSJQ826RK7GZEB2\r\n" + "p3p: policyref=\"http://www.amazon.com/w3c/p3p.xml\",CP=\"CAO DSP LAW CUR ADM IVAo IVDo CONo OTPo OUR DELi PUBi OTRi BUS PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA HEA PRE LOC GOV OTC \"\r\n" + "x-amz-id-2: STN69VZxIFSz9YJLbz1GDbxpbjG6Qjmmq5E3DxRhOUw+Et0p4hr7c/Q8qNcx4oAD\r\n" + "Location: http://www.amazon.com/Dan-Brown/e/B000AP9DSU/ref=s9_pop_gw_al1?_encoding=UTF8&refinementId=618073011&pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-2&pf_rd_r=0SHYY5BZXN3KR20BNFAY&pf_rd_t=101&pf_rd_p=1263340922&pf_rd_i=507846\r\n" + "Vary: Accept-Encoding,User-Agent\r\n" + "Content-Type: text/html; charset=ISO-8859-1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "1\r\n" + "\n\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 301 + ,.response_status= "MovedPermanently" + ,.num_headers= 9 + ,.headers= { { "Date", "Wed, 15 May 2013 17:06:33 GMT" } + , { "Server", "Server" } + , { "x-amz-id-1", "0GPHKXSJQ826RK7GZEB2" } + , { "p3p", "policyref=\"http://www.amazon.com/w3c/p3p.xml\",CP=\"CAO DSP LAW CUR ADM IVAo IVDo CONo OTPo OUR DELi PUBi OTRi BUS PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA HEA PRE LOC GOV OTC \"" } + , { "x-amz-id-2", "STN69VZxIFSz9YJLbz1GDbxpbjG6Qjmmq5E3DxRhOUw+Et0p4hr7c/Q8qNcx4oAD" } + , { "Location", "http://www.amazon.com/Dan-Brown/e/B000AP9DSU/ref=s9_pop_gw_al1?_encoding=UTF8&refinementId=618073011&pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-2&pf_rd_r=0SHYY5BZXN3KR20BNFAY&pf_rd_t=101&pf_rd_p=1263340922&pf_rd_i=507846" } + , { "Vary", "Accept-Encoding,User-Agent" } + , { "Content-Type", "text/html; charset=ISO-8859-1" } + , { "Transfer-Encoding", "chunked" } + } + ,.body= "\n" + ,.num_chunks_complete= 2 + ,.chunk_lengths= { 1 } + } + +#define EMPTY_REASON_PHRASE_AFTER_SPACE 20 +, {.name= "empty reason phrase after space" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 \r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "" + ,.num_headers= 0 + ,.headers= {} + ,.body= "" + } + +#define CONTENT_LENGTH_X 21 +, {.name= "Content-Length-X" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Content-Length-X: 0\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\n" + "OK\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 2 + ,.headers= { { "Content-Length-X", "0" } + , { "Transfer-Encoding", "chunked" } + } + ,.body= "OK" + ,.num_chunks_complete= 2 + ,.chunk_lengths= { 2 } + } + +#define HTTP_101_RESPONSE_WITH_UPGRADE_HEADER 22 +, {.name= "HTTP 101 response with Upgrade header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "\r\n" + "proto" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 101 + ,.response_status= "Switching Protocols" + ,.upgrade= "proto" + ,.num_headers= 2 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + } + } + +#define HTTP_101_RESPONSE_WITH_UPGRADE_HEADER_AND_CONTENT_LENGTH 23 +, {.name= "HTTP 101 response with Upgrade and Content-Length header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Content-Length: 4\r\n" + "\r\n" + "body" + "proto" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 101 + ,.response_status= "Switching Protocols" + ,.body= "body" + ,.upgrade= "proto" + ,.num_headers= 3 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Content-Length", "4" } + } + } + +#define HTTP_101_RESPONSE_WITH_UPGRADE_HEADER_AND_TRANSFER_ENCODING 24 +, {.name= "HTTP 101 response with Upgrade and Transfer-Encoding header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\n" + "bo\r\n" + "2\r\n" + "dy\r\n" + "0\r\n" + "\r\n" + "proto" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 101 + ,.response_status= "Switching Protocols" + ,.body= "body" + ,.upgrade= "proto" + ,.num_headers= 3 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Transfer-Encoding", "chunked" } + } + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 2, 2 } + } + +#define HTTP_200_RESPONSE_WITH_UPGRADE_HEADER 25 +, {.name= "HTTP 200 response with Upgrade header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "\r\n" + "body" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.body= "body" + ,.upgrade= NULL + ,.num_headers= 2 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + } + } + +#define HTTP_200_RESPONSE_WITH_UPGRADE_HEADER_AND_CONTENT_LENGTH 26 +, {.name= "HTTP 200 response with Upgrade and Content-Length header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Content-Length: 4\r\n" + "\r\n" + "body" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 3 + ,.body= "body" + ,.upgrade= NULL + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Content-Length", "4" } + } + } + +#define HTTP_200_RESPONSE_WITH_UPGRADE_HEADER_AND_TRANSFER_ENCODING 27 +, {.name= "HTTP 200 response with Upgrade and Transfer-Encoding header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\n" + "bo\r\n" + "2\r\n" + "dy\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 3 + ,.body= "body" + ,.upgrade= NULL + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Transfer-Encoding", "chunked" } + } + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 2, 2 } + } + +, {.name= NULL } /* sentinel */ +}; + +/* strnlen() is a POSIX.2008 addition. Can't rely on it being available so + * define it ourselves. + */ +size_t +strnlen(const char *s, size_t maxlen) +{ + const char *p; + + p = memchr(s, '\0', maxlen); + if (p == NULL) + return maxlen; + + return p - s; +} + +size_t +strlncat(char *dst, size_t len, const char *src, size_t n) +{ + size_t slen; + size_t dlen; + size_t rlen; + size_t ncpy; + + slen = strnlen(src, n); + dlen = strnlen(dst, len); + + if (dlen < len) { + rlen = len - dlen; + ncpy = slen < rlen ? slen : (rlen - 1); + memcpy(dst + dlen, src, ncpy); + dst[dlen + ncpy] = '\0'; + } + + assert(len > slen + dlen); + return slen + dlen; +} + +size_t +strlcat(char *dst, const char *src, size_t len) +{ + return strlncat(dst, len, src, (size_t) -1); +} + +size_t +strlncpy(char *dst, size_t len, const char *src, size_t n) +{ + size_t slen; + size_t ncpy; + + slen = strnlen(src, n); + + if (len > 0) { + ncpy = slen < len ? slen : (len - 1); + memcpy(dst, src, ncpy); + dst[ncpy] = '\0'; + } + + assert(len > slen); + return slen; +} + +size_t +strlcpy(char *dst, const char *src, size_t len) +{ + return strlncpy(dst, len, src, (size_t) -1); +} + +int +request_url_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == parser); + strlncat(messages[num_messages].request_url, + sizeof(messages[num_messages].request_url), + buf, + len); + return 0; +} + +int +header_field_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == parser); + struct message *m = &messages[num_messages]; + + if (m->last_header_element != FIELD) + m->num_headers++; + + strlncat(m->headers[m->num_headers-1][0], + sizeof(m->headers[m->num_headers-1][0]), + buf, + len); + + m->last_header_element = FIELD; + + return 0; +} + +int +header_value_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == parser); + struct message *m = &messages[num_messages]; + + strlncat(m->headers[m->num_headers-1][1], + sizeof(m->headers[m->num_headers-1][1]), + buf, + len); + + m->last_header_element = VALUE; + + return 0; +} + +void +check_body_is_final (const http_parser *p) +{ + if (messages[num_messages].body_is_final) { + fprintf(stderr, "\n\n *** Error http_body_is_final() should return 1 " + "on last on_body callback call " + "but it doesn't! ***\n\n"); + assert(0); + abort(); + } + messages[num_messages].body_is_final = http_body_is_final(p); +} + +int +body_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == parser); + strlncat(messages[num_messages].body, + sizeof(messages[num_messages].body), + buf, + len); + messages[num_messages].body_size += len; + check_body_is_final(p); + // printf("body_cb: '%s'\n", requests[num_messages].body); + return 0; +} + +int +count_body_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == parser); + assert(buf); + messages[num_messages].body_size += len; + check_body_is_final(p); + return 0; +} + +int +message_begin_cb (http_parser *p) +{ + assert(p == parser); + messages[num_messages].message_begin_cb_called = TRUE; + return 0; +} + +int +headers_complete_cb (http_parser *p) +{ + assert(p == parser); + messages[num_messages].method = parser->method; + messages[num_messages].status_code = parser->status_code; + messages[num_messages].http_major = parser->http_major; + messages[num_messages].http_minor = parser->http_minor; + messages[num_messages].headers_complete_cb_called = TRUE; + messages[num_messages].should_keep_alive = http_should_keep_alive(parser); + return 0; +} + +int +message_complete_cb (http_parser *p) +{ + assert(p == parser); + if (messages[num_messages].should_keep_alive != http_should_keep_alive(parser)) + { + fprintf(stderr, "\n\n *** Error http_should_keep_alive() should have same " + "value in both on_message_complete and on_headers_complete " + "but it doesn't! ***\n\n"); + assert(0); + abort(); + } + + if (messages[num_messages].body_size && + http_body_is_final(p) && + !messages[num_messages].body_is_final) + { + fprintf(stderr, "\n\n *** Error http_body_is_final() should return 1 " + "on last on_body callback call " + "but it doesn't! ***\n\n"); + assert(0); + abort(); + } + + messages[num_messages].message_complete_cb_called = TRUE; + + messages[num_messages].message_complete_on_eof = currently_parsing_eof; + + num_messages++; + return 0; +} + +int +response_status_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == parser); + + messages[num_messages].status_cb_called = TRUE; + + strlncat(messages[num_messages].response_status, + sizeof(messages[num_messages].response_status), + buf, + len); + return 0; +} + +int +chunk_header_cb (http_parser *p) +{ + assert(p == parser); + int chunk_idx = messages[num_messages].num_chunks; + messages[num_messages].num_chunks++; + if (chunk_idx < MAX_CHUNKS) { + messages[num_messages].chunk_lengths[chunk_idx] = p->content_length; + } + + return 0; +} + +int +chunk_complete_cb (http_parser *p) +{ + assert(p == parser); + + /* Here we want to verify that each chunk_header_cb is matched by a + * chunk_complete_cb, so not only should the total number of calls to + * both callbacks be the same, but they also should be interleaved + * properly */ + assert(messages[num_messages].num_chunks == + messages[num_messages].num_chunks_complete + 1); + + messages[num_messages].num_chunks_complete++; + return 0; +} + +/* These dontcall_* callbacks exist so that we can verify that when we're + * paused, no additional callbacks are invoked */ +int +dontcall_message_begin_cb (http_parser *p) +{ + if (p) { } // gcc + fprintf(stderr, "\n\n*** on_message_begin() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_header_field_cb (http_parser *p, const char *buf, size_t len) +{ + if (p || buf || len) { } // gcc + fprintf(stderr, "\n\n*** on_header_field() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_header_value_cb (http_parser *p, const char *buf, size_t len) +{ + if (p || buf || len) { } // gcc + fprintf(stderr, "\n\n*** on_header_value() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_request_url_cb (http_parser *p, const char *buf, size_t len) +{ + if (p || buf || len) { } // gcc + fprintf(stderr, "\n\n*** on_request_url() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_body_cb (http_parser *p, const char *buf, size_t len) +{ + if (p || buf || len) { } // gcc + fprintf(stderr, "\n\n*** on_body_cb() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_headers_complete_cb (http_parser *p) +{ + if (p) { } // gcc + fprintf(stderr, "\n\n*** on_headers_complete() called on paused " + "parser ***\n\n"); + abort(); +} + +int +dontcall_message_complete_cb (http_parser *p) +{ + if (p) { } // gcc + fprintf(stderr, "\n\n*** on_message_complete() called on paused " + "parser ***\n\n"); + abort(); +} + +int +dontcall_response_status_cb (http_parser *p, const char *buf, size_t len) +{ + if (p || buf || len) { } // gcc + fprintf(stderr, "\n\n*** on_status() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_chunk_header_cb (http_parser *p) +{ + if (p) { } // gcc + fprintf(stderr, "\n\n*** on_chunk_header() called on paused parser ***\n\n"); + exit(1); +} + +int +dontcall_chunk_complete_cb (http_parser *p) +{ + if (p) { } // gcc + fprintf(stderr, "\n\n*** on_chunk_complete() " + "called on paused parser ***\n\n"); + exit(1); +} + +static http_parser_settings settings_dontcall = + {.on_message_begin = dontcall_message_begin_cb + ,.on_header_field = dontcall_header_field_cb + ,.on_header_value = dontcall_header_value_cb + ,.on_url = dontcall_request_url_cb + ,.on_status = dontcall_response_status_cb + ,.on_body = dontcall_body_cb + ,.on_headers_complete = dontcall_headers_complete_cb + ,.on_message_complete = dontcall_message_complete_cb + ,.on_chunk_header = dontcall_chunk_header_cb + ,.on_chunk_complete = dontcall_chunk_complete_cb + }; + +/* These pause_* callbacks always pause the parser and just invoke the regular + * callback that tracks content. Before returning, we overwrite the parser + * settings to point to the _dontcall variety so that we can verify that + * the pause actually did, you know, pause. */ +int +pause_message_begin_cb (http_parser *p) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return message_begin_cb(p); +} + +int +pause_header_field_cb (http_parser *p, const char *buf, size_t len) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return header_field_cb(p, buf, len); +} + +int +pause_header_value_cb (http_parser *p, const char *buf, size_t len) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return header_value_cb(p, buf, len); +} + +int +pause_request_url_cb (http_parser *p, const char *buf, size_t len) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return request_url_cb(p, buf, len); +} + +int +pause_body_cb (http_parser *p, const char *buf, size_t len) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return body_cb(p, buf, len); +} + +int +pause_headers_complete_cb (http_parser *p) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return headers_complete_cb(p); +} + +int +pause_message_complete_cb (http_parser *p) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return message_complete_cb(p); +} + +int +pause_response_status_cb (http_parser *p, const char *buf, size_t len) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return response_status_cb(p, buf, len); +} + +int +pause_chunk_header_cb (http_parser *p) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return chunk_header_cb(p); +} + +int +pause_chunk_complete_cb (http_parser *p) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return chunk_complete_cb(p); +} + +int +connect_headers_complete_cb (http_parser *p) +{ + headers_complete_cb(p); + return 1; +} + +int +connect_message_complete_cb (http_parser *p) +{ + messages[num_messages].should_keep_alive = http_should_keep_alive(parser); + return message_complete_cb(p); +} + +static http_parser_settings settings_pause = + {.on_message_begin = pause_message_begin_cb + ,.on_header_field = pause_header_field_cb + ,.on_header_value = pause_header_value_cb + ,.on_url = pause_request_url_cb + ,.on_status = pause_response_status_cb + ,.on_body = pause_body_cb + ,.on_headers_complete = pause_headers_complete_cb + ,.on_message_complete = pause_message_complete_cb + ,.on_chunk_header = pause_chunk_header_cb + ,.on_chunk_complete = pause_chunk_complete_cb + }; + +static http_parser_settings settings = + {.on_message_begin = message_begin_cb + ,.on_header_field = header_field_cb + ,.on_header_value = header_value_cb + ,.on_url = request_url_cb + ,.on_status = response_status_cb + ,.on_body = body_cb + ,.on_headers_complete = headers_complete_cb + ,.on_message_complete = message_complete_cb + ,.on_chunk_header = chunk_header_cb + ,.on_chunk_complete = chunk_complete_cb + }; + +static http_parser_settings settings_count_body = + {.on_message_begin = message_begin_cb + ,.on_header_field = header_field_cb + ,.on_header_value = header_value_cb + ,.on_url = request_url_cb + ,.on_status = response_status_cb + ,.on_body = count_body_cb + ,.on_headers_complete = headers_complete_cb + ,.on_message_complete = message_complete_cb + ,.on_chunk_header = chunk_header_cb + ,.on_chunk_complete = chunk_complete_cb + }; + +static http_parser_settings settings_connect = + {.on_message_begin = message_begin_cb + ,.on_header_field = header_field_cb + ,.on_header_value = header_value_cb + ,.on_url = request_url_cb + ,.on_status = response_status_cb + ,.on_body = dontcall_body_cb + ,.on_headers_complete = connect_headers_complete_cb + ,.on_message_complete = connect_message_complete_cb + ,.on_chunk_header = chunk_header_cb + ,.on_chunk_complete = chunk_complete_cb + }; + +static http_parser_settings settings_null = + {.on_message_begin = 0 + ,.on_header_field = 0 + ,.on_header_value = 0 + ,.on_url = 0 + ,.on_status = 0 + ,.on_body = 0 + ,.on_headers_complete = 0 + ,.on_message_complete = 0 + ,.on_chunk_header = 0 + ,.on_chunk_complete = 0 + }; + +void +parser_init (enum http_parser_type type) +{ + num_messages = 0; + + assert(parser == NULL); + + parser = malloc(sizeof(http_parser)); + + http_parser_init(parser, type); + + memset(&messages, 0, sizeof messages); + +} + +void +parser_free () +{ + assert(parser); + free(parser); + parser = NULL; +} + +size_t parse (const char *buf, size_t len) +{ + size_t nparsed; + currently_parsing_eof = (len == 0); + nparsed = http_parser_execute(parser, &settings, buf, len); + return nparsed; +} + +size_t parse_count_body (const char *buf, size_t len) +{ + size_t nparsed; + currently_parsing_eof = (len == 0); + nparsed = http_parser_execute(parser, &settings_count_body, buf, len); + return nparsed; +} + +size_t parse_pause (const char *buf, size_t len) +{ + size_t nparsed; + http_parser_settings s = settings_pause; + + currently_parsing_eof = (len == 0); + current_pause_parser = &s; + nparsed = http_parser_execute(parser, current_pause_parser, buf, len); + return nparsed; +} + +size_t parse_connect (const char *buf, size_t len) +{ + size_t nparsed; + currently_parsing_eof = (len == 0); + nparsed = http_parser_execute(parser, &settings_connect, buf, len); + return nparsed; +} + +static inline int +check_str_eq (const struct message *m, + const char *prop, + const char *expected, + const char *found) { + if ((expected == NULL) != (found == NULL)) { + printf("\n*** Error: %s in '%s' ***\n\n", prop, m->name); + printf("expected %s\n", (expected == NULL) ? "NULL" : expected); + printf(" found %s\n", (found == NULL) ? "NULL" : found); + return 0; + } + if (expected != NULL && 0 != strcmp(expected, found)) { + printf("\n*** Error: %s in '%s' ***\n\n", prop, m->name); + printf("expected '%s'\n", expected); + printf(" found '%s'\n", found); + return 0; + } + return 1; +} + +static inline int +check_num_eq (const struct message *m, + const char *prop, + int expected, + int found) { + if (expected != found) { + printf("\n*** Error: %s in '%s' ***\n\n", prop, m->name); + printf("expected %d\n", expected); + printf(" found %d\n", found); + return 0; + } + return 1; +} + +#define MESSAGE_CHECK_STR_EQ(expected, found, prop) \ + if (!check_str_eq(expected, #prop, expected->prop, found->prop)) return 0 + +#define MESSAGE_CHECK_NUM_EQ(expected, found, prop) \ + if (!check_num_eq(expected, #prop, expected->prop, found->prop)) return 0 + +#define MESSAGE_CHECK_URL_EQ(u, expected, found, prop, fn) \ +do { \ + char ubuf[256]; \ + \ + if ((u)->field_set & (1 << (fn))) { \ + memcpy(ubuf, (found)->request_url + (u)->field_data[(fn)].off, \ + (u)->field_data[(fn)].len); \ + ubuf[(u)->field_data[(fn)].len] = '\0'; \ + } else { \ + ubuf[0] = '\0'; \ + } \ + \ + check_str_eq(expected, #prop, expected->prop, ubuf); \ +} while(0) + +int +message_eq (int index, int connect, const struct message *expected) +{ + int i; + struct message *m = &messages[index]; + + MESSAGE_CHECK_NUM_EQ(expected, m, http_major); + MESSAGE_CHECK_NUM_EQ(expected, m, http_minor); + + if (expected->type == HTTP_REQUEST) { + MESSAGE_CHECK_NUM_EQ(expected, m, method); + } else { + MESSAGE_CHECK_NUM_EQ(expected, m, status_code); + MESSAGE_CHECK_STR_EQ(expected, m, response_status); + assert(m->status_cb_called); + } + + if (!connect) { + MESSAGE_CHECK_NUM_EQ(expected, m, should_keep_alive); + MESSAGE_CHECK_NUM_EQ(expected, m, message_complete_on_eof); + } + + assert(m->message_begin_cb_called); + assert(m->headers_complete_cb_called); + assert(m->message_complete_cb_called); + + + MESSAGE_CHECK_STR_EQ(expected, m, request_url); + + /* Check URL components; we can't do this w/ CONNECT since it doesn't + * send us a well-formed URL. + */ + if (*m->request_url && m->method != HTTP_CONNECT) { + struct http_parser_url u; + + if (http_parser_parse_url(m->request_url, strlen(m->request_url), 0, &u)) { + fprintf(stderr, "\n\n*** failed to parse URL %s ***\n\n", + m->request_url); + abort(); + } + + if (expected->host) { + MESSAGE_CHECK_URL_EQ(&u, expected, m, host, UF_HOST); + } + + if (expected->userinfo) { + MESSAGE_CHECK_URL_EQ(&u, expected, m, userinfo, UF_USERINFO); + } + + m->port = (u.field_set & (1 << UF_PORT)) ? + u.port : 0; + + MESSAGE_CHECK_URL_EQ(&u, expected, m, query_string, UF_QUERY); + MESSAGE_CHECK_URL_EQ(&u, expected, m, fragment, UF_FRAGMENT); + MESSAGE_CHECK_URL_EQ(&u, expected, m, request_path, UF_PATH); + MESSAGE_CHECK_NUM_EQ(expected, m, port); + } + + if (connect) { + check_num_eq(m, "body_size", 0, m->body_size); + } else if (expected->body_size) { + MESSAGE_CHECK_NUM_EQ(expected, m, body_size); + } else { + MESSAGE_CHECK_STR_EQ(expected, m, body); + } + + if (connect) { + check_num_eq(m, "num_chunks_complete", 0, m->num_chunks_complete); + } else { + assert(m->num_chunks == m->num_chunks_complete); + MESSAGE_CHECK_NUM_EQ(expected, m, num_chunks_complete); + for (i = 0; i < m->num_chunks && i < MAX_CHUNKS; i++) { + MESSAGE_CHECK_NUM_EQ(expected, m, chunk_lengths[i]); + } + } + + MESSAGE_CHECK_NUM_EQ(expected, m, num_headers); + + int r; + for (i = 0; i < m->num_headers; i++) { + r = check_str_eq(expected, "header field", expected->headers[i][0], m->headers[i][0]); + if (!r) return 0; + r = check_str_eq(expected, "header value", expected->headers[i][1], m->headers[i][1]); + if (!r) return 0; + } + + if (!connect) { + MESSAGE_CHECK_STR_EQ(expected, m, upgrade); + } + + return 1; +} + +/* Given a sequence of varargs messages, return the number of them that the + * parser should successfully parse, taking into account that upgraded + * messages prevent all subsequent messages from being parsed. + */ +size_t +count_parsed_messages(const size_t nmsgs, ...) { + size_t i; + va_list ap; + + va_start(ap, nmsgs); + + for (i = 0; i < nmsgs; i++) { + struct message *m = va_arg(ap, struct message *); + + if (m->upgrade) { + va_end(ap); + return i + 1; + } + } + + va_end(ap); + return nmsgs; +} + +/* Given a sequence of bytes and the number of these that we were able to + * parse, verify that upgrade bodies are correct. + */ +void +upgrade_message_fix(char *body, const size_t nread, const size_t nmsgs, ...) { + va_list ap; + size_t i; + size_t off = 0; + + va_start(ap, nmsgs); + + for (i = 0; i < nmsgs; i++) { + struct message *m = va_arg(ap, struct message *); + + off += strlen(m->raw); + + if (m->upgrade) { + off -= strlen(m->upgrade); + + /* Check the portion of the response after its specified upgrade */ + if (!check_str_eq(m, "upgrade", body + off, body + nread)) { + abort(); + } + + /* Fix up the response so that message_eq() will verify the beginning + * of the upgrade */ + *(body + nread + strlen(m->upgrade)) = '\0'; + messages[num_messages -1 ].upgrade = body + nread; + + va_end(ap); + return; + } + } + + va_end(ap); + printf("\n\n*** Error: expected a message with upgrade ***\n"); + + abort(); +} + +static void +print_error (const char *raw, size_t error_location) +{ + fprintf(stderr, "\n*** %s ***\n\n", + http_errno_description(HTTP_PARSER_ERRNO(parser))); + + int this_line = 0, char_len = 0; + size_t i, j, len = strlen(raw), error_location_line = 0; + for (i = 0; i < len; i++) { + if (i == error_location) this_line = 1; + switch (raw[i]) { + case '\r': + char_len = 2; + fprintf(stderr, "\\r"); + break; + + case '\n': + fprintf(stderr, "\\n\n"); + + if (this_line) goto print; + + error_location_line = 0; + continue; + + default: + char_len = 1; + fputc(raw[i], stderr); + break; + } + if (!this_line) error_location_line += char_len; + } + + fprintf(stderr, "[eof]\n"); + + print: + for (j = 0; j < error_location_line; j++) { + fputc(' ', stderr); + } + fprintf(stderr, "^\n\nerror location: %u\n", (unsigned int)error_location); +} + +void +test_preserve_data (void) +{ + char my_data[] = "application-specific data"; + http_parser parser; + parser.data = my_data; + http_parser_init(&parser, HTTP_REQUEST); + if (parser.data != my_data) { + printf("\n*** parser.data not preserved accross http_parser_init ***\n\n"); + abort(); + } +} + +struct url_test { + const char *name; + const char *url; + int is_connect; + struct http_parser_url u; + int rv; +}; + +const struct url_test url_tests[] = +{ {.name="proxy request" + ,.url="http://hostname/" + ,.is_connect=0 + ,.u= + {.field_set=(1 << UF_SCHEMA) | (1 << UF_HOST) | (1 << UF_PATH) + ,.port=0 + ,.field_data= + {{ 0, 4 } /* UF_SCHEMA */ + ,{ 7, 8 } /* UF_HOST */ + ,{ 0, 0 } /* UF_PORT */ + ,{ 15, 1 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="proxy request with port" + ,.url="http://hostname:444/" + ,.is_connect=0 + ,.u= + {.field_set=(1 << UF_SCHEMA) | (1 << UF_HOST) | (1 << UF_PORT) | (1 << UF_PATH) + ,.port=444 + ,.field_data= + {{ 0, 4 } /* UF_SCHEMA */ + ,{ 7, 8 } /* UF_HOST */ + ,{ 16, 3 } /* UF_PORT */ + ,{ 19, 1 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="CONNECT request" + ,.url="hostname:443" + ,.is_connect=1 + ,.u= + {.field_set=(1 << UF_HOST) | (1 << UF_PORT) + ,.port=443 + ,.field_data= + {{ 0, 0 } /* UF_SCHEMA */ + ,{ 0, 8 } /* UF_HOST */ + ,{ 9, 3 } /* UF_PORT */ + ,{ 0, 0 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="CONNECT request but not connect" + ,.url="hostname:443" + ,.is_connect=0 + ,.rv=1 + } + +, {.name="proxy ipv6 request" + ,.url="http://[1:2::3:4]/" + ,.is_connect=0 + ,.u= + {.field_set=(1 << UF_SCHEMA) | (1 << UF_HOST) | (1 << UF_PATH) + ,.port=0 + ,.field_data= + {{ 0, 4 } /* UF_SCHEMA */ + ,{ 8, 8 } /* UF_HOST */ + ,{ 0, 0 } /* UF_PORT */ + ,{ 17, 1 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="proxy ipv6 request with port" + ,.url="http://[1:2::3:4]:67/" + ,.is_connect=0 + ,.u= + {.field_set=(1 << UF_SCHEMA) | (1 << UF_HOST) | (1 << UF_PORT) | (1 << UF_PATH) + ,.port=67 + ,.field_data= + {{ 0, 4 } /* UF_SCHEMA */ + ,{ 8, 8 } /* UF_HOST */ + ,{ 18, 2 } /* UF_PORT */ + ,{ 20, 1 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="CONNECT ipv6 address" + ,.url="[1:2::3:4]:443" + ,.is_connect=1 + ,.u= + {.field_set=(1 << UF_HOST) | (1 << UF_PORT) + ,.port=443 + ,.field_data= + {{ 0, 0 } /* UF_SCHEMA */ + ,{ 1, 8 } /* UF_HOST */ + ,{ 11, 3 } /* UF_PORT */ + ,{ 0, 0 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="ipv4 in ipv6 address" + ,.url="http://[2001:0000:0000:0000:0000:0000:1.9.1.1]/" + ,.is_connect=0 + ,.u= + {.field_set=(1 << UF_SCHEMA) | (1 << UF_HOST) | (1 << UF_PATH) + ,.port=0 + ,.field_data= + {{ 0, 4 } /* UF_SCHEMA */ + ,{ 8, 37 } /* UF_HOST */ + ,{ 0, 0 } /* UF_PORT */ + ,{ 46, 1 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="extra ? in query string" + ,.url="http://a.tbcdn.cn/p/fp/2010c/??fp-header-min.css,fp-base-min.css," + "fp-channel-min.css,fp-product-min.css,fp-mall-min.css,fp-category-min.css," + "fp-sub-min.css,fp-gdp4p-min.css,fp-css3-min.css,fp-misc-min.css?t=20101022.css" + ,.is_connect=0 + ,.u= + {.field_set=(1<field_set, u->port); + for (i = 0; i < UF_MAX; i++) { + if ((u->field_set & (1 << i)) == 0) { + printf("\tfield_data[%u]: unset\n", i); + continue; + } + + printf("\tfield_data[%u]: off: %u len: %u part: \"%.*s\n\"", + i, + u->field_data[i].off, + u->field_data[i].len, + u->field_data[i].len, + url + u->field_data[i].off); + } +} + +void +test_parse_url (void) +{ + struct http_parser_url u; + const struct url_test *test; + unsigned int i; + int rv; + + for (i = 0; i < (sizeof(url_tests) / sizeof(url_tests[0])); i++) { + test = &url_tests[i]; + memset(&u, 0, sizeof(u)); + + rv = http_parser_parse_url(test->url, + strlen(test->url), + test->is_connect, + &u); + + if (test->rv == 0) { + if (rv != 0) { + printf("\n*** http_parser_parse_url(\"%s\") \"%s\" test failed, " + "unexpected rv %d ***\n\n", test->url, test->name, rv); + abort(); + } + + if (memcmp(&u, &test->u, sizeof(u)) != 0) { + printf("\n*** http_parser_parse_url(\"%s\") \"%s\" failed ***\n", + test->url, test->name); + + printf("target http_parser_url:\n"); + dump_url(test->url, &test->u); + printf("result http_parser_url:\n"); + dump_url(test->url, &u); + + abort(); + } + } else { + /* test->rv != 0 */ + if (rv == 0) { + printf("\n*** http_parser_parse_url(\"%s\") \"%s\" test failed, " + "unexpected rv %d ***\n\n", test->url, test->name, rv); + abort(); + } + } + } +} + +void +test_method_str (void) +{ + assert(0 == strcmp("GET", http_method_str(HTTP_GET))); + assert(0 == strcmp("", http_method_str(1337))); +} + +void +test_message (const struct message *message) +{ + size_t raw_len = strlen(message->raw); + size_t msg1len; + for (msg1len = 0; msg1len < raw_len; msg1len++) { + parser_init(message->type); + + size_t read; + const char *msg1 = message->raw; + const char *msg2 = msg1 + msg1len; + size_t msg2len = raw_len - msg1len; + + if (msg1len) { + read = parse(msg1, msg1len); + + if (message->upgrade && parser->upgrade && num_messages > 0) { + messages[num_messages - 1].upgrade = msg1 + read; + goto test; + } + + if (read != msg1len) { + print_error(msg1, read); + abort(); + } + } + + + read = parse(msg2, msg2len); + + if (message->upgrade && parser->upgrade) { + messages[num_messages - 1].upgrade = msg2 + read; + goto test; + } + + if (read != msg2len) { + print_error(msg2, read); + abort(); + } + + read = parse(NULL, 0); + + if (read != 0) { + print_error(message->raw, read); + abort(); + } + + test: + + if (num_messages != 1) { + printf("\n*** num_messages != 1 after testing '%s' ***\n\n", message->name); + abort(); + } + + if(!message_eq(0, 0, message)) abort(); + + parser_free(); + } +} + +void +test_message_count_body (const struct message *message) +{ + parser_init(message->type); + + size_t read; + size_t l = strlen(message->raw); + size_t i, toread; + size_t chunk = 4024; + + for (i = 0; i < l; i+= chunk) { + toread = MIN(l-i, chunk); + read = parse_count_body(message->raw + i, toread); + if (read != toread) { + print_error(message->raw, read); + abort(); + } + } + + + read = parse_count_body(NULL, 0); + if (read != 0) { + print_error(message->raw, read); + abort(); + } + + if (num_messages != 1) { + printf("\n*** num_messages != 1 after testing '%s' ***\n\n", message->name); + abort(); + } + + if(!message_eq(0, 0, message)) abort(); + + parser_free(); +} + +void +test_simple_type (const char *buf, + enum http_errno err_expected, + enum http_parser_type type) +{ + parser_init(type); + + enum http_errno err; + + parse(buf, strlen(buf)); + err = HTTP_PARSER_ERRNO(parser); + parse(NULL, 0); + + parser_free(); + + /* In strict mode, allow us to pass with an unexpected HPE_STRICT as + * long as the caller isn't expecting success. + */ +#if HTTP_PARSER_STRICT + if (err_expected != err && err_expected != HPE_OK && err != HPE_STRICT) { +#else + if (err_expected != err) { +#endif + fprintf(stderr, "\n*** test_simple expected %s, but saw %s ***\n\n%s\n", + http_errno_name(err_expected), http_errno_name(err), buf); + abort(); + } +} + +void +test_simple (const char *buf, enum http_errno err_expected) +{ + test_simple_type(buf, err_expected, HTTP_REQUEST); +} + +void +test_invalid_header_content (int req, const char* str) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = str; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_INVALID_HEADER_TOKEN); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in invalid header content test ***\n"); + abort(); +} + +void +test_invalid_header_field_content_error (int req) +{ + test_invalid_header_content(req, "Foo: F\01ailure"); + test_invalid_header_content(req, "Foo: B\02ar"); +} + +void +test_invalid_header_field (int req, const char* str) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = str; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_INVALID_HEADER_TOKEN); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in invalid header token test ***\n"); + abort(); +} + +void +test_invalid_header_field_token_error (int req) +{ + test_invalid_header_field(req, "Fo@: Failure"); + test_invalid_header_field(req, "Foo\01\test: Bar"); +} + +void +test_double_content_length_error (int req) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = "Content-Length: 0\r\nContent-Length: 1\r\n\r\n"; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_UNEXPECTED_CONTENT_LENGTH); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in double content-length test ***\n"); + abort(); +} + +void +test_chunked_content_length_error (int req) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = "Transfer-Encoding: chunked\r\nContent-Length: 1\r\n\r\n"; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_UNEXPECTED_CONTENT_LENGTH); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in chunked content-length test ***\n"); + abort(); +} + +void +test_header_cr_no_lf_error (int req) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = "Foo: 1\rBar: 1\r\n\r\n"; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_LF_EXPECTED); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in header whitespace test ***\n"); + abort(); +} + +void +test_header_overflow_error (int req) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? "GET / HTTP/1.1\r\n" : "HTTP/1.0 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = "header-key: header-value\r\n"; + size_t buflen = strlen(buf); + + int i; + for (i = 0; i < 10000; i++) { + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + //fprintf(stderr, "error found on iter %d\n", i); + assert(HTTP_PARSER_ERRNO(&parser) == HPE_HEADER_OVERFLOW); + return; + } + } + + fprintf(stderr, "\n*** Error expected but none in header overflow test ***\n"); + abort(); +} + + +void +test_header_nread_value () +{ + http_parser parser; + http_parser_init(&parser, HTTP_REQUEST); + size_t parsed; + const char *buf; + buf = "GET / HTTP/1.1\r\nheader: value\nhdr: value\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + assert(parser.nread == strlen(buf)); +} + + +static void +test_content_length_overflow (const char *buf, size_t buflen, int expect_ok) +{ + http_parser parser; + http_parser_init(&parser, HTTP_RESPONSE); + http_parser_execute(&parser, &settings_null, buf, buflen); + + if (expect_ok) + assert(HTTP_PARSER_ERRNO(&parser) == HPE_OK); + else + assert(HTTP_PARSER_ERRNO(&parser) == HPE_INVALID_CONTENT_LENGTH); +} + +void +test_header_content_length_overflow_error (void) +{ +#define X(size) \ + "HTTP/1.1 200 OK\r\n" \ + "Content-Length: " #size "\r\n" \ + "\r\n" + const char a[] = X(1844674407370955160); /* 2^64 / 10 - 1 */ + const char b[] = X(18446744073709551615); /* 2^64-1 */ + const char c[] = X(18446744073709551616); /* 2^64 */ +#undef X + test_content_length_overflow(a, sizeof(a) - 1, 1); /* expect ok */ + test_content_length_overflow(b, sizeof(b) - 1, 0); /* expect failure */ + test_content_length_overflow(c, sizeof(c) - 1, 0); /* expect failure */ +} + +void +test_chunk_content_length_overflow_error (void) +{ +#define X(size) \ + "HTTP/1.1 200 OK\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "\r\n" \ + #size "\r\n" \ + "..." + const char a[] = X(FFFFFFFFFFFFFFE); /* 2^64 / 16 - 1 */ + const char b[] = X(FFFFFFFFFFFFFFFF); /* 2^64-1 */ + const char c[] = X(10000000000000000); /* 2^64 */ +#undef X + test_content_length_overflow(a, sizeof(a) - 1, 1); /* expect ok */ + test_content_length_overflow(b, sizeof(b) - 1, 0); /* expect failure */ + test_content_length_overflow(c, sizeof(c) - 1, 0); /* expect failure */ +} + +void +test_no_overflow_long_body (int req, size_t length) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + size_t i; + char buf1[3000]; + size_t buf1len = sprintf(buf1, "%s\r\nConnection: Keep-Alive\r\nContent-Length: %lu\r\n\r\n", + req ? "POST / HTTP/1.0" : "HTTP/1.0 200 OK", (unsigned long)length); + parsed = http_parser_execute(&parser, &settings_null, buf1, buf1len); + if (parsed != buf1len) + goto err; + + for (i = 0; i < length; i++) { + char foo = 'a'; + parsed = http_parser_execute(&parser, &settings_null, &foo, 1); + if (parsed != 1) + goto err; + } + + parsed = http_parser_execute(&parser, &settings_null, buf1, buf1len); + if (parsed != buf1len) goto err; + return; + + err: + fprintf(stderr, + "\n*** error in test_no_overflow_long_body %s of length %lu ***\n", + req ? "REQUEST" : "RESPONSE", + (unsigned long)length); + abort(); +} + +void +test_multiple3 (const struct message *r1, const struct message *r2, const struct message *r3) +{ + int message_count = count_parsed_messages(3, r1, r2, r3); + + char total[ strlen(r1->raw) + + strlen(r2->raw) + + strlen(r3->raw) + + 1 + ]; + total[0] = '\0'; + + strcat(total, r1->raw); + strcat(total, r2->raw); + strcat(total, r3->raw); + + parser_init(r1->type); + + size_t read; + + read = parse(total, strlen(total)); + + if (parser->upgrade) { + upgrade_message_fix(total, read, 3, r1, r2, r3); + goto test; + } + + if (read != strlen(total)) { + print_error(total, read); + abort(); + } + + read = parse(NULL, 0); + + if (read != 0) { + print_error(total, read); + abort(); + } + +test: + + if (message_count != num_messages) { + fprintf(stderr, "\n\n*** Parser didn't see 3 messages only %d *** \n", num_messages); + abort(); + } + + if (!message_eq(0, 0, r1)) abort(); + if (message_count > 1 && !message_eq(1, 0, r2)) abort(); + if (message_count > 2 && !message_eq(2, 0, r3)) abort(); + + parser_free(); +} + +/* SCAN through every possible breaking to make sure the + * parser can handle getting the content in any chunks that + * might come from the socket + */ +void +test_scan (const struct message *r1, const struct message *r2, const struct message *r3) +{ + char total[80*1024] = "\0"; + char buf1[80*1024] = "\0"; + char buf2[80*1024] = "\0"; + char buf3[80*1024] = "\0"; + + strcat(total, r1->raw); + strcat(total, r2->raw); + strcat(total, r3->raw); + + size_t read; + + int total_len = strlen(total); + + int total_ops = 2 * (total_len - 1) * (total_len - 2) / 2; + int ops = 0 ; + + size_t buf1_len, buf2_len, buf3_len; + int message_count = count_parsed_messages(3, r1, r2, r3); + + int i,j,type_both; + for (type_both = 0; type_both < 2; type_both ++ ) { + for (j = 2; j < total_len; j ++ ) { + for (i = 1; i < j; i ++ ) { + + if (ops % 1000 == 0) { + printf("\b\b\b\b%3.0f%%", 100 * (float)ops /(float)total_ops); + fflush(stdout); + } + ops += 1; + + parser_init(type_both ? HTTP_BOTH : r1->type); + + buf1_len = i; + strlncpy(buf1, sizeof(buf1), total, buf1_len); + buf1[buf1_len] = 0; + + buf2_len = j - i; + strlncpy(buf2, sizeof(buf1), total+i, buf2_len); + buf2[buf2_len] = 0; + + buf3_len = total_len - j; + strlncpy(buf3, sizeof(buf1), total+j, buf3_len); + buf3[buf3_len] = 0; + + read = parse(buf1, buf1_len); + + if (parser->upgrade) goto test; + + if (read != buf1_len) { + print_error(buf1, read); + goto error; + } + + read += parse(buf2, buf2_len); + + if (parser->upgrade) goto test; + + if (read != buf1_len + buf2_len) { + print_error(buf2, read); + goto error; + } + + read += parse(buf3, buf3_len); + + if (parser->upgrade) goto test; + + if (read != buf1_len + buf2_len + buf3_len) { + print_error(buf3, read); + goto error; + } + + parse(NULL, 0); + +test: + if (parser->upgrade) { + upgrade_message_fix(total, read, 3, r1, r2, r3); + } + + if (message_count != num_messages) { + fprintf(stderr, "\n\nParser didn't see %d messages only %d\n", + message_count, num_messages); + goto error; + } + + if (!message_eq(0, 0, r1)) { + fprintf(stderr, "\n\nError matching messages[0] in test_scan.\n"); + goto error; + } + + if (message_count > 1 && !message_eq(1, 0, r2)) { + fprintf(stderr, "\n\nError matching messages[1] in test_scan.\n"); + goto error; + } + + if (message_count > 2 && !message_eq(2, 0, r3)) { + fprintf(stderr, "\n\nError matching messages[2] in test_scan.\n"); + goto error; + } + + parser_free(); + } + } + } + puts("\b\b\b\b100%"); + return; + + error: + fprintf(stderr, "i=%d j=%d\n", i, j); + fprintf(stderr, "buf1 (%u) %s\n\n", (unsigned int)buf1_len, buf1); + fprintf(stderr, "buf2 (%u) %s\n\n", (unsigned int)buf2_len , buf2); + fprintf(stderr, "buf3 (%u) %s\n", (unsigned int)buf3_len, buf3); + abort(); +} + +// user required to free the result +// string terminated by \0 +char * +create_large_chunked_message (int body_size_in_kb, const char* headers) +{ + int i; + size_t wrote = 0; + size_t headers_len = strlen(headers); + size_t bufsize = headers_len + (5+1024+2)*body_size_in_kb + 6; + char * buf = malloc(bufsize); + + memcpy(buf, headers, headers_len); + wrote += headers_len; + + for (i = 0; i < body_size_in_kb; i++) { + // write 1kb chunk into the body. + memcpy(buf + wrote, "400\r\n", 5); + wrote += 5; + memset(buf + wrote, 'C', 1024); + wrote += 1024; + strcpy(buf + wrote, "\r\n"); + wrote += 2; + } + + memcpy(buf + wrote, "0\r\n\r\n", 6); + wrote += 6; + assert(wrote == bufsize); + + return buf; +} + +/* Verify that we can pause parsing at any of the bytes in the + * message and still get the result that we're expecting. */ +void +test_message_pause (const struct message *msg) +{ + char *buf = (char*) msg->raw; + size_t buflen = strlen(msg->raw); + size_t nread; + + parser_init(msg->type); + + do { + nread = parse_pause(buf, buflen); + + // We can only set the upgrade buffer once we've gotten our message + // completion callback. + if (messages[0].message_complete_cb_called && + msg->upgrade && + parser->upgrade) { + messages[0].upgrade = buf + nread; + goto test; + } + + if (nread < buflen) { + + // Not much do to if we failed a strict-mode check + if (HTTP_PARSER_ERRNO(parser) == HPE_STRICT) { + parser_free(); + return; + } + + assert (HTTP_PARSER_ERRNO(parser) == HPE_PAUSED); + } + + buf += nread; + buflen -= nread; + http_parser_pause(parser, 0); + } while (buflen > 0); + + nread = parse_pause(NULL, 0); + assert (nread == 0); + +test: + if (num_messages != 1) { + printf("\n*** num_messages != 1 after testing '%s' ***\n\n", msg->name); + abort(); + } + + if(!message_eq(0, 0, msg)) abort(); + + parser_free(); +} + +/* Verify that body and next message won't be parsed in responses to CONNECT */ +void +test_message_connect (const struct message *msg) +{ + char *buf = (char*) msg->raw; + size_t buflen = strlen(msg->raw); + + parser_init(msg->type); + + parse_connect(buf, buflen); + + if (num_messages != 1) { + printf("\n*** num_messages != 1 after testing '%s' ***\n\n", msg->name); + abort(); + } + + if(!message_eq(0, 1, msg)) abort(); + + parser_free(); +} + +int +main (void) +{ + parser = NULL; + int i, j, k; + int request_count; + int response_count; + unsigned long version; + unsigned major; + unsigned minor; + unsigned patch; + + version = http_parser_version(); + major = (version >> 16) & 255; + minor = (version >> 8) & 255; + patch = version & 255; + printf("http_parser v%u.%u.%u (0x%06lx)\n", major, minor, patch, version); + + printf("sizeof(http_parser) = %u\n", (unsigned int)sizeof(http_parser)); + + for (request_count = 0; requests[request_count].name; request_count++); + for (response_count = 0; responses[response_count].name; response_count++); + + //// API + test_preserve_data(); + test_parse_url(); + test_method_str(); + + //// NREAD + test_header_nread_value(); + + //// OVERFLOW CONDITIONS + + test_header_overflow_error(HTTP_REQUEST); + test_no_overflow_long_body(HTTP_REQUEST, 1000); + test_no_overflow_long_body(HTTP_REQUEST, 100000); + + test_header_overflow_error(HTTP_RESPONSE); + test_no_overflow_long_body(HTTP_RESPONSE, 1000); + test_no_overflow_long_body(HTTP_RESPONSE, 100000); + + test_header_content_length_overflow_error(); + test_chunk_content_length_overflow_error(); + + //// HEADER FIELD CONDITIONS + test_double_content_length_error(HTTP_REQUEST); + test_chunked_content_length_error(HTTP_REQUEST); + test_header_cr_no_lf_error(HTTP_REQUEST); + test_invalid_header_field_token_error(HTTP_REQUEST); + test_invalid_header_field_content_error(HTTP_REQUEST); + test_double_content_length_error(HTTP_RESPONSE); + test_chunked_content_length_error(HTTP_RESPONSE); + test_header_cr_no_lf_error(HTTP_RESPONSE); + test_invalid_header_field_token_error(HTTP_RESPONSE); + test_invalid_header_field_content_error(HTTP_RESPONSE); + + //// RESPONSES + + test_simple_type("HTP/1.1 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/01.1 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/11.1 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/1.01 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/1.1\t200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + + for (i = 0; i < response_count; i++) { + test_message(&responses[i]); + } + + for (i = 0; i < response_count; i++) { + test_message_pause(&responses[i]); + } + + for (i = 0; i < response_count; i++) { + test_message_connect(&responses[i]); + } + + for (i = 0; i < response_count; i++) { + if (!responses[i].should_keep_alive) continue; + for (j = 0; j < response_count; j++) { + if (!responses[j].should_keep_alive) continue; + for (k = 0; k < response_count; k++) { + test_multiple3(&responses[i], &responses[j], &responses[k]); + } + } + } + + test_message_count_body(&responses[NO_HEADERS_NO_BODY_404]); + test_message_count_body(&responses[TRAILING_SPACE_ON_CHUNKED_BODY]); + + // test very large chunked response + { + char * msg = create_large_chunked_message(31337, + "HTTP/1.0 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Type: text/plain\r\n" + "\r\n"); + struct message large_chunked = + {.name= "large chunked" + ,.type= HTTP_RESPONSE + ,.raw= msg + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 2 + ,.headers= + { { "Transfer-Encoding", "chunked" } + , { "Content-Type", "text/plain" } + } + ,.body_size= 31337*1024 + ,.num_chunks_complete= 31338 + }; + for (i = 0; i < MAX_CHUNKS; i++) { + large_chunked.chunk_lengths[i] = 1024; + } + test_message_count_body(&large_chunked); + free(msg); + } + + + + printf("response scan 1/2 "); + test_scan( &responses[TRAILING_SPACE_ON_CHUNKED_BODY] + , &responses[NO_BODY_HTTP10_KA_204] + , &responses[NO_REASON_PHRASE] + ); + + printf("response scan 2/2 "); + test_scan( &responses[BONJOUR_MADAME_FR] + , &responses[UNDERSTORE_HEADER_KEY] + , &responses[NO_CARRIAGE_RET] + ); + + puts("responses okay"); + + + /// REQUESTS + + test_simple("GET / HTP/1.1\r\n\r\n", HPE_INVALID_VERSION); + test_simple("GET / HTTP/01.1\r\n\r\n", HPE_INVALID_VERSION); + test_simple("GET / HTTP/11.1\r\n\r\n", HPE_INVALID_VERSION); + test_simple("GET / HTTP/1.01\r\n\r\n", HPE_INVALID_VERSION); + + // Extended characters - see nodejs/test/parallel/test-http-headers-obstext.js + test_simple("GET / HTTP/1.1\r\n" + "Test: Düsseldorf\r\n", + HPE_OK); + + // Well-formed but incomplete + test_simple("GET / HTTP/1.1\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: 6\r\n" + "\r\n" + "fooba", + HPE_OK); + + static const char *all_methods[] = { + "DELETE", + "GET", + "HEAD", + "POST", + "PUT", + //"CONNECT", //CONNECT can't be tested like other methods, it's a tunnel + "OPTIONS", + "TRACE", + "COPY", + "LOCK", + "MKCOL", + "MOVE", + "PROPFIND", + "PROPPATCH", + "SEARCH", + "UNLOCK", + "BIND", + "REBIND", + "UNBIND", + "ACL", + "REPORT", + "MKACTIVITY", + "CHECKOUT", + "MERGE", + "M-SEARCH", + "NOTIFY", + "SUBSCRIBE", + "UNSUBSCRIBE", + "PATCH", + "PURGE", + "MKCALENDAR", + "LINK", + "UNLINK", + 0 }; + const char **this_method; + for (this_method = all_methods; *this_method; this_method++) { + char buf[200]; + sprintf(buf, "%s / HTTP/1.1\r\n\r\n", *this_method); + test_simple(buf, HPE_OK); + } + + static const char *bad_methods[] = { + "ASDF", + "C******", + "COLA", + "GEM", + "GETA", + "M****", + "MKCOLA", + "PROPPATCHA", + "PUN", + "PX", + "SA", + "hello world", + 0 }; + for (this_method = bad_methods; *this_method; this_method++) { + char buf[200]; + sprintf(buf, "%s / HTTP/1.1\r\n\r\n", *this_method); + test_simple(buf, HPE_INVALID_METHOD); + } + + // illegal header field name line folding + test_simple("GET / HTTP/1.1\r\n" + "name\r\n" + " : value\r\n" + "\r\n", + HPE_INVALID_HEADER_TOKEN); + + const char *dumbfuck2 = + "GET / HTTP/1.1\r\n" + "X-SSL-Bullshit: -----BEGIN CERTIFICATE-----\r\n" + "\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n" + "\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n" + "\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n" + "\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n" + "\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n" + "\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n" + "\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n" + "\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n" + "\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n" + "\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n" + "\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n" + "\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n" + "\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n" + "\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n" + "\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n" + "\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n" + "\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n" + "\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n" + "\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n" + "\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n" + "\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n" + "\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n" + "\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n" + "\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n" + "\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n" + "\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n" + "\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n" + "\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n" + "\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n" + "\tRA==\r\n" + "\t-----END CERTIFICATE-----\r\n" + "\r\n"; + test_simple(dumbfuck2, HPE_OK); + + const char *corrupted_connection = + "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Connection\r\033\065\325eep-Alive\r\n" + "Accept-Encoding: gzip\r\n" + "\r\n"; + test_simple(corrupted_connection, HPE_INVALID_HEADER_TOKEN); + + const char *corrupted_header_name = + "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "X-Some-Header\r\033\065\325eep-Alive\r\n" + "Accept-Encoding: gzip\r\n" + "\r\n"; + test_simple(corrupted_header_name, HPE_INVALID_HEADER_TOKEN); + +#if 0 + // NOTE(Wed Nov 18 11:57:27 CET 2009) this seems okay. we just read body + // until EOF. + // + // no content-length + // error if there is a body without content length + const char *bad_get_no_headers_no_body = "GET /bad_get_no_headers_no_body/world HTTP/1.1\r\n" + "Accept: */*\r\n" + "\r\n" + "HELLO"; + test_simple(bad_get_no_headers_no_body, 0); +#endif + /* TODO sending junk and large headers gets rejected */ + + + /* check to make sure our predefined requests are okay */ + for (i = 0; requests[i].name; i++) { + test_message(&requests[i]); + } + + for (i = 0; i < request_count; i++) { + test_message_pause(&requests[i]); + } + + for (i = 0; i < request_count; i++) { + if (!requests[i].should_keep_alive) continue; + for (j = 0; j < request_count; j++) { + if (!requests[j].should_keep_alive) continue; + for (k = 0; k < request_count; k++) { + test_multiple3(&requests[i], &requests[j], &requests[k]); + } + } + } + + printf("request scan 1/4 "); + test_scan( &requests[GET_NO_HEADERS_NO_BODY] + , &requests[GET_ONE_HEADER_NO_BODY] + , &requests[GET_NO_HEADERS_NO_BODY] + ); + + printf("request scan 2/4 "); + test_scan( &requests[POST_CHUNKED_ALL_YOUR_BASE] + , &requests[POST_IDENTITY_BODY_WORLD] + , &requests[GET_FUNKY_CONTENT_LENGTH] + ); + + printf("request scan 3/4 "); + test_scan( &requests[TWO_CHUNKS_MULT_ZERO_END] + , &requests[CHUNKED_W_TRAILING_HEADERS] + , &requests[CHUNKED_W_BULLSHIT_AFTER_LENGTH] + ); + + printf("request scan 4/4 "); + test_scan( &requests[QUERY_URL_WITH_QUESTION_MARK_GET] + , &requests[PREFIX_NEWLINE_GET ] + , &requests[CONNECT_REQUEST] + ); + + puts("requests okay"); + + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/Changes b/deps/ngtcp2/ngtcp2/third-party/libev/Changes new file mode 100644 index 00000000000000..d200b9ee11e5aa --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/Changes @@ -0,0 +1,617 @@ +Revision history for libev, a high-performance and full-featured event loop. + +TODO: for next ABI/API change, consider moving EV__IOFDSSET into io->fd instead and provide a getter. +TODO: document EV_TSTAMP_T + +4.33 Wed Mar 18 13:22:29 CET 2020 + - no changes w.r.t. 4.32. + +4.32 (EV only) + - the 4.31 timerfd code wrongly changed the priority of the signal + fd watcher, which is usually harmless unless signal fds are + also used (found via cpan tester service). + - the documentation wrongly claimed that user may modify fd and events + members in io watchers when the watcher was stopped + (found by b_jonas). + - new ev_io_modify mutator which changes only the events member, + which can be faster. also added ev::io::set (int events) method + to ev++.h. + - officially allow a zero events mask for io watchers. this should + work with older libev versions as well but was not officially + allowed before. + - do not wake up every minute when timerfd is used to detect timejumps. + - do not wake up every minute when periodics are disabled and we have + a monotonic clock. + - support a lot more "uncommon" compile time configurations, + such as ev_embed enabled but ev_timer disabled. + - use a start/stop wrapper class to reduce code duplication in + ev++.h and make it needlessly more c++-y. + - the linux aio backend is no longer compiled in by default. + - update to libecb version 0x00010008. + +4.31 Fri Dec 20 21:58:29 CET 2019 + - handle backends with minimum wait time a bit better by not + waiting in the presence of already-expired timers + (behaviour reported by Felipe Gasper). + - new feature: use timerfd to detect timejumps quickly, + can be disabled with the new EVFLAG_NOTIMERFD loop flag. + - document EV_USE_SIGNALFD feature macro. + +4.30 (EV only) + - change non-autoconf test for __kernel_rwf_t by testing + LINUX_VERSION_CODE, the most direct test I could find. + - fix a bug in the io_uring backend that polled the wrong + backend fd, causing it to not work in many cases. + +4.29 (EV only) + - add io uring autoconf and non-autoconf detection. + - disable io_uring when some header files are too old. + +4.28 (EV only) + - linuxaio backend resulted in random memory corruption + when loop is forked. + - linuxaio backend might have tried to cancel an iocb + multiple times (was unable to trigger this). + - linuxaio backend now employs a generation counter to + avoid handling spurious events from cancelled requests. + - io_cancel can return EINTR, deal with it. also, assume + io_submit also returns EINTR. + - fix some other minor bugs in linuxaio backend. + - ev_tstamp type can now be overriden by defining EV_TSTAMP_T. + - cleanup: replace expect_true/false and noinline by their + libecb counterparts. + - move syscall infrastructure from ev_linuxaio.c to ev.c. + - prepare io_uring integration. + - tweak ev_floor. + - epoll, poll, win32 Sleep and other places that use millisecond + reslution now all try to round up times. + - solaris port backend didn't compile. + - abstract time constants into their macros, for more flexibility. + +4.27 Thu Jun 27 22:43:44 CEST 2019 + - linux aio backend almost completely rewritten to work around its + limitations. + - linux aio backend now requires linux 4.19+. + - epoll backend now mandatory for linux aio backend. + - fail assertions more aggressively on invalid fd's detected + in the event loop, do not just silently fd_kill in case of + user error. + - ev_io_start/ev_io_stop now verify the watcher fd using + a syscall when EV_VERIFY is 2 or higher. + +4.26 (EV only) + - update to libecb 0x00010006. + - new experimental linux aio backend (linux 4.18+). + - removed redundant 0-ptr check in ev_once. + - updated/extended ev_set_allocator documentation. + - replaced EMPTY2 macro by array_needsize_noinit. + - minor code cleanups. + - epoll backend now uses epoll_create1 also after fork. + +4.25 Fri Dec 21 07:49:20 CET 2018 + - INCOMPATIBLE CHANGE: EV_THROW was renamed to EV_NOEXCEPT + (EV_THROW still provided) and now uses noexcept on C++11 or newer. + - move the darwin select workaround higher in ev.c, as newer versions of + darwin managed to break their broken select even more. + - ANDROID => __ANDROID__ (reported by enh@google.com). + - disable epoll_create1 on android because it has broken header files + and google is unwilling to fix them (reported by enh@google.com). + - avoid a minor compilation warning on win32. + - c++: remove deprecated dynamic throw() specifications. + - c++: improve the (unsupported) bad_loop exception class. + - backport perl ev_periodic example to C, untested. + - update libecb, biggets change is to include a memory fence + in ECB_MEMORY_FENCE_RELEASE on x86/amd64. + - minor autoconf/automake modernisation. + +4.24 Wed Dec 28 05:19:55 CET 2016 + - bump version to 4.24, as the release tarball inexplicably + didn't have the right version in ev.h, even though the cvs-tagged + version did have the right one (reported by Ales Teska). + +4.23 Wed Nov 16 18:23:41 CET 2016 + - move some declarations at the beginning to help certain retarded + microsoft compilers, even though their documentation claims + otherwise (reported by Ruslan Osmanov). + +4.22 Sun Dec 20 22:11:50 CET 2015 + - when epoll detects unremovable fds in the fd set, rebuild + only the epoll descriptor, not the signal pipe, to avoid + SIGPIPE in ev_async_send. This doesn't solve it on fork, + so document what needs to be done in ev_loop_fork + (analyzed by Benjamin Mahler). + - remove superfluous sys/timeb.h include on win32 + (analyzed by Jason Madden). + - updated libecb. + +4.20 Sat Jun 20 13:01:43 CEST 2015 + - prefer noexcept over throw () with C++ 11. + - update ecb.h due to incompatibilities with c11. + - fix a potential aliasing issue when reading and writing + watcher callbacks. + +4.19 Thu Sep 25 08:18:25 CEST 2014 + - ev.h wasn't valid C++ anymore, which tripped compilers other than + clang, msvc or gcc (analyzed by Raphael 'kena' Poss). Unfortunately, + C++ doesn't support typedefs for function pointers fully, so the affected + declarations have to spell out the types each time. + - when not using autoconf, tighten the check for clock_gettime and related + functionality. + +4.18 Fri Sep 5 17:55:26 CEST 2014 + - events on files were not always generated properly with the + epoll backend (testcase by Assaf Inbal). + - mark event pipe fd as cloexec after a fork (analyzed by Sami Farin). + - (ecb) support m68k, m88k and sh (patch by Miod Vallat). + - use a reasonable fallback for EV_NSIG instead of erroring out + when we can't detect the signal set size. + - in the absence of autoconf, do not use the clock syscall + on glibc >= 2.17 (avoids the syscall AND -lrt on systems + doing clock_gettime in userspace). + - ensure extern "C" function pointers are used for externally-visible + loop callbacks (not watcher callbacks yet). + - (ecb) work around memory barriers and volatile apparently both being + broken in visual studio 2008 and later (analysed and patch by Nicolas Noble). + +4.15 Fri Mar 1 12:04:50 CET 2013 + - destroying a non-default loop would stop the global waitpid + watcher (Denis Bilenko). + - queueing pending watchers of higher priority from a watcher now invokes + them in a timely fashion (reported by Denis Bilenko). + - add throw() to all libev functions that cannot throw exceptions, for + further code size decrease when compiling for C++. + - add throw () to callbacks that must not throw exceptions (allocator, + syserr, loop acquire/release, periodic reschedule cbs). + - fix event_base_loop return code, add event_get_callback, event_base_new, + event_base_get_method calls to improve libevent 1.x emulation and add + some libevent 2.x functionality (based on a patch by Jeff Davey). + - add more memory fences to fix a bug reported by Jeff Davey. Better + be overfenced than underprotected. + - ev_run now returns a boolean status (true meaning watchers are + still active). + - ev_once: undef EV_ERROR in ev_kqueue.c, to avoid clashing with + libev's EV_ERROR (reported by 191919). + - (ecb) add memory fence support for xlC (Darin McBride). + - (ecb) add memory fence support for gcc-mips (Anton Kirilov). + - (ecb) add memory fence support for gcc-alpha (Christian Weisgerber). + - work around some kernels losing file descriptors by leaking + the kqueue descriptor in the child. + - work around linux inotify not reporting IN_ATTRIB changes for directories + in many cases. + - include sys/syscall.h instead of plain syscall.h. + - check for io watcher loops in ev_verify, check for the most + common reported usage bug in ev_io_start. + - choose socket vs. WSASocket at compiletime using EV_USE_WSASOCKET. + - always use WSASend/WSARecv directly on windows, hoping that this + works in all cases (unlike read/write/send/recv...). + - try to detect signals around a fork faster (test program by + Denis Bilenko). + - work around recent glibc versions that leak memory in realloc. + - rename ev::embed::set to ev::embed::set_embed to avoid clashing + the watcher base set (loop) method. + - rewrite the async/signal pipe logic to always keep a valid fd, which + simplifies (and hopefully correctifies :) the race checking + on fork, at the cost of one extra fd. + - add fat, msdos, jffs2, ramfs, ntfs and btrfs to the list of + inotify-supporting filesystems. + - move orig_CFLAGS assignment to after AC_INIT, as newer autoconf + versions ignore it before + (https://bugzilla.redhat.com/show_bug.cgi?id=908096). + - add some untested android support. + - enum expressions must be of type int (reported by Juan Pablo L). + +4.11 Sat Feb 4 19:52:39 CET 2012 + - INCOMPATIBLE CHANGE: ev_timer_again now clears the pending status, as + was documented already, but not implemented in the repeating case. + - new compiletime symbols: EV_NO_SMP and EV_NO_THREADS. + - fix a race where the workaround against the epoll fork bugs + caused signals to not be handled anymore. + - correct backend_fudge for most backends, and implement a windows + specific workaround to avoid looping because we call both + select and Sleep, both with different time resolutions. + - document range and guarantees of ev_sleep. + - document reasonable ranges for periodics interval and offset. + - rename backend_fudge to backend_mintime to avoid future confusion :) + - change the default periodic reschedule function to hopefully be more + exact and correct even in corner cases or in the far future. + - do not rely on -lm anymore: use it when available but use our + own floor () if it is missing. This should make it easier to embed, + as no external libraries are required. + - strategically import macros from libecb and mark rarely-used functions + as cache-cold (saving almost 2k code size on typical amd64 setups). + - add Symbols.ev and Symbols.event files, that were missing. + - fix backend_mintime value for epoll (was 1/1024, is 1/1000 now). + - fix #3 "be smart about timeouts" to not "deadlock" when + timeout == now, also improve the section overall. + - avoid "AVOIDING FINISHING BEFORE RETURNING" idiom. + - support new EV_API_STATIC mode to make all libev symbols + static. + - supply default CFLAGS of -g -O3 with gcc when original CFLAGS + were empty. + +4.04 Wed Feb 16 09:01:51 CET 2011 + - fix two problems in the native win32 backend, where reuse of fd's + with different underlying handles caused handles not to be removed + or added to the select set (analyzed and tested by Bert Belder). + - do no rely on ceil() in ev_e?poll.c. + - backport libev to HP-UX versions before 11 v3. + - configure did not detect nanosleep and clock_gettime properly when + they are available in the libc (as opposed to -lrt). + +4.03 Tue Jan 11 14:37:25 CET 2011 + - officially support polling files with all backends. + - support files, /dev/zero etc. the same way as select in the epoll + backend, by generating events on our own. + - ports backend: work around solaris bug 6874410 and many related ones + (EINTR, maybe more), with no performance loss (note that the solaris + bug report is actually wrong, reality is far more bizarre and broken + than that). + - define EV_READ/EV_WRITE as macros in event.h, as some programs use + #ifdef to test for them. + - new (experimental) function: ev_feed_signal. + - new (to become default) EVFLAG_NOSIGMASK flag. + - new EVBACKEND_MASK symbol. + - updated COMMON IDIOMS SECTION. + +4.01 Fri Nov 5 21:51:29 CET 2010 + - automake fucked it up, apparently, --add-missing -f is not quite enough + to make it update its files, so 4.00 didn't install ev++.h and + event.h on make install. grrr. + - ev_loop(count|depth) didn't return anything (Robin Haberkorn). + - change EV_UNDEF to 0xffffffff to silence some overzealous compilers. + - use "(libev) " prefix for all libev error messages now. + +4.00 Mon Oct 25 12:32:12 CEST 2010 + - "PORTING FROM LIBEV 3.X TO 4.X" (in ev.pod) is recommended reading. + - ev_embed_stop did not correctly stop the watcher (very good + testcase by Vladimir Timofeev). + - ev_run will now always update the current loop time - it erroneously + didn't when idle watchers were active, causing timers not to fire. + - fix a bug where a timeout of zero caused the timer not to fire + in the libevent emulation (testcase by Péter Szabó). + - applied win32 fixes by Michael Lenaghan (also James Mansion). + - replace EV_MINIMAL by EV_FEATURES. + - prefer EPOLL_CTL_ADD over EPOLL_CTL_MOD in some more cases, as it + seems the former is *much* faster than the latter. + - linux kernel version detection (for inotify bug workarounds) + did not work properly. + - reduce the number of spurious wake-ups with the ports backend. + - remove dependency on sys/queue.h on freebsd (patch by Vanilla Hsu). + - do async init within ev_async_start, not ev_async_set, which avoids + an API quirk where the set function must be called in the C++ API + even when there is nothing to set. + - add (undocumented) EV_ENABLE when adding events with kqueue, + this might help with OS X, which seems to need it despite documenting + not to need it (helpfully pointed out by Tilghman Lesher). + - do not use poll by default on freebsd, it's broken (what isn't + on freebsd...). + - allow to embed epoll on kernels >= 2.6.32. + - configure now prepends -O3, not appends it, so one can still + override it. + - ev.pod: greatly expanded the portability section, added a porting + section, a description of watcher states and made lots of minor fixes. + - disable poll backend on AIX, the poll header spams the namespace + and it's not worth working around dead platforms (reported + and analyzed by Aivars Kalvans). + - improve header file compatibility of the standalone eventfd code + in an obscure case. + - implement EV_AVOID_STDIO option. + - do not use sscanf to parse linux version number (smaller, faster, + no sscanf dependency). + - new EV_CHILD_ENABLE and EV_SIGNAL_ENABLE configurable settings. + - update libev.m4 HAVE_CLOCK_SYSCALL test for newer glibcs. + - add section on accept() problems to the manpage. + - rename EV_TIMEOUT to EV_TIMER. + - rename ev_loop_count/depth/verify/loop/unloop. + - remove ev_default_destroy and ev_default_fork. + - switch to two-digit minor version. + - work around an apparent gentoo compiler bug. + - define _DARWIN_UNLIMITED_SELECT. just so. + - use enum instead of #define for most constants. + - improve compatibility to older C++ compilers. + - (experimental) ev_run/ev_default_loop/ev_break/ev_loop_new have now + default arguments when compiled as C++. + - enable automake dependency tracking. + - ev_loop_new no longer leaks memory when loop creation failed. + - new ev_cleanup watcher type. + +3.9 Thu Dec 31 07:59:59 CET 2009 + - signalfd is no longer used by default and has to be requested + explicitly - this means that easy to catch bugs become hard to + catch race conditions, but the users have spoken. + - point out the unspecified signal mask in the documentation, and + that this is a race condition regardless of EV_SIGNALFD. + - backport inotify code to C89. + - inotify file descriptors could leak into child processes. + - ev_stat watchers could keep an erroneous extra ref on the loop, + preventing exit when unregistering all watchers (testcases + provided by ry@tinyclouds.org). + - implement EV_WIN32_HANDLE_TO_FD and EV_WIN32_CLOSE_FD configuration + symbols to make it easier for apps to do their own fd management. + - support EV_IDLE_ENABLE being disabled in ev++.h + (patch by Didier Spezia). + - take advantage of inotify_init1, if available, to set cloexec/nonblock + on fd creation, to avoid races. + - the signal handling pipe wasn't always initialised under windows + (analysed by lekma). + - changed minimum glibc requirement from glibc 2.9 to 2.7, for + signalfd. + - add missing string.h include (Denis F. Latypoff). + - only replace ev_stat.prev when we detect an actual difference, + so prev is (almost) always different to attr. this might + have caused the problems with 04_stat.t. + - add ev::timer->remaining () method to C++ API. + +3.8 Sun Aug 9 14:30:45 CEST 2009 + - incompatible change: do not necessarily reset signal handler + to SIG_DFL when a sighandler is stopped. + - ev_default_destroy did not properly free or zero some members, + potentially causing crashes and memory corruption on repeated + ev_default_destroy/ev_default_loop calls. + - take advantage of signalfd on GNU/Linux systems. + - document that the signal mask might be in an unspecified + state when using libev's signal handling. + - take advantage of some GNU/Linux calls to set cloexec/nonblock + on fd creation, to avoid race conditions. + +3.7 Fri Jul 17 16:36:32 CEST 2009 + - ev_unloop and ev_loop wrongly used a global variable to exit loops, + instead of using a per-loop variable (bug caught by accident...). + - the ev_set_io_collect_interval interpretation has changed. + - add new functionality: ev_set_userdata, ev_userdata, + ev_set_invoke_pending_cb, ev_set_loop_release_cb, + ev_invoke_pending, ev_pending_count, together with a long example + about thread locking. + - add ev_timer_remaining (as requested by Denis F. Latypoff). + - add ev_loop_depth. + - calling ev_unloop in fork/prepare watchers will no longer poll + for new events. + - Denis F. Latypoff corrected many typos in example code snippets. + - honor autoconf detection of EV_USE_CLOCK_SYSCALL, also double- + check that the syscall number is available before trying to + use it (reported by ry@tinyclouds). + - use GetSystemTimeAsFileTime instead of _timeb on windows, for + slightly higher accuracy. + - properly declare ev_loop_verify and ev_now_update even when + !EV_MULTIPLICITY. + - do not compile in any priority code when EV_MAXPRI == EV_MINPRI. + - support EV_MINIMAL==2 for a reduced API. + - actually 0-initialise struct sigaction when installing signals. + - add section on hibernate and stopped processes to ev_timer docs. + +3.6 Tue Apr 28 02:49:30 CEST 2009 + - multiple timers becoming ready within an event loop iteration + will be invoked in the "correct" order now. + - do not leave the event loop early just because we have no active + watchers, fixing a problem when embedding a kqueue loop + that has active kernel events but no registered watchers + (reported by blacksand blacksand). + - correctly zero the idx values for arrays, so destroying and + reinitialising the default loop actually works (patch by + Malek Hadj-Ali). + - implement ev_suspend and ev_resume. + - new EV_CUSTOM revents flag for use by applications. + - add documentation section about priorities. + - add a glossary to the documentation. + - extend the ev_fork description slightly. + - optimize a jump out of call_pending. + +3.53 Sun Feb 15 02:38:20 CET 2009 + - fix a bug in event pipe creation on win32 that would cause a + failed assertion on event loop creation (patch by Malek Hadj-Ali). + - probe for CLOCK_REALTIME support at runtime as well and fall + back to gettimeofday if there is an error, to support older + operating systems with newer header files/libraries. + - prefer gettimeofday over clock_gettime with USE_CLOCK_SYSCALL + (default most everywhere), otherwise not. + +3.52 Wed Jan 7 21:43:02 CET 2009 + - fix compilation of select backend in fd_set mode when NFDBITS is + missing (to get it to compile on QNX, reported by Rodrigo Campos). + - better select-nfds handling when select backend is in fd_set mode. + - diagnose fd_set overruns when select backend is in fd_set mode. + - due to a thinko, instead of disabling everything but + select on the borked OS X platform, everything but select was + allowed (reported by Emanuele Giaquinta). + - actually verify that local and remote port are matching in + libev's socketpair emulation, which makes denial-of-service + attacks harder (but not impossible - it's windows). Make sure + it even works under vista, which thinks that getpeer/sockname + should return fantasy port numbers. + - include "libev" in all assertion messages for potentially + clearer diagnostics. + - event_get_version (libevent compatibility) returned + a useless string instead of the expected version string + (patch by W.C.A. Wijngaards). + +3.51 Wed Dec 24 23:00:11 CET 2008 + - fix a bug where an inotify watcher was added twice, causing + freezes on hash collisions (reported and analysed by Graham Leggett). + - new config symbol, EV_USE_CLOCK_SYSCALL, to make libev use + a direct syscall - slower, but no dependency on librt et al. + - assume negative return values != -1 signals success of port_getn + (http://cvs.epicsol.org/cgi/viewcvs.cgi/epic5/source/newio.c?rev=1.52) + (no known failure reports, but it doesn't hurt). + - fork detection in ev_embed now stops and restarts the watcher + automatically. + - EXPERIMENTAL: default the method to operator () in ev++.h, + to make it nicer to use functors (requested by Benedek László). + - fixed const object callbacks in ev++.h. + - replaced loop_ref argument of watcher.set (loop) by a direct + ev_loop * in ev++.h, to avoid clashes with functor patch. + - do not try to watch the empty string via inotify. + - inotify watchers could be leaked under certain circumstances. + - OS X 10.5 is actually even more broken than earlier versions, + so fall back to select on that piece of garbage. + - fixed some weirdness in the ev_embed documentation. + +3.49 Wed Nov 19 11:26:53 CET 2008 + - ev_stat watchers will now use inotify as a mere hint on + kernels <2.6.25, or if the filesystem is not in the + "known to be good" list. + - better mingw32 compatibility (it's not as borked as native win32) + (analysed by Roger Pack). + - include stdio.h in the example program, as too many people are + confused by the weird C language otherwise. I guess the next thing + I get told is that the "..." ellipses in the examples don't compile + with their C compiler. + +3.48 Thu Oct 30 09:02:37 CET 2008 + - further optimise away the EPOLL_CTL_ADD/MOD combo in the epoll + backend by assuming the kernel event mask hasn't changed if + ADD fails with EEXIST. + - work around spurious event notification bugs in epoll by using + a 32-bit generation counter. recreate kernel state if we receive + spurious notifications or unwanted events. this is very costly, + but I didn't come up with this horrible design. + - use memset to initialise most arrays now and do away with the + init functions. + - expand time-out strategies into a "Be smart about timeouts" section. + - drop the "struct" from all ev_watcher declarations in the + documentation and did other clarifications (yeah, it was a mistake + to have a struct AND a function called ev_loop). + - fix a bug where ev_default would not initialise the default + loop again after it was destroyed with ev_default_destroy. + - rename syserr to ev_syserr to avoid name clashes when embedding, + do similar changes for event.c. + +3.45 Tue Oct 21 21:59:26 CEST 2008 + - disable inotify usage on linux <2.6.25, as it is broken + (reported by Yoann Vandoorselaere). + - ev_stat erroneously would try to add inotify watchers + even when inotify wasn't available (this should only + have a performance impact). + - ev_once now passes both timeout and io to the callback if both + occur concurrently, instead of giving timeouts precedence. + - disable EV_USE_INOTIFY when sys/inotify.h is too old. + +3.44 Mon Sep 29 05:18:39 CEST 2008 + - embed watchers now automatically invoke ev_loop_fork on the + embedded loop when the parent loop forks. + - new function: ev_now_update (loop). + - verify_watcher was not marked static. + - improve the "associating..." manpage section. + - documentation tweaks here and there. + +3.43 Sun Jul 6 05:34:41 CEST 2008 + - include more include files on windows to get struct _stati64 + (reported by Chris Hulbert, but doesn't quite fix his issue). + - add missing #include in ev.c on windows (reported by + Matt Tolton). + +3.42 Tue Jun 17 12:12:07 CEST 2008 + - work around yet another windows bug: FD_SET actually adds fd's + multiple times to the fd_*SET*, despite official MSN docs claiming + otherwise. Reported and well-analysed by Matt Tolton. + - define NFDBITS to 0 when EV_SELECT_IS_WINSOCKET to make it compile + (reported any analysed by Chris Hulbert). + - fix a bug in ev_ebadf (this function is only used to catch + programming errors in the libev user). reported by Matt Tolton. + - fix a bug in fd_intern on win32 (could lead to compile errors + under some circumstances, but would work correctly if it compiles). + reported by Matt Tolton. + - (try to) work around missing lstat on windows. + - pass in the write fd set as except fd set under windows. windows + is so uncontrollably lame that it requires this. this means that + switching off oobinline is not supported (but tcp/ip doesn't + have oob, so that would be stupid anyways. + - use posix module symbol to auto-detect monotonic clock presence + and some other default values. + +3.41 Fri May 23 18:42:54 CEST 2008 + - work around an obscure bug in winsocket select: if you + provide only empty fd sets then select returns WSAEINVAL. how sucky. + - improve timer scheduling stability and reduce use of time_epsilon. + - use 1-based 2-heap for EV_MINIMAL, simplifies code, reduces + codesize and makes for better cache-efficiency. + - use 3-based 4-heap for !EV_MINIMAL. this makes better use + of cpu cache lines and gives better growth behaviour than + 2-based heaps. + - cache timestamp within heap for !EV_MINIMAL, to avoid random + memory accesses. + - document/add EV_USE_4HEAP and EV_HEAP_CACHE_AT. + - fix a potential aliasing issue in ev_timer_again. + - add/document ev_periodic_at, retract direct access to ->at. + - improve ev_stat docs. + - add portability requirements section. + - fix manpage headers etc. + - normalise WSA error codes to lower range on windows. + - add consistency check code that can be called automatically + or on demand to check for internal structures (ev_loop_verify). + +3.31 Wed Apr 16 20:45:04 CEST 2008 + - added last minute fix for ev_poll.c by Brandon Black. + +3.3 Wed Apr 16 19:04:10 CEST 2008 + - event_base_loopexit should return 0 on success + (W.C.A. Wijngaards). + - added linux eventfd support. + - try to autodetect epoll and inotify support + by libc header version if not using autoconf. + - new symbols: EV_DEFAULT_UC and EV_DEFAULT_UC_. + - declare functions defined in ev.h as inline if + C99 or gcc are available. + - enable inlining with gcc versions 2 and 3. + - work around broken poll implementations potentially + not clearing revents field in ev_poll (Brandon Black) + (no such systems are known at this time). + - work around a bug in realloc on openbsd and darwin, + also makes the erroneous valgrind complaints + go away (noted by various people). + - fix ev_async_pending, add c++ wrapper for ev_async + (based on patch sent by Johannes Deisenhofer). + - add sensible set method to ev::embed. + - made integer constants type int in ev.h. + +3.2 Wed Apr 2 17:11:19 CEST 2008 + - fix a 64 bit overflow issue in the select backend, + by using fd_mask instead of int for the mask. + - rename internal sighandler to avoid clash with very old perls. + - entering ev_loop will not clear the ONESHOT or NONBLOCKING + flags of any outer loops anymore. + - add ev_async_pending. + +3.1 Thu Mar 13 13:45:22 CET 2008 + - implement ev_async watchers. + - only initialise signal pipe on demand. + - make use of sig_atomic_t configurable. + - improved documentation. + +3.0 Mon Jan 28 13:14:47 CET 2008 + - API/ABI bump to version 3.0. + - ev++.h includes "ev.h" by default now, not . + - slightly improved documentation. + - speed up signal detection after a fork. + - only optionally return trace status changed in ev_child + watchers. + - experimental (and undocumented) loop wrappers for ev++.h. + +2.01 Tue Dec 25 08:04:41 CET 2007 + - separate Changes file. + - fix ev_path_set => ev_stat_set typo. + - remove event_compat.h from the libev tarball. + - change how include files are found. + - doc updates. + - update licenses, explicitly allow for GPL relicensing. + +2.0 Sat Dec 22 17:47:03 CET 2007 + - new ev_sleep, ev_set_(io|timeout)_collect_interval. + - removed epoll from embeddable fd set. + - fix embed watchers. + - renamed ev_embed.loop to other. + - added exported Symbol tables. + - undefine member wrapper macros at the end of ev.c. + - respect EV_H in ev++.h. + +1.86 Tue Dec 18 02:36:57 CET 2007 + - fix memleak on loop destroy (not relevant for perl). + +1.85 Fri Dec 14 20:32:40 CET 2007 + - fix some aliasing issues w.r.t. timers and periodics + (not relevant for perl). + +(for historic versions refer to EV/Changes, found in the Perl interface) + +0.1 Wed Oct 31 21:31:48 CET 2007 + - original version; hacked together in <24h. + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/LICENSE b/deps/ngtcp2/ngtcp2/third-party/libev/LICENSE new file mode 100644 index 00000000000000..2fdabd48afad85 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/LICENSE @@ -0,0 +1,37 @@ +All files in libev are +Copyright (c)2007,2008,2009,2010,2011,2012,2013 Marc Alexander Lehmann. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Alternatively, the contents of this package may be used under the terms +of the GNU General Public License ("GPL") version 2 or any later version, +in which case the provisions of the GPL are applicable instead of the +above. If you wish to allow the use of your version of this package only +under the terms of the GPL and not to allow others to use your version of +this file under the BSD license, indicate your decision by deleting the +provisions above and replace them with the notice and other provisions +required by the GPL in this and the other files of this package. If you do +not delete the provisions above, a recipient may use your version of this +file under either the BSD or the GPL. diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/README b/deps/ngtcp2/ngtcp2/third-party/libev/README new file mode 100644 index 00000000000000..fca5fdf1a8ec18 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/README @@ -0,0 +1,59 @@ +libev is a high-performance event loop/event model with lots of features. +(see benchmark at http://libev.schmorp.de/bench.html) + + +ABOUT + + Homepage: http://software.schmorp.de/pkg/libev + Mailinglist: libev@lists.schmorp.de + http://lists.schmorp.de/cgi-bin/mailman/listinfo/libev + Library Documentation: http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod + + Libev is modelled (very losely) after libevent and the Event perl + module, but is faster, scales better and is more correct, and also more + featureful. And also smaller. Yay. + + Some of the specialties of libev not commonly found elsewhere are: + + - extensive and detailed, readable documentation (not doxygen garbage). + - fully supports fork, can detect fork in various ways and automatically + re-arms kernel mechanisms that do not support fork. + - highly optimised select, poll, linux epoll, linux aio, bsd kqueue + and solaris event ports backends. + - filesystem object (path) watching (with optional linux inotify support). + - wallclock-based times (using absolute time, cron-like). + - relative timers/timeouts (handle time jumps). + - fast intra-thread communication between multiple + event loops (with optional fast linux eventfd backend). + - extremely easy to embed (fully documented, no dependencies, + autoconf supported but optional). + - very small codebase, no bloated library, simple code. + - fully extensible by being able to plug into the event loop, + integrate other event loops, integrate other event loop users. + - very little memory use (small watchers, small event loop data). + - optional C++ interface allowing method and function callbacks + at no extra memory or runtime overhead. + - optional Perl interface with similar characteristics (capable + of running Glib/Gtk2 on libev). + - support for other languages (multiple C++ interfaces, D, Ruby, + Python) available from third-parties. + + Examples of programs that embed libev: the EV perl module, node.js, + auditd, rxvt-unicode, gvpe (GNU Virtual Private Ethernet), the + Deliantra MMORPG server (http://www.deliantra.net/), Rubinius (a + next-generation Ruby VM), the Ebb web server, the Rev event toolkit. + + +CONTRIBUTORS + + libev was written and designed by Marc Lehmann and Emanuele Giaquinta. + + The following people sent in patches or made other noteworthy + contributions to the design (for minor patches, see the Changes + file. If I forgot to include you, please shout at me, it was an + accident): + + W.C.A. Wijngaards + Christopher Layne + Chris Brody + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/Symbols.ev b/deps/ngtcp2/ngtcp2/third-party/libev/Symbols.ev new file mode 100644 index 00000000000000..fe169fa064dab6 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/Symbols.ev @@ -0,0 +1,73 @@ +ev_async_send +ev_async_start +ev_async_stop +ev_backend +ev_break +ev_check_start +ev_check_stop +ev_child_start +ev_child_stop +ev_cleanup_start +ev_cleanup_stop +ev_clear_pending +ev_default_loop +ev_default_loop_ptr +ev_depth +ev_embeddable_backends +ev_embed_start +ev_embed_stop +ev_embed_sweep +ev_feed_event +ev_feed_fd_event +ev_feed_signal +ev_feed_signal_event +ev_fork_start +ev_fork_stop +ev_idle_start +ev_idle_stop +ev_invoke +ev_invoke_pending +ev_io_start +ev_io_stop +ev_iteration +ev_loop_destroy +ev_loop_fork +ev_loop_new +ev_now +ev_now_update +ev_once +ev_pending_count +ev_periodic_again +ev_periodic_start +ev_periodic_stop +ev_prepare_start +ev_prepare_stop +ev_recommended_backends +ev_ref +ev_resume +ev_run +ev_set_allocator +ev_set_invoke_pending_cb +ev_set_io_collect_interval +ev_set_loop_release_cb +ev_set_syserr_cb +ev_set_timeout_collect_interval +ev_set_userdata +ev_signal_start +ev_signal_stop +ev_sleep +ev_stat_start +ev_stat_stat +ev_stat_stop +ev_supported_backends +ev_suspend +ev_time +ev_timer_again +ev_timer_remaining +ev_timer_start +ev_timer_stop +ev_unref +ev_userdata +ev_verify +ev_version_major +ev_version_minor diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/Symbols.event b/deps/ngtcp2/ngtcp2/third-party/libev/Symbols.event new file mode 100644 index 00000000000000..799d4246e4e07a --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/Symbols.event @@ -0,0 +1,24 @@ +event_active +event_add +event_base_dispatch +event_base_free +event_base_get_method +event_base_loop +event_base_loopexit +event_base_new +event_base_once +event_base_priority_init +event_base_set +event_del +event_dispatch +event_get_callback +event_get_method +event_get_version +event_init +event_loop +event_loopexit +event_once +event_pending +event_priority_init +event_priority_set +event_set diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/TODO b/deps/ngtcp2/ngtcp2/third-party/libev/TODO new file mode 100644 index 00000000000000..a9d2c91f61d356 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/TODO @@ -0,0 +1,14 @@ +TODO: ev_loop_wakeup +TODO: EV_STANDALONE == NO_HASSLE (do not use clock_gettime in ev_standalone) +TODO: faq, process a thing in each iteration +TODO: dbeugging tips, ev_verify, ev_init twice +TODO: ev_break for immediate exit (EVBREAK_NOW?) +TODO: ev_feed_child_event +TODO: document the special problem of signals around fork. +TODO: store pid for each signal +TODO: document file descriptor usage per loop +TODO: store loop pid_t and compare isndie signal handler,store 1 for same, 2 for differign pid, clean up in loop_fork +TODO: embed watchers need updating when fd changes +TODO: document portability requirements for atomic pointer access +TODO: document requirements for function pointers and calling conventions. + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/aclocal.m4 b/deps/ngtcp2/ngtcp2/third-party/libev/aclocal.m4 new file mode 100644 index 00000000000000..00d36ebd6340b1 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/aclocal.m4 @@ -0,0 +1,10219 @@ +# generated automatically by aclocal 1.16.1 -*- Autoconf -*- + +# Copyright (C) 1996-2018 Free Software Foundation, Inc. + +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +m4_ifndef([AC_CONFIG_MACRO_DIRS], [m4_defun([_AM_CONFIG_MACRO_DIRS], [])m4_defun([AC_CONFIG_MACRO_DIRS], [_AM_CONFIG_MACRO_DIRS($@)])]) +m4_ifndef([AC_AUTOCONF_VERSION], + [m4_copy([m4_PACKAGE_VERSION], [AC_AUTOCONF_VERSION])])dnl +m4_if(m4_defn([AC_AUTOCONF_VERSION]), [2.69],, +[m4_warning([this file was generated for autoconf 2.69. +You have another version of autoconf. It may work, but is not guaranteed to. +If you have problems, you may need to regenerate the build system entirely. +To do so, use the procedure documented by the package, typically 'autoreconf'.])]) + +# libtool.m4 - Configure libtool for the host system. -*-Autoconf-*- +# +# Copyright (C) 1996-2001, 2003-2015 Free Software Foundation, Inc. +# Written by Gordon Matzigkeit, 1996 +# +# This file is free software; the Free Software Foundation gives +# unlimited permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. + +m4_define([_LT_COPYING], [dnl +# Copyright (C) 2014 Free Software Foundation, Inc. +# This is free software; see the source for copying conditions. There is NO +# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +# GNU Libtool is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of of the License, or +# (at your option) any later version. +# +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program or library that is built +# using GNU Libtool, you may include this file under the same +# distribution terms that you use for the rest of that program. +# +# GNU Libtool is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +]) + +# serial 58 LT_INIT + + +# LT_PREREQ(VERSION) +# ------------------ +# Complain and exit if this libtool version is less that VERSION. +m4_defun([LT_PREREQ], +[m4_if(m4_version_compare(m4_defn([LT_PACKAGE_VERSION]), [$1]), -1, + [m4_default([$3], + [m4_fatal([Libtool version $1 or higher is required], + 63)])], + [$2])]) + + +# _LT_CHECK_BUILDDIR +# ------------------ +# Complain if the absolute build directory name contains unusual characters +m4_defun([_LT_CHECK_BUILDDIR], +[case `pwd` in + *\ * | *\ *) + AC_MSG_WARN([Libtool does not cope well with whitespace in `pwd`]) ;; +esac +]) + + +# LT_INIT([OPTIONS]) +# ------------------ +AC_DEFUN([LT_INIT], +[AC_PREREQ([2.62])dnl We use AC_PATH_PROGS_FEATURE_CHECK +AC_REQUIRE([AC_CONFIG_AUX_DIR_DEFAULT])dnl +AC_BEFORE([$0], [LT_LANG])dnl +AC_BEFORE([$0], [LT_OUTPUT])dnl +AC_BEFORE([$0], [LTDL_INIT])dnl +m4_require([_LT_CHECK_BUILDDIR])dnl + +dnl Autoconf doesn't catch unexpanded LT_ macros by default: +m4_pattern_forbid([^_?LT_[A-Z_]+$])dnl +m4_pattern_allow([^(_LT_EOF|LT_DLGLOBAL|LT_DLLAZY_OR_NOW|LT_MULTI_MODULE)$])dnl +dnl aclocal doesn't pull ltoptions.m4, ltsugar.m4, or ltversion.m4 +dnl unless we require an AC_DEFUNed macro: +AC_REQUIRE([LTOPTIONS_VERSION])dnl +AC_REQUIRE([LTSUGAR_VERSION])dnl +AC_REQUIRE([LTVERSION_VERSION])dnl +AC_REQUIRE([LTOBSOLETE_VERSION])dnl +m4_require([_LT_PROG_LTMAIN])dnl + +_LT_SHELL_INIT([SHELL=${CONFIG_SHELL-/bin/sh}]) + +dnl Parse OPTIONS +_LT_SET_OPTIONS([$0], [$1]) + +# This can be used to rebuild libtool when needed +LIBTOOL_DEPS=$ltmain + +# Always use our own libtool. +LIBTOOL='$(SHELL) $(top_builddir)/libtool' +AC_SUBST(LIBTOOL)dnl + +_LT_SETUP + +# Only expand once: +m4_define([LT_INIT]) +])# LT_INIT + +# Old names: +AU_ALIAS([AC_PROG_LIBTOOL], [LT_INIT]) +AU_ALIAS([AM_PROG_LIBTOOL], [LT_INIT]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_PROG_LIBTOOL], []) +dnl AC_DEFUN([AM_PROG_LIBTOOL], []) + + +# _LT_PREPARE_CC_BASENAME +# ----------------------- +m4_defun([_LT_PREPARE_CC_BASENAME], [ +# Calculate cc_basename. Skip known compiler wrappers and cross-prefix. +func_cc_basename () +{ + for cc_temp in @S|@*""; do + case $cc_temp in + compile | *[[\\/]]compile | ccache | *[[\\/]]ccache ) ;; + distcc | *[[\\/]]distcc | purify | *[[\\/]]purify ) ;; + \-*) ;; + *) break;; + esac + done + func_cc_basename_result=`$ECHO "$cc_temp" | $SED "s%.*/%%; s%^$host_alias-%%"` +} +])# _LT_PREPARE_CC_BASENAME + + +# _LT_CC_BASENAME(CC) +# ------------------- +# It would be clearer to call AC_REQUIREs from _LT_PREPARE_CC_BASENAME, +# but that macro is also expanded into generated libtool script, which +# arranges for $SED and $ECHO to be set by different means. +m4_defun([_LT_CC_BASENAME], +[m4_require([_LT_PREPARE_CC_BASENAME])dnl +AC_REQUIRE([_LT_DECL_SED])dnl +AC_REQUIRE([_LT_PROG_ECHO_BACKSLASH])dnl +func_cc_basename $1 +cc_basename=$func_cc_basename_result +]) + + +# _LT_FILEUTILS_DEFAULTS +# ---------------------- +# It is okay to use these file commands and assume they have been set +# sensibly after 'm4_require([_LT_FILEUTILS_DEFAULTS])'. +m4_defun([_LT_FILEUTILS_DEFAULTS], +[: ${CP="cp -f"} +: ${MV="mv -f"} +: ${RM="rm -f"} +])# _LT_FILEUTILS_DEFAULTS + + +# _LT_SETUP +# --------- +m4_defun([_LT_SETUP], +[AC_REQUIRE([AC_CANONICAL_HOST])dnl +AC_REQUIRE([AC_CANONICAL_BUILD])dnl +AC_REQUIRE([_LT_PREPARE_SED_QUOTE_VARS])dnl +AC_REQUIRE([_LT_PROG_ECHO_BACKSLASH])dnl + +_LT_DECL([], [PATH_SEPARATOR], [1], [The PATH separator for the build system])dnl +dnl +_LT_DECL([], [host_alias], [0], [The host system])dnl +_LT_DECL([], [host], [0])dnl +_LT_DECL([], [host_os], [0])dnl +dnl +_LT_DECL([], [build_alias], [0], [The build system])dnl +_LT_DECL([], [build], [0])dnl +_LT_DECL([], [build_os], [0])dnl +dnl +AC_REQUIRE([AC_PROG_CC])dnl +AC_REQUIRE([LT_PATH_LD])dnl +AC_REQUIRE([LT_PATH_NM])dnl +dnl +AC_REQUIRE([AC_PROG_LN_S])dnl +test -z "$LN_S" && LN_S="ln -s" +_LT_DECL([], [LN_S], [1], [Whether we need soft or hard links])dnl +dnl +AC_REQUIRE([LT_CMD_MAX_LEN])dnl +_LT_DECL([objext], [ac_objext], [0], [Object file suffix (normally "o")])dnl +_LT_DECL([], [exeext], [0], [Executable file suffix (normally "")])dnl +dnl +m4_require([_LT_FILEUTILS_DEFAULTS])dnl +m4_require([_LT_CHECK_SHELL_FEATURES])dnl +m4_require([_LT_PATH_CONVERSION_FUNCTIONS])dnl +m4_require([_LT_CMD_RELOAD])dnl +m4_require([_LT_CHECK_MAGIC_METHOD])dnl +m4_require([_LT_CHECK_SHAREDLIB_FROM_LINKLIB])dnl +m4_require([_LT_CMD_OLD_ARCHIVE])dnl +m4_require([_LT_CMD_GLOBAL_SYMBOLS])dnl +m4_require([_LT_WITH_SYSROOT])dnl +m4_require([_LT_CMD_TRUNCATE])dnl + +_LT_CONFIG_LIBTOOL_INIT([ +# See if we are running on zsh, and set the options that allow our +# commands through without removal of \ escapes INIT. +if test -n "\${ZSH_VERSION+set}"; then + setopt NO_GLOB_SUBST +fi +]) +if test -n "${ZSH_VERSION+set}"; then + setopt NO_GLOB_SUBST +fi + +_LT_CHECK_OBJDIR + +m4_require([_LT_TAG_COMPILER])dnl + +case $host_os in +aix3*) + # AIX sometimes has problems with the GCC collect2 program. For some + # reason, if we set the COLLECT_NAMES environment variable, the problems + # vanish in a puff of smoke. + if test set != "${COLLECT_NAMES+set}"; then + COLLECT_NAMES= + export COLLECT_NAMES + fi + ;; +esac + +# Global variables: +ofile=libtool +can_build_shared=yes + +# All known linkers require a '.a' archive for static linking (except MSVC, +# which needs '.lib'). +libext=a + +with_gnu_ld=$lt_cv_prog_gnu_ld + +old_CC=$CC +old_CFLAGS=$CFLAGS + +# Set sane defaults for various variables +test -z "$CC" && CC=cc +test -z "$LTCC" && LTCC=$CC +test -z "$LTCFLAGS" && LTCFLAGS=$CFLAGS +test -z "$LD" && LD=ld +test -z "$ac_objext" && ac_objext=o + +_LT_CC_BASENAME([$compiler]) + +# Only perform the check for file, if the check method requires it +test -z "$MAGIC_CMD" && MAGIC_CMD=file +case $deplibs_check_method in +file_magic*) + if test "$file_magic_cmd" = '$MAGIC_CMD'; then + _LT_PATH_MAGIC + fi + ;; +esac + +# Use C for the default configuration in the libtool script +LT_SUPPORTED_TAG([CC]) +_LT_LANG_C_CONFIG +_LT_LANG_DEFAULT_CONFIG +_LT_CONFIG_COMMANDS +])# _LT_SETUP + + +# _LT_PREPARE_SED_QUOTE_VARS +# -------------------------- +# Define a few sed substitution that help us do robust quoting. +m4_defun([_LT_PREPARE_SED_QUOTE_VARS], +[# Backslashify metacharacters that are still active within +# double-quoted strings. +sed_quote_subst='s/\([["`$\\]]\)/\\\1/g' + +# Same as above, but do not quote variable references. +double_quote_subst='s/\([["`\\]]\)/\\\1/g' + +# Sed substitution to delay expansion of an escaped shell variable in a +# double_quote_subst'ed string. +delay_variable_subst='s/\\\\\\\\\\\$/\\\\\\$/g' + +# Sed substitution to delay expansion of an escaped single quote. +delay_single_quote_subst='s/'\''/'\'\\\\\\\'\''/g' + +# Sed substitution to avoid accidental globbing in evaled expressions +no_glob_subst='s/\*/\\\*/g' +]) + +# _LT_PROG_LTMAIN +# --------------- +# Note that this code is called both from 'configure', and 'config.status' +# now that we use AC_CONFIG_COMMANDS to generate libtool. Notably, +# 'config.status' has no value for ac_aux_dir unless we are using Automake, +# so we pass a copy along to make sure it has a sensible value anyway. +m4_defun([_LT_PROG_LTMAIN], +[m4_ifdef([AC_REQUIRE_AUX_FILE], [AC_REQUIRE_AUX_FILE([ltmain.sh])])dnl +_LT_CONFIG_LIBTOOL_INIT([ac_aux_dir='$ac_aux_dir']) +ltmain=$ac_aux_dir/ltmain.sh +])# _LT_PROG_LTMAIN + + + +# So that we can recreate a full libtool script including additional +# tags, we accumulate the chunks of code to send to AC_CONFIG_COMMANDS +# in macros and then make a single call at the end using the 'libtool' +# label. + + +# _LT_CONFIG_LIBTOOL_INIT([INIT-COMMANDS]) +# ---------------------------------------- +# Register INIT-COMMANDS to be passed to AC_CONFIG_COMMANDS later. +m4_define([_LT_CONFIG_LIBTOOL_INIT], +[m4_ifval([$1], + [m4_append([_LT_OUTPUT_LIBTOOL_INIT], + [$1 +])])]) + +# Initialize. +m4_define([_LT_OUTPUT_LIBTOOL_INIT]) + + +# _LT_CONFIG_LIBTOOL([COMMANDS]) +# ------------------------------ +# Register COMMANDS to be passed to AC_CONFIG_COMMANDS later. +m4_define([_LT_CONFIG_LIBTOOL], +[m4_ifval([$1], + [m4_append([_LT_OUTPUT_LIBTOOL_COMMANDS], + [$1 +])])]) + +# Initialize. +m4_define([_LT_OUTPUT_LIBTOOL_COMMANDS]) + + +# _LT_CONFIG_SAVE_COMMANDS([COMMANDS], [INIT_COMMANDS]) +# ----------------------------------------------------- +m4_defun([_LT_CONFIG_SAVE_COMMANDS], +[_LT_CONFIG_LIBTOOL([$1]) +_LT_CONFIG_LIBTOOL_INIT([$2]) +]) + + +# _LT_FORMAT_COMMENT([COMMENT]) +# ----------------------------- +# Add leading comment marks to the start of each line, and a trailing +# full-stop to the whole comment if one is not present already. +m4_define([_LT_FORMAT_COMMENT], +[m4_ifval([$1], [ +m4_bpatsubst([m4_bpatsubst([$1], [^ *], [# ])], + [['`$\]], [\\\&])]m4_bmatch([$1], [[!?.]$], [], [.]) +)]) + + + + + +# _LT_DECL([CONFIGNAME], VARNAME, VALUE, [DESCRIPTION], [IS-TAGGED?]) +# ------------------------------------------------------------------- +# CONFIGNAME is the name given to the value in the libtool script. +# VARNAME is the (base) name used in the configure script. +# VALUE may be 0, 1 or 2 for a computed quote escaped value based on +# VARNAME. Any other value will be used directly. +m4_define([_LT_DECL], +[lt_if_append_uniq([lt_decl_varnames], [$2], [, ], + [lt_dict_add_subkey([lt_decl_dict], [$2], [libtool_name], + [m4_ifval([$1], [$1], [$2])]) + lt_dict_add_subkey([lt_decl_dict], [$2], [value], [$3]) + m4_ifval([$4], + [lt_dict_add_subkey([lt_decl_dict], [$2], [description], [$4])]) + lt_dict_add_subkey([lt_decl_dict], [$2], + [tagged?], [m4_ifval([$5], [yes], [no])])]) +]) + + +# _LT_TAGDECL([CONFIGNAME], VARNAME, VALUE, [DESCRIPTION]) +# -------------------------------------------------------- +m4_define([_LT_TAGDECL], [_LT_DECL([$1], [$2], [$3], [$4], [yes])]) + + +# lt_decl_tag_varnames([SEPARATOR], [VARNAME1...]) +# ------------------------------------------------ +m4_define([lt_decl_tag_varnames], +[_lt_decl_filter([tagged?], [yes], $@)]) + + +# _lt_decl_filter(SUBKEY, VALUE, [SEPARATOR], [VARNAME1..]) +# --------------------------------------------------------- +m4_define([_lt_decl_filter], +[m4_case([$#], + [0], [m4_fatal([$0: too few arguments: $#])], + [1], [m4_fatal([$0: too few arguments: $#: $1])], + [2], [lt_dict_filter([lt_decl_dict], [$1], [$2], [], lt_decl_varnames)], + [3], [lt_dict_filter([lt_decl_dict], [$1], [$2], [$3], lt_decl_varnames)], + [lt_dict_filter([lt_decl_dict], $@)])[]dnl +]) + + +# lt_decl_quote_varnames([SEPARATOR], [VARNAME1...]) +# -------------------------------------------------- +m4_define([lt_decl_quote_varnames], +[_lt_decl_filter([value], [1], $@)]) + + +# lt_decl_dquote_varnames([SEPARATOR], [VARNAME1...]) +# --------------------------------------------------- +m4_define([lt_decl_dquote_varnames], +[_lt_decl_filter([value], [2], $@)]) + + +# lt_decl_varnames_tagged([SEPARATOR], [VARNAME1...]) +# --------------------------------------------------- +m4_define([lt_decl_varnames_tagged], +[m4_assert([$# <= 2])dnl +_$0(m4_quote(m4_default([$1], [[, ]])), + m4_ifval([$2], [[$2]], [m4_dquote(lt_decl_tag_varnames)]), + m4_split(m4_normalize(m4_quote(_LT_TAGS)), [ ]))]) +m4_define([_lt_decl_varnames_tagged], +[m4_ifval([$3], [lt_combine([$1], [$2], [_], $3)])]) + + +# lt_decl_all_varnames([SEPARATOR], [VARNAME1...]) +# ------------------------------------------------ +m4_define([lt_decl_all_varnames], +[_$0(m4_quote(m4_default([$1], [[, ]])), + m4_if([$2], [], + m4_quote(lt_decl_varnames), + m4_quote(m4_shift($@))))[]dnl +]) +m4_define([_lt_decl_all_varnames], +[lt_join($@, lt_decl_varnames_tagged([$1], + lt_decl_tag_varnames([[, ]], m4_shift($@))))dnl +]) + + +# _LT_CONFIG_STATUS_DECLARE([VARNAME]) +# ------------------------------------ +# Quote a variable value, and forward it to 'config.status' so that its +# declaration there will have the same value as in 'configure'. VARNAME +# must have a single quote delimited value for this to work. +m4_define([_LT_CONFIG_STATUS_DECLARE], +[$1='`$ECHO "$][$1" | $SED "$delay_single_quote_subst"`']) + + +# _LT_CONFIG_STATUS_DECLARATIONS +# ------------------------------ +# We delimit libtool config variables with single quotes, so when +# we write them to config.status, we have to be sure to quote all +# embedded single quotes properly. In configure, this macro expands +# each variable declared with _LT_DECL (and _LT_TAGDECL) into: +# +# ='`$ECHO "$" | $SED "$delay_single_quote_subst"`' +m4_defun([_LT_CONFIG_STATUS_DECLARATIONS], +[m4_foreach([_lt_var], m4_quote(lt_decl_all_varnames), + [m4_n([_LT_CONFIG_STATUS_DECLARE(_lt_var)])])]) + + +# _LT_LIBTOOL_TAGS +# ---------------- +# Output comment and list of tags supported by the script +m4_defun([_LT_LIBTOOL_TAGS], +[_LT_FORMAT_COMMENT([The names of the tagged configurations supported by this script])dnl +available_tags='_LT_TAGS'dnl +]) + + +# _LT_LIBTOOL_DECLARE(VARNAME, [TAG]) +# ----------------------------------- +# Extract the dictionary values for VARNAME (optionally with TAG) and +# expand to a commented shell variable setting: +# +# # Some comment about what VAR is for. +# visible_name=$lt_internal_name +m4_define([_LT_LIBTOOL_DECLARE], +[_LT_FORMAT_COMMENT(m4_quote(lt_dict_fetch([lt_decl_dict], [$1], + [description])))[]dnl +m4_pushdef([_libtool_name], + m4_quote(lt_dict_fetch([lt_decl_dict], [$1], [libtool_name])))[]dnl +m4_case(m4_quote(lt_dict_fetch([lt_decl_dict], [$1], [value])), + [0], [_libtool_name=[$]$1], + [1], [_libtool_name=$lt_[]$1], + [2], [_libtool_name=$lt_[]$1], + [_libtool_name=lt_dict_fetch([lt_decl_dict], [$1], [value])])[]dnl +m4_ifval([$2], [_$2])[]m4_popdef([_libtool_name])[]dnl +]) + + +# _LT_LIBTOOL_CONFIG_VARS +# ----------------------- +# Produce commented declarations of non-tagged libtool config variables +# suitable for insertion in the LIBTOOL CONFIG section of the 'libtool' +# script. Tagged libtool config variables (even for the LIBTOOL CONFIG +# section) are produced by _LT_LIBTOOL_TAG_VARS. +m4_defun([_LT_LIBTOOL_CONFIG_VARS], +[m4_foreach([_lt_var], + m4_quote(_lt_decl_filter([tagged?], [no], [], lt_decl_varnames)), + [m4_n([_LT_LIBTOOL_DECLARE(_lt_var)])])]) + + +# _LT_LIBTOOL_TAG_VARS(TAG) +# ------------------------- +m4_define([_LT_LIBTOOL_TAG_VARS], +[m4_foreach([_lt_var], m4_quote(lt_decl_tag_varnames), + [m4_n([_LT_LIBTOOL_DECLARE(_lt_var, [$1])])])]) + + +# _LT_TAGVAR(VARNAME, [TAGNAME]) +# ------------------------------ +m4_define([_LT_TAGVAR], [m4_ifval([$2], [$1_$2], [$1])]) + + +# _LT_CONFIG_COMMANDS +# ------------------- +# Send accumulated output to $CONFIG_STATUS. Thanks to the lists of +# variables for single and double quote escaping we saved from calls +# to _LT_DECL, we can put quote escaped variables declarations +# into 'config.status', and then the shell code to quote escape them in +# for loops in 'config.status'. Finally, any additional code accumulated +# from calls to _LT_CONFIG_LIBTOOL_INIT is expanded. +m4_defun([_LT_CONFIG_COMMANDS], +[AC_PROVIDE_IFELSE([LT_OUTPUT], + dnl If the libtool generation code has been placed in $CONFIG_LT, + dnl instead of duplicating it all over again into config.status, + dnl then we will have config.status run $CONFIG_LT later, so it + dnl needs to know what name is stored there: + [AC_CONFIG_COMMANDS([libtool], + [$SHELL $CONFIG_LT || AS_EXIT(1)], [CONFIG_LT='$CONFIG_LT'])], + dnl If the libtool generation code is destined for config.status, + dnl expand the accumulated commands and init code now: + [AC_CONFIG_COMMANDS([libtool], + [_LT_OUTPUT_LIBTOOL_COMMANDS], [_LT_OUTPUT_LIBTOOL_COMMANDS_INIT])]) +])#_LT_CONFIG_COMMANDS + + +# Initialize. +m4_define([_LT_OUTPUT_LIBTOOL_COMMANDS_INIT], +[ + +# The HP-UX ksh and POSIX shell print the target directory to stdout +# if CDPATH is set. +(unset CDPATH) >/dev/null 2>&1 && unset CDPATH + +sed_quote_subst='$sed_quote_subst' +double_quote_subst='$double_quote_subst' +delay_variable_subst='$delay_variable_subst' +_LT_CONFIG_STATUS_DECLARATIONS +LTCC='$LTCC' +LTCFLAGS='$LTCFLAGS' +compiler='$compiler_DEFAULT' + +# A function that is used when there is no print builtin or printf. +func_fallback_echo () +{ + eval 'cat <<_LTECHO_EOF +\$[]1 +_LTECHO_EOF' +} + +# Quote evaled strings. +for var in lt_decl_all_varnames([[ \ +]], lt_decl_quote_varnames); do + case \`eval \\\\\$ECHO \\\\""\\\\\$\$var"\\\\"\` in + *[[\\\\\\\`\\"\\\$]]*) + eval "lt_\$var=\\\\\\"\\\`\\\$ECHO \\"\\\$\$var\\" | \\\$SED \\"\\\$sed_quote_subst\\"\\\`\\\\\\"" ## exclude from sc_prohibit_nested_quotes + ;; + *) + eval "lt_\$var=\\\\\\"\\\$\$var\\\\\\"" + ;; + esac +done + +# Double-quote double-evaled strings. +for var in lt_decl_all_varnames([[ \ +]], lt_decl_dquote_varnames); do + case \`eval \\\\\$ECHO \\\\""\\\\\$\$var"\\\\"\` in + *[[\\\\\\\`\\"\\\$]]*) + eval "lt_\$var=\\\\\\"\\\`\\\$ECHO \\"\\\$\$var\\" | \\\$SED -e \\"\\\$double_quote_subst\\" -e \\"\\\$sed_quote_subst\\" -e \\"\\\$delay_variable_subst\\"\\\`\\\\\\"" ## exclude from sc_prohibit_nested_quotes + ;; + *) + eval "lt_\$var=\\\\\\"\\\$\$var\\\\\\"" + ;; + esac +done + +_LT_OUTPUT_LIBTOOL_INIT +]) + +# _LT_GENERATED_FILE_INIT(FILE, [COMMENT]) +# ------------------------------------ +# Generate a child script FILE with all initialization necessary to +# reuse the environment learned by the parent script, and make the +# file executable. If COMMENT is supplied, it is inserted after the +# '#!' sequence but before initialization text begins. After this +# macro, additional text can be appended to FILE to form the body of +# the child script. The macro ends with non-zero status if the +# file could not be fully written (such as if the disk is full). +m4_ifdef([AS_INIT_GENERATED], +[m4_defun([_LT_GENERATED_FILE_INIT],[AS_INIT_GENERATED($@)])], +[m4_defun([_LT_GENERATED_FILE_INIT], +[m4_require([AS_PREPARE])]dnl +[m4_pushdef([AS_MESSAGE_LOG_FD])]dnl +[lt_write_fail=0 +cat >$1 <<_ASEOF || lt_write_fail=1 +#! $SHELL +# Generated by $as_me. +$2 +SHELL=\${CONFIG_SHELL-$SHELL} +export SHELL +_ASEOF +cat >>$1 <<\_ASEOF || lt_write_fail=1 +AS_SHELL_SANITIZE +_AS_PREPARE +exec AS_MESSAGE_FD>&1 +_ASEOF +test 0 = "$lt_write_fail" && chmod +x $1[]dnl +m4_popdef([AS_MESSAGE_LOG_FD])])])# _LT_GENERATED_FILE_INIT + +# LT_OUTPUT +# --------- +# This macro allows early generation of the libtool script (before +# AC_OUTPUT is called), incase it is used in configure for compilation +# tests. +AC_DEFUN([LT_OUTPUT], +[: ${CONFIG_LT=./config.lt} +AC_MSG_NOTICE([creating $CONFIG_LT]) +_LT_GENERATED_FILE_INIT(["$CONFIG_LT"], +[# Run this file to recreate a libtool stub with the current configuration.]) + +cat >>"$CONFIG_LT" <<\_LTEOF +lt_cl_silent=false +exec AS_MESSAGE_LOG_FD>>config.log +{ + echo + AS_BOX([Running $as_me.]) +} >&AS_MESSAGE_LOG_FD + +lt_cl_help="\ +'$as_me' creates a local libtool stub from the current configuration, +for use in further configure time tests before the real libtool is +generated. + +Usage: $[0] [[OPTIONS]] + + -h, --help print this help, then exit + -V, --version print version number, then exit + -q, --quiet do not print progress messages + -d, --debug don't remove temporary files + +Report bugs to ." + +lt_cl_version="\ +m4_ifset([AC_PACKAGE_NAME], [AC_PACKAGE_NAME ])config.lt[]dnl +m4_ifset([AC_PACKAGE_VERSION], [ AC_PACKAGE_VERSION]) +configured by $[0], generated by m4_PACKAGE_STRING. + +Copyright (C) 2011 Free Software Foundation, Inc. +This config.lt script is free software; the Free Software Foundation +gives unlimited permision to copy, distribute and modify it." + +while test 0 != $[#] +do + case $[1] in + --version | --v* | -V ) + echo "$lt_cl_version"; exit 0 ;; + --help | --h* | -h ) + echo "$lt_cl_help"; exit 0 ;; + --debug | --d* | -d ) + debug=: ;; + --quiet | --q* | --silent | --s* | -q ) + lt_cl_silent=: ;; + + -*) AC_MSG_ERROR([unrecognized option: $[1] +Try '$[0] --help' for more information.]) ;; + + *) AC_MSG_ERROR([unrecognized argument: $[1] +Try '$[0] --help' for more information.]) ;; + esac + shift +done + +if $lt_cl_silent; then + exec AS_MESSAGE_FD>/dev/null +fi +_LTEOF + +cat >>"$CONFIG_LT" <<_LTEOF +_LT_OUTPUT_LIBTOOL_COMMANDS_INIT +_LTEOF + +cat >>"$CONFIG_LT" <<\_LTEOF +AC_MSG_NOTICE([creating $ofile]) +_LT_OUTPUT_LIBTOOL_COMMANDS +AS_EXIT(0) +_LTEOF +chmod +x "$CONFIG_LT" + +# configure is writing to config.log, but config.lt does its own redirection, +# appending to config.log, which fails on DOS, as config.log is still kept +# open by configure. Here we exec the FD to /dev/null, effectively closing +# config.log, so it can be properly (re)opened and appended to by config.lt. +lt_cl_success=: +test yes = "$silent" && + lt_config_lt_args="$lt_config_lt_args --quiet" +exec AS_MESSAGE_LOG_FD>/dev/null +$SHELL "$CONFIG_LT" $lt_config_lt_args || lt_cl_success=false +exec AS_MESSAGE_LOG_FD>>config.log +$lt_cl_success || AS_EXIT(1) +])# LT_OUTPUT + + +# _LT_CONFIG(TAG) +# --------------- +# If TAG is the built-in tag, create an initial libtool script with a +# default configuration from the untagged config vars. Otherwise add code +# to config.status for appending the configuration named by TAG from the +# matching tagged config vars. +m4_defun([_LT_CONFIG], +[m4_require([_LT_FILEUTILS_DEFAULTS])dnl +_LT_CONFIG_SAVE_COMMANDS([ + m4_define([_LT_TAG], m4_if([$1], [], [C], [$1]))dnl + m4_if(_LT_TAG, [C], [ + # See if we are running on zsh, and set the options that allow our + # commands through without removal of \ escapes. + if test -n "${ZSH_VERSION+set}"; then + setopt NO_GLOB_SUBST + fi + + cfgfile=${ofile}T + trap "$RM \"$cfgfile\"; exit 1" 1 2 15 + $RM "$cfgfile" + + cat <<_LT_EOF >> "$cfgfile" +#! $SHELL +# Generated automatically by $as_me ($PACKAGE) $VERSION +# NOTE: Changes made to this file will be lost: look at ltmain.sh. + +# Provide generalized library-building support services. +# Written by Gordon Matzigkeit, 1996 + +_LT_COPYING +_LT_LIBTOOL_TAGS + +# Configured defaults for sys_lib_dlsearch_path munging. +: \${LT_SYS_LIBRARY_PATH="$configure_time_lt_sys_library_path"} + +# ### BEGIN LIBTOOL CONFIG +_LT_LIBTOOL_CONFIG_VARS +_LT_LIBTOOL_TAG_VARS +# ### END LIBTOOL CONFIG + +_LT_EOF + + cat <<'_LT_EOF' >> "$cfgfile" + +# ### BEGIN FUNCTIONS SHARED WITH CONFIGURE + +_LT_PREPARE_MUNGE_PATH_LIST +_LT_PREPARE_CC_BASENAME + +# ### END FUNCTIONS SHARED WITH CONFIGURE + +_LT_EOF + + case $host_os in + aix3*) + cat <<\_LT_EOF >> "$cfgfile" +# AIX sometimes has problems with the GCC collect2 program. For some +# reason, if we set the COLLECT_NAMES environment variable, the problems +# vanish in a puff of smoke. +if test set != "${COLLECT_NAMES+set}"; then + COLLECT_NAMES= + export COLLECT_NAMES +fi +_LT_EOF + ;; + esac + + _LT_PROG_LTMAIN + + # We use sed instead of cat because bash on DJGPP gets confused if + # if finds mixed CR/LF and LF-only lines. Since sed operates in + # text mode, it properly converts lines to CR/LF. This bash problem + # is reportedly fixed, but why not run on old versions too? + sed '$q' "$ltmain" >> "$cfgfile" \ + || (rm -f "$cfgfile"; exit 1) + + mv -f "$cfgfile" "$ofile" || + (rm -f "$ofile" && cp "$cfgfile" "$ofile" && rm -f "$cfgfile") + chmod +x "$ofile" +], +[cat <<_LT_EOF >> "$ofile" + +dnl Unfortunately we have to use $1 here, since _LT_TAG is not expanded +dnl in a comment (ie after a #). +# ### BEGIN LIBTOOL TAG CONFIG: $1 +_LT_LIBTOOL_TAG_VARS(_LT_TAG) +# ### END LIBTOOL TAG CONFIG: $1 +_LT_EOF +])dnl /m4_if +], +[m4_if([$1], [], [ + PACKAGE='$PACKAGE' + VERSION='$VERSION' + RM='$RM' + ofile='$ofile'], []) +])dnl /_LT_CONFIG_SAVE_COMMANDS +])# _LT_CONFIG + + +# LT_SUPPORTED_TAG(TAG) +# --------------------- +# Trace this macro to discover what tags are supported by the libtool +# --tag option, using: +# autoconf --trace 'LT_SUPPORTED_TAG:$1' +AC_DEFUN([LT_SUPPORTED_TAG], []) + + +# C support is built-in for now +m4_define([_LT_LANG_C_enabled], []) +m4_define([_LT_TAGS], []) + + +# LT_LANG(LANG) +# ------------- +# Enable libtool support for the given language if not already enabled. +AC_DEFUN([LT_LANG], +[AC_BEFORE([$0], [LT_OUTPUT])dnl +m4_case([$1], + [C], [_LT_LANG(C)], + [C++], [_LT_LANG(CXX)], + [Go], [_LT_LANG(GO)], + [Java], [_LT_LANG(GCJ)], + [Fortran 77], [_LT_LANG(F77)], + [Fortran], [_LT_LANG(FC)], + [Windows Resource], [_LT_LANG(RC)], + [m4_ifdef([_LT_LANG_]$1[_CONFIG], + [_LT_LANG($1)], + [m4_fatal([$0: unsupported language: "$1"])])])dnl +])# LT_LANG + + +# _LT_LANG(LANGNAME) +# ------------------ +m4_defun([_LT_LANG], +[m4_ifdef([_LT_LANG_]$1[_enabled], [], + [LT_SUPPORTED_TAG([$1])dnl + m4_append([_LT_TAGS], [$1 ])dnl + m4_define([_LT_LANG_]$1[_enabled], [])dnl + _LT_LANG_$1_CONFIG($1)])dnl +])# _LT_LANG + + +m4_ifndef([AC_PROG_GO], [ +# NOTE: This macro has been submitted for inclusion into # +# GNU Autoconf as AC_PROG_GO. When it is available in # +# a released version of Autoconf we should remove this # +# macro and use it instead. # +m4_defun([AC_PROG_GO], +[AC_LANG_PUSH(Go)dnl +AC_ARG_VAR([GOC], [Go compiler command])dnl +AC_ARG_VAR([GOFLAGS], [Go compiler flags])dnl +_AC_ARG_VAR_LDFLAGS()dnl +AC_CHECK_TOOL(GOC, gccgo) +if test -z "$GOC"; then + if test -n "$ac_tool_prefix"; then + AC_CHECK_PROG(GOC, [${ac_tool_prefix}gccgo], [${ac_tool_prefix}gccgo]) + fi +fi +if test -z "$GOC"; then + AC_CHECK_PROG(GOC, gccgo, gccgo, false) +fi +])#m4_defun +])#m4_ifndef + + +# _LT_LANG_DEFAULT_CONFIG +# ----------------------- +m4_defun([_LT_LANG_DEFAULT_CONFIG], +[AC_PROVIDE_IFELSE([AC_PROG_CXX], + [LT_LANG(CXX)], + [m4_define([AC_PROG_CXX], defn([AC_PROG_CXX])[LT_LANG(CXX)])]) + +AC_PROVIDE_IFELSE([AC_PROG_F77], + [LT_LANG(F77)], + [m4_define([AC_PROG_F77], defn([AC_PROG_F77])[LT_LANG(F77)])]) + +AC_PROVIDE_IFELSE([AC_PROG_FC], + [LT_LANG(FC)], + [m4_define([AC_PROG_FC], defn([AC_PROG_FC])[LT_LANG(FC)])]) + +dnl The call to [A][M_PROG_GCJ] is quoted like that to stop aclocal +dnl pulling things in needlessly. +AC_PROVIDE_IFELSE([AC_PROG_GCJ], + [LT_LANG(GCJ)], + [AC_PROVIDE_IFELSE([A][M_PROG_GCJ], + [LT_LANG(GCJ)], + [AC_PROVIDE_IFELSE([LT_PROG_GCJ], + [LT_LANG(GCJ)], + [m4_ifdef([AC_PROG_GCJ], + [m4_define([AC_PROG_GCJ], defn([AC_PROG_GCJ])[LT_LANG(GCJ)])]) + m4_ifdef([A][M_PROG_GCJ], + [m4_define([A][M_PROG_GCJ], defn([A][M_PROG_GCJ])[LT_LANG(GCJ)])]) + m4_ifdef([LT_PROG_GCJ], + [m4_define([LT_PROG_GCJ], defn([LT_PROG_GCJ])[LT_LANG(GCJ)])])])])]) + +AC_PROVIDE_IFELSE([AC_PROG_GO], + [LT_LANG(GO)], + [m4_define([AC_PROG_GO], defn([AC_PROG_GO])[LT_LANG(GO)])]) + +AC_PROVIDE_IFELSE([LT_PROG_RC], + [LT_LANG(RC)], + [m4_define([LT_PROG_RC], defn([LT_PROG_RC])[LT_LANG(RC)])]) +])# _LT_LANG_DEFAULT_CONFIG + +# Obsolete macros: +AU_DEFUN([AC_LIBTOOL_CXX], [LT_LANG(C++)]) +AU_DEFUN([AC_LIBTOOL_F77], [LT_LANG(Fortran 77)]) +AU_DEFUN([AC_LIBTOOL_FC], [LT_LANG(Fortran)]) +AU_DEFUN([AC_LIBTOOL_GCJ], [LT_LANG(Java)]) +AU_DEFUN([AC_LIBTOOL_RC], [LT_LANG(Windows Resource)]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_LIBTOOL_CXX], []) +dnl AC_DEFUN([AC_LIBTOOL_F77], []) +dnl AC_DEFUN([AC_LIBTOOL_FC], []) +dnl AC_DEFUN([AC_LIBTOOL_GCJ], []) +dnl AC_DEFUN([AC_LIBTOOL_RC], []) + + +# _LT_TAG_COMPILER +# ---------------- +m4_defun([_LT_TAG_COMPILER], +[AC_REQUIRE([AC_PROG_CC])dnl + +_LT_DECL([LTCC], [CC], [1], [A C compiler])dnl +_LT_DECL([LTCFLAGS], [CFLAGS], [1], [LTCC compiler flags])dnl +_LT_TAGDECL([CC], [compiler], [1], [A language specific compiler])dnl +_LT_TAGDECL([with_gcc], [GCC], [0], [Is the compiler the GNU compiler?])dnl + +# If no C compiler was specified, use CC. +LTCC=${LTCC-"$CC"} + +# If no C compiler flags were specified, use CFLAGS. +LTCFLAGS=${LTCFLAGS-"$CFLAGS"} + +# Allow CC to be a program name with arguments. +compiler=$CC +])# _LT_TAG_COMPILER + + +# _LT_COMPILER_BOILERPLATE +# ------------------------ +# Check for compiler boilerplate output or warnings with +# the simple compiler test code. +m4_defun([_LT_COMPILER_BOILERPLATE], +[m4_require([_LT_DECL_SED])dnl +ac_outfile=conftest.$ac_objext +echo "$lt_simple_compile_test_code" >conftest.$ac_ext +eval "$ac_compile" 2>&1 >/dev/null | $SED '/^$/d; /^ *+/d' >conftest.err +_lt_compiler_boilerplate=`cat conftest.err` +$RM conftest* +])# _LT_COMPILER_BOILERPLATE + + +# _LT_LINKER_BOILERPLATE +# ---------------------- +# Check for linker boilerplate output or warnings with +# the simple link test code. +m4_defun([_LT_LINKER_BOILERPLATE], +[m4_require([_LT_DECL_SED])dnl +ac_outfile=conftest.$ac_objext +echo "$lt_simple_link_test_code" >conftest.$ac_ext +eval "$ac_link" 2>&1 >/dev/null | $SED '/^$/d; /^ *+/d' >conftest.err +_lt_linker_boilerplate=`cat conftest.err` +$RM -r conftest* +])# _LT_LINKER_BOILERPLATE + +# _LT_REQUIRED_DARWIN_CHECKS +# ------------------------- +m4_defun_once([_LT_REQUIRED_DARWIN_CHECKS],[ + case $host_os in + rhapsody* | darwin*) + AC_CHECK_TOOL([DSYMUTIL], [dsymutil], [:]) + AC_CHECK_TOOL([NMEDIT], [nmedit], [:]) + AC_CHECK_TOOL([LIPO], [lipo], [:]) + AC_CHECK_TOOL([OTOOL], [otool], [:]) + AC_CHECK_TOOL([OTOOL64], [otool64], [:]) + _LT_DECL([], [DSYMUTIL], [1], + [Tool to manipulate archived DWARF debug symbol files on Mac OS X]) + _LT_DECL([], [NMEDIT], [1], + [Tool to change global to local symbols on Mac OS X]) + _LT_DECL([], [LIPO], [1], + [Tool to manipulate fat objects and archives on Mac OS X]) + _LT_DECL([], [OTOOL], [1], + [ldd/readelf like tool for Mach-O binaries on Mac OS X]) + _LT_DECL([], [OTOOL64], [1], + [ldd/readelf like tool for 64 bit Mach-O binaries on Mac OS X 10.4]) + + AC_CACHE_CHECK([for -single_module linker flag],[lt_cv_apple_cc_single_mod], + [lt_cv_apple_cc_single_mod=no + if test -z "$LT_MULTI_MODULE"; then + # By default we will add the -single_module flag. You can override + # by either setting the environment variable LT_MULTI_MODULE + # non-empty at configure time, or by adding -multi_module to the + # link flags. + rm -rf libconftest.dylib* + echo "int foo(void){return 1;}" > conftest.c + echo "$LTCC $LTCFLAGS $LDFLAGS -o libconftest.dylib \ +-dynamiclib -Wl,-single_module conftest.c" >&AS_MESSAGE_LOG_FD + $LTCC $LTCFLAGS $LDFLAGS -o libconftest.dylib \ + -dynamiclib -Wl,-single_module conftest.c 2>conftest.err + _lt_result=$? + # If there is a non-empty error log, and "single_module" + # appears in it, assume the flag caused a linker warning + if test -s conftest.err && $GREP single_module conftest.err; then + cat conftest.err >&AS_MESSAGE_LOG_FD + # Otherwise, if the output was created with a 0 exit code from + # the compiler, it worked. + elif test -f libconftest.dylib && test 0 = "$_lt_result"; then + lt_cv_apple_cc_single_mod=yes + else + cat conftest.err >&AS_MESSAGE_LOG_FD + fi + rm -rf libconftest.dylib* + rm -f conftest.* + fi]) + + AC_CACHE_CHECK([for -exported_symbols_list linker flag], + [lt_cv_ld_exported_symbols_list], + [lt_cv_ld_exported_symbols_list=no + save_LDFLAGS=$LDFLAGS + echo "_main" > conftest.sym + LDFLAGS="$LDFLAGS -Wl,-exported_symbols_list,conftest.sym" + AC_LINK_IFELSE([AC_LANG_PROGRAM([],[])], + [lt_cv_ld_exported_symbols_list=yes], + [lt_cv_ld_exported_symbols_list=no]) + LDFLAGS=$save_LDFLAGS + ]) + + AC_CACHE_CHECK([for -force_load linker flag],[lt_cv_ld_force_load], + [lt_cv_ld_force_load=no + cat > conftest.c << _LT_EOF +int forced_loaded() { return 2;} +_LT_EOF + echo "$LTCC $LTCFLAGS -c -o conftest.o conftest.c" >&AS_MESSAGE_LOG_FD + $LTCC $LTCFLAGS -c -o conftest.o conftest.c 2>&AS_MESSAGE_LOG_FD + echo "$AR cru libconftest.a conftest.o" >&AS_MESSAGE_LOG_FD + $AR cru libconftest.a conftest.o 2>&AS_MESSAGE_LOG_FD + echo "$RANLIB libconftest.a" >&AS_MESSAGE_LOG_FD + $RANLIB libconftest.a 2>&AS_MESSAGE_LOG_FD + cat > conftest.c << _LT_EOF +int main() { return 0;} +_LT_EOF + echo "$LTCC $LTCFLAGS $LDFLAGS -o conftest conftest.c -Wl,-force_load,./libconftest.a" >&AS_MESSAGE_LOG_FD + $LTCC $LTCFLAGS $LDFLAGS -o conftest conftest.c -Wl,-force_load,./libconftest.a 2>conftest.err + _lt_result=$? + if test -s conftest.err && $GREP force_load conftest.err; then + cat conftest.err >&AS_MESSAGE_LOG_FD + elif test -f conftest && test 0 = "$_lt_result" && $GREP forced_load conftest >/dev/null 2>&1; then + lt_cv_ld_force_load=yes + else + cat conftest.err >&AS_MESSAGE_LOG_FD + fi + rm -f conftest.err libconftest.a conftest conftest.c + rm -rf conftest.dSYM + ]) + case $host_os in + rhapsody* | darwin1.[[012]]) + _lt_dar_allow_undefined='$wl-undefined ${wl}suppress' ;; + darwin1.*) + _lt_dar_allow_undefined='$wl-flat_namespace $wl-undefined ${wl}suppress' ;; + darwin*) # darwin 5.x on + # if running on 10.5 or later, the deployment target defaults + # to the OS version, if on x86, and 10.4, the deployment + # target defaults to 10.4. Don't you love it? + case ${MACOSX_DEPLOYMENT_TARGET-10.0},$host in + 10.0,*86*-darwin8*|10.0,*-darwin[[91]]*) + _lt_dar_allow_undefined='$wl-undefined ${wl}dynamic_lookup' ;; + 10.[[012]][[,.]]*) + _lt_dar_allow_undefined='$wl-flat_namespace $wl-undefined ${wl}suppress' ;; + 10.*) + _lt_dar_allow_undefined='$wl-undefined ${wl}dynamic_lookup' ;; + esac + ;; + esac + if test yes = "$lt_cv_apple_cc_single_mod"; then + _lt_dar_single_mod='$single_module' + fi + if test yes = "$lt_cv_ld_exported_symbols_list"; then + _lt_dar_export_syms=' $wl-exported_symbols_list,$output_objdir/$libname-symbols.expsym' + else + _lt_dar_export_syms='~$NMEDIT -s $output_objdir/$libname-symbols.expsym $lib' + fi + if test : != "$DSYMUTIL" && test no = "$lt_cv_ld_force_load"; then + _lt_dsymutil='~$DSYMUTIL $lib || :' + else + _lt_dsymutil= + fi + ;; + esac +]) + + +# _LT_DARWIN_LINKER_FEATURES([TAG]) +# --------------------------------- +# Checks for linker and compiler features on darwin +m4_defun([_LT_DARWIN_LINKER_FEATURES], +[ + m4_require([_LT_REQUIRED_DARWIN_CHECKS]) + _LT_TAGVAR(archive_cmds_need_lc, $1)=no + _LT_TAGVAR(hardcode_direct, $1)=no + _LT_TAGVAR(hardcode_automatic, $1)=yes + _LT_TAGVAR(hardcode_shlibpath_var, $1)=unsupported + if test yes = "$lt_cv_ld_force_load"; then + _LT_TAGVAR(whole_archive_flag_spec, $1)='`for conv in $convenience\"\"; do test -n \"$conv\" && new_convenience=\"$new_convenience $wl-force_load,$conv\"; done; func_echo_all \"$new_convenience\"`' + m4_case([$1], [F77], [_LT_TAGVAR(compiler_needs_object, $1)=yes], + [FC], [_LT_TAGVAR(compiler_needs_object, $1)=yes]) + else + _LT_TAGVAR(whole_archive_flag_spec, $1)='' + fi + _LT_TAGVAR(link_all_deplibs, $1)=yes + _LT_TAGVAR(allow_undefined_flag, $1)=$_lt_dar_allow_undefined + case $cc_basename in + ifort*|nagfor*) _lt_dar_can_shared=yes ;; + *) _lt_dar_can_shared=$GCC ;; + esac + if test yes = "$_lt_dar_can_shared"; then + output_verbose_link_cmd=func_echo_all + _LT_TAGVAR(archive_cmds, $1)="\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$libobjs \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring $_lt_dar_single_mod$_lt_dsymutil" + _LT_TAGVAR(module_cmds, $1)="\$CC \$allow_undefined_flag -o \$lib -bundle \$libobjs \$deplibs \$compiler_flags$_lt_dsymutil" + _LT_TAGVAR(archive_expsym_cmds, $1)="sed 's|^|_|' < \$export_symbols > \$output_objdir/\$libname-symbols.expsym~\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$libobjs \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring $_lt_dar_single_mod$_lt_dar_export_syms$_lt_dsymutil" + _LT_TAGVAR(module_expsym_cmds, $1)="sed -e 's|^|_|' < \$export_symbols > \$output_objdir/\$libname-symbols.expsym~\$CC \$allow_undefined_flag -o \$lib -bundle \$libobjs \$deplibs \$compiler_flags$_lt_dar_export_syms$_lt_dsymutil" + m4_if([$1], [CXX], +[ if test yes != "$lt_cv_apple_cc_single_mod"; then + _LT_TAGVAR(archive_cmds, $1)="\$CC -r -keep_private_externs -nostdlib -o \$lib-master.o \$libobjs~\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$lib-master.o \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring$_lt_dsymutil" + _LT_TAGVAR(archive_expsym_cmds, $1)="sed 's|^|_|' < \$export_symbols > \$output_objdir/\$libname-symbols.expsym~\$CC -r -keep_private_externs -nostdlib -o \$lib-master.o \$libobjs~\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$lib-master.o \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring$_lt_dar_export_syms$_lt_dsymutil" + fi +],[]) + else + _LT_TAGVAR(ld_shlibs, $1)=no + fi +]) + +# _LT_SYS_MODULE_PATH_AIX([TAGNAME]) +# ---------------------------------- +# Links a minimal program and checks the executable +# for the system default hardcoded library path. In most cases, +# this is /usr/lib:/lib, but when the MPI compilers are used +# the location of the communication and MPI libs are included too. +# If we don't find anything, use the default library path according +# to the aix ld manual. +# Store the results from the different compilers for each TAGNAME. +# Allow to override them for all tags through lt_cv_aix_libpath. +m4_defun([_LT_SYS_MODULE_PATH_AIX], +[m4_require([_LT_DECL_SED])dnl +if test set = "${lt_cv_aix_libpath+set}"; then + aix_libpath=$lt_cv_aix_libpath +else + AC_CACHE_VAL([_LT_TAGVAR([lt_cv_aix_libpath_], [$1])], + [AC_LINK_IFELSE([AC_LANG_PROGRAM],[ + lt_aix_libpath_sed='[ + /Import File Strings/,/^$/ { + /^0/ { + s/^0 *\([^ ]*\) *$/\1/ + p + } + }]' + _LT_TAGVAR([lt_cv_aix_libpath_], [$1])=`dump -H conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"` + # Check for a 64-bit object if we didn't find anything. + if test -z "$_LT_TAGVAR([lt_cv_aix_libpath_], [$1])"; then + _LT_TAGVAR([lt_cv_aix_libpath_], [$1])=`dump -HX64 conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"` + fi],[]) + if test -z "$_LT_TAGVAR([lt_cv_aix_libpath_], [$1])"; then + _LT_TAGVAR([lt_cv_aix_libpath_], [$1])=/usr/lib:/lib + fi + ]) + aix_libpath=$_LT_TAGVAR([lt_cv_aix_libpath_], [$1]) +fi +])# _LT_SYS_MODULE_PATH_AIX + + +# _LT_SHELL_INIT(ARG) +# ------------------- +m4_define([_LT_SHELL_INIT], +[m4_divert_text([M4SH-INIT], [$1 +])])# _LT_SHELL_INIT + + + +# _LT_PROG_ECHO_BACKSLASH +# ----------------------- +# Find how we can fake an echo command that does not interpret backslash. +# In particular, with Autoconf 2.60 or later we add some code to the start +# of the generated configure script that will find a shell with a builtin +# printf (that we can use as an echo command). +m4_defun([_LT_PROG_ECHO_BACKSLASH], +[ECHO='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' +ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO +ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO$ECHO + +AC_MSG_CHECKING([how to print strings]) +# Test print first, because it will be a builtin if present. +if test "X`( print -r -- -n ) 2>/dev/null`" = X-n && \ + test "X`print -r -- $ECHO 2>/dev/null`" = "X$ECHO"; then + ECHO='print -r --' +elif test "X`printf %s $ECHO 2>/dev/null`" = "X$ECHO"; then + ECHO='printf %s\n' +else + # Use this function as a fallback that always works. + func_fallback_echo () + { + eval 'cat <<_LTECHO_EOF +$[]1 +_LTECHO_EOF' + } + ECHO='func_fallback_echo' +fi + +# func_echo_all arg... +# Invoke $ECHO with all args, space-separated. +func_echo_all () +{ + $ECHO "$*" +} + +case $ECHO in + printf*) AC_MSG_RESULT([printf]) ;; + print*) AC_MSG_RESULT([print -r]) ;; + *) AC_MSG_RESULT([cat]) ;; +esac + +m4_ifdef([_AS_DETECT_SUGGESTED], +[_AS_DETECT_SUGGESTED([ + test -n "${ZSH_VERSION+set}${BASH_VERSION+set}" || ( + ECHO='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' + ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO + ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO$ECHO + PATH=/empty FPATH=/empty; export PATH FPATH + test "X`printf %s $ECHO`" = "X$ECHO" \ + || test "X`print -r -- $ECHO`" = "X$ECHO" )])]) + +_LT_DECL([], [SHELL], [1], [Shell to use when invoking shell scripts]) +_LT_DECL([], [ECHO], [1], [An echo program that protects backslashes]) +])# _LT_PROG_ECHO_BACKSLASH + + +# _LT_WITH_SYSROOT +# ---------------- +AC_DEFUN([_LT_WITH_SYSROOT], +[AC_MSG_CHECKING([for sysroot]) +AC_ARG_WITH([sysroot], +[AS_HELP_STRING([--with-sysroot@<:@=DIR@:>@], + [Search for dependent libraries within DIR (or the compiler's sysroot + if not specified).])], +[], [with_sysroot=no]) + +dnl lt_sysroot will always be passed unquoted. We quote it here +dnl in case the user passed a directory name. +lt_sysroot= +case $with_sysroot in #( + yes) + if test yes = "$GCC"; then + lt_sysroot=`$CC --print-sysroot 2>/dev/null` + fi + ;; #( + /*) + lt_sysroot=`echo "$with_sysroot" | sed -e "$sed_quote_subst"` + ;; #( + no|'') + ;; #( + *) + AC_MSG_RESULT([$with_sysroot]) + AC_MSG_ERROR([The sysroot must be an absolute path.]) + ;; +esac + + AC_MSG_RESULT([${lt_sysroot:-no}]) +_LT_DECL([], [lt_sysroot], [0], [The root where to search for ]dnl +[dependent libraries, and where our libraries should be installed.])]) + +# _LT_ENABLE_LOCK +# --------------- +m4_defun([_LT_ENABLE_LOCK], +[AC_ARG_ENABLE([libtool-lock], + [AS_HELP_STRING([--disable-libtool-lock], + [avoid locking (might break parallel builds)])]) +test no = "$enable_libtool_lock" || enable_libtool_lock=yes + +# Some flags need to be propagated to the compiler or linker for good +# libtool support. +case $host in +ia64-*-hpux*) + # Find out what ABI is being produced by ac_compile, and set mode + # options accordingly. + echo 'int i;' > conftest.$ac_ext + if AC_TRY_EVAL(ac_compile); then + case `/usr/bin/file conftest.$ac_objext` in + *ELF-32*) + HPUX_IA64_MODE=32 + ;; + *ELF-64*) + HPUX_IA64_MODE=64 + ;; + esac + fi + rm -rf conftest* + ;; +*-*-irix6*) + # Find out what ABI is being produced by ac_compile, and set linker + # options accordingly. + echo '[#]line '$LINENO' "configure"' > conftest.$ac_ext + if AC_TRY_EVAL(ac_compile); then + if test yes = "$lt_cv_prog_gnu_ld"; then + case `/usr/bin/file conftest.$ac_objext` in + *32-bit*) + LD="${LD-ld} -melf32bsmip" + ;; + *N32*) + LD="${LD-ld} -melf32bmipn32" + ;; + *64-bit*) + LD="${LD-ld} -melf64bmip" + ;; + esac + else + case `/usr/bin/file conftest.$ac_objext` in + *32-bit*) + LD="${LD-ld} -32" + ;; + *N32*) + LD="${LD-ld} -n32" + ;; + *64-bit*) + LD="${LD-ld} -64" + ;; + esac + fi + fi + rm -rf conftest* + ;; + +mips64*-*linux*) + # Find out what ABI is being produced by ac_compile, and set linker + # options accordingly. + echo '[#]line '$LINENO' "configure"' > conftest.$ac_ext + if AC_TRY_EVAL(ac_compile); then + emul=elf + case `/usr/bin/file conftest.$ac_objext` in + *32-bit*) + emul="${emul}32" + ;; + *64-bit*) + emul="${emul}64" + ;; + esac + case `/usr/bin/file conftest.$ac_objext` in + *MSB*) + emul="${emul}btsmip" + ;; + *LSB*) + emul="${emul}ltsmip" + ;; + esac + case `/usr/bin/file conftest.$ac_objext` in + *N32*) + emul="${emul}n32" + ;; + esac + LD="${LD-ld} -m $emul" + fi + rm -rf conftest* + ;; + +x86_64-*kfreebsd*-gnu|x86_64-*linux*|powerpc*-*linux*| \ +s390*-*linux*|s390*-*tpf*|sparc*-*linux*) + # Find out what ABI is being produced by ac_compile, and set linker + # options accordingly. Note that the listed cases only cover the + # situations where additional linker options are needed (such as when + # doing 32-bit compilation for a host where ld defaults to 64-bit, or + # vice versa); the common cases where no linker options are needed do + # not appear in the list. + echo 'int i;' > conftest.$ac_ext + if AC_TRY_EVAL(ac_compile); then + case `/usr/bin/file conftest.o` in + *32-bit*) + case $host in + x86_64-*kfreebsd*-gnu) + LD="${LD-ld} -m elf_i386_fbsd" + ;; + x86_64-*linux*) + case `/usr/bin/file conftest.o` in + *x86-64*) + LD="${LD-ld} -m elf32_x86_64" + ;; + *) + LD="${LD-ld} -m elf_i386" + ;; + esac + ;; + powerpc64le-*linux*) + LD="${LD-ld} -m elf32lppclinux" + ;; + powerpc64-*linux*) + LD="${LD-ld} -m elf32ppclinux" + ;; + s390x-*linux*) + LD="${LD-ld} -m elf_s390" + ;; + sparc64-*linux*) + LD="${LD-ld} -m elf32_sparc" + ;; + esac + ;; + *64-bit*) + case $host in + x86_64-*kfreebsd*-gnu) + LD="${LD-ld} -m elf_x86_64_fbsd" + ;; + x86_64-*linux*) + LD="${LD-ld} -m elf_x86_64" + ;; + powerpcle-*linux*) + LD="${LD-ld} -m elf64lppc" + ;; + powerpc-*linux*) + LD="${LD-ld} -m elf64ppc" + ;; + s390*-*linux*|s390*-*tpf*) + LD="${LD-ld} -m elf64_s390" + ;; + sparc*-*linux*) + LD="${LD-ld} -m elf64_sparc" + ;; + esac + ;; + esac + fi + rm -rf conftest* + ;; + +*-*-sco3.2v5*) + # On SCO OpenServer 5, we need -belf to get full-featured binaries. + SAVE_CFLAGS=$CFLAGS + CFLAGS="$CFLAGS -belf" + AC_CACHE_CHECK([whether the C compiler needs -belf], lt_cv_cc_needs_belf, + [AC_LANG_PUSH(C) + AC_LINK_IFELSE([AC_LANG_PROGRAM([[]],[[]])],[lt_cv_cc_needs_belf=yes],[lt_cv_cc_needs_belf=no]) + AC_LANG_POP]) + if test yes != "$lt_cv_cc_needs_belf"; then + # this is probably gcc 2.8.0, egcs 1.0 or newer; no need for -belf + CFLAGS=$SAVE_CFLAGS + fi + ;; +*-*solaris*) + # Find out what ABI is being produced by ac_compile, and set linker + # options accordingly. + echo 'int i;' > conftest.$ac_ext + if AC_TRY_EVAL(ac_compile); then + case `/usr/bin/file conftest.o` in + *64-bit*) + case $lt_cv_prog_gnu_ld in + yes*) + case $host in + i?86-*-solaris*|x86_64-*-solaris*) + LD="${LD-ld} -m elf_x86_64" + ;; + sparc*-*-solaris*) + LD="${LD-ld} -m elf64_sparc" + ;; + esac + # GNU ld 2.21 introduced _sol2 emulations. Use them if available. + if ${LD-ld} -V | grep _sol2 >/dev/null 2>&1; then + LD=${LD-ld}_sol2 + fi + ;; + *) + if ${LD-ld} -64 -r -o conftest2.o conftest.o >/dev/null 2>&1; then + LD="${LD-ld} -64" + fi + ;; + esac + ;; + esac + fi + rm -rf conftest* + ;; +esac + +need_locks=$enable_libtool_lock +])# _LT_ENABLE_LOCK + + +# _LT_PROG_AR +# ----------- +m4_defun([_LT_PROG_AR], +[AC_CHECK_TOOLS(AR, [ar], false) +: ${AR=ar} +: ${AR_FLAGS=cru} +_LT_DECL([], [AR], [1], [The archiver]) +_LT_DECL([], [AR_FLAGS], [1], [Flags to create an archive]) + +AC_CACHE_CHECK([for archiver @FILE support], [lt_cv_ar_at_file], + [lt_cv_ar_at_file=no + AC_COMPILE_IFELSE([AC_LANG_PROGRAM], + [echo conftest.$ac_objext > conftest.lst + lt_ar_try='$AR $AR_FLAGS libconftest.a @conftest.lst >&AS_MESSAGE_LOG_FD' + AC_TRY_EVAL([lt_ar_try]) + if test 0 -eq "$ac_status"; then + # Ensure the archiver fails upon bogus file names. + rm -f conftest.$ac_objext libconftest.a + AC_TRY_EVAL([lt_ar_try]) + if test 0 -ne "$ac_status"; then + lt_cv_ar_at_file=@ + fi + fi + rm -f conftest.* libconftest.a + ]) + ]) + +if test no = "$lt_cv_ar_at_file"; then + archiver_list_spec= +else + archiver_list_spec=$lt_cv_ar_at_file +fi +_LT_DECL([], [archiver_list_spec], [1], + [How to feed a file listing to the archiver]) +])# _LT_PROG_AR + + +# _LT_CMD_OLD_ARCHIVE +# ------------------- +m4_defun([_LT_CMD_OLD_ARCHIVE], +[_LT_PROG_AR + +AC_CHECK_TOOL(STRIP, strip, :) +test -z "$STRIP" && STRIP=: +_LT_DECL([], [STRIP], [1], [A symbol stripping program]) + +AC_CHECK_TOOL(RANLIB, ranlib, :) +test -z "$RANLIB" && RANLIB=: +_LT_DECL([], [RANLIB], [1], + [Commands used to install an old-style archive]) + +# Determine commands to create old-style static archives. +old_archive_cmds='$AR $AR_FLAGS $oldlib$oldobjs' +old_postinstall_cmds='chmod 644 $oldlib' +old_postuninstall_cmds= + +if test -n "$RANLIB"; then + case $host_os in + bitrig* | openbsd*) + old_postinstall_cmds="$old_postinstall_cmds~\$RANLIB -t \$tool_oldlib" + ;; + *) + old_postinstall_cmds="$old_postinstall_cmds~\$RANLIB \$tool_oldlib" + ;; + esac + old_archive_cmds="$old_archive_cmds~\$RANLIB \$tool_oldlib" +fi + +case $host_os in + darwin*) + lock_old_archive_extraction=yes ;; + *) + lock_old_archive_extraction=no ;; +esac +_LT_DECL([], [old_postinstall_cmds], [2]) +_LT_DECL([], [old_postuninstall_cmds], [2]) +_LT_TAGDECL([], [old_archive_cmds], [2], + [Commands used to build an old-style archive]) +_LT_DECL([], [lock_old_archive_extraction], [0], + [Whether to use a lock for old archive extraction]) +])# _LT_CMD_OLD_ARCHIVE + + +# _LT_COMPILER_OPTION(MESSAGE, VARIABLE-NAME, FLAGS, +# [OUTPUT-FILE], [ACTION-SUCCESS], [ACTION-FAILURE]) +# ---------------------------------------------------------------- +# Check whether the given compiler option works +AC_DEFUN([_LT_COMPILER_OPTION], +[m4_require([_LT_FILEUTILS_DEFAULTS])dnl +m4_require([_LT_DECL_SED])dnl +AC_CACHE_CHECK([$1], [$2], + [$2=no + m4_if([$4], , [ac_outfile=conftest.$ac_objext], [ac_outfile=$4]) + echo "$lt_simple_compile_test_code" > conftest.$ac_ext + lt_compiler_flag="$3" ## exclude from sc_useless_quotes_in_assignment + # Insert the option either (1) after the last *FLAGS variable, or + # (2) before a word containing "conftest.", or (3) at the end. + # Note that $ac_compile itself does not contain backslashes and begins + # with a dollar sign (not a hyphen), so the echo should work correctly. + # The option is referenced via a variable to avoid confusing sed. + lt_compile=`echo "$ac_compile" | $SED \ + -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ + -e 's: [[^ ]]*conftest\.: $lt_compiler_flag&:; t' \ + -e 's:$: $lt_compiler_flag:'` + (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&AS_MESSAGE_LOG_FD) + (eval "$lt_compile" 2>conftest.err) + ac_status=$? + cat conftest.err >&AS_MESSAGE_LOG_FD + echo "$as_me:$LINENO: \$? = $ac_status" >&AS_MESSAGE_LOG_FD + if (exit $ac_status) && test -s "$ac_outfile"; then + # The compiler can only warn and ignore the option if not recognized + # So say no if there are warnings other than the usual output. + $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' >conftest.exp + $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2 + if test ! -s conftest.er2 || diff conftest.exp conftest.er2 >/dev/null; then + $2=yes + fi + fi + $RM conftest* +]) + +if test yes = "[$]$2"; then + m4_if([$5], , :, [$5]) +else + m4_if([$6], , :, [$6]) +fi +])# _LT_COMPILER_OPTION + +# Old name: +AU_ALIAS([AC_LIBTOOL_COMPILER_OPTION], [_LT_COMPILER_OPTION]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_LIBTOOL_COMPILER_OPTION], []) + + +# _LT_LINKER_OPTION(MESSAGE, VARIABLE-NAME, FLAGS, +# [ACTION-SUCCESS], [ACTION-FAILURE]) +# ---------------------------------------------------- +# Check whether the given linker option works +AC_DEFUN([_LT_LINKER_OPTION], +[m4_require([_LT_FILEUTILS_DEFAULTS])dnl +m4_require([_LT_DECL_SED])dnl +AC_CACHE_CHECK([$1], [$2], + [$2=no + save_LDFLAGS=$LDFLAGS + LDFLAGS="$LDFLAGS $3" + echo "$lt_simple_link_test_code" > conftest.$ac_ext + if (eval $ac_link 2>conftest.err) && test -s conftest$ac_exeext; then + # The linker can only warn and ignore the option if not recognized + # So say no if there are warnings + if test -s conftest.err; then + # Append any errors to the config.log. + cat conftest.err 1>&AS_MESSAGE_LOG_FD + $ECHO "$_lt_linker_boilerplate" | $SED '/^$/d' > conftest.exp + $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2 + if diff conftest.exp conftest.er2 >/dev/null; then + $2=yes + fi + else + $2=yes + fi + fi + $RM -r conftest* + LDFLAGS=$save_LDFLAGS +]) + +if test yes = "[$]$2"; then + m4_if([$4], , :, [$4]) +else + m4_if([$5], , :, [$5]) +fi +])# _LT_LINKER_OPTION + +# Old name: +AU_ALIAS([AC_LIBTOOL_LINKER_OPTION], [_LT_LINKER_OPTION]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_LIBTOOL_LINKER_OPTION], []) + + +# LT_CMD_MAX_LEN +#--------------- +AC_DEFUN([LT_CMD_MAX_LEN], +[AC_REQUIRE([AC_CANONICAL_HOST])dnl +# find the maximum length of command line arguments +AC_MSG_CHECKING([the maximum length of command line arguments]) +AC_CACHE_VAL([lt_cv_sys_max_cmd_len], [dnl + i=0 + teststring=ABCD + + case $build_os in + msdosdjgpp*) + # On DJGPP, this test can blow up pretty badly due to problems in libc + # (any single argument exceeding 2000 bytes causes a buffer overrun + # during glob expansion). Even if it were fixed, the result of this + # check would be larger than it should be. + lt_cv_sys_max_cmd_len=12288; # 12K is about right + ;; + + gnu*) + # Under GNU Hurd, this test is not required because there is + # no limit to the length of command line arguments. + # Libtool will interpret -1 as no limit whatsoever + lt_cv_sys_max_cmd_len=-1; + ;; + + cygwin* | mingw* | cegcc*) + # On Win9x/ME, this test blows up -- it succeeds, but takes + # about 5 minutes as the teststring grows exponentially. + # Worse, since 9x/ME are not pre-emptively multitasking, + # you end up with a "frozen" computer, even though with patience + # the test eventually succeeds (with a max line length of 256k). + # Instead, let's just punt: use the minimum linelength reported by + # all of the supported platforms: 8192 (on NT/2K/XP). + lt_cv_sys_max_cmd_len=8192; + ;; + + mint*) + # On MiNT this can take a long time and run out of memory. + lt_cv_sys_max_cmd_len=8192; + ;; + + amigaos*) + # On AmigaOS with pdksh, this test takes hours, literally. + # So we just punt and use a minimum line length of 8192. + lt_cv_sys_max_cmd_len=8192; + ;; + + bitrig* | darwin* | dragonfly* | freebsd* | netbsd* | openbsd*) + # This has been around since 386BSD, at least. Likely further. + if test -x /sbin/sysctl; then + lt_cv_sys_max_cmd_len=`/sbin/sysctl -n kern.argmax` + elif test -x /usr/sbin/sysctl; then + lt_cv_sys_max_cmd_len=`/usr/sbin/sysctl -n kern.argmax` + else + lt_cv_sys_max_cmd_len=65536 # usable default for all BSDs + fi + # And add a safety zone + lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 4` + lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \* 3` + ;; + + interix*) + # We know the value 262144 and hardcode it with a safety zone (like BSD) + lt_cv_sys_max_cmd_len=196608 + ;; + + os2*) + # The test takes a long time on OS/2. + lt_cv_sys_max_cmd_len=8192 + ;; + + osf*) + # Dr. Hans Ekkehard Plesser reports seeing a kernel panic running configure + # due to this test when exec_disable_arg_limit is 1 on Tru64. It is not + # nice to cause kernel panics so lets avoid the loop below. + # First set a reasonable default. + lt_cv_sys_max_cmd_len=16384 + # + if test -x /sbin/sysconfig; then + case `/sbin/sysconfig -q proc exec_disable_arg_limit` in + *1*) lt_cv_sys_max_cmd_len=-1 ;; + esac + fi + ;; + sco3.2v5*) + lt_cv_sys_max_cmd_len=102400 + ;; + sysv5* | sco5v6* | sysv4.2uw2*) + kargmax=`grep ARG_MAX /etc/conf/cf.d/stune 2>/dev/null` + if test -n "$kargmax"; then + lt_cv_sys_max_cmd_len=`echo $kargmax | sed 's/.*[[ ]]//'` + else + lt_cv_sys_max_cmd_len=32768 + fi + ;; + *) + lt_cv_sys_max_cmd_len=`(getconf ARG_MAX) 2> /dev/null` + if test -n "$lt_cv_sys_max_cmd_len" && \ + test undefined != "$lt_cv_sys_max_cmd_len"; then + lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 4` + lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \* 3` + else + # Make teststring a little bigger before we do anything with it. + # a 1K string should be a reasonable start. + for i in 1 2 3 4 5 6 7 8; do + teststring=$teststring$teststring + done + SHELL=${SHELL-${CONFIG_SHELL-/bin/sh}} + # If test is not a shell built-in, we'll probably end up computing a + # maximum length that is only half of the actual maximum length, but + # we can't tell. + while { test X`env echo "$teststring$teststring" 2>/dev/null` \ + = "X$teststring$teststring"; } >/dev/null 2>&1 && + test 17 != "$i" # 1/2 MB should be enough + do + i=`expr $i + 1` + teststring=$teststring$teststring + done + # Only check the string length outside the loop. + lt_cv_sys_max_cmd_len=`expr "X$teststring" : ".*" 2>&1` + teststring= + # Add a significant safety factor because C++ compilers can tack on + # massive amounts of additional arguments before passing them to the + # linker. It appears as though 1/2 is a usable value. + lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 2` + fi + ;; + esac +]) +if test -n "$lt_cv_sys_max_cmd_len"; then + AC_MSG_RESULT($lt_cv_sys_max_cmd_len) +else + AC_MSG_RESULT(none) +fi +max_cmd_len=$lt_cv_sys_max_cmd_len +_LT_DECL([], [max_cmd_len], [0], + [What is the maximum length of a command?]) +])# LT_CMD_MAX_LEN + +# Old name: +AU_ALIAS([AC_LIBTOOL_SYS_MAX_CMD_LEN], [LT_CMD_MAX_LEN]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_LIBTOOL_SYS_MAX_CMD_LEN], []) + + +# _LT_HEADER_DLFCN +# ---------------- +m4_defun([_LT_HEADER_DLFCN], +[AC_CHECK_HEADERS([dlfcn.h], [], [], [AC_INCLUDES_DEFAULT])dnl +])# _LT_HEADER_DLFCN + + +# _LT_TRY_DLOPEN_SELF (ACTION-IF-TRUE, ACTION-IF-TRUE-W-USCORE, +# ACTION-IF-FALSE, ACTION-IF-CROSS-COMPILING) +# ---------------------------------------------------------------- +m4_defun([_LT_TRY_DLOPEN_SELF], +[m4_require([_LT_HEADER_DLFCN])dnl +if test yes = "$cross_compiling"; then : + [$4] +else + lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2 + lt_status=$lt_dlunknown + cat > conftest.$ac_ext <<_LT_EOF +[#line $LINENO "configure" +#include "confdefs.h" + +#if HAVE_DLFCN_H +#include +#endif + +#include + +#ifdef RTLD_GLOBAL +# define LT_DLGLOBAL RTLD_GLOBAL +#else +# ifdef DL_GLOBAL +# define LT_DLGLOBAL DL_GLOBAL +# else +# define LT_DLGLOBAL 0 +# endif +#endif + +/* We may have to define LT_DLLAZY_OR_NOW in the command line if we + find out it does not work in some platform. */ +#ifndef LT_DLLAZY_OR_NOW +# ifdef RTLD_LAZY +# define LT_DLLAZY_OR_NOW RTLD_LAZY +# else +# ifdef DL_LAZY +# define LT_DLLAZY_OR_NOW DL_LAZY +# else +# ifdef RTLD_NOW +# define LT_DLLAZY_OR_NOW RTLD_NOW +# else +# ifdef DL_NOW +# define LT_DLLAZY_OR_NOW DL_NOW +# else +# define LT_DLLAZY_OR_NOW 0 +# endif +# endif +# endif +# endif +#endif + +/* When -fvisibility=hidden is used, assume the code has been annotated + correspondingly for the symbols needed. */ +#if defined __GNUC__ && (((__GNUC__ == 3) && (__GNUC_MINOR__ >= 3)) || (__GNUC__ > 3)) +int fnord () __attribute__((visibility("default"))); +#endif + +int fnord () { return 42; } +int main () +{ + void *self = dlopen (0, LT_DLGLOBAL|LT_DLLAZY_OR_NOW); + int status = $lt_dlunknown; + + if (self) + { + if (dlsym (self,"fnord")) status = $lt_dlno_uscore; + else + { + if (dlsym( self,"_fnord")) status = $lt_dlneed_uscore; + else puts (dlerror ()); + } + /* dlclose (self); */ + } + else + puts (dlerror ()); + + return status; +}] +_LT_EOF + if AC_TRY_EVAL(ac_link) && test -s "conftest$ac_exeext" 2>/dev/null; then + (./conftest; exit; ) >&AS_MESSAGE_LOG_FD 2>/dev/null + lt_status=$? + case x$lt_status in + x$lt_dlno_uscore) $1 ;; + x$lt_dlneed_uscore) $2 ;; + x$lt_dlunknown|x*) $3 ;; + esac + else : + # compilation failed + $3 + fi +fi +rm -fr conftest* +])# _LT_TRY_DLOPEN_SELF + + +# LT_SYS_DLOPEN_SELF +# ------------------ +AC_DEFUN([LT_SYS_DLOPEN_SELF], +[m4_require([_LT_HEADER_DLFCN])dnl +if test yes != "$enable_dlopen"; then + enable_dlopen=unknown + enable_dlopen_self=unknown + enable_dlopen_self_static=unknown +else + lt_cv_dlopen=no + lt_cv_dlopen_libs= + + case $host_os in + beos*) + lt_cv_dlopen=load_add_on + lt_cv_dlopen_libs= + lt_cv_dlopen_self=yes + ;; + + mingw* | pw32* | cegcc*) + lt_cv_dlopen=LoadLibrary + lt_cv_dlopen_libs= + ;; + + cygwin*) + lt_cv_dlopen=dlopen + lt_cv_dlopen_libs= + ;; + + darwin*) + # if libdl is installed we need to link against it + AC_CHECK_LIB([dl], [dlopen], + [lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-ldl],[ + lt_cv_dlopen=dyld + lt_cv_dlopen_libs= + lt_cv_dlopen_self=yes + ]) + ;; + + tpf*) + # Don't try to run any link tests for TPF. We know it's impossible + # because TPF is a cross-compiler, and we know how we open DSOs. + lt_cv_dlopen=dlopen + lt_cv_dlopen_libs= + lt_cv_dlopen_self=no + ;; + + *) + AC_CHECK_FUNC([shl_load], + [lt_cv_dlopen=shl_load], + [AC_CHECK_LIB([dld], [shl_load], + [lt_cv_dlopen=shl_load lt_cv_dlopen_libs=-ldld], + [AC_CHECK_FUNC([dlopen], + [lt_cv_dlopen=dlopen], + [AC_CHECK_LIB([dl], [dlopen], + [lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-ldl], + [AC_CHECK_LIB([svld], [dlopen], + [lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-lsvld], + [AC_CHECK_LIB([dld], [dld_link], + [lt_cv_dlopen=dld_link lt_cv_dlopen_libs=-ldld]) + ]) + ]) + ]) + ]) + ]) + ;; + esac + + if test no = "$lt_cv_dlopen"; then + enable_dlopen=no + else + enable_dlopen=yes + fi + + case $lt_cv_dlopen in + dlopen) + save_CPPFLAGS=$CPPFLAGS + test yes = "$ac_cv_header_dlfcn_h" && CPPFLAGS="$CPPFLAGS -DHAVE_DLFCN_H" + + save_LDFLAGS=$LDFLAGS + wl=$lt_prog_compiler_wl eval LDFLAGS=\"\$LDFLAGS $export_dynamic_flag_spec\" + + save_LIBS=$LIBS + LIBS="$lt_cv_dlopen_libs $LIBS" + + AC_CACHE_CHECK([whether a program can dlopen itself], + lt_cv_dlopen_self, [dnl + _LT_TRY_DLOPEN_SELF( + lt_cv_dlopen_self=yes, lt_cv_dlopen_self=yes, + lt_cv_dlopen_self=no, lt_cv_dlopen_self=cross) + ]) + + if test yes = "$lt_cv_dlopen_self"; then + wl=$lt_prog_compiler_wl eval LDFLAGS=\"\$LDFLAGS $lt_prog_compiler_static\" + AC_CACHE_CHECK([whether a statically linked program can dlopen itself], + lt_cv_dlopen_self_static, [dnl + _LT_TRY_DLOPEN_SELF( + lt_cv_dlopen_self_static=yes, lt_cv_dlopen_self_static=yes, + lt_cv_dlopen_self_static=no, lt_cv_dlopen_self_static=cross) + ]) + fi + + CPPFLAGS=$save_CPPFLAGS + LDFLAGS=$save_LDFLAGS + LIBS=$save_LIBS + ;; + esac + + case $lt_cv_dlopen_self in + yes|no) enable_dlopen_self=$lt_cv_dlopen_self ;; + *) enable_dlopen_self=unknown ;; + esac + + case $lt_cv_dlopen_self_static in + yes|no) enable_dlopen_self_static=$lt_cv_dlopen_self_static ;; + *) enable_dlopen_self_static=unknown ;; + esac +fi +_LT_DECL([dlopen_support], [enable_dlopen], [0], + [Whether dlopen is supported]) +_LT_DECL([dlopen_self], [enable_dlopen_self], [0], + [Whether dlopen of programs is supported]) +_LT_DECL([dlopen_self_static], [enable_dlopen_self_static], [0], + [Whether dlopen of statically linked programs is supported]) +])# LT_SYS_DLOPEN_SELF + +# Old name: +AU_ALIAS([AC_LIBTOOL_DLOPEN_SELF], [LT_SYS_DLOPEN_SELF]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_LIBTOOL_DLOPEN_SELF], []) + + +# _LT_COMPILER_C_O([TAGNAME]) +# --------------------------- +# Check to see if options -c and -o are simultaneously supported by compiler. +# This macro does not hard code the compiler like AC_PROG_CC_C_O. +m4_defun([_LT_COMPILER_C_O], +[m4_require([_LT_DECL_SED])dnl +m4_require([_LT_FILEUTILS_DEFAULTS])dnl +m4_require([_LT_TAG_COMPILER])dnl +AC_CACHE_CHECK([if $compiler supports -c -o file.$ac_objext], + [_LT_TAGVAR(lt_cv_prog_compiler_c_o, $1)], + [_LT_TAGVAR(lt_cv_prog_compiler_c_o, $1)=no + $RM -r conftest 2>/dev/null + mkdir conftest + cd conftest + mkdir out + echo "$lt_simple_compile_test_code" > conftest.$ac_ext + + lt_compiler_flag="-o out/conftest2.$ac_objext" + # Insert the option either (1) after the last *FLAGS variable, or + # (2) before a word containing "conftest.", or (3) at the end. + # Note that $ac_compile itself does not contain backslashes and begins + # with a dollar sign (not a hyphen), so the echo should work correctly. + lt_compile=`echo "$ac_compile" | $SED \ + -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ + -e 's: [[^ ]]*conftest\.: $lt_compiler_flag&:; t' \ + -e 's:$: $lt_compiler_flag:'` + (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&AS_MESSAGE_LOG_FD) + (eval "$lt_compile" 2>out/conftest.err) + ac_status=$? + cat out/conftest.err >&AS_MESSAGE_LOG_FD + echo "$as_me:$LINENO: \$? = $ac_status" >&AS_MESSAGE_LOG_FD + if (exit $ac_status) && test -s out/conftest2.$ac_objext + then + # The compiler can only warn and ignore the option if not recognized + # So say no if there are warnings + $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' > out/conftest.exp + $SED '/^$/d; /^ *+/d' out/conftest.err >out/conftest.er2 + if test ! -s out/conftest.er2 || diff out/conftest.exp out/conftest.er2 >/dev/null; then + _LT_TAGVAR(lt_cv_prog_compiler_c_o, $1)=yes + fi + fi + chmod u+w . 2>&AS_MESSAGE_LOG_FD + $RM conftest* + # SGI C++ compiler will create directory out/ii_files/ for + # template instantiation + test -d out/ii_files && $RM out/ii_files/* && rmdir out/ii_files + $RM out/* && rmdir out + cd .. + $RM -r conftest + $RM conftest* +]) +_LT_TAGDECL([compiler_c_o], [lt_cv_prog_compiler_c_o], [1], + [Does compiler simultaneously support -c and -o options?]) +])# _LT_COMPILER_C_O + + +# _LT_COMPILER_FILE_LOCKS([TAGNAME]) +# ---------------------------------- +# Check to see if we can do hard links to lock some files if needed +m4_defun([_LT_COMPILER_FILE_LOCKS], +[m4_require([_LT_ENABLE_LOCK])dnl +m4_require([_LT_FILEUTILS_DEFAULTS])dnl +_LT_COMPILER_C_O([$1]) + +hard_links=nottested +if test no = "$_LT_TAGVAR(lt_cv_prog_compiler_c_o, $1)" && test no != "$need_locks"; then + # do not overwrite the value of need_locks provided by the user + AC_MSG_CHECKING([if we can lock with hard links]) + hard_links=yes + $RM conftest* + ln conftest.a conftest.b 2>/dev/null && hard_links=no + touch conftest.a + ln conftest.a conftest.b 2>&5 || hard_links=no + ln conftest.a conftest.b 2>/dev/null && hard_links=no + AC_MSG_RESULT([$hard_links]) + if test no = "$hard_links"; then + AC_MSG_WARN(['$CC' does not support '-c -o', so 'make -j' may be unsafe]) + need_locks=warn + fi +else + need_locks=no +fi +_LT_DECL([], [need_locks], [1], [Must we lock files when doing compilation?]) +])# _LT_COMPILER_FILE_LOCKS + + +# _LT_CHECK_OBJDIR +# ---------------- +m4_defun([_LT_CHECK_OBJDIR], +[AC_CACHE_CHECK([for objdir], [lt_cv_objdir], +[rm -f .libs 2>/dev/null +mkdir .libs 2>/dev/null +if test -d .libs; then + lt_cv_objdir=.libs +else + # MS-DOS does not allow filenames that begin with a dot. + lt_cv_objdir=_libs +fi +rmdir .libs 2>/dev/null]) +objdir=$lt_cv_objdir +_LT_DECL([], [objdir], [0], + [The name of the directory that contains temporary libtool files])dnl +m4_pattern_allow([LT_OBJDIR])dnl +AC_DEFINE_UNQUOTED([LT_OBJDIR], "$lt_cv_objdir/", + [Define to the sub-directory where libtool stores uninstalled libraries.]) +])# _LT_CHECK_OBJDIR + + +# _LT_LINKER_HARDCODE_LIBPATH([TAGNAME]) +# -------------------------------------- +# Check hardcoding attributes. +m4_defun([_LT_LINKER_HARDCODE_LIBPATH], +[AC_MSG_CHECKING([how to hardcode library paths into programs]) +_LT_TAGVAR(hardcode_action, $1)= +if test -n "$_LT_TAGVAR(hardcode_libdir_flag_spec, $1)" || + test -n "$_LT_TAGVAR(runpath_var, $1)" || + test yes = "$_LT_TAGVAR(hardcode_automatic, $1)"; then + + # We can hardcode non-existent directories. + if test no != "$_LT_TAGVAR(hardcode_direct, $1)" && + # If the only mechanism to avoid hardcoding is shlibpath_var, we + # have to relink, otherwise we might link with an installed library + # when we should be linking with a yet-to-be-installed one + ## test no != "$_LT_TAGVAR(hardcode_shlibpath_var, $1)" && + test no != "$_LT_TAGVAR(hardcode_minus_L, $1)"; then + # Linking always hardcodes the temporary library directory. + _LT_TAGVAR(hardcode_action, $1)=relink + else + # We can link without hardcoding, and we can hardcode nonexisting dirs. + _LT_TAGVAR(hardcode_action, $1)=immediate + fi +else + # We cannot hardcode anything, or else we can only hardcode existing + # directories. + _LT_TAGVAR(hardcode_action, $1)=unsupported +fi +AC_MSG_RESULT([$_LT_TAGVAR(hardcode_action, $1)]) + +if test relink = "$_LT_TAGVAR(hardcode_action, $1)" || + test yes = "$_LT_TAGVAR(inherit_rpath, $1)"; then + # Fast installation is not supported + enable_fast_install=no +elif test yes = "$shlibpath_overrides_runpath" || + test no = "$enable_shared"; then + # Fast installation is not necessary + enable_fast_install=needless +fi +_LT_TAGDECL([], [hardcode_action], [0], + [How to hardcode a shared library path into an executable]) +])# _LT_LINKER_HARDCODE_LIBPATH + + +# _LT_CMD_STRIPLIB +# ---------------- +m4_defun([_LT_CMD_STRIPLIB], +[m4_require([_LT_DECL_EGREP]) +striplib= +old_striplib= +AC_MSG_CHECKING([whether stripping libraries is possible]) +if test -n "$STRIP" && $STRIP -V 2>&1 | $GREP "GNU strip" >/dev/null; then + test -z "$old_striplib" && old_striplib="$STRIP --strip-debug" + test -z "$striplib" && striplib="$STRIP --strip-unneeded" + AC_MSG_RESULT([yes]) +else +# FIXME - insert some real tests, host_os isn't really good enough + case $host_os in + darwin*) + if test -n "$STRIP"; then + striplib="$STRIP -x" + old_striplib="$STRIP -S" + AC_MSG_RESULT([yes]) + else + AC_MSG_RESULT([no]) + fi + ;; + *) + AC_MSG_RESULT([no]) + ;; + esac +fi +_LT_DECL([], [old_striplib], [1], [Commands to strip libraries]) +_LT_DECL([], [striplib], [1]) +])# _LT_CMD_STRIPLIB + + +# _LT_PREPARE_MUNGE_PATH_LIST +# --------------------------- +# Make sure func_munge_path_list() is defined correctly. +m4_defun([_LT_PREPARE_MUNGE_PATH_LIST], +[[# func_munge_path_list VARIABLE PATH +# ----------------------------------- +# VARIABLE is name of variable containing _space_ separated list of +# directories to be munged by the contents of PATH, which is string +# having a format: +# "DIR[:DIR]:" +# string "DIR[ DIR]" will be prepended to VARIABLE +# ":DIR[:DIR]" +# string "DIR[ DIR]" will be appended to VARIABLE +# "DIRP[:DIRP]::[DIRA:]DIRA" +# string "DIRP[ DIRP]" will be prepended to VARIABLE and string +# "DIRA[ DIRA]" will be appended to VARIABLE +# "DIR[:DIR]" +# VARIABLE will be replaced by "DIR[ DIR]" +func_munge_path_list () +{ + case x@S|@2 in + x) + ;; + *:) + eval @S|@1=\"`$ECHO @S|@2 | $SED 's/:/ /g'` \@S|@@S|@1\" + ;; + x:*) + eval @S|@1=\"\@S|@@S|@1 `$ECHO @S|@2 | $SED 's/:/ /g'`\" + ;; + *::*) + eval @S|@1=\"\@S|@@S|@1\ `$ECHO @S|@2 | $SED -e 's/.*:://' -e 's/:/ /g'`\" + eval @S|@1=\"`$ECHO @S|@2 | $SED -e 's/::.*//' -e 's/:/ /g'`\ \@S|@@S|@1\" + ;; + *) + eval @S|@1=\"`$ECHO @S|@2 | $SED 's/:/ /g'`\" + ;; + esac +} +]])# _LT_PREPARE_PATH_LIST + + +# _LT_SYS_DYNAMIC_LINKER([TAG]) +# ----------------------------- +# PORTME Fill in your ld.so characteristics +m4_defun([_LT_SYS_DYNAMIC_LINKER], +[AC_REQUIRE([AC_CANONICAL_HOST])dnl +m4_require([_LT_DECL_EGREP])dnl +m4_require([_LT_FILEUTILS_DEFAULTS])dnl +m4_require([_LT_DECL_OBJDUMP])dnl +m4_require([_LT_DECL_SED])dnl +m4_require([_LT_CHECK_SHELL_FEATURES])dnl +m4_require([_LT_PREPARE_MUNGE_PATH_LIST])dnl +AC_MSG_CHECKING([dynamic linker characteristics]) +m4_if([$1], + [], [ +if test yes = "$GCC"; then + case $host_os in + darwin*) lt_awk_arg='/^libraries:/,/LR/' ;; + *) lt_awk_arg='/^libraries:/' ;; + esac + case $host_os in + mingw* | cegcc*) lt_sed_strip_eq='s|=\([[A-Za-z]]:\)|\1|g' ;; + *) lt_sed_strip_eq='s|=/|/|g' ;; + esac + lt_search_path_spec=`$CC -print-search-dirs | awk $lt_awk_arg | $SED -e "s/^libraries://" -e $lt_sed_strip_eq` + case $lt_search_path_spec in + *\;*) + # if the path contains ";" then we assume it to be the separator + # otherwise default to the standard path separator (i.e. ":") - it is + # assumed that no part of a normal pathname contains ";" but that should + # okay in the real world where ";" in dirpaths is itself problematic. + lt_search_path_spec=`$ECHO "$lt_search_path_spec" | $SED 's/;/ /g'` + ;; + *) + lt_search_path_spec=`$ECHO "$lt_search_path_spec" | $SED "s/$PATH_SEPARATOR/ /g"` + ;; + esac + # Ok, now we have the path, separated by spaces, we can step through it + # and add multilib dir if necessary... + lt_tmp_lt_search_path_spec= + lt_multi_os_dir=/`$CC $CPPFLAGS $CFLAGS $LDFLAGS -print-multi-os-directory 2>/dev/null` + # ...but if some path component already ends with the multilib dir we assume + # that all is fine and trust -print-search-dirs as is (GCC 4.2? or newer). + case "$lt_multi_os_dir; $lt_search_path_spec " in + "/; "* | "/.; "* | "/./; "* | *"$lt_multi_os_dir "* | *"$lt_multi_os_dir/ "*) + lt_multi_os_dir= + ;; + esac + for lt_sys_path in $lt_search_path_spec; do + if test -d "$lt_sys_path$lt_multi_os_dir"; then + lt_tmp_lt_search_path_spec="$lt_tmp_lt_search_path_spec $lt_sys_path$lt_multi_os_dir" + elif test -n "$lt_multi_os_dir"; then + test -d "$lt_sys_path" && \ + lt_tmp_lt_search_path_spec="$lt_tmp_lt_search_path_spec $lt_sys_path" + fi + done + lt_search_path_spec=`$ECHO "$lt_tmp_lt_search_path_spec" | awk ' +BEGIN {RS = " "; FS = "/|\n";} { + lt_foo = ""; + lt_count = 0; + for (lt_i = NF; lt_i > 0; lt_i--) { + if ($lt_i != "" && $lt_i != ".") { + if ($lt_i == "..") { + lt_count++; + } else { + if (lt_count == 0) { + lt_foo = "/" $lt_i lt_foo; + } else { + lt_count--; + } + } + } + } + if (lt_foo != "") { lt_freq[[lt_foo]]++; } + if (lt_freq[[lt_foo]] == 1) { print lt_foo; } +}'` + # AWK program above erroneously prepends '/' to C:/dos/paths + # for these hosts. + case $host_os in + mingw* | cegcc*) lt_search_path_spec=`$ECHO "$lt_search_path_spec" |\ + $SED 's|/\([[A-Za-z]]:\)|\1|g'` ;; + esac + sys_lib_search_path_spec=`$ECHO "$lt_search_path_spec" | $lt_NL2SP` +else + sys_lib_search_path_spec="/lib /usr/lib /usr/local/lib" +fi]) +library_names_spec= +libname_spec='lib$name' +soname_spec= +shrext_cmds=.so +postinstall_cmds= +postuninstall_cmds= +finish_cmds= +finish_eval= +shlibpath_var= +shlibpath_overrides_runpath=unknown +version_type=none +dynamic_linker="$host_os ld.so" +sys_lib_dlsearch_path_spec="/lib /usr/lib" +need_lib_prefix=unknown +hardcode_into_libs=no + +# when you set need_version to no, make sure it does not cause -set_version +# flags to be left without arguments +need_version=unknown + +AC_ARG_VAR([LT_SYS_LIBRARY_PATH], +[User-defined run-time library search path.]) + +case $host_os in +aix3*) + version_type=linux # correct to gnu/linux during the next big refactor + library_names_spec='$libname$release$shared_ext$versuffix $libname.a' + shlibpath_var=LIBPATH + + # AIX 3 has no versioning support, so we append a major version to the name. + soname_spec='$libname$release$shared_ext$major' + ;; + +aix[[4-9]]*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + hardcode_into_libs=yes + if test ia64 = "$host_cpu"; then + # AIX 5 supports IA64 + library_names_spec='$libname$release$shared_ext$major $libname$release$shared_ext$versuffix $libname$shared_ext' + shlibpath_var=LD_LIBRARY_PATH + else + # With GCC up to 2.95.x, collect2 would create an import file + # for dependence libraries. The import file would start with + # the line '#! .'. This would cause the generated library to + # depend on '.', always an invalid library. This was fixed in + # development snapshots of GCC prior to 3.0. + case $host_os in + aix4 | aix4.[[01]] | aix4.[[01]].*) + if { echo '#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 97)' + echo ' yes ' + echo '#endif'; } | $CC -E - | $GREP yes > /dev/null; then + : + else + can_build_shared=no + fi + ;; + esac + # Using Import Files as archive members, it is possible to support + # filename-based versioning of shared library archives on AIX. While + # this would work for both with and without runtime linking, it will + # prevent static linking of such archives. So we do filename-based + # shared library versioning with .so extension only, which is used + # when both runtime linking and shared linking is enabled. + # Unfortunately, runtime linking may impact performance, so we do + # not want this to be the default eventually. Also, we use the + # versioned .so libs for executables only if there is the -brtl + # linker flag in LDFLAGS as well, or --with-aix-soname=svr4 only. + # To allow for filename-based versioning support, we need to create + # libNAME.so.V as an archive file, containing: + # *) an Import File, referring to the versioned filename of the + # archive as well as the shared archive member, telling the + # bitwidth (32 or 64) of that shared object, and providing the + # list of exported symbols of that shared object, eventually + # decorated with the 'weak' keyword + # *) the shared object with the F_LOADONLY flag set, to really avoid + # it being seen by the linker. + # At run time we better use the real file rather than another symlink, + # but for link time we create the symlink libNAME.so -> libNAME.so.V + + case $with_aix_soname,$aix_use_runtimelinking in + # AIX (on Power*) has no versioning support, so currently we cannot hardcode correct + # soname into executable. Probably we can add versioning support to + # collect2, so additional links can be useful in future. + aix,yes) # traditional libtool + dynamic_linker='AIX unversionable lib.so' + # If using run time linking (on AIX 4.2 or later) use lib.so + # instead of lib.a to let people know that these are not + # typical AIX shared libraries. + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + ;; + aix,no) # traditional AIX only + dynamic_linker='AIX lib.a[(]lib.so.V[)]' + # We preserve .a as extension for shared libraries through AIX4.2 + # and later when we are not doing run time linking. + library_names_spec='$libname$release.a $libname.a' + soname_spec='$libname$release$shared_ext$major' + ;; + svr4,*) # full svr4 only + dynamic_linker="AIX lib.so.V[(]$shared_archive_member_spec.o[)]" + library_names_spec='$libname$release$shared_ext$major $libname$shared_ext' + # We do not specify a path in Import Files, so LIBPATH fires. + shlibpath_overrides_runpath=yes + ;; + *,yes) # both, prefer svr4 + dynamic_linker="AIX lib.so.V[(]$shared_archive_member_spec.o[)], lib.a[(]lib.so.V[)]" + library_names_spec='$libname$release$shared_ext$major $libname$shared_ext' + # unpreferred sharedlib libNAME.a needs extra handling + postinstall_cmds='test -n "$linkname" || linkname="$realname"~func_stripname "" ".so" "$linkname"~$install_shared_prog "$dir/$func_stripname_result.$libext" "$destdir/$func_stripname_result.$libext"~test -z "$tstripme" || test -z "$striplib" || $striplib "$destdir/$func_stripname_result.$libext"' + postuninstall_cmds='for n in $library_names $old_library; do :; done~func_stripname "" ".so" "$n"~test "$func_stripname_result" = "$n" || func_append rmfiles " $odir/$func_stripname_result.$libext"' + # We do not specify a path in Import Files, so LIBPATH fires. + shlibpath_overrides_runpath=yes + ;; + *,no) # both, prefer aix + dynamic_linker="AIX lib.a[(]lib.so.V[)], lib.so.V[(]$shared_archive_member_spec.o[)]" + library_names_spec='$libname$release.a $libname.a' + soname_spec='$libname$release$shared_ext$major' + # unpreferred sharedlib libNAME.so.V and symlink libNAME.so need extra handling + postinstall_cmds='test -z "$dlname" || $install_shared_prog $dir/$dlname $destdir/$dlname~test -z "$tstripme" || test -z "$striplib" || $striplib $destdir/$dlname~test -n "$linkname" || linkname=$realname~func_stripname "" ".a" "$linkname"~(cd "$destdir" && $LN_S -f $dlname $func_stripname_result.so)' + postuninstall_cmds='test -z "$dlname" || func_append rmfiles " $odir/$dlname"~for n in $old_library $library_names; do :; done~func_stripname "" ".a" "$n"~func_append rmfiles " $odir/$func_stripname_result.so"' + ;; + esac + shlibpath_var=LIBPATH + fi + ;; + +amigaos*) + case $host_cpu in + powerpc) + # Since July 2007 AmigaOS4 officially supports .so libraries. + # When compiling the executable, add -use-dynld -Lsobjs: to the compileline. + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + ;; + m68k) + library_names_spec='$libname.ixlibrary $libname.a' + # Create ${libname}_ixlibrary.a entries in /sys/libs. + finish_eval='for lib in `ls $libdir/*.ixlibrary 2>/dev/null`; do libname=`func_echo_all "$lib" | $SED '\''s%^.*/\([[^/]]*\)\.ixlibrary$%\1%'\''`; $RM /sys/libs/${libname}_ixlibrary.a; $show "cd /sys/libs && $LN_S $lib ${libname}_ixlibrary.a"; cd /sys/libs && $LN_S $lib ${libname}_ixlibrary.a || exit 1; done' + ;; + esac + ;; + +beos*) + library_names_spec='$libname$shared_ext' + dynamic_linker="$host_os ld.so" + shlibpath_var=LIBRARY_PATH + ;; + +bsdi[[45]]*) + version_type=linux # correct to gnu/linux during the next big refactor + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + finish_cmds='PATH="\$PATH:/sbin" ldconfig $libdir' + shlibpath_var=LD_LIBRARY_PATH + sys_lib_search_path_spec="/shlib /usr/lib /usr/X11/lib /usr/contrib/lib /lib /usr/local/lib" + sys_lib_dlsearch_path_spec="/shlib /usr/lib /usr/local/lib" + # the default ld.so.conf also contains /usr/contrib/lib and + # /usr/X11R6/lib (/usr/X11 is a link to /usr/X11R6), but let us allow + # libtool to hard-code these into programs + ;; + +cygwin* | mingw* | pw32* | cegcc*) + version_type=windows + shrext_cmds=.dll + need_version=no + need_lib_prefix=no + + case $GCC,$cc_basename in + yes,*) + # gcc + library_names_spec='$libname.dll.a' + # DLL is installed to $(libdir)/../bin by postinstall_cmds + postinstall_cmds='base_file=`basename \$file`~ + dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; echo \$dlname'\''`~ + dldir=$destdir/`dirname \$dlpath`~ + test -d \$dldir || mkdir -p \$dldir~ + $install_prog $dir/$dlname \$dldir/$dlname~ + chmod a+x \$dldir/$dlname~ + if test -n '\''$stripme'\'' && test -n '\''$striplib'\''; then + eval '\''$striplib \$dldir/$dlname'\'' || exit \$?; + fi' + postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; echo \$dlname'\''`~ + dlpath=$dir/\$dldll~ + $RM \$dlpath' + shlibpath_overrides_runpath=yes + + case $host_os in + cygwin*) + # Cygwin DLLs use 'cyg' prefix rather than 'lib' + soname_spec='`echo $libname | sed -e 's/^lib/cyg/'``echo $release | $SED -e 's/[[.]]/-/g'`$versuffix$shared_ext' +m4_if([$1], [],[ + sys_lib_search_path_spec="$sys_lib_search_path_spec /usr/lib/w32api"]) + ;; + mingw* | cegcc*) + # MinGW DLLs use traditional 'lib' prefix + soname_spec='$libname`echo $release | $SED -e 's/[[.]]/-/g'`$versuffix$shared_ext' + ;; + pw32*) + # pw32 DLLs use 'pw' prefix rather than 'lib' + library_names_spec='`echo $libname | sed -e 's/^lib/pw/'``echo $release | $SED -e 's/[[.]]/-/g'`$versuffix$shared_ext' + ;; + esac + dynamic_linker='Win32 ld.exe' + ;; + + *,cl*) + # Native MSVC + libname_spec='$name' + soname_spec='$libname`echo $release | $SED -e 's/[[.]]/-/g'`$versuffix$shared_ext' + library_names_spec='$libname.dll.lib' + + case $build_os in + mingw*) + sys_lib_search_path_spec= + lt_save_ifs=$IFS + IFS=';' + for lt_path in $LIB + do + IFS=$lt_save_ifs + # Let DOS variable expansion print the short 8.3 style file name. + lt_path=`cd "$lt_path" 2>/dev/null && cmd //C "for %i in (".") do @echo %~si"` + sys_lib_search_path_spec="$sys_lib_search_path_spec $lt_path" + done + IFS=$lt_save_ifs + # Convert to MSYS style. + sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | sed -e 's|\\\\|/|g' -e 's| \\([[a-zA-Z]]\\):| /\\1|g' -e 's|^ ||'` + ;; + cygwin*) + # Convert to unix form, then to dos form, then back to unix form + # but this time dos style (no spaces!) so that the unix form looks + # like /cygdrive/c/PROGRA~1:/cygdr... + sys_lib_search_path_spec=`cygpath --path --unix "$LIB"` + sys_lib_search_path_spec=`cygpath --path --dos "$sys_lib_search_path_spec" 2>/dev/null` + sys_lib_search_path_spec=`cygpath --path --unix "$sys_lib_search_path_spec" | $SED -e "s/$PATH_SEPARATOR/ /g"` + ;; + *) + sys_lib_search_path_spec=$LIB + if $ECHO "$sys_lib_search_path_spec" | [$GREP ';[c-zC-Z]:/' >/dev/null]; then + # It is most probably a Windows format PATH. + sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | $SED -e 's/;/ /g'` + else + sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | $SED -e "s/$PATH_SEPARATOR/ /g"` + fi + # FIXME: find the short name or the path components, as spaces are + # common. (e.g. "Program Files" -> "PROGRA~1") + ;; + esac + + # DLL is installed to $(libdir)/../bin by postinstall_cmds + postinstall_cmds='base_file=`basename \$file`~ + dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; echo \$dlname'\''`~ + dldir=$destdir/`dirname \$dlpath`~ + test -d \$dldir || mkdir -p \$dldir~ + $install_prog $dir/$dlname \$dldir/$dlname' + postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; echo \$dlname'\''`~ + dlpath=$dir/\$dldll~ + $RM \$dlpath' + shlibpath_overrides_runpath=yes + dynamic_linker='Win32 link.exe' + ;; + + *) + # Assume MSVC wrapper + library_names_spec='$libname`echo $release | $SED -e 's/[[.]]/-/g'`$versuffix$shared_ext $libname.lib' + dynamic_linker='Win32 ld.exe' + ;; + esac + # FIXME: first we should search . and the directory the executable is in + shlibpath_var=PATH + ;; + +darwin* | rhapsody*) + dynamic_linker="$host_os dyld" + version_type=darwin + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$major$shared_ext $libname$shared_ext' + soname_spec='$libname$release$major$shared_ext' + shlibpath_overrides_runpath=yes + shlibpath_var=DYLD_LIBRARY_PATH + shrext_cmds='`test .$module = .yes && echo .so || echo .dylib`' +m4_if([$1], [],[ + sys_lib_search_path_spec="$sys_lib_search_path_spec /usr/local/lib"]) + sys_lib_dlsearch_path_spec='/usr/local/lib /lib /usr/lib' + ;; + +dgux*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + ;; + +freebsd* | dragonfly*) + # DragonFly does not have aout. When/if they implement a new + # versioning mechanism, adjust this. + if test -x /usr/bin/objformat; then + objformat=`/usr/bin/objformat` + else + case $host_os in + freebsd[[23]].*) objformat=aout ;; + *) objformat=elf ;; + esac + fi + version_type=freebsd-$objformat + case $version_type in + freebsd-elf*) + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + need_version=no + need_lib_prefix=no + ;; + freebsd-*) + library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix' + need_version=yes + ;; + esac + shlibpath_var=LD_LIBRARY_PATH + case $host_os in + freebsd2.*) + shlibpath_overrides_runpath=yes + ;; + freebsd3.[[01]]* | freebsdelf3.[[01]]*) + shlibpath_overrides_runpath=yes + hardcode_into_libs=yes + ;; + freebsd3.[[2-9]]* | freebsdelf3.[[2-9]]* | \ + freebsd4.[[0-5]] | freebsdelf4.[[0-5]] | freebsd4.1.1 | freebsdelf4.1.1) + shlibpath_overrides_runpath=no + hardcode_into_libs=yes + ;; + *) # from 4.6 on, and DragonFly + shlibpath_overrides_runpath=yes + hardcode_into_libs=yes + ;; + esac + ;; + +haiku*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + dynamic_linker="$host_os runtime_loader" + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LIBRARY_PATH + shlibpath_overrides_runpath=no + sys_lib_dlsearch_path_spec='/boot/home/config/lib /boot/common/lib /boot/system/lib' + hardcode_into_libs=yes + ;; + +hpux9* | hpux10* | hpux11*) + # Give a soname corresponding to the major version so that dld.sl refuses to + # link against other versions. + version_type=sunos + need_lib_prefix=no + need_version=no + case $host_cpu in + ia64*) + shrext_cmds='.so' + hardcode_into_libs=yes + dynamic_linker="$host_os dld.so" + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes # Unless +noenvvar is specified. + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + if test 32 = "$HPUX_IA64_MODE"; then + sys_lib_search_path_spec="/usr/lib/hpux32 /usr/local/lib/hpux32 /usr/local/lib" + sys_lib_dlsearch_path_spec=/usr/lib/hpux32 + else + sys_lib_search_path_spec="/usr/lib/hpux64 /usr/local/lib/hpux64" + sys_lib_dlsearch_path_spec=/usr/lib/hpux64 + fi + ;; + hppa*64*) + shrext_cmds='.sl' + hardcode_into_libs=yes + dynamic_linker="$host_os dld.sl" + shlibpath_var=LD_LIBRARY_PATH # How should we handle SHLIB_PATH + shlibpath_overrides_runpath=yes # Unless +noenvvar is specified. + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + sys_lib_search_path_spec="/usr/lib/pa20_64 /usr/ccs/lib/pa20_64" + sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec + ;; + *) + shrext_cmds='.sl' + dynamic_linker="$host_os dld.sl" + shlibpath_var=SHLIB_PATH + shlibpath_overrides_runpath=no # +s is required to enable SHLIB_PATH + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + ;; + esac + # HP-UX runs *really* slowly unless shared libraries are mode 555, ... + postinstall_cmds='chmod 555 $lib' + # or fails outright, so override atomically: + install_override_mode=555 + ;; + +interix[[3-9]]*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + dynamic_linker='Interix 3.x ld.so.1 (PE, like ELF)' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=no + hardcode_into_libs=yes + ;; + +irix5* | irix6* | nonstopux*) + case $host_os in + nonstopux*) version_type=nonstopux ;; + *) + if test yes = "$lt_cv_prog_gnu_ld"; then + version_type=linux # correct to gnu/linux during the next big refactor + else + version_type=irix + fi ;; + esac + need_lib_prefix=no + need_version=no + soname_spec='$libname$release$shared_ext$major' + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$release$shared_ext $libname$shared_ext' + case $host_os in + irix5* | nonstopux*) + libsuff= shlibsuff= + ;; + *) + case $LD in # libtool.m4 will add one of these switches to LD + *-32|*"-32 "|*-melf32bsmip|*"-melf32bsmip ") + libsuff= shlibsuff= libmagic=32-bit;; + *-n32|*"-n32 "|*-melf32bmipn32|*"-melf32bmipn32 ") + libsuff=32 shlibsuff=N32 libmagic=N32;; + *-64|*"-64 "|*-melf64bmip|*"-melf64bmip ") + libsuff=64 shlibsuff=64 libmagic=64-bit;; + *) libsuff= shlibsuff= libmagic=never-match;; + esac + ;; + esac + shlibpath_var=LD_LIBRARY${shlibsuff}_PATH + shlibpath_overrides_runpath=no + sys_lib_search_path_spec="/usr/lib$libsuff /lib$libsuff /usr/local/lib$libsuff" + sys_lib_dlsearch_path_spec="/usr/lib$libsuff /lib$libsuff" + hardcode_into_libs=yes + ;; + +# No shared lib support for Linux oldld, aout, or coff. +linux*oldld* | linux*aout* | linux*coff*) + dynamic_linker=no + ;; + +linux*android*) + version_type=none # Android doesn't support versioned libraries. + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext' + soname_spec='$libname$release$shared_ext' + finish_cmds= + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + + # This implies no fast_install, which is unacceptable. + # Some rework will be needed to allow for fast_install + # before this can be enabled. + hardcode_into_libs=yes + + dynamic_linker='Android linker' + # Don't embed -rpath directories since the linker doesn't support them. + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + ;; + +# This must be glibc/ELF. +linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + finish_cmds='PATH="\$PATH:/sbin" ldconfig -n $libdir' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=no + + # Some binutils ld are patched to set DT_RUNPATH + AC_CACHE_VAL([lt_cv_shlibpath_overrides_runpath], + [lt_cv_shlibpath_overrides_runpath=no + save_LDFLAGS=$LDFLAGS + save_libdir=$libdir + eval "libdir=/foo; wl=\"$_LT_TAGVAR(lt_prog_compiler_wl, $1)\"; \ + LDFLAGS=\"\$LDFLAGS $_LT_TAGVAR(hardcode_libdir_flag_spec, $1)\"" + AC_LINK_IFELSE([AC_LANG_PROGRAM([],[])], + [AS_IF([ ($OBJDUMP -p conftest$ac_exeext) 2>/dev/null | grep "RUNPATH.*$libdir" >/dev/null], + [lt_cv_shlibpath_overrides_runpath=yes])]) + LDFLAGS=$save_LDFLAGS + libdir=$save_libdir + ]) + shlibpath_overrides_runpath=$lt_cv_shlibpath_overrides_runpath + + # This implies no fast_install, which is unacceptable. + # Some rework will be needed to allow for fast_install + # before this can be enabled. + hardcode_into_libs=yes + + # Ideally, we could use ldconfig to report *all* directores which are + # searched for libraries, however this is still not possible. Aside from not + # being certain /sbin/ldconfig is available, command + # 'ldconfig -N -X -v | grep ^/' on 64bit Fedora does not report /usr/lib64, + # even though it is searched at run-time. Try to do the best guess by + # appending ld.so.conf contents (and includes) to the search path. + if test -f /etc/ld.so.conf; then + lt_ld_extra=`awk '/^include / { system(sprintf("cd /etc; cat %s 2>/dev/null", \[$]2)); skip = 1; } { if (!skip) print \[$]0; skip = 0; }' < /etc/ld.so.conf | $SED -e 's/#.*//;/^[ ]*hwcap[ ]/d;s/[:, ]/ /g;s/=[^=]*$//;s/=[^= ]* / /g;s/"//g;/^$/d' | tr '\n' ' '` + sys_lib_dlsearch_path_spec="/lib /usr/lib $lt_ld_extra" + fi + + # We used to test for /lib/ld.so.1 and disable shared libraries on + # powerpc, because MkLinux only supported shared libraries with the + # GNU dynamic linker. Since this was broken with cross compilers, + # most powerpc-linux boxes support dynamic linking these days and + # people can always --disable-shared, the test was removed, and we + # assume the GNU/Linux dynamic linker is in use. + dynamic_linker='GNU/Linux ld.so' + ;; + +netbsdelf*-gnu) + version_type=linux + need_lib_prefix=no + need_version=no + library_names_spec='${libname}${release}${shared_ext}$versuffix ${libname}${release}${shared_ext}$major ${libname}${shared_ext}' + soname_spec='${libname}${release}${shared_ext}$major' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=no + hardcode_into_libs=yes + dynamic_linker='NetBSD ld.elf_so' + ;; + +netbsd*) + version_type=sunos + need_lib_prefix=no + need_version=no + if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then + library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix' + finish_cmds='PATH="\$PATH:/sbin" ldconfig -m $libdir' + dynamic_linker='NetBSD (a.out) ld.so' + else + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + dynamic_linker='NetBSD ld.elf_so' + fi + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + hardcode_into_libs=yes + ;; + +newsos6) + version_type=linux # correct to gnu/linux during the next big refactor + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + ;; + +*nto* | *qnx*) + version_type=qnx + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=no + hardcode_into_libs=yes + dynamic_linker='ldqnx.so' + ;; + +openbsd* | bitrig*) + version_type=sunos + sys_lib_dlsearch_path_spec=/usr/lib + need_lib_prefix=no + if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then + need_version=no + else + need_version=yes + fi + library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix' + finish_cmds='PATH="\$PATH:/sbin" ldconfig -m $libdir' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + ;; + +os2*) + libname_spec='$name' + version_type=windows + shrext_cmds=.dll + need_version=no + need_lib_prefix=no + # OS/2 can only load a DLL with a base name of 8 characters or less. + soname_spec='`test -n "$os2dllname" && libname="$os2dllname"; + v=$($ECHO $release$versuffix | tr -d .-); + n=$($ECHO $libname | cut -b -$((8 - ${#v})) | tr . _); + $ECHO $n$v`$shared_ext' + library_names_spec='${libname}_dll.$libext' + dynamic_linker='OS/2 ld.exe' + shlibpath_var=BEGINLIBPATH + sys_lib_search_path_spec="/lib /usr/lib /usr/local/lib" + sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec + postinstall_cmds='base_file=`basename \$file`~ + dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; $ECHO \$dlname'\''`~ + dldir=$destdir/`dirname \$dlpath`~ + test -d \$dldir || mkdir -p \$dldir~ + $install_prog $dir/$dlname \$dldir/$dlname~ + chmod a+x \$dldir/$dlname~ + if test -n '\''$stripme'\'' && test -n '\''$striplib'\''; then + eval '\''$striplib \$dldir/$dlname'\'' || exit \$?; + fi' + postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; $ECHO \$dlname'\''`~ + dlpath=$dir/\$dldll~ + $RM \$dlpath' + ;; + +osf3* | osf4* | osf5*) + version_type=osf + need_lib_prefix=no + need_version=no + soname_spec='$libname$release$shared_ext$major' + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + shlibpath_var=LD_LIBRARY_PATH + sys_lib_search_path_spec="/usr/shlib /usr/ccs/lib /usr/lib/cmplrs/cc /usr/lib /usr/local/lib /var/shlib" + sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec + ;; + +rdos*) + dynamic_linker=no + ;; + +solaris*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + hardcode_into_libs=yes + # ldd complains unless libraries are executable + postinstall_cmds='chmod +x $lib' + ;; + +sunos4*) + version_type=sunos + library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix' + finish_cmds='PATH="\$PATH:/usr/etc" ldconfig $libdir' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + if test yes = "$with_gnu_ld"; then + need_lib_prefix=no + fi + need_version=yes + ;; + +sysv4 | sysv4.3*) + version_type=linux # correct to gnu/linux during the next big refactor + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + case $host_vendor in + sni) + shlibpath_overrides_runpath=no + need_lib_prefix=no + runpath_var=LD_RUN_PATH + ;; + siemens) + need_lib_prefix=no + ;; + motorola) + need_lib_prefix=no + need_version=no + shlibpath_overrides_runpath=no + sys_lib_search_path_spec='/lib /usr/lib /usr/ccs/lib' + ;; + esac + ;; + +sysv4*MP*) + if test -d /usr/nec; then + version_type=linux # correct to gnu/linux during the next big refactor + library_names_spec='$libname$shared_ext.$versuffix $libname$shared_ext.$major $libname$shared_ext' + soname_spec='$libname$shared_ext.$major' + shlibpath_var=LD_LIBRARY_PATH + fi + ;; + +sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX* | sysv4*uw2*) + version_type=sco + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + hardcode_into_libs=yes + if test yes = "$with_gnu_ld"; then + sys_lib_search_path_spec='/usr/local/lib /usr/gnu/lib /usr/ccs/lib /usr/lib /lib' + else + sys_lib_search_path_spec='/usr/ccs/lib /usr/lib' + case $host_os in + sco3.2v5*) + sys_lib_search_path_spec="$sys_lib_search_path_spec /lib" + ;; + esac + fi + sys_lib_dlsearch_path_spec='/usr/lib' + ;; + +tpf*) + # TPF is a cross-target only. Preferred cross-host = GNU/Linux. + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=no + hardcode_into_libs=yes + ;; + +uts4*) + version_type=linux # correct to gnu/linux during the next big refactor + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + ;; + +*) + dynamic_linker=no + ;; +esac +AC_MSG_RESULT([$dynamic_linker]) +test no = "$dynamic_linker" && can_build_shared=no + +variables_saved_for_relink="PATH $shlibpath_var $runpath_var" +if test yes = "$GCC"; then + variables_saved_for_relink="$variables_saved_for_relink GCC_EXEC_PREFIX COMPILER_PATH LIBRARY_PATH" +fi + +if test set = "${lt_cv_sys_lib_search_path_spec+set}"; then + sys_lib_search_path_spec=$lt_cv_sys_lib_search_path_spec +fi + +if test set = "${lt_cv_sys_lib_dlsearch_path_spec+set}"; then + sys_lib_dlsearch_path_spec=$lt_cv_sys_lib_dlsearch_path_spec +fi + +# remember unaugmented sys_lib_dlsearch_path content for libtool script decls... +configure_time_dlsearch_path=$sys_lib_dlsearch_path_spec + +# ... but it needs LT_SYS_LIBRARY_PATH munging for other configure-time code +func_munge_path_list sys_lib_dlsearch_path_spec "$LT_SYS_LIBRARY_PATH" + +# to be used as default LT_SYS_LIBRARY_PATH value in generated libtool +configure_time_lt_sys_library_path=$LT_SYS_LIBRARY_PATH + +_LT_DECL([], [variables_saved_for_relink], [1], + [Variables whose values should be saved in libtool wrapper scripts and + restored at link time]) +_LT_DECL([], [need_lib_prefix], [0], + [Do we need the "lib" prefix for modules?]) +_LT_DECL([], [need_version], [0], [Do we need a version for libraries?]) +_LT_DECL([], [version_type], [0], [Library versioning type]) +_LT_DECL([], [runpath_var], [0], [Shared library runtime path variable]) +_LT_DECL([], [shlibpath_var], [0],[Shared library path variable]) +_LT_DECL([], [shlibpath_overrides_runpath], [0], + [Is shlibpath searched before the hard-coded library search path?]) +_LT_DECL([], [libname_spec], [1], [Format of library name prefix]) +_LT_DECL([], [library_names_spec], [1], + [[List of archive names. First name is the real one, the rest are links. + The last name is the one that the linker finds with -lNAME]]) +_LT_DECL([], [soname_spec], [1], + [[The coded name of the library, if different from the real name]]) +_LT_DECL([], [install_override_mode], [1], + [Permission mode override for installation of shared libraries]) +_LT_DECL([], [postinstall_cmds], [2], + [Command to use after installation of a shared archive]) +_LT_DECL([], [postuninstall_cmds], [2], + [Command to use after uninstallation of a shared archive]) +_LT_DECL([], [finish_cmds], [2], + [Commands used to finish a libtool library installation in a directory]) +_LT_DECL([], [finish_eval], [1], + [[As "finish_cmds", except a single script fragment to be evaled but + not shown]]) +_LT_DECL([], [hardcode_into_libs], [0], + [Whether we should hardcode library paths into libraries]) +_LT_DECL([], [sys_lib_search_path_spec], [2], + [Compile-time system search path for libraries]) +_LT_DECL([sys_lib_dlsearch_path_spec], [configure_time_dlsearch_path], [2], + [Detected run-time system search path for libraries]) +_LT_DECL([], [configure_time_lt_sys_library_path], [2], + [Explicit LT_SYS_LIBRARY_PATH set during ./configure time]) +])# _LT_SYS_DYNAMIC_LINKER + + +# _LT_PATH_TOOL_PREFIX(TOOL) +# -------------------------- +# find a file program that can recognize shared library +AC_DEFUN([_LT_PATH_TOOL_PREFIX], +[m4_require([_LT_DECL_EGREP])dnl +AC_MSG_CHECKING([for $1]) +AC_CACHE_VAL(lt_cv_path_MAGIC_CMD, +[case $MAGIC_CMD in +[[\\/*] | ?:[\\/]*]) + lt_cv_path_MAGIC_CMD=$MAGIC_CMD # Let the user override the test with a path. + ;; +*) + lt_save_MAGIC_CMD=$MAGIC_CMD + lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR +dnl $ac_dummy forces splitting on constant user-supplied paths. +dnl POSIX.2 word splitting is done only on the output of word expansions, +dnl not every word. This closes a longstanding sh security hole. + ac_dummy="m4_if([$2], , $PATH, [$2])" + for ac_dir in $ac_dummy; do + IFS=$lt_save_ifs + test -z "$ac_dir" && ac_dir=. + if test -f "$ac_dir/$1"; then + lt_cv_path_MAGIC_CMD=$ac_dir/"$1" + if test -n "$file_magic_test_file"; then + case $deplibs_check_method in + "file_magic "*) + file_magic_regex=`expr "$deplibs_check_method" : "file_magic \(.*\)"` + MAGIC_CMD=$lt_cv_path_MAGIC_CMD + if eval $file_magic_cmd \$file_magic_test_file 2> /dev/null | + $EGREP "$file_magic_regex" > /dev/null; then + : + else + cat <<_LT_EOF 1>&2 + +*** Warning: the command libtool uses to detect shared libraries, +*** $file_magic_cmd, produces output that libtool cannot recognize. +*** The result is that libtool may fail to recognize shared libraries +*** as such. This will affect the creation of libtool libraries that +*** depend on shared libraries, but programs linked with such libtool +*** libraries will work regardless of this problem. Nevertheless, you +*** may want to report the problem to your system manager and/or to +*** bug-libtool@gnu.org + +_LT_EOF + fi ;; + esac + fi + break + fi + done + IFS=$lt_save_ifs + MAGIC_CMD=$lt_save_MAGIC_CMD + ;; +esac]) +MAGIC_CMD=$lt_cv_path_MAGIC_CMD +if test -n "$MAGIC_CMD"; then + AC_MSG_RESULT($MAGIC_CMD) +else + AC_MSG_RESULT(no) +fi +_LT_DECL([], [MAGIC_CMD], [0], + [Used to examine libraries when file_magic_cmd begins with "file"])dnl +])# _LT_PATH_TOOL_PREFIX + +# Old name: +AU_ALIAS([AC_PATH_TOOL_PREFIX], [_LT_PATH_TOOL_PREFIX]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_PATH_TOOL_PREFIX], []) + + +# _LT_PATH_MAGIC +# -------------- +# find a file program that can recognize a shared library +m4_defun([_LT_PATH_MAGIC], +[_LT_PATH_TOOL_PREFIX(${ac_tool_prefix}file, /usr/bin$PATH_SEPARATOR$PATH) +if test -z "$lt_cv_path_MAGIC_CMD"; then + if test -n "$ac_tool_prefix"; then + _LT_PATH_TOOL_PREFIX(file, /usr/bin$PATH_SEPARATOR$PATH) + else + MAGIC_CMD=: + fi +fi +])# _LT_PATH_MAGIC + + +# LT_PATH_LD +# ---------- +# find the pathname to the GNU or non-GNU linker +AC_DEFUN([LT_PATH_LD], +[AC_REQUIRE([AC_PROG_CC])dnl +AC_REQUIRE([AC_CANONICAL_HOST])dnl +AC_REQUIRE([AC_CANONICAL_BUILD])dnl +m4_require([_LT_DECL_SED])dnl +m4_require([_LT_DECL_EGREP])dnl +m4_require([_LT_PROG_ECHO_BACKSLASH])dnl + +AC_ARG_WITH([gnu-ld], + [AS_HELP_STRING([--with-gnu-ld], + [assume the C compiler uses GNU ld @<:@default=no@:>@])], + [test no = "$withval" || with_gnu_ld=yes], + [with_gnu_ld=no])dnl + +ac_prog=ld +if test yes = "$GCC"; then + # Check if gcc -print-prog-name=ld gives a path. + AC_MSG_CHECKING([for ld used by $CC]) + case $host in + *-*-mingw*) + # gcc leaves a trailing carriage return, which upsets mingw + ac_prog=`($CC -print-prog-name=ld) 2>&5 | tr -d '\015'` ;; + *) + ac_prog=`($CC -print-prog-name=ld) 2>&5` ;; + esac + case $ac_prog in + # Accept absolute paths. + [[\\/]]* | ?:[[\\/]]*) + re_direlt='/[[^/]][[^/]]*/\.\./' + # Canonicalize the pathname of ld + ac_prog=`$ECHO "$ac_prog"| $SED 's%\\\\%/%g'` + while $ECHO "$ac_prog" | $GREP "$re_direlt" > /dev/null 2>&1; do + ac_prog=`$ECHO $ac_prog| $SED "s%$re_direlt%/%"` + done + test -z "$LD" && LD=$ac_prog + ;; + "") + # If it fails, then pretend we aren't using GCC. + ac_prog=ld + ;; + *) + # If it is relative, then search for the first ld in PATH. + with_gnu_ld=unknown + ;; + esac +elif test yes = "$with_gnu_ld"; then + AC_MSG_CHECKING([for GNU ld]) +else + AC_MSG_CHECKING([for non-GNU ld]) +fi +AC_CACHE_VAL(lt_cv_path_LD, +[if test -z "$LD"; then + lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR + for ac_dir in $PATH; do + IFS=$lt_save_ifs + test -z "$ac_dir" && ac_dir=. + if test -f "$ac_dir/$ac_prog" || test -f "$ac_dir/$ac_prog$ac_exeext"; then + lt_cv_path_LD=$ac_dir/$ac_prog + # Check to see if the program is GNU ld. I'd rather use --version, + # but apparently some variants of GNU ld only accept -v. + # Break only if it was the GNU/non-GNU ld that we prefer. + case `"$lt_cv_path_LD" -v 2>&1 &1 conftest.i +cat conftest.i conftest.i >conftest2.i +: ${lt_DD:=$DD} +AC_PATH_PROGS_FEATURE_CHECK([lt_DD], [dd], +[if "$ac_path_lt_DD" bs=32 count=1 conftest.out 2>/dev/null; then + cmp -s conftest.i conftest.out \ + && ac_cv_path_lt_DD="$ac_path_lt_DD" ac_path_lt_DD_found=: +fi]) +rm -f conftest.i conftest2.i conftest.out]) +])# _LT_PATH_DD + + +# _LT_CMD_TRUNCATE +# ---------------- +# find command to truncate a binary pipe +m4_defun([_LT_CMD_TRUNCATE], +[m4_require([_LT_PATH_DD]) +AC_CACHE_CHECK([how to truncate binary pipes], [lt_cv_truncate_bin], +[printf 0123456789abcdef0123456789abcdef >conftest.i +cat conftest.i conftest.i >conftest2.i +lt_cv_truncate_bin= +if "$ac_cv_path_lt_DD" bs=32 count=1 conftest.out 2>/dev/null; then + cmp -s conftest.i conftest.out \ + && lt_cv_truncate_bin="$ac_cv_path_lt_DD bs=4096 count=1" +fi +rm -f conftest.i conftest2.i conftest.out +test -z "$lt_cv_truncate_bin" && lt_cv_truncate_bin="$SED -e 4q"]) +_LT_DECL([lt_truncate_bin], [lt_cv_truncate_bin], [1], + [Command to truncate a binary pipe]) +])# _LT_CMD_TRUNCATE + + +# _LT_CHECK_MAGIC_METHOD +# ---------------------- +# how to check for library dependencies +# -- PORTME fill in with the dynamic library characteristics +m4_defun([_LT_CHECK_MAGIC_METHOD], +[m4_require([_LT_DECL_EGREP]) +m4_require([_LT_DECL_OBJDUMP]) +AC_CACHE_CHECK([how to recognize dependent libraries], +lt_cv_deplibs_check_method, +[lt_cv_file_magic_cmd='$MAGIC_CMD' +lt_cv_file_magic_test_file= +lt_cv_deplibs_check_method='unknown' +# Need to set the preceding variable on all platforms that support +# interlibrary dependencies. +# 'none' -- dependencies not supported. +# 'unknown' -- same as none, but documents that we really don't know. +# 'pass_all' -- all dependencies passed with no checks. +# 'test_compile' -- check by making test program. +# 'file_magic [[regex]]' -- check by looking for files in library path +# that responds to the $file_magic_cmd with a given extended regex. +# If you have 'file' or equivalent on your system and you're not sure +# whether 'pass_all' will *always* work, you probably want this one. + +case $host_os in +aix[[4-9]]*) + lt_cv_deplibs_check_method=pass_all + ;; + +beos*) + lt_cv_deplibs_check_method=pass_all + ;; + +bsdi[[45]]*) + lt_cv_deplibs_check_method='file_magic ELF [[0-9]][[0-9]]*-bit [[ML]]SB (shared object|dynamic lib)' + lt_cv_file_magic_cmd='/usr/bin/file -L' + lt_cv_file_magic_test_file=/shlib/libc.so + ;; + +cygwin*) + # func_win32_libid is a shell function defined in ltmain.sh + lt_cv_deplibs_check_method='file_magic ^x86 archive import|^x86 DLL' + lt_cv_file_magic_cmd='func_win32_libid' + ;; + +mingw* | pw32*) + # Base MSYS/MinGW do not provide the 'file' command needed by + # func_win32_libid shell function, so use a weaker test based on 'objdump', + # unless we find 'file', for example because we are cross-compiling. + if ( file / ) >/dev/null 2>&1; then + lt_cv_deplibs_check_method='file_magic ^x86 archive import|^x86 DLL' + lt_cv_file_magic_cmd='func_win32_libid' + else + # Keep this pattern in sync with the one in func_win32_libid. + lt_cv_deplibs_check_method='file_magic file format (pei*-i386(.*architecture: i386)?|pe-arm-wince|pe-x86-64)' + lt_cv_file_magic_cmd='$OBJDUMP -f' + fi + ;; + +cegcc*) + # use the weaker test based on 'objdump'. See mingw*. + lt_cv_deplibs_check_method='file_magic file format pe-arm-.*little(.*architecture: arm)?' + lt_cv_file_magic_cmd='$OBJDUMP -f' + ;; + +darwin* | rhapsody*) + lt_cv_deplibs_check_method=pass_all + ;; + +freebsd* | dragonfly*) + if echo __ELF__ | $CC -E - | $GREP __ELF__ > /dev/null; then + case $host_cpu in + i*86 ) + # Not sure whether the presence of OpenBSD here was a mistake. + # Let's accept both of them until this is cleared up. + lt_cv_deplibs_check_method='file_magic (FreeBSD|OpenBSD|DragonFly)/i[[3-9]]86 (compact )?demand paged shared library' + lt_cv_file_magic_cmd=/usr/bin/file + lt_cv_file_magic_test_file=`echo /usr/lib/libc.so.*` + ;; + esac + else + lt_cv_deplibs_check_method=pass_all + fi + ;; + +haiku*) + lt_cv_deplibs_check_method=pass_all + ;; + +hpux10.20* | hpux11*) + lt_cv_file_magic_cmd=/usr/bin/file + case $host_cpu in + ia64*) + lt_cv_deplibs_check_method='file_magic (s[[0-9]][[0-9]][[0-9]]|ELF-[[0-9]][[0-9]]) shared object file - IA64' + lt_cv_file_magic_test_file=/usr/lib/hpux32/libc.so + ;; + hppa*64*) + [lt_cv_deplibs_check_method='file_magic (s[0-9][0-9][0-9]|ELF[ -][0-9][0-9])(-bit)?( [LM]SB)? shared object( file)?[, -]* PA-RISC [0-9]\.[0-9]'] + lt_cv_file_magic_test_file=/usr/lib/pa20_64/libc.sl + ;; + *) + lt_cv_deplibs_check_method='file_magic (s[[0-9]][[0-9]][[0-9]]|PA-RISC[[0-9]]\.[[0-9]]) shared library' + lt_cv_file_magic_test_file=/usr/lib/libc.sl + ;; + esac + ;; + +interix[[3-9]]*) + # PIC code is broken on Interix 3.x, that's why |\.a not |_pic\.a here + lt_cv_deplibs_check_method='match_pattern /lib[[^/]]+(\.so|\.a)$' + ;; + +irix5* | irix6* | nonstopux*) + case $LD in + *-32|*"-32 ") libmagic=32-bit;; + *-n32|*"-n32 ") libmagic=N32;; + *-64|*"-64 ") libmagic=64-bit;; + *) libmagic=never-match;; + esac + lt_cv_deplibs_check_method=pass_all + ;; + +# This must be glibc/ELF. +linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*) + lt_cv_deplibs_check_method=pass_all + ;; + +netbsd* | netbsdelf*-gnu) + if echo __ELF__ | $CC -E - | $GREP __ELF__ > /dev/null; then + lt_cv_deplibs_check_method='match_pattern /lib[[^/]]+(\.so\.[[0-9]]+\.[[0-9]]+|_pic\.a)$' + else + lt_cv_deplibs_check_method='match_pattern /lib[[^/]]+(\.so|_pic\.a)$' + fi + ;; + +newos6*) + lt_cv_deplibs_check_method='file_magic ELF [[0-9]][[0-9]]*-bit [[ML]]SB (executable|dynamic lib)' + lt_cv_file_magic_cmd=/usr/bin/file + lt_cv_file_magic_test_file=/usr/lib/libnls.so + ;; + +*nto* | *qnx*) + lt_cv_deplibs_check_method=pass_all + ;; + +openbsd* | bitrig*) + if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then + lt_cv_deplibs_check_method='match_pattern /lib[[^/]]+(\.so\.[[0-9]]+\.[[0-9]]+|\.so|_pic\.a)$' + else + lt_cv_deplibs_check_method='match_pattern /lib[[^/]]+(\.so\.[[0-9]]+\.[[0-9]]+|_pic\.a)$' + fi + ;; + +osf3* | osf4* | osf5*) + lt_cv_deplibs_check_method=pass_all + ;; + +rdos*) + lt_cv_deplibs_check_method=pass_all + ;; + +solaris*) + lt_cv_deplibs_check_method=pass_all + ;; + +sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX* | sysv4*uw2*) + lt_cv_deplibs_check_method=pass_all + ;; + +sysv4 | sysv4.3*) + case $host_vendor in + motorola) + lt_cv_deplibs_check_method='file_magic ELF [[0-9]][[0-9]]*-bit [[ML]]SB (shared object|dynamic lib) M[[0-9]][[0-9]]* Version [[0-9]]' + lt_cv_file_magic_test_file=`echo /usr/lib/libc.so*` + ;; + ncr) + lt_cv_deplibs_check_method=pass_all + ;; + sequent) + lt_cv_file_magic_cmd='/bin/file' + lt_cv_deplibs_check_method='file_magic ELF [[0-9]][[0-9]]*-bit [[LM]]SB (shared object|dynamic lib )' + ;; + sni) + lt_cv_file_magic_cmd='/bin/file' + lt_cv_deplibs_check_method="file_magic ELF [[0-9]][[0-9]]*-bit [[LM]]SB dynamic lib" + lt_cv_file_magic_test_file=/lib/libc.so + ;; + siemens) + lt_cv_deplibs_check_method=pass_all + ;; + pc) + lt_cv_deplibs_check_method=pass_all + ;; + esac + ;; + +tpf*) + lt_cv_deplibs_check_method=pass_all + ;; +os2*) + lt_cv_deplibs_check_method=pass_all + ;; +esac +]) + +file_magic_glob= +want_nocaseglob=no +if test "$build" = "$host"; then + case $host_os in + mingw* | pw32*) + if ( shopt | grep nocaseglob ) >/dev/null 2>&1; then + want_nocaseglob=yes + else + file_magic_glob=`echo aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ | $SED -e "s/\(..\)/s\/[[\1]]\/[[\1]]\/g;/g"` + fi + ;; + esac +fi + +file_magic_cmd=$lt_cv_file_magic_cmd +deplibs_check_method=$lt_cv_deplibs_check_method +test -z "$deplibs_check_method" && deplibs_check_method=unknown + +_LT_DECL([], [deplibs_check_method], [1], + [Method to check whether dependent libraries are shared objects]) +_LT_DECL([], [file_magic_cmd], [1], + [Command to use when deplibs_check_method = "file_magic"]) +_LT_DECL([], [file_magic_glob], [1], + [How to find potential files when deplibs_check_method = "file_magic"]) +_LT_DECL([], [want_nocaseglob], [1], + [Find potential files using nocaseglob when deplibs_check_method = "file_magic"]) +])# _LT_CHECK_MAGIC_METHOD + + +# LT_PATH_NM +# ---------- +# find the pathname to a BSD- or MS-compatible name lister +AC_DEFUN([LT_PATH_NM], +[AC_REQUIRE([AC_PROG_CC])dnl +AC_CACHE_CHECK([for BSD- or MS-compatible name lister (nm)], lt_cv_path_NM, +[if test -n "$NM"; then + # Let the user override the test. + lt_cv_path_NM=$NM +else + lt_nm_to_check=${ac_tool_prefix}nm + if test -n "$ac_tool_prefix" && test "$build" = "$host"; then + lt_nm_to_check="$lt_nm_to_check nm" + fi + for lt_tmp_nm in $lt_nm_to_check; do + lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR + for ac_dir in $PATH /usr/ccs/bin/elf /usr/ccs/bin /usr/ucb /bin; do + IFS=$lt_save_ifs + test -z "$ac_dir" && ac_dir=. + tmp_nm=$ac_dir/$lt_tmp_nm + if test -f "$tmp_nm" || test -f "$tmp_nm$ac_exeext"; then + # Check to see if the nm accepts a BSD-compat flag. + # Adding the 'sed 1q' prevents false positives on HP-UX, which says: + # nm: unknown option "B" ignored + # Tru64's nm complains that /dev/null is an invalid object file + # MSYS converts /dev/null to NUL, MinGW nm treats NUL as empty + case $build_os in + mingw*) lt_bad_file=conftest.nm/nofile ;; + *) lt_bad_file=/dev/null ;; + esac + case `"$tmp_nm" -B $lt_bad_file 2>&1 | sed '1q'` in + *$lt_bad_file* | *'Invalid file or object type'*) + lt_cv_path_NM="$tmp_nm -B" + break 2 + ;; + *) + case `"$tmp_nm" -p /dev/null 2>&1 | sed '1q'` in + */dev/null*) + lt_cv_path_NM="$tmp_nm -p" + break 2 + ;; + *) + lt_cv_path_NM=${lt_cv_path_NM="$tmp_nm"} # keep the first match, but + continue # so that we can try to find one that supports BSD flags + ;; + esac + ;; + esac + fi + done + IFS=$lt_save_ifs + done + : ${lt_cv_path_NM=no} +fi]) +if test no != "$lt_cv_path_NM"; then + NM=$lt_cv_path_NM +else + # Didn't find any BSD compatible name lister, look for dumpbin. + if test -n "$DUMPBIN"; then : + # Let the user override the test. + else + AC_CHECK_TOOLS(DUMPBIN, [dumpbin "link -dump"], :) + case `$DUMPBIN -symbols -headers /dev/null 2>&1 | sed '1q'` in + *COFF*) + DUMPBIN="$DUMPBIN -symbols -headers" + ;; + *) + DUMPBIN=: + ;; + esac + fi + AC_SUBST([DUMPBIN]) + if test : != "$DUMPBIN"; then + NM=$DUMPBIN + fi +fi +test -z "$NM" && NM=nm +AC_SUBST([NM]) +_LT_DECL([], [NM], [1], [A BSD- or MS-compatible name lister])dnl + +AC_CACHE_CHECK([the name lister ($NM) interface], [lt_cv_nm_interface], + [lt_cv_nm_interface="BSD nm" + echo "int some_variable = 0;" > conftest.$ac_ext + (eval echo "\"\$as_me:$LINENO: $ac_compile\"" >&AS_MESSAGE_LOG_FD) + (eval "$ac_compile" 2>conftest.err) + cat conftest.err >&AS_MESSAGE_LOG_FD + (eval echo "\"\$as_me:$LINENO: $NM \\\"conftest.$ac_objext\\\"\"" >&AS_MESSAGE_LOG_FD) + (eval "$NM \"conftest.$ac_objext\"" 2>conftest.err > conftest.out) + cat conftest.err >&AS_MESSAGE_LOG_FD + (eval echo "\"\$as_me:$LINENO: output\"" >&AS_MESSAGE_LOG_FD) + cat conftest.out >&AS_MESSAGE_LOG_FD + if $GREP 'External.*some_variable' conftest.out > /dev/null; then + lt_cv_nm_interface="MS dumpbin" + fi + rm -f conftest*]) +])# LT_PATH_NM + +# Old names: +AU_ALIAS([AM_PROG_NM], [LT_PATH_NM]) +AU_ALIAS([AC_PROG_NM], [LT_PATH_NM]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AM_PROG_NM], []) +dnl AC_DEFUN([AC_PROG_NM], []) + +# _LT_CHECK_SHAREDLIB_FROM_LINKLIB +# -------------------------------- +# how to determine the name of the shared library +# associated with a specific link library. +# -- PORTME fill in with the dynamic library characteristics +m4_defun([_LT_CHECK_SHAREDLIB_FROM_LINKLIB], +[m4_require([_LT_DECL_EGREP]) +m4_require([_LT_DECL_OBJDUMP]) +m4_require([_LT_DECL_DLLTOOL]) +AC_CACHE_CHECK([how to associate runtime and link libraries], +lt_cv_sharedlib_from_linklib_cmd, +[lt_cv_sharedlib_from_linklib_cmd='unknown' + +case $host_os in +cygwin* | mingw* | pw32* | cegcc*) + # two different shell functions defined in ltmain.sh; + # decide which one to use based on capabilities of $DLLTOOL + case `$DLLTOOL --help 2>&1` in + *--identify-strict*) + lt_cv_sharedlib_from_linklib_cmd=func_cygming_dll_for_implib + ;; + *) + lt_cv_sharedlib_from_linklib_cmd=func_cygming_dll_for_implib_fallback + ;; + esac + ;; +*) + # fallback: assume linklib IS sharedlib + lt_cv_sharedlib_from_linklib_cmd=$ECHO + ;; +esac +]) +sharedlib_from_linklib_cmd=$lt_cv_sharedlib_from_linklib_cmd +test -z "$sharedlib_from_linklib_cmd" && sharedlib_from_linklib_cmd=$ECHO + +_LT_DECL([], [sharedlib_from_linklib_cmd], [1], + [Command to associate shared and link libraries]) +])# _LT_CHECK_SHAREDLIB_FROM_LINKLIB + + +# _LT_PATH_MANIFEST_TOOL +# ---------------------- +# locate the manifest tool +m4_defun([_LT_PATH_MANIFEST_TOOL], +[AC_CHECK_TOOL(MANIFEST_TOOL, mt, :) +test -z "$MANIFEST_TOOL" && MANIFEST_TOOL=mt +AC_CACHE_CHECK([if $MANIFEST_TOOL is a manifest tool], [lt_cv_path_mainfest_tool], + [lt_cv_path_mainfest_tool=no + echo "$as_me:$LINENO: $MANIFEST_TOOL '-?'" >&AS_MESSAGE_LOG_FD + $MANIFEST_TOOL '-?' 2>conftest.err > conftest.out + cat conftest.err >&AS_MESSAGE_LOG_FD + if $GREP 'Manifest Tool' conftest.out > /dev/null; then + lt_cv_path_mainfest_tool=yes + fi + rm -f conftest*]) +if test yes != "$lt_cv_path_mainfest_tool"; then + MANIFEST_TOOL=: +fi +_LT_DECL([], [MANIFEST_TOOL], [1], [Manifest tool])dnl +])# _LT_PATH_MANIFEST_TOOL + + +# _LT_DLL_DEF_P([FILE]) +# --------------------- +# True iff FILE is a Windows DLL '.def' file. +# Keep in sync with func_dll_def_p in the libtool script +AC_DEFUN([_LT_DLL_DEF_P], +[dnl + test DEF = "`$SED -n dnl + -e '\''s/^[[ ]]*//'\'' dnl Strip leading whitespace + -e '\''/^\(;.*\)*$/d'\'' dnl Delete empty lines and comments + -e '\''s/^\(EXPORTS\|LIBRARY\)\([[ ]].*\)*$/DEF/p'\'' dnl + -e q dnl Only consider the first "real" line + $1`" dnl +])# _LT_DLL_DEF_P + + +# LT_LIB_M +# -------- +# check for math library +AC_DEFUN([LT_LIB_M], +[AC_REQUIRE([AC_CANONICAL_HOST])dnl +LIBM= +case $host in +*-*-beos* | *-*-cegcc* | *-*-cygwin* | *-*-haiku* | *-*-pw32* | *-*-darwin*) + # These system don't have libm, or don't need it + ;; +*-ncr-sysv4.3*) + AC_CHECK_LIB(mw, _mwvalidcheckl, LIBM=-lmw) + AC_CHECK_LIB(m, cos, LIBM="$LIBM -lm") + ;; +*) + AC_CHECK_LIB(m, cos, LIBM=-lm) + ;; +esac +AC_SUBST([LIBM]) +])# LT_LIB_M + +# Old name: +AU_ALIAS([AC_CHECK_LIBM], [LT_LIB_M]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_CHECK_LIBM], []) + + +# _LT_COMPILER_NO_RTTI([TAGNAME]) +# ------------------------------- +m4_defun([_LT_COMPILER_NO_RTTI], +[m4_require([_LT_TAG_COMPILER])dnl + +_LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)= + +if test yes = "$GCC"; then + case $cc_basename in + nvcc*) + _LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)=' -Xcompiler -fno-builtin' ;; + *) + _LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)=' -fno-builtin' ;; + esac + + _LT_COMPILER_OPTION([if $compiler supports -fno-rtti -fno-exceptions], + lt_cv_prog_compiler_rtti_exceptions, + [-fno-rtti -fno-exceptions], [], + [_LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)="$_LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1) -fno-rtti -fno-exceptions"]) +fi +_LT_TAGDECL([no_builtin_flag], [lt_prog_compiler_no_builtin_flag], [1], + [Compiler flag to turn off builtin functions]) +])# _LT_COMPILER_NO_RTTI + + +# _LT_CMD_GLOBAL_SYMBOLS +# ---------------------- +m4_defun([_LT_CMD_GLOBAL_SYMBOLS], +[AC_REQUIRE([AC_CANONICAL_HOST])dnl +AC_REQUIRE([AC_PROG_CC])dnl +AC_REQUIRE([AC_PROG_AWK])dnl +AC_REQUIRE([LT_PATH_NM])dnl +AC_REQUIRE([LT_PATH_LD])dnl +m4_require([_LT_DECL_SED])dnl +m4_require([_LT_DECL_EGREP])dnl +m4_require([_LT_TAG_COMPILER])dnl + +# Check for command to grab the raw symbol name followed by C symbol from nm. +AC_MSG_CHECKING([command to parse $NM output from $compiler object]) +AC_CACHE_VAL([lt_cv_sys_global_symbol_pipe], +[ +# These are sane defaults that work on at least a few old systems. +# [They come from Ultrix. What could be older than Ultrix?!! ;)] + +# Character class describing NM global symbol codes. +symcode='[[BCDEGRST]]' + +# Regexp to match symbols that can be accessed directly from C. +sympat='\([[_A-Za-z]][[_A-Za-z0-9]]*\)' + +# Define system-specific variables. +case $host_os in +aix*) + symcode='[[BCDT]]' + ;; +cygwin* | mingw* | pw32* | cegcc*) + symcode='[[ABCDGISTW]]' + ;; +hpux*) + if test ia64 = "$host_cpu"; then + symcode='[[ABCDEGRST]]' + fi + ;; +irix* | nonstopux*) + symcode='[[BCDEGRST]]' + ;; +osf*) + symcode='[[BCDEGQRST]]' + ;; +solaris*) + symcode='[[BDRT]]' + ;; +sco3.2v5*) + symcode='[[DT]]' + ;; +sysv4.2uw2*) + symcode='[[DT]]' + ;; +sysv5* | sco5v6* | unixware* | OpenUNIX*) + symcode='[[ABDT]]' + ;; +sysv4) + symcode='[[DFNSTU]]' + ;; +esac + +# If we're using GNU nm, then use its standard symbol codes. +case `$NM -V 2>&1` in +*GNU* | *'with BFD'*) + symcode='[[ABCDGIRSTW]]' ;; +esac + +if test "$lt_cv_nm_interface" = "MS dumpbin"; then + # Gets list of data symbols to import. + lt_cv_sys_global_symbol_to_import="sed -n -e 's/^I .* \(.*\)$/\1/p'" + # Adjust the below global symbol transforms to fixup imported variables. + lt_cdecl_hook=" -e 's/^I .* \(.*\)$/extern __declspec(dllimport) char \1;/p'" + lt_c_name_hook=" -e 's/^I .* \(.*\)$/ {\"\1\", (void *) 0},/p'" + lt_c_name_lib_hook="\ + -e 's/^I .* \(lib.*\)$/ {\"\1\", (void *) 0},/p'\ + -e 's/^I .* \(.*\)$/ {\"lib\1\", (void *) 0},/p'" +else + # Disable hooks by default. + lt_cv_sys_global_symbol_to_import= + lt_cdecl_hook= + lt_c_name_hook= + lt_c_name_lib_hook= +fi + +# Transform an extracted symbol line into a proper C declaration. +# Some systems (esp. on ia64) link data and code symbols differently, +# so use this general approach. +lt_cv_sys_global_symbol_to_cdecl="sed -n"\ +$lt_cdecl_hook\ +" -e 's/^T .* \(.*\)$/extern int \1();/p'"\ +" -e 's/^$symcode$symcode* .* \(.*\)$/extern char \1;/p'" + +# Transform an extracted symbol line into symbol name and symbol address +lt_cv_sys_global_symbol_to_c_name_address="sed -n"\ +$lt_c_name_hook\ +" -e 's/^: \(.*\) .*$/ {\"\1\", (void *) 0},/p'"\ +" -e 's/^$symcode$symcode* .* \(.*\)$/ {\"\1\", (void *) \&\1},/p'" + +# Transform an extracted symbol line into symbol name with lib prefix and +# symbol address. +lt_cv_sys_global_symbol_to_c_name_address_lib_prefix="sed -n"\ +$lt_c_name_lib_hook\ +" -e 's/^: \(.*\) .*$/ {\"\1\", (void *) 0},/p'"\ +" -e 's/^$symcode$symcode* .* \(lib.*\)$/ {\"\1\", (void *) \&\1},/p'"\ +" -e 's/^$symcode$symcode* .* \(.*\)$/ {\"lib\1\", (void *) \&\1},/p'" + +# Handle CRLF in mingw tool chain +opt_cr= +case $build_os in +mingw*) + opt_cr=`$ECHO 'x\{0,1\}' | tr x '\015'` # option cr in regexp + ;; +esac + +# Try without a prefix underscore, then with it. +for ac_symprfx in "" "_"; do + + # Transform symcode, sympat, and symprfx into a raw symbol and a C symbol. + symxfrm="\\1 $ac_symprfx\\2 \\2" + + # Write the raw and C identifiers. + if test "$lt_cv_nm_interface" = "MS dumpbin"; then + # Fake it for dumpbin and say T for any non-static function, + # D for any global variable and I for any imported variable. + # Also find C++ and __fastcall symbols from MSVC++, + # which start with @ or ?. + lt_cv_sys_global_symbol_pipe="$AWK ['"\ +" {last_section=section; section=\$ 3};"\ +" /^COFF SYMBOL TABLE/{for(i in hide) delete hide[i]};"\ +" /Section length .*#relocs.*(pick any)/{hide[last_section]=1};"\ +" /^ *Symbol name *: /{split(\$ 0,sn,\":\"); si=substr(sn[2],2)};"\ +" /^ *Type *: code/{print \"T\",si,substr(si,length(prfx))};"\ +" /^ *Type *: data/{print \"I\",si,substr(si,length(prfx))};"\ +" \$ 0!~/External *\|/{next};"\ +" / 0+ UNDEF /{next}; / UNDEF \([^|]\)*()/{next};"\ +" {if(hide[section]) next};"\ +" {f=\"D\"}; \$ 0~/\(\).*\|/{f=\"T\"};"\ +" {split(\$ 0,a,/\||\r/); split(a[2],s)};"\ +" s[1]~/^[@?]/{print f,s[1],s[1]; next};"\ +" s[1]~prfx {split(s[1],t,\"@\"); print f,t[1],substr(t[1],length(prfx))}"\ +" ' prfx=^$ac_symprfx]" + else + lt_cv_sys_global_symbol_pipe="sed -n -e 's/^.*[[ ]]\($symcode$symcode*\)[[ ]][[ ]]*$ac_symprfx$sympat$opt_cr$/$symxfrm/p'" + fi + lt_cv_sys_global_symbol_pipe="$lt_cv_sys_global_symbol_pipe | sed '/ __gnu_lto/d'" + + # Check to see that the pipe works correctly. + pipe_works=no + + rm -f conftest* + cat > conftest.$ac_ext <<_LT_EOF +#ifdef __cplusplus +extern "C" { +#endif +char nm_test_var; +void nm_test_func(void); +void nm_test_func(void){} +#ifdef __cplusplus +} +#endif +int main(){nm_test_var='a';nm_test_func();return(0);} +_LT_EOF + + if AC_TRY_EVAL(ac_compile); then + # Now try to grab the symbols. + nlist=conftest.nm + $ECHO "$as_me:$LINENO: $NM conftest.$ac_objext | $lt_cv_sys_global_symbol_pipe > $nlist" >&AS_MESSAGE_LOG_FD + if eval "$NM" conftest.$ac_objext \| "$lt_cv_sys_global_symbol_pipe" \> $nlist 2>&AS_MESSAGE_LOG_FD && test -s "$nlist"; then + # Try sorting and uniquifying the output. + if sort "$nlist" | uniq > "$nlist"T; then + mv -f "$nlist"T "$nlist" + else + rm -f "$nlist"T + fi + + # Make sure that we snagged all the symbols we need. + if $GREP ' nm_test_var$' "$nlist" >/dev/null; then + if $GREP ' nm_test_func$' "$nlist" >/dev/null; then + cat <<_LT_EOF > conftest.$ac_ext +/* Keep this code in sync between libtool.m4, ltmain, lt_system.h, and tests. */ +#if defined _WIN32 || defined __CYGWIN__ || defined _WIN32_WCE +/* DATA imports from DLLs on WIN32 can't be const, because runtime + relocations are performed -- see ld's documentation on pseudo-relocs. */ +# define LT@&t@_DLSYM_CONST +#elif defined __osf__ +/* This system does not cope well with relocations in const data. */ +# define LT@&t@_DLSYM_CONST +#else +# define LT@&t@_DLSYM_CONST const +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +_LT_EOF + # Now generate the symbol file. + eval "$lt_cv_sys_global_symbol_to_cdecl"' < "$nlist" | $GREP -v main >> conftest.$ac_ext' + + cat <<_LT_EOF >> conftest.$ac_ext + +/* The mapping between symbol names and symbols. */ +LT@&t@_DLSYM_CONST struct { + const char *name; + void *address; +} +lt__PROGRAM__LTX_preloaded_symbols[[]] = +{ + { "@PROGRAM@", (void *) 0 }, +_LT_EOF + $SED "s/^$symcode$symcode* .* \(.*\)$/ {\"\1\", (void *) \&\1},/" < "$nlist" | $GREP -v main >> conftest.$ac_ext + cat <<\_LT_EOF >> conftest.$ac_ext + {0, (void *) 0} +}; + +/* This works around a problem in FreeBSD linker */ +#ifdef FREEBSD_WORKAROUND +static const void *lt_preloaded_setup() { + return lt__PROGRAM__LTX_preloaded_symbols; +} +#endif + +#ifdef __cplusplus +} +#endif +_LT_EOF + # Now try linking the two files. + mv conftest.$ac_objext conftstm.$ac_objext + lt_globsym_save_LIBS=$LIBS + lt_globsym_save_CFLAGS=$CFLAGS + LIBS=conftstm.$ac_objext + CFLAGS="$CFLAGS$_LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)" + if AC_TRY_EVAL(ac_link) && test -s conftest$ac_exeext; then + pipe_works=yes + fi + LIBS=$lt_globsym_save_LIBS + CFLAGS=$lt_globsym_save_CFLAGS + else + echo "cannot find nm_test_func in $nlist" >&AS_MESSAGE_LOG_FD + fi + else + echo "cannot find nm_test_var in $nlist" >&AS_MESSAGE_LOG_FD + fi + else + echo "cannot run $lt_cv_sys_global_symbol_pipe" >&AS_MESSAGE_LOG_FD + fi + else + echo "$progname: failed program was:" >&AS_MESSAGE_LOG_FD + cat conftest.$ac_ext >&5 + fi + rm -rf conftest* conftst* + + # Do not use the global_symbol_pipe unless it works. + if test yes = "$pipe_works"; then + break + else + lt_cv_sys_global_symbol_pipe= + fi +done +]) +if test -z "$lt_cv_sys_global_symbol_pipe"; then + lt_cv_sys_global_symbol_to_cdecl= +fi +if test -z "$lt_cv_sys_global_symbol_pipe$lt_cv_sys_global_symbol_to_cdecl"; then + AC_MSG_RESULT(failed) +else + AC_MSG_RESULT(ok) +fi + +# Response file support. +if test "$lt_cv_nm_interface" = "MS dumpbin"; then + nm_file_list_spec='@' +elif $NM --help 2>/dev/null | grep '[[@]]FILE' >/dev/null; then + nm_file_list_spec='@' +fi + +_LT_DECL([global_symbol_pipe], [lt_cv_sys_global_symbol_pipe], [1], + [Take the output of nm and produce a listing of raw symbols and C names]) +_LT_DECL([global_symbol_to_cdecl], [lt_cv_sys_global_symbol_to_cdecl], [1], + [Transform the output of nm in a proper C declaration]) +_LT_DECL([global_symbol_to_import], [lt_cv_sys_global_symbol_to_import], [1], + [Transform the output of nm into a list of symbols to manually relocate]) +_LT_DECL([global_symbol_to_c_name_address], + [lt_cv_sys_global_symbol_to_c_name_address], [1], + [Transform the output of nm in a C name address pair]) +_LT_DECL([global_symbol_to_c_name_address_lib_prefix], + [lt_cv_sys_global_symbol_to_c_name_address_lib_prefix], [1], + [Transform the output of nm in a C name address pair when lib prefix is needed]) +_LT_DECL([nm_interface], [lt_cv_nm_interface], [1], + [The name lister interface]) +_LT_DECL([], [nm_file_list_spec], [1], + [Specify filename containing input files for $NM]) +]) # _LT_CMD_GLOBAL_SYMBOLS + + +# _LT_COMPILER_PIC([TAGNAME]) +# --------------------------- +m4_defun([_LT_COMPILER_PIC], +[m4_require([_LT_TAG_COMPILER])dnl +_LT_TAGVAR(lt_prog_compiler_wl, $1)= +_LT_TAGVAR(lt_prog_compiler_pic, $1)= +_LT_TAGVAR(lt_prog_compiler_static, $1)= + +m4_if([$1], [CXX], [ + # C++ specific cases for pic, static, wl, etc. + if test yes = "$GXX"; then + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-static' + + case $host_os in + aix*) + # All AIX code is PIC. + if test ia64 = "$host_cpu"; then + # AIX 5 now supports IA64 processor + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + fi + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + ;; + + amigaos*) + case $host_cpu in + powerpc) + # see comment about AmigaOS4 .so support + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + ;; + m68k) + # FIXME: we need at least 68020 code to build shared libraries, but + # adding the '-m68020' flag to GCC prevents building anything better, + # like '-m68040'. + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-m68020 -resident32 -malways-restore-a4' + ;; + esac + ;; + + beos* | irix5* | irix6* | nonstopux* | osf3* | osf4* | osf5*) + # PIC is the default for these OSes. + ;; + mingw* | cygwin* | os2* | pw32* | cegcc*) + # This hack is so that the source file can tell whether it is being + # built for inclusion in a dll (and should export symbols for example). + # Although the cygwin gcc ignores -fPIC, still need this for old-style + # (--disable-auto-import) libraries + m4_if([$1], [GCJ], [], + [_LT_TAGVAR(lt_prog_compiler_pic, $1)='-DDLL_EXPORT']) + case $host_os in + os2*) + _LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-static' + ;; + esac + ;; + darwin* | rhapsody*) + # PIC is the default on this platform + # Common symbols not allowed in MH_DYLIB files + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fno-common' + ;; + *djgpp*) + # DJGPP does not support shared libraries at all + _LT_TAGVAR(lt_prog_compiler_pic, $1)= + ;; + haiku*) + # PIC is the default for Haiku. + # The "-static" flag exists, but is broken. + _LT_TAGVAR(lt_prog_compiler_static, $1)= + ;; + interix[[3-9]]*) + # Interix 3.x gcc -fpic/-fPIC options generate broken code. + # Instead, we relocate shared libraries at runtime. + ;; + sysv4*MP*) + if test -d /usr/nec; then + _LT_TAGVAR(lt_prog_compiler_pic, $1)=-Kconform_pic + fi + ;; + hpux*) + # PIC is the default for 64-bit PA HP-UX, but not for 32-bit + # PA HP-UX. On IA64 HP-UX, PIC is the default but the pic flag + # sets the default TLS model and affects inlining. + case $host_cpu in + hppa*64*) + ;; + *) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + ;; + esac + ;; + *qnx* | *nto*) + # QNX uses GNU C++, but need to define -shared option too, otherwise + # it will coredump. + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC -shared' + ;; + *) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + ;; + esac + else + case $host_os in + aix[[4-9]]*) + # All AIX code is PIC. + if test ia64 = "$host_cpu"; then + # AIX 5 now supports IA64 processor + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + else + _LT_TAGVAR(lt_prog_compiler_static, $1)='-bnso -bI:/lib/syscalls.exp' + fi + ;; + chorus*) + case $cc_basename in + cxch68*) + # Green Hills C++ Compiler + # _LT_TAGVAR(lt_prog_compiler_static, $1)="--no_auto_instantiation -u __main -u __premain -u _abort -r $COOL_DIR/lib/libOrb.a $MVME_DIR/lib/CC/libC.a $MVME_DIR/lib/classix/libcx.s.a" + ;; + esac + ;; + mingw* | cygwin* | os2* | pw32* | cegcc*) + # This hack is so that the source file can tell whether it is being + # built for inclusion in a dll (and should export symbols for example). + m4_if([$1], [GCJ], [], + [_LT_TAGVAR(lt_prog_compiler_pic, $1)='-DDLL_EXPORT']) + ;; + dgux*) + case $cc_basename in + ec++*) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + ;; + ghcx*) + # Green Hills C++ Compiler + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-pic' + ;; + *) + ;; + esac + ;; + freebsd* | dragonfly*) + # FreeBSD uses GNU C++ + ;; + hpux9* | hpux10* | hpux11*) + case $cc_basename in + CC*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-a ${wl}archive' + if test ia64 != "$host_cpu"; then + _LT_TAGVAR(lt_prog_compiler_pic, $1)='+Z' + fi + ;; + aCC*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-a ${wl}archive' + case $host_cpu in + hppa*64*|ia64*) + # +Z the default + ;; + *) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='+Z' + ;; + esac + ;; + *) + ;; + esac + ;; + interix*) + # This is c89, which is MS Visual C++ (no shared libs) + # Anyone wants to do a port? + ;; + irix5* | irix6* | nonstopux*) + case $cc_basename in + CC*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared' + # CC pic flag -KPIC is the default. + ;; + *) + ;; + esac + ;; + linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*) + case $cc_basename in + KCC*) + # KAI C++ Compiler + _LT_TAGVAR(lt_prog_compiler_wl, $1)='--backend -Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + ;; + ecpc* ) + # old Intel C++ for x86_64, which still supported -KPIC. + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-static' + ;; + icpc* ) + # Intel C++, used to be incompatible with GCC. + # ICC 10 doesn't accept -KPIC any more. + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-static' + ;; + pgCC* | pgcpp*) + # Portland Group C++ compiler + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fpic' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + cxx*) + # Compaq C++ + # Make sure the PIC flag is empty. It appears that all Alpha + # Linux and Compaq Tru64 Unix objects are PIC. + _LT_TAGVAR(lt_prog_compiler_pic, $1)= + _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared' + ;; + xlc* | xlC* | bgxl[[cC]]* | mpixl[[cC]]*) + # IBM XL 8.0, 9.0 on PPC and BlueGene + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-qpic' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-qstaticlink' + ;; + *) + case `$CC -V 2>&1 | sed 5q` in + *Sun\ C*) + # Sun C++ 5.9 + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Qoption ld ' + ;; + esac + ;; + esac + ;; + lynxos*) + ;; + m88k*) + ;; + mvs*) + case $cc_basename in + cxx*) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-W c,exportall' + ;; + *) + ;; + esac + ;; + netbsd* | netbsdelf*-gnu) + ;; + *qnx* | *nto*) + # QNX uses GNU C++, but need to define -shared option too, otherwise + # it will coredump. + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC -shared' + ;; + osf3* | osf4* | osf5*) + case $cc_basename in + KCC*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='--backend -Wl,' + ;; + RCC*) + # Rational C++ 2.4.1 + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-pic' + ;; + cxx*) + # Digital/Compaq C++ + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + # Make sure the PIC flag is empty. It appears that all Alpha + # Linux and Compaq Tru64 Unix objects are PIC. + _LT_TAGVAR(lt_prog_compiler_pic, $1)= + _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared' + ;; + *) + ;; + esac + ;; + psos*) + ;; + solaris*) + case $cc_basename in + CC* | sunCC*) + # Sun C++ 4.2, 5.x and Centerline C++ + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Qoption ld ' + ;; + gcx*) + # Green Hills C++ Compiler + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-PIC' + ;; + *) + ;; + esac + ;; + sunos4*) + case $cc_basename in + CC*) + # Sun C++ 4.x + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-pic' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + lcc*) + # Lucid + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-pic' + ;; + *) + ;; + esac + ;; + sysv5* | unixware* | sco3.2v5* | sco5v6* | OpenUNIX*) + case $cc_basename in + CC*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + esac + ;; + tandem*) + case $cc_basename in + NCC*) + # NonStop-UX NCC 3.20 + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + ;; + *) + ;; + esac + ;; + vxworks*) + ;; + *) + _LT_TAGVAR(lt_prog_compiler_can_build_shared, $1)=no + ;; + esac + fi +], +[ + if test yes = "$GCC"; then + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-static' + + case $host_os in + aix*) + # All AIX code is PIC. + if test ia64 = "$host_cpu"; then + # AIX 5 now supports IA64 processor + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + fi + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + ;; + + amigaos*) + case $host_cpu in + powerpc) + # see comment about AmigaOS4 .so support + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + ;; + m68k) + # FIXME: we need at least 68020 code to build shared libraries, but + # adding the '-m68020' flag to GCC prevents building anything better, + # like '-m68040'. + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-m68020 -resident32 -malways-restore-a4' + ;; + esac + ;; + + beos* | irix5* | irix6* | nonstopux* | osf3* | osf4* | osf5*) + # PIC is the default for these OSes. + ;; + + mingw* | cygwin* | pw32* | os2* | cegcc*) + # This hack is so that the source file can tell whether it is being + # built for inclusion in a dll (and should export symbols for example). + # Although the cygwin gcc ignores -fPIC, still need this for old-style + # (--disable-auto-import) libraries + m4_if([$1], [GCJ], [], + [_LT_TAGVAR(lt_prog_compiler_pic, $1)='-DDLL_EXPORT']) + case $host_os in + os2*) + _LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-static' + ;; + esac + ;; + + darwin* | rhapsody*) + # PIC is the default on this platform + # Common symbols not allowed in MH_DYLIB files + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fno-common' + ;; + + haiku*) + # PIC is the default for Haiku. + # The "-static" flag exists, but is broken. + _LT_TAGVAR(lt_prog_compiler_static, $1)= + ;; + + hpux*) + # PIC is the default for 64-bit PA HP-UX, but not for 32-bit + # PA HP-UX. On IA64 HP-UX, PIC is the default but the pic flag + # sets the default TLS model and affects inlining. + case $host_cpu in + hppa*64*) + # +Z the default + ;; + *) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + ;; + esac + ;; + + interix[[3-9]]*) + # Interix 3.x gcc -fpic/-fPIC options generate broken code. + # Instead, we relocate shared libraries at runtime. + ;; + + msdosdjgpp*) + # Just because we use GCC doesn't mean we suddenly get shared libraries + # on systems that don't support them. + _LT_TAGVAR(lt_prog_compiler_can_build_shared, $1)=no + enable_shared=no + ;; + + *nto* | *qnx*) + # QNX uses GNU C++, but need to define -shared option too, otherwise + # it will coredump. + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC -shared' + ;; + + sysv4*MP*) + if test -d /usr/nec; then + _LT_TAGVAR(lt_prog_compiler_pic, $1)=-Kconform_pic + fi + ;; + + *) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + ;; + esac + + case $cc_basename in + nvcc*) # Cuda Compiler Driver 2.2 + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Xlinker ' + if test -n "$_LT_TAGVAR(lt_prog_compiler_pic, $1)"; then + _LT_TAGVAR(lt_prog_compiler_pic, $1)="-Xcompiler $_LT_TAGVAR(lt_prog_compiler_pic, $1)" + fi + ;; + esac + else + # PORTME Check for flag to pass linker flags through the system compiler. + case $host_os in + aix*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + if test ia64 = "$host_cpu"; then + # AIX 5 now supports IA64 processor + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + else + _LT_TAGVAR(lt_prog_compiler_static, $1)='-bnso -bI:/lib/syscalls.exp' + fi + ;; + + darwin* | rhapsody*) + # PIC is the default on this platform + # Common symbols not allowed in MH_DYLIB files + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fno-common' + case $cc_basename in + nagfor*) + # NAG Fortran compiler + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,-Wl,,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-PIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + esac + ;; + + mingw* | cygwin* | pw32* | os2* | cegcc*) + # This hack is so that the source file can tell whether it is being + # built for inclusion in a dll (and should export symbols for example). + m4_if([$1], [GCJ], [], + [_LT_TAGVAR(lt_prog_compiler_pic, $1)='-DDLL_EXPORT']) + case $host_os in + os2*) + _LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-static' + ;; + esac + ;; + + hpux9* | hpux10* | hpux11*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + # PIC is the default for IA64 HP-UX and 64-bit HP-UX, but + # not for PA HP-UX. + case $host_cpu in + hppa*64*|ia64*) + # +Z the default + ;; + *) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='+Z' + ;; + esac + # Is there a better lt_prog_compiler_static that works with the bundled CC? + _LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-a ${wl}archive' + ;; + + irix5* | irix6* | nonstopux*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + # PIC (with -KPIC) is the default. + _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared' + ;; + + linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*) + case $cc_basename in + # old Intel for x86_64, which still supported -KPIC. + ecc*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-static' + ;; + # icc used to be incompatible with GCC. + # ICC 10 doesn't accept -KPIC any more. + icc* | ifort*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-static' + ;; + # Lahey Fortran 8.1. + lf95*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='--shared' + _LT_TAGVAR(lt_prog_compiler_static, $1)='--static' + ;; + nagfor*) + # NAG Fortran compiler + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,-Wl,,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-PIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + tcc*) + # Fabrice Bellard et al's Tiny C Compiler + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-static' + ;; + pgcc* | pgf77* | pgf90* | pgf95* | pgfortran*) + # Portland Group compilers (*not* the Pentium gcc compiler, + # which looks to be a dead project) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fpic' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + ccc*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + # All Alpha code is PIC. + _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared' + ;; + xl* | bgxl* | bgf* | mpixl*) + # IBM XL C 8.0/Fortran 10.1, 11.1 on PPC and BlueGene + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-qpic' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-qstaticlink' + ;; + *) + case `$CC -V 2>&1 | sed 5q` in + *Sun\ Ceres\ Fortran* | *Sun*Fortran*\ [[1-7]].* | *Sun*Fortran*\ 8.[[0-3]]*) + # Sun Fortran 8.3 passes all unrecognized flags to the linker + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + _LT_TAGVAR(lt_prog_compiler_wl, $1)='' + ;; + *Sun\ F* | *Sun*Fortran*) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Qoption ld ' + ;; + *Sun\ C*) + # Sun C 5.9 + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + ;; + *Intel*\ [[CF]]*Compiler*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-static' + ;; + *Portland\ Group*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fpic' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + esac + ;; + esac + ;; + + newsos6) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + + *nto* | *qnx*) + # QNX uses GNU C++, but need to define -shared option too, otherwise + # it will coredump. + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC -shared' + ;; + + osf3* | osf4* | osf5*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + # All OSF/1 code is PIC. + _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared' + ;; + + rdos*) + _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared' + ;; + + solaris*) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + case $cc_basename in + f77* | f90* | f95* | sunf77* | sunf90* | sunf95*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Qoption ld ';; + *) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,';; + esac + ;; + + sunos4*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Qoption ld ' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-PIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + + sysv4 | sysv4.2uw2* | sysv4.3*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + + sysv4*MP*) + if test -d /usr/nec; then + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-Kconform_pic' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + fi + ;; + + sysv5* | unixware* | sco3.2v5* | sco5v6* | OpenUNIX*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + + unicos*) + _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,' + _LT_TAGVAR(lt_prog_compiler_can_build_shared, $1)=no + ;; + + uts4*) + _LT_TAGVAR(lt_prog_compiler_pic, $1)='-pic' + _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic' + ;; + + *) + _LT_TAGVAR(lt_prog_compiler_can_build_shared, $1)=no + ;; + esac + fi +]) +case $host_os in + # For platforms that do not support PIC, -DPIC is meaningless: + *djgpp*) + _LT_TAGVAR(lt_prog_compiler_pic, $1)= + ;; + *) + _LT_TAGVAR(lt_prog_compiler_pic, $1)="$_LT_TAGVAR(lt_prog_compiler_pic, $1)@&t@m4_if([$1],[],[ -DPIC],[m4_if([$1],[CXX],[ -DPIC],[])])" + ;; +esac + +AC_CACHE_CHECK([for $compiler option to produce PIC], + [_LT_TAGVAR(lt_cv_prog_compiler_pic, $1)], + [_LT_TAGVAR(lt_cv_prog_compiler_pic, $1)=$_LT_TAGVAR(lt_prog_compiler_pic, $1)]) +_LT_TAGVAR(lt_prog_compiler_pic, $1)=$_LT_TAGVAR(lt_cv_prog_compiler_pic, $1) + +# +# Check to make sure the PIC flag actually works. +# +if test -n "$_LT_TAGVAR(lt_prog_compiler_pic, $1)"; then + _LT_COMPILER_OPTION([if $compiler PIC flag $_LT_TAGVAR(lt_prog_compiler_pic, $1) works], + [_LT_TAGVAR(lt_cv_prog_compiler_pic_works, $1)], + [$_LT_TAGVAR(lt_prog_compiler_pic, $1)@&t@m4_if([$1],[],[ -DPIC],[m4_if([$1],[CXX],[ -DPIC],[])])], [], + [case $_LT_TAGVAR(lt_prog_compiler_pic, $1) in + "" | " "*) ;; + *) _LT_TAGVAR(lt_prog_compiler_pic, $1)=" $_LT_TAGVAR(lt_prog_compiler_pic, $1)" ;; + esac], + [_LT_TAGVAR(lt_prog_compiler_pic, $1)= + _LT_TAGVAR(lt_prog_compiler_can_build_shared, $1)=no]) +fi +_LT_TAGDECL([pic_flag], [lt_prog_compiler_pic], [1], + [Additional compiler flags for building library objects]) + +_LT_TAGDECL([wl], [lt_prog_compiler_wl], [1], + [How to pass a linker flag through the compiler]) +# +# Check to make sure the static flag actually works. +# +wl=$_LT_TAGVAR(lt_prog_compiler_wl, $1) eval lt_tmp_static_flag=\"$_LT_TAGVAR(lt_prog_compiler_static, $1)\" +_LT_LINKER_OPTION([if $compiler static flag $lt_tmp_static_flag works], + _LT_TAGVAR(lt_cv_prog_compiler_static_works, $1), + $lt_tmp_static_flag, + [], + [_LT_TAGVAR(lt_prog_compiler_static, $1)=]) +_LT_TAGDECL([link_static_flag], [lt_prog_compiler_static], [1], + [Compiler flag to prevent dynamic linking]) +])# _LT_COMPILER_PIC + + +# _LT_LINKER_SHLIBS([TAGNAME]) +# ---------------------------- +# See if the linker supports building shared libraries. +m4_defun([_LT_LINKER_SHLIBS], +[AC_REQUIRE([LT_PATH_LD])dnl +AC_REQUIRE([LT_PATH_NM])dnl +m4_require([_LT_PATH_MANIFEST_TOOL])dnl +m4_require([_LT_FILEUTILS_DEFAULTS])dnl +m4_require([_LT_DECL_EGREP])dnl +m4_require([_LT_DECL_SED])dnl +m4_require([_LT_CMD_GLOBAL_SYMBOLS])dnl +m4_require([_LT_TAG_COMPILER])dnl +AC_MSG_CHECKING([whether the $compiler linker ($LD) supports shared libraries]) +m4_if([$1], [CXX], [ + _LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED '\''s/.* //'\'' | sort | uniq > $export_symbols' + _LT_TAGVAR(exclude_expsyms, $1)=['_GLOBAL_OFFSET_TABLE_|_GLOBAL__F[ID]_.*'] + case $host_os in + aix[[4-9]]*) + # If we're using GNU nm, then we don't want the "-C" option. + # -C means demangle to GNU nm, but means don't demangle to AIX nm. + # Without the "-l" option, or with the "-B" option, AIX nm treats + # weak defined symbols like other global defined symbols, whereas + # GNU nm marks them as "W". + # While the 'weak' keyword is ignored in the Export File, we need + # it in the Import File for the 'aix-soname' feature, so we have + # to replace the "-B" option with "-P" for AIX nm. + if $NM -V 2>&1 | $GREP 'GNU' > /dev/null; then + _LT_TAGVAR(export_symbols_cmds, $1)='$NM -Bpg $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "W")) && ([substr](\$ 3,1,1) != ".")) { if (\$ 2 == "W") { print \$ 3 " weak" } else { print \$ 3 } } }'\'' | sort -u > $export_symbols' + else + _LT_TAGVAR(export_symbols_cmds, $1)='`func_echo_all $NM | $SED -e '\''s/B\([[^B]]*\)$/P\1/'\''` -PCpgl $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) && ([substr](\$ 1,1,1) != ".")) { if ((\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) { print \$ 1 " weak" } else { print \$ 1 } } }'\'' | sort -u > $export_symbols' + fi + ;; + pw32*) + _LT_TAGVAR(export_symbols_cmds, $1)=$ltdll_cmds + ;; + cygwin* | mingw* | cegcc*) + case $cc_basename in + cl*) + _LT_TAGVAR(exclude_expsyms, $1)='_NULL_IMPORT_DESCRIPTOR|_IMPORT_DESCRIPTOR_.*' + ;; + *) + _LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED -e '\''/^[[BCDGRS]][[ ]]/s/.*[[ ]]\([[^ ]]*\)/\1 DATA/;s/^.*[[ ]]__nm__\([[^ ]]*\)[[ ]][[^ ]]*/\1 DATA/;/^I[[ ]]/d;/^[[AITW]][[ ]]/s/.* //'\'' | sort | uniq > $export_symbols' + _LT_TAGVAR(exclude_expsyms, $1)=['[_]+GLOBAL_OFFSET_TABLE_|[_]+GLOBAL__[FID]_.*|[_]+head_[A-Za-z0-9_]+_dll|[A-Za-z0-9_]+_dll_iname'] + ;; + esac + ;; + linux* | k*bsd*-gnu | gnu*) + _LT_TAGVAR(link_all_deplibs, $1)=no + ;; + *) + _LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED '\''s/.* //'\'' | sort | uniq > $export_symbols' + ;; + esac +], [ + runpath_var= + _LT_TAGVAR(allow_undefined_flag, $1)= + _LT_TAGVAR(always_export_symbols, $1)=no + _LT_TAGVAR(archive_cmds, $1)= + _LT_TAGVAR(archive_expsym_cmds, $1)= + _LT_TAGVAR(compiler_needs_object, $1)=no + _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=no + _LT_TAGVAR(export_dynamic_flag_spec, $1)= + _LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED '\''s/.* //'\'' | sort | uniq > $export_symbols' + _LT_TAGVAR(hardcode_automatic, $1)=no + _LT_TAGVAR(hardcode_direct, $1)=no + _LT_TAGVAR(hardcode_direct_absolute, $1)=no + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)= + _LT_TAGVAR(hardcode_libdir_separator, $1)= + _LT_TAGVAR(hardcode_minus_L, $1)=no + _LT_TAGVAR(hardcode_shlibpath_var, $1)=unsupported + _LT_TAGVAR(inherit_rpath, $1)=no + _LT_TAGVAR(link_all_deplibs, $1)=unknown + _LT_TAGVAR(module_cmds, $1)= + _LT_TAGVAR(module_expsym_cmds, $1)= + _LT_TAGVAR(old_archive_from_new_cmds, $1)= + _LT_TAGVAR(old_archive_from_expsyms_cmds, $1)= + _LT_TAGVAR(thread_safe_flag_spec, $1)= + _LT_TAGVAR(whole_archive_flag_spec, $1)= + # include_expsyms should be a list of space-separated symbols to be *always* + # included in the symbol list + _LT_TAGVAR(include_expsyms, $1)= + # exclude_expsyms can be an extended regexp of symbols to exclude + # it will be wrapped by ' (' and ')$', so one must not match beginning or + # end of line. Example: 'a|bc|.*d.*' will exclude the symbols 'a' and 'bc', + # as well as any symbol that contains 'd'. + _LT_TAGVAR(exclude_expsyms, $1)=['_GLOBAL_OFFSET_TABLE_|_GLOBAL__F[ID]_.*'] + # Although _GLOBAL_OFFSET_TABLE_ is a valid symbol C name, most a.out + # platforms (ab)use it in PIC code, but their linkers get confused if + # the symbol is explicitly referenced. Since portable code cannot + # rely on this symbol name, it's probably fine to never include it in + # preloaded symbol tables. + # Exclude shared library initialization/finalization symbols. +dnl Note also adjust exclude_expsyms for C++ above. + extract_expsyms_cmds= + + case $host_os in + cygwin* | mingw* | pw32* | cegcc*) + # FIXME: the MSVC++ port hasn't been tested in a loooong time + # When not using gcc, we currently assume that we are using + # Microsoft Visual C++. + if test yes != "$GCC"; then + with_gnu_ld=no + fi + ;; + interix*) + # we just hope/assume this is gcc and not c89 (= MSVC++) + with_gnu_ld=yes + ;; + openbsd* | bitrig*) + with_gnu_ld=no + ;; + linux* | k*bsd*-gnu | gnu*) + _LT_TAGVAR(link_all_deplibs, $1)=no + ;; + esac + + _LT_TAGVAR(ld_shlibs, $1)=yes + + # On some targets, GNU ld is compatible enough with the native linker + # that we're better off using the native interface for both. + lt_use_gnu_ld_interface=no + if test yes = "$with_gnu_ld"; then + case $host_os in + aix*) + # The AIX port of GNU ld has always aspired to compatibility + # with the native linker. However, as the warning in the GNU ld + # block says, versions before 2.19.5* couldn't really create working + # shared libraries, regardless of the interface used. + case `$LD -v 2>&1` in + *\ \(GNU\ Binutils\)\ 2.19.5*) ;; + *\ \(GNU\ Binutils\)\ 2.[[2-9]]*) ;; + *\ \(GNU\ Binutils\)\ [[3-9]]*) ;; + *) + lt_use_gnu_ld_interface=yes + ;; + esac + ;; + *) + lt_use_gnu_ld_interface=yes + ;; + esac + fi + + if test yes = "$lt_use_gnu_ld_interface"; then + # If archive_cmds runs LD, not CC, wlarc should be empty + wlarc='$wl' + + # Set some defaults for GNU ld with shared library support. These + # are reset later if shared libraries are not supported. Putting them + # here allows them to be overridden if necessary. + runpath_var=LD_RUN_PATH + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic' + # ancient GNU ld didn't support --whole-archive et. al. + if $LD --help 2>&1 | $GREP 'no-whole-archive' > /dev/null; then + _LT_TAGVAR(whole_archive_flag_spec, $1)=$wlarc'--whole-archive$convenience '$wlarc'--no-whole-archive' + else + _LT_TAGVAR(whole_archive_flag_spec, $1)= + fi + supports_anon_versioning=no + case `$LD -v | $SED -e 's/([^)]\+)\s\+//' 2>&1` in + *GNU\ gold*) supports_anon_versioning=yes ;; + *\ [[01]].* | *\ 2.[[0-9]].* | *\ 2.10.*) ;; # catch versions < 2.11 + *\ 2.11.93.0.2\ *) supports_anon_versioning=yes ;; # RH7.3 ... + *\ 2.11.92.0.12\ *) supports_anon_versioning=yes ;; # Mandrake 8.2 ... + *\ 2.11.*) ;; # other 2.11 versions + *) supports_anon_versioning=yes ;; + esac + + # See if GNU ld supports shared libraries. + case $host_os in + aix[[3-9]]*) + # On AIX/PPC, the GNU linker is very broken + if test ia64 != "$host_cpu"; then + _LT_TAGVAR(ld_shlibs, $1)=no + cat <<_LT_EOF 1>&2 + +*** Warning: the GNU linker, at least up to release 2.19, is reported +*** to be unable to reliably create shared libraries on AIX. +*** Therefore, libtool is disabling shared libraries support. If you +*** really care for shared libraries, you may want to install binutils +*** 2.20 or above, or modify your PATH so that a non-GNU linker is found. +*** You will then need to restart the configuration process. + +_LT_EOF + fi + ;; + + amigaos*) + case $host_cpu in + powerpc) + # see comment about AmigaOS4 .so support + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='' + ;; + m68k) + _LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/a2ixlibrary.data~$ECHO "#define NAME $libname" > $output_objdir/a2ixlibrary.data~$ECHO "#define LIBRARY_ID 1" >> $output_objdir/a2ixlibrary.data~$ECHO "#define VERSION $major" >> $output_objdir/a2ixlibrary.data~$ECHO "#define REVISION $revision" >> $output_objdir/a2ixlibrary.data~$AR $AR_FLAGS $lib $libobjs~$RANLIB $lib~(cd $output_objdir && a2ixlibrary -32)' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(hardcode_minus_L, $1)=yes + ;; + esac + ;; + + beos*) + if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then + _LT_TAGVAR(allow_undefined_flag, $1)=unsupported + # Joseph Beckenbach says some releases of gcc + # support --undefined. This deserves some investigation. FIXME + _LT_TAGVAR(archive_cmds, $1)='$CC -nostart $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + else + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + + cygwin* | mingw* | pw32* | cegcc*) + # _LT_TAGVAR(hardcode_libdir_flag_spec, $1) is actually meaningless, + # as there is no search path for DLLs. + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-all-symbols' + _LT_TAGVAR(allow_undefined_flag, $1)=unsupported + _LT_TAGVAR(always_export_symbols, $1)=no + _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes + _LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED -e '\''/^[[BCDGRS]][[ ]]/s/.*[[ ]]\([[^ ]]*\)/\1 DATA/;s/^.*[[ ]]__nm__\([[^ ]]*\)[[ ]][[^ ]]*/\1 DATA/;/^I[[ ]]/d;/^[[AITW]][[ ]]/s/.* //'\'' | sort | uniq > $export_symbols' + _LT_TAGVAR(exclude_expsyms, $1)=['[_]+GLOBAL_OFFSET_TABLE_|[_]+GLOBAL__[FID]_.*|[_]+head_[A-Za-z0-9_]+_dll|[A-Za-z0-9_]+_dll_iname'] + + if $LD --help 2>&1 | $GREP 'auto-import' > /dev/null; then + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib' + # If the export-symbols file already is a .def file, use it as + # is; otherwise, prepend EXPORTS... + _LT_TAGVAR(archive_expsym_cmds, $1)='if _LT_DLL_DEF_P([$export_symbols]); then + cp $export_symbols $output_objdir/$soname.def; + else + echo EXPORTS > $output_objdir/$soname.def; + cat $export_symbols >> $output_objdir/$soname.def; + fi~ + $CC -shared $output_objdir/$soname.def $libobjs $deplibs $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib' + else + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + + haiku*) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(link_all_deplibs, $1)=yes + ;; + + os2*) + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(hardcode_minus_L, $1)=yes + _LT_TAGVAR(allow_undefined_flag, $1)=unsupported + shrext_cmds=.dll + _LT_TAGVAR(archive_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~ + $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~ + $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~ + $ECHO EXPORTS >> $output_objdir/$libname.def~ + emxexp $libobjs | $SED /"_DLL_InitTerm"/d >> $output_objdir/$libname.def~ + $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~ + emximp -o $lib $output_objdir/$libname.def' + _LT_TAGVAR(archive_expsym_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~ + $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~ + $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~ + $ECHO EXPORTS >> $output_objdir/$libname.def~ + prefix_cmds="$SED"~ + if test EXPORTS = "`$SED 1q $export_symbols`"; then + prefix_cmds="$prefix_cmds -e 1d"; + fi~ + prefix_cmds="$prefix_cmds -e \"s/^\(.*\)$/_\1/g\""~ + cat $export_symbols | $prefix_cmds >> $output_objdir/$libname.def~ + $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~ + emximp -o $lib $output_objdir/$libname.def' + _LT_TAGVAR(old_archive_From_new_cmds, $1)='emximp -o $output_objdir/${libname}_dll.a $output_objdir/$libname.def' + _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes + ;; + + interix[[3-9]]*) + _LT_TAGVAR(hardcode_direct, $1)=no + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E' + # Hack: On Interix 3.x, we cannot compile PIC because of a broken gcc. + # Instead, shared libraries are loaded at an image base (0x10000000 by + # default) and relocated if they conflict, which is a slow very memory + # consuming and fragmenting process. To avoid this, we pick a random, + # 256 KiB-aligned image base between 0x50000000 and 0x6FFC0000 at link + # time. Moving up from 0x10000000 also allows more sbrk(2) space. + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='sed "s|^|_|" $export_symbols >$output_objdir/$soname.expsym~$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--retain-symbols-file,$output_objdir/$soname.expsym $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib' + ;; + + gnu* | linux* | tpf* | k*bsd*-gnu | kopensolaris*-gnu) + tmp_diet=no + if test linux-dietlibc = "$host_os"; then + case $cc_basename in + diet\ *) tmp_diet=yes;; # linux-dietlibc with static linking (!diet-dyn) + esac + fi + if $LD --help 2>&1 | $EGREP ': supported targets:.* elf' > /dev/null \ + && test no = "$tmp_diet" + then + tmp_addflag=' $pic_flag' + tmp_sharedflag='-shared' + case $cc_basename,$host_cpu in + pgcc*) # Portland Group C compiler + _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`for conv in $convenience\"\"; do test -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive' + tmp_addflag=' $pic_flag' + ;; + pgf77* | pgf90* | pgf95* | pgfortran*) + # Portland Group f77 and f90 compilers + _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`for conv in $convenience\"\"; do test -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive' + tmp_addflag=' $pic_flag -Mnomain' ;; + ecc*,ia64* | icc*,ia64*) # Intel C compiler on ia64 + tmp_addflag=' -i_dynamic' ;; + efc*,ia64* | ifort*,ia64*) # Intel Fortran compiler on ia64 + tmp_addflag=' -i_dynamic -nofor_main' ;; + ifc* | ifort*) # Intel Fortran compiler + tmp_addflag=' -nofor_main' ;; + lf95*) # Lahey Fortran 8.1 + _LT_TAGVAR(whole_archive_flag_spec, $1)= + tmp_sharedflag='--shared' ;; + nagfor*) # NAGFOR 5.3 + tmp_sharedflag='-Wl,-shared' ;; + xl[[cC]]* | bgxl[[cC]]* | mpixl[[cC]]*) # IBM XL C 8.0 on PPC (deal with xlf below) + tmp_sharedflag='-qmkshrobj' + tmp_addflag= ;; + nvcc*) # Cuda Compiler Driver 2.2 + _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`for conv in $convenience\"\"; do test -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive' + _LT_TAGVAR(compiler_needs_object, $1)=yes + ;; + esac + case `$CC -V 2>&1 | sed 5q` in + *Sun\ C*) # Sun C 5.9 + _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`new_convenience=; for conv in $convenience\"\"; do test -z \"$conv\" || new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive' + _LT_TAGVAR(compiler_needs_object, $1)=yes + tmp_sharedflag='-G' ;; + *Sun\ F*) # Sun Fortran 8.3 + tmp_sharedflag='-G' ;; + esac + _LT_TAGVAR(archive_cmds, $1)='$CC '"$tmp_sharedflag""$tmp_addflag"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + + if test yes = "$supports_anon_versioning"; then + _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $output_objdir/$libname.ver~ + cat $export_symbols | sed -e "s/\(.*\)/\1;/" >> $output_objdir/$libname.ver~ + echo "local: *; };" >> $output_objdir/$libname.ver~ + $CC '"$tmp_sharedflag""$tmp_addflag"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-version-script $wl$output_objdir/$libname.ver -o $lib' + fi + + case $cc_basename in + tcc*) + _LT_TAGVAR(export_dynamic_flag_spec, $1)='-rdynamic' + ;; + xlf* | bgf* | bgxlf* | mpixlf*) + # IBM XL Fortran 10.1 on PPC cannot create shared libs itself + _LT_TAGVAR(whole_archive_flag_spec, $1)='--whole-archive$convenience --no-whole-archive' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + _LT_TAGVAR(archive_cmds, $1)='$LD -shared $libobjs $deplibs $linker_flags -soname $soname -o $lib' + if test yes = "$supports_anon_versioning"; then + _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $output_objdir/$libname.ver~ + cat $export_symbols | sed -e "s/\(.*\)/\1;/" >> $output_objdir/$libname.ver~ + echo "local: *; };" >> $output_objdir/$libname.ver~ + $LD -shared $libobjs $deplibs $linker_flags -soname $soname -version-script $output_objdir/$libname.ver -o $lib' + fi + ;; + esac + else + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + + netbsd* | netbsdelf*-gnu) + if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then + _LT_TAGVAR(archive_cmds, $1)='$LD -Bshareable $libobjs $deplibs $linker_flags -o $lib' + wlarc= + else + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + fi + ;; + + solaris*) + if $LD -v 2>&1 | $GREP 'BFD 2\.8' > /dev/null; then + _LT_TAGVAR(ld_shlibs, $1)=no + cat <<_LT_EOF 1>&2 + +*** Warning: The releases 2.8.* of the GNU linker cannot reliably +*** create shared libraries on Solaris systems. Therefore, libtool +*** is disabling shared libraries support. We urge you to upgrade GNU +*** binutils to release 2.9.1 or newer. Another option is to modify +*** your PATH or compiler configuration so that the native linker is +*** used, and then restart. + +_LT_EOF + elif $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + else + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + + sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX*) + case `$LD -v 2>&1` in + *\ [[01]].* | *\ 2.[[0-9]].* | *\ 2.1[[0-5]].*) + _LT_TAGVAR(ld_shlibs, $1)=no + cat <<_LT_EOF 1>&2 + +*** Warning: Releases of the GNU linker prior to 2.16.91.0.3 cannot +*** reliably create shared libraries on SCO systems. Therefore, libtool +*** is disabling shared libraries support. We urge you to upgrade GNU +*** binutils to release 2.16.91.0.3 or newer. Another option is to modify +*** your PATH or compiler configuration so that the native linker is +*** used, and then restart. + +_LT_EOF + ;; + *) + # For security reasons, it is highly recommended that you always + # use absolute paths for naming shared libraries, and exclude the + # DT_RUNPATH tag from executables and libraries. But doing so + # requires that you compile everything twice, which is a pain. + if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + else + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + esac + ;; + + sunos4*) + _LT_TAGVAR(archive_cmds, $1)='$LD -assert pure-text -Bshareable -o $lib $libobjs $deplibs $linker_flags' + wlarc= + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + + *) + if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + else + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + esac + + if test no = "$_LT_TAGVAR(ld_shlibs, $1)"; then + runpath_var= + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)= + _LT_TAGVAR(export_dynamic_flag_spec, $1)= + _LT_TAGVAR(whole_archive_flag_spec, $1)= + fi + else + # PORTME fill in a description of your system's linker (not GNU ld) + case $host_os in + aix3*) + _LT_TAGVAR(allow_undefined_flag, $1)=unsupported + _LT_TAGVAR(always_export_symbols, $1)=yes + _LT_TAGVAR(archive_expsym_cmds, $1)='$LD -o $output_objdir/$soname $libobjs $deplibs $linker_flags -bE:$export_symbols -T512 -H512 -bM:SRE~$AR $AR_FLAGS $lib $output_objdir/$soname' + # Note: this linker hardcodes the directories in LIBPATH if there + # are no directories specified by -L. + _LT_TAGVAR(hardcode_minus_L, $1)=yes + if test yes = "$GCC" && test -z "$lt_prog_compiler_static"; then + # Neither direct hardcoding nor static linking is supported with a + # broken collect2. + _LT_TAGVAR(hardcode_direct, $1)=unsupported + fi + ;; + + aix[[4-9]]*) + if test ia64 = "$host_cpu"; then + # On IA64, the linker does run time linking by default, so we don't + # have to do anything special. + aix_use_runtimelinking=no + exp_sym_flag='-Bexport' + no_entry_flag= + else + # If we're using GNU nm, then we don't want the "-C" option. + # -C means demangle to GNU nm, but means don't demangle to AIX nm. + # Without the "-l" option, or with the "-B" option, AIX nm treats + # weak defined symbols like other global defined symbols, whereas + # GNU nm marks them as "W". + # While the 'weak' keyword is ignored in the Export File, we need + # it in the Import File for the 'aix-soname' feature, so we have + # to replace the "-B" option with "-P" for AIX nm. + if $NM -V 2>&1 | $GREP 'GNU' > /dev/null; then + _LT_TAGVAR(export_symbols_cmds, $1)='$NM -Bpg $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "W")) && ([substr](\$ 3,1,1) != ".")) { if (\$ 2 == "W") { print \$ 3 " weak" } else { print \$ 3 } } }'\'' | sort -u > $export_symbols' + else + _LT_TAGVAR(export_symbols_cmds, $1)='`func_echo_all $NM | $SED -e '\''s/B\([[^B]]*\)$/P\1/'\''` -PCpgl $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) && ([substr](\$ 1,1,1) != ".")) { if ((\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) { print \$ 1 " weak" } else { print \$ 1 } } }'\'' | sort -u > $export_symbols' + fi + aix_use_runtimelinking=no + + # Test if we are trying to use run time linking or normal + # AIX style linking. If -brtl is somewhere in LDFLAGS, we + # have runtime linking enabled, and use it for executables. + # For shared libraries, we enable/disable runtime linking + # depending on the kind of the shared library created - + # when "with_aix_soname,aix_use_runtimelinking" is: + # "aix,no" lib.a(lib.so.V) shared, rtl:no, for executables + # "aix,yes" lib.so shared, rtl:yes, for executables + # lib.a static archive + # "both,no" lib.so.V(shr.o) shared, rtl:yes + # lib.a(lib.so.V) shared, rtl:no, for executables + # "both,yes" lib.so.V(shr.o) shared, rtl:yes, for executables + # lib.a(lib.so.V) shared, rtl:no + # "svr4,*" lib.so.V(shr.o) shared, rtl:yes, for executables + # lib.a static archive + case $host_os in aix4.[[23]]|aix4.[[23]].*|aix[[5-9]]*) + for ld_flag in $LDFLAGS; do + if (test x-brtl = "x$ld_flag" || test x-Wl,-brtl = "x$ld_flag"); then + aix_use_runtimelinking=yes + break + fi + done + if test svr4,no = "$with_aix_soname,$aix_use_runtimelinking"; then + # With aix-soname=svr4, we create the lib.so.V shared archives only, + # so we don't have lib.a shared libs to link our executables. + # We have to force runtime linking in this case. + aix_use_runtimelinking=yes + LDFLAGS="$LDFLAGS -Wl,-brtl" + fi + ;; + esac + + exp_sym_flag='-bexport' + no_entry_flag='-bnoentry' + fi + + # When large executables or shared objects are built, AIX ld can + # have problems creating the table of contents. If linking a library + # or program results in "error TOC overflow" add -mminimal-toc to + # CXXFLAGS/CFLAGS for g++/gcc. In the cases where that is not + # enough to fix the problem, add -Wl,-bbigtoc to LDFLAGS. + + _LT_TAGVAR(archive_cmds, $1)='' + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_direct_absolute, $1)=yes + _LT_TAGVAR(hardcode_libdir_separator, $1)=':' + _LT_TAGVAR(link_all_deplibs, $1)=yes + _LT_TAGVAR(file_list_spec, $1)='$wl-f,' + case $with_aix_soname,$aix_use_runtimelinking in + aix,*) ;; # traditional, no import file + svr4,* | *,yes) # use import file + # The Import File defines what to hardcode. + _LT_TAGVAR(hardcode_direct, $1)=no + _LT_TAGVAR(hardcode_direct_absolute, $1)=no + ;; + esac + + if test yes = "$GCC"; then + case $host_os in aix4.[[012]]|aix4.[[012]].*) + # We only want to do this on AIX 4.2 and lower, the check + # below for broken collect2 doesn't work under 4.3+ + collect2name=`$CC -print-prog-name=collect2` + if test -f "$collect2name" && + strings "$collect2name" | $GREP resolve_lib_name >/dev/null + then + # We have reworked collect2 + : + else + # We have old collect2 + _LT_TAGVAR(hardcode_direct, $1)=unsupported + # It fails to find uninstalled libraries when the uninstalled + # path is not listed in the libpath. Setting hardcode_minus_L + # to unsupported forces relinking + _LT_TAGVAR(hardcode_minus_L, $1)=yes + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)= + fi + ;; + esac + shared_flag='-shared' + if test yes = "$aix_use_runtimelinking"; then + shared_flag="$shared_flag "'$wl-G' + fi + # Need to ensure runtime linking is disabled for the traditional + # shared library, or the linker may eventually find shared libraries + # /with/ Import File - we do not want to mix them. + shared_flag_aix='-shared' + shared_flag_svr4='-shared $wl-G' + else + # not using gcc + if test ia64 = "$host_cpu"; then + # VisualAge C++, Version 5.5 for AIX 5L for IA-64, Beta 3 Release + # chokes on -Wl,-G. The following line is correct: + shared_flag='-G' + else + if test yes = "$aix_use_runtimelinking"; then + shared_flag='$wl-G' + else + shared_flag='$wl-bM:SRE' + fi + shared_flag_aix='$wl-bM:SRE' + shared_flag_svr4='$wl-G' + fi + fi + + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-bexpall' + # It seems that -bexpall does not export symbols beginning with + # underscore (_), so it is better to generate a list of symbols to export. + _LT_TAGVAR(always_export_symbols, $1)=yes + if test aix,yes = "$with_aix_soname,$aix_use_runtimelinking"; then + # Warning - without using the other runtime loading flags (-brtl), + # -berok will link without error, but may produce a broken library. + _LT_TAGVAR(allow_undefined_flag, $1)='-berok' + # Determine the default libpath from the value encoded in an + # empty executable. + _LT_SYS_MODULE_PATH_AIX([$1]) + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-blibpath:$libdir:'"$aix_libpath" + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -o $output_objdir/$soname $libobjs $deplibs $wl'$no_entry_flag' $compiler_flags `if test -n "$allow_undefined_flag"; then func_echo_all "$wl$allow_undefined_flag"; else :; fi` $wl'$exp_sym_flag:\$export_symbols' '$shared_flag + else + if test ia64 = "$host_cpu"; then + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-R $libdir:/usr/lib:/lib' + _LT_TAGVAR(allow_undefined_flag, $1)="-z nodefs" + _LT_TAGVAR(archive_expsym_cmds, $1)="\$CC $shared_flag"' -o $output_objdir/$soname $libobjs $deplibs '"\$wl$no_entry_flag"' $compiler_flags $wl$allow_undefined_flag '"\$wl$exp_sym_flag:\$export_symbols" + else + # Determine the default libpath from the value encoded in an + # empty executable. + _LT_SYS_MODULE_PATH_AIX([$1]) + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-blibpath:$libdir:'"$aix_libpath" + # Warning - without using the other run time loading flags, + # -berok will link without error, but may produce a broken library. + _LT_TAGVAR(no_undefined_flag, $1)=' $wl-bernotok' + _LT_TAGVAR(allow_undefined_flag, $1)=' $wl-berok' + if test yes = "$with_gnu_ld"; then + # We only use this code for GNU lds that support --whole-archive. + _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive$convenience $wl--no-whole-archive' + else + # Exported symbols can be pulled into shared objects from archives + _LT_TAGVAR(whole_archive_flag_spec, $1)='$convenience' + fi + _LT_TAGVAR(archive_cmds_need_lc, $1)=yes + _LT_TAGVAR(archive_expsym_cmds, $1)='$RM -r $output_objdir/$realname.d~$MKDIR $output_objdir/$realname.d' + # -brtl affects multiple linker settings, -berok does not and is overridden later + compiler_flags_filtered='`func_echo_all "$compiler_flags " | $SED -e "s%-brtl\\([[, ]]\\)%-berok\\1%g"`' + if test svr4 != "$with_aix_soname"; then + # This is similar to how AIX traditionally builds its shared libraries. + _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$CC '$shared_flag_aix' -o $output_objdir/$realname.d/$soname $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$AR $AR_FLAGS $output_objdir/$libname$release.a $output_objdir/$realname.d/$soname' + fi + if test aix != "$with_aix_soname"; then + _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$CC '$shared_flag_svr4' -o $output_objdir/$realname.d/$shared_archive_member_spec.o $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$STRIP -e $output_objdir/$realname.d/$shared_archive_member_spec.o~( func_echo_all "#! $soname($shared_archive_member_spec.o)"; if test shr_64 = "$shared_archive_member_spec"; then func_echo_all "# 64"; else func_echo_all "# 32"; fi; cat $export_symbols ) > $output_objdir/$realname.d/$shared_archive_member_spec.imp~$AR $AR_FLAGS $output_objdir/$soname $output_objdir/$realname.d/$shared_archive_member_spec.o $output_objdir/$realname.d/$shared_archive_member_spec.imp' + else + # used by -dlpreopen to get the symbols + _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$MV $output_objdir/$realname.d/$soname $output_objdir' + fi + _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$RM -r $output_objdir/$realname.d' + fi + fi + ;; + + amigaos*) + case $host_cpu in + powerpc) + # see comment about AmigaOS4 .so support + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='' + ;; + m68k) + _LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/a2ixlibrary.data~$ECHO "#define NAME $libname" > $output_objdir/a2ixlibrary.data~$ECHO "#define LIBRARY_ID 1" >> $output_objdir/a2ixlibrary.data~$ECHO "#define VERSION $major" >> $output_objdir/a2ixlibrary.data~$ECHO "#define REVISION $revision" >> $output_objdir/a2ixlibrary.data~$AR $AR_FLAGS $lib $libobjs~$RANLIB $lib~(cd $output_objdir && a2ixlibrary -32)' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(hardcode_minus_L, $1)=yes + ;; + esac + ;; + + bsdi[[45]]*) + _LT_TAGVAR(export_dynamic_flag_spec, $1)=-rdynamic + ;; + + cygwin* | mingw* | pw32* | cegcc*) + # When not using gcc, we currently assume that we are using + # Microsoft Visual C++. + # hardcode_libdir_flag_spec is actually meaningless, as there is + # no search path for DLLs. + case $cc_basename in + cl*) + # Native MSVC + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)=' ' + _LT_TAGVAR(allow_undefined_flag, $1)=unsupported + _LT_TAGVAR(always_export_symbols, $1)=yes + _LT_TAGVAR(file_list_spec, $1)='@' + # Tell ltmain to make .lib files, not .a files. + libext=lib + # Tell ltmain to make .dll files, not .so files. + shrext_cmds=.dll + # FIXME: Setting linknames here is a bad hack. + _LT_TAGVAR(archive_cmds, $1)='$CC -o $output_objdir/$soname $libobjs $compiler_flags $deplibs -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~linknames=' + _LT_TAGVAR(archive_expsym_cmds, $1)='if _LT_DLL_DEF_P([$export_symbols]); then + cp "$export_symbols" "$output_objdir/$soname.def"; + echo "$tool_output_objdir$soname.def" > "$output_objdir/$soname.exp"; + else + $SED -e '\''s/^/-link -EXPORT:/'\'' < $export_symbols > $output_objdir/$soname.exp; + fi~ + $CC -o $tool_output_objdir$soname $libobjs $compiler_flags $deplibs "@$tool_output_objdir$soname.exp" -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~ + linknames=' + # The linker will not automatically build a static lib if we build a DLL. + # _LT_TAGVAR(old_archive_from_new_cmds, $1)='true' + _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes + _LT_TAGVAR(exclude_expsyms, $1)='_NULL_IMPORT_DESCRIPTOR|_IMPORT_DESCRIPTOR_.*' + _LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED -e '\''/^[[BCDGRS]][[ ]]/s/.*[[ ]]\([[^ ]]*\)/\1,DATA/'\'' | $SED -e '\''/^[[AITW]][[ ]]/s/.*[[ ]]//'\'' | sort | uniq > $export_symbols' + # Don't use ranlib + _LT_TAGVAR(old_postinstall_cmds, $1)='chmod 644 $oldlib' + _LT_TAGVAR(postlink_cmds, $1)='lt_outputfile="@OUTPUT@"~ + lt_tool_outputfile="@TOOL_OUTPUT@"~ + case $lt_outputfile in + *.exe|*.EXE) ;; + *) + lt_outputfile=$lt_outputfile.exe + lt_tool_outputfile=$lt_tool_outputfile.exe + ;; + esac~ + if test : != "$MANIFEST_TOOL" && test -f "$lt_outputfile.manifest"; then + $MANIFEST_TOOL -manifest "$lt_tool_outputfile.manifest" -outputresource:"$lt_tool_outputfile" || exit 1; + $RM "$lt_outputfile.manifest"; + fi' + ;; + *) + # Assume MSVC wrapper + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)=' ' + _LT_TAGVAR(allow_undefined_flag, $1)=unsupported + # Tell ltmain to make .lib files, not .a files. + libext=lib + # Tell ltmain to make .dll files, not .so files. + shrext_cmds=.dll + # FIXME: Setting linknames here is a bad hack. + _LT_TAGVAR(archive_cmds, $1)='$CC -o $lib $libobjs $compiler_flags `func_echo_all "$deplibs" | $SED '\''s/ -lc$//'\''` -link -dll~linknames=' + # The linker will automatically build a .lib file if we build a DLL. + _LT_TAGVAR(old_archive_from_new_cmds, $1)='true' + # FIXME: Should let the user specify the lib program. + _LT_TAGVAR(old_archive_cmds, $1)='lib -OUT:$oldlib$oldobjs$old_deplibs' + _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes + ;; + esac + ;; + + darwin* | rhapsody*) + _LT_DARWIN_LINKER_FEATURES($1) + ;; + + dgux*) + _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + + # FreeBSD 2.2.[012] allows us to include c++rt0.o to get C++ constructor + # support. Future versions do this automatically, but an explicit c++rt0.o + # does not break anything, and helps significantly (at the cost of a little + # extra space). + freebsd2.2*) + _LT_TAGVAR(archive_cmds, $1)='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags /usr/lib/c++rt0.o' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir' + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + + # Unfortunately, older versions of FreeBSD 2 do not have this feature. + freebsd2.*) + _LT_TAGVAR(archive_cmds, $1)='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags' + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_minus_L, $1)=yes + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + + # FreeBSD 3 and greater uses gcc -shared to do shared libraries. + freebsd* | dragonfly*) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir' + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + + hpux9*) + if test yes = "$GCC"; then + _LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/$soname~$CC -shared $pic_flag $wl+b $wl$install_libdir -o $output_objdir/$soname $libobjs $deplibs $compiler_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib' + else + _LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/$soname~$LD -b +b $install_libdir -o $output_objdir/$soname $libobjs $deplibs $linker_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib' + fi + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl+b $wl$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + _LT_TAGVAR(hardcode_direct, $1)=yes + + # hardcode_minus_L: Not really in the search PATH, + # but as the default location of the library. + _LT_TAGVAR(hardcode_minus_L, $1)=yes + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E' + ;; + + hpux10*) + if test yes,no = "$GCC,$with_gnu_ld"; then + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags' + else + _LT_TAGVAR(archive_cmds, $1)='$LD -b +h $soname +b $install_libdir -o $lib $libobjs $deplibs $linker_flags' + fi + if test no = "$with_gnu_ld"; then + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl+b $wl$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_direct_absolute, $1)=yes + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E' + # hardcode_minus_L: Not really in the search PATH, + # but as the default location of the library. + _LT_TAGVAR(hardcode_minus_L, $1)=yes + fi + ;; + + hpux11*) + if test yes,no = "$GCC,$with_gnu_ld"; then + case $host_cpu in + hppa*64*) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $wl+h $wl$soname -o $lib $libobjs $deplibs $compiler_flags' + ;; + ia64*) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $wl+h $wl$soname $wl+nodefaultrpath -o $lib $libobjs $deplibs $compiler_flags' + ;; + *) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags' + ;; + esac + else + case $host_cpu in + hppa*64*) + _LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname -o $lib $libobjs $deplibs $compiler_flags' + ;; + ia64*) + _LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname $wl+nodefaultrpath -o $lib $libobjs $deplibs $compiler_flags' + ;; + *) + m4_if($1, [], [ + # Older versions of the 11.00 compiler do not understand -b yet + # (HP92453-01 A.11.01.20 doesn't, HP92453-01 B.11.X.35175-35176.GP does) + _LT_LINKER_OPTION([if $CC understands -b], + _LT_TAGVAR(lt_cv_prog_compiler__b, $1), [-b], + [_LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags'], + [_LT_TAGVAR(archive_cmds, $1)='$LD -b +h $soname +b $install_libdir -o $lib $libobjs $deplibs $linker_flags'])], + [_LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags']) + ;; + esac + fi + if test no = "$with_gnu_ld"; then + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl+b $wl$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + + case $host_cpu in + hppa*64*|ia64*) + _LT_TAGVAR(hardcode_direct, $1)=no + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + *) + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_direct_absolute, $1)=yes + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E' + + # hardcode_minus_L: Not really in the search PATH, + # but as the default location of the library. + _LT_TAGVAR(hardcode_minus_L, $1)=yes + ;; + esac + fi + ;; + + irix5* | irix6* | nonstopux*) + if test yes = "$GCC"; then + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib' + # Try to use the -exported_symbol ld option, if it does not + # work, assume that -exports_file does not work either and + # implicitly export all symbols. + # This should be the same for all languages, so no per-tag cache variable. + AC_CACHE_CHECK([whether the $host_os linker accepts -exported_symbol], + [lt_cv_irix_exported_symbol], + [save_LDFLAGS=$LDFLAGS + LDFLAGS="$LDFLAGS -shared $wl-exported_symbol ${wl}foo $wl-update_registry $wl/dev/null" + AC_LINK_IFELSE( + [AC_LANG_SOURCE( + [AC_LANG_CASE([C], [[int foo (void) { return 0; }]], + [C++], [[int foo (void) { return 0; }]], + [Fortran 77], [[ + subroutine foo + end]], + [Fortran], [[ + subroutine foo + end]])])], + [lt_cv_irix_exported_symbol=yes], + [lt_cv_irix_exported_symbol=no]) + LDFLAGS=$save_LDFLAGS]) + if test yes = "$lt_cv_irix_exported_symbol"; then + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations $wl-exports_file $wl$export_symbols -o $lib' + fi + _LT_TAGVAR(link_all_deplibs, $1)=no + else + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -exports_file $export_symbols -o $lib' + fi + _LT_TAGVAR(archive_cmds_need_lc, $1)='no' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + _LT_TAGVAR(inherit_rpath, $1)=yes + _LT_TAGVAR(link_all_deplibs, $1)=yes + ;; + + linux*) + case $cc_basename in + tcc*) + # Fabrice Bellard et al's Tiny C Compiler + _LT_TAGVAR(ld_shlibs, $1)=yes + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags' + ;; + esac + ;; + + netbsd* | netbsdelf*-gnu) + if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then + _LT_TAGVAR(archive_cmds, $1)='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags' # a.out + else + _LT_TAGVAR(archive_cmds, $1)='$LD -shared -o $lib $libobjs $deplibs $linker_flags' # ELF + fi + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir' + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + + newsos6) + _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + + *nto* | *qnx*) + ;; + + openbsd* | bitrig*) + if test -f /usr/libexec/ld.so; then + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + _LT_TAGVAR(hardcode_direct_absolute, $1)=yes + if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags $wl-retain-symbols-file,$export_symbols' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E' + else + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir' + fi + else + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + + os2*) + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(hardcode_minus_L, $1)=yes + _LT_TAGVAR(allow_undefined_flag, $1)=unsupported + shrext_cmds=.dll + _LT_TAGVAR(archive_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~ + $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~ + $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~ + $ECHO EXPORTS >> $output_objdir/$libname.def~ + emxexp $libobjs | $SED /"_DLL_InitTerm"/d >> $output_objdir/$libname.def~ + $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~ + emximp -o $lib $output_objdir/$libname.def' + _LT_TAGVAR(archive_expsym_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~ + $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~ + $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~ + $ECHO EXPORTS >> $output_objdir/$libname.def~ + prefix_cmds="$SED"~ + if test EXPORTS = "`$SED 1q $export_symbols`"; then + prefix_cmds="$prefix_cmds -e 1d"; + fi~ + prefix_cmds="$prefix_cmds -e \"s/^\(.*\)$/_\1/g\""~ + cat $export_symbols | $prefix_cmds >> $output_objdir/$libname.def~ + $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~ + emximp -o $lib $output_objdir/$libname.def' + _LT_TAGVAR(old_archive_From_new_cmds, $1)='emximp -o $output_objdir/${libname}_dll.a $output_objdir/$libname.def' + _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes + ;; + + osf3*) + if test yes = "$GCC"; then + _LT_TAGVAR(allow_undefined_flag, $1)=' $wl-expect_unresolved $wl\*' + _LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib' + else + _LT_TAGVAR(allow_undefined_flag, $1)=' -expect_unresolved \*' + _LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib' + fi + _LT_TAGVAR(archive_cmds_need_lc, $1)='no' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + ;; + + osf4* | osf5*) # as osf3* with the addition of -msym flag + if test yes = "$GCC"; then + _LT_TAGVAR(allow_undefined_flag, $1)=' $wl-expect_unresolved $wl\*' + _LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $pic_flag $libobjs $deplibs $compiler_flags $wl-msym $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + else + _LT_TAGVAR(allow_undefined_flag, $1)=' -expect_unresolved \*' + _LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags -msym -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='for i in `cat $export_symbols`; do printf "%s %s\\n" -exported_symbol "\$i" >> $lib.exp; done; printf "%s\\n" "-hidden">> $lib.exp~ + $CC -shared$allow_undefined_flag $wl-input $wl$lib.exp $compiler_flags $libobjs $deplibs -soname $soname `test -n "$verstring" && $ECHO "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib~$RM $lib.exp' + + # Both c and cxx compiler support -rpath directly + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-rpath $libdir' + fi + _LT_TAGVAR(archive_cmds_need_lc, $1)='no' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + ;; + + solaris*) + _LT_TAGVAR(no_undefined_flag, $1)=' -z defs' + if test yes = "$GCC"; then + wlarc='$wl' + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $wl-z ${wl}text $wl-h $wl$soname -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~ + $CC -shared $pic_flag $wl-z ${wl}text $wl-M $wl$lib.exp $wl-h $wl$soname -o $lib $libobjs $deplibs $compiler_flags~$RM $lib.exp' + else + case `$CC -V 2>&1` in + *"Compilers 5.0"*) + wlarc='' + _LT_TAGVAR(archive_cmds, $1)='$LD -G$allow_undefined_flag -h $soname -o $lib $libobjs $deplibs $linker_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~ + $LD -G$allow_undefined_flag -M $lib.exp -h $soname -o $lib $libobjs $deplibs $linker_flags~$RM $lib.exp' + ;; + *) + wlarc='$wl' + _LT_TAGVAR(archive_cmds, $1)='$CC -G$allow_undefined_flag -h $soname -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~ + $CC -G$allow_undefined_flag -M $lib.exp -h $soname -o $lib $libobjs $deplibs $compiler_flags~$RM $lib.exp' + ;; + esac + fi + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir' + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + case $host_os in + solaris2.[[0-5]] | solaris2.[[0-5]].*) ;; + *) + # The compiler driver will combine and reorder linker options, + # but understands '-z linker_flag'. GCC discards it without '$wl', + # but is careful enough not to reorder. + # Supported since Solaris 2.6 (maybe 2.5.1?) + if test yes = "$GCC"; then + _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl-z ${wl}allextract$convenience $wl-z ${wl}defaultextract' + else + _LT_TAGVAR(whole_archive_flag_spec, $1)='-z allextract$convenience -z defaultextract' + fi + ;; + esac + _LT_TAGVAR(link_all_deplibs, $1)=yes + ;; + + sunos4*) + if test sequent = "$host_vendor"; then + # Use $CC to link under sequent, because it throws in some extra .o + # files that make .init and .fini sections work. + _LT_TAGVAR(archive_cmds, $1)='$CC -G $wl-h $soname -o $lib $libobjs $deplibs $compiler_flags' + else + _LT_TAGVAR(archive_cmds, $1)='$LD -assert pure-text -Bstatic -o $lib $libobjs $deplibs $linker_flags' + fi + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_minus_L, $1)=yes + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + + sysv4) + case $host_vendor in + sni) + _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + _LT_TAGVAR(hardcode_direct, $1)=yes # is this really true??? + ;; + siemens) + ## LD is ld it makes a PLAMLIB + ## CC just makes a GrossModule. + _LT_TAGVAR(archive_cmds, $1)='$LD -G -o $lib $libobjs $deplibs $linker_flags' + _LT_TAGVAR(reload_cmds, $1)='$CC -r -o $output$reload_objs' + _LT_TAGVAR(hardcode_direct, $1)=no + ;; + motorola) + _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + _LT_TAGVAR(hardcode_direct, $1)=no #Motorola manual says yes, but my tests say they lie + ;; + esac + runpath_var='LD_RUN_PATH' + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + + sysv4.3*) + _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + _LT_TAGVAR(export_dynamic_flag_spec, $1)='-Bexport' + ;; + + sysv4*MP*) + if test -d /usr/nec; then + _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + runpath_var=LD_RUN_PATH + hardcode_runpath_var=yes + _LT_TAGVAR(ld_shlibs, $1)=yes + fi + ;; + + sysv4*uw2* | sysv5OpenUNIX* | sysv5UnixWare7.[[01]].[[10]]* | unixware7* | sco3.2v5.0.[[024]]*) + _LT_TAGVAR(no_undefined_flag, $1)='$wl-z,text' + _LT_TAGVAR(archive_cmds_need_lc, $1)=no + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + runpath_var='LD_RUN_PATH' + + if test yes = "$GCC"; then + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + else + _LT_TAGVAR(archive_cmds, $1)='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + fi + ;; + + sysv5* | sco3.2v5* | sco5v6*) + # Note: We CANNOT use -z defs as we might desire, because we do not + # link with -lc, and that would cause any symbols used from libc to + # always be unresolved, which means just about no library would + # ever link correctly. If we're not using GNU ld we use -z text + # though, which does catch some bad symbols but isn't as heavy-handed + # as -z defs. + _LT_TAGVAR(no_undefined_flag, $1)='$wl-z,text' + _LT_TAGVAR(allow_undefined_flag, $1)='$wl-z,nodefs' + _LT_TAGVAR(archive_cmds_need_lc, $1)=no + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-R,$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=':' + _LT_TAGVAR(link_all_deplibs, $1)=yes + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-Bexport' + runpath_var='LD_RUN_PATH' + + if test yes = "$GCC"; then + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + else + _LT_TAGVAR(archive_cmds, $1)='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + fi + ;; + + uts4*) + _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + + *) + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + esac + + if test sni = "$host_vendor"; then + case $host in + sysv4 | sysv4.2uw2* | sysv4.3* | sysv5*) + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-Blargedynsym' + ;; + esac + fi + fi +]) +AC_MSG_RESULT([$_LT_TAGVAR(ld_shlibs, $1)]) +test no = "$_LT_TAGVAR(ld_shlibs, $1)" && can_build_shared=no + +_LT_TAGVAR(with_gnu_ld, $1)=$with_gnu_ld + +_LT_DECL([], [libext], [0], [Old archive suffix (normally "a")])dnl +_LT_DECL([], [shrext_cmds], [1], [Shared library suffix (normally ".so")])dnl +_LT_DECL([], [extract_expsyms_cmds], [2], + [The commands to extract the exported symbol list from a shared archive]) + +# +# Do we need to explicitly link libc? +# +case "x$_LT_TAGVAR(archive_cmds_need_lc, $1)" in +x|xyes) + # Assume -lc should be added + _LT_TAGVAR(archive_cmds_need_lc, $1)=yes + + if test yes,yes = "$GCC,$enable_shared"; then + case $_LT_TAGVAR(archive_cmds, $1) in + *'~'*) + # FIXME: we may have to deal with multi-command sequences. + ;; + '$CC '*) + # Test whether the compiler implicitly links with -lc since on some + # systems, -lgcc has to come before -lc. If gcc already passes -lc + # to ld, don't add -lc before -lgcc. + AC_CACHE_CHECK([whether -lc should be explicitly linked in], + [lt_cv_]_LT_TAGVAR(archive_cmds_need_lc, $1), + [$RM conftest* + echo "$lt_simple_compile_test_code" > conftest.$ac_ext + + if AC_TRY_EVAL(ac_compile) 2>conftest.err; then + soname=conftest + lib=conftest + libobjs=conftest.$ac_objext + deplibs= + wl=$_LT_TAGVAR(lt_prog_compiler_wl, $1) + pic_flag=$_LT_TAGVAR(lt_prog_compiler_pic, $1) + compiler_flags=-v + linker_flags=-v + verstring= + output_objdir=. + libname=conftest + lt_save_allow_undefined_flag=$_LT_TAGVAR(allow_undefined_flag, $1) + _LT_TAGVAR(allow_undefined_flag, $1)= + if AC_TRY_EVAL(_LT_TAGVAR(archive_cmds, $1) 2\>\&1 \| $GREP \" -lc \" \>/dev/null 2\>\&1) + then + lt_cv_[]_LT_TAGVAR(archive_cmds_need_lc, $1)=no + else + lt_cv_[]_LT_TAGVAR(archive_cmds_need_lc, $1)=yes + fi + _LT_TAGVAR(allow_undefined_flag, $1)=$lt_save_allow_undefined_flag + else + cat conftest.err 1>&5 + fi + $RM conftest* + ]) + _LT_TAGVAR(archive_cmds_need_lc, $1)=$lt_cv_[]_LT_TAGVAR(archive_cmds_need_lc, $1) + ;; + esac + fi + ;; +esac + +_LT_TAGDECL([build_libtool_need_lc], [archive_cmds_need_lc], [0], + [Whether or not to add -lc for building shared libraries]) +_LT_TAGDECL([allow_libtool_libs_with_static_runtimes], + [enable_shared_with_static_runtimes], [0], + [Whether or not to disallow shared libs when runtime libs are static]) +_LT_TAGDECL([], [export_dynamic_flag_spec], [1], + [Compiler flag to allow reflexive dlopens]) +_LT_TAGDECL([], [whole_archive_flag_spec], [1], + [Compiler flag to generate shared objects directly from archives]) +_LT_TAGDECL([], [compiler_needs_object], [1], + [Whether the compiler copes with passing no objects directly]) +_LT_TAGDECL([], [old_archive_from_new_cmds], [2], + [Create an old-style archive from a shared archive]) +_LT_TAGDECL([], [old_archive_from_expsyms_cmds], [2], + [Create a temporary old-style archive to link instead of a shared archive]) +_LT_TAGDECL([], [archive_cmds], [2], [Commands used to build a shared archive]) +_LT_TAGDECL([], [archive_expsym_cmds], [2]) +_LT_TAGDECL([], [module_cmds], [2], + [Commands used to build a loadable module if different from building + a shared archive.]) +_LT_TAGDECL([], [module_expsym_cmds], [2]) +_LT_TAGDECL([], [with_gnu_ld], [1], + [Whether we are building with GNU ld or not]) +_LT_TAGDECL([], [allow_undefined_flag], [1], + [Flag that allows shared libraries with undefined symbols to be built]) +_LT_TAGDECL([], [no_undefined_flag], [1], + [Flag that enforces no undefined symbols]) +_LT_TAGDECL([], [hardcode_libdir_flag_spec], [1], + [Flag to hardcode $libdir into a binary during linking. + This must work even if $libdir does not exist]) +_LT_TAGDECL([], [hardcode_libdir_separator], [1], + [Whether we need a single "-rpath" flag with a separated argument]) +_LT_TAGDECL([], [hardcode_direct], [0], + [Set to "yes" if using DIR/libNAME$shared_ext during linking hardcodes + DIR into the resulting binary]) +_LT_TAGDECL([], [hardcode_direct_absolute], [0], + [Set to "yes" if using DIR/libNAME$shared_ext during linking hardcodes + DIR into the resulting binary and the resulting library dependency is + "absolute", i.e impossible to change by setting $shlibpath_var if the + library is relocated]) +_LT_TAGDECL([], [hardcode_minus_L], [0], + [Set to "yes" if using the -LDIR flag during linking hardcodes DIR + into the resulting binary]) +_LT_TAGDECL([], [hardcode_shlibpath_var], [0], + [Set to "yes" if using SHLIBPATH_VAR=DIR during linking hardcodes DIR + into the resulting binary]) +_LT_TAGDECL([], [hardcode_automatic], [0], + [Set to "yes" if building a shared library automatically hardcodes DIR + into the library and all subsequent libraries and executables linked + against it]) +_LT_TAGDECL([], [inherit_rpath], [0], + [Set to yes if linker adds runtime paths of dependent libraries + to runtime path list]) +_LT_TAGDECL([], [link_all_deplibs], [0], + [Whether libtool must link a program against all its dependency libraries]) +_LT_TAGDECL([], [always_export_symbols], [0], + [Set to "yes" if exported symbols are required]) +_LT_TAGDECL([], [export_symbols_cmds], [2], + [The commands to list exported symbols]) +_LT_TAGDECL([], [exclude_expsyms], [1], + [Symbols that should not be listed in the preloaded symbols]) +_LT_TAGDECL([], [include_expsyms], [1], + [Symbols that must always be exported]) +_LT_TAGDECL([], [prelink_cmds], [2], + [Commands necessary for linking programs (against libraries) with templates]) +_LT_TAGDECL([], [postlink_cmds], [2], + [Commands necessary for finishing linking programs]) +_LT_TAGDECL([], [file_list_spec], [1], + [Specify filename containing input files]) +dnl FIXME: Not yet implemented +dnl _LT_TAGDECL([], [thread_safe_flag_spec], [1], +dnl [Compiler flag to generate thread safe objects]) +])# _LT_LINKER_SHLIBS + + +# _LT_LANG_C_CONFIG([TAG]) +# ------------------------ +# Ensure that the configuration variables for a C compiler are suitably +# defined. These variables are subsequently used by _LT_CONFIG to write +# the compiler configuration to 'libtool'. +m4_defun([_LT_LANG_C_CONFIG], +[m4_require([_LT_DECL_EGREP])dnl +lt_save_CC=$CC +AC_LANG_PUSH(C) + +# Source file extension for C test sources. +ac_ext=c + +# Object file extension for compiled C test sources. +objext=o +_LT_TAGVAR(objext, $1)=$objext + +# Code to be used in simple compile tests +lt_simple_compile_test_code="int some_variable = 0;" + +# Code to be used in simple link tests +lt_simple_link_test_code='int main(){return(0);}' + +_LT_TAG_COMPILER +# Save the default compiler, since it gets overwritten when the other +# tags are being tested, and _LT_TAGVAR(compiler, []) is a NOP. +compiler_DEFAULT=$CC + +# save warnings/boilerplate of simple test code +_LT_COMPILER_BOILERPLATE +_LT_LINKER_BOILERPLATE + +if test -n "$compiler"; then + _LT_COMPILER_NO_RTTI($1) + _LT_COMPILER_PIC($1) + _LT_COMPILER_C_O($1) + _LT_COMPILER_FILE_LOCKS($1) + _LT_LINKER_SHLIBS($1) + _LT_SYS_DYNAMIC_LINKER($1) + _LT_LINKER_HARDCODE_LIBPATH($1) + LT_SYS_DLOPEN_SELF + _LT_CMD_STRIPLIB + + # Report what library types will actually be built + AC_MSG_CHECKING([if libtool supports shared libraries]) + AC_MSG_RESULT([$can_build_shared]) + + AC_MSG_CHECKING([whether to build shared libraries]) + test no = "$can_build_shared" && enable_shared=no + + # On AIX, shared libraries and static libraries use the same namespace, and + # are all built from PIC. + case $host_os in + aix3*) + test yes = "$enable_shared" && enable_static=no + if test -n "$RANLIB"; then + archive_cmds="$archive_cmds~\$RANLIB \$lib" + postinstall_cmds='$RANLIB $lib' + fi + ;; + + aix[[4-9]]*) + if test ia64 != "$host_cpu"; then + case $enable_shared,$with_aix_soname,$aix_use_runtimelinking in + yes,aix,yes) ;; # shared object as lib.so file only + yes,svr4,*) ;; # shared object as lib.so archive member only + yes,*) enable_static=no ;; # shared object in lib.a archive as well + esac + fi + ;; + esac + AC_MSG_RESULT([$enable_shared]) + + AC_MSG_CHECKING([whether to build static libraries]) + # Make sure either enable_shared or enable_static is yes. + test yes = "$enable_shared" || enable_static=yes + AC_MSG_RESULT([$enable_static]) + + _LT_CONFIG($1) +fi +AC_LANG_POP +CC=$lt_save_CC +])# _LT_LANG_C_CONFIG + + +# _LT_LANG_CXX_CONFIG([TAG]) +# -------------------------- +# Ensure that the configuration variables for a C++ compiler are suitably +# defined. These variables are subsequently used by _LT_CONFIG to write +# the compiler configuration to 'libtool'. +m4_defun([_LT_LANG_CXX_CONFIG], +[m4_require([_LT_FILEUTILS_DEFAULTS])dnl +m4_require([_LT_DECL_EGREP])dnl +m4_require([_LT_PATH_MANIFEST_TOOL])dnl +if test -n "$CXX" && ( test no != "$CXX" && + ( (test g++ = "$CXX" && `g++ -v >/dev/null 2>&1` ) || + (test g++ != "$CXX"))); then + AC_PROG_CXXCPP +else + _lt_caught_CXX_error=yes +fi + +AC_LANG_PUSH(C++) +_LT_TAGVAR(archive_cmds_need_lc, $1)=no +_LT_TAGVAR(allow_undefined_flag, $1)= +_LT_TAGVAR(always_export_symbols, $1)=no +_LT_TAGVAR(archive_expsym_cmds, $1)= +_LT_TAGVAR(compiler_needs_object, $1)=no +_LT_TAGVAR(export_dynamic_flag_spec, $1)= +_LT_TAGVAR(hardcode_direct, $1)=no +_LT_TAGVAR(hardcode_direct_absolute, $1)=no +_LT_TAGVAR(hardcode_libdir_flag_spec, $1)= +_LT_TAGVAR(hardcode_libdir_separator, $1)= +_LT_TAGVAR(hardcode_minus_L, $1)=no +_LT_TAGVAR(hardcode_shlibpath_var, $1)=unsupported +_LT_TAGVAR(hardcode_automatic, $1)=no +_LT_TAGVAR(inherit_rpath, $1)=no +_LT_TAGVAR(module_cmds, $1)= +_LT_TAGVAR(module_expsym_cmds, $1)= +_LT_TAGVAR(link_all_deplibs, $1)=unknown +_LT_TAGVAR(old_archive_cmds, $1)=$old_archive_cmds +_LT_TAGVAR(reload_flag, $1)=$reload_flag +_LT_TAGVAR(reload_cmds, $1)=$reload_cmds +_LT_TAGVAR(no_undefined_flag, $1)= +_LT_TAGVAR(whole_archive_flag_spec, $1)= +_LT_TAGVAR(enable_shared_with_static_runtimes, $1)=no + +# Source file extension for C++ test sources. +ac_ext=cpp + +# Object file extension for compiled C++ test sources. +objext=o +_LT_TAGVAR(objext, $1)=$objext + +# No sense in running all these tests if we already determined that +# the CXX compiler isn't working. Some variables (like enable_shared) +# are currently assumed to apply to all compilers on this platform, +# and will be corrupted by setting them based on a non-working compiler. +if test yes != "$_lt_caught_CXX_error"; then + # Code to be used in simple compile tests + lt_simple_compile_test_code="int some_variable = 0;" + + # Code to be used in simple link tests + lt_simple_link_test_code='int main(int, char *[[]]) { return(0); }' + + # ltmain only uses $CC for tagged configurations so make sure $CC is set. + _LT_TAG_COMPILER + + # save warnings/boilerplate of simple test code + _LT_COMPILER_BOILERPLATE + _LT_LINKER_BOILERPLATE + + # Allow CC to be a program name with arguments. + lt_save_CC=$CC + lt_save_CFLAGS=$CFLAGS + lt_save_LD=$LD + lt_save_GCC=$GCC + GCC=$GXX + lt_save_with_gnu_ld=$with_gnu_ld + lt_save_path_LD=$lt_cv_path_LD + if test -n "${lt_cv_prog_gnu_ldcxx+set}"; then + lt_cv_prog_gnu_ld=$lt_cv_prog_gnu_ldcxx + else + $as_unset lt_cv_prog_gnu_ld + fi + if test -n "${lt_cv_path_LDCXX+set}"; then + lt_cv_path_LD=$lt_cv_path_LDCXX + else + $as_unset lt_cv_path_LD + fi + test -z "${LDCXX+set}" || LD=$LDCXX + CC=${CXX-"c++"} + CFLAGS=$CXXFLAGS + compiler=$CC + _LT_TAGVAR(compiler, $1)=$CC + _LT_CC_BASENAME([$compiler]) + + if test -n "$compiler"; then + # We don't want -fno-exception when compiling C++ code, so set the + # no_builtin_flag separately + if test yes = "$GXX"; then + _LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)=' -fno-builtin' + else + _LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)= + fi + + if test yes = "$GXX"; then + # Set up default GNU C++ configuration + + LT_PATH_LD + + # Check if GNU C++ uses GNU ld as the underlying linker, since the + # archiving commands below assume that GNU ld is being used. + if test yes = "$with_gnu_ld"; then + _LT_TAGVAR(archive_cmds, $1)='$CC $pic_flag -shared -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC $pic_flag -shared -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic' + + # If archive_cmds runs LD, not CC, wlarc should be empty + # XXX I think wlarc can be eliminated in ltcf-cxx, but I need to + # investigate it a little bit more. (MM) + wlarc='$wl' + + # ancient GNU ld didn't support --whole-archive et. al. + if eval "`$CC -print-prog-name=ld` --help 2>&1" | + $GREP 'no-whole-archive' > /dev/null; then + _LT_TAGVAR(whole_archive_flag_spec, $1)=$wlarc'--whole-archive$convenience '$wlarc'--no-whole-archive' + else + _LT_TAGVAR(whole_archive_flag_spec, $1)= + fi + else + with_gnu_ld=no + wlarc= + + # A generic and very simple default shared library creation + # command for GNU C++ for the case where it uses the native + # linker, instead of GNU ld. If possible, this setting should + # overridden to take advantage of the native linker features on + # the platform it is being used on. + _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -o $lib' + fi + + # Commands to make compiler produce verbose output that lists + # what "hidden" libraries, object files and flags are used when + # linking a shared library. + output_verbose_link_cmd='$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP -v "^Configured with:" | $GREP " \-L"' + + else + GXX=no + with_gnu_ld=no + wlarc= + fi + + # PORTME: fill in a description of your system's C++ link characteristics + AC_MSG_CHECKING([whether the $compiler linker ($LD) supports shared libraries]) + _LT_TAGVAR(ld_shlibs, $1)=yes + case $host_os in + aix3*) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + aix[[4-9]]*) + if test ia64 = "$host_cpu"; then + # On IA64, the linker does run time linking by default, so we don't + # have to do anything special. + aix_use_runtimelinking=no + exp_sym_flag='-Bexport' + no_entry_flag= + else + aix_use_runtimelinking=no + + # Test if we are trying to use run time linking or normal + # AIX style linking. If -brtl is somewhere in LDFLAGS, we + # have runtime linking enabled, and use it for executables. + # For shared libraries, we enable/disable runtime linking + # depending on the kind of the shared library created - + # when "with_aix_soname,aix_use_runtimelinking" is: + # "aix,no" lib.a(lib.so.V) shared, rtl:no, for executables + # "aix,yes" lib.so shared, rtl:yes, for executables + # lib.a static archive + # "both,no" lib.so.V(shr.o) shared, rtl:yes + # lib.a(lib.so.V) shared, rtl:no, for executables + # "both,yes" lib.so.V(shr.o) shared, rtl:yes, for executables + # lib.a(lib.so.V) shared, rtl:no + # "svr4,*" lib.so.V(shr.o) shared, rtl:yes, for executables + # lib.a static archive + case $host_os in aix4.[[23]]|aix4.[[23]].*|aix[[5-9]]*) + for ld_flag in $LDFLAGS; do + case $ld_flag in + *-brtl*) + aix_use_runtimelinking=yes + break + ;; + esac + done + if test svr4,no = "$with_aix_soname,$aix_use_runtimelinking"; then + # With aix-soname=svr4, we create the lib.so.V shared archives only, + # so we don't have lib.a shared libs to link our executables. + # We have to force runtime linking in this case. + aix_use_runtimelinking=yes + LDFLAGS="$LDFLAGS -Wl,-brtl" + fi + ;; + esac + + exp_sym_flag='-bexport' + no_entry_flag='-bnoentry' + fi + + # When large executables or shared objects are built, AIX ld can + # have problems creating the table of contents. If linking a library + # or program results in "error TOC overflow" add -mminimal-toc to + # CXXFLAGS/CFLAGS for g++/gcc. In the cases where that is not + # enough to fix the problem, add -Wl,-bbigtoc to LDFLAGS. + + _LT_TAGVAR(archive_cmds, $1)='' + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_direct_absolute, $1)=yes + _LT_TAGVAR(hardcode_libdir_separator, $1)=':' + _LT_TAGVAR(link_all_deplibs, $1)=yes + _LT_TAGVAR(file_list_spec, $1)='$wl-f,' + case $with_aix_soname,$aix_use_runtimelinking in + aix,*) ;; # no import file + svr4,* | *,yes) # use import file + # The Import File defines what to hardcode. + _LT_TAGVAR(hardcode_direct, $1)=no + _LT_TAGVAR(hardcode_direct_absolute, $1)=no + ;; + esac + + if test yes = "$GXX"; then + case $host_os in aix4.[[012]]|aix4.[[012]].*) + # We only want to do this on AIX 4.2 and lower, the check + # below for broken collect2 doesn't work under 4.3+ + collect2name=`$CC -print-prog-name=collect2` + if test -f "$collect2name" && + strings "$collect2name" | $GREP resolve_lib_name >/dev/null + then + # We have reworked collect2 + : + else + # We have old collect2 + _LT_TAGVAR(hardcode_direct, $1)=unsupported + # It fails to find uninstalled libraries when the uninstalled + # path is not listed in the libpath. Setting hardcode_minus_L + # to unsupported forces relinking + _LT_TAGVAR(hardcode_minus_L, $1)=yes + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)= + fi + esac + shared_flag='-shared' + if test yes = "$aix_use_runtimelinking"; then + shared_flag=$shared_flag' $wl-G' + fi + # Need to ensure runtime linking is disabled for the traditional + # shared library, or the linker may eventually find shared libraries + # /with/ Import File - we do not want to mix them. + shared_flag_aix='-shared' + shared_flag_svr4='-shared $wl-G' + else + # not using gcc + if test ia64 = "$host_cpu"; then + # VisualAge C++, Version 5.5 for AIX 5L for IA-64, Beta 3 Release + # chokes on -Wl,-G. The following line is correct: + shared_flag='-G' + else + if test yes = "$aix_use_runtimelinking"; then + shared_flag='$wl-G' + else + shared_flag='$wl-bM:SRE' + fi + shared_flag_aix='$wl-bM:SRE' + shared_flag_svr4='$wl-G' + fi + fi + + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-bexpall' + # It seems that -bexpall does not export symbols beginning with + # underscore (_), so it is better to generate a list of symbols to + # export. + _LT_TAGVAR(always_export_symbols, $1)=yes + if test aix,yes = "$with_aix_soname,$aix_use_runtimelinking"; then + # Warning - without using the other runtime loading flags (-brtl), + # -berok will link without error, but may produce a broken library. + # The "-G" linker flag allows undefined symbols. + _LT_TAGVAR(no_undefined_flag, $1)='-bernotok' + # Determine the default libpath from the value encoded in an empty + # executable. + _LT_SYS_MODULE_PATH_AIX([$1]) + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-blibpath:$libdir:'"$aix_libpath" + + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -o $output_objdir/$soname $libobjs $deplibs $wl'$no_entry_flag' $compiler_flags `if test -n "$allow_undefined_flag"; then func_echo_all "$wl$allow_undefined_flag"; else :; fi` $wl'$exp_sym_flag:\$export_symbols' '$shared_flag + else + if test ia64 = "$host_cpu"; then + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-R $libdir:/usr/lib:/lib' + _LT_TAGVAR(allow_undefined_flag, $1)="-z nodefs" + _LT_TAGVAR(archive_expsym_cmds, $1)="\$CC $shared_flag"' -o $output_objdir/$soname $libobjs $deplibs '"\$wl$no_entry_flag"' $compiler_flags $wl$allow_undefined_flag '"\$wl$exp_sym_flag:\$export_symbols" + else + # Determine the default libpath from the value encoded in an + # empty executable. + _LT_SYS_MODULE_PATH_AIX([$1]) + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-blibpath:$libdir:'"$aix_libpath" + # Warning - without using the other run time loading flags, + # -berok will link without error, but may produce a broken library. + _LT_TAGVAR(no_undefined_flag, $1)=' $wl-bernotok' + _LT_TAGVAR(allow_undefined_flag, $1)=' $wl-berok' + if test yes = "$with_gnu_ld"; then + # We only use this code for GNU lds that support --whole-archive. + _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive$convenience $wl--no-whole-archive' + else + # Exported symbols can be pulled into shared objects from archives + _LT_TAGVAR(whole_archive_flag_spec, $1)='$convenience' + fi + _LT_TAGVAR(archive_cmds_need_lc, $1)=yes + _LT_TAGVAR(archive_expsym_cmds, $1)='$RM -r $output_objdir/$realname.d~$MKDIR $output_objdir/$realname.d' + # -brtl affects multiple linker settings, -berok does not and is overridden later + compiler_flags_filtered='`func_echo_all "$compiler_flags " | $SED -e "s%-brtl\\([[, ]]\\)%-berok\\1%g"`' + if test svr4 != "$with_aix_soname"; then + # This is similar to how AIX traditionally builds its shared + # libraries. Need -bnortl late, we may have -brtl in LDFLAGS. + _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$CC '$shared_flag_aix' -o $output_objdir/$realname.d/$soname $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$AR $AR_FLAGS $output_objdir/$libname$release.a $output_objdir/$realname.d/$soname' + fi + if test aix != "$with_aix_soname"; then + _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$CC '$shared_flag_svr4' -o $output_objdir/$realname.d/$shared_archive_member_spec.o $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$STRIP -e $output_objdir/$realname.d/$shared_archive_member_spec.o~( func_echo_all "#! $soname($shared_archive_member_spec.o)"; if test shr_64 = "$shared_archive_member_spec"; then func_echo_all "# 64"; else func_echo_all "# 32"; fi; cat $export_symbols ) > $output_objdir/$realname.d/$shared_archive_member_spec.imp~$AR $AR_FLAGS $output_objdir/$soname $output_objdir/$realname.d/$shared_archive_member_spec.o $output_objdir/$realname.d/$shared_archive_member_spec.imp' + else + # used by -dlpreopen to get the symbols + _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$MV $output_objdir/$realname.d/$soname $output_objdir' + fi + _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$RM -r $output_objdir/$realname.d' + fi + fi + ;; + + beos*) + if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then + _LT_TAGVAR(allow_undefined_flag, $1)=unsupported + # Joseph Beckenbach says some releases of gcc + # support --undefined. This deserves some investigation. FIXME + _LT_TAGVAR(archive_cmds, $1)='$CC -nostart $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + else + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + + chorus*) + case $cc_basename in + *) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + esac + ;; + + cygwin* | mingw* | pw32* | cegcc*) + case $GXX,$cc_basename in + ,cl* | no,cl*) + # Native MSVC + # hardcode_libdir_flag_spec is actually meaningless, as there is + # no search path for DLLs. + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)=' ' + _LT_TAGVAR(allow_undefined_flag, $1)=unsupported + _LT_TAGVAR(always_export_symbols, $1)=yes + _LT_TAGVAR(file_list_spec, $1)='@' + # Tell ltmain to make .lib files, not .a files. + libext=lib + # Tell ltmain to make .dll files, not .so files. + shrext_cmds=.dll + # FIXME: Setting linknames here is a bad hack. + _LT_TAGVAR(archive_cmds, $1)='$CC -o $output_objdir/$soname $libobjs $compiler_flags $deplibs -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~linknames=' + _LT_TAGVAR(archive_expsym_cmds, $1)='if _LT_DLL_DEF_P([$export_symbols]); then + cp "$export_symbols" "$output_objdir/$soname.def"; + echo "$tool_output_objdir$soname.def" > "$output_objdir/$soname.exp"; + else + $SED -e '\''s/^/-link -EXPORT:/'\'' < $export_symbols > $output_objdir/$soname.exp; + fi~ + $CC -o $tool_output_objdir$soname $libobjs $compiler_flags $deplibs "@$tool_output_objdir$soname.exp" -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~ + linknames=' + # The linker will not automatically build a static lib if we build a DLL. + # _LT_TAGVAR(old_archive_from_new_cmds, $1)='true' + _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes + # Don't use ranlib + _LT_TAGVAR(old_postinstall_cmds, $1)='chmod 644 $oldlib' + _LT_TAGVAR(postlink_cmds, $1)='lt_outputfile="@OUTPUT@"~ + lt_tool_outputfile="@TOOL_OUTPUT@"~ + case $lt_outputfile in + *.exe|*.EXE) ;; + *) + lt_outputfile=$lt_outputfile.exe + lt_tool_outputfile=$lt_tool_outputfile.exe + ;; + esac~ + func_to_tool_file "$lt_outputfile"~ + if test : != "$MANIFEST_TOOL" && test -f "$lt_outputfile.manifest"; then + $MANIFEST_TOOL -manifest "$lt_tool_outputfile.manifest" -outputresource:"$lt_tool_outputfile" || exit 1; + $RM "$lt_outputfile.manifest"; + fi' + ;; + *) + # g++ + # _LT_TAGVAR(hardcode_libdir_flag_spec, $1) is actually meaningless, + # as there is no search path for DLLs. + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-all-symbols' + _LT_TAGVAR(allow_undefined_flag, $1)=unsupported + _LT_TAGVAR(always_export_symbols, $1)=no + _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes + + if $LD --help 2>&1 | $GREP 'auto-import' > /dev/null; then + _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib' + # If the export-symbols file already is a .def file, use it as + # is; otherwise, prepend EXPORTS... + _LT_TAGVAR(archive_expsym_cmds, $1)='if _LT_DLL_DEF_P([$export_symbols]); then + cp $export_symbols $output_objdir/$soname.def; + else + echo EXPORTS > $output_objdir/$soname.def; + cat $export_symbols >> $output_objdir/$soname.def; + fi~ + $CC -shared -nostdlib $output_objdir/$soname.def $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib' + else + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + esac + ;; + darwin* | rhapsody*) + _LT_DARWIN_LINKER_FEATURES($1) + ;; + + os2*) + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir' + _LT_TAGVAR(hardcode_minus_L, $1)=yes + _LT_TAGVAR(allow_undefined_flag, $1)=unsupported + shrext_cmds=.dll + _LT_TAGVAR(archive_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~ + $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~ + $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~ + $ECHO EXPORTS >> $output_objdir/$libname.def~ + emxexp $libobjs | $SED /"_DLL_InitTerm"/d >> $output_objdir/$libname.def~ + $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~ + emximp -o $lib $output_objdir/$libname.def' + _LT_TAGVAR(archive_expsym_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~ + $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~ + $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~ + $ECHO EXPORTS >> $output_objdir/$libname.def~ + prefix_cmds="$SED"~ + if test EXPORTS = "`$SED 1q $export_symbols`"; then + prefix_cmds="$prefix_cmds -e 1d"; + fi~ + prefix_cmds="$prefix_cmds -e \"s/^\(.*\)$/_\1/g\""~ + cat $export_symbols | $prefix_cmds >> $output_objdir/$libname.def~ + $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~ + emximp -o $lib $output_objdir/$libname.def' + _LT_TAGVAR(old_archive_From_new_cmds, $1)='emximp -o $output_objdir/${libname}_dll.a $output_objdir/$libname.def' + _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes + ;; + + dgux*) + case $cc_basename in + ec++*) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + ghcx*) + # Green Hills C++ Compiler + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + *) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + esac + ;; + + freebsd2.*) + # C++ shared libraries reported to be fairly broken before + # switch to ELF + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + + freebsd-elf*) + _LT_TAGVAR(archive_cmds_need_lc, $1)=no + ;; + + freebsd* | dragonfly*) + # FreeBSD 3 and later use GNU C++ and GNU ld with standard ELF + # conventions + _LT_TAGVAR(ld_shlibs, $1)=yes + ;; + + haiku*) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(link_all_deplibs, $1)=yes + ;; + + hpux9*) + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl+b $wl$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E' + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_minus_L, $1)=yes # Not in the search PATH, + # but as the default + # location of the library. + + case $cc_basename in + CC*) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + aCC*) + _LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/$soname~$CC -b $wl+b $wl$install_libdir -o $output_objdir/$soname $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib' + # Commands to make compiler produce verbose output that lists + # what "hidden" libraries, object files and flags are used when + # linking a shared library. + # + # There doesn't appear to be a way to prevent this compiler from + # explicitly linking system object files so we need to strip them + # from the output so that they don't get included in the library + # dependencies. + output_verbose_link_cmd='templist=`($CC -b $CFLAGS -v conftest.$objext 2>&1) | $EGREP " \-L"`; list= ; for z in $templist; do case $z in conftest.$objext) list="$list $z";; *.$objext);; *) list="$list $z";;esac; done; func_echo_all "$list"' + ;; + *) + if test yes = "$GXX"; then + _LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/$soname~$CC -shared -nostdlib $pic_flag $wl+b $wl$install_libdir -o $output_objdir/$soname $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib' + else + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + esac + ;; + + hpux10*|hpux11*) + if test no = "$with_gnu_ld"; then + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl+b $wl$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + + case $host_cpu in + hppa*64*|ia64*) + ;; + *) + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E' + ;; + esac + fi + case $host_cpu in + hppa*64*|ia64*) + _LT_TAGVAR(hardcode_direct, $1)=no + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + ;; + *) + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_direct_absolute, $1)=yes + _LT_TAGVAR(hardcode_minus_L, $1)=yes # Not in the search PATH, + # but as the default + # location of the library. + ;; + esac + + case $cc_basename in + CC*) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + aCC*) + case $host_cpu in + hppa*64*) + _LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags' + ;; + ia64*) + _LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname $wl+nodefaultrpath -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags' + ;; + *) + _LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags' + ;; + esac + # Commands to make compiler produce verbose output that lists + # what "hidden" libraries, object files and flags are used when + # linking a shared library. + # + # There doesn't appear to be a way to prevent this compiler from + # explicitly linking system object files so we need to strip them + # from the output so that they don't get included in the library + # dependencies. + output_verbose_link_cmd='templist=`($CC -b $CFLAGS -v conftest.$objext 2>&1) | $GREP " \-L"`; list= ; for z in $templist; do case $z in conftest.$objext) list="$list $z";; *.$objext);; *) list="$list $z";;esac; done; func_echo_all "$list"' + ;; + *) + if test yes = "$GXX"; then + if test no = "$with_gnu_ld"; then + case $host_cpu in + hppa*64*) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib -fPIC $wl+h $wl$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags' + ;; + ia64*) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib $pic_flag $wl+h $wl$soname $wl+nodefaultrpath -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags' + ;; + *) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib $pic_flag $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags' + ;; + esac + fi + else + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + esac + ;; + + interix[[3-9]]*) + _LT_TAGVAR(hardcode_direct, $1)=no + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E' + # Hack: On Interix 3.x, we cannot compile PIC because of a broken gcc. + # Instead, shared libraries are loaded at an image base (0x10000000 by + # default) and relocated if they conflict, which is a slow very memory + # consuming and fragmenting process. To avoid this, we pick a random, + # 256 KiB-aligned image base between 0x50000000 and 0x6FFC0000 at link + # time. Moving up from 0x10000000 also allows more sbrk(2) space. + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='sed "s|^|_|" $export_symbols >$output_objdir/$soname.expsym~$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--retain-symbols-file,$output_objdir/$soname.expsym $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib' + ;; + irix5* | irix6*) + case $cc_basename in + CC*) + # SGI C++ + _LT_TAGVAR(archive_cmds, $1)='$CC -shared -all -multigot $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib' + + # Archives containing C++ object files must be created using + # "CC -ar", where "CC" is the IRIX C++ compiler. This is + # necessary to make sure instantiated templates are included + # in the archive. + _LT_TAGVAR(old_archive_cmds, $1)='$CC -ar -WR,-u -o $oldlib $oldobjs' + ;; + *) + if test yes = "$GXX"; then + if test no = "$with_gnu_ld"; then + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib' + else + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` -o $lib' + fi + fi + _LT_TAGVAR(link_all_deplibs, $1)=yes + ;; + esac + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + _LT_TAGVAR(inherit_rpath, $1)=yes + ;; + + linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*) + case $cc_basename in + KCC*) + # Kuck and Associates, Inc. (KAI) C++ Compiler + + # KCC will only create a shared library if the output file + # ends with ".so" (or ".sl" for HP-UX), so rename the library + # to its proper name (with version) after linking. + _LT_TAGVAR(archive_cmds, $1)='tempext=`echo $shared_ext | $SED -e '\''s/\([[^()0-9A-Za-z{}]]\)/\\\\\1/g'\''`; templib=`echo $lib | $SED -e "s/\$tempext\..*/.so/"`; $CC $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags --soname $soname -o \$templib; mv \$templib $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='tempext=`echo $shared_ext | $SED -e '\''s/\([[^()0-9A-Za-z{}]]\)/\\\\\1/g'\''`; templib=`echo $lib | $SED -e "s/\$tempext\..*/.so/"`; $CC $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags --soname $soname -o \$templib $wl-retain-symbols-file,$export_symbols; mv \$templib $lib' + # Commands to make compiler produce verbose output that lists + # what "hidden" libraries, object files and flags are used when + # linking a shared library. + # + # There doesn't appear to be a way to prevent this compiler from + # explicitly linking system object files so we need to strip them + # from the output so that they don't get included in the library + # dependencies. + output_verbose_link_cmd='templist=`$CC $CFLAGS -v conftest.$objext -o libconftest$shared_ext 2>&1 | $GREP "ld"`; rm -f libconftest$shared_ext; list= ; for z in $templist; do case $z in conftest.$objext) list="$list $z";; *.$objext);; *) list="$list $z";;esac; done; func_echo_all "$list"' + + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic' + + # Archives containing C++ object files must be created using + # "CC -Bstatic", where "CC" is the KAI C++ compiler. + _LT_TAGVAR(old_archive_cmds, $1)='$CC -Bstatic -o $oldlib $oldobjs' + ;; + icpc* | ecpc* ) + # Intel C++ + with_gnu_ld=yes + # version 8.0 and above of icpc choke on multiply defined symbols + # if we add $predep_objects and $postdep_objects, however 7.1 and + # earlier do not add the objects themselves. + case `$CC -V 2>&1` in + *"Version 7."*) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + ;; + *) # Version 8.0 or newer + tmp_idyn= + case $host_cpu in + ia64*) tmp_idyn=' -i_dynamic';; + esac + _LT_TAGVAR(archive_cmds, $1)='$CC -shared'"$tmp_idyn"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared'"$tmp_idyn"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + ;; + esac + _LT_TAGVAR(archive_cmds_need_lc, $1)=no + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic' + _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive$convenience $wl--no-whole-archive' + ;; + pgCC* | pgcpp*) + # Portland Group C++ compiler + case `$CC -V` in + *pgCC\ [[1-5]].* | *pgcpp\ [[1-5]].*) + _LT_TAGVAR(prelink_cmds, $1)='tpldir=Template.dir~ + rm -rf $tpldir~ + $CC --prelink_objects --instantiation_dir $tpldir $objs $libobjs $compile_deplibs~ + compile_command="$compile_command `find $tpldir -name \*.o | sort | $NL2SP`"' + _LT_TAGVAR(old_archive_cmds, $1)='tpldir=Template.dir~ + rm -rf $tpldir~ + $CC --prelink_objects --instantiation_dir $tpldir $oldobjs$old_deplibs~ + $AR $AR_FLAGS $oldlib$oldobjs$old_deplibs `find $tpldir -name \*.o | sort | $NL2SP`~ + $RANLIB $oldlib' + _LT_TAGVAR(archive_cmds, $1)='tpldir=Template.dir~ + rm -rf $tpldir~ + $CC --prelink_objects --instantiation_dir $tpldir $predep_objects $libobjs $deplibs $convenience $postdep_objects~ + $CC -shared $pic_flag $predep_objects $libobjs $deplibs `find $tpldir -name \*.o | sort | $NL2SP` $postdep_objects $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='tpldir=Template.dir~ + rm -rf $tpldir~ + $CC --prelink_objects --instantiation_dir $tpldir $predep_objects $libobjs $deplibs $convenience $postdep_objects~ + $CC -shared $pic_flag $predep_objects $libobjs $deplibs `find $tpldir -name \*.o | sort | $NL2SP` $postdep_objects $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + ;; + *) # Version 6 and above use weak symbols + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + ;; + esac + + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl--rpath $wl$libdir' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic' + _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`for conv in $convenience\"\"; do test -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive' + ;; + cxx*) + # Compaq C++ + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname -o $lib $wl-retain-symbols-file $wl$export_symbols' + + runpath_var=LD_RUN_PATH + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-rpath $libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + + # Commands to make compiler produce verbose output that lists + # what "hidden" libraries, object files and flags are used when + # linking a shared library. + # + # There doesn't appear to be a way to prevent this compiler from + # explicitly linking system object files so we need to strip them + # from the output so that they don't get included in the library + # dependencies. + output_verbose_link_cmd='templist=`$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP "ld"`; templist=`func_echo_all "$templist" | $SED "s/\(^.*ld.*\)\( .*ld .*$\)/\1/"`; list= ; for z in $templist; do case $z in conftest.$objext) list="$list $z";; *.$objext);; *) list="$list $z";;esac; done; func_echo_all "X$list" | $Xsed' + ;; + xl* | mpixl* | bgxl*) + # IBM XL 8.0 on PPC, with GNU ld + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic' + _LT_TAGVAR(archive_cmds, $1)='$CC -qmkshrobj $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + if test yes = "$supports_anon_versioning"; then + _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $output_objdir/$libname.ver~ + cat $export_symbols | sed -e "s/\(.*\)/\1;/" >> $output_objdir/$libname.ver~ + echo "local: *; };" >> $output_objdir/$libname.ver~ + $CC -qmkshrobj $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-version-script $wl$output_objdir/$libname.ver -o $lib' + fi + ;; + *) + case `$CC -V 2>&1 | sed 5q` in + *Sun\ C*) + # Sun C++ 5.9 + _LT_TAGVAR(no_undefined_flag, $1)=' -zdefs' + _LT_TAGVAR(archive_cmds, $1)='$CC -G$allow_undefined_flag -h$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -G$allow_undefined_flag -h$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-retain-symbols-file $wl$export_symbols' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir' + _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`new_convenience=; for conv in $convenience\"\"; do test -z \"$conv\" || new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive' + _LT_TAGVAR(compiler_needs_object, $1)=yes + + # Not sure whether something based on + # $CC $CFLAGS -v conftest.$objext -o libconftest$shared_ext 2>&1 + # would be better. + output_verbose_link_cmd='func_echo_all' + + # Archives containing C++ object files must be created using + # "CC -xar", where "CC" is the Sun C++ compiler. This is + # necessary to make sure instantiated templates are included + # in the archive. + _LT_TAGVAR(old_archive_cmds, $1)='$CC -xar -o $oldlib $oldobjs' + ;; + esac + ;; + esac + ;; + + lynxos*) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + + m88k*) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + + mvs*) + case $cc_basename in + cxx*) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + *) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + esac + ;; + + netbsd*) + if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then + _LT_TAGVAR(archive_cmds, $1)='$LD -Bshareable -o $lib $predep_objects $libobjs $deplibs $postdep_objects $linker_flags' + wlarc= + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir' + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + fi + # Workaround some broken pre-1.5 toolchains + output_verbose_link_cmd='$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP conftest.$objext | $SED -e "s:-lgcc -lc -lgcc::"' + ;; + + *nto* | *qnx*) + _LT_TAGVAR(ld_shlibs, $1)=yes + ;; + + openbsd* | bitrig*) + if test -f /usr/libexec/ld.so; then + _LT_TAGVAR(hardcode_direct, $1)=yes + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + _LT_TAGVAR(hardcode_direct_absolute, $1)=yes + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -o $lib' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir' + if test -z "`echo __ELF__ | $CC -E - | grep __ELF__`"; then + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-retain-symbols-file,$export_symbols -o $lib' + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E' + _LT_TAGVAR(whole_archive_flag_spec, $1)=$wlarc'--whole-archive$convenience '$wlarc'--no-whole-archive' + fi + output_verbose_link_cmd=func_echo_all + else + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + + osf3* | osf4* | osf5*) + case $cc_basename in + KCC*) + # Kuck and Associates, Inc. (KAI) C++ Compiler + + # KCC will only create a shared library if the output file + # ends with ".so" (or ".sl" for HP-UX), so rename the library + # to its proper name (with version) after linking. + _LT_TAGVAR(archive_cmds, $1)='tempext=`echo $shared_ext | $SED -e '\''s/\([[^()0-9A-Za-z{}]]\)/\\\\\1/g'\''`; templib=`echo "$lib" | $SED -e "s/\$tempext\..*/.so/"`; $CC $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags --soname $soname -o \$templib; mv \$templib $lib' + + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + + # Archives containing C++ object files must be created using + # the KAI C++ compiler. + case $host in + osf3*) _LT_TAGVAR(old_archive_cmds, $1)='$CC -Bstatic -o $oldlib $oldobjs' ;; + *) _LT_TAGVAR(old_archive_cmds, $1)='$CC -o $oldlib $oldobjs' ;; + esac + ;; + RCC*) + # Rational C++ 2.4.1 + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + cxx*) + case $host in + osf3*) + _LT_TAGVAR(allow_undefined_flag, $1)=' $wl-expect_unresolved $wl\*' + _LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $soname `test -n "$verstring" && func_echo_all "$wl-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + ;; + *) + _LT_TAGVAR(allow_undefined_flag, $1)=' -expect_unresolved \*' + _LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -msym -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='for i in `cat $export_symbols`; do printf "%s %s\\n" -exported_symbol "\$i" >> $lib.exp; done~ + echo "-hidden">> $lib.exp~ + $CC -shared$allow_undefined_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -msym -soname $soname $wl-input $wl$lib.exp `test -n "$verstring" && $ECHO "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib~ + $RM $lib.exp' + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-rpath $libdir' + ;; + esac + + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + + # Commands to make compiler produce verbose output that lists + # what "hidden" libraries, object files and flags are used when + # linking a shared library. + # + # There doesn't appear to be a way to prevent this compiler from + # explicitly linking system object files so we need to strip them + # from the output so that they don't get included in the library + # dependencies. + output_verbose_link_cmd='templist=`$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP "ld" | $GREP -v "ld:"`; templist=`func_echo_all "$templist" | $SED "s/\(^.*ld.*\)\( .*ld.*$\)/\1/"`; list= ; for z in $templist; do case $z in conftest.$objext) list="$list $z";; *.$objext);; *) list="$list $z";;esac; done; func_echo_all "$list"' + ;; + *) + if test yes,no = "$GXX,$with_gnu_ld"; then + _LT_TAGVAR(allow_undefined_flag, $1)=' $wl-expect_unresolved $wl\*' + case $host in + osf3*) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib $allow_undefined_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib' + ;; + *) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -nostdlib $allow_undefined_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-msym $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib' + ;; + esac + + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=: + + # Commands to make compiler produce verbose output that lists + # what "hidden" libraries, object files and flags are used when + # linking a shared library. + output_verbose_link_cmd='$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP -v "^Configured with:" | $GREP " \-L"' + + else + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + fi + ;; + esac + ;; + + psos*) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + + sunos4*) + case $cc_basename in + CC*) + # Sun C++ 4.x + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + lcc*) + # Lucid + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + *) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + esac + ;; + + solaris*) + case $cc_basename in + CC* | sunCC*) + # Sun C++ 4.2, 5.x and Centerline C++ + _LT_TAGVAR(archive_cmds_need_lc,$1)=yes + _LT_TAGVAR(no_undefined_flag, $1)=' -zdefs' + _LT_TAGVAR(archive_cmds, $1)='$CC -G$allow_undefined_flag -h$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~ + $CC -G$allow_undefined_flag $wl-M $wl$lib.exp -h$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags~$RM $lib.exp' + + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir' + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + case $host_os in + solaris2.[[0-5]] | solaris2.[[0-5]].*) ;; + *) + # The compiler driver will combine and reorder linker options, + # but understands '-z linker_flag'. + # Supported since Solaris 2.6 (maybe 2.5.1?) + _LT_TAGVAR(whole_archive_flag_spec, $1)='-z allextract$convenience -z defaultextract' + ;; + esac + _LT_TAGVAR(link_all_deplibs, $1)=yes + + output_verbose_link_cmd='func_echo_all' + + # Archives containing C++ object files must be created using + # "CC -xar", where "CC" is the Sun C++ compiler. This is + # necessary to make sure instantiated templates are included + # in the archive. + _LT_TAGVAR(old_archive_cmds, $1)='$CC -xar -o $oldlib $oldobjs' + ;; + gcx*) + # Green Hills C++ Compiler + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-h $wl$soname -o $lib' + + # The C++ compiler must be used to create the archive. + _LT_TAGVAR(old_archive_cmds, $1)='$CC $LDFLAGS -archive -o $oldlib $oldobjs' + ;; + *) + # GNU C++ compiler with Solaris linker + if test yes,no = "$GXX,$with_gnu_ld"; then + _LT_TAGVAR(no_undefined_flag, $1)=' $wl-z ${wl}defs' + if $CC --version | $GREP -v '^2\.7' > /dev/null; then + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-h $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~ + $CC -shared $pic_flag -nostdlib $wl-M $wl$lib.exp $wl-h $wl$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags~$RM $lib.exp' + + # Commands to make compiler produce verbose output that lists + # what "hidden" libraries, object files and flags are used when + # linking a shared library. + output_verbose_link_cmd='$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP -v "^Configured with:" | $GREP " \-L"' + else + # g++ 2.7 appears to require '-G' NOT '-shared' on this + # platform. + _LT_TAGVAR(archive_cmds, $1)='$CC -G -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-h $wl$soname -o $lib' + _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~ + $CC -G -nostdlib $wl-M $wl$lib.exp $wl-h $wl$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags~$RM $lib.exp' + + # Commands to make compiler produce verbose output that lists + # what "hidden" libraries, object files and flags are used when + # linking a shared library. + output_verbose_link_cmd='$CC -G $CFLAGS -v conftest.$objext 2>&1 | $GREP -v "^Configured with:" | $GREP " \-L"' + fi + + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-R $wl$libdir' + case $host_os in + solaris2.[[0-5]] | solaris2.[[0-5]].*) ;; + *) + _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl-z ${wl}allextract$convenience $wl-z ${wl}defaultextract' + ;; + esac + fi + ;; + esac + ;; + + sysv4*uw2* | sysv5OpenUNIX* | sysv5UnixWare7.[[01]].[[10]]* | unixware7* | sco3.2v5.0.[[024]]*) + _LT_TAGVAR(no_undefined_flag, $1)='$wl-z,text' + _LT_TAGVAR(archive_cmds_need_lc, $1)=no + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + runpath_var='LD_RUN_PATH' + + case $cc_basename in + CC*) + _LT_TAGVAR(archive_cmds, $1)='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + ;; + *) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + ;; + esac + ;; + + sysv5* | sco3.2v5* | sco5v6*) + # Note: We CANNOT use -z defs as we might desire, because we do not + # link with -lc, and that would cause any symbols used from libc to + # always be unresolved, which means just about no library would + # ever link correctly. If we're not using GNU ld we use -z text + # though, which does catch some bad symbols but isn't as heavy-handed + # as -z defs. + _LT_TAGVAR(no_undefined_flag, $1)='$wl-z,text' + _LT_TAGVAR(allow_undefined_flag, $1)='$wl-z,nodefs' + _LT_TAGVAR(archive_cmds_need_lc, $1)=no + _LT_TAGVAR(hardcode_shlibpath_var, $1)=no + _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-R,$libdir' + _LT_TAGVAR(hardcode_libdir_separator, $1)=':' + _LT_TAGVAR(link_all_deplibs, $1)=yes + _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-Bexport' + runpath_var='LD_RUN_PATH' + + case $cc_basename in + CC*) + _LT_TAGVAR(archive_cmds, $1)='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(old_archive_cmds, $1)='$CC -Tprelink_objects $oldobjs~ + '"$_LT_TAGVAR(old_archive_cmds, $1)" + _LT_TAGVAR(reload_cmds, $1)='$CC -Tprelink_objects $reload_objs~ + '"$_LT_TAGVAR(reload_cmds, $1)" + ;; + *) + _LT_TAGVAR(archive_cmds, $1)='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + ;; + esac + ;; + + tandem*) + case $cc_basename in + NCC*) + # NonStop-UX NCC 3.20 + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + *) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + esac + ;; + + vxworks*) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + + *) + # FIXME: insert proper C++ library support + _LT_TAGVAR(ld_shlibs, $1)=no + ;; + esac + + AC_MSG_RESULT([$_LT_TAGVAR(ld_shlibs, $1)]) + test no = "$_LT_TAGVAR(ld_shlibs, $1)" && can_build_shared=no + + _LT_TAGVAR(GCC, $1)=$GXX + _LT_TAGVAR(LD, $1)=$LD + + ## CAVEAT EMPTOR: + ## There is no encapsulation within the following macros, do not change + ## the running order or otherwise move them around unless you know exactly + ## what you are doing... + _LT_SYS_HIDDEN_LIBDEPS($1) + _LT_COMPILER_PIC($1) + _LT_COMPILER_C_O($1) + _LT_COMPILER_FILE_LOCKS($1) + _LT_LINKER_SHLIBS($1) + _LT_SYS_DYNAMIC_LINKER($1) + _LT_LINKER_HARDCODE_LIBPATH($1) + + _LT_CONFIG($1) + fi # test -n "$compiler" + + CC=$lt_save_CC + CFLAGS=$lt_save_CFLAGS + LDCXX=$LD + LD=$lt_save_LD + GCC=$lt_save_GCC + with_gnu_ld=$lt_save_with_gnu_ld + lt_cv_path_LDCXX=$lt_cv_path_LD + lt_cv_path_LD=$lt_save_path_LD + lt_cv_prog_gnu_ldcxx=$lt_cv_prog_gnu_ld + lt_cv_prog_gnu_ld=$lt_save_with_gnu_ld +fi # test yes != "$_lt_caught_CXX_error" + +AC_LANG_POP +])# _LT_LANG_CXX_CONFIG + + +# _LT_FUNC_STRIPNAME_CNF +# ---------------------- +# func_stripname_cnf prefix suffix name +# strip PREFIX and SUFFIX off of NAME. +# PREFIX and SUFFIX must not contain globbing or regex special +# characters, hashes, percent signs, but SUFFIX may contain a leading +# dot (in which case that matches only a dot). +# +# This function is identical to the (non-XSI) version of func_stripname, +# except this one can be used by m4 code that may be executed by configure, +# rather than the libtool script. +m4_defun([_LT_FUNC_STRIPNAME_CNF],[dnl +AC_REQUIRE([_LT_DECL_SED]) +AC_REQUIRE([_LT_PROG_ECHO_BACKSLASH]) +func_stripname_cnf () +{ + case @S|@2 in + .*) func_stripname_result=`$ECHO "@S|@3" | $SED "s%^@S|@1%%; s%\\\\@S|@2\$%%"`;; + *) func_stripname_result=`$ECHO "@S|@3" | $SED "s%^@S|@1%%; s%@S|@2\$%%"`;; + esac +} # func_stripname_cnf +])# _LT_FUNC_STRIPNAME_CNF + + +# _LT_SYS_HIDDEN_LIBDEPS([TAGNAME]) +# --------------------------------- +# Figure out "hidden" library dependencies from verbose +# compiler output when linking a shared library. +# Parse the compiler output and extract the necessary +# objects, libraries and library flags. +m4_defun([_LT_SYS_HIDDEN_LIBDEPS], +[m4_require([_LT_FILEUTILS_DEFAULTS])dnl +AC_REQUIRE([_LT_FUNC_STRIPNAME_CNF])dnl +# Dependencies to place before and after the object being linked: +_LT_TAGVAR(predep_objects, $1)= +_LT_TAGVAR(postdep_objects, $1)= +_LT_TAGVAR(predeps, $1)= +_LT_TAGVAR(postdeps, $1)= +_LT_TAGVAR(compiler_lib_search_path, $1)= + +dnl we can't use the lt_simple_compile_test_code here, +dnl because it contains code intended for an executable, +dnl not a library. It's possible we should let each +dnl tag define a new lt_????_link_test_code variable, +dnl but it's only used here... +m4_if([$1], [], [cat > conftest.$ac_ext <<_LT_EOF +int a; +void foo (void) { a = 0; } +_LT_EOF +], [$1], [CXX], [cat > conftest.$ac_ext <<_LT_EOF +class Foo +{ +public: + Foo (void) { a = 0; } +private: + int a; +}; +_LT_EOF +], [$1], [F77], [cat > conftest.$ac_ext <<_LT_EOF + subroutine foo + implicit none + integer*4 a + a=0 + return + end +_LT_EOF +], [$1], [FC], [cat > conftest.$ac_ext <<_LT_EOF + subroutine foo + implicit none + integer a + a=0 + return + end +_LT_EOF +], [$1], [GCJ], [cat > conftest.$ac_ext <<_LT_EOF +public class foo { + private int a; + public void bar (void) { + a = 0; + } +}; +_LT_EOF +], [$1], [GO], [cat > conftest.$ac_ext <<_LT_EOF +package foo +func foo() { +} +_LT_EOF +]) + +_lt_libdeps_save_CFLAGS=$CFLAGS +case "$CC $CFLAGS " in #( +*\ -flto*\ *) CFLAGS="$CFLAGS -fno-lto" ;; +*\ -fwhopr*\ *) CFLAGS="$CFLAGS -fno-whopr" ;; +*\ -fuse-linker-plugin*\ *) CFLAGS="$CFLAGS -fno-use-linker-plugin" ;; +esac + +dnl Parse the compiler output and extract the necessary +dnl objects, libraries and library flags. +if AC_TRY_EVAL(ac_compile); then + # Parse the compiler output and extract the necessary + # objects, libraries and library flags. + + # Sentinel used to keep track of whether or not we are before + # the conftest object file. + pre_test_object_deps_done=no + + for p in `eval "$output_verbose_link_cmd"`; do + case $prev$p in + + -L* | -R* | -l*) + # Some compilers place space between "-{L,R}" and the path. + # Remove the space. + if test x-L = "$p" || + test x-R = "$p"; then + prev=$p + continue + fi + + # Expand the sysroot to ease extracting the directories later. + if test -z "$prev"; then + case $p in + -L*) func_stripname_cnf '-L' '' "$p"; prev=-L; p=$func_stripname_result ;; + -R*) func_stripname_cnf '-R' '' "$p"; prev=-R; p=$func_stripname_result ;; + -l*) func_stripname_cnf '-l' '' "$p"; prev=-l; p=$func_stripname_result ;; + esac + fi + case $p in + =*) func_stripname_cnf '=' '' "$p"; p=$lt_sysroot$func_stripname_result ;; + esac + if test no = "$pre_test_object_deps_done"; then + case $prev in + -L | -R) + # Internal compiler library paths should come after those + # provided the user. The postdeps already come after the + # user supplied libs so there is no need to process them. + if test -z "$_LT_TAGVAR(compiler_lib_search_path, $1)"; then + _LT_TAGVAR(compiler_lib_search_path, $1)=$prev$p + else + _LT_TAGVAR(compiler_lib_search_path, $1)="${_LT_TAGVAR(compiler_lib_search_path, $1)} $prev$p" + fi + ;; + # The "-l" case would never come before the object being + # linked, so don't bother handling this case. + esac + else + if test -z "$_LT_TAGVAR(postdeps, $1)"; then + _LT_TAGVAR(postdeps, $1)=$prev$p + else + _LT_TAGVAR(postdeps, $1)="${_LT_TAGVAR(postdeps, $1)} $prev$p" + fi + fi + prev= + ;; + + *.lto.$objext) ;; # Ignore GCC LTO objects + *.$objext) + # This assumes that the test object file only shows up + # once in the compiler output. + if test "$p" = "conftest.$objext"; then + pre_test_object_deps_done=yes + continue + fi + + if test no = "$pre_test_object_deps_done"; then + if test -z "$_LT_TAGVAR(predep_objects, $1)"; then + _LT_TAGVAR(predep_objects, $1)=$p + else + _LT_TAGVAR(predep_objects, $1)="$_LT_TAGVAR(predep_objects, $1) $p" + fi + else + if test -z "$_LT_TAGVAR(postdep_objects, $1)"; then + _LT_TAGVAR(postdep_objects, $1)=$p + else + _LT_TAGVAR(postdep_objects, $1)="$_LT_TAGVAR(postdep_objects, $1) $p" + fi + fi + ;; + + *) ;; # Ignore the rest. + + esac + done + + # Clean up. + rm -f a.out a.exe +else + echo "libtool.m4: error: problem compiling $1 test program" +fi + +$RM -f confest.$objext +CFLAGS=$_lt_libdeps_save_CFLAGS + +# PORTME: override above test on systems where it is broken +m4_if([$1], [CXX], +[case $host_os in +interix[[3-9]]*) + # Interix 3.5 installs completely hosed .la files for C++, so rather than + # hack all around it, let's just trust "g++" to DTRT. + _LT_TAGVAR(predep_objects,$1)= + _LT_TAGVAR(postdep_objects,$1)= + _LT_TAGVAR(postdeps,$1)= + ;; +esac +]) + +case " $_LT_TAGVAR(postdeps, $1) " in +*" -lc "*) _LT_TAGVAR(archive_cmds_need_lc, $1)=no ;; +esac + _LT_TAGVAR(compiler_lib_search_dirs, $1)= +if test -n "${_LT_TAGVAR(compiler_lib_search_path, $1)}"; then + _LT_TAGVAR(compiler_lib_search_dirs, $1)=`echo " ${_LT_TAGVAR(compiler_lib_search_path, $1)}" | $SED -e 's! -L! !g' -e 's!^ !!'` +fi +_LT_TAGDECL([], [compiler_lib_search_dirs], [1], + [The directories searched by this compiler when creating a shared library]) +_LT_TAGDECL([], [predep_objects], [1], + [Dependencies to place before and after the objects being linked to + create a shared library]) +_LT_TAGDECL([], [postdep_objects], [1]) +_LT_TAGDECL([], [predeps], [1]) +_LT_TAGDECL([], [postdeps], [1]) +_LT_TAGDECL([], [compiler_lib_search_path], [1], + [The library search path used internally by the compiler when linking + a shared library]) +])# _LT_SYS_HIDDEN_LIBDEPS + + +# _LT_LANG_F77_CONFIG([TAG]) +# -------------------------- +# Ensure that the configuration variables for a Fortran 77 compiler are +# suitably defined. These variables are subsequently used by _LT_CONFIG +# to write the compiler configuration to 'libtool'. +m4_defun([_LT_LANG_F77_CONFIG], +[AC_LANG_PUSH(Fortran 77) +if test -z "$F77" || test no = "$F77"; then + _lt_disable_F77=yes +fi + +_LT_TAGVAR(archive_cmds_need_lc, $1)=no +_LT_TAGVAR(allow_undefined_flag, $1)= +_LT_TAGVAR(always_export_symbols, $1)=no +_LT_TAGVAR(archive_expsym_cmds, $1)= +_LT_TAGVAR(export_dynamic_flag_spec, $1)= +_LT_TAGVAR(hardcode_direct, $1)=no +_LT_TAGVAR(hardcode_direct_absolute, $1)=no +_LT_TAGVAR(hardcode_libdir_flag_spec, $1)= +_LT_TAGVAR(hardcode_libdir_separator, $1)= +_LT_TAGVAR(hardcode_minus_L, $1)=no +_LT_TAGVAR(hardcode_automatic, $1)=no +_LT_TAGVAR(inherit_rpath, $1)=no +_LT_TAGVAR(module_cmds, $1)= +_LT_TAGVAR(module_expsym_cmds, $1)= +_LT_TAGVAR(link_all_deplibs, $1)=unknown +_LT_TAGVAR(old_archive_cmds, $1)=$old_archive_cmds +_LT_TAGVAR(reload_flag, $1)=$reload_flag +_LT_TAGVAR(reload_cmds, $1)=$reload_cmds +_LT_TAGVAR(no_undefined_flag, $1)= +_LT_TAGVAR(whole_archive_flag_spec, $1)= +_LT_TAGVAR(enable_shared_with_static_runtimes, $1)=no + +# Source file extension for f77 test sources. +ac_ext=f + +# Object file extension for compiled f77 test sources. +objext=o +_LT_TAGVAR(objext, $1)=$objext + +# No sense in running all these tests if we already determined that +# the F77 compiler isn't working. Some variables (like enable_shared) +# are currently assumed to apply to all compilers on this platform, +# and will be corrupted by setting them based on a non-working compiler. +if test yes != "$_lt_disable_F77"; then + # Code to be used in simple compile tests + lt_simple_compile_test_code="\ + subroutine t + return + end +" + + # Code to be used in simple link tests + lt_simple_link_test_code="\ + program t + end +" + + # ltmain only uses $CC for tagged configurations so make sure $CC is set. + _LT_TAG_COMPILER + + # save warnings/boilerplate of simple test code + _LT_COMPILER_BOILERPLATE + _LT_LINKER_BOILERPLATE + + # Allow CC to be a program name with arguments. + lt_save_CC=$CC + lt_save_GCC=$GCC + lt_save_CFLAGS=$CFLAGS + CC=${F77-"f77"} + CFLAGS=$FFLAGS + compiler=$CC + _LT_TAGVAR(compiler, $1)=$CC + _LT_CC_BASENAME([$compiler]) + GCC=$G77 + if test -n "$compiler"; then + AC_MSG_CHECKING([if libtool supports shared libraries]) + AC_MSG_RESULT([$can_build_shared]) + + AC_MSG_CHECKING([whether to build shared libraries]) + test no = "$can_build_shared" && enable_shared=no + + # On AIX, shared libraries and static libraries use the same namespace, and + # are all built from PIC. + case $host_os in + aix3*) + test yes = "$enable_shared" && enable_static=no + if test -n "$RANLIB"; then + archive_cmds="$archive_cmds~\$RANLIB \$lib" + postinstall_cmds='$RANLIB $lib' + fi + ;; + aix[[4-9]]*) + if test ia64 != "$host_cpu"; then + case $enable_shared,$with_aix_soname,$aix_use_runtimelinking in + yes,aix,yes) ;; # shared object as lib.so file only + yes,svr4,*) ;; # shared object as lib.so archive member only + yes,*) enable_static=no ;; # shared object in lib.a archive as well + esac + fi + ;; + esac + AC_MSG_RESULT([$enable_shared]) + + AC_MSG_CHECKING([whether to build static libraries]) + # Make sure either enable_shared or enable_static is yes. + test yes = "$enable_shared" || enable_static=yes + AC_MSG_RESULT([$enable_static]) + + _LT_TAGVAR(GCC, $1)=$G77 + _LT_TAGVAR(LD, $1)=$LD + + ## CAVEAT EMPTOR: + ## There is no encapsulation within the following macros, do not change + ## the running order or otherwise move them around unless you know exactly + ## what you are doing... + _LT_COMPILER_PIC($1) + _LT_COMPILER_C_O($1) + _LT_COMPILER_FILE_LOCKS($1) + _LT_LINKER_SHLIBS($1) + _LT_SYS_DYNAMIC_LINKER($1) + _LT_LINKER_HARDCODE_LIBPATH($1) + + _LT_CONFIG($1) + fi # test -n "$compiler" + + GCC=$lt_save_GCC + CC=$lt_save_CC + CFLAGS=$lt_save_CFLAGS +fi # test yes != "$_lt_disable_F77" + +AC_LANG_POP +])# _LT_LANG_F77_CONFIG + + +# _LT_LANG_FC_CONFIG([TAG]) +# ------------------------- +# Ensure that the configuration variables for a Fortran compiler are +# suitably defined. These variables are subsequently used by _LT_CONFIG +# to write the compiler configuration to 'libtool'. +m4_defun([_LT_LANG_FC_CONFIG], +[AC_LANG_PUSH(Fortran) + +if test -z "$FC" || test no = "$FC"; then + _lt_disable_FC=yes +fi + +_LT_TAGVAR(archive_cmds_need_lc, $1)=no +_LT_TAGVAR(allow_undefined_flag, $1)= +_LT_TAGVAR(always_export_symbols, $1)=no +_LT_TAGVAR(archive_expsym_cmds, $1)= +_LT_TAGVAR(export_dynamic_flag_spec, $1)= +_LT_TAGVAR(hardcode_direct, $1)=no +_LT_TAGVAR(hardcode_direct_absolute, $1)=no +_LT_TAGVAR(hardcode_libdir_flag_spec, $1)= +_LT_TAGVAR(hardcode_libdir_separator, $1)= +_LT_TAGVAR(hardcode_minus_L, $1)=no +_LT_TAGVAR(hardcode_automatic, $1)=no +_LT_TAGVAR(inherit_rpath, $1)=no +_LT_TAGVAR(module_cmds, $1)= +_LT_TAGVAR(module_expsym_cmds, $1)= +_LT_TAGVAR(link_all_deplibs, $1)=unknown +_LT_TAGVAR(old_archive_cmds, $1)=$old_archive_cmds +_LT_TAGVAR(reload_flag, $1)=$reload_flag +_LT_TAGVAR(reload_cmds, $1)=$reload_cmds +_LT_TAGVAR(no_undefined_flag, $1)= +_LT_TAGVAR(whole_archive_flag_spec, $1)= +_LT_TAGVAR(enable_shared_with_static_runtimes, $1)=no + +# Source file extension for fc test sources. +ac_ext=${ac_fc_srcext-f} + +# Object file extension for compiled fc test sources. +objext=o +_LT_TAGVAR(objext, $1)=$objext + +# No sense in running all these tests if we already determined that +# the FC compiler isn't working. Some variables (like enable_shared) +# are currently assumed to apply to all compilers on this platform, +# and will be corrupted by setting them based on a non-working compiler. +if test yes != "$_lt_disable_FC"; then + # Code to be used in simple compile tests + lt_simple_compile_test_code="\ + subroutine t + return + end +" + + # Code to be used in simple link tests + lt_simple_link_test_code="\ + program t + end +" + + # ltmain only uses $CC for tagged configurations so make sure $CC is set. + _LT_TAG_COMPILER + + # save warnings/boilerplate of simple test code + _LT_COMPILER_BOILERPLATE + _LT_LINKER_BOILERPLATE + + # Allow CC to be a program name with arguments. + lt_save_CC=$CC + lt_save_GCC=$GCC + lt_save_CFLAGS=$CFLAGS + CC=${FC-"f95"} + CFLAGS=$FCFLAGS + compiler=$CC + GCC=$ac_cv_fc_compiler_gnu + + _LT_TAGVAR(compiler, $1)=$CC + _LT_CC_BASENAME([$compiler]) + + if test -n "$compiler"; then + AC_MSG_CHECKING([if libtool supports shared libraries]) + AC_MSG_RESULT([$can_build_shared]) + + AC_MSG_CHECKING([whether to build shared libraries]) + test no = "$can_build_shared" && enable_shared=no + + # On AIX, shared libraries and static libraries use the same namespace, and + # are all built from PIC. + case $host_os in + aix3*) + test yes = "$enable_shared" && enable_static=no + if test -n "$RANLIB"; then + archive_cmds="$archive_cmds~\$RANLIB \$lib" + postinstall_cmds='$RANLIB $lib' + fi + ;; + aix[[4-9]]*) + if test ia64 != "$host_cpu"; then + case $enable_shared,$with_aix_soname,$aix_use_runtimelinking in + yes,aix,yes) ;; # shared object as lib.so file only + yes,svr4,*) ;; # shared object as lib.so archive member only + yes,*) enable_static=no ;; # shared object in lib.a archive as well + esac + fi + ;; + esac + AC_MSG_RESULT([$enable_shared]) + + AC_MSG_CHECKING([whether to build static libraries]) + # Make sure either enable_shared or enable_static is yes. + test yes = "$enable_shared" || enable_static=yes + AC_MSG_RESULT([$enable_static]) + + _LT_TAGVAR(GCC, $1)=$ac_cv_fc_compiler_gnu + _LT_TAGVAR(LD, $1)=$LD + + ## CAVEAT EMPTOR: + ## There is no encapsulation within the following macros, do not change + ## the running order or otherwise move them around unless you know exactly + ## what you are doing... + _LT_SYS_HIDDEN_LIBDEPS($1) + _LT_COMPILER_PIC($1) + _LT_COMPILER_C_O($1) + _LT_COMPILER_FILE_LOCKS($1) + _LT_LINKER_SHLIBS($1) + _LT_SYS_DYNAMIC_LINKER($1) + _LT_LINKER_HARDCODE_LIBPATH($1) + + _LT_CONFIG($1) + fi # test -n "$compiler" + + GCC=$lt_save_GCC + CC=$lt_save_CC + CFLAGS=$lt_save_CFLAGS +fi # test yes != "$_lt_disable_FC" + +AC_LANG_POP +])# _LT_LANG_FC_CONFIG + + +# _LT_LANG_GCJ_CONFIG([TAG]) +# -------------------------- +# Ensure that the configuration variables for the GNU Java Compiler compiler +# are suitably defined. These variables are subsequently used by _LT_CONFIG +# to write the compiler configuration to 'libtool'. +m4_defun([_LT_LANG_GCJ_CONFIG], +[AC_REQUIRE([LT_PROG_GCJ])dnl +AC_LANG_SAVE + +# Source file extension for Java test sources. +ac_ext=java + +# Object file extension for compiled Java test sources. +objext=o +_LT_TAGVAR(objext, $1)=$objext + +# Code to be used in simple compile tests +lt_simple_compile_test_code="class foo {}" + +# Code to be used in simple link tests +lt_simple_link_test_code='public class conftest { public static void main(String[[]] argv) {}; }' + +# ltmain only uses $CC for tagged configurations so make sure $CC is set. +_LT_TAG_COMPILER + +# save warnings/boilerplate of simple test code +_LT_COMPILER_BOILERPLATE +_LT_LINKER_BOILERPLATE + +# Allow CC to be a program name with arguments. +lt_save_CC=$CC +lt_save_CFLAGS=$CFLAGS +lt_save_GCC=$GCC +GCC=yes +CC=${GCJ-"gcj"} +CFLAGS=$GCJFLAGS +compiler=$CC +_LT_TAGVAR(compiler, $1)=$CC +_LT_TAGVAR(LD, $1)=$LD +_LT_CC_BASENAME([$compiler]) + +# GCJ did not exist at the time GCC didn't implicitly link libc in. +_LT_TAGVAR(archive_cmds_need_lc, $1)=no + +_LT_TAGVAR(old_archive_cmds, $1)=$old_archive_cmds +_LT_TAGVAR(reload_flag, $1)=$reload_flag +_LT_TAGVAR(reload_cmds, $1)=$reload_cmds + +if test -n "$compiler"; then + _LT_COMPILER_NO_RTTI($1) + _LT_COMPILER_PIC($1) + _LT_COMPILER_C_O($1) + _LT_COMPILER_FILE_LOCKS($1) + _LT_LINKER_SHLIBS($1) + _LT_LINKER_HARDCODE_LIBPATH($1) + + _LT_CONFIG($1) +fi + +AC_LANG_RESTORE + +GCC=$lt_save_GCC +CC=$lt_save_CC +CFLAGS=$lt_save_CFLAGS +])# _LT_LANG_GCJ_CONFIG + + +# _LT_LANG_GO_CONFIG([TAG]) +# -------------------------- +# Ensure that the configuration variables for the GNU Go compiler +# are suitably defined. These variables are subsequently used by _LT_CONFIG +# to write the compiler configuration to 'libtool'. +m4_defun([_LT_LANG_GO_CONFIG], +[AC_REQUIRE([LT_PROG_GO])dnl +AC_LANG_SAVE + +# Source file extension for Go test sources. +ac_ext=go + +# Object file extension for compiled Go test sources. +objext=o +_LT_TAGVAR(objext, $1)=$objext + +# Code to be used in simple compile tests +lt_simple_compile_test_code="package main; func main() { }" + +# Code to be used in simple link tests +lt_simple_link_test_code='package main; func main() { }' + +# ltmain only uses $CC for tagged configurations so make sure $CC is set. +_LT_TAG_COMPILER + +# save warnings/boilerplate of simple test code +_LT_COMPILER_BOILERPLATE +_LT_LINKER_BOILERPLATE + +# Allow CC to be a program name with arguments. +lt_save_CC=$CC +lt_save_CFLAGS=$CFLAGS +lt_save_GCC=$GCC +GCC=yes +CC=${GOC-"gccgo"} +CFLAGS=$GOFLAGS +compiler=$CC +_LT_TAGVAR(compiler, $1)=$CC +_LT_TAGVAR(LD, $1)=$LD +_LT_CC_BASENAME([$compiler]) + +# Go did not exist at the time GCC didn't implicitly link libc in. +_LT_TAGVAR(archive_cmds_need_lc, $1)=no + +_LT_TAGVAR(old_archive_cmds, $1)=$old_archive_cmds +_LT_TAGVAR(reload_flag, $1)=$reload_flag +_LT_TAGVAR(reload_cmds, $1)=$reload_cmds + +if test -n "$compiler"; then + _LT_COMPILER_NO_RTTI($1) + _LT_COMPILER_PIC($1) + _LT_COMPILER_C_O($1) + _LT_COMPILER_FILE_LOCKS($1) + _LT_LINKER_SHLIBS($1) + _LT_LINKER_HARDCODE_LIBPATH($1) + + _LT_CONFIG($1) +fi + +AC_LANG_RESTORE + +GCC=$lt_save_GCC +CC=$lt_save_CC +CFLAGS=$lt_save_CFLAGS +])# _LT_LANG_GO_CONFIG + + +# _LT_LANG_RC_CONFIG([TAG]) +# ------------------------- +# Ensure that the configuration variables for the Windows resource compiler +# are suitably defined. These variables are subsequently used by _LT_CONFIG +# to write the compiler configuration to 'libtool'. +m4_defun([_LT_LANG_RC_CONFIG], +[AC_REQUIRE([LT_PROG_RC])dnl +AC_LANG_SAVE + +# Source file extension for RC test sources. +ac_ext=rc + +# Object file extension for compiled RC test sources. +objext=o +_LT_TAGVAR(objext, $1)=$objext + +# Code to be used in simple compile tests +lt_simple_compile_test_code='sample MENU { MENUITEM "&Soup", 100, CHECKED }' + +# Code to be used in simple link tests +lt_simple_link_test_code=$lt_simple_compile_test_code + +# ltmain only uses $CC for tagged configurations so make sure $CC is set. +_LT_TAG_COMPILER + +# save warnings/boilerplate of simple test code +_LT_COMPILER_BOILERPLATE +_LT_LINKER_BOILERPLATE + +# Allow CC to be a program name with arguments. +lt_save_CC=$CC +lt_save_CFLAGS=$CFLAGS +lt_save_GCC=$GCC +GCC= +CC=${RC-"windres"} +CFLAGS= +compiler=$CC +_LT_TAGVAR(compiler, $1)=$CC +_LT_CC_BASENAME([$compiler]) +_LT_TAGVAR(lt_cv_prog_compiler_c_o, $1)=yes + +if test -n "$compiler"; then + : + _LT_CONFIG($1) +fi + +GCC=$lt_save_GCC +AC_LANG_RESTORE +CC=$lt_save_CC +CFLAGS=$lt_save_CFLAGS +])# _LT_LANG_RC_CONFIG + + +# LT_PROG_GCJ +# ----------- +AC_DEFUN([LT_PROG_GCJ], +[m4_ifdef([AC_PROG_GCJ], [AC_PROG_GCJ], + [m4_ifdef([A][M_PROG_GCJ], [A][M_PROG_GCJ], + [AC_CHECK_TOOL(GCJ, gcj,) + test set = "${GCJFLAGS+set}" || GCJFLAGS="-g -O2" + AC_SUBST(GCJFLAGS)])])[]dnl +]) + +# Old name: +AU_ALIAS([LT_AC_PROG_GCJ], [LT_PROG_GCJ]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([LT_AC_PROG_GCJ], []) + + +# LT_PROG_GO +# ---------- +AC_DEFUN([LT_PROG_GO], +[AC_CHECK_TOOL(GOC, gccgo,) +]) + + +# LT_PROG_RC +# ---------- +AC_DEFUN([LT_PROG_RC], +[AC_CHECK_TOOL(RC, windres,) +]) + +# Old name: +AU_ALIAS([LT_AC_PROG_RC], [LT_PROG_RC]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([LT_AC_PROG_RC], []) + + +# _LT_DECL_EGREP +# -------------- +# If we don't have a new enough Autoconf to choose the best grep +# available, choose the one first in the user's PATH. +m4_defun([_LT_DECL_EGREP], +[AC_REQUIRE([AC_PROG_EGREP])dnl +AC_REQUIRE([AC_PROG_FGREP])dnl +test -z "$GREP" && GREP=grep +_LT_DECL([], [GREP], [1], [A grep program that handles long lines]) +_LT_DECL([], [EGREP], [1], [An ERE matcher]) +_LT_DECL([], [FGREP], [1], [A literal string matcher]) +dnl Non-bleeding-edge autoconf doesn't subst GREP, so do it here too +AC_SUBST([GREP]) +]) + + +# _LT_DECL_OBJDUMP +# -------------- +# If we don't have a new enough Autoconf to choose the best objdump +# available, choose the one first in the user's PATH. +m4_defun([_LT_DECL_OBJDUMP], +[AC_CHECK_TOOL(OBJDUMP, objdump, false) +test -z "$OBJDUMP" && OBJDUMP=objdump +_LT_DECL([], [OBJDUMP], [1], [An object symbol dumper]) +AC_SUBST([OBJDUMP]) +]) + +# _LT_DECL_DLLTOOL +# ---------------- +# Ensure DLLTOOL variable is set. +m4_defun([_LT_DECL_DLLTOOL], +[AC_CHECK_TOOL(DLLTOOL, dlltool, false) +test -z "$DLLTOOL" && DLLTOOL=dlltool +_LT_DECL([], [DLLTOOL], [1], [DLL creation program]) +AC_SUBST([DLLTOOL]) +]) + +# _LT_DECL_SED +# ------------ +# Check for a fully-functional sed program, that truncates +# as few characters as possible. Prefer GNU sed if found. +m4_defun([_LT_DECL_SED], +[AC_PROG_SED +test -z "$SED" && SED=sed +Xsed="$SED -e 1s/^X//" +_LT_DECL([], [SED], [1], [A sed program that does not truncate output]) +_LT_DECL([], [Xsed], ["\$SED -e 1s/^X//"], + [Sed that helps us avoid accidentally triggering echo(1) options like -n]) +])# _LT_DECL_SED + +m4_ifndef([AC_PROG_SED], [ +# NOTE: This macro has been submitted for inclusion into # +# GNU Autoconf as AC_PROG_SED. When it is available in # +# a released version of Autoconf we should remove this # +# macro and use it instead. # + +m4_defun([AC_PROG_SED], +[AC_MSG_CHECKING([for a sed that does not truncate output]) +AC_CACHE_VAL(lt_cv_path_SED, +[# Loop through the user's path and test for sed and gsed. +# Then use that list of sed's as ones to test for truncation. +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for lt_ac_prog in sed gsed; do + for ac_exec_ext in '' $ac_executable_extensions; do + if $as_executable_p "$as_dir/$lt_ac_prog$ac_exec_ext"; then + lt_ac_sed_list="$lt_ac_sed_list $as_dir/$lt_ac_prog$ac_exec_ext" + fi + done + done +done +IFS=$as_save_IFS +lt_ac_max=0 +lt_ac_count=0 +# Add /usr/xpg4/bin/sed as it is typically found on Solaris +# along with /bin/sed that truncates output. +for lt_ac_sed in $lt_ac_sed_list /usr/xpg4/bin/sed; do + test ! -f "$lt_ac_sed" && continue + cat /dev/null > conftest.in + lt_ac_count=0 + echo $ECHO_N "0123456789$ECHO_C" >conftest.in + # Check for GNU sed and select it if it is found. + if "$lt_ac_sed" --version 2>&1 < /dev/null | grep 'GNU' > /dev/null; then + lt_cv_path_SED=$lt_ac_sed + break + fi + while true; do + cat conftest.in conftest.in >conftest.tmp + mv conftest.tmp conftest.in + cp conftest.in conftest.nl + echo >>conftest.nl + $lt_ac_sed -e 's/a$//' < conftest.nl >conftest.out || break + cmp -s conftest.out conftest.nl || break + # 10000 chars as input seems more than enough + test 10 -lt "$lt_ac_count" && break + lt_ac_count=`expr $lt_ac_count + 1` + if test "$lt_ac_count" -gt "$lt_ac_max"; then + lt_ac_max=$lt_ac_count + lt_cv_path_SED=$lt_ac_sed + fi + done +done +]) +SED=$lt_cv_path_SED +AC_SUBST([SED]) +AC_MSG_RESULT([$SED]) +])#AC_PROG_SED +])#m4_ifndef + +# Old name: +AU_ALIAS([LT_AC_PROG_SED], [AC_PROG_SED]) +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([LT_AC_PROG_SED], []) + + +# _LT_CHECK_SHELL_FEATURES +# ------------------------ +# Find out whether the shell is Bourne or XSI compatible, +# or has some other useful features. +m4_defun([_LT_CHECK_SHELL_FEATURES], +[if ( (MAIL=60; unset MAIL) || exit) >/dev/null 2>&1; then + lt_unset=unset +else + lt_unset=false +fi +_LT_DECL([], [lt_unset], [0], [whether the shell understands "unset"])dnl + +# test EBCDIC or ASCII +case `echo X|tr X '\101'` in + A) # ASCII based system + # \n is not interpreted correctly by Solaris 8 /usr/ucb/tr + lt_SP2NL='tr \040 \012' + lt_NL2SP='tr \015\012 \040\040' + ;; + *) # EBCDIC based system + lt_SP2NL='tr \100 \n' + lt_NL2SP='tr \r\n \100\100' + ;; +esac +_LT_DECL([SP2NL], [lt_SP2NL], [1], [turn spaces into newlines])dnl +_LT_DECL([NL2SP], [lt_NL2SP], [1], [turn newlines into spaces])dnl +])# _LT_CHECK_SHELL_FEATURES + + +# _LT_PATH_CONVERSION_FUNCTIONS +# ----------------------------- +# Determine what file name conversion functions should be used by +# func_to_host_file (and, implicitly, by func_to_host_path). These are needed +# for certain cross-compile configurations and native mingw. +m4_defun([_LT_PATH_CONVERSION_FUNCTIONS], +[AC_REQUIRE([AC_CANONICAL_HOST])dnl +AC_REQUIRE([AC_CANONICAL_BUILD])dnl +AC_MSG_CHECKING([how to convert $build file names to $host format]) +AC_CACHE_VAL(lt_cv_to_host_file_cmd, +[case $host in + *-*-mingw* ) + case $build in + *-*-mingw* ) # actually msys + lt_cv_to_host_file_cmd=func_convert_file_msys_to_w32 + ;; + *-*-cygwin* ) + lt_cv_to_host_file_cmd=func_convert_file_cygwin_to_w32 + ;; + * ) # otherwise, assume *nix + lt_cv_to_host_file_cmd=func_convert_file_nix_to_w32 + ;; + esac + ;; + *-*-cygwin* ) + case $build in + *-*-mingw* ) # actually msys + lt_cv_to_host_file_cmd=func_convert_file_msys_to_cygwin + ;; + *-*-cygwin* ) + lt_cv_to_host_file_cmd=func_convert_file_noop + ;; + * ) # otherwise, assume *nix + lt_cv_to_host_file_cmd=func_convert_file_nix_to_cygwin + ;; + esac + ;; + * ) # unhandled hosts (and "normal" native builds) + lt_cv_to_host_file_cmd=func_convert_file_noop + ;; +esac +]) +to_host_file_cmd=$lt_cv_to_host_file_cmd +AC_MSG_RESULT([$lt_cv_to_host_file_cmd]) +_LT_DECL([to_host_file_cmd], [lt_cv_to_host_file_cmd], + [0], [convert $build file names to $host format])dnl + +AC_MSG_CHECKING([how to convert $build file names to toolchain format]) +AC_CACHE_VAL(lt_cv_to_tool_file_cmd, +[#assume ordinary cross tools, or native build. +lt_cv_to_tool_file_cmd=func_convert_file_noop +case $host in + *-*-mingw* ) + case $build in + *-*-mingw* ) # actually msys + lt_cv_to_tool_file_cmd=func_convert_file_msys_to_w32 + ;; + esac + ;; +esac +]) +to_tool_file_cmd=$lt_cv_to_tool_file_cmd +AC_MSG_RESULT([$lt_cv_to_tool_file_cmd]) +_LT_DECL([to_tool_file_cmd], [lt_cv_to_tool_file_cmd], + [0], [convert $build files to toolchain format])dnl +])# _LT_PATH_CONVERSION_FUNCTIONS + +# Helper functions for option handling. -*- Autoconf -*- +# +# Copyright (C) 2004-2005, 2007-2009, 2011-2015 Free Software +# Foundation, Inc. +# Written by Gary V. Vaughan, 2004 +# +# This file is free software; the Free Software Foundation gives +# unlimited permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. + +# serial 8 ltoptions.m4 + +# This is to help aclocal find these macros, as it can't see m4_define. +AC_DEFUN([LTOPTIONS_VERSION], [m4_if([1])]) + + +# _LT_MANGLE_OPTION(MACRO-NAME, OPTION-NAME) +# ------------------------------------------ +m4_define([_LT_MANGLE_OPTION], +[[_LT_OPTION_]m4_bpatsubst($1__$2, [[^a-zA-Z0-9_]], [_])]) + + +# _LT_SET_OPTION(MACRO-NAME, OPTION-NAME) +# --------------------------------------- +# Set option OPTION-NAME for macro MACRO-NAME, and if there is a +# matching handler defined, dispatch to it. Other OPTION-NAMEs are +# saved as a flag. +m4_define([_LT_SET_OPTION], +[m4_define(_LT_MANGLE_OPTION([$1], [$2]))dnl +m4_ifdef(_LT_MANGLE_DEFUN([$1], [$2]), + _LT_MANGLE_DEFUN([$1], [$2]), + [m4_warning([Unknown $1 option '$2'])])[]dnl +]) + + +# _LT_IF_OPTION(MACRO-NAME, OPTION-NAME, IF-SET, [IF-NOT-SET]) +# ------------------------------------------------------------ +# Execute IF-SET if OPTION is set, IF-NOT-SET otherwise. +m4_define([_LT_IF_OPTION], +[m4_ifdef(_LT_MANGLE_OPTION([$1], [$2]), [$3], [$4])]) + + +# _LT_UNLESS_OPTIONS(MACRO-NAME, OPTION-LIST, IF-NOT-SET) +# ------------------------------------------------------- +# Execute IF-NOT-SET unless all options in OPTION-LIST for MACRO-NAME +# are set. +m4_define([_LT_UNLESS_OPTIONS], +[m4_foreach([_LT_Option], m4_split(m4_normalize([$2])), + [m4_ifdef(_LT_MANGLE_OPTION([$1], _LT_Option), + [m4_define([$0_found])])])[]dnl +m4_ifdef([$0_found], [m4_undefine([$0_found])], [$3 +])[]dnl +]) + + +# _LT_SET_OPTIONS(MACRO-NAME, OPTION-LIST) +# ---------------------------------------- +# OPTION-LIST is a space-separated list of Libtool options associated +# with MACRO-NAME. If any OPTION has a matching handler declared with +# LT_OPTION_DEFINE, dispatch to that macro; otherwise complain about +# the unknown option and exit. +m4_defun([_LT_SET_OPTIONS], +[# Set options +m4_foreach([_LT_Option], m4_split(m4_normalize([$2])), + [_LT_SET_OPTION([$1], _LT_Option)]) + +m4_if([$1],[LT_INIT],[ + dnl + dnl Simply set some default values (i.e off) if boolean options were not + dnl specified: + _LT_UNLESS_OPTIONS([LT_INIT], [dlopen], [enable_dlopen=no + ]) + _LT_UNLESS_OPTIONS([LT_INIT], [win32-dll], [enable_win32_dll=no + ]) + dnl + dnl If no reference was made to various pairs of opposing options, then + dnl we run the default mode handler for the pair. For example, if neither + dnl 'shared' nor 'disable-shared' was passed, we enable building of shared + dnl archives by default: + _LT_UNLESS_OPTIONS([LT_INIT], [shared disable-shared], [_LT_ENABLE_SHARED]) + _LT_UNLESS_OPTIONS([LT_INIT], [static disable-static], [_LT_ENABLE_STATIC]) + _LT_UNLESS_OPTIONS([LT_INIT], [pic-only no-pic], [_LT_WITH_PIC]) + _LT_UNLESS_OPTIONS([LT_INIT], [fast-install disable-fast-install], + [_LT_ENABLE_FAST_INSTALL]) + _LT_UNLESS_OPTIONS([LT_INIT], [aix-soname=aix aix-soname=both aix-soname=svr4], + [_LT_WITH_AIX_SONAME([aix])]) + ]) +])# _LT_SET_OPTIONS + + + +# _LT_MANGLE_DEFUN(MACRO-NAME, OPTION-NAME) +# ----------------------------------------- +m4_define([_LT_MANGLE_DEFUN], +[[_LT_OPTION_DEFUN_]m4_bpatsubst(m4_toupper([$1__$2]), [[^A-Z0-9_]], [_])]) + + +# LT_OPTION_DEFINE(MACRO-NAME, OPTION-NAME, CODE) +# ----------------------------------------------- +m4_define([LT_OPTION_DEFINE], +[m4_define(_LT_MANGLE_DEFUN([$1], [$2]), [$3])[]dnl +])# LT_OPTION_DEFINE + + +# dlopen +# ------ +LT_OPTION_DEFINE([LT_INIT], [dlopen], [enable_dlopen=yes +]) + +AU_DEFUN([AC_LIBTOOL_DLOPEN], +[_LT_SET_OPTION([LT_INIT], [dlopen]) +AC_DIAGNOSE([obsolete], +[$0: Remove this warning and the call to _LT_SET_OPTION when you +put the 'dlopen' option into LT_INIT's first parameter.]) +]) + +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_LIBTOOL_DLOPEN], []) + + +# win32-dll +# --------- +# Declare package support for building win32 dll's. +LT_OPTION_DEFINE([LT_INIT], [win32-dll], +[enable_win32_dll=yes + +case $host in +*-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-cegcc*) + AC_CHECK_TOOL(AS, as, false) + AC_CHECK_TOOL(DLLTOOL, dlltool, false) + AC_CHECK_TOOL(OBJDUMP, objdump, false) + ;; +esac + +test -z "$AS" && AS=as +_LT_DECL([], [AS], [1], [Assembler program])dnl + +test -z "$DLLTOOL" && DLLTOOL=dlltool +_LT_DECL([], [DLLTOOL], [1], [DLL creation program])dnl + +test -z "$OBJDUMP" && OBJDUMP=objdump +_LT_DECL([], [OBJDUMP], [1], [Object dumper program])dnl +])# win32-dll + +AU_DEFUN([AC_LIBTOOL_WIN32_DLL], +[AC_REQUIRE([AC_CANONICAL_HOST])dnl +_LT_SET_OPTION([LT_INIT], [win32-dll]) +AC_DIAGNOSE([obsolete], +[$0: Remove this warning and the call to _LT_SET_OPTION when you +put the 'win32-dll' option into LT_INIT's first parameter.]) +]) + +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_LIBTOOL_WIN32_DLL], []) + + +# _LT_ENABLE_SHARED([DEFAULT]) +# ---------------------------- +# implement the --enable-shared flag, and supports the 'shared' and +# 'disable-shared' LT_INIT options. +# DEFAULT is either 'yes' or 'no'. If omitted, it defaults to 'yes'. +m4_define([_LT_ENABLE_SHARED], +[m4_define([_LT_ENABLE_SHARED_DEFAULT], [m4_if($1, no, no, yes)])dnl +AC_ARG_ENABLE([shared], + [AS_HELP_STRING([--enable-shared@<:@=PKGS@:>@], + [build shared libraries @<:@default=]_LT_ENABLE_SHARED_DEFAULT[@:>@])], + [p=${PACKAGE-default} + case $enableval in + yes) enable_shared=yes ;; + no) enable_shared=no ;; + *) + enable_shared=no + # Look at the argument we got. We use all the common list separators. + lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR, + for pkg in $enableval; do + IFS=$lt_save_ifs + if test "X$pkg" = "X$p"; then + enable_shared=yes + fi + done + IFS=$lt_save_ifs + ;; + esac], + [enable_shared=]_LT_ENABLE_SHARED_DEFAULT) + + _LT_DECL([build_libtool_libs], [enable_shared], [0], + [Whether or not to build shared libraries]) +])# _LT_ENABLE_SHARED + +LT_OPTION_DEFINE([LT_INIT], [shared], [_LT_ENABLE_SHARED([yes])]) +LT_OPTION_DEFINE([LT_INIT], [disable-shared], [_LT_ENABLE_SHARED([no])]) + +# Old names: +AC_DEFUN([AC_ENABLE_SHARED], +[_LT_SET_OPTION([LT_INIT], m4_if([$1], [no], [disable-])[shared]) +]) + +AC_DEFUN([AC_DISABLE_SHARED], +[_LT_SET_OPTION([LT_INIT], [disable-shared]) +]) + +AU_DEFUN([AM_ENABLE_SHARED], [AC_ENABLE_SHARED($@)]) +AU_DEFUN([AM_DISABLE_SHARED], [AC_DISABLE_SHARED($@)]) + +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AM_ENABLE_SHARED], []) +dnl AC_DEFUN([AM_DISABLE_SHARED], []) + + + +# _LT_ENABLE_STATIC([DEFAULT]) +# ---------------------------- +# implement the --enable-static flag, and support the 'static' and +# 'disable-static' LT_INIT options. +# DEFAULT is either 'yes' or 'no'. If omitted, it defaults to 'yes'. +m4_define([_LT_ENABLE_STATIC], +[m4_define([_LT_ENABLE_STATIC_DEFAULT], [m4_if($1, no, no, yes)])dnl +AC_ARG_ENABLE([static], + [AS_HELP_STRING([--enable-static@<:@=PKGS@:>@], + [build static libraries @<:@default=]_LT_ENABLE_STATIC_DEFAULT[@:>@])], + [p=${PACKAGE-default} + case $enableval in + yes) enable_static=yes ;; + no) enable_static=no ;; + *) + enable_static=no + # Look at the argument we got. We use all the common list separators. + lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR, + for pkg in $enableval; do + IFS=$lt_save_ifs + if test "X$pkg" = "X$p"; then + enable_static=yes + fi + done + IFS=$lt_save_ifs + ;; + esac], + [enable_static=]_LT_ENABLE_STATIC_DEFAULT) + + _LT_DECL([build_old_libs], [enable_static], [0], + [Whether or not to build static libraries]) +])# _LT_ENABLE_STATIC + +LT_OPTION_DEFINE([LT_INIT], [static], [_LT_ENABLE_STATIC([yes])]) +LT_OPTION_DEFINE([LT_INIT], [disable-static], [_LT_ENABLE_STATIC([no])]) + +# Old names: +AC_DEFUN([AC_ENABLE_STATIC], +[_LT_SET_OPTION([LT_INIT], m4_if([$1], [no], [disable-])[static]) +]) + +AC_DEFUN([AC_DISABLE_STATIC], +[_LT_SET_OPTION([LT_INIT], [disable-static]) +]) + +AU_DEFUN([AM_ENABLE_STATIC], [AC_ENABLE_STATIC($@)]) +AU_DEFUN([AM_DISABLE_STATIC], [AC_DISABLE_STATIC($@)]) + +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AM_ENABLE_STATIC], []) +dnl AC_DEFUN([AM_DISABLE_STATIC], []) + + + +# _LT_ENABLE_FAST_INSTALL([DEFAULT]) +# ---------------------------------- +# implement the --enable-fast-install flag, and support the 'fast-install' +# and 'disable-fast-install' LT_INIT options. +# DEFAULT is either 'yes' or 'no'. If omitted, it defaults to 'yes'. +m4_define([_LT_ENABLE_FAST_INSTALL], +[m4_define([_LT_ENABLE_FAST_INSTALL_DEFAULT], [m4_if($1, no, no, yes)])dnl +AC_ARG_ENABLE([fast-install], + [AS_HELP_STRING([--enable-fast-install@<:@=PKGS@:>@], + [optimize for fast installation @<:@default=]_LT_ENABLE_FAST_INSTALL_DEFAULT[@:>@])], + [p=${PACKAGE-default} + case $enableval in + yes) enable_fast_install=yes ;; + no) enable_fast_install=no ;; + *) + enable_fast_install=no + # Look at the argument we got. We use all the common list separators. + lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR, + for pkg in $enableval; do + IFS=$lt_save_ifs + if test "X$pkg" = "X$p"; then + enable_fast_install=yes + fi + done + IFS=$lt_save_ifs + ;; + esac], + [enable_fast_install=]_LT_ENABLE_FAST_INSTALL_DEFAULT) + +_LT_DECL([fast_install], [enable_fast_install], [0], + [Whether or not to optimize for fast installation])dnl +])# _LT_ENABLE_FAST_INSTALL + +LT_OPTION_DEFINE([LT_INIT], [fast-install], [_LT_ENABLE_FAST_INSTALL([yes])]) +LT_OPTION_DEFINE([LT_INIT], [disable-fast-install], [_LT_ENABLE_FAST_INSTALL([no])]) + +# Old names: +AU_DEFUN([AC_ENABLE_FAST_INSTALL], +[_LT_SET_OPTION([LT_INIT], m4_if([$1], [no], [disable-])[fast-install]) +AC_DIAGNOSE([obsolete], +[$0: Remove this warning and the call to _LT_SET_OPTION when you put +the 'fast-install' option into LT_INIT's first parameter.]) +]) + +AU_DEFUN([AC_DISABLE_FAST_INSTALL], +[_LT_SET_OPTION([LT_INIT], [disable-fast-install]) +AC_DIAGNOSE([obsolete], +[$0: Remove this warning and the call to _LT_SET_OPTION when you put +the 'disable-fast-install' option into LT_INIT's first parameter.]) +]) + +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_ENABLE_FAST_INSTALL], []) +dnl AC_DEFUN([AM_DISABLE_FAST_INSTALL], []) + + +# _LT_WITH_AIX_SONAME([DEFAULT]) +# ---------------------------------- +# implement the --with-aix-soname flag, and support the `aix-soname=aix' +# and `aix-soname=both' and `aix-soname=svr4' LT_INIT options. DEFAULT +# is either `aix', `both' or `svr4'. If omitted, it defaults to `aix'. +m4_define([_LT_WITH_AIX_SONAME], +[m4_define([_LT_WITH_AIX_SONAME_DEFAULT], [m4_if($1, svr4, svr4, m4_if($1, both, both, aix))])dnl +shared_archive_member_spec= +case $host,$enable_shared in +power*-*-aix[[5-9]]*,yes) + AC_MSG_CHECKING([which variant of shared library versioning to provide]) + AC_ARG_WITH([aix-soname], + [AS_HELP_STRING([--with-aix-soname=aix|svr4|both], + [shared library versioning (aka "SONAME") variant to provide on AIX, @<:@default=]_LT_WITH_AIX_SONAME_DEFAULT[@:>@.])], + [case $withval in + aix|svr4|both) + ;; + *) + AC_MSG_ERROR([Unknown argument to --with-aix-soname]) + ;; + esac + lt_cv_with_aix_soname=$with_aix_soname], + [AC_CACHE_VAL([lt_cv_with_aix_soname], + [lt_cv_with_aix_soname=]_LT_WITH_AIX_SONAME_DEFAULT) + with_aix_soname=$lt_cv_with_aix_soname]) + AC_MSG_RESULT([$with_aix_soname]) + if test aix != "$with_aix_soname"; then + # For the AIX way of multilib, we name the shared archive member + # based on the bitwidth used, traditionally 'shr.o' or 'shr_64.o', + # and 'shr.imp' or 'shr_64.imp', respectively, for the Import File. + # Even when GNU compilers ignore OBJECT_MODE but need '-maix64' flag, + # the AIX toolchain works better with OBJECT_MODE set (default 32). + if test 64 = "${OBJECT_MODE-32}"; then + shared_archive_member_spec=shr_64 + else + shared_archive_member_spec=shr + fi + fi + ;; +*) + with_aix_soname=aix + ;; +esac + +_LT_DECL([], [shared_archive_member_spec], [0], + [Shared archive member basename, for filename based shared library versioning on AIX])dnl +])# _LT_WITH_AIX_SONAME + +LT_OPTION_DEFINE([LT_INIT], [aix-soname=aix], [_LT_WITH_AIX_SONAME([aix])]) +LT_OPTION_DEFINE([LT_INIT], [aix-soname=both], [_LT_WITH_AIX_SONAME([both])]) +LT_OPTION_DEFINE([LT_INIT], [aix-soname=svr4], [_LT_WITH_AIX_SONAME([svr4])]) + + +# _LT_WITH_PIC([MODE]) +# -------------------- +# implement the --with-pic flag, and support the 'pic-only' and 'no-pic' +# LT_INIT options. +# MODE is either 'yes' or 'no'. If omitted, it defaults to 'both'. +m4_define([_LT_WITH_PIC], +[AC_ARG_WITH([pic], + [AS_HELP_STRING([--with-pic@<:@=PKGS@:>@], + [try to use only PIC/non-PIC objects @<:@default=use both@:>@])], + [lt_p=${PACKAGE-default} + case $withval in + yes|no) pic_mode=$withval ;; + *) + pic_mode=default + # Look at the argument we got. We use all the common list separators. + lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR, + for lt_pkg in $withval; do + IFS=$lt_save_ifs + if test "X$lt_pkg" = "X$lt_p"; then + pic_mode=yes + fi + done + IFS=$lt_save_ifs + ;; + esac], + [pic_mode=m4_default([$1], [default])]) + +_LT_DECL([], [pic_mode], [0], [What type of objects to build])dnl +])# _LT_WITH_PIC + +LT_OPTION_DEFINE([LT_INIT], [pic-only], [_LT_WITH_PIC([yes])]) +LT_OPTION_DEFINE([LT_INIT], [no-pic], [_LT_WITH_PIC([no])]) + +# Old name: +AU_DEFUN([AC_LIBTOOL_PICMODE], +[_LT_SET_OPTION([LT_INIT], [pic-only]) +AC_DIAGNOSE([obsolete], +[$0: Remove this warning and the call to _LT_SET_OPTION when you +put the 'pic-only' option into LT_INIT's first parameter.]) +]) + +dnl aclocal-1.4 backwards compatibility: +dnl AC_DEFUN([AC_LIBTOOL_PICMODE], []) + + +m4_define([_LTDL_MODE], []) +LT_OPTION_DEFINE([LTDL_INIT], [nonrecursive], + [m4_define([_LTDL_MODE], [nonrecursive])]) +LT_OPTION_DEFINE([LTDL_INIT], [recursive], + [m4_define([_LTDL_MODE], [recursive])]) +LT_OPTION_DEFINE([LTDL_INIT], [subproject], + [m4_define([_LTDL_MODE], [subproject])]) + +m4_define([_LTDL_TYPE], []) +LT_OPTION_DEFINE([LTDL_INIT], [installable], + [m4_define([_LTDL_TYPE], [installable])]) +LT_OPTION_DEFINE([LTDL_INIT], [convenience], + [m4_define([_LTDL_TYPE], [convenience])]) + +# ltsugar.m4 -- libtool m4 base layer. -*-Autoconf-*- +# +# Copyright (C) 2004-2005, 2007-2008, 2011-2015 Free Software +# Foundation, Inc. +# Written by Gary V. Vaughan, 2004 +# +# This file is free software; the Free Software Foundation gives +# unlimited permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. + +# serial 6 ltsugar.m4 + +# This is to help aclocal find these macros, as it can't see m4_define. +AC_DEFUN([LTSUGAR_VERSION], [m4_if([0.1])]) + + +# lt_join(SEP, ARG1, [ARG2...]) +# ----------------------------- +# Produce ARG1SEPARG2...SEPARGn, omitting [] arguments and their +# associated separator. +# Needed until we can rely on m4_join from Autoconf 2.62, since all earlier +# versions in m4sugar had bugs. +m4_define([lt_join], +[m4_if([$#], [1], [], + [$#], [2], [[$2]], + [m4_if([$2], [], [], [[$2]_])$0([$1], m4_shift(m4_shift($@)))])]) +m4_define([_lt_join], +[m4_if([$#$2], [2], [], + [m4_if([$2], [], [], [[$1$2]])$0([$1], m4_shift(m4_shift($@)))])]) + + +# lt_car(LIST) +# lt_cdr(LIST) +# ------------ +# Manipulate m4 lists. +# These macros are necessary as long as will still need to support +# Autoconf-2.59, which quotes differently. +m4_define([lt_car], [[$1]]) +m4_define([lt_cdr], +[m4_if([$#], 0, [m4_fatal([$0: cannot be called without arguments])], + [$#], 1, [], + [m4_dquote(m4_shift($@))])]) +m4_define([lt_unquote], $1) + + +# lt_append(MACRO-NAME, STRING, [SEPARATOR]) +# ------------------------------------------ +# Redefine MACRO-NAME to hold its former content plus 'SEPARATOR''STRING'. +# Note that neither SEPARATOR nor STRING are expanded; they are appended +# to MACRO-NAME as is (leaving the expansion for when MACRO-NAME is invoked). +# No SEPARATOR is output if MACRO-NAME was previously undefined (different +# than defined and empty). +# +# This macro is needed until we can rely on Autoconf 2.62, since earlier +# versions of m4sugar mistakenly expanded SEPARATOR but not STRING. +m4_define([lt_append], +[m4_define([$1], + m4_ifdef([$1], [m4_defn([$1])[$3]])[$2])]) + + + +# lt_combine(SEP, PREFIX-LIST, INFIX, SUFFIX1, [SUFFIX2...]) +# ---------------------------------------------------------- +# Produce a SEP delimited list of all paired combinations of elements of +# PREFIX-LIST with SUFFIX1 through SUFFIXn. Each element of the list +# has the form PREFIXmINFIXSUFFIXn. +# Needed until we can rely on m4_combine added in Autoconf 2.62. +m4_define([lt_combine], +[m4_if(m4_eval([$# > 3]), [1], + [m4_pushdef([_Lt_sep], [m4_define([_Lt_sep], m4_defn([lt_car]))])]]dnl +[[m4_foreach([_Lt_prefix], [$2], + [m4_foreach([_Lt_suffix], + ]m4_dquote(m4_dquote(m4_shift(m4_shift(m4_shift($@)))))[, + [_Lt_sep([$1])[]m4_defn([_Lt_prefix])[$3]m4_defn([_Lt_suffix])])])])]) + + +# lt_if_append_uniq(MACRO-NAME, VARNAME, [SEPARATOR], [UNIQ], [NOT-UNIQ]) +# ----------------------------------------------------------------------- +# Iff MACRO-NAME does not yet contain VARNAME, then append it (delimited +# by SEPARATOR if supplied) and expand UNIQ, else NOT-UNIQ. +m4_define([lt_if_append_uniq], +[m4_ifdef([$1], + [m4_if(m4_index([$3]m4_defn([$1])[$3], [$3$2$3]), [-1], + [lt_append([$1], [$2], [$3])$4], + [$5])], + [lt_append([$1], [$2], [$3])$4])]) + + +# lt_dict_add(DICT, KEY, VALUE) +# ----------------------------- +m4_define([lt_dict_add], +[m4_define([$1($2)], [$3])]) + + +# lt_dict_add_subkey(DICT, KEY, SUBKEY, VALUE) +# -------------------------------------------- +m4_define([lt_dict_add_subkey], +[m4_define([$1($2:$3)], [$4])]) + + +# lt_dict_fetch(DICT, KEY, [SUBKEY]) +# ---------------------------------- +m4_define([lt_dict_fetch], +[m4_ifval([$3], + m4_ifdef([$1($2:$3)], [m4_defn([$1($2:$3)])]), + m4_ifdef([$1($2)], [m4_defn([$1($2)])]))]) + + +# lt_if_dict_fetch(DICT, KEY, [SUBKEY], VALUE, IF-TRUE, [IF-FALSE]) +# ----------------------------------------------------------------- +m4_define([lt_if_dict_fetch], +[m4_if(lt_dict_fetch([$1], [$2], [$3]), [$4], + [$5], + [$6])]) + + +# lt_dict_filter(DICT, [SUBKEY], VALUE, [SEPARATOR], KEY, [...]) +# -------------------------------------------------------------- +m4_define([lt_dict_filter], +[m4_if([$5], [], [], + [lt_join(m4_quote(m4_default([$4], [[, ]])), + lt_unquote(m4_split(m4_normalize(m4_foreach(_Lt_key, lt_car([m4_shiftn(4, $@)]), + [lt_if_dict_fetch([$1], _Lt_key, [$2], [$3], [_Lt_key ])])))))])[]dnl +]) + +# ltversion.m4 -- version numbers -*- Autoconf -*- +# +# Copyright (C) 2004, 2011-2015 Free Software Foundation, Inc. +# Written by Scott James Remnant, 2004 +# +# This file is free software; the Free Software Foundation gives +# unlimited permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. + +# @configure_input@ + +# serial 4179 ltversion.m4 +# This file is part of GNU Libtool + +m4_define([LT_PACKAGE_VERSION], [2.4.6]) +m4_define([LT_PACKAGE_REVISION], [2.4.6]) + +AC_DEFUN([LTVERSION_VERSION], +[macro_version='2.4.6' +macro_revision='2.4.6' +_LT_DECL(, macro_version, 0, [Which release of libtool.m4 was used?]) +_LT_DECL(, macro_revision, 0) +]) + +# lt~obsolete.m4 -- aclocal satisfying obsolete definitions. -*-Autoconf-*- +# +# Copyright (C) 2004-2005, 2007, 2009, 2011-2015 Free Software +# Foundation, Inc. +# Written by Scott James Remnant, 2004. +# +# This file is free software; the Free Software Foundation gives +# unlimited permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. + +# serial 5 lt~obsolete.m4 + +# These exist entirely to fool aclocal when bootstrapping libtool. +# +# In the past libtool.m4 has provided macros via AC_DEFUN (or AU_DEFUN), +# which have later been changed to m4_define as they aren't part of the +# exported API, or moved to Autoconf or Automake where they belong. +# +# The trouble is, aclocal is a bit thick. It'll see the old AC_DEFUN +# in /usr/share/aclocal/libtool.m4 and remember it, then when it sees us +# using a macro with the same name in our local m4/libtool.m4 it'll +# pull the old libtool.m4 in (it doesn't see our shiny new m4_define +# and doesn't know about Autoconf macros at all.) +# +# So we provide this file, which has a silly filename so it's always +# included after everything else. This provides aclocal with the +# AC_DEFUNs it wants, but when m4 processes it, it doesn't do anything +# because those macros already exist, or will be overwritten later. +# We use AC_DEFUN over AU_DEFUN for compatibility with aclocal-1.6. +# +# Anytime we withdraw an AC_DEFUN or AU_DEFUN, remember to add it here. +# Yes, that means every name once taken will need to remain here until +# we give up compatibility with versions before 1.7, at which point +# we need to keep only those names which we still refer to. + +# This is to help aclocal find these macros, as it can't see m4_define. +AC_DEFUN([LTOBSOLETE_VERSION], [m4_if([1])]) + +m4_ifndef([AC_LIBTOOL_LINKER_OPTION], [AC_DEFUN([AC_LIBTOOL_LINKER_OPTION])]) +m4_ifndef([AC_PROG_EGREP], [AC_DEFUN([AC_PROG_EGREP])]) +m4_ifndef([_LT_AC_PROG_ECHO_BACKSLASH], [AC_DEFUN([_LT_AC_PROG_ECHO_BACKSLASH])]) +m4_ifndef([_LT_AC_SHELL_INIT], [AC_DEFUN([_LT_AC_SHELL_INIT])]) +m4_ifndef([_LT_AC_SYS_LIBPATH_AIX], [AC_DEFUN([_LT_AC_SYS_LIBPATH_AIX])]) +m4_ifndef([_LT_PROG_LTMAIN], [AC_DEFUN([_LT_PROG_LTMAIN])]) +m4_ifndef([_LT_AC_TAGVAR], [AC_DEFUN([_LT_AC_TAGVAR])]) +m4_ifndef([AC_LTDL_ENABLE_INSTALL], [AC_DEFUN([AC_LTDL_ENABLE_INSTALL])]) +m4_ifndef([AC_LTDL_PREOPEN], [AC_DEFUN([AC_LTDL_PREOPEN])]) +m4_ifndef([_LT_AC_SYS_COMPILER], [AC_DEFUN([_LT_AC_SYS_COMPILER])]) +m4_ifndef([_LT_AC_LOCK], [AC_DEFUN([_LT_AC_LOCK])]) +m4_ifndef([AC_LIBTOOL_SYS_OLD_ARCHIVE], [AC_DEFUN([AC_LIBTOOL_SYS_OLD_ARCHIVE])]) +m4_ifndef([_LT_AC_TRY_DLOPEN_SELF], [AC_DEFUN([_LT_AC_TRY_DLOPEN_SELF])]) +m4_ifndef([AC_LIBTOOL_PROG_CC_C_O], [AC_DEFUN([AC_LIBTOOL_PROG_CC_C_O])]) +m4_ifndef([AC_LIBTOOL_SYS_HARD_LINK_LOCKS], [AC_DEFUN([AC_LIBTOOL_SYS_HARD_LINK_LOCKS])]) +m4_ifndef([AC_LIBTOOL_OBJDIR], [AC_DEFUN([AC_LIBTOOL_OBJDIR])]) +m4_ifndef([AC_LTDL_OBJDIR], [AC_DEFUN([AC_LTDL_OBJDIR])]) +m4_ifndef([AC_LIBTOOL_PROG_LD_HARDCODE_LIBPATH], [AC_DEFUN([AC_LIBTOOL_PROG_LD_HARDCODE_LIBPATH])]) +m4_ifndef([AC_LIBTOOL_SYS_LIB_STRIP], [AC_DEFUN([AC_LIBTOOL_SYS_LIB_STRIP])]) +m4_ifndef([AC_PATH_MAGIC], [AC_DEFUN([AC_PATH_MAGIC])]) +m4_ifndef([AC_PROG_LD_GNU], [AC_DEFUN([AC_PROG_LD_GNU])]) +m4_ifndef([AC_PROG_LD_RELOAD_FLAG], [AC_DEFUN([AC_PROG_LD_RELOAD_FLAG])]) +m4_ifndef([AC_DEPLIBS_CHECK_METHOD], [AC_DEFUN([AC_DEPLIBS_CHECK_METHOD])]) +m4_ifndef([AC_LIBTOOL_PROG_COMPILER_NO_RTTI], [AC_DEFUN([AC_LIBTOOL_PROG_COMPILER_NO_RTTI])]) +m4_ifndef([AC_LIBTOOL_SYS_GLOBAL_SYMBOL_PIPE], [AC_DEFUN([AC_LIBTOOL_SYS_GLOBAL_SYMBOL_PIPE])]) +m4_ifndef([AC_LIBTOOL_PROG_COMPILER_PIC], [AC_DEFUN([AC_LIBTOOL_PROG_COMPILER_PIC])]) +m4_ifndef([AC_LIBTOOL_PROG_LD_SHLIBS], [AC_DEFUN([AC_LIBTOOL_PROG_LD_SHLIBS])]) +m4_ifndef([AC_LIBTOOL_POSTDEP_PREDEP], [AC_DEFUN([AC_LIBTOOL_POSTDEP_PREDEP])]) +m4_ifndef([LT_AC_PROG_EGREP], [AC_DEFUN([LT_AC_PROG_EGREP])]) +m4_ifndef([LT_AC_PROG_SED], [AC_DEFUN([LT_AC_PROG_SED])]) +m4_ifndef([_LT_CC_BASENAME], [AC_DEFUN([_LT_CC_BASENAME])]) +m4_ifndef([_LT_COMPILER_BOILERPLATE], [AC_DEFUN([_LT_COMPILER_BOILERPLATE])]) +m4_ifndef([_LT_LINKER_BOILERPLATE], [AC_DEFUN([_LT_LINKER_BOILERPLATE])]) +m4_ifndef([_AC_PROG_LIBTOOL], [AC_DEFUN([_AC_PROG_LIBTOOL])]) +m4_ifndef([AC_LIBTOOL_SETUP], [AC_DEFUN([AC_LIBTOOL_SETUP])]) +m4_ifndef([_LT_AC_CHECK_DLFCN], [AC_DEFUN([_LT_AC_CHECK_DLFCN])]) +m4_ifndef([AC_LIBTOOL_SYS_DYNAMIC_LINKER], [AC_DEFUN([AC_LIBTOOL_SYS_DYNAMIC_LINKER])]) +m4_ifndef([_LT_AC_TAGCONFIG], [AC_DEFUN([_LT_AC_TAGCONFIG])]) +m4_ifndef([AC_DISABLE_FAST_INSTALL], [AC_DEFUN([AC_DISABLE_FAST_INSTALL])]) +m4_ifndef([_LT_AC_LANG_CXX], [AC_DEFUN([_LT_AC_LANG_CXX])]) +m4_ifndef([_LT_AC_LANG_F77], [AC_DEFUN([_LT_AC_LANG_F77])]) +m4_ifndef([_LT_AC_LANG_GCJ], [AC_DEFUN([_LT_AC_LANG_GCJ])]) +m4_ifndef([AC_LIBTOOL_LANG_C_CONFIG], [AC_DEFUN([AC_LIBTOOL_LANG_C_CONFIG])]) +m4_ifndef([_LT_AC_LANG_C_CONFIG], [AC_DEFUN([_LT_AC_LANG_C_CONFIG])]) +m4_ifndef([AC_LIBTOOL_LANG_CXX_CONFIG], [AC_DEFUN([AC_LIBTOOL_LANG_CXX_CONFIG])]) +m4_ifndef([_LT_AC_LANG_CXX_CONFIG], [AC_DEFUN([_LT_AC_LANG_CXX_CONFIG])]) +m4_ifndef([AC_LIBTOOL_LANG_F77_CONFIG], [AC_DEFUN([AC_LIBTOOL_LANG_F77_CONFIG])]) +m4_ifndef([_LT_AC_LANG_F77_CONFIG], [AC_DEFUN([_LT_AC_LANG_F77_CONFIG])]) +m4_ifndef([AC_LIBTOOL_LANG_GCJ_CONFIG], [AC_DEFUN([AC_LIBTOOL_LANG_GCJ_CONFIG])]) +m4_ifndef([_LT_AC_LANG_GCJ_CONFIG], [AC_DEFUN([_LT_AC_LANG_GCJ_CONFIG])]) +m4_ifndef([AC_LIBTOOL_LANG_RC_CONFIG], [AC_DEFUN([AC_LIBTOOL_LANG_RC_CONFIG])]) +m4_ifndef([_LT_AC_LANG_RC_CONFIG], [AC_DEFUN([_LT_AC_LANG_RC_CONFIG])]) +m4_ifndef([AC_LIBTOOL_CONFIG], [AC_DEFUN([AC_LIBTOOL_CONFIG])]) +m4_ifndef([_LT_AC_FILE_LTDLL_C], [AC_DEFUN([_LT_AC_FILE_LTDLL_C])]) +m4_ifndef([_LT_REQUIRED_DARWIN_CHECKS], [AC_DEFUN([_LT_REQUIRED_DARWIN_CHECKS])]) +m4_ifndef([_LT_AC_PROG_CXXCPP], [AC_DEFUN([_LT_AC_PROG_CXXCPP])]) +m4_ifndef([_LT_PREPARE_SED_QUOTE_VARS], [AC_DEFUN([_LT_PREPARE_SED_QUOTE_VARS])]) +m4_ifndef([_LT_PROG_ECHO_BACKSLASH], [AC_DEFUN([_LT_PROG_ECHO_BACKSLASH])]) +m4_ifndef([_LT_PROG_F77], [AC_DEFUN([_LT_PROG_F77])]) +m4_ifndef([_LT_PROG_FC], [AC_DEFUN([_LT_PROG_FC])]) +m4_ifndef([_LT_PROG_CXX], [AC_DEFUN([_LT_PROG_CXX])]) + +# Copyright (C) 2002-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# AM_AUTOMAKE_VERSION(VERSION) +# ---------------------------- +# Automake X.Y traces this macro to ensure aclocal.m4 has been +# generated from the m4 files accompanying Automake X.Y. +# (This private macro should not be called outside this file.) +AC_DEFUN([AM_AUTOMAKE_VERSION], +[am__api_version='1.16' +dnl Some users find AM_AUTOMAKE_VERSION and mistake it for a way to +dnl require some minimum version. Point them to the right macro. +m4_if([$1], [1.16.1], [], + [AC_FATAL([Do not call $0, use AM_INIT_AUTOMAKE([$1]).])])dnl +]) + +# _AM_AUTOCONF_VERSION(VERSION) +# ----------------------------- +# aclocal traces this macro to find the Autoconf version. +# This is a private macro too. Using m4_define simplifies +# the logic in aclocal, which can simply ignore this definition. +m4_define([_AM_AUTOCONF_VERSION], []) + +# AM_SET_CURRENT_AUTOMAKE_VERSION +# ------------------------------- +# Call AM_AUTOMAKE_VERSION and AM_AUTOMAKE_VERSION so they can be traced. +# This function is AC_REQUIREd by AM_INIT_AUTOMAKE. +AC_DEFUN([AM_SET_CURRENT_AUTOMAKE_VERSION], +[AM_AUTOMAKE_VERSION([1.16.1])dnl +m4_ifndef([AC_AUTOCONF_VERSION], + [m4_copy([m4_PACKAGE_VERSION], [AC_AUTOCONF_VERSION])])dnl +_AM_AUTOCONF_VERSION(m4_defn([AC_AUTOCONF_VERSION]))]) + +# AM_AUX_DIR_EXPAND -*- Autoconf -*- + +# Copyright (C) 2001-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# For projects using AC_CONFIG_AUX_DIR([foo]), Autoconf sets +# $ac_aux_dir to '$srcdir/foo'. In other projects, it is set to +# '$srcdir', '$srcdir/..', or '$srcdir/../..'. +# +# Of course, Automake must honor this variable whenever it calls a +# tool from the auxiliary directory. The problem is that $srcdir (and +# therefore $ac_aux_dir as well) can be either absolute or relative, +# depending on how configure is run. This is pretty annoying, since +# it makes $ac_aux_dir quite unusable in subdirectories: in the top +# source directory, any form will work fine, but in subdirectories a +# relative path needs to be adjusted first. +# +# $ac_aux_dir/missing +# fails when called from a subdirectory if $ac_aux_dir is relative +# $top_srcdir/$ac_aux_dir/missing +# fails if $ac_aux_dir is absolute, +# fails when called from a subdirectory in a VPATH build with +# a relative $ac_aux_dir +# +# The reason of the latter failure is that $top_srcdir and $ac_aux_dir +# are both prefixed by $srcdir. In an in-source build this is usually +# harmless because $srcdir is '.', but things will broke when you +# start a VPATH build or use an absolute $srcdir. +# +# So we could use something similar to $top_srcdir/$ac_aux_dir/missing, +# iff we strip the leading $srcdir from $ac_aux_dir. That would be: +# am_aux_dir='\$(top_srcdir)/'`expr "$ac_aux_dir" : "$srcdir//*\(.*\)"` +# and then we would define $MISSING as +# MISSING="\${SHELL} $am_aux_dir/missing" +# This will work as long as MISSING is not called from configure, because +# unfortunately $(top_srcdir) has no meaning in configure. +# However there are other variables, like CC, which are often used in +# configure, and could therefore not use this "fixed" $ac_aux_dir. +# +# Another solution, used here, is to always expand $ac_aux_dir to an +# absolute PATH. The drawback is that using absolute paths prevent a +# configured tree to be moved without reconfiguration. + +AC_DEFUN([AM_AUX_DIR_EXPAND], +[AC_REQUIRE([AC_CONFIG_AUX_DIR_DEFAULT])dnl +# Expand $ac_aux_dir to an absolute path. +am_aux_dir=`cd "$ac_aux_dir" && pwd` +]) + +# AM_CONDITIONAL -*- Autoconf -*- + +# Copyright (C) 1997-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# AM_CONDITIONAL(NAME, SHELL-CONDITION) +# ------------------------------------- +# Define a conditional. +AC_DEFUN([AM_CONDITIONAL], +[AC_PREREQ([2.52])dnl + m4_if([$1], [TRUE], [AC_FATAL([$0: invalid condition: $1])], + [$1], [FALSE], [AC_FATAL([$0: invalid condition: $1])])dnl +AC_SUBST([$1_TRUE])dnl +AC_SUBST([$1_FALSE])dnl +_AM_SUBST_NOTMAKE([$1_TRUE])dnl +_AM_SUBST_NOTMAKE([$1_FALSE])dnl +m4_define([_AM_COND_VALUE_$1], [$2])dnl +if $2; then + $1_TRUE= + $1_FALSE='#' +else + $1_TRUE='#' + $1_FALSE= +fi +AC_CONFIG_COMMANDS_PRE( +[if test -z "${$1_TRUE}" && test -z "${$1_FALSE}"; then + AC_MSG_ERROR([[conditional "$1" was never defined. +Usually this means the macro was only invoked conditionally.]]) +fi])]) + +# Copyright (C) 1999-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + + +# There are a few dirty hacks below to avoid letting 'AC_PROG_CC' be +# written in clear, in which case automake, when reading aclocal.m4, +# will think it sees a *use*, and therefore will trigger all it's +# C support machinery. Also note that it means that autoscan, seeing +# CC etc. in the Makefile, will ask for an AC_PROG_CC use... + + +# _AM_DEPENDENCIES(NAME) +# ---------------------- +# See how the compiler implements dependency checking. +# NAME is "CC", "CXX", "OBJC", "OBJCXX", "UPC", or "GJC". +# We try a few techniques and use that to set a single cache variable. +# +# We don't AC_REQUIRE the corresponding AC_PROG_CC since the latter was +# modified to invoke _AM_DEPENDENCIES(CC); we would have a circular +# dependency, and given that the user is not expected to run this macro, +# just rely on AC_PROG_CC. +AC_DEFUN([_AM_DEPENDENCIES], +[AC_REQUIRE([AM_SET_DEPDIR])dnl +AC_REQUIRE([AM_OUTPUT_DEPENDENCY_COMMANDS])dnl +AC_REQUIRE([AM_MAKE_INCLUDE])dnl +AC_REQUIRE([AM_DEP_TRACK])dnl + +m4_if([$1], [CC], [depcc="$CC" am_compiler_list=], + [$1], [CXX], [depcc="$CXX" am_compiler_list=], + [$1], [OBJC], [depcc="$OBJC" am_compiler_list='gcc3 gcc'], + [$1], [OBJCXX], [depcc="$OBJCXX" am_compiler_list='gcc3 gcc'], + [$1], [UPC], [depcc="$UPC" am_compiler_list=], + [$1], [GCJ], [depcc="$GCJ" am_compiler_list='gcc3 gcc'], + [depcc="$$1" am_compiler_list=]) + +AC_CACHE_CHECK([dependency style of $depcc], + [am_cv_$1_dependencies_compiler_type], +[if test -z "$AMDEP_TRUE" && test -f "$am_depcomp"; then + # We make a subdir and do the tests there. Otherwise we can end up + # making bogus files that we don't know about and never remove. For + # instance it was reported that on HP-UX the gcc test will end up + # making a dummy file named 'D' -- because '-MD' means "put the output + # in D". + rm -rf conftest.dir + mkdir conftest.dir + # Copy depcomp to subdir because otherwise we won't find it if we're + # using a relative directory. + cp "$am_depcomp" conftest.dir + cd conftest.dir + # We will build objects and dependencies in a subdirectory because + # it helps to detect inapplicable dependency modes. For instance + # both Tru64's cc and ICC support -MD to output dependencies as a + # side effect of compilation, but ICC will put the dependencies in + # the current directory while Tru64 will put them in the object + # directory. + mkdir sub + + am_cv_$1_dependencies_compiler_type=none + if test "$am_compiler_list" = ""; then + am_compiler_list=`sed -n ['s/^#*\([a-zA-Z0-9]*\))$/\1/p'] < ./depcomp` + fi + am__universal=false + m4_case([$1], [CC], + [case " $depcc " in #( + *\ -arch\ *\ -arch\ *) am__universal=true ;; + esac], + [CXX], + [case " $depcc " in #( + *\ -arch\ *\ -arch\ *) am__universal=true ;; + esac]) + + for depmode in $am_compiler_list; do + # Setup a source with many dependencies, because some compilers + # like to wrap large dependency lists on column 80 (with \), and + # we should not choose a depcomp mode which is confused by this. + # + # We need to recreate these files for each test, as the compiler may + # overwrite some of them when testing with obscure command lines. + # This happens at least with the AIX C compiler. + : > sub/conftest.c + for i in 1 2 3 4 5 6; do + echo '#include "conftst'$i'.h"' >> sub/conftest.c + # Using ": > sub/conftst$i.h" creates only sub/conftst1.h with + # Solaris 10 /bin/sh. + echo '/* dummy */' > sub/conftst$i.h + done + echo "${am__include} ${am__quote}sub/conftest.Po${am__quote}" > confmf + + # We check with '-c' and '-o' for the sake of the "dashmstdout" + # mode. It turns out that the SunPro C++ compiler does not properly + # handle '-M -o', and we need to detect this. Also, some Intel + # versions had trouble with output in subdirs. + am__obj=sub/conftest.${OBJEXT-o} + am__minus_obj="-o $am__obj" + case $depmode in + gcc) + # This depmode causes a compiler race in universal mode. + test "$am__universal" = false || continue + ;; + nosideeffect) + # After this tag, mechanisms are not by side-effect, so they'll + # only be used when explicitly requested. + if test "x$enable_dependency_tracking" = xyes; then + continue + else + break + fi + ;; + msvc7 | msvc7msys | msvisualcpp | msvcmsys) + # This compiler won't grok '-c -o', but also, the minuso test has + # not run yet. These depmodes are late enough in the game, and + # so weak that their functioning should not be impacted. + am__obj=conftest.${OBJEXT-o} + am__minus_obj= + ;; + none) break ;; + esac + if depmode=$depmode \ + source=sub/conftest.c object=$am__obj \ + depfile=sub/conftest.Po tmpdepfile=sub/conftest.TPo \ + $SHELL ./depcomp $depcc -c $am__minus_obj sub/conftest.c \ + >/dev/null 2>conftest.err && + grep sub/conftst1.h sub/conftest.Po > /dev/null 2>&1 && + grep sub/conftst6.h sub/conftest.Po > /dev/null 2>&1 && + grep $am__obj sub/conftest.Po > /dev/null 2>&1 && + ${MAKE-make} -s -f confmf > /dev/null 2>&1; then + # icc doesn't choke on unknown options, it will just issue warnings + # or remarks (even with -Werror). So we grep stderr for any message + # that says an option was ignored or not supported. + # When given -MP, icc 7.0 and 7.1 complain thusly: + # icc: Command line warning: ignoring option '-M'; no argument required + # The diagnosis changed in icc 8.0: + # icc: Command line remark: option '-MP' not supported + if (grep 'ignoring option' conftest.err || + grep 'not supported' conftest.err) >/dev/null 2>&1; then :; else + am_cv_$1_dependencies_compiler_type=$depmode + break + fi + fi + done + + cd .. + rm -rf conftest.dir +else + am_cv_$1_dependencies_compiler_type=none +fi +]) +AC_SUBST([$1DEPMODE], [depmode=$am_cv_$1_dependencies_compiler_type]) +AM_CONDITIONAL([am__fastdep$1], [ + test "x$enable_dependency_tracking" != xno \ + && test "$am_cv_$1_dependencies_compiler_type" = gcc3]) +]) + + +# AM_SET_DEPDIR +# ------------- +# Choose a directory name for dependency files. +# This macro is AC_REQUIREd in _AM_DEPENDENCIES. +AC_DEFUN([AM_SET_DEPDIR], +[AC_REQUIRE([AM_SET_LEADING_DOT])dnl +AC_SUBST([DEPDIR], ["${am__leading_dot}deps"])dnl +]) + + +# AM_DEP_TRACK +# ------------ +AC_DEFUN([AM_DEP_TRACK], +[AC_ARG_ENABLE([dependency-tracking], [dnl +AS_HELP_STRING( + [--enable-dependency-tracking], + [do not reject slow dependency extractors]) +AS_HELP_STRING( + [--disable-dependency-tracking], + [speeds up one-time build])]) +if test "x$enable_dependency_tracking" != xno; then + am_depcomp="$ac_aux_dir/depcomp" + AMDEPBACKSLASH='\' + am__nodep='_no' +fi +AM_CONDITIONAL([AMDEP], [test "x$enable_dependency_tracking" != xno]) +AC_SUBST([AMDEPBACKSLASH])dnl +_AM_SUBST_NOTMAKE([AMDEPBACKSLASH])dnl +AC_SUBST([am__nodep])dnl +_AM_SUBST_NOTMAKE([am__nodep])dnl +]) + +# Generate code to set up dependency tracking. -*- Autoconf -*- + +# Copyright (C) 1999-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# _AM_OUTPUT_DEPENDENCY_COMMANDS +# ------------------------------ +AC_DEFUN([_AM_OUTPUT_DEPENDENCY_COMMANDS], +[{ + # Older Autoconf quotes --file arguments for eval, but not when files + # are listed without --file. Let's play safe and only enable the eval + # if we detect the quoting. + # TODO: see whether this extra hack can be removed once we start + # requiring Autoconf 2.70 or later. + AS_CASE([$CONFIG_FILES], + [*\'*], [eval set x "$CONFIG_FILES"], + [*], [set x $CONFIG_FILES]) + shift + # Used to flag and report bootstrapping failures. + am_rc=0 + for am_mf + do + # Strip MF so we end up with the name of the file. + am_mf=`AS_ECHO(["$am_mf"]) | sed -e 's/:.*$//'` + # Check whether this is an Automake generated Makefile which includes + # dependency-tracking related rules and includes. + # Grep'ing the whole file directly is not great: AIX grep has a line + # limit of 2048, but all sed's we know have understand at least 4000. + sed -n 's,^am--depfiles:.*,X,p' "$am_mf" | grep X >/dev/null 2>&1 \ + || continue + am_dirpart=`AS_DIRNAME(["$am_mf"])` + am_filepart=`AS_BASENAME(["$am_mf"])` + AM_RUN_LOG([cd "$am_dirpart" \ + && sed -e '/# am--include-marker/d' "$am_filepart" \ + | $MAKE -f - am--depfiles]) || am_rc=$? + done + if test $am_rc -ne 0; then + AC_MSG_FAILURE([Something went wrong bootstrapping makefile fragments + for automatic dependency tracking. Try re-running configure with the + '--disable-dependency-tracking' option to at least be able to build + the package (albeit without support for automatic dependency tracking).]) + fi + AS_UNSET([am_dirpart]) + AS_UNSET([am_filepart]) + AS_UNSET([am_mf]) + AS_UNSET([am_rc]) + rm -f conftest-deps.mk +} +])# _AM_OUTPUT_DEPENDENCY_COMMANDS + + +# AM_OUTPUT_DEPENDENCY_COMMANDS +# ----------------------------- +# This macro should only be invoked once -- use via AC_REQUIRE. +# +# This code is only required when automatic dependency tracking is enabled. +# This creates each '.Po' and '.Plo' makefile fragment that we'll need in +# order to bootstrap the dependency handling code. +AC_DEFUN([AM_OUTPUT_DEPENDENCY_COMMANDS], +[AC_CONFIG_COMMANDS([depfiles], + [test x"$AMDEP_TRUE" != x"" || _AM_OUTPUT_DEPENDENCY_COMMANDS], + [AMDEP_TRUE="$AMDEP_TRUE" MAKE="${MAKE-make}"])]) + +# Do all the work for Automake. -*- Autoconf -*- + +# Copyright (C) 1996-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This macro actually does too much. Some checks are only needed if +# your package does certain things. But this isn't really a big deal. + +dnl Redefine AC_PROG_CC to automatically invoke _AM_PROG_CC_C_O. +m4_define([AC_PROG_CC], +m4_defn([AC_PROG_CC]) +[_AM_PROG_CC_C_O +]) + +# AM_INIT_AUTOMAKE(PACKAGE, VERSION, [NO-DEFINE]) +# AM_INIT_AUTOMAKE([OPTIONS]) +# ----------------------------------------------- +# The call with PACKAGE and VERSION arguments is the old style +# call (pre autoconf-2.50), which is being phased out. PACKAGE +# and VERSION should now be passed to AC_INIT and removed from +# the call to AM_INIT_AUTOMAKE. +# We support both call styles for the transition. After +# the next Automake release, Autoconf can make the AC_INIT +# arguments mandatory, and then we can depend on a new Autoconf +# release and drop the old call support. +AC_DEFUN([AM_INIT_AUTOMAKE], +[AC_PREREQ([2.65])dnl +dnl Autoconf wants to disallow AM_ names. We explicitly allow +dnl the ones we care about. +m4_pattern_allow([^AM_[A-Z]+FLAGS$])dnl +AC_REQUIRE([AM_SET_CURRENT_AUTOMAKE_VERSION])dnl +AC_REQUIRE([AC_PROG_INSTALL])dnl +if test "`cd $srcdir && pwd`" != "`pwd`"; then + # Use -I$(srcdir) only when $(srcdir) != ., so that make's output + # is not polluted with repeated "-I." + AC_SUBST([am__isrc], [' -I$(srcdir)'])_AM_SUBST_NOTMAKE([am__isrc])dnl + # test to see if srcdir already configured + if test -f $srcdir/config.status; then + AC_MSG_ERROR([source directory already configured; run "make distclean" there first]) + fi +fi + +# test whether we have cygpath +if test -z "$CYGPATH_W"; then + if (cygpath --version) >/dev/null 2>/dev/null; then + CYGPATH_W='cygpath -w' + else + CYGPATH_W=echo + fi +fi +AC_SUBST([CYGPATH_W]) + +# Define the identity of the package. +dnl Distinguish between old-style and new-style calls. +m4_ifval([$2], +[AC_DIAGNOSE([obsolete], + [$0: two- and three-arguments forms are deprecated.]) +m4_ifval([$3], [_AM_SET_OPTION([no-define])])dnl + AC_SUBST([PACKAGE], [$1])dnl + AC_SUBST([VERSION], [$2])], +[_AM_SET_OPTIONS([$1])dnl +dnl Diagnose old-style AC_INIT with new-style AM_AUTOMAKE_INIT. +m4_if( + m4_ifdef([AC_PACKAGE_NAME], [ok]):m4_ifdef([AC_PACKAGE_VERSION], [ok]), + [ok:ok],, + [m4_fatal([AC_INIT should be called with package and version arguments])])dnl + AC_SUBST([PACKAGE], ['AC_PACKAGE_TARNAME'])dnl + AC_SUBST([VERSION], ['AC_PACKAGE_VERSION'])])dnl + +_AM_IF_OPTION([no-define],, +[AC_DEFINE_UNQUOTED([PACKAGE], ["$PACKAGE"], [Name of package]) + AC_DEFINE_UNQUOTED([VERSION], ["$VERSION"], [Version number of package])])dnl + +# Some tools Automake needs. +AC_REQUIRE([AM_SANITY_CHECK])dnl +AC_REQUIRE([AC_ARG_PROGRAM])dnl +AM_MISSING_PROG([ACLOCAL], [aclocal-${am__api_version}]) +AM_MISSING_PROG([AUTOCONF], [autoconf]) +AM_MISSING_PROG([AUTOMAKE], [automake-${am__api_version}]) +AM_MISSING_PROG([AUTOHEADER], [autoheader]) +AM_MISSING_PROG([MAKEINFO], [makeinfo]) +AC_REQUIRE([AM_PROG_INSTALL_SH])dnl +AC_REQUIRE([AM_PROG_INSTALL_STRIP])dnl +AC_REQUIRE([AC_PROG_MKDIR_P])dnl +# For better backward compatibility. To be removed once Automake 1.9.x +# dies out for good. For more background, see: +# +# +AC_SUBST([mkdir_p], ['$(MKDIR_P)']) +# We need awk for the "check" target (and possibly the TAP driver). The +# system "awk" is bad on some platforms. +AC_REQUIRE([AC_PROG_AWK])dnl +AC_REQUIRE([AC_PROG_MAKE_SET])dnl +AC_REQUIRE([AM_SET_LEADING_DOT])dnl +_AM_IF_OPTION([tar-ustar], [_AM_PROG_TAR([ustar])], + [_AM_IF_OPTION([tar-pax], [_AM_PROG_TAR([pax])], + [_AM_PROG_TAR([v7])])]) +_AM_IF_OPTION([no-dependencies],, +[AC_PROVIDE_IFELSE([AC_PROG_CC], + [_AM_DEPENDENCIES([CC])], + [m4_define([AC_PROG_CC], + m4_defn([AC_PROG_CC])[_AM_DEPENDENCIES([CC])])])dnl +AC_PROVIDE_IFELSE([AC_PROG_CXX], + [_AM_DEPENDENCIES([CXX])], + [m4_define([AC_PROG_CXX], + m4_defn([AC_PROG_CXX])[_AM_DEPENDENCIES([CXX])])])dnl +AC_PROVIDE_IFELSE([AC_PROG_OBJC], + [_AM_DEPENDENCIES([OBJC])], + [m4_define([AC_PROG_OBJC], + m4_defn([AC_PROG_OBJC])[_AM_DEPENDENCIES([OBJC])])])dnl +AC_PROVIDE_IFELSE([AC_PROG_OBJCXX], + [_AM_DEPENDENCIES([OBJCXX])], + [m4_define([AC_PROG_OBJCXX], + m4_defn([AC_PROG_OBJCXX])[_AM_DEPENDENCIES([OBJCXX])])])dnl +]) +AC_REQUIRE([AM_SILENT_RULES])dnl +dnl The testsuite driver may need to know about EXEEXT, so add the +dnl 'am__EXEEXT' conditional if _AM_COMPILER_EXEEXT was seen. This +dnl macro is hooked onto _AC_COMPILER_EXEEXT early, see below. +AC_CONFIG_COMMANDS_PRE(dnl +[m4_provide_if([_AM_COMPILER_EXEEXT], + [AM_CONDITIONAL([am__EXEEXT], [test -n "$EXEEXT"])])])dnl + +# POSIX will say in a future version that running "rm -f" with no argument +# is OK; and we want to be able to make that assumption in our Makefile +# recipes. So use an aggressive probe to check that the usage we want is +# actually supported "in the wild" to an acceptable degree. +# See automake bug#10828. +# To make any issue more visible, cause the running configure to be aborted +# by default if the 'rm' program in use doesn't match our expectations; the +# user can still override this though. +if rm -f && rm -fr && rm -rf; then : OK; else + cat >&2 <<'END' +Oops! + +Your 'rm' program seems unable to run without file operands specified +on the command line, even when the '-f' option is present. This is contrary +to the behaviour of most rm programs out there, and not conforming with +the upcoming POSIX standard: + +Please tell bug-automake@gnu.org about your system, including the value +of your $PATH and any error possibly output before this message. This +can help us improve future automake versions. + +END + if test x"$ACCEPT_INFERIOR_RM_PROGRAM" = x"yes"; then + echo 'Configuration will proceed anyway, since you have set the' >&2 + echo 'ACCEPT_INFERIOR_RM_PROGRAM variable to "yes"' >&2 + echo >&2 + else + cat >&2 <<'END' +Aborting the configuration process, to ensure you take notice of the issue. + +You can download and install GNU coreutils to get an 'rm' implementation +that behaves properly: . + +If you want to complete the configuration process using your problematic +'rm' anyway, export the environment variable ACCEPT_INFERIOR_RM_PROGRAM +to "yes", and re-run configure. + +END + AC_MSG_ERROR([Your 'rm' program is bad, sorry.]) + fi +fi +dnl The trailing newline in this macro's definition is deliberate, for +dnl backward compatibility and to allow trailing 'dnl'-style comments +dnl after the AM_INIT_AUTOMAKE invocation. See automake bug#16841. +]) + +dnl Hook into '_AC_COMPILER_EXEEXT' early to learn its expansion. Do not +dnl add the conditional right here, as _AC_COMPILER_EXEEXT may be further +dnl mangled by Autoconf and run in a shell conditional statement. +m4_define([_AC_COMPILER_EXEEXT], +m4_defn([_AC_COMPILER_EXEEXT])[m4_provide([_AM_COMPILER_EXEEXT])]) + +# When config.status generates a header, we must update the stamp-h file. +# This file resides in the same directory as the config header +# that is generated. The stamp files are numbered to have different names. + +# Autoconf calls _AC_AM_CONFIG_HEADER_HOOK (when defined) in the +# loop where config.status creates the headers, so we can generate +# our stamp files there. +AC_DEFUN([_AC_AM_CONFIG_HEADER_HOOK], +[# Compute $1's index in $config_headers. +_am_arg=$1 +_am_stamp_count=1 +for _am_header in $config_headers :; do + case $_am_header in + $_am_arg | $_am_arg:* ) + break ;; + * ) + _am_stamp_count=`expr $_am_stamp_count + 1` ;; + esac +done +echo "timestamp for $_am_arg" >`AS_DIRNAME(["$_am_arg"])`/stamp-h[]$_am_stamp_count]) + +# Copyright (C) 2001-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# AM_PROG_INSTALL_SH +# ------------------ +# Define $install_sh. +AC_DEFUN([AM_PROG_INSTALL_SH], +[AC_REQUIRE([AM_AUX_DIR_EXPAND])dnl +if test x"${install_sh+set}" != xset; then + case $am_aux_dir in + *\ * | *\ *) + install_sh="\${SHELL} '$am_aux_dir/install-sh'" ;; + *) + install_sh="\${SHELL} $am_aux_dir/install-sh" + esac +fi +AC_SUBST([install_sh])]) + +# Copyright (C) 2003-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# Check whether the underlying file-system supports filenames +# with a leading dot. For instance MS-DOS doesn't. +AC_DEFUN([AM_SET_LEADING_DOT], +[rm -rf .tst 2>/dev/null +mkdir .tst 2>/dev/null +if test -d .tst; then + am__leading_dot=. +else + am__leading_dot=_ +fi +rmdir .tst 2>/dev/null +AC_SUBST([am__leading_dot])]) + +# Add --enable-maintainer-mode option to configure. -*- Autoconf -*- +# From Jim Meyering + +# Copyright (C) 1996-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# AM_MAINTAINER_MODE([DEFAULT-MODE]) +# ---------------------------------- +# Control maintainer-specific portions of Makefiles. +# Default is to disable them, unless 'enable' is passed literally. +# For symmetry, 'disable' may be passed as well. Anyway, the user +# can override the default with the --enable/--disable switch. +AC_DEFUN([AM_MAINTAINER_MODE], +[m4_case(m4_default([$1], [disable]), + [enable], [m4_define([am_maintainer_other], [disable])], + [disable], [m4_define([am_maintainer_other], [enable])], + [m4_define([am_maintainer_other], [enable]) + m4_warn([syntax], [unexpected argument to AM@&t@_MAINTAINER_MODE: $1])]) +AC_MSG_CHECKING([whether to enable maintainer-specific portions of Makefiles]) + dnl maintainer-mode's default is 'disable' unless 'enable' is passed + AC_ARG_ENABLE([maintainer-mode], + [AS_HELP_STRING([--]am_maintainer_other[-maintainer-mode], + am_maintainer_other[ make rules and dependencies not useful + (and sometimes confusing) to the casual installer])], + [USE_MAINTAINER_MODE=$enableval], + [USE_MAINTAINER_MODE=]m4_if(am_maintainer_other, [enable], [no], [yes])) + AC_MSG_RESULT([$USE_MAINTAINER_MODE]) + AM_CONDITIONAL([MAINTAINER_MODE], [test $USE_MAINTAINER_MODE = yes]) + MAINT=$MAINTAINER_MODE_TRUE + AC_SUBST([MAINT])dnl +] +) + +# Check to see how 'make' treats includes. -*- Autoconf -*- + +# Copyright (C) 2001-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# AM_MAKE_INCLUDE() +# ----------------- +# Check whether make has an 'include' directive that can support all +# the idioms we need for our automatic dependency tracking code. +AC_DEFUN([AM_MAKE_INCLUDE], +[AC_MSG_CHECKING([whether ${MAKE-make} supports the include directive]) +cat > confinc.mk << 'END' +am__doit: + @echo this is the am__doit target >confinc.out +.PHONY: am__doit +END +am__include="#" +am__quote= +# BSD make does it like this. +echo '.include "confinc.mk" # ignored' > confmf.BSD +# Other make implementations (GNU, Solaris 10, AIX) do it like this. +echo 'include confinc.mk # ignored' > confmf.GNU +_am_result=no +for s in GNU BSD; do + AM_RUN_LOG([${MAKE-make} -f confmf.$s && cat confinc.out]) + AS_CASE([$?:`cat confinc.out 2>/dev/null`], + ['0:this is the am__doit target'], + [AS_CASE([$s], + [BSD], [am__include='.include' am__quote='"'], + [am__include='include' am__quote=''])]) + if test "$am__include" != "#"; then + _am_result="yes ($s style)" + break + fi +done +rm -f confinc.* confmf.* +AC_MSG_RESULT([${_am_result}]) +AC_SUBST([am__include])]) +AC_SUBST([am__quote])]) + +# Fake the existence of programs that GNU maintainers use. -*- Autoconf -*- + +# Copyright (C) 1997-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# AM_MISSING_PROG(NAME, PROGRAM) +# ------------------------------ +AC_DEFUN([AM_MISSING_PROG], +[AC_REQUIRE([AM_MISSING_HAS_RUN]) +$1=${$1-"${am_missing_run}$2"} +AC_SUBST($1)]) + +# AM_MISSING_HAS_RUN +# ------------------ +# Define MISSING if not defined so far and test if it is modern enough. +# If it is, set am_missing_run to use it, otherwise, to nothing. +AC_DEFUN([AM_MISSING_HAS_RUN], +[AC_REQUIRE([AM_AUX_DIR_EXPAND])dnl +AC_REQUIRE_AUX_FILE([missing])dnl +if test x"${MISSING+set}" != xset; then + case $am_aux_dir in + *\ * | *\ *) + MISSING="\${SHELL} \"$am_aux_dir/missing\"" ;; + *) + MISSING="\${SHELL} $am_aux_dir/missing" ;; + esac +fi +# Use eval to expand $SHELL +if eval "$MISSING --is-lightweight"; then + am_missing_run="$MISSING " +else + am_missing_run= + AC_MSG_WARN(['missing' script is too old or missing]) +fi +]) + +# Helper functions for option handling. -*- Autoconf -*- + +# Copyright (C) 2001-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# _AM_MANGLE_OPTION(NAME) +# ----------------------- +AC_DEFUN([_AM_MANGLE_OPTION], +[[_AM_OPTION_]m4_bpatsubst($1, [[^a-zA-Z0-9_]], [_])]) + +# _AM_SET_OPTION(NAME) +# -------------------- +# Set option NAME. Presently that only means defining a flag for this option. +AC_DEFUN([_AM_SET_OPTION], +[m4_define(_AM_MANGLE_OPTION([$1]), [1])]) + +# _AM_SET_OPTIONS(OPTIONS) +# ------------------------ +# OPTIONS is a space-separated list of Automake options. +AC_DEFUN([_AM_SET_OPTIONS], +[m4_foreach_w([_AM_Option], [$1], [_AM_SET_OPTION(_AM_Option)])]) + +# _AM_IF_OPTION(OPTION, IF-SET, [IF-NOT-SET]) +# ------------------------------------------- +# Execute IF-SET if OPTION is set, IF-NOT-SET otherwise. +AC_DEFUN([_AM_IF_OPTION], +[m4_ifset(_AM_MANGLE_OPTION([$1]), [$2], [$3])]) + +# Copyright (C) 1999-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# _AM_PROG_CC_C_O +# --------------- +# Like AC_PROG_CC_C_O, but changed for automake. We rewrite AC_PROG_CC +# to automatically call this. +AC_DEFUN([_AM_PROG_CC_C_O], +[AC_REQUIRE([AM_AUX_DIR_EXPAND])dnl +AC_REQUIRE_AUX_FILE([compile])dnl +AC_LANG_PUSH([C])dnl +AC_CACHE_CHECK( + [whether $CC understands -c and -o together], + [am_cv_prog_cc_c_o], + [AC_LANG_CONFTEST([AC_LANG_PROGRAM([])]) + # Make sure it works both with $CC and with simple cc. + # Following AC_PROG_CC_C_O, we do the test twice because some + # compilers refuse to overwrite an existing .o file with -o, + # though they will create one. + am_cv_prog_cc_c_o=yes + for am_i in 1 2; do + if AM_RUN_LOG([$CC -c conftest.$ac_ext -o conftest2.$ac_objext]) \ + && test -f conftest2.$ac_objext; then + : OK + else + am_cv_prog_cc_c_o=no + break + fi + done + rm -f core conftest* + unset am_i]) +if test "$am_cv_prog_cc_c_o" != yes; then + # Losing compiler, so override with the script. + # FIXME: It is wrong to rewrite CC. + # But if we don't then we get into trouble of one sort or another. + # A longer-term fix would be to have automake use am__CC in this case, + # and then we could set am__CC="\$(top_srcdir)/compile \$(CC)" + CC="$am_aux_dir/compile $CC" +fi +AC_LANG_POP([C])]) + +# For backward compatibility. +AC_DEFUN_ONCE([AM_PROG_CC_C_O], [AC_REQUIRE([AC_PROG_CC])]) + +# Copyright (C) 2001-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# AM_RUN_LOG(COMMAND) +# ------------------- +# Run COMMAND, save the exit status in ac_status, and log it. +# (This has been adapted from Autoconf's _AC_RUN_LOG macro.) +AC_DEFUN([AM_RUN_LOG], +[{ echo "$as_me:$LINENO: $1" >&AS_MESSAGE_LOG_FD + ($1) >&AS_MESSAGE_LOG_FD 2>&AS_MESSAGE_LOG_FD + ac_status=$? + echo "$as_me:$LINENO: \$? = $ac_status" >&AS_MESSAGE_LOG_FD + (exit $ac_status); }]) + +# Check to make sure that the build environment is sane. -*- Autoconf -*- + +# Copyright (C) 1996-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# AM_SANITY_CHECK +# --------------- +AC_DEFUN([AM_SANITY_CHECK], +[AC_MSG_CHECKING([whether build environment is sane]) +# Reject unsafe characters in $srcdir or the absolute working directory +# name. Accept space and tab only in the latter. +am_lf=' +' +case `pwd` in + *[[\\\"\#\$\&\'\`$am_lf]]*) + AC_MSG_ERROR([unsafe absolute working directory name]);; +esac +case $srcdir in + *[[\\\"\#\$\&\'\`$am_lf\ \ ]]*) + AC_MSG_ERROR([unsafe srcdir value: '$srcdir']);; +esac + +# Do 'set' in a subshell so we don't clobber the current shell's +# arguments. Must try -L first in case configure is actually a +# symlink; some systems play weird games with the mod time of symlinks +# (eg FreeBSD returns the mod time of the symlink's containing +# directory). +if ( + am_has_slept=no + for am_try in 1 2; do + echo "timestamp, slept: $am_has_slept" > conftest.file + set X `ls -Lt "$srcdir/configure" conftest.file 2> /dev/null` + if test "$[*]" = "X"; then + # -L didn't work. + set X `ls -t "$srcdir/configure" conftest.file` + fi + if test "$[*]" != "X $srcdir/configure conftest.file" \ + && test "$[*]" != "X conftest.file $srcdir/configure"; then + + # If neither matched, then we have a broken ls. This can happen + # if, for instance, CONFIG_SHELL is bash and it inherits a + # broken ls alias from the environment. This has actually + # happened. Such a system could not be considered "sane". + AC_MSG_ERROR([ls -t appears to fail. Make sure there is not a broken + alias in your environment]) + fi + if test "$[2]" = conftest.file || test $am_try -eq 2; then + break + fi + # Just in case. + sleep 1 + am_has_slept=yes + done + test "$[2]" = conftest.file + ) +then + # Ok. + : +else + AC_MSG_ERROR([newly created file is older than distributed files! +Check your system clock]) +fi +AC_MSG_RESULT([yes]) +# If we didn't sleep, we still need to ensure time stamps of config.status and +# generated files are strictly newer. +am_sleep_pid= +if grep 'slept: no' conftest.file >/dev/null 2>&1; then + ( sleep 1 ) & + am_sleep_pid=$! +fi +AC_CONFIG_COMMANDS_PRE( + [AC_MSG_CHECKING([that generated files are newer than configure]) + if test -n "$am_sleep_pid"; then + # Hide warnings about reused PIDs. + wait $am_sleep_pid 2>/dev/null + fi + AC_MSG_RESULT([done])]) +rm -f conftest.file +]) + +# Copyright (C) 2009-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# AM_SILENT_RULES([DEFAULT]) +# -------------------------- +# Enable less verbose build rules; with the default set to DEFAULT +# ("yes" being less verbose, "no" or empty being verbose). +AC_DEFUN([AM_SILENT_RULES], +[AC_ARG_ENABLE([silent-rules], [dnl +AS_HELP_STRING( + [--enable-silent-rules], + [less verbose build output (undo: "make V=1")]) +AS_HELP_STRING( + [--disable-silent-rules], + [verbose build output (undo: "make V=0")])dnl +]) +case $enable_silent_rules in @%:@ ((( + yes) AM_DEFAULT_VERBOSITY=0;; + no) AM_DEFAULT_VERBOSITY=1;; + *) AM_DEFAULT_VERBOSITY=m4_if([$1], [yes], [0], [1]);; +esac +dnl +dnl A few 'make' implementations (e.g., NonStop OS and NextStep) +dnl do not support nested variable expansions. +dnl See automake bug#9928 and bug#10237. +am_make=${MAKE-make} +AC_CACHE_CHECK([whether $am_make supports nested variables], + [am_cv_make_support_nested_variables], + [if AS_ECHO([['TRUE=$(BAR$(V)) +BAR0=false +BAR1=true +V=1 +am__doit: + @$(TRUE) +.PHONY: am__doit']]) | $am_make -f - >/dev/null 2>&1; then + am_cv_make_support_nested_variables=yes +else + am_cv_make_support_nested_variables=no +fi]) +if test $am_cv_make_support_nested_variables = yes; then + dnl Using '$V' instead of '$(V)' breaks IRIX make. + AM_V='$(V)' + AM_DEFAULT_V='$(AM_DEFAULT_VERBOSITY)' +else + AM_V=$AM_DEFAULT_VERBOSITY + AM_DEFAULT_V=$AM_DEFAULT_VERBOSITY +fi +AC_SUBST([AM_V])dnl +AM_SUBST_NOTMAKE([AM_V])dnl +AC_SUBST([AM_DEFAULT_V])dnl +AM_SUBST_NOTMAKE([AM_DEFAULT_V])dnl +AC_SUBST([AM_DEFAULT_VERBOSITY])dnl +AM_BACKSLASH='\' +AC_SUBST([AM_BACKSLASH])dnl +_AM_SUBST_NOTMAKE([AM_BACKSLASH])dnl +]) + +# Copyright (C) 2001-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# AM_PROG_INSTALL_STRIP +# --------------------- +# One issue with vendor 'install' (even GNU) is that you can't +# specify the program used to strip binaries. This is especially +# annoying in cross-compiling environments, where the build's strip +# is unlikely to handle the host's binaries. +# Fortunately install-sh will honor a STRIPPROG variable, so we +# always use install-sh in "make install-strip", and initialize +# STRIPPROG with the value of the STRIP variable (set by the user). +AC_DEFUN([AM_PROG_INSTALL_STRIP], +[AC_REQUIRE([AM_PROG_INSTALL_SH])dnl +# Installed binaries are usually stripped using 'strip' when the user +# run "make install-strip". However 'strip' might not be the right +# tool to use in cross-compilation environments, therefore Automake +# will honor the 'STRIP' environment variable to overrule this program. +dnl Don't test for $cross_compiling = yes, because it might be 'maybe'. +if test "$cross_compiling" != no; then + AC_CHECK_TOOL([STRIP], [strip], :) +fi +INSTALL_STRIP_PROGRAM="\$(install_sh) -c -s" +AC_SUBST([INSTALL_STRIP_PROGRAM])]) + +# Copyright (C) 2006-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# _AM_SUBST_NOTMAKE(VARIABLE) +# --------------------------- +# Prevent Automake from outputting VARIABLE = @VARIABLE@ in Makefile.in. +# This macro is traced by Automake. +AC_DEFUN([_AM_SUBST_NOTMAKE]) + +# AM_SUBST_NOTMAKE(VARIABLE) +# -------------------------- +# Public sister of _AM_SUBST_NOTMAKE. +AC_DEFUN([AM_SUBST_NOTMAKE], [_AM_SUBST_NOTMAKE($@)]) + +# Check how to create a tarball. -*- Autoconf -*- + +# Copyright (C) 2004-2018 Free Software Foundation, Inc. +# +# This file is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# _AM_PROG_TAR(FORMAT) +# -------------------- +# Check how to create a tarball in format FORMAT. +# FORMAT should be one of 'v7', 'ustar', or 'pax'. +# +# Substitute a variable $(am__tar) that is a command +# writing to stdout a FORMAT-tarball containing the directory +# $tardir. +# tardir=directory && $(am__tar) > result.tar +# +# Substitute a variable $(am__untar) that extract such +# a tarball read from stdin. +# $(am__untar) < result.tar +# +AC_DEFUN([_AM_PROG_TAR], +[# Always define AMTAR for backward compatibility. Yes, it's still used +# in the wild :-( We should find a proper way to deprecate it ... +AC_SUBST([AMTAR], ['$${TAR-tar}']) + +# We'll loop over all known methods to create a tar archive until one works. +_am_tools='gnutar m4_if([$1], [ustar], [plaintar]) pax cpio none' + +m4_if([$1], [v7], + [am__tar='$${TAR-tar} chof - "$$tardir"' am__untar='$${TAR-tar} xf -'], + + [m4_case([$1], + [ustar], + [# The POSIX 1988 'ustar' format is defined with fixed-size fields. + # There is notably a 21 bits limit for the UID and the GID. In fact, + # the 'pax' utility can hang on bigger UID/GID (see automake bug#8343 + # and bug#13588). + am_max_uid=2097151 # 2^21 - 1 + am_max_gid=$am_max_uid + # The $UID and $GID variables are not portable, so we need to resort + # to the POSIX-mandated id(1) utility. Errors in the 'id' calls + # below are definitely unexpected, so allow the users to see them + # (that is, avoid stderr redirection). + am_uid=`id -u || echo unknown` + am_gid=`id -g || echo unknown` + AC_MSG_CHECKING([whether UID '$am_uid' is supported by ustar format]) + if test $am_uid -le $am_max_uid; then + AC_MSG_RESULT([yes]) + else + AC_MSG_RESULT([no]) + _am_tools=none + fi + AC_MSG_CHECKING([whether GID '$am_gid' is supported by ustar format]) + if test $am_gid -le $am_max_gid; then + AC_MSG_RESULT([yes]) + else + AC_MSG_RESULT([no]) + _am_tools=none + fi], + + [pax], + [], + + [m4_fatal([Unknown tar format])]) + + AC_MSG_CHECKING([how to create a $1 tar archive]) + + # Go ahead even if we have the value already cached. We do so because we + # need to set the values for the 'am__tar' and 'am__untar' variables. + _am_tools=${am_cv_prog_tar_$1-$_am_tools} + + for _am_tool in $_am_tools; do + case $_am_tool in + gnutar) + for _am_tar in tar gnutar gtar; do + AM_RUN_LOG([$_am_tar --version]) && break + done + am__tar="$_am_tar --format=m4_if([$1], [pax], [posix], [$1]) -chf - "'"$$tardir"' + am__tar_="$_am_tar --format=m4_if([$1], [pax], [posix], [$1]) -chf - "'"$tardir"' + am__untar="$_am_tar -xf -" + ;; + plaintar) + # Must skip GNU tar: if it does not support --format= it doesn't create + # ustar tarball either. + (tar --version) >/dev/null 2>&1 && continue + am__tar='tar chf - "$$tardir"' + am__tar_='tar chf - "$tardir"' + am__untar='tar xf -' + ;; + pax) + am__tar='pax -L -x $1 -w "$$tardir"' + am__tar_='pax -L -x $1 -w "$tardir"' + am__untar='pax -r' + ;; + cpio) + am__tar='find "$$tardir" -print | cpio -o -H $1 -L' + am__tar_='find "$tardir" -print | cpio -o -H $1 -L' + am__untar='cpio -i -H $1 -d' + ;; + none) + am__tar=false + am__tar_=false + am__untar=false + ;; + esac + + # If the value was cached, stop now. We just wanted to have am__tar + # and am__untar set. + test -n "${am_cv_prog_tar_$1}" && break + + # tar/untar a dummy directory, and stop if the command works. + rm -rf conftest.dir + mkdir conftest.dir + echo GrepMe > conftest.dir/file + AM_RUN_LOG([tardir=conftest.dir && eval $am__tar_ >conftest.tar]) + rm -rf conftest.dir + if test -s conftest.tar; then + AM_RUN_LOG([$am__untar /dev/null 2>&1 && break + fi + done + rm -rf conftest.dir + + AC_CACHE_VAL([am_cv_prog_tar_$1], [am_cv_prog_tar_$1=$_am_tool]) + AC_MSG_RESULT([$am_cv_prog_tar_$1])]) + +AC_SUBST([am__tar]) +AC_SUBST([am__untar]) +]) # _AM_PROG_TAR + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/autogen.sh b/deps/ngtcp2/ngtcp2/third-party/libev/autogen.sh new file mode 100644 index 00000000000000..8056ee7f9be66c --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/autogen.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +autoreconf --install --symlink --force diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/compile b/deps/ngtcp2/ngtcp2/third-party/libev/compile new file mode 100755 index 00000000000000..99e50524b3bade --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/compile @@ -0,0 +1,348 @@ +#! /bin/sh +# Wrapper for compilers which do not understand '-c -o'. + +scriptversion=2018-03-07.03; # UTC + +# Copyright (C) 1999-2018 Free Software Foundation, Inc. +# Written by Tom Tromey . +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program that contains a +# configuration script generated by Autoconf, you may include it under +# the same distribution terms that you use for the rest of that program. + +# This file is maintained in Automake, please report +# bugs to or send patches to +# . + +nl=' +' + +# We need space, tab and new line, in precisely that order. Quoting is +# there to prevent tools from complaining about whitespace usage. +IFS=" "" $nl" + +file_conv= + +# func_file_conv build_file lazy +# Convert a $build file to $host form and store it in $file +# Currently only supports Windows hosts. If the determined conversion +# type is listed in (the comma separated) LAZY, no conversion will +# take place. +func_file_conv () +{ + file=$1 + case $file in + / | /[!/]*) # absolute file, and not a UNC file + if test -z "$file_conv"; then + # lazily determine how to convert abs files + case `uname -s` in + MINGW*) + file_conv=mingw + ;; + CYGWIN*) + file_conv=cygwin + ;; + *) + file_conv=wine + ;; + esac + fi + case $file_conv/,$2, in + *,$file_conv,*) + ;; + mingw/*) + file=`cmd //C echo "$file " | sed -e 's/"\(.*\) " *$/\1/'` + ;; + cygwin/*) + file=`cygpath -m "$file" || echo "$file"` + ;; + wine/*) + file=`winepath -w "$file" || echo "$file"` + ;; + esac + ;; + esac +} + +# func_cl_dashL linkdir +# Make cl look for libraries in LINKDIR +func_cl_dashL () +{ + func_file_conv "$1" + if test -z "$lib_path"; then + lib_path=$file + else + lib_path="$lib_path;$file" + fi + linker_opts="$linker_opts -LIBPATH:$file" +} + +# func_cl_dashl library +# Do a library search-path lookup for cl +func_cl_dashl () +{ + lib=$1 + found=no + save_IFS=$IFS + IFS=';' + for dir in $lib_path $LIB + do + IFS=$save_IFS + if $shared && test -f "$dir/$lib.dll.lib"; then + found=yes + lib=$dir/$lib.dll.lib + break + fi + if test -f "$dir/$lib.lib"; then + found=yes + lib=$dir/$lib.lib + break + fi + if test -f "$dir/lib$lib.a"; then + found=yes + lib=$dir/lib$lib.a + break + fi + done + IFS=$save_IFS + + if test "$found" != yes; then + lib=$lib.lib + fi +} + +# func_cl_wrapper cl arg... +# Adjust compile command to suit cl +func_cl_wrapper () +{ + # Assume a capable shell + lib_path= + shared=: + linker_opts= + for arg + do + if test -n "$eat"; then + eat= + else + case $1 in + -o) + # configure might choose to run compile as 'compile cc -o foo foo.c'. + eat=1 + case $2 in + *.o | *.[oO][bB][jJ]) + func_file_conv "$2" + set x "$@" -Fo"$file" + shift + ;; + *) + func_file_conv "$2" + set x "$@" -Fe"$file" + shift + ;; + esac + ;; + -I) + eat=1 + func_file_conv "$2" mingw + set x "$@" -I"$file" + shift + ;; + -I*) + func_file_conv "${1#-I}" mingw + set x "$@" -I"$file" + shift + ;; + -l) + eat=1 + func_cl_dashl "$2" + set x "$@" "$lib" + shift + ;; + -l*) + func_cl_dashl "${1#-l}" + set x "$@" "$lib" + shift + ;; + -L) + eat=1 + func_cl_dashL "$2" + ;; + -L*) + func_cl_dashL "${1#-L}" + ;; + -static) + shared=false + ;; + -Wl,*) + arg=${1#-Wl,} + save_ifs="$IFS"; IFS=',' + for flag in $arg; do + IFS="$save_ifs" + linker_opts="$linker_opts $flag" + done + IFS="$save_ifs" + ;; + -Xlinker) + eat=1 + linker_opts="$linker_opts $2" + ;; + -*) + set x "$@" "$1" + shift + ;; + *.cc | *.CC | *.cxx | *.CXX | *.[cC]++) + func_file_conv "$1" + set x "$@" -Tp"$file" + shift + ;; + *.c | *.cpp | *.CPP | *.lib | *.LIB | *.Lib | *.OBJ | *.obj | *.[oO]) + func_file_conv "$1" mingw + set x "$@" "$file" + shift + ;; + *) + set x "$@" "$1" + shift + ;; + esac + fi + shift + done + if test -n "$linker_opts"; then + linker_opts="-link$linker_opts" + fi + exec "$@" $linker_opts + exit 1 +} + +eat= + +case $1 in + '') + echo "$0: No command. Try '$0 --help' for more information." 1>&2 + exit 1; + ;; + -h | --h*) + cat <<\EOF +Usage: compile [--help] [--version] PROGRAM [ARGS] + +Wrapper for compilers which do not understand '-c -o'. +Remove '-o dest.o' from ARGS, run PROGRAM with the remaining +arguments, and rename the output as expected. + +If you are trying to build a whole package this is not the +right script to run: please start by reading the file 'INSTALL'. + +Report bugs to . +EOF + exit $? + ;; + -v | --v*) + echo "compile $scriptversion" + exit $? + ;; + cl | *[/\\]cl | cl.exe | *[/\\]cl.exe | \ + icl | *[/\\]icl | icl.exe | *[/\\]icl.exe ) + func_cl_wrapper "$@" # Doesn't return... + ;; +esac + +ofile= +cfile= + +for arg +do + if test -n "$eat"; then + eat= + else + case $1 in + -o) + # configure might choose to run compile as 'compile cc -o foo foo.c'. + # So we strip '-o arg' only if arg is an object. + eat=1 + case $2 in + *.o | *.obj) + ofile=$2 + ;; + *) + set x "$@" -o "$2" + shift + ;; + esac + ;; + *.c) + cfile=$1 + set x "$@" "$1" + shift + ;; + *) + set x "$@" "$1" + shift + ;; + esac + fi + shift +done + +if test -z "$ofile" || test -z "$cfile"; then + # If no '-o' option was seen then we might have been invoked from a + # pattern rule where we don't need one. That is ok -- this is a + # normal compilation that the losing compiler can handle. If no + # '.c' file was seen then we are probably linking. That is also + # ok. + exec "$@" +fi + +# Name of file we expect compiler to create. +cofile=`echo "$cfile" | sed 's|^.*[\\/]||; s|^[a-zA-Z]:||; s/\.c$/.o/'` + +# Create the lock directory. +# Note: use '[/\\:.-]' here to ensure that we don't use the same name +# that we are using for the .o file. Also, base the name on the expected +# object file name, since that is what matters with a parallel build. +lockdir=`echo "$cofile" | sed -e 's|[/\\:.-]|_|g'`.d +while true; do + if mkdir "$lockdir" >/dev/null 2>&1; then + break + fi + sleep 1 +done +# FIXME: race condition here if user kills between mkdir and trap. +trap "rmdir '$lockdir'; exit 1" 1 2 15 + +# Run the compile. +"$@" +ret=$? + +if test -f "$cofile"; then + test "$cofile" = "$ofile" || mv "$cofile" "$ofile" +elif test -f "${cofile}bj"; then + test "${cofile}bj" = "$ofile" || mv "${cofile}bj" "$ofile" +fi + +rmdir "$lockdir" +exit $ret + +# Local Variables: +# mode: shell-script +# sh-indentation: 2 +# eval: (add-hook 'before-save-hook 'time-stamp) +# time-stamp-start: "scriptversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-time-zone: "UTC0" +# time-stamp-end: "; # UTC" +# End: diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/config.guess b/deps/ngtcp2/ngtcp2/third-party/libev/config.guess new file mode 100755 index 00000000000000..f50dcdb6de2af0 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/config.guess @@ -0,0 +1,1480 @@ +#! /bin/sh +# Attempt to guess a canonical system name. +# Copyright 1992-2018 Free Software Foundation, Inc. + +timestamp='2018-02-24' + +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . +# +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program that contains a +# configuration script generated by Autoconf, you may include it under +# the same distribution terms that you use for the rest of that +# program. This Exception is an additional permission under section 7 +# of the GNU General Public License, version 3 ("GPLv3"). +# +# Originally written by Per Bothner; maintained since 2000 by Ben Elliston. +# +# You can get the latest version of this script from: +# https://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess +# +# Please send patches to . + + +me=`echo "$0" | sed -e 's,.*/,,'` + +usage="\ +Usage: $0 [OPTION] + +Output the configuration name of the system \`$me' is run on. + +Options: + -h, --help print this help, then exit + -t, --time-stamp print date of last modification, then exit + -v, --version print version number, then exit + +Report bugs and patches to ." + +version="\ +GNU config.guess ($timestamp) + +Originally written by Per Bothner. +Copyright 1992-2018 Free Software Foundation, Inc. + +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." + +help=" +Try \`$me --help' for more information." + +# Parse command line +while test $# -gt 0 ; do + case $1 in + --time-stamp | --time* | -t ) + echo "$timestamp" ; exit ;; + --version | -v ) + echo "$version" ; exit ;; + --help | --h* | -h ) + echo "$usage"; exit ;; + -- ) # Stop option processing + shift; break ;; + - ) # Use stdin as input. + break ;; + -* ) + echo "$me: invalid option $1$help" >&2 + exit 1 ;; + * ) + break ;; + esac +done + +if test $# != 0; then + echo "$me: too many arguments$help" >&2 + exit 1 +fi + +trap 'exit 1' 1 2 15 + +# CC_FOR_BUILD -- compiler used by this script. Note that the use of a +# compiler to aid in system detection is discouraged as it requires +# temporary files to be created and, as you can see below, it is a +# headache to deal with in a portable fashion. + +# Historically, `CC_FOR_BUILD' used to be named `HOST_CC'. We still +# use `HOST_CC' if defined, but it is deprecated. + +# Portable tmp directory creation inspired by the Autoconf team. + +set_cc_for_build=' +trap "exitcode=\$?; (rm -f \$tmpfiles 2>/dev/null; rmdir \$tmp 2>/dev/null) && exit \$exitcode" 0 ; +trap "rm -f \$tmpfiles 2>/dev/null; rmdir \$tmp 2>/dev/null; exit 1" 1 2 13 15 ; +: ${TMPDIR=/tmp} ; + { tmp=`(umask 077 && mktemp -d "$TMPDIR/cgXXXXXX") 2>/dev/null` && test -n "$tmp" && test -d "$tmp" ; } || + { test -n "$RANDOM" && tmp=$TMPDIR/cg$$-$RANDOM && (umask 077 && mkdir $tmp) ; } || + { tmp=$TMPDIR/cg-$$ && (umask 077 && mkdir $tmp) && echo "Warning: creating insecure temp directory" >&2 ; } || + { echo "$me: cannot create a temporary directory in $TMPDIR" >&2 ; exit 1 ; } ; +dummy=$tmp/dummy ; +tmpfiles="$dummy.c $dummy.o $dummy.rel $dummy" ; +case $CC_FOR_BUILD,$HOST_CC,$CC in + ,,) echo "int x;" > "$dummy.c" ; + for c in cc gcc c89 c99 ; do + if ($c -c -o "$dummy.o" "$dummy.c") >/dev/null 2>&1 ; then + CC_FOR_BUILD="$c"; break ; + fi ; + done ; + if test x"$CC_FOR_BUILD" = x ; then + CC_FOR_BUILD=no_compiler_found ; + fi + ;; + ,,*) CC_FOR_BUILD=$CC ;; + ,*,*) CC_FOR_BUILD=$HOST_CC ;; +esac ; set_cc_for_build= ;' + +# This is needed to find uname on a Pyramid OSx when run in the BSD universe. +# (ghazi@noc.rutgers.edu 1994-08-24) +if (test -f /.attbin/uname) >/dev/null 2>&1 ; then + PATH=$PATH:/.attbin ; export PATH +fi + +UNAME_MACHINE=`(uname -m) 2>/dev/null` || UNAME_MACHINE=unknown +UNAME_RELEASE=`(uname -r) 2>/dev/null` || UNAME_RELEASE=unknown +UNAME_SYSTEM=`(uname -s) 2>/dev/null` || UNAME_SYSTEM=unknown +UNAME_VERSION=`(uname -v) 2>/dev/null` || UNAME_VERSION=unknown + +case "$UNAME_SYSTEM" in +Linux|GNU|GNU/*) + # If the system lacks a compiler, then just pick glibc. + # We could probably try harder. + LIBC=gnu + + eval "$set_cc_for_build" + cat <<-EOF > "$dummy.c" + #include + #if defined(__UCLIBC__) + LIBC=uclibc + #elif defined(__dietlibc__) + LIBC=dietlibc + #else + LIBC=gnu + #endif + EOF + eval "`$CC_FOR_BUILD -E "$dummy.c" 2>/dev/null | grep '^LIBC' | sed 's, ,,g'`" + + # If ldd exists, use it to detect musl libc. + if command -v ldd >/dev/null && \ + ldd --version 2>&1 | grep -q ^musl + then + LIBC=musl + fi + ;; +esac + +# Note: order is significant - the case branches are not exclusive. + +case "$UNAME_MACHINE:$UNAME_SYSTEM:$UNAME_RELEASE:$UNAME_VERSION" in + *:NetBSD:*:*) + # NetBSD (nbsd) targets should (where applicable) match one or + # more of the tuples: *-*-netbsdelf*, *-*-netbsdaout*, + # *-*-netbsdecoff* and *-*-netbsd*. For targets that recently + # switched to ELF, *-*-netbsd* would select the old + # object file format. This provides both forward + # compatibility and a consistent mechanism for selecting the + # object file format. + # + # Note: NetBSD doesn't particularly care about the vendor + # portion of the name. We always set it to "unknown". + sysctl="sysctl -n hw.machine_arch" + UNAME_MACHINE_ARCH=`(uname -p 2>/dev/null || \ + "/sbin/$sysctl" 2>/dev/null || \ + "/usr/sbin/$sysctl" 2>/dev/null || \ + echo unknown)` + case "$UNAME_MACHINE_ARCH" in + armeb) machine=armeb-unknown ;; + arm*) machine=arm-unknown ;; + sh3el) machine=shl-unknown ;; + sh3eb) machine=sh-unknown ;; + sh5el) machine=sh5le-unknown ;; + earmv*) + arch=`echo "$UNAME_MACHINE_ARCH" | sed -e 's,^e\(armv[0-9]\).*$,\1,'` + endian=`echo "$UNAME_MACHINE_ARCH" | sed -ne 's,^.*\(eb\)$,\1,p'` + machine="${arch}${endian}"-unknown + ;; + *) machine="$UNAME_MACHINE_ARCH"-unknown ;; + esac + # The Operating System including object format, if it has switched + # to ELF recently (or will in the future) and ABI. + case "$UNAME_MACHINE_ARCH" in + earm*) + os=netbsdelf + ;; + arm*|i386|m68k|ns32k|sh3*|sparc|vax) + eval "$set_cc_for_build" + if echo __ELF__ | $CC_FOR_BUILD -E - 2>/dev/null \ + | grep -q __ELF__ + then + # Once all utilities can be ECOFF (netbsdecoff) or a.out (netbsdaout). + # Return netbsd for either. FIX? + os=netbsd + else + os=netbsdelf + fi + ;; + *) + os=netbsd + ;; + esac + # Determine ABI tags. + case "$UNAME_MACHINE_ARCH" in + earm*) + expr='s/^earmv[0-9]/-eabi/;s/eb$//' + abi=`echo "$UNAME_MACHINE_ARCH" | sed -e "$expr"` + ;; + esac + # The OS release + # Debian GNU/NetBSD machines have a different userland, and + # thus, need a distinct triplet. However, they do not need + # kernel version information, so it can be replaced with a + # suitable tag, in the style of linux-gnu. + case "$UNAME_VERSION" in + Debian*) + release='-gnu' + ;; + *) + release=`echo "$UNAME_RELEASE" | sed -e 's/[-_].*//' | cut -d. -f1,2` + ;; + esac + # Since CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM: + # contains redundant information, the shorter form: + # CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM is used. + echo "$machine-${os}${release}${abi}" + exit ;; + *:Bitrig:*:*) + UNAME_MACHINE_ARCH=`arch | sed 's/Bitrig.//'` + echo "$UNAME_MACHINE_ARCH"-unknown-bitrig"$UNAME_RELEASE" + exit ;; + *:OpenBSD:*:*) + UNAME_MACHINE_ARCH=`arch | sed 's/OpenBSD.//'` + echo "$UNAME_MACHINE_ARCH"-unknown-openbsd"$UNAME_RELEASE" + exit ;; + *:LibertyBSD:*:*) + UNAME_MACHINE_ARCH=`arch | sed 's/^.*BSD\.//'` + echo "$UNAME_MACHINE_ARCH"-unknown-libertybsd"$UNAME_RELEASE" + exit ;; + *:MidnightBSD:*:*) + echo "$UNAME_MACHINE"-unknown-midnightbsd"$UNAME_RELEASE" + exit ;; + *:ekkoBSD:*:*) + echo "$UNAME_MACHINE"-unknown-ekkobsd"$UNAME_RELEASE" + exit ;; + *:SolidBSD:*:*) + echo "$UNAME_MACHINE"-unknown-solidbsd"$UNAME_RELEASE" + exit ;; + macppc:MirBSD:*:*) + echo powerpc-unknown-mirbsd"$UNAME_RELEASE" + exit ;; + *:MirBSD:*:*) + echo "$UNAME_MACHINE"-unknown-mirbsd"$UNAME_RELEASE" + exit ;; + *:Sortix:*:*) + echo "$UNAME_MACHINE"-unknown-sortix + exit ;; + *:Redox:*:*) + echo "$UNAME_MACHINE"-unknown-redox + exit ;; + mips:OSF1:*.*) + echo mips-dec-osf1 + exit ;; + alpha:OSF1:*:*) + case $UNAME_RELEASE in + *4.0) + UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $3}'` + ;; + *5.*) + UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $4}'` + ;; + esac + # According to Compaq, /usr/sbin/psrinfo has been available on + # OSF/1 and Tru64 systems produced since 1995. I hope that + # covers most systems running today. This code pipes the CPU + # types through head -n 1, so we only detect the type of CPU 0. + ALPHA_CPU_TYPE=`/usr/sbin/psrinfo -v | sed -n -e 's/^ The alpha \(.*\) processor.*$/\1/p' | head -n 1` + case "$ALPHA_CPU_TYPE" in + "EV4 (21064)") + UNAME_MACHINE=alpha ;; + "EV4.5 (21064)") + UNAME_MACHINE=alpha ;; + "LCA4 (21066/21068)") + UNAME_MACHINE=alpha ;; + "EV5 (21164)") + UNAME_MACHINE=alphaev5 ;; + "EV5.6 (21164A)") + UNAME_MACHINE=alphaev56 ;; + "EV5.6 (21164PC)") + UNAME_MACHINE=alphapca56 ;; + "EV5.7 (21164PC)") + UNAME_MACHINE=alphapca57 ;; + "EV6 (21264)") + UNAME_MACHINE=alphaev6 ;; + "EV6.7 (21264A)") + UNAME_MACHINE=alphaev67 ;; + "EV6.8CB (21264C)") + UNAME_MACHINE=alphaev68 ;; + "EV6.8AL (21264B)") + UNAME_MACHINE=alphaev68 ;; + "EV6.8CX (21264D)") + UNAME_MACHINE=alphaev68 ;; + "EV6.9A (21264/EV69A)") + UNAME_MACHINE=alphaev69 ;; + "EV7 (21364)") + UNAME_MACHINE=alphaev7 ;; + "EV7.9 (21364A)") + UNAME_MACHINE=alphaev79 ;; + esac + # A Pn.n version is a patched version. + # A Vn.n version is a released version. + # A Tn.n version is a released field test version. + # A Xn.n version is an unreleased experimental baselevel. + # 1.2 uses "1.2" for uname -r. + echo "$UNAME_MACHINE"-dec-osf"`echo "$UNAME_RELEASE" | sed -e 's/^[PVTX]//' | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz`" + # Reset EXIT trap before exiting to avoid spurious non-zero exit code. + exitcode=$? + trap '' 0 + exit $exitcode ;; + Amiga*:UNIX_System_V:4.0:*) + echo m68k-unknown-sysv4 + exit ;; + *:[Aa]miga[Oo][Ss]:*:*) + echo "$UNAME_MACHINE"-unknown-amigaos + exit ;; + *:[Mm]orph[Oo][Ss]:*:*) + echo "$UNAME_MACHINE"-unknown-morphos + exit ;; + *:OS/390:*:*) + echo i370-ibm-openedition + exit ;; + *:z/VM:*:*) + echo s390-ibm-zvmoe + exit ;; + *:OS400:*:*) + echo powerpc-ibm-os400 + exit ;; + arm:RISC*:1.[012]*:*|arm:riscix:1.[012]*:*) + echo arm-acorn-riscix"$UNAME_RELEASE" + exit ;; + arm*:riscos:*:*|arm*:RISCOS:*:*) + echo arm-unknown-riscos + exit ;; + SR2?01:HI-UX/MPP:*:* | SR8000:HI-UX/MPP:*:*) + echo hppa1.1-hitachi-hiuxmpp + exit ;; + Pyramid*:OSx*:*:* | MIS*:OSx*:*:* | MIS*:SMP_DC-OSx*:*:*) + # akee@wpdis03.wpafb.af.mil (Earle F. Ake) contributed MIS and NILE. + if test "`(/bin/universe) 2>/dev/null`" = att ; then + echo pyramid-pyramid-sysv3 + else + echo pyramid-pyramid-bsd + fi + exit ;; + NILE*:*:*:dcosx) + echo pyramid-pyramid-svr4 + exit ;; + DRS?6000:unix:4.0:6*) + echo sparc-icl-nx6 + exit ;; + DRS?6000:UNIX_SV:4.2*:7* | DRS?6000:isis:4.2*:7*) + case `/usr/bin/uname -p` in + sparc) echo sparc-icl-nx7; exit ;; + esac ;; + s390x:SunOS:*:*) + echo "$UNAME_MACHINE"-ibm-solaris2"`echo "$UNAME_RELEASE" | sed -e 's/[^.]*//'`" + exit ;; + sun4H:SunOS:5.*:*) + echo sparc-hal-solaris2"`echo "$UNAME_RELEASE"|sed -e 's/[^.]*//'`" + exit ;; + sun4*:SunOS:5.*:* | tadpole*:SunOS:5.*:*) + echo sparc-sun-solaris2"`echo "$UNAME_RELEASE" | sed -e 's/[^.]*//'`" + exit ;; + i86pc:AuroraUX:5.*:* | i86xen:AuroraUX:5.*:*) + echo i386-pc-auroraux"$UNAME_RELEASE" + exit ;; + i86pc:SunOS:5.*:* | i86xen:SunOS:5.*:*) + eval "$set_cc_for_build" + SUN_ARCH=i386 + # If there is a compiler, see if it is configured for 64-bit objects. + # Note that the Sun cc does not turn __LP64__ into 1 like gcc does. + # This test works for both compilers. + if [ "$CC_FOR_BUILD" != no_compiler_found ]; then + if (echo '#ifdef __amd64'; echo IS_64BIT_ARCH; echo '#endif') | \ + (CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) | \ + grep IS_64BIT_ARCH >/dev/null + then + SUN_ARCH=x86_64 + fi + fi + echo "$SUN_ARCH"-pc-solaris2"`echo "$UNAME_RELEASE"|sed -e 's/[^.]*//'`" + exit ;; + sun4*:SunOS:6*:*) + # According to config.sub, this is the proper way to canonicalize + # SunOS6. Hard to guess exactly what SunOS6 will be like, but + # it's likely to be more like Solaris than SunOS4. + echo sparc-sun-solaris3"`echo "$UNAME_RELEASE"|sed -e 's/[^.]*//'`" + exit ;; + sun4*:SunOS:*:*) + case "`/usr/bin/arch -k`" in + Series*|S4*) + UNAME_RELEASE=`uname -v` + ;; + esac + # Japanese Language versions have a version number like `4.1.3-JL'. + echo sparc-sun-sunos"`echo "$UNAME_RELEASE"|sed -e 's/-/_/'`" + exit ;; + sun3*:SunOS:*:*) + echo m68k-sun-sunos"$UNAME_RELEASE" + exit ;; + sun*:*:4.2BSD:*) + UNAME_RELEASE=`(sed 1q /etc/motd | awk '{print substr($5,1,3)}') 2>/dev/null` + test "x$UNAME_RELEASE" = x && UNAME_RELEASE=3 + case "`/bin/arch`" in + sun3) + echo m68k-sun-sunos"$UNAME_RELEASE" + ;; + sun4) + echo sparc-sun-sunos"$UNAME_RELEASE" + ;; + esac + exit ;; + aushp:SunOS:*:*) + echo sparc-auspex-sunos"$UNAME_RELEASE" + exit ;; + # The situation for MiNT is a little confusing. The machine name + # can be virtually everything (everything which is not + # "atarist" or "atariste" at least should have a processor + # > m68000). The system name ranges from "MiNT" over "FreeMiNT" + # to the lowercase version "mint" (or "freemint"). Finally + # the system name "TOS" denotes a system which is actually not + # MiNT. But MiNT is downward compatible to TOS, so this should + # be no problem. + atarist[e]:*MiNT:*:* | atarist[e]:*mint:*:* | atarist[e]:*TOS:*:*) + echo m68k-atari-mint"$UNAME_RELEASE" + exit ;; + atari*:*MiNT:*:* | atari*:*mint:*:* | atarist[e]:*TOS:*:*) + echo m68k-atari-mint"$UNAME_RELEASE" + exit ;; + *falcon*:*MiNT:*:* | *falcon*:*mint:*:* | *falcon*:*TOS:*:*) + echo m68k-atari-mint"$UNAME_RELEASE" + exit ;; + milan*:*MiNT:*:* | milan*:*mint:*:* | *milan*:*TOS:*:*) + echo m68k-milan-mint"$UNAME_RELEASE" + exit ;; + hades*:*MiNT:*:* | hades*:*mint:*:* | *hades*:*TOS:*:*) + echo m68k-hades-mint"$UNAME_RELEASE" + exit ;; + *:*MiNT:*:* | *:*mint:*:* | *:*TOS:*:*) + echo m68k-unknown-mint"$UNAME_RELEASE" + exit ;; + m68k:machten:*:*) + echo m68k-apple-machten"$UNAME_RELEASE" + exit ;; + powerpc:machten:*:*) + echo powerpc-apple-machten"$UNAME_RELEASE" + exit ;; + RISC*:Mach:*:*) + echo mips-dec-mach_bsd4.3 + exit ;; + RISC*:ULTRIX:*:*) + echo mips-dec-ultrix"$UNAME_RELEASE" + exit ;; + VAX*:ULTRIX*:*:*) + echo vax-dec-ultrix"$UNAME_RELEASE" + exit ;; + 2020:CLIX:*:* | 2430:CLIX:*:*) + echo clipper-intergraph-clix"$UNAME_RELEASE" + exit ;; + mips:*:*:UMIPS | mips:*:*:RISCos) + eval "$set_cc_for_build" + sed 's/^ //' << EOF > "$dummy.c" +#ifdef __cplusplus +#include /* for printf() prototype */ + int main (int argc, char *argv[]) { +#else + int main (argc, argv) int argc; char *argv[]; { +#endif + #if defined (host_mips) && defined (MIPSEB) + #if defined (SYSTYPE_SYSV) + printf ("mips-mips-riscos%ssysv\\n", argv[1]); exit (0); + #endif + #if defined (SYSTYPE_SVR4) + printf ("mips-mips-riscos%ssvr4\\n", argv[1]); exit (0); + #endif + #if defined (SYSTYPE_BSD43) || defined(SYSTYPE_BSD) + printf ("mips-mips-riscos%sbsd\\n", argv[1]); exit (0); + #endif + #endif + exit (-1); + } +EOF + $CC_FOR_BUILD -o "$dummy" "$dummy.c" && + dummyarg=`echo "$UNAME_RELEASE" | sed -n 's/\([0-9]*\).*/\1/p'` && + SYSTEM_NAME=`"$dummy" "$dummyarg"` && + { echo "$SYSTEM_NAME"; exit; } + echo mips-mips-riscos"$UNAME_RELEASE" + exit ;; + Motorola:PowerMAX_OS:*:*) + echo powerpc-motorola-powermax + exit ;; + Motorola:*:4.3:PL8-*) + echo powerpc-harris-powermax + exit ;; + Night_Hawk:*:*:PowerMAX_OS | Synergy:PowerMAX_OS:*:*) + echo powerpc-harris-powermax + exit ;; + Night_Hawk:Power_UNIX:*:*) + echo powerpc-harris-powerunix + exit ;; + m88k:CX/UX:7*:*) + echo m88k-harris-cxux7 + exit ;; + m88k:*:4*:R4*) + echo m88k-motorola-sysv4 + exit ;; + m88k:*:3*:R3*) + echo m88k-motorola-sysv3 + exit ;; + AViiON:dgux:*:*) + # DG/UX returns AViiON for all architectures + UNAME_PROCESSOR=`/usr/bin/uname -p` + if [ "$UNAME_PROCESSOR" = mc88100 ] || [ "$UNAME_PROCESSOR" = mc88110 ] + then + if [ "$TARGET_BINARY_INTERFACE"x = m88kdguxelfx ] || \ + [ "$TARGET_BINARY_INTERFACE"x = x ] + then + echo m88k-dg-dgux"$UNAME_RELEASE" + else + echo m88k-dg-dguxbcs"$UNAME_RELEASE" + fi + else + echo i586-dg-dgux"$UNAME_RELEASE" + fi + exit ;; + M88*:DolphinOS:*:*) # DolphinOS (SVR3) + echo m88k-dolphin-sysv3 + exit ;; + M88*:*:R3*:*) + # Delta 88k system running SVR3 + echo m88k-motorola-sysv3 + exit ;; + XD88*:*:*:*) # Tektronix XD88 system running UTekV (SVR3) + echo m88k-tektronix-sysv3 + exit ;; + Tek43[0-9][0-9]:UTek:*:*) # Tektronix 4300 system running UTek (BSD) + echo m68k-tektronix-bsd + exit ;; + *:IRIX*:*:*) + echo mips-sgi-irix"`echo "$UNAME_RELEASE"|sed -e 's/-/_/g'`" + exit ;; + ????????:AIX?:[12].1:2) # AIX 2.2.1 or AIX 2.1.1 is RT/PC AIX. + echo romp-ibm-aix # uname -m gives an 8 hex-code CPU id + exit ;; # Note that: echo "'`uname -s`'" gives 'AIX ' + i*86:AIX:*:*) + echo i386-ibm-aix + exit ;; + ia64:AIX:*:*) + if [ -x /usr/bin/oslevel ] ; then + IBM_REV=`/usr/bin/oslevel` + else + IBM_REV="$UNAME_VERSION.$UNAME_RELEASE" + fi + echo "$UNAME_MACHINE"-ibm-aix"$IBM_REV" + exit ;; + *:AIX:2:3) + if grep bos325 /usr/include/stdio.h >/dev/null 2>&1; then + eval "$set_cc_for_build" + sed 's/^ //' << EOF > "$dummy.c" + #include + + main() + { + if (!__power_pc()) + exit(1); + puts("powerpc-ibm-aix3.2.5"); + exit(0); + } +EOF + if $CC_FOR_BUILD -o "$dummy" "$dummy.c" && SYSTEM_NAME=`"$dummy"` + then + echo "$SYSTEM_NAME" + else + echo rs6000-ibm-aix3.2.5 + fi + elif grep bos324 /usr/include/stdio.h >/dev/null 2>&1; then + echo rs6000-ibm-aix3.2.4 + else + echo rs6000-ibm-aix3.2 + fi + exit ;; + *:AIX:*:[4567]) + IBM_CPU_ID=`/usr/sbin/lsdev -C -c processor -S available | sed 1q | awk '{ print $1 }'` + if /usr/sbin/lsattr -El "$IBM_CPU_ID" | grep ' POWER' >/dev/null 2>&1; then + IBM_ARCH=rs6000 + else + IBM_ARCH=powerpc + fi + if [ -x /usr/bin/lslpp ] ; then + IBM_REV=`/usr/bin/lslpp -Lqc bos.rte.libc | + awk -F: '{ print $3 }' | sed s/[0-9]*$/0/` + else + IBM_REV="$UNAME_VERSION.$UNAME_RELEASE" + fi + echo "$IBM_ARCH"-ibm-aix"$IBM_REV" + exit ;; + *:AIX:*:*) + echo rs6000-ibm-aix + exit ;; + ibmrt:4.4BSD:*|romp-ibm:4.4BSD:*) + echo romp-ibm-bsd4.4 + exit ;; + ibmrt:*BSD:*|romp-ibm:BSD:*) # covers RT/PC BSD and + echo romp-ibm-bsd"$UNAME_RELEASE" # 4.3 with uname added to + exit ;; # report: romp-ibm BSD 4.3 + *:BOSX:*:*) + echo rs6000-bull-bosx + exit ;; + DPX/2?00:B.O.S.:*:*) + echo m68k-bull-sysv3 + exit ;; + 9000/[34]??:4.3bsd:1.*:*) + echo m68k-hp-bsd + exit ;; + hp300:4.4BSD:*:* | 9000/[34]??:4.3bsd:2.*:*) + echo m68k-hp-bsd4.4 + exit ;; + 9000/[34678]??:HP-UX:*:*) + HPUX_REV=`echo "$UNAME_RELEASE"|sed -e 's/[^.]*.[0B]*//'` + case "$UNAME_MACHINE" in + 9000/31?) HP_ARCH=m68000 ;; + 9000/[34]??) HP_ARCH=m68k ;; + 9000/[678][0-9][0-9]) + if [ -x /usr/bin/getconf ]; then + sc_cpu_version=`/usr/bin/getconf SC_CPU_VERSION 2>/dev/null` + sc_kernel_bits=`/usr/bin/getconf SC_KERNEL_BITS 2>/dev/null` + case "$sc_cpu_version" in + 523) HP_ARCH=hppa1.0 ;; # CPU_PA_RISC1_0 + 528) HP_ARCH=hppa1.1 ;; # CPU_PA_RISC1_1 + 532) # CPU_PA_RISC2_0 + case "$sc_kernel_bits" in + 32) HP_ARCH=hppa2.0n ;; + 64) HP_ARCH=hppa2.0w ;; + '') HP_ARCH=hppa2.0 ;; # HP-UX 10.20 + esac ;; + esac + fi + if [ "$HP_ARCH" = "" ]; then + eval "$set_cc_for_build" + sed 's/^ //' << EOF > "$dummy.c" + + #define _HPUX_SOURCE + #include + #include + + int main () + { + #if defined(_SC_KERNEL_BITS) + long bits = sysconf(_SC_KERNEL_BITS); + #endif + long cpu = sysconf (_SC_CPU_VERSION); + + switch (cpu) + { + case CPU_PA_RISC1_0: puts ("hppa1.0"); break; + case CPU_PA_RISC1_1: puts ("hppa1.1"); break; + case CPU_PA_RISC2_0: + #if defined(_SC_KERNEL_BITS) + switch (bits) + { + case 64: puts ("hppa2.0w"); break; + case 32: puts ("hppa2.0n"); break; + default: puts ("hppa2.0"); break; + } break; + #else /* !defined(_SC_KERNEL_BITS) */ + puts ("hppa2.0"); break; + #endif + default: puts ("hppa1.0"); break; + } + exit (0); + } +EOF + (CCOPTS="" $CC_FOR_BUILD -o "$dummy" "$dummy.c" 2>/dev/null) && HP_ARCH=`"$dummy"` + test -z "$HP_ARCH" && HP_ARCH=hppa + fi ;; + esac + if [ "$HP_ARCH" = hppa2.0w ] + then + eval "$set_cc_for_build" + + # hppa2.0w-hp-hpux* has a 64-bit kernel and a compiler generating + # 32-bit code. hppa64-hp-hpux* has the same kernel and a compiler + # generating 64-bit code. GNU and HP use different nomenclature: + # + # $ CC_FOR_BUILD=cc ./config.guess + # => hppa2.0w-hp-hpux11.23 + # $ CC_FOR_BUILD="cc +DA2.0w" ./config.guess + # => hppa64-hp-hpux11.23 + + if echo __LP64__ | (CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) | + grep -q __LP64__ + then + HP_ARCH=hppa2.0w + else + HP_ARCH=hppa64 + fi + fi + echo "$HP_ARCH"-hp-hpux"$HPUX_REV" + exit ;; + ia64:HP-UX:*:*) + HPUX_REV=`echo "$UNAME_RELEASE"|sed -e 's/[^.]*.[0B]*//'` + echo ia64-hp-hpux"$HPUX_REV" + exit ;; + 3050*:HI-UX:*:*) + eval "$set_cc_for_build" + sed 's/^ //' << EOF > "$dummy.c" + #include + int + main () + { + long cpu = sysconf (_SC_CPU_VERSION); + /* The order matters, because CPU_IS_HP_MC68K erroneously returns + true for CPU_PA_RISC1_0. CPU_IS_PA_RISC returns correct + results, however. */ + if (CPU_IS_PA_RISC (cpu)) + { + switch (cpu) + { + case CPU_PA_RISC1_0: puts ("hppa1.0-hitachi-hiuxwe2"); break; + case CPU_PA_RISC1_1: puts ("hppa1.1-hitachi-hiuxwe2"); break; + case CPU_PA_RISC2_0: puts ("hppa2.0-hitachi-hiuxwe2"); break; + default: puts ("hppa-hitachi-hiuxwe2"); break; + } + } + else if (CPU_IS_HP_MC68K (cpu)) + puts ("m68k-hitachi-hiuxwe2"); + else puts ("unknown-hitachi-hiuxwe2"); + exit (0); + } +EOF + $CC_FOR_BUILD -o "$dummy" "$dummy.c" && SYSTEM_NAME=`"$dummy"` && + { echo "$SYSTEM_NAME"; exit; } + echo unknown-hitachi-hiuxwe2 + exit ;; + 9000/7??:4.3bsd:*:* | 9000/8?[79]:4.3bsd:*:*) + echo hppa1.1-hp-bsd + exit ;; + 9000/8??:4.3bsd:*:*) + echo hppa1.0-hp-bsd + exit ;; + *9??*:MPE/iX:*:* | *3000*:MPE/iX:*:*) + echo hppa1.0-hp-mpeix + exit ;; + hp7??:OSF1:*:* | hp8?[79]:OSF1:*:*) + echo hppa1.1-hp-osf + exit ;; + hp8??:OSF1:*:*) + echo hppa1.0-hp-osf + exit ;; + i*86:OSF1:*:*) + if [ -x /usr/sbin/sysversion ] ; then + echo "$UNAME_MACHINE"-unknown-osf1mk + else + echo "$UNAME_MACHINE"-unknown-osf1 + fi + exit ;; + parisc*:Lites*:*:*) + echo hppa1.1-hp-lites + exit ;; + C1*:ConvexOS:*:* | convex:ConvexOS:C1*:*) + echo c1-convex-bsd + exit ;; + C2*:ConvexOS:*:* | convex:ConvexOS:C2*:*) + if getsysinfo -f scalar_acc + then echo c32-convex-bsd + else echo c2-convex-bsd + fi + exit ;; + C34*:ConvexOS:*:* | convex:ConvexOS:C34*:*) + echo c34-convex-bsd + exit ;; + C38*:ConvexOS:*:* | convex:ConvexOS:C38*:*) + echo c38-convex-bsd + exit ;; + C4*:ConvexOS:*:* | convex:ConvexOS:C4*:*) + echo c4-convex-bsd + exit ;; + CRAY*Y-MP:*:*:*) + echo ymp-cray-unicos"$UNAME_RELEASE" | sed -e 's/\.[^.]*$/.X/' + exit ;; + CRAY*[A-Z]90:*:*:*) + echo "$UNAME_MACHINE"-cray-unicos"$UNAME_RELEASE" \ + | sed -e 's/CRAY.*\([A-Z]90\)/\1/' \ + -e y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/ \ + -e 's/\.[^.]*$/.X/' + exit ;; + CRAY*TS:*:*:*) + echo t90-cray-unicos"$UNAME_RELEASE" | sed -e 's/\.[^.]*$/.X/' + exit ;; + CRAY*T3E:*:*:*) + echo alphaev5-cray-unicosmk"$UNAME_RELEASE" | sed -e 's/\.[^.]*$/.X/' + exit ;; + CRAY*SV1:*:*:*) + echo sv1-cray-unicos"$UNAME_RELEASE" | sed -e 's/\.[^.]*$/.X/' + exit ;; + *:UNICOS/mp:*:*) + echo craynv-cray-unicosmp"$UNAME_RELEASE" | sed -e 's/\.[^.]*$/.X/' + exit ;; + F30[01]:UNIX_System_V:*:* | F700:UNIX_System_V:*:*) + FUJITSU_PROC=`uname -m | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz` + FUJITSU_SYS=`uname -p | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz | sed -e 's/\///'` + FUJITSU_REL=`echo "$UNAME_RELEASE" | sed -e 's/ /_/'` + echo "${FUJITSU_PROC}-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}" + exit ;; + 5000:UNIX_System_V:4.*:*) + FUJITSU_SYS=`uname -p | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz | sed -e 's/\///'` + FUJITSU_REL=`echo "$UNAME_RELEASE" | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz | sed -e 's/ /_/'` + echo "sparc-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}" + exit ;; + i*86:BSD/386:*:* | i*86:BSD/OS:*:* | *:Ascend\ Embedded/OS:*:*) + echo "$UNAME_MACHINE"-pc-bsdi"$UNAME_RELEASE" + exit ;; + sparc*:BSD/OS:*:*) + echo sparc-unknown-bsdi"$UNAME_RELEASE" + exit ;; + *:BSD/OS:*:*) + echo "$UNAME_MACHINE"-unknown-bsdi"$UNAME_RELEASE" + exit ;; + *:FreeBSD:*:*) + UNAME_PROCESSOR=`/usr/bin/uname -p` + case "$UNAME_PROCESSOR" in + amd64) + UNAME_PROCESSOR=x86_64 ;; + i386) + UNAME_PROCESSOR=i586 ;; + esac + echo "$UNAME_PROCESSOR"-unknown-freebsd"`echo "$UNAME_RELEASE"|sed -e 's/[-(].*//'`" + exit ;; + i*:CYGWIN*:*) + echo "$UNAME_MACHINE"-pc-cygwin + exit ;; + *:MINGW64*:*) + echo "$UNAME_MACHINE"-pc-mingw64 + exit ;; + *:MINGW*:*) + echo "$UNAME_MACHINE"-pc-mingw32 + exit ;; + *:MSYS*:*) + echo "$UNAME_MACHINE"-pc-msys + exit ;; + i*:PW*:*) + echo "$UNAME_MACHINE"-pc-pw32 + exit ;; + *:Interix*:*) + case "$UNAME_MACHINE" in + x86) + echo i586-pc-interix"$UNAME_RELEASE" + exit ;; + authenticamd | genuineintel | EM64T) + echo x86_64-unknown-interix"$UNAME_RELEASE" + exit ;; + IA64) + echo ia64-unknown-interix"$UNAME_RELEASE" + exit ;; + esac ;; + i*:UWIN*:*) + echo "$UNAME_MACHINE"-pc-uwin + exit ;; + amd64:CYGWIN*:*:* | x86_64:CYGWIN*:*:*) + echo x86_64-unknown-cygwin + exit ;; + prep*:SunOS:5.*:*) + echo powerpcle-unknown-solaris2"`echo "$UNAME_RELEASE"|sed -e 's/[^.]*//'`" + exit ;; + *:GNU:*:*) + # the GNU system + echo "`echo "$UNAME_MACHINE"|sed -e 's,[-/].*$,,'`-unknown-$LIBC`echo "$UNAME_RELEASE"|sed -e 's,/.*$,,'`" + exit ;; + *:GNU/*:*:*) + # other systems with GNU libc and userland + echo "$UNAME_MACHINE-unknown-`echo "$UNAME_SYSTEM" | sed 's,^[^/]*/,,' | tr "[:upper:]" "[:lower:]"``echo "$UNAME_RELEASE"|sed -e 's/[-(].*//'`-$LIBC" + exit ;; + i*86:Minix:*:*) + echo "$UNAME_MACHINE"-pc-minix + exit ;; + aarch64:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + aarch64_be:Linux:*:*) + UNAME_MACHINE=aarch64_be + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + alpha:Linux:*:*) + case `sed -n '/^cpu model/s/^.*: \(.*\)/\1/p' < /proc/cpuinfo` in + EV5) UNAME_MACHINE=alphaev5 ;; + EV56) UNAME_MACHINE=alphaev56 ;; + PCA56) UNAME_MACHINE=alphapca56 ;; + PCA57) UNAME_MACHINE=alphapca56 ;; + EV6) UNAME_MACHINE=alphaev6 ;; + EV67) UNAME_MACHINE=alphaev67 ;; + EV68*) UNAME_MACHINE=alphaev68 ;; + esac + objdump --private-headers /bin/sh | grep -q ld.so.1 + if test "$?" = 0 ; then LIBC=gnulibc1 ; fi + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + arc:Linux:*:* | arceb:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + arm*:Linux:*:*) + eval "$set_cc_for_build" + if echo __ARM_EABI__ | $CC_FOR_BUILD -E - 2>/dev/null \ + | grep -q __ARM_EABI__ + then + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + else + if echo __ARM_PCS_VFP | $CC_FOR_BUILD -E - 2>/dev/null \ + | grep -q __ARM_PCS_VFP + then + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC"eabi + else + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC"eabihf + fi + fi + exit ;; + avr32*:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + cris:Linux:*:*) + echo "$UNAME_MACHINE"-axis-linux-"$LIBC" + exit ;; + crisv32:Linux:*:*) + echo "$UNAME_MACHINE"-axis-linux-"$LIBC" + exit ;; + e2k:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + frv:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + hexagon:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + i*86:Linux:*:*) + echo "$UNAME_MACHINE"-pc-linux-"$LIBC" + exit ;; + ia64:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + k1om:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + m32r*:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + m68*:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + mips:Linux:*:* | mips64:Linux:*:*) + eval "$set_cc_for_build" + sed 's/^ //' << EOF > "$dummy.c" + #undef CPU + #undef ${UNAME_MACHINE} + #undef ${UNAME_MACHINE}el + #if defined(__MIPSEL__) || defined(__MIPSEL) || defined(_MIPSEL) || defined(MIPSEL) + CPU=${UNAME_MACHINE}el + #else + #if defined(__MIPSEB__) || defined(__MIPSEB) || defined(_MIPSEB) || defined(MIPSEB) + CPU=${UNAME_MACHINE} + #else + CPU= + #endif + #endif +EOF + eval "`$CC_FOR_BUILD -E "$dummy.c" 2>/dev/null | grep '^CPU'`" + test "x$CPU" != x && { echo "$CPU-unknown-linux-$LIBC"; exit; } + ;; + mips64el:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + openrisc*:Linux:*:*) + echo or1k-unknown-linux-"$LIBC" + exit ;; + or32:Linux:*:* | or1k*:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + padre:Linux:*:*) + echo sparc-unknown-linux-"$LIBC" + exit ;; + parisc64:Linux:*:* | hppa64:Linux:*:*) + echo hppa64-unknown-linux-"$LIBC" + exit ;; + parisc:Linux:*:* | hppa:Linux:*:*) + # Look for CPU level + case `grep '^cpu[^a-z]*:' /proc/cpuinfo 2>/dev/null | cut -d' ' -f2` in + PA7*) echo hppa1.1-unknown-linux-"$LIBC" ;; + PA8*) echo hppa2.0-unknown-linux-"$LIBC" ;; + *) echo hppa-unknown-linux-"$LIBC" ;; + esac + exit ;; + ppc64:Linux:*:*) + echo powerpc64-unknown-linux-"$LIBC" + exit ;; + ppc:Linux:*:*) + echo powerpc-unknown-linux-"$LIBC" + exit ;; + ppc64le:Linux:*:*) + echo powerpc64le-unknown-linux-"$LIBC" + exit ;; + ppcle:Linux:*:*) + echo powerpcle-unknown-linux-"$LIBC" + exit ;; + riscv32:Linux:*:* | riscv64:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + s390:Linux:*:* | s390x:Linux:*:*) + echo "$UNAME_MACHINE"-ibm-linux-"$LIBC" + exit ;; + sh64*:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + sh*:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + sparc:Linux:*:* | sparc64:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + tile*:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + vax:Linux:*:*) + echo "$UNAME_MACHINE"-dec-linux-"$LIBC" + exit ;; + x86_64:Linux:*:*) + if objdump -f /bin/sh | grep -q elf32-x86-64; then + echo "$UNAME_MACHINE"-pc-linux-"$LIBC"x32 + else + echo "$UNAME_MACHINE"-pc-linux-"$LIBC" + fi + exit ;; + xtensa*:Linux:*:*) + echo "$UNAME_MACHINE"-unknown-linux-"$LIBC" + exit ;; + i*86:DYNIX/ptx:4*:*) + # ptx 4.0 does uname -s correctly, with DYNIX/ptx in there. + # earlier versions are messed up and put the nodename in both + # sysname and nodename. + echo i386-sequent-sysv4 + exit ;; + i*86:UNIX_SV:4.2MP:2.*) + # Unixware is an offshoot of SVR4, but it has its own version + # number series starting with 2... + # I am not positive that other SVR4 systems won't match this, + # I just have to hope. -- rms. + # Use sysv4.2uw... so that sysv4* matches it. + echo "$UNAME_MACHINE"-pc-sysv4.2uw"$UNAME_VERSION" + exit ;; + i*86:OS/2:*:*) + # If we were able to find `uname', then EMX Unix compatibility + # is probably installed. + echo "$UNAME_MACHINE"-pc-os2-emx + exit ;; + i*86:XTS-300:*:STOP) + echo "$UNAME_MACHINE"-unknown-stop + exit ;; + i*86:atheos:*:*) + echo "$UNAME_MACHINE"-unknown-atheos + exit ;; + i*86:syllable:*:*) + echo "$UNAME_MACHINE"-pc-syllable + exit ;; + i*86:LynxOS:2.*:* | i*86:LynxOS:3.[01]*:* | i*86:LynxOS:4.[02]*:*) + echo i386-unknown-lynxos"$UNAME_RELEASE" + exit ;; + i*86:*DOS:*:*) + echo "$UNAME_MACHINE"-pc-msdosdjgpp + exit ;; + i*86:*:4.*:*) + UNAME_REL=`echo "$UNAME_RELEASE" | sed 's/\/MP$//'` + if grep Novell /usr/include/link.h >/dev/null 2>/dev/null; then + echo "$UNAME_MACHINE"-univel-sysv"$UNAME_REL" + else + echo "$UNAME_MACHINE"-pc-sysv"$UNAME_REL" + fi + exit ;; + i*86:*:5:[678]*) + # UnixWare 7.x, OpenUNIX and OpenServer 6. + case `/bin/uname -X | grep "^Machine"` in + *486*) UNAME_MACHINE=i486 ;; + *Pentium) UNAME_MACHINE=i586 ;; + *Pent*|*Celeron) UNAME_MACHINE=i686 ;; + esac + echo "$UNAME_MACHINE-unknown-sysv${UNAME_RELEASE}${UNAME_SYSTEM}{$UNAME_VERSION}" + exit ;; + i*86:*:3.2:*) + if test -f /usr/options/cb.name; then + UNAME_REL=`sed -n 's/.*Version //p' /dev/null >/dev/null ; then + UNAME_REL=`(/bin/uname -X|grep Release|sed -e 's/.*= //')` + (/bin/uname -X|grep i80486 >/dev/null) && UNAME_MACHINE=i486 + (/bin/uname -X|grep '^Machine.*Pentium' >/dev/null) \ + && UNAME_MACHINE=i586 + (/bin/uname -X|grep '^Machine.*Pent *II' >/dev/null) \ + && UNAME_MACHINE=i686 + (/bin/uname -X|grep '^Machine.*Pentium Pro' >/dev/null) \ + && UNAME_MACHINE=i686 + echo "$UNAME_MACHINE"-pc-sco"$UNAME_REL" + else + echo "$UNAME_MACHINE"-pc-sysv32 + fi + exit ;; + pc:*:*:*) + # Left here for compatibility: + # uname -m prints for DJGPP always 'pc', but it prints nothing about + # the processor, so we play safe by assuming i586. + # Note: whatever this is, it MUST be the same as what config.sub + # prints for the "djgpp" host, or else GDB configure will decide that + # this is a cross-build. + echo i586-pc-msdosdjgpp + exit ;; + Intel:Mach:3*:*) + echo i386-pc-mach3 + exit ;; + paragon:*:*:*) + echo i860-intel-osf1 + exit ;; + i860:*:4.*:*) # i860-SVR4 + if grep Stardent /usr/include/sys/uadmin.h >/dev/null 2>&1 ; then + echo i860-stardent-sysv"$UNAME_RELEASE" # Stardent Vistra i860-SVR4 + else # Add other i860-SVR4 vendors below as they are discovered. + echo i860-unknown-sysv"$UNAME_RELEASE" # Unknown i860-SVR4 + fi + exit ;; + mini*:CTIX:SYS*5:*) + # "miniframe" + echo m68010-convergent-sysv + exit ;; + mc68k:UNIX:SYSTEM5:3.51m) + echo m68k-convergent-sysv + exit ;; + M680?0:D-NIX:5.3:*) + echo m68k-diab-dnix + exit ;; + M68*:*:R3V[5678]*:*) + test -r /sysV68 && { echo 'm68k-motorola-sysv'; exit; } ;; + 3[345]??:*:4.0:3.0 | 3[34]??A:*:4.0:3.0 | 3[34]??,*:*:4.0:3.0 | 3[34]??/*:*:4.0:3.0 | 4400:*:4.0:3.0 | 4850:*:4.0:3.0 | SKA40:*:4.0:3.0 | SDS2:*:4.0:3.0 | SHG2:*:4.0:3.0 | S7501*:*:4.0:3.0) + OS_REL='' + test -r /etc/.relid \ + && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid` + /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ + && { echo i486-ncr-sysv4.3"$OS_REL"; exit; } + /bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \ + && { echo i586-ncr-sysv4.3"$OS_REL"; exit; } ;; + 3[34]??:*:4.0:* | 3[34]??,*:*:4.0:*) + /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ + && { echo i486-ncr-sysv4; exit; } ;; + NCR*:*:4.2:* | MPRAS*:*:4.2:*) + OS_REL='.3' + test -r /etc/.relid \ + && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid` + /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ + && { echo i486-ncr-sysv4.3"$OS_REL"; exit; } + /bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \ + && { echo i586-ncr-sysv4.3"$OS_REL"; exit; } + /bin/uname -p 2>/dev/null | /bin/grep pteron >/dev/null \ + && { echo i586-ncr-sysv4.3"$OS_REL"; exit; } ;; + m68*:LynxOS:2.*:* | m68*:LynxOS:3.0*:*) + echo m68k-unknown-lynxos"$UNAME_RELEASE" + exit ;; + mc68030:UNIX_System_V:4.*:*) + echo m68k-atari-sysv4 + exit ;; + TSUNAMI:LynxOS:2.*:*) + echo sparc-unknown-lynxos"$UNAME_RELEASE" + exit ;; + rs6000:LynxOS:2.*:*) + echo rs6000-unknown-lynxos"$UNAME_RELEASE" + exit ;; + PowerPC:LynxOS:2.*:* | PowerPC:LynxOS:3.[01]*:* | PowerPC:LynxOS:4.[02]*:*) + echo powerpc-unknown-lynxos"$UNAME_RELEASE" + exit ;; + SM[BE]S:UNIX_SV:*:*) + echo mips-dde-sysv"$UNAME_RELEASE" + exit ;; + RM*:ReliantUNIX-*:*:*) + echo mips-sni-sysv4 + exit ;; + RM*:SINIX-*:*:*) + echo mips-sni-sysv4 + exit ;; + *:SINIX-*:*:*) + if uname -p 2>/dev/null >/dev/null ; then + UNAME_MACHINE=`(uname -p) 2>/dev/null` + echo "$UNAME_MACHINE"-sni-sysv4 + else + echo ns32k-sni-sysv + fi + exit ;; + PENTIUM:*:4.0*:*) # Unisys `ClearPath HMP IX 4000' SVR4/MP effort + # says + echo i586-unisys-sysv4 + exit ;; + *:UNIX_System_V:4*:FTX*) + # From Gerald Hewes . + # How about differentiating between stratus architectures? -djm + echo hppa1.1-stratus-sysv4 + exit ;; + *:*:*:FTX*) + # From seanf@swdc.stratus.com. + echo i860-stratus-sysv4 + exit ;; + i*86:VOS:*:*) + # From Paul.Green@stratus.com. + echo "$UNAME_MACHINE"-stratus-vos + exit ;; + *:VOS:*:*) + # From Paul.Green@stratus.com. + echo hppa1.1-stratus-vos + exit ;; + mc68*:A/UX:*:*) + echo m68k-apple-aux"$UNAME_RELEASE" + exit ;; + news*:NEWS-OS:6*:*) + echo mips-sony-newsos6 + exit ;; + R[34]000:*System_V*:*:* | R4000:UNIX_SYSV:*:* | R*000:UNIX_SV:*:*) + if [ -d /usr/nec ]; then + echo mips-nec-sysv"$UNAME_RELEASE" + else + echo mips-unknown-sysv"$UNAME_RELEASE" + fi + exit ;; + BeBox:BeOS:*:*) # BeOS running on hardware made by Be, PPC only. + echo powerpc-be-beos + exit ;; + BeMac:BeOS:*:*) # BeOS running on Mac or Mac clone, PPC only. + echo powerpc-apple-beos + exit ;; + BePC:BeOS:*:*) # BeOS running on Intel PC compatible. + echo i586-pc-beos + exit ;; + BePC:Haiku:*:*) # Haiku running on Intel PC compatible. + echo i586-pc-haiku + exit ;; + x86_64:Haiku:*:*) + echo x86_64-unknown-haiku + exit ;; + SX-4:SUPER-UX:*:*) + echo sx4-nec-superux"$UNAME_RELEASE" + exit ;; + SX-5:SUPER-UX:*:*) + echo sx5-nec-superux"$UNAME_RELEASE" + exit ;; + SX-6:SUPER-UX:*:*) + echo sx6-nec-superux"$UNAME_RELEASE" + exit ;; + SX-7:SUPER-UX:*:*) + echo sx7-nec-superux"$UNAME_RELEASE" + exit ;; + SX-8:SUPER-UX:*:*) + echo sx8-nec-superux"$UNAME_RELEASE" + exit ;; + SX-8R:SUPER-UX:*:*) + echo sx8r-nec-superux"$UNAME_RELEASE" + exit ;; + SX-ACE:SUPER-UX:*:*) + echo sxace-nec-superux"$UNAME_RELEASE" + exit ;; + Power*:Rhapsody:*:*) + echo powerpc-apple-rhapsody"$UNAME_RELEASE" + exit ;; + *:Rhapsody:*:*) + echo "$UNAME_MACHINE"-apple-rhapsody"$UNAME_RELEASE" + exit ;; + *:Darwin:*:*) + UNAME_PROCESSOR=`uname -p` || UNAME_PROCESSOR=unknown + eval "$set_cc_for_build" + if test "$UNAME_PROCESSOR" = unknown ; then + UNAME_PROCESSOR=powerpc + fi + if test "`echo "$UNAME_RELEASE" | sed -e 's/\..*//'`" -le 10 ; then + if [ "$CC_FOR_BUILD" != no_compiler_found ]; then + if (echo '#ifdef __LP64__'; echo IS_64BIT_ARCH; echo '#endif') | \ + (CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) | \ + grep IS_64BIT_ARCH >/dev/null + then + case $UNAME_PROCESSOR in + i386) UNAME_PROCESSOR=x86_64 ;; + powerpc) UNAME_PROCESSOR=powerpc64 ;; + esac + fi + # On 10.4-10.6 one might compile for PowerPC via gcc -arch ppc + if (echo '#ifdef __POWERPC__'; echo IS_PPC; echo '#endif') | \ + (CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) | \ + grep IS_PPC >/dev/null + then + UNAME_PROCESSOR=powerpc + fi + fi + elif test "$UNAME_PROCESSOR" = i386 ; then + # Avoid executing cc on OS X 10.9, as it ships with a stub + # that puts up a graphical alert prompting to install + # developer tools. Any system running Mac OS X 10.7 or + # later (Darwin 11 and later) is required to have a 64-bit + # processor. This is not true of the ARM version of Darwin + # that Apple uses in portable devices. + UNAME_PROCESSOR=x86_64 + fi + echo "$UNAME_PROCESSOR"-apple-darwin"$UNAME_RELEASE" + exit ;; + *:procnto*:*:* | *:QNX:[0123456789]*:*) + UNAME_PROCESSOR=`uname -p` + if test "$UNAME_PROCESSOR" = x86; then + UNAME_PROCESSOR=i386 + UNAME_MACHINE=pc + fi + echo "$UNAME_PROCESSOR"-"$UNAME_MACHINE"-nto-qnx"$UNAME_RELEASE" + exit ;; + *:QNX:*:4*) + echo i386-pc-qnx + exit ;; + NEO-*:NONSTOP_KERNEL:*:*) + echo neo-tandem-nsk"$UNAME_RELEASE" + exit ;; + NSE-*:NONSTOP_KERNEL:*:*) + echo nse-tandem-nsk"$UNAME_RELEASE" + exit ;; + NSR-*:NONSTOP_KERNEL:*:*) + echo nsr-tandem-nsk"$UNAME_RELEASE" + exit ;; + NSV-*:NONSTOP_KERNEL:*:*) + echo nsv-tandem-nsk"$UNAME_RELEASE" + exit ;; + NSX-*:NONSTOP_KERNEL:*:*) + echo nsx-tandem-nsk"$UNAME_RELEASE" + exit ;; + *:NonStop-UX:*:*) + echo mips-compaq-nonstopux + exit ;; + BS2000:POSIX*:*:*) + echo bs2000-siemens-sysv + exit ;; + DS/*:UNIX_System_V:*:*) + echo "$UNAME_MACHINE"-"$UNAME_SYSTEM"-"$UNAME_RELEASE" + exit ;; + *:Plan9:*:*) + # "uname -m" is not consistent, so use $cputype instead. 386 + # is converted to i386 for consistency with other x86 + # operating systems. + if test "$cputype" = 386; then + UNAME_MACHINE=i386 + else + UNAME_MACHINE="$cputype" + fi + echo "$UNAME_MACHINE"-unknown-plan9 + exit ;; + *:TOPS-10:*:*) + echo pdp10-unknown-tops10 + exit ;; + *:TENEX:*:*) + echo pdp10-unknown-tenex + exit ;; + KS10:TOPS-20:*:* | KL10:TOPS-20:*:* | TYPE4:TOPS-20:*:*) + echo pdp10-dec-tops20 + exit ;; + XKL-1:TOPS-20:*:* | TYPE5:TOPS-20:*:*) + echo pdp10-xkl-tops20 + exit ;; + *:TOPS-20:*:*) + echo pdp10-unknown-tops20 + exit ;; + *:ITS:*:*) + echo pdp10-unknown-its + exit ;; + SEI:*:*:SEIUX) + echo mips-sei-seiux"$UNAME_RELEASE" + exit ;; + *:DragonFly:*:*) + echo "$UNAME_MACHINE"-unknown-dragonfly"`echo "$UNAME_RELEASE"|sed -e 's/[-(].*//'`" + exit ;; + *:*VMS:*:*) + UNAME_MACHINE=`(uname -p) 2>/dev/null` + case "$UNAME_MACHINE" in + A*) echo alpha-dec-vms ; exit ;; + I*) echo ia64-dec-vms ; exit ;; + V*) echo vax-dec-vms ; exit ;; + esac ;; + *:XENIX:*:SysV) + echo i386-pc-xenix + exit ;; + i*86:skyos:*:*) + echo "$UNAME_MACHINE"-pc-skyos"`echo "$UNAME_RELEASE" | sed -e 's/ .*$//'`" + exit ;; + i*86:rdos:*:*) + echo "$UNAME_MACHINE"-pc-rdos + exit ;; + i*86:AROS:*:*) + echo "$UNAME_MACHINE"-pc-aros + exit ;; + x86_64:VMkernel:*:*) + echo "$UNAME_MACHINE"-unknown-esx + exit ;; + amd64:Isilon\ OneFS:*:*) + echo x86_64-unknown-onefs + exit ;; +esac + +echo "$0: unable to guess system type" >&2 + +case "$UNAME_MACHINE:$UNAME_SYSTEM" in + mips:Linux | mips64:Linux) + # If we got here on MIPS GNU/Linux, output extra information. + cat >&2 <&2 </dev/null || echo unknown` +uname -r = `(uname -r) 2>/dev/null || echo unknown` +uname -s = `(uname -s) 2>/dev/null || echo unknown` +uname -v = `(uname -v) 2>/dev/null || echo unknown` + +/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null` +/bin/uname -X = `(/bin/uname -X) 2>/dev/null` + +hostinfo = `(hostinfo) 2>/dev/null` +/bin/universe = `(/bin/universe) 2>/dev/null` +/usr/bin/arch -k = `(/usr/bin/arch -k) 2>/dev/null` +/bin/arch = `(/bin/arch) 2>/dev/null` +/usr/bin/oslevel = `(/usr/bin/oslevel) 2>/dev/null` +/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null` + +UNAME_MACHINE = "$UNAME_MACHINE" +UNAME_RELEASE = "$UNAME_RELEASE" +UNAME_SYSTEM = "$UNAME_SYSTEM" +UNAME_VERSION = "$UNAME_VERSION" +EOF + +exit 1 + +# Local variables: +# eval: (add-hook 'write-file-functions 'time-stamp) +# time-stamp-start: "timestamp='" +# time-stamp-format: "%:y-%02m-%02d" +# time-stamp-end: "'" +# End: diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/config.sub b/deps/ngtcp2/ngtcp2/third-party/libev/config.sub new file mode 100755 index 00000000000000..1d8e98bcee23a0 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/config.sub @@ -0,0 +1,1801 @@ +#! /bin/sh +# Configuration validation subroutine script. +# Copyright 1992-2018 Free Software Foundation, Inc. + +timestamp='2018-02-22' + +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . +# +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program that contains a +# configuration script generated by Autoconf, you may include it under +# the same distribution terms that you use for the rest of that +# program. This Exception is an additional permission under section 7 +# of the GNU General Public License, version 3 ("GPLv3"). + + +# Please send patches to . +# +# Configuration subroutine to validate and canonicalize a configuration type. +# Supply the specified configuration type as an argument. +# If it is invalid, we print an error message on stderr and exit with code 1. +# Otherwise, we print the canonical config type on stdout and succeed. + +# You can get the latest version of this script from: +# https://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub + +# This file is supposed to be the same for all GNU packages +# and recognize all the CPU types, system types and aliases +# that are meaningful with *any* GNU software. +# Each package is responsible for reporting which valid configurations +# it does not support. The user should be able to distinguish +# a failure to support a valid configuration from a meaningless +# configuration. + +# The goal of this file is to map all the various variations of a given +# machine specification into a single specification in the form: +# CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM +# or in some cases, the newer four-part form: +# CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM +# It is wrong to echo any other type of specification. + +me=`echo "$0" | sed -e 's,.*/,,'` + +usage="\ +Usage: $0 [OPTION] CPU-MFR-OPSYS or ALIAS + +Canonicalize a configuration name. + +Options: + -h, --help print this help, then exit + -t, --time-stamp print date of last modification, then exit + -v, --version print version number, then exit + +Report bugs and patches to ." + +version="\ +GNU config.sub ($timestamp) + +Copyright 1992-2018 Free Software Foundation, Inc. + +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." + +help=" +Try \`$me --help' for more information." + +# Parse command line +while test $# -gt 0 ; do + case $1 in + --time-stamp | --time* | -t ) + echo "$timestamp" ; exit ;; + --version | -v ) + echo "$version" ; exit ;; + --help | --h* | -h ) + echo "$usage"; exit ;; + -- ) # Stop option processing + shift; break ;; + - ) # Use stdin as input. + break ;; + -* ) + echo "$me: invalid option $1$help" + exit 1 ;; + + *local*) + # First pass through any local machine types. + echo "$1" + exit ;; + + * ) + break ;; + esac +done + +case $# in + 0) echo "$me: missing argument$help" >&2 + exit 1;; + 1) ;; + *) echo "$me: too many arguments$help" >&2 + exit 1;; +esac + +# Separate what the user gave into CPU-COMPANY and OS or KERNEL-OS (if any). +# Here we must recognize all the valid KERNEL-OS combinations. +maybe_os=`echo "$1" | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\2/'` +case $maybe_os in + nto-qnx* | linux-gnu* | linux-android* | linux-dietlibc | linux-newlib* | \ + linux-musl* | linux-uclibc* | uclinux-uclibc* | uclinux-gnu* | kfreebsd*-gnu* | \ + knetbsd*-gnu* | netbsd*-gnu* | netbsd*-eabi* | \ + kopensolaris*-gnu* | cloudabi*-eabi* | \ + storm-chaos* | os2-emx* | rtmk-nova*) + os=-$maybe_os + basic_machine=`echo "$1" | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\1/'` + ;; + android-linux) + os=-linux-android + basic_machine=`echo "$1" | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\1/'`-unknown + ;; + *) + basic_machine=`echo "$1" | sed 's/-[^-]*$//'` + if [ "$basic_machine" != "$1" ] + then os=`echo "$1" | sed 's/.*-/-/'` + else os=; fi + ;; +esac + +### Let's recognize common machines as not being operating systems so +### that things like config.sub decstation-3100 work. We also +### recognize some manufacturers as not being operating systems, so we +### can provide default operating systems below. +case $os in + -sun*os*) + # Prevent following clause from handling this invalid input. + ;; + -dec* | -mips* | -sequent* | -encore* | -pc532* | -sgi* | -sony* | \ + -att* | -7300* | -3300* | -delta* | -motorola* | -sun[234]* | \ + -unicom* | -ibm* | -next | -hp | -isi* | -apollo | -altos* | \ + -convergent* | -ncr* | -news | -32* | -3600* | -3100* | -hitachi* |\ + -c[123]* | -convex* | -sun | -crds | -omron* | -dg | -ultra | -tti* | \ + -harris | -dolphin | -highlevel | -gould | -cbm | -ns | -masscomp | \ + -apple | -axis | -knuth | -cray | -microblaze*) + os= + basic_machine=$1 + ;; + -bluegene*) + os=-cnk + ;; + -sim | -cisco | -oki | -wec | -winbond) + os= + basic_machine=$1 + ;; + -scout) + ;; + -wrs) + os=-vxworks + basic_machine=$1 + ;; + -chorusos*) + os=-chorusos + basic_machine=$1 + ;; + -chorusrdb) + os=-chorusrdb + basic_machine=$1 + ;; + -hiux*) + os=-hiuxwe2 + ;; + -sco6) + os=-sco5v6 + basic_machine=`echo "$1" | sed -e 's/86-.*/86-pc/'` + ;; + -sco5) + os=-sco3.2v5 + basic_machine=`echo "$1" | sed -e 's/86-.*/86-pc/'` + ;; + -sco4) + os=-sco3.2v4 + basic_machine=`echo "$1" | sed -e 's/86-.*/86-pc/'` + ;; + -sco3.2.[4-9]*) + os=`echo $os | sed -e 's/sco3.2./sco3.2v/'` + basic_machine=`echo "$1" | sed -e 's/86-.*/86-pc/'` + ;; + -sco3.2v[4-9]*) + # Don't forget version if it is 3.2v4 or newer. + basic_machine=`echo "$1" | sed -e 's/86-.*/86-pc/'` + ;; + -sco5v6*) + # Don't forget version if it is 3.2v4 or newer. + basic_machine=`echo "$1" | sed -e 's/86-.*/86-pc/'` + ;; + -sco*) + os=-sco3.2v2 + basic_machine=`echo "$1" | sed -e 's/86-.*/86-pc/'` + ;; + -udk*) + basic_machine=`echo "$1" | sed -e 's/86-.*/86-pc/'` + ;; + -isc) + os=-isc2.2 + basic_machine=`echo "$1" | sed -e 's/86-.*/86-pc/'` + ;; + -clix*) + basic_machine=clipper-intergraph + ;; + -isc*) + basic_machine=`echo "$1" | sed -e 's/86-.*/86-pc/'` + ;; + -lynx*178) + os=-lynxos178 + ;; + -lynx*5) + os=-lynxos5 + ;; + -lynx*) + os=-lynxos + ;; + -ptx*) + basic_machine=`echo "$1" | sed -e 's/86-.*/86-sequent/'` + ;; + -psos*) + os=-psos + ;; + -mint | -mint[0-9]*) + basic_machine=m68k-atari + os=-mint + ;; +esac + +# Decode aliases for certain CPU-COMPANY combinations. +case $basic_machine in + # Recognize the basic CPU types without company name. + # Some are omitted here because they have special meanings below. + 1750a | 580 \ + | a29k \ + | aarch64 | aarch64_be \ + | alpha | alphaev[4-8] | alphaev56 | alphaev6[78] | alphapca5[67] \ + | alpha64 | alpha64ev[4-8] | alpha64ev56 | alpha64ev6[78] | alpha64pca5[67] \ + | am33_2.0 \ + | arc | arceb \ + | arm | arm[bl]e | arme[lb] | armv[2-8] | armv[3-8][lb] | armv7[arm] \ + | avr | avr32 \ + | ba \ + | be32 | be64 \ + | bfin \ + | c4x | c8051 | clipper \ + | d10v | d30v | dlx | dsp16xx \ + | e2k | epiphany \ + | fido | fr30 | frv | ft32 \ + | h8300 | h8500 | hppa | hppa1.[01] | hppa2.0 | hppa2.0[nw] | hppa64 \ + | hexagon \ + | i370 | i860 | i960 | ia16 | ia64 \ + | ip2k | iq2000 \ + | k1om \ + | le32 | le64 \ + | lm32 \ + | m32c | m32r | m32rle | m68000 | m68k | m88k \ + | maxq | mb | microblaze | microblazeel | mcore | mep | metag \ + | mips | mipsbe | mipseb | mipsel | mipsle \ + | mips16 \ + | mips64 | mips64el \ + | mips64octeon | mips64octeonel \ + | mips64orion | mips64orionel \ + | mips64r5900 | mips64r5900el \ + | mips64vr | mips64vrel \ + | mips64vr4100 | mips64vr4100el \ + | mips64vr4300 | mips64vr4300el \ + | mips64vr5000 | mips64vr5000el \ + | mips64vr5900 | mips64vr5900el \ + | mipsisa32 | mipsisa32el \ + | mipsisa32r2 | mipsisa32r2el \ + | mipsisa32r6 | mipsisa32r6el \ + | mipsisa64 | mipsisa64el \ + | mipsisa64r2 | mipsisa64r2el \ + | mipsisa64r6 | mipsisa64r6el \ + | mipsisa64sb1 | mipsisa64sb1el \ + | mipsisa64sr71k | mipsisa64sr71kel \ + | mipsr5900 | mipsr5900el \ + | mipstx39 | mipstx39el \ + | mn10200 | mn10300 \ + | moxie \ + | mt \ + | msp430 \ + | nds32 | nds32le | nds32be \ + | nios | nios2 | nios2eb | nios2el \ + | ns16k | ns32k \ + | open8 | or1k | or1knd | or32 \ + | pdp10 | pj | pjl \ + | powerpc | powerpc64 | powerpc64le | powerpcle \ + | pru \ + | pyramid \ + | riscv32 | riscv64 \ + | rl78 | rx \ + | score \ + | sh | sh[1234] | sh[24]a | sh[24]aeb | sh[23]e | sh[234]eb | sheb | shbe | shle | sh[1234]le | sh3ele \ + | sh64 | sh64le \ + | sparc | sparc64 | sparc64b | sparc64v | sparc86x | sparclet | sparclite \ + | sparcv8 | sparcv9 | sparcv9b | sparcv9v \ + | spu \ + | tahoe | tic4x | tic54x | tic55x | tic6x | tic80 | tron \ + | ubicom32 \ + | v850 | v850e | v850e1 | v850e2 | v850es | v850e2v3 \ + | visium \ + | wasm32 \ + | x86 | xc16x | xstormy16 | xtensa \ + | z8k | z80) + basic_machine=$basic_machine-unknown + ;; + c54x) + basic_machine=tic54x-unknown + ;; + c55x) + basic_machine=tic55x-unknown + ;; + c6x) + basic_machine=tic6x-unknown + ;; + leon|leon[3-9]) + basic_machine=sparc-$basic_machine + ;; + m6811 | m68hc11 | m6812 | m68hc12 | m68hcs12x | nvptx | picochip) + basic_machine=$basic_machine-unknown + os=-none + ;; + m88110 | m680[12346]0 | m683?2 | m68360 | m5200 | v70 | w65) + ;; + ms1) + basic_machine=mt-unknown + ;; + + strongarm | thumb | xscale) + basic_machine=arm-unknown + ;; + xgate) + basic_machine=$basic_machine-unknown + os=-none + ;; + xscaleeb) + basic_machine=armeb-unknown + ;; + + xscaleel) + basic_machine=armel-unknown + ;; + + # We use `pc' rather than `unknown' + # because (1) that's what they normally are, and + # (2) the word "unknown" tends to confuse beginning users. + i*86 | x86_64) + basic_machine=$basic_machine-pc + ;; + # Object if more than one company name word. + *-*-*) + echo Invalid configuration \`"$1"\': machine \`"$basic_machine"\' not recognized 1>&2 + exit 1 + ;; + # Recognize the basic CPU types with company name. + 580-* \ + | a29k-* \ + | aarch64-* | aarch64_be-* \ + | alpha-* | alphaev[4-8]-* | alphaev56-* | alphaev6[78]-* \ + | alpha64-* | alpha64ev[4-8]-* | alpha64ev56-* | alpha64ev6[78]-* \ + | alphapca5[67]-* | alpha64pca5[67]-* | arc-* | arceb-* \ + | arm-* | armbe-* | armle-* | armeb-* | armv*-* \ + | avr-* | avr32-* \ + | ba-* \ + | be32-* | be64-* \ + | bfin-* | bs2000-* \ + | c[123]* | c30-* | [cjt]90-* | c4x-* \ + | c8051-* | clipper-* | craynv-* | cydra-* \ + | d10v-* | d30v-* | dlx-* \ + | e2k-* | elxsi-* \ + | f30[01]-* | f700-* | fido-* | fr30-* | frv-* | fx80-* \ + | h8300-* | h8500-* \ + | hppa-* | hppa1.[01]-* | hppa2.0-* | hppa2.0[nw]-* | hppa64-* \ + | hexagon-* \ + | i*86-* | i860-* | i960-* | ia16-* | ia64-* \ + | ip2k-* | iq2000-* \ + | k1om-* \ + | le32-* | le64-* \ + | lm32-* \ + | m32c-* | m32r-* | m32rle-* \ + | m68000-* | m680[012346]0-* | m68360-* | m683?2-* | m68k-* \ + | m88110-* | m88k-* | maxq-* | mcore-* | metag-* \ + | microblaze-* | microblazeel-* \ + | mips-* | mipsbe-* | mipseb-* | mipsel-* | mipsle-* \ + | mips16-* \ + | mips64-* | mips64el-* \ + | mips64octeon-* | mips64octeonel-* \ + | mips64orion-* | mips64orionel-* \ + | mips64r5900-* | mips64r5900el-* \ + | mips64vr-* | mips64vrel-* \ + | mips64vr4100-* | mips64vr4100el-* \ + | mips64vr4300-* | mips64vr4300el-* \ + | mips64vr5000-* | mips64vr5000el-* \ + | mips64vr5900-* | mips64vr5900el-* \ + | mipsisa32-* | mipsisa32el-* \ + | mipsisa32r2-* | mipsisa32r2el-* \ + | mipsisa32r6-* | mipsisa32r6el-* \ + | mipsisa64-* | mipsisa64el-* \ + | mipsisa64r2-* | mipsisa64r2el-* \ + | mipsisa64r6-* | mipsisa64r6el-* \ + | mipsisa64sb1-* | mipsisa64sb1el-* \ + | mipsisa64sr71k-* | mipsisa64sr71kel-* \ + | mipsr5900-* | mipsr5900el-* \ + | mipstx39-* | mipstx39el-* \ + | mmix-* \ + | mt-* \ + | msp430-* \ + | nds32-* | nds32le-* | nds32be-* \ + | nios-* | nios2-* | nios2eb-* | nios2el-* \ + | none-* | np1-* | ns16k-* | ns32k-* \ + | open8-* \ + | or1k*-* \ + | orion-* \ + | pdp10-* | pdp11-* | pj-* | pjl-* | pn-* | power-* \ + | powerpc-* | powerpc64-* | powerpc64le-* | powerpcle-* \ + | pru-* \ + | pyramid-* \ + | riscv32-* | riscv64-* \ + | rl78-* | romp-* | rs6000-* | rx-* \ + | sh-* | sh[1234]-* | sh[24]a-* | sh[24]aeb-* | sh[23]e-* | sh[34]eb-* | sheb-* | shbe-* \ + | shle-* | sh[1234]le-* | sh3ele-* | sh64-* | sh64le-* \ + | sparc-* | sparc64-* | sparc64b-* | sparc64v-* | sparc86x-* | sparclet-* \ + | sparclite-* \ + | sparcv8-* | sparcv9-* | sparcv9b-* | sparcv9v-* | sv1-* | sx*-* \ + | tahoe-* \ + | tic30-* | tic4x-* | tic54x-* | tic55x-* | tic6x-* | tic80-* \ + | tile*-* \ + | tron-* \ + | ubicom32-* \ + | v850-* | v850e-* | v850e1-* | v850es-* | v850e2-* | v850e2v3-* \ + | vax-* \ + | visium-* \ + | wasm32-* \ + | we32k-* \ + | x86-* | x86_64-* | xc16x-* | xps100-* \ + | xstormy16-* | xtensa*-* \ + | ymp-* \ + | z8k-* | z80-*) + ;; + # Recognize the basic CPU types without company name, with glob match. + xtensa*) + basic_machine=$basic_machine-unknown + ;; + # Recognize the various machine names and aliases which stand + # for a CPU type and a company and sometimes even an OS. + 386bsd) + basic_machine=i386-pc + os=-bsd + ;; + 3b1 | 7300 | 7300-att | att-7300 | pc7300 | safari | unixpc) + basic_machine=m68000-att + ;; + 3b*) + basic_machine=we32k-att + ;; + a29khif) + basic_machine=a29k-amd + os=-udi + ;; + abacus) + basic_machine=abacus-unknown + ;; + adobe68k) + basic_machine=m68010-adobe + os=-scout + ;; + alliant | fx80) + basic_machine=fx80-alliant + ;; + altos | altos3068) + basic_machine=m68k-altos + ;; + am29k) + basic_machine=a29k-none + os=-bsd + ;; + amd64) + basic_machine=x86_64-pc + ;; + amd64-*) + basic_machine=x86_64-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + amdahl) + basic_machine=580-amdahl + os=-sysv + ;; + amiga | amiga-*) + basic_machine=m68k-unknown + ;; + amigaos | amigados) + basic_machine=m68k-unknown + os=-amigaos + ;; + amigaunix | amix) + basic_machine=m68k-unknown + os=-sysv4 + ;; + apollo68) + basic_machine=m68k-apollo + os=-sysv + ;; + apollo68bsd) + basic_machine=m68k-apollo + os=-bsd + ;; + aros) + basic_machine=i386-pc + os=-aros + ;; + asmjs) + basic_machine=asmjs-unknown + ;; + aux) + basic_machine=m68k-apple + os=-aux + ;; + balance) + basic_machine=ns32k-sequent + os=-dynix + ;; + blackfin) + basic_machine=bfin-unknown + os=-linux + ;; + blackfin-*) + basic_machine=bfin-`echo "$basic_machine" | sed 's/^[^-]*-//'` + os=-linux + ;; + bluegene*) + basic_machine=powerpc-ibm + os=-cnk + ;; + c54x-*) + basic_machine=tic54x-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + c55x-*) + basic_machine=tic55x-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + c6x-*) + basic_machine=tic6x-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + c90) + basic_machine=c90-cray + os=-unicos + ;; + cegcc) + basic_machine=arm-unknown + os=-cegcc + ;; + convex-c1) + basic_machine=c1-convex + os=-bsd + ;; + convex-c2) + basic_machine=c2-convex + os=-bsd + ;; + convex-c32) + basic_machine=c32-convex + os=-bsd + ;; + convex-c34) + basic_machine=c34-convex + os=-bsd + ;; + convex-c38) + basic_machine=c38-convex + os=-bsd + ;; + cray | j90) + basic_machine=j90-cray + os=-unicos + ;; + craynv) + basic_machine=craynv-cray + os=-unicosmp + ;; + cr16 | cr16-*) + basic_machine=cr16-unknown + os=-elf + ;; + crds | unos) + basic_machine=m68k-crds + ;; + crisv32 | crisv32-* | etraxfs*) + basic_machine=crisv32-axis + ;; + cris | cris-* | etrax*) + basic_machine=cris-axis + ;; + crx) + basic_machine=crx-unknown + os=-elf + ;; + da30 | da30-*) + basic_machine=m68k-da30 + ;; + decstation | decstation-3100 | pmax | pmax-* | pmin | dec3100 | decstatn) + basic_machine=mips-dec + ;; + decsystem10* | dec10*) + basic_machine=pdp10-dec + os=-tops10 + ;; + decsystem20* | dec20*) + basic_machine=pdp10-dec + os=-tops20 + ;; + delta | 3300 | motorola-3300 | motorola-delta \ + | 3300-motorola | delta-motorola) + basic_machine=m68k-motorola + ;; + delta88) + basic_machine=m88k-motorola + os=-sysv3 + ;; + dicos) + basic_machine=i686-pc + os=-dicos + ;; + djgpp) + basic_machine=i586-pc + os=-msdosdjgpp + ;; + dpx20 | dpx20-*) + basic_machine=rs6000-bull + os=-bosx + ;; + dpx2*) + basic_machine=m68k-bull + os=-sysv3 + ;; + e500v[12]) + basic_machine=powerpc-unknown + os=$os"spe" + ;; + e500v[12]-*) + basic_machine=powerpc-`echo "$basic_machine" | sed 's/^[^-]*-//'` + os=$os"spe" + ;; + ebmon29k) + basic_machine=a29k-amd + os=-ebmon + ;; + elxsi) + basic_machine=elxsi-elxsi + os=-bsd + ;; + encore | umax | mmax) + basic_machine=ns32k-encore + ;; + es1800 | OSE68k | ose68k | ose | OSE) + basic_machine=m68k-ericsson + os=-ose + ;; + fx2800) + basic_machine=i860-alliant + ;; + genix) + basic_machine=ns32k-ns + ;; + gmicro) + basic_machine=tron-gmicro + os=-sysv + ;; + go32) + basic_machine=i386-pc + os=-go32 + ;; + h3050r* | hiux*) + basic_machine=hppa1.1-hitachi + os=-hiuxwe2 + ;; + h8300hms) + basic_machine=h8300-hitachi + os=-hms + ;; + h8300xray) + basic_machine=h8300-hitachi + os=-xray + ;; + h8500hms) + basic_machine=h8500-hitachi + os=-hms + ;; + harris) + basic_machine=m88k-harris + os=-sysv3 + ;; + hp300-*) + basic_machine=m68k-hp + ;; + hp300bsd) + basic_machine=m68k-hp + os=-bsd + ;; + hp300hpux) + basic_machine=m68k-hp + os=-hpux + ;; + hp3k9[0-9][0-9] | hp9[0-9][0-9]) + basic_machine=hppa1.0-hp + ;; + hp9k2[0-9][0-9] | hp9k31[0-9]) + basic_machine=m68000-hp + ;; + hp9k3[2-9][0-9]) + basic_machine=m68k-hp + ;; + hp9k6[0-9][0-9] | hp6[0-9][0-9]) + basic_machine=hppa1.0-hp + ;; + hp9k7[0-79][0-9] | hp7[0-79][0-9]) + basic_machine=hppa1.1-hp + ;; + hp9k78[0-9] | hp78[0-9]) + # FIXME: really hppa2.0-hp + basic_machine=hppa1.1-hp + ;; + hp9k8[67]1 | hp8[67]1 | hp9k80[24] | hp80[24] | hp9k8[78]9 | hp8[78]9 | hp9k893 | hp893) + # FIXME: really hppa2.0-hp + basic_machine=hppa1.1-hp + ;; + hp9k8[0-9][13679] | hp8[0-9][13679]) + basic_machine=hppa1.1-hp + ;; + hp9k8[0-9][0-9] | hp8[0-9][0-9]) + basic_machine=hppa1.0-hp + ;; + hppaosf) + basic_machine=hppa1.1-hp + os=-osf + ;; + hppro) + basic_machine=hppa1.1-hp + os=-proelf + ;; + i370-ibm* | ibm*) + basic_machine=i370-ibm + ;; + i*86v32) + basic_machine=`echo "$1" | sed -e 's/86.*/86-pc/'` + os=-sysv32 + ;; + i*86v4*) + basic_machine=`echo "$1" | sed -e 's/86.*/86-pc/'` + os=-sysv4 + ;; + i*86v) + basic_machine=`echo "$1" | sed -e 's/86.*/86-pc/'` + os=-sysv + ;; + i*86sol2) + basic_machine=`echo "$1" | sed -e 's/86.*/86-pc/'` + os=-solaris2 + ;; + i386mach) + basic_machine=i386-mach + os=-mach + ;; + vsta) + basic_machine=i386-unknown + os=-vsta + ;; + iris | iris4d) + basic_machine=mips-sgi + case $os in + -irix*) + ;; + *) + os=-irix4 + ;; + esac + ;; + isi68 | isi) + basic_machine=m68k-isi + os=-sysv + ;; + leon-*|leon[3-9]-*) + basic_machine=sparc-`echo "$basic_machine" | sed 's/-.*//'` + ;; + m68knommu) + basic_machine=m68k-unknown + os=-linux + ;; + m68knommu-*) + basic_machine=m68k-`echo "$basic_machine" | sed 's/^[^-]*-//'` + os=-linux + ;; + magnum | m3230) + basic_machine=mips-mips + os=-sysv + ;; + merlin) + basic_machine=ns32k-utek + os=-sysv + ;; + microblaze*) + basic_machine=microblaze-xilinx + ;; + mingw64) + basic_machine=x86_64-pc + os=-mingw64 + ;; + mingw32) + basic_machine=i686-pc + os=-mingw32 + ;; + mingw32ce) + basic_machine=arm-unknown + os=-mingw32ce + ;; + miniframe) + basic_machine=m68000-convergent + ;; + *mint | -mint[0-9]* | *MiNT | *MiNT[0-9]*) + basic_machine=m68k-atari + os=-mint + ;; + mips3*-*) + basic_machine=`echo "$basic_machine" | sed -e 's/mips3/mips64/'` + ;; + mips3*) + basic_machine=`echo "$basic_machine" | sed -e 's/mips3/mips64/'`-unknown + ;; + monitor) + basic_machine=m68k-rom68k + os=-coff + ;; + morphos) + basic_machine=powerpc-unknown + os=-morphos + ;; + moxiebox) + basic_machine=moxie-unknown + os=-moxiebox + ;; + msdos) + basic_machine=i386-pc + os=-msdos + ;; + ms1-*) + basic_machine=`echo "$basic_machine" | sed -e 's/ms1-/mt-/'` + ;; + msys) + basic_machine=i686-pc + os=-msys + ;; + mvs) + basic_machine=i370-ibm + os=-mvs + ;; + nacl) + basic_machine=le32-unknown + os=-nacl + ;; + ncr3000) + basic_machine=i486-ncr + os=-sysv4 + ;; + netbsd386) + basic_machine=i386-unknown + os=-netbsd + ;; + netwinder) + basic_machine=armv4l-rebel + os=-linux + ;; + news | news700 | news800 | news900) + basic_machine=m68k-sony + os=-newsos + ;; + news1000) + basic_machine=m68030-sony + os=-newsos + ;; + news-3600 | risc-news) + basic_machine=mips-sony + os=-newsos + ;; + necv70) + basic_machine=v70-nec + os=-sysv + ;; + next | m*-next) + basic_machine=m68k-next + case $os in + -nextstep* ) + ;; + -ns2*) + os=-nextstep2 + ;; + *) + os=-nextstep3 + ;; + esac + ;; + nh3000) + basic_machine=m68k-harris + os=-cxux + ;; + nh[45]000) + basic_machine=m88k-harris + os=-cxux + ;; + nindy960) + basic_machine=i960-intel + os=-nindy + ;; + mon960) + basic_machine=i960-intel + os=-mon960 + ;; + nonstopux) + basic_machine=mips-compaq + os=-nonstopux + ;; + np1) + basic_machine=np1-gould + ;; + neo-tandem) + basic_machine=neo-tandem + ;; + nse-tandem) + basic_machine=nse-tandem + ;; + nsr-tandem) + basic_machine=nsr-tandem + ;; + nsv-tandem) + basic_machine=nsv-tandem + ;; + nsx-tandem) + basic_machine=nsx-tandem + ;; + op50n-* | op60c-*) + basic_machine=hppa1.1-oki + os=-proelf + ;; + openrisc | openrisc-*) + basic_machine=or32-unknown + ;; + os400) + basic_machine=powerpc-ibm + os=-os400 + ;; + OSE68000 | ose68000) + basic_machine=m68000-ericsson + os=-ose + ;; + os68k) + basic_machine=m68k-none + os=-os68k + ;; + pa-hitachi) + basic_machine=hppa1.1-hitachi + os=-hiuxwe2 + ;; + paragon) + basic_machine=i860-intel + os=-osf + ;; + parisc) + basic_machine=hppa-unknown + os=-linux + ;; + parisc-*) + basic_machine=hppa-`echo "$basic_machine" | sed 's/^[^-]*-//'` + os=-linux + ;; + pbd) + basic_machine=sparc-tti + ;; + pbb) + basic_machine=m68k-tti + ;; + pc532 | pc532-*) + basic_machine=ns32k-pc532 + ;; + pc98) + basic_machine=i386-pc + ;; + pc98-*) + basic_machine=i386-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + pentium | p5 | k5 | k6 | nexgen | viac3) + basic_machine=i586-pc + ;; + pentiumpro | p6 | 6x86 | athlon | athlon_*) + basic_machine=i686-pc + ;; + pentiumii | pentium2 | pentiumiii | pentium3) + basic_machine=i686-pc + ;; + pentium4) + basic_machine=i786-pc + ;; + pentium-* | p5-* | k5-* | k6-* | nexgen-* | viac3-*) + basic_machine=i586-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + pentiumpro-* | p6-* | 6x86-* | athlon-*) + basic_machine=i686-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + pentiumii-* | pentium2-* | pentiumiii-* | pentium3-*) + basic_machine=i686-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + pentium4-*) + basic_machine=i786-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + pn) + basic_machine=pn-gould + ;; + power) basic_machine=power-ibm + ;; + ppc | ppcbe) basic_machine=powerpc-unknown + ;; + ppc-* | ppcbe-*) + basic_machine=powerpc-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + ppcle | powerpclittle) + basic_machine=powerpcle-unknown + ;; + ppcle-* | powerpclittle-*) + basic_machine=powerpcle-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + ppc64) basic_machine=powerpc64-unknown + ;; + ppc64-*) basic_machine=powerpc64-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + ppc64le | powerpc64little) + basic_machine=powerpc64le-unknown + ;; + ppc64le-* | powerpc64little-*) + basic_machine=powerpc64le-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + ps2) + basic_machine=i386-ibm + ;; + pw32) + basic_machine=i586-unknown + os=-pw32 + ;; + rdos | rdos64) + basic_machine=x86_64-pc + os=-rdos + ;; + rdos32) + basic_machine=i386-pc + os=-rdos + ;; + rom68k) + basic_machine=m68k-rom68k + os=-coff + ;; + rm[46]00) + basic_machine=mips-siemens + ;; + rtpc | rtpc-*) + basic_machine=romp-ibm + ;; + s390 | s390-*) + basic_machine=s390-ibm + ;; + s390x | s390x-*) + basic_machine=s390x-ibm + ;; + sa29200) + basic_machine=a29k-amd + os=-udi + ;; + sb1) + basic_machine=mipsisa64sb1-unknown + ;; + sb1el) + basic_machine=mipsisa64sb1el-unknown + ;; + sde) + basic_machine=mipsisa32-sde + os=-elf + ;; + sei) + basic_machine=mips-sei + os=-seiux + ;; + sequent) + basic_machine=i386-sequent + ;; + sh5el) + basic_machine=sh5le-unknown + ;; + simso-wrs) + basic_machine=sparclite-wrs + os=-vxworks + ;; + sps7) + basic_machine=m68k-bull + os=-sysv2 + ;; + spur) + basic_machine=spur-unknown + ;; + st2000) + basic_machine=m68k-tandem + ;; + stratus) + basic_machine=i860-stratus + os=-sysv4 + ;; + strongarm-* | thumb-*) + basic_machine=arm-`echo "$basic_machine" | sed 's/^[^-]*-//'` + ;; + sun2) + basic_machine=m68000-sun + ;; + sun2os3) + basic_machine=m68000-sun + os=-sunos3 + ;; + sun2os4) + basic_machine=m68000-sun + os=-sunos4 + ;; + sun3os3) + basic_machine=m68k-sun + os=-sunos3 + ;; + sun3os4) + basic_machine=m68k-sun + os=-sunos4 + ;; + sun4os3) + basic_machine=sparc-sun + os=-sunos3 + ;; + sun4os4) + basic_machine=sparc-sun + os=-sunos4 + ;; + sun4sol2) + basic_machine=sparc-sun + os=-solaris2 + ;; + sun3 | sun3-*) + basic_machine=m68k-sun + ;; + sun4) + basic_machine=sparc-sun + ;; + sun386 | sun386i | roadrunner) + basic_machine=i386-sun + ;; + sv1) + basic_machine=sv1-cray + os=-unicos + ;; + symmetry) + basic_machine=i386-sequent + os=-dynix + ;; + t3e) + basic_machine=alphaev5-cray + os=-unicos + ;; + t90) + basic_machine=t90-cray + os=-unicos + ;; + tile*) + basic_machine=$basic_machine-unknown + os=-linux-gnu + ;; + tx39) + basic_machine=mipstx39-unknown + ;; + tx39el) + basic_machine=mipstx39el-unknown + ;; + toad1) + basic_machine=pdp10-xkl + os=-tops20 + ;; + tower | tower-32) + basic_machine=m68k-ncr + ;; + tpf) + basic_machine=s390x-ibm + os=-tpf + ;; + udi29k) + basic_machine=a29k-amd + os=-udi + ;; + ultra3) + basic_machine=a29k-nyu + os=-sym1 + ;; + v810 | necv810) + basic_machine=v810-nec + os=-none + ;; + vaxv) + basic_machine=vax-dec + os=-sysv + ;; + vms) + basic_machine=vax-dec + os=-vms + ;; + vpp*|vx|vx-*) + basic_machine=f301-fujitsu + ;; + vxworks960) + basic_machine=i960-wrs + os=-vxworks + ;; + vxworks68) + basic_machine=m68k-wrs + os=-vxworks + ;; + vxworks29k) + basic_machine=a29k-wrs + os=-vxworks + ;; + w65*) + basic_machine=w65-wdc + os=-none + ;; + w89k-*) + basic_machine=hppa1.1-winbond + os=-proelf + ;; + x64) + basic_machine=x86_64-pc + ;; + xbox) + basic_machine=i686-pc + os=-mingw32 + ;; + xps | xps100) + basic_machine=xps100-honeywell + ;; + xscale-* | xscalee[bl]-*) + basic_machine=`echo "$basic_machine" | sed 's/^xscale/arm/'` + ;; + ymp) + basic_machine=ymp-cray + os=-unicos + ;; + none) + basic_machine=none-none + os=-none + ;; + +# Here we handle the default manufacturer of certain CPU types. It is in +# some cases the only manufacturer, in others, it is the most popular. + w89k) + basic_machine=hppa1.1-winbond + ;; + op50n) + basic_machine=hppa1.1-oki + ;; + op60c) + basic_machine=hppa1.1-oki + ;; + romp) + basic_machine=romp-ibm + ;; + mmix) + basic_machine=mmix-knuth + ;; + rs6000) + basic_machine=rs6000-ibm + ;; + vax) + basic_machine=vax-dec + ;; + pdp11) + basic_machine=pdp11-dec + ;; + we32k) + basic_machine=we32k-att + ;; + sh[1234] | sh[24]a | sh[24]aeb | sh[34]eb | sh[1234]le | sh[23]ele) + basic_machine=sh-unknown + ;; + cydra) + basic_machine=cydra-cydrome + ;; + orion) + basic_machine=orion-highlevel + ;; + orion105) + basic_machine=clipper-highlevel + ;; + mac | mpw | mac-mpw) + basic_machine=m68k-apple + ;; + pmac | pmac-mpw) + basic_machine=powerpc-apple + ;; + *-unknown) + # Make sure to match an already-canonicalized machine name. + ;; + *) + echo Invalid configuration \`"$1"\': machine \`"$basic_machine"\' not recognized 1>&2 + exit 1 + ;; +esac + +# Here we canonicalize certain aliases for manufacturers. +case $basic_machine in + *-digital*) + basic_machine=`echo "$basic_machine" | sed 's/digital.*/dec/'` + ;; + *-commodore*) + basic_machine=`echo "$basic_machine" | sed 's/commodore.*/cbm/'` + ;; + *) + ;; +esac + +# Decode manufacturer-specific aliases for certain operating systems. + +if [ x"$os" != x"" ] +then +case $os in + # First match some system type aliases that might get confused + # with valid system types. + # -solaris* is a basic system type, with this one exception. + -auroraux) + os=-auroraux + ;; + -solaris1 | -solaris1.*) + os=`echo $os | sed -e 's|solaris1|sunos4|'` + ;; + -solaris) + os=-solaris2 + ;; + -unixware*) + os=-sysv4.2uw + ;; + -gnu/linux*) + os=`echo $os | sed -e 's|gnu/linux|linux-gnu|'` + ;; + # es1800 is here to avoid being matched by es* (a different OS) + -es1800*) + os=-ose + ;; + # Now accept the basic system types. + # The portable systems comes first. + # Each alternative MUST end in a * to match a version number. + # -sysv* is not here because it comes later, after sysvr4. + -gnu* | -bsd* | -mach* | -minix* | -genix* | -ultrix* | -irix* \ + | -*vms* | -sco* | -esix* | -isc* | -aix* | -cnk* | -sunos | -sunos[34]*\ + | -hpux* | -unos* | -osf* | -luna* | -dgux* | -auroraux* | -solaris* \ + | -sym* | -kopensolaris* | -plan9* \ + | -amigaos* | -amigados* | -msdos* | -newsos* | -unicos* | -aof* \ + | -aos* | -aros* | -cloudabi* | -sortix* \ + | -nindy* | -vxsim* | -vxworks* | -ebmon* | -hms* | -mvs* \ + | -clix* | -riscos* | -uniplus* | -iris* | -rtu* | -xenix* \ + | -hiux* | -knetbsd* | -mirbsd* | -netbsd* \ + | -bitrig* | -openbsd* | -solidbsd* | -libertybsd* \ + | -ekkobsd* | -kfreebsd* | -freebsd* | -riscix* | -lynxos* \ + | -bosx* | -nextstep* | -cxux* | -aout* | -elf* | -oabi* \ + | -ptx* | -coff* | -ecoff* | -winnt* | -domain* | -vsta* \ + | -udi* | -eabi* | -lites* | -ieee* | -go32* | -aux* \ + | -chorusos* | -chorusrdb* | -cegcc* | -glidix* \ + | -cygwin* | -msys* | -pe* | -psos* | -moss* | -proelf* | -rtems* \ + | -midipix* | -mingw32* | -mingw64* | -linux-gnu* | -linux-android* \ + | -linux-newlib* | -linux-musl* | -linux-uclibc* \ + | -uxpv* | -beos* | -mpeix* | -udk* | -moxiebox* \ + | -interix* | -uwin* | -mks* | -rhapsody* | -darwin* \ + | -openstep* | -oskit* | -conix* | -pw32* | -nonstopux* \ + | -storm-chaos* | -tops10* | -tenex* | -tops20* | -its* \ + | -os2* | -vos* | -palmos* | -uclinux* | -nucleus* \ + | -morphos* | -superux* | -rtmk* | -windiss* \ + | -powermax* | -dnix* | -nx6 | -nx7 | -sei* | -dragonfly* \ + | -skyos* | -haiku* | -rdos* | -toppers* | -drops* | -es* \ + | -onefs* | -tirtos* | -phoenix* | -fuchsia* | -redox* | -bme* \ + | -midnightbsd*) + # Remember, each alternative MUST END IN *, to match a version number. + ;; + -qnx*) + case $basic_machine in + x86-* | i*86-*) + ;; + *) + os=-nto$os + ;; + esac + ;; + -nto-qnx*) + ;; + -nto*) + os=`echo $os | sed -e 's|nto|nto-qnx|'` + ;; + -sim | -xray | -os68k* | -v88r* \ + | -windows* | -osx | -abug | -netware* | -os9* \ + | -macos* | -mpw* | -magic* | -mmixware* | -mon960* | -lnews*) + ;; + -mac*) + os=`echo "$os" | sed -e 's|mac|macos|'` + ;; + -linux-dietlibc) + os=-linux-dietlibc + ;; + -linux*) + os=`echo $os | sed -e 's|linux|linux-gnu|'` + ;; + -sunos5*) + os=`echo "$os" | sed -e 's|sunos5|solaris2|'` + ;; + -sunos6*) + os=`echo "$os" | sed -e 's|sunos6|solaris3|'` + ;; + -opened*) + os=-openedition + ;; + -os400*) + os=-os400 + ;; + -wince*) + os=-wince + ;; + -utek*) + os=-bsd + ;; + -dynix*) + os=-bsd + ;; + -acis*) + os=-aos + ;; + -atheos*) + os=-atheos + ;; + -syllable*) + os=-syllable + ;; + -386bsd) + os=-bsd + ;; + -ctix* | -uts*) + os=-sysv + ;; + -nova*) + os=-rtmk-nova + ;; + -ns2) + os=-nextstep2 + ;; + -nsk*) + os=-nsk + ;; + # Preserve the version number of sinix5. + -sinix5.*) + os=`echo $os | sed -e 's|sinix|sysv|'` + ;; + -sinix*) + os=-sysv4 + ;; + -tpf*) + os=-tpf + ;; + -triton*) + os=-sysv3 + ;; + -oss*) + os=-sysv3 + ;; + -svr4*) + os=-sysv4 + ;; + -svr3) + os=-sysv3 + ;; + -sysvr4) + os=-sysv4 + ;; + # This must come after -sysvr4. + -sysv*) + ;; + -ose*) + os=-ose + ;; + -*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*) + os=-mint + ;; + -zvmoe) + os=-zvmoe + ;; + -dicos*) + os=-dicos + ;; + -pikeos*) + # Until real need of OS specific support for + # particular features comes up, bare metal + # configurations are quite functional. + case $basic_machine in + arm*) + os=-eabi + ;; + *) + os=-elf + ;; + esac + ;; + -nacl*) + ;; + -ios) + ;; + -none) + ;; + *) + # Get rid of the `-' at the beginning of $os. + os=`echo $os | sed 's/[^-]*-//'` + echo Invalid configuration \`"$1"\': system \`"$os"\' not recognized 1>&2 + exit 1 + ;; +esac +else + +# Here we handle the default operating systems that come with various machines. +# The value should be what the vendor currently ships out the door with their +# machine or put another way, the most popular os provided with the machine. + +# Note that if you're going to try to match "-MANUFACTURER" here (say, +# "-sun"), then you have to tell the case statement up towards the top +# that MANUFACTURER isn't an operating system. Otherwise, code above +# will signal an error saying that MANUFACTURER isn't an operating +# system, and we'll never get to this point. + +case $basic_machine in + score-*) + os=-elf + ;; + spu-*) + os=-elf + ;; + *-acorn) + os=-riscix1.2 + ;; + arm*-rebel) + os=-linux + ;; + arm*-semi) + os=-aout + ;; + c4x-* | tic4x-*) + os=-coff + ;; + c8051-*) + os=-elf + ;; + hexagon-*) + os=-elf + ;; + tic54x-*) + os=-coff + ;; + tic55x-*) + os=-coff + ;; + tic6x-*) + os=-coff + ;; + # This must come before the *-dec entry. + pdp10-*) + os=-tops20 + ;; + pdp11-*) + os=-none + ;; + *-dec | vax-*) + os=-ultrix4.2 + ;; + m68*-apollo) + os=-domain + ;; + i386-sun) + os=-sunos4.0.2 + ;; + m68000-sun) + os=-sunos3 + ;; + m68*-cisco) + os=-aout + ;; + mep-*) + os=-elf + ;; + mips*-cisco) + os=-elf + ;; + mips*-*) + os=-elf + ;; + or32-*) + os=-coff + ;; + *-tti) # must be before sparc entry or we get the wrong os. + os=-sysv3 + ;; + sparc-* | *-sun) + os=-sunos4.1.1 + ;; + pru-*) + os=-elf + ;; + *-be) + os=-beos + ;; + *-ibm) + os=-aix + ;; + *-knuth) + os=-mmixware + ;; + *-wec) + os=-proelf + ;; + *-winbond) + os=-proelf + ;; + *-oki) + os=-proelf + ;; + *-hp) + os=-hpux + ;; + *-hitachi) + os=-hiux + ;; + i860-* | *-att | *-ncr | *-altos | *-motorola | *-convergent) + os=-sysv + ;; + *-cbm) + os=-amigaos + ;; + *-dg) + os=-dgux + ;; + *-dolphin) + os=-sysv3 + ;; + m68k-ccur) + os=-rtu + ;; + m88k-omron*) + os=-luna + ;; + *-next) + os=-nextstep + ;; + *-sequent) + os=-ptx + ;; + *-crds) + os=-unos + ;; + *-ns) + os=-genix + ;; + i370-*) + os=-mvs + ;; + *-gould) + os=-sysv + ;; + *-highlevel) + os=-bsd + ;; + *-encore) + os=-bsd + ;; + *-sgi) + os=-irix + ;; + *-siemens) + os=-sysv4 + ;; + *-masscomp) + os=-rtu + ;; + f30[01]-fujitsu | f700-fujitsu) + os=-uxpv + ;; + *-rom68k) + os=-coff + ;; + *-*bug) + os=-coff + ;; + *-apple) + os=-macos + ;; + *-atari*) + os=-mint + ;; + *) + os=-none + ;; +esac +fi + +# Here we handle the case where we know the os, and the CPU type, but not the +# manufacturer. We pick the logical manufacturer. +vendor=unknown +case $basic_machine in + *-unknown) + case $os in + -riscix*) + vendor=acorn + ;; + -sunos*) + vendor=sun + ;; + -cnk*|-aix*) + vendor=ibm + ;; + -beos*) + vendor=be + ;; + -hpux*) + vendor=hp + ;; + -mpeix*) + vendor=hp + ;; + -hiux*) + vendor=hitachi + ;; + -unos*) + vendor=crds + ;; + -dgux*) + vendor=dg + ;; + -luna*) + vendor=omron + ;; + -genix*) + vendor=ns + ;; + -mvs* | -opened*) + vendor=ibm + ;; + -os400*) + vendor=ibm + ;; + -ptx*) + vendor=sequent + ;; + -tpf*) + vendor=ibm + ;; + -vxsim* | -vxworks* | -windiss*) + vendor=wrs + ;; + -aux*) + vendor=apple + ;; + -hms*) + vendor=hitachi + ;; + -mpw* | -macos*) + vendor=apple + ;; + -*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*) + vendor=atari + ;; + -vos*) + vendor=stratus + ;; + esac + basic_machine=`echo "$basic_machine" | sed "s/unknown/$vendor/"` + ;; +esac + +echo "$basic_machine$os" +exit + +# Local variables: +# eval: (add-hook 'write-file-functions 'time-stamp) +# time-stamp-start: "timestamp='" +# time-stamp-format: "%:y-%02m-%02d" +# time-stamp-end: "'" +# End: diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/configure b/deps/ngtcp2/ngtcp2/third-party/libev/configure new file mode 100755 index 00000000000000..e4c30cce25cf11 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/configure @@ -0,0 +1,14624 @@ +#! /bin/sh +# Guess values for system-dependent variables and create Makefiles. +# Generated by GNU Autoconf 2.69 for libev 4.33. +# +# +# Copyright (C) 1992-1996, 1998-2012 Free Software Foundation, Inc. +# +# +# This configure script is free software; the Free Software Foundation +# gives unlimited permission to copy, distribute and modify it. +## -------------------- ## +## M4sh Initialization. ## +## -------------------- ## + +# Be more Bourne compatible +DUALCASE=1; export DUALCASE # for MKS sh +if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then : + emulate sh + NULLCMD=: + # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which + # is contrary to our usage. Disable this feature. + alias -g '${1+"$@"}'='"$@"' + setopt NO_GLOB_SUBST +else + case `(set -o) 2>/dev/null` in #( + *posix*) : + set -o posix ;; #( + *) : + ;; +esac +fi + + +as_nl=' +' +export as_nl +# Printing a long string crashes Solaris 7 /usr/bin/printf. +as_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' +as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo +as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo +# Prefer a ksh shell builtin over an external printf program on Solaris, +# but without wasting forks for bash or zsh. +if test -z "$BASH_VERSION$ZSH_VERSION" \ + && (test "X`print -r -- $as_echo`" = "X$as_echo") 2>/dev/null; then + as_echo='print -r --' + as_echo_n='print -rn --' +elif (test "X`printf %s $as_echo`" = "X$as_echo") 2>/dev/null; then + as_echo='printf %s\n' + as_echo_n='printf %s' +else + if test "X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`" = "X-n $as_echo"; then + as_echo_body='eval /usr/ucb/echo -n "$1$as_nl"' + as_echo_n='/usr/ucb/echo -n' + else + as_echo_body='eval expr "X$1" : "X\\(.*\\)"' + as_echo_n_body='eval + arg=$1; + case $arg in #( + *"$as_nl"*) + expr "X$arg" : "X\\(.*\\)$as_nl"; + arg=`expr "X$arg" : ".*$as_nl\\(.*\\)"`;; + esac; + expr "X$arg" : "X\\(.*\\)" | tr -d "$as_nl" + ' + export as_echo_n_body + as_echo_n='sh -c $as_echo_n_body as_echo' + fi + export as_echo_body + as_echo='sh -c $as_echo_body as_echo' +fi + +# The user is always right. +if test "${PATH_SEPARATOR+set}" != set; then + PATH_SEPARATOR=: + (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { + (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || + PATH_SEPARATOR=';' + } +fi + + +# IFS +# We need space, tab and new line, in precisely that order. Quoting is +# there to prevent editors from complaining about space-tab. +# (If _AS_PATH_WALK were called with IFS unset, it would disable word +# splitting by setting IFS to empty value.) +IFS=" "" $as_nl" + +# Find who we are. Look in the path if we contain no directory separator. +as_myself= +case $0 in #(( + *[\\/]* ) as_myself=$0 ;; + *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + test -r "$as_dir/$0" && as_myself=$as_dir/$0 && break + done +IFS=$as_save_IFS + + ;; +esac +# We did not find ourselves, most probably we were run as `sh COMMAND' +# in which case we are not to be found in the path. +if test "x$as_myself" = x; then + as_myself=$0 +fi +if test ! -f "$as_myself"; then + $as_echo "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 + exit 1 +fi + +# Unset variables that we do not need and which cause bugs (e.g. in +# pre-3.0 UWIN ksh). But do not cause bugs in bash 2.01; the "|| exit 1" +# suppresses any "Segmentation fault" message there. '((' could +# trigger a bug in pdksh 5.2.14. +for as_var in BASH_ENV ENV MAIL MAILPATH +do eval test x\${$as_var+set} = xset \ + && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : +done +PS1='$ ' +PS2='> ' +PS4='+ ' + +# NLS nuisances. +LC_ALL=C +export LC_ALL +LANGUAGE=C +export LANGUAGE + +# CDPATH. +(unset CDPATH) >/dev/null 2>&1 && unset CDPATH + +# Use a proper internal environment variable to ensure we don't fall + # into an infinite loop, continuously re-executing ourselves. + if test x"${_as_can_reexec}" != xno && test "x$CONFIG_SHELL" != x; then + _as_can_reexec=no; export _as_can_reexec; + # We cannot yet assume a decent shell, so we have to provide a +# neutralization value for shells without unset; and this also +# works around shells that cannot unset nonexistent variables. +# Preserve -v and -x to the replacement shell. +BASH_ENV=/dev/null +ENV=/dev/null +(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV +case $- in # (((( + *v*x* | *x*v* ) as_opts=-vx ;; + *v* ) as_opts=-v ;; + *x* ) as_opts=-x ;; + * ) as_opts= ;; +esac +exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} +# Admittedly, this is quite paranoid, since all the known shells bail +# out after a failed `exec'. +$as_echo "$0: could not re-execute with $CONFIG_SHELL" >&2 +as_fn_exit 255 + fi + # We don't want this to propagate to other subprocesses. + { _as_can_reexec=; unset _as_can_reexec;} +if test "x$CONFIG_SHELL" = x; then + as_bourne_compatible="if test -n \"\${ZSH_VERSION+set}\" && (emulate sh) >/dev/null 2>&1; then : + emulate sh + NULLCMD=: + # Pre-4.2 versions of Zsh do word splitting on \${1+\"\$@\"}, which + # is contrary to our usage. Disable this feature. + alias -g '\${1+\"\$@\"}'='\"\$@\"' + setopt NO_GLOB_SUBST +else + case \`(set -o) 2>/dev/null\` in #( + *posix*) : + set -o posix ;; #( + *) : + ;; +esac +fi +" + as_required="as_fn_return () { (exit \$1); } +as_fn_success () { as_fn_return 0; } +as_fn_failure () { as_fn_return 1; } +as_fn_ret_success () { return 0; } +as_fn_ret_failure () { return 1; } + +exitcode=0 +as_fn_success || { exitcode=1; echo as_fn_success failed.; } +as_fn_failure && { exitcode=1; echo as_fn_failure succeeded.; } +as_fn_ret_success || { exitcode=1; echo as_fn_ret_success failed.; } +as_fn_ret_failure && { exitcode=1; echo as_fn_ret_failure succeeded.; } +if ( set x; as_fn_ret_success y && test x = \"\$1\" ); then : + +else + exitcode=1; echo positional parameters were not saved. +fi +test x\$exitcode = x0 || exit 1 +test -x / || exit 1" + as_suggested=" as_lineno_1=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_1a=\$LINENO + as_lineno_2=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_2a=\$LINENO + eval 'test \"x\$as_lineno_1'\$as_run'\" != \"x\$as_lineno_2'\$as_run'\" && + test \"x\`expr \$as_lineno_1'\$as_run' + 1\`\" = \"x\$as_lineno_2'\$as_run'\"' || exit 1 + + test -n \"\${ZSH_VERSION+set}\${BASH_VERSION+set}\" || ( + ECHO='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' + ECHO=\$ECHO\$ECHO\$ECHO\$ECHO\$ECHO + ECHO=\$ECHO\$ECHO\$ECHO\$ECHO\$ECHO\$ECHO + PATH=/empty FPATH=/empty; export PATH FPATH + test \"X\`printf %s \$ECHO\`\" = \"X\$ECHO\" \\ + || test \"X\`print -r -- \$ECHO\`\" = \"X\$ECHO\" ) || exit 1 +test \$(( 1 + 1 )) = 2 || exit 1" + if (eval "$as_required") 2>/dev/null; then : + as_have_required=yes +else + as_have_required=no +fi + if test x$as_have_required = xyes && (eval "$as_suggested") 2>/dev/null; then : + +else + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +as_found=false +for as_dir in /bin$PATH_SEPARATOR/usr/bin$PATH_SEPARATOR$PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + as_found=: + case $as_dir in #( + /*) + for as_base in sh bash ksh sh5; do + # Try only shells that exist, to save several forks. + as_shell=$as_dir/$as_base + if { test -f "$as_shell" || test -f "$as_shell.exe"; } && + { $as_echo "$as_bourne_compatible""$as_required" | as_run=a "$as_shell"; } 2>/dev/null; then : + CONFIG_SHELL=$as_shell as_have_required=yes + if { $as_echo "$as_bourne_compatible""$as_suggested" | as_run=a "$as_shell"; } 2>/dev/null; then : + break 2 +fi +fi + done;; + esac + as_found=false +done +$as_found || { if { test -f "$SHELL" || test -f "$SHELL.exe"; } && + { $as_echo "$as_bourne_compatible""$as_required" | as_run=a "$SHELL"; } 2>/dev/null; then : + CONFIG_SHELL=$SHELL as_have_required=yes +fi; } +IFS=$as_save_IFS + + + if test "x$CONFIG_SHELL" != x; then : + export CONFIG_SHELL + # We cannot yet assume a decent shell, so we have to provide a +# neutralization value for shells without unset; and this also +# works around shells that cannot unset nonexistent variables. +# Preserve -v and -x to the replacement shell. +BASH_ENV=/dev/null +ENV=/dev/null +(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV +case $- in # (((( + *v*x* | *x*v* ) as_opts=-vx ;; + *v* ) as_opts=-v ;; + *x* ) as_opts=-x ;; + * ) as_opts= ;; +esac +exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} +# Admittedly, this is quite paranoid, since all the known shells bail +# out after a failed `exec'. +$as_echo "$0: could not re-execute with $CONFIG_SHELL" >&2 +exit 255 +fi + + if test x$as_have_required = xno; then : + $as_echo "$0: This script requires a shell more modern than all" + $as_echo "$0: the shells that I found on your system." + if test x${ZSH_VERSION+set} = xset ; then + $as_echo "$0: In particular, zsh $ZSH_VERSION has bugs and should" + $as_echo "$0: be upgraded to zsh 4.3.4 or later." + else + $as_echo "$0: Please tell bug-autoconf@gnu.org about your system, +$0: including any error possibly output before this +$0: message. Then install a modern shell, or manually run +$0: the script under such a shell if you do have one." + fi + exit 1 +fi +fi +fi +SHELL=${CONFIG_SHELL-/bin/sh} +export SHELL +# Unset more variables known to interfere with behavior of common tools. +CLICOLOR_FORCE= GREP_OPTIONS= +unset CLICOLOR_FORCE GREP_OPTIONS + +## --------------------- ## +## M4sh Shell Functions. ## +## --------------------- ## +# as_fn_unset VAR +# --------------- +# Portably unset VAR. +as_fn_unset () +{ + { eval $1=; unset $1;} +} +as_unset=as_fn_unset + +# as_fn_set_status STATUS +# ----------------------- +# Set $? to STATUS, without forking. +as_fn_set_status () +{ + return $1 +} # as_fn_set_status + +# as_fn_exit STATUS +# ----------------- +# Exit the shell with STATUS, even in a "trap 0" or "set -e" context. +as_fn_exit () +{ + set +e + as_fn_set_status $1 + exit $1 +} # as_fn_exit + +# as_fn_mkdir_p +# ------------- +# Create "$as_dir" as a directory, including parents if necessary. +as_fn_mkdir_p () +{ + + case $as_dir in #( + -*) as_dir=./$as_dir;; + esac + test -d "$as_dir" || eval $as_mkdir_p || { + as_dirs= + while :; do + case $as_dir in #( + *\'*) as_qdir=`$as_echo "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( + *) as_qdir=$as_dir;; + esac + as_dirs="'$as_qdir' $as_dirs" + as_dir=`$as_dirname -- "$as_dir" || +$as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ + X"$as_dir" : 'X\(//\)[^/]' \| \ + X"$as_dir" : 'X\(//\)$' \| \ + X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || +$as_echo X"$as_dir" | + sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ + s//\1/ + q + } + /^X\(\/\/\)[^/].*/{ + s//\1/ + q + } + /^X\(\/\/\)$/{ + s//\1/ + q + } + /^X\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + test -d "$as_dir" && break + done + test -z "$as_dirs" || eval "mkdir $as_dirs" + } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" + + +} # as_fn_mkdir_p + +# as_fn_executable_p FILE +# ----------------------- +# Test if FILE is an executable regular file. +as_fn_executable_p () +{ + test -f "$1" && test -x "$1" +} # as_fn_executable_p +# as_fn_append VAR VALUE +# ---------------------- +# Append the text in VALUE to the end of the definition contained in VAR. Take +# advantage of any shell optimizations that allow amortized linear growth over +# repeated appends, instead of the typical quadratic growth present in naive +# implementations. +if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null; then : + eval 'as_fn_append () + { + eval $1+=\$2 + }' +else + as_fn_append () + { + eval $1=\$$1\$2 + } +fi # as_fn_append + +# as_fn_arith ARG... +# ------------------ +# Perform arithmetic evaluation on the ARGs, and store the result in the +# global $as_val. Take advantage of shells that can avoid forks. The arguments +# must be portable across $(()) and expr. +if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null; then : + eval 'as_fn_arith () + { + as_val=$(( $* )) + }' +else + as_fn_arith () + { + as_val=`expr "$@" || test $? -eq 1` + } +fi # as_fn_arith + + +# as_fn_error STATUS ERROR [LINENO LOG_FD] +# ---------------------------------------- +# Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are +# provided, also output the error to LOG_FD, referencing LINENO. Then exit the +# script with STATUS, using 1 if that was 0. +as_fn_error () +{ + as_status=$1; test $as_status -eq 0 && as_status=1 + if test "$4"; then + as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 + fi + $as_echo "$as_me: error: $2" >&2 + as_fn_exit $as_status +} # as_fn_error + +if expr a : '\(a\)' >/dev/null 2>&1 && + test "X`expr 00001 : '.*\(...\)'`" = X001; then + as_expr=expr +else + as_expr=false +fi + +if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then + as_basename=basename +else + as_basename=false +fi + +if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then + as_dirname=dirname +else + as_dirname=false +fi + +as_me=`$as_basename -- "$0" || +$as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ + X"$0" : 'X\(//\)$' \| \ + X"$0" : 'X\(/\)' \| . 2>/dev/null || +$as_echo X/"$0" | + sed '/^.*\/\([^/][^/]*\)\/*$/{ + s//\1/ + q + } + /^X\/\(\/\/\)$/{ + s//\1/ + q + } + /^X\/\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + +# Avoid depending upon Character Ranges. +as_cr_letters='abcdefghijklmnopqrstuvwxyz' +as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ' +as_cr_Letters=$as_cr_letters$as_cr_LETTERS +as_cr_digits='0123456789' +as_cr_alnum=$as_cr_Letters$as_cr_digits + + + as_lineno_1=$LINENO as_lineno_1a=$LINENO + as_lineno_2=$LINENO as_lineno_2a=$LINENO + eval 'test "x$as_lineno_1'$as_run'" != "x$as_lineno_2'$as_run'" && + test "x`expr $as_lineno_1'$as_run' + 1`" = "x$as_lineno_2'$as_run'"' || { + # Blame Lee E. McMahon (1931-1989) for sed's syntax. :-) + sed -n ' + p + /[$]LINENO/= + ' <$as_myself | + sed ' + s/[$]LINENO.*/&-/ + t lineno + b + :lineno + N + :loop + s/[$]LINENO\([^'$as_cr_alnum'_].*\n\)\(.*\)/\2\1\2/ + t loop + s/-\n.*// + ' >$as_me.lineno && + chmod +x "$as_me.lineno" || + { $as_echo "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; } + + # If we had to re-execute with $CONFIG_SHELL, we're ensured to have + # already done that, so ensure we don't try to do so again and fall + # in an infinite loop. This has already happened in practice. + _as_can_reexec=no; export _as_can_reexec + # Don't try to exec as it changes $[0], causing all sort of problems + # (the dirname of $[0] is not the place where we might find the + # original and so on. Autoconf is especially sensitive to this). + . "./$as_me.lineno" + # Exit status is that of the last command. + exit +} + +ECHO_C= ECHO_N= ECHO_T= +case `echo -n x` in #((((( +-n*) + case `echo 'xy\c'` in + *c*) ECHO_T=' ';; # ECHO_T is single tab character. + xy) ECHO_C='\c';; + *) echo `echo ksh88 bug on AIX 6.1` > /dev/null + ECHO_T=' ';; + esac;; +*) + ECHO_N='-n';; +esac + +rm -f conf$$ conf$$.exe conf$$.file +if test -d conf$$.dir; then + rm -f conf$$.dir/conf$$.file +else + rm -f conf$$.dir + mkdir conf$$.dir 2>/dev/null +fi +if (echo >conf$$.file) 2>/dev/null; then + if ln -s conf$$.file conf$$ 2>/dev/null; then + as_ln_s='ln -s' + # ... but there are two gotchas: + # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. + # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. + # In both cases, we have to default to `cp -pR'. + ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || + as_ln_s='cp -pR' + elif ln conf$$.file conf$$ 2>/dev/null; then + as_ln_s=ln + else + as_ln_s='cp -pR' + fi +else + as_ln_s='cp -pR' +fi +rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file +rmdir conf$$.dir 2>/dev/null + +if mkdir -p . 2>/dev/null; then + as_mkdir_p='mkdir -p "$as_dir"' +else + test -d ./-p && rmdir ./-p + as_mkdir_p=false +fi + +as_test_x='test -x' +as_executable_p=as_fn_executable_p + +# Sed expression to map a string onto a valid CPP name. +as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" + +# Sed expression to map a string onto a valid variable name. +as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" + +SHELL=${CONFIG_SHELL-/bin/sh} + + +test -n "$DJDIR" || exec 7<&0 &1 + +# Name of the host. +# hostname on some systems (SVR3.2, old GNU/Linux) returns a bogus exit status, +# so uname gets run too. +ac_hostname=`(hostname || uname -n) 2>/dev/null | sed 1q` + +# +# Initializations. +# +ac_default_prefix=/usr/local +ac_clean_files= +ac_config_libobj_dir=. +LIBOBJS= +cross_compiling=no +subdirs= +MFLAGS= +MAKEFLAGS= + +# Identity of this package. +PACKAGE_NAME='libev' +PACKAGE_TARNAME='libev' +PACKAGE_VERSION='4.33' +PACKAGE_STRING='libev 4.33' +PACKAGE_BUGREPORT='' +PACKAGE_URL='' + +ac_unique_file="ev_epoll.c" +# Factoring default headers for most tests. +ac_includes_default="\ +#include +#ifdef HAVE_SYS_TYPES_H +# include +#endif +#ifdef HAVE_SYS_STAT_H +# include +#endif +#ifdef STDC_HEADERS +# include +# include +#else +# ifdef HAVE_STDLIB_H +# include +# endif +#endif +#ifdef HAVE_STRING_H +# if !defined STDC_HEADERS && defined HAVE_MEMORY_H +# include +# endif +# include +#endif +#ifdef HAVE_STRINGS_H +# include +#endif +#ifdef HAVE_INTTYPES_H +# include +#endif +#ifdef HAVE_STDINT_H +# include +#endif +#ifdef HAVE_UNISTD_H +# include +#endif" + +ac_subst_vars='am__EXEEXT_FALSE +am__EXEEXT_TRUE +LTLIBOBJS +LIBOBJS +CPP +LT_SYS_LIBRARY_PATH +OTOOL64 +OTOOL +LIPO +NMEDIT +DSYMUTIL +MANIFEST_TOOL +RANLIB +ac_ct_AR +AR +DLLTOOL +OBJDUMP +LN_S +NM +ac_ct_DUMPBIN +DUMPBIN +LD +FGREP +EGREP +GREP +SED +host_os +host_vendor +host_cpu +host +build_os +build_vendor +build_cpu +build +LIBTOOL +am__fastdepCC_FALSE +am__fastdepCC_TRUE +CCDEPMODE +am__nodep +AMDEPBACKSLASH +AMDEP_FALSE +AMDEP_TRUE +am__include +DEPDIR +OBJEXT +EXEEXT +ac_ct_CC +CPPFLAGS +LDFLAGS +CFLAGS +CC +MAINT +MAINTAINER_MODE_FALSE +MAINTAINER_MODE_TRUE +AM_BACKSLASH +AM_DEFAULT_VERBOSITY +AM_DEFAULT_V +AM_V +am__untar +am__tar +AMTAR +am__leading_dot +SET_MAKE +AWK +mkdir_p +MKDIR_P +INSTALL_STRIP_PROGRAM +STRIP +install_sh +MAKEINFO +AUTOHEADER +AUTOMAKE +AUTOCONF +ACLOCAL +VERSION +PACKAGE +CYGPATH_W +am__isrc +INSTALL_DATA +INSTALL_SCRIPT +INSTALL_PROGRAM +target_alias +host_alias +build_alias +LIBS +ECHO_T +ECHO_N +ECHO_C +DEFS +mandir +localedir +libdir +psdir +pdfdir +dvidir +htmldir +infodir +docdir +oldincludedir +includedir +runstatedir +localstatedir +sharedstatedir +sysconfdir +datadir +datarootdir +libexecdir +sbindir +bindir +program_transform_name +prefix +exec_prefix +PACKAGE_URL +PACKAGE_BUGREPORT +PACKAGE_STRING +PACKAGE_VERSION +PACKAGE_TARNAME +PACKAGE_NAME +PATH_SEPARATOR +SHELL +am__quote' +ac_subst_files='' +ac_user_opts=' +enable_option_checking +enable_silent_rules +enable_maintainer_mode +enable_dependency_tracking +enable_shared +enable_static +with_pic +enable_fast_install +with_aix_soname +with_gnu_ld +with_sysroot +enable_libtool_lock +' + ac_precious_vars='build_alias +host_alias +target_alias +CC +CFLAGS +LDFLAGS +LIBS +CPPFLAGS +LT_SYS_LIBRARY_PATH +CPP' + + +# Initialize some variables set by options. +ac_init_help= +ac_init_version=false +ac_unrecognized_opts= +ac_unrecognized_sep= +# The variables have the same names as the options, with +# dashes changed to underlines. +cache_file=/dev/null +exec_prefix=NONE +no_create= +no_recursion= +prefix=NONE +program_prefix=NONE +program_suffix=NONE +program_transform_name=s,x,x, +silent= +site= +srcdir= +verbose= +x_includes=NONE +x_libraries=NONE + +# Installation directory options. +# These are left unexpanded so users can "make install exec_prefix=/foo" +# and all the variables that are supposed to be based on exec_prefix +# by default will actually change. +# Use braces instead of parens because sh, perl, etc. also accept them. +# (The list follows the same order as the GNU Coding Standards.) +bindir='${exec_prefix}/bin' +sbindir='${exec_prefix}/sbin' +libexecdir='${exec_prefix}/libexec' +datarootdir='${prefix}/share' +datadir='${datarootdir}' +sysconfdir='${prefix}/etc' +sharedstatedir='${prefix}/com' +localstatedir='${prefix}/var' +runstatedir='${localstatedir}/run' +includedir='${prefix}/include' +oldincludedir='/usr/include' +docdir='${datarootdir}/doc/${PACKAGE_TARNAME}' +infodir='${datarootdir}/info' +htmldir='${docdir}' +dvidir='${docdir}' +pdfdir='${docdir}' +psdir='${docdir}' +libdir='${exec_prefix}/lib' +localedir='${datarootdir}/locale' +mandir='${datarootdir}/man' + +ac_prev= +ac_dashdash= +for ac_option +do + # If the previous option needs an argument, assign it. + if test -n "$ac_prev"; then + eval $ac_prev=\$ac_option + ac_prev= + continue + fi + + case $ac_option in + *=?*) ac_optarg=`expr "X$ac_option" : '[^=]*=\(.*\)'` ;; + *=) ac_optarg= ;; + *) ac_optarg=yes ;; + esac + + # Accept the important Cygnus configure options, so we can diagnose typos. + + case $ac_dashdash$ac_option in + --) + ac_dashdash=yes ;; + + -bindir | --bindir | --bindi | --bind | --bin | --bi) + ac_prev=bindir ;; + -bindir=* | --bindir=* | --bindi=* | --bind=* | --bin=* | --bi=*) + bindir=$ac_optarg ;; + + -build | --build | --buil | --bui | --bu) + ac_prev=build_alias ;; + -build=* | --build=* | --buil=* | --bui=* | --bu=*) + build_alias=$ac_optarg ;; + + -cache-file | --cache-file | --cache-fil | --cache-fi \ + | --cache-f | --cache- | --cache | --cach | --cac | --ca | --c) + ac_prev=cache_file ;; + -cache-file=* | --cache-file=* | --cache-fil=* | --cache-fi=* \ + | --cache-f=* | --cache-=* | --cache=* | --cach=* | --cac=* | --ca=* | --c=*) + cache_file=$ac_optarg ;; + + --config-cache | -C) + cache_file=config.cache ;; + + -datadir | --datadir | --datadi | --datad) + ac_prev=datadir ;; + -datadir=* | --datadir=* | --datadi=* | --datad=*) + datadir=$ac_optarg ;; + + -datarootdir | --datarootdir | --datarootdi | --datarootd | --dataroot \ + | --dataroo | --dataro | --datar) + ac_prev=datarootdir ;; + -datarootdir=* | --datarootdir=* | --datarootdi=* | --datarootd=* \ + | --dataroot=* | --dataroo=* | --dataro=* | --datar=*) + datarootdir=$ac_optarg ;; + + -disable-* | --disable-*) + ac_useropt=`expr "x$ac_option" : 'x-*disable-\(.*\)'` + # Reject names that are not valid shell variable names. + expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && + as_fn_error $? "invalid feature name: $ac_useropt" + ac_useropt_orig=$ac_useropt + ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` + case $ac_user_opts in + *" +"enable_$ac_useropt" +"*) ;; + *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--disable-$ac_useropt_orig" + ac_unrecognized_sep=', ';; + esac + eval enable_$ac_useropt=no ;; + + -docdir | --docdir | --docdi | --doc | --do) + ac_prev=docdir ;; + -docdir=* | --docdir=* | --docdi=* | --doc=* | --do=*) + docdir=$ac_optarg ;; + + -dvidir | --dvidir | --dvidi | --dvid | --dvi | --dv) + ac_prev=dvidir ;; + -dvidir=* | --dvidir=* | --dvidi=* | --dvid=* | --dvi=* | --dv=*) + dvidir=$ac_optarg ;; + + -enable-* | --enable-*) + ac_useropt=`expr "x$ac_option" : 'x-*enable-\([^=]*\)'` + # Reject names that are not valid shell variable names. + expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && + as_fn_error $? "invalid feature name: $ac_useropt" + ac_useropt_orig=$ac_useropt + ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` + case $ac_user_opts in + *" +"enable_$ac_useropt" +"*) ;; + *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--enable-$ac_useropt_orig" + ac_unrecognized_sep=', ';; + esac + eval enable_$ac_useropt=\$ac_optarg ;; + + -exec-prefix | --exec_prefix | --exec-prefix | --exec-prefi \ + | --exec-pref | --exec-pre | --exec-pr | --exec-p | --exec- \ + | --exec | --exe | --ex) + ac_prev=exec_prefix ;; + -exec-prefix=* | --exec_prefix=* | --exec-prefix=* | --exec-prefi=* \ + | --exec-pref=* | --exec-pre=* | --exec-pr=* | --exec-p=* | --exec-=* \ + | --exec=* | --exe=* | --ex=*) + exec_prefix=$ac_optarg ;; + + -gas | --gas | --ga | --g) + # Obsolete; use --with-gas. + with_gas=yes ;; + + -help | --help | --hel | --he | -h) + ac_init_help=long ;; + -help=r* | --help=r* | --hel=r* | --he=r* | -hr*) + ac_init_help=recursive ;; + -help=s* | --help=s* | --hel=s* | --he=s* | -hs*) + ac_init_help=short ;; + + -host | --host | --hos | --ho) + ac_prev=host_alias ;; + -host=* | --host=* | --hos=* | --ho=*) + host_alias=$ac_optarg ;; + + -htmldir | --htmldir | --htmldi | --htmld | --html | --htm | --ht) + ac_prev=htmldir ;; + -htmldir=* | --htmldir=* | --htmldi=* | --htmld=* | --html=* | --htm=* \ + | --ht=*) + htmldir=$ac_optarg ;; + + -includedir | --includedir | --includedi | --included | --include \ + | --includ | --inclu | --incl | --inc) + ac_prev=includedir ;; + -includedir=* | --includedir=* | --includedi=* | --included=* | --include=* \ + | --includ=* | --inclu=* | --incl=* | --inc=*) + includedir=$ac_optarg ;; + + -infodir | --infodir | --infodi | --infod | --info | --inf) + ac_prev=infodir ;; + -infodir=* | --infodir=* | --infodi=* | --infod=* | --info=* | --inf=*) + infodir=$ac_optarg ;; + + -libdir | --libdir | --libdi | --libd) + ac_prev=libdir ;; + -libdir=* | --libdir=* | --libdi=* | --libd=*) + libdir=$ac_optarg ;; + + -libexecdir | --libexecdir | --libexecdi | --libexecd | --libexec \ + | --libexe | --libex | --libe) + ac_prev=libexecdir ;; + -libexecdir=* | --libexecdir=* | --libexecdi=* | --libexecd=* | --libexec=* \ + | --libexe=* | --libex=* | --libe=*) + libexecdir=$ac_optarg ;; + + -localedir | --localedir | --localedi | --localed | --locale) + ac_prev=localedir ;; + -localedir=* | --localedir=* | --localedi=* | --localed=* | --locale=*) + localedir=$ac_optarg ;; + + -localstatedir | --localstatedir | --localstatedi | --localstated \ + | --localstate | --localstat | --localsta | --localst | --locals) + ac_prev=localstatedir ;; + -localstatedir=* | --localstatedir=* | --localstatedi=* | --localstated=* \ + | --localstate=* | --localstat=* | --localsta=* | --localst=* | --locals=*) + localstatedir=$ac_optarg ;; + + -mandir | --mandir | --mandi | --mand | --man | --ma | --m) + ac_prev=mandir ;; + -mandir=* | --mandir=* | --mandi=* | --mand=* | --man=* | --ma=* | --m=*) + mandir=$ac_optarg ;; + + -nfp | --nfp | --nf) + # Obsolete; use --without-fp. + with_fp=no ;; + + -no-create | --no-create | --no-creat | --no-crea | --no-cre \ + | --no-cr | --no-c | -n) + no_create=yes ;; + + -no-recursion | --no-recursion | --no-recursio | --no-recursi \ + | --no-recurs | --no-recur | --no-recu | --no-rec | --no-re | --no-r) + no_recursion=yes ;; + + -oldincludedir | --oldincludedir | --oldincludedi | --oldincluded \ + | --oldinclude | --oldinclud | --oldinclu | --oldincl | --oldinc \ + | --oldin | --oldi | --old | --ol | --o) + ac_prev=oldincludedir ;; + -oldincludedir=* | --oldincludedir=* | --oldincludedi=* | --oldincluded=* \ + | --oldinclude=* | --oldinclud=* | --oldinclu=* | --oldincl=* | --oldinc=* \ + | --oldin=* | --oldi=* | --old=* | --ol=* | --o=*) + oldincludedir=$ac_optarg ;; + + -prefix | --prefix | --prefi | --pref | --pre | --pr | --p) + ac_prev=prefix ;; + -prefix=* | --prefix=* | --prefi=* | --pref=* | --pre=* | --pr=* | --p=*) + prefix=$ac_optarg ;; + + -program-prefix | --program-prefix | --program-prefi | --program-pref \ + | --program-pre | --program-pr | --program-p) + ac_prev=program_prefix ;; + -program-prefix=* | --program-prefix=* | --program-prefi=* \ + | --program-pref=* | --program-pre=* | --program-pr=* | --program-p=*) + program_prefix=$ac_optarg ;; + + -program-suffix | --program-suffix | --program-suffi | --program-suff \ + | --program-suf | --program-su | --program-s) + ac_prev=program_suffix ;; + -program-suffix=* | --program-suffix=* | --program-suffi=* \ + | --program-suff=* | --program-suf=* | --program-su=* | --program-s=*) + program_suffix=$ac_optarg ;; + + -program-transform-name | --program-transform-name \ + | --program-transform-nam | --program-transform-na \ + | --program-transform-n | --program-transform- \ + | --program-transform | --program-transfor \ + | --program-transfo | --program-transf \ + | --program-trans | --program-tran \ + | --progr-tra | --program-tr | --program-t) + ac_prev=program_transform_name ;; + -program-transform-name=* | --program-transform-name=* \ + | --program-transform-nam=* | --program-transform-na=* \ + | --program-transform-n=* | --program-transform-=* \ + | --program-transform=* | --program-transfor=* \ + | --program-transfo=* | --program-transf=* \ + | --program-trans=* | --program-tran=* \ + | --progr-tra=* | --program-tr=* | --program-t=*) + program_transform_name=$ac_optarg ;; + + -pdfdir | --pdfdir | --pdfdi | --pdfd | --pdf | --pd) + ac_prev=pdfdir ;; + -pdfdir=* | --pdfdir=* | --pdfdi=* | --pdfd=* | --pdf=* | --pd=*) + pdfdir=$ac_optarg ;; + + -psdir | --psdir | --psdi | --psd | --ps) + ac_prev=psdir ;; + -psdir=* | --psdir=* | --psdi=* | --psd=* | --ps=*) + psdir=$ac_optarg ;; + + -q | -quiet | --quiet | --quie | --qui | --qu | --q \ + | -silent | --silent | --silen | --sile | --sil) + silent=yes ;; + + -runstatedir | --runstatedir | --runstatedi | --runstated \ + | --runstate | --runstat | --runsta | --runst | --runs \ + | --run | --ru | --r) + ac_prev=runstatedir ;; + -runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \ + | --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \ + | --run=* | --ru=* | --r=*) + runstatedir=$ac_optarg ;; + + -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb) + ac_prev=sbindir ;; + -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \ + | --sbi=* | --sb=*) + sbindir=$ac_optarg ;; + + -sharedstatedir | --sharedstatedir | --sharedstatedi \ + | --sharedstated | --sharedstate | --sharedstat | --sharedsta \ + | --sharedst | --shareds | --shared | --share | --shar \ + | --sha | --sh) + ac_prev=sharedstatedir ;; + -sharedstatedir=* | --sharedstatedir=* | --sharedstatedi=* \ + | --sharedstated=* | --sharedstate=* | --sharedstat=* | --sharedsta=* \ + | --sharedst=* | --shareds=* | --shared=* | --share=* | --shar=* \ + | --sha=* | --sh=*) + sharedstatedir=$ac_optarg ;; + + -site | --site | --sit) + ac_prev=site ;; + -site=* | --site=* | --sit=*) + site=$ac_optarg ;; + + -srcdir | --srcdir | --srcdi | --srcd | --src | --sr) + ac_prev=srcdir ;; + -srcdir=* | --srcdir=* | --srcdi=* | --srcd=* | --src=* | --sr=*) + srcdir=$ac_optarg ;; + + -sysconfdir | --sysconfdir | --sysconfdi | --sysconfd | --sysconf \ + | --syscon | --sysco | --sysc | --sys | --sy) + ac_prev=sysconfdir ;; + -sysconfdir=* | --sysconfdir=* | --sysconfdi=* | --sysconfd=* | --sysconf=* \ + | --syscon=* | --sysco=* | --sysc=* | --sys=* | --sy=*) + sysconfdir=$ac_optarg ;; + + -target | --target | --targe | --targ | --tar | --ta | --t) + ac_prev=target_alias ;; + -target=* | --target=* | --targe=* | --targ=* | --tar=* | --ta=* | --t=*) + target_alias=$ac_optarg ;; + + -v | -verbose | --verbose | --verbos | --verbo | --verb) + verbose=yes ;; + + -version | --version | --versio | --versi | --vers | -V) + ac_init_version=: ;; + + -with-* | --with-*) + ac_useropt=`expr "x$ac_option" : 'x-*with-\([^=]*\)'` + # Reject names that are not valid shell variable names. + expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && + as_fn_error $? "invalid package name: $ac_useropt" + ac_useropt_orig=$ac_useropt + ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` + case $ac_user_opts in + *" +"with_$ac_useropt" +"*) ;; + *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--with-$ac_useropt_orig" + ac_unrecognized_sep=', ';; + esac + eval with_$ac_useropt=\$ac_optarg ;; + + -without-* | --without-*) + ac_useropt=`expr "x$ac_option" : 'x-*without-\(.*\)'` + # Reject names that are not valid shell variable names. + expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && + as_fn_error $? "invalid package name: $ac_useropt" + ac_useropt_orig=$ac_useropt + ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` + case $ac_user_opts in + *" +"with_$ac_useropt" +"*) ;; + *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--without-$ac_useropt_orig" + ac_unrecognized_sep=', ';; + esac + eval with_$ac_useropt=no ;; + + --x) + # Obsolete; use --with-x. + with_x=yes ;; + + -x-includes | --x-includes | --x-include | --x-includ | --x-inclu \ + | --x-incl | --x-inc | --x-in | --x-i) + ac_prev=x_includes ;; + -x-includes=* | --x-includes=* | --x-include=* | --x-includ=* | --x-inclu=* \ + | --x-incl=* | --x-inc=* | --x-in=* | --x-i=*) + x_includes=$ac_optarg ;; + + -x-libraries | --x-libraries | --x-librarie | --x-librari \ + | --x-librar | --x-libra | --x-libr | --x-lib | --x-li | --x-l) + ac_prev=x_libraries ;; + -x-libraries=* | --x-libraries=* | --x-librarie=* | --x-librari=* \ + | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*) + x_libraries=$ac_optarg ;; + + -*) as_fn_error $? "unrecognized option: \`$ac_option' +Try \`$0 --help' for more information" + ;; + + *=*) + ac_envvar=`expr "x$ac_option" : 'x\([^=]*\)='` + # Reject names that are not valid shell variable names. + case $ac_envvar in #( + '' | [0-9]* | *[!_$as_cr_alnum]* ) + as_fn_error $? "invalid variable name: \`$ac_envvar'" ;; + esac + eval $ac_envvar=\$ac_optarg + export $ac_envvar ;; + + *) + # FIXME: should be removed in autoconf 3.0. + $as_echo "$as_me: WARNING: you should use --build, --host, --target" >&2 + expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null && + $as_echo "$as_me: WARNING: invalid host type: $ac_option" >&2 + : "${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}" + ;; + + esac +done + +if test -n "$ac_prev"; then + ac_option=--`echo $ac_prev | sed 's/_/-/g'` + as_fn_error $? "missing argument to $ac_option" +fi + +if test -n "$ac_unrecognized_opts"; then + case $enable_option_checking in + no) ;; + fatal) as_fn_error $? "unrecognized options: $ac_unrecognized_opts" ;; + *) $as_echo "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; + esac +fi + +# Check all directory arguments for consistency. +for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \ + datadir sysconfdir sharedstatedir localstatedir includedir \ + oldincludedir docdir infodir htmldir dvidir pdfdir psdir \ + libdir localedir mandir runstatedir +do + eval ac_val=\$$ac_var + # Remove trailing slashes. + case $ac_val in + */ ) + ac_val=`expr "X$ac_val" : 'X\(.*[^/]\)' \| "X$ac_val" : 'X\(.*\)'` + eval $ac_var=\$ac_val;; + esac + # Be sure to have absolute directory names. + case $ac_val in + [\\/$]* | ?:[\\/]* ) continue;; + NONE | '' ) case $ac_var in *prefix ) continue;; esac;; + esac + as_fn_error $? "expected an absolute directory name for --$ac_var: $ac_val" +done + +# There might be people who depend on the old broken behavior: `$host' +# used to hold the argument of --host etc. +# FIXME: To remove some day. +build=$build_alias +host=$host_alias +target=$target_alias + +# FIXME: To remove some day. +if test "x$host_alias" != x; then + if test "x$build_alias" = x; then + cross_compiling=maybe + elif test "x$build_alias" != "x$host_alias"; then + cross_compiling=yes + fi +fi + +ac_tool_prefix= +test -n "$host_alias" && ac_tool_prefix=$host_alias- + +test "$silent" = yes && exec 6>/dev/null + + +ac_pwd=`pwd` && test -n "$ac_pwd" && +ac_ls_di=`ls -di .` && +ac_pwd_ls_di=`cd "$ac_pwd" && ls -di .` || + as_fn_error $? "working directory cannot be determined" +test "X$ac_ls_di" = "X$ac_pwd_ls_di" || + as_fn_error $? "pwd does not report name of working directory" + + +# Find the source files, if location was not specified. +if test -z "$srcdir"; then + ac_srcdir_defaulted=yes + # Try the directory containing this script, then the parent directory. + ac_confdir=`$as_dirname -- "$as_myself" || +$as_expr X"$as_myself" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ + X"$as_myself" : 'X\(//\)[^/]' \| \ + X"$as_myself" : 'X\(//\)$' \| \ + X"$as_myself" : 'X\(/\)' \| . 2>/dev/null || +$as_echo X"$as_myself" | + sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ + s//\1/ + q + } + /^X\(\/\/\)[^/].*/{ + s//\1/ + q + } + /^X\(\/\/\)$/{ + s//\1/ + q + } + /^X\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + srcdir=$ac_confdir + if test ! -r "$srcdir/$ac_unique_file"; then + srcdir=.. + fi +else + ac_srcdir_defaulted=no +fi +if test ! -r "$srcdir/$ac_unique_file"; then + test "$ac_srcdir_defaulted" = yes && srcdir="$ac_confdir or .." + as_fn_error $? "cannot find sources ($ac_unique_file) in $srcdir" +fi +ac_msg="sources are in $srcdir, but \`cd $srcdir' does not work" +ac_abs_confdir=`( + cd "$srcdir" && test -r "./$ac_unique_file" || as_fn_error $? "$ac_msg" + pwd)` +# When building in place, set srcdir=. +if test "$ac_abs_confdir" = "$ac_pwd"; then + srcdir=. +fi +# Remove unnecessary trailing slashes from srcdir. +# Double slashes in file names in object file debugging info +# mess up M-x gdb in Emacs. +case $srcdir in +*/) srcdir=`expr "X$srcdir" : 'X\(.*[^/]\)' \| "X$srcdir" : 'X\(.*\)'`;; +esac +for ac_var in $ac_precious_vars; do + eval ac_env_${ac_var}_set=\${${ac_var}+set} + eval ac_env_${ac_var}_value=\$${ac_var} + eval ac_cv_env_${ac_var}_set=\${${ac_var}+set} + eval ac_cv_env_${ac_var}_value=\$${ac_var} +done + +# +# Report the --help message. +# +if test "$ac_init_help" = "long"; then + # Omit some internal or obsolete options to make the list less imposing. + # This message is too long to be a string in the A/UX 3.1 sh. + cat <<_ACEOF +\`configure' configures libev 4.33 to adapt to many kinds of systems. + +Usage: $0 [OPTION]... [VAR=VALUE]... + +To assign environment variables (e.g., CC, CFLAGS...), specify them as +VAR=VALUE. See below for descriptions of some of the useful variables. + +Defaults for the options are specified in brackets. + +Configuration: + -h, --help display this help and exit + --help=short display options specific to this package + --help=recursive display the short help of all the included packages + -V, --version display version information and exit + -q, --quiet, --silent do not print \`checking ...' messages + --cache-file=FILE cache test results in FILE [disabled] + -C, --config-cache alias for \`--cache-file=config.cache' + -n, --no-create do not create output files + --srcdir=DIR find the sources in DIR [configure dir or \`..'] + +Installation directories: + --prefix=PREFIX install architecture-independent files in PREFIX + [$ac_default_prefix] + --exec-prefix=EPREFIX install architecture-dependent files in EPREFIX + [PREFIX] + +By default, \`make install' will install all the files in +\`$ac_default_prefix/bin', \`$ac_default_prefix/lib' etc. You can specify +an installation prefix other than \`$ac_default_prefix' using \`--prefix', +for instance \`--prefix=\$HOME'. + +For better control, use the options below. + +Fine tuning of the installation directories: + --bindir=DIR user executables [EPREFIX/bin] + --sbindir=DIR system admin executables [EPREFIX/sbin] + --libexecdir=DIR program executables [EPREFIX/libexec] + --sysconfdir=DIR read-only single-machine data [PREFIX/etc] + --sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com] + --localstatedir=DIR modifiable single-machine data [PREFIX/var] + --runstatedir=DIR modifiable per-process data [LOCALSTATEDIR/run] + --libdir=DIR object code libraries [EPREFIX/lib] + --includedir=DIR C header files [PREFIX/include] + --oldincludedir=DIR C header files for non-gcc [/usr/include] + --datarootdir=DIR read-only arch.-independent data root [PREFIX/share] + --datadir=DIR read-only architecture-independent data [DATAROOTDIR] + --infodir=DIR info documentation [DATAROOTDIR/info] + --localedir=DIR locale-dependent data [DATAROOTDIR/locale] + --mandir=DIR man documentation [DATAROOTDIR/man] + --docdir=DIR documentation root [DATAROOTDIR/doc/libev] + --htmldir=DIR html documentation [DOCDIR] + --dvidir=DIR dvi documentation [DOCDIR] + --pdfdir=DIR pdf documentation [DOCDIR] + --psdir=DIR ps documentation [DOCDIR] +_ACEOF + + cat <<\_ACEOF + +Program names: + --program-prefix=PREFIX prepend PREFIX to installed program names + --program-suffix=SUFFIX append SUFFIX to installed program names + --program-transform-name=PROGRAM run sed PROGRAM on installed program names + +System types: + --build=BUILD configure for building on BUILD [guessed] + --host=HOST cross-compile to build programs to run on HOST [BUILD] +_ACEOF +fi + +if test -n "$ac_init_help"; then + case $ac_init_help in + short | recursive ) echo "Configuration of libev 4.33:";; + esac + cat <<\_ACEOF + +Optional Features: + --disable-option-checking ignore unrecognized --enable/--with options + --disable-FEATURE do not include FEATURE (same as --enable-FEATURE=no) + --enable-FEATURE[=ARG] include FEATURE [ARG=yes] + --enable-silent-rules less verbose build output (undo: "make V=1") + --disable-silent-rules verbose build output (undo: "make V=0") + --enable-maintainer-mode + enable make rules and dependencies not useful (and + sometimes confusing) to the casual installer + --enable-dependency-tracking + do not reject slow dependency extractors + --disable-dependency-tracking + speeds up one-time build + --enable-shared[=PKGS] build shared libraries [default=yes] + --enable-static[=PKGS] build static libraries [default=yes] + --enable-fast-install[=PKGS] + optimize for fast installation [default=yes] + --disable-libtool-lock avoid locking (might break parallel builds) + +Optional Packages: + --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] + --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no) + --with-pic[=PKGS] try to use only PIC/non-PIC objects [default=use + both] + --with-aix-soname=aix|svr4|both + shared library versioning (aka "SONAME") variant to + provide on AIX, [default=aix]. + --with-gnu-ld assume the C compiler uses GNU ld [default=no] + --with-sysroot[=DIR] Search for dependent libraries within DIR (or the + compiler's sysroot if not specified). + +Some influential environment variables: + CC C compiler command + CFLAGS C compiler flags + LDFLAGS linker flags, e.g. -L if you have libraries in a + nonstandard directory + LIBS libraries to pass to the linker, e.g. -l + CPPFLAGS (Objective) C/C++ preprocessor flags, e.g. -I if + you have headers in a nonstandard directory + LT_SYS_LIBRARY_PATH + User-defined run-time library search path. + CPP C preprocessor + +Use these variables to override the choices made by `configure' or to help +it to find libraries and programs with nonstandard names/locations. + +Report bugs to the package provider. +_ACEOF +ac_status=$? +fi + +if test "$ac_init_help" = "recursive"; then + # If there are subdirs, report their specific --help. + for ac_dir in : $ac_subdirs_all; do test "x$ac_dir" = x: && continue + test -d "$ac_dir" || + { cd "$srcdir" && ac_pwd=`pwd` && srcdir=. && test -d "$ac_dir"; } || + continue + ac_builddir=. + +case "$ac_dir" in +.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; +*) + ac_dir_suffix=/`$as_echo "$ac_dir" | sed 's|^\.[\\/]||'` + # A ".." for each directory in $ac_dir_suffix. + ac_top_builddir_sub=`$as_echo "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + case $ac_top_builddir_sub in + "") ac_top_builddir_sub=. ac_top_build_prefix= ;; + *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; + esac ;; +esac +ac_abs_top_builddir=$ac_pwd +ac_abs_builddir=$ac_pwd$ac_dir_suffix +# for backward compatibility: +ac_top_builddir=$ac_top_build_prefix + +case $srcdir in + .) # We are building in place. + ac_srcdir=. + ac_top_srcdir=$ac_top_builddir_sub + ac_abs_top_srcdir=$ac_pwd ;; + [\\/]* | ?:[\\/]* ) # Absolute name. + ac_srcdir=$srcdir$ac_dir_suffix; + ac_top_srcdir=$srcdir + ac_abs_top_srcdir=$srcdir ;; + *) # Relative name. + ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix + ac_top_srcdir=$ac_top_build_prefix$srcdir + ac_abs_top_srcdir=$ac_pwd/$srcdir ;; +esac +ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix + + cd "$ac_dir" || { ac_status=$?; continue; } + # Check for guested configure. + if test -f "$ac_srcdir/configure.gnu"; then + echo && + $SHELL "$ac_srcdir/configure.gnu" --help=recursive + elif test -f "$ac_srcdir/configure"; then + echo && + $SHELL "$ac_srcdir/configure" --help=recursive + else + $as_echo "$as_me: WARNING: no configuration information is in $ac_dir" >&2 + fi || ac_status=$? + cd "$ac_pwd" || { ac_status=$?; break; } + done +fi + +test -n "$ac_init_help" && exit $ac_status +if $ac_init_version; then + cat <<\_ACEOF +libev configure 4.33 +generated by GNU Autoconf 2.69 + +Copyright (C) 2012 Free Software Foundation, Inc. +This configure script is free software; the Free Software Foundation +gives unlimited permission to copy, distribute and modify it. +_ACEOF + exit +fi + +## ------------------------ ## +## Autoconf initialization. ## +## ------------------------ ## + +# ac_fn_c_try_compile LINENO +# -------------------------- +# Try to compile conftest.$ac_ext, and return whether this succeeded. +ac_fn_c_try_compile () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + rm -f conftest.$ac_objext + if { { ac_try="$ac_compile" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_compile") 2>conftest.err + ac_status=$? + if test -s conftest.err; then + grep -v '^ *+' conftest.err >conftest.er1 + cat conftest.er1 >&5 + mv -f conftest.er1 conftest.err + fi + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } && { + test -z "$ac_c_werror_flag" || + test ! -s conftest.err + } && test -s conftest.$ac_objext; then : + ac_retval=0 +else + $as_echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + + ac_retval=1 +fi + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + as_fn_set_status $ac_retval + +} # ac_fn_c_try_compile + +# ac_fn_c_try_link LINENO +# ----------------------- +# Try to link conftest.$ac_ext, and return whether this succeeded. +ac_fn_c_try_link () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + rm -f conftest.$ac_objext conftest$ac_exeext + if { { ac_try="$ac_link" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_link") 2>conftest.err + ac_status=$? + if test -s conftest.err; then + grep -v '^ *+' conftest.err >conftest.er1 + cat conftest.er1 >&5 + mv -f conftest.er1 conftest.err + fi + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } && { + test -z "$ac_c_werror_flag" || + test ! -s conftest.err + } && test -s conftest$ac_exeext && { + test "$cross_compiling" = yes || + test -x conftest$ac_exeext + }; then : + ac_retval=0 +else + $as_echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + + ac_retval=1 +fi + # Delete the IPA/IPO (Inter Procedural Analysis/Optimization) information + # created by the PGI compiler (conftest_ipa8_conftest.oo), as it would + # interfere with the next link command; also delete a directory that is + # left behind by Apple's compiler. We do this before executing the actions. + rm -rf conftest.dSYM conftest_ipa8_conftest.oo + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + as_fn_set_status $ac_retval + +} # ac_fn_c_try_link + +# ac_fn_c_check_header_compile LINENO HEADER VAR INCLUDES +# ------------------------------------------------------- +# Tests whether HEADER exists and can be compiled using the include files in +# INCLUDES, setting the cache variable VAR accordingly. +ac_fn_c_check_header_compile () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 +$as_echo_n "checking for $2... " >&6; } +if eval \${$3+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$4 +#include <$2> +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + eval "$3=yes" +else + eval "$3=no" +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +fi +eval ac_res=\$$3 + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +$as_echo "$ac_res" >&6; } + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + +} # ac_fn_c_check_header_compile + +# ac_fn_c_try_cpp LINENO +# ---------------------- +# Try to preprocess conftest.$ac_ext, and return whether this succeeded. +ac_fn_c_try_cpp () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + if { { ac_try="$ac_cpp conftest.$ac_ext" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_cpp conftest.$ac_ext") 2>conftest.err + ac_status=$? + if test -s conftest.err; then + grep -v '^ *+' conftest.err >conftest.er1 + cat conftest.er1 >&5 + mv -f conftest.er1 conftest.err + fi + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } > conftest.i && { + test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" || + test ! -s conftest.err + }; then : + ac_retval=0 +else + $as_echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + + ac_retval=1 +fi + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + as_fn_set_status $ac_retval + +} # ac_fn_c_try_cpp + +# ac_fn_c_try_run LINENO +# ---------------------- +# Try to link conftest.$ac_ext, and return whether this succeeded. Assumes +# that executables *can* be run. +ac_fn_c_try_run () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + if { { ac_try="$ac_link" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_link") 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } && { ac_try='./conftest$ac_exeext' + { { case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_try") 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; }; then : + ac_retval=0 +else + $as_echo "$as_me: program exited with status $ac_status" >&5 + $as_echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + + ac_retval=$ac_status +fi + rm -rf conftest.dSYM conftest_ipa8_conftest.oo + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + as_fn_set_status $ac_retval + +} # ac_fn_c_try_run + +# ac_fn_c_check_func LINENO FUNC VAR +# ---------------------------------- +# Tests whether FUNC exists, setting the cache variable VAR accordingly +ac_fn_c_check_func () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 +$as_echo_n "checking for $2... " >&6; } +if eval \${$3+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +/* Define $2 to an innocuous variant, in case declares $2. + For example, HP-UX 11i declares gettimeofday. */ +#define $2 innocuous_$2 + +/* System header to define __stub macros and hopefully few prototypes, + which can conflict with char $2 (); below. + Prefer to if __STDC__ is defined, since + exists even on freestanding compilers. */ + +#ifdef __STDC__ +# include +#else +# include +#endif + +#undef $2 + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char $2 (); +/* The GNU C library defines this for functions which it implements + to always fail with ENOSYS. Some functions are actually named + something starting with __ and the normal name is an alias. */ +#if defined __stub_$2 || defined __stub___$2 +choke me +#endif + +int +main () +{ +return $2 (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + eval "$3=yes" +else + eval "$3=no" +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +fi +eval ac_res=\$$3 + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +$as_echo "$ac_res" >&6; } + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + +} # ac_fn_c_check_func + +# ac_fn_c_check_header_mongrel LINENO HEADER VAR INCLUDES +# ------------------------------------------------------- +# Tests whether HEADER exists, giving a warning if it cannot be compiled using +# the include files in INCLUDES and setting the cache variable VAR +# accordingly. +ac_fn_c_check_header_mongrel () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + if eval \${$3+:} false; then : + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 +$as_echo_n "checking for $2... " >&6; } +if eval \${$3+:} false; then : + $as_echo_n "(cached) " >&6 +fi +eval ac_res=\$$3 + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +$as_echo "$ac_res" >&6; } +else + # Is the header compilable? +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking $2 usability" >&5 +$as_echo_n "checking $2 usability... " >&6; } +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$4 +#include <$2> +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + ac_header_compiler=yes +else + ac_header_compiler=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_header_compiler" >&5 +$as_echo "$ac_header_compiler" >&6; } + +# Is the header present? +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking $2 presence" >&5 +$as_echo_n "checking $2 presence... " >&6; } +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include <$2> +_ACEOF +if ac_fn_c_try_cpp "$LINENO"; then : + ac_header_preproc=yes +else + ac_header_preproc=no +fi +rm -f conftest.err conftest.i conftest.$ac_ext +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_header_preproc" >&5 +$as_echo "$ac_header_preproc" >&6; } + +# So? What about this header? +case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in #(( + yes:no: ) + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: accepted by the compiler, rejected by the preprocessor!" >&5 +$as_echo "$as_me: WARNING: $2: accepted by the compiler, rejected by the preprocessor!" >&2;} + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: proceeding with the compiler's result" >&5 +$as_echo "$as_me: WARNING: $2: proceeding with the compiler's result" >&2;} + ;; + no:yes:* ) + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: present but cannot be compiled" >&5 +$as_echo "$as_me: WARNING: $2: present but cannot be compiled" >&2;} + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: check for missing prerequisite headers?" >&5 +$as_echo "$as_me: WARNING: $2: check for missing prerequisite headers?" >&2;} + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: see the Autoconf documentation" >&5 +$as_echo "$as_me: WARNING: $2: see the Autoconf documentation" >&2;} + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: section \"Present But Cannot Be Compiled\"" >&5 +$as_echo "$as_me: WARNING: $2: section \"Present But Cannot Be Compiled\"" >&2;} + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: proceeding with the compiler's result" >&5 +$as_echo "$as_me: WARNING: $2: proceeding with the compiler's result" >&2;} + ;; +esac + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 +$as_echo_n "checking for $2... " >&6; } +if eval \${$3+:} false; then : + $as_echo_n "(cached) " >&6 +else + eval "$3=\$ac_header_compiler" +fi +eval ac_res=\$$3 + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +$as_echo "$ac_res" >&6; } +fi + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + +} # ac_fn_c_check_header_mongrel + +# ac_fn_c_check_type LINENO TYPE VAR INCLUDES +# ------------------------------------------- +# Tests whether TYPE exists after having included INCLUDES, setting cache +# variable VAR accordingly. +ac_fn_c_check_type () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 +$as_echo_n "checking for $2... " >&6; } +if eval \${$3+:} false; then : + $as_echo_n "(cached) " >&6 +else + eval "$3=no" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$4 +int +main () +{ +if (sizeof ($2)) + return 0; + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$4 +int +main () +{ +if (sizeof (($2))) + return 0; + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + +else + eval "$3=yes" +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +fi +eval ac_res=\$$3 + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +$as_echo "$ac_res" >&6; } + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + +} # ac_fn_c_check_type +cat >config.log <<_ACEOF +This file contains any messages produced by compilers while +running configure, to aid debugging if configure makes a mistake. + +It was created by libev $as_me 4.33, which was +generated by GNU Autoconf 2.69. Invocation command line was + + $ $0 $@ + +_ACEOF +exec 5>>config.log +{ +cat <<_ASUNAME +## --------- ## +## Platform. ## +## --------- ## + +hostname = `(hostname || uname -n) 2>/dev/null | sed 1q` +uname -m = `(uname -m) 2>/dev/null || echo unknown` +uname -r = `(uname -r) 2>/dev/null || echo unknown` +uname -s = `(uname -s) 2>/dev/null || echo unknown` +uname -v = `(uname -v) 2>/dev/null || echo unknown` + +/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null || echo unknown` +/bin/uname -X = `(/bin/uname -X) 2>/dev/null || echo unknown` + +/bin/arch = `(/bin/arch) 2>/dev/null || echo unknown` +/usr/bin/arch -k = `(/usr/bin/arch -k) 2>/dev/null || echo unknown` +/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null || echo unknown` +/usr/bin/hostinfo = `(/usr/bin/hostinfo) 2>/dev/null || echo unknown` +/bin/machine = `(/bin/machine) 2>/dev/null || echo unknown` +/usr/bin/oslevel = `(/usr/bin/oslevel) 2>/dev/null || echo unknown` +/bin/universe = `(/bin/universe) 2>/dev/null || echo unknown` + +_ASUNAME + +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + $as_echo "PATH: $as_dir" + done +IFS=$as_save_IFS + +} >&5 + +cat >&5 <<_ACEOF + + +## ----------- ## +## Core tests. ## +## ----------- ## + +_ACEOF + + +# Keep a trace of the command line. +# Strip out --no-create and --no-recursion so they do not pile up. +# Strip out --silent because we don't want to record it for future runs. +# Also quote any args containing shell meta-characters. +# Make two passes to allow for proper duplicate-argument suppression. +ac_configure_args= +ac_configure_args0= +ac_configure_args1= +ac_must_keep_next=false +for ac_pass in 1 2 +do + for ac_arg + do + case $ac_arg in + -no-create | --no-c* | -n | -no-recursion | --no-r*) continue ;; + -q | -quiet | --quiet | --quie | --qui | --qu | --q \ + | -silent | --silent | --silen | --sile | --sil) + continue ;; + *\'*) + ac_arg=`$as_echo "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + esac + case $ac_pass in + 1) as_fn_append ac_configure_args0 " '$ac_arg'" ;; + 2) + as_fn_append ac_configure_args1 " '$ac_arg'" + if test $ac_must_keep_next = true; then + ac_must_keep_next=false # Got value, back to normal. + else + case $ac_arg in + *=* | --config-cache | -C | -disable-* | --disable-* \ + | -enable-* | --enable-* | -gas | --g* | -nfp | --nf* \ + | -q | -quiet | --q* | -silent | --sil* | -v | -verb* \ + | -with-* | --with-* | -without-* | --without-* | --x) + case "$ac_configure_args0 " in + "$ac_configure_args1"*" '$ac_arg' "* ) continue ;; + esac + ;; + -* ) ac_must_keep_next=true ;; + esac + fi + as_fn_append ac_configure_args " '$ac_arg'" + ;; + esac + done +done +{ ac_configure_args0=; unset ac_configure_args0;} +{ ac_configure_args1=; unset ac_configure_args1;} + +# When interrupted or exit'd, cleanup temporary files, and complete +# config.log. We remove comments because anyway the quotes in there +# would cause problems or look ugly. +# WARNING: Use '\'' to represent an apostrophe within the trap. +# WARNING: Do not start the trap code with a newline, due to a FreeBSD 4.0 bug. +trap 'exit_status=$? + # Save into config.log some information that might help in debugging. + { + echo + + $as_echo "## ---------------- ## +## Cache variables. ## +## ---------------- ##" + echo + # The following way of writing the cache mishandles newlines in values, +( + for ac_var in `(set) 2>&1 | sed -n '\''s/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'\''`; do + eval ac_val=\$$ac_var + case $ac_val in #( + *${as_nl}*) + case $ac_var in #( + *_cv_*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 +$as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; + esac + case $ac_var in #( + _ | IFS | as_nl) ;; #( + BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( + *) { eval $ac_var=; unset $ac_var;} ;; + esac ;; + esac + done + (set) 2>&1 | + case $as_nl`(ac_space='\'' '\''; set) 2>&1` in #( + *${as_nl}ac_space=\ *) + sed -n \ + "s/'\''/'\''\\\\'\'''\''/g; + s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\''\\2'\''/p" + ;; #( + *) + sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" + ;; + esac | + sort +) + echo + + $as_echo "## ----------------- ## +## Output variables. ## +## ----------------- ##" + echo + for ac_var in $ac_subst_vars + do + eval ac_val=\$$ac_var + case $ac_val in + *\'\''*) ac_val=`$as_echo "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; + esac + $as_echo "$ac_var='\''$ac_val'\''" + done | sort + echo + + if test -n "$ac_subst_files"; then + $as_echo "## ------------------- ## +## File substitutions. ## +## ------------------- ##" + echo + for ac_var in $ac_subst_files + do + eval ac_val=\$$ac_var + case $ac_val in + *\'\''*) ac_val=`$as_echo "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; + esac + $as_echo "$ac_var='\''$ac_val'\''" + done | sort + echo + fi + + if test -s confdefs.h; then + $as_echo "## ----------- ## +## confdefs.h. ## +## ----------- ##" + echo + cat confdefs.h + echo + fi + test "$ac_signal" != 0 && + $as_echo "$as_me: caught signal $ac_signal" + $as_echo "$as_me: exit $exit_status" + } >&5 + rm -f core *.core core.conftest.* && + rm -f -r conftest* confdefs* conf$$* $ac_clean_files && + exit $exit_status +' 0 +for ac_signal in 1 2 13 15; do + trap 'ac_signal='$ac_signal'; as_fn_exit 1' $ac_signal +done +ac_signal=0 + +# confdefs.h avoids OS command line length limits that DEFS can exceed. +rm -f -r conftest* confdefs.h + +$as_echo "/* confdefs.h */" > confdefs.h + +# Predefined preprocessor variables. + +cat >>confdefs.h <<_ACEOF +#define PACKAGE_NAME "$PACKAGE_NAME" +_ACEOF + +cat >>confdefs.h <<_ACEOF +#define PACKAGE_TARNAME "$PACKAGE_TARNAME" +_ACEOF + +cat >>confdefs.h <<_ACEOF +#define PACKAGE_VERSION "$PACKAGE_VERSION" +_ACEOF + +cat >>confdefs.h <<_ACEOF +#define PACKAGE_STRING "$PACKAGE_STRING" +_ACEOF + +cat >>confdefs.h <<_ACEOF +#define PACKAGE_BUGREPORT "$PACKAGE_BUGREPORT" +_ACEOF + +cat >>confdefs.h <<_ACEOF +#define PACKAGE_URL "$PACKAGE_URL" +_ACEOF + + +# Let the site file select an alternate cache file if it wants to. +# Prefer an explicitly selected file to automatically selected ones. +ac_site_file1=NONE +ac_site_file2=NONE +if test -n "$CONFIG_SITE"; then + # We do not want a PATH search for config.site. + case $CONFIG_SITE in #(( + -*) ac_site_file1=./$CONFIG_SITE;; + */*) ac_site_file1=$CONFIG_SITE;; + *) ac_site_file1=./$CONFIG_SITE;; + esac +elif test "x$prefix" != xNONE; then + ac_site_file1=$prefix/share/config.site + ac_site_file2=$prefix/etc/config.site +else + ac_site_file1=$ac_default_prefix/share/config.site + ac_site_file2=$ac_default_prefix/etc/config.site +fi +for ac_site_file in "$ac_site_file1" "$ac_site_file2" +do + test "x$ac_site_file" = xNONE && continue + if test /dev/null != "$ac_site_file" && test -r "$ac_site_file"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 +$as_echo "$as_me: loading site script $ac_site_file" >&6;} + sed 's/^/| /' "$ac_site_file" >&5 + . "$ac_site_file" \ + || { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "failed to load site script $ac_site_file +See \`config.log' for more details" "$LINENO" 5; } + fi +done + +if test -r "$cache_file"; then + # Some versions of bash will fail to source /dev/null (special files + # actually), so we avoid doing that. DJGPP emulates it as a regular file. + if test /dev/null != "$cache_file" && test -f "$cache_file"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 +$as_echo "$as_me: loading cache $cache_file" >&6;} + case $cache_file in + [\\/]* | ?:[\\/]* ) . "$cache_file";; + *) . "./$cache_file";; + esac + fi +else + { $as_echo "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 +$as_echo "$as_me: creating cache $cache_file" >&6;} + >$cache_file +fi + +# Check that the precious variables saved in the cache have kept the same +# value. +ac_cache_corrupted=false +for ac_var in $ac_precious_vars; do + eval ac_old_set=\$ac_cv_env_${ac_var}_set + eval ac_new_set=\$ac_env_${ac_var}_set + eval ac_old_val=\$ac_cv_env_${ac_var}_value + eval ac_new_val=\$ac_env_${ac_var}_value + case $ac_old_set,$ac_new_set in + set,) + { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&5 +$as_echo "$as_me: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&2;} + ac_cache_corrupted=: ;; + ,set) + { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was not set in the previous run" >&5 +$as_echo "$as_me: error: \`$ac_var' was not set in the previous run" >&2;} + ac_cache_corrupted=: ;; + ,);; + *) + if test "x$ac_old_val" != "x$ac_new_val"; then + # differences in whitespace do not lead to failure. + ac_old_val_w=`echo x $ac_old_val` + ac_new_val_w=`echo x $ac_new_val` + if test "$ac_old_val_w" != "$ac_new_val_w"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' has changed since the previous run:" >&5 +$as_echo "$as_me: error: \`$ac_var' has changed since the previous run:" >&2;} + ac_cache_corrupted=: + else + { $as_echo "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&5 +$as_echo "$as_me: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&2;} + eval $ac_var=\$ac_old_val + fi + { $as_echo "$as_me:${as_lineno-$LINENO}: former value: \`$ac_old_val'" >&5 +$as_echo "$as_me: former value: \`$ac_old_val'" >&2;} + { $as_echo "$as_me:${as_lineno-$LINENO}: current value: \`$ac_new_val'" >&5 +$as_echo "$as_me: current value: \`$ac_new_val'" >&2;} + fi;; + esac + # Pass precious variables to config.status. + if test "$ac_new_set" = set; then + case $ac_new_val in + *\'*) ac_arg=$ac_var=`$as_echo "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;; + *) ac_arg=$ac_var=$ac_new_val ;; + esac + case " $ac_configure_args " in + *" '$ac_arg' "*) ;; # Avoid dups. Use of quotes ensures accuracy. + *) as_fn_append ac_configure_args " '$ac_arg'" ;; + esac + fi +done +if $ac_cache_corrupted; then + { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} + { $as_echo "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 +$as_echo "$as_me: error: changes in the environment can compromise the build" >&2;} + as_fn_error $? "run \`make distclean' and/or \`rm $cache_file' and start over" "$LINENO" 5 +fi +## -------------------- ## +## Main body of script. ## +## -------------------- ## + +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + + + +orig_CFLAGS="$CFLAGS" + + +am__api_version='1.16' + +ac_aux_dir= +for ac_dir in "$srcdir" "$srcdir/.." "$srcdir/../.."; do + if test -f "$ac_dir/install-sh"; then + ac_aux_dir=$ac_dir + ac_install_sh="$ac_aux_dir/install-sh -c" + break + elif test -f "$ac_dir/install.sh"; then + ac_aux_dir=$ac_dir + ac_install_sh="$ac_aux_dir/install.sh -c" + break + elif test -f "$ac_dir/shtool"; then + ac_aux_dir=$ac_dir + ac_install_sh="$ac_aux_dir/shtool install -c" + break + fi +done +if test -z "$ac_aux_dir"; then + as_fn_error $? "cannot find install-sh, install.sh, or shtool in \"$srcdir\" \"$srcdir/..\" \"$srcdir/../..\"" "$LINENO" 5 +fi + +# These three variables are undocumented and unsupported, +# and are intended to be withdrawn in a future Autoconf release. +# They can cause serious problems if a builder's source tree is in a directory +# whose full name contains unusual characters. +ac_config_guess="$SHELL $ac_aux_dir/config.guess" # Please don't use this var. +ac_config_sub="$SHELL $ac_aux_dir/config.sub" # Please don't use this var. +ac_configure="$SHELL $ac_aux_dir/configure" # Please don't use this var. + + +# Find a good install program. We prefer a C program (faster), +# so one script is as good as another. But avoid the broken or +# incompatible versions: +# SysV /etc/install, /usr/sbin/install +# SunOS /usr/etc/install +# IRIX /sbin/install +# AIX /bin/install +# AmigaOS /C/install, which installs bootblocks on floppy discs +# AIX 4 /usr/bin/installbsd, which doesn't work without a -g flag +# AFS /usr/afsws/bin/install, which mishandles nonexistent args +# SVR4 /usr/ucb/install, which tries to use the nonexistent group "staff" +# OS/2's system install, which has a completely different semantic +# ./install, which can be erroneously created by make from ./install.sh. +# Reject install programs that cannot install multiple files. +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5 +$as_echo_n "checking for a BSD-compatible install... " >&6; } +if test -z "$INSTALL"; then +if ${ac_cv_path_install+:} false; then : + $as_echo_n "(cached) " >&6 +else + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + # Account for people who put trailing slashes in PATH elements. +case $as_dir/ in #(( + ./ | .// | /[cC]/* | \ + /etc/* | /usr/sbin/* | /usr/etc/* | /sbin/* | /usr/afsws/bin/* | \ + ?:[\\/]os2[\\/]install[\\/]* | ?:[\\/]OS2[\\/]INSTALL[\\/]* | \ + /usr/ucb/* ) ;; + *) + # OSF1 and SCO ODT 3.0 have their own names for install. + # Don't use installbsd from OSF since it installs stuff as root + # by default. + for ac_prog in ginstall scoinst install; do + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_prog$ac_exec_ext"; then + if test $ac_prog = install && + grep dspmsg "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then + # AIX install. It has an incompatible calling convention. + : + elif test $ac_prog = install && + grep pwplus "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then + # program-specific install script used by HP pwplus--don't use. + : + else + rm -rf conftest.one conftest.two conftest.dir + echo one > conftest.one + echo two > conftest.two + mkdir conftest.dir + if "$as_dir/$ac_prog$ac_exec_ext" -c conftest.one conftest.two "`pwd`/conftest.dir" && + test -s conftest.one && test -s conftest.two && + test -s conftest.dir/conftest.one && + test -s conftest.dir/conftest.two + then + ac_cv_path_install="$as_dir/$ac_prog$ac_exec_ext -c" + break 3 + fi + fi + fi + done + done + ;; +esac + + done +IFS=$as_save_IFS + +rm -rf conftest.one conftest.two conftest.dir + +fi + if test "${ac_cv_path_install+set}" = set; then + INSTALL=$ac_cv_path_install + else + # As a last resort, use the slow shell script. Don't cache a + # value for INSTALL within a source directory, because that will + # break other packages using the cache if that directory is + # removed, or if the value is a relative name. + INSTALL=$ac_install_sh + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $INSTALL" >&5 +$as_echo "$INSTALL" >&6; } + +# Use test -z because SunOS4 sh mishandles braces in ${var-val}. +# It thinks the first close brace ends the variable substitution. +test -z "$INSTALL_PROGRAM" && INSTALL_PROGRAM='${INSTALL}' + +test -z "$INSTALL_SCRIPT" && INSTALL_SCRIPT='${INSTALL}' + +test -z "$INSTALL_DATA" && INSTALL_DATA='${INSTALL} -m 644' + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether build environment is sane" >&5 +$as_echo_n "checking whether build environment is sane... " >&6; } +# Reject unsafe characters in $srcdir or the absolute working directory +# name. Accept space and tab only in the latter. +am_lf=' +' +case `pwd` in + *[\\\"\#\$\&\'\`$am_lf]*) + as_fn_error $? "unsafe absolute working directory name" "$LINENO" 5;; +esac +case $srcdir in + *[\\\"\#\$\&\'\`$am_lf\ \ ]*) + as_fn_error $? "unsafe srcdir value: '$srcdir'" "$LINENO" 5;; +esac + +# Do 'set' in a subshell so we don't clobber the current shell's +# arguments. Must try -L first in case configure is actually a +# symlink; some systems play weird games with the mod time of symlinks +# (eg FreeBSD returns the mod time of the symlink's containing +# directory). +if ( + am_has_slept=no + for am_try in 1 2; do + echo "timestamp, slept: $am_has_slept" > conftest.file + set X `ls -Lt "$srcdir/configure" conftest.file 2> /dev/null` + if test "$*" = "X"; then + # -L didn't work. + set X `ls -t "$srcdir/configure" conftest.file` + fi + if test "$*" != "X $srcdir/configure conftest.file" \ + && test "$*" != "X conftest.file $srcdir/configure"; then + + # If neither matched, then we have a broken ls. This can happen + # if, for instance, CONFIG_SHELL is bash and it inherits a + # broken ls alias from the environment. This has actually + # happened. Such a system could not be considered "sane". + as_fn_error $? "ls -t appears to fail. Make sure there is not a broken + alias in your environment" "$LINENO" 5 + fi + if test "$2" = conftest.file || test $am_try -eq 2; then + break + fi + # Just in case. + sleep 1 + am_has_slept=yes + done + test "$2" = conftest.file + ) +then + # Ok. + : +else + as_fn_error $? "newly created file is older than distributed files! +Check your system clock" "$LINENO" 5 +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +$as_echo "yes" >&6; } +# If we didn't sleep, we still need to ensure time stamps of config.status and +# generated files are strictly newer. +am_sleep_pid= +if grep 'slept: no' conftest.file >/dev/null 2>&1; then + ( sleep 1 ) & + am_sleep_pid=$! +fi + +rm -f conftest.file + +test "$program_prefix" != NONE && + program_transform_name="s&^&$program_prefix&;$program_transform_name" +# Use a double $ so make ignores it. +test "$program_suffix" != NONE && + program_transform_name="s&\$&$program_suffix&;$program_transform_name" +# Double any \ or $. +# By default was `s,x,x', remove it if useless. +ac_script='s/[\\$]/&&/g;s/;s,x,x,$//' +program_transform_name=`$as_echo "$program_transform_name" | sed "$ac_script"` + +# Expand $ac_aux_dir to an absolute path. +am_aux_dir=`cd "$ac_aux_dir" && pwd` + +if test x"${MISSING+set}" != xset; then + case $am_aux_dir in + *\ * | *\ *) + MISSING="\${SHELL} \"$am_aux_dir/missing\"" ;; + *) + MISSING="\${SHELL} $am_aux_dir/missing" ;; + esac +fi +# Use eval to expand $SHELL +if eval "$MISSING --is-lightweight"; then + am_missing_run="$MISSING " +else + am_missing_run= + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: 'missing' script is too old or missing" >&5 +$as_echo "$as_me: WARNING: 'missing' script is too old or missing" >&2;} +fi + +if test x"${install_sh+set}" != xset; then + case $am_aux_dir in + *\ * | *\ *) + install_sh="\${SHELL} '$am_aux_dir/install-sh'" ;; + *) + install_sh="\${SHELL} $am_aux_dir/install-sh" + esac +fi + +# Installed binaries are usually stripped using 'strip' when the user +# run "make install-strip". However 'strip' might not be the right +# tool to use in cross-compilation environments, therefore Automake +# will honor the 'STRIP' environment variable to overrule this program. +if test "$cross_compiling" != no; then + if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}strip", so it can be a program name with args. +set dummy ${ac_tool_prefix}strip; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_STRIP+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$STRIP"; then + ac_cv_prog_STRIP="$STRIP" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_STRIP="${ac_tool_prefix}strip" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +STRIP=$ac_cv_prog_STRIP +if test -n "$STRIP"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $STRIP" >&5 +$as_echo "$STRIP" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_STRIP"; then + ac_ct_STRIP=$STRIP + # Extract the first word of "strip", so it can be a program name with args. +set dummy strip; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_STRIP+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_STRIP"; then + ac_cv_prog_ac_ct_STRIP="$ac_ct_STRIP" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_STRIP="strip" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_STRIP=$ac_cv_prog_ac_ct_STRIP +if test -n "$ac_ct_STRIP"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_STRIP" >&5 +$as_echo "$ac_ct_STRIP" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_STRIP" = x; then + STRIP=":" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + STRIP=$ac_ct_STRIP + fi +else + STRIP="$ac_cv_prog_STRIP" +fi + +fi +INSTALL_STRIP_PROGRAM="\$(install_sh) -c -s" + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for a thread-safe mkdir -p" >&5 +$as_echo_n "checking for a thread-safe mkdir -p... " >&6; } +if test -z "$MKDIR_P"; then + if ${ac_cv_path_mkdir+:} false; then : + $as_echo_n "(cached) " >&6 +else + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH$PATH_SEPARATOR/opt/sfw/bin +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_prog in mkdir gmkdir; do + for ac_exec_ext in '' $ac_executable_extensions; do + as_fn_executable_p "$as_dir/$ac_prog$ac_exec_ext" || continue + case `"$as_dir/$ac_prog$ac_exec_ext" --version 2>&1` in #( + 'mkdir (GNU coreutils) '* | \ + 'mkdir (coreutils) '* | \ + 'mkdir (fileutils) '4.1*) + ac_cv_path_mkdir=$as_dir/$ac_prog$ac_exec_ext + break 3;; + esac + done + done + done +IFS=$as_save_IFS + +fi + + test -d ./--version && rmdir ./--version + if test "${ac_cv_path_mkdir+set}" = set; then + MKDIR_P="$ac_cv_path_mkdir -p" + else + # As a last resort, use the slow shell script. Don't cache a + # value for MKDIR_P within a source directory, because that will + # break other packages using the cache if that directory is + # removed, or if the value is a relative name. + MKDIR_P="$ac_install_sh -d" + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $MKDIR_P" >&5 +$as_echo "$MKDIR_P" >&6; } + +for ac_prog in gawk mawk nawk awk +do + # Extract the first word of "$ac_prog", so it can be a program name with args. +set dummy $ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_AWK+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$AWK"; then + ac_cv_prog_AWK="$AWK" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_AWK="$ac_prog" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +AWK=$ac_cv_prog_AWK +if test -n "$AWK"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $AWK" >&5 +$as_echo "$AWK" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + test -n "$AWK" && break +done + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${MAKE-make} sets \$(MAKE)" >&5 +$as_echo_n "checking whether ${MAKE-make} sets \$(MAKE)... " >&6; } +set x ${MAKE-make} +ac_make=`$as_echo "$2" | sed 's/+/p/g; s/[^a-zA-Z0-9_]/_/g'` +if eval \${ac_cv_prog_make_${ac_make}_set+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat >conftest.make <<\_ACEOF +SHELL = /bin/sh +all: + @echo '@@@%%%=$(MAKE)=@@@%%%' +_ACEOF +# GNU make sometimes prints "make[1]: Entering ...", which would confuse us. +case `${MAKE-make} -f conftest.make 2>/dev/null` in + *@@@%%%=?*=@@@%%%*) + eval ac_cv_prog_make_${ac_make}_set=yes;; + *) + eval ac_cv_prog_make_${ac_make}_set=no;; +esac +rm -f conftest.make +fi +if eval test \$ac_cv_prog_make_${ac_make}_set = yes; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +$as_echo "yes" >&6; } + SET_MAKE= +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } + SET_MAKE="MAKE=${MAKE-make}" +fi + +rm -rf .tst 2>/dev/null +mkdir .tst 2>/dev/null +if test -d .tst; then + am__leading_dot=. +else + am__leading_dot=_ +fi +rmdir .tst 2>/dev/null + +# Check whether --enable-silent-rules was given. +if test "${enable_silent_rules+set}" = set; then : + enableval=$enable_silent_rules; +fi + +case $enable_silent_rules in # ((( + yes) AM_DEFAULT_VERBOSITY=0;; + no) AM_DEFAULT_VERBOSITY=1;; + *) AM_DEFAULT_VERBOSITY=1;; +esac +am_make=${MAKE-make} +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether $am_make supports nested variables" >&5 +$as_echo_n "checking whether $am_make supports nested variables... " >&6; } +if ${am_cv_make_support_nested_variables+:} false; then : + $as_echo_n "(cached) " >&6 +else + if $as_echo 'TRUE=$(BAR$(V)) +BAR0=false +BAR1=true +V=1 +am__doit: + @$(TRUE) +.PHONY: am__doit' | $am_make -f - >/dev/null 2>&1; then + am_cv_make_support_nested_variables=yes +else + am_cv_make_support_nested_variables=no +fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $am_cv_make_support_nested_variables" >&5 +$as_echo "$am_cv_make_support_nested_variables" >&6; } +if test $am_cv_make_support_nested_variables = yes; then + AM_V='$(V)' + AM_DEFAULT_V='$(AM_DEFAULT_VERBOSITY)' +else + AM_V=$AM_DEFAULT_VERBOSITY + AM_DEFAULT_V=$AM_DEFAULT_VERBOSITY +fi +AM_BACKSLASH='\' + +if test "`cd $srcdir && pwd`" != "`pwd`"; then + # Use -I$(srcdir) only when $(srcdir) != ., so that make's output + # is not polluted with repeated "-I." + am__isrc=' -I$(srcdir)' + # test to see if srcdir already configured + if test -f $srcdir/config.status; then + as_fn_error $? "source directory already configured; run \"make distclean\" there first" "$LINENO" 5 + fi +fi + +# test whether we have cygpath +if test -z "$CYGPATH_W"; then + if (cygpath --version) >/dev/null 2>/dev/null; then + CYGPATH_W='cygpath -w' + else + CYGPATH_W=echo + fi +fi + + +# Define the identity of the package. + PACKAGE='libev' + VERSION='4.33' + + +cat >>confdefs.h <<_ACEOF +#define PACKAGE "$PACKAGE" +_ACEOF + + +cat >>confdefs.h <<_ACEOF +#define VERSION "$VERSION" +_ACEOF + +# Some tools Automake needs. + +ACLOCAL=${ACLOCAL-"${am_missing_run}aclocal-${am__api_version}"} + + +AUTOCONF=${AUTOCONF-"${am_missing_run}autoconf"} + + +AUTOMAKE=${AUTOMAKE-"${am_missing_run}automake-${am__api_version}"} + + +AUTOHEADER=${AUTOHEADER-"${am_missing_run}autoheader"} + + +MAKEINFO=${MAKEINFO-"${am_missing_run}makeinfo"} + +# For better backward compatibility. To be removed once Automake 1.9.x +# dies out for good. For more background, see: +# +# +mkdir_p='$(MKDIR_P)' + +# We need awk for the "check" target (and possibly the TAP driver). The +# system "awk" is bad on some platforms. +# Always define AMTAR for backward compatibility. Yes, it's still used +# in the wild :-( We should find a proper way to deprecate it ... +AMTAR='$${TAR-tar}' + + +# We'll loop over all known methods to create a tar archive until one works. +_am_tools='gnutar pax cpio none' + +am__tar='$${TAR-tar} chof - "$$tardir"' am__untar='$${TAR-tar} xf -' + + + + + + +# POSIX will say in a future version that running "rm -f" with no argument +# is OK; and we want to be able to make that assumption in our Makefile +# recipes. So use an aggressive probe to check that the usage we want is +# actually supported "in the wild" to an acceptable degree. +# See automake bug#10828. +# To make any issue more visible, cause the running configure to be aborted +# by default if the 'rm' program in use doesn't match our expectations; the +# user can still override this though. +if rm -f && rm -fr && rm -rf; then : OK; else + cat >&2 <<'END' +Oops! + +Your 'rm' program seems unable to run without file operands specified +on the command line, even when the '-f' option is present. This is contrary +to the behaviour of most rm programs out there, and not conforming with +the upcoming POSIX standard: + +Please tell bug-automake@gnu.org about your system, including the value +of your $PATH and any error possibly output before this message. This +can help us improve future automake versions. + +END + if test x"$ACCEPT_INFERIOR_RM_PROGRAM" = x"yes"; then + echo 'Configuration will proceed anyway, since you have set the' >&2 + echo 'ACCEPT_INFERIOR_RM_PROGRAM variable to "yes"' >&2 + echo >&2 + else + cat >&2 <<'END' +Aborting the configuration process, to ensure you take notice of the issue. + +You can download and install GNU coreutils to get an 'rm' implementation +that behaves properly: . + +If you want to complete the configuration process using your problematic +'rm' anyway, export the environment variable ACCEPT_INFERIOR_RM_PROGRAM +to "yes", and re-run configure. + +END + as_fn_error $? "Your 'rm' program is bad, sorry." "$LINENO" 5 + fi +fi + + +ac_config_headers="$ac_config_headers config.h" + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable maintainer-specific portions of Makefiles" >&5 +$as_echo_n "checking whether to enable maintainer-specific portions of Makefiles... " >&6; } + # Check whether --enable-maintainer-mode was given. +if test "${enable_maintainer_mode+set}" = set; then : + enableval=$enable_maintainer_mode; USE_MAINTAINER_MODE=$enableval +else + USE_MAINTAINER_MODE=no +fi + + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $USE_MAINTAINER_MODE" >&5 +$as_echo "$USE_MAINTAINER_MODE" >&6; } + if test $USE_MAINTAINER_MODE = yes; then + MAINTAINER_MODE_TRUE= + MAINTAINER_MODE_FALSE='#' +else + MAINTAINER_MODE_TRUE='#' + MAINTAINER_MODE_FALSE= +fi + + MAINT=$MAINTAINER_MODE_TRUE + + + +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu +if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}gcc", so it can be a program name with args. +set dummy ${ac_tool_prefix}gcc; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_CC="${ac_tool_prefix}gcc" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +CC=$ac_cv_prog_CC +if test -n "$CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +$as_echo "$CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_CC"; then + ac_ct_CC=$CC + # Extract the first word of "gcc", so it can be a program name with args. +set dummy gcc; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_CC"; then + ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_CC="gcc" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_CC=$ac_cv_prog_ac_ct_CC +if test -n "$ac_ct_CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +$as_echo "$ac_ct_CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_CC" = x; then + CC="" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + CC=$ac_ct_CC + fi +else + CC="$ac_cv_prog_CC" +fi + +if test -z "$CC"; then + if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}cc", so it can be a program name with args. +set dummy ${ac_tool_prefix}cc; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_CC="${ac_tool_prefix}cc" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +CC=$ac_cv_prog_CC +if test -n "$CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +$as_echo "$CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + fi +fi +if test -z "$CC"; then + # Extract the first word of "cc", so it can be a program name with args. +set dummy cc; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else + ac_prog_rejected=no +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if test "$as_dir/$ac_word$ac_exec_ext" = "/usr/ucb/cc"; then + ac_prog_rejected=yes + continue + fi + ac_cv_prog_CC="cc" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +if test $ac_prog_rejected = yes; then + # We found a bogon in the path, so make sure we never use it. + set dummy $ac_cv_prog_CC + shift + if test $# != 0; then + # We chose a different compiler from the bogus one. + # However, it has the same basename, so the bogon will be chosen + # first if we set CC to just the basename; use the full file name. + shift + ac_cv_prog_CC="$as_dir/$ac_word${1+' '}$@" + fi +fi +fi +fi +CC=$ac_cv_prog_CC +if test -n "$CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +$as_echo "$CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$CC"; then + if test -n "$ac_tool_prefix"; then + for ac_prog in cl.exe + do + # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args. +set dummy $ac_tool_prefix$ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_CC="$ac_tool_prefix$ac_prog" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +CC=$ac_cv_prog_CC +if test -n "$CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +$as_echo "$CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + test -n "$CC" && break + done +fi +if test -z "$CC"; then + ac_ct_CC=$CC + for ac_prog in cl.exe +do + # Extract the first word of "$ac_prog", so it can be a program name with args. +set dummy $ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_CC"; then + ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_CC="$ac_prog" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_CC=$ac_cv_prog_ac_ct_CC +if test -n "$ac_ct_CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +$as_echo "$ac_ct_CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + test -n "$ac_ct_CC" && break +done + + if test "x$ac_ct_CC" = x; then + CC="" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + CC=$ac_ct_CC + fi +fi + +fi + + +test -z "$CC" && { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "no acceptable C compiler found in \$PATH +See \`config.log' for more details" "$LINENO" 5; } + +# Provide some information about the compiler. +$as_echo "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 +set X $ac_compile +ac_compiler=$2 +for ac_option in --version -v -V -qversion; do + { { ac_try="$ac_compiler $ac_option >&5" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_compiler $ac_option >&5") 2>conftest.err + ac_status=$? + if test -s conftest.err; then + sed '10a\ +... rest of stderr output deleted ... + 10q' conftest.err >conftest.er1 + cat conftest.er1 >&5 + fi + rm -f conftest.er1 conftest.err + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } +done + +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +ac_clean_files_save=$ac_clean_files +ac_clean_files="$ac_clean_files a.out a.out.dSYM a.exe b.out" +# Try to create an executable without -o first, disregard a.out. +# It will help us diagnose broken compilers, and finding out an intuition +# of exeext. +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether the C compiler works" >&5 +$as_echo_n "checking whether the C compiler works... " >&6; } +ac_link_default=`$as_echo "$ac_link" | sed 's/ -o *conftest[^ ]*//'` + +# The possible output files: +ac_files="a.out conftest.exe conftest a.exe a_out.exe b.out conftest.*" + +ac_rmfiles= +for ac_file in $ac_files +do + case $ac_file in + *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM | *.o | *.obj ) ;; + * ) ac_rmfiles="$ac_rmfiles $ac_file";; + esac +done +rm -f $ac_rmfiles + +if { { ac_try="$ac_link_default" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_link_default") 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then : + # Autoconf-2.13 could set the ac_cv_exeext variable to `no'. +# So ignore a value of `no', otherwise this would lead to `EXEEXT = no' +# in a Makefile. We should not override ac_cv_exeext if it was cached, +# so that the user can short-circuit this test for compilers unknown to +# Autoconf. +for ac_file in $ac_files '' +do + test -f "$ac_file" || continue + case $ac_file in + *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM | *.o | *.obj ) + ;; + [ab].out ) + # We found the default executable, but exeext='' is most + # certainly right. + break;; + *.* ) + if test "${ac_cv_exeext+set}" = set && test "$ac_cv_exeext" != no; + then :; else + ac_cv_exeext=`expr "$ac_file" : '[^.]*\(\..*\)'` + fi + # We set ac_cv_exeext here because the later test for it is not + # safe: cross compilers may not add the suffix if given an `-o' + # argument, so we may need to know it at that point already. + # Even if this section looks crufty: it has the advantage of + # actually working. + break;; + * ) + break;; + esac +done +test "$ac_cv_exeext" = no && ac_cv_exeext= + +else + ac_file='' +fi +if test -z "$ac_file"; then : + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +$as_echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + +{ { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error 77 "C compiler cannot create executables +See \`config.log' for more details" "$LINENO" 5; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +$as_echo "yes" >&6; } +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for C compiler default output file name" >&5 +$as_echo_n "checking for C compiler default output file name... " >&6; } +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_file" >&5 +$as_echo "$ac_file" >&6; } +ac_exeext=$ac_cv_exeext + +rm -f -r a.out a.out.dSYM a.exe conftest$ac_cv_exeext b.out +ac_clean_files=$ac_clean_files_save +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for suffix of executables" >&5 +$as_echo_n "checking for suffix of executables... " >&6; } +if { { ac_try="$ac_link" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_link") 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then : + # If both `conftest.exe' and `conftest' are `present' (well, observable) +# catch `conftest.exe'. For instance with Cygwin, `ls conftest' will +# work properly (i.e., refer to `conftest.exe'), while it won't with +# `rm'. +for ac_file in conftest.exe conftest conftest.*; do + test -f "$ac_file" || continue + case $ac_file in + *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM | *.o | *.obj ) ;; + *.* ) ac_cv_exeext=`expr "$ac_file" : '[^.]*\(\..*\)'` + break;; + * ) break;; + esac +done +else + { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "cannot compute suffix of executables: cannot compile and link +See \`config.log' for more details" "$LINENO" 5; } +fi +rm -f conftest conftest$ac_cv_exeext +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5 +$as_echo "$ac_cv_exeext" >&6; } + +rm -f conftest.$ac_ext +EXEEXT=$ac_cv_exeext +ac_exeext=$EXEEXT +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include +int +main () +{ +FILE *f = fopen ("conftest.out", "w"); + return ferror (f) || fclose (f) != 0; + + ; + return 0; +} +_ACEOF +ac_clean_files="$ac_clean_files conftest.out" +# Check that the compiler produces executables we can run. If not, either +# the compiler is broken, or we cross compile. +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether we are cross compiling" >&5 +$as_echo_n "checking whether we are cross compiling... " >&6; } +if test "$cross_compiling" != yes; then + { { ac_try="$ac_link" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_link") 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } + if { ac_try='./conftest$ac_cv_exeext' + { { case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_try") 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; }; then + cross_compiling=no + else + if test "$cross_compiling" = maybe; then + cross_compiling=yes + else + { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "cannot run C compiled programs. +If you meant to cross compile, use \`--host'. +See \`config.log' for more details" "$LINENO" 5; } + fi + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $cross_compiling" >&5 +$as_echo "$cross_compiling" >&6; } + +rm -f conftest.$ac_ext conftest$ac_cv_exeext conftest.out +ac_clean_files=$ac_clean_files_save +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for suffix of object files" >&5 +$as_echo_n "checking for suffix of object files... " >&6; } +if ${ac_cv_objext+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +rm -f conftest.o conftest.obj +if { { ac_try="$ac_compile" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_compile") 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then : + for ac_file in conftest.o conftest.obj conftest.*; do + test -f "$ac_file" || continue; + case $ac_file in + *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM ) ;; + *) ac_cv_objext=`expr "$ac_file" : '.*\.\(.*\)'` + break;; + esac +done +else + $as_echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + +{ { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "cannot compute suffix of object files: cannot compile +See \`config.log' for more details" "$LINENO" 5; } +fi +rm -f conftest.$ac_cv_objext conftest.$ac_ext +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_objext" >&5 +$as_echo "$ac_cv_objext" >&6; } +OBJEXT=$ac_cv_objext +ac_objext=$OBJEXT +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether we are using the GNU C compiler" >&5 +$as_echo_n "checking whether we are using the GNU C compiler... " >&6; } +if ${ac_cv_c_compiler_gnu+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ +#ifndef __GNUC__ + choke me +#endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + ac_compiler_gnu=yes +else + ac_compiler_gnu=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +ac_cv_c_compiler_gnu=$ac_compiler_gnu + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 +$as_echo "$ac_cv_c_compiler_gnu" >&6; } +if test $ac_compiler_gnu = yes; then + GCC=yes +else + GCC= +fi +ac_test_CFLAGS=${CFLAGS+set} +ac_save_CFLAGS=$CFLAGS +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5 +$as_echo_n "checking whether $CC accepts -g... " >&6; } +if ${ac_cv_prog_cc_g+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_save_c_werror_flag=$ac_c_werror_flag + ac_c_werror_flag=yes + ac_cv_prog_cc_g=no + CFLAGS="-g" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + ac_cv_prog_cc_g=yes +else + CFLAGS="" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + +else + ac_c_werror_flag=$ac_save_c_werror_flag + CFLAGS="-g" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + ac_cv_prog_cc_g=yes +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext + ac_c_werror_flag=$ac_save_c_werror_flag +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 +$as_echo "$ac_cv_prog_cc_g" >&6; } +if test "$ac_test_CFLAGS" = set; then + CFLAGS=$ac_save_CFLAGS +elif test $ac_cv_prog_cc_g = yes; then + if test "$GCC" = yes; then + CFLAGS="-g -O2" + else + CFLAGS="-g" + fi +else + if test "$GCC" = yes; then + CFLAGS="-O2" + else + CFLAGS= + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $CC option to accept ISO C89" >&5 +$as_echo_n "checking for $CC option to accept ISO C89... " >&6; } +if ${ac_cv_prog_cc_c89+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_cv_prog_cc_c89=no +ac_save_CC=$CC +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include +#include +struct stat; +/* Most of the following tests are stolen from RCS 5.7's src/conf.sh. */ +struct buf { int x; }; +FILE * (*rcsopen) (struct buf *, struct stat *, int); +static char *e (p, i) + char **p; + int i; +{ + return p[i]; +} +static char *f (char * (*g) (char **, int), char **p, ...) +{ + char *s; + va_list v; + va_start (v,p); + s = g (p, va_arg (v,int)); + va_end (v); + return s; +} + +/* OSF 4.0 Compaq cc is some sort of almost-ANSI by default. It has + function prototypes and stuff, but not '\xHH' hex character constants. + These don't provoke an error unfortunately, instead are silently treated + as 'x'. The following induces an error, until -std is added to get + proper ANSI mode. Curiously '\x00'!='x' always comes out true, for an + array size at least. It's necessary to write '\x00'==0 to get something + that's true only with -std. */ +int osf4_cc_array ['\x00' == 0 ? 1 : -1]; + +/* IBM C 6 for AIX is almost-ANSI by default, but it replaces macro parameters + inside strings and character constants. */ +#define FOO(x) 'x' +int xlc6_cc_array[FOO(a) == 'x' ? 1 : -1]; + +int test (int i, double x); +struct s1 {int (*f) (int a);}; +struct s2 {int (*f) (double a);}; +int pairnames (int, char **, FILE *(*)(struct buf *, struct stat *, int), int, int); +int argc; +char **argv; +int +main () +{ +return f (e, argv, 0) != argv[0] || f (e, argv, 1) != argv[1]; + ; + return 0; +} +_ACEOF +for ac_arg in '' -qlanglvl=extc89 -qlanglvl=ansi -std \ + -Ae "-Aa -D_HPUX_SOURCE" "-Xc -D__EXTENSIONS__" +do + CC="$ac_save_CC $ac_arg" + if ac_fn_c_try_compile "$LINENO"; then : + ac_cv_prog_cc_c89=$ac_arg +fi +rm -f core conftest.err conftest.$ac_objext + test "x$ac_cv_prog_cc_c89" != "xno" && break +done +rm -f conftest.$ac_ext +CC=$ac_save_CC + +fi +# AC_CACHE_VAL +case "x$ac_cv_prog_cc_c89" in + x) + { $as_echo "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +$as_echo "none needed" >&6; } ;; + xno) + { $as_echo "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +$as_echo "unsupported" >&6; } ;; + *) + CC="$CC $ac_cv_prog_cc_c89" + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 +$as_echo "$ac_cv_prog_cc_c89" >&6; } ;; +esac +if test "x$ac_cv_prog_cc_c89" != xno; then : + +fi + +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether $CC understands -c and -o together" >&5 +$as_echo_n "checking whether $CC understands -c and -o together... " >&6; } +if ${am_cv_prog_cc_c_o+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF + # Make sure it works both with $CC and with simple cc. + # Following AC_PROG_CC_C_O, we do the test twice because some + # compilers refuse to overwrite an existing .o file with -o, + # though they will create one. + am_cv_prog_cc_c_o=yes + for am_i in 1 2; do + if { echo "$as_me:$LINENO: $CC -c conftest.$ac_ext -o conftest2.$ac_objext" >&5 + ($CC -c conftest.$ac_ext -o conftest2.$ac_objext) >&5 2>&5 + ac_status=$? + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + (exit $ac_status); } \ + && test -f conftest2.$ac_objext; then + : OK + else + am_cv_prog_cc_c_o=no + break + fi + done + rm -f core conftest* + unset am_i +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $am_cv_prog_cc_c_o" >&5 +$as_echo "$am_cv_prog_cc_c_o" >&6; } +if test "$am_cv_prog_cc_c_o" != yes; then + # Losing compiler, so override with the script. + # FIXME: It is wrong to rewrite CC. + # But if we don't then we get into trouble of one sort or another. + # A longer-term fix would be to have automake use am__CC in this case, + # and then we could set am__CC="\$(top_srcdir)/compile \$(CC)" + CC="$am_aux_dir/compile $CC" +fi +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +DEPDIR="${am__leading_dot}deps" + +ac_config_commands="$ac_config_commands depfiles" + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${MAKE-make} supports the include directive" >&5 +$as_echo_n "checking whether ${MAKE-make} supports the include directive... " >&6; } +cat > confinc.mk << 'END' +am__doit: + @echo this is the am__doit target >confinc.out +.PHONY: am__doit +END +am__include="#" +am__quote= +# BSD make does it like this. +echo '.include "confinc.mk" # ignored' > confmf.BSD +# Other make implementations (GNU, Solaris 10, AIX) do it like this. +echo 'include confinc.mk # ignored' > confmf.GNU +_am_result=no +for s in GNU BSD; do + { echo "$as_me:$LINENO: ${MAKE-make} -f confmf.$s && cat confinc.out" >&5 + (${MAKE-make} -f confmf.$s && cat confinc.out) >&5 2>&5 + ac_status=$? + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + (exit $ac_status); } + case $?:`cat confinc.out 2>/dev/null` in #( + '0:this is the am__doit target') : + case $s in #( + BSD) : + am__include='.include' am__quote='"' ;; #( + *) : + am__include='include' am__quote='' ;; +esac ;; #( + *) : + ;; +esac + if test "$am__include" != "#"; then + _am_result="yes ($s style)" + break + fi +done +rm -f confinc.* confmf.* +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: ${_am_result}" >&5 +$as_echo "${_am_result}" >&6; } + +# Check whether --enable-dependency-tracking was given. +if test "${enable_dependency_tracking+set}" = set; then : + enableval=$enable_dependency_tracking; +fi + +if test "x$enable_dependency_tracking" != xno; then + am_depcomp="$ac_aux_dir/depcomp" + AMDEPBACKSLASH='\' + am__nodep='_no' +fi + if test "x$enable_dependency_tracking" != xno; then + AMDEP_TRUE= + AMDEP_FALSE='#' +else + AMDEP_TRUE='#' + AMDEP_FALSE= +fi + + + +depcc="$CC" am_compiler_list= + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking dependency style of $depcc" >&5 +$as_echo_n "checking dependency style of $depcc... " >&6; } +if ${am_cv_CC_dependencies_compiler_type+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -z "$AMDEP_TRUE" && test -f "$am_depcomp"; then + # We make a subdir and do the tests there. Otherwise we can end up + # making bogus files that we don't know about and never remove. For + # instance it was reported that on HP-UX the gcc test will end up + # making a dummy file named 'D' -- because '-MD' means "put the output + # in D". + rm -rf conftest.dir + mkdir conftest.dir + # Copy depcomp to subdir because otherwise we won't find it if we're + # using a relative directory. + cp "$am_depcomp" conftest.dir + cd conftest.dir + # We will build objects and dependencies in a subdirectory because + # it helps to detect inapplicable dependency modes. For instance + # both Tru64's cc and ICC support -MD to output dependencies as a + # side effect of compilation, but ICC will put the dependencies in + # the current directory while Tru64 will put them in the object + # directory. + mkdir sub + + am_cv_CC_dependencies_compiler_type=none + if test "$am_compiler_list" = ""; then + am_compiler_list=`sed -n 's/^#*\([a-zA-Z0-9]*\))$/\1/p' < ./depcomp` + fi + am__universal=false + case " $depcc " in #( + *\ -arch\ *\ -arch\ *) am__universal=true ;; + esac + + for depmode in $am_compiler_list; do + # Setup a source with many dependencies, because some compilers + # like to wrap large dependency lists on column 80 (with \), and + # we should not choose a depcomp mode which is confused by this. + # + # We need to recreate these files for each test, as the compiler may + # overwrite some of them when testing with obscure command lines. + # This happens at least with the AIX C compiler. + : > sub/conftest.c + for i in 1 2 3 4 5 6; do + echo '#include "conftst'$i'.h"' >> sub/conftest.c + # Using ": > sub/conftst$i.h" creates only sub/conftst1.h with + # Solaris 10 /bin/sh. + echo '/* dummy */' > sub/conftst$i.h + done + echo "${am__include} ${am__quote}sub/conftest.Po${am__quote}" > confmf + + # We check with '-c' and '-o' for the sake of the "dashmstdout" + # mode. It turns out that the SunPro C++ compiler does not properly + # handle '-M -o', and we need to detect this. Also, some Intel + # versions had trouble with output in subdirs. + am__obj=sub/conftest.${OBJEXT-o} + am__minus_obj="-o $am__obj" + case $depmode in + gcc) + # This depmode causes a compiler race in universal mode. + test "$am__universal" = false || continue + ;; + nosideeffect) + # After this tag, mechanisms are not by side-effect, so they'll + # only be used when explicitly requested. + if test "x$enable_dependency_tracking" = xyes; then + continue + else + break + fi + ;; + msvc7 | msvc7msys | msvisualcpp | msvcmsys) + # This compiler won't grok '-c -o', but also, the minuso test has + # not run yet. These depmodes are late enough in the game, and + # so weak that their functioning should not be impacted. + am__obj=conftest.${OBJEXT-o} + am__minus_obj= + ;; + none) break ;; + esac + if depmode=$depmode \ + source=sub/conftest.c object=$am__obj \ + depfile=sub/conftest.Po tmpdepfile=sub/conftest.TPo \ + $SHELL ./depcomp $depcc -c $am__minus_obj sub/conftest.c \ + >/dev/null 2>conftest.err && + grep sub/conftst1.h sub/conftest.Po > /dev/null 2>&1 && + grep sub/conftst6.h sub/conftest.Po > /dev/null 2>&1 && + grep $am__obj sub/conftest.Po > /dev/null 2>&1 && + ${MAKE-make} -s -f confmf > /dev/null 2>&1; then + # icc doesn't choke on unknown options, it will just issue warnings + # or remarks (even with -Werror). So we grep stderr for any message + # that says an option was ignored or not supported. + # When given -MP, icc 7.0 and 7.1 complain thusly: + # icc: Command line warning: ignoring option '-M'; no argument required + # The diagnosis changed in icc 8.0: + # icc: Command line remark: option '-MP' not supported + if (grep 'ignoring option' conftest.err || + grep 'not supported' conftest.err) >/dev/null 2>&1; then :; else + am_cv_CC_dependencies_compiler_type=$depmode + break + fi + fi + done + + cd .. + rm -rf conftest.dir +else + am_cv_CC_dependencies_compiler_type=none +fi + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $am_cv_CC_dependencies_compiler_type" >&5 +$as_echo "$am_cv_CC_dependencies_compiler_type" >&6; } +CCDEPMODE=depmode=$am_cv_CC_dependencies_compiler_type + + if + test "x$enable_dependency_tracking" != xno \ + && test "$am_cv_CC_dependencies_compiler_type" = gcc3; then + am__fastdepCC_TRUE= + am__fastdepCC_FALSE='#' +else + am__fastdepCC_TRUE='#' + am__fastdepCC_FALSE= +fi + + + +if test -z "$orig_CFLAGS"; then + if test x$GCC = xyes; then + CFLAGS="-g -O3" + fi +fi + + +case `pwd` in + *\ * | *\ *) + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: Libtool does not cope well with whitespace in \`pwd\`" >&5 +$as_echo "$as_me: WARNING: Libtool does not cope well with whitespace in \`pwd\`" >&2;} ;; +esac + + + +macro_version='2.4.6' +macro_revision='2.4.6' + + + + + + + + + + + + + +ltmain=$ac_aux_dir/ltmain.sh + +# Make sure we can run config.sub. +$SHELL "$ac_aux_dir/config.sub" sun4 >/dev/null 2>&1 || + as_fn_error $? "cannot run $SHELL $ac_aux_dir/config.sub" "$LINENO" 5 + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking build system type" >&5 +$as_echo_n "checking build system type... " >&6; } +if ${ac_cv_build+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_build_alias=$build_alias +test "x$ac_build_alias" = x && + ac_build_alias=`$SHELL "$ac_aux_dir/config.guess"` +test "x$ac_build_alias" = x && + as_fn_error $? "cannot guess build type; you must specify one" "$LINENO" 5 +ac_cv_build=`$SHELL "$ac_aux_dir/config.sub" $ac_build_alias` || + as_fn_error $? "$SHELL $ac_aux_dir/config.sub $ac_build_alias failed" "$LINENO" 5 + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_build" >&5 +$as_echo "$ac_cv_build" >&6; } +case $ac_cv_build in +*-*-*) ;; +*) as_fn_error $? "invalid value of canonical build" "$LINENO" 5;; +esac +build=$ac_cv_build +ac_save_IFS=$IFS; IFS='-' +set x $ac_cv_build +shift +build_cpu=$1 +build_vendor=$2 +shift; shift +# Remember, the first character of IFS is used to create $*, +# except with old shells: +build_os=$* +IFS=$ac_save_IFS +case $build_os in *\ *) build_os=`echo "$build_os" | sed 's/ /-/g'`;; esac + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking host system type" >&5 +$as_echo_n "checking host system type... " >&6; } +if ${ac_cv_host+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test "x$host_alias" = x; then + ac_cv_host=$ac_cv_build +else + ac_cv_host=`$SHELL "$ac_aux_dir/config.sub" $host_alias` || + as_fn_error $? "$SHELL $ac_aux_dir/config.sub $host_alias failed" "$LINENO" 5 +fi + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_host" >&5 +$as_echo "$ac_cv_host" >&6; } +case $ac_cv_host in +*-*-*) ;; +*) as_fn_error $? "invalid value of canonical host" "$LINENO" 5;; +esac +host=$ac_cv_host +ac_save_IFS=$IFS; IFS='-' +set x $ac_cv_host +shift +host_cpu=$1 +host_vendor=$2 +shift; shift +# Remember, the first character of IFS is used to create $*, +# except with old shells: +host_os=$* +IFS=$ac_save_IFS +case $host_os in *\ *) host_os=`echo "$host_os" | sed 's/ /-/g'`;; esac + + +# Backslashify metacharacters that are still active within +# double-quoted strings. +sed_quote_subst='s/\(["`$\\]\)/\\\1/g' + +# Same as above, but do not quote variable references. +double_quote_subst='s/\(["`\\]\)/\\\1/g' + +# Sed substitution to delay expansion of an escaped shell variable in a +# double_quote_subst'ed string. +delay_variable_subst='s/\\\\\\\\\\\$/\\\\\\$/g' + +# Sed substitution to delay expansion of an escaped single quote. +delay_single_quote_subst='s/'\''/'\'\\\\\\\'\''/g' + +# Sed substitution to avoid accidental globbing in evaled expressions +no_glob_subst='s/\*/\\\*/g' + +ECHO='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' +ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO +ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO$ECHO + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking how to print strings" >&5 +$as_echo_n "checking how to print strings... " >&6; } +# Test print first, because it will be a builtin if present. +if test "X`( print -r -- -n ) 2>/dev/null`" = X-n && \ + test "X`print -r -- $ECHO 2>/dev/null`" = "X$ECHO"; then + ECHO='print -r --' +elif test "X`printf %s $ECHO 2>/dev/null`" = "X$ECHO"; then + ECHO='printf %s\n' +else + # Use this function as a fallback that always works. + func_fallback_echo () + { + eval 'cat <<_LTECHO_EOF +$1 +_LTECHO_EOF' + } + ECHO='func_fallback_echo' +fi + +# func_echo_all arg... +# Invoke $ECHO with all args, space-separated. +func_echo_all () +{ + $ECHO "" +} + +case $ECHO in + printf*) { $as_echo "$as_me:${as_lineno-$LINENO}: result: printf" >&5 +$as_echo "printf" >&6; } ;; + print*) { $as_echo "$as_me:${as_lineno-$LINENO}: result: print -r" >&5 +$as_echo "print -r" >&6; } ;; + *) { $as_echo "$as_me:${as_lineno-$LINENO}: result: cat" >&5 +$as_echo "cat" >&6; } ;; +esac + + + + + + + + + + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for a sed that does not truncate output" >&5 +$as_echo_n "checking for a sed that does not truncate output... " >&6; } +if ${ac_cv_path_SED+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_script=s/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/ + for ac_i in 1 2 3 4 5 6 7; do + ac_script="$ac_script$as_nl$ac_script" + done + echo "$ac_script" 2>/dev/null | sed 99q >conftest.sed + { ac_script=; unset ac_script;} + if test -z "$SED"; then + ac_path_SED_found=false + # Loop through the user's path and test for each of PROGNAME-LIST + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_prog in sed gsed; do + for ac_exec_ext in '' $ac_executable_extensions; do + ac_path_SED="$as_dir/$ac_prog$ac_exec_ext" + as_fn_executable_p "$ac_path_SED" || continue +# Check for GNU ac_path_SED and select it if it is found. + # Check for GNU $ac_path_SED +case `"$ac_path_SED" --version 2>&1` in +*GNU*) + ac_cv_path_SED="$ac_path_SED" ac_path_SED_found=:;; +*) + ac_count=0 + $as_echo_n 0123456789 >"conftest.in" + while : + do + cat "conftest.in" "conftest.in" >"conftest.tmp" + mv "conftest.tmp" "conftest.in" + cp "conftest.in" "conftest.nl" + $as_echo '' >> "conftest.nl" + "$ac_path_SED" -f conftest.sed < "conftest.nl" >"conftest.out" 2>/dev/null || break + diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break + as_fn_arith $ac_count + 1 && ac_count=$as_val + if test $ac_count -gt ${ac_path_SED_max-0}; then + # Best one so far, save it but keep looking for a better one + ac_cv_path_SED="$ac_path_SED" + ac_path_SED_max=$ac_count + fi + # 10*(2^10) chars as input seems more than enough + test $ac_count -gt 10 && break + done + rm -f conftest.in conftest.tmp conftest.nl conftest.out;; +esac + + $ac_path_SED_found && break 3 + done + done + done +IFS=$as_save_IFS + if test -z "$ac_cv_path_SED"; then + as_fn_error $? "no acceptable sed could be found in \$PATH" "$LINENO" 5 + fi +else + ac_cv_path_SED=$SED +fi + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_SED" >&5 +$as_echo "$ac_cv_path_SED" >&6; } + SED="$ac_cv_path_SED" + rm -f conftest.sed + +test -z "$SED" && SED=sed +Xsed="$SED -e 1s/^X//" + + + + + + + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for grep that handles long lines and -e" >&5 +$as_echo_n "checking for grep that handles long lines and -e... " >&6; } +if ${ac_cv_path_GREP+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -z "$GREP"; then + ac_path_GREP_found=false + # Loop through the user's path and test for each of PROGNAME-LIST + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_prog in grep ggrep; do + for ac_exec_ext in '' $ac_executable_extensions; do + ac_path_GREP="$as_dir/$ac_prog$ac_exec_ext" + as_fn_executable_p "$ac_path_GREP" || continue +# Check for GNU ac_path_GREP and select it if it is found. + # Check for GNU $ac_path_GREP +case `"$ac_path_GREP" --version 2>&1` in +*GNU*) + ac_cv_path_GREP="$ac_path_GREP" ac_path_GREP_found=:;; +*) + ac_count=0 + $as_echo_n 0123456789 >"conftest.in" + while : + do + cat "conftest.in" "conftest.in" >"conftest.tmp" + mv "conftest.tmp" "conftest.in" + cp "conftest.in" "conftest.nl" + $as_echo 'GREP' >> "conftest.nl" + "$ac_path_GREP" -e 'GREP$' -e '-(cannot match)-' < "conftest.nl" >"conftest.out" 2>/dev/null || break + diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break + as_fn_arith $ac_count + 1 && ac_count=$as_val + if test $ac_count -gt ${ac_path_GREP_max-0}; then + # Best one so far, save it but keep looking for a better one + ac_cv_path_GREP="$ac_path_GREP" + ac_path_GREP_max=$ac_count + fi + # 10*(2^10) chars as input seems more than enough + test $ac_count -gt 10 && break + done + rm -f conftest.in conftest.tmp conftest.nl conftest.out;; +esac + + $ac_path_GREP_found && break 3 + done + done + done +IFS=$as_save_IFS + if test -z "$ac_cv_path_GREP"; then + as_fn_error $? "no acceptable grep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5 + fi +else + ac_cv_path_GREP=$GREP +fi + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_GREP" >&5 +$as_echo "$ac_cv_path_GREP" >&6; } + GREP="$ac_cv_path_GREP" + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for egrep" >&5 +$as_echo_n "checking for egrep... " >&6; } +if ${ac_cv_path_EGREP+:} false; then : + $as_echo_n "(cached) " >&6 +else + if echo a | $GREP -E '(a|b)' >/dev/null 2>&1 + then ac_cv_path_EGREP="$GREP -E" + else + if test -z "$EGREP"; then + ac_path_EGREP_found=false + # Loop through the user's path and test for each of PROGNAME-LIST + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_prog in egrep; do + for ac_exec_ext in '' $ac_executable_extensions; do + ac_path_EGREP="$as_dir/$ac_prog$ac_exec_ext" + as_fn_executable_p "$ac_path_EGREP" || continue +# Check for GNU ac_path_EGREP and select it if it is found. + # Check for GNU $ac_path_EGREP +case `"$ac_path_EGREP" --version 2>&1` in +*GNU*) + ac_cv_path_EGREP="$ac_path_EGREP" ac_path_EGREP_found=:;; +*) + ac_count=0 + $as_echo_n 0123456789 >"conftest.in" + while : + do + cat "conftest.in" "conftest.in" >"conftest.tmp" + mv "conftest.tmp" "conftest.in" + cp "conftest.in" "conftest.nl" + $as_echo 'EGREP' >> "conftest.nl" + "$ac_path_EGREP" 'EGREP$' < "conftest.nl" >"conftest.out" 2>/dev/null || break + diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break + as_fn_arith $ac_count + 1 && ac_count=$as_val + if test $ac_count -gt ${ac_path_EGREP_max-0}; then + # Best one so far, save it but keep looking for a better one + ac_cv_path_EGREP="$ac_path_EGREP" + ac_path_EGREP_max=$ac_count + fi + # 10*(2^10) chars as input seems more than enough + test $ac_count -gt 10 && break + done + rm -f conftest.in conftest.tmp conftest.nl conftest.out;; +esac + + $ac_path_EGREP_found && break 3 + done + done + done +IFS=$as_save_IFS + if test -z "$ac_cv_path_EGREP"; then + as_fn_error $? "no acceptable egrep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5 + fi +else + ac_cv_path_EGREP=$EGREP +fi + + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_EGREP" >&5 +$as_echo "$ac_cv_path_EGREP" >&6; } + EGREP="$ac_cv_path_EGREP" + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for fgrep" >&5 +$as_echo_n "checking for fgrep... " >&6; } +if ${ac_cv_path_FGREP+:} false; then : + $as_echo_n "(cached) " >&6 +else + if echo 'ab*c' | $GREP -F 'ab*c' >/dev/null 2>&1 + then ac_cv_path_FGREP="$GREP -F" + else + if test -z "$FGREP"; then + ac_path_FGREP_found=false + # Loop through the user's path and test for each of PROGNAME-LIST + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_prog in fgrep; do + for ac_exec_ext in '' $ac_executable_extensions; do + ac_path_FGREP="$as_dir/$ac_prog$ac_exec_ext" + as_fn_executable_p "$ac_path_FGREP" || continue +# Check for GNU ac_path_FGREP and select it if it is found. + # Check for GNU $ac_path_FGREP +case `"$ac_path_FGREP" --version 2>&1` in +*GNU*) + ac_cv_path_FGREP="$ac_path_FGREP" ac_path_FGREP_found=:;; +*) + ac_count=0 + $as_echo_n 0123456789 >"conftest.in" + while : + do + cat "conftest.in" "conftest.in" >"conftest.tmp" + mv "conftest.tmp" "conftest.in" + cp "conftest.in" "conftest.nl" + $as_echo 'FGREP' >> "conftest.nl" + "$ac_path_FGREP" FGREP < "conftest.nl" >"conftest.out" 2>/dev/null || break + diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break + as_fn_arith $ac_count + 1 && ac_count=$as_val + if test $ac_count -gt ${ac_path_FGREP_max-0}; then + # Best one so far, save it but keep looking for a better one + ac_cv_path_FGREP="$ac_path_FGREP" + ac_path_FGREP_max=$ac_count + fi + # 10*(2^10) chars as input seems more than enough + test $ac_count -gt 10 && break + done + rm -f conftest.in conftest.tmp conftest.nl conftest.out;; +esac + + $ac_path_FGREP_found && break 3 + done + done + done +IFS=$as_save_IFS + if test -z "$ac_cv_path_FGREP"; then + as_fn_error $? "no acceptable fgrep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5 + fi +else + ac_cv_path_FGREP=$FGREP +fi + + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_FGREP" >&5 +$as_echo "$ac_cv_path_FGREP" >&6; } + FGREP="$ac_cv_path_FGREP" + + +test -z "$GREP" && GREP=grep + + + + + + + + + + + + + + + + + + + +# Check whether --with-gnu-ld was given. +if test "${with_gnu_ld+set}" = set; then : + withval=$with_gnu_ld; test no = "$withval" || with_gnu_ld=yes +else + with_gnu_ld=no +fi + +ac_prog=ld +if test yes = "$GCC"; then + # Check if gcc -print-prog-name=ld gives a path. + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for ld used by $CC" >&5 +$as_echo_n "checking for ld used by $CC... " >&6; } + case $host in + *-*-mingw*) + # gcc leaves a trailing carriage return, which upsets mingw + ac_prog=`($CC -print-prog-name=ld) 2>&5 | tr -d '\015'` ;; + *) + ac_prog=`($CC -print-prog-name=ld) 2>&5` ;; + esac + case $ac_prog in + # Accept absolute paths. + [\\/]* | ?:[\\/]*) + re_direlt='/[^/][^/]*/\.\./' + # Canonicalize the pathname of ld + ac_prog=`$ECHO "$ac_prog"| $SED 's%\\\\%/%g'` + while $ECHO "$ac_prog" | $GREP "$re_direlt" > /dev/null 2>&1; do + ac_prog=`$ECHO $ac_prog| $SED "s%$re_direlt%/%"` + done + test -z "$LD" && LD=$ac_prog + ;; + "") + # If it fails, then pretend we aren't using GCC. + ac_prog=ld + ;; + *) + # If it is relative, then search for the first ld in PATH. + with_gnu_ld=unknown + ;; + esac +elif test yes = "$with_gnu_ld"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for GNU ld" >&5 +$as_echo_n "checking for GNU ld... " >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for non-GNU ld" >&5 +$as_echo_n "checking for non-GNU ld... " >&6; } +fi +if ${lt_cv_path_LD+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -z "$LD"; then + lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR + for ac_dir in $PATH; do + IFS=$lt_save_ifs + test -z "$ac_dir" && ac_dir=. + if test -f "$ac_dir/$ac_prog" || test -f "$ac_dir/$ac_prog$ac_exeext"; then + lt_cv_path_LD=$ac_dir/$ac_prog + # Check to see if the program is GNU ld. I'd rather use --version, + # but apparently some variants of GNU ld only accept -v. + # Break only if it was the GNU/non-GNU ld that we prefer. + case `"$lt_cv_path_LD" -v 2>&1 &5 +$as_echo "$LD" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi +test -z "$LD" && as_fn_error $? "no acceptable ld found in \$PATH" "$LINENO" 5 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking if the linker ($LD) is GNU ld" >&5 +$as_echo_n "checking if the linker ($LD) is GNU ld... " >&6; } +if ${lt_cv_prog_gnu_ld+:} false; then : + $as_echo_n "(cached) " >&6 +else + # I'd rather use --version here, but apparently some GNU lds only accept -v. +case `$LD -v 2>&1 &5 +$as_echo "$lt_cv_prog_gnu_ld" >&6; } +with_gnu_ld=$lt_cv_prog_gnu_ld + + + + + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for BSD- or MS-compatible name lister (nm)" >&5 +$as_echo_n "checking for BSD- or MS-compatible name lister (nm)... " >&6; } +if ${lt_cv_path_NM+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$NM"; then + # Let the user override the test. + lt_cv_path_NM=$NM +else + lt_nm_to_check=${ac_tool_prefix}nm + if test -n "$ac_tool_prefix" && test "$build" = "$host"; then + lt_nm_to_check="$lt_nm_to_check nm" + fi + for lt_tmp_nm in $lt_nm_to_check; do + lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR + for ac_dir in $PATH /usr/ccs/bin/elf /usr/ccs/bin /usr/ucb /bin; do + IFS=$lt_save_ifs + test -z "$ac_dir" && ac_dir=. + tmp_nm=$ac_dir/$lt_tmp_nm + if test -f "$tmp_nm" || test -f "$tmp_nm$ac_exeext"; then + # Check to see if the nm accepts a BSD-compat flag. + # Adding the 'sed 1q' prevents false positives on HP-UX, which says: + # nm: unknown option "B" ignored + # Tru64's nm complains that /dev/null is an invalid object file + # MSYS converts /dev/null to NUL, MinGW nm treats NUL as empty + case $build_os in + mingw*) lt_bad_file=conftest.nm/nofile ;; + *) lt_bad_file=/dev/null ;; + esac + case `"$tmp_nm" -B $lt_bad_file 2>&1 | sed '1q'` in + *$lt_bad_file* | *'Invalid file or object type'*) + lt_cv_path_NM="$tmp_nm -B" + break 2 + ;; + *) + case `"$tmp_nm" -p /dev/null 2>&1 | sed '1q'` in + */dev/null*) + lt_cv_path_NM="$tmp_nm -p" + break 2 + ;; + *) + lt_cv_path_NM=${lt_cv_path_NM="$tmp_nm"} # keep the first match, but + continue # so that we can try to find one that supports BSD flags + ;; + esac + ;; + esac + fi + done + IFS=$lt_save_ifs + done + : ${lt_cv_path_NM=no} +fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_path_NM" >&5 +$as_echo "$lt_cv_path_NM" >&6; } +if test no != "$lt_cv_path_NM"; then + NM=$lt_cv_path_NM +else + # Didn't find any BSD compatible name lister, look for dumpbin. + if test -n "$DUMPBIN"; then : + # Let the user override the test. + else + if test -n "$ac_tool_prefix"; then + for ac_prog in dumpbin "link -dump" + do + # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args. +set dummy $ac_tool_prefix$ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_DUMPBIN+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$DUMPBIN"; then + ac_cv_prog_DUMPBIN="$DUMPBIN" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_DUMPBIN="$ac_tool_prefix$ac_prog" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +DUMPBIN=$ac_cv_prog_DUMPBIN +if test -n "$DUMPBIN"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $DUMPBIN" >&5 +$as_echo "$DUMPBIN" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + test -n "$DUMPBIN" && break + done +fi +if test -z "$DUMPBIN"; then + ac_ct_DUMPBIN=$DUMPBIN + for ac_prog in dumpbin "link -dump" +do + # Extract the first word of "$ac_prog", so it can be a program name with args. +set dummy $ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_DUMPBIN+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_DUMPBIN"; then + ac_cv_prog_ac_ct_DUMPBIN="$ac_ct_DUMPBIN" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_DUMPBIN="$ac_prog" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_DUMPBIN=$ac_cv_prog_ac_ct_DUMPBIN +if test -n "$ac_ct_DUMPBIN"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_DUMPBIN" >&5 +$as_echo "$ac_ct_DUMPBIN" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + test -n "$ac_ct_DUMPBIN" && break +done + + if test "x$ac_ct_DUMPBIN" = x; then + DUMPBIN=":" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + DUMPBIN=$ac_ct_DUMPBIN + fi +fi + + case `$DUMPBIN -symbols -headers /dev/null 2>&1 | sed '1q'` in + *COFF*) + DUMPBIN="$DUMPBIN -symbols -headers" + ;; + *) + DUMPBIN=: + ;; + esac + fi + + if test : != "$DUMPBIN"; then + NM=$DUMPBIN + fi +fi +test -z "$NM" && NM=nm + + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking the name lister ($NM) interface" >&5 +$as_echo_n "checking the name lister ($NM) interface... " >&6; } +if ${lt_cv_nm_interface+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_nm_interface="BSD nm" + echo "int some_variable = 0;" > conftest.$ac_ext + (eval echo "\"\$as_me:$LINENO: $ac_compile\"" >&5) + (eval "$ac_compile" 2>conftest.err) + cat conftest.err >&5 + (eval echo "\"\$as_me:$LINENO: $NM \\\"conftest.$ac_objext\\\"\"" >&5) + (eval "$NM \"conftest.$ac_objext\"" 2>conftest.err > conftest.out) + cat conftest.err >&5 + (eval echo "\"\$as_me:$LINENO: output\"" >&5) + cat conftest.out >&5 + if $GREP 'External.*some_variable' conftest.out > /dev/null; then + lt_cv_nm_interface="MS dumpbin" + fi + rm -f conftest* +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_nm_interface" >&5 +$as_echo "$lt_cv_nm_interface" >&6; } + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ln -s works" >&5 +$as_echo_n "checking whether ln -s works... " >&6; } +LN_S=$as_ln_s +if test "$LN_S" = "ln -s"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +$as_echo "yes" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no, using $LN_S" >&5 +$as_echo "no, using $LN_S" >&6; } +fi + +# find the maximum length of command line arguments +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking the maximum length of command line arguments" >&5 +$as_echo_n "checking the maximum length of command line arguments... " >&6; } +if ${lt_cv_sys_max_cmd_len+:} false; then : + $as_echo_n "(cached) " >&6 +else + i=0 + teststring=ABCD + + case $build_os in + msdosdjgpp*) + # On DJGPP, this test can blow up pretty badly due to problems in libc + # (any single argument exceeding 2000 bytes causes a buffer overrun + # during glob expansion). Even if it were fixed, the result of this + # check would be larger than it should be. + lt_cv_sys_max_cmd_len=12288; # 12K is about right + ;; + + gnu*) + # Under GNU Hurd, this test is not required because there is + # no limit to the length of command line arguments. + # Libtool will interpret -1 as no limit whatsoever + lt_cv_sys_max_cmd_len=-1; + ;; + + cygwin* | mingw* | cegcc*) + # On Win9x/ME, this test blows up -- it succeeds, but takes + # about 5 minutes as the teststring grows exponentially. + # Worse, since 9x/ME are not pre-emptively multitasking, + # you end up with a "frozen" computer, even though with patience + # the test eventually succeeds (with a max line length of 256k). + # Instead, let's just punt: use the minimum linelength reported by + # all of the supported platforms: 8192 (on NT/2K/XP). + lt_cv_sys_max_cmd_len=8192; + ;; + + mint*) + # On MiNT this can take a long time and run out of memory. + lt_cv_sys_max_cmd_len=8192; + ;; + + amigaos*) + # On AmigaOS with pdksh, this test takes hours, literally. + # So we just punt and use a minimum line length of 8192. + lt_cv_sys_max_cmd_len=8192; + ;; + + bitrig* | darwin* | dragonfly* | freebsd* | netbsd* | openbsd*) + # This has been around since 386BSD, at least. Likely further. + if test -x /sbin/sysctl; then + lt_cv_sys_max_cmd_len=`/sbin/sysctl -n kern.argmax` + elif test -x /usr/sbin/sysctl; then + lt_cv_sys_max_cmd_len=`/usr/sbin/sysctl -n kern.argmax` + else + lt_cv_sys_max_cmd_len=65536 # usable default for all BSDs + fi + # And add a safety zone + lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 4` + lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \* 3` + ;; + + interix*) + # We know the value 262144 and hardcode it with a safety zone (like BSD) + lt_cv_sys_max_cmd_len=196608 + ;; + + os2*) + # The test takes a long time on OS/2. + lt_cv_sys_max_cmd_len=8192 + ;; + + osf*) + # Dr. Hans Ekkehard Plesser reports seeing a kernel panic running configure + # due to this test when exec_disable_arg_limit is 1 on Tru64. It is not + # nice to cause kernel panics so lets avoid the loop below. + # First set a reasonable default. + lt_cv_sys_max_cmd_len=16384 + # + if test -x /sbin/sysconfig; then + case `/sbin/sysconfig -q proc exec_disable_arg_limit` in + *1*) lt_cv_sys_max_cmd_len=-1 ;; + esac + fi + ;; + sco3.2v5*) + lt_cv_sys_max_cmd_len=102400 + ;; + sysv5* | sco5v6* | sysv4.2uw2*) + kargmax=`grep ARG_MAX /etc/conf/cf.d/stune 2>/dev/null` + if test -n "$kargmax"; then + lt_cv_sys_max_cmd_len=`echo $kargmax | sed 's/.*[ ]//'` + else + lt_cv_sys_max_cmd_len=32768 + fi + ;; + *) + lt_cv_sys_max_cmd_len=`(getconf ARG_MAX) 2> /dev/null` + if test -n "$lt_cv_sys_max_cmd_len" && \ + test undefined != "$lt_cv_sys_max_cmd_len"; then + lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 4` + lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \* 3` + else + # Make teststring a little bigger before we do anything with it. + # a 1K string should be a reasonable start. + for i in 1 2 3 4 5 6 7 8; do + teststring=$teststring$teststring + done + SHELL=${SHELL-${CONFIG_SHELL-/bin/sh}} + # If test is not a shell built-in, we'll probably end up computing a + # maximum length that is only half of the actual maximum length, but + # we can't tell. + while { test X`env echo "$teststring$teststring" 2>/dev/null` \ + = "X$teststring$teststring"; } >/dev/null 2>&1 && + test 17 != "$i" # 1/2 MB should be enough + do + i=`expr $i + 1` + teststring=$teststring$teststring + done + # Only check the string length outside the loop. + lt_cv_sys_max_cmd_len=`expr "X$teststring" : ".*" 2>&1` + teststring= + # Add a significant safety factor because C++ compilers can tack on + # massive amounts of additional arguments before passing them to the + # linker. It appears as though 1/2 is a usable value. + lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 2` + fi + ;; + esac + +fi + +if test -n "$lt_cv_sys_max_cmd_len"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_sys_max_cmd_len" >&5 +$as_echo "$lt_cv_sys_max_cmd_len" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: none" >&5 +$as_echo "none" >&6; } +fi +max_cmd_len=$lt_cv_sys_max_cmd_len + + + + + + +: ${CP="cp -f"} +: ${MV="mv -f"} +: ${RM="rm -f"} + +if ( (MAIL=60; unset MAIL) || exit) >/dev/null 2>&1; then + lt_unset=unset +else + lt_unset=false +fi + + + + + +# test EBCDIC or ASCII +case `echo X|tr X '\101'` in + A) # ASCII based system + # \n is not interpreted correctly by Solaris 8 /usr/ucb/tr + lt_SP2NL='tr \040 \012' + lt_NL2SP='tr \015\012 \040\040' + ;; + *) # EBCDIC based system + lt_SP2NL='tr \100 \n' + lt_NL2SP='tr \r\n \100\100' + ;; +esac + + + + + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking how to convert $build file names to $host format" >&5 +$as_echo_n "checking how to convert $build file names to $host format... " >&6; } +if ${lt_cv_to_host_file_cmd+:} false; then : + $as_echo_n "(cached) " >&6 +else + case $host in + *-*-mingw* ) + case $build in + *-*-mingw* ) # actually msys + lt_cv_to_host_file_cmd=func_convert_file_msys_to_w32 + ;; + *-*-cygwin* ) + lt_cv_to_host_file_cmd=func_convert_file_cygwin_to_w32 + ;; + * ) # otherwise, assume *nix + lt_cv_to_host_file_cmd=func_convert_file_nix_to_w32 + ;; + esac + ;; + *-*-cygwin* ) + case $build in + *-*-mingw* ) # actually msys + lt_cv_to_host_file_cmd=func_convert_file_msys_to_cygwin + ;; + *-*-cygwin* ) + lt_cv_to_host_file_cmd=func_convert_file_noop + ;; + * ) # otherwise, assume *nix + lt_cv_to_host_file_cmd=func_convert_file_nix_to_cygwin + ;; + esac + ;; + * ) # unhandled hosts (and "normal" native builds) + lt_cv_to_host_file_cmd=func_convert_file_noop + ;; +esac + +fi + +to_host_file_cmd=$lt_cv_to_host_file_cmd +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_to_host_file_cmd" >&5 +$as_echo "$lt_cv_to_host_file_cmd" >&6; } + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking how to convert $build file names to toolchain format" >&5 +$as_echo_n "checking how to convert $build file names to toolchain format... " >&6; } +if ${lt_cv_to_tool_file_cmd+:} false; then : + $as_echo_n "(cached) " >&6 +else + #assume ordinary cross tools, or native build. +lt_cv_to_tool_file_cmd=func_convert_file_noop +case $host in + *-*-mingw* ) + case $build in + *-*-mingw* ) # actually msys + lt_cv_to_tool_file_cmd=func_convert_file_msys_to_w32 + ;; + esac + ;; +esac + +fi + +to_tool_file_cmd=$lt_cv_to_tool_file_cmd +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_to_tool_file_cmd" >&5 +$as_echo "$lt_cv_to_tool_file_cmd" >&6; } + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $LD option to reload object files" >&5 +$as_echo_n "checking for $LD option to reload object files... " >&6; } +if ${lt_cv_ld_reload_flag+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_ld_reload_flag='-r' +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_ld_reload_flag" >&5 +$as_echo "$lt_cv_ld_reload_flag" >&6; } +reload_flag=$lt_cv_ld_reload_flag +case $reload_flag in +"" | " "*) ;; +*) reload_flag=" $reload_flag" ;; +esac +reload_cmds='$LD$reload_flag -o $output$reload_objs' +case $host_os in + cygwin* | mingw* | pw32* | cegcc*) + if test yes != "$GCC"; then + reload_cmds=false + fi + ;; + darwin*) + if test yes = "$GCC"; then + reload_cmds='$LTCC $LTCFLAGS -nostdlib $wl-r -o $output$reload_objs' + else + reload_cmds='$LD$reload_flag -o $output$reload_objs' + fi + ;; +esac + + + + + + + + + +if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}objdump", so it can be a program name with args. +set dummy ${ac_tool_prefix}objdump; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_OBJDUMP+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$OBJDUMP"; then + ac_cv_prog_OBJDUMP="$OBJDUMP" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_OBJDUMP="${ac_tool_prefix}objdump" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +OBJDUMP=$ac_cv_prog_OBJDUMP +if test -n "$OBJDUMP"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $OBJDUMP" >&5 +$as_echo "$OBJDUMP" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_OBJDUMP"; then + ac_ct_OBJDUMP=$OBJDUMP + # Extract the first word of "objdump", so it can be a program name with args. +set dummy objdump; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_OBJDUMP+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_OBJDUMP"; then + ac_cv_prog_ac_ct_OBJDUMP="$ac_ct_OBJDUMP" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_OBJDUMP="objdump" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_OBJDUMP=$ac_cv_prog_ac_ct_OBJDUMP +if test -n "$ac_ct_OBJDUMP"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_OBJDUMP" >&5 +$as_echo "$ac_ct_OBJDUMP" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_OBJDUMP" = x; then + OBJDUMP="false" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + OBJDUMP=$ac_ct_OBJDUMP + fi +else + OBJDUMP="$ac_cv_prog_OBJDUMP" +fi + +test -z "$OBJDUMP" && OBJDUMP=objdump + + + + + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking how to recognize dependent libraries" >&5 +$as_echo_n "checking how to recognize dependent libraries... " >&6; } +if ${lt_cv_deplibs_check_method+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_file_magic_cmd='$MAGIC_CMD' +lt_cv_file_magic_test_file= +lt_cv_deplibs_check_method='unknown' +# Need to set the preceding variable on all platforms that support +# interlibrary dependencies. +# 'none' -- dependencies not supported. +# 'unknown' -- same as none, but documents that we really don't know. +# 'pass_all' -- all dependencies passed with no checks. +# 'test_compile' -- check by making test program. +# 'file_magic [[regex]]' -- check by looking for files in library path +# that responds to the $file_magic_cmd with a given extended regex. +# If you have 'file' or equivalent on your system and you're not sure +# whether 'pass_all' will *always* work, you probably want this one. + +case $host_os in +aix[4-9]*) + lt_cv_deplibs_check_method=pass_all + ;; + +beos*) + lt_cv_deplibs_check_method=pass_all + ;; + +bsdi[45]*) + lt_cv_deplibs_check_method='file_magic ELF [0-9][0-9]*-bit [ML]SB (shared object|dynamic lib)' + lt_cv_file_magic_cmd='/usr/bin/file -L' + lt_cv_file_magic_test_file=/shlib/libc.so + ;; + +cygwin*) + # func_win32_libid is a shell function defined in ltmain.sh + lt_cv_deplibs_check_method='file_magic ^x86 archive import|^x86 DLL' + lt_cv_file_magic_cmd='func_win32_libid' + ;; + +mingw* | pw32*) + # Base MSYS/MinGW do not provide the 'file' command needed by + # func_win32_libid shell function, so use a weaker test based on 'objdump', + # unless we find 'file', for example because we are cross-compiling. + if ( file / ) >/dev/null 2>&1; then + lt_cv_deplibs_check_method='file_magic ^x86 archive import|^x86 DLL' + lt_cv_file_magic_cmd='func_win32_libid' + else + # Keep this pattern in sync with the one in func_win32_libid. + lt_cv_deplibs_check_method='file_magic file format (pei*-i386(.*architecture: i386)?|pe-arm-wince|pe-x86-64)' + lt_cv_file_magic_cmd='$OBJDUMP -f' + fi + ;; + +cegcc*) + # use the weaker test based on 'objdump'. See mingw*. + lt_cv_deplibs_check_method='file_magic file format pe-arm-.*little(.*architecture: arm)?' + lt_cv_file_magic_cmd='$OBJDUMP -f' + ;; + +darwin* | rhapsody*) + lt_cv_deplibs_check_method=pass_all + ;; + +freebsd* | dragonfly*) + if echo __ELF__ | $CC -E - | $GREP __ELF__ > /dev/null; then + case $host_cpu in + i*86 ) + # Not sure whether the presence of OpenBSD here was a mistake. + # Let's accept both of them until this is cleared up. + lt_cv_deplibs_check_method='file_magic (FreeBSD|OpenBSD|DragonFly)/i[3-9]86 (compact )?demand paged shared library' + lt_cv_file_magic_cmd=/usr/bin/file + lt_cv_file_magic_test_file=`echo /usr/lib/libc.so.*` + ;; + esac + else + lt_cv_deplibs_check_method=pass_all + fi + ;; + +haiku*) + lt_cv_deplibs_check_method=pass_all + ;; + +hpux10.20* | hpux11*) + lt_cv_file_magic_cmd=/usr/bin/file + case $host_cpu in + ia64*) + lt_cv_deplibs_check_method='file_magic (s[0-9][0-9][0-9]|ELF-[0-9][0-9]) shared object file - IA64' + lt_cv_file_magic_test_file=/usr/lib/hpux32/libc.so + ;; + hppa*64*) + lt_cv_deplibs_check_method='file_magic (s[0-9][0-9][0-9]|ELF[ -][0-9][0-9])(-bit)?( [LM]SB)? shared object( file)?[, -]* PA-RISC [0-9]\.[0-9]' + lt_cv_file_magic_test_file=/usr/lib/pa20_64/libc.sl + ;; + *) + lt_cv_deplibs_check_method='file_magic (s[0-9][0-9][0-9]|PA-RISC[0-9]\.[0-9]) shared library' + lt_cv_file_magic_test_file=/usr/lib/libc.sl + ;; + esac + ;; + +interix[3-9]*) + # PIC code is broken on Interix 3.x, that's why |\.a not |_pic\.a here + lt_cv_deplibs_check_method='match_pattern /lib[^/]+(\.so|\.a)$' + ;; + +irix5* | irix6* | nonstopux*) + case $LD in + *-32|*"-32 ") libmagic=32-bit;; + *-n32|*"-n32 ") libmagic=N32;; + *-64|*"-64 ") libmagic=64-bit;; + *) libmagic=never-match;; + esac + lt_cv_deplibs_check_method=pass_all + ;; + +# This must be glibc/ELF. +linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*) + lt_cv_deplibs_check_method=pass_all + ;; + +netbsd* | netbsdelf*-gnu) + if echo __ELF__ | $CC -E - | $GREP __ELF__ > /dev/null; then + lt_cv_deplibs_check_method='match_pattern /lib[^/]+(\.so\.[0-9]+\.[0-9]+|_pic\.a)$' + else + lt_cv_deplibs_check_method='match_pattern /lib[^/]+(\.so|_pic\.a)$' + fi + ;; + +newos6*) + lt_cv_deplibs_check_method='file_magic ELF [0-9][0-9]*-bit [ML]SB (executable|dynamic lib)' + lt_cv_file_magic_cmd=/usr/bin/file + lt_cv_file_magic_test_file=/usr/lib/libnls.so + ;; + +*nto* | *qnx*) + lt_cv_deplibs_check_method=pass_all + ;; + +openbsd* | bitrig*) + if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then + lt_cv_deplibs_check_method='match_pattern /lib[^/]+(\.so\.[0-9]+\.[0-9]+|\.so|_pic\.a)$' + else + lt_cv_deplibs_check_method='match_pattern /lib[^/]+(\.so\.[0-9]+\.[0-9]+|_pic\.a)$' + fi + ;; + +osf3* | osf4* | osf5*) + lt_cv_deplibs_check_method=pass_all + ;; + +rdos*) + lt_cv_deplibs_check_method=pass_all + ;; + +solaris*) + lt_cv_deplibs_check_method=pass_all + ;; + +sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX* | sysv4*uw2*) + lt_cv_deplibs_check_method=pass_all + ;; + +sysv4 | sysv4.3*) + case $host_vendor in + motorola) + lt_cv_deplibs_check_method='file_magic ELF [0-9][0-9]*-bit [ML]SB (shared object|dynamic lib) M[0-9][0-9]* Version [0-9]' + lt_cv_file_magic_test_file=`echo /usr/lib/libc.so*` + ;; + ncr) + lt_cv_deplibs_check_method=pass_all + ;; + sequent) + lt_cv_file_magic_cmd='/bin/file' + lt_cv_deplibs_check_method='file_magic ELF [0-9][0-9]*-bit [LM]SB (shared object|dynamic lib )' + ;; + sni) + lt_cv_file_magic_cmd='/bin/file' + lt_cv_deplibs_check_method="file_magic ELF [0-9][0-9]*-bit [LM]SB dynamic lib" + lt_cv_file_magic_test_file=/lib/libc.so + ;; + siemens) + lt_cv_deplibs_check_method=pass_all + ;; + pc) + lt_cv_deplibs_check_method=pass_all + ;; + esac + ;; + +tpf*) + lt_cv_deplibs_check_method=pass_all + ;; +os2*) + lt_cv_deplibs_check_method=pass_all + ;; +esac + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_deplibs_check_method" >&5 +$as_echo "$lt_cv_deplibs_check_method" >&6; } + +file_magic_glob= +want_nocaseglob=no +if test "$build" = "$host"; then + case $host_os in + mingw* | pw32*) + if ( shopt | grep nocaseglob ) >/dev/null 2>&1; then + want_nocaseglob=yes + else + file_magic_glob=`echo aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ | $SED -e "s/\(..\)/s\/[\1]\/[\1]\/g;/g"` + fi + ;; + esac +fi + +file_magic_cmd=$lt_cv_file_magic_cmd +deplibs_check_method=$lt_cv_deplibs_check_method +test -z "$deplibs_check_method" && deplibs_check_method=unknown + + + + + + + + + + + + + + + + + + + + + + +if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}dlltool", so it can be a program name with args. +set dummy ${ac_tool_prefix}dlltool; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_DLLTOOL+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$DLLTOOL"; then + ac_cv_prog_DLLTOOL="$DLLTOOL" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_DLLTOOL="${ac_tool_prefix}dlltool" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +DLLTOOL=$ac_cv_prog_DLLTOOL +if test -n "$DLLTOOL"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $DLLTOOL" >&5 +$as_echo "$DLLTOOL" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_DLLTOOL"; then + ac_ct_DLLTOOL=$DLLTOOL + # Extract the first word of "dlltool", so it can be a program name with args. +set dummy dlltool; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_DLLTOOL+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_DLLTOOL"; then + ac_cv_prog_ac_ct_DLLTOOL="$ac_ct_DLLTOOL" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_DLLTOOL="dlltool" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_DLLTOOL=$ac_cv_prog_ac_ct_DLLTOOL +if test -n "$ac_ct_DLLTOOL"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_DLLTOOL" >&5 +$as_echo "$ac_ct_DLLTOOL" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_DLLTOOL" = x; then + DLLTOOL="false" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + DLLTOOL=$ac_ct_DLLTOOL + fi +else + DLLTOOL="$ac_cv_prog_DLLTOOL" +fi + +test -z "$DLLTOOL" && DLLTOOL=dlltool + + + + + + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking how to associate runtime and link libraries" >&5 +$as_echo_n "checking how to associate runtime and link libraries... " >&6; } +if ${lt_cv_sharedlib_from_linklib_cmd+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_sharedlib_from_linklib_cmd='unknown' + +case $host_os in +cygwin* | mingw* | pw32* | cegcc*) + # two different shell functions defined in ltmain.sh; + # decide which one to use based on capabilities of $DLLTOOL + case `$DLLTOOL --help 2>&1` in + *--identify-strict*) + lt_cv_sharedlib_from_linklib_cmd=func_cygming_dll_for_implib + ;; + *) + lt_cv_sharedlib_from_linklib_cmd=func_cygming_dll_for_implib_fallback + ;; + esac + ;; +*) + # fallback: assume linklib IS sharedlib + lt_cv_sharedlib_from_linklib_cmd=$ECHO + ;; +esac + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_sharedlib_from_linklib_cmd" >&5 +$as_echo "$lt_cv_sharedlib_from_linklib_cmd" >&6; } +sharedlib_from_linklib_cmd=$lt_cv_sharedlib_from_linklib_cmd +test -z "$sharedlib_from_linklib_cmd" && sharedlib_from_linklib_cmd=$ECHO + + + + + + + + +if test -n "$ac_tool_prefix"; then + for ac_prog in ar + do + # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args. +set dummy $ac_tool_prefix$ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_AR+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$AR"; then + ac_cv_prog_AR="$AR" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_AR="$ac_tool_prefix$ac_prog" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +AR=$ac_cv_prog_AR +if test -n "$AR"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $AR" >&5 +$as_echo "$AR" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + test -n "$AR" && break + done +fi +if test -z "$AR"; then + ac_ct_AR=$AR + for ac_prog in ar +do + # Extract the first word of "$ac_prog", so it can be a program name with args. +set dummy $ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_AR+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_AR"; then + ac_cv_prog_ac_ct_AR="$ac_ct_AR" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_AR="$ac_prog" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_AR=$ac_cv_prog_ac_ct_AR +if test -n "$ac_ct_AR"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_AR" >&5 +$as_echo "$ac_ct_AR" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + test -n "$ac_ct_AR" && break +done + + if test "x$ac_ct_AR" = x; then + AR="false" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + AR=$ac_ct_AR + fi +fi + +: ${AR=ar} +: ${AR_FLAGS=cru} + + + + + + + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for archiver @FILE support" >&5 +$as_echo_n "checking for archiver @FILE support... " >&6; } +if ${lt_cv_ar_at_file+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_ar_at_file=no + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + echo conftest.$ac_objext > conftest.lst + lt_ar_try='$AR $AR_FLAGS libconftest.a @conftest.lst >&5' + { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$lt_ar_try\""; } >&5 + (eval $lt_ar_try) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } + if test 0 -eq "$ac_status"; then + # Ensure the archiver fails upon bogus file names. + rm -f conftest.$ac_objext libconftest.a + { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$lt_ar_try\""; } >&5 + (eval $lt_ar_try) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } + if test 0 -ne "$ac_status"; then + lt_cv_ar_at_file=@ + fi + fi + rm -f conftest.* libconftest.a + +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_ar_at_file" >&5 +$as_echo "$lt_cv_ar_at_file" >&6; } + +if test no = "$lt_cv_ar_at_file"; then + archiver_list_spec= +else + archiver_list_spec=$lt_cv_ar_at_file +fi + + + + + + + +if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}strip", so it can be a program name with args. +set dummy ${ac_tool_prefix}strip; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_STRIP+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$STRIP"; then + ac_cv_prog_STRIP="$STRIP" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_STRIP="${ac_tool_prefix}strip" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +STRIP=$ac_cv_prog_STRIP +if test -n "$STRIP"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $STRIP" >&5 +$as_echo "$STRIP" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_STRIP"; then + ac_ct_STRIP=$STRIP + # Extract the first word of "strip", so it can be a program name with args. +set dummy strip; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_STRIP+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_STRIP"; then + ac_cv_prog_ac_ct_STRIP="$ac_ct_STRIP" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_STRIP="strip" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_STRIP=$ac_cv_prog_ac_ct_STRIP +if test -n "$ac_ct_STRIP"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_STRIP" >&5 +$as_echo "$ac_ct_STRIP" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_STRIP" = x; then + STRIP=":" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + STRIP=$ac_ct_STRIP + fi +else + STRIP="$ac_cv_prog_STRIP" +fi + +test -z "$STRIP" && STRIP=: + + + + + + +if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}ranlib", so it can be a program name with args. +set dummy ${ac_tool_prefix}ranlib; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_RANLIB+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$RANLIB"; then + ac_cv_prog_RANLIB="$RANLIB" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_RANLIB="${ac_tool_prefix}ranlib" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +RANLIB=$ac_cv_prog_RANLIB +if test -n "$RANLIB"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $RANLIB" >&5 +$as_echo "$RANLIB" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_RANLIB"; then + ac_ct_RANLIB=$RANLIB + # Extract the first word of "ranlib", so it can be a program name with args. +set dummy ranlib; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_RANLIB+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_RANLIB"; then + ac_cv_prog_ac_ct_RANLIB="$ac_ct_RANLIB" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_RANLIB="ranlib" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_RANLIB=$ac_cv_prog_ac_ct_RANLIB +if test -n "$ac_ct_RANLIB"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_RANLIB" >&5 +$as_echo "$ac_ct_RANLIB" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_RANLIB" = x; then + RANLIB=":" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + RANLIB=$ac_ct_RANLIB + fi +else + RANLIB="$ac_cv_prog_RANLIB" +fi + +test -z "$RANLIB" && RANLIB=: + + + + + + +# Determine commands to create old-style static archives. +old_archive_cmds='$AR $AR_FLAGS $oldlib$oldobjs' +old_postinstall_cmds='chmod 644 $oldlib' +old_postuninstall_cmds= + +if test -n "$RANLIB"; then + case $host_os in + bitrig* | openbsd*) + old_postinstall_cmds="$old_postinstall_cmds~\$RANLIB -t \$tool_oldlib" + ;; + *) + old_postinstall_cmds="$old_postinstall_cmds~\$RANLIB \$tool_oldlib" + ;; + esac + old_archive_cmds="$old_archive_cmds~\$RANLIB \$tool_oldlib" +fi + +case $host_os in + darwin*) + lock_old_archive_extraction=yes ;; + *) + lock_old_archive_extraction=no ;; +esac + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +# If no C compiler was specified, use CC. +LTCC=${LTCC-"$CC"} + +# If no C compiler flags were specified, use CFLAGS. +LTCFLAGS=${LTCFLAGS-"$CFLAGS"} + +# Allow CC to be a program name with arguments. +compiler=$CC + + +# Check for command to grab the raw symbol name followed by C symbol from nm. +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking command to parse $NM output from $compiler object" >&5 +$as_echo_n "checking command to parse $NM output from $compiler object... " >&6; } +if ${lt_cv_sys_global_symbol_pipe+:} false; then : + $as_echo_n "(cached) " >&6 +else + +# These are sane defaults that work on at least a few old systems. +# [They come from Ultrix. What could be older than Ultrix?!! ;)] + +# Character class describing NM global symbol codes. +symcode='[BCDEGRST]' + +# Regexp to match symbols that can be accessed directly from C. +sympat='\([_A-Za-z][_A-Za-z0-9]*\)' + +# Define system-specific variables. +case $host_os in +aix*) + symcode='[BCDT]' + ;; +cygwin* | mingw* | pw32* | cegcc*) + symcode='[ABCDGISTW]' + ;; +hpux*) + if test ia64 = "$host_cpu"; then + symcode='[ABCDEGRST]' + fi + ;; +irix* | nonstopux*) + symcode='[BCDEGRST]' + ;; +osf*) + symcode='[BCDEGQRST]' + ;; +solaris*) + symcode='[BDRT]' + ;; +sco3.2v5*) + symcode='[DT]' + ;; +sysv4.2uw2*) + symcode='[DT]' + ;; +sysv5* | sco5v6* | unixware* | OpenUNIX*) + symcode='[ABDT]' + ;; +sysv4) + symcode='[DFNSTU]' + ;; +esac + +# If we're using GNU nm, then use its standard symbol codes. +case `$NM -V 2>&1` in +*GNU* | *'with BFD'*) + symcode='[ABCDGIRSTW]' ;; +esac + +if test "$lt_cv_nm_interface" = "MS dumpbin"; then + # Gets list of data symbols to import. + lt_cv_sys_global_symbol_to_import="sed -n -e 's/^I .* \(.*\)$/\1/p'" + # Adjust the below global symbol transforms to fixup imported variables. + lt_cdecl_hook=" -e 's/^I .* \(.*\)$/extern __declspec(dllimport) char \1;/p'" + lt_c_name_hook=" -e 's/^I .* \(.*\)$/ {\"\1\", (void *) 0},/p'" + lt_c_name_lib_hook="\ + -e 's/^I .* \(lib.*\)$/ {\"\1\", (void *) 0},/p'\ + -e 's/^I .* \(.*\)$/ {\"lib\1\", (void *) 0},/p'" +else + # Disable hooks by default. + lt_cv_sys_global_symbol_to_import= + lt_cdecl_hook= + lt_c_name_hook= + lt_c_name_lib_hook= +fi + +# Transform an extracted symbol line into a proper C declaration. +# Some systems (esp. on ia64) link data and code symbols differently, +# so use this general approach. +lt_cv_sys_global_symbol_to_cdecl="sed -n"\ +$lt_cdecl_hook\ +" -e 's/^T .* \(.*\)$/extern int \1();/p'"\ +" -e 's/^$symcode$symcode* .* \(.*\)$/extern char \1;/p'" + +# Transform an extracted symbol line into symbol name and symbol address +lt_cv_sys_global_symbol_to_c_name_address="sed -n"\ +$lt_c_name_hook\ +" -e 's/^: \(.*\) .*$/ {\"\1\", (void *) 0},/p'"\ +" -e 's/^$symcode$symcode* .* \(.*\)$/ {\"\1\", (void *) \&\1},/p'" + +# Transform an extracted symbol line into symbol name with lib prefix and +# symbol address. +lt_cv_sys_global_symbol_to_c_name_address_lib_prefix="sed -n"\ +$lt_c_name_lib_hook\ +" -e 's/^: \(.*\) .*$/ {\"\1\", (void *) 0},/p'"\ +" -e 's/^$symcode$symcode* .* \(lib.*\)$/ {\"\1\", (void *) \&\1},/p'"\ +" -e 's/^$symcode$symcode* .* \(.*\)$/ {\"lib\1\", (void *) \&\1},/p'" + +# Handle CRLF in mingw tool chain +opt_cr= +case $build_os in +mingw*) + opt_cr=`$ECHO 'x\{0,1\}' | tr x '\015'` # option cr in regexp + ;; +esac + +# Try without a prefix underscore, then with it. +for ac_symprfx in "" "_"; do + + # Transform symcode, sympat, and symprfx into a raw symbol and a C symbol. + symxfrm="\\1 $ac_symprfx\\2 \\2" + + # Write the raw and C identifiers. + if test "$lt_cv_nm_interface" = "MS dumpbin"; then + # Fake it for dumpbin and say T for any non-static function, + # D for any global variable and I for any imported variable. + # Also find C++ and __fastcall symbols from MSVC++, + # which start with @ or ?. + lt_cv_sys_global_symbol_pipe="$AWK '"\ +" {last_section=section; section=\$ 3};"\ +" /^COFF SYMBOL TABLE/{for(i in hide) delete hide[i]};"\ +" /Section length .*#relocs.*(pick any)/{hide[last_section]=1};"\ +" /^ *Symbol name *: /{split(\$ 0,sn,\":\"); si=substr(sn[2],2)};"\ +" /^ *Type *: code/{print \"T\",si,substr(si,length(prfx))};"\ +" /^ *Type *: data/{print \"I\",si,substr(si,length(prfx))};"\ +" \$ 0!~/External *\|/{next};"\ +" / 0+ UNDEF /{next}; / UNDEF \([^|]\)*()/{next};"\ +" {if(hide[section]) next};"\ +" {f=\"D\"}; \$ 0~/\(\).*\|/{f=\"T\"};"\ +" {split(\$ 0,a,/\||\r/); split(a[2],s)};"\ +" s[1]~/^[@?]/{print f,s[1],s[1]; next};"\ +" s[1]~prfx {split(s[1],t,\"@\"); print f,t[1],substr(t[1],length(prfx))}"\ +" ' prfx=^$ac_symprfx" + else + lt_cv_sys_global_symbol_pipe="sed -n -e 's/^.*[ ]\($symcode$symcode*\)[ ][ ]*$ac_symprfx$sympat$opt_cr$/$symxfrm/p'" + fi + lt_cv_sys_global_symbol_pipe="$lt_cv_sys_global_symbol_pipe | sed '/ __gnu_lto/d'" + + # Check to see that the pipe works correctly. + pipe_works=no + + rm -f conftest* + cat > conftest.$ac_ext <<_LT_EOF +#ifdef __cplusplus +extern "C" { +#endif +char nm_test_var; +void nm_test_func(void); +void nm_test_func(void){} +#ifdef __cplusplus +} +#endif +int main(){nm_test_var='a';nm_test_func();return(0);} +_LT_EOF + + if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5 + (eval $ac_compile) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + # Now try to grab the symbols. + nlist=conftest.nm + $ECHO "$as_me:$LINENO: $NM conftest.$ac_objext | $lt_cv_sys_global_symbol_pipe > $nlist" >&5 + if eval "$NM" conftest.$ac_objext \| "$lt_cv_sys_global_symbol_pipe" \> $nlist 2>&5 && test -s "$nlist"; then + # Try sorting and uniquifying the output. + if sort "$nlist" | uniq > "$nlist"T; then + mv -f "$nlist"T "$nlist" + else + rm -f "$nlist"T + fi + + # Make sure that we snagged all the symbols we need. + if $GREP ' nm_test_var$' "$nlist" >/dev/null; then + if $GREP ' nm_test_func$' "$nlist" >/dev/null; then + cat <<_LT_EOF > conftest.$ac_ext +/* Keep this code in sync between libtool.m4, ltmain, lt_system.h, and tests. */ +#if defined _WIN32 || defined __CYGWIN__ || defined _WIN32_WCE +/* DATA imports from DLLs on WIN32 can't be const, because runtime + relocations are performed -- see ld's documentation on pseudo-relocs. */ +# define LT_DLSYM_CONST +#elif defined __osf__ +/* This system does not cope well with relocations in const data. */ +# define LT_DLSYM_CONST +#else +# define LT_DLSYM_CONST const +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +_LT_EOF + # Now generate the symbol file. + eval "$lt_cv_sys_global_symbol_to_cdecl"' < "$nlist" | $GREP -v main >> conftest.$ac_ext' + + cat <<_LT_EOF >> conftest.$ac_ext + +/* The mapping between symbol names and symbols. */ +LT_DLSYM_CONST struct { + const char *name; + void *address; +} +lt__PROGRAM__LTX_preloaded_symbols[] = +{ + { "@PROGRAM@", (void *) 0 }, +_LT_EOF + $SED "s/^$symcode$symcode* .* \(.*\)$/ {\"\1\", (void *) \&\1},/" < "$nlist" | $GREP -v main >> conftest.$ac_ext + cat <<\_LT_EOF >> conftest.$ac_ext + {0, (void *) 0} +}; + +/* This works around a problem in FreeBSD linker */ +#ifdef FREEBSD_WORKAROUND +static const void *lt_preloaded_setup() { + return lt__PROGRAM__LTX_preloaded_symbols; +} +#endif + +#ifdef __cplusplus +} +#endif +_LT_EOF + # Now try linking the two files. + mv conftest.$ac_objext conftstm.$ac_objext + lt_globsym_save_LIBS=$LIBS + lt_globsym_save_CFLAGS=$CFLAGS + LIBS=conftstm.$ac_objext + CFLAGS="$CFLAGS$lt_prog_compiler_no_builtin_flag" + if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_link\""; } >&5 + (eval $ac_link) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } && test -s conftest$ac_exeext; then + pipe_works=yes + fi + LIBS=$lt_globsym_save_LIBS + CFLAGS=$lt_globsym_save_CFLAGS + else + echo "cannot find nm_test_func in $nlist" >&5 + fi + else + echo "cannot find nm_test_var in $nlist" >&5 + fi + else + echo "cannot run $lt_cv_sys_global_symbol_pipe" >&5 + fi + else + echo "$progname: failed program was:" >&5 + cat conftest.$ac_ext >&5 + fi + rm -rf conftest* conftst* + + # Do not use the global_symbol_pipe unless it works. + if test yes = "$pipe_works"; then + break + else + lt_cv_sys_global_symbol_pipe= + fi +done + +fi + +if test -z "$lt_cv_sys_global_symbol_pipe"; then + lt_cv_sys_global_symbol_to_cdecl= +fi +if test -z "$lt_cv_sys_global_symbol_pipe$lt_cv_sys_global_symbol_to_cdecl"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: failed" >&5 +$as_echo "failed" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: ok" >&5 +$as_echo "ok" >&6; } +fi + +# Response file support. +if test "$lt_cv_nm_interface" = "MS dumpbin"; then + nm_file_list_spec='@' +elif $NM --help 2>/dev/null | grep '[@]FILE' >/dev/null; then + nm_file_list_spec='@' +fi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for sysroot" >&5 +$as_echo_n "checking for sysroot... " >&6; } + +# Check whether --with-sysroot was given. +if test "${with_sysroot+set}" = set; then : + withval=$with_sysroot; +else + with_sysroot=no +fi + + +lt_sysroot= +case $with_sysroot in #( + yes) + if test yes = "$GCC"; then + lt_sysroot=`$CC --print-sysroot 2>/dev/null` + fi + ;; #( + /*) + lt_sysroot=`echo "$with_sysroot" | sed -e "$sed_quote_subst"` + ;; #( + no|'') + ;; #( + *) + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_sysroot" >&5 +$as_echo "$with_sysroot" >&6; } + as_fn_error $? "The sysroot must be an absolute path." "$LINENO" 5 + ;; +esac + + { $as_echo "$as_me:${as_lineno-$LINENO}: result: ${lt_sysroot:-no}" >&5 +$as_echo "${lt_sysroot:-no}" >&6; } + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for a working dd" >&5 +$as_echo_n "checking for a working dd... " >&6; } +if ${ac_cv_path_lt_DD+:} false; then : + $as_echo_n "(cached) " >&6 +else + printf 0123456789abcdef0123456789abcdef >conftest.i +cat conftest.i conftest.i >conftest2.i +: ${lt_DD:=$DD} +if test -z "$lt_DD"; then + ac_path_lt_DD_found=false + # Loop through the user's path and test for each of PROGNAME-LIST + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_prog in dd; do + for ac_exec_ext in '' $ac_executable_extensions; do + ac_path_lt_DD="$as_dir/$ac_prog$ac_exec_ext" + as_fn_executable_p "$ac_path_lt_DD" || continue +if "$ac_path_lt_DD" bs=32 count=1 conftest.out 2>/dev/null; then + cmp -s conftest.i conftest.out \ + && ac_cv_path_lt_DD="$ac_path_lt_DD" ac_path_lt_DD_found=: +fi + $ac_path_lt_DD_found && break 3 + done + done + done +IFS=$as_save_IFS + if test -z "$ac_cv_path_lt_DD"; then + : + fi +else + ac_cv_path_lt_DD=$lt_DD +fi + +rm -f conftest.i conftest2.i conftest.out +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_lt_DD" >&5 +$as_echo "$ac_cv_path_lt_DD" >&6; } + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking how to truncate binary pipes" >&5 +$as_echo_n "checking how to truncate binary pipes... " >&6; } +if ${lt_cv_truncate_bin+:} false; then : + $as_echo_n "(cached) " >&6 +else + printf 0123456789abcdef0123456789abcdef >conftest.i +cat conftest.i conftest.i >conftest2.i +lt_cv_truncate_bin= +if "$ac_cv_path_lt_DD" bs=32 count=1 conftest.out 2>/dev/null; then + cmp -s conftest.i conftest.out \ + && lt_cv_truncate_bin="$ac_cv_path_lt_DD bs=4096 count=1" +fi +rm -f conftest.i conftest2.i conftest.out +test -z "$lt_cv_truncate_bin" && lt_cv_truncate_bin="$SED -e 4q" +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_truncate_bin" >&5 +$as_echo "$lt_cv_truncate_bin" >&6; } + + + + + + + +# Calculate cc_basename. Skip known compiler wrappers and cross-prefix. +func_cc_basename () +{ + for cc_temp in $*""; do + case $cc_temp in + compile | *[\\/]compile | ccache | *[\\/]ccache ) ;; + distcc | *[\\/]distcc | purify | *[\\/]purify ) ;; + \-*) ;; + *) break;; + esac + done + func_cc_basename_result=`$ECHO "$cc_temp" | $SED "s%.*/%%; s%^$host_alias-%%"` +} + +# Check whether --enable-libtool-lock was given. +if test "${enable_libtool_lock+set}" = set; then : + enableval=$enable_libtool_lock; +fi + +test no = "$enable_libtool_lock" || enable_libtool_lock=yes + +# Some flags need to be propagated to the compiler or linker for good +# libtool support. +case $host in +ia64-*-hpux*) + # Find out what ABI is being produced by ac_compile, and set mode + # options accordingly. + echo 'int i;' > conftest.$ac_ext + if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5 + (eval $ac_compile) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + case `/usr/bin/file conftest.$ac_objext` in + *ELF-32*) + HPUX_IA64_MODE=32 + ;; + *ELF-64*) + HPUX_IA64_MODE=64 + ;; + esac + fi + rm -rf conftest* + ;; +*-*-irix6*) + # Find out what ABI is being produced by ac_compile, and set linker + # options accordingly. + echo '#line '$LINENO' "configure"' > conftest.$ac_ext + if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5 + (eval $ac_compile) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + if test yes = "$lt_cv_prog_gnu_ld"; then + case `/usr/bin/file conftest.$ac_objext` in + *32-bit*) + LD="${LD-ld} -melf32bsmip" + ;; + *N32*) + LD="${LD-ld} -melf32bmipn32" + ;; + *64-bit*) + LD="${LD-ld} -melf64bmip" + ;; + esac + else + case `/usr/bin/file conftest.$ac_objext` in + *32-bit*) + LD="${LD-ld} -32" + ;; + *N32*) + LD="${LD-ld} -n32" + ;; + *64-bit*) + LD="${LD-ld} -64" + ;; + esac + fi + fi + rm -rf conftest* + ;; + +mips64*-*linux*) + # Find out what ABI is being produced by ac_compile, and set linker + # options accordingly. + echo '#line '$LINENO' "configure"' > conftest.$ac_ext + if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5 + (eval $ac_compile) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + emul=elf + case `/usr/bin/file conftest.$ac_objext` in + *32-bit*) + emul="${emul}32" + ;; + *64-bit*) + emul="${emul}64" + ;; + esac + case `/usr/bin/file conftest.$ac_objext` in + *MSB*) + emul="${emul}btsmip" + ;; + *LSB*) + emul="${emul}ltsmip" + ;; + esac + case `/usr/bin/file conftest.$ac_objext` in + *N32*) + emul="${emul}n32" + ;; + esac + LD="${LD-ld} -m $emul" + fi + rm -rf conftest* + ;; + +x86_64-*kfreebsd*-gnu|x86_64-*linux*|powerpc*-*linux*| \ +s390*-*linux*|s390*-*tpf*|sparc*-*linux*) + # Find out what ABI is being produced by ac_compile, and set linker + # options accordingly. Note that the listed cases only cover the + # situations where additional linker options are needed (such as when + # doing 32-bit compilation for a host where ld defaults to 64-bit, or + # vice versa); the common cases where no linker options are needed do + # not appear in the list. + echo 'int i;' > conftest.$ac_ext + if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5 + (eval $ac_compile) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + case `/usr/bin/file conftest.o` in + *32-bit*) + case $host in + x86_64-*kfreebsd*-gnu) + LD="${LD-ld} -m elf_i386_fbsd" + ;; + x86_64-*linux*) + case `/usr/bin/file conftest.o` in + *x86-64*) + LD="${LD-ld} -m elf32_x86_64" + ;; + *) + LD="${LD-ld} -m elf_i386" + ;; + esac + ;; + powerpc64le-*linux*) + LD="${LD-ld} -m elf32lppclinux" + ;; + powerpc64-*linux*) + LD="${LD-ld} -m elf32ppclinux" + ;; + s390x-*linux*) + LD="${LD-ld} -m elf_s390" + ;; + sparc64-*linux*) + LD="${LD-ld} -m elf32_sparc" + ;; + esac + ;; + *64-bit*) + case $host in + x86_64-*kfreebsd*-gnu) + LD="${LD-ld} -m elf_x86_64_fbsd" + ;; + x86_64-*linux*) + LD="${LD-ld} -m elf_x86_64" + ;; + powerpcle-*linux*) + LD="${LD-ld} -m elf64lppc" + ;; + powerpc-*linux*) + LD="${LD-ld} -m elf64ppc" + ;; + s390*-*linux*|s390*-*tpf*) + LD="${LD-ld} -m elf64_s390" + ;; + sparc*-*linux*) + LD="${LD-ld} -m elf64_sparc" + ;; + esac + ;; + esac + fi + rm -rf conftest* + ;; + +*-*-sco3.2v5*) + # On SCO OpenServer 5, we need -belf to get full-featured binaries. + SAVE_CFLAGS=$CFLAGS + CFLAGS="$CFLAGS -belf" + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether the C compiler needs -belf" >&5 +$as_echo_n "checking whether the C compiler needs -belf... " >&6; } +if ${lt_cv_cc_needs_belf+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + lt_cv_cc_needs_belf=yes +else + lt_cv_cc_needs_belf=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext + ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_cc_needs_belf" >&5 +$as_echo "$lt_cv_cc_needs_belf" >&6; } + if test yes != "$lt_cv_cc_needs_belf"; then + # this is probably gcc 2.8.0, egcs 1.0 or newer; no need for -belf + CFLAGS=$SAVE_CFLAGS + fi + ;; +*-*solaris*) + # Find out what ABI is being produced by ac_compile, and set linker + # options accordingly. + echo 'int i;' > conftest.$ac_ext + if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5 + (eval $ac_compile) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + case `/usr/bin/file conftest.o` in + *64-bit*) + case $lt_cv_prog_gnu_ld in + yes*) + case $host in + i?86-*-solaris*|x86_64-*-solaris*) + LD="${LD-ld} -m elf_x86_64" + ;; + sparc*-*-solaris*) + LD="${LD-ld} -m elf64_sparc" + ;; + esac + # GNU ld 2.21 introduced _sol2 emulations. Use them if available. + if ${LD-ld} -V | grep _sol2 >/dev/null 2>&1; then + LD=${LD-ld}_sol2 + fi + ;; + *) + if ${LD-ld} -64 -r -o conftest2.o conftest.o >/dev/null 2>&1; then + LD="${LD-ld} -64" + fi + ;; + esac + ;; + esac + fi + rm -rf conftest* + ;; +esac + +need_locks=$enable_libtool_lock + +if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}mt", so it can be a program name with args. +set dummy ${ac_tool_prefix}mt; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_MANIFEST_TOOL+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$MANIFEST_TOOL"; then + ac_cv_prog_MANIFEST_TOOL="$MANIFEST_TOOL" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_MANIFEST_TOOL="${ac_tool_prefix}mt" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +MANIFEST_TOOL=$ac_cv_prog_MANIFEST_TOOL +if test -n "$MANIFEST_TOOL"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $MANIFEST_TOOL" >&5 +$as_echo "$MANIFEST_TOOL" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_MANIFEST_TOOL"; then + ac_ct_MANIFEST_TOOL=$MANIFEST_TOOL + # Extract the first word of "mt", so it can be a program name with args. +set dummy mt; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_MANIFEST_TOOL+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_MANIFEST_TOOL"; then + ac_cv_prog_ac_ct_MANIFEST_TOOL="$ac_ct_MANIFEST_TOOL" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_MANIFEST_TOOL="mt" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_MANIFEST_TOOL=$ac_cv_prog_ac_ct_MANIFEST_TOOL +if test -n "$ac_ct_MANIFEST_TOOL"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_MANIFEST_TOOL" >&5 +$as_echo "$ac_ct_MANIFEST_TOOL" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_MANIFEST_TOOL" = x; then + MANIFEST_TOOL=":" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + MANIFEST_TOOL=$ac_ct_MANIFEST_TOOL + fi +else + MANIFEST_TOOL="$ac_cv_prog_MANIFEST_TOOL" +fi + +test -z "$MANIFEST_TOOL" && MANIFEST_TOOL=mt +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking if $MANIFEST_TOOL is a manifest tool" >&5 +$as_echo_n "checking if $MANIFEST_TOOL is a manifest tool... " >&6; } +if ${lt_cv_path_mainfest_tool+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_path_mainfest_tool=no + echo "$as_me:$LINENO: $MANIFEST_TOOL '-?'" >&5 + $MANIFEST_TOOL '-?' 2>conftest.err > conftest.out + cat conftest.err >&5 + if $GREP 'Manifest Tool' conftest.out > /dev/null; then + lt_cv_path_mainfest_tool=yes + fi + rm -f conftest* +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_path_mainfest_tool" >&5 +$as_echo "$lt_cv_path_mainfest_tool" >&6; } +if test yes != "$lt_cv_path_mainfest_tool"; then + MANIFEST_TOOL=: +fi + + + + + + + case $host_os in + rhapsody* | darwin*) + if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}dsymutil", so it can be a program name with args. +set dummy ${ac_tool_prefix}dsymutil; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_DSYMUTIL+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$DSYMUTIL"; then + ac_cv_prog_DSYMUTIL="$DSYMUTIL" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_DSYMUTIL="${ac_tool_prefix}dsymutil" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +DSYMUTIL=$ac_cv_prog_DSYMUTIL +if test -n "$DSYMUTIL"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $DSYMUTIL" >&5 +$as_echo "$DSYMUTIL" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_DSYMUTIL"; then + ac_ct_DSYMUTIL=$DSYMUTIL + # Extract the first word of "dsymutil", so it can be a program name with args. +set dummy dsymutil; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_DSYMUTIL+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_DSYMUTIL"; then + ac_cv_prog_ac_ct_DSYMUTIL="$ac_ct_DSYMUTIL" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_DSYMUTIL="dsymutil" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_DSYMUTIL=$ac_cv_prog_ac_ct_DSYMUTIL +if test -n "$ac_ct_DSYMUTIL"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_DSYMUTIL" >&5 +$as_echo "$ac_ct_DSYMUTIL" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_DSYMUTIL" = x; then + DSYMUTIL=":" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + DSYMUTIL=$ac_ct_DSYMUTIL + fi +else + DSYMUTIL="$ac_cv_prog_DSYMUTIL" +fi + + if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}nmedit", so it can be a program name with args. +set dummy ${ac_tool_prefix}nmedit; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_NMEDIT+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$NMEDIT"; then + ac_cv_prog_NMEDIT="$NMEDIT" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_NMEDIT="${ac_tool_prefix}nmedit" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +NMEDIT=$ac_cv_prog_NMEDIT +if test -n "$NMEDIT"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $NMEDIT" >&5 +$as_echo "$NMEDIT" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_NMEDIT"; then + ac_ct_NMEDIT=$NMEDIT + # Extract the first word of "nmedit", so it can be a program name with args. +set dummy nmedit; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_NMEDIT+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_NMEDIT"; then + ac_cv_prog_ac_ct_NMEDIT="$ac_ct_NMEDIT" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_NMEDIT="nmedit" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_NMEDIT=$ac_cv_prog_ac_ct_NMEDIT +if test -n "$ac_ct_NMEDIT"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_NMEDIT" >&5 +$as_echo "$ac_ct_NMEDIT" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_NMEDIT" = x; then + NMEDIT=":" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + NMEDIT=$ac_ct_NMEDIT + fi +else + NMEDIT="$ac_cv_prog_NMEDIT" +fi + + if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}lipo", so it can be a program name with args. +set dummy ${ac_tool_prefix}lipo; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_LIPO+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$LIPO"; then + ac_cv_prog_LIPO="$LIPO" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_LIPO="${ac_tool_prefix}lipo" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +LIPO=$ac_cv_prog_LIPO +if test -n "$LIPO"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $LIPO" >&5 +$as_echo "$LIPO" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_LIPO"; then + ac_ct_LIPO=$LIPO + # Extract the first word of "lipo", so it can be a program name with args. +set dummy lipo; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_LIPO+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_LIPO"; then + ac_cv_prog_ac_ct_LIPO="$ac_ct_LIPO" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_LIPO="lipo" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_LIPO=$ac_cv_prog_ac_ct_LIPO +if test -n "$ac_ct_LIPO"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_LIPO" >&5 +$as_echo "$ac_ct_LIPO" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_LIPO" = x; then + LIPO=":" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + LIPO=$ac_ct_LIPO + fi +else + LIPO="$ac_cv_prog_LIPO" +fi + + if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}otool", so it can be a program name with args. +set dummy ${ac_tool_prefix}otool; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_OTOOL+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$OTOOL"; then + ac_cv_prog_OTOOL="$OTOOL" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_OTOOL="${ac_tool_prefix}otool" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +OTOOL=$ac_cv_prog_OTOOL +if test -n "$OTOOL"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $OTOOL" >&5 +$as_echo "$OTOOL" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_OTOOL"; then + ac_ct_OTOOL=$OTOOL + # Extract the first word of "otool", so it can be a program name with args. +set dummy otool; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_OTOOL+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_OTOOL"; then + ac_cv_prog_ac_ct_OTOOL="$ac_ct_OTOOL" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_OTOOL="otool" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_OTOOL=$ac_cv_prog_ac_ct_OTOOL +if test -n "$ac_ct_OTOOL"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_OTOOL" >&5 +$as_echo "$ac_ct_OTOOL" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_OTOOL" = x; then + OTOOL=":" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + OTOOL=$ac_ct_OTOOL + fi +else + OTOOL="$ac_cv_prog_OTOOL" +fi + + if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}otool64", so it can be a program name with args. +set dummy ${ac_tool_prefix}otool64; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_OTOOL64+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$OTOOL64"; then + ac_cv_prog_OTOOL64="$OTOOL64" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_OTOOL64="${ac_tool_prefix}otool64" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +OTOOL64=$ac_cv_prog_OTOOL64 +if test -n "$OTOOL64"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $OTOOL64" >&5 +$as_echo "$OTOOL64" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_OTOOL64"; then + ac_ct_OTOOL64=$OTOOL64 + # Extract the first word of "otool64", so it can be a program name with args. +set dummy otool64; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_OTOOL64+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_OTOOL64"; then + ac_cv_prog_ac_ct_OTOOL64="$ac_ct_OTOOL64" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_OTOOL64="otool64" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_OTOOL64=$ac_cv_prog_ac_ct_OTOOL64 +if test -n "$ac_ct_OTOOL64"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_OTOOL64" >&5 +$as_echo "$ac_ct_OTOOL64" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_OTOOL64" = x; then + OTOOL64=":" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + OTOOL64=$ac_ct_OTOOL64 + fi +else + OTOOL64="$ac_cv_prog_OTOOL64" +fi + + + + + + + + + + + + + + + + + + + + + + + + + + + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for -single_module linker flag" >&5 +$as_echo_n "checking for -single_module linker flag... " >&6; } +if ${lt_cv_apple_cc_single_mod+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_apple_cc_single_mod=no + if test -z "$LT_MULTI_MODULE"; then + # By default we will add the -single_module flag. You can override + # by either setting the environment variable LT_MULTI_MODULE + # non-empty at configure time, or by adding -multi_module to the + # link flags. + rm -rf libconftest.dylib* + echo "int foo(void){return 1;}" > conftest.c + echo "$LTCC $LTCFLAGS $LDFLAGS -o libconftest.dylib \ +-dynamiclib -Wl,-single_module conftest.c" >&5 + $LTCC $LTCFLAGS $LDFLAGS -o libconftest.dylib \ + -dynamiclib -Wl,-single_module conftest.c 2>conftest.err + _lt_result=$? + # If there is a non-empty error log, and "single_module" + # appears in it, assume the flag caused a linker warning + if test -s conftest.err && $GREP single_module conftest.err; then + cat conftest.err >&5 + # Otherwise, if the output was created with a 0 exit code from + # the compiler, it worked. + elif test -f libconftest.dylib && test 0 = "$_lt_result"; then + lt_cv_apple_cc_single_mod=yes + else + cat conftest.err >&5 + fi + rm -rf libconftest.dylib* + rm -f conftest.* + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_apple_cc_single_mod" >&5 +$as_echo "$lt_cv_apple_cc_single_mod" >&6; } + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for -exported_symbols_list linker flag" >&5 +$as_echo_n "checking for -exported_symbols_list linker flag... " >&6; } +if ${lt_cv_ld_exported_symbols_list+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_ld_exported_symbols_list=no + save_LDFLAGS=$LDFLAGS + echo "_main" > conftest.sym + LDFLAGS="$LDFLAGS -Wl,-exported_symbols_list,conftest.sym" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + lt_cv_ld_exported_symbols_list=yes +else + lt_cv_ld_exported_symbols_list=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext + LDFLAGS=$save_LDFLAGS + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_ld_exported_symbols_list" >&5 +$as_echo "$lt_cv_ld_exported_symbols_list" >&6; } + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for -force_load linker flag" >&5 +$as_echo_n "checking for -force_load linker flag... " >&6; } +if ${lt_cv_ld_force_load+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_ld_force_load=no + cat > conftest.c << _LT_EOF +int forced_loaded() { return 2;} +_LT_EOF + echo "$LTCC $LTCFLAGS -c -o conftest.o conftest.c" >&5 + $LTCC $LTCFLAGS -c -o conftest.o conftest.c 2>&5 + echo "$AR cru libconftest.a conftest.o" >&5 + $AR cru libconftest.a conftest.o 2>&5 + echo "$RANLIB libconftest.a" >&5 + $RANLIB libconftest.a 2>&5 + cat > conftest.c << _LT_EOF +int main() { return 0;} +_LT_EOF + echo "$LTCC $LTCFLAGS $LDFLAGS -o conftest conftest.c -Wl,-force_load,./libconftest.a" >&5 + $LTCC $LTCFLAGS $LDFLAGS -o conftest conftest.c -Wl,-force_load,./libconftest.a 2>conftest.err + _lt_result=$? + if test -s conftest.err && $GREP force_load conftest.err; then + cat conftest.err >&5 + elif test -f conftest && test 0 = "$_lt_result" && $GREP forced_load conftest >/dev/null 2>&1; then + lt_cv_ld_force_load=yes + else + cat conftest.err >&5 + fi + rm -f conftest.err libconftest.a conftest conftest.c + rm -rf conftest.dSYM + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_ld_force_load" >&5 +$as_echo "$lt_cv_ld_force_load" >&6; } + case $host_os in + rhapsody* | darwin1.[012]) + _lt_dar_allow_undefined='$wl-undefined ${wl}suppress' ;; + darwin1.*) + _lt_dar_allow_undefined='$wl-flat_namespace $wl-undefined ${wl}suppress' ;; + darwin*) # darwin 5.x on + # if running on 10.5 or later, the deployment target defaults + # to the OS version, if on x86, and 10.4, the deployment + # target defaults to 10.4. Don't you love it? + case ${MACOSX_DEPLOYMENT_TARGET-10.0},$host in + 10.0,*86*-darwin8*|10.0,*-darwin[91]*) + _lt_dar_allow_undefined='$wl-undefined ${wl}dynamic_lookup' ;; + 10.[012][,.]*) + _lt_dar_allow_undefined='$wl-flat_namespace $wl-undefined ${wl}suppress' ;; + 10.*) + _lt_dar_allow_undefined='$wl-undefined ${wl}dynamic_lookup' ;; + esac + ;; + esac + if test yes = "$lt_cv_apple_cc_single_mod"; then + _lt_dar_single_mod='$single_module' + fi + if test yes = "$lt_cv_ld_exported_symbols_list"; then + _lt_dar_export_syms=' $wl-exported_symbols_list,$output_objdir/$libname-symbols.expsym' + else + _lt_dar_export_syms='~$NMEDIT -s $output_objdir/$libname-symbols.expsym $lib' + fi + if test : != "$DSYMUTIL" && test no = "$lt_cv_ld_force_load"; then + _lt_dsymutil='~$DSYMUTIL $lib || :' + else + _lt_dsymutil= + fi + ;; + esac + +# func_munge_path_list VARIABLE PATH +# ----------------------------------- +# VARIABLE is name of variable containing _space_ separated list of +# directories to be munged by the contents of PATH, which is string +# having a format: +# "DIR[:DIR]:" +# string "DIR[ DIR]" will be prepended to VARIABLE +# ":DIR[:DIR]" +# string "DIR[ DIR]" will be appended to VARIABLE +# "DIRP[:DIRP]::[DIRA:]DIRA" +# string "DIRP[ DIRP]" will be prepended to VARIABLE and string +# "DIRA[ DIRA]" will be appended to VARIABLE +# "DIR[:DIR]" +# VARIABLE will be replaced by "DIR[ DIR]" +func_munge_path_list () +{ + case x$2 in + x) + ;; + *:) + eval $1=\"`$ECHO $2 | $SED 's/:/ /g'` \$$1\" + ;; + x:*) + eval $1=\"\$$1 `$ECHO $2 | $SED 's/:/ /g'`\" + ;; + *::*) + eval $1=\"\$$1\ `$ECHO $2 | $SED -e 's/.*:://' -e 's/:/ /g'`\" + eval $1=\"`$ECHO $2 | $SED -e 's/::.*//' -e 's/:/ /g'`\ \$$1\" + ;; + *) + eval $1=\"`$ECHO $2 | $SED 's/:/ /g'`\" + ;; + esac +} + +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking how to run the C preprocessor" >&5 +$as_echo_n "checking how to run the C preprocessor... " >&6; } +# On Suns, sometimes $CPP names a directory. +if test -n "$CPP" && test -d "$CPP"; then + CPP= +fi +if test -z "$CPP"; then + if ${ac_cv_prog_CPP+:} false; then : + $as_echo_n "(cached) " >&6 +else + # Double quotes because CPP needs to be expanded + for CPP in "$CC -E" "$CC -E -traditional-cpp" "/lib/cpp" + do + ac_preproc_ok=false +for ac_c_preproc_warn_flag in '' yes +do + # Use a header file that comes with gcc, so configuring glibc + # with a fresh cross-compiler works. + # Prefer to if __STDC__ is defined, since + # exists even on freestanding compilers. + # On the NeXT, cc -E runs the code through the compiler's parser, + # not just through cpp. "Syntax error" is here to catch this case. + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#ifdef __STDC__ +# include +#else +# include +#endif + Syntax error +_ACEOF +if ac_fn_c_try_cpp "$LINENO"; then : + +else + # Broken: fails on valid input. +continue +fi +rm -f conftest.err conftest.i conftest.$ac_ext + + # OK, works on sane cases. Now check whether nonexistent headers + # can be detected and how. + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include +_ACEOF +if ac_fn_c_try_cpp "$LINENO"; then : + # Broken: success on invalid input. +continue +else + # Passes both tests. +ac_preproc_ok=: +break +fi +rm -f conftest.err conftest.i conftest.$ac_ext + +done +# Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped. +rm -f conftest.i conftest.err conftest.$ac_ext +if $ac_preproc_ok; then : + break +fi + + done + ac_cv_prog_CPP=$CPP + +fi + CPP=$ac_cv_prog_CPP +else + ac_cv_prog_CPP=$CPP +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $CPP" >&5 +$as_echo "$CPP" >&6; } +ac_preproc_ok=false +for ac_c_preproc_warn_flag in '' yes +do + # Use a header file that comes with gcc, so configuring glibc + # with a fresh cross-compiler works. + # Prefer to if __STDC__ is defined, since + # exists even on freestanding compilers. + # On the NeXT, cc -E runs the code through the compiler's parser, + # not just through cpp. "Syntax error" is here to catch this case. + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#ifdef __STDC__ +# include +#else +# include +#endif + Syntax error +_ACEOF +if ac_fn_c_try_cpp "$LINENO"; then : + +else + # Broken: fails on valid input. +continue +fi +rm -f conftest.err conftest.i conftest.$ac_ext + + # OK, works on sane cases. Now check whether nonexistent headers + # can be detected and how. + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include +_ACEOF +if ac_fn_c_try_cpp "$LINENO"; then : + # Broken: success on invalid input. +continue +else + # Passes both tests. +ac_preproc_ok=: +break +fi +rm -f conftest.err conftest.i conftest.$ac_ext + +done +# Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped. +rm -f conftest.i conftest.err conftest.$ac_ext +if $ac_preproc_ok; then : + +else + { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "C preprocessor \"$CPP\" fails sanity check +See \`config.log' for more details" "$LINENO" 5; } +fi + +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for ANSI C header files" >&5 +$as_echo_n "checking for ANSI C header files... " >&6; } +if ${ac_cv_header_stdc+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include +#include +#include +#include + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + ac_cv_header_stdc=yes +else + ac_cv_header_stdc=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext + +if test $ac_cv_header_stdc = yes; then + # SunOS 4.x string.h does not declare mem*, contrary to ANSI. + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include + +_ACEOF +if (eval "$ac_cpp conftest.$ac_ext") 2>&5 | + $EGREP "memchr" >/dev/null 2>&1; then : + +else + ac_cv_header_stdc=no +fi +rm -f conftest* + +fi + +if test $ac_cv_header_stdc = yes; then + # ISC 2.0.2 stdlib.h does not declare free, contrary to ANSI. + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include + +_ACEOF +if (eval "$ac_cpp conftest.$ac_ext") 2>&5 | + $EGREP "free" >/dev/null 2>&1; then : + +else + ac_cv_header_stdc=no +fi +rm -f conftest* + +fi + +if test $ac_cv_header_stdc = yes; then + # /bin/cc in Irix-4.0.5 gets non-ANSI ctype macros unless using -ansi. + if test "$cross_compiling" = yes; then : + : +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include +#include +#if ((' ' & 0x0FF) == 0x020) +# define ISLOWER(c) ('a' <= (c) && (c) <= 'z') +# define TOUPPER(c) (ISLOWER(c) ? 'A' + ((c) - 'a') : (c)) +#else +# define ISLOWER(c) \ + (('a' <= (c) && (c) <= 'i') \ + || ('j' <= (c) && (c) <= 'r') \ + || ('s' <= (c) && (c) <= 'z')) +# define TOUPPER(c) (ISLOWER(c) ? ((c) | 0x40) : (c)) +#endif + +#define XOR(e, f) (((e) && !(f)) || (!(e) && (f))) +int +main () +{ + int i; + for (i = 0; i < 256; i++) + if (XOR (islower (i), ISLOWER (i)) + || toupper (i) != TOUPPER (i)) + return 2; + return 0; +} +_ACEOF +if ac_fn_c_try_run "$LINENO"; then : + +else + ac_cv_header_stdc=no +fi +rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ + conftest.$ac_objext conftest.beam conftest.$ac_ext +fi + +fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_stdc" >&5 +$as_echo "$ac_cv_header_stdc" >&6; } +if test $ac_cv_header_stdc = yes; then + +$as_echo "#define STDC_HEADERS 1" >>confdefs.h + +fi + +# On IRIX 5.3, sys/types and inttypes.h are conflicting. +for ac_header in sys/types.h sys/stat.h stdlib.h string.h memory.h strings.h \ + inttypes.h stdint.h unistd.h +do : + as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` +ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default +" +if eval test \"x\$"$as_ac_Header"\" = x"yes"; then : + cat >>confdefs.h <<_ACEOF +#define `$as_echo "HAVE_$ac_header" | $as_tr_cpp` 1 +_ACEOF + +fi + +done + + +for ac_header in dlfcn.h +do : + ac_fn_c_check_header_compile "$LINENO" "dlfcn.h" "ac_cv_header_dlfcn_h" "$ac_includes_default +" +if test "x$ac_cv_header_dlfcn_h" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_DLFCN_H 1 +_ACEOF + +fi + +done + + + + + +# Set options + + + + enable_dlopen=no + + + enable_win32_dll=no + + + # Check whether --enable-shared was given. +if test "${enable_shared+set}" = set; then : + enableval=$enable_shared; p=${PACKAGE-default} + case $enableval in + yes) enable_shared=yes ;; + no) enable_shared=no ;; + *) + enable_shared=no + # Look at the argument we got. We use all the common list separators. + lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR, + for pkg in $enableval; do + IFS=$lt_save_ifs + if test "X$pkg" = "X$p"; then + enable_shared=yes + fi + done + IFS=$lt_save_ifs + ;; + esac +else + enable_shared=yes +fi + + + + + + + + + + # Check whether --enable-static was given. +if test "${enable_static+set}" = set; then : + enableval=$enable_static; p=${PACKAGE-default} + case $enableval in + yes) enable_static=yes ;; + no) enable_static=no ;; + *) + enable_static=no + # Look at the argument we got. We use all the common list separators. + lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR, + for pkg in $enableval; do + IFS=$lt_save_ifs + if test "X$pkg" = "X$p"; then + enable_static=yes + fi + done + IFS=$lt_save_ifs + ;; + esac +else + enable_static=yes +fi + + + + + + + + + + +# Check whether --with-pic was given. +if test "${with_pic+set}" = set; then : + withval=$with_pic; lt_p=${PACKAGE-default} + case $withval in + yes|no) pic_mode=$withval ;; + *) + pic_mode=default + # Look at the argument we got. We use all the common list separators. + lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR, + for lt_pkg in $withval; do + IFS=$lt_save_ifs + if test "X$lt_pkg" = "X$lt_p"; then + pic_mode=yes + fi + done + IFS=$lt_save_ifs + ;; + esac +else + pic_mode=default +fi + + + + + + + + + # Check whether --enable-fast-install was given. +if test "${enable_fast_install+set}" = set; then : + enableval=$enable_fast_install; p=${PACKAGE-default} + case $enableval in + yes) enable_fast_install=yes ;; + no) enable_fast_install=no ;; + *) + enable_fast_install=no + # Look at the argument we got. We use all the common list separators. + lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR, + for pkg in $enableval; do + IFS=$lt_save_ifs + if test "X$pkg" = "X$p"; then + enable_fast_install=yes + fi + done + IFS=$lt_save_ifs + ;; + esac +else + enable_fast_install=yes +fi + + + + + + + + + shared_archive_member_spec= +case $host,$enable_shared in +power*-*-aix[5-9]*,yes) + { $as_echo "$as_me:${as_lineno-$LINENO}: checking which variant of shared library versioning to provide" >&5 +$as_echo_n "checking which variant of shared library versioning to provide... " >&6; } + +# Check whether --with-aix-soname was given. +if test "${with_aix_soname+set}" = set; then : + withval=$with_aix_soname; case $withval in + aix|svr4|both) + ;; + *) + as_fn_error $? "Unknown argument to --with-aix-soname" "$LINENO" 5 + ;; + esac + lt_cv_with_aix_soname=$with_aix_soname +else + if ${lt_cv_with_aix_soname+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_with_aix_soname=aix +fi + + with_aix_soname=$lt_cv_with_aix_soname +fi + + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_aix_soname" >&5 +$as_echo "$with_aix_soname" >&6; } + if test aix != "$with_aix_soname"; then + # For the AIX way of multilib, we name the shared archive member + # based on the bitwidth used, traditionally 'shr.o' or 'shr_64.o', + # and 'shr.imp' or 'shr_64.imp', respectively, for the Import File. + # Even when GNU compilers ignore OBJECT_MODE but need '-maix64' flag, + # the AIX toolchain works better with OBJECT_MODE set (default 32). + if test 64 = "${OBJECT_MODE-32}"; then + shared_archive_member_spec=shr_64 + else + shared_archive_member_spec=shr + fi + fi + ;; +*) + with_aix_soname=aix + ;; +esac + + + + + + + + + + +# This can be used to rebuild libtool when needed +LIBTOOL_DEPS=$ltmain + +# Always use our own libtool. +LIBTOOL='$(SHELL) $(top_builddir)/libtool' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +test -z "$LN_S" && LN_S="ln -s" + + + + + + + + + + + + + + +if test -n "${ZSH_VERSION+set}"; then + setopt NO_GLOB_SUBST +fi + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for objdir" >&5 +$as_echo_n "checking for objdir... " >&6; } +if ${lt_cv_objdir+:} false; then : + $as_echo_n "(cached) " >&6 +else + rm -f .libs 2>/dev/null +mkdir .libs 2>/dev/null +if test -d .libs; then + lt_cv_objdir=.libs +else + # MS-DOS does not allow filenames that begin with a dot. + lt_cv_objdir=_libs +fi +rmdir .libs 2>/dev/null +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_objdir" >&5 +$as_echo "$lt_cv_objdir" >&6; } +objdir=$lt_cv_objdir + + + + + +cat >>confdefs.h <<_ACEOF +#define LT_OBJDIR "$lt_cv_objdir/" +_ACEOF + + + + +case $host_os in +aix3*) + # AIX sometimes has problems with the GCC collect2 program. For some + # reason, if we set the COLLECT_NAMES environment variable, the problems + # vanish in a puff of smoke. + if test set != "${COLLECT_NAMES+set}"; then + COLLECT_NAMES= + export COLLECT_NAMES + fi + ;; +esac + +# Global variables: +ofile=libtool +can_build_shared=yes + +# All known linkers require a '.a' archive for static linking (except MSVC, +# which needs '.lib'). +libext=a + +with_gnu_ld=$lt_cv_prog_gnu_ld + +old_CC=$CC +old_CFLAGS=$CFLAGS + +# Set sane defaults for various variables +test -z "$CC" && CC=cc +test -z "$LTCC" && LTCC=$CC +test -z "$LTCFLAGS" && LTCFLAGS=$CFLAGS +test -z "$LD" && LD=ld +test -z "$ac_objext" && ac_objext=o + +func_cc_basename $compiler +cc_basename=$func_cc_basename_result + + +# Only perform the check for file, if the check method requires it +test -z "$MAGIC_CMD" && MAGIC_CMD=file +case $deplibs_check_method in +file_magic*) + if test "$file_magic_cmd" = '$MAGIC_CMD'; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for ${ac_tool_prefix}file" >&5 +$as_echo_n "checking for ${ac_tool_prefix}file... " >&6; } +if ${lt_cv_path_MAGIC_CMD+:} false; then : + $as_echo_n "(cached) " >&6 +else + case $MAGIC_CMD in +[\\/*] | ?:[\\/]*) + lt_cv_path_MAGIC_CMD=$MAGIC_CMD # Let the user override the test with a path. + ;; +*) + lt_save_MAGIC_CMD=$MAGIC_CMD + lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR + ac_dummy="/usr/bin$PATH_SEPARATOR$PATH" + for ac_dir in $ac_dummy; do + IFS=$lt_save_ifs + test -z "$ac_dir" && ac_dir=. + if test -f "$ac_dir/${ac_tool_prefix}file"; then + lt_cv_path_MAGIC_CMD=$ac_dir/"${ac_tool_prefix}file" + if test -n "$file_magic_test_file"; then + case $deplibs_check_method in + "file_magic "*) + file_magic_regex=`expr "$deplibs_check_method" : "file_magic \(.*\)"` + MAGIC_CMD=$lt_cv_path_MAGIC_CMD + if eval $file_magic_cmd \$file_magic_test_file 2> /dev/null | + $EGREP "$file_magic_regex" > /dev/null; then + : + else + cat <<_LT_EOF 1>&2 + +*** Warning: the command libtool uses to detect shared libraries, +*** $file_magic_cmd, produces output that libtool cannot recognize. +*** The result is that libtool may fail to recognize shared libraries +*** as such. This will affect the creation of libtool libraries that +*** depend on shared libraries, but programs linked with such libtool +*** libraries will work regardless of this problem. Nevertheless, you +*** may want to report the problem to your system manager and/or to +*** bug-libtool@gnu.org + +_LT_EOF + fi ;; + esac + fi + break + fi + done + IFS=$lt_save_ifs + MAGIC_CMD=$lt_save_MAGIC_CMD + ;; +esac +fi + +MAGIC_CMD=$lt_cv_path_MAGIC_CMD +if test -n "$MAGIC_CMD"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $MAGIC_CMD" >&5 +$as_echo "$MAGIC_CMD" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + + + +if test -z "$lt_cv_path_MAGIC_CMD"; then + if test -n "$ac_tool_prefix"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for file" >&5 +$as_echo_n "checking for file... " >&6; } +if ${lt_cv_path_MAGIC_CMD+:} false; then : + $as_echo_n "(cached) " >&6 +else + case $MAGIC_CMD in +[\\/*] | ?:[\\/]*) + lt_cv_path_MAGIC_CMD=$MAGIC_CMD # Let the user override the test with a path. + ;; +*) + lt_save_MAGIC_CMD=$MAGIC_CMD + lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR + ac_dummy="/usr/bin$PATH_SEPARATOR$PATH" + for ac_dir in $ac_dummy; do + IFS=$lt_save_ifs + test -z "$ac_dir" && ac_dir=. + if test -f "$ac_dir/file"; then + lt_cv_path_MAGIC_CMD=$ac_dir/"file" + if test -n "$file_magic_test_file"; then + case $deplibs_check_method in + "file_magic "*) + file_magic_regex=`expr "$deplibs_check_method" : "file_magic \(.*\)"` + MAGIC_CMD=$lt_cv_path_MAGIC_CMD + if eval $file_magic_cmd \$file_magic_test_file 2> /dev/null | + $EGREP "$file_magic_regex" > /dev/null; then + : + else + cat <<_LT_EOF 1>&2 + +*** Warning: the command libtool uses to detect shared libraries, +*** $file_magic_cmd, produces output that libtool cannot recognize. +*** The result is that libtool may fail to recognize shared libraries +*** as such. This will affect the creation of libtool libraries that +*** depend on shared libraries, but programs linked with such libtool +*** libraries will work regardless of this problem. Nevertheless, you +*** may want to report the problem to your system manager and/or to +*** bug-libtool@gnu.org + +_LT_EOF + fi ;; + esac + fi + break + fi + done + IFS=$lt_save_ifs + MAGIC_CMD=$lt_save_MAGIC_CMD + ;; +esac +fi + +MAGIC_CMD=$lt_cv_path_MAGIC_CMD +if test -n "$MAGIC_CMD"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $MAGIC_CMD" >&5 +$as_echo "$MAGIC_CMD" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + else + MAGIC_CMD=: + fi +fi + + fi + ;; +esac + +# Use C for the default configuration in the libtool script + +lt_save_CC=$CC +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + + +# Source file extension for C test sources. +ac_ext=c + +# Object file extension for compiled C test sources. +objext=o +objext=$objext + +# Code to be used in simple compile tests +lt_simple_compile_test_code="int some_variable = 0;" + +# Code to be used in simple link tests +lt_simple_link_test_code='int main(){return(0);}' + + + + + + + +# If no C compiler was specified, use CC. +LTCC=${LTCC-"$CC"} + +# If no C compiler flags were specified, use CFLAGS. +LTCFLAGS=${LTCFLAGS-"$CFLAGS"} + +# Allow CC to be a program name with arguments. +compiler=$CC + +# Save the default compiler, since it gets overwritten when the other +# tags are being tested, and _LT_TAGVAR(compiler, []) is a NOP. +compiler_DEFAULT=$CC + +# save warnings/boilerplate of simple test code +ac_outfile=conftest.$ac_objext +echo "$lt_simple_compile_test_code" >conftest.$ac_ext +eval "$ac_compile" 2>&1 >/dev/null | $SED '/^$/d; /^ *+/d' >conftest.err +_lt_compiler_boilerplate=`cat conftest.err` +$RM conftest* + +ac_outfile=conftest.$ac_objext +echo "$lt_simple_link_test_code" >conftest.$ac_ext +eval "$ac_link" 2>&1 >/dev/null | $SED '/^$/d; /^ *+/d' >conftest.err +_lt_linker_boilerplate=`cat conftest.err` +$RM -r conftest* + + +if test -n "$compiler"; then + +lt_prog_compiler_no_builtin_flag= + +if test yes = "$GCC"; then + case $cc_basename in + nvcc*) + lt_prog_compiler_no_builtin_flag=' -Xcompiler -fno-builtin' ;; + *) + lt_prog_compiler_no_builtin_flag=' -fno-builtin' ;; + esac + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking if $compiler supports -fno-rtti -fno-exceptions" >&5 +$as_echo_n "checking if $compiler supports -fno-rtti -fno-exceptions... " >&6; } +if ${lt_cv_prog_compiler_rtti_exceptions+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_prog_compiler_rtti_exceptions=no + ac_outfile=conftest.$ac_objext + echo "$lt_simple_compile_test_code" > conftest.$ac_ext + lt_compiler_flag="-fno-rtti -fno-exceptions" ## exclude from sc_useless_quotes_in_assignment + # Insert the option either (1) after the last *FLAGS variable, or + # (2) before a word containing "conftest.", or (3) at the end. + # Note that $ac_compile itself does not contain backslashes and begins + # with a dollar sign (not a hyphen), so the echo should work correctly. + # The option is referenced via a variable to avoid confusing sed. + lt_compile=`echo "$ac_compile" | $SED \ + -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ + -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ + -e 's:$: $lt_compiler_flag:'` + (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&5) + (eval "$lt_compile" 2>conftest.err) + ac_status=$? + cat conftest.err >&5 + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + if (exit $ac_status) && test -s "$ac_outfile"; then + # The compiler can only warn and ignore the option if not recognized + # So say no if there are warnings other than the usual output. + $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' >conftest.exp + $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2 + if test ! -s conftest.er2 || diff conftest.exp conftest.er2 >/dev/null; then + lt_cv_prog_compiler_rtti_exceptions=yes + fi + fi + $RM conftest* + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_rtti_exceptions" >&5 +$as_echo "$lt_cv_prog_compiler_rtti_exceptions" >&6; } + +if test yes = "$lt_cv_prog_compiler_rtti_exceptions"; then + lt_prog_compiler_no_builtin_flag="$lt_prog_compiler_no_builtin_flag -fno-rtti -fno-exceptions" +else + : +fi + +fi + + + + + + + lt_prog_compiler_wl= +lt_prog_compiler_pic= +lt_prog_compiler_static= + + + if test yes = "$GCC"; then + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_static='-static' + + case $host_os in + aix*) + # All AIX code is PIC. + if test ia64 = "$host_cpu"; then + # AIX 5 now supports IA64 processor + lt_prog_compiler_static='-Bstatic' + fi + lt_prog_compiler_pic='-fPIC' + ;; + + amigaos*) + case $host_cpu in + powerpc) + # see comment about AmigaOS4 .so support + lt_prog_compiler_pic='-fPIC' + ;; + m68k) + # FIXME: we need at least 68020 code to build shared libraries, but + # adding the '-m68020' flag to GCC prevents building anything better, + # like '-m68040'. + lt_prog_compiler_pic='-m68020 -resident32 -malways-restore-a4' + ;; + esac + ;; + + beos* | irix5* | irix6* | nonstopux* | osf3* | osf4* | osf5*) + # PIC is the default for these OSes. + ;; + + mingw* | cygwin* | pw32* | os2* | cegcc*) + # This hack is so that the source file can tell whether it is being + # built for inclusion in a dll (and should export symbols for example). + # Although the cygwin gcc ignores -fPIC, still need this for old-style + # (--disable-auto-import) libraries + lt_prog_compiler_pic='-DDLL_EXPORT' + case $host_os in + os2*) + lt_prog_compiler_static='$wl-static' + ;; + esac + ;; + + darwin* | rhapsody*) + # PIC is the default on this platform + # Common symbols not allowed in MH_DYLIB files + lt_prog_compiler_pic='-fno-common' + ;; + + haiku*) + # PIC is the default for Haiku. + # The "-static" flag exists, but is broken. + lt_prog_compiler_static= + ;; + + hpux*) + # PIC is the default for 64-bit PA HP-UX, but not for 32-bit + # PA HP-UX. On IA64 HP-UX, PIC is the default but the pic flag + # sets the default TLS model and affects inlining. + case $host_cpu in + hppa*64*) + # +Z the default + ;; + *) + lt_prog_compiler_pic='-fPIC' + ;; + esac + ;; + + interix[3-9]*) + # Interix 3.x gcc -fpic/-fPIC options generate broken code. + # Instead, we relocate shared libraries at runtime. + ;; + + msdosdjgpp*) + # Just because we use GCC doesn't mean we suddenly get shared libraries + # on systems that don't support them. + lt_prog_compiler_can_build_shared=no + enable_shared=no + ;; + + *nto* | *qnx*) + # QNX uses GNU C++, but need to define -shared option too, otherwise + # it will coredump. + lt_prog_compiler_pic='-fPIC -shared' + ;; + + sysv4*MP*) + if test -d /usr/nec; then + lt_prog_compiler_pic=-Kconform_pic + fi + ;; + + *) + lt_prog_compiler_pic='-fPIC' + ;; + esac + + case $cc_basename in + nvcc*) # Cuda Compiler Driver 2.2 + lt_prog_compiler_wl='-Xlinker ' + if test -n "$lt_prog_compiler_pic"; then + lt_prog_compiler_pic="-Xcompiler $lt_prog_compiler_pic" + fi + ;; + esac + else + # PORTME Check for flag to pass linker flags through the system compiler. + case $host_os in + aix*) + lt_prog_compiler_wl='-Wl,' + if test ia64 = "$host_cpu"; then + # AIX 5 now supports IA64 processor + lt_prog_compiler_static='-Bstatic' + else + lt_prog_compiler_static='-bnso -bI:/lib/syscalls.exp' + fi + ;; + + darwin* | rhapsody*) + # PIC is the default on this platform + # Common symbols not allowed in MH_DYLIB files + lt_prog_compiler_pic='-fno-common' + case $cc_basename in + nagfor*) + # NAG Fortran compiler + lt_prog_compiler_wl='-Wl,-Wl,,' + lt_prog_compiler_pic='-PIC' + lt_prog_compiler_static='-Bstatic' + ;; + esac + ;; + + mingw* | cygwin* | pw32* | os2* | cegcc*) + # This hack is so that the source file can tell whether it is being + # built for inclusion in a dll (and should export symbols for example). + lt_prog_compiler_pic='-DDLL_EXPORT' + case $host_os in + os2*) + lt_prog_compiler_static='$wl-static' + ;; + esac + ;; + + hpux9* | hpux10* | hpux11*) + lt_prog_compiler_wl='-Wl,' + # PIC is the default for IA64 HP-UX and 64-bit HP-UX, but + # not for PA HP-UX. + case $host_cpu in + hppa*64*|ia64*) + # +Z the default + ;; + *) + lt_prog_compiler_pic='+Z' + ;; + esac + # Is there a better lt_prog_compiler_static that works with the bundled CC? + lt_prog_compiler_static='$wl-a ${wl}archive' + ;; + + irix5* | irix6* | nonstopux*) + lt_prog_compiler_wl='-Wl,' + # PIC (with -KPIC) is the default. + lt_prog_compiler_static='-non_shared' + ;; + + linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*) + case $cc_basename in + # old Intel for x86_64, which still supported -KPIC. + ecc*) + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_pic='-KPIC' + lt_prog_compiler_static='-static' + ;; + # icc used to be incompatible with GCC. + # ICC 10 doesn't accept -KPIC any more. + icc* | ifort*) + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_pic='-fPIC' + lt_prog_compiler_static='-static' + ;; + # Lahey Fortran 8.1. + lf95*) + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_pic='--shared' + lt_prog_compiler_static='--static' + ;; + nagfor*) + # NAG Fortran compiler + lt_prog_compiler_wl='-Wl,-Wl,,' + lt_prog_compiler_pic='-PIC' + lt_prog_compiler_static='-Bstatic' + ;; + tcc*) + # Fabrice Bellard et al's Tiny C Compiler + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_pic='-fPIC' + lt_prog_compiler_static='-static' + ;; + pgcc* | pgf77* | pgf90* | pgf95* | pgfortran*) + # Portland Group compilers (*not* the Pentium gcc compiler, + # which looks to be a dead project) + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_pic='-fpic' + lt_prog_compiler_static='-Bstatic' + ;; + ccc*) + lt_prog_compiler_wl='-Wl,' + # All Alpha code is PIC. + lt_prog_compiler_static='-non_shared' + ;; + xl* | bgxl* | bgf* | mpixl*) + # IBM XL C 8.0/Fortran 10.1, 11.1 on PPC and BlueGene + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_pic='-qpic' + lt_prog_compiler_static='-qstaticlink' + ;; + *) + case `$CC -V 2>&1 | sed 5q` in + *Sun\ Ceres\ Fortran* | *Sun*Fortran*\ [1-7].* | *Sun*Fortran*\ 8.[0-3]*) + # Sun Fortran 8.3 passes all unrecognized flags to the linker + lt_prog_compiler_pic='-KPIC' + lt_prog_compiler_static='-Bstatic' + lt_prog_compiler_wl='' + ;; + *Sun\ F* | *Sun*Fortran*) + lt_prog_compiler_pic='-KPIC' + lt_prog_compiler_static='-Bstatic' + lt_prog_compiler_wl='-Qoption ld ' + ;; + *Sun\ C*) + # Sun C 5.9 + lt_prog_compiler_pic='-KPIC' + lt_prog_compiler_static='-Bstatic' + lt_prog_compiler_wl='-Wl,' + ;; + *Intel*\ [CF]*Compiler*) + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_pic='-fPIC' + lt_prog_compiler_static='-static' + ;; + *Portland\ Group*) + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_pic='-fpic' + lt_prog_compiler_static='-Bstatic' + ;; + esac + ;; + esac + ;; + + newsos6) + lt_prog_compiler_pic='-KPIC' + lt_prog_compiler_static='-Bstatic' + ;; + + *nto* | *qnx*) + # QNX uses GNU C++, but need to define -shared option too, otherwise + # it will coredump. + lt_prog_compiler_pic='-fPIC -shared' + ;; + + osf3* | osf4* | osf5*) + lt_prog_compiler_wl='-Wl,' + # All OSF/1 code is PIC. + lt_prog_compiler_static='-non_shared' + ;; + + rdos*) + lt_prog_compiler_static='-non_shared' + ;; + + solaris*) + lt_prog_compiler_pic='-KPIC' + lt_prog_compiler_static='-Bstatic' + case $cc_basename in + f77* | f90* | f95* | sunf77* | sunf90* | sunf95*) + lt_prog_compiler_wl='-Qoption ld ';; + *) + lt_prog_compiler_wl='-Wl,';; + esac + ;; + + sunos4*) + lt_prog_compiler_wl='-Qoption ld ' + lt_prog_compiler_pic='-PIC' + lt_prog_compiler_static='-Bstatic' + ;; + + sysv4 | sysv4.2uw2* | sysv4.3*) + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_pic='-KPIC' + lt_prog_compiler_static='-Bstatic' + ;; + + sysv4*MP*) + if test -d /usr/nec; then + lt_prog_compiler_pic='-Kconform_pic' + lt_prog_compiler_static='-Bstatic' + fi + ;; + + sysv5* | unixware* | sco3.2v5* | sco5v6* | OpenUNIX*) + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_pic='-KPIC' + lt_prog_compiler_static='-Bstatic' + ;; + + unicos*) + lt_prog_compiler_wl='-Wl,' + lt_prog_compiler_can_build_shared=no + ;; + + uts4*) + lt_prog_compiler_pic='-pic' + lt_prog_compiler_static='-Bstatic' + ;; + + *) + lt_prog_compiler_can_build_shared=no + ;; + esac + fi + +case $host_os in + # For platforms that do not support PIC, -DPIC is meaningless: + *djgpp*) + lt_prog_compiler_pic= + ;; + *) + lt_prog_compiler_pic="$lt_prog_compiler_pic -DPIC" + ;; +esac + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $compiler option to produce PIC" >&5 +$as_echo_n "checking for $compiler option to produce PIC... " >&6; } +if ${lt_cv_prog_compiler_pic+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_prog_compiler_pic=$lt_prog_compiler_pic +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_pic" >&5 +$as_echo "$lt_cv_prog_compiler_pic" >&6; } +lt_prog_compiler_pic=$lt_cv_prog_compiler_pic + +# +# Check to make sure the PIC flag actually works. +# +if test -n "$lt_prog_compiler_pic"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking if $compiler PIC flag $lt_prog_compiler_pic works" >&5 +$as_echo_n "checking if $compiler PIC flag $lt_prog_compiler_pic works... " >&6; } +if ${lt_cv_prog_compiler_pic_works+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_prog_compiler_pic_works=no + ac_outfile=conftest.$ac_objext + echo "$lt_simple_compile_test_code" > conftest.$ac_ext + lt_compiler_flag="$lt_prog_compiler_pic -DPIC" ## exclude from sc_useless_quotes_in_assignment + # Insert the option either (1) after the last *FLAGS variable, or + # (2) before a word containing "conftest.", or (3) at the end. + # Note that $ac_compile itself does not contain backslashes and begins + # with a dollar sign (not a hyphen), so the echo should work correctly. + # The option is referenced via a variable to avoid confusing sed. + lt_compile=`echo "$ac_compile" | $SED \ + -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ + -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ + -e 's:$: $lt_compiler_flag:'` + (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&5) + (eval "$lt_compile" 2>conftest.err) + ac_status=$? + cat conftest.err >&5 + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + if (exit $ac_status) && test -s "$ac_outfile"; then + # The compiler can only warn and ignore the option if not recognized + # So say no if there are warnings other than the usual output. + $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' >conftest.exp + $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2 + if test ! -s conftest.er2 || diff conftest.exp conftest.er2 >/dev/null; then + lt_cv_prog_compiler_pic_works=yes + fi + fi + $RM conftest* + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_pic_works" >&5 +$as_echo "$lt_cv_prog_compiler_pic_works" >&6; } + +if test yes = "$lt_cv_prog_compiler_pic_works"; then + case $lt_prog_compiler_pic in + "" | " "*) ;; + *) lt_prog_compiler_pic=" $lt_prog_compiler_pic" ;; + esac +else + lt_prog_compiler_pic= + lt_prog_compiler_can_build_shared=no +fi + +fi + + + + + + + + + + + +# +# Check to make sure the static flag actually works. +# +wl=$lt_prog_compiler_wl eval lt_tmp_static_flag=\"$lt_prog_compiler_static\" +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking if $compiler static flag $lt_tmp_static_flag works" >&5 +$as_echo_n "checking if $compiler static flag $lt_tmp_static_flag works... " >&6; } +if ${lt_cv_prog_compiler_static_works+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_prog_compiler_static_works=no + save_LDFLAGS=$LDFLAGS + LDFLAGS="$LDFLAGS $lt_tmp_static_flag" + echo "$lt_simple_link_test_code" > conftest.$ac_ext + if (eval $ac_link 2>conftest.err) && test -s conftest$ac_exeext; then + # The linker can only warn and ignore the option if not recognized + # So say no if there are warnings + if test -s conftest.err; then + # Append any errors to the config.log. + cat conftest.err 1>&5 + $ECHO "$_lt_linker_boilerplate" | $SED '/^$/d' > conftest.exp + $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2 + if diff conftest.exp conftest.er2 >/dev/null; then + lt_cv_prog_compiler_static_works=yes + fi + else + lt_cv_prog_compiler_static_works=yes + fi + fi + $RM -r conftest* + LDFLAGS=$save_LDFLAGS + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_static_works" >&5 +$as_echo "$lt_cv_prog_compiler_static_works" >&6; } + +if test yes = "$lt_cv_prog_compiler_static_works"; then + : +else + lt_prog_compiler_static= +fi + + + + + + + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking if $compiler supports -c -o file.$ac_objext" >&5 +$as_echo_n "checking if $compiler supports -c -o file.$ac_objext... " >&6; } +if ${lt_cv_prog_compiler_c_o+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_prog_compiler_c_o=no + $RM -r conftest 2>/dev/null + mkdir conftest + cd conftest + mkdir out + echo "$lt_simple_compile_test_code" > conftest.$ac_ext + + lt_compiler_flag="-o out/conftest2.$ac_objext" + # Insert the option either (1) after the last *FLAGS variable, or + # (2) before a word containing "conftest.", or (3) at the end. + # Note that $ac_compile itself does not contain backslashes and begins + # with a dollar sign (not a hyphen), so the echo should work correctly. + lt_compile=`echo "$ac_compile" | $SED \ + -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ + -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ + -e 's:$: $lt_compiler_flag:'` + (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&5) + (eval "$lt_compile" 2>out/conftest.err) + ac_status=$? + cat out/conftest.err >&5 + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + if (exit $ac_status) && test -s out/conftest2.$ac_objext + then + # The compiler can only warn and ignore the option if not recognized + # So say no if there are warnings + $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' > out/conftest.exp + $SED '/^$/d; /^ *+/d' out/conftest.err >out/conftest.er2 + if test ! -s out/conftest.er2 || diff out/conftest.exp out/conftest.er2 >/dev/null; then + lt_cv_prog_compiler_c_o=yes + fi + fi + chmod u+w . 2>&5 + $RM conftest* + # SGI C++ compiler will create directory out/ii_files/ for + # template instantiation + test -d out/ii_files && $RM out/ii_files/* && rmdir out/ii_files + $RM out/* && rmdir out + cd .. + $RM -r conftest + $RM conftest* + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_c_o" >&5 +$as_echo "$lt_cv_prog_compiler_c_o" >&6; } + + + + + + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking if $compiler supports -c -o file.$ac_objext" >&5 +$as_echo_n "checking if $compiler supports -c -o file.$ac_objext... " >&6; } +if ${lt_cv_prog_compiler_c_o+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_prog_compiler_c_o=no + $RM -r conftest 2>/dev/null + mkdir conftest + cd conftest + mkdir out + echo "$lt_simple_compile_test_code" > conftest.$ac_ext + + lt_compiler_flag="-o out/conftest2.$ac_objext" + # Insert the option either (1) after the last *FLAGS variable, or + # (2) before a word containing "conftest.", or (3) at the end. + # Note that $ac_compile itself does not contain backslashes and begins + # with a dollar sign (not a hyphen), so the echo should work correctly. + lt_compile=`echo "$ac_compile" | $SED \ + -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ + -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ + -e 's:$: $lt_compiler_flag:'` + (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&5) + (eval "$lt_compile" 2>out/conftest.err) + ac_status=$? + cat out/conftest.err >&5 + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + if (exit $ac_status) && test -s out/conftest2.$ac_objext + then + # The compiler can only warn and ignore the option if not recognized + # So say no if there are warnings + $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' > out/conftest.exp + $SED '/^$/d; /^ *+/d' out/conftest.err >out/conftest.er2 + if test ! -s out/conftest.er2 || diff out/conftest.exp out/conftest.er2 >/dev/null; then + lt_cv_prog_compiler_c_o=yes + fi + fi + chmod u+w . 2>&5 + $RM conftest* + # SGI C++ compiler will create directory out/ii_files/ for + # template instantiation + test -d out/ii_files && $RM out/ii_files/* && rmdir out/ii_files + $RM out/* && rmdir out + cd .. + $RM -r conftest + $RM conftest* + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_c_o" >&5 +$as_echo "$lt_cv_prog_compiler_c_o" >&6; } + + + + +hard_links=nottested +if test no = "$lt_cv_prog_compiler_c_o" && test no != "$need_locks"; then + # do not overwrite the value of need_locks provided by the user + { $as_echo "$as_me:${as_lineno-$LINENO}: checking if we can lock with hard links" >&5 +$as_echo_n "checking if we can lock with hard links... " >&6; } + hard_links=yes + $RM conftest* + ln conftest.a conftest.b 2>/dev/null && hard_links=no + touch conftest.a + ln conftest.a conftest.b 2>&5 || hard_links=no + ln conftest.a conftest.b 2>/dev/null && hard_links=no + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $hard_links" >&5 +$as_echo "$hard_links" >&6; } + if test no = "$hard_links"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: '$CC' does not support '-c -o', so 'make -j' may be unsafe" >&5 +$as_echo "$as_me: WARNING: '$CC' does not support '-c -o', so 'make -j' may be unsafe" >&2;} + need_locks=warn + fi +else + need_locks=no +fi + + + + + + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether the $compiler linker ($LD) supports shared libraries" >&5 +$as_echo_n "checking whether the $compiler linker ($LD) supports shared libraries... " >&6; } + + runpath_var= + allow_undefined_flag= + always_export_symbols=no + archive_cmds= + archive_expsym_cmds= + compiler_needs_object=no + enable_shared_with_static_runtimes=no + export_dynamic_flag_spec= + export_symbols_cmds='$NM $libobjs $convenience | $global_symbol_pipe | $SED '\''s/.* //'\'' | sort | uniq > $export_symbols' + hardcode_automatic=no + hardcode_direct=no + hardcode_direct_absolute=no + hardcode_libdir_flag_spec= + hardcode_libdir_separator= + hardcode_minus_L=no + hardcode_shlibpath_var=unsupported + inherit_rpath=no + link_all_deplibs=unknown + module_cmds= + module_expsym_cmds= + old_archive_from_new_cmds= + old_archive_from_expsyms_cmds= + thread_safe_flag_spec= + whole_archive_flag_spec= + # include_expsyms should be a list of space-separated symbols to be *always* + # included in the symbol list + include_expsyms= + # exclude_expsyms can be an extended regexp of symbols to exclude + # it will be wrapped by ' (' and ')$', so one must not match beginning or + # end of line. Example: 'a|bc|.*d.*' will exclude the symbols 'a' and 'bc', + # as well as any symbol that contains 'd'. + exclude_expsyms='_GLOBAL_OFFSET_TABLE_|_GLOBAL__F[ID]_.*' + # Although _GLOBAL_OFFSET_TABLE_ is a valid symbol C name, most a.out + # platforms (ab)use it in PIC code, but their linkers get confused if + # the symbol is explicitly referenced. Since portable code cannot + # rely on this symbol name, it's probably fine to never include it in + # preloaded symbol tables. + # Exclude shared library initialization/finalization symbols. + extract_expsyms_cmds= + + case $host_os in + cygwin* | mingw* | pw32* | cegcc*) + # FIXME: the MSVC++ port hasn't been tested in a loooong time + # When not using gcc, we currently assume that we are using + # Microsoft Visual C++. + if test yes != "$GCC"; then + with_gnu_ld=no + fi + ;; + interix*) + # we just hope/assume this is gcc and not c89 (= MSVC++) + with_gnu_ld=yes + ;; + openbsd* | bitrig*) + with_gnu_ld=no + ;; + linux* | k*bsd*-gnu | gnu*) + link_all_deplibs=no + ;; + esac + + ld_shlibs=yes + + # On some targets, GNU ld is compatible enough with the native linker + # that we're better off using the native interface for both. + lt_use_gnu_ld_interface=no + if test yes = "$with_gnu_ld"; then + case $host_os in + aix*) + # The AIX port of GNU ld has always aspired to compatibility + # with the native linker. However, as the warning in the GNU ld + # block says, versions before 2.19.5* couldn't really create working + # shared libraries, regardless of the interface used. + case `$LD -v 2>&1` in + *\ \(GNU\ Binutils\)\ 2.19.5*) ;; + *\ \(GNU\ Binutils\)\ 2.[2-9]*) ;; + *\ \(GNU\ Binutils\)\ [3-9]*) ;; + *) + lt_use_gnu_ld_interface=yes + ;; + esac + ;; + *) + lt_use_gnu_ld_interface=yes + ;; + esac + fi + + if test yes = "$lt_use_gnu_ld_interface"; then + # If archive_cmds runs LD, not CC, wlarc should be empty + wlarc='$wl' + + # Set some defaults for GNU ld with shared library support. These + # are reset later if shared libraries are not supported. Putting them + # here allows them to be overridden if necessary. + runpath_var=LD_RUN_PATH + hardcode_libdir_flag_spec='$wl-rpath $wl$libdir' + export_dynamic_flag_spec='$wl--export-dynamic' + # ancient GNU ld didn't support --whole-archive et. al. + if $LD --help 2>&1 | $GREP 'no-whole-archive' > /dev/null; then + whole_archive_flag_spec=$wlarc'--whole-archive$convenience '$wlarc'--no-whole-archive' + else + whole_archive_flag_spec= + fi + supports_anon_versioning=no + case `$LD -v | $SED -e 's/(^)\+)\s\+//' 2>&1` in + *GNU\ gold*) supports_anon_versioning=yes ;; + *\ [01].* | *\ 2.[0-9].* | *\ 2.10.*) ;; # catch versions < 2.11 + *\ 2.11.93.0.2\ *) supports_anon_versioning=yes ;; # RH7.3 ... + *\ 2.11.92.0.12\ *) supports_anon_versioning=yes ;; # Mandrake 8.2 ... + *\ 2.11.*) ;; # other 2.11 versions + *) supports_anon_versioning=yes ;; + esac + + # See if GNU ld supports shared libraries. + case $host_os in + aix[3-9]*) + # On AIX/PPC, the GNU linker is very broken + if test ia64 != "$host_cpu"; then + ld_shlibs=no + cat <<_LT_EOF 1>&2 + +*** Warning: the GNU linker, at least up to release 2.19, is reported +*** to be unable to reliably create shared libraries on AIX. +*** Therefore, libtool is disabling shared libraries support. If you +*** really care for shared libraries, you may want to install binutils +*** 2.20 or above, or modify your PATH so that a non-GNU linker is found. +*** You will then need to restart the configuration process. + +_LT_EOF + fi + ;; + + amigaos*) + case $host_cpu in + powerpc) + # see comment about AmigaOS4 .so support + archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + archive_expsym_cmds='' + ;; + m68k) + archive_cmds='$RM $output_objdir/a2ixlibrary.data~$ECHO "#define NAME $libname" > $output_objdir/a2ixlibrary.data~$ECHO "#define LIBRARY_ID 1" >> $output_objdir/a2ixlibrary.data~$ECHO "#define VERSION $major" >> $output_objdir/a2ixlibrary.data~$ECHO "#define REVISION $revision" >> $output_objdir/a2ixlibrary.data~$AR $AR_FLAGS $lib $libobjs~$RANLIB $lib~(cd $output_objdir && a2ixlibrary -32)' + hardcode_libdir_flag_spec='-L$libdir' + hardcode_minus_L=yes + ;; + esac + ;; + + beos*) + if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then + allow_undefined_flag=unsupported + # Joseph Beckenbach says some releases of gcc + # support --undefined. This deserves some investigation. FIXME + archive_cmds='$CC -nostart $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + else + ld_shlibs=no + fi + ;; + + cygwin* | mingw* | pw32* | cegcc*) + # _LT_TAGVAR(hardcode_libdir_flag_spec, ) is actually meaningless, + # as there is no search path for DLLs. + hardcode_libdir_flag_spec='-L$libdir' + export_dynamic_flag_spec='$wl--export-all-symbols' + allow_undefined_flag=unsupported + always_export_symbols=no + enable_shared_with_static_runtimes=yes + export_symbols_cmds='$NM $libobjs $convenience | $global_symbol_pipe | $SED -e '\''/^[BCDGRS][ ]/s/.*[ ]\([^ ]*\)/\1 DATA/;s/^.*[ ]__nm__\([^ ]*\)[ ][^ ]*/\1 DATA/;/^I[ ]/d;/^[AITW][ ]/s/.* //'\'' | sort | uniq > $export_symbols' + exclude_expsyms='[_]+GLOBAL_OFFSET_TABLE_|[_]+GLOBAL__[FID]_.*|[_]+head_[A-Za-z0-9_]+_dll|[A-Za-z0-9_]+_dll_iname' + + if $LD --help 2>&1 | $GREP 'auto-import' > /dev/null; then + archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib' + # If the export-symbols file already is a .def file, use it as + # is; otherwise, prepend EXPORTS... + archive_expsym_cmds='if test DEF = "`$SED -n -e '\''s/^[ ]*//'\'' -e '\''/^\(;.*\)*$/d'\'' -e '\''s/^\(EXPORTS\|LIBRARY\)\([ ].*\)*$/DEF/p'\'' -e q $export_symbols`" ; then + cp $export_symbols $output_objdir/$soname.def; + else + echo EXPORTS > $output_objdir/$soname.def; + cat $export_symbols >> $output_objdir/$soname.def; + fi~ + $CC -shared $output_objdir/$soname.def $libobjs $deplibs $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib' + else + ld_shlibs=no + fi + ;; + + haiku*) + archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + link_all_deplibs=yes + ;; + + os2*) + hardcode_libdir_flag_spec='-L$libdir' + hardcode_minus_L=yes + allow_undefined_flag=unsupported + shrext_cmds=.dll + archive_cmds='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~ + $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~ + $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~ + $ECHO EXPORTS >> $output_objdir/$libname.def~ + emxexp $libobjs | $SED /"_DLL_InitTerm"/d >> $output_objdir/$libname.def~ + $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~ + emximp -o $lib $output_objdir/$libname.def' + archive_expsym_cmds='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~ + $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~ + $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~ + $ECHO EXPORTS >> $output_objdir/$libname.def~ + prefix_cmds="$SED"~ + if test EXPORTS = "`$SED 1q $export_symbols`"; then + prefix_cmds="$prefix_cmds -e 1d"; + fi~ + prefix_cmds="$prefix_cmds -e \"s/^\(.*\)$/_\1/g\""~ + cat $export_symbols | $prefix_cmds >> $output_objdir/$libname.def~ + $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~ + emximp -o $lib $output_objdir/$libname.def' + old_archive_From_new_cmds='emximp -o $output_objdir/${libname}_dll.a $output_objdir/$libname.def' + enable_shared_with_static_runtimes=yes + ;; + + interix[3-9]*) + hardcode_direct=no + hardcode_shlibpath_var=no + hardcode_libdir_flag_spec='$wl-rpath,$libdir' + export_dynamic_flag_spec='$wl-E' + # Hack: On Interix 3.x, we cannot compile PIC because of a broken gcc. + # Instead, shared libraries are loaded at an image base (0x10000000 by + # default) and relocated if they conflict, which is a slow very memory + # consuming and fragmenting process. To avoid this, we pick a random, + # 256 KiB-aligned image base between 0x50000000 and 0x6FFC0000 at link + # time. Moving up from 0x10000000 also allows more sbrk(2) space. + archive_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib' + archive_expsym_cmds='sed "s|^|_|" $export_symbols >$output_objdir/$soname.expsym~$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--retain-symbols-file,$output_objdir/$soname.expsym $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib' + ;; + + gnu* | linux* | tpf* | k*bsd*-gnu | kopensolaris*-gnu) + tmp_diet=no + if test linux-dietlibc = "$host_os"; then + case $cc_basename in + diet\ *) tmp_diet=yes;; # linux-dietlibc with static linking (!diet-dyn) + esac + fi + if $LD --help 2>&1 | $EGREP ': supported targets:.* elf' > /dev/null \ + && test no = "$tmp_diet" + then + tmp_addflag=' $pic_flag' + tmp_sharedflag='-shared' + case $cc_basename,$host_cpu in + pgcc*) # Portland Group C compiler + whole_archive_flag_spec='$wl--whole-archive`for conv in $convenience\"\"; do test -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive' + tmp_addflag=' $pic_flag' + ;; + pgf77* | pgf90* | pgf95* | pgfortran*) + # Portland Group f77 and f90 compilers + whole_archive_flag_spec='$wl--whole-archive`for conv in $convenience\"\"; do test -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive' + tmp_addflag=' $pic_flag -Mnomain' ;; + ecc*,ia64* | icc*,ia64*) # Intel C compiler on ia64 + tmp_addflag=' -i_dynamic' ;; + efc*,ia64* | ifort*,ia64*) # Intel Fortran compiler on ia64 + tmp_addflag=' -i_dynamic -nofor_main' ;; + ifc* | ifort*) # Intel Fortran compiler + tmp_addflag=' -nofor_main' ;; + lf95*) # Lahey Fortran 8.1 + whole_archive_flag_spec= + tmp_sharedflag='--shared' ;; + nagfor*) # NAGFOR 5.3 + tmp_sharedflag='-Wl,-shared' ;; + xl[cC]* | bgxl[cC]* | mpixl[cC]*) # IBM XL C 8.0 on PPC (deal with xlf below) + tmp_sharedflag='-qmkshrobj' + tmp_addflag= ;; + nvcc*) # Cuda Compiler Driver 2.2 + whole_archive_flag_spec='$wl--whole-archive`for conv in $convenience\"\"; do test -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive' + compiler_needs_object=yes + ;; + esac + case `$CC -V 2>&1 | sed 5q` in + *Sun\ C*) # Sun C 5.9 + whole_archive_flag_spec='$wl--whole-archive`new_convenience=; for conv in $convenience\"\"; do test -z \"$conv\" || new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive' + compiler_needs_object=yes + tmp_sharedflag='-G' ;; + *Sun\ F*) # Sun Fortran 8.3 + tmp_sharedflag='-G' ;; + esac + archive_cmds='$CC '"$tmp_sharedflag""$tmp_addflag"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + + if test yes = "$supports_anon_versioning"; then + archive_expsym_cmds='echo "{ global:" > $output_objdir/$libname.ver~ + cat $export_symbols | sed -e "s/\(.*\)/\1;/" >> $output_objdir/$libname.ver~ + echo "local: *; };" >> $output_objdir/$libname.ver~ + $CC '"$tmp_sharedflag""$tmp_addflag"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-version-script $wl$output_objdir/$libname.ver -o $lib' + fi + + case $cc_basename in + tcc*) + export_dynamic_flag_spec='-rdynamic' + ;; + xlf* | bgf* | bgxlf* | mpixlf*) + # IBM XL Fortran 10.1 on PPC cannot create shared libs itself + whole_archive_flag_spec='--whole-archive$convenience --no-whole-archive' + hardcode_libdir_flag_spec='$wl-rpath $wl$libdir' + archive_cmds='$LD -shared $libobjs $deplibs $linker_flags -soname $soname -o $lib' + if test yes = "$supports_anon_versioning"; then + archive_expsym_cmds='echo "{ global:" > $output_objdir/$libname.ver~ + cat $export_symbols | sed -e "s/\(.*\)/\1;/" >> $output_objdir/$libname.ver~ + echo "local: *; };" >> $output_objdir/$libname.ver~ + $LD -shared $libobjs $deplibs $linker_flags -soname $soname -version-script $output_objdir/$libname.ver -o $lib' + fi + ;; + esac + else + ld_shlibs=no + fi + ;; + + netbsd* | netbsdelf*-gnu) + if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then + archive_cmds='$LD -Bshareable $libobjs $deplibs $linker_flags -o $lib' + wlarc= + else + archive_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + archive_expsym_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + fi + ;; + + solaris*) + if $LD -v 2>&1 | $GREP 'BFD 2\.8' > /dev/null; then + ld_shlibs=no + cat <<_LT_EOF 1>&2 + +*** Warning: The releases 2.8.* of the GNU linker cannot reliably +*** create shared libraries on Solaris systems. Therefore, libtool +*** is disabling shared libraries support. We urge you to upgrade GNU +*** binutils to release 2.9.1 or newer. Another option is to modify +*** your PATH or compiler configuration so that the native linker is +*** used, and then restart. + +_LT_EOF + elif $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then + archive_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + archive_expsym_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + else + ld_shlibs=no + fi + ;; + + sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX*) + case `$LD -v 2>&1` in + *\ [01].* | *\ 2.[0-9].* | *\ 2.1[0-5].*) + ld_shlibs=no + cat <<_LT_EOF 1>&2 + +*** Warning: Releases of the GNU linker prior to 2.16.91.0.3 cannot +*** reliably create shared libraries on SCO systems. Therefore, libtool +*** is disabling shared libraries support. We urge you to upgrade GNU +*** binutils to release 2.16.91.0.3 or newer. Another option is to modify +*** your PATH or compiler configuration so that the native linker is +*** used, and then restart. + +_LT_EOF + ;; + *) + # For security reasons, it is highly recommended that you always + # use absolute paths for naming shared libraries, and exclude the + # DT_RUNPATH tag from executables and libraries. But doing so + # requires that you compile everything twice, which is a pain. + if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then + hardcode_libdir_flag_spec='$wl-rpath $wl$libdir' + archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + archive_expsym_cmds='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + else + ld_shlibs=no + fi + ;; + esac + ;; + + sunos4*) + archive_cmds='$LD -assert pure-text -Bshareable -o $lib $libobjs $deplibs $linker_flags' + wlarc= + hardcode_direct=yes + hardcode_shlibpath_var=no + ;; + + *) + if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then + archive_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + archive_expsym_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib' + else + ld_shlibs=no + fi + ;; + esac + + if test no = "$ld_shlibs"; then + runpath_var= + hardcode_libdir_flag_spec= + export_dynamic_flag_spec= + whole_archive_flag_spec= + fi + else + # PORTME fill in a description of your system's linker (not GNU ld) + case $host_os in + aix3*) + allow_undefined_flag=unsupported + always_export_symbols=yes + archive_expsym_cmds='$LD -o $output_objdir/$soname $libobjs $deplibs $linker_flags -bE:$export_symbols -T512 -H512 -bM:SRE~$AR $AR_FLAGS $lib $output_objdir/$soname' + # Note: this linker hardcodes the directories in LIBPATH if there + # are no directories specified by -L. + hardcode_minus_L=yes + if test yes = "$GCC" && test -z "$lt_prog_compiler_static"; then + # Neither direct hardcoding nor static linking is supported with a + # broken collect2. + hardcode_direct=unsupported + fi + ;; + + aix[4-9]*) + if test ia64 = "$host_cpu"; then + # On IA64, the linker does run time linking by default, so we don't + # have to do anything special. + aix_use_runtimelinking=no + exp_sym_flag='-Bexport' + no_entry_flag= + else + # If we're using GNU nm, then we don't want the "-C" option. + # -C means demangle to GNU nm, but means don't demangle to AIX nm. + # Without the "-l" option, or with the "-B" option, AIX nm treats + # weak defined symbols like other global defined symbols, whereas + # GNU nm marks them as "W". + # While the 'weak' keyword is ignored in the Export File, we need + # it in the Import File for the 'aix-soname' feature, so we have + # to replace the "-B" option with "-P" for AIX nm. + if $NM -V 2>&1 | $GREP 'GNU' > /dev/null; then + export_symbols_cmds='$NM -Bpg $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "W")) && (substr(\$ 3,1,1) != ".")) { if (\$ 2 == "W") { print \$ 3 " weak" } else { print \$ 3 } } }'\'' | sort -u > $export_symbols' + else + export_symbols_cmds='`func_echo_all $NM | $SED -e '\''s/B\([^B]*\)$/P\1/'\''` -PCpgl $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) && (substr(\$ 1,1,1) != ".")) { if ((\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) { print \$ 1 " weak" } else { print \$ 1 } } }'\'' | sort -u > $export_symbols' + fi + aix_use_runtimelinking=no + + # Test if we are trying to use run time linking or normal + # AIX style linking. If -brtl is somewhere in LDFLAGS, we + # have runtime linking enabled, and use it for executables. + # For shared libraries, we enable/disable runtime linking + # depending on the kind of the shared library created - + # when "with_aix_soname,aix_use_runtimelinking" is: + # "aix,no" lib.a(lib.so.V) shared, rtl:no, for executables + # "aix,yes" lib.so shared, rtl:yes, for executables + # lib.a static archive + # "both,no" lib.so.V(shr.o) shared, rtl:yes + # lib.a(lib.so.V) shared, rtl:no, for executables + # "both,yes" lib.so.V(shr.o) shared, rtl:yes, for executables + # lib.a(lib.so.V) shared, rtl:no + # "svr4,*" lib.so.V(shr.o) shared, rtl:yes, for executables + # lib.a static archive + case $host_os in aix4.[23]|aix4.[23].*|aix[5-9]*) + for ld_flag in $LDFLAGS; do + if (test x-brtl = "x$ld_flag" || test x-Wl,-brtl = "x$ld_flag"); then + aix_use_runtimelinking=yes + break + fi + done + if test svr4,no = "$with_aix_soname,$aix_use_runtimelinking"; then + # With aix-soname=svr4, we create the lib.so.V shared archives only, + # so we don't have lib.a shared libs to link our executables. + # We have to force runtime linking in this case. + aix_use_runtimelinking=yes + LDFLAGS="$LDFLAGS -Wl,-brtl" + fi + ;; + esac + + exp_sym_flag='-bexport' + no_entry_flag='-bnoentry' + fi + + # When large executables or shared objects are built, AIX ld can + # have problems creating the table of contents. If linking a library + # or program results in "error TOC overflow" add -mminimal-toc to + # CXXFLAGS/CFLAGS for g++/gcc. In the cases where that is not + # enough to fix the problem, add -Wl,-bbigtoc to LDFLAGS. + + archive_cmds='' + hardcode_direct=yes + hardcode_direct_absolute=yes + hardcode_libdir_separator=':' + link_all_deplibs=yes + file_list_spec='$wl-f,' + case $with_aix_soname,$aix_use_runtimelinking in + aix,*) ;; # traditional, no import file + svr4,* | *,yes) # use import file + # The Import File defines what to hardcode. + hardcode_direct=no + hardcode_direct_absolute=no + ;; + esac + + if test yes = "$GCC"; then + case $host_os in aix4.[012]|aix4.[012].*) + # We only want to do this on AIX 4.2 and lower, the check + # below for broken collect2 doesn't work under 4.3+ + collect2name=`$CC -print-prog-name=collect2` + if test -f "$collect2name" && + strings "$collect2name" | $GREP resolve_lib_name >/dev/null + then + # We have reworked collect2 + : + else + # We have old collect2 + hardcode_direct=unsupported + # It fails to find uninstalled libraries when the uninstalled + # path is not listed in the libpath. Setting hardcode_minus_L + # to unsupported forces relinking + hardcode_minus_L=yes + hardcode_libdir_flag_spec='-L$libdir' + hardcode_libdir_separator= + fi + ;; + esac + shared_flag='-shared' + if test yes = "$aix_use_runtimelinking"; then + shared_flag="$shared_flag "'$wl-G' + fi + # Need to ensure runtime linking is disabled for the traditional + # shared library, or the linker may eventually find shared libraries + # /with/ Import File - we do not want to mix them. + shared_flag_aix='-shared' + shared_flag_svr4='-shared $wl-G' + else + # not using gcc + if test ia64 = "$host_cpu"; then + # VisualAge C++, Version 5.5 for AIX 5L for IA-64, Beta 3 Release + # chokes on -Wl,-G. The following line is correct: + shared_flag='-G' + else + if test yes = "$aix_use_runtimelinking"; then + shared_flag='$wl-G' + else + shared_flag='$wl-bM:SRE' + fi + shared_flag_aix='$wl-bM:SRE' + shared_flag_svr4='$wl-G' + fi + fi + + export_dynamic_flag_spec='$wl-bexpall' + # It seems that -bexpall does not export symbols beginning with + # underscore (_), so it is better to generate a list of symbols to export. + always_export_symbols=yes + if test aix,yes = "$with_aix_soname,$aix_use_runtimelinking"; then + # Warning - without using the other runtime loading flags (-brtl), + # -berok will link without error, but may produce a broken library. + allow_undefined_flag='-berok' + # Determine the default libpath from the value encoded in an + # empty executable. + if test set = "${lt_cv_aix_libpath+set}"; then + aix_libpath=$lt_cv_aix_libpath +else + if ${lt_cv_aix_libpath_+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + + lt_aix_libpath_sed=' + /Import File Strings/,/^$/ { + /^0/ { + s/^0 *\([^ ]*\) *$/\1/ + p + } + }' + lt_cv_aix_libpath_=`dump -H conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"` + # Check for a 64-bit object if we didn't find anything. + if test -z "$lt_cv_aix_libpath_"; then + lt_cv_aix_libpath_=`dump -HX64 conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"` + fi +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext + if test -z "$lt_cv_aix_libpath_"; then + lt_cv_aix_libpath_=/usr/lib:/lib + fi + +fi + + aix_libpath=$lt_cv_aix_libpath_ +fi + + hardcode_libdir_flag_spec='$wl-blibpath:$libdir:'"$aix_libpath" + archive_expsym_cmds='$CC -o $output_objdir/$soname $libobjs $deplibs $wl'$no_entry_flag' $compiler_flags `if test -n "$allow_undefined_flag"; then func_echo_all "$wl$allow_undefined_flag"; else :; fi` $wl'$exp_sym_flag:\$export_symbols' '$shared_flag + else + if test ia64 = "$host_cpu"; then + hardcode_libdir_flag_spec='$wl-R $libdir:/usr/lib:/lib' + allow_undefined_flag="-z nodefs" + archive_expsym_cmds="\$CC $shared_flag"' -o $output_objdir/$soname $libobjs $deplibs '"\$wl$no_entry_flag"' $compiler_flags $wl$allow_undefined_flag '"\$wl$exp_sym_flag:\$export_symbols" + else + # Determine the default libpath from the value encoded in an + # empty executable. + if test set = "${lt_cv_aix_libpath+set}"; then + aix_libpath=$lt_cv_aix_libpath +else + if ${lt_cv_aix_libpath_+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + + lt_aix_libpath_sed=' + /Import File Strings/,/^$/ { + /^0/ { + s/^0 *\([^ ]*\) *$/\1/ + p + } + }' + lt_cv_aix_libpath_=`dump -H conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"` + # Check for a 64-bit object if we didn't find anything. + if test -z "$lt_cv_aix_libpath_"; then + lt_cv_aix_libpath_=`dump -HX64 conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"` + fi +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext + if test -z "$lt_cv_aix_libpath_"; then + lt_cv_aix_libpath_=/usr/lib:/lib + fi + +fi + + aix_libpath=$lt_cv_aix_libpath_ +fi + + hardcode_libdir_flag_spec='$wl-blibpath:$libdir:'"$aix_libpath" + # Warning - without using the other run time loading flags, + # -berok will link without error, but may produce a broken library. + no_undefined_flag=' $wl-bernotok' + allow_undefined_flag=' $wl-berok' + if test yes = "$with_gnu_ld"; then + # We only use this code for GNU lds that support --whole-archive. + whole_archive_flag_spec='$wl--whole-archive$convenience $wl--no-whole-archive' + else + # Exported symbols can be pulled into shared objects from archives + whole_archive_flag_spec='$convenience' + fi + archive_cmds_need_lc=yes + archive_expsym_cmds='$RM -r $output_objdir/$realname.d~$MKDIR $output_objdir/$realname.d' + # -brtl affects multiple linker settings, -berok does not and is overridden later + compiler_flags_filtered='`func_echo_all "$compiler_flags " | $SED -e "s%-brtl\\([, ]\\)%-berok\\1%g"`' + if test svr4 != "$with_aix_soname"; then + # This is similar to how AIX traditionally builds its shared libraries. + archive_expsym_cmds="$archive_expsym_cmds"'~$CC '$shared_flag_aix' -o $output_objdir/$realname.d/$soname $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$AR $AR_FLAGS $output_objdir/$libname$release.a $output_objdir/$realname.d/$soname' + fi + if test aix != "$with_aix_soname"; then + archive_expsym_cmds="$archive_expsym_cmds"'~$CC '$shared_flag_svr4' -o $output_objdir/$realname.d/$shared_archive_member_spec.o $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$STRIP -e $output_objdir/$realname.d/$shared_archive_member_spec.o~( func_echo_all "#! $soname($shared_archive_member_spec.o)"; if test shr_64 = "$shared_archive_member_spec"; then func_echo_all "# 64"; else func_echo_all "# 32"; fi; cat $export_symbols ) > $output_objdir/$realname.d/$shared_archive_member_spec.imp~$AR $AR_FLAGS $output_objdir/$soname $output_objdir/$realname.d/$shared_archive_member_spec.o $output_objdir/$realname.d/$shared_archive_member_spec.imp' + else + # used by -dlpreopen to get the symbols + archive_expsym_cmds="$archive_expsym_cmds"'~$MV $output_objdir/$realname.d/$soname $output_objdir' + fi + archive_expsym_cmds="$archive_expsym_cmds"'~$RM -r $output_objdir/$realname.d' + fi + fi + ;; + + amigaos*) + case $host_cpu in + powerpc) + # see comment about AmigaOS4 .so support + archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib' + archive_expsym_cmds='' + ;; + m68k) + archive_cmds='$RM $output_objdir/a2ixlibrary.data~$ECHO "#define NAME $libname" > $output_objdir/a2ixlibrary.data~$ECHO "#define LIBRARY_ID 1" >> $output_objdir/a2ixlibrary.data~$ECHO "#define VERSION $major" >> $output_objdir/a2ixlibrary.data~$ECHO "#define REVISION $revision" >> $output_objdir/a2ixlibrary.data~$AR $AR_FLAGS $lib $libobjs~$RANLIB $lib~(cd $output_objdir && a2ixlibrary -32)' + hardcode_libdir_flag_spec='-L$libdir' + hardcode_minus_L=yes + ;; + esac + ;; + + bsdi[45]*) + export_dynamic_flag_spec=-rdynamic + ;; + + cygwin* | mingw* | pw32* | cegcc*) + # When not using gcc, we currently assume that we are using + # Microsoft Visual C++. + # hardcode_libdir_flag_spec is actually meaningless, as there is + # no search path for DLLs. + case $cc_basename in + cl*) + # Native MSVC + hardcode_libdir_flag_spec=' ' + allow_undefined_flag=unsupported + always_export_symbols=yes + file_list_spec='@' + # Tell ltmain to make .lib files, not .a files. + libext=lib + # Tell ltmain to make .dll files, not .so files. + shrext_cmds=.dll + # FIXME: Setting linknames here is a bad hack. + archive_cmds='$CC -o $output_objdir/$soname $libobjs $compiler_flags $deplibs -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~linknames=' + archive_expsym_cmds='if test DEF = "`$SED -n -e '\''s/^[ ]*//'\'' -e '\''/^\(;.*\)*$/d'\'' -e '\''s/^\(EXPORTS\|LIBRARY\)\([ ].*\)*$/DEF/p'\'' -e q $export_symbols`" ; then + cp "$export_symbols" "$output_objdir/$soname.def"; + echo "$tool_output_objdir$soname.def" > "$output_objdir/$soname.exp"; + else + $SED -e '\''s/^/-link -EXPORT:/'\'' < $export_symbols > $output_objdir/$soname.exp; + fi~ + $CC -o $tool_output_objdir$soname $libobjs $compiler_flags $deplibs "@$tool_output_objdir$soname.exp" -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~ + linknames=' + # The linker will not automatically build a static lib if we build a DLL. + # _LT_TAGVAR(old_archive_from_new_cmds, )='true' + enable_shared_with_static_runtimes=yes + exclude_expsyms='_NULL_IMPORT_DESCRIPTOR|_IMPORT_DESCRIPTOR_.*' + export_symbols_cmds='$NM $libobjs $convenience | $global_symbol_pipe | $SED -e '\''/^[BCDGRS][ ]/s/.*[ ]\([^ ]*\)/\1,DATA/'\'' | $SED -e '\''/^[AITW][ ]/s/.*[ ]//'\'' | sort | uniq > $export_symbols' + # Don't use ranlib + old_postinstall_cmds='chmod 644 $oldlib' + postlink_cmds='lt_outputfile="@OUTPUT@"~ + lt_tool_outputfile="@TOOL_OUTPUT@"~ + case $lt_outputfile in + *.exe|*.EXE) ;; + *) + lt_outputfile=$lt_outputfile.exe + lt_tool_outputfile=$lt_tool_outputfile.exe + ;; + esac~ + if test : != "$MANIFEST_TOOL" && test -f "$lt_outputfile.manifest"; then + $MANIFEST_TOOL -manifest "$lt_tool_outputfile.manifest" -outputresource:"$lt_tool_outputfile" || exit 1; + $RM "$lt_outputfile.manifest"; + fi' + ;; + *) + # Assume MSVC wrapper + hardcode_libdir_flag_spec=' ' + allow_undefined_flag=unsupported + # Tell ltmain to make .lib files, not .a files. + libext=lib + # Tell ltmain to make .dll files, not .so files. + shrext_cmds=.dll + # FIXME: Setting linknames here is a bad hack. + archive_cmds='$CC -o $lib $libobjs $compiler_flags `func_echo_all "$deplibs" | $SED '\''s/ -lc$//'\''` -link -dll~linknames=' + # The linker will automatically build a .lib file if we build a DLL. + old_archive_from_new_cmds='true' + # FIXME: Should let the user specify the lib program. + old_archive_cmds='lib -OUT:$oldlib$oldobjs$old_deplibs' + enable_shared_with_static_runtimes=yes + ;; + esac + ;; + + darwin* | rhapsody*) + + + archive_cmds_need_lc=no + hardcode_direct=no + hardcode_automatic=yes + hardcode_shlibpath_var=unsupported + if test yes = "$lt_cv_ld_force_load"; then + whole_archive_flag_spec='`for conv in $convenience\"\"; do test -n \"$conv\" && new_convenience=\"$new_convenience $wl-force_load,$conv\"; done; func_echo_all \"$new_convenience\"`' + + else + whole_archive_flag_spec='' + fi + link_all_deplibs=yes + allow_undefined_flag=$_lt_dar_allow_undefined + case $cc_basename in + ifort*|nagfor*) _lt_dar_can_shared=yes ;; + *) _lt_dar_can_shared=$GCC ;; + esac + if test yes = "$_lt_dar_can_shared"; then + output_verbose_link_cmd=func_echo_all + archive_cmds="\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$libobjs \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring $_lt_dar_single_mod$_lt_dsymutil" + module_cmds="\$CC \$allow_undefined_flag -o \$lib -bundle \$libobjs \$deplibs \$compiler_flags$_lt_dsymutil" + archive_expsym_cmds="sed 's|^|_|' < \$export_symbols > \$output_objdir/\$libname-symbols.expsym~\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$libobjs \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring $_lt_dar_single_mod$_lt_dar_export_syms$_lt_dsymutil" + module_expsym_cmds="sed -e 's|^|_|' < \$export_symbols > \$output_objdir/\$libname-symbols.expsym~\$CC \$allow_undefined_flag -o \$lib -bundle \$libobjs \$deplibs \$compiler_flags$_lt_dar_export_syms$_lt_dsymutil" + + else + ld_shlibs=no + fi + + ;; + + dgux*) + archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + hardcode_libdir_flag_spec='-L$libdir' + hardcode_shlibpath_var=no + ;; + + # FreeBSD 2.2.[012] allows us to include c++rt0.o to get C++ constructor + # support. Future versions do this automatically, but an explicit c++rt0.o + # does not break anything, and helps significantly (at the cost of a little + # extra space). + freebsd2.2*) + archive_cmds='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags /usr/lib/c++rt0.o' + hardcode_libdir_flag_spec='-R$libdir' + hardcode_direct=yes + hardcode_shlibpath_var=no + ;; + + # Unfortunately, older versions of FreeBSD 2 do not have this feature. + freebsd2.*) + archive_cmds='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags' + hardcode_direct=yes + hardcode_minus_L=yes + hardcode_shlibpath_var=no + ;; + + # FreeBSD 3 and greater uses gcc -shared to do shared libraries. + freebsd* | dragonfly*) + archive_cmds='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags' + hardcode_libdir_flag_spec='-R$libdir' + hardcode_direct=yes + hardcode_shlibpath_var=no + ;; + + hpux9*) + if test yes = "$GCC"; then + archive_cmds='$RM $output_objdir/$soname~$CC -shared $pic_flag $wl+b $wl$install_libdir -o $output_objdir/$soname $libobjs $deplibs $compiler_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib' + else + archive_cmds='$RM $output_objdir/$soname~$LD -b +b $install_libdir -o $output_objdir/$soname $libobjs $deplibs $linker_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib' + fi + hardcode_libdir_flag_spec='$wl+b $wl$libdir' + hardcode_libdir_separator=: + hardcode_direct=yes + + # hardcode_minus_L: Not really in the search PATH, + # but as the default location of the library. + hardcode_minus_L=yes + export_dynamic_flag_spec='$wl-E' + ;; + + hpux10*) + if test yes,no = "$GCC,$with_gnu_ld"; then + archive_cmds='$CC -shared $pic_flag $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags' + else + archive_cmds='$LD -b +h $soname +b $install_libdir -o $lib $libobjs $deplibs $linker_flags' + fi + if test no = "$with_gnu_ld"; then + hardcode_libdir_flag_spec='$wl+b $wl$libdir' + hardcode_libdir_separator=: + hardcode_direct=yes + hardcode_direct_absolute=yes + export_dynamic_flag_spec='$wl-E' + # hardcode_minus_L: Not really in the search PATH, + # but as the default location of the library. + hardcode_minus_L=yes + fi + ;; + + hpux11*) + if test yes,no = "$GCC,$with_gnu_ld"; then + case $host_cpu in + hppa*64*) + archive_cmds='$CC -shared $wl+h $wl$soname -o $lib $libobjs $deplibs $compiler_flags' + ;; + ia64*) + archive_cmds='$CC -shared $pic_flag $wl+h $wl$soname $wl+nodefaultrpath -o $lib $libobjs $deplibs $compiler_flags' + ;; + *) + archive_cmds='$CC -shared $pic_flag $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags' + ;; + esac + else + case $host_cpu in + hppa*64*) + archive_cmds='$CC -b $wl+h $wl$soname -o $lib $libobjs $deplibs $compiler_flags' + ;; + ia64*) + archive_cmds='$CC -b $wl+h $wl$soname $wl+nodefaultrpath -o $lib $libobjs $deplibs $compiler_flags' + ;; + *) + + # Older versions of the 11.00 compiler do not understand -b yet + # (HP92453-01 A.11.01.20 doesn't, HP92453-01 B.11.X.35175-35176.GP does) + { $as_echo "$as_me:${as_lineno-$LINENO}: checking if $CC understands -b" >&5 +$as_echo_n "checking if $CC understands -b... " >&6; } +if ${lt_cv_prog_compiler__b+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_prog_compiler__b=no + save_LDFLAGS=$LDFLAGS + LDFLAGS="$LDFLAGS -b" + echo "$lt_simple_link_test_code" > conftest.$ac_ext + if (eval $ac_link 2>conftest.err) && test -s conftest$ac_exeext; then + # The linker can only warn and ignore the option if not recognized + # So say no if there are warnings + if test -s conftest.err; then + # Append any errors to the config.log. + cat conftest.err 1>&5 + $ECHO "$_lt_linker_boilerplate" | $SED '/^$/d' > conftest.exp + $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2 + if diff conftest.exp conftest.er2 >/dev/null; then + lt_cv_prog_compiler__b=yes + fi + else + lt_cv_prog_compiler__b=yes + fi + fi + $RM -r conftest* + LDFLAGS=$save_LDFLAGS + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler__b" >&5 +$as_echo "$lt_cv_prog_compiler__b" >&6; } + +if test yes = "$lt_cv_prog_compiler__b"; then + archive_cmds='$CC -b $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags' +else + archive_cmds='$LD -b +h $soname +b $install_libdir -o $lib $libobjs $deplibs $linker_flags' +fi + + ;; + esac + fi + if test no = "$with_gnu_ld"; then + hardcode_libdir_flag_spec='$wl+b $wl$libdir' + hardcode_libdir_separator=: + + case $host_cpu in + hppa*64*|ia64*) + hardcode_direct=no + hardcode_shlibpath_var=no + ;; + *) + hardcode_direct=yes + hardcode_direct_absolute=yes + export_dynamic_flag_spec='$wl-E' + + # hardcode_minus_L: Not really in the search PATH, + # but as the default location of the library. + hardcode_minus_L=yes + ;; + esac + fi + ;; + + irix5* | irix6* | nonstopux*) + if test yes = "$GCC"; then + archive_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib' + # Try to use the -exported_symbol ld option, if it does not + # work, assume that -exports_file does not work either and + # implicitly export all symbols. + # This should be the same for all languages, so no per-tag cache variable. + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether the $host_os linker accepts -exported_symbol" >&5 +$as_echo_n "checking whether the $host_os linker accepts -exported_symbol... " >&6; } +if ${lt_cv_irix_exported_symbol+:} false; then : + $as_echo_n "(cached) " >&6 +else + save_LDFLAGS=$LDFLAGS + LDFLAGS="$LDFLAGS -shared $wl-exported_symbol ${wl}foo $wl-update_registry $wl/dev/null" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +int foo (void) { return 0; } +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + lt_cv_irix_exported_symbol=yes +else + lt_cv_irix_exported_symbol=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext + LDFLAGS=$save_LDFLAGS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_irix_exported_symbol" >&5 +$as_echo "$lt_cv_irix_exported_symbol" >&6; } + if test yes = "$lt_cv_irix_exported_symbol"; then + archive_expsym_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations $wl-exports_file $wl$export_symbols -o $lib' + fi + link_all_deplibs=no + else + archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib' + archive_expsym_cmds='$CC -shared $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -exports_file $export_symbols -o $lib' + fi + archive_cmds_need_lc='no' + hardcode_libdir_flag_spec='$wl-rpath $wl$libdir' + hardcode_libdir_separator=: + inherit_rpath=yes + link_all_deplibs=yes + ;; + + linux*) + case $cc_basename in + tcc*) + # Fabrice Bellard et al's Tiny C Compiler + ld_shlibs=yes + archive_cmds='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags' + ;; + esac + ;; + + netbsd* | netbsdelf*-gnu) + if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then + archive_cmds='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags' # a.out + else + archive_cmds='$LD -shared -o $lib $libobjs $deplibs $linker_flags' # ELF + fi + hardcode_libdir_flag_spec='-R$libdir' + hardcode_direct=yes + hardcode_shlibpath_var=no + ;; + + newsos6) + archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + hardcode_direct=yes + hardcode_libdir_flag_spec='$wl-rpath $wl$libdir' + hardcode_libdir_separator=: + hardcode_shlibpath_var=no + ;; + + *nto* | *qnx*) + ;; + + openbsd* | bitrig*) + if test -f /usr/libexec/ld.so; then + hardcode_direct=yes + hardcode_shlibpath_var=no + hardcode_direct_absolute=yes + if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then + archive_cmds='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags' + archive_expsym_cmds='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags $wl-retain-symbols-file,$export_symbols' + hardcode_libdir_flag_spec='$wl-rpath,$libdir' + export_dynamic_flag_spec='$wl-E' + else + archive_cmds='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags' + hardcode_libdir_flag_spec='$wl-rpath,$libdir' + fi + else + ld_shlibs=no + fi + ;; + + os2*) + hardcode_libdir_flag_spec='-L$libdir' + hardcode_minus_L=yes + allow_undefined_flag=unsupported + shrext_cmds=.dll + archive_cmds='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~ + $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~ + $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~ + $ECHO EXPORTS >> $output_objdir/$libname.def~ + emxexp $libobjs | $SED /"_DLL_InitTerm"/d >> $output_objdir/$libname.def~ + $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~ + emximp -o $lib $output_objdir/$libname.def' + archive_expsym_cmds='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~ + $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~ + $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~ + $ECHO EXPORTS >> $output_objdir/$libname.def~ + prefix_cmds="$SED"~ + if test EXPORTS = "`$SED 1q $export_symbols`"; then + prefix_cmds="$prefix_cmds -e 1d"; + fi~ + prefix_cmds="$prefix_cmds -e \"s/^\(.*\)$/_\1/g\""~ + cat $export_symbols | $prefix_cmds >> $output_objdir/$libname.def~ + $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~ + emximp -o $lib $output_objdir/$libname.def' + old_archive_From_new_cmds='emximp -o $output_objdir/${libname}_dll.a $output_objdir/$libname.def' + enable_shared_with_static_runtimes=yes + ;; + + osf3*) + if test yes = "$GCC"; then + allow_undefined_flag=' $wl-expect_unresolved $wl\*' + archive_cmds='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib' + else + allow_undefined_flag=' -expect_unresolved \*' + archive_cmds='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib' + fi + archive_cmds_need_lc='no' + hardcode_libdir_flag_spec='$wl-rpath $wl$libdir' + hardcode_libdir_separator=: + ;; + + osf4* | osf5*) # as osf3* with the addition of -msym flag + if test yes = "$GCC"; then + allow_undefined_flag=' $wl-expect_unresolved $wl\*' + archive_cmds='$CC -shared$allow_undefined_flag $pic_flag $libobjs $deplibs $compiler_flags $wl-msym $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib' + hardcode_libdir_flag_spec='$wl-rpath $wl$libdir' + else + allow_undefined_flag=' -expect_unresolved \*' + archive_cmds='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags -msym -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib' + archive_expsym_cmds='for i in `cat $export_symbols`; do printf "%s %s\\n" -exported_symbol "\$i" >> $lib.exp; done; printf "%s\\n" "-hidden">> $lib.exp~ + $CC -shared$allow_undefined_flag $wl-input $wl$lib.exp $compiler_flags $libobjs $deplibs -soname $soname `test -n "$verstring" && $ECHO "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib~$RM $lib.exp' + + # Both c and cxx compiler support -rpath directly + hardcode_libdir_flag_spec='-rpath $libdir' + fi + archive_cmds_need_lc='no' + hardcode_libdir_separator=: + ;; + + solaris*) + no_undefined_flag=' -z defs' + if test yes = "$GCC"; then + wlarc='$wl' + archive_cmds='$CC -shared $pic_flag $wl-z ${wl}text $wl-h $wl$soname -o $lib $libobjs $deplibs $compiler_flags' + archive_expsym_cmds='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~ + $CC -shared $pic_flag $wl-z ${wl}text $wl-M $wl$lib.exp $wl-h $wl$soname -o $lib $libobjs $deplibs $compiler_flags~$RM $lib.exp' + else + case `$CC -V 2>&1` in + *"Compilers 5.0"*) + wlarc='' + archive_cmds='$LD -G$allow_undefined_flag -h $soname -o $lib $libobjs $deplibs $linker_flags' + archive_expsym_cmds='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~ + $LD -G$allow_undefined_flag -M $lib.exp -h $soname -o $lib $libobjs $deplibs $linker_flags~$RM $lib.exp' + ;; + *) + wlarc='$wl' + archive_cmds='$CC -G$allow_undefined_flag -h $soname -o $lib $libobjs $deplibs $compiler_flags' + archive_expsym_cmds='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~ + $CC -G$allow_undefined_flag -M $lib.exp -h $soname -o $lib $libobjs $deplibs $compiler_flags~$RM $lib.exp' + ;; + esac + fi + hardcode_libdir_flag_spec='-R$libdir' + hardcode_shlibpath_var=no + case $host_os in + solaris2.[0-5] | solaris2.[0-5].*) ;; + *) + # The compiler driver will combine and reorder linker options, + # but understands '-z linker_flag'. GCC discards it without '$wl', + # but is careful enough not to reorder. + # Supported since Solaris 2.6 (maybe 2.5.1?) + if test yes = "$GCC"; then + whole_archive_flag_spec='$wl-z ${wl}allextract$convenience $wl-z ${wl}defaultextract' + else + whole_archive_flag_spec='-z allextract$convenience -z defaultextract' + fi + ;; + esac + link_all_deplibs=yes + ;; + + sunos4*) + if test sequent = "$host_vendor"; then + # Use $CC to link under sequent, because it throws in some extra .o + # files that make .init and .fini sections work. + archive_cmds='$CC -G $wl-h $soname -o $lib $libobjs $deplibs $compiler_flags' + else + archive_cmds='$LD -assert pure-text -Bstatic -o $lib $libobjs $deplibs $linker_flags' + fi + hardcode_libdir_flag_spec='-L$libdir' + hardcode_direct=yes + hardcode_minus_L=yes + hardcode_shlibpath_var=no + ;; + + sysv4) + case $host_vendor in + sni) + archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + hardcode_direct=yes # is this really true??? + ;; + siemens) + ## LD is ld it makes a PLAMLIB + ## CC just makes a GrossModule. + archive_cmds='$LD -G -o $lib $libobjs $deplibs $linker_flags' + reload_cmds='$CC -r -o $output$reload_objs' + hardcode_direct=no + ;; + motorola) + archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + hardcode_direct=no #Motorola manual says yes, but my tests say they lie + ;; + esac + runpath_var='LD_RUN_PATH' + hardcode_shlibpath_var=no + ;; + + sysv4.3*) + archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + hardcode_shlibpath_var=no + export_dynamic_flag_spec='-Bexport' + ;; + + sysv4*MP*) + if test -d /usr/nec; then + archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + hardcode_shlibpath_var=no + runpath_var=LD_RUN_PATH + hardcode_runpath_var=yes + ld_shlibs=yes + fi + ;; + + sysv4*uw2* | sysv5OpenUNIX* | sysv5UnixWare7.[01].[10]* | unixware7* | sco3.2v5.0.[024]*) + no_undefined_flag='$wl-z,text' + archive_cmds_need_lc=no + hardcode_shlibpath_var=no + runpath_var='LD_RUN_PATH' + + if test yes = "$GCC"; then + archive_cmds='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + archive_expsym_cmds='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + else + archive_cmds='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + archive_expsym_cmds='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + fi + ;; + + sysv5* | sco3.2v5* | sco5v6*) + # Note: We CANNOT use -z defs as we might desire, because we do not + # link with -lc, and that would cause any symbols used from libc to + # always be unresolved, which means just about no library would + # ever link correctly. If we're not using GNU ld we use -z text + # though, which does catch some bad symbols but isn't as heavy-handed + # as -z defs. + no_undefined_flag='$wl-z,text' + allow_undefined_flag='$wl-z,nodefs' + archive_cmds_need_lc=no + hardcode_shlibpath_var=no + hardcode_libdir_flag_spec='$wl-R,$libdir' + hardcode_libdir_separator=':' + link_all_deplibs=yes + export_dynamic_flag_spec='$wl-Bexport' + runpath_var='LD_RUN_PATH' + + if test yes = "$GCC"; then + archive_cmds='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + archive_expsym_cmds='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + else + archive_cmds='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + archive_expsym_cmds='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags' + fi + ;; + + uts4*) + archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags' + hardcode_libdir_flag_spec='-L$libdir' + hardcode_shlibpath_var=no + ;; + + *) + ld_shlibs=no + ;; + esac + + if test sni = "$host_vendor"; then + case $host in + sysv4 | sysv4.2uw2* | sysv4.3* | sysv5*) + export_dynamic_flag_spec='$wl-Blargedynsym' + ;; + esac + fi + fi + +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ld_shlibs" >&5 +$as_echo "$ld_shlibs" >&6; } +test no = "$ld_shlibs" && can_build_shared=no + +with_gnu_ld=$with_gnu_ld + + + + + + + + + + + + + + + +# +# Do we need to explicitly link libc? +# +case "x$archive_cmds_need_lc" in +x|xyes) + # Assume -lc should be added + archive_cmds_need_lc=yes + + if test yes,yes = "$GCC,$enable_shared"; then + case $archive_cmds in + *'~'*) + # FIXME: we may have to deal with multi-command sequences. + ;; + '$CC '*) + # Test whether the compiler implicitly links with -lc since on some + # systems, -lgcc has to come before -lc. If gcc already passes -lc + # to ld, don't add -lc before -lgcc. + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether -lc should be explicitly linked in" >&5 +$as_echo_n "checking whether -lc should be explicitly linked in... " >&6; } +if ${lt_cv_archive_cmds_need_lc+:} false; then : + $as_echo_n "(cached) " >&6 +else + $RM conftest* + echo "$lt_simple_compile_test_code" > conftest.$ac_ext + + if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5 + (eval $ac_compile) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } 2>conftest.err; then + soname=conftest + lib=conftest + libobjs=conftest.$ac_objext + deplibs= + wl=$lt_prog_compiler_wl + pic_flag=$lt_prog_compiler_pic + compiler_flags=-v + linker_flags=-v + verstring= + output_objdir=. + libname=conftest + lt_save_allow_undefined_flag=$allow_undefined_flag + allow_undefined_flag= + if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$archive_cmds 2\>\&1 \| $GREP \" -lc \" \>/dev/null 2\>\&1\""; } >&5 + (eval $archive_cmds 2\>\&1 \| $GREP \" -lc \" \>/dev/null 2\>\&1) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } + then + lt_cv_archive_cmds_need_lc=no + else + lt_cv_archive_cmds_need_lc=yes + fi + allow_undefined_flag=$lt_save_allow_undefined_flag + else + cat conftest.err 1>&5 + fi + $RM conftest* + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_archive_cmds_need_lc" >&5 +$as_echo "$lt_cv_archive_cmds_need_lc" >&6; } + archive_cmds_need_lc=$lt_cv_archive_cmds_need_lc + ;; + esac + fi + ;; +esac + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking dynamic linker characteristics" >&5 +$as_echo_n "checking dynamic linker characteristics... " >&6; } + +if test yes = "$GCC"; then + case $host_os in + darwin*) lt_awk_arg='/^libraries:/,/LR/' ;; + *) lt_awk_arg='/^libraries:/' ;; + esac + case $host_os in + mingw* | cegcc*) lt_sed_strip_eq='s|=\([A-Za-z]:\)|\1|g' ;; + *) lt_sed_strip_eq='s|=/|/|g' ;; + esac + lt_search_path_spec=`$CC -print-search-dirs | awk $lt_awk_arg | $SED -e "s/^libraries://" -e $lt_sed_strip_eq` + case $lt_search_path_spec in + *\;*) + # if the path contains ";" then we assume it to be the separator + # otherwise default to the standard path separator (i.e. ":") - it is + # assumed that no part of a normal pathname contains ";" but that should + # okay in the real world where ";" in dirpaths is itself problematic. + lt_search_path_spec=`$ECHO "$lt_search_path_spec" | $SED 's/;/ /g'` + ;; + *) + lt_search_path_spec=`$ECHO "$lt_search_path_spec" | $SED "s/$PATH_SEPARATOR/ /g"` + ;; + esac + # Ok, now we have the path, separated by spaces, we can step through it + # and add multilib dir if necessary... + lt_tmp_lt_search_path_spec= + lt_multi_os_dir=/`$CC $CPPFLAGS $CFLAGS $LDFLAGS -print-multi-os-directory 2>/dev/null` + # ...but if some path component already ends with the multilib dir we assume + # that all is fine and trust -print-search-dirs as is (GCC 4.2? or newer). + case "$lt_multi_os_dir; $lt_search_path_spec " in + "/; "* | "/.; "* | "/./; "* | *"$lt_multi_os_dir "* | *"$lt_multi_os_dir/ "*) + lt_multi_os_dir= + ;; + esac + for lt_sys_path in $lt_search_path_spec; do + if test -d "$lt_sys_path$lt_multi_os_dir"; then + lt_tmp_lt_search_path_spec="$lt_tmp_lt_search_path_spec $lt_sys_path$lt_multi_os_dir" + elif test -n "$lt_multi_os_dir"; then + test -d "$lt_sys_path" && \ + lt_tmp_lt_search_path_spec="$lt_tmp_lt_search_path_spec $lt_sys_path" + fi + done + lt_search_path_spec=`$ECHO "$lt_tmp_lt_search_path_spec" | awk ' +BEGIN {RS = " "; FS = "/|\n";} { + lt_foo = ""; + lt_count = 0; + for (lt_i = NF; lt_i > 0; lt_i--) { + if ($lt_i != "" && $lt_i != ".") { + if ($lt_i == "..") { + lt_count++; + } else { + if (lt_count == 0) { + lt_foo = "/" $lt_i lt_foo; + } else { + lt_count--; + } + } + } + } + if (lt_foo != "") { lt_freq[lt_foo]++; } + if (lt_freq[lt_foo] == 1) { print lt_foo; } +}'` + # AWK program above erroneously prepends '/' to C:/dos/paths + # for these hosts. + case $host_os in + mingw* | cegcc*) lt_search_path_spec=`$ECHO "$lt_search_path_spec" |\ + $SED 's|/\([A-Za-z]:\)|\1|g'` ;; + esac + sys_lib_search_path_spec=`$ECHO "$lt_search_path_spec" | $lt_NL2SP` +else + sys_lib_search_path_spec="/lib /usr/lib /usr/local/lib" +fi +library_names_spec= +libname_spec='lib$name' +soname_spec= +shrext_cmds=.so +postinstall_cmds= +postuninstall_cmds= +finish_cmds= +finish_eval= +shlibpath_var= +shlibpath_overrides_runpath=unknown +version_type=none +dynamic_linker="$host_os ld.so" +sys_lib_dlsearch_path_spec="/lib /usr/lib" +need_lib_prefix=unknown +hardcode_into_libs=no + +# when you set need_version to no, make sure it does not cause -set_version +# flags to be left without arguments +need_version=unknown + + + +case $host_os in +aix3*) + version_type=linux # correct to gnu/linux during the next big refactor + library_names_spec='$libname$release$shared_ext$versuffix $libname.a' + shlibpath_var=LIBPATH + + # AIX 3 has no versioning support, so we append a major version to the name. + soname_spec='$libname$release$shared_ext$major' + ;; + +aix[4-9]*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + hardcode_into_libs=yes + if test ia64 = "$host_cpu"; then + # AIX 5 supports IA64 + library_names_spec='$libname$release$shared_ext$major $libname$release$shared_ext$versuffix $libname$shared_ext' + shlibpath_var=LD_LIBRARY_PATH + else + # With GCC up to 2.95.x, collect2 would create an import file + # for dependence libraries. The import file would start with + # the line '#! .'. This would cause the generated library to + # depend on '.', always an invalid library. This was fixed in + # development snapshots of GCC prior to 3.0. + case $host_os in + aix4 | aix4.[01] | aix4.[01].*) + if { echo '#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 97)' + echo ' yes ' + echo '#endif'; } | $CC -E - | $GREP yes > /dev/null; then + : + else + can_build_shared=no + fi + ;; + esac + # Using Import Files as archive members, it is possible to support + # filename-based versioning of shared library archives on AIX. While + # this would work for both with and without runtime linking, it will + # prevent static linking of such archives. So we do filename-based + # shared library versioning with .so extension only, which is used + # when both runtime linking and shared linking is enabled. + # Unfortunately, runtime linking may impact performance, so we do + # not want this to be the default eventually. Also, we use the + # versioned .so libs for executables only if there is the -brtl + # linker flag in LDFLAGS as well, or --with-aix-soname=svr4 only. + # To allow for filename-based versioning support, we need to create + # libNAME.so.V as an archive file, containing: + # *) an Import File, referring to the versioned filename of the + # archive as well as the shared archive member, telling the + # bitwidth (32 or 64) of that shared object, and providing the + # list of exported symbols of that shared object, eventually + # decorated with the 'weak' keyword + # *) the shared object with the F_LOADONLY flag set, to really avoid + # it being seen by the linker. + # At run time we better use the real file rather than another symlink, + # but for link time we create the symlink libNAME.so -> libNAME.so.V + + case $with_aix_soname,$aix_use_runtimelinking in + # AIX (on Power*) has no versioning support, so currently we cannot hardcode correct + # soname into executable. Probably we can add versioning support to + # collect2, so additional links can be useful in future. + aix,yes) # traditional libtool + dynamic_linker='AIX unversionable lib.so' + # If using run time linking (on AIX 4.2 or later) use lib.so + # instead of lib.a to let people know that these are not + # typical AIX shared libraries. + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + ;; + aix,no) # traditional AIX only + dynamic_linker='AIX lib.a(lib.so.V)' + # We preserve .a as extension for shared libraries through AIX4.2 + # and later when we are not doing run time linking. + library_names_spec='$libname$release.a $libname.a' + soname_spec='$libname$release$shared_ext$major' + ;; + svr4,*) # full svr4 only + dynamic_linker="AIX lib.so.V($shared_archive_member_spec.o)" + library_names_spec='$libname$release$shared_ext$major $libname$shared_ext' + # We do not specify a path in Import Files, so LIBPATH fires. + shlibpath_overrides_runpath=yes + ;; + *,yes) # both, prefer svr4 + dynamic_linker="AIX lib.so.V($shared_archive_member_spec.o), lib.a(lib.so.V)" + library_names_spec='$libname$release$shared_ext$major $libname$shared_ext' + # unpreferred sharedlib libNAME.a needs extra handling + postinstall_cmds='test -n "$linkname" || linkname="$realname"~func_stripname "" ".so" "$linkname"~$install_shared_prog "$dir/$func_stripname_result.$libext" "$destdir/$func_stripname_result.$libext"~test -z "$tstripme" || test -z "$striplib" || $striplib "$destdir/$func_stripname_result.$libext"' + postuninstall_cmds='for n in $library_names $old_library; do :; done~func_stripname "" ".so" "$n"~test "$func_stripname_result" = "$n" || func_append rmfiles " $odir/$func_stripname_result.$libext"' + # We do not specify a path in Import Files, so LIBPATH fires. + shlibpath_overrides_runpath=yes + ;; + *,no) # both, prefer aix + dynamic_linker="AIX lib.a(lib.so.V), lib.so.V($shared_archive_member_spec.o)" + library_names_spec='$libname$release.a $libname.a' + soname_spec='$libname$release$shared_ext$major' + # unpreferred sharedlib libNAME.so.V and symlink libNAME.so need extra handling + postinstall_cmds='test -z "$dlname" || $install_shared_prog $dir/$dlname $destdir/$dlname~test -z "$tstripme" || test -z "$striplib" || $striplib $destdir/$dlname~test -n "$linkname" || linkname=$realname~func_stripname "" ".a" "$linkname"~(cd "$destdir" && $LN_S -f $dlname $func_stripname_result.so)' + postuninstall_cmds='test -z "$dlname" || func_append rmfiles " $odir/$dlname"~for n in $old_library $library_names; do :; done~func_stripname "" ".a" "$n"~func_append rmfiles " $odir/$func_stripname_result.so"' + ;; + esac + shlibpath_var=LIBPATH + fi + ;; + +amigaos*) + case $host_cpu in + powerpc) + # Since July 2007 AmigaOS4 officially supports .so libraries. + # When compiling the executable, add -use-dynld -Lsobjs: to the compileline. + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + ;; + m68k) + library_names_spec='$libname.ixlibrary $libname.a' + # Create ${libname}_ixlibrary.a entries in /sys/libs. + finish_eval='for lib in `ls $libdir/*.ixlibrary 2>/dev/null`; do libname=`func_echo_all "$lib" | $SED '\''s%^.*/\([^/]*\)\.ixlibrary$%\1%'\''`; $RM /sys/libs/${libname}_ixlibrary.a; $show "cd /sys/libs && $LN_S $lib ${libname}_ixlibrary.a"; cd /sys/libs && $LN_S $lib ${libname}_ixlibrary.a || exit 1; done' + ;; + esac + ;; + +beos*) + library_names_spec='$libname$shared_ext' + dynamic_linker="$host_os ld.so" + shlibpath_var=LIBRARY_PATH + ;; + +bsdi[45]*) + version_type=linux # correct to gnu/linux during the next big refactor + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + finish_cmds='PATH="\$PATH:/sbin" ldconfig $libdir' + shlibpath_var=LD_LIBRARY_PATH + sys_lib_search_path_spec="/shlib /usr/lib /usr/X11/lib /usr/contrib/lib /lib /usr/local/lib" + sys_lib_dlsearch_path_spec="/shlib /usr/lib /usr/local/lib" + # the default ld.so.conf also contains /usr/contrib/lib and + # /usr/X11R6/lib (/usr/X11 is a link to /usr/X11R6), but let us allow + # libtool to hard-code these into programs + ;; + +cygwin* | mingw* | pw32* | cegcc*) + version_type=windows + shrext_cmds=.dll + need_version=no + need_lib_prefix=no + + case $GCC,$cc_basename in + yes,*) + # gcc + library_names_spec='$libname.dll.a' + # DLL is installed to $(libdir)/../bin by postinstall_cmds + postinstall_cmds='base_file=`basename \$file`~ + dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; echo \$dlname'\''`~ + dldir=$destdir/`dirname \$dlpath`~ + test -d \$dldir || mkdir -p \$dldir~ + $install_prog $dir/$dlname \$dldir/$dlname~ + chmod a+x \$dldir/$dlname~ + if test -n '\''$stripme'\'' && test -n '\''$striplib'\''; then + eval '\''$striplib \$dldir/$dlname'\'' || exit \$?; + fi' + postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; echo \$dlname'\''`~ + dlpath=$dir/\$dldll~ + $RM \$dlpath' + shlibpath_overrides_runpath=yes + + case $host_os in + cygwin*) + # Cygwin DLLs use 'cyg' prefix rather than 'lib' + soname_spec='`echo $libname | sed -e 's/^lib/cyg/'``echo $release | $SED -e 's/[.]/-/g'`$versuffix$shared_ext' + + sys_lib_search_path_spec="$sys_lib_search_path_spec /usr/lib/w32api" + ;; + mingw* | cegcc*) + # MinGW DLLs use traditional 'lib' prefix + soname_spec='$libname`echo $release | $SED -e 's/[.]/-/g'`$versuffix$shared_ext' + ;; + pw32*) + # pw32 DLLs use 'pw' prefix rather than 'lib' + library_names_spec='`echo $libname | sed -e 's/^lib/pw/'``echo $release | $SED -e 's/[.]/-/g'`$versuffix$shared_ext' + ;; + esac + dynamic_linker='Win32 ld.exe' + ;; + + *,cl*) + # Native MSVC + libname_spec='$name' + soname_spec='$libname`echo $release | $SED -e 's/[.]/-/g'`$versuffix$shared_ext' + library_names_spec='$libname.dll.lib' + + case $build_os in + mingw*) + sys_lib_search_path_spec= + lt_save_ifs=$IFS + IFS=';' + for lt_path in $LIB + do + IFS=$lt_save_ifs + # Let DOS variable expansion print the short 8.3 style file name. + lt_path=`cd "$lt_path" 2>/dev/null && cmd //C "for %i in (".") do @echo %~si"` + sys_lib_search_path_spec="$sys_lib_search_path_spec $lt_path" + done + IFS=$lt_save_ifs + # Convert to MSYS style. + sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | sed -e 's|\\\\|/|g' -e 's| \\([a-zA-Z]\\):| /\\1|g' -e 's|^ ||'` + ;; + cygwin*) + # Convert to unix form, then to dos form, then back to unix form + # but this time dos style (no spaces!) so that the unix form looks + # like /cygdrive/c/PROGRA~1:/cygdr... + sys_lib_search_path_spec=`cygpath --path --unix "$LIB"` + sys_lib_search_path_spec=`cygpath --path --dos "$sys_lib_search_path_spec" 2>/dev/null` + sys_lib_search_path_spec=`cygpath --path --unix "$sys_lib_search_path_spec" | $SED -e "s/$PATH_SEPARATOR/ /g"` + ;; + *) + sys_lib_search_path_spec=$LIB + if $ECHO "$sys_lib_search_path_spec" | $GREP ';[c-zC-Z]:/' >/dev/null; then + # It is most probably a Windows format PATH. + sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | $SED -e 's/;/ /g'` + else + sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | $SED -e "s/$PATH_SEPARATOR/ /g"` + fi + # FIXME: find the short name or the path components, as spaces are + # common. (e.g. "Program Files" -> "PROGRA~1") + ;; + esac + + # DLL is installed to $(libdir)/../bin by postinstall_cmds + postinstall_cmds='base_file=`basename \$file`~ + dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; echo \$dlname'\''`~ + dldir=$destdir/`dirname \$dlpath`~ + test -d \$dldir || mkdir -p \$dldir~ + $install_prog $dir/$dlname \$dldir/$dlname' + postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; echo \$dlname'\''`~ + dlpath=$dir/\$dldll~ + $RM \$dlpath' + shlibpath_overrides_runpath=yes + dynamic_linker='Win32 link.exe' + ;; + + *) + # Assume MSVC wrapper + library_names_spec='$libname`echo $release | $SED -e 's/[.]/-/g'`$versuffix$shared_ext $libname.lib' + dynamic_linker='Win32 ld.exe' + ;; + esac + # FIXME: first we should search . and the directory the executable is in + shlibpath_var=PATH + ;; + +darwin* | rhapsody*) + dynamic_linker="$host_os dyld" + version_type=darwin + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$major$shared_ext $libname$shared_ext' + soname_spec='$libname$release$major$shared_ext' + shlibpath_overrides_runpath=yes + shlibpath_var=DYLD_LIBRARY_PATH + shrext_cmds='`test .$module = .yes && echo .so || echo .dylib`' + + sys_lib_search_path_spec="$sys_lib_search_path_spec /usr/local/lib" + sys_lib_dlsearch_path_spec='/usr/local/lib /lib /usr/lib' + ;; + +dgux*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + ;; + +freebsd* | dragonfly*) + # DragonFly does not have aout. When/if they implement a new + # versioning mechanism, adjust this. + if test -x /usr/bin/objformat; then + objformat=`/usr/bin/objformat` + else + case $host_os in + freebsd[23].*) objformat=aout ;; + *) objformat=elf ;; + esac + fi + version_type=freebsd-$objformat + case $version_type in + freebsd-elf*) + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + need_version=no + need_lib_prefix=no + ;; + freebsd-*) + library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix' + need_version=yes + ;; + esac + shlibpath_var=LD_LIBRARY_PATH + case $host_os in + freebsd2.*) + shlibpath_overrides_runpath=yes + ;; + freebsd3.[01]* | freebsdelf3.[01]*) + shlibpath_overrides_runpath=yes + hardcode_into_libs=yes + ;; + freebsd3.[2-9]* | freebsdelf3.[2-9]* | \ + freebsd4.[0-5] | freebsdelf4.[0-5] | freebsd4.1.1 | freebsdelf4.1.1) + shlibpath_overrides_runpath=no + hardcode_into_libs=yes + ;; + *) # from 4.6 on, and DragonFly + shlibpath_overrides_runpath=yes + hardcode_into_libs=yes + ;; + esac + ;; + +haiku*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + dynamic_linker="$host_os runtime_loader" + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LIBRARY_PATH + shlibpath_overrides_runpath=no + sys_lib_dlsearch_path_spec='/boot/home/config/lib /boot/common/lib /boot/system/lib' + hardcode_into_libs=yes + ;; + +hpux9* | hpux10* | hpux11*) + # Give a soname corresponding to the major version so that dld.sl refuses to + # link against other versions. + version_type=sunos + need_lib_prefix=no + need_version=no + case $host_cpu in + ia64*) + shrext_cmds='.so' + hardcode_into_libs=yes + dynamic_linker="$host_os dld.so" + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes # Unless +noenvvar is specified. + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + if test 32 = "$HPUX_IA64_MODE"; then + sys_lib_search_path_spec="/usr/lib/hpux32 /usr/local/lib/hpux32 /usr/local/lib" + sys_lib_dlsearch_path_spec=/usr/lib/hpux32 + else + sys_lib_search_path_spec="/usr/lib/hpux64 /usr/local/lib/hpux64" + sys_lib_dlsearch_path_spec=/usr/lib/hpux64 + fi + ;; + hppa*64*) + shrext_cmds='.sl' + hardcode_into_libs=yes + dynamic_linker="$host_os dld.sl" + shlibpath_var=LD_LIBRARY_PATH # How should we handle SHLIB_PATH + shlibpath_overrides_runpath=yes # Unless +noenvvar is specified. + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + sys_lib_search_path_spec="/usr/lib/pa20_64 /usr/ccs/lib/pa20_64" + sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec + ;; + *) + shrext_cmds='.sl' + dynamic_linker="$host_os dld.sl" + shlibpath_var=SHLIB_PATH + shlibpath_overrides_runpath=no # +s is required to enable SHLIB_PATH + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + ;; + esac + # HP-UX runs *really* slowly unless shared libraries are mode 555, ... + postinstall_cmds='chmod 555 $lib' + # or fails outright, so override atomically: + install_override_mode=555 + ;; + +interix[3-9]*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + dynamic_linker='Interix 3.x ld.so.1 (PE, like ELF)' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=no + hardcode_into_libs=yes + ;; + +irix5* | irix6* | nonstopux*) + case $host_os in + nonstopux*) version_type=nonstopux ;; + *) + if test yes = "$lt_cv_prog_gnu_ld"; then + version_type=linux # correct to gnu/linux during the next big refactor + else + version_type=irix + fi ;; + esac + need_lib_prefix=no + need_version=no + soname_spec='$libname$release$shared_ext$major' + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$release$shared_ext $libname$shared_ext' + case $host_os in + irix5* | nonstopux*) + libsuff= shlibsuff= + ;; + *) + case $LD in # libtool.m4 will add one of these switches to LD + *-32|*"-32 "|*-melf32bsmip|*"-melf32bsmip ") + libsuff= shlibsuff= libmagic=32-bit;; + *-n32|*"-n32 "|*-melf32bmipn32|*"-melf32bmipn32 ") + libsuff=32 shlibsuff=N32 libmagic=N32;; + *-64|*"-64 "|*-melf64bmip|*"-melf64bmip ") + libsuff=64 shlibsuff=64 libmagic=64-bit;; + *) libsuff= shlibsuff= libmagic=never-match;; + esac + ;; + esac + shlibpath_var=LD_LIBRARY${shlibsuff}_PATH + shlibpath_overrides_runpath=no + sys_lib_search_path_spec="/usr/lib$libsuff /lib$libsuff /usr/local/lib$libsuff" + sys_lib_dlsearch_path_spec="/usr/lib$libsuff /lib$libsuff" + hardcode_into_libs=yes + ;; + +# No shared lib support for Linux oldld, aout, or coff. +linux*oldld* | linux*aout* | linux*coff*) + dynamic_linker=no + ;; + +linux*android*) + version_type=none # Android doesn't support versioned libraries. + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext' + soname_spec='$libname$release$shared_ext' + finish_cmds= + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + + # This implies no fast_install, which is unacceptable. + # Some rework will be needed to allow for fast_install + # before this can be enabled. + hardcode_into_libs=yes + + dynamic_linker='Android linker' + # Don't embed -rpath directories since the linker doesn't support them. + hardcode_libdir_flag_spec='-L$libdir' + ;; + +# This must be glibc/ELF. +linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + finish_cmds='PATH="\$PATH:/sbin" ldconfig -n $libdir' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=no + + # Some binutils ld are patched to set DT_RUNPATH + if ${lt_cv_shlibpath_overrides_runpath+:} false; then : + $as_echo_n "(cached) " >&6 +else + lt_cv_shlibpath_overrides_runpath=no + save_LDFLAGS=$LDFLAGS + save_libdir=$libdir + eval "libdir=/foo; wl=\"$lt_prog_compiler_wl\"; \ + LDFLAGS=\"\$LDFLAGS $hardcode_libdir_flag_spec\"" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + if ($OBJDUMP -p conftest$ac_exeext) 2>/dev/null | grep "RUNPATH.*$libdir" >/dev/null; then : + lt_cv_shlibpath_overrides_runpath=yes +fi +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext + LDFLAGS=$save_LDFLAGS + libdir=$save_libdir + +fi + + shlibpath_overrides_runpath=$lt_cv_shlibpath_overrides_runpath + + # This implies no fast_install, which is unacceptable. + # Some rework will be needed to allow for fast_install + # before this can be enabled. + hardcode_into_libs=yes + + # Ideally, we could use ldconfig to report *all* directores which are + # searched for libraries, however this is still not possible. Aside from not + # being certain /sbin/ldconfig is available, command + # 'ldconfig -N -X -v | grep ^/' on 64bit Fedora does not report /usr/lib64, + # even though it is searched at run-time. Try to do the best guess by + # appending ld.so.conf contents (and includes) to the search path. + if test -f /etc/ld.so.conf; then + lt_ld_extra=`awk '/^include / { system(sprintf("cd /etc; cat %s 2>/dev/null", \$2)); skip = 1; } { if (!skip) print \$0; skip = 0; }' < /etc/ld.so.conf | $SED -e 's/#.*//;/^[ ]*hwcap[ ]/d;s/[:, ]/ /g;s/=[^=]*$//;s/=[^= ]* / /g;s/"//g;/^$/d' | tr '\n' ' '` + sys_lib_dlsearch_path_spec="/lib /usr/lib $lt_ld_extra" + fi + + # We used to test for /lib/ld.so.1 and disable shared libraries on + # powerpc, because MkLinux only supported shared libraries with the + # GNU dynamic linker. Since this was broken with cross compilers, + # most powerpc-linux boxes support dynamic linking these days and + # people can always --disable-shared, the test was removed, and we + # assume the GNU/Linux dynamic linker is in use. + dynamic_linker='GNU/Linux ld.so' + ;; + +netbsdelf*-gnu) + version_type=linux + need_lib_prefix=no + need_version=no + library_names_spec='${libname}${release}${shared_ext}$versuffix ${libname}${release}${shared_ext}$major ${libname}${shared_ext}' + soname_spec='${libname}${release}${shared_ext}$major' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=no + hardcode_into_libs=yes + dynamic_linker='NetBSD ld.elf_so' + ;; + +netbsd*) + version_type=sunos + need_lib_prefix=no + need_version=no + if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then + library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix' + finish_cmds='PATH="\$PATH:/sbin" ldconfig -m $libdir' + dynamic_linker='NetBSD (a.out) ld.so' + else + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + dynamic_linker='NetBSD ld.elf_so' + fi + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + hardcode_into_libs=yes + ;; + +newsos6) + version_type=linux # correct to gnu/linux during the next big refactor + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + ;; + +*nto* | *qnx*) + version_type=qnx + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=no + hardcode_into_libs=yes + dynamic_linker='ldqnx.so' + ;; + +openbsd* | bitrig*) + version_type=sunos + sys_lib_dlsearch_path_spec=/usr/lib + need_lib_prefix=no + if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then + need_version=no + else + need_version=yes + fi + library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix' + finish_cmds='PATH="\$PATH:/sbin" ldconfig -m $libdir' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + ;; + +os2*) + libname_spec='$name' + version_type=windows + shrext_cmds=.dll + need_version=no + need_lib_prefix=no + # OS/2 can only load a DLL with a base name of 8 characters or less. + soname_spec='`test -n "$os2dllname" && libname="$os2dllname"; + v=$($ECHO $release$versuffix | tr -d .-); + n=$($ECHO $libname | cut -b -$((8 - ${#v})) | tr . _); + $ECHO $n$v`$shared_ext' + library_names_spec='${libname}_dll.$libext' + dynamic_linker='OS/2 ld.exe' + shlibpath_var=BEGINLIBPATH + sys_lib_search_path_spec="/lib /usr/lib /usr/local/lib" + sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec + postinstall_cmds='base_file=`basename \$file`~ + dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; $ECHO \$dlname'\''`~ + dldir=$destdir/`dirname \$dlpath`~ + test -d \$dldir || mkdir -p \$dldir~ + $install_prog $dir/$dlname \$dldir/$dlname~ + chmod a+x \$dldir/$dlname~ + if test -n '\''$stripme'\'' && test -n '\''$striplib'\''; then + eval '\''$striplib \$dldir/$dlname'\'' || exit \$?; + fi' + postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; $ECHO \$dlname'\''`~ + dlpath=$dir/\$dldll~ + $RM \$dlpath' + ;; + +osf3* | osf4* | osf5*) + version_type=osf + need_lib_prefix=no + need_version=no + soname_spec='$libname$release$shared_ext$major' + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + shlibpath_var=LD_LIBRARY_PATH + sys_lib_search_path_spec="/usr/shlib /usr/ccs/lib /usr/lib/cmplrs/cc /usr/lib /usr/local/lib /var/shlib" + sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec + ;; + +rdos*) + dynamic_linker=no + ;; + +solaris*) + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + hardcode_into_libs=yes + # ldd complains unless libraries are executable + postinstall_cmds='chmod +x $lib' + ;; + +sunos4*) + version_type=sunos + library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix' + finish_cmds='PATH="\$PATH:/usr/etc" ldconfig $libdir' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + if test yes = "$with_gnu_ld"; then + need_lib_prefix=no + fi + need_version=yes + ;; + +sysv4 | sysv4.3*) + version_type=linux # correct to gnu/linux during the next big refactor + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + case $host_vendor in + sni) + shlibpath_overrides_runpath=no + need_lib_prefix=no + runpath_var=LD_RUN_PATH + ;; + siemens) + need_lib_prefix=no + ;; + motorola) + need_lib_prefix=no + need_version=no + shlibpath_overrides_runpath=no + sys_lib_search_path_spec='/lib /usr/lib /usr/ccs/lib' + ;; + esac + ;; + +sysv4*MP*) + if test -d /usr/nec; then + version_type=linux # correct to gnu/linux during the next big refactor + library_names_spec='$libname$shared_ext.$versuffix $libname$shared_ext.$major $libname$shared_ext' + soname_spec='$libname$shared_ext.$major' + shlibpath_var=LD_LIBRARY_PATH + fi + ;; + +sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX* | sysv4*uw2*) + version_type=sco + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=yes + hardcode_into_libs=yes + if test yes = "$with_gnu_ld"; then + sys_lib_search_path_spec='/usr/local/lib /usr/gnu/lib /usr/ccs/lib /usr/lib /lib' + else + sys_lib_search_path_spec='/usr/ccs/lib /usr/lib' + case $host_os in + sco3.2v5*) + sys_lib_search_path_spec="$sys_lib_search_path_spec /lib" + ;; + esac + fi + sys_lib_dlsearch_path_spec='/usr/lib' + ;; + +tpf*) + # TPF is a cross-target only. Preferred cross-host = GNU/Linux. + version_type=linux # correct to gnu/linux during the next big refactor + need_lib_prefix=no + need_version=no + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + shlibpath_var=LD_LIBRARY_PATH + shlibpath_overrides_runpath=no + hardcode_into_libs=yes + ;; + +uts4*) + version_type=linux # correct to gnu/linux during the next big refactor + library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext' + soname_spec='$libname$release$shared_ext$major' + shlibpath_var=LD_LIBRARY_PATH + ;; + +*) + dynamic_linker=no + ;; +esac +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $dynamic_linker" >&5 +$as_echo "$dynamic_linker" >&6; } +test no = "$dynamic_linker" && can_build_shared=no + +variables_saved_for_relink="PATH $shlibpath_var $runpath_var" +if test yes = "$GCC"; then + variables_saved_for_relink="$variables_saved_for_relink GCC_EXEC_PREFIX COMPILER_PATH LIBRARY_PATH" +fi + +if test set = "${lt_cv_sys_lib_search_path_spec+set}"; then + sys_lib_search_path_spec=$lt_cv_sys_lib_search_path_spec +fi + +if test set = "${lt_cv_sys_lib_dlsearch_path_spec+set}"; then + sys_lib_dlsearch_path_spec=$lt_cv_sys_lib_dlsearch_path_spec +fi + +# remember unaugmented sys_lib_dlsearch_path content for libtool script decls... +configure_time_dlsearch_path=$sys_lib_dlsearch_path_spec + +# ... but it needs LT_SYS_LIBRARY_PATH munging for other configure-time code +func_munge_path_list sys_lib_dlsearch_path_spec "$LT_SYS_LIBRARY_PATH" + +# to be used as default LT_SYS_LIBRARY_PATH value in generated libtool +configure_time_lt_sys_library_path=$LT_SYS_LIBRARY_PATH + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking how to hardcode library paths into programs" >&5 +$as_echo_n "checking how to hardcode library paths into programs... " >&6; } +hardcode_action= +if test -n "$hardcode_libdir_flag_spec" || + test -n "$runpath_var" || + test yes = "$hardcode_automatic"; then + + # We can hardcode non-existent directories. + if test no != "$hardcode_direct" && + # If the only mechanism to avoid hardcoding is shlibpath_var, we + # have to relink, otherwise we might link with an installed library + # when we should be linking with a yet-to-be-installed one + ## test no != "$_LT_TAGVAR(hardcode_shlibpath_var, )" && + test no != "$hardcode_minus_L"; then + # Linking always hardcodes the temporary library directory. + hardcode_action=relink + else + # We can link without hardcoding, and we can hardcode nonexisting dirs. + hardcode_action=immediate + fi +else + # We cannot hardcode anything, or else we can only hardcode existing + # directories. + hardcode_action=unsupported +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $hardcode_action" >&5 +$as_echo "$hardcode_action" >&6; } + +if test relink = "$hardcode_action" || + test yes = "$inherit_rpath"; then + # Fast installation is not supported + enable_fast_install=no +elif test yes = "$shlibpath_overrides_runpath" || + test no = "$enable_shared"; then + # Fast installation is not necessary + enable_fast_install=needless +fi + + + + + + + if test yes != "$enable_dlopen"; then + enable_dlopen=unknown + enable_dlopen_self=unknown + enable_dlopen_self_static=unknown +else + lt_cv_dlopen=no + lt_cv_dlopen_libs= + + case $host_os in + beos*) + lt_cv_dlopen=load_add_on + lt_cv_dlopen_libs= + lt_cv_dlopen_self=yes + ;; + + mingw* | pw32* | cegcc*) + lt_cv_dlopen=LoadLibrary + lt_cv_dlopen_libs= + ;; + + cygwin*) + lt_cv_dlopen=dlopen + lt_cv_dlopen_libs= + ;; + + darwin*) + # if libdl is installed we need to link against it + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for dlopen in -ldl" >&5 +$as_echo_n "checking for dlopen in -ldl... " >&6; } +if ${ac_cv_lib_dl_dlopen+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-ldl $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char dlopen (); +int +main () +{ +return dlopen (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_dl_dlopen=yes +else + ac_cv_lib_dl_dlopen=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_dl_dlopen" >&5 +$as_echo "$ac_cv_lib_dl_dlopen" >&6; } +if test "x$ac_cv_lib_dl_dlopen" = xyes; then : + lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-ldl +else + + lt_cv_dlopen=dyld + lt_cv_dlopen_libs= + lt_cv_dlopen_self=yes + +fi + + ;; + + tpf*) + # Don't try to run any link tests for TPF. We know it's impossible + # because TPF is a cross-compiler, and we know how we open DSOs. + lt_cv_dlopen=dlopen + lt_cv_dlopen_libs= + lt_cv_dlopen_self=no + ;; + + *) + ac_fn_c_check_func "$LINENO" "shl_load" "ac_cv_func_shl_load" +if test "x$ac_cv_func_shl_load" = xyes; then : + lt_cv_dlopen=shl_load +else + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for shl_load in -ldld" >&5 +$as_echo_n "checking for shl_load in -ldld... " >&6; } +if ${ac_cv_lib_dld_shl_load+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-ldld $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char shl_load (); +int +main () +{ +return shl_load (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_dld_shl_load=yes +else + ac_cv_lib_dld_shl_load=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_dld_shl_load" >&5 +$as_echo "$ac_cv_lib_dld_shl_load" >&6; } +if test "x$ac_cv_lib_dld_shl_load" = xyes; then : + lt_cv_dlopen=shl_load lt_cv_dlopen_libs=-ldld +else + ac_fn_c_check_func "$LINENO" "dlopen" "ac_cv_func_dlopen" +if test "x$ac_cv_func_dlopen" = xyes; then : + lt_cv_dlopen=dlopen +else + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for dlopen in -ldl" >&5 +$as_echo_n "checking for dlopen in -ldl... " >&6; } +if ${ac_cv_lib_dl_dlopen+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-ldl $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char dlopen (); +int +main () +{ +return dlopen (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_dl_dlopen=yes +else + ac_cv_lib_dl_dlopen=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_dl_dlopen" >&5 +$as_echo "$ac_cv_lib_dl_dlopen" >&6; } +if test "x$ac_cv_lib_dl_dlopen" = xyes; then : + lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-ldl +else + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for dlopen in -lsvld" >&5 +$as_echo_n "checking for dlopen in -lsvld... " >&6; } +if ${ac_cv_lib_svld_dlopen+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lsvld $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char dlopen (); +int +main () +{ +return dlopen (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_svld_dlopen=yes +else + ac_cv_lib_svld_dlopen=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_svld_dlopen" >&5 +$as_echo "$ac_cv_lib_svld_dlopen" >&6; } +if test "x$ac_cv_lib_svld_dlopen" = xyes; then : + lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-lsvld +else + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for dld_link in -ldld" >&5 +$as_echo_n "checking for dld_link in -ldld... " >&6; } +if ${ac_cv_lib_dld_dld_link+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-ldld $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char dld_link (); +int +main () +{ +return dld_link (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_dld_dld_link=yes +else + ac_cv_lib_dld_dld_link=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_dld_dld_link" >&5 +$as_echo "$ac_cv_lib_dld_dld_link" >&6; } +if test "x$ac_cv_lib_dld_dld_link" = xyes; then : + lt_cv_dlopen=dld_link lt_cv_dlopen_libs=-ldld +fi + + +fi + + +fi + + +fi + + +fi + + +fi + + ;; + esac + + if test no = "$lt_cv_dlopen"; then + enable_dlopen=no + else + enable_dlopen=yes + fi + + case $lt_cv_dlopen in + dlopen) + save_CPPFLAGS=$CPPFLAGS + test yes = "$ac_cv_header_dlfcn_h" && CPPFLAGS="$CPPFLAGS -DHAVE_DLFCN_H" + + save_LDFLAGS=$LDFLAGS + wl=$lt_prog_compiler_wl eval LDFLAGS=\"\$LDFLAGS $export_dynamic_flag_spec\" + + save_LIBS=$LIBS + LIBS="$lt_cv_dlopen_libs $LIBS" + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether a program can dlopen itself" >&5 +$as_echo_n "checking whether a program can dlopen itself... " >&6; } +if ${lt_cv_dlopen_self+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test yes = "$cross_compiling"; then : + lt_cv_dlopen_self=cross +else + lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2 + lt_status=$lt_dlunknown + cat > conftest.$ac_ext <<_LT_EOF +#line $LINENO "configure" +#include "confdefs.h" + +#if HAVE_DLFCN_H +#include +#endif + +#include + +#ifdef RTLD_GLOBAL +# define LT_DLGLOBAL RTLD_GLOBAL +#else +# ifdef DL_GLOBAL +# define LT_DLGLOBAL DL_GLOBAL +# else +# define LT_DLGLOBAL 0 +# endif +#endif + +/* We may have to define LT_DLLAZY_OR_NOW in the command line if we + find out it does not work in some platform. */ +#ifndef LT_DLLAZY_OR_NOW +# ifdef RTLD_LAZY +# define LT_DLLAZY_OR_NOW RTLD_LAZY +# else +# ifdef DL_LAZY +# define LT_DLLAZY_OR_NOW DL_LAZY +# else +# ifdef RTLD_NOW +# define LT_DLLAZY_OR_NOW RTLD_NOW +# else +# ifdef DL_NOW +# define LT_DLLAZY_OR_NOW DL_NOW +# else +# define LT_DLLAZY_OR_NOW 0 +# endif +# endif +# endif +# endif +#endif + +/* When -fvisibility=hidden is used, assume the code has been annotated + correspondingly for the symbols needed. */ +#if defined __GNUC__ && (((__GNUC__ == 3) && (__GNUC_MINOR__ >= 3)) || (__GNUC__ > 3)) +int fnord () __attribute__((visibility("default"))); +#endif + +int fnord () { return 42; } +int main () +{ + void *self = dlopen (0, LT_DLGLOBAL|LT_DLLAZY_OR_NOW); + int status = $lt_dlunknown; + + if (self) + { + if (dlsym (self,"fnord")) status = $lt_dlno_uscore; + else + { + if (dlsym( self,"_fnord")) status = $lt_dlneed_uscore; + else puts (dlerror ()); + } + /* dlclose (self); */ + } + else + puts (dlerror ()); + + return status; +} +_LT_EOF + if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_link\""; } >&5 + (eval $ac_link) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } && test -s "conftest$ac_exeext" 2>/dev/null; then + (./conftest; exit; ) >&5 2>/dev/null + lt_status=$? + case x$lt_status in + x$lt_dlno_uscore) lt_cv_dlopen_self=yes ;; + x$lt_dlneed_uscore) lt_cv_dlopen_self=yes ;; + x$lt_dlunknown|x*) lt_cv_dlopen_self=no ;; + esac + else : + # compilation failed + lt_cv_dlopen_self=no + fi +fi +rm -fr conftest* + + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_dlopen_self" >&5 +$as_echo "$lt_cv_dlopen_self" >&6; } + + if test yes = "$lt_cv_dlopen_self"; then + wl=$lt_prog_compiler_wl eval LDFLAGS=\"\$LDFLAGS $lt_prog_compiler_static\" + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether a statically linked program can dlopen itself" >&5 +$as_echo_n "checking whether a statically linked program can dlopen itself... " >&6; } +if ${lt_cv_dlopen_self_static+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test yes = "$cross_compiling"; then : + lt_cv_dlopen_self_static=cross +else + lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2 + lt_status=$lt_dlunknown + cat > conftest.$ac_ext <<_LT_EOF +#line $LINENO "configure" +#include "confdefs.h" + +#if HAVE_DLFCN_H +#include +#endif + +#include + +#ifdef RTLD_GLOBAL +# define LT_DLGLOBAL RTLD_GLOBAL +#else +# ifdef DL_GLOBAL +# define LT_DLGLOBAL DL_GLOBAL +# else +# define LT_DLGLOBAL 0 +# endif +#endif + +/* We may have to define LT_DLLAZY_OR_NOW in the command line if we + find out it does not work in some platform. */ +#ifndef LT_DLLAZY_OR_NOW +# ifdef RTLD_LAZY +# define LT_DLLAZY_OR_NOW RTLD_LAZY +# else +# ifdef DL_LAZY +# define LT_DLLAZY_OR_NOW DL_LAZY +# else +# ifdef RTLD_NOW +# define LT_DLLAZY_OR_NOW RTLD_NOW +# else +# ifdef DL_NOW +# define LT_DLLAZY_OR_NOW DL_NOW +# else +# define LT_DLLAZY_OR_NOW 0 +# endif +# endif +# endif +# endif +#endif + +/* When -fvisibility=hidden is used, assume the code has been annotated + correspondingly for the symbols needed. */ +#if defined __GNUC__ && (((__GNUC__ == 3) && (__GNUC_MINOR__ >= 3)) || (__GNUC__ > 3)) +int fnord () __attribute__((visibility("default"))); +#endif + +int fnord () { return 42; } +int main () +{ + void *self = dlopen (0, LT_DLGLOBAL|LT_DLLAZY_OR_NOW); + int status = $lt_dlunknown; + + if (self) + { + if (dlsym (self,"fnord")) status = $lt_dlno_uscore; + else + { + if (dlsym( self,"_fnord")) status = $lt_dlneed_uscore; + else puts (dlerror ()); + } + /* dlclose (self); */ + } + else + puts (dlerror ()); + + return status; +} +_LT_EOF + if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_link\""; } >&5 + (eval $ac_link) 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } && test -s "conftest$ac_exeext" 2>/dev/null; then + (./conftest; exit; ) >&5 2>/dev/null + lt_status=$? + case x$lt_status in + x$lt_dlno_uscore) lt_cv_dlopen_self_static=yes ;; + x$lt_dlneed_uscore) lt_cv_dlopen_self_static=yes ;; + x$lt_dlunknown|x*) lt_cv_dlopen_self_static=no ;; + esac + else : + # compilation failed + lt_cv_dlopen_self_static=no + fi +fi +rm -fr conftest* + + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $lt_cv_dlopen_self_static" >&5 +$as_echo "$lt_cv_dlopen_self_static" >&6; } + fi + + CPPFLAGS=$save_CPPFLAGS + LDFLAGS=$save_LDFLAGS + LIBS=$save_LIBS + ;; + esac + + case $lt_cv_dlopen_self in + yes|no) enable_dlopen_self=$lt_cv_dlopen_self ;; + *) enable_dlopen_self=unknown ;; + esac + + case $lt_cv_dlopen_self_static in + yes|no) enable_dlopen_self_static=$lt_cv_dlopen_self_static ;; + *) enable_dlopen_self_static=unknown ;; + esac +fi + + + + + + + + + + + + + + + + + +striplib= +old_striplib= +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether stripping libraries is possible" >&5 +$as_echo_n "checking whether stripping libraries is possible... " >&6; } +if test -n "$STRIP" && $STRIP -V 2>&1 | $GREP "GNU strip" >/dev/null; then + test -z "$old_striplib" && old_striplib="$STRIP --strip-debug" + test -z "$striplib" && striplib="$STRIP --strip-unneeded" + { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +$as_echo "yes" >&6; } +else +# FIXME - insert some real tests, host_os isn't really good enough + case $host_os in + darwin*) + if test -n "$STRIP"; then + striplib="$STRIP -x" + old_striplib="$STRIP -S" + { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +$as_echo "yes" >&6; } + else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } + fi + ;; + *) + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } + ;; + esac +fi + + + + + + + + + + + + + # Report what library types will actually be built + { $as_echo "$as_me:${as_lineno-$LINENO}: checking if libtool supports shared libraries" >&5 +$as_echo_n "checking if libtool supports shared libraries... " >&6; } + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $can_build_shared" >&5 +$as_echo "$can_build_shared" >&6; } + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build shared libraries" >&5 +$as_echo_n "checking whether to build shared libraries... " >&6; } + test no = "$can_build_shared" && enable_shared=no + + # On AIX, shared libraries and static libraries use the same namespace, and + # are all built from PIC. + case $host_os in + aix3*) + test yes = "$enable_shared" && enable_static=no + if test -n "$RANLIB"; then + archive_cmds="$archive_cmds~\$RANLIB \$lib" + postinstall_cmds='$RANLIB $lib' + fi + ;; + + aix[4-9]*) + if test ia64 != "$host_cpu"; then + case $enable_shared,$with_aix_soname,$aix_use_runtimelinking in + yes,aix,yes) ;; # shared object as lib.so file only + yes,svr4,*) ;; # shared object as lib.so archive member only + yes,*) enable_static=no ;; # shared object in lib.a archive as well + esac + fi + ;; + esac + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $enable_shared" >&5 +$as_echo "$enable_shared" >&6; } + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build static libraries" >&5 +$as_echo_n "checking whether to build static libraries... " >&6; } + # Make sure either enable_shared or enable_static is yes. + test yes = "$enable_shared" || enable_static=yes + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $enable_static" >&5 +$as_echo "$enable_static" >&6; } + + + + +fi +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +CC=$lt_save_CC + + + + + + + + + + + + + + + + ac_config_commands="$ac_config_commands libtool" + + + + +# Only expand once: + + + + +for ac_header in sys/inotify.h sys/epoll.h sys/event.h port.h poll.h sys/timerfd.h +do : + as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` +ac_fn_c_check_header_mongrel "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" +if eval test \"x\$"$as_ac_Header"\" = x"yes"; then : + cat >>confdefs.h <<_ACEOF +#define `$as_echo "HAVE_$ac_header" | $as_tr_cpp` 1 +_ACEOF + +fi + +done + +for ac_header in sys/select.h sys/eventfd.h sys/signalfd.h linux/aio_abi.h linux/fs.h +do : + as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` +ac_fn_c_check_header_mongrel "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" +if eval test \"x\$"$as_ac_Header"\" = x"yes"; then : + cat >>confdefs.h <<_ACEOF +#define `$as_echo "HAVE_$ac_header" | $as_tr_cpp` 1 +_ACEOF + +fi + +done + + +for ac_func in inotify_init epoll_ctl kqueue port_create poll select eventfd signalfd +do : + as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh` +ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var" +if eval test \"x\$"$as_ac_var"\" = x"yes"; then : + cat >>confdefs.h <<_ACEOF +#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1 +_ACEOF + +fi +done + + +for ac_func in clock_gettime +do : + ac_fn_c_check_func "$LINENO" "clock_gettime" "ac_cv_func_clock_gettime" +if test "x$ac_cv_func_clock_gettime" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_CLOCK_GETTIME 1 +_ACEOF + +else + + if test $(uname) = Linux; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for clock_gettime syscall" >&5 +$as_echo_n "checking for clock_gettime syscall... " >&6; } + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include + #include + #include +int +main () +{ +struct timespec ts; int status = syscall (SYS_clock_gettime, CLOCK_REALTIME, &ts) + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_have_clock_syscall=1 + +$as_echo "#define HAVE_CLOCK_SYSCALL 1" >>confdefs.h + + { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +$as_echo "yes" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext + fi + if test -z "$LIBEV_M4_AVOID_LIBRT" && test -z "$ac_have_clock_syscall"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for clock_gettime in -lrt" >&5 +$as_echo_n "checking for clock_gettime in -lrt... " >&6; } +if ${ac_cv_lib_rt_clock_gettime+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lrt $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char clock_gettime (); +int +main () +{ +return clock_gettime (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_rt_clock_gettime=yes +else + ac_cv_lib_rt_clock_gettime=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_rt_clock_gettime" >&5 +$as_echo "$ac_cv_lib_rt_clock_gettime" >&6; } +if test "x$ac_cv_lib_rt_clock_gettime" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBRT 1 +_ACEOF + + LIBS="-lrt $LIBS" + +fi + + unset ac_cv_func_clock_gettime + for ac_func in clock_gettime +do : + ac_fn_c_check_func "$LINENO" "clock_gettime" "ac_cv_func_clock_gettime" +if test "x$ac_cv_func_clock_gettime" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_CLOCK_GETTIME 1 +_ACEOF + +fi +done + + fi + +fi +done + + +for ac_func in nanosleep +do : + ac_fn_c_check_func "$LINENO" "nanosleep" "ac_cv_func_nanosleep" +if test "x$ac_cv_func_nanosleep" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_NANOSLEEP 1 +_ACEOF + +else + + if test -z "$LIBEV_M4_AVOID_LIBRT"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for nanosleep in -lrt" >&5 +$as_echo_n "checking for nanosleep in -lrt... " >&6; } +if ${ac_cv_lib_rt_nanosleep+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lrt $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char nanosleep (); +int +main () +{ +return nanosleep (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_rt_nanosleep=yes +else + ac_cv_lib_rt_nanosleep=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_rt_nanosleep" >&5 +$as_echo "$ac_cv_lib_rt_nanosleep" >&6; } +if test "x$ac_cv_lib_rt_nanosleep" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBRT 1 +_ACEOF + + LIBS="-lrt $LIBS" + +fi + + unset ac_cv_func_nanosleep + for ac_func in nanosleep +do : + ac_fn_c_check_func "$LINENO" "nanosleep" "ac_cv_func_nanosleep" +if test "x$ac_cv_func_nanosleep" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_NANOSLEEP 1 +_ACEOF + +fi +done + + fi + +fi +done + + +ac_fn_c_check_type "$LINENO" "__kernel_rwf_t" "ac_cv_type___kernel_rwf_t" "#include +" +if test "x$ac_cv_type___kernel_rwf_t" = xyes; then : + + +$as_echo "#define HAVE_KERNEL_RWF_T 1" >>confdefs.h + + +fi + + +if test -z "$LIBEV_M4_AVOID_LIBM"; then + LIBM=m +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for library containing floor" >&5 +$as_echo_n "checking for library containing floor... " >&6; } +if ${ac_cv_search_floor+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_func_search_save_LIBS=$LIBS +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char floor (); +int +main () +{ +return floor (); + ; + return 0; +} +_ACEOF +for ac_lib in '' $LIBM; do + if test -z "$ac_lib"; then + ac_res="none required" + else + ac_res=-l$ac_lib + LIBS="-l$ac_lib $ac_func_search_save_LIBS" + fi + if ac_fn_c_try_link "$LINENO"; then : + ac_cv_search_floor=$ac_res +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext + if ${ac_cv_search_floor+:} false; then : + break +fi +done +if ${ac_cv_search_floor+:} false; then : + +else + ac_cv_search_floor=no +fi +rm conftest.$ac_ext +LIBS=$ac_func_search_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_floor" >&5 +$as_echo "$ac_cv_search_floor" >&6; } +ac_res=$ac_cv_search_floor +if test "$ac_res" != no; then : + test "$ac_res" = "none required" || LIBS="$ac_res $LIBS" + +$as_echo "#define HAVE_FLOOR 1" >>confdefs.h + +fi + + + + +ac_config_files="$ac_config_files Makefile" + +cat >confcache <<\_ACEOF +# This file is a shell script that caches the results of configure +# tests run on this system so they can be shared between configure +# scripts and configure runs, see configure's option --config-cache. +# It is not useful on other systems. If it contains results you don't +# want to keep, you may remove or edit it. +# +# config.status only pays attention to the cache file if you give it +# the --recheck option to rerun configure. +# +# `ac_cv_env_foo' variables (set or unset) will be overridden when +# loading this file, other *unset* `ac_cv_foo' will be assigned the +# following values. + +_ACEOF + +# The following way of writing the cache mishandles newlines in values, +# but we know of no workaround that is simple, portable, and efficient. +# So, we kill variables containing newlines. +# Ultrix sh set writes to stderr and can't be redirected directly, +# and sets the high bit in the cache file unless we assign to the vars. +( + for ac_var in `(set) 2>&1 | sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'`; do + eval ac_val=\$$ac_var + case $ac_val in #( + *${as_nl}*) + case $ac_var in #( + *_cv_*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 +$as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; + esac + case $ac_var in #( + _ | IFS | as_nl) ;; #( + BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( + *) { eval $ac_var=; unset $ac_var;} ;; + esac ;; + esac + done + + (set) 2>&1 | + case $as_nl`(ac_space=' '; set) 2>&1` in #( + *${as_nl}ac_space=\ *) + # `set' does not quote correctly, so add quotes: double-quote + # substitution turns \\\\ into \\, and sed turns \\ into \. + sed -n \ + "s/'/'\\\\''/g; + s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p" + ;; #( + *) + # `set' quotes correctly as required by POSIX, so do not add quotes. + sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" + ;; + esac | + sort +) | + sed ' + /^ac_cv_env_/b end + t clear + :clear + s/^\([^=]*\)=\(.*[{}].*\)$/test "${\1+set}" = set || &/ + t end + s/^\([^=]*\)=\(.*\)$/\1=${\1=\2}/ + :end' >>confcache +if diff "$cache_file" confcache >/dev/null 2>&1; then :; else + if test -w "$cache_file"; then + if test "x$cache_file" != "x/dev/null"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 +$as_echo "$as_me: updating cache $cache_file" >&6;} + if test ! -f "$cache_file" || test -h "$cache_file"; then + cat confcache >"$cache_file" + else + case $cache_file in #( + */* | ?:*) + mv -f confcache "$cache_file"$$ && + mv -f "$cache_file"$$ "$cache_file" ;; #( + *) + mv -f confcache "$cache_file" ;; + esac + fi + fi + else + { $as_echo "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 +$as_echo "$as_me: not updating unwritable cache $cache_file" >&6;} + fi +fi +rm -f confcache + +test "x$prefix" = xNONE && prefix=$ac_default_prefix +# Let make expand exec_prefix. +test "x$exec_prefix" = xNONE && exec_prefix='${prefix}' + +DEFS=-DHAVE_CONFIG_H + +ac_libobjs= +ac_ltlibobjs= +U= +for ac_i in : $LIBOBJS; do test "x$ac_i" = x: && continue + # 1. Remove the extension, and $U if already installed. + ac_script='s/\$U\././;s/\.o$//;s/\.obj$//' + ac_i=`$as_echo "$ac_i" | sed "$ac_script"` + # 2. Prepend LIBOBJDIR. When used with automake>=1.10 LIBOBJDIR + # will be set to the directory where LIBOBJS objects are built. + as_fn_append ac_libobjs " \${LIBOBJDIR}$ac_i\$U.$ac_objext" + as_fn_append ac_ltlibobjs " \${LIBOBJDIR}$ac_i"'$U.lo' +done +LIBOBJS=$ac_libobjs + +LTLIBOBJS=$ac_ltlibobjs + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking that generated files are newer than configure" >&5 +$as_echo_n "checking that generated files are newer than configure... " >&6; } + if test -n "$am_sleep_pid"; then + # Hide warnings about reused PIDs. + wait $am_sleep_pid 2>/dev/null + fi + { $as_echo "$as_me:${as_lineno-$LINENO}: result: done" >&5 +$as_echo "done" >&6; } + if test -n "$EXEEXT"; then + am__EXEEXT_TRUE= + am__EXEEXT_FALSE='#' +else + am__EXEEXT_TRUE='#' + am__EXEEXT_FALSE= +fi + +if test -z "${MAINTAINER_MODE_TRUE}" && test -z "${MAINTAINER_MODE_FALSE}"; then + as_fn_error $? "conditional \"MAINTAINER_MODE\" was never defined. +Usually this means the macro was only invoked conditionally." "$LINENO" 5 +fi +if test -z "${AMDEP_TRUE}" && test -z "${AMDEP_FALSE}"; then + as_fn_error $? "conditional \"AMDEP\" was never defined. +Usually this means the macro was only invoked conditionally." "$LINENO" 5 +fi +if test -z "${am__fastdepCC_TRUE}" && test -z "${am__fastdepCC_FALSE}"; then + as_fn_error $? "conditional \"am__fastdepCC\" was never defined. +Usually this means the macro was only invoked conditionally." "$LINENO" 5 +fi + +: "${CONFIG_STATUS=./config.status}" +ac_write_fail=0 +ac_clean_files_save=$ac_clean_files +ac_clean_files="$ac_clean_files $CONFIG_STATUS" +{ $as_echo "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 +$as_echo "$as_me: creating $CONFIG_STATUS" >&6;} +as_write_fail=0 +cat >$CONFIG_STATUS <<_ASEOF || as_write_fail=1 +#! $SHELL +# Generated by $as_me. +# Run this file to recreate the current configuration. +# Compiler output produced by configure, useful for debugging +# configure, is in config.log if it exists. + +debug=false +ac_cs_recheck=false +ac_cs_silent=false + +SHELL=\${CONFIG_SHELL-$SHELL} +export SHELL +_ASEOF +cat >>$CONFIG_STATUS <<\_ASEOF || as_write_fail=1 +## -------------------- ## +## M4sh Initialization. ## +## -------------------- ## + +# Be more Bourne compatible +DUALCASE=1; export DUALCASE # for MKS sh +if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then : + emulate sh + NULLCMD=: + # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which + # is contrary to our usage. Disable this feature. + alias -g '${1+"$@"}'='"$@"' + setopt NO_GLOB_SUBST +else + case `(set -o) 2>/dev/null` in #( + *posix*) : + set -o posix ;; #( + *) : + ;; +esac +fi + + +as_nl=' +' +export as_nl +# Printing a long string crashes Solaris 7 /usr/bin/printf. +as_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' +as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo +as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo +# Prefer a ksh shell builtin over an external printf program on Solaris, +# but without wasting forks for bash or zsh. +if test -z "$BASH_VERSION$ZSH_VERSION" \ + && (test "X`print -r -- $as_echo`" = "X$as_echo") 2>/dev/null; then + as_echo='print -r --' + as_echo_n='print -rn --' +elif (test "X`printf %s $as_echo`" = "X$as_echo") 2>/dev/null; then + as_echo='printf %s\n' + as_echo_n='printf %s' +else + if test "X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`" = "X-n $as_echo"; then + as_echo_body='eval /usr/ucb/echo -n "$1$as_nl"' + as_echo_n='/usr/ucb/echo -n' + else + as_echo_body='eval expr "X$1" : "X\\(.*\\)"' + as_echo_n_body='eval + arg=$1; + case $arg in #( + *"$as_nl"*) + expr "X$arg" : "X\\(.*\\)$as_nl"; + arg=`expr "X$arg" : ".*$as_nl\\(.*\\)"`;; + esac; + expr "X$arg" : "X\\(.*\\)" | tr -d "$as_nl" + ' + export as_echo_n_body + as_echo_n='sh -c $as_echo_n_body as_echo' + fi + export as_echo_body + as_echo='sh -c $as_echo_body as_echo' +fi + +# The user is always right. +if test "${PATH_SEPARATOR+set}" != set; then + PATH_SEPARATOR=: + (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { + (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || + PATH_SEPARATOR=';' + } +fi + + +# IFS +# We need space, tab and new line, in precisely that order. Quoting is +# there to prevent editors from complaining about space-tab. +# (If _AS_PATH_WALK were called with IFS unset, it would disable word +# splitting by setting IFS to empty value.) +IFS=" "" $as_nl" + +# Find who we are. Look in the path if we contain no directory separator. +as_myself= +case $0 in #(( + *[\\/]* ) as_myself=$0 ;; + *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + test -r "$as_dir/$0" && as_myself=$as_dir/$0 && break + done +IFS=$as_save_IFS + + ;; +esac +# We did not find ourselves, most probably we were run as `sh COMMAND' +# in which case we are not to be found in the path. +if test "x$as_myself" = x; then + as_myself=$0 +fi +if test ! -f "$as_myself"; then + $as_echo "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 + exit 1 +fi + +# Unset variables that we do not need and which cause bugs (e.g. in +# pre-3.0 UWIN ksh). But do not cause bugs in bash 2.01; the "|| exit 1" +# suppresses any "Segmentation fault" message there. '((' could +# trigger a bug in pdksh 5.2.14. +for as_var in BASH_ENV ENV MAIL MAILPATH +do eval test x\${$as_var+set} = xset \ + && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : +done +PS1='$ ' +PS2='> ' +PS4='+ ' + +# NLS nuisances. +LC_ALL=C +export LC_ALL +LANGUAGE=C +export LANGUAGE + +# CDPATH. +(unset CDPATH) >/dev/null 2>&1 && unset CDPATH + + +# as_fn_error STATUS ERROR [LINENO LOG_FD] +# ---------------------------------------- +# Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are +# provided, also output the error to LOG_FD, referencing LINENO. Then exit the +# script with STATUS, using 1 if that was 0. +as_fn_error () +{ + as_status=$1; test $as_status -eq 0 && as_status=1 + if test "$4"; then + as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 + fi + $as_echo "$as_me: error: $2" >&2 + as_fn_exit $as_status +} # as_fn_error + + +# as_fn_set_status STATUS +# ----------------------- +# Set $? to STATUS, without forking. +as_fn_set_status () +{ + return $1 +} # as_fn_set_status + +# as_fn_exit STATUS +# ----------------- +# Exit the shell with STATUS, even in a "trap 0" or "set -e" context. +as_fn_exit () +{ + set +e + as_fn_set_status $1 + exit $1 +} # as_fn_exit + +# as_fn_unset VAR +# --------------- +# Portably unset VAR. +as_fn_unset () +{ + { eval $1=; unset $1;} +} +as_unset=as_fn_unset +# as_fn_append VAR VALUE +# ---------------------- +# Append the text in VALUE to the end of the definition contained in VAR. Take +# advantage of any shell optimizations that allow amortized linear growth over +# repeated appends, instead of the typical quadratic growth present in naive +# implementations. +if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null; then : + eval 'as_fn_append () + { + eval $1+=\$2 + }' +else + as_fn_append () + { + eval $1=\$$1\$2 + } +fi # as_fn_append + +# as_fn_arith ARG... +# ------------------ +# Perform arithmetic evaluation on the ARGs, and store the result in the +# global $as_val. Take advantage of shells that can avoid forks. The arguments +# must be portable across $(()) and expr. +if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null; then : + eval 'as_fn_arith () + { + as_val=$(( $* )) + }' +else + as_fn_arith () + { + as_val=`expr "$@" || test $? -eq 1` + } +fi # as_fn_arith + + +if expr a : '\(a\)' >/dev/null 2>&1 && + test "X`expr 00001 : '.*\(...\)'`" = X001; then + as_expr=expr +else + as_expr=false +fi + +if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then + as_basename=basename +else + as_basename=false +fi + +if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then + as_dirname=dirname +else + as_dirname=false +fi + +as_me=`$as_basename -- "$0" || +$as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ + X"$0" : 'X\(//\)$' \| \ + X"$0" : 'X\(/\)' \| . 2>/dev/null || +$as_echo X/"$0" | + sed '/^.*\/\([^/][^/]*\)\/*$/{ + s//\1/ + q + } + /^X\/\(\/\/\)$/{ + s//\1/ + q + } + /^X\/\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + +# Avoid depending upon Character Ranges. +as_cr_letters='abcdefghijklmnopqrstuvwxyz' +as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ' +as_cr_Letters=$as_cr_letters$as_cr_LETTERS +as_cr_digits='0123456789' +as_cr_alnum=$as_cr_Letters$as_cr_digits + +ECHO_C= ECHO_N= ECHO_T= +case `echo -n x` in #((((( +-n*) + case `echo 'xy\c'` in + *c*) ECHO_T=' ';; # ECHO_T is single tab character. + xy) ECHO_C='\c';; + *) echo `echo ksh88 bug on AIX 6.1` > /dev/null + ECHO_T=' ';; + esac;; +*) + ECHO_N='-n';; +esac + +rm -f conf$$ conf$$.exe conf$$.file +if test -d conf$$.dir; then + rm -f conf$$.dir/conf$$.file +else + rm -f conf$$.dir + mkdir conf$$.dir 2>/dev/null +fi +if (echo >conf$$.file) 2>/dev/null; then + if ln -s conf$$.file conf$$ 2>/dev/null; then + as_ln_s='ln -s' + # ... but there are two gotchas: + # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. + # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. + # In both cases, we have to default to `cp -pR'. + ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || + as_ln_s='cp -pR' + elif ln conf$$.file conf$$ 2>/dev/null; then + as_ln_s=ln + else + as_ln_s='cp -pR' + fi +else + as_ln_s='cp -pR' +fi +rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file +rmdir conf$$.dir 2>/dev/null + + +# as_fn_mkdir_p +# ------------- +# Create "$as_dir" as a directory, including parents if necessary. +as_fn_mkdir_p () +{ + + case $as_dir in #( + -*) as_dir=./$as_dir;; + esac + test -d "$as_dir" || eval $as_mkdir_p || { + as_dirs= + while :; do + case $as_dir in #( + *\'*) as_qdir=`$as_echo "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( + *) as_qdir=$as_dir;; + esac + as_dirs="'$as_qdir' $as_dirs" + as_dir=`$as_dirname -- "$as_dir" || +$as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ + X"$as_dir" : 'X\(//\)[^/]' \| \ + X"$as_dir" : 'X\(//\)$' \| \ + X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || +$as_echo X"$as_dir" | + sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ + s//\1/ + q + } + /^X\(\/\/\)[^/].*/{ + s//\1/ + q + } + /^X\(\/\/\)$/{ + s//\1/ + q + } + /^X\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + test -d "$as_dir" && break + done + test -z "$as_dirs" || eval "mkdir $as_dirs" + } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" + + +} # as_fn_mkdir_p +if mkdir -p . 2>/dev/null; then + as_mkdir_p='mkdir -p "$as_dir"' +else + test -d ./-p && rmdir ./-p + as_mkdir_p=false +fi + + +# as_fn_executable_p FILE +# ----------------------- +# Test if FILE is an executable regular file. +as_fn_executable_p () +{ + test -f "$1" && test -x "$1" +} # as_fn_executable_p +as_test_x='test -x' +as_executable_p=as_fn_executable_p + +# Sed expression to map a string onto a valid CPP name. +as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" + +# Sed expression to map a string onto a valid variable name. +as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" + + +exec 6>&1 +## ----------------------------------- ## +## Main body of $CONFIG_STATUS script. ## +## ----------------------------------- ## +_ASEOF +test $as_write_fail = 0 && chmod +x $CONFIG_STATUS || ac_write_fail=1 + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +# Save the log message, to keep $0 and so on meaningful, and to +# report actual input values of CONFIG_FILES etc. instead of their +# values after options handling. +ac_log=" +This file was extended by libev $as_me 4.33, which was +generated by GNU Autoconf 2.69. Invocation command line was + + CONFIG_FILES = $CONFIG_FILES + CONFIG_HEADERS = $CONFIG_HEADERS + CONFIG_LINKS = $CONFIG_LINKS + CONFIG_COMMANDS = $CONFIG_COMMANDS + $ $0 $@ + +on `(hostname || uname -n) 2>/dev/null | sed 1q` +" + +_ACEOF + +case $ac_config_files in *" +"*) set x $ac_config_files; shift; ac_config_files=$*;; +esac + +case $ac_config_headers in *" +"*) set x $ac_config_headers; shift; ac_config_headers=$*;; +esac + + +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +# Files that config.status was made for. +config_files="$ac_config_files" +config_headers="$ac_config_headers" +config_commands="$ac_config_commands" + +_ACEOF + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +ac_cs_usage="\ +\`$as_me' instantiates files and other configuration actions +from templates according to the current configuration. Unless the files +and actions are specified as TAGs, all are instantiated by default. + +Usage: $0 [OPTION]... [TAG]... + + -h, --help print this help, then exit + -V, --version print version number and configuration settings, then exit + --config print configuration, then exit + -q, --quiet, --silent + do not print progress messages + -d, --debug don't remove temporary files + --recheck update $as_me by reconfiguring in the same conditions + --file=FILE[:TEMPLATE] + instantiate the configuration file FILE + --header=FILE[:TEMPLATE] + instantiate the configuration header FILE + +Configuration files: +$config_files + +Configuration headers: +$config_headers + +Configuration commands: +$config_commands + +Report bugs to the package provider." + +_ACEOF +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`" +ac_cs_version="\\ +libev config.status 4.33 +configured by $0, generated by GNU Autoconf 2.69, + with options \\"\$ac_cs_config\\" + +Copyright (C) 2012 Free Software Foundation, Inc. +This config.status script is free software; the Free Software Foundation +gives unlimited permission to copy, distribute and modify it." + +ac_pwd='$ac_pwd' +srcdir='$srcdir' +INSTALL='$INSTALL' +MKDIR_P='$MKDIR_P' +AWK='$AWK' +test -n "\$AWK" || AWK=awk +_ACEOF + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +# The default lists apply if the user does not specify any file. +ac_need_defaults=: +while test $# != 0 +do + case $1 in + --*=?*) + ac_option=`expr "X$1" : 'X\([^=]*\)='` + ac_optarg=`expr "X$1" : 'X[^=]*=\(.*\)'` + ac_shift=: + ;; + --*=) + ac_option=`expr "X$1" : 'X\([^=]*\)='` + ac_optarg= + ac_shift=: + ;; + *) + ac_option=$1 + ac_optarg=$2 + ac_shift=shift + ;; + esac + + case $ac_option in + # Handling of the options. + -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r) + ac_cs_recheck=: ;; + --version | --versio | --versi | --vers | --ver | --ve | --v | -V ) + $as_echo "$ac_cs_version"; exit ;; + --config | --confi | --conf | --con | --co | --c ) + $as_echo "$ac_cs_config"; exit ;; + --debug | --debu | --deb | --de | --d | -d ) + debug=: ;; + --file | --fil | --fi | --f ) + $ac_shift + case $ac_optarg in + *\'*) ac_optarg=`$as_echo "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; + '') as_fn_error $? "missing file argument" ;; + esac + as_fn_append CONFIG_FILES " '$ac_optarg'" + ac_need_defaults=false;; + --header | --heade | --head | --hea ) + $ac_shift + case $ac_optarg in + *\'*) ac_optarg=`$as_echo "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; + esac + as_fn_append CONFIG_HEADERS " '$ac_optarg'" + ac_need_defaults=false;; + --he | --h) + # Conflict between --help and --header + as_fn_error $? "ambiguous option: \`$1' +Try \`$0 --help' for more information.";; + --help | --hel | -h ) + $as_echo "$ac_cs_usage"; exit ;; + -q | -quiet | --quiet | --quie | --qui | --qu | --q \ + | -silent | --silent | --silen | --sile | --sil | --si | --s) + ac_cs_silent=: ;; + + # This is an error. + -*) as_fn_error $? "unrecognized option: \`$1' +Try \`$0 --help' for more information." ;; + + *) as_fn_append ac_config_targets " $1" + ac_need_defaults=false ;; + + esac + shift +done + +ac_configure_extra_args= + +if $ac_cs_silent; then + exec 6>/dev/null + ac_configure_extra_args="$ac_configure_extra_args --silent" +fi + +_ACEOF +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +if \$ac_cs_recheck; then + set X $SHELL '$0' $ac_configure_args \$ac_configure_extra_args --no-create --no-recursion + shift + \$as_echo "running CONFIG_SHELL=$SHELL \$*" >&6 + CONFIG_SHELL='$SHELL' + export CONFIG_SHELL + exec "\$@" +fi + +_ACEOF +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +exec 5>>config.log +{ + echo + sed 'h;s/./-/g;s/^.../## /;s/...$/ ##/;p;x;p;x' <<_ASBOX +## Running $as_me. ## +_ASBOX + $as_echo "$ac_log" +} >&5 + +_ACEOF +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +# +# INIT-COMMANDS +# +AMDEP_TRUE="$AMDEP_TRUE" MAKE="${MAKE-make}" + + +# The HP-UX ksh and POSIX shell print the target directory to stdout +# if CDPATH is set. +(unset CDPATH) >/dev/null 2>&1 && unset CDPATH + +sed_quote_subst='$sed_quote_subst' +double_quote_subst='$double_quote_subst' +delay_variable_subst='$delay_variable_subst' +macro_version='`$ECHO "$macro_version" | $SED "$delay_single_quote_subst"`' +macro_revision='`$ECHO "$macro_revision" | $SED "$delay_single_quote_subst"`' +enable_shared='`$ECHO "$enable_shared" | $SED "$delay_single_quote_subst"`' +enable_static='`$ECHO "$enable_static" | $SED "$delay_single_quote_subst"`' +pic_mode='`$ECHO "$pic_mode" | $SED "$delay_single_quote_subst"`' +enable_fast_install='`$ECHO "$enable_fast_install" | $SED "$delay_single_quote_subst"`' +shared_archive_member_spec='`$ECHO "$shared_archive_member_spec" | $SED "$delay_single_quote_subst"`' +SHELL='`$ECHO "$SHELL" | $SED "$delay_single_quote_subst"`' +ECHO='`$ECHO "$ECHO" | $SED "$delay_single_quote_subst"`' +PATH_SEPARATOR='`$ECHO "$PATH_SEPARATOR" | $SED "$delay_single_quote_subst"`' +host_alias='`$ECHO "$host_alias" | $SED "$delay_single_quote_subst"`' +host='`$ECHO "$host" | $SED "$delay_single_quote_subst"`' +host_os='`$ECHO "$host_os" | $SED "$delay_single_quote_subst"`' +build_alias='`$ECHO "$build_alias" | $SED "$delay_single_quote_subst"`' +build='`$ECHO "$build" | $SED "$delay_single_quote_subst"`' +build_os='`$ECHO "$build_os" | $SED "$delay_single_quote_subst"`' +SED='`$ECHO "$SED" | $SED "$delay_single_quote_subst"`' +Xsed='`$ECHO "$Xsed" | $SED "$delay_single_quote_subst"`' +GREP='`$ECHO "$GREP" | $SED "$delay_single_quote_subst"`' +EGREP='`$ECHO "$EGREP" | $SED "$delay_single_quote_subst"`' +FGREP='`$ECHO "$FGREP" | $SED "$delay_single_quote_subst"`' +LD='`$ECHO "$LD" | $SED "$delay_single_quote_subst"`' +NM='`$ECHO "$NM" | $SED "$delay_single_quote_subst"`' +LN_S='`$ECHO "$LN_S" | $SED "$delay_single_quote_subst"`' +max_cmd_len='`$ECHO "$max_cmd_len" | $SED "$delay_single_quote_subst"`' +ac_objext='`$ECHO "$ac_objext" | $SED "$delay_single_quote_subst"`' +exeext='`$ECHO "$exeext" | $SED "$delay_single_quote_subst"`' +lt_unset='`$ECHO "$lt_unset" | $SED "$delay_single_quote_subst"`' +lt_SP2NL='`$ECHO "$lt_SP2NL" | $SED "$delay_single_quote_subst"`' +lt_NL2SP='`$ECHO "$lt_NL2SP" | $SED "$delay_single_quote_subst"`' +lt_cv_to_host_file_cmd='`$ECHO "$lt_cv_to_host_file_cmd" | $SED "$delay_single_quote_subst"`' +lt_cv_to_tool_file_cmd='`$ECHO "$lt_cv_to_tool_file_cmd" | $SED "$delay_single_quote_subst"`' +reload_flag='`$ECHO "$reload_flag" | $SED "$delay_single_quote_subst"`' +reload_cmds='`$ECHO "$reload_cmds" | $SED "$delay_single_quote_subst"`' +OBJDUMP='`$ECHO "$OBJDUMP" | $SED "$delay_single_quote_subst"`' +deplibs_check_method='`$ECHO "$deplibs_check_method" | $SED "$delay_single_quote_subst"`' +file_magic_cmd='`$ECHO "$file_magic_cmd" | $SED "$delay_single_quote_subst"`' +file_magic_glob='`$ECHO "$file_magic_glob" | $SED "$delay_single_quote_subst"`' +want_nocaseglob='`$ECHO "$want_nocaseglob" | $SED "$delay_single_quote_subst"`' +DLLTOOL='`$ECHO "$DLLTOOL" | $SED "$delay_single_quote_subst"`' +sharedlib_from_linklib_cmd='`$ECHO "$sharedlib_from_linklib_cmd" | $SED "$delay_single_quote_subst"`' +AR='`$ECHO "$AR" | $SED "$delay_single_quote_subst"`' +AR_FLAGS='`$ECHO "$AR_FLAGS" | $SED "$delay_single_quote_subst"`' +archiver_list_spec='`$ECHO "$archiver_list_spec" | $SED "$delay_single_quote_subst"`' +STRIP='`$ECHO "$STRIP" | $SED "$delay_single_quote_subst"`' +RANLIB='`$ECHO "$RANLIB" | $SED "$delay_single_quote_subst"`' +old_postinstall_cmds='`$ECHO "$old_postinstall_cmds" | $SED "$delay_single_quote_subst"`' +old_postuninstall_cmds='`$ECHO "$old_postuninstall_cmds" | $SED "$delay_single_quote_subst"`' +old_archive_cmds='`$ECHO "$old_archive_cmds" | $SED "$delay_single_quote_subst"`' +lock_old_archive_extraction='`$ECHO "$lock_old_archive_extraction" | $SED "$delay_single_quote_subst"`' +CC='`$ECHO "$CC" | $SED "$delay_single_quote_subst"`' +CFLAGS='`$ECHO "$CFLAGS" | $SED "$delay_single_quote_subst"`' +compiler='`$ECHO "$compiler" | $SED "$delay_single_quote_subst"`' +GCC='`$ECHO "$GCC" | $SED "$delay_single_quote_subst"`' +lt_cv_sys_global_symbol_pipe='`$ECHO "$lt_cv_sys_global_symbol_pipe" | $SED "$delay_single_quote_subst"`' +lt_cv_sys_global_symbol_to_cdecl='`$ECHO "$lt_cv_sys_global_symbol_to_cdecl" | $SED "$delay_single_quote_subst"`' +lt_cv_sys_global_symbol_to_import='`$ECHO "$lt_cv_sys_global_symbol_to_import" | $SED "$delay_single_quote_subst"`' +lt_cv_sys_global_symbol_to_c_name_address='`$ECHO "$lt_cv_sys_global_symbol_to_c_name_address" | $SED "$delay_single_quote_subst"`' +lt_cv_sys_global_symbol_to_c_name_address_lib_prefix='`$ECHO "$lt_cv_sys_global_symbol_to_c_name_address_lib_prefix" | $SED "$delay_single_quote_subst"`' +lt_cv_nm_interface='`$ECHO "$lt_cv_nm_interface" | $SED "$delay_single_quote_subst"`' +nm_file_list_spec='`$ECHO "$nm_file_list_spec" | $SED "$delay_single_quote_subst"`' +lt_sysroot='`$ECHO "$lt_sysroot" | $SED "$delay_single_quote_subst"`' +lt_cv_truncate_bin='`$ECHO "$lt_cv_truncate_bin" | $SED "$delay_single_quote_subst"`' +objdir='`$ECHO "$objdir" | $SED "$delay_single_quote_subst"`' +MAGIC_CMD='`$ECHO "$MAGIC_CMD" | $SED "$delay_single_quote_subst"`' +lt_prog_compiler_no_builtin_flag='`$ECHO "$lt_prog_compiler_no_builtin_flag" | $SED "$delay_single_quote_subst"`' +lt_prog_compiler_pic='`$ECHO "$lt_prog_compiler_pic" | $SED "$delay_single_quote_subst"`' +lt_prog_compiler_wl='`$ECHO "$lt_prog_compiler_wl" | $SED "$delay_single_quote_subst"`' +lt_prog_compiler_static='`$ECHO "$lt_prog_compiler_static" | $SED "$delay_single_quote_subst"`' +lt_cv_prog_compiler_c_o='`$ECHO "$lt_cv_prog_compiler_c_o" | $SED "$delay_single_quote_subst"`' +need_locks='`$ECHO "$need_locks" | $SED "$delay_single_quote_subst"`' +MANIFEST_TOOL='`$ECHO "$MANIFEST_TOOL" | $SED "$delay_single_quote_subst"`' +DSYMUTIL='`$ECHO "$DSYMUTIL" | $SED "$delay_single_quote_subst"`' +NMEDIT='`$ECHO "$NMEDIT" | $SED "$delay_single_quote_subst"`' +LIPO='`$ECHO "$LIPO" | $SED "$delay_single_quote_subst"`' +OTOOL='`$ECHO "$OTOOL" | $SED "$delay_single_quote_subst"`' +OTOOL64='`$ECHO "$OTOOL64" | $SED "$delay_single_quote_subst"`' +libext='`$ECHO "$libext" | $SED "$delay_single_quote_subst"`' +shrext_cmds='`$ECHO "$shrext_cmds" | $SED "$delay_single_quote_subst"`' +extract_expsyms_cmds='`$ECHO "$extract_expsyms_cmds" | $SED "$delay_single_quote_subst"`' +archive_cmds_need_lc='`$ECHO "$archive_cmds_need_lc" | $SED "$delay_single_quote_subst"`' +enable_shared_with_static_runtimes='`$ECHO "$enable_shared_with_static_runtimes" | $SED "$delay_single_quote_subst"`' +export_dynamic_flag_spec='`$ECHO "$export_dynamic_flag_spec" | $SED "$delay_single_quote_subst"`' +whole_archive_flag_spec='`$ECHO "$whole_archive_flag_spec" | $SED "$delay_single_quote_subst"`' +compiler_needs_object='`$ECHO "$compiler_needs_object" | $SED "$delay_single_quote_subst"`' +old_archive_from_new_cmds='`$ECHO "$old_archive_from_new_cmds" | $SED "$delay_single_quote_subst"`' +old_archive_from_expsyms_cmds='`$ECHO "$old_archive_from_expsyms_cmds" | $SED "$delay_single_quote_subst"`' +archive_cmds='`$ECHO "$archive_cmds" | $SED "$delay_single_quote_subst"`' +archive_expsym_cmds='`$ECHO "$archive_expsym_cmds" | $SED "$delay_single_quote_subst"`' +module_cmds='`$ECHO "$module_cmds" | $SED "$delay_single_quote_subst"`' +module_expsym_cmds='`$ECHO "$module_expsym_cmds" | $SED "$delay_single_quote_subst"`' +with_gnu_ld='`$ECHO "$with_gnu_ld" | $SED "$delay_single_quote_subst"`' +allow_undefined_flag='`$ECHO "$allow_undefined_flag" | $SED "$delay_single_quote_subst"`' +no_undefined_flag='`$ECHO "$no_undefined_flag" | $SED "$delay_single_quote_subst"`' +hardcode_libdir_flag_spec='`$ECHO "$hardcode_libdir_flag_spec" | $SED "$delay_single_quote_subst"`' +hardcode_libdir_separator='`$ECHO "$hardcode_libdir_separator" | $SED "$delay_single_quote_subst"`' +hardcode_direct='`$ECHO "$hardcode_direct" | $SED "$delay_single_quote_subst"`' +hardcode_direct_absolute='`$ECHO "$hardcode_direct_absolute" | $SED "$delay_single_quote_subst"`' +hardcode_minus_L='`$ECHO "$hardcode_minus_L" | $SED "$delay_single_quote_subst"`' +hardcode_shlibpath_var='`$ECHO "$hardcode_shlibpath_var" | $SED "$delay_single_quote_subst"`' +hardcode_automatic='`$ECHO "$hardcode_automatic" | $SED "$delay_single_quote_subst"`' +inherit_rpath='`$ECHO "$inherit_rpath" | $SED "$delay_single_quote_subst"`' +link_all_deplibs='`$ECHO "$link_all_deplibs" | $SED "$delay_single_quote_subst"`' +always_export_symbols='`$ECHO "$always_export_symbols" | $SED "$delay_single_quote_subst"`' +export_symbols_cmds='`$ECHO "$export_symbols_cmds" | $SED "$delay_single_quote_subst"`' +exclude_expsyms='`$ECHO "$exclude_expsyms" | $SED "$delay_single_quote_subst"`' +include_expsyms='`$ECHO "$include_expsyms" | $SED "$delay_single_quote_subst"`' +prelink_cmds='`$ECHO "$prelink_cmds" | $SED "$delay_single_quote_subst"`' +postlink_cmds='`$ECHO "$postlink_cmds" | $SED "$delay_single_quote_subst"`' +file_list_spec='`$ECHO "$file_list_spec" | $SED "$delay_single_quote_subst"`' +variables_saved_for_relink='`$ECHO "$variables_saved_for_relink" | $SED "$delay_single_quote_subst"`' +need_lib_prefix='`$ECHO "$need_lib_prefix" | $SED "$delay_single_quote_subst"`' +need_version='`$ECHO "$need_version" | $SED "$delay_single_quote_subst"`' +version_type='`$ECHO "$version_type" | $SED "$delay_single_quote_subst"`' +runpath_var='`$ECHO "$runpath_var" | $SED "$delay_single_quote_subst"`' +shlibpath_var='`$ECHO "$shlibpath_var" | $SED "$delay_single_quote_subst"`' +shlibpath_overrides_runpath='`$ECHO "$shlibpath_overrides_runpath" | $SED "$delay_single_quote_subst"`' +libname_spec='`$ECHO "$libname_spec" | $SED "$delay_single_quote_subst"`' +library_names_spec='`$ECHO "$library_names_spec" | $SED "$delay_single_quote_subst"`' +soname_spec='`$ECHO "$soname_spec" | $SED "$delay_single_quote_subst"`' +install_override_mode='`$ECHO "$install_override_mode" | $SED "$delay_single_quote_subst"`' +postinstall_cmds='`$ECHO "$postinstall_cmds" | $SED "$delay_single_quote_subst"`' +postuninstall_cmds='`$ECHO "$postuninstall_cmds" | $SED "$delay_single_quote_subst"`' +finish_cmds='`$ECHO "$finish_cmds" | $SED "$delay_single_quote_subst"`' +finish_eval='`$ECHO "$finish_eval" | $SED "$delay_single_quote_subst"`' +hardcode_into_libs='`$ECHO "$hardcode_into_libs" | $SED "$delay_single_quote_subst"`' +sys_lib_search_path_spec='`$ECHO "$sys_lib_search_path_spec" | $SED "$delay_single_quote_subst"`' +configure_time_dlsearch_path='`$ECHO "$configure_time_dlsearch_path" | $SED "$delay_single_quote_subst"`' +configure_time_lt_sys_library_path='`$ECHO "$configure_time_lt_sys_library_path" | $SED "$delay_single_quote_subst"`' +hardcode_action='`$ECHO "$hardcode_action" | $SED "$delay_single_quote_subst"`' +enable_dlopen='`$ECHO "$enable_dlopen" | $SED "$delay_single_quote_subst"`' +enable_dlopen_self='`$ECHO "$enable_dlopen_self" | $SED "$delay_single_quote_subst"`' +enable_dlopen_self_static='`$ECHO "$enable_dlopen_self_static" | $SED "$delay_single_quote_subst"`' +old_striplib='`$ECHO "$old_striplib" | $SED "$delay_single_quote_subst"`' +striplib='`$ECHO "$striplib" | $SED "$delay_single_quote_subst"`' + +LTCC='$LTCC' +LTCFLAGS='$LTCFLAGS' +compiler='$compiler_DEFAULT' + +# A function that is used when there is no print builtin or printf. +func_fallback_echo () +{ + eval 'cat <<_LTECHO_EOF +\$1 +_LTECHO_EOF' +} + +# Quote evaled strings. +for var in SHELL \ +ECHO \ +PATH_SEPARATOR \ +SED \ +GREP \ +EGREP \ +FGREP \ +LD \ +NM \ +LN_S \ +lt_SP2NL \ +lt_NL2SP \ +reload_flag \ +OBJDUMP \ +deplibs_check_method \ +file_magic_cmd \ +file_magic_glob \ +want_nocaseglob \ +DLLTOOL \ +sharedlib_from_linklib_cmd \ +AR \ +AR_FLAGS \ +archiver_list_spec \ +STRIP \ +RANLIB \ +CC \ +CFLAGS \ +compiler \ +lt_cv_sys_global_symbol_pipe \ +lt_cv_sys_global_symbol_to_cdecl \ +lt_cv_sys_global_symbol_to_import \ +lt_cv_sys_global_symbol_to_c_name_address \ +lt_cv_sys_global_symbol_to_c_name_address_lib_prefix \ +lt_cv_nm_interface \ +nm_file_list_spec \ +lt_cv_truncate_bin \ +lt_prog_compiler_no_builtin_flag \ +lt_prog_compiler_pic \ +lt_prog_compiler_wl \ +lt_prog_compiler_static \ +lt_cv_prog_compiler_c_o \ +need_locks \ +MANIFEST_TOOL \ +DSYMUTIL \ +NMEDIT \ +LIPO \ +OTOOL \ +OTOOL64 \ +shrext_cmds \ +export_dynamic_flag_spec \ +whole_archive_flag_spec \ +compiler_needs_object \ +with_gnu_ld \ +allow_undefined_flag \ +no_undefined_flag \ +hardcode_libdir_flag_spec \ +hardcode_libdir_separator \ +exclude_expsyms \ +include_expsyms \ +file_list_spec \ +variables_saved_for_relink \ +libname_spec \ +library_names_spec \ +soname_spec \ +install_override_mode \ +finish_eval \ +old_striplib \ +striplib; do + case \`eval \\\\\$ECHO \\\\""\\\\\$\$var"\\\\"\` in + *[\\\\\\\`\\"\\\$]*) + eval "lt_\$var=\\\\\\"\\\`\\\$ECHO \\"\\\$\$var\\" | \\\$SED \\"\\\$sed_quote_subst\\"\\\`\\\\\\"" ## exclude from sc_prohibit_nested_quotes + ;; + *) + eval "lt_\$var=\\\\\\"\\\$\$var\\\\\\"" + ;; + esac +done + +# Double-quote double-evaled strings. +for var in reload_cmds \ +old_postinstall_cmds \ +old_postuninstall_cmds \ +old_archive_cmds \ +extract_expsyms_cmds \ +old_archive_from_new_cmds \ +old_archive_from_expsyms_cmds \ +archive_cmds \ +archive_expsym_cmds \ +module_cmds \ +module_expsym_cmds \ +export_symbols_cmds \ +prelink_cmds \ +postlink_cmds \ +postinstall_cmds \ +postuninstall_cmds \ +finish_cmds \ +sys_lib_search_path_spec \ +configure_time_dlsearch_path \ +configure_time_lt_sys_library_path; do + case \`eval \\\\\$ECHO \\\\""\\\\\$\$var"\\\\"\` in + *[\\\\\\\`\\"\\\$]*) + eval "lt_\$var=\\\\\\"\\\`\\\$ECHO \\"\\\$\$var\\" | \\\$SED -e \\"\\\$double_quote_subst\\" -e \\"\\\$sed_quote_subst\\" -e \\"\\\$delay_variable_subst\\"\\\`\\\\\\"" ## exclude from sc_prohibit_nested_quotes + ;; + *) + eval "lt_\$var=\\\\\\"\\\$\$var\\\\\\"" + ;; + esac +done + +ac_aux_dir='$ac_aux_dir' + +# See if we are running on zsh, and set the options that allow our +# commands through without removal of \ escapes INIT. +if test -n "\${ZSH_VERSION+set}"; then + setopt NO_GLOB_SUBST +fi + + + PACKAGE='$PACKAGE' + VERSION='$VERSION' + RM='$RM' + ofile='$ofile' + + + + +_ACEOF + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 + +# Handling of arguments. +for ac_config_target in $ac_config_targets +do + case $ac_config_target in + "config.h") CONFIG_HEADERS="$CONFIG_HEADERS config.h" ;; + "depfiles") CONFIG_COMMANDS="$CONFIG_COMMANDS depfiles" ;; + "libtool") CONFIG_COMMANDS="$CONFIG_COMMANDS libtool" ;; + "Makefile") CONFIG_FILES="$CONFIG_FILES Makefile" ;; + + *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; + esac +done + + +# If the user did not use the arguments to specify the items to instantiate, +# then the envvar interface is used. Set only those that are not. +# We use the long form for the default assignment because of an extremely +# bizarre bug on SunOS 4.1.3. +if $ac_need_defaults; then + test "${CONFIG_FILES+set}" = set || CONFIG_FILES=$config_files + test "${CONFIG_HEADERS+set}" = set || CONFIG_HEADERS=$config_headers + test "${CONFIG_COMMANDS+set}" = set || CONFIG_COMMANDS=$config_commands +fi + +# Have a temporary directory for convenience. Make it in the build tree +# simply because there is no reason against having it here, and in addition, +# creating and moving files from /tmp can sometimes cause problems. +# Hook for its removal unless debugging. +# Note that there is a small window in which the directory will not be cleaned: +# after its creation but before its name has been assigned to `$tmp'. +$debug || +{ + tmp= ac_tmp= + trap 'exit_status=$? + : "${ac_tmp:=$tmp}" + { test ! -d "$ac_tmp" || rm -fr "$ac_tmp"; } && exit $exit_status +' 0 + trap 'as_fn_exit 1' 1 2 13 15 +} +# Create a (secure) tmp directory for tmp files. + +{ + tmp=`(umask 077 && mktemp -d "./confXXXXXX") 2>/dev/null` && + test -d "$tmp" +} || +{ + tmp=./conf$$-$RANDOM + (umask 077 && mkdir "$tmp") +} || as_fn_error $? "cannot create a temporary directory in ." "$LINENO" 5 +ac_tmp=$tmp + +# Set up the scripts for CONFIG_FILES section. +# No need to generate them if there are no CONFIG_FILES. +# This happens for instance with `./config.status config.h'. +if test -n "$CONFIG_FILES"; then + + +ac_cr=`echo X | tr X '\015'` +# On cygwin, bash can eat \r inside `` if the user requested igncr. +# But we know of no other shell where ac_cr would be empty at this +# point, so we can use a bashism as a fallback. +if test "x$ac_cr" = x; then + eval ac_cr=\$\'\\r\' +fi +ac_cs_awk_cr=`$AWK 'BEGIN { print "a\rb" }' /dev/null` +if test "$ac_cs_awk_cr" = "a${ac_cr}b"; then + ac_cs_awk_cr='\\r' +else + ac_cs_awk_cr=$ac_cr +fi + +echo 'BEGIN {' >"$ac_tmp/subs1.awk" && +_ACEOF + + +{ + echo "cat >conf$$subs.awk <<_ACEOF" && + echo "$ac_subst_vars" | sed 's/.*/&!$&$ac_delim/' && + echo "_ACEOF" +} >conf$$subs.sh || + as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 +ac_delim_num=`echo "$ac_subst_vars" | grep -c '^'` +ac_delim='%!_!# ' +for ac_last_try in false false false false false :; do + . ./conf$$subs.sh || + as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 + + ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | grep -c X` + if test $ac_delim_n = $ac_delim_num; then + break + elif $ac_last_try; then + as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 + else + ac_delim="$ac_delim!$ac_delim _$ac_delim!! " + fi +done +rm -f conf$$subs.sh + +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +cat >>"\$ac_tmp/subs1.awk" <<\\_ACAWK && +_ACEOF +sed -n ' +h +s/^/S["/; s/!.*/"]=/ +p +g +s/^[^!]*!// +:repl +t repl +s/'"$ac_delim"'$// +t delim +:nl +h +s/\(.\{148\}\)..*/\1/ +t more1 +s/["\\]/\\&/g; s/^/"/; s/$/\\n"\\/ +p +n +b repl +:more1 +s/["\\]/\\&/g; s/^/"/; s/$/"\\/ +p +g +s/.\{148\}// +t nl +:delim +h +s/\(.\{148\}\)..*/\1/ +t more2 +s/["\\]/\\&/g; s/^/"/; s/$/"/ +p +b +:more2 +s/["\\]/\\&/g; s/^/"/; s/$/"\\/ +p +g +s/.\{148\}// +t delim +' >$CONFIG_STATUS || ac_write_fail=1 +rm -f conf$$subs.awk +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +_ACAWK +cat >>"\$ac_tmp/subs1.awk" <<_ACAWK && + for (key in S) S_is_set[key] = 1 + FS = "" + +} +{ + line = $ 0 + nfields = split(line, field, "@") + substed = 0 + len = length(field[1]) + for (i = 2; i < nfields; i++) { + key = field[i] + keylen = length(key) + if (S_is_set[key]) { + value = S[key] + line = substr(line, 1, len) "" value "" substr(line, len + keylen + 3) + len += length(value) + length(field[++i]) + substed = 1 + } else + len += 1 + keylen + } + + print line +} + +_ACAWK +_ACEOF +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +if sed "s/$ac_cr//" < /dev/null > /dev/null 2>&1; then + sed "s/$ac_cr\$//; s/$ac_cr/$ac_cs_awk_cr/g" +else + cat +fi < "$ac_tmp/subs1.awk" > "$ac_tmp/subs.awk" \ + || as_fn_error $? "could not setup config files machinery" "$LINENO" 5 +_ACEOF + +# VPATH may cause trouble with some makes, so we remove sole $(srcdir), +# ${srcdir} and @srcdir@ entries from VPATH if srcdir is ".", strip leading and +# trailing colons and then remove the whole line if VPATH becomes empty +# (actually we leave an empty line to preserve line numbers). +if test "x$srcdir" = x.; then + ac_vpsub='/^[ ]*VPATH[ ]*=[ ]*/{ +h +s/// +s/^/:/ +s/[ ]*$/:/ +s/:\$(srcdir):/:/g +s/:\${srcdir}:/:/g +s/:@srcdir@:/:/g +s/^:*// +s/:*$// +x +s/\(=[ ]*\).*/\1/ +G +s/\n// +s/^[^=]*=[ ]*$// +}' +fi + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +fi # test -n "$CONFIG_FILES" + +# Set up the scripts for CONFIG_HEADERS section. +# No need to generate them if there are no CONFIG_HEADERS. +# This happens for instance with `./config.status Makefile'. +if test -n "$CONFIG_HEADERS"; then +cat >"$ac_tmp/defines.awk" <<\_ACAWK || +BEGIN { +_ACEOF + +# Transform confdefs.h into an awk script `defines.awk', embedded as +# here-document in config.status, that substitutes the proper values into +# config.h.in to produce config.h. + +# Create a delimiter string that does not exist in confdefs.h, to ease +# handling of long lines. +ac_delim='%!_!# ' +for ac_last_try in false false :; do + ac_tt=`sed -n "/$ac_delim/p" confdefs.h` + if test -z "$ac_tt"; then + break + elif $ac_last_try; then + as_fn_error $? "could not make $CONFIG_HEADERS" "$LINENO" 5 + else + ac_delim="$ac_delim!$ac_delim _$ac_delim!! " + fi +done + +# For the awk script, D is an array of macro values keyed by name, +# likewise P contains macro parameters if any. Preserve backslash +# newline sequences. + +ac_word_re=[_$as_cr_Letters][_$as_cr_alnum]* +sed -n ' +s/.\{148\}/&'"$ac_delim"'/g +t rset +:rset +s/^[ ]*#[ ]*define[ ][ ]*/ / +t def +d +:def +s/\\$// +t bsnl +s/["\\]/\\&/g +s/^ \('"$ac_word_re"'\)\(([^()]*)\)[ ]*\(.*\)/P["\1"]="\2"\ +D["\1"]=" \3"/p +s/^ \('"$ac_word_re"'\)[ ]*\(.*\)/D["\1"]=" \2"/p +d +:bsnl +s/["\\]/\\&/g +s/^ \('"$ac_word_re"'\)\(([^()]*)\)[ ]*\(.*\)/P["\1"]="\2"\ +D["\1"]=" \3\\\\\\n"\\/p +t cont +s/^ \('"$ac_word_re"'\)[ ]*\(.*\)/D["\1"]=" \2\\\\\\n"\\/p +t cont +d +:cont +n +s/.\{148\}/&'"$ac_delim"'/g +t clear +:clear +s/\\$// +t bsnlc +s/["\\]/\\&/g; s/^/"/; s/$/"/p +d +:bsnlc +s/["\\]/\\&/g; s/^/"/; s/$/\\\\\\n"\\/p +b cont +' >$CONFIG_STATUS || ac_write_fail=1 + +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 + for (key in D) D_is_set[key] = 1 + FS = "" +} +/^[\t ]*#[\t ]*(define|undef)[\t ]+$ac_word_re([\t (]|\$)/ { + line = \$ 0 + split(line, arg, " ") + if (arg[1] == "#") { + defundef = arg[2] + mac1 = arg[3] + } else { + defundef = substr(arg[1], 2) + mac1 = arg[2] + } + split(mac1, mac2, "(") #) + macro = mac2[1] + prefix = substr(line, 1, index(line, defundef) - 1) + if (D_is_set[macro]) { + # Preserve the white space surrounding the "#". + print prefix "define", macro P[macro] D[macro] + next + } else { + # Replace #undef with comments. This is necessary, for example, + # in the case of _POSIX_SOURCE, which is predefined and required + # on some systems where configure will not decide to define it. + if (defundef == "undef") { + print "/*", prefix defundef, macro, "*/" + next + } + } +} +{ print } +_ACAWK +_ACEOF +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 + as_fn_error $? "could not setup config headers machinery" "$LINENO" 5 +fi # test -n "$CONFIG_HEADERS" + + +eval set X " :F $CONFIG_FILES :H $CONFIG_HEADERS :C $CONFIG_COMMANDS" +shift +for ac_tag +do + case $ac_tag in + :[FHLC]) ac_mode=$ac_tag; continue;; + esac + case $ac_mode$ac_tag in + :[FHL]*:*);; + :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5;; + :[FH]-) ac_tag=-:-;; + :[FH]*) ac_tag=$ac_tag:$ac_tag.in;; + esac + ac_save_IFS=$IFS + IFS=: + set x $ac_tag + IFS=$ac_save_IFS + shift + ac_file=$1 + shift + + case $ac_mode in + :L) ac_source=$1;; + :[FH]) + ac_file_inputs= + for ac_f + do + case $ac_f in + -) ac_f="$ac_tmp/stdin";; + *) # Look for the file first in the build tree, then in the source tree + # (if the path is not absolute). The absolute path cannot be DOS-style, + # because $ac_f cannot contain `:'. + test -f "$ac_f" || + case $ac_f in + [\\/$]*) false;; + *) test -f "$srcdir/$ac_f" && ac_f="$srcdir/$ac_f";; + esac || + as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5;; + esac + case $ac_f in *\'*) ac_f=`$as_echo "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac + as_fn_append ac_file_inputs " '$ac_f'" + done + + # Let's still pretend it is `configure' which instantiates (i.e., don't + # use $as_me), people would be surprised to read: + # /* config.h. Generated by config.status. */ + configure_input='Generated from '` + $as_echo "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' + `' by configure.' + if test x"$ac_file" != x-; then + configure_input="$ac_file. $configure_input" + { $as_echo "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 +$as_echo "$as_me: creating $ac_file" >&6;} + fi + # Neutralize special characters interpreted by sed in replacement strings. + case $configure_input in #( + *\&* | *\|* | *\\* ) + ac_sed_conf_input=`$as_echo "$configure_input" | + sed 's/[\\\\&|]/\\\\&/g'`;; #( + *) ac_sed_conf_input=$configure_input;; + esac + + case $ac_tag in + *:-:* | *:-) cat >"$ac_tmp/stdin" \ + || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;; + esac + ;; + esac + + ac_dir=`$as_dirname -- "$ac_file" || +$as_expr X"$ac_file" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ + X"$ac_file" : 'X\(//\)[^/]' \| \ + X"$ac_file" : 'X\(//\)$' \| \ + X"$ac_file" : 'X\(/\)' \| . 2>/dev/null || +$as_echo X"$ac_file" | + sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ + s//\1/ + q + } + /^X\(\/\/\)[^/].*/{ + s//\1/ + q + } + /^X\(\/\/\)$/{ + s//\1/ + q + } + /^X\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + as_dir="$ac_dir"; as_fn_mkdir_p + ac_builddir=. + +case "$ac_dir" in +.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; +*) + ac_dir_suffix=/`$as_echo "$ac_dir" | sed 's|^\.[\\/]||'` + # A ".." for each directory in $ac_dir_suffix. + ac_top_builddir_sub=`$as_echo "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + case $ac_top_builddir_sub in + "") ac_top_builddir_sub=. ac_top_build_prefix= ;; + *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; + esac ;; +esac +ac_abs_top_builddir=$ac_pwd +ac_abs_builddir=$ac_pwd$ac_dir_suffix +# for backward compatibility: +ac_top_builddir=$ac_top_build_prefix + +case $srcdir in + .) # We are building in place. + ac_srcdir=. + ac_top_srcdir=$ac_top_builddir_sub + ac_abs_top_srcdir=$ac_pwd ;; + [\\/]* | ?:[\\/]* ) # Absolute name. + ac_srcdir=$srcdir$ac_dir_suffix; + ac_top_srcdir=$srcdir + ac_abs_top_srcdir=$srcdir ;; + *) # Relative name. + ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix + ac_top_srcdir=$ac_top_build_prefix$srcdir + ac_abs_top_srcdir=$ac_pwd/$srcdir ;; +esac +ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix + + + case $ac_mode in + :F) + # + # CONFIG_FILE + # + + case $INSTALL in + [\\/$]* | ?:[\\/]* ) ac_INSTALL=$INSTALL ;; + *) ac_INSTALL=$ac_top_build_prefix$INSTALL ;; + esac + ac_MKDIR_P=$MKDIR_P + case $MKDIR_P in + [\\/$]* | ?:[\\/]* ) ;; + */*) ac_MKDIR_P=$ac_top_build_prefix$MKDIR_P ;; + esac +_ACEOF + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +# If the template does not know about datarootdir, expand it. +# FIXME: This hack should be removed a few years after 2.60. +ac_datarootdir_hack=; ac_datarootdir_seen= +ac_sed_dataroot=' +/datarootdir/ { + p + q +} +/@datadir@/p +/@docdir@/p +/@infodir@/p +/@localedir@/p +/@mandir@/p' +case `eval "sed -n \"\$ac_sed_dataroot\" $ac_file_inputs"` in +*datarootdir*) ac_datarootdir_seen=yes;; +*@datadir@*|*@docdir@*|*@infodir@*|*@localedir@*|*@mandir@*) + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 +$as_echo "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;} +_ACEOF +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 + ac_datarootdir_hack=' + s&@datadir@&$datadir&g + s&@docdir@&$docdir&g + s&@infodir@&$infodir&g + s&@localedir@&$localedir&g + s&@mandir@&$mandir&g + s&\\\${datarootdir}&$datarootdir&g' ;; +esac +_ACEOF + +# Neutralize VPATH when `$srcdir' = `.'. +# Shell code in configure.ac might set extrasub. +# FIXME: do we really want to maintain this feature? +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +ac_sed_extra="$ac_vpsub +$extrasub +_ACEOF +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +:t +/@[a-zA-Z_][a-zA-Z_0-9]*@/!b +s|@configure_input@|$ac_sed_conf_input|;t t +s&@top_builddir@&$ac_top_builddir_sub&;t t +s&@top_build_prefix@&$ac_top_build_prefix&;t t +s&@srcdir@&$ac_srcdir&;t t +s&@abs_srcdir@&$ac_abs_srcdir&;t t +s&@top_srcdir@&$ac_top_srcdir&;t t +s&@abs_top_srcdir@&$ac_abs_top_srcdir&;t t +s&@builddir@&$ac_builddir&;t t +s&@abs_builddir@&$ac_abs_builddir&;t t +s&@abs_top_builddir@&$ac_abs_top_builddir&;t t +s&@INSTALL@&$ac_INSTALL&;t t +s&@MKDIR_P@&$ac_MKDIR_P&;t t +$ac_datarootdir_hack +" +eval sed \"\$ac_sed_extra\" "$ac_file_inputs" | $AWK -f "$ac_tmp/subs.awk" \ + >$ac_tmp/out || as_fn_error $? "could not create $ac_file" "$LINENO" 5 + +test -z "$ac_datarootdir_hack$ac_datarootdir_seen" && + { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } && + { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' \ + "$ac_tmp/out"`; test -z "$ac_out"; } && + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \`datarootdir' +which seems to be undefined. Please make sure it is defined" >&5 +$as_echo "$as_me: WARNING: $ac_file contains a reference to the variable \`datarootdir' +which seems to be undefined. Please make sure it is defined" >&2;} + + rm -f "$ac_tmp/stdin" + case $ac_file in + -) cat "$ac_tmp/out" && rm -f "$ac_tmp/out";; + *) rm -f "$ac_file" && mv "$ac_tmp/out" "$ac_file";; + esac \ + || as_fn_error $? "could not create $ac_file" "$LINENO" 5 + ;; + :H) + # + # CONFIG_HEADER + # + if test x"$ac_file" != x-; then + { + $as_echo "/* $configure_input */" \ + && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs" + } >"$ac_tmp/config.h" \ + || as_fn_error $? "could not create $ac_file" "$LINENO" 5 + if diff "$ac_file" "$ac_tmp/config.h" >/dev/null 2>&1; then + { $as_echo "$as_me:${as_lineno-$LINENO}: $ac_file is unchanged" >&5 +$as_echo "$as_me: $ac_file is unchanged" >&6;} + else + rm -f "$ac_file" + mv "$ac_tmp/config.h" "$ac_file" \ + || as_fn_error $? "could not create $ac_file" "$LINENO" 5 + fi + else + $as_echo "/* $configure_input */" \ + && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs" \ + || as_fn_error $? "could not create -" "$LINENO" 5 + fi +# Compute "$ac_file"'s index in $config_headers. +_am_arg="$ac_file" +_am_stamp_count=1 +for _am_header in $config_headers :; do + case $_am_header in + $_am_arg | $_am_arg:* ) + break ;; + * ) + _am_stamp_count=`expr $_am_stamp_count + 1` ;; + esac +done +echo "timestamp for $_am_arg" >`$as_dirname -- "$_am_arg" || +$as_expr X"$_am_arg" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ + X"$_am_arg" : 'X\(//\)[^/]' \| \ + X"$_am_arg" : 'X\(//\)$' \| \ + X"$_am_arg" : 'X\(/\)' \| . 2>/dev/null || +$as_echo X"$_am_arg" | + sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ + s//\1/ + q + } + /^X\(\/\/\)[^/].*/{ + s//\1/ + q + } + /^X\(\/\/\)$/{ + s//\1/ + q + } + /^X\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'`/stamp-h$_am_stamp_count + ;; + + :C) { $as_echo "$as_me:${as_lineno-$LINENO}: executing $ac_file commands" >&5 +$as_echo "$as_me: executing $ac_file commands" >&6;} + ;; + esac + + + case $ac_file$ac_mode in + "depfiles":C) test x"$AMDEP_TRUE" != x"" || { + # Older Autoconf quotes --file arguments for eval, but not when files + # are listed without --file. Let's play safe and only enable the eval + # if we detect the quoting. + # TODO: see whether this extra hack can be removed once we start + # requiring Autoconf 2.70 or later. + case $CONFIG_FILES in #( + *\'*) : + eval set x "$CONFIG_FILES" ;; #( + *) : + set x $CONFIG_FILES ;; #( + *) : + ;; +esac + shift + # Used to flag and report bootstrapping failures. + am_rc=0 + for am_mf + do + # Strip MF so we end up with the name of the file. + am_mf=`$as_echo "$am_mf" | sed -e 's/:.*$//'` + # Check whether this is an Automake generated Makefile which includes + # dependency-tracking related rules and includes. + # Grep'ing the whole file directly is not great: AIX grep has a line + # limit of 2048, but all sed's we know have understand at least 4000. + sed -n 's,^am--depfiles:.*,X,p' "$am_mf" | grep X >/dev/null 2>&1 \ + || continue + am_dirpart=`$as_dirname -- "$am_mf" || +$as_expr X"$am_mf" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ + X"$am_mf" : 'X\(//\)[^/]' \| \ + X"$am_mf" : 'X\(//\)$' \| \ + X"$am_mf" : 'X\(/\)' \| . 2>/dev/null || +$as_echo X"$am_mf" | + sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ + s//\1/ + q + } + /^X\(\/\/\)[^/].*/{ + s//\1/ + q + } + /^X\(\/\/\)$/{ + s//\1/ + q + } + /^X\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + am_filepart=`$as_basename -- "$am_mf" || +$as_expr X/"$am_mf" : '.*/\([^/][^/]*\)/*$' \| \ + X"$am_mf" : 'X\(//\)$' \| \ + X"$am_mf" : 'X\(/\)' \| . 2>/dev/null || +$as_echo X/"$am_mf" | + sed '/^.*\/\([^/][^/]*\)\/*$/{ + s//\1/ + q + } + /^X\/\(\/\/\)$/{ + s//\1/ + q + } + /^X\/\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + { echo "$as_me:$LINENO: cd "$am_dirpart" \ + && sed -e '/# am--include-marker/d' "$am_filepart" \ + | $MAKE -f - am--depfiles" >&5 + (cd "$am_dirpart" \ + && sed -e '/# am--include-marker/d' "$am_filepart" \ + | $MAKE -f - am--depfiles) >&5 2>&5 + ac_status=$? + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + (exit $ac_status); } || am_rc=$? + done + if test $am_rc -ne 0; then + { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "Something went wrong bootstrapping makefile fragments + for automatic dependency tracking. Try re-running configure with the + '--disable-dependency-tracking' option to at least be able to build + the package (albeit without support for automatic dependency tracking). +See \`config.log' for more details" "$LINENO" 5; } + fi + { am_dirpart=; unset am_dirpart;} + { am_filepart=; unset am_filepart;} + { am_mf=; unset am_mf;} + { am_rc=; unset am_rc;} + rm -f conftest-deps.mk +} + ;; + "libtool":C) + + # See if we are running on zsh, and set the options that allow our + # commands through without removal of \ escapes. + if test -n "${ZSH_VERSION+set}"; then + setopt NO_GLOB_SUBST + fi + + cfgfile=${ofile}T + trap "$RM \"$cfgfile\"; exit 1" 1 2 15 + $RM "$cfgfile" + + cat <<_LT_EOF >> "$cfgfile" +#! $SHELL +# Generated automatically by $as_me ($PACKAGE) $VERSION +# NOTE: Changes made to this file will be lost: look at ltmain.sh. + +# Provide generalized library-building support services. +# Written by Gordon Matzigkeit, 1996 + +# Copyright (C) 2014 Free Software Foundation, Inc. +# This is free software; see the source for copying conditions. There is NO +# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +# GNU Libtool is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of of the License, or +# (at your option) any later version. +# +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program or library that is built +# using GNU Libtool, you may include this file under the same +# distribution terms that you use for the rest of that program. +# +# GNU Libtool is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# The names of the tagged configurations supported by this script. +available_tags='' + +# Configured defaults for sys_lib_dlsearch_path munging. +: \${LT_SYS_LIBRARY_PATH="$configure_time_lt_sys_library_path"} + +# ### BEGIN LIBTOOL CONFIG + +# Which release of libtool.m4 was used? +macro_version=$macro_version +macro_revision=$macro_revision + +# Whether or not to build shared libraries. +build_libtool_libs=$enable_shared + +# Whether or not to build static libraries. +build_old_libs=$enable_static + +# What type of objects to build. +pic_mode=$pic_mode + +# Whether or not to optimize for fast installation. +fast_install=$enable_fast_install + +# Shared archive member basename,for filename based shared library versioning on AIX. +shared_archive_member_spec=$shared_archive_member_spec + +# Shell to use when invoking shell scripts. +SHELL=$lt_SHELL + +# An echo program that protects backslashes. +ECHO=$lt_ECHO + +# The PATH separator for the build system. +PATH_SEPARATOR=$lt_PATH_SEPARATOR + +# The host system. +host_alias=$host_alias +host=$host +host_os=$host_os + +# The build system. +build_alias=$build_alias +build=$build +build_os=$build_os + +# A sed program that does not truncate output. +SED=$lt_SED + +# Sed that helps us avoid accidentally triggering echo(1) options like -n. +Xsed="\$SED -e 1s/^X//" + +# A grep program that handles long lines. +GREP=$lt_GREP + +# An ERE matcher. +EGREP=$lt_EGREP + +# A literal string matcher. +FGREP=$lt_FGREP + +# A BSD- or MS-compatible name lister. +NM=$lt_NM + +# Whether we need soft or hard links. +LN_S=$lt_LN_S + +# What is the maximum length of a command? +max_cmd_len=$max_cmd_len + +# Object file suffix (normally "o"). +objext=$ac_objext + +# Executable file suffix (normally ""). +exeext=$exeext + +# whether the shell understands "unset". +lt_unset=$lt_unset + +# turn spaces into newlines. +SP2NL=$lt_lt_SP2NL + +# turn newlines into spaces. +NL2SP=$lt_lt_NL2SP + +# convert \$build file names to \$host format. +to_host_file_cmd=$lt_cv_to_host_file_cmd + +# convert \$build files to toolchain format. +to_tool_file_cmd=$lt_cv_to_tool_file_cmd + +# An object symbol dumper. +OBJDUMP=$lt_OBJDUMP + +# Method to check whether dependent libraries are shared objects. +deplibs_check_method=$lt_deplibs_check_method + +# Command to use when deplibs_check_method = "file_magic". +file_magic_cmd=$lt_file_magic_cmd + +# How to find potential files when deplibs_check_method = "file_magic". +file_magic_glob=$lt_file_magic_glob + +# Find potential files using nocaseglob when deplibs_check_method = "file_magic". +want_nocaseglob=$lt_want_nocaseglob + +# DLL creation program. +DLLTOOL=$lt_DLLTOOL + +# Command to associate shared and link libraries. +sharedlib_from_linklib_cmd=$lt_sharedlib_from_linklib_cmd + +# The archiver. +AR=$lt_AR + +# Flags to create an archive. +AR_FLAGS=$lt_AR_FLAGS + +# How to feed a file listing to the archiver. +archiver_list_spec=$lt_archiver_list_spec + +# A symbol stripping program. +STRIP=$lt_STRIP + +# Commands used to install an old-style archive. +RANLIB=$lt_RANLIB +old_postinstall_cmds=$lt_old_postinstall_cmds +old_postuninstall_cmds=$lt_old_postuninstall_cmds + +# Whether to use a lock for old archive extraction. +lock_old_archive_extraction=$lock_old_archive_extraction + +# A C compiler. +LTCC=$lt_CC + +# LTCC compiler flags. +LTCFLAGS=$lt_CFLAGS + +# Take the output of nm and produce a listing of raw symbols and C names. +global_symbol_pipe=$lt_lt_cv_sys_global_symbol_pipe + +# Transform the output of nm in a proper C declaration. +global_symbol_to_cdecl=$lt_lt_cv_sys_global_symbol_to_cdecl + +# Transform the output of nm into a list of symbols to manually relocate. +global_symbol_to_import=$lt_lt_cv_sys_global_symbol_to_import + +# Transform the output of nm in a C name address pair. +global_symbol_to_c_name_address=$lt_lt_cv_sys_global_symbol_to_c_name_address + +# Transform the output of nm in a C name address pair when lib prefix is needed. +global_symbol_to_c_name_address_lib_prefix=$lt_lt_cv_sys_global_symbol_to_c_name_address_lib_prefix + +# The name lister interface. +nm_interface=$lt_lt_cv_nm_interface + +# Specify filename containing input files for \$NM. +nm_file_list_spec=$lt_nm_file_list_spec + +# The root where to search for dependent libraries,and where our libraries should be installed. +lt_sysroot=$lt_sysroot + +# Command to truncate a binary pipe. +lt_truncate_bin=$lt_lt_cv_truncate_bin + +# The name of the directory that contains temporary libtool files. +objdir=$objdir + +# Used to examine libraries when file_magic_cmd begins with "file". +MAGIC_CMD=$MAGIC_CMD + +# Must we lock files when doing compilation? +need_locks=$lt_need_locks + +# Manifest tool. +MANIFEST_TOOL=$lt_MANIFEST_TOOL + +# Tool to manipulate archived DWARF debug symbol files on Mac OS X. +DSYMUTIL=$lt_DSYMUTIL + +# Tool to change global to local symbols on Mac OS X. +NMEDIT=$lt_NMEDIT + +# Tool to manipulate fat objects and archives on Mac OS X. +LIPO=$lt_LIPO + +# ldd/readelf like tool for Mach-O binaries on Mac OS X. +OTOOL=$lt_OTOOL + +# ldd/readelf like tool for 64 bit Mach-O binaries on Mac OS X 10.4. +OTOOL64=$lt_OTOOL64 + +# Old archive suffix (normally "a"). +libext=$libext + +# Shared library suffix (normally ".so"). +shrext_cmds=$lt_shrext_cmds + +# The commands to extract the exported symbol list from a shared archive. +extract_expsyms_cmds=$lt_extract_expsyms_cmds + +# Variables whose values should be saved in libtool wrapper scripts and +# restored at link time. +variables_saved_for_relink=$lt_variables_saved_for_relink + +# Do we need the "lib" prefix for modules? +need_lib_prefix=$need_lib_prefix + +# Do we need a version for libraries? +need_version=$need_version + +# Library versioning type. +version_type=$version_type + +# Shared library runtime path variable. +runpath_var=$runpath_var + +# Shared library path variable. +shlibpath_var=$shlibpath_var + +# Is shlibpath searched before the hard-coded library search path? +shlibpath_overrides_runpath=$shlibpath_overrides_runpath + +# Format of library name prefix. +libname_spec=$lt_libname_spec + +# List of archive names. First name is the real one, the rest are links. +# The last name is the one that the linker finds with -lNAME +library_names_spec=$lt_library_names_spec + +# The coded name of the library, if different from the real name. +soname_spec=$lt_soname_spec + +# Permission mode override for installation of shared libraries. +install_override_mode=$lt_install_override_mode + +# Command to use after installation of a shared archive. +postinstall_cmds=$lt_postinstall_cmds + +# Command to use after uninstallation of a shared archive. +postuninstall_cmds=$lt_postuninstall_cmds + +# Commands used to finish a libtool library installation in a directory. +finish_cmds=$lt_finish_cmds + +# As "finish_cmds", except a single script fragment to be evaled but +# not shown. +finish_eval=$lt_finish_eval + +# Whether we should hardcode library paths into libraries. +hardcode_into_libs=$hardcode_into_libs + +# Compile-time system search path for libraries. +sys_lib_search_path_spec=$lt_sys_lib_search_path_spec + +# Detected run-time system search path for libraries. +sys_lib_dlsearch_path_spec=$lt_configure_time_dlsearch_path + +# Explicit LT_SYS_LIBRARY_PATH set during ./configure time. +configure_time_lt_sys_library_path=$lt_configure_time_lt_sys_library_path + +# Whether dlopen is supported. +dlopen_support=$enable_dlopen + +# Whether dlopen of programs is supported. +dlopen_self=$enable_dlopen_self + +# Whether dlopen of statically linked programs is supported. +dlopen_self_static=$enable_dlopen_self_static + +# Commands to strip libraries. +old_striplib=$lt_old_striplib +striplib=$lt_striplib + + +# The linker used to build libraries. +LD=$lt_LD + +# How to create reloadable object files. +reload_flag=$lt_reload_flag +reload_cmds=$lt_reload_cmds + +# Commands used to build an old-style archive. +old_archive_cmds=$lt_old_archive_cmds + +# A language specific compiler. +CC=$lt_compiler + +# Is the compiler the GNU compiler? +with_gcc=$GCC + +# Compiler flag to turn off builtin functions. +no_builtin_flag=$lt_lt_prog_compiler_no_builtin_flag + +# Additional compiler flags for building library objects. +pic_flag=$lt_lt_prog_compiler_pic + +# How to pass a linker flag through the compiler. +wl=$lt_lt_prog_compiler_wl + +# Compiler flag to prevent dynamic linking. +link_static_flag=$lt_lt_prog_compiler_static + +# Does compiler simultaneously support -c and -o options? +compiler_c_o=$lt_lt_cv_prog_compiler_c_o + +# Whether or not to add -lc for building shared libraries. +build_libtool_need_lc=$archive_cmds_need_lc + +# Whether or not to disallow shared libs when runtime libs are static. +allow_libtool_libs_with_static_runtimes=$enable_shared_with_static_runtimes + +# Compiler flag to allow reflexive dlopens. +export_dynamic_flag_spec=$lt_export_dynamic_flag_spec + +# Compiler flag to generate shared objects directly from archives. +whole_archive_flag_spec=$lt_whole_archive_flag_spec + +# Whether the compiler copes with passing no objects directly. +compiler_needs_object=$lt_compiler_needs_object + +# Create an old-style archive from a shared archive. +old_archive_from_new_cmds=$lt_old_archive_from_new_cmds + +# Create a temporary old-style archive to link instead of a shared archive. +old_archive_from_expsyms_cmds=$lt_old_archive_from_expsyms_cmds + +# Commands used to build a shared archive. +archive_cmds=$lt_archive_cmds +archive_expsym_cmds=$lt_archive_expsym_cmds + +# Commands used to build a loadable module if different from building +# a shared archive. +module_cmds=$lt_module_cmds +module_expsym_cmds=$lt_module_expsym_cmds + +# Whether we are building with GNU ld or not. +with_gnu_ld=$lt_with_gnu_ld + +# Flag that allows shared libraries with undefined symbols to be built. +allow_undefined_flag=$lt_allow_undefined_flag + +# Flag that enforces no undefined symbols. +no_undefined_flag=$lt_no_undefined_flag + +# Flag to hardcode \$libdir into a binary during linking. +# This must work even if \$libdir does not exist +hardcode_libdir_flag_spec=$lt_hardcode_libdir_flag_spec + +# Whether we need a single "-rpath" flag with a separated argument. +hardcode_libdir_separator=$lt_hardcode_libdir_separator + +# Set to "yes" if using DIR/libNAME\$shared_ext during linking hardcodes +# DIR into the resulting binary. +hardcode_direct=$hardcode_direct + +# Set to "yes" if using DIR/libNAME\$shared_ext during linking hardcodes +# DIR into the resulting binary and the resulting library dependency is +# "absolute",i.e impossible to change by setting \$shlibpath_var if the +# library is relocated. +hardcode_direct_absolute=$hardcode_direct_absolute + +# Set to "yes" if using the -LDIR flag during linking hardcodes DIR +# into the resulting binary. +hardcode_minus_L=$hardcode_minus_L + +# Set to "yes" if using SHLIBPATH_VAR=DIR during linking hardcodes DIR +# into the resulting binary. +hardcode_shlibpath_var=$hardcode_shlibpath_var + +# Set to "yes" if building a shared library automatically hardcodes DIR +# into the library and all subsequent libraries and executables linked +# against it. +hardcode_automatic=$hardcode_automatic + +# Set to yes if linker adds runtime paths of dependent libraries +# to runtime path list. +inherit_rpath=$inherit_rpath + +# Whether libtool must link a program against all its dependency libraries. +link_all_deplibs=$link_all_deplibs + +# Set to "yes" if exported symbols are required. +always_export_symbols=$always_export_symbols + +# The commands to list exported symbols. +export_symbols_cmds=$lt_export_symbols_cmds + +# Symbols that should not be listed in the preloaded symbols. +exclude_expsyms=$lt_exclude_expsyms + +# Symbols that must always be exported. +include_expsyms=$lt_include_expsyms + +# Commands necessary for linking programs (against libraries) with templates. +prelink_cmds=$lt_prelink_cmds + +# Commands necessary for finishing linking programs. +postlink_cmds=$lt_postlink_cmds + +# Specify filename containing input files. +file_list_spec=$lt_file_list_spec + +# How to hardcode a shared library path into an executable. +hardcode_action=$hardcode_action + +# ### END LIBTOOL CONFIG + +_LT_EOF + + cat <<'_LT_EOF' >> "$cfgfile" + +# ### BEGIN FUNCTIONS SHARED WITH CONFIGURE + +# func_munge_path_list VARIABLE PATH +# ----------------------------------- +# VARIABLE is name of variable containing _space_ separated list of +# directories to be munged by the contents of PATH, which is string +# having a format: +# "DIR[:DIR]:" +# string "DIR[ DIR]" will be prepended to VARIABLE +# ":DIR[:DIR]" +# string "DIR[ DIR]" will be appended to VARIABLE +# "DIRP[:DIRP]::[DIRA:]DIRA" +# string "DIRP[ DIRP]" will be prepended to VARIABLE and string +# "DIRA[ DIRA]" will be appended to VARIABLE +# "DIR[:DIR]" +# VARIABLE will be replaced by "DIR[ DIR]" +func_munge_path_list () +{ + case x$2 in + x) + ;; + *:) + eval $1=\"`$ECHO $2 | $SED 's/:/ /g'` \$$1\" + ;; + x:*) + eval $1=\"\$$1 `$ECHO $2 | $SED 's/:/ /g'`\" + ;; + *::*) + eval $1=\"\$$1\ `$ECHO $2 | $SED -e 's/.*:://' -e 's/:/ /g'`\" + eval $1=\"`$ECHO $2 | $SED -e 's/::.*//' -e 's/:/ /g'`\ \$$1\" + ;; + *) + eval $1=\"`$ECHO $2 | $SED 's/:/ /g'`\" + ;; + esac +} + + +# Calculate cc_basename. Skip known compiler wrappers and cross-prefix. +func_cc_basename () +{ + for cc_temp in $*""; do + case $cc_temp in + compile | *[\\/]compile | ccache | *[\\/]ccache ) ;; + distcc | *[\\/]distcc | purify | *[\\/]purify ) ;; + \-*) ;; + *) break;; + esac + done + func_cc_basename_result=`$ECHO "$cc_temp" | $SED "s%.*/%%; s%^$host_alias-%%"` +} + + +# ### END FUNCTIONS SHARED WITH CONFIGURE + +_LT_EOF + + case $host_os in + aix3*) + cat <<\_LT_EOF >> "$cfgfile" +# AIX sometimes has problems with the GCC collect2 program. For some +# reason, if we set the COLLECT_NAMES environment variable, the problems +# vanish in a puff of smoke. +if test set != "${COLLECT_NAMES+set}"; then + COLLECT_NAMES= + export COLLECT_NAMES +fi +_LT_EOF + ;; + esac + + +ltmain=$ac_aux_dir/ltmain.sh + + + # We use sed instead of cat because bash on DJGPP gets confused if + # if finds mixed CR/LF and LF-only lines. Since sed operates in + # text mode, it properly converts lines to CR/LF. This bash problem + # is reportedly fixed, but why not run on old versions too? + sed '$q' "$ltmain" >> "$cfgfile" \ + || (rm -f "$cfgfile"; exit 1) + + mv -f "$cfgfile" "$ofile" || + (rm -f "$ofile" && cp "$cfgfile" "$ofile" && rm -f "$cfgfile") + chmod +x "$ofile" + + ;; + + esac +done # for ac_tag + + +as_fn_exit 0 +_ACEOF +ac_clean_files=$ac_clean_files_save + +test $ac_write_fail = 0 || + as_fn_error $? "write failure creating $CONFIG_STATUS" "$LINENO" 5 + + +# configure is writing to config.log, and then calls config.status. +# config.status does its own redirection, appending to config.log. +# Unfortunately, on DOS this fails, as config.log is still kept open +# by configure, so config.status won't be able to write to it; its +# output is simply discarded. So we exec the FD to /dev/null, +# effectively closing config.log, so it can be properly (re)opened and +# appended to by config.status. When coming back to configure, we +# need to make the FD available again. +if test "$no_create" != yes; then + ac_cs_success=: + ac_config_status_args= + test "$silent" = yes && + ac_config_status_args="$ac_config_status_args --quiet" + exec 5>/dev/null + $SHELL $CONFIG_STATUS $ac_config_status_args || ac_cs_success=false + exec 5>>config.log + # Use ||, not &&, to avoid exiting from the if with $? = 1, which + # would make configure fail if this is the last instruction. + $ac_cs_success || as_fn_exit 1 +fi +if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 +$as_echo "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} +fi + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/configure.ac b/deps/ngtcp2/ngtcp2/third-party/libev/configure.ac new file mode 100644 index 00000000000000..196640b56dd008 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/configure.ac @@ -0,0 +1,27 @@ +dnl also update ev.h! +AC_INIT([libev], [4.33]) + +orig_CFLAGS="$CFLAGS" + +AC_CONFIG_SRCDIR([ev_epoll.c]) +AM_INIT_AUTOMAKE + +AC_CONFIG_HEADERS([config.h]) +AM_MAINTAINER_MODE + +AC_PROG_CC + +dnl Supply default CFLAGS, if not specified +if test -z "$orig_CFLAGS"; then + if test x$GCC = xyes; then + CFLAGS="-g -O3" + fi +fi + +AC_PROG_INSTALL +AC_PROG_LIBTOOL + +m4_include([libev.m4]) + +AC_CONFIG_FILES([Makefile]) +AC_OUTPUT diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/depcomp b/deps/ngtcp2/ngtcp2/third-party/libev/depcomp new file mode 100755 index 00000000000000..65cbf7093a1e4c --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/depcomp @@ -0,0 +1,791 @@ +#! /bin/sh +# depcomp - compile a program generating dependencies as side-effects + +scriptversion=2018-03-07.03; # UTC + +# Copyright (C) 1999-2018 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program that contains a +# configuration script generated by Autoconf, you may include it under +# the same distribution terms that you use for the rest of that program. + +# Originally written by Alexandre Oliva . + +case $1 in + '') + echo "$0: No command. Try '$0 --help' for more information." 1>&2 + exit 1; + ;; + -h | --h*) + cat <<\EOF +Usage: depcomp [--help] [--version] PROGRAM [ARGS] + +Run PROGRAMS ARGS to compile a file, generating dependencies +as side-effects. + +Environment variables: + depmode Dependency tracking mode. + source Source file read by 'PROGRAMS ARGS'. + object Object file output by 'PROGRAMS ARGS'. + DEPDIR directory where to store dependencies. + depfile Dependency file to output. + tmpdepfile Temporary file to use when outputting dependencies. + libtool Whether libtool is used (yes/no). + +Report bugs to . +EOF + exit $? + ;; + -v | --v*) + echo "depcomp $scriptversion" + exit $? + ;; +esac + +# Get the directory component of the given path, and save it in the +# global variables '$dir'. Note that this directory component will +# be either empty or ending with a '/' character. This is deliberate. +set_dir_from () +{ + case $1 in + */*) dir=`echo "$1" | sed -e 's|/[^/]*$|/|'`;; + *) dir=;; + esac +} + +# Get the suffix-stripped basename of the given path, and save it the +# global variable '$base'. +set_base_from () +{ + base=`echo "$1" | sed -e 's|^.*/||' -e 's/\.[^.]*$//'` +} + +# If no dependency file was actually created by the compiler invocation, +# we still have to create a dummy depfile, to avoid errors with the +# Makefile "include basename.Plo" scheme. +make_dummy_depfile () +{ + echo "#dummy" > "$depfile" +} + +# Factor out some common post-processing of the generated depfile. +# Requires the auxiliary global variable '$tmpdepfile' to be set. +aix_post_process_depfile () +{ + # If the compiler actually managed to produce a dependency file, + # post-process it. + if test -f "$tmpdepfile"; then + # Each line is of the form 'foo.o: dependency.h'. + # Do two passes, one to just change these to + # $object: dependency.h + # and one to simply output + # dependency.h: + # which is needed to avoid the deleted-header problem. + { sed -e "s,^.*\.[$lower]*:,$object:," < "$tmpdepfile" + sed -e "s,^.*\.[$lower]*:[$tab ]*,," -e 's,$,:,' < "$tmpdepfile" + } > "$depfile" + rm -f "$tmpdepfile" + else + make_dummy_depfile + fi +} + +# A tabulation character. +tab=' ' +# A newline character. +nl=' +' +# Character ranges might be problematic outside the C locale. +# These definitions help. +upper=ABCDEFGHIJKLMNOPQRSTUVWXYZ +lower=abcdefghijklmnopqrstuvwxyz +digits=0123456789 +alpha=${upper}${lower} + +if test -z "$depmode" || test -z "$source" || test -z "$object"; then + echo "depcomp: Variables source, object and depmode must be set" 1>&2 + exit 1 +fi + +# Dependencies for sub/bar.o or sub/bar.obj go into sub/.deps/bar.Po. +depfile=${depfile-`echo "$object" | + sed 's|[^\\/]*$|'${DEPDIR-.deps}'/&|;s|\.\([^.]*\)$|.P\1|;s|Pobj$|Po|'`} +tmpdepfile=${tmpdepfile-`echo "$depfile" | sed 's/\.\([^.]*\)$/.T\1/'`} + +rm -f "$tmpdepfile" + +# Avoid interferences from the environment. +gccflag= dashmflag= + +# Some modes work just like other modes, but use different flags. We +# parameterize here, but still list the modes in the big case below, +# to make depend.m4 easier to write. Note that we *cannot* use a case +# here, because this file can only contain one case statement. +if test "$depmode" = hp; then + # HP compiler uses -M and no extra arg. + gccflag=-M + depmode=gcc +fi + +if test "$depmode" = dashXmstdout; then + # This is just like dashmstdout with a different argument. + dashmflag=-xM + depmode=dashmstdout +fi + +cygpath_u="cygpath -u -f -" +if test "$depmode" = msvcmsys; then + # This is just like msvisualcpp but w/o cygpath translation. + # Just convert the backslash-escaped backslashes to single forward + # slashes to satisfy depend.m4 + cygpath_u='sed s,\\\\,/,g' + depmode=msvisualcpp +fi + +if test "$depmode" = msvc7msys; then + # This is just like msvc7 but w/o cygpath translation. + # Just convert the backslash-escaped backslashes to single forward + # slashes to satisfy depend.m4 + cygpath_u='sed s,\\\\,/,g' + depmode=msvc7 +fi + +if test "$depmode" = xlc; then + # IBM C/C++ Compilers xlc/xlC can output gcc-like dependency information. + gccflag=-qmakedep=gcc,-MF + depmode=gcc +fi + +case "$depmode" in +gcc3) +## gcc 3 implements dependency tracking that does exactly what +## we want. Yay! Note: for some reason libtool 1.4 doesn't like +## it if -MD -MP comes after the -MF stuff. Hmm. +## Unfortunately, FreeBSD c89 acceptance of flags depends upon +## the command line argument order; so add the flags where they +## appear in depend2.am. Note that the slowdown incurred here +## affects only configure: in makefiles, %FASTDEP% shortcuts this. + for arg + do + case $arg in + -c) set fnord "$@" -MT "$object" -MD -MP -MF "$tmpdepfile" "$arg" ;; + *) set fnord "$@" "$arg" ;; + esac + shift # fnord + shift # $arg + done + "$@" + stat=$? + if test $stat -ne 0; then + rm -f "$tmpdepfile" + exit $stat + fi + mv "$tmpdepfile" "$depfile" + ;; + +gcc) +## Note that this doesn't just cater to obsosete pre-3.x GCC compilers. +## but also to in-use compilers like IMB xlc/xlC and the HP C compiler. +## (see the conditional assignment to $gccflag above). +## There are various ways to get dependency output from gcc. Here's +## why we pick this rather obscure method: +## - Don't want to use -MD because we'd like the dependencies to end +## up in a subdir. Having to rename by hand is ugly. +## (We might end up doing this anyway to support other compilers.) +## - The DEPENDENCIES_OUTPUT environment variable makes gcc act like +## -MM, not -M (despite what the docs say). Also, it might not be +## supported by the other compilers which use the 'gcc' depmode. +## - Using -M directly means running the compiler twice (even worse +## than renaming). + if test -z "$gccflag"; then + gccflag=-MD, + fi + "$@" -Wp,"$gccflag$tmpdepfile" + stat=$? + if test $stat -ne 0; then + rm -f "$tmpdepfile" + exit $stat + fi + rm -f "$depfile" + echo "$object : \\" > "$depfile" + # The second -e expression handles DOS-style file names with drive + # letters. + sed -e 's/^[^:]*: / /' \ + -e 's/^['$alpha']:\/[^:]*: / /' < "$tmpdepfile" >> "$depfile" +## This next piece of magic avoids the "deleted header file" problem. +## The problem is that when a header file which appears in a .P file +## is deleted, the dependency causes make to die (because there is +## typically no way to rebuild the header). We avoid this by adding +## dummy dependencies for each header file. Too bad gcc doesn't do +## this for us directly. +## Some versions of gcc put a space before the ':'. On the theory +## that the space means something, we add a space to the output as +## well. hp depmode also adds that space, but also prefixes the VPATH +## to the object. Take care to not repeat it in the output. +## Some versions of the HPUX 10.20 sed can't process this invocation +## correctly. Breaking it into two sed invocations is a workaround. + tr ' ' "$nl" < "$tmpdepfile" \ + | sed -e 's/^\\$//' -e '/^$/d' -e "s|.*$object$||" -e '/:$/d' \ + | sed -e 's/$/ :/' >> "$depfile" + rm -f "$tmpdepfile" + ;; + +hp) + # This case exists only to let depend.m4 do its work. It works by + # looking at the text of this script. This case will never be run, + # since it is checked for above. + exit 1 + ;; + +sgi) + if test "$libtool" = yes; then + "$@" "-Wp,-MDupdate,$tmpdepfile" + else + "$@" -MDupdate "$tmpdepfile" + fi + stat=$? + if test $stat -ne 0; then + rm -f "$tmpdepfile" + exit $stat + fi + rm -f "$depfile" + + if test -f "$tmpdepfile"; then # yes, the sourcefile depend on other files + echo "$object : \\" > "$depfile" + # Clip off the initial element (the dependent). Don't try to be + # clever and replace this with sed code, as IRIX sed won't handle + # lines with more than a fixed number of characters (4096 in + # IRIX 6.2 sed, 8192 in IRIX 6.5). We also remove comment lines; + # the IRIX cc adds comments like '#:fec' to the end of the + # dependency line. + tr ' ' "$nl" < "$tmpdepfile" \ + | sed -e 's/^.*\.o://' -e 's/#.*$//' -e '/^$/ d' \ + | tr "$nl" ' ' >> "$depfile" + echo >> "$depfile" + # The second pass generates a dummy entry for each header file. + tr ' ' "$nl" < "$tmpdepfile" \ + | sed -e 's/^.*\.o://' -e 's/#.*$//' -e '/^$/ d' -e 's/$/:/' \ + >> "$depfile" + else + make_dummy_depfile + fi + rm -f "$tmpdepfile" + ;; + +xlc) + # This case exists only to let depend.m4 do its work. It works by + # looking at the text of this script. This case will never be run, + # since it is checked for above. + exit 1 + ;; + +aix) + # The C for AIX Compiler uses -M and outputs the dependencies + # in a .u file. In older versions, this file always lives in the + # current directory. Also, the AIX compiler puts '$object:' at the + # start of each line; $object doesn't have directory information. + # Version 6 uses the directory in both cases. + set_dir_from "$object" + set_base_from "$object" + if test "$libtool" = yes; then + tmpdepfile1=$dir$base.u + tmpdepfile2=$base.u + tmpdepfile3=$dir.libs/$base.u + "$@" -Wc,-M + else + tmpdepfile1=$dir$base.u + tmpdepfile2=$dir$base.u + tmpdepfile3=$dir$base.u + "$@" -M + fi + stat=$? + if test $stat -ne 0; then + rm -f "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3" + exit $stat + fi + + for tmpdepfile in "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3" + do + test -f "$tmpdepfile" && break + done + aix_post_process_depfile + ;; + +tcc) + # tcc (Tiny C Compiler) understand '-MD -MF file' since version 0.9.26 + # FIXME: That version still under development at the moment of writing. + # Make that this statement remains true also for stable, released + # versions. + # It will wrap lines (doesn't matter whether long or short) with a + # trailing '\', as in: + # + # foo.o : \ + # foo.c \ + # foo.h \ + # + # It will put a trailing '\' even on the last line, and will use leading + # spaces rather than leading tabs (at least since its commit 0394caf7 + # "Emit spaces for -MD"). + "$@" -MD -MF "$tmpdepfile" + stat=$? + if test $stat -ne 0; then + rm -f "$tmpdepfile" + exit $stat + fi + rm -f "$depfile" + # Each non-empty line is of the form 'foo.o : \' or ' dep.h \'. + # We have to change lines of the first kind to '$object: \'. + sed -e "s|.*:|$object :|" < "$tmpdepfile" > "$depfile" + # And for each line of the second kind, we have to emit a 'dep.h:' + # dummy dependency, to avoid the deleted-header problem. + sed -n -e 's|^ *\(.*\) *\\$|\1:|p' < "$tmpdepfile" >> "$depfile" + rm -f "$tmpdepfile" + ;; + +## The order of this option in the case statement is important, since the +## shell code in configure will try each of these formats in the order +## listed in this file. A plain '-MD' option would be understood by many +## compilers, so we must ensure this comes after the gcc and icc options. +pgcc) + # Portland's C compiler understands '-MD'. + # Will always output deps to 'file.d' where file is the root name of the + # source file under compilation, even if file resides in a subdirectory. + # The object file name does not affect the name of the '.d' file. + # pgcc 10.2 will output + # foo.o: sub/foo.c sub/foo.h + # and will wrap long lines using '\' : + # foo.o: sub/foo.c ... \ + # sub/foo.h ... \ + # ... + set_dir_from "$object" + # Use the source, not the object, to determine the base name, since + # that's sadly what pgcc will do too. + set_base_from "$source" + tmpdepfile=$base.d + + # For projects that build the same source file twice into different object + # files, the pgcc approach of using the *source* file root name can cause + # problems in parallel builds. Use a locking strategy to avoid stomping on + # the same $tmpdepfile. + lockdir=$base.d-lock + trap " + echo '$0: caught signal, cleaning up...' >&2 + rmdir '$lockdir' + exit 1 + " 1 2 13 15 + numtries=100 + i=$numtries + while test $i -gt 0; do + # mkdir is a portable test-and-set. + if mkdir "$lockdir" 2>/dev/null; then + # This process acquired the lock. + "$@" -MD + stat=$? + # Release the lock. + rmdir "$lockdir" + break + else + # If the lock is being held by a different process, wait + # until the winning process is done or we timeout. + while test -d "$lockdir" && test $i -gt 0; do + sleep 1 + i=`expr $i - 1` + done + fi + i=`expr $i - 1` + done + trap - 1 2 13 15 + if test $i -le 0; then + echo "$0: failed to acquire lock after $numtries attempts" >&2 + echo "$0: check lockdir '$lockdir'" >&2 + exit 1 + fi + + if test $stat -ne 0; then + rm -f "$tmpdepfile" + exit $stat + fi + rm -f "$depfile" + # Each line is of the form `foo.o: dependent.h', + # or `foo.o: dep1.h dep2.h \', or ` dep3.h dep4.h \'. + # Do two passes, one to just change these to + # `$object: dependent.h' and one to simply `dependent.h:'. + sed "s,^[^:]*:,$object :," < "$tmpdepfile" > "$depfile" + # Some versions of the HPUX 10.20 sed can't process this invocation + # correctly. Breaking it into two sed invocations is a workaround. + sed 's,^[^:]*: \(.*\)$,\1,;s/^\\$//;/^$/d;/:$/d' < "$tmpdepfile" \ + | sed -e 's/$/ :/' >> "$depfile" + rm -f "$tmpdepfile" + ;; + +hp2) + # The "hp" stanza above does not work with aCC (C++) and HP's ia64 + # compilers, which have integrated preprocessors. The correct option + # to use with these is +Maked; it writes dependencies to a file named + # 'foo.d', which lands next to the object file, wherever that + # happens to be. + # Much of this is similar to the tru64 case; see comments there. + set_dir_from "$object" + set_base_from "$object" + if test "$libtool" = yes; then + tmpdepfile1=$dir$base.d + tmpdepfile2=$dir.libs/$base.d + "$@" -Wc,+Maked + else + tmpdepfile1=$dir$base.d + tmpdepfile2=$dir$base.d + "$@" +Maked + fi + stat=$? + if test $stat -ne 0; then + rm -f "$tmpdepfile1" "$tmpdepfile2" + exit $stat + fi + + for tmpdepfile in "$tmpdepfile1" "$tmpdepfile2" + do + test -f "$tmpdepfile" && break + done + if test -f "$tmpdepfile"; then + sed -e "s,^.*\.[$lower]*:,$object:," "$tmpdepfile" > "$depfile" + # Add 'dependent.h:' lines. + sed -ne '2,${ + s/^ *// + s/ \\*$// + s/$/:/ + p + }' "$tmpdepfile" >> "$depfile" + else + make_dummy_depfile + fi + rm -f "$tmpdepfile" "$tmpdepfile2" + ;; + +tru64) + # The Tru64 compiler uses -MD to generate dependencies as a side + # effect. 'cc -MD -o foo.o ...' puts the dependencies into 'foo.o.d'. + # At least on Alpha/Redhat 6.1, Compaq CCC V6.2-504 seems to put + # dependencies in 'foo.d' instead, so we check for that too. + # Subdirectories are respected. + set_dir_from "$object" + set_base_from "$object" + + if test "$libtool" = yes; then + # Libtool generates 2 separate objects for the 2 libraries. These + # two compilations output dependencies in $dir.libs/$base.o.d and + # in $dir$base.o.d. We have to check for both files, because + # one of the two compilations can be disabled. We should prefer + # $dir$base.o.d over $dir.libs/$base.o.d because the latter is + # automatically cleaned when .libs/ is deleted, while ignoring + # the former would cause a distcleancheck panic. + tmpdepfile1=$dir$base.o.d # libtool 1.5 + tmpdepfile2=$dir.libs/$base.o.d # Likewise. + tmpdepfile3=$dir.libs/$base.d # Compaq CCC V6.2-504 + "$@" -Wc,-MD + else + tmpdepfile1=$dir$base.d + tmpdepfile2=$dir$base.d + tmpdepfile3=$dir$base.d + "$@" -MD + fi + + stat=$? + if test $stat -ne 0; then + rm -f "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3" + exit $stat + fi + + for tmpdepfile in "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3" + do + test -f "$tmpdepfile" && break + done + # Same post-processing that is required for AIX mode. + aix_post_process_depfile + ;; + +msvc7) + if test "$libtool" = yes; then + showIncludes=-Wc,-showIncludes + else + showIncludes=-showIncludes + fi + "$@" $showIncludes > "$tmpdepfile" + stat=$? + grep -v '^Note: including file: ' "$tmpdepfile" + if test $stat -ne 0; then + rm -f "$tmpdepfile" + exit $stat + fi + rm -f "$depfile" + echo "$object : \\" > "$depfile" + # The first sed program below extracts the file names and escapes + # backslashes for cygpath. The second sed program outputs the file + # name when reading, but also accumulates all include files in the + # hold buffer in order to output them again at the end. This only + # works with sed implementations that can handle large buffers. + sed < "$tmpdepfile" -n ' +/^Note: including file: *\(.*\)/ { + s//\1/ + s/\\/\\\\/g + p +}' | $cygpath_u | sort -u | sed -n ' +s/ /\\ /g +s/\(.*\)/'"$tab"'\1 \\/p +s/.\(.*\) \\/\1:/ +H +$ { + s/.*/'"$tab"'/ + G + p +}' >> "$depfile" + echo >> "$depfile" # make sure the fragment doesn't end with a backslash + rm -f "$tmpdepfile" + ;; + +msvc7msys) + # This case exists only to let depend.m4 do its work. It works by + # looking at the text of this script. This case will never be run, + # since it is checked for above. + exit 1 + ;; + +#nosideeffect) + # This comment above is used by automake to tell side-effect + # dependency tracking mechanisms from slower ones. + +dashmstdout) + # Important note: in order to support this mode, a compiler *must* + # always write the preprocessed file to stdout, regardless of -o. + "$@" || exit $? + + # Remove the call to Libtool. + if test "$libtool" = yes; then + while test "X$1" != 'X--mode=compile'; do + shift + done + shift + fi + + # Remove '-o $object'. + IFS=" " + for arg + do + case $arg in + -o) + shift + ;; + $object) + shift + ;; + *) + set fnord "$@" "$arg" + shift # fnord + shift # $arg + ;; + esac + done + + test -z "$dashmflag" && dashmflag=-M + # Require at least two characters before searching for ':' + # in the target name. This is to cope with DOS-style filenames: + # a dependency such as 'c:/foo/bar' could be seen as target 'c' otherwise. + "$@" $dashmflag | + sed "s|^[$tab ]*[^:$tab ][^:][^:]*:[$tab ]*|$object: |" > "$tmpdepfile" + rm -f "$depfile" + cat < "$tmpdepfile" > "$depfile" + # Some versions of the HPUX 10.20 sed can't process this sed invocation + # correctly. Breaking it into two sed invocations is a workaround. + tr ' ' "$nl" < "$tmpdepfile" \ + | sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' \ + | sed -e 's/$/ :/' >> "$depfile" + rm -f "$tmpdepfile" + ;; + +dashXmstdout) + # This case only exists to satisfy depend.m4. It is never actually + # run, as this mode is specially recognized in the preamble. + exit 1 + ;; + +makedepend) + "$@" || exit $? + # Remove any Libtool call + if test "$libtool" = yes; then + while test "X$1" != 'X--mode=compile'; do + shift + done + shift + fi + # X makedepend + shift + cleared=no eat=no + for arg + do + case $cleared in + no) + set ""; shift + cleared=yes ;; + esac + if test $eat = yes; then + eat=no + continue + fi + case "$arg" in + -D*|-I*) + set fnord "$@" "$arg"; shift ;; + # Strip any option that makedepend may not understand. Remove + # the object too, otherwise makedepend will parse it as a source file. + -arch) + eat=yes ;; + -*|$object) + ;; + *) + set fnord "$@" "$arg"; shift ;; + esac + done + obj_suffix=`echo "$object" | sed 's/^.*\././'` + touch "$tmpdepfile" + ${MAKEDEPEND-makedepend} -o"$obj_suffix" -f"$tmpdepfile" "$@" + rm -f "$depfile" + # makedepend may prepend the VPATH from the source file name to the object. + # No need to regex-escape $object, excess matching of '.' is harmless. + sed "s|^.*\($object *:\)|\1|" "$tmpdepfile" > "$depfile" + # Some versions of the HPUX 10.20 sed can't process the last invocation + # correctly. Breaking it into two sed invocations is a workaround. + sed '1,2d' "$tmpdepfile" \ + | tr ' ' "$nl" \ + | sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' \ + | sed -e 's/$/ :/' >> "$depfile" + rm -f "$tmpdepfile" "$tmpdepfile".bak + ;; + +cpp) + # Important note: in order to support this mode, a compiler *must* + # always write the preprocessed file to stdout. + "$@" || exit $? + + # Remove the call to Libtool. + if test "$libtool" = yes; then + while test "X$1" != 'X--mode=compile'; do + shift + done + shift + fi + + # Remove '-o $object'. + IFS=" " + for arg + do + case $arg in + -o) + shift + ;; + $object) + shift + ;; + *) + set fnord "$@" "$arg" + shift # fnord + shift # $arg + ;; + esac + done + + "$@" -E \ + | sed -n -e '/^# [0-9][0-9]* "\([^"]*\)".*/ s:: \1 \\:p' \ + -e '/^#line [0-9][0-9]* "\([^"]*\)".*/ s:: \1 \\:p' \ + | sed '$ s: \\$::' > "$tmpdepfile" + rm -f "$depfile" + echo "$object : \\" > "$depfile" + cat < "$tmpdepfile" >> "$depfile" + sed < "$tmpdepfile" '/^$/d;s/^ //;s/ \\$//;s/$/ :/' >> "$depfile" + rm -f "$tmpdepfile" + ;; + +msvisualcpp) + # Important note: in order to support this mode, a compiler *must* + # always write the preprocessed file to stdout. + "$@" || exit $? + + # Remove the call to Libtool. + if test "$libtool" = yes; then + while test "X$1" != 'X--mode=compile'; do + shift + done + shift + fi + + IFS=" " + for arg + do + case "$arg" in + -o) + shift + ;; + $object) + shift + ;; + "-Gm"|"/Gm"|"-Gi"|"/Gi"|"-ZI"|"/ZI") + set fnord "$@" + shift + shift + ;; + *) + set fnord "$@" "$arg" + shift + shift + ;; + esac + done + "$@" -E 2>/dev/null | + sed -n '/^#line [0-9][0-9]* "\([^"]*\)"/ s::\1:p' | $cygpath_u | sort -u > "$tmpdepfile" + rm -f "$depfile" + echo "$object : \\" > "$depfile" + sed < "$tmpdepfile" -n -e 's% %\\ %g' -e '/^\(.*\)$/ s::'"$tab"'\1 \\:p' >> "$depfile" + echo "$tab" >> "$depfile" + sed < "$tmpdepfile" -n -e 's% %\\ %g' -e '/^\(.*\)$/ s::\1\::p' >> "$depfile" + rm -f "$tmpdepfile" + ;; + +msvcmsys) + # This case exists only to let depend.m4 do its work. It works by + # looking at the text of this script. This case will never be run, + # since it is checked for above. + exit 1 + ;; + +none) + exec "$@" + ;; + +*) + echo "Unknown depmode $depmode" 1>&2 + exit 1 + ;; +esac + +exit 0 + +# Local Variables: +# mode: shell-script +# sh-indentation: 2 +# eval: (add-hook 'before-save-hook 'time-stamp) +# time-stamp-start: "scriptversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-time-zone: "UTC0" +# time-stamp-end: "; # UTC" +# End: diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev++.h b/deps/ngtcp2/ngtcp2/third-party/libev/ev++.h new file mode 100644 index 00000000000000..22dfcf58d942ef --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev++.h @@ -0,0 +1,818 @@ +/* + * libev simple C++ wrapper classes + * + * Copyright (c) 2007,2008,2010,2018,2020 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +#ifndef EVPP_H__ +#define EVPP_H__ + +#ifdef EV_H +# include EV_H +#else +# include "ev.h" +#endif + +#ifndef EV_USE_STDEXCEPT +# define EV_USE_STDEXCEPT 1 +#endif + +#if EV_USE_STDEXCEPT +# include +#endif + +namespace ev { + + typedef ev_tstamp tstamp; + + enum { + UNDEF = EV_UNDEF, + NONE = EV_NONE, + READ = EV_READ, + WRITE = EV_WRITE, +#if EV_COMPAT3 + TIMEOUT = EV_TIMEOUT, +#endif + TIMER = EV_TIMER, + PERIODIC = EV_PERIODIC, + SIGNAL = EV_SIGNAL, + CHILD = EV_CHILD, + STAT = EV_STAT, + IDLE = EV_IDLE, + CHECK = EV_CHECK, + PREPARE = EV_PREPARE, + FORK = EV_FORK, + ASYNC = EV_ASYNC, + EMBED = EV_EMBED, +# undef ERROR // some systems stupidly #define ERROR + ERROR = EV_ERROR + }; + + enum + { + AUTO = EVFLAG_AUTO, + NOENV = EVFLAG_NOENV, + FORKCHECK = EVFLAG_FORKCHECK, + + SELECT = EVBACKEND_SELECT, + POLL = EVBACKEND_POLL, + EPOLL = EVBACKEND_EPOLL, + KQUEUE = EVBACKEND_KQUEUE, + DEVPOLL = EVBACKEND_DEVPOLL, + PORT = EVBACKEND_PORT + }; + + enum + { +#if EV_COMPAT3 + NONBLOCK = EVLOOP_NONBLOCK, + ONESHOT = EVLOOP_ONESHOT, +#endif + NOWAIT = EVRUN_NOWAIT, + ONCE = EVRUN_ONCE + }; + + enum how_t + { + ONE = EVBREAK_ONE, + ALL = EVBREAK_ALL + }; + + struct bad_loop +#if EV_USE_STDEXCEPT + : std::exception +#endif + { +#if EV_USE_STDEXCEPT + const char *what () const EV_NOEXCEPT + { + return "libev event loop cannot be initialized, bad value of LIBEV_FLAGS?"; + } +#endif + }; + +#ifdef EV_AX +# undef EV_AX +#endif + +#ifdef EV_AX_ +# undef EV_AX_ +#endif + +#if EV_MULTIPLICITY +# define EV_AX raw_loop +# define EV_AX_ raw_loop, +#else +# define EV_AX +# define EV_AX_ +#endif + + struct loop_ref + { + loop_ref (EV_P) EV_NOEXCEPT +#if EV_MULTIPLICITY + : EV_AX (EV_A) +#endif + { + } + + bool operator == (const loop_ref &other) const EV_NOEXCEPT + { +#if EV_MULTIPLICITY + return EV_AX == other.EV_AX; +#else + return true; +#endif + } + + bool operator != (const loop_ref &other) const EV_NOEXCEPT + { +#if EV_MULTIPLICITY + return ! (*this == other); +#else + return false; +#endif + } + +#if EV_MULTIPLICITY + bool operator == (const EV_P) const EV_NOEXCEPT + { + return this->EV_AX == EV_A; + } + + bool operator != (const EV_P) const EV_NOEXCEPT + { + return ! (*this == EV_A); + } + + operator struct ev_loop * () const EV_NOEXCEPT + { + return EV_AX; + } + + operator const struct ev_loop * () const EV_NOEXCEPT + { + return EV_AX; + } + + bool is_default () const EV_NOEXCEPT + { + return EV_AX == ev_default_loop (0); + } +#endif + +#if EV_COMPAT3 + void loop (int flags = 0) + { + ev_run (EV_AX_ flags); + } + + void unloop (how_t how = ONE) EV_NOEXCEPT + { + ev_break (EV_AX_ how); + } +#endif + + void run (int flags = 0) + { + ev_run (EV_AX_ flags); + } + + void break_loop (how_t how = ONE) EV_NOEXCEPT + { + ev_break (EV_AX_ how); + } + + void post_fork () EV_NOEXCEPT + { + ev_loop_fork (EV_AX); + } + + unsigned int backend () const EV_NOEXCEPT + { + return ev_backend (EV_AX); + } + + tstamp now () const EV_NOEXCEPT + { + return ev_now (EV_AX); + } + + void ref () EV_NOEXCEPT + { + ev_ref (EV_AX); + } + + void unref () EV_NOEXCEPT + { + ev_unref (EV_AX); + } + +#if EV_FEATURE_API + unsigned int iteration () const EV_NOEXCEPT + { + return ev_iteration (EV_AX); + } + + unsigned int depth () const EV_NOEXCEPT + { + return ev_depth (EV_AX); + } + + void set_io_collect_interval (tstamp interval) EV_NOEXCEPT + { + ev_set_io_collect_interval (EV_AX_ interval); + } + + void set_timeout_collect_interval (tstamp interval) EV_NOEXCEPT + { + ev_set_timeout_collect_interval (EV_AX_ interval); + } +#endif + + // function callback + void once (int fd, int events, tstamp timeout, void (*cb)(int, void *), void *arg = 0) EV_NOEXCEPT + { + ev_once (EV_AX_ fd, events, timeout, cb, arg); + } + + // method callback + template + void once (int fd, int events, tstamp timeout, K *object) EV_NOEXCEPT + { + once (fd, events, timeout, method_thunk, object); + } + + // default method == operator () + template + void once (int fd, int events, tstamp timeout, K *object) EV_NOEXCEPT + { + once (fd, events, timeout, method_thunk, object); + } + + template + static void method_thunk (int revents, void *arg) + { + (static_cast(arg)->*method) + (revents); + } + + // no-argument method callback + template + void once (int fd, int events, tstamp timeout, K *object) EV_NOEXCEPT + { + once (fd, events, timeout, method_noargs_thunk, object); + } + + template + static void method_noargs_thunk (int revents, void *arg) + { + (static_cast(arg)->*method) + (); + } + + // simpler function callback + template + void once (int fd, int events, tstamp timeout) EV_NOEXCEPT + { + once (fd, events, timeout, simpler_func_thunk); + } + + template + static void simpler_func_thunk (int revents, void *arg) + { + (*cb) + (revents); + } + + // simplest function callback + template + void once (int fd, int events, tstamp timeout) EV_NOEXCEPT + { + once (fd, events, timeout, simplest_func_thunk); + } + + template + static void simplest_func_thunk (int revents, void *arg) + { + (*cb) + (); + } + + void feed_fd_event (int fd, int revents) EV_NOEXCEPT + { + ev_feed_fd_event (EV_AX_ fd, revents); + } + + void feed_signal_event (int signum) EV_NOEXCEPT + { + ev_feed_signal_event (EV_AX_ signum); + } + +#if EV_MULTIPLICITY + struct ev_loop* EV_AX; +#endif + + }; + +#if EV_MULTIPLICITY + struct dynamic_loop : loop_ref + { + + dynamic_loop (unsigned int flags = AUTO) + : loop_ref (ev_loop_new (flags)) + { + if (!EV_AX) + throw bad_loop (); + } + + ~dynamic_loop () EV_NOEXCEPT + { + ev_loop_destroy (EV_AX); + EV_AX = 0; + } + + private: + + dynamic_loop (const dynamic_loop &); + + dynamic_loop & operator= (const dynamic_loop &); + + }; +#endif + + struct default_loop : loop_ref + { + default_loop (unsigned int flags = AUTO) +#if EV_MULTIPLICITY + : loop_ref (ev_default_loop (flags)) +#endif + { + if ( +#if EV_MULTIPLICITY + !EV_AX +#else + !ev_default_loop (flags) +#endif + ) + throw bad_loop (); + } + + private: + default_loop (const default_loop &); + default_loop &operator = (const default_loop &); + }; + + inline loop_ref get_default_loop () EV_NOEXCEPT + { +#if EV_MULTIPLICITY + return ev_default_loop (0); +#else + return loop_ref (); +#endif + } + +#undef EV_AX +#undef EV_AX_ + +#undef EV_PX +#undef EV_PX_ +#if EV_MULTIPLICITY +# define EV_PX loop_ref EV_A +# define EV_PX_ loop_ref EV_A_ +#else +# define EV_PX +# define EV_PX_ +#endif + + template + struct base : ev_watcher + { + // scoped pause/unpause of a watcher + struct freeze_guard + { + watcher &w; + bool active; + + freeze_guard (watcher *self) EV_NOEXCEPT + : w (*self), active (w.is_active ()) + { + if (active) w.stop (); + } + + ~freeze_guard () + { + if (active) w.start (); + } + }; + + #if EV_MULTIPLICITY + EV_PX; + + // loop set + void set (EV_P) EV_NOEXCEPT + { + this->EV_A = EV_A; + } + #endif + + base (EV_PX) EV_NOEXCEPT + #if EV_MULTIPLICITY + : EV_A (EV_A) + #endif + { + ev_init (this, 0); + } + + void set_ (const void *data, void (*cb)(EV_P_ ev_watcher *w, int revents)) EV_NOEXCEPT + { + this->data = (void *)data; + ev_set_cb (static_cast(this), cb); + } + + // function callback + template + void set (void *data = 0) EV_NOEXCEPT + { + set_ (data, function_thunk); + } + + template + static void function_thunk (EV_P_ ev_watcher *w, int revents) + { + function + (*static_cast(w), revents); + } + + // method callback + template + void set (K *object) EV_NOEXCEPT + { + set_ (object, method_thunk); + } + + // default method == operator () + template + void set (K *object) EV_NOEXCEPT + { + set_ (object, method_thunk); + } + + template + static void method_thunk (EV_P_ ev_watcher *w, int revents) + { + (static_cast(w->data)->*method) + (*static_cast(w), revents); + } + + // no-argument callback + template + void set (K *object) EV_NOEXCEPT + { + set_ (object, method_noargs_thunk); + } + + template + static void method_noargs_thunk (EV_P_ ev_watcher *w, int revents) + { + (static_cast(w->data)->*method) + (); + } + + void operator ()(int events = EV_UNDEF) + { + return + ev_cb (static_cast(this)) + (static_cast(this), events); + } + + bool is_active () const EV_NOEXCEPT + { + return ev_is_active (static_cast(this)); + } + + bool is_pending () const EV_NOEXCEPT + { + return ev_is_pending (static_cast(this)); + } + + void feed_event (int revents) EV_NOEXCEPT + { + ev_feed_event (EV_A_ static_cast(this), revents); + } + }; + + inline tstamp now (EV_P) EV_NOEXCEPT + { + return ev_now (EV_A); + } + + inline void delay (tstamp interval) EV_NOEXCEPT + { + ev_sleep (interval); + } + + inline int version_major () EV_NOEXCEPT + { + return ev_version_major (); + } + + inline int version_minor () EV_NOEXCEPT + { + return ev_version_minor (); + } + + inline unsigned int supported_backends () EV_NOEXCEPT + { + return ev_supported_backends (); + } + + inline unsigned int recommended_backends () EV_NOEXCEPT + { + return ev_recommended_backends (); + } + + inline unsigned int embeddable_backends () EV_NOEXCEPT + { + return ev_embeddable_backends (); + } + + inline void set_allocator (void *(*cb)(void *ptr, long size) EV_NOEXCEPT) EV_NOEXCEPT + { + ev_set_allocator (cb); + } + + inline void set_syserr_cb (void (*cb)(const char *msg) EV_NOEXCEPT) EV_NOEXCEPT + { + ev_set_syserr_cb (cb); + } + + #if EV_MULTIPLICITY + #define EV_CONSTRUCT(cppstem,cstem) \ + (EV_PX = get_default_loop ()) EV_NOEXCEPT \ + : base (EV_A) \ + { \ + } + #else + #define EV_CONSTRUCT(cppstem,cstem) \ + () EV_NOEXCEPT \ + { \ + } + #endif + + /* using a template here would require quite a few more lines, + * so a macro solution was chosen */ + #define EV_BEGIN_WATCHER(cppstem,cstem) \ + \ + struct cppstem : base \ + { \ + void start () EV_NOEXCEPT \ + { \ + ev_ ## cstem ## _start (EV_A_ static_cast(this)); \ + } \ + \ + void stop () EV_NOEXCEPT \ + { \ + ev_ ## cstem ## _stop (EV_A_ static_cast(this)); \ + } \ + \ + cppstem EV_CONSTRUCT(cppstem,cstem) \ + \ + ~cppstem () EV_NOEXCEPT \ + { \ + stop (); \ + } \ + \ + using base::set; \ + \ + private: \ + \ + cppstem (const cppstem &o); \ + \ + cppstem &operator =(const cppstem &o); \ + \ + public: + + #define EV_END_WATCHER(cppstem,cstem) \ + }; + + EV_BEGIN_WATCHER (io, io) + void set (int fd, int events) EV_NOEXCEPT + { + freeze_guard freeze (this); + ev_io_set (static_cast(this), fd, events); + } + + void set (int events) EV_NOEXCEPT + { + freeze_guard freeze (this); + ev_io_modify (static_cast(this), events); + } + + void start (int fd, int events) EV_NOEXCEPT + { + set (fd, events); + start (); + } + EV_END_WATCHER (io, io) + + EV_BEGIN_WATCHER (timer, timer) + void set (ev_tstamp after, ev_tstamp repeat = 0.) EV_NOEXCEPT + { + freeze_guard freeze (this); + ev_timer_set (static_cast(this), after, repeat); + } + + void start (ev_tstamp after, ev_tstamp repeat = 0.) EV_NOEXCEPT + { + set (after, repeat); + start (); + } + + void again () EV_NOEXCEPT + { + ev_timer_again (EV_A_ static_cast(this)); + } + + ev_tstamp remaining () + { + return ev_timer_remaining (EV_A_ static_cast(this)); + } + EV_END_WATCHER (timer, timer) + + #if EV_PERIODIC_ENABLE + EV_BEGIN_WATCHER (periodic, periodic) + void set (ev_tstamp at, ev_tstamp interval = 0.) EV_NOEXCEPT + { + freeze_guard freeze (this); + ev_periodic_set (static_cast(this), at, interval, 0); + } + + void start (ev_tstamp at, ev_tstamp interval = 0.) EV_NOEXCEPT + { + set (at, interval); + start (); + } + + void again () EV_NOEXCEPT + { + ev_periodic_again (EV_A_ static_cast(this)); + } + EV_END_WATCHER (periodic, periodic) + #endif + + #if EV_SIGNAL_ENABLE + EV_BEGIN_WATCHER (sig, signal) + void set (int signum) EV_NOEXCEPT + { + freeze_guard freeze (this); + ev_signal_set (static_cast(this), signum); + } + + void start (int signum) EV_NOEXCEPT + { + set (signum); + start (); + } + EV_END_WATCHER (sig, signal) + #endif + + #if EV_CHILD_ENABLE + EV_BEGIN_WATCHER (child, child) + void set (int pid, int trace = 0) EV_NOEXCEPT + { + freeze_guard freeze (this); + ev_child_set (static_cast(this), pid, trace); + } + + void start (int pid, int trace = 0) EV_NOEXCEPT + { + set (pid, trace); + start (); + } + EV_END_WATCHER (child, child) + #endif + + #if EV_STAT_ENABLE + EV_BEGIN_WATCHER (stat, stat) + void set (const char *path, ev_tstamp interval = 0.) EV_NOEXCEPT + { + freeze_guard freeze (this); + ev_stat_set (static_cast(this), path, interval); + } + + void start (const char *path, ev_tstamp interval = 0.) EV_NOEXCEPT + { + stop (); + set (path, interval); + start (); + } + + void update () EV_NOEXCEPT + { + ev_stat_stat (EV_A_ static_cast(this)); + } + EV_END_WATCHER (stat, stat) + #endif + + #if EV_IDLE_ENABLE + EV_BEGIN_WATCHER (idle, idle) + void set () EV_NOEXCEPT { } + EV_END_WATCHER (idle, idle) + #endif + + #if EV_PREPARE_ENABLE + EV_BEGIN_WATCHER (prepare, prepare) + void set () EV_NOEXCEPT { } + EV_END_WATCHER (prepare, prepare) + #endif + + #if EV_CHECK_ENABLE + EV_BEGIN_WATCHER (check, check) + void set () EV_NOEXCEPT { } + EV_END_WATCHER (check, check) + #endif + + #if EV_EMBED_ENABLE + EV_BEGIN_WATCHER (embed, embed) + void set_embed (struct ev_loop *embedded_loop) EV_NOEXCEPT + { + freeze_guard freeze (this); + ev_embed_set (static_cast(this), embedded_loop); + } + + void start (struct ev_loop *embedded_loop) EV_NOEXCEPT + { + set (embedded_loop); + start (); + } + + void sweep () + { + ev_embed_sweep (EV_A_ static_cast(this)); + } + EV_END_WATCHER (embed, embed) + #endif + + #if EV_FORK_ENABLE + EV_BEGIN_WATCHER (fork, fork) + void set () EV_NOEXCEPT { } + EV_END_WATCHER (fork, fork) + #endif + + #if EV_ASYNC_ENABLE + EV_BEGIN_WATCHER (async, async) + void send () EV_NOEXCEPT + { + ev_async_send (EV_A_ static_cast(this)); + } + + bool async_pending () EV_NOEXCEPT + { + return ev_async_pending (static_cast(this)); + } + EV_END_WATCHER (async, async) + #endif + + #undef EV_PX + #undef EV_PX_ + #undef EV_CONSTRUCT + #undef EV_BEGIN_WATCHER + #undef EV_END_WATCHER +} + +#endif + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev.3 b/deps/ngtcp2/ngtcp2/third-party/libev/ev.3 new file mode 100644 index 00000000000000..af578f0bb4eebd --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev.3 @@ -0,0 +1,5819 @@ +.\" Automatically generated by Pod::Man 4.11 (Pod::Simple 3.35) +.\" +.\" Standard preamble: +.\" ======================================================================== +.de Sp \" Vertical space (when we can't use .PP) +.if t .sp .5v +.if n .sp +.. +.de Vb \" Begin verbatim text +.ft CW +.nf +.ne \\$1 +.. +.de Ve \" End verbatim text +.ft R +.fi +.. +.\" Set up some character translations and predefined strings. \*(-- will +.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left +.\" double quote, and \*(R" will give a right double quote. \*(C+ will +.\" give a nicer C++. Capital omega is used to do unbreakable dashes and +.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, +.\" nothing in troff, for use with C<>. +.tr \(*W- +.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' +.ie n \{\ +. ds -- \(*W- +. ds PI pi +. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch +. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch +. ds L" "" +. ds R" "" +. ds C` "" +. ds C' "" +'br\} +.el\{\ +. ds -- \|\(em\| +. ds PI \(*p +. ds L" `` +. ds R" '' +. ds C` +. ds C' +'br\} +.\" +.\" Escape single quotes in literal strings from groff's Unicode transform. +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\" +.\" If the F register is >0, we'll generate index entries on stderr for +.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index +.\" entries marked with X<> in POD. Of course, you'll have to process the +.\" output yourself in some meaningful fashion. +.\" +.\" Avoid warning from groff about undefined register 'F'. +.de IX +.. +.nr rF 0 +.if \n(.g .if rF .nr rF 1 +.if (\n(rF:(\n(.g==0)) \{\ +. if \nF \{\ +. de IX +. tm Index:\\$1\t\\n%\t"\\$2" +.. +. if !\nF==2 \{\ +. nr % 0 +. nr F 2 +. \} +. \} +.\} +.rr rF +.\" +.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). +.\" Fear. Run. Save yourself. No user-serviceable parts. +. \" fudge factors for nroff and troff +.if n \{\ +. ds #H 0 +. ds #V .8m +. ds #F .3m +. ds #[ \f1 +. ds #] \fP +.\} +.if t \{\ +. ds #H ((1u-(\\\\n(.fu%2u))*.13m) +. ds #V .6m +. ds #F 0 +. ds #[ \& +. ds #] \& +.\} +. \" simple accents for nroff and troff +.if n \{\ +. ds ' \& +. ds ` \& +. ds ^ \& +. ds , \& +. ds ~ ~ +. ds / +.\} +.if t \{\ +. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" +. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' +. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' +. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' +. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' +. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' +.\} +. \" troff and (daisy-wheel) nroff accents +.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' +.ds 8 \h'\*(#H'\(*b\h'-\*(#H' +.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] +.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' +.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' +.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] +.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] +.ds ae a\h'-(\w'a'u*4/10)'e +.ds Ae A\h'-(\w'A'u*4/10)'E +. \" corrections for vroff +.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' +.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' +. \" for low resolution devices (crt and lpr) +.if \n(.H>23 .if \n(.V>19 \ +\{\ +. ds : e +. ds 8 ss +. ds o a +. ds d- d\h'-1'\(ga +. ds D- D\h'-1'\(hy +. ds th \o'bp' +. ds Th \o'LP' +. ds ae ae +. ds Ae AE +.\} +.rm #[ #] #H #V #F C +.\" ======================================================================== +.\" +.IX Title "LIBEV 3" +.TH LIBEV 3 "2020-03-12" "libev-4.31" "libev - high performance full featured event loop" +.\" For nroff, turn off justification. Always turn off hyphenation; it makes +.\" way too many mistakes in technical documents. +.if n .ad l +.nh +.SH "NAME" +libev \- a high performance full\-featured event loop written in C +.SH "SYNOPSIS" +.IX Header "SYNOPSIS" +.Vb 1 +\& #include +.Ve +.SS "\s-1EXAMPLE PROGRAM\s0" +.IX Subsection "EXAMPLE PROGRAM" +.Vb 2 +\& // a single header file is required +\& #include +\& +\& #include // for puts +\& +\& // every watcher type has its own typedef\*(Aqd struct +\& // with the name ev_TYPE +\& ev_io stdin_watcher; +\& ev_timer timeout_watcher; +\& +\& // all watcher callbacks have a similar signature +\& // this callback is called when data is readable on stdin +\& static void +\& stdin_cb (EV_P_ ev_io *w, int revents) +\& { +\& puts ("stdin ready"); +\& // for one\-shot events, one must manually stop the watcher +\& // with its corresponding stop function. +\& ev_io_stop (EV_A_ w); +\& +\& // this causes all nested ev_run\*(Aqs to stop iterating +\& ev_break (EV_A_ EVBREAK_ALL); +\& } +\& +\& // another callback, this time for a time\-out +\& static void +\& timeout_cb (EV_P_ ev_timer *w, int revents) +\& { +\& puts ("timeout"); +\& // this causes the innermost ev_run to stop iterating +\& ev_break (EV_A_ EVBREAK_ONE); +\& } +\& +\& int +\& main (void) +\& { +\& // use the default event loop unless you have special needs +\& struct ev_loop *loop = EV_DEFAULT; +\& +\& // initialise an io watcher, then start it +\& // this one will watch for stdin to become readable +\& ev_io_init (&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ); +\& ev_io_start (loop, &stdin_watcher); +\& +\& // initialise a timer watcher, then start it +\& // simple non\-repeating 5.5 second timeout +\& ev_timer_init (&timeout_watcher, timeout_cb, 5.5, 0.); +\& ev_timer_start (loop, &timeout_watcher); +\& +\& // now wait for events to arrive +\& ev_run (loop, 0); +\& +\& // break was called, so exit +\& return 0; +\& } +.Ve +.SH "ABOUT THIS DOCUMENT" +.IX Header "ABOUT THIS DOCUMENT" +This document documents the libev software package. +.PP +The newest version of this document is also available as an html-formatted +web page you might find easier to navigate when reading it for the first +time: . +.PP +While this document tries to be as complete as possible in documenting +libev, its usage and the rationale behind its design, it is not a tutorial +on event-based programming, nor will it introduce event-based programming +with libev. +.PP +Familiarity with event based programming techniques in general is assumed +throughout this document. +.SH "WHAT TO READ WHEN IN A HURRY" +.IX Header "WHAT TO READ WHEN IN A HURRY" +This manual tries to be very detailed, but unfortunately, this also makes +it very long. If you just want to know the basics of libev, I suggest +reading \*(L"\s-1ANATOMY OF A WATCHER\*(R"\s0, then the \*(L"\s-1EXAMPLE PROGRAM\*(R"\s0 above and +look up the missing functions in \*(L"\s-1GLOBAL FUNCTIONS\*(R"\s0 and the \f(CW\*(C`ev_io\*(C'\fR and +\&\f(CW\*(C`ev_timer\*(C'\fR sections in \*(L"\s-1WATCHER TYPES\*(R"\s0. +.SH "ABOUT LIBEV" +.IX Header "ABOUT LIBEV" +Libev is an event loop: you register interest in certain events (such as a +file descriptor being readable or a timeout occurring), and it will manage +these event sources and provide your program with events. +.PP +To do this, it must take more or less complete control over your process +(or thread) by executing the \fIevent loop\fR handler, and will then +communicate events via a callback mechanism. +.PP +You register interest in certain events by registering so-called \fIevent +watchers\fR, which are relatively small C structures you initialise with the +details of the event, and then hand it over to libev by \fIstarting\fR the +watcher. +.SS "\s-1FEATURES\s0" +.IX Subsection "FEATURES" +Libev supports \f(CW\*(C`select\*(C'\fR, \f(CW\*(C`poll\*(C'\fR, the Linux-specific aio and \f(CW\*(C`epoll\*(C'\fR +interfaces, the BSD-specific \f(CW\*(C`kqueue\*(C'\fR and the Solaris-specific event port +mechanisms for file descriptor events (\f(CW\*(C`ev_io\*(C'\fR), the Linux \f(CW\*(C`inotify\*(C'\fR +interface (for \f(CW\*(C`ev_stat\*(C'\fR), Linux eventfd/signalfd (for faster and cleaner +inter-thread wakeup (\f(CW\*(C`ev_async\*(C'\fR)/signal handling (\f(CW\*(C`ev_signal\*(C'\fR)) relative +timers (\f(CW\*(C`ev_timer\*(C'\fR), absolute timers with customised rescheduling +(\f(CW\*(C`ev_periodic\*(C'\fR), synchronous signals (\f(CW\*(C`ev_signal\*(C'\fR), process status +change events (\f(CW\*(C`ev_child\*(C'\fR), and event watchers dealing with the event +loop mechanism itself (\f(CW\*(C`ev_idle\*(C'\fR, \f(CW\*(C`ev_embed\*(C'\fR, \f(CW\*(C`ev_prepare\*(C'\fR and +\&\f(CW\*(C`ev_check\*(C'\fR watchers) as well as file watchers (\f(CW\*(C`ev_stat\*(C'\fR) and even +limited support for fork events (\f(CW\*(C`ev_fork\*(C'\fR). +.PP +It also is quite fast (see this +benchmark comparing it to libevent +for example). +.SS "\s-1CONVENTIONS\s0" +.IX Subsection "CONVENTIONS" +Libev is very configurable. In this manual the default (and most common) +configuration will be described, which supports multiple event loops. For +more info about various configuration options please have a look at +\&\fB\s-1EMBED\s0\fR section in this manual. If libev was configured without support +for multiple event loops, then all functions taking an initial argument of +name \f(CW\*(C`loop\*(C'\fR (which is always of type \f(CW\*(C`struct ev_loop *\*(C'\fR) will not have +this argument. +.SS "\s-1TIME REPRESENTATION\s0" +.IX Subsection "TIME REPRESENTATION" +Libev represents time as a single floating point number, representing +the (fractional) number of seconds since the (\s-1POSIX\s0) epoch (in practice +somewhere near the beginning of 1970, details are complicated, don't +ask). This type is called \f(CW\*(C`ev_tstamp\*(C'\fR, which is what you should use +too. It usually aliases to the \f(CW\*(C`double\*(C'\fR type in C. When you need to do +any calculations on it, you should treat it as some floating point value. +.PP +Unlike the name component \f(CW\*(C`stamp\*(C'\fR might indicate, it is also used for +time differences (e.g. delays) throughout libev. +.SH "ERROR HANDLING" +.IX Header "ERROR HANDLING" +Libev knows three classes of errors: operating system errors, usage errors +and internal errors (bugs). +.PP +When libev catches an operating system error it cannot handle (for example +a system call indicating a condition libev cannot fix), it calls the callback +set via \f(CW\*(C`ev_set_syserr_cb\*(C'\fR, which is supposed to fix the problem or +abort. The default is to print a diagnostic message and to call \f(CW\*(C`abort +()\*(C'\fR. +.PP +When libev detects a usage error such as a negative timer interval, then +it will print a diagnostic message and abort (via the \f(CW\*(C`assert\*(C'\fR mechanism, +so \f(CW\*(C`NDEBUG\*(C'\fR will disable this checking): these are programming errors in +the libev caller and need to be fixed there. +.PP +Via the \f(CW\*(C`EV_FREQUENT\*(C'\fR macro you can compile in and/or enable extensive +consistency checking code inside libev that can be used to check for +internal inconsistencies, suually caused by application bugs. +.PP +Libev also has a few internal error-checking \f(CW\*(C`assert\*(C'\fRions. These do not +trigger under normal circumstances, as they indicate either a bug in libev +or worse. +.SH "GLOBAL FUNCTIONS" +.IX Header "GLOBAL FUNCTIONS" +These functions can be called anytime, even before initialising the +library in any way. +.IP "ev_tstamp ev_time ()" 4 +.IX Item "ev_tstamp ev_time ()" +Returns the current time as libev would use it. Please note that the +\&\f(CW\*(C`ev_now\*(C'\fR function is usually faster and also often returns the timestamp +you actually want to know. Also interesting is the combination of +\&\f(CW\*(C`ev_now_update\*(C'\fR and \f(CW\*(C`ev_now\*(C'\fR. +.IP "ev_sleep (ev_tstamp interval)" 4 +.IX Item "ev_sleep (ev_tstamp interval)" +Sleep for the given interval: The current thread will be blocked +until either it is interrupted or the given time interval has +passed (approximately \- it might return a bit earlier even if not +interrupted). Returns immediately if \f(CW\*(C`interval <= 0\*(C'\fR. +.Sp +Basically this is a sub-second-resolution \f(CW\*(C`sleep ()\*(C'\fR. +.Sp +The range of the \f(CW\*(C`interval\*(C'\fR is limited \- libev only guarantees to work +with sleep times of up to one day (\f(CW\*(C`interval <= 86400\*(C'\fR). +.IP "int ev_version_major ()" 4 +.IX Item "int ev_version_major ()" +.PD 0 +.IP "int ev_version_minor ()" 4 +.IX Item "int ev_version_minor ()" +.PD +You can find out the major and minor \s-1ABI\s0 version numbers of the library +you linked against by calling the functions \f(CW\*(C`ev_version_major\*(C'\fR and +\&\f(CW\*(C`ev_version_minor\*(C'\fR. If you want, you can compare against the global +symbols \f(CW\*(C`EV_VERSION_MAJOR\*(C'\fR and \f(CW\*(C`EV_VERSION_MINOR\*(C'\fR, which specify the +version of the library your program was compiled against. +.Sp +These version numbers refer to the \s-1ABI\s0 version of the library, not the +release version. +.Sp +Usually, it's a good idea to terminate if the major versions mismatch, +as this indicates an incompatible change. Minor versions are usually +compatible to older versions, so a larger minor version alone is usually +not a problem. +.Sp +Example: Make sure we haven't accidentally been linked against the wrong +version (note, however, that this will not detect other \s-1ABI\s0 mismatches, +such as \s-1LFS\s0 or reentrancy). +.Sp +.Vb 3 +\& assert (("libev version mismatch", +\& ev_version_major () == EV_VERSION_MAJOR +\& && ev_version_minor () >= EV_VERSION_MINOR)); +.Ve +.IP "unsigned int ev_supported_backends ()" 4 +.IX Item "unsigned int ev_supported_backends ()" +Return the set of all backends (i.e. their corresponding \f(CW\*(C`EV_BACKEND_*\*(C'\fR +value) compiled into this binary of libev (independent of their +availability on the system you are running on). See \f(CW\*(C`ev_default_loop\*(C'\fR for +a description of the set values. +.Sp +Example: make sure we have the epoll method, because yeah this is cool and +a must have and can we have a torrent of it please!!!11 +.Sp +.Vb 2 +\& assert (("sorry, no epoll, no sex", +\& ev_supported_backends () & EVBACKEND_EPOLL)); +.Ve +.IP "unsigned int ev_recommended_backends ()" 4 +.IX Item "unsigned int ev_recommended_backends ()" +Return the set of all backends compiled into this binary of libev and +also recommended for this platform, meaning it will work for most file +descriptor types. This set is often smaller than the one returned by +\&\f(CW\*(C`ev_supported_backends\*(C'\fR, as for example kqueue is broken on most BSDs +and will not be auto-detected unless you explicitly request it (assuming +you know what you are doing). This is the set of backends that libev will +probe for if you specify no backends explicitly. +.IP "unsigned int ev_embeddable_backends ()" 4 +.IX Item "unsigned int ev_embeddable_backends ()" +Returns the set of backends that are embeddable in other event loops. This +value is platform-specific but can include backends not available on the +current system. To find which embeddable backends might be supported on +the current system, you would need to look at \f(CW\*(C`ev_embeddable_backends () +& ev_supported_backends ()\*(C'\fR, likewise for recommended ones. +.Sp +See the description of \f(CW\*(C`ev_embed\*(C'\fR watchers for more info. +.IP "ev_set_allocator (void *(*cb)(void *ptr, long size) throw ())" 4 +.IX Item "ev_set_allocator (void *(*cb)(void *ptr, long size) throw ())" +Sets the allocation function to use (the prototype is similar \- the +semantics are identical to the \f(CW\*(C`realloc\*(C'\fR C89/SuS/POSIX function). It is +used to allocate and free memory (no surprises here). If it returns zero +when memory needs to be allocated (\f(CW\*(C`size != 0\*(C'\fR), the library might abort +or take some potentially destructive action. +.Sp +Since some systems (at least OpenBSD and Darwin) fail to implement +correct \f(CW\*(C`realloc\*(C'\fR semantics, libev will use a wrapper around the system +\&\f(CW\*(C`realloc\*(C'\fR and \f(CW\*(C`free\*(C'\fR functions by default. +.Sp +You could override this function in high-availability programs to, say, +free some memory if it cannot allocate memory, to use a special allocator, +or even to sleep a while and retry until some memory is available. +.Sp +Example: The following is the \f(CW\*(C`realloc\*(C'\fR function that libev itself uses +which should work with \f(CW\*(C`realloc\*(C'\fR and \f(CW\*(C`free\*(C'\fR functions of all kinds and +is probably a good basis for your own implementation. +.Sp +.Vb 5 +\& static void * +\& ev_realloc_emul (void *ptr, long size) EV_NOEXCEPT +\& { +\& if (size) +\& return realloc (ptr, size); +\& +\& free (ptr); +\& return 0; +\& } +.Ve +.Sp +Example: Replace the libev allocator with one that waits a bit and then +retries. +.Sp +.Vb 8 +\& static void * +\& persistent_realloc (void *ptr, size_t size) +\& { +\& if (!size) +\& { +\& free (ptr); +\& return 0; +\& } +\& +\& for (;;) +\& { +\& void *newptr = realloc (ptr, size); +\& +\& if (newptr) +\& return newptr; +\& +\& sleep (60); +\& } +\& } +\& +\& ... +\& ev_set_allocator (persistent_realloc); +.Ve +.IP "ev_set_syserr_cb (void (*cb)(const char *msg) throw ())" 4 +.IX Item "ev_set_syserr_cb (void (*cb)(const char *msg) throw ())" +Set the callback function to call on a retryable system call error (such +as failed select, poll, epoll_wait). The message is a printable string +indicating the system call or subsystem causing the problem. If this +callback is set, then libev will expect it to remedy the situation, no +matter what, when it returns. That is, libev will generally retry the +requested operation, or, if the condition doesn't go away, do bad stuff +(such as abort). +.Sp +Example: This is basically the same thing that libev does internally, too. +.Sp +.Vb 6 +\& static void +\& fatal_error (const char *msg) +\& { +\& perror (msg); +\& abort (); +\& } +\& +\& ... +\& ev_set_syserr_cb (fatal_error); +.Ve +.IP "ev_feed_signal (int signum)" 4 +.IX Item "ev_feed_signal (int signum)" +This function can be used to \*(L"simulate\*(R" a signal receive. It is completely +safe to call this function at any time, from any context, including signal +handlers or random threads. +.Sp +Its main use is to customise signal handling in your process, especially +in the presence of threads. For example, you could block signals +by default in all threads (and specifying \f(CW\*(C`EVFLAG_NOSIGMASK\*(C'\fR when +creating any loops), and in one thread, use \f(CW\*(C`sigwait\*(C'\fR or any other +mechanism to wait for signals, then \*(L"deliver\*(R" them to libev by calling +\&\f(CW\*(C`ev_feed_signal\*(C'\fR. +.SH "FUNCTIONS CONTROLLING EVENT LOOPS" +.IX Header "FUNCTIONS CONTROLLING EVENT LOOPS" +An event loop is described by a \f(CW\*(C`struct ev_loop *\*(C'\fR (the \f(CW\*(C`struct\*(C'\fR is +\&\fInot\fR optional in this case unless libev 3 compatibility is disabled, as +libev 3 had an \f(CW\*(C`ev_loop\*(C'\fR function colliding with the struct name). +.PP +The library knows two types of such loops, the \fIdefault\fR loop, which +supports child process events, and dynamically created event loops which +do not. +.IP "struct ev_loop *ev_default_loop (unsigned int flags)" 4 +.IX Item "struct ev_loop *ev_default_loop (unsigned int flags)" +This returns the \*(L"default\*(R" event loop object, which is what you should +normally use when you just need \*(L"the event loop\*(R". Event loop objects and +the \f(CW\*(C`flags\*(C'\fR parameter are described in more detail in the entry for +\&\f(CW\*(C`ev_loop_new\*(C'\fR. +.Sp +If the default loop is already initialised then this function simply +returns it (and ignores the flags. If that is troubling you, check +\&\f(CW\*(C`ev_backend ()\*(C'\fR afterwards). Otherwise it will create it with the given +flags, which should almost always be \f(CW0\fR, unless the caller is also the +one calling \f(CW\*(C`ev_run\*(C'\fR or otherwise qualifies as \*(L"the main program\*(R". +.Sp +If you don't know what event loop to use, use the one returned from this +function (or via the \f(CW\*(C`EV_DEFAULT\*(C'\fR macro). +.Sp +Note that this function is \fInot\fR thread-safe, so if you want to use it +from multiple threads, you have to employ some kind of mutex (note also +that this case is unlikely, as loops cannot be shared easily between +threads anyway). +.Sp +The default loop is the only loop that can handle \f(CW\*(C`ev_child\*(C'\fR watchers, +and to do this, it always registers a handler for \f(CW\*(C`SIGCHLD\*(C'\fR. If this is +a problem for your application you can either create a dynamic loop with +\&\f(CW\*(C`ev_loop_new\*(C'\fR which doesn't do that, or you can simply overwrite the +\&\f(CW\*(C`SIGCHLD\*(C'\fR signal handler \fIafter\fR calling \f(CW\*(C`ev_default_init\*(C'\fR. +.Sp +Example: This is the most typical usage. +.Sp +.Vb 2 +\& if (!ev_default_loop (0)) +\& fatal ("could not initialise libev, bad $LIBEV_FLAGS in environment?"); +.Ve +.Sp +Example: Restrict libev to the select and poll backends, and do not allow +environment settings to be taken into account: +.Sp +.Vb 1 +\& ev_default_loop (EVBACKEND_POLL | EVBACKEND_SELECT | EVFLAG_NOENV); +.Ve +.IP "struct ev_loop *ev_loop_new (unsigned int flags)" 4 +.IX Item "struct ev_loop *ev_loop_new (unsigned int flags)" +This will create and initialise a new event loop object. If the loop +could not be initialised, returns false. +.Sp +This function is thread-safe, and one common way to use libev with +threads is indeed to create one loop per thread, and using the default +loop in the \*(L"main\*(R" or \*(L"initial\*(R" thread. +.Sp +The flags argument can be used to specify special behaviour or specific +backends to use, and is usually specified as \f(CW0\fR (or \f(CW\*(C`EVFLAG_AUTO\*(C'\fR). +.Sp +The following flags are supported: +.RS 4 +.ie n .IP """EVFLAG_AUTO""" 4 +.el .IP "\f(CWEVFLAG_AUTO\fR" 4 +.IX Item "EVFLAG_AUTO" +The default flags value. Use this if you have no clue (it's the right +thing, believe me). +.ie n .IP """EVFLAG_NOENV""" 4 +.el .IP "\f(CWEVFLAG_NOENV\fR" 4 +.IX Item "EVFLAG_NOENV" +If this flag bit is or'ed into the flag value (or the program runs setuid +or setgid) then libev will \fInot\fR look at the environment variable +\&\f(CW\*(C`LIBEV_FLAGS\*(C'\fR. Otherwise (the default), this environment variable will +override the flags completely if it is found in the environment. This is +useful to try out specific backends to test their performance, to work +around bugs, or to make libev threadsafe (accessing environment variables +cannot be done in a threadsafe way, but usually it works if no other +thread modifies them). +.ie n .IP """EVFLAG_FORKCHECK""" 4 +.el .IP "\f(CWEVFLAG_FORKCHECK\fR" 4 +.IX Item "EVFLAG_FORKCHECK" +Instead of calling \f(CW\*(C`ev_loop_fork\*(C'\fR manually after a fork, you can also +make libev check for a fork in each iteration by enabling this flag. +.Sp +This works by calling \f(CW\*(C`getpid ()\*(C'\fR on every iteration of the loop, +and thus this might slow down your event loop if you do a lot of loop +iterations and little real work, but is usually not noticeable (on my +GNU/Linux system for example, \f(CW\*(C`getpid\*(C'\fR is actually a simple 5\-insn +sequence without a system call and thus \fIvery\fR fast, but my GNU/Linux +system also has \f(CW\*(C`pthread_atfork\*(C'\fR which is even faster). (Update: glibc +versions 2.25 apparently removed the \f(CW\*(C`getpid\*(C'\fR optimisation again). +.Sp +The big advantage of this flag is that you can forget about fork (and +forget about forgetting to tell libev about forking, although you still +have to ignore \f(CW\*(C`SIGPIPE\*(C'\fR) when you use this flag. +.Sp +This flag setting cannot be overridden or specified in the \f(CW\*(C`LIBEV_FLAGS\*(C'\fR +environment variable. +.ie n .IP """EVFLAG_NOINOTIFY""" 4 +.el .IP "\f(CWEVFLAG_NOINOTIFY\fR" 4 +.IX Item "EVFLAG_NOINOTIFY" +When this flag is specified, then libev will not attempt to use the +\&\fIinotify\fR \s-1API\s0 for its \f(CW\*(C`ev_stat\*(C'\fR watchers. Apart from debugging and +testing, this flag can be useful to conserve inotify file descriptors, as +otherwise each loop using \f(CW\*(C`ev_stat\*(C'\fR watchers consumes one inotify handle. +.ie n .IP """EVFLAG_SIGNALFD""" 4 +.el .IP "\f(CWEVFLAG_SIGNALFD\fR" 4 +.IX Item "EVFLAG_SIGNALFD" +When this flag is specified, then libev will attempt to use the +\&\fIsignalfd\fR \s-1API\s0 for its \f(CW\*(C`ev_signal\*(C'\fR (and \f(CW\*(C`ev_child\*(C'\fR) watchers. This \s-1API\s0 +delivers signals synchronously, which makes it both faster and might make +it possible to get the queued signal data. It can also simplify signal +handling with threads, as long as you properly block signals in your +threads that are not interested in handling them. +.Sp +Signalfd will not be used by default as this changes your signal mask, and +there are a lot of shoddy libraries and programs (glib's threadpool for +example) that can't properly initialise their signal masks. +.ie n .IP """EVFLAG_NOSIGMASK""" 4 +.el .IP "\f(CWEVFLAG_NOSIGMASK\fR" 4 +.IX Item "EVFLAG_NOSIGMASK" +When this flag is specified, then libev will avoid to modify the signal +mask. Specifically, this means you have to make sure signals are unblocked +when you want to receive them. +.Sp +This behaviour is useful when you want to do your own signal handling, or +want to handle signals only in specific threads and want to avoid libev +unblocking the signals. +.Sp +It's also required by \s-1POSIX\s0 in a threaded program, as libev calls +\&\f(CW\*(C`sigprocmask\*(C'\fR, whose behaviour is officially unspecified. +.ie n .IP """EVFLAG_NOTIMERFD""" 4 +.el .IP "\f(CWEVFLAG_NOTIMERFD\fR" 4 +.IX Item "EVFLAG_NOTIMERFD" +When this flag is specified, the libev will avoid using a \f(CW\*(C`timerfd\*(C'\fR to +detect time jumps. It will still be able to detect time jumps, but takes +longer and has a lower accuracy in doing so, but saves a file descriptor +per loop. +.Sp +The current implementation only tries to use a \f(CW\*(C`timerfd\*(C'\fR when the first +\&\f(CW\*(C`ev_periodic\*(C'\fR watcher is started and falls back on other methods if it +cannot be created, but this behaviour might change in the future. +.ie n .IP """EVBACKEND_SELECT"" (value 1, portable select backend)" 4 +.el .IP "\f(CWEVBACKEND_SELECT\fR (value 1, portable select backend)" 4 +.IX Item "EVBACKEND_SELECT (value 1, portable select backend)" +This is your standard \fBselect\fR\|(2) backend. Not \fIcompletely\fR standard, as +libev tries to roll its own fd_set with no limits on the number of fds, +but if that fails, expect a fairly low limit on the number of fds when +using this backend. It doesn't scale too well (O(highest_fd)), but its +usually the fastest backend for a low number of (low-numbered :) fds. +.Sp +To get good performance out of this backend you need a high amount of +parallelism (most of the file descriptors should be busy). If you are +writing a server, you should \f(CW\*(C`accept ()\*(C'\fR in a loop to accept as many +connections as possible during one iteration. You might also want to have +a look at \f(CW\*(C`ev_set_io_collect_interval ()\*(C'\fR to increase the amount of +readiness notifications you get per iteration. +.Sp +This backend maps \f(CW\*(C`EV_READ\*(C'\fR to the \f(CW\*(C`readfds\*(C'\fR set and \f(CW\*(C`EV_WRITE\*(C'\fR to the +\&\f(CW\*(C`writefds\*(C'\fR set (and to work around Microsoft Windows bugs, also onto the +\&\f(CW\*(C`exceptfds\*(C'\fR set on that platform). +.ie n .IP """EVBACKEND_POLL"" (value 2, poll backend, available everywhere except on windows)" 4 +.el .IP "\f(CWEVBACKEND_POLL\fR (value 2, poll backend, available everywhere except on windows)" 4 +.IX Item "EVBACKEND_POLL (value 2, poll backend, available everywhere except on windows)" +And this is your standard \fBpoll\fR\|(2) backend. It's more complicated +than select, but handles sparse fds better and has no artificial +limit on the number of fds you can use (except it will slow down +considerably with a lot of inactive fds). It scales similarly to select, +i.e. O(total_fds). See the entry for \f(CW\*(C`EVBACKEND_SELECT\*(C'\fR, above, for +performance tips. +.Sp +This backend maps \f(CW\*(C`EV_READ\*(C'\fR to \f(CW\*(C`POLLIN | POLLERR | POLLHUP\*(C'\fR, and +\&\f(CW\*(C`EV_WRITE\*(C'\fR to \f(CW\*(C`POLLOUT | POLLERR | POLLHUP\*(C'\fR. +.ie n .IP """EVBACKEND_EPOLL"" (value 4, Linux)" 4 +.el .IP "\f(CWEVBACKEND_EPOLL\fR (value 4, Linux)" 4 +.IX Item "EVBACKEND_EPOLL (value 4, Linux)" +Use the Linux-specific \fBepoll\fR\|(7) interface (for both pre\- and post\-2.6.9 +kernels). +.Sp +For few fds, this backend is a bit little slower than poll and select, but +it scales phenomenally better. While poll and select usually scale like +O(total_fds) where total_fds is the total number of fds (or the highest +fd), epoll scales either O(1) or O(active_fds). +.Sp +The epoll mechanism deserves honorable mention as the most misdesigned +of the more advanced event mechanisms: mere annoyances include silently +dropping file descriptors, requiring a system call per change per file +descriptor (and unnecessary guessing of parameters), problems with dup, +returning before the timeout value, resulting in additional iterations +(and only giving 5ms accuracy while select on the same platform gives +0.1ms) and so on. The biggest issue is fork races, however \- if a program +forks then \fIboth\fR parent and child process have to recreate the epoll +set, which can take considerable time (one syscall per file descriptor) +and is of course hard to detect. +.Sp +Epoll is also notoriously buggy \- embedding epoll fds \fIshould\fR work, +but of course \fIdoesn't\fR, and epoll just loves to report events for +totally \fIdifferent\fR file descriptors (even already closed ones, so +one cannot even remove them from the set) than registered in the set +(especially on \s-1SMP\s0 systems). Libev tries to counter these spurious +notifications by employing an additional generation counter and comparing +that against the events to filter out spurious ones, recreating the set +when required. Epoll also erroneously rounds down timeouts, but gives you +no way to know when and by how much, so sometimes you have to busy-wait +because epoll returns immediately despite a nonzero timeout. And last +not least, it also refuses to work with some file descriptors which work +perfectly fine with \f(CW\*(C`select\*(C'\fR (files, many character devices...). +.Sp +Epoll is truly the train wreck among event poll mechanisms, a frankenpoll, +cobbled together in a hurry, no thought to design or interaction with +others. Oh, the pain, will it ever stop... +.Sp +While stopping, setting and starting an I/O watcher in the same iteration +will result in some caching, there is still a system call per such +incident (because the same \fIfile descriptor\fR could point to a different +\&\fIfile description\fR now), so its best to avoid that. Also, \f(CW\*(C`dup ()\*(C'\fR'ed +file descriptors might not work very well if you register events for both +file descriptors. +.Sp +Best performance from this backend is achieved by not unregistering all +watchers for a file descriptor until it has been closed, if possible, +i.e. keep at least one watcher active per fd at all times. Stopping and +starting a watcher (without re-setting it) also usually doesn't cause +extra overhead. A fork can both result in spurious notifications as well +as in libev having to destroy and recreate the epoll object, which can +take considerable time and thus should be avoided. +.Sp +All this means that, in practice, \f(CW\*(C`EVBACKEND_SELECT\*(C'\fR can be as fast or +faster than epoll for maybe up to a hundred file descriptors, depending on +the usage. So sad. +.Sp +While nominally embeddable in other event loops, this feature is broken in +a lot of kernel revisions, but probably(!) works in current versions. +.Sp +This backend maps \f(CW\*(C`EV_READ\*(C'\fR and \f(CW\*(C`EV_WRITE\*(C'\fR in the same way as +\&\f(CW\*(C`EVBACKEND_POLL\*(C'\fR. +.ie n .IP """EVBACKEND_LINUXAIO"" (value 64, Linux)" 4 +.el .IP "\f(CWEVBACKEND_LINUXAIO\fR (value 64, Linux)" 4 +.IX Item "EVBACKEND_LINUXAIO (value 64, Linux)" +Use the Linux-specific Linux \s-1AIO\s0 (\fInot\fR \f(CWaio(7)\fR but \f(CWio_submit(2)\fR) event interface available in post\-4.18 kernels (but libev +only tries to use it in 4.19+). +.Sp +This is another Linux train wreck of an event interface. +.Sp +If this backend works for you (as of this writing, it was very +experimental), it is the best event interface available on Linux and might +be well worth enabling it \- if it isn't available in your kernel this will +be detected and this backend will be skipped. +.Sp +This backend can batch oneshot requests and supports a user-space ring +buffer to receive events. It also doesn't suffer from most of the design +problems of epoll (such as not being able to remove event sources from +the epoll set), and generally sounds too good to be true. Because, this +being the Linux kernel, of course it suffers from a whole new set of +limitations, forcing you to fall back to epoll, inheriting all its design +issues. +.Sp +For one, it is not easily embeddable (but probably could be done using +an event fd at some extra overhead). It also is subject to a system wide +limit that can be configured in \fI/proc/sys/fs/aio\-max\-nr\fR. If no \s-1AIO\s0 +requests are left, this backend will be skipped during initialisation, and +will switch to epoll when the loop is active. +.Sp +Most problematic in practice, however, is that not all file descriptors +work with it. For example, in Linux 5.1, \s-1TCP\s0 sockets, pipes, event fds, +files, \fI/dev/null\fR and many others are supported, but ttys do not work +properly (a known bug that the kernel developers don't care about, see +), so this is not +(yet?) a generic event polling interface. +.Sp +Overall, it seems the Linux developers just don't want it to have a +generic event handling mechanism other than \f(CW\*(C`select\*(C'\fR or \f(CW\*(C`poll\*(C'\fR. +.Sp +To work around all these problem, the current version of libev uses its +epoll backend as a fallback for file descriptor types that do not work. Or +falls back completely to epoll if the kernel acts up. +.Sp +This backend maps \f(CW\*(C`EV_READ\*(C'\fR and \f(CW\*(C`EV_WRITE\*(C'\fR in the same way as +\&\f(CW\*(C`EVBACKEND_POLL\*(C'\fR. +.ie n .IP """EVBACKEND_KQUEUE"" (value 8, most \s-1BSD\s0 clones)" 4 +.el .IP "\f(CWEVBACKEND_KQUEUE\fR (value 8, most \s-1BSD\s0 clones)" 4 +.IX Item "EVBACKEND_KQUEUE (value 8, most BSD clones)" +Kqueue deserves special mention, as at the time this backend was +implemented, it was broken on all BSDs except NetBSD (usually it doesn't +work reliably with anything but sockets and pipes, except on Darwin, +where of course it's completely useless). Unlike epoll, however, whose +brokenness is by design, these kqueue bugs can be (and mostly have been) +fixed without \s-1API\s0 changes to existing programs. For this reason it's not +being \*(L"auto-detected\*(R" on all platforms unless you explicitly specify it +in the flags (i.e. using \f(CW\*(C`EVBACKEND_KQUEUE\*(C'\fR) or libev was compiled on a +known-to-be-good (\-enough) system like NetBSD. +.Sp +You still can embed kqueue into a normal poll or select backend and use it +only for sockets (after having made sure that sockets work with kqueue on +the target platform). See \f(CW\*(C`ev_embed\*(C'\fR watchers for more info. +.Sp +It scales in the same way as the epoll backend, but the interface to the +kernel is more efficient (which says nothing about its actual speed, of +course). While stopping, setting and starting an I/O watcher does never +cause an extra system call as with \f(CW\*(C`EVBACKEND_EPOLL\*(C'\fR, it still adds up to +two event changes per incident. Support for \f(CW\*(C`fork ()\*(C'\fR is very bad (you +might have to leak fds on fork, but it's more sane than epoll) and it +drops fds silently in similarly hard-to-detect cases. +.Sp +This backend usually performs well under most conditions. +.Sp +While nominally embeddable in other event loops, this doesn't work +everywhere, so you might need to test for this. And since it is broken +almost everywhere, you should only use it when you have a lot of sockets +(for which it usually works), by embedding it into another event loop +(e.g. \f(CW\*(C`EVBACKEND_SELECT\*(C'\fR or \f(CW\*(C`EVBACKEND_POLL\*(C'\fR (but \f(CW\*(C`poll\*(C'\fR is of course +also broken on \s-1OS X\s0)) and, did I mention it, using it only for sockets. +.Sp +This backend maps \f(CW\*(C`EV_READ\*(C'\fR into an \f(CW\*(C`EVFILT_READ\*(C'\fR kevent with +\&\f(CW\*(C`NOTE_EOF\*(C'\fR, and \f(CW\*(C`EV_WRITE\*(C'\fR into an \f(CW\*(C`EVFILT_WRITE\*(C'\fR kevent with +\&\f(CW\*(C`NOTE_EOF\*(C'\fR. +.ie n .IP """EVBACKEND_DEVPOLL"" (value 16, Solaris 8)" 4 +.el .IP "\f(CWEVBACKEND_DEVPOLL\fR (value 16, Solaris 8)" 4 +.IX Item "EVBACKEND_DEVPOLL (value 16, Solaris 8)" +This is not implemented yet (and might never be, unless you send me an +implementation). According to reports, \f(CW\*(C`/dev/poll\*(C'\fR only supports sockets +and is not embeddable, which would limit the usefulness of this backend +immensely. +.ie n .IP """EVBACKEND_PORT"" (value 32, Solaris 10)" 4 +.el .IP "\f(CWEVBACKEND_PORT\fR (value 32, Solaris 10)" 4 +.IX Item "EVBACKEND_PORT (value 32, Solaris 10)" +This uses the Solaris 10 event port mechanism. As with everything on Solaris, +it's really slow, but it still scales very well (O(active_fds)). +.Sp +While this backend scales well, it requires one system call per active +file descriptor per loop iteration. For small and medium numbers of file +descriptors a \*(L"slow\*(R" \f(CW\*(C`EVBACKEND_SELECT\*(C'\fR or \f(CW\*(C`EVBACKEND_POLL\*(C'\fR backend +might perform better. +.Sp +On the positive side, this backend actually performed fully to +specification in all tests and is fully embeddable, which is a rare feat +among the OS-specific backends (I vastly prefer correctness over speed +hacks). +.Sp +On the negative side, the interface is \fIbizarre\fR \- so bizarre that +even sun itself gets it wrong in their code examples: The event polling +function sometimes returns events to the caller even though an error +occurred, but with no indication whether it has done so or not (yes, it's +even documented that way) \- deadly for edge-triggered interfaces where you +absolutely have to know whether an event occurred or not because you have +to re-arm the watcher. +.Sp +Fortunately libev seems to be able to work around these idiocies. +.Sp +This backend maps \f(CW\*(C`EV_READ\*(C'\fR and \f(CW\*(C`EV_WRITE\*(C'\fR in the same way as +\&\f(CW\*(C`EVBACKEND_POLL\*(C'\fR. +.ie n .IP """EVBACKEND_ALL""" 4 +.el .IP "\f(CWEVBACKEND_ALL\fR" 4 +.IX Item "EVBACKEND_ALL" +Try all backends (even potentially broken ones that wouldn't be tried +with \f(CW\*(C`EVFLAG_AUTO\*(C'\fR). Since this is a mask, you can do stuff such as +\&\f(CW\*(C`EVBACKEND_ALL & ~EVBACKEND_KQUEUE\*(C'\fR. +.Sp +It is definitely not recommended to use this flag, use whatever +\&\f(CW\*(C`ev_recommended_backends ()\*(C'\fR returns, or simply do not specify a backend +at all. +.ie n .IP """EVBACKEND_MASK""" 4 +.el .IP "\f(CWEVBACKEND_MASK\fR" 4 +.IX Item "EVBACKEND_MASK" +Not a backend at all, but a mask to select all backend bits from a +\&\f(CW\*(C`flags\*(C'\fR value, in case you want to mask out any backends from a flags +value (e.g. when modifying the \f(CW\*(C`LIBEV_FLAGS\*(C'\fR environment variable). +.RE +.RS 4 +.Sp +If one or more of the backend flags are or'ed into the flags value, +then only these backends will be tried (in the reverse order as listed +here). If none are specified, all backends in \f(CW\*(C`ev_recommended_backends +()\*(C'\fR will be tried. +.Sp +Example: Try to create a event loop that uses epoll and nothing else. +.Sp +.Vb 3 +\& struct ev_loop *epoller = ev_loop_new (EVBACKEND_EPOLL | EVFLAG_NOENV); +\& if (!epoller) +\& fatal ("no epoll found here, maybe it hides under your chair"); +.Ve +.Sp +Example: Use whatever libev has to offer, but make sure that kqueue is +used if available. +.Sp +.Vb 1 +\& struct ev_loop *loop = ev_loop_new (ev_recommended_backends () | EVBACKEND_KQUEUE); +.Ve +.Sp +Example: Similarly, on linux, you mgiht want to take advantage of the +linux aio backend if possible, but fall back to something else if that +isn't available. +.Sp +.Vb 1 +\& struct ev_loop *loop = ev_loop_new (ev_recommended_backends () | EVBACKEND_LINUXAIO); +.Ve +.RE +.IP "ev_loop_destroy (loop)" 4 +.IX Item "ev_loop_destroy (loop)" +Destroys an event loop object (frees all memory and kernel state +etc.). None of the active event watchers will be stopped in the normal +sense, so e.g. \f(CW\*(C`ev_is_active\*(C'\fR might still return true. It is your +responsibility to either stop all watchers cleanly yourself \fIbefore\fR +calling this function, or cope with the fact afterwards (which is usually +the easiest thing, you can just ignore the watchers and/or \f(CW\*(C`free ()\*(C'\fR them +for example). +.Sp +Note that certain global state, such as signal state (and installed signal +handlers), will not be freed by this function, and related watchers (such +as signal and child watchers) would need to be stopped manually. +.Sp +This function is normally used on loop objects allocated by +\&\f(CW\*(C`ev_loop_new\*(C'\fR, but it can also be used on the default loop returned by +\&\f(CW\*(C`ev_default_loop\*(C'\fR, in which case it is not thread-safe. +.Sp +Note that it is not advisable to call this function on the default loop +except in the rare occasion where you really need to free its resources. +If you need dynamically allocated loops it is better to use \f(CW\*(C`ev_loop_new\*(C'\fR +and \f(CW\*(C`ev_loop_destroy\*(C'\fR. +.IP "ev_loop_fork (loop)" 4 +.IX Item "ev_loop_fork (loop)" +This function sets a flag that causes subsequent \f(CW\*(C`ev_run\*(C'\fR iterations +to reinitialise the kernel state for backends that have one. Despite +the name, you can call it anytime you are allowed to start or stop +watchers (except inside an \f(CW\*(C`ev_prepare\*(C'\fR callback), but it makes most +sense after forking, in the child process. You \fImust\fR call it (or use +\&\f(CW\*(C`EVFLAG_FORKCHECK\*(C'\fR) in the child before resuming or calling \f(CW\*(C`ev_run\*(C'\fR. +.Sp +In addition, if you want to reuse a loop (via this function or +\&\f(CW\*(C`EVFLAG_FORKCHECK\*(C'\fR), you \fIalso\fR have to ignore \f(CW\*(C`SIGPIPE\*(C'\fR. +.Sp +Again, you \fIhave\fR to call it on \fIany\fR loop that you want to re-use after +a fork, \fIeven if you do not plan to use the loop in the parent\fR. This is +because some kernel interfaces *cough* \fIkqueue\fR *cough* do funny things +during fork. +.Sp +On the other hand, you only need to call this function in the child +process if and only if you want to use the event loop in the child. If +you just fork+exec or create a new loop in the child, you don't have to +call it at all (in fact, \f(CW\*(C`epoll\*(C'\fR is so badly broken that it makes a +difference, but libev will usually detect this case on its own and do a +costly reset of the backend). +.Sp +The function itself is quite fast and it's usually not a problem to call +it just in case after a fork. +.Sp +Example: Automate calling \f(CW\*(C`ev_loop_fork\*(C'\fR on the default loop when +using pthreads. +.Sp +.Vb 5 +\& static void +\& post_fork_child (void) +\& { +\& ev_loop_fork (EV_DEFAULT); +\& } +\& +\& ... +\& pthread_atfork (0, 0, post_fork_child); +.Ve +.IP "int ev_is_default_loop (loop)" 4 +.IX Item "int ev_is_default_loop (loop)" +Returns true when the given loop is, in fact, the default loop, and false +otherwise. +.IP "unsigned int ev_iteration (loop)" 4 +.IX Item "unsigned int ev_iteration (loop)" +Returns the current iteration count for the event loop, which is identical +to the number of times libev did poll for new events. It starts at \f(CW0\fR +and happily wraps around with enough iterations. +.Sp +This value can sometimes be useful as a generation counter of sorts (it +\&\*(L"ticks\*(R" the number of loop iterations), as it roughly corresponds with +\&\f(CW\*(C`ev_prepare\*(C'\fR and \f(CW\*(C`ev_check\*(C'\fR calls \- and is incremented between the +prepare and check phases. +.IP "unsigned int ev_depth (loop)" 4 +.IX Item "unsigned int ev_depth (loop)" +Returns the number of times \f(CW\*(C`ev_run\*(C'\fR was entered minus the number of +times \f(CW\*(C`ev_run\*(C'\fR was exited normally, in other words, the recursion depth. +.Sp +Outside \f(CW\*(C`ev_run\*(C'\fR, this number is zero. In a callback, this number is +\&\f(CW1\fR, unless \f(CW\*(C`ev_run\*(C'\fR was invoked recursively (or from another thread), +in which case it is higher. +.Sp +Leaving \f(CW\*(C`ev_run\*(C'\fR abnormally (setjmp/longjmp, cancelling the thread, +throwing an exception etc.), doesn't count as \*(L"exit\*(R" \- consider this +as a hint to avoid such ungentleman-like behaviour unless it's really +convenient, in which case it is fully supported. +.IP "unsigned int ev_backend (loop)" 4 +.IX Item "unsigned int ev_backend (loop)" +Returns one of the \f(CW\*(C`EVBACKEND_*\*(C'\fR flags indicating the event backend in +use. +.IP "ev_tstamp ev_now (loop)" 4 +.IX Item "ev_tstamp ev_now (loop)" +Returns the current \*(L"event loop time\*(R", which is the time the event loop +received events and started processing them. This timestamp does not +change as long as callbacks are being processed, and this is also the base +time used for relative timers. You can treat it as the timestamp of the +event occurring (or more correctly, libev finding out about it). +.IP "ev_now_update (loop)" 4 +.IX Item "ev_now_update (loop)" +Establishes the current time by querying the kernel, updating the time +returned by \f(CW\*(C`ev_now ()\*(C'\fR in the progress. This is a costly operation and +is usually done automatically within \f(CW\*(C`ev_run ()\*(C'\fR. +.Sp +This function is rarely useful, but when some event callback runs for a +very long time without entering the event loop, updating libev's idea of +the current time is a good idea. +.Sp +See also \*(L"The special problem of time updates\*(R" in the \f(CW\*(C`ev_timer\*(C'\fR section. +.IP "ev_suspend (loop)" 4 +.IX Item "ev_suspend (loop)" +.PD 0 +.IP "ev_resume (loop)" 4 +.IX Item "ev_resume (loop)" +.PD +These two functions suspend and resume an event loop, for use when the +loop is not used for a while and timeouts should not be processed. +.Sp +A typical use case would be an interactive program such as a game: When +the user presses \f(CW\*(C`^Z\*(C'\fR to suspend the game and resumes it an hour later it +would be best to handle timeouts as if no time had actually passed while +the program was suspended. This can be achieved by calling \f(CW\*(C`ev_suspend\*(C'\fR +in your \f(CW\*(C`SIGTSTP\*(C'\fR handler, sending yourself a \f(CW\*(C`SIGSTOP\*(C'\fR and calling +\&\f(CW\*(C`ev_resume\*(C'\fR directly afterwards to resume timer processing. +.Sp +Effectively, all \f(CW\*(C`ev_timer\*(C'\fR watchers will be delayed by the time spend +between \f(CW\*(C`ev_suspend\*(C'\fR and \f(CW\*(C`ev_resume\*(C'\fR, and all \f(CW\*(C`ev_periodic\*(C'\fR watchers +will be rescheduled (that is, they will lose any events that would have +occurred while suspended). +.Sp +After calling \f(CW\*(C`ev_suspend\*(C'\fR you \fBmust not\fR call \fIany\fR function on the +given loop other than \f(CW\*(C`ev_resume\*(C'\fR, and you \fBmust not\fR call \f(CW\*(C`ev_resume\*(C'\fR +without a previous call to \f(CW\*(C`ev_suspend\*(C'\fR. +.Sp +Calling \f(CW\*(C`ev_suspend\*(C'\fR/\f(CW\*(C`ev_resume\*(C'\fR has the side effect of updating the +event loop time (see \f(CW\*(C`ev_now_update\*(C'\fR). +.IP "bool ev_run (loop, int flags)" 4 +.IX Item "bool ev_run (loop, int flags)" +Finally, this is it, the event handler. This function usually is called +after you have initialised all your watchers and you want to start +handling events. It will ask the operating system for any new events, call +the watcher callbacks, and then repeat the whole process indefinitely: This +is why event loops are called \fIloops\fR. +.Sp +If the flags argument is specified as \f(CW0\fR, it will keep handling events +until either no event watchers are active anymore or \f(CW\*(C`ev_break\*(C'\fR was +called. +.Sp +The return value is false if there are no more active watchers (which +usually means \*(L"all jobs done\*(R" or \*(L"deadlock\*(R"), and true in all other cases +(which usually means " you should call \f(CW\*(C`ev_run\*(C'\fR again"). +.Sp +Please note that an explicit \f(CW\*(C`ev_break\*(C'\fR is usually better than +relying on all watchers to be stopped when deciding when a program has +finished (especially in interactive programs), but having a program +that automatically loops as long as it has to and no longer by virtue +of relying on its watchers stopping correctly, that is truly a thing of +beauty. +.Sp +This function is \fImostly\fR exception-safe \- you can break out of a +\&\f(CW\*(C`ev_run\*(C'\fR call by calling \f(CW\*(C`longjmp\*(C'\fR in a callback, throwing a \*(C+ +exception and so on. This does not decrement the \f(CW\*(C`ev_depth\*(C'\fR value, nor +will it clear any outstanding \f(CW\*(C`EVBREAK_ONE\*(C'\fR breaks. +.Sp +A flags value of \f(CW\*(C`EVRUN_NOWAIT\*(C'\fR will look for new events, will handle +those events and any already outstanding ones, but will not wait and +block your process in case there are no events and will return after one +iteration of the loop. This is sometimes useful to poll and handle new +events while doing lengthy calculations, to keep the program responsive. +.Sp +A flags value of \f(CW\*(C`EVRUN_ONCE\*(C'\fR will look for new events (waiting if +necessary) and will handle those and any already outstanding ones. It +will block your process until at least one new event arrives (which could +be an event internal to libev itself, so there is no guarantee that a +user-registered callback will be called), and will return after one +iteration of the loop. +.Sp +This is useful if you are waiting for some external event in conjunction +with something not expressible using other libev watchers (i.e. "roll your +own \f(CW\*(C`ev_run\*(C'\fR"). However, a pair of \f(CW\*(C`ev_prepare\*(C'\fR/\f(CW\*(C`ev_check\*(C'\fR watchers is +usually a better approach for this kind of thing. +.Sp +Here are the gory details of what \f(CW\*(C`ev_run\*(C'\fR does (this is for your +understanding, not a guarantee that things will work exactly like this in +future versions): +.Sp +.Vb 10 +\& \- Increment loop depth. +\& \- Reset the ev_break status. +\& \- Before the first iteration, call any pending watchers. +\& LOOP: +\& \- If EVFLAG_FORKCHECK was used, check for a fork. +\& \- If a fork was detected (by any means), queue and call all fork watchers. +\& \- Queue and call all prepare watchers. +\& \- If ev_break was called, goto FINISH. +\& \- If we have been forked, detach and recreate the kernel state +\& as to not disturb the other process. +\& \- Update the kernel state with all outstanding changes. +\& \- Update the "event loop time" (ev_now ()). +\& \- Calculate for how long to sleep or block, if at all +\& (active idle watchers, EVRUN_NOWAIT or not having +\& any active watchers at all will result in not sleeping). +\& \- Sleep if the I/O and timer collect interval say so. +\& \- Increment loop iteration counter. +\& \- Block the process, waiting for any events. +\& \- Queue all outstanding I/O (fd) events. +\& \- Update the "event loop time" (ev_now ()), and do time jump adjustments. +\& \- Queue all expired timers. +\& \- Queue all expired periodics. +\& \- Queue all idle watchers with priority higher than that of pending events. +\& \- Queue all check watchers. +\& \- Call all queued watchers in reverse order (i.e. check watchers first). +\& Signals and child watchers are implemented as I/O watchers, and will +\& be handled here by queueing them when their watcher gets executed. +\& \- If ev_break has been called, or EVRUN_ONCE or EVRUN_NOWAIT +\& were used, or there are no active watchers, goto FINISH, otherwise +\& continue with step LOOP. +\& FINISH: +\& \- Reset the ev_break status iff it was EVBREAK_ONE. +\& \- Decrement the loop depth. +\& \- Return. +.Ve +.Sp +Example: Queue some jobs and then loop until no events are outstanding +anymore. +.Sp +.Vb 4 +\& ... queue jobs here, make sure they register event watchers as long +\& ... as they still have work to do (even an idle watcher will do..) +\& ev_run (my_loop, 0); +\& ... jobs done or somebody called break. yeah! +.Ve +.IP "ev_break (loop, how)" 4 +.IX Item "ev_break (loop, how)" +Can be used to make a call to \f(CW\*(C`ev_run\*(C'\fR return early (but only after it +has processed all outstanding events). The \f(CW\*(C`how\*(C'\fR argument must be either +\&\f(CW\*(C`EVBREAK_ONE\*(C'\fR, which will make the innermost \f(CW\*(C`ev_run\*(C'\fR call return, or +\&\f(CW\*(C`EVBREAK_ALL\*(C'\fR, which will make all nested \f(CW\*(C`ev_run\*(C'\fR calls return. +.Sp +This \*(L"break state\*(R" will be cleared on the next call to \f(CW\*(C`ev_run\*(C'\fR. +.Sp +It is safe to call \f(CW\*(C`ev_break\*(C'\fR from outside any \f(CW\*(C`ev_run\*(C'\fR calls, too, in +which case it will have no effect. +.IP "ev_ref (loop)" 4 +.IX Item "ev_ref (loop)" +.PD 0 +.IP "ev_unref (loop)" 4 +.IX Item "ev_unref (loop)" +.PD +Ref/unref can be used to add or remove a reference count on the event +loop: Every watcher keeps one reference, and as long as the reference +count is nonzero, \f(CW\*(C`ev_run\*(C'\fR will not return on its own. +.Sp +This is useful when you have a watcher that you never intend to +unregister, but that nevertheless should not keep \f(CW\*(C`ev_run\*(C'\fR from +returning. In such a case, call \f(CW\*(C`ev_unref\*(C'\fR after starting, and \f(CW\*(C`ev_ref\*(C'\fR +before stopping it. +.Sp +As an example, libev itself uses this for its internal signal pipe: It +is not visible to the libev user and should not keep \f(CW\*(C`ev_run\*(C'\fR from +exiting if no event watchers registered by it are active. It is also an +excellent way to do this for generic recurring timers or from within +third-party libraries. Just remember to \fIunref after start\fR and \fIref +before stop\fR (but only if the watcher wasn't active before, or was active +before, respectively. Note also that libev might stop watchers itself +(e.g. non-repeating timers) in which case you have to \f(CW\*(C`ev_ref\*(C'\fR +in the callback). +.Sp +Example: Create a signal watcher, but keep it from keeping \f(CW\*(C`ev_run\*(C'\fR +running when nothing else is active. +.Sp +.Vb 4 +\& ev_signal exitsig; +\& ev_signal_init (&exitsig, sig_cb, SIGINT); +\& ev_signal_start (loop, &exitsig); +\& ev_unref (loop); +.Ve +.Sp +Example: For some weird reason, unregister the above signal handler again. +.Sp +.Vb 2 +\& ev_ref (loop); +\& ev_signal_stop (loop, &exitsig); +.Ve +.IP "ev_set_io_collect_interval (loop, ev_tstamp interval)" 4 +.IX Item "ev_set_io_collect_interval (loop, ev_tstamp interval)" +.PD 0 +.IP "ev_set_timeout_collect_interval (loop, ev_tstamp interval)" 4 +.IX Item "ev_set_timeout_collect_interval (loop, ev_tstamp interval)" +.PD +These advanced functions influence the time that libev will spend waiting +for events. Both time intervals are by default \f(CW0\fR, meaning that libev +will try to invoke timer/periodic callbacks and I/O callbacks with minimum +latency. +.Sp +Setting these to a higher value (the \f(CW\*(C`interval\*(C'\fR \fImust\fR be >= \f(CW0\fR) +allows libev to delay invocation of I/O and timer/periodic callbacks +to increase efficiency of loop iterations (or to increase power-saving +opportunities). +.Sp +The idea is that sometimes your program runs just fast enough to handle +one (or very few) event(s) per loop iteration. While this makes the +program responsive, it also wastes a lot of \s-1CPU\s0 time to poll for new +events, especially with backends like \f(CW\*(C`select ()\*(C'\fR which have a high +overhead for the actual polling but can deliver many events at once. +.Sp +By setting a higher \fIio collect interval\fR you allow libev to spend more +time collecting I/O events, so you can handle more events per iteration, +at the cost of increasing latency. Timeouts (both \f(CW\*(C`ev_periodic\*(C'\fR and +\&\f(CW\*(C`ev_timer\*(C'\fR) will not be affected. Setting this to a non-null value will +introduce an additional \f(CW\*(C`ev_sleep ()\*(C'\fR call into most loop iterations. The +sleep time ensures that libev will not poll for I/O events more often then +once per this interval, on average (as long as the host time resolution is +good enough). +.Sp +Likewise, by setting a higher \fItimeout collect interval\fR you allow libev +to spend more time collecting timeouts, at the expense of increased +latency/jitter/inexactness (the watcher callback will be called +later). \f(CW\*(C`ev_io\*(C'\fR watchers will not be affected. Setting this to a non-null +value will not introduce any overhead in libev. +.Sp +Many (busy) programs can usually benefit by setting the I/O collect +interval to a value near \f(CW0.1\fR or so, which is often enough for +interactive servers (of course not for games), likewise for timeouts. It +usually doesn't make much sense to set it to a lower value than \f(CW0.01\fR, +as this approaches the timing granularity of most systems. Note that if +you do transactions with the outside world and you can't increase the +parallelity, then this setting will limit your transaction rate (if you +need to poll once per transaction and the I/O collect interval is 0.01, +then you can't do more than 100 transactions per second). +.Sp +Setting the \fItimeout collect interval\fR can improve the opportunity for +saving power, as the program will \*(L"bundle\*(R" timer callback invocations that +are \*(L"near\*(R" in time together, by delaying some, thus reducing the number of +times the process sleeps and wakes up again. Another useful technique to +reduce iterations/wake\-ups is to use \f(CW\*(C`ev_periodic\*(C'\fR watchers and make sure +they fire on, say, one-second boundaries only. +.Sp +Example: we only need 0.1s timeout granularity, and we wish not to poll +more often than 100 times per second: +.Sp +.Vb 2 +\& ev_set_timeout_collect_interval (EV_DEFAULT_UC_ 0.1); +\& ev_set_io_collect_interval (EV_DEFAULT_UC_ 0.01); +.Ve +.IP "ev_invoke_pending (loop)" 4 +.IX Item "ev_invoke_pending (loop)" +This call will simply invoke all pending watchers while resetting their +pending state. Normally, \f(CW\*(C`ev_run\*(C'\fR does this automatically when required, +but when overriding the invoke callback this call comes handy. This +function can be invoked from a watcher \- this can be useful for example +when you want to do some lengthy calculation and want to pass further +event handling to another thread (you still have to make sure only one +thread executes within \f(CW\*(C`ev_invoke_pending\*(C'\fR or \f(CW\*(C`ev_run\*(C'\fR of course). +.IP "int ev_pending_count (loop)" 4 +.IX Item "int ev_pending_count (loop)" +Returns the number of pending watchers \- zero indicates that no watchers +are pending. +.IP "ev_set_invoke_pending_cb (loop, void (*invoke_pending_cb)(\s-1EV_P\s0))" 4 +.IX Item "ev_set_invoke_pending_cb (loop, void (*invoke_pending_cb)(EV_P))" +This overrides the invoke pending functionality of the loop: Instead of +invoking all pending watchers when there are any, \f(CW\*(C`ev_run\*(C'\fR will call +this callback instead. This is useful, for example, when you want to +invoke the actual watchers inside another context (another thread etc.). +.Sp +If you want to reset the callback, use \f(CW\*(C`ev_invoke_pending\*(C'\fR as new +callback. +.IP "ev_set_loop_release_cb (loop, void (*release)(\s-1EV_P\s0) throw (), void (*acquire)(\s-1EV_P\s0) throw ())" 4 +.IX Item "ev_set_loop_release_cb (loop, void (*release)(EV_P) throw (), void (*acquire)(EV_P) throw ())" +Sometimes you want to share the same loop between multiple threads. This +can be done relatively simply by putting mutex_lock/unlock calls around +each call to a libev function. +.Sp +However, \f(CW\*(C`ev_run\*(C'\fR can run an indefinite time, so it is not feasible +to wait for it to return. One way around this is to wake up the event +loop via \f(CW\*(C`ev_break\*(C'\fR and \f(CW\*(C`ev_async_send\*(C'\fR, another way is to set these +\&\fIrelease\fR and \fIacquire\fR callbacks on the loop. +.Sp +When set, then \f(CW\*(C`release\*(C'\fR will be called just before the thread is +suspended waiting for new events, and \f(CW\*(C`acquire\*(C'\fR is called just +afterwards. +.Sp +Ideally, \f(CW\*(C`release\*(C'\fR will just call your mutex_unlock function, and +\&\f(CW\*(C`acquire\*(C'\fR will just call the mutex_lock function again. +.Sp +While event loop modifications are allowed between invocations of +\&\f(CW\*(C`release\*(C'\fR and \f(CW\*(C`acquire\*(C'\fR (that's their only purpose after all), no +modifications done will affect the event loop, i.e. adding watchers will +have no effect on the set of file descriptors being watched, or the time +waited. Use an \f(CW\*(C`ev_async\*(C'\fR watcher to wake up \f(CW\*(C`ev_run\*(C'\fR when you want it +to take note of any changes you made. +.Sp +In theory, threads executing \f(CW\*(C`ev_run\*(C'\fR will be async-cancel safe between +invocations of \f(CW\*(C`release\*(C'\fR and \f(CW\*(C`acquire\*(C'\fR. +.Sp +See also the locking example in the \f(CW\*(C`THREADS\*(C'\fR section later in this +document. +.IP "ev_set_userdata (loop, void *data)" 4 +.IX Item "ev_set_userdata (loop, void *data)" +.PD 0 +.IP "void *ev_userdata (loop)" 4 +.IX Item "void *ev_userdata (loop)" +.PD +Set and retrieve a single \f(CW\*(C`void *\*(C'\fR associated with a loop. When +\&\f(CW\*(C`ev_set_userdata\*(C'\fR has never been called, then \f(CW\*(C`ev_userdata\*(C'\fR returns +\&\f(CW0\fR. +.Sp +These two functions can be used to associate arbitrary data with a loop, +and are intended solely for the \f(CW\*(C`invoke_pending_cb\*(C'\fR, \f(CW\*(C`release\*(C'\fR and +\&\f(CW\*(C`acquire\*(C'\fR callbacks described above, but of course can be (ab\-)used for +any other purpose as well. +.IP "ev_verify (loop)" 4 +.IX Item "ev_verify (loop)" +This function only does something when \f(CW\*(C`EV_VERIFY\*(C'\fR support has been +compiled in, which is the default for non-minimal builds. It tries to go +through all internal structures and checks them for validity. If anything +is found to be inconsistent, it will print an error message to standard +error and call \f(CW\*(C`abort ()\*(C'\fR. +.Sp +This can be used to catch bugs inside libev itself: under normal +circumstances, this function will never abort as of course libev keeps its +data structures consistent. +.SH "ANATOMY OF A WATCHER" +.IX Header "ANATOMY OF A WATCHER" +In the following description, uppercase \f(CW\*(C`TYPE\*(C'\fR in names stands for the +watcher type, e.g. \f(CW\*(C`ev_TYPE_start\*(C'\fR can mean \f(CW\*(C`ev_timer_start\*(C'\fR for timer +watchers and \f(CW\*(C`ev_io_start\*(C'\fR for I/O watchers. +.PP +A watcher is an opaque structure that you allocate and register to record +your interest in some event. To make a concrete example, imagine you want +to wait for \s-1STDIN\s0 to become readable, you would create an \f(CW\*(C`ev_io\*(C'\fR watcher +for that: +.PP +.Vb 5 +\& static void my_cb (struct ev_loop *loop, ev_io *w, int revents) +\& { +\& ev_io_stop (w); +\& ev_break (loop, EVBREAK_ALL); +\& } +\& +\& struct ev_loop *loop = ev_default_loop (0); +\& +\& ev_io stdin_watcher; +\& +\& ev_init (&stdin_watcher, my_cb); +\& ev_io_set (&stdin_watcher, STDIN_FILENO, EV_READ); +\& ev_io_start (loop, &stdin_watcher); +\& +\& ev_run (loop, 0); +.Ve +.PP +As you can see, you are responsible for allocating the memory for your +watcher structures (and it is \fIusually\fR a bad idea to do this on the +stack). +.PP +Each watcher has an associated watcher structure (called \f(CW\*(C`struct ev_TYPE\*(C'\fR +or simply \f(CW\*(C`ev_TYPE\*(C'\fR, as typedefs are provided for all watcher structs). +.PP +Each watcher structure must be initialised by a call to \f(CW\*(C`ev_init (watcher +*, callback)\*(C'\fR, which expects a callback to be provided. This callback is +invoked each time the event occurs (or, in the case of I/O watchers, each +time the event loop detects that the file descriptor given is readable +and/or writable). +.PP +Each watcher type further has its own \f(CW\*(C`ev_TYPE_set (watcher *, ...)\*(C'\fR +macro to configure it, with arguments specific to the watcher type. There +is also a macro to combine initialisation and setting in one call: \f(CW\*(C`ev_TYPE_init (watcher *, callback, ...)\*(C'\fR. +.PP +To make the watcher actually watch out for events, you have to start it +with a watcher-specific start function (\f(CW\*(C`ev_TYPE_start (loop, watcher +*)\*(C'\fR), and you can stop watching for events at any time by calling the +corresponding stop function (\f(CW\*(C`ev_TYPE_stop (loop, watcher *)\*(C'\fR. +.PP +As long as your watcher is active (has been started but not stopped) you +must not touch the values stored in it except when explicitly documented +otherwise. Most specifically you must never reinitialise it or call its +\&\f(CW\*(C`ev_TYPE_set\*(C'\fR macro. +.PP +Each and every callback receives the event loop pointer as first, the +registered watcher structure as second, and a bitset of received events as +third argument. +.PP +The received events usually include a single bit per event type received +(you can receive multiple events at the same time). The possible bit masks +are: +.ie n .IP """EV_READ""" 4 +.el .IP "\f(CWEV_READ\fR" 4 +.IX Item "EV_READ" +.PD 0 +.ie n .IP """EV_WRITE""" 4 +.el .IP "\f(CWEV_WRITE\fR" 4 +.IX Item "EV_WRITE" +.PD +The file descriptor in the \f(CW\*(C`ev_io\*(C'\fR watcher has become readable and/or +writable. +.ie n .IP """EV_TIMER""" 4 +.el .IP "\f(CWEV_TIMER\fR" 4 +.IX Item "EV_TIMER" +The \f(CW\*(C`ev_timer\*(C'\fR watcher has timed out. +.ie n .IP """EV_PERIODIC""" 4 +.el .IP "\f(CWEV_PERIODIC\fR" 4 +.IX Item "EV_PERIODIC" +The \f(CW\*(C`ev_periodic\*(C'\fR watcher has timed out. +.ie n .IP """EV_SIGNAL""" 4 +.el .IP "\f(CWEV_SIGNAL\fR" 4 +.IX Item "EV_SIGNAL" +The signal specified in the \f(CW\*(C`ev_signal\*(C'\fR watcher has been received by a thread. +.ie n .IP """EV_CHILD""" 4 +.el .IP "\f(CWEV_CHILD\fR" 4 +.IX Item "EV_CHILD" +The pid specified in the \f(CW\*(C`ev_child\*(C'\fR watcher has received a status change. +.ie n .IP """EV_STAT""" 4 +.el .IP "\f(CWEV_STAT\fR" 4 +.IX Item "EV_STAT" +The path specified in the \f(CW\*(C`ev_stat\*(C'\fR watcher changed its attributes somehow. +.ie n .IP """EV_IDLE""" 4 +.el .IP "\f(CWEV_IDLE\fR" 4 +.IX Item "EV_IDLE" +The \f(CW\*(C`ev_idle\*(C'\fR watcher has determined that you have nothing better to do. +.ie n .IP """EV_PREPARE""" 4 +.el .IP "\f(CWEV_PREPARE\fR" 4 +.IX Item "EV_PREPARE" +.PD 0 +.ie n .IP """EV_CHECK""" 4 +.el .IP "\f(CWEV_CHECK\fR" 4 +.IX Item "EV_CHECK" +.PD +All \f(CW\*(C`ev_prepare\*(C'\fR watchers are invoked just \fIbefore\fR \f(CW\*(C`ev_run\*(C'\fR starts to +gather new events, and all \f(CW\*(C`ev_check\*(C'\fR watchers are queued (not invoked) +just after \f(CW\*(C`ev_run\*(C'\fR has gathered them, but before it queues any callbacks +for any received events. That means \f(CW\*(C`ev_prepare\*(C'\fR watchers are the last +watchers invoked before the event loop sleeps or polls for new events, and +\&\f(CW\*(C`ev_check\*(C'\fR watchers will be invoked before any other watchers of the same +or lower priority within an event loop iteration. +.Sp +Callbacks of both watcher types can start and stop as many watchers as +they want, and all of them will be taken into account (for example, a +\&\f(CW\*(C`ev_prepare\*(C'\fR watcher might start an idle watcher to keep \f(CW\*(C`ev_run\*(C'\fR from +blocking). +.ie n .IP """EV_EMBED""" 4 +.el .IP "\f(CWEV_EMBED\fR" 4 +.IX Item "EV_EMBED" +The embedded event loop specified in the \f(CW\*(C`ev_embed\*(C'\fR watcher needs attention. +.ie n .IP """EV_FORK""" 4 +.el .IP "\f(CWEV_FORK\fR" 4 +.IX Item "EV_FORK" +The event loop has been resumed in the child process after fork (see +\&\f(CW\*(C`ev_fork\*(C'\fR). +.ie n .IP """EV_CLEANUP""" 4 +.el .IP "\f(CWEV_CLEANUP\fR" 4 +.IX Item "EV_CLEANUP" +The event loop is about to be destroyed (see \f(CW\*(C`ev_cleanup\*(C'\fR). +.ie n .IP """EV_ASYNC""" 4 +.el .IP "\f(CWEV_ASYNC\fR" 4 +.IX Item "EV_ASYNC" +The given async watcher has been asynchronously notified (see \f(CW\*(C`ev_async\*(C'\fR). +.ie n .IP """EV_CUSTOM""" 4 +.el .IP "\f(CWEV_CUSTOM\fR" 4 +.IX Item "EV_CUSTOM" +Not ever sent (or otherwise used) by libev itself, but can be freely used +by libev users to signal watchers (e.g. via \f(CW\*(C`ev_feed_event\*(C'\fR). +.ie n .IP """EV_ERROR""" 4 +.el .IP "\f(CWEV_ERROR\fR" 4 +.IX Item "EV_ERROR" +An unspecified error has occurred, the watcher has been stopped. This might +happen because the watcher could not be properly started because libev +ran out of memory, a file descriptor was found to be closed or any other +problem. Libev considers these application bugs. +.Sp +You best act on it by reporting the problem and somehow coping with the +watcher being stopped. Note that well-written programs should not receive +an error ever, so when your watcher receives it, this usually indicates a +bug in your program. +.Sp +Libev will usually signal a few \*(L"dummy\*(R" events together with an error, for +example it might indicate that a fd is readable or writable, and if your +callbacks is well-written it can just attempt the operation and cope with +the error from \fBread()\fR or \fBwrite()\fR. This will not work in multi-threaded +programs, though, as the fd could already be closed and reused for another +thing, so beware. +.SS "\s-1GENERIC WATCHER FUNCTIONS\s0" +.IX Subsection "GENERIC WATCHER FUNCTIONS" +.ie n .IP """ev_init"" (ev_TYPE *watcher, callback)" 4 +.el .IP "\f(CWev_init\fR (ev_TYPE *watcher, callback)" 4 +.IX Item "ev_init (ev_TYPE *watcher, callback)" +This macro initialises the generic portion of a watcher. The contents +of the watcher object can be arbitrary (so \f(CW\*(C`malloc\*(C'\fR will do). Only +the generic parts of the watcher are initialised, you \fIneed\fR to call +the type-specific \f(CW\*(C`ev_TYPE_set\*(C'\fR macro afterwards to initialise the +type-specific parts. For each type there is also a \f(CW\*(C`ev_TYPE_init\*(C'\fR macro +which rolls both calls into one. +.Sp +You can reinitialise a watcher at any time as long as it has been stopped +(or never started) and there are no pending events outstanding. +.Sp +The callback is always of type \f(CW\*(C`void (*)(struct ev_loop *loop, ev_TYPE *watcher, +int revents)\*(C'\fR. +.Sp +Example: Initialise an \f(CW\*(C`ev_io\*(C'\fR watcher in two steps. +.Sp +.Vb 3 +\& ev_io w; +\& ev_init (&w, my_cb); +\& ev_io_set (&w, STDIN_FILENO, EV_READ); +.Ve +.ie n .IP """ev_TYPE_set"" (ev_TYPE *watcher, [args])" 4 +.el .IP "\f(CWev_TYPE_set\fR (ev_TYPE *watcher, [args])" 4 +.IX Item "ev_TYPE_set (ev_TYPE *watcher, [args])" +This macro initialises the type-specific parts of a watcher. You need to +call \f(CW\*(C`ev_init\*(C'\fR at least once before you call this macro, but you can +call \f(CW\*(C`ev_TYPE_set\*(C'\fR any number of times. You must not, however, call this +macro on a watcher that is active (it can be pending, however, which is a +difference to the \f(CW\*(C`ev_init\*(C'\fR macro). +.Sp +Although some watcher types do not have type-specific arguments +(e.g. \f(CW\*(C`ev_prepare\*(C'\fR) you still need to call its \f(CW\*(C`set\*(C'\fR macro. +.Sp +See \f(CW\*(C`ev_init\*(C'\fR, above, for an example. +.ie n .IP """ev_TYPE_init"" (ev_TYPE *watcher, callback, [args])" 4 +.el .IP "\f(CWev_TYPE_init\fR (ev_TYPE *watcher, callback, [args])" 4 +.IX Item "ev_TYPE_init (ev_TYPE *watcher, callback, [args])" +This convenience macro rolls both \f(CW\*(C`ev_init\*(C'\fR and \f(CW\*(C`ev_TYPE_set\*(C'\fR macro +calls into a single call. This is the most convenient method to initialise +a watcher. The same limitations apply, of course. +.Sp +Example: Initialise and set an \f(CW\*(C`ev_io\*(C'\fR watcher in one step. +.Sp +.Vb 1 +\& ev_io_init (&w, my_cb, STDIN_FILENO, EV_READ); +.Ve +.ie n .IP """ev_TYPE_start"" (loop, ev_TYPE *watcher)" 4 +.el .IP "\f(CWev_TYPE_start\fR (loop, ev_TYPE *watcher)" 4 +.IX Item "ev_TYPE_start (loop, ev_TYPE *watcher)" +Starts (activates) the given watcher. Only active watchers will receive +events. If the watcher is already active nothing will happen. +.Sp +Example: Start the \f(CW\*(C`ev_io\*(C'\fR watcher that is being abused as example in this +whole section. +.Sp +.Vb 1 +\& ev_io_start (EV_DEFAULT_UC, &w); +.Ve +.ie n .IP """ev_TYPE_stop"" (loop, ev_TYPE *watcher)" 4 +.el .IP "\f(CWev_TYPE_stop\fR (loop, ev_TYPE *watcher)" 4 +.IX Item "ev_TYPE_stop (loop, ev_TYPE *watcher)" +Stops the given watcher if active, and clears the pending status (whether +the watcher was active or not). +.Sp +It is possible that stopped watchers are pending \- for example, +non-repeating timers are being stopped when they become pending \- but +calling \f(CW\*(C`ev_TYPE_stop\*(C'\fR ensures that the watcher is neither active nor +pending. If you want to free or reuse the memory used by the watcher it is +therefore a good idea to always call its \f(CW\*(C`ev_TYPE_stop\*(C'\fR function. +.IP "bool ev_is_active (ev_TYPE *watcher)" 4 +.IX Item "bool ev_is_active (ev_TYPE *watcher)" +Returns a true value iff the watcher is active (i.e. it has been started +and not yet been stopped). As long as a watcher is active you must not modify +it. +.IP "bool ev_is_pending (ev_TYPE *watcher)" 4 +.IX Item "bool ev_is_pending (ev_TYPE *watcher)" +Returns a true value iff the watcher is pending, (i.e. it has outstanding +events but its callback has not yet been invoked). As long as a watcher +is pending (but not active) you must not call an init function on it (but +\&\f(CW\*(C`ev_TYPE_set\*(C'\fR is safe), you must not change its priority, and you must +make sure the watcher is available to libev (e.g. you cannot \f(CW\*(C`free ()\*(C'\fR +it). +.IP "callback ev_cb (ev_TYPE *watcher)" 4 +.IX Item "callback ev_cb (ev_TYPE *watcher)" +Returns the callback currently set on the watcher. +.IP "ev_set_cb (ev_TYPE *watcher, callback)" 4 +.IX Item "ev_set_cb (ev_TYPE *watcher, callback)" +Change the callback. You can change the callback at virtually any time +(modulo threads). +.IP "ev_set_priority (ev_TYPE *watcher, int priority)" 4 +.IX Item "ev_set_priority (ev_TYPE *watcher, int priority)" +.PD 0 +.IP "int ev_priority (ev_TYPE *watcher)" 4 +.IX Item "int ev_priority (ev_TYPE *watcher)" +.PD +Set and query the priority of the watcher. The priority is a small +integer between \f(CW\*(C`EV_MAXPRI\*(C'\fR (default: \f(CW2\fR) and \f(CW\*(C`EV_MINPRI\*(C'\fR +(default: \f(CW\*(C`\-2\*(C'\fR). Pending watchers with higher priority will be invoked +before watchers with lower priority, but priority will not keep watchers +from being executed (except for \f(CW\*(C`ev_idle\*(C'\fR watchers). +.Sp +If you need to suppress invocation when higher priority events are pending +you need to look at \f(CW\*(C`ev_idle\*(C'\fR watchers, which provide this functionality. +.Sp +You \fImust not\fR change the priority of a watcher as long as it is active or +pending. +.Sp +Setting a priority outside the range of \f(CW\*(C`EV_MINPRI\*(C'\fR to \f(CW\*(C`EV_MAXPRI\*(C'\fR is +fine, as long as you do not mind that the priority value you query might +or might not have been clamped to the valid range. +.Sp +The default priority used by watchers when no priority has been set is +always \f(CW0\fR, which is supposed to not be too high and not be too low :). +.Sp +See \*(L"\s-1WATCHER PRIORITY MODELS\*(R"\s0, below, for a more thorough treatment of +priorities. +.IP "ev_invoke (loop, ev_TYPE *watcher, int revents)" 4 +.IX Item "ev_invoke (loop, ev_TYPE *watcher, int revents)" +Invoke the \f(CW\*(C`watcher\*(C'\fR with the given \f(CW\*(C`loop\*(C'\fR and \f(CW\*(C`revents\*(C'\fR. Neither +\&\f(CW\*(C`loop\*(C'\fR nor \f(CW\*(C`revents\*(C'\fR need to be valid as long as the watcher callback +can deal with that fact, as both are simply passed through to the +callback. +.IP "int ev_clear_pending (loop, ev_TYPE *watcher)" 4 +.IX Item "int ev_clear_pending (loop, ev_TYPE *watcher)" +If the watcher is pending, this function clears its pending status and +returns its \f(CW\*(C`revents\*(C'\fR bitset (as if its callback was invoked). If the +watcher isn't pending it does nothing and returns \f(CW0\fR. +.Sp +Sometimes it can be useful to \*(L"poll\*(R" a watcher instead of waiting for its +callback to be invoked, which can be accomplished with this function. +.IP "ev_feed_event (loop, ev_TYPE *watcher, int revents)" 4 +.IX Item "ev_feed_event (loop, ev_TYPE *watcher, int revents)" +Feeds the given event set into the event loop, as if the specified event +had happened for the specified watcher (which must be a pointer to an +initialised but not necessarily started event watcher). Obviously you must +not free the watcher as long as it has pending events. +.Sp +Stopping the watcher, letting libev invoke it, or calling +\&\f(CW\*(C`ev_clear_pending\*(C'\fR will clear the pending event, even if the watcher was +not started in the first place. +.Sp +See also \f(CW\*(C`ev_feed_fd_event\*(C'\fR and \f(CW\*(C`ev_feed_signal_event\*(C'\fR for related +functions that do not need a watcher. +.PP +See also the \*(L"\s-1ASSOCIATING CUSTOM DATA WITH A WATCHER\*(R"\s0 and \*(L"\s-1BUILDING YOUR +OWN COMPOSITE WATCHERS\*(R"\s0 idioms. +.SS "\s-1WATCHER STATES\s0" +.IX Subsection "WATCHER STATES" +There are various watcher states mentioned throughout this manual \- +active, pending and so on. In this section these states and the rules to +transition between them will be described in more detail \- and while these +rules might look complicated, they usually do \*(L"the right thing\*(R". +.IP "initialised" 4 +.IX Item "initialised" +Before a watcher can be registered with the event loop it has to be +initialised. This can be done with a call to \f(CW\*(C`ev_TYPE_init\*(C'\fR, or calls to +\&\f(CW\*(C`ev_init\*(C'\fR followed by the watcher-specific \f(CW\*(C`ev_TYPE_set\*(C'\fR function. +.Sp +In this state it is simply some block of memory that is suitable for +use in an event loop. It can be moved around, freed, reused etc. at +will \- as long as you either keep the memory contents intact, or call +\&\f(CW\*(C`ev_TYPE_init\*(C'\fR again. +.IP "started/running/active" 4 +.IX Item "started/running/active" +Once a watcher has been started with a call to \f(CW\*(C`ev_TYPE_start\*(C'\fR it becomes +property of the event loop, and is actively waiting for events. While in +this state it cannot be accessed (except in a few documented ways), moved, +freed or anything else \- the only legal thing is to keep a pointer to it, +and call libev functions on it that are documented to work on active watchers. +.IP "pending" 4 +.IX Item "pending" +If a watcher is active and libev determines that an event it is interested +in has occurred (such as a timer expiring), it will become pending. It will +stay in this pending state until either it is stopped or its callback is +about to be invoked, so it is not normally pending inside the watcher +callback. +.Sp +The watcher might or might not be active while it is pending (for example, +an expired non-repeating timer can be pending but no longer active). If it +is stopped, it can be freely accessed (e.g. by calling \f(CW\*(C`ev_TYPE_set\*(C'\fR), +but it is still property of the event loop at this time, so cannot be +moved, freed or reused. And if it is active the rules described in the +previous item still apply. +.Sp +It is also possible to feed an event on a watcher that is not active (e.g. +via \f(CW\*(C`ev_feed_event\*(C'\fR), in which case it becomes pending without being +active. +.IP "stopped" 4 +.IX Item "stopped" +A watcher can be stopped implicitly by libev (in which case it might still +be pending), or explicitly by calling its \f(CW\*(C`ev_TYPE_stop\*(C'\fR function. The +latter will clear any pending state the watcher might be in, regardless +of whether it was active or not, so stopping a watcher explicitly before +freeing it is often a good idea. +.Sp +While stopped (and not pending) the watcher is essentially in the +initialised state, that is, it can be reused, moved, modified in any way +you wish (but when you trash the memory block, you need to \f(CW\*(C`ev_TYPE_init\*(C'\fR +it again). +.SS "\s-1WATCHER PRIORITY MODELS\s0" +.IX Subsection "WATCHER PRIORITY MODELS" +Many event loops support \fIwatcher priorities\fR, which are usually small +integers that influence the ordering of event callback invocation +between watchers in some way, all else being equal. +.PP +In libev, watcher priorities can be set using \f(CW\*(C`ev_set_priority\*(C'\fR. See its +description for the more technical details such as the actual priority +range. +.PP +There are two common ways how these these priorities are being interpreted +by event loops: +.PP +In the more common lock-out model, higher priorities \*(L"lock out\*(R" invocation +of lower priority watchers, which means as long as higher priority +watchers receive events, lower priority watchers are not being invoked. +.PP +The less common only-for-ordering model uses priorities solely to order +callback invocation within a single event loop iteration: Higher priority +watchers are invoked before lower priority ones, but they all get invoked +before polling for new events. +.PP +Libev uses the second (only-for-ordering) model for all its watchers +except for idle watchers (which use the lock-out model). +.PP +The rationale behind this is that implementing the lock-out model for +watchers is not well supported by most kernel interfaces, and most event +libraries will just poll for the same events again and again as long as +their callbacks have not been executed, which is very inefficient in the +common case of one high-priority watcher locking out a mass of lower +priority ones. +.PP +Static (ordering) priorities are most useful when you have two or more +watchers handling the same resource: a typical usage example is having an +\&\f(CW\*(C`ev_io\*(C'\fR watcher to receive data, and an associated \f(CW\*(C`ev_timer\*(C'\fR to handle +timeouts. Under load, data might be received while the program handles +other jobs, but since timers normally get invoked first, the timeout +handler will be executed before checking for data. In that case, giving +the timer a lower priority than the I/O watcher ensures that I/O will be +handled first even under adverse conditions (which is usually, but not +always, what you want). +.PP +Since idle watchers use the \*(L"lock-out\*(R" model, meaning that idle watchers +will only be executed when no same or higher priority watchers have +received events, they can be used to implement the \*(L"lock-out\*(R" model when +required. +.PP +For example, to emulate how many other event libraries handle priorities, +you can associate an \f(CW\*(C`ev_idle\*(C'\fR watcher to each such watcher, and in +the normal watcher callback, you just start the idle watcher. The real +processing is done in the idle watcher callback. This causes libev to +continuously poll and process kernel event data for the watcher, but when +the lock-out case is known to be rare (which in turn is rare :), this is +workable. +.PP +Usually, however, the lock-out model implemented that way will perform +miserably under the type of load it was designed to handle. In that case, +it might be preferable to stop the real watcher before starting the +idle watcher, so the kernel will not have to process the event in case +the actual processing will be delayed for considerable time. +.PP +Here is an example of an I/O watcher that should run at a strictly lower +priority than the default, and which should only process data when no +other events are pending: +.PP +.Vb 2 +\& ev_idle idle; // actual processing watcher +\& ev_io io; // actual event watcher +\& +\& static void +\& io_cb (EV_P_ ev_io *w, int revents) +\& { +\& // stop the I/O watcher, we received the event, but +\& // are not yet ready to handle it. +\& ev_io_stop (EV_A_ w); +\& +\& // start the idle watcher to handle the actual event. +\& // it will not be executed as long as other watchers +\& // with the default priority are receiving events. +\& ev_idle_start (EV_A_ &idle); +\& } +\& +\& static void +\& idle_cb (EV_P_ ev_idle *w, int revents) +\& { +\& // actual processing +\& read (STDIN_FILENO, ...); +\& +\& // have to start the I/O watcher again, as +\& // we have handled the event +\& ev_io_start (EV_P_ &io); +\& } +\& +\& // initialisation +\& ev_idle_init (&idle, idle_cb); +\& ev_io_init (&io, io_cb, STDIN_FILENO, EV_READ); +\& ev_io_start (EV_DEFAULT_ &io); +.Ve +.PP +In the \*(L"real\*(R" world, it might also be beneficial to start a timer, so that +low-priority connections can not be locked out forever under load. This +enables your program to keep a lower latency for important connections +during short periods of high load, while not completely locking out less +important ones. +.SH "WATCHER TYPES" +.IX Header "WATCHER TYPES" +This section describes each watcher in detail, but will not repeat +information given in the last section. Any initialisation/set macros, +functions and members specific to the watcher type are explained. +.PP +Most members are additionally marked with either \fI[read\-only]\fR, meaning +that, while the watcher is active, you can look at the member and expect +some sensible content, but you must not modify it (you can modify it while +the watcher is stopped to your hearts content), or \fI[read\-write]\fR, which +means you can expect it to have some sensible content while the watcher is +active, but you can also modify it (within the same thread as the event +loop, i.e. without creating data races). Modifying it may not do something +sensible or take immediate effect (or do anything at all), but libev will +not crash or malfunction in any way. +.PP +In any case, the documentation for each member will explain what the +effects are, and if there are any additional access restrictions. +.ie n .SS """ev_io"" \- is this file descriptor readable or writable?" +.el .SS "\f(CWev_io\fP \- is this file descriptor readable or writable?" +.IX Subsection "ev_io - is this file descriptor readable or writable?" +I/O watchers check whether a file descriptor is readable or writable +in each iteration of the event loop, or, more precisely, when reading +would not block the process and writing would at least be able to write +some data. This behaviour is called level-triggering because you keep +receiving events as long as the condition persists. Remember you can stop +the watcher if you don't want to act on the event and neither want to +receive future events. +.PP +In general you can register as many read and/or write event watchers per +fd as you want (as long as you don't confuse yourself). Setting all file +descriptors to non-blocking mode is also usually a good idea (but not +required if you know what you are doing). +.PP +Another thing you have to watch out for is that it is quite easy to +receive \*(L"spurious\*(R" readiness notifications, that is, your callback might +be called with \f(CW\*(C`EV_READ\*(C'\fR but a subsequent \f(CW\*(C`read\*(C'\fR(2) will actually block +because there is no data. It is very easy to get into this situation even +with a relatively standard program structure. Thus it is best to always +use non-blocking I/O: An extra \f(CW\*(C`read\*(C'\fR(2) returning \f(CW\*(C`EAGAIN\*(C'\fR is far +preferable to a program hanging until some data arrives. +.PP +If you cannot run the fd in non-blocking mode (for example you should +not play around with an Xlib connection), then you have to separately +re-test whether a file descriptor is really ready with a known-to-be good +interface such as poll (fortunately in the case of Xlib, it already does +this on its own, so its quite safe to use). Some people additionally +use \f(CW\*(C`SIGALRM\*(C'\fR and an interval timer, just to be sure you won't block +indefinitely. +.PP +But really, best use non-blocking mode. +.PP +\fIThe special problem of disappearing file descriptors\fR +.IX Subsection "The special problem of disappearing file descriptors" +.PP +Some backends (e.g. kqueue, epoll, linuxaio) need to be told about closing +a file descriptor (either due to calling \f(CW\*(C`close\*(C'\fR explicitly or any other +means, such as \f(CW\*(C`dup2\*(C'\fR). The reason is that you register interest in some +file descriptor, but when it goes away, the operating system will silently +drop this interest. If another file descriptor with the same number then +is registered with libev, there is no efficient way to see that this is, +in fact, a different file descriptor. +.PP +To avoid having to explicitly tell libev about such cases, libev follows +the following policy: Each time \f(CW\*(C`ev_io_set\*(C'\fR is being called, libev +will assume that this is potentially a new file descriptor, otherwise +it is assumed that the file descriptor stays the same. That means that +you \fIhave\fR to call \f(CW\*(C`ev_io_set\*(C'\fR (or \f(CW\*(C`ev_io_init\*(C'\fR) when you change the +descriptor even if the file descriptor number itself did not change. +.PP +This is how one would do it normally anyway, the important point is that +the libev application should not optimise around libev but should leave +optimisations to libev. +.PP +\fIThe special problem of dup'ed file descriptors\fR +.IX Subsection "The special problem of dup'ed file descriptors" +.PP +Some backends (e.g. epoll), cannot register events for file descriptors, +but only events for the underlying file descriptions. That means when you +have \f(CW\*(C`dup ()\*(C'\fR'ed file descriptors or weirder constellations, and register +events for them, only one file descriptor might actually receive events. +.PP +There is no workaround possible except not registering events +for potentially \f(CW\*(C`dup ()\*(C'\fR'ed file descriptors, or to resort to +\&\f(CW\*(C`EVBACKEND_SELECT\*(C'\fR or \f(CW\*(C`EVBACKEND_POLL\*(C'\fR. +.PP +\fIThe special problem of files\fR +.IX Subsection "The special problem of files" +.PP +Many people try to use \f(CW\*(C`select\*(C'\fR (or libev) on file descriptors +representing files, and expect it to become ready when their program +doesn't block on disk accesses (which can take a long time on their own). +.PP +However, this cannot ever work in the \*(L"expected\*(R" way \- you get a readiness +notification as soon as the kernel knows whether and how much data is +there, and in the case of open files, that's always the case, so you +always get a readiness notification instantly, and your read (or possibly +write) will still block on the disk I/O. +.PP +Another way to view it is that in the case of sockets, pipes, character +devices and so on, there is another party (the sender) that delivers data +on its own, but in the case of files, there is no such thing: the disk +will not send data on its own, simply because it doesn't know what you +wish to read \- you would first have to request some data. +.PP +Since files are typically not-so-well supported by advanced notification +mechanism, libev tries hard to emulate \s-1POSIX\s0 behaviour with respect +to files, even though you should not use it. The reason for this is +convenience: sometimes you want to watch \s-1STDIN\s0 or \s-1STDOUT,\s0 which is +usually a tty, often a pipe, but also sometimes files or special devices +(for example, \f(CW\*(C`epoll\*(C'\fR on Linux works with \fI/dev/random\fR but not with +\&\fI/dev/urandom\fR), and even though the file might better be served with +asynchronous I/O instead of with non-blocking I/O, it is still useful when +it \*(L"just works\*(R" instead of freezing. +.PP +So avoid file descriptors pointing to files when you know it (e.g. use +libeio), but use them when it is convenient, e.g. for \s-1STDIN/STDOUT,\s0 or +when you rarely read from a file instead of from a socket, and want to +reuse the same code path. +.PP +\fIThe special problem of fork\fR +.IX Subsection "The special problem of fork" +.PP +Some backends (epoll, kqueue, linuxaio, iouring) do not support \f(CW\*(C`fork ()\*(C'\fR +at all or exhibit useless behaviour. Libev fully supports fork, but needs +to be told about it in the child if you want to continue to use it in the +child. +.PP +To support fork in your child processes, you have to call \f(CW\*(C`ev_loop_fork +()\*(C'\fR after a fork in the child, enable \f(CW\*(C`EVFLAG_FORKCHECK\*(C'\fR, or resort to +\&\f(CW\*(C`EVBACKEND_SELECT\*(C'\fR or \f(CW\*(C`EVBACKEND_POLL\*(C'\fR. +.PP +\fIThe special problem of \s-1SIGPIPE\s0\fR +.IX Subsection "The special problem of SIGPIPE" +.PP +While not really specific to libev, it is easy to forget about \f(CW\*(C`SIGPIPE\*(C'\fR: +when writing to a pipe whose other end has been closed, your program gets +sent a \s-1SIGPIPE,\s0 which, by default, aborts your program. For most programs +this is sensible behaviour, for daemons, this is usually undesirable. +.PP +So when you encounter spurious, unexplained daemon exits, make sure you +ignore \s-1SIGPIPE\s0 (and maybe make sure you log the exit status of your daemon +somewhere, as that would have given you a big clue). +.PP +\fIThe special problem of \f(BIaccept()\fIing when you can't\fR +.IX Subsection "The special problem of accept()ing when you can't" +.PP +Many implementations of the \s-1POSIX\s0 \f(CW\*(C`accept\*(C'\fR function (for example, +found in post\-2004 Linux) have the peculiar behaviour of not removing a +connection from the pending queue in all error cases. +.PP +For example, larger servers often run out of file descriptors (because +of resource limits), causing \f(CW\*(C`accept\*(C'\fR to fail with \f(CW\*(C`ENFILE\*(C'\fR but not +rejecting the connection, leading to libev signalling readiness on +the next iteration again (the connection still exists after all), and +typically causing the program to loop at 100% \s-1CPU\s0 usage. +.PP +Unfortunately, the set of errors that cause this issue differs between +operating systems, there is usually little the app can do to remedy the +situation, and no known thread-safe method of removing the connection to +cope with overload is known (to me). +.PP +One of the easiest ways to handle this situation is to just ignore it +\&\- when the program encounters an overload, it will just loop until the +situation is over. While this is a form of busy waiting, no \s-1OS\s0 offers an +event-based way to handle this situation, so it's the best one can do. +.PP +A better way to handle the situation is to log any errors other than +\&\f(CW\*(C`EAGAIN\*(C'\fR and \f(CW\*(C`EWOULDBLOCK\*(C'\fR, making sure not to flood the log with such +messages, and continue as usual, which at least gives the user an idea of +what could be wrong (\*(L"raise the ulimit!\*(R"). For extra points one could stop +the \f(CW\*(C`ev_io\*(C'\fR watcher on the listening fd \*(L"for a while\*(R", which reduces \s-1CPU\s0 +usage. +.PP +If your program is single-threaded, then you could also keep a dummy file +descriptor for overload situations (e.g. by opening \fI/dev/null\fR), and +when you run into \f(CW\*(C`ENFILE\*(C'\fR or \f(CW\*(C`EMFILE\*(C'\fR, close it, run \f(CW\*(C`accept\*(C'\fR, +close that fd, and create a new dummy fd. This will gracefully refuse +clients under typical overload conditions. +.PP +The last way to handle it is to simply log the error and \f(CW\*(C`exit\*(C'\fR, as +is often done with \f(CW\*(C`malloc\*(C'\fR failures, but this results in an easy +opportunity for a DoS attack. +.PP +\fIWatcher-Specific Functions\fR +.IX Subsection "Watcher-Specific Functions" +.IP "ev_io_init (ev_io *, callback, int fd, int events)" 4 +.IX Item "ev_io_init (ev_io *, callback, int fd, int events)" +.PD 0 +.IP "ev_io_set (ev_io *, int fd, int events)" 4 +.IX Item "ev_io_set (ev_io *, int fd, int events)" +.PD +Configures an \f(CW\*(C`ev_io\*(C'\fR watcher. The \f(CW\*(C`fd\*(C'\fR is the file descriptor to +receive events for and \f(CW\*(C`events\*(C'\fR is either \f(CW\*(C`EV_READ\*(C'\fR, \f(CW\*(C`EV_WRITE\*(C'\fR, both +\&\f(CW\*(C`EV_READ | EV_WRITE\*(C'\fR or \f(CW0\fR, to express the desire to receive the given +events. +.Sp +Note that setting the \f(CW\*(C`events\*(C'\fR to \f(CW0\fR and starting the watcher is +supported, but not specially optimized \- if your program sometimes happens +to generate this combination this is fine, but if it is easy to avoid +starting an io watcher watching for no events you should do so. +.IP "ev_io_modify (ev_io *, int events)" 4 +.IX Item "ev_io_modify (ev_io *, int events)" +Similar to \f(CW\*(C`ev_io_set\*(C'\fR, but only changes the requested events. Using this +might be faster with some backends, as libev can assume that the \f(CW\*(C`fd\*(C'\fR +still refers to the same underlying file description, something it cannot +do when using \f(CW\*(C`ev_io_set\*(C'\fR. +.IP "int fd [no\-modify]" 4 +.IX Item "int fd [no-modify]" +The file descriptor being watched. While it can be read at any time, you +must not modify this member even when the watcher is stopped \- always use +\&\f(CW\*(C`ev_io_set\*(C'\fR for that. +.IP "int events [no\-modify]" 4 +.IX Item "int events [no-modify]" +The set of events the fd is being watched for, among other flags. Remember +that this is a bit set \- to test for \f(CW\*(C`EV_READ\*(C'\fR, use \f(CW\*(C`w\->events & +EV_READ\*(C'\fR, and similarly for \f(CW\*(C`EV_WRITE\*(C'\fR. +.Sp +As with \f(CW\*(C`fd\*(C'\fR, you must not modify this member even when the watcher is +stopped, always use \f(CW\*(C`ev_io_set\*(C'\fR or \f(CW\*(C`ev_io_modify\*(C'\fR for that. +.PP +\fIExamples\fR +.IX Subsection "Examples" +.PP +Example: Call \f(CW\*(C`stdin_readable_cb\*(C'\fR when \s-1STDIN_FILENO\s0 has become, well +readable, but only once. Since it is likely line-buffered, you could +attempt to read a whole line in the callback. +.PP +.Vb 6 +\& static void +\& stdin_readable_cb (struct ev_loop *loop, ev_io *w, int revents) +\& { +\& ev_io_stop (loop, w); +\& .. read from stdin here (or from w\->fd) and handle any I/O errors +\& } +\& +\& ... +\& struct ev_loop *loop = ev_default_init (0); +\& ev_io stdin_readable; +\& ev_io_init (&stdin_readable, stdin_readable_cb, STDIN_FILENO, EV_READ); +\& ev_io_start (loop, &stdin_readable); +\& ev_run (loop, 0); +.Ve +.ie n .SS """ev_timer"" \- relative and optionally repeating timeouts" +.el .SS "\f(CWev_timer\fP \- relative and optionally repeating timeouts" +.IX Subsection "ev_timer - relative and optionally repeating timeouts" +Timer watchers are simple relative timers that generate an event after a +given time, and optionally repeating in regular intervals after that. +.PP +The timers are based on real time, that is, if you register an event that +times out after an hour and you reset your system clock to January last +year, it will still time out after (roughly) one hour. \*(L"Roughly\*(R" because +detecting time jumps is hard, and some inaccuracies are unavoidable (the +monotonic clock option helps a lot here). +.PP +The callback is guaranteed to be invoked only \fIafter\fR its timeout has +passed (not \fIat\fR, so on systems with very low-resolution clocks this +might introduce a small delay, see \*(L"the special problem of being too +early\*(R", below). If multiple timers become ready during the same loop +iteration then the ones with earlier time-out values are invoked before +ones of the same priority with later time-out values (but this is no +longer true when a callback calls \f(CW\*(C`ev_run\*(C'\fR recursively). +.PP +\fIBe smart about timeouts\fR +.IX Subsection "Be smart about timeouts" +.PP +Many real-world problems involve some kind of timeout, usually for error +recovery. A typical example is an \s-1HTTP\s0 request \- if the other side hangs, +you want to raise some error after a while. +.PP +What follows are some ways to handle this problem, from obvious and +inefficient to smart and efficient. +.PP +In the following, a 60 second activity timeout is assumed \- a timeout that +gets reset to 60 seconds each time there is activity (e.g. each time some +data or other life sign was received). +.IP "1. Use a timer and stop, reinitialise and start it on activity." 4 +.IX Item "1. Use a timer and stop, reinitialise and start it on activity." +This is the most obvious, but not the most simple way: In the beginning, +start the watcher: +.Sp +.Vb 2 +\& ev_timer_init (timer, callback, 60., 0.); +\& ev_timer_start (loop, timer); +.Ve +.Sp +Then, each time there is some activity, \f(CW\*(C`ev_timer_stop\*(C'\fR it, initialise it +and start it again: +.Sp +.Vb 3 +\& ev_timer_stop (loop, timer); +\& ev_timer_set (timer, 60., 0.); +\& ev_timer_start (loop, timer); +.Ve +.Sp +This is relatively simple to implement, but means that each time there is +some activity, libev will first have to remove the timer from its internal +data structure and then add it again. Libev tries to be fast, but it's +still not a constant-time operation. +.ie n .IP "2. Use a timer and re-start it with ""ev_timer_again"" inactivity." 4 +.el .IP "2. Use a timer and re-start it with \f(CWev_timer_again\fR inactivity." 4 +.IX Item "2. Use a timer and re-start it with ev_timer_again inactivity." +This is the easiest way, and involves using \f(CW\*(C`ev_timer_again\*(C'\fR instead of +\&\f(CW\*(C`ev_timer_start\*(C'\fR. +.Sp +To implement this, configure an \f(CW\*(C`ev_timer\*(C'\fR with a \f(CW\*(C`repeat\*(C'\fR value +of \f(CW60\fR and then call \f(CW\*(C`ev_timer_again\*(C'\fR at start and each time you +successfully read or write some data. If you go into an idle state where +you do not expect data to travel on the socket, you can \f(CW\*(C`ev_timer_stop\*(C'\fR +the timer, and \f(CW\*(C`ev_timer_again\*(C'\fR will automatically restart it if need be. +.Sp +That means you can ignore both the \f(CW\*(C`ev_timer_start\*(C'\fR function and the +\&\f(CW\*(C`after\*(C'\fR argument to \f(CW\*(C`ev_timer_set\*(C'\fR, and only ever use the \f(CW\*(C`repeat\*(C'\fR +member and \f(CW\*(C`ev_timer_again\*(C'\fR. +.Sp +At start: +.Sp +.Vb 3 +\& ev_init (timer, callback); +\& timer\->repeat = 60.; +\& ev_timer_again (loop, timer); +.Ve +.Sp +Each time there is some activity: +.Sp +.Vb 1 +\& ev_timer_again (loop, timer); +.Ve +.Sp +It is even possible to change the time-out on the fly, regardless of +whether the watcher is active or not: +.Sp +.Vb 2 +\& timer\->repeat = 30.; +\& ev_timer_again (loop, timer); +.Ve +.Sp +This is slightly more efficient then stopping/starting the timer each time +you want to modify its timeout value, as libev does not have to completely +remove and re-insert the timer from/into its internal data structure. +.Sp +It is, however, even simpler than the \*(L"obvious\*(R" way to do it. +.IP "3. Let the timer time out, but then re-arm it as required." 4 +.IX Item "3. Let the timer time out, but then re-arm it as required." +This method is more tricky, but usually most efficient: Most timeouts are +relatively long compared to the intervals between other activity \- in +our example, within 60 seconds, there are usually many I/O events with +associated activity resets. +.Sp +In this case, it would be more efficient to leave the \f(CW\*(C`ev_timer\*(C'\fR alone, +but remember the time of last activity, and check for a real timeout only +within the callback: +.Sp +.Vb 3 +\& ev_tstamp timeout = 60.; +\& ev_tstamp last_activity; // time of last activity +\& ev_timer timer; +\& +\& static void +\& callback (EV_P_ ev_timer *w, int revents) +\& { +\& // calculate when the timeout would happen +\& ev_tstamp after = last_activity \- ev_now (EV_A) + timeout; +\& +\& // if negative, it means we the timeout already occurred +\& if (after < 0.) +\& { +\& // timeout occurred, take action +\& } +\& else +\& { +\& // callback was invoked, but there was some recent +\& // activity. simply restart the timer to time out +\& // after "after" seconds, which is the earliest time +\& // the timeout can occur. +\& ev_timer_set (w, after, 0.); +\& ev_timer_start (EV_A_ w); +\& } +\& } +.Ve +.Sp +To summarise the callback: first calculate in how many seconds the +timeout will occur (by calculating the absolute time when it would occur, +\&\f(CW\*(C`last_activity + timeout\*(C'\fR, and subtracting the current time, \f(CW\*(C`ev_now +(EV_A)\*(C'\fR from that). +.Sp +If this value is negative, then we are already past the timeout, i.e. we +timed out, and need to do whatever is needed in this case. +.Sp +Otherwise, we now the earliest time at which the timeout would trigger, +and simply start the timer with this timeout value. +.Sp +In other words, each time the callback is invoked it will check whether +the timeout occurred. If not, it will simply reschedule itself to check +again at the earliest time it could time out. Rinse. Repeat. +.Sp +This scheme causes more callback invocations (about one every 60 seconds +minus half the average time between activity), but virtually no calls to +libev to change the timeout. +.Sp +To start the machinery, simply initialise the watcher and set +\&\f(CW\*(C`last_activity\*(C'\fR to the current time (meaning there was some activity just +now), then call the callback, which will \*(L"do the right thing\*(R" and start +the timer: +.Sp +.Vb 3 +\& last_activity = ev_now (EV_A); +\& ev_init (&timer, callback); +\& callback (EV_A_ &timer, 0); +.Ve +.Sp +When there is some activity, simply store the current time in +\&\f(CW\*(C`last_activity\*(C'\fR, no libev calls at all: +.Sp +.Vb 2 +\& if (activity detected) +\& last_activity = ev_now (EV_A); +.Ve +.Sp +When your timeout value changes, then the timeout can be changed by simply +providing a new value, stopping the timer and calling the callback, which +will again do the right thing (for example, time out immediately :). +.Sp +.Vb 3 +\& timeout = new_value; +\& ev_timer_stop (EV_A_ &timer); +\& callback (EV_A_ &timer, 0); +.Ve +.Sp +This technique is slightly more complex, but in most cases where the +time-out is unlikely to be triggered, much more efficient. +.IP "4. Wee, just use a double-linked list for your timeouts." 4 +.IX Item "4. Wee, just use a double-linked list for your timeouts." +If there is not one request, but many thousands (millions...), all +employing some kind of timeout with the same timeout value, then one can +do even better: +.Sp +When starting the timeout, calculate the timeout value and put the timeout +at the \fIend\fR of the list. +.Sp +Then use an \f(CW\*(C`ev_timer\*(C'\fR to fire when the timeout at the \fIbeginning\fR of +the list is expected to fire (for example, using the technique #3). +.Sp +When there is some activity, remove the timer from the list, recalculate +the timeout, append it to the end of the list again, and make sure to +update the \f(CW\*(C`ev_timer\*(C'\fR if it was taken from the beginning of the list. +.Sp +This way, one can manage an unlimited number of timeouts in O(1) time for +starting, stopping and updating the timers, at the expense of a major +complication, and having to use a constant timeout. The constant timeout +ensures that the list stays sorted. +.PP +So which method the best? +.PP +Method #2 is a simple no-brain-required solution that is adequate in most +situations. Method #3 requires a bit more thinking, but handles many cases +better, and isn't very complicated either. In most case, choosing either +one is fine, with #3 being better in typical situations. +.PP +Method #1 is almost always a bad idea, and buys you nothing. Method #4 is +rather complicated, but extremely efficient, something that really pays +off after the first million or so of active timers, i.e. it's usually +overkill :) +.PP +\fIThe special problem of being too early\fR +.IX Subsection "The special problem of being too early" +.PP +If you ask a timer to call your callback after three seconds, then +you expect it to be invoked after three seconds \- but of course, this +cannot be guaranteed to infinite precision. Less obviously, it cannot be +guaranteed to any precision by libev \- imagine somebody suspending the +process with a \s-1STOP\s0 signal for a few hours for example. +.PP +So, libev tries to invoke your callback as soon as possible \fIafter\fR the +delay has occurred, but cannot guarantee this. +.PP +A less obvious failure mode is calling your callback too early: many event +loops compare timestamps with a \*(L"elapsed delay >= requested delay\*(R", but +this can cause your callback to be invoked much earlier than you would +expect. +.PP +To see why, imagine a system with a clock that only offers full second +resolution (think windows if you can't come up with a broken enough \s-1OS\s0 +yourself). If you schedule a one-second timer at the time 500.9, then the +event loop will schedule your timeout to elapse at a system time of 500 +(500.9 truncated to the resolution) + 1, or 501. +.PP +If an event library looks at the timeout 0.1s later, it will see \*(L"501 >= +501\*(R" and invoke the callback 0.1s after it was started, even though a +one-second delay was requested \- this is being \*(L"too early\*(R", despite best +intentions. +.PP +This is the reason why libev will never invoke the callback if the elapsed +delay equals the requested delay, but only when the elapsed delay is +larger than the requested delay. In the example above, libev would only invoke +the callback at system time 502, or 1.1s after the timer was started. +.PP +So, while libev cannot guarantee that your callback will be invoked +exactly when requested, it \fIcan\fR and \fIdoes\fR guarantee that the requested +delay has actually elapsed, or in other words, it always errs on the \*(L"too +late\*(R" side of things. +.PP +\fIThe special problem of time updates\fR +.IX Subsection "The special problem of time updates" +.PP +Establishing the current time is a costly operation (it usually takes +at least one system call): \s-1EV\s0 therefore updates its idea of the current +time only before and after \f(CW\*(C`ev_run\*(C'\fR collects new events, which causes a +growing difference between \f(CW\*(C`ev_now ()\*(C'\fR and \f(CW\*(C`ev_time ()\*(C'\fR when handling +lots of events in one iteration. +.PP +The relative timeouts are calculated relative to the \f(CW\*(C`ev_now ()\*(C'\fR +time. This is usually the right thing as this timestamp refers to the time +of the event triggering whatever timeout you are modifying/starting. If +you suspect event processing to be delayed and you \fIneed\fR to base the +timeout on the current time, use something like the following to adjust +for it: +.PP +.Vb 1 +\& ev_timer_set (&timer, after + (ev_time () \- ev_now ()), 0.); +.Ve +.PP +If the event loop is suspended for a long time, you can also force an +update of the time returned by \f(CW\*(C`ev_now ()\*(C'\fR by calling \f(CW\*(C`ev_now_update +()\*(C'\fR, although that will push the event time of all outstanding events +further into the future. +.PP +\fIThe special problem of unsynchronised clocks\fR +.IX Subsection "The special problem of unsynchronised clocks" +.PP +Modern systems have a variety of clocks \- libev itself uses the normal +\&\*(L"wall clock\*(R" clock and, if available, the monotonic clock (to avoid time +jumps). +.PP +Neither of these clocks is synchronised with each other or any other clock +on the system, so \f(CW\*(C`ev_time ()\*(C'\fR might return a considerably different time +than \f(CW\*(C`gettimeofday ()\*(C'\fR or \f(CW\*(C`time ()\*(C'\fR. On a GNU/Linux system, for example, +a call to \f(CW\*(C`gettimeofday\*(C'\fR might return a second count that is one higher +than a directly following call to \f(CW\*(C`time\*(C'\fR. +.PP +The moral of this is to only compare libev-related timestamps with +\&\f(CW\*(C`ev_time ()\*(C'\fR and \f(CW\*(C`ev_now ()\*(C'\fR, at least if you want better precision than +a second or so. +.PP +One more problem arises due to this lack of synchronisation: if libev uses +the system monotonic clock and you compare timestamps from \f(CW\*(C`ev_time\*(C'\fR +or \f(CW\*(C`ev_now\*(C'\fR from when you started your timer and when your callback is +invoked, you will find that sometimes the callback is a bit \*(L"early\*(R". +.PP +This is because \f(CW\*(C`ev_timer\*(C'\fRs work in real time, not wall clock time, so +libev makes sure your callback is not invoked before the delay happened, +\&\fImeasured according to the real time\fR, not the system clock. +.PP +If your timeouts are based on a physical timescale (e.g. \*(L"time out this +connection after 100 seconds\*(R") then this shouldn't bother you as it is +exactly the right behaviour. +.PP +If you want to compare wall clock/system timestamps to your timers, then +you need to use \f(CW\*(C`ev_periodic\*(C'\fRs, as these are based on the wall clock +time, where your comparisons will always generate correct results. +.PP +\fIThe special problems of suspended animation\fR +.IX Subsection "The special problems of suspended animation" +.PP +When you leave the server world it is quite customary to hit machines that +can suspend/hibernate \- what happens to the clocks during such a suspend? +.PP +Some quick tests made with a Linux 2.6.28 indicate that a suspend freezes +all processes, while the clocks (\f(CW\*(C`times\*(C'\fR, \f(CW\*(C`CLOCK_MONOTONIC\*(C'\fR) continue +to run until the system is suspended, but they will not advance while the +system is suspended. That means, on resume, it will be as if the program +was frozen for a few seconds, but the suspend time will not be counted +towards \f(CW\*(C`ev_timer\*(C'\fR when a monotonic clock source is used. The real time +clock advanced as expected, but if it is used as sole clocksource, then a +long suspend would be detected as a time jump by libev, and timers would +be adjusted accordingly. +.PP +I would not be surprised to see different behaviour in different between +operating systems, \s-1OS\s0 versions or even different hardware. +.PP +The other form of suspend (job control, or sending a \s-1SIGSTOP\s0) will see a +time jump in the monotonic clocks and the realtime clock. If the program +is suspended for a very long time, and monotonic clock sources are in use, +then you can expect \f(CW\*(C`ev_timer\*(C'\fRs to expire as the full suspension time +will be counted towards the timers. When no monotonic clock source is in +use, then libev will again assume a timejump and adjust accordingly. +.PP +It might be beneficial for this latter case to call \f(CW\*(C`ev_suspend\*(C'\fR +and \f(CW\*(C`ev_resume\*(C'\fR in code that handles \f(CW\*(C`SIGTSTP\*(C'\fR, to at least get +deterministic behaviour in this case (you can do nothing against +\&\f(CW\*(C`SIGSTOP\*(C'\fR). +.PP +\fIWatcher-Specific Functions and Data Members\fR +.IX Subsection "Watcher-Specific Functions and Data Members" +.IP "ev_timer_init (ev_timer *, callback, ev_tstamp after, ev_tstamp repeat)" 4 +.IX Item "ev_timer_init (ev_timer *, callback, ev_tstamp after, ev_tstamp repeat)" +.PD 0 +.IP "ev_timer_set (ev_timer *, ev_tstamp after, ev_tstamp repeat)" 4 +.IX Item "ev_timer_set (ev_timer *, ev_tstamp after, ev_tstamp repeat)" +.PD +Configure the timer to trigger after \f(CW\*(C`after\*(C'\fR seconds (fractional and +negative values are supported). If \f(CW\*(C`repeat\*(C'\fR is \f(CW0.\fR, then it will +automatically be stopped once the timeout is reached. If it is positive, +then the timer will automatically be configured to trigger again \f(CW\*(C`repeat\*(C'\fR +seconds later, again, and again, until stopped manually. +.Sp +The timer itself will do a best-effort at avoiding drift, that is, if +you configure a timer to trigger every 10 seconds, then it will normally +trigger at exactly 10 second intervals. If, however, your program cannot +keep up with the timer (because it takes longer than those 10 seconds to +do stuff) the timer will not fire more than once per event loop iteration. +.IP "ev_timer_again (loop, ev_timer *)" 4 +.IX Item "ev_timer_again (loop, ev_timer *)" +This will act as if the timer timed out, and restarts it again if it is +repeating. It basically works like calling \f(CW\*(C`ev_timer_stop\*(C'\fR, updating the +timeout to the \f(CW\*(C`repeat\*(C'\fR value and calling \f(CW\*(C`ev_timer_start\*(C'\fR. +.Sp +The exact semantics are as in the following rules, all of which will be +applied to the watcher: +.RS 4 +.IP "If the timer is pending, the pending status is always cleared." 4 +.IX Item "If the timer is pending, the pending status is always cleared." +.PD 0 +.IP "If the timer is started but non-repeating, stop it (as if it timed out, without invoking it)." 4 +.IX Item "If the timer is started but non-repeating, stop it (as if it timed out, without invoking it)." +.ie n .IP "If the timer is repeating, make the ""repeat"" value the new timeout and start the timer, if necessary." 4 +.el .IP "If the timer is repeating, make the \f(CWrepeat\fR value the new timeout and start the timer, if necessary." 4 +.IX Item "If the timer is repeating, make the repeat value the new timeout and start the timer, if necessary." +.RE +.RS 4 +.PD +.Sp +This sounds a bit complicated, see \*(L"Be smart about timeouts\*(R", above, for a +usage example. +.RE +.IP "ev_tstamp ev_timer_remaining (loop, ev_timer *)" 4 +.IX Item "ev_tstamp ev_timer_remaining (loop, ev_timer *)" +Returns the remaining time until a timer fires. If the timer is active, +then this time is relative to the current event loop time, otherwise it's +the timeout value currently configured. +.Sp +That is, after an \f(CW\*(C`ev_timer_set (w, 5, 7)\*(C'\fR, \f(CW\*(C`ev_timer_remaining\*(C'\fR returns +\&\f(CW5\fR. When the timer is started and one second passes, \f(CW\*(C`ev_timer_remaining\*(C'\fR +will return \f(CW4\fR. When the timer expires and is restarted, it will return +roughly \f(CW7\fR (likely slightly less as callback invocation takes some time, +too), and so on. +.IP "ev_tstamp repeat [read\-write]" 4 +.IX Item "ev_tstamp repeat [read-write]" +The current \f(CW\*(C`repeat\*(C'\fR value. Will be used each time the watcher times out +or \f(CW\*(C`ev_timer_again\*(C'\fR is called, and determines the next timeout (if any), +which is also when any modifications are taken into account. +.PP +\fIExamples\fR +.IX Subsection "Examples" +.PP +Example: Create a timer that fires after 60 seconds. +.PP +.Vb 5 +\& static void +\& one_minute_cb (struct ev_loop *loop, ev_timer *w, int revents) +\& { +\& .. one minute over, w is actually stopped right here +\& } +\& +\& ev_timer mytimer; +\& ev_timer_init (&mytimer, one_minute_cb, 60., 0.); +\& ev_timer_start (loop, &mytimer); +.Ve +.PP +Example: Create a timeout timer that times out after 10 seconds of +inactivity. +.PP +.Vb 5 +\& static void +\& timeout_cb (struct ev_loop *loop, ev_timer *w, int revents) +\& { +\& .. ten seconds without any activity +\& } +\& +\& ev_timer mytimer; +\& ev_timer_init (&mytimer, timeout_cb, 0., 10.); /* note, only repeat used */ +\& ev_timer_again (&mytimer); /* start timer */ +\& ev_run (loop, 0); +\& +\& // and in some piece of code that gets executed on any "activity": +\& // reset the timeout to start ticking again at 10 seconds +\& ev_timer_again (&mytimer); +.Ve +.ie n .SS """ev_periodic"" \- to cron or not to cron?" +.el .SS "\f(CWev_periodic\fP \- to cron or not to cron?" +.IX Subsection "ev_periodic - to cron or not to cron?" +Periodic watchers are also timers of a kind, but they are very versatile +(and unfortunately a bit complex). +.PP +Unlike \f(CW\*(C`ev_timer\*(C'\fR, periodic watchers are not based on real time (or +relative time, the physical time that passes) but on wall clock time +(absolute time, the thing you can read on your calendar or clock). The +difference is that wall clock time can run faster or slower than real +time, and time jumps are not uncommon (e.g. when you adjust your +wrist-watch). +.PP +You can tell a periodic watcher to trigger after some specific point +in time: for example, if you tell a periodic watcher to trigger \*(L"in 10 +seconds\*(R" (by specifying e.g. \f(CW\*(C`ev_now () + 10.\*(C'\fR, that is, an absolute time +not a delay) and then reset your system clock to January of the previous +year, then it will take a year or more to trigger the event (unlike an +\&\f(CW\*(C`ev_timer\*(C'\fR, which would still trigger roughly 10 seconds after starting +it, as it uses a relative timeout). +.PP +\&\f(CW\*(C`ev_periodic\*(C'\fR watchers can also be used to implement vastly more complex +timers, such as triggering an event on each \*(L"midnight, local time\*(R", or +other complicated rules. This cannot easily be done with \f(CW\*(C`ev_timer\*(C'\fR +watchers, as those cannot react to time jumps. +.PP +As with timers, the callback is guaranteed to be invoked only when the +point in time where it is supposed to trigger has passed. If multiple +timers become ready during the same loop iteration then the ones with +earlier time-out values are invoked before ones with later time-out values +(but this is no longer true when a callback calls \f(CW\*(C`ev_run\*(C'\fR recursively). +.PP +\fIWatcher-Specific Functions and Data Members\fR +.IX Subsection "Watcher-Specific Functions and Data Members" +.IP "ev_periodic_init (ev_periodic *, callback, ev_tstamp offset, ev_tstamp interval, reschedule_cb)" 4 +.IX Item "ev_periodic_init (ev_periodic *, callback, ev_tstamp offset, ev_tstamp interval, reschedule_cb)" +.PD 0 +.IP "ev_periodic_set (ev_periodic *, ev_tstamp offset, ev_tstamp interval, reschedule_cb)" 4 +.IX Item "ev_periodic_set (ev_periodic *, ev_tstamp offset, ev_tstamp interval, reschedule_cb)" +.PD +Lots of arguments, let's sort it out... There are basically three modes of +operation, and we will explain them from simplest to most complex: +.RS 4 +.IP "\(bu" 4 +absolute timer (offset = absolute time, interval = 0, reschedule_cb = 0) +.Sp +In this configuration the watcher triggers an event after the wall clock +time \f(CW\*(C`offset\*(C'\fR has passed. It will not repeat and will not adjust when a +time jump occurs, that is, if it is to be run at January 1st 2011 then it +will be stopped and invoked when the system clock reaches or surpasses +this point in time. +.IP "\(bu" 4 +repeating interval timer (offset = offset within interval, interval > 0, reschedule_cb = 0) +.Sp +In this mode the watcher will always be scheduled to time out at the next +\&\f(CW\*(C`offset + N * interval\*(C'\fR time (for some integer N, which can also be +negative) and then repeat, regardless of any time jumps. The \f(CW\*(C`offset\*(C'\fR +argument is merely an offset into the \f(CW\*(C`interval\*(C'\fR periods. +.Sp +This can be used to create timers that do not drift with respect to the +system clock, for example, here is an \f(CW\*(C`ev_periodic\*(C'\fR that triggers each +hour, on the hour (with respect to \s-1UTC\s0): +.Sp +.Vb 1 +\& ev_periodic_set (&periodic, 0., 3600., 0); +.Ve +.Sp +This doesn't mean there will always be 3600 seconds in between triggers, +but only that the callback will be called when the system time shows a +full hour (\s-1UTC\s0), or more correctly, when the system time is evenly divisible +by 3600. +.Sp +Another way to think about it (for the mathematically inclined) is that +\&\f(CW\*(C`ev_periodic\*(C'\fR will try to run the callback in this mode at the next possible +time where \f(CW\*(C`time = offset (mod interval)\*(C'\fR, regardless of any time jumps. +.Sp +The \f(CW\*(C`interval\*(C'\fR \fI\s-1MUST\s0\fR be positive, and for numerical stability, the +interval value should be higher than \f(CW\*(C`1/8192\*(C'\fR (which is around 100 +microseconds) and \f(CW\*(C`offset\*(C'\fR should be higher than \f(CW0\fR and should have +at most a similar magnitude as the current time (say, within a factor of +ten). Typical values for offset are, in fact, \f(CW0\fR or something between +\&\f(CW0\fR and \f(CW\*(C`interval\*(C'\fR, which is also the recommended range. +.Sp +Note also that there is an upper limit to how often a timer can fire (\s-1CPU\s0 +speed for example), so if \f(CW\*(C`interval\*(C'\fR is very small then timing stability +will of course deteriorate. Libev itself tries to be exact to be about one +millisecond (if the \s-1OS\s0 supports it and the machine is fast enough). +.IP "\(bu" 4 +manual reschedule mode (offset ignored, interval ignored, reschedule_cb = callback) +.Sp +In this mode the values for \f(CW\*(C`interval\*(C'\fR and \f(CW\*(C`offset\*(C'\fR are both being +ignored. Instead, each time the periodic watcher gets scheduled, the +reschedule callback will be called with the watcher as first, and the +current time as second argument. +.Sp +\&\s-1NOTE:\s0 \fIThis callback \s-1MUST NOT\s0 stop or destroy any periodic watcher, ever, +or make \s-1ANY\s0 other event loop modifications whatsoever, unless explicitly +allowed by documentation here\fR. +.Sp +If you need to stop it, return \f(CW\*(C`now + 1e30\*(C'\fR (or so, fudge fudge) and stop +it afterwards (e.g. by starting an \f(CW\*(C`ev_prepare\*(C'\fR watcher, which is the +only event loop modification you are allowed to do). +.Sp +The callback prototype is \f(CW\*(C`ev_tstamp (*reschedule_cb)(ev_periodic +*w, ev_tstamp now)\*(C'\fR, e.g.: +.Sp +.Vb 5 +\& static ev_tstamp +\& my_rescheduler (ev_periodic *w, ev_tstamp now) +\& { +\& return now + 60.; +\& } +.Ve +.Sp +It must return the next time to trigger, based on the passed time value +(that is, the lowest time value larger than to the second argument). It +will usually be called just before the callback will be triggered, but +might be called at other times, too. +.Sp +\&\s-1NOTE:\s0 \fIThis callback must always return a time that is higher than or +equal to the passed \f(CI\*(C`now\*(C'\fI value\fR. +.Sp +This can be used to create very complex timers, such as a timer that +triggers on \*(L"next midnight, local time\*(R". To do this, you would calculate +the next midnight after \f(CW\*(C`now\*(C'\fR and return the timestamp value for +this. Here is a (completely untested, no error checking) example on how to +do this: +.Sp +.Vb 1 +\& #include +\& +\& static ev_tstamp +\& my_rescheduler (ev_periodic *w, ev_tstamp now) +\& { +\& time_t tnow = (time_t)now; +\& struct tm tm; +\& localtime_r (&tnow, &tm); +\& +\& tm.tm_sec = tm.tm_min = tm.tm_hour = 0; // midnight current day +\& ++tm.tm_mday; // midnight next day +\& +\& return mktime (&tm); +\& } +.Ve +.Sp +Note: this code might run into trouble on days that have more then two +midnights (beginning and end). +.RE +.RS 4 +.RE +.IP "ev_periodic_again (loop, ev_periodic *)" 4 +.IX Item "ev_periodic_again (loop, ev_periodic *)" +Simply stops and restarts the periodic watcher again. This is only useful +when you changed some parameters or the reschedule callback would return +a different time than the last time it was called (e.g. in a crond like +program when the crontabs have changed). +.IP "ev_tstamp ev_periodic_at (ev_periodic *)" 4 +.IX Item "ev_tstamp ev_periodic_at (ev_periodic *)" +When active, returns the absolute time that the watcher is supposed +to trigger next. This is not the same as the \f(CW\*(C`offset\*(C'\fR argument to +\&\f(CW\*(C`ev_periodic_set\*(C'\fR, but indeed works even in interval and manual +rescheduling modes. +.IP "ev_tstamp offset [read\-write]" 4 +.IX Item "ev_tstamp offset [read-write]" +When repeating, this contains the offset value, otherwise this is the +absolute point in time (the \f(CW\*(C`offset\*(C'\fR value passed to \f(CW\*(C`ev_periodic_set\*(C'\fR, +although libev might modify this value for better numerical stability). +.Sp +Can be modified any time, but changes only take effect when the periodic +timer fires or \f(CW\*(C`ev_periodic_again\*(C'\fR is being called. +.IP "ev_tstamp interval [read\-write]" 4 +.IX Item "ev_tstamp interval [read-write]" +The current interval value. Can be modified any time, but changes only +take effect when the periodic timer fires or \f(CW\*(C`ev_periodic_again\*(C'\fR is being +called. +.IP "ev_tstamp (*reschedule_cb)(ev_periodic *w, ev_tstamp now) [read\-write]" 4 +.IX Item "ev_tstamp (*reschedule_cb)(ev_periodic *w, ev_tstamp now) [read-write]" +The current reschedule callback, or \f(CW0\fR, if this functionality is +switched off. Can be changed any time, but changes only take effect when +the periodic timer fires or \f(CW\*(C`ev_periodic_again\*(C'\fR is being called. +.PP +\fIExamples\fR +.IX Subsection "Examples" +.PP +Example: Call a callback every hour, or, more precisely, whenever the +system time is divisible by 3600. The callback invocation times have +potentially a lot of jitter, but good long-term stability. +.PP +.Vb 5 +\& static void +\& clock_cb (struct ev_loop *loop, ev_periodic *w, int revents) +\& { +\& ... its now a full hour (UTC, or TAI or whatever your clock follows) +\& } +\& +\& ev_periodic hourly_tick; +\& ev_periodic_init (&hourly_tick, clock_cb, 0., 3600., 0); +\& ev_periodic_start (loop, &hourly_tick); +.Ve +.PP +Example: The same as above, but use a reschedule callback to do it: +.PP +.Vb 1 +\& #include +\& +\& static ev_tstamp +\& my_scheduler_cb (ev_periodic *w, ev_tstamp now) +\& { +\& return now + (3600. \- fmod (now, 3600.)); +\& } +\& +\& ev_periodic_init (&hourly_tick, clock_cb, 0., 0., my_scheduler_cb); +.Ve +.PP +Example: Call a callback every hour, starting now: +.PP +.Vb 4 +\& ev_periodic hourly_tick; +\& ev_periodic_init (&hourly_tick, clock_cb, +\& fmod (ev_now (loop), 3600.), 3600., 0); +\& ev_periodic_start (loop, &hourly_tick); +.Ve +.ie n .SS """ev_signal"" \- signal me when a signal gets signalled!" +.el .SS "\f(CWev_signal\fP \- signal me when a signal gets signalled!" +.IX Subsection "ev_signal - signal me when a signal gets signalled!" +Signal watchers will trigger an event when the process receives a specific +signal one or more times. Even though signals are very asynchronous, libev +will try its best to deliver signals synchronously, i.e. as part of the +normal event processing, like any other event. +.PP +If you want signals to be delivered truly asynchronously, just use +\&\f(CW\*(C`sigaction\*(C'\fR as you would do without libev and forget about sharing +the signal. You can even use \f(CW\*(C`ev_async\*(C'\fR from a signal handler to +synchronously wake up an event loop. +.PP +You can configure as many watchers as you like for the same signal, but +only within the same loop, i.e. you can watch for \f(CW\*(C`SIGINT\*(C'\fR in your +default loop and for \f(CW\*(C`SIGIO\*(C'\fR in another loop, but you cannot watch for +\&\f(CW\*(C`SIGINT\*(C'\fR in both the default loop and another loop at the same time. At +the moment, \f(CW\*(C`SIGCHLD\*(C'\fR is permanently tied to the default loop. +.PP +Only after the first watcher for a signal is started will libev actually +register something with the kernel. It thus coexists with your own signal +handlers as long as you don't register any with libev for the same signal. +.PP +If possible and supported, libev will install its handlers with +\&\f(CW\*(C`SA_RESTART\*(C'\fR (or equivalent) behaviour enabled, so system calls should +not be unduly interrupted. If you have a problem with system calls getting +interrupted by signals you can block all signals in an \f(CW\*(C`ev_check\*(C'\fR watcher +and unblock them in an \f(CW\*(C`ev_prepare\*(C'\fR watcher. +.PP +\fIThe special problem of inheritance over fork/execve/pthread_create\fR +.IX Subsection "The special problem of inheritance over fork/execve/pthread_create" +.PP +Both the signal mask (\f(CW\*(C`sigprocmask\*(C'\fR) and the signal disposition +(\f(CW\*(C`sigaction\*(C'\fR) are unspecified after starting a signal watcher (and after +stopping it again), that is, libev might or might not block the signal, +and might or might not set or restore the installed signal handler (but +see \f(CW\*(C`EVFLAG_NOSIGMASK\*(C'\fR). +.PP +While this does not matter for the signal disposition (libev never +sets signals to \f(CW\*(C`SIG_IGN\*(C'\fR, so handlers will be reset to \f(CW\*(C`SIG_DFL\*(C'\fR on +\&\f(CW\*(C`execve\*(C'\fR), this matters for the signal mask: many programs do not expect +certain signals to be blocked. +.PP +This means that before calling \f(CW\*(C`exec\*(C'\fR (from the child) you should reset +the signal mask to whatever \*(L"default\*(R" you expect (all clear is a good +choice usually). +.PP +The simplest way to ensure that the signal mask is reset in the child is +to install a fork handler with \f(CW\*(C`pthread_atfork\*(C'\fR that resets it. That will +catch fork calls done by libraries (such as the libc) as well. +.PP +In current versions of libev, the signal will not be blocked indefinitely +unless you use the \f(CW\*(C`signalfd\*(C'\fR \s-1API\s0 (\f(CW\*(C`EV_SIGNALFD\*(C'\fR). While this reduces +the window of opportunity for problems, it will not go away, as libev +\&\fIhas\fR to modify the signal mask, at least temporarily. +.PP +So I can't stress this enough: \fIIf you do not reset your signal mask when +you expect it to be empty, you have a race condition in your code\fR. This +is not a libev-specific thing, this is true for most event libraries. +.PP +\fIThe special problem of threads signal handling\fR +.IX Subsection "The special problem of threads signal handling" +.PP +\&\s-1POSIX\s0 threads has problematic signal handling semantics, specifically, +a lot of functionality (sigfd, sigwait etc.) only really works if all +threads in a process block signals, which is hard to achieve. +.PP +When you want to use sigwait (or mix libev signal handling with your own +for the same signals), you can tackle this problem by globally blocking +all signals before creating any threads (or creating them with a fully set +sigprocmask) and also specifying the \f(CW\*(C`EVFLAG_NOSIGMASK\*(C'\fR when creating +loops. Then designate one thread as \*(L"signal receiver thread\*(R" which handles +these signals. You can pass on any signals that libev might be interested +in by calling \f(CW\*(C`ev_feed_signal\*(C'\fR. +.PP +\fIWatcher-Specific Functions and Data Members\fR +.IX Subsection "Watcher-Specific Functions and Data Members" +.IP "ev_signal_init (ev_signal *, callback, int signum)" 4 +.IX Item "ev_signal_init (ev_signal *, callback, int signum)" +.PD 0 +.IP "ev_signal_set (ev_signal *, int signum)" 4 +.IX Item "ev_signal_set (ev_signal *, int signum)" +.PD +Configures the watcher to trigger on the given signal number (usually one +of the \f(CW\*(C`SIGxxx\*(C'\fR constants). +.IP "int signum [read\-only]" 4 +.IX Item "int signum [read-only]" +The signal the watcher watches out for. +.PP +\fIExamples\fR +.IX Subsection "Examples" +.PP +Example: Try to exit cleanly on \s-1SIGINT.\s0 +.PP +.Vb 5 +\& static void +\& sigint_cb (struct ev_loop *loop, ev_signal *w, int revents) +\& { +\& ev_break (loop, EVBREAK_ALL); +\& } +\& +\& ev_signal signal_watcher; +\& ev_signal_init (&signal_watcher, sigint_cb, SIGINT); +\& ev_signal_start (loop, &signal_watcher); +.Ve +.ie n .SS """ev_child"" \- watch out for process status changes" +.el .SS "\f(CWev_child\fP \- watch out for process status changes" +.IX Subsection "ev_child - watch out for process status changes" +Child watchers trigger when your process receives a \s-1SIGCHLD\s0 in response to +some child status changes (most typically when a child of yours dies or +exits). It is permissible to install a child watcher \fIafter\fR the child +has been forked (which implies it might have already exited), as long +as the event loop isn't entered (or is continued from a watcher), i.e., +forking and then immediately registering a watcher for the child is fine, +but forking and registering a watcher a few event loop iterations later or +in the next callback invocation is not. +.PP +Only the default event loop is capable of handling signals, and therefore +you can only register child watchers in the default event loop. +.PP +Due to some design glitches inside libev, child watchers will always be +handled at maximum priority (their priority is set to \f(CW\*(C`EV_MAXPRI\*(C'\fR by +libev) +.PP +\fIProcess Interaction\fR +.IX Subsection "Process Interaction" +.PP +Libev grabs \f(CW\*(C`SIGCHLD\*(C'\fR as soon as the default event loop is +initialised. This is necessary to guarantee proper behaviour even if the +first child watcher is started after the child exits. The occurrence +of \f(CW\*(C`SIGCHLD\*(C'\fR is recorded asynchronously, but child reaping is done +synchronously as part of the event loop processing. Libev always reaps all +children, even ones not watched. +.PP +\fIOverriding the Built-In Processing\fR +.IX Subsection "Overriding the Built-In Processing" +.PP +Libev offers no special support for overriding the built-in child +processing, but if your application collides with libev's default child +handler, you can override it easily by installing your own handler for +\&\f(CW\*(C`SIGCHLD\*(C'\fR after initialising the default loop, and making sure the +default loop never gets destroyed. You are encouraged, however, to use an +event-based approach to child reaping and thus use libev's support for +that, so other libev users can use \f(CW\*(C`ev_child\*(C'\fR watchers freely. +.PP +\fIStopping the Child Watcher\fR +.IX Subsection "Stopping the Child Watcher" +.PP +Currently, the child watcher never gets stopped, even when the +child terminates, so normally one needs to stop the watcher in the +callback. Future versions of libev might stop the watcher automatically +when a child exit is detected (calling \f(CW\*(C`ev_child_stop\*(C'\fR twice is not a +problem). +.PP +\fIWatcher-Specific Functions and Data Members\fR +.IX Subsection "Watcher-Specific Functions and Data Members" +.IP "ev_child_init (ev_child *, callback, int pid, int trace)" 4 +.IX Item "ev_child_init (ev_child *, callback, int pid, int trace)" +.PD 0 +.IP "ev_child_set (ev_child *, int pid, int trace)" 4 +.IX Item "ev_child_set (ev_child *, int pid, int trace)" +.PD +Configures the watcher to wait for status changes of process \f(CW\*(C`pid\*(C'\fR (or +\&\fIany\fR process if \f(CW\*(C`pid\*(C'\fR is specified as \f(CW0\fR). The callback can look +at the \f(CW\*(C`rstatus\*(C'\fR member of the \f(CW\*(C`ev_child\*(C'\fR watcher structure to see +the status word (use the macros from \f(CW\*(C`sys/wait.h\*(C'\fR and see your systems +\&\f(CW\*(C`waitpid\*(C'\fR documentation). The \f(CW\*(C`rpid\*(C'\fR member contains the pid of the +process causing the status change. \f(CW\*(C`trace\*(C'\fR must be either \f(CW0\fR (only +activate the watcher when the process terminates) or \f(CW1\fR (additionally +activate the watcher when the process is stopped or continued). +.IP "int pid [read\-only]" 4 +.IX Item "int pid [read-only]" +The process id this watcher watches out for, or \f(CW0\fR, meaning any process id. +.IP "int rpid [read\-write]" 4 +.IX Item "int rpid [read-write]" +The process id that detected a status change. +.IP "int rstatus [read\-write]" 4 +.IX Item "int rstatus [read-write]" +The process exit/trace status caused by \f(CW\*(C`rpid\*(C'\fR (see your systems +\&\f(CW\*(C`waitpid\*(C'\fR and \f(CW\*(C`sys/wait.h\*(C'\fR documentation for details). +.PP +\fIExamples\fR +.IX Subsection "Examples" +.PP +Example: \f(CW\*(C`fork()\*(C'\fR a new process and install a child handler to wait for +its completion. +.PP +.Vb 1 +\& ev_child cw; +\& +\& static void +\& child_cb (EV_P_ ev_child *w, int revents) +\& { +\& ev_child_stop (EV_A_ w); +\& printf ("process %d exited with status %x\en", w\->rpid, w\->rstatus); +\& } +\& +\& pid_t pid = fork (); +\& +\& if (pid < 0) +\& // error +\& else if (pid == 0) +\& { +\& // the forked child executes here +\& exit (1); +\& } +\& else +\& { +\& ev_child_init (&cw, child_cb, pid, 0); +\& ev_child_start (EV_DEFAULT_ &cw); +\& } +.Ve +.ie n .SS """ev_stat"" \- did the file attributes just change?" +.el .SS "\f(CWev_stat\fP \- did the file attributes just change?" +.IX Subsection "ev_stat - did the file attributes just change?" +This watches a file system path for attribute changes. That is, it calls +\&\f(CW\*(C`stat\*(C'\fR on that path in regular intervals (or when the \s-1OS\s0 says it changed) +and sees if it changed compared to the last time, invoking the callback +if it did. Starting the watcher \f(CW\*(C`stat\*(C'\fR's the file, so only changes that +happen after the watcher has been started will be reported. +.PP +The path does not need to exist: changing from \*(L"path exists\*(R" to \*(L"path does +not exist\*(R" is a status change like any other. The condition \*(L"path does not +exist\*(R" (or more correctly \*(L"path cannot be stat'ed\*(R") is signified by the +\&\f(CW\*(C`st_nlink\*(C'\fR field being zero (which is otherwise always forced to be at +least one) and all the other fields of the stat buffer having unspecified +contents. +.PP +The path \fImust not\fR end in a slash or contain special components such as +\&\f(CW\*(C`.\*(C'\fR or \f(CW\*(C`..\*(C'\fR. The path \fIshould\fR be absolute: If it is relative and +your working directory changes, then the behaviour is undefined. +.PP +Since there is no portable change notification interface available, the +portable implementation simply calls \f(CWstat(2)\fR regularly on the path +to see if it changed somehow. You can specify a recommended polling +interval for this case. If you specify a polling interval of \f(CW0\fR (highly +recommended!) then a \fIsuitable, unspecified default\fR value will be used +(which you can expect to be around five seconds, although this might +change dynamically). Libev will also impose a minimum interval which is +currently around \f(CW0.1\fR, but that's usually overkill. +.PP +This watcher type is not meant for massive numbers of stat watchers, +as even with OS-supported change notifications, this can be +resource-intensive. +.PP +At the time of this writing, the only OS-specific interface implemented +is the Linux inotify interface (implementing kqueue support is left as an +exercise for the reader. Note, however, that the author sees no way of +implementing \f(CW\*(C`ev_stat\*(C'\fR semantics with kqueue, except as a hint). +.PP +\fI\s-1ABI\s0 Issues (Largefile Support)\fR +.IX Subsection "ABI Issues (Largefile Support)" +.PP +Libev by default (unless the user overrides this) uses the default +compilation environment, which means that on systems with large file +support disabled by default, you get the 32 bit version of the stat +structure. When using the library from programs that change the \s-1ABI\s0 to +use 64 bit file offsets the programs will fail. In that case you have to +compile libev with the same flags to get binary compatibility. This is +obviously the case with any flags that change the \s-1ABI,\s0 but the problem is +most noticeably displayed with ev_stat and large file support. +.PP +The solution for this is to lobby your distribution maker to make large +file interfaces available by default (as e.g. FreeBSD does) and not +optional. Libev cannot simply switch on large file support because it has +to exchange stat structures with application programs compiled using the +default compilation environment. +.PP +\fIInotify and Kqueue\fR +.IX Subsection "Inotify and Kqueue" +.PP +When \f(CW\*(C`inotify (7)\*(C'\fR support has been compiled into libev and present at +runtime, it will be used to speed up change detection where possible. The +inotify descriptor will be created lazily when the first \f(CW\*(C`ev_stat\*(C'\fR +watcher is being started. +.PP +Inotify presence does not change the semantics of \f(CW\*(C`ev_stat\*(C'\fR watchers +except that changes might be detected earlier, and in some cases, to avoid +making regular \f(CW\*(C`stat\*(C'\fR calls. Even in the presence of inotify support +there are many cases where libev has to resort to regular \f(CW\*(C`stat\*(C'\fR polling, +but as long as kernel 2.6.25 or newer is used (2.6.24 and older have too +many bugs), the path exists (i.e. stat succeeds), and the path resides on +a local filesystem (libev currently assumes only ext2/3, jfs, reiserfs and +xfs are fully working) libev usually gets away without polling. +.PP +There is no support for kqueue, as apparently it cannot be used to +implement this functionality, due to the requirement of having a file +descriptor open on the object at all times, and detecting renames, unlinks +etc. is difficult. +.PP +\fI\f(CI\*(C`stat ()\*(C'\fI is a synchronous operation\fR +.IX Subsection "stat () is a synchronous operation" +.PP +Libev doesn't normally do any kind of I/O itself, and so is not blocking +the process. The exception are \f(CW\*(C`ev_stat\*(C'\fR watchers \- those call \f(CW\*(C`stat +()\*(C'\fR, which is a synchronous operation. +.PP +For local paths, this usually doesn't matter: unless the system is very +busy or the intervals between stat's are large, a stat call will be fast, +as the path data is usually in memory already (except when starting the +watcher). +.PP +For networked file systems, calling \f(CW\*(C`stat ()\*(C'\fR can block an indefinite +time due to network issues, and even under good conditions, a stat call +often takes multiple milliseconds. +.PP +Therefore, it is best to avoid using \f(CW\*(C`ev_stat\*(C'\fR watchers on networked +paths, although this is fully supported by libev. +.PP +\fIThe special problem of stat time resolution\fR +.IX Subsection "The special problem of stat time resolution" +.PP +The \f(CW\*(C`stat ()\*(C'\fR system call only supports full-second resolution portably, +and even on systems where the resolution is higher, most file systems +still only support whole seconds. +.PP +That means that, if the time is the only thing that changes, you can +easily miss updates: on the first update, \f(CW\*(C`ev_stat\*(C'\fR detects a change and +calls your callback, which does something. When there is another update +within the same second, \f(CW\*(C`ev_stat\*(C'\fR will be unable to detect unless the +stat data does change in other ways (e.g. file size). +.PP +The solution to this is to delay acting on a change for slightly more +than a second (or till slightly after the next full second boundary), using +a roughly one-second-delay \f(CW\*(C`ev_timer\*(C'\fR (e.g. \f(CW\*(C`ev_timer_set (w, 0., 1.02); +ev_timer_again (loop, w)\*(C'\fR). +.PP +The \f(CW.02\fR offset is added to work around small timing inconsistencies +of some operating systems (where the second counter of the current time +might be be delayed. One such system is the Linux kernel, where a call to +\&\f(CW\*(C`gettimeofday\*(C'\fR might return a timestamp with a full second later than +a subsequent \f(CW\*(C`time\*(C'\fR call \- if the equivalent of \f(CW\*(C`time ()\*(C'\fR is used to +update file times then there will be a small window where the kernel uses +the previous second to update file times but libev might already execute +the timer callback). +.PP +\fIWatcher-Specific Functions and Data Members\fR +.IX Subsection "Watcher-Specific Functions and Data Members" +.IP "ev_stat_init (ev_stat *, callback, const char *path, ev_tstamp interval)" 4 +.IX Item "ev_stat_init (ev_stat *, callback, const char *path, ev_tstamp interval)" +.PD 0 +.IP "ev_stat_set (ev_stat *, const char *path, ev_tstamp interval)" 4 +.IX Item "ev_stat_set (ev_stat *, const char *path, ev_tstamp interval)" +.PD +Configures the watcher to wait for status changes of the given +\&\f(CW\*(C`path\*(C'\fR. The \f(CW\*(C`interval\*(C'\fR is a hint on how quickly a change is expected to +be detected and should normally be specified as \f(CW0\fR to let libev choose +a suitable value. The memory pointed to by \f(CW\*(C`path\*(C'\fR must point to the same +path for as long as the watcher is active. +.Sp +The callback will receive an \f(CW\*(C`EV_STAT\*(C'\fR event when a change was detected, +relative to the attributes at the time the watcher was started (or the +last change was detected). +.IP "ev_stat_stat (loop, ev_stat *)" 4 +.IX Item "ev_stat_stat (loop, ev_stat *)" +Updates the stat buffer immediately with new values. If you change the +watched path in your callback, you could call this function to avoid +detecting this change (while introducing a race condition if you are not +the only one changing the path). Can also be useful simply to find out the +new values. +.IP "ev_statdata attr [read\-only]" 4 +.IX Item "ev_statdata attr [read-only]" +The most-recently detected attributes of the file. Although the type is +\&\f(CW\*(C`ev_statdata\*(C'\fR, this is usually the (or one of the) \f(CW\*(C`struct stat\*(C'\fR types +suitable for your system, but you can only rely on the POSIX-standardised +members to be present. If the \f(CW\*(C`st_nlink\*(C'\fR member is \f(CW0\fR, then there was +some error while \f(CW\*(C`stat\*(C'\fRing the file. +.IP "ev_statdata prev [read\-only]" 4 +.IX Item "ev_statdata prev [read-only]" +The previous attributes of the file. The callback gets invoked whenever +\&\f(CW\*(C`prev\*(C'\fR != \f(CW\*(C`attr\*(C'\fR, or, more precisely, one or more of these members +differ: \f(CW\*(C`st_dev\*(C'\fR, \f(CW\*(C`st_ino\*(C'\fR, \f(CW\*(C`st_mode\*(C'\fR, \f(CW\*(C`st_nlink\*(C'\fR, \f(CW\*(C`st_uid\*(C'\fR, +\&\f(CW\*(C`st_gid\*(C'\fR, \f(CW\*(C`st_rdev\*(C'\fR, \f(CW\*(C`st_size\*(C'\fR, \f(CW\*(C`st_atime\*(C'\fR, \f(CW\*(C`st_mtime\*(C'\fR, \f(CW\*(C`st_ctime\*(C'\fR. +.IP "ev_tstamp interval [read\-only]" 4 +.IX Item "ev_tstamp interval [read-only]" +The specified interval. +.IP "const char *path [read\-only]" 4 +.IX Item "const char *path [read-only]" +The file system path that is being watched. +.PP +\fIExamples\fR +.IX Subsection "Examples" +.PP +Example: Watch \f(CW\*(C`/etc/passwd\*(C'\fR for attribute changes. +.PP +.Vb 10 +\& static void +\& passwd_cb (struct ev_loop *loop, ev_stat *w, int revents) +\& { +\& /* /etc/passwd changed in some way */ +\& if (w\->attr.st_nlink) +\& { +\& printf ("passwd current size %ld\en", (long)w\->attr.st_size); +\& printf ("passwd current atime %ld\en", (long)w\->attr.st_mtime); +\& printf ("passwd current mtime %ld\en", (long)w\->attr.st_mtime); +\& } +\& else +\& /* you shalt not abuse printf for puts */ +\& puts ("wow, /etc/passwd is not there, expect problems. " +\& "if this is windows, they already arrived\en"); +\& } +\& +\& ... +\& ev_stat passwd; +\& +\& ev_stat_init (&passwd, passwd_cb, "/etc/passwd", 0.); +\& ev_stat_start (loop, &passwd); +.Ve +.PP +Example: Like above, but additionally use a one-second delay so we do not +miss updates (however, frequent updates will delay processing, too, so +one might do the work both on \f(CW\*(C`ev_stat\*(C'\fR callback invocation \fIand\fR on +\&\f(CW\*(C`ev_timer\*(C'\fR callback invocation). +.PP +.Vb 2 +\& static ev_stat passwd; +\& static ev_timer timer; +\& +\& static void +\& timer_cb (EV_P_ ev_timer *w, int revents) +\& { +\& ev_timer_stop (EV_A_ w); +\& +\& /* now it\*(Aqs one second after the most recent passwd change */ +\& } +\& +\& static void +\& stat_cb (EV_P_ ev_stat *w, int revents) +\& { +\& /* reset the one\-second timer */ +\& ev_timer_again (EV_A_ &timer); +\& } +\& +\& ... +\& ev_stat_init (&passwd, stat_cb, "/etc/passwd", 0.); +\& ev_stat_start (loop, &passwd); +\& ev_timer_init (&timer, timer_cb, 0., 1.02); +.Ve +.ie n .SS """ev_idle"" \- when you've got nothing better to do..." +.el .SS "\f(CWev_idle\fP \- when you've got nothing better to do..." +.IX Subsection "ev_idle - when you've got nothing better to do..." +Idle watchers trigger events when no other events of the same or higher +priority are pending (prepare, check and other idle watchers do not count +as receiving \*(L"events\*(R"). +.PP +That is, as long as your process is busy handling sockets or timeouts +(or even signals, imagine) of the same or higher priority it will not be +triggered. But when your process is idle (or only lower-priority watchers +are pending), the idle watchers are being called once per event loop +iteration \- until stopped, that is, or your process receives more events +and becomes busy again with higher priority stuff. +.PP +The most noteworthy effect is that as long as any idle watchers are +active, the process will not block when waiting for new events. +.PP +Apart from keeping your process non-blocking (which is a useful +effect on its own sometimes), idle watchers are a good place to do +\&\*(L"pseudo-background processing\*(R", or delay processing stuff to after the +event loop has handled all outstanding events. +.PP +\fIAbusing an \f(CI\*(C`ev_idle\*(C'\fI watcher for its side-effect\fR +.IX Subsection "Abusing an ev_idle watcher for its side-effect" +.PP +As long as there is at least one active idle watcher, libev will never +sleep unnecessarily. Or in other words, it will loop as fast as possible. +For this to work, the idle watcher doesn't need to be invoked at all \- the +lowest priority will do. +.PP +This mode of operation can be useful together with an \f(CW\*(C`ev_check\*(C'\fR watcher, +to do something on each event loop iteration \- for example to balance load +between different connections. +.PP +See \*(L"Abusing an ev_check watcher for its side-effect\*(R" for a longer +example. +.PP +\fIWatcher-Specific Functions and Data Members\fR +.IX Subsection "Watcher-Specific Functions and Data Members" +.IP "ev_idle_init (ev_idle *, callback)" 4 +.IX Item "ev_idle_init (ev_idle *, callback)" +Initialises and configures the idle watcher \- it has no parameters of any +kind. There is a \f(CW\*(C`ev_idle_set\*(C'\fR macro, but using it is utterly pointless, +believe me. +.PP +\fIExamples\fR +.IX Subsection "Examples" +.PP +Example: Dynamically allocate an \f(CW\*(C`ev_idle\*(C'\fR watcher, start it, and in the +callback, free it. Also, use no error checking, as usual. +.PP +.Vb 5 +\& static void +\& idle_cb (struct ev_loop *loop, ev_idle *w, int revents) +\& { +\& // stop the watcher +\& ev_idle_stop (loop, w); +\& +\& // now we can free it +\& free (w); +\& +\& // now do something you wanted to do when the program has +\& // no longer anything immediate to do. +\& } +\& +\& ev_idle *idle_watcher = malloc (sizeof (ev_idle)); +\& ev_idle_init (idle_watcher, idle_cb); +\& ev_idle_start (loop, idle_watcher); +.Ve +.ie n .SS """ev_prepare"" and ""ev_check"" \- customise your event loop!" +.el .SS "\f(CWev_prepare\fP and \f(CWev_check\fP \- customise your event loop!" +.IX Subsection "ev_prepare and ev_check - customise your event loop!" +Prepare and check watchers are often (but not always) used in pairs: +prepare watchers get invoked before the process blocks and check watchers +afterwards. +.PP +You \fImust not\fR call \f(CW\*(C`ev_run\*(C'\fR (or similar functions that enter the +current event loop) or \f(CW\*(C`ev_loop_fork\*(C'\fR from either \f(CW\*(C`ev_prepare\*(C'\fR or +\&\f(CW\*(C`ev_check\*(C'\fR watchers. Other loops than the current one are fine, +however. The rationale behind this is that you do not need to check +for recursion in those watchers, i.e. the sequence will always be +\&\f(CW\*(C`ev_prepare\*(C'\fR, blocking, \f(CW\*(C`ev_check\*(C'\fR so if you have one watcher of each +kind they will always be called in pairs bracketing the blocking call. +.PP +Their main purpose is to integrate other event mechanisms into libev and +their use is somewhat advanced. They could be used, for example, to track +variable changes, implement your own watchers, integrate net-snmp or a +coroutine library and lots more. They are also occasionally useful if +you cache some data and want to flush it before blocking (for example, +in X programs you might want to do an \f(CW\*(C`XFlush ()\*(C'\fR in an \f(CW\*(C`ev_prepare\*(C'\fR +watcher). +.PP +This is done by examining in each prepare call which file descriptors +need to be watched by the other library, registering \f(CW\*(C`ev_io\*(C'\fR watchers +for them and starting an \f(CW\*(C`ev_timer\*(C'\fR watcher for any timeouts (many +libraries provide exactly this functionality). Then, in the check watcher, +you check for any events that occurred (by checking the pending status +of all watchers and stopping them) and call back into the library. The +I/O and timer callbacks will never actually be called (but must be valid +nevertheless, because you never know, you know?). +.PP +As another example, the Perl Coro module uses these hooks to integrate +coroutines into libev programs, by yielding to other active coroutines +during each prepare and only letting the process block if no coroutines +are ready to run (it's actually more complicated: it only runs coroutines +with priority higher than or equal to the event loop and one coroutine +of lower priority, but only once, using idle watchers to keep the event +loop from blocking if lower-priority coroutines are active, thus mapping +low-priority coroutines to idle/background tasks). +.PP +When used for this purpose, it is recommended to give \f(CW\*(C`ev_check\*(C'\fR watchers +highest (\f(CW\*(C`EV_MAXPRI\*(C'\fR) priority, to ensure that they are being run before +any other watchers after the poll (this doesn't matter for \f(CW\*(C`ev_prepare\*(C'\fR +watchers). +.PP +Also, \f(CW\*(C`ev_check\*(C'\fR watchers (and \f(CW\*(C`ev_prepare\*(C'\fR watchers, too) should not +activate (\*(L"feed\*(R") events into libev. While libev fully supports this, they +might get executed before other \f(CW\*(C`ev_check\*(C'\fR watchers did their job. As +\&\f(CW\*(C`ev_check\*(C'\fR watchers are often used to embed other (non-libev) event +loops those other event loops might be in an unusable state until their +\&\f(CW\*(C`ev_check\*(C'\fR watcher ran (always remind yourself to coexist peacefully with +others). +.PP +\fIAbusing an \f(CI\*(C`ev_check\*(C'\fI watcher for its side-effect\fR +.IX Subsection "Abusing an ev_check watcher for its side-effect" +.PP +\&\f(CW\*(C`ev_check\*(C'\fR (and less often also \f(CW\*(C`ev_prepare\*(C'\fR) watchers can also be +useful because they are called once per event loop iteration. For +example, if you want to handle a large number of connections fairly, you +normally only do a bit of work for each active connection, and if there +is more work to do, you wait for the next event loop iteration, so other +connections have a chance of making progress. +.PP +Using an \f(CW\*(C`ev_check\*(C'\fR watcher is almost enough: it will be called on the +next event loop iteration. However, that isn't as soon as possible \- +without external events, your \f(CW\*(C`ev_check\*(C'\fR watcher will not be invoked. +.PP +This is where \f(CW\*(C`ev_idle\*(C'\fR watchers come in handy \- all you need is a +single global idle watcher that is active as long as you have one active +\&\f(CW\*(C`ev_check\*(C'\fR watcher. The \f(CW\*(C`ev_idle\*(C'\fR watcher makes sure the event loop +will not sleep, and the \f(CW\*(C`ev_check\*(C'\fR watcher makes sure a callback gets +invoked. Neither watcher alone can do that. +.PP +\fIWatcher-Specific Functions and Data Members\fR +.IX Subsection "Watcher-Specific Functions and Data Members" +.IP "ev_prepare_init (ev_prepare *, callback)" 4 +.IX Item "ev_prepare_init (ev_prepare *, callback)" +.PD 0 +.IP "ev_check_init (ev_check *, callback)" 4 +.IX Item "ev_check_init (ev_check *, callback)" +.PD +Initialises and configures the prepare or check watcher \- they have no +parameters of any kind. There are \f(CW\*(C`ev_prepare_set\*(C'\fR and \f(CW\*(C`ev_check_set\*(C'\fR +macros, but using them is utterly, utterly, utterly and completely +pointless. +.PP +\fIExamples\fR +.IX Subsection "Examples" +.PP +There are a number of principal ways to embed other event loops or modules +into libev. Here are some ideas on how to include libadns into libev +(there is a Perl module named \f(CW\*(C`EV::ADNS\*(C'\fR that does this, which you could +use as a working example. Another Perl module named \f(CW\*(C`EV::Glib\*(C'\fR embeds a +Glib main context into libev, and finally, \f(CW\*(C`Glib::EV\*(C'\fR embeds \s-1EV\s0 into the +Glib event loop). +.PP +Method 1: Add \s-1IO\s0 watchers and a timeout watcher in a prepare handler, +and in a check watcher, destroy them and call into libadns. What follows +is pseudo-code only of course. This requires you to either use a low +priority for the check watcher or use \f(CW\*(C`ev_clear_pending\*(C'\fR explicitly, as +the callbacks for the IO/timeout watchers might not have been called yet. +.PP +.Vb 2 +\& static ev_io iow [nfd]; +\& static ev_timer tw; +\& +\& static void +\& io_cb (struct ev_loop *loop, ev_io *w, int revents) +\& { +\& } +\& +\& // create io watchers for each fd and a timer before blocking +\& static void +\& adns_prepare_cb (struct ev_loop *loop, ev_prepare *w, int revents) +\& { +\& int timeout = 3600000; +\& struct pollfd fds [nfd]; +\& // actual code will need to loop here and realloc etc. +\& adns_beforepoll (ads, fds, &nfd, &timeout, timeval_from (ev_time ())); +\& +\& /* the callback is illegal, but won\*(Aqt be called as we stop during check */ +\& ev_timer_init (&tw, 0, timeout * 1e\-3, 0.); +\& ev_timer_start (loop, &tw); +\& +\& // create one ev_io per pollfd +\& for (int i = 0; i < nfd; ++i) +\& { +\& ev_io_init (iow + i, io_cb, fds [i].fd, +\& ((fds [i].events & POLLIN ? EV_READ : 0) +\& | (fds [i].events & POLLOUT ? EV_WRITE : 0))); +\& +\& fds [i].revents = 0; +\& ev_io_start (loop, iow + i); +\& } +\& } +\& +\& // stop all watchers after blocking +\& static void +\& adns_check_cb (struct ev_loop *loop, ev_check *w, int revents) +\& { +\& ev_timer_stop (loop, &tw); +\& +\& for (int i = 0; i < nfd; ++i) +\& { +\& // set the relevant poll flags +\& // could also call adns_processreadable etc. here +\& struct pollfd *fd = fds + i; +\& int revents = ev_clear_pending (iow + i); +\& if (revents & EV_READ ) fd\->revents |= fd\->events & POLLIN; +\& if (revents & EV_WRITE) fd\->revents |= fd\->events & POLLOUT; +\& +\& // now stop the watcher +\& ev_io_stop (loop, iow + i); +\& } +\& +\& adns_afterpoll (adns, fds, nfd, timeval_from (ev_now (loop)); +\& } +.Ve +.PP +Method 2: This would be just like method 1, but you run \f(CW\*(C`adns_afterpoll\*(C'\fR +in the prepare watcher and would dispose of the check watcher. +.PP +Method 3: If the module to be embedded supports explicit event +notification (libadns does), you can also make use of the actual watcher +callbacks, and only destroy/create the watchers in the prepare watcher. +.PP +.Vb 5 +\& static void +\& timer_cb (EV_P_ ev_timer *w, int revents) +\& { +\& adns_state ads = (adns_state)w\->data; +\& update_now (EV_A); +\& +\& adns_processtimeouts (ads, &tv_now); +\& } +\& +\& static void +\& io_cb (EV_P_ ev_io *w, int revents) +\& { +\& adns_state ads = (adns_state)w\->data; +\& update_now (EV_A); +\& +\& if (revents & EV_READ ) adns_processreadable (ads, w\->fd, &tv_now); +\& if (revents & EV_WRITE) adns_processwriteable (ads, w\->fd, &tv_now); +\& } +\& +\& // do not ever call adns_afterpoll +.Ve +.PP +Method 4: Do not use a prepare or check watcher because the module you +want to embed is not flexible enough to support it. Instead, you can +override their poll function. The drawback with this solution is that the +main loop is now no longer controllable by \s-1EV.\s0 The \f(CW\*(C`Glib::EV\*(C'\fR module uses +this approach, effectively embedding \s-1EV\s0 as a client into the horrible +libglib event loop. +.PP +.Vb 4 +\& static gint +\& event_poll_func (GPollFD *fds, guint nfds, gint timeout) +\& { +\& int got_events = 0; +\& +\& for (n = 0; n < nfds; ++n) +\& // create/start io watcher that sets the relevant bits in fds[n] and increment got_events +\& +\& if (timeout >= 0) +\& // create/start timer +\& +\& // poll +\& ev_run (EV_A_ 0); +\& +\& // stop timer again +\& if (timeout >= 0) +\& ev_timer_stop (EV_A_ &to); +\& +\& // stop io watchers again \- their callbacks should have set +\& for (n = 0; n < nfds; ++n) +\& ev_io_stop (EV_A_ iow [n]); +\& +\& return got_events; +\& } +.Ve +.ie n .SS """ev_embed"" \- when one backend isn't enough..." +.el .SS "\f(CWev_embed\fP \- when one backend isn't enough..." +.IX Subsection "ev_embed - when one backend isn't enough..." +This is a rather advanced watcher type that lets you embed one event loop +into another (currently only \f(CW\*(C`ev_io\*(C'\fR events are supported in the embedded +loop, other types of watchers might be handled in a delayed or incorrect +fashion and must not be used). +.PP +There are primarily two reasons you would want that: work around bugs and +prioritise I/O. +.PP +As an example for a bug workaround, the kqueue backend might only support +sockets on some platform, so it is unusable as generic backend, but you +still want to make use of it because you have many sockets and it scales +so nicely. In this case, you would create a kqueue-based loop and embed +it into your default loop (which might use e.g. poll). Overall operation +will be a bit slower because first libev has to call \f(CW\*(C`poll\*(C'\fR and then +\&\f(CW\*(C`kevent\*(C'\fR, but at least you can use both mechanisms for what they are +best: \f(CW\*(C`kqueue\*(C'\fR for scalable sockets and \f(CW\*(C`poll\*(C'\fR if you want it to work :) +.PP +As for prioritising I/O: under rare circumstances you have the case where +some fds have to be watched and handled very quickly (with low latency), +and even priorities and idle watchers might have too much overhead. In +this case you would put all the high priority stuff in one loop and all +the rest in a second one, and embed the second one in the first. +.PP +As long as the watcher is active, the callback will be invoked every +time there might be events pending in the embedded loop. The callback +must then call \f(CW\*(C`ev_embed_sweep (mainloop, watcher)\*(C'\fR to make a single +sweep and invoke their callbacks (the callback doesn't need to invoke the +\&\f(CW\*(C`ev_embed_sweep\*(C'\fR function directly, it could also start an idle watcher +to give the embedded loop strictly lower priority for example). +.PP +You can also set the callback to \f(CW0\fR, in which case the embed watcher +will automatically execute the embedded loop sweep whenever necessary. +.PP +Fork detection will be handled transparently while the \f(CW\*(C`ev_embed\*(C'\fR watcher +is active, i.e., the embedded loop will automatically be forked when the +embedding loop forks. In other cases, the user is responsible for calling +\&\f(CW\*(C`ev_loop_fork\*(C'\fR on the embedded loop. +.PP +Unfortunately, not all backends are embeddable: only the ones returned by +\&\f(CW\*(C`ev_embeddable_backends\*(C'\fR are, which, unfortunately, does not include any +portable one. +.PP +So when you want to use this feature you will always have to be prepared +that you cannot get an embeddable loop. The recommended way to get around +this is to have a separate variables for your embeddable loop, try to +create it, and if that fails, use the normal loop for everything. +.PP +\fI\f(CI\*(C`ev_embed\*(C'\fI and fork\fR +.IX Subsection "ev_embed and fork" +.PP +While the \f(CW\*(C`ev_embed\*(C'\fR watcher is running, forks in the embedding loop will +automatically be applied to the embedded loop as well, so no special +fork handling is required in that case. When the watcher is not running, +however, it is still the task of the libev user to call \f(CW\*(C`ev_loop_fork ()\*(C'\fR +as applicable. +.PP +\fIWatcher-Specific Functions and Data Members\fR +.IX Subsection "Watcher-Specific Functions and Data Members" +.IP "ev_embed_init (ev_embed *, callback, struct ev_loop *embedded_loop)" 4 +.IX Item "ev_embed_init (ev_embed *, callback, struct ev_loop *embedded_loop)" +.PD 0 +.IP "ev_embed_set (ev_embed *, struct ev_loop *embedded_loop)" 4 +.IX Item "ev_embed_set (ev_embed *, struct ev_loop *embedded_loop)" +.PD +Configures the watcher to embed the given loop, which must be +embeddable. If the callback is \f(CW0\fR, then \f(CW\*(C`ev_embed_sweep\*(C'\fR will be +invoked automatically, otherwise it is the responsibility of the callback +to invoke it (it will continue to be called until the sweep has been done, +if you do not want that, you need to temporarily stop the embed watcher). +.IP "ev_embed_sweep (loop, ev_embed *)" 4 +.IX Item "ev_embed_sweep (loop, ev_embed *)" +Make a single, non-blocking sweep over the embedded loop. This works +similarly to \f(CW\*(C`ev_run (embedded_loop, EVRUN_NOWAIT)\*(C'\fR, but in the most +appropriate way for embedded loops. +.IP "struct ev_loop *other [read\-only]" 4 +.IX Item "struct ev_loop *other [read-only]" +The embedded event loop. +.PP +\fIExamples\fR +.IX Subsection "Examples" +.PP +Example: Try to get an embeddable event loop and embed it into the default +event loop. If that is not possible, use the default loop. The default +loop is stored in \f(CW\*(C`loop_hi\*(C'\fR, while the embeddable loop is stored in +\&\f(CW\*(C`loop_lo\*(C'\fR (which is \f(CW\*(C`loop_hi\*(C'\fR in the case no embeddable loop can be +used). +.PP +.Vb 3 +\& struct ev_loop *loop_hi = ev_default_init (0); +\& struct ev_loop *loop_lo = 0; +\& ev_embed embed; +\& +\& // see if there is a chance of getting one that works +\& // (remember that a flags value of 0 means autodetection) +\& loop_lo = ev_embeddable_backends () & ev_recommended_backends () +\& ? ev_loop_new (ev_embeddable_backends () & ev_recommended_backends ()) +\& : 0; +\& +\& // if we got one, then embed it, otherwise default to loop_hi +\& if (loop_lo) +\& { +\& ev_embed_init (&embed, 0, loop_lo); +\& ev_embed_start (loop_hi, &embed); +\& } +\& else +\& loop_lo = loop_hi; +.Ve +.PP +Example: Check if kqueue is available but not recommended and create +a kqueue backend for use with sockets (which usually work with any +kqueue implementation). Store the kqueue/socket\-only event loop in +\&\f(CW\*(C`loop_socket\*(C'\fR. (One might optionally use \f(CW\*(C`EVFLAG_NOENV\*(C'\fR, too). +.PP +.Vb 3 +\& struct ev_loop *loop = ev_default_init (0); +\& struct ev_loop *loop_socket = 0; +\& ev_embed embed; +\& +\& if (ev_supported_backends () & ~ev_recommended_backends () & EVBACKEND_KQUEUE) +\& if ((loop_socket = ev_loop_new (EVBACKEND_KQUEUE)) +\& { +\& ev_embed_init (&embed, 0, loop_socket); +\& ev_embed_start (loop, &embed); +\& } +\& +\& if (!loop_socket) +\& loop_socket = loop; +\& +\& // now use loop_socket for all sockets, and loop for everything else +.Ve +.ie n .SS """ev_fork"" \- the audacity to resume the event loop after a fork" +.el .SS "\f(CWev_fork\fP \- the audacity to resume the event loop after a fork" +.IX Subsection "ev_fork - the audacity to resume the event loop after a fork" +Fork watchers are called when a \f(CW\*(C`fork ()\*(C'\fR was detected (usually because +whoever is a good citizen cared to tell libev about it by calling +\&\f(CW\*(C`ev_loop_fork\*(C'\fR). The invocation is done before the event loop blocks next +and before \f(CW\*(C`ev_check\*(C'\fR watchers are being called, and only in the child +after the fork. If whoever good citizen calling \f(CW\*(C`ev_default_fork\*(C'\fR cheats +and calls it in the wrong process, the fork handlers will be invoked, too, +of course. +.PP +\fIThe special problem of life after fork \- how is it possible?\fR +.IX Subsection "The special problem of life after fork - how is it possible?" +.PP +Most uses of \f(CW\*(C`fork ()\*(C'\fR consist of forking, then some simple calls to set +up/change the process environment, followed by a call to \f(CW\*(C`exec()\*(C'\fR. This +sequence should be handled by libev without any problems. +.PP +This changes when the application actually wants to do event handling +in the child, or both parent in child, in effect \*(L"continuing\*(R" after the +fork. +.PP +The default mode of operation (for libev, with application help to detect +forks) is to duplicate all the state in the child, as would be expected +when \fIeither\fR the parent \fIor\fR the child process continues. +.PP +When both processes want to continue using libev, then this is usually the +wrong result. In that case, usually one process (typically the parent) is +supposed to continue with all watchers in place as before, while the other +process typically wants to start fresh, i.e. without any active watchers. +.PP +The cleanest and most efficient way to achieve that with libev is to +simply create a new event loop, which of course will be \*(L"empty\*(R", and +use that for new watchers. This has the advantage of not touching more +memory than necessary, and thus avoiding the copy-on-write, and the +disadvantage of having to use multiple event loops (which do not support +signal watchers). +.PP +When this is not possible, or you want to use the default loop for +other reasons, then in the process that wants to start \*(L"fresh\*(R", call +\&\f(CW\*(C`ev_loop_destroy (EV_DEFAULT)\*(C'\fR followed by \f(CW\*(C`ev_default_loop (...)\*(C'\fR. +Destroying the default loop will \*(L"orphan\*(R" (not stop) all registered +watchers, so you have to be careful not to execute code that modifies +those watchers. Note also that in that case, you have to re-register any +signal watchers. +.PP +\fIWatcher-Specific Functions and Data Members\fR +.IX Subsection "Watcher-Specific Functions and Data Members" +.IP "ev_fork_init (ev_fork *, callback)" 4 +.IX Item "ev_fork_init (ev_fork *, callback)" +Initialises and configures the fork watcher \- it has no parameters of any +kind. There is a \f(CW\*(C`ev_fork_set\*(C'\fR macro, but using it is utterly pointless, +really. +.ie n .SS """ev_cleanup"" \- even the best things end" +.el .SS "\f(CWev_cleanup\fP \- even the best things end" +.IX Subsection "ev_cleanup - even the best things end" +Cleanup watchers are called just before the event loop is being destroyed +by a call to \f(CW\*(C`ev_loop_destroy\*(C'\fR. +.PP +While there is no guarantee that the event loop gets destroyed, cleanup +watchers provide a convenient method to install cleanup hooks for your +program, worker threads and so on \- you just to make sure to destroy the +loop when you want them to be invoked. +.PP +Cleanup watchers are invoked in the same way as any other watcher. Unlike +all other watchers, they do not keep a reference to the event loop (which +makes a lot of sense if you think about it). Like all other watchers, you +can call libev functions in the callback, except \f(CW\*(C`ev_cleanup_start\*(C'\fR. +.PP +\fIWatcher-Specific Functions and Data Members\fR +.IX Subsection "Watcher-Specific Functions and Data Members" +.IP "ev_cleanup_init (ev_cleanup *, callback)" 4 +.IX Item "ev_cleanup_init (ev_cleanup *, callback)" +Initialises and configures the cleanup watcher \- it has no parameters of +any kind. There is a \f(CW\*(C`ev_cleanup_set\*(C'\fR macro, but using it is utterly +pointless, I assure you. +.PP +Example: Register an atexit handler to destroy the default loop, so any +cleanup functions are called. +.PP +.Vb 5 +\& static void +\& program_exits (void) +\& { +\& ev_loop_destroy (EV_DEFAULT_UC); +\& } +\& +\& ... +\& atexit (program_exits); +.Ve +.ie n .SS """ev_async"" \- how to wake up an event loop" +.el .SS "\f(CWev_async\fP \- how to wake up an event loop" +.IX Subsection "ev_async - how to wake up an event loop" +In general, you cannot use an \f(CW\*(C`ev_loop\*(C'\fR from multiple threads or other +asynchronous sources such as signal handlers (as opposed to multiple event +loops \- those are of course safe to use in different threads). +.PP +Sometimes, however, you need to wake up an event loop you do not control, +for example because it belongs to another thread. This is what \f(CW\*(C`ev_async\*(C'\fR +watchers do: as long as the \f(CW\*(C`ev_async\*(C'\fR watcher is active, you can signal +it by calling \f(CW\*(C`ev_async_send\*(C'\fR, which is thread\- and signal safe. +.PP +This functionality is very similar to \f(CW\*(C`ev_signal\*(C'\fR watchers, as signals, +too, are asynchronous in nature, and signals, too, will be compressed +(i.e. the number of callback invocations may be less than the number of +\&\f(CW\*(C`ev_async_send\*(C'\fR calls). In fact, you could use signal watchers as a kind +of \*(L"global async watchers\*(R" by using a watcher on an otherwise unused +signal, and \f(CW\*(C`ev_feed_signal\*(C'\fR to signal this watcher from another thread, +even without knowing which loop owns the signal. +.PP +\fIQueueing\fR +.IX Subsection "Queueing" +.PP +\&\f(CW\*(C`ev_async\*(C'\fR does not support queueing of data in any way. The reason +is that the author does not know of a simple (or any) algorithm for a +multiple-writer-single-reader queue that works in all cases and doesn't +need elaborate support such as pthreads or unportable memory access +semantics. +.PP +That means that if you want to queue data, you have to provide your own +queue. But at least I can tell you how to implement locking around your +queue: +.IP "queueing from a signal handler context" 4 +.IX Item "queueing from a signal handler context" +To implement race-free queueing, you simply add to the queue in the signal +handler but you block the signal handler in the watcher callback. Here is +an example that does that for some fictitious \s-1SIGUSR1\s0 handler: +.Sp +.Vb 1 +\& static ev_async mysig; +\& +\& static void +\& sigusr1_handler (void) +\& { +\& sometype data; +\& +\& // no locking etc. +\& queue_put (data); +\& ev_async_send (EV_DEFAULT_ &mysig); +\& } +\& +\& static void +\& mysig_cb (EV_P_ ev_async *w, int revents) +\& { +\& sometype data; +\& sigset_t block, prev; +\& +\& sigemptyset (&block); +\& sigaddset (&block, SIGUSR1); +\& sigprocmask (SIG_BLOCK, &block, &prev); +\& +\& while (queue_get (&data)) +\& process (data); +\& +\& if (sigismember (&prev, SIGUSR1) +\& sigprocmask (SIG_UNBLOCK, &block, 0); +\& } +.Ve +.Sp +(Note: pthreads in theory requires you to use \f(CW\*(C`pthread_setmask\*(C'\fR +instead of \f(CW\*(C`sigprocmask\*(C'\fR when you use threads, but libev doesn't do it +either...). +.IP "queueing from a thread context" 4 +.IX Item "queueing from a thread context" +The strategy for threads is different, as you cannot (easily) block +threads but you can easily preempt them, so to queue safely you need to +employ a traditional mutex lock, such as in this pthread example: +.Sp +.Vb 2 +\& static ev_async mysig; +\& static pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER; +\& +\& static void +\& otherthread (void) +\& { +\& // only need to lock the actual queueing operation +\& pthread_mutex_lock (&mymutex); +\& queue_put (data); +\& pthread_mutex_unlock (&mymutex); +\& +\& ev_async_send (EV_DEFAULT_ &mysig); +\& } +\& +\& static void +\& mysig_cb (EV_P_ ev_async *w, int revents) +\& { +\& pthread_mutex_lock (&mymutex); +\& +\& while (queue_get (&data)) +\& process (data); +\& +\& pthread_mutex_unlock (&mymutex); +\& } +.Ve +.PP +\fIWatcher-Specific Functions and Data Members\fR +.IX Subsection "Watcher-Specific Functions and Data Members" +.IP "ev_async_init (ev_async *, callback)" 4 +.IX Item "ev_async_init (ev_async *, callback)" +Initialises and configures the async watcher \- it has no parameters of any +kind. There is a \f(CW\*(C`ev_async_set\*(C'\fR macro, but using it is utterly pointless, +trust me. +.IP "ev_async_send (loop, ev_async *)" 4 +.IX Item "ev_async_send (loop, ev_async *)" +Sends/signals/activates the given \f(CW\*(C`ev_async\*(C'\fR watcher, that is, feeds +an \f(CW\*(C`EV_ASYNC\*(C'\fR event on the watcher into the event loop, and instantly +returns. +.Sp +Unlike \f(CW\*(C`ev_feed_event\*(C'\fR, this call is safe to do from other threads, +signal or similar contexts (see the discussion of \f(CW\*(C`EV_ATOMIC_T\*(C'\fR in the +embedding section below on what exactly this means). +.Sp +Note that, as with other watchers in libev, multiple events might get +compressed into a single callback invocation (another way to look at +this is that \f(CW\*(C`ev_async\*(C'\fR watchers are level-triggered: they are set on +\&\f(CW\*(C`ev_async_send\*(C'\fR, reset when the event loop detects that). +.Sp +This call incurs the overhead of at most one extra system call per event +loop iteration, if the event loop is blocked, and no syscall at all if +the event loop (or your program) is processing events. That means that +repeated calls are basically free (there is no need to avoid calls for +performance reasons) and that the overhead becomes smaller (typically +zero) under load. +.IP "bool = ev_async_pending (ev_async *)" 4 +.IX Item "bool = ev_async_pending (ev_async *)" +Returns a non-zero value when \f(CW\*(C`ev_async_send\*(C'\fR has been called on the +watcher but the event has not yet been processed (or even noted) by the +event loop. +.Sp +\&\f(CW\*(C`ev_async_send\*(C'\fR sets a flag in the watcher and wakes up the loop. When +the loop iterates next and checks for the watcher to have become active, +it will reset the flag again. \f(CW\*(C`ev_async_pending\*(C'\fR can be used to very +quickly check whether invoking the loop might be a good idea. +.Sp +Not that this does \fInot\fR check whether the watcher itself is pending, +only whether it has been requested to make this watcher pending: there +is a time window between the event loop checking and resetting the async +notification, and the callback being invoked. +.SH "OTHER FUNCTIONS" +.IX Header "OTHER FUNCTIONS" +There are some other functions of possible interest. Described. Here. Now. +.IP "ev_once (loop, int fd, int events, ev_tstamp timeout, callback, arg)" 4 +.IX Item "ev_once (loop, int fd, int events, ev_tstamp timeout, callback, arg)" +This function combines a simple timer and an I/O watcher, calls your +callback on whichever event happens first and automatically stops both +watchers. This is useful if you want to wait for a single event on an fd +or timeout without having to allocate/configure/start/stop/free one or +more watchers yourself. +.Sp +If \f(CW\*(C`fd\*(C'\fR is less than 0, then no I/O watcher will be started and the +\&\f(CW\*(C`events\*(C'\fR argument is being ignored. Otherwise, an \f(CW\*(C`ev_io\*(C'\fR watcher for +the given \f(CW\*(C`fd\*(C'\fR and \f(CW\*(C`events\*(C'\fR set will be created and started. +.Sp +If \f(CW\*(C`timeout\*(C'\fR is less than 0, then no timeout watcher will be +started. Otherwise an \f(CW\*(C`ev_timer\*(C'\fR watcher with after = \f(CW\*(C`timeout\*(C'\fR (and +repeat = 0) will be started. \f(CW0\fR is a valid timeout. +.Sp +The callback has the type \f(CW\*(C`void (*cb)(int revents, void *arg)\*(C'\fR and is +passed an \f(CW\*(C`revents\*(C'\fR set like normal event callbacks (a combination of +\&\f(CW\*(C`EV_ERROR\*(C'\fR, \f(CW\*(C`EV_READ\*(C'\fR, \f(CW\*(C`EV_WRITE\*(C'\fR or \f(CW\*(C`EV_TIMER\*(C'\fR) and the \f(CW\*(C`arg\*(C'\fR +value passed to \f(CW\*(C`ev_once\*(C'\fR. Note that it is possible to receive \fIboth\fR +a timeout and an io event at the same time \- you probably should give io +events precedence. +.Sp +Example: wait up to ten seconds for data to appear on \s-1STDIN_FILENO.\s0 +.Sp +.Vb 7 +\& static void stdin_ready (int revents, void *arg) +\& { +\& if (revents & EV_READ) +\& /* stdin might have data for us, joy! */; +\& else if (revents & EV_TIMER) +\& /* doh, nothing entered */; +\& } +\& +\& ev_once (STDIN_FILENO, EV_READ, 10., stdin_ready, 0); +.Ve +.IP "ev_feed_fd_event (loop, int fd, int revents)" 4 +.IX Item "ev_feed_fd_event (loop, int fd, int revents)" +Feed an event on the given fd, as if a file descriptor backend detected +the given events. +.IP "ev_feed_signal_event (loop, int signum)" 4 +.IX Item "ev_feed_signal_event (loop, int signum)" +Feed an event as if the given signal occurred. See also \f(CW\*(C`ev_feed_signal\*(C'\fR, +which is async-safe. +.SH "COMMON OR USEFUL IDIOMS (OR BOTH)" +.IX Header "COMMON OR USEFUL IDIOMS (OR BOTH)" +This section explains some common idioms that are not immediately +obvious. Note that examples are sprinkled over the whole manual, and this +section only contains stuff that wouldn't fit anywhere else. +.SS "\s-1ASSOCIATING CUSTOM DATA WITH A WATCHER\s0" +.IX Subsection "ASSOCIATING CUSTOM DATA WITH A WATCHER" +Each watcher has, by default, a \f(CW\*(C`void *data\*(C'\fR member that you can read +or modify at any time: libev will completely ignore it. This can be used +to associate arbitrary data with your watcher. If you need more data and +don't want to allocate memory separately and store a pointer to it in that +data member, you can also \*(L"subclass\*(R" the watcher type and provide your own +data: +.PP +.Vb 7 +\& struct my_io +\& { +\& ev_io io; +\& int otherfd; +\& void *somedata; +\& struct whatever *mostinteresting; +\& }; +\& +\& ... +\& struct my_io w; +\& ev_io_init (&w.io, my_cb, fd, EV_READ); +.Ve +.PP +And since your callback will be called with a pointer to the watcher, you +can cast it back to your own type: +.PP +.Vb 5 +\& static void my_cb (struct ev_loop *loop, ev_io *w_, int revents) +\& { +\& struct my_io *w = (struct my_io *)w_; +\& ... +\& } +.Ve +.PP +More interesting and less C\-conformant ways of casting your callback +function type instead have been omitted. +.SS "\s-1BUILDING YOUR OWN COMPOSITE WATCHERS\s0" +.IX Subsection "BUILDING YOUR OWN COMPOSITE WATCHERS" +Another common scenario is to use some data structure with multiple +embedded watchers, in effect creating your own watcher that combines +multiple libev event sources into one \*(L"super-watcher\*(R": +.PP +.Vb 6 +\& struct my_biggy +\& { +\& int some_data; +\& ev_timer t1; +\& ev_timer t2; +\& } +.Ve +.PP +In this case getting the pointer to \f(CW\*(C`my_biggy\*(C'\fR is a bit more +complicated: Either you store the address of your \f(CW\*(C`my_biggy\*(C'\fR struct in +the \f(CW\*(C`data\*(C'\fR member of the watcher (for woozies or \*(C+ coders), or you need +to use some pointer arithmetic using \f(CW\*(C`offsetof\*(C'\fR inside your watchers (for +real programmers): +.PP +.Vb 1 +\& #include +\& +\& static void +\& t1_cb (EV_P_ ev_timer *w, int revents) +\& { +\& struct my_biggy big = (struct my_biggy *) +\& (((char *)w) \- offsetof (struct my_biggy, t1)); +\& } +\& +\& static void +\& t2_cb (EV_P_ ev_timer *w, int revents) +\& { +\& struct my_biggy big = (struct my_biggy *) +\& (((char *)w) \- offsetof (struct my_biggy, t2)); +\& } +.Ve +.SS "\s-1AVOIDING FINISHING BEFORE RETURNING\s0" +.IX Subsection "AVOIDING FINISHING BEFORE RETURNING" +Often you have structures like this in event-based programs: +.PP +.Vb 4 +\& callback () +\& { +\& free (request); +\& } +\& +\& request = start_new_request (..., callback); +.Ve +.PP +The intent is to start some \*(L"lengthy\*(R" operation. The \f(CW\*(C`request\*(C'\fR could be +used to cancel the operation, or do other things with it. +.PP +It's not uncommon to have code paths in \f(CW\*(C`start_new_request\*(C'\fR that +immediately invoke the callback, for example, to report errors. Or you add +some caching layer that finds that it can skip the lengthy aspects of the +operation and simply invoke the callback with the result. +.PP +The problem here is that this will happen \fIbefore\fR \f(CW\*(C`start_new_request\*(C'\fR +has returned, so \f(CW\*(C`request\*(C'\fR is not set. +.PP +Even if you pass the request by some safer means to the callback, you +might want to do something to the request after starting it, such as +canceling it, which probably isn't working so well when the callback has +already been invoked. +.PP +A common way around all these issues is to make sure that +\&\f(CW\*(C`start_new_request\*(C'\fR \fIalways\fR returns before the callback is invoked. If +\&\f(CW\*(C`start_new_request\*(C'\fR immediately knows the result, it can artificially +delay invoking the callback by using a \f(CW\*(C`prepare\*(C'\fR or \f(CW\*(C`idle\*(C'\fR watcher for +example, or more sneakily, by reusing an existing (stopped) watcher and +pushing it into the pending queue: +.PP +.Vb 2 +\& ev_set_cb (watcher, callback); +\& ev_feed_event (EV_A_ watcher, 0); +.Ve +.PP +This way, \f(CW\*(C`start_new_request\*(C'\fR can safely return before the callback is +invoked, while not delaying callback invocation too much. +.SS "\s-1MODEL/NESTED EVENT LOOP INVOCATIONS AND EXIT CONDITIONS\s0" +.IX Subsection "MODEL/NESTED EVENT LOOP INVOCATIONS AND EXIT CONDITIONS" +Often (especially in \s-1GUI\s0 toolkits) there are places where you have +\&\fImodal\fR interaction, which is most easily implemented by recursively +invoking \f(CW\*(C`ev_run\*(C'\fR. +.PP +This brings the problem of exiting \- a callback might want to finish the +main \f(CW\*(C`ev_run\*(C'\fR call, but not the nested one (e.g. user clicked \*(L"Quit\*(R", but +a modal \*(L"Are you sure?\*(R" dialog is still waiting), or just the nested one +and not the main one (e.g. user clocked \*(L"Ok\*(R" in a modal dialog), or some +other combination: In these cases, a simple \f(CW\*(C`ev_break\*(C'\fR will not work. +.PP +The solution is to maintain \*(L"break this loop\*(R" variable for each \f(CW\*(C`ev_run\*(C'\fR +invocation, and use a loop around \f(CW\*(C`ev_run\*(C'\fR until the condition is +triggered, using \f(CW\*(C`EVRUN_ONCE\*(C'\fR: +.PP +.Vb 2 +\& // main loop +\& int exit_main_loop = 0; +\& +\& while (!exit_main_loop) +\& ev_run (EV_DEFAULT_ EVRUN_ONCE); +\& +\& // in a modal watcher +\& int exit_nested_loop = 0; +\& +\& while (!exit_nested_loop) +\& ev_run (EV_A_ EVRUN_ONCE); +.Ve +.PP +To exit from any of these loops, just set the corresponding exit variable: +.PP +.Vb 2 +\& // exit modal loop +\& exit_nested_loop = 1; +\& +\& // exit main program, after modal loop is finished +\& exit_main_loop = 1; +\& +\& // exit both +\& exit_main_loop = exit_nested_loop = 1; +.Ve +.SS "\s-1THREAD LOCKING EXAMPLE\s0" +.IX Subsection "THREAD LOCKING EXAMPLE" +Here is a fictitious example of how to run an event loop in a different +thread from where callbacks are being invoked and watchers are +created/added/removed. +.PP +For a real-world example, see the \f(CW\*(C`EV::Loop::Async\*(C'\fR perl module, +which uses exactly this technique (which is suited for many high-level +languages). +.PP +The example uses a pthread mutex to protect the loop data, a condition +variable to wait for callback invocations, an async watcher to notify the +event loop thread and an unspecified mechanism to wake up the main thread. +.PP +First, you need to associate some data with the event loop: +.PP +.Vb 6 +\& typedef struct { +\& mutex_t lock; /* global loop lock */ +\& ev_async async_w; +\& thread_t tid; +\& cond_t invoke_cv; +\& } userdata; +\& +\& void prepare_loop (EV_P) +\& { +\& // for simplicity, we use a static userdata struct. +\& static userdata u; +\& +\& ev_async_init (&u\->async_w, async_cb); +\& ev_async_start (EV_A_ &u\->async_w); +\& +\& pthread_mutex_init (&u\->lock, 0); +\& pthread_cond_init (&u\->invoke_cv, 0); +\& +\& // now associate this with the loop +\& ev_set_userdata (EV_A_ u); +\& ev_set_invoke_pending_cb (EV_A_ l_invoke); +\& ev_set_loop_release_cb (EV_A_ l_release, l_acquire); +\& +\& // then create the thread running ev_run +\& pthread_create (&u\->tid, 0, l_run, EV_A); +\& } +.Ve +.PP +The callback for the \f(CW\*(C`ev_async\*(C'\fR watcher does nothing: the watcher is used +solely to wake up the event loop so it takes notice of any new watchers +that might have been added: +.PP +.Vb 5 +\& static void +\& async_cb (EV_P_ ev_async *w, int revents) +\& { +\& // just used for the side effects +\& } +.Ve +.PP +The \f(CW\*(C`l_release\*(C'\fR and \f(CW\*(C`l_acquire\*(C'\fR callbacks simply unlock/lock the mutex +protecting the loop data, respectively. +.PP +.Vb 6 +\& static void +\& l_release (EV_P) +\& { +\& userdata *u = ev_userdata (EV_A); +\& pthread_mutex_unlock (&u\->lock); +\& } +\& +\& static void +\& l_acquire (EV_P) +\& { +\& userdata *u = ev_userdata (EV_A); +\& pthread_mutex_lock (&u\->lock); +\& } +.Ve +.PP +The event loop thread first acquires the mutex, and then jumps straight +into \f(CW\*(C`ev_run\*(C'\fR: +.PP +.Vb 4 +\& void * +\& l_run (void *thr_arg) +\& { +\& struct ev_loop *loop = (struct ev_loop *)thr_arg; +\& +\& l_acquire (EV_A); +\& pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, 0); +\& ev_run (EV_A_ 0); +\& l_release (EV_A); +\& +\& return 0; +\& } +.Ve +.PP +Instead of invoking all pending watchers, the \f(CW\*(C`l_invoke\*(C'\fR callback will +signal the main thread via some unspecified mechanism (signals? pipe +writes? \f(CW\*(C`Async::Interrupt\*(C'\fR?) and then waits until all pending watchers +have been called (in a while loop because a) spurious wakeups are possible +and b) skipping inter-thread-communication when there are no pending +watchers is very beneficial): +.PP +.Vb 4 +\& static void +\& l_invoke (EV_P) +\& { +\& userdata *u = ev_userdata (EV_A); +\& +\& while (ev_pending_count (EV_A)) +\& { +\& wake_up_other_thread_in_some_magic_or_not_so_magic_way (); +\& pthread_cond_wait (&u\->invoke_cv, &u\->lock); +\& } +\& } +.Ve +.PP +Now, whenever the main thread gets told to invoke pending watchers, it +will grab the lock, call \f(CW\*(C`ev_invoke_pending\*(C'\fR and then signal the loop +thread to continue: +.PP +.Vb 4 +\& static void +\& real_invoke_pending (EV_P) +\& { +\& userdata *u = ev_userdata (EV_A); +\& +\& pthread_mutex_lock (&u\->lock); +\& ev_invoke_pending (EV_A); +\& pthread_cond_signal (&u\->invoke_cv); +\& pthread_mutex_unlock (&u\->lock); +\& } +.Ve +.PP +Whenever you want to start/stop a watcher or do other modifications to an +event loop, you will now have to lock: +.PP +.Vb 2 +\& ev_timer timeout_watcher; +\& userdata *u = ev_userdata (EV_A); +\& +\& ev_timer_init (&timeout_watcher, timeout_cb, 5.5, 0.); +\& +\& pthread_mutex_lock (&u\->lock); +\& ev_timer_start (EV_A_ &timeout_watcher); +\& ev_async_send (EV_A_ &u\->async_w); +\& pthread_mutex_unlock (&u\->lock); +.Ve +.PP +Note that sending the \f(CW\*(C`ev_async\*(C'\fR watcher is required because otherwise +an event loop currently blocking in the kernel will have no knowledge +about the newly added timer. By waking up the loop it will pick up any new +watchers in the next event loop iteration. +.SS "\s-1THREADS, COROUTINES, CONTINUATIONS, QUEUES... INSTEAD OF CALLBACKS\s0" +.IX Subsection "THREADS, COROUTINES, CONTINUATIONS, QUEUES... INSTEAD OF CALLBACKS" +While the overhead of a callback that e.g. schedules a thread is small, it +is still an overhead. If you embed libev, and your main usage is with some +kind of threads or coroutines, you might want to customise libev so that +doesn't need callbacks anymore. +.PP +Imagine you have coroutines that you can switch to using a function +\&\f(CW\*(C`switch_to (coro)\*(C'\fR, that libev runs in a coroutine called \f(CW\*(C`libev_coro\*(C'\fR +and that due to some magic, the currently active coroutine is stored in a +global called \f(CW\*(C`current_coro\*(C'\fR. Then you can build your own \*(L"wait for libev +event\*(R" primitive by changing \f(CW\*(C`EV_CB_DECLARE\*(C'\fR and \f(CW\*(C`EV_CB_INVOKE\*(C'\fR (note +the differing \f(CW\*(C`;\*(C'\fR conventions): +.PP +.Vb 2 +\& #define EV_CB_DECLARE(type) struct my_coro *cb; +\& #define EV_CB_INVOKE(watcher) switch_to ((watcher)\->cb) +.Ve +.PP +That means instead of having a C callback function, you store the +coroutine to switch to in each watcher, and instead of having libev call +your callback, you instead have it switch to that coroutine. +.PP +A coroutine might now wait for an event with a function called +\&\f(CW\*(C`wait_for_event\*(C'\fR. (the watcher needs to be started, as always, but it doesn't +matter when, or whether the watcher is active or not when this function is +called): +.PP +.Vb 6 +\& void +\& wait_for_event (ev_watcher *w) +\& { +\& ev_set_cb (w, current_coro); +\& switch_to (libev_coro); +\& } +.Ve +.PP +That basically suspends the coroutine inside \f(CW\*(C`wait_for_event\*(C'\fR and +continues the libev coroutine, which, when appropriate, switches back to +this or any other coroutine. +.PP +You can do similar tricks if you have, say, threads with an event queue \- +instead of storing a coroutine, you store the queue object and instead of +switching to a coroutine, you push the watcher onto the queue and notify +any waiters. +.PP +To embed libev, see \*(L"\s-1EMBEDDING\*(R"\s0, but in short, it's easiest to create two +files, \fImy_ev.h\fR and \fImy_ev.c\fR that include the respective libev files: +.PP +.Vb 4 +\& // my_ev.h +\& #define EV_CB_DECLARE(type) struct my_coro *cb; +\& #define EV_CB_INVOKE(watcher) switch_to ((watcher)\->cb) +\& #include "../libev/ev.h" +\& +\& // my_ev.c +\& #define EV_H "my_ev.h" +\& #include "../libev/ev.c" +.Ve +.PP +And then use \fImy_ev.h\fR when you would normally use \fIev.h\fR, and compile +\&\fImy_ev.c\fR into your project. When properly specifying include paths, you +can even use \fIev.h\fR as header file name directly. +.SH "LIBEVENT EMULATION" +.IX Header "LIBEVENT EMULATION" +Libev offers a compatibility emulation layer for libevent. It cannot +emulate the internals of libevent, so here are some usage hints: +.IP "\(bu" 4 +Only the libevent\-1.4.1\-beta \s-1API\s0 is being emulated. +.Sp +This was the newest libevent version available when libev was implemented, +and is still mostly unchanged in 2010. +.IP "\(bu" 4 +Use it by including , as usual. +.IP "\(bu" 4 +The following members are fully supported: ev_base, ev_callback, +ev_arg, ev_fd, ev_res, ev_events. +.IP "\(bu" 4 +Avoid using ev_flags and the EVLIST_*\-macros, while it is +maintained by libev, it does not work exactly the same way as in libevent (consider +it a private \s-1API\s0). +.IP "\(bu" 4 +Priorities are not currently supported. Initialising priorities +will fail and all watchers will have the same priority, even though there +is an ev_pri field. +.IP "\(bu" 4 +In libevent, the last base created gets the signals, in libev, the +base that registered the signal gets the signals. +.IP "\(bu" 4 +Other members are not supported. +.IP "\(bu" 4 +The libev emulation is \fInot\fR \s-1ABI\s0 compatible to libevent, you need +to use the libev header file and library. +.SH "\*(C+ SUPPORT" +.IX Header " SUPPORT" +.SS "C \s-1API\s0" +.IX Subsection "C API" +The normal C \s-1API\s0 should work fine when used from \*(C+: both ev.h and the +libev sources can be compiled as \*(C+. Therefore, code that uses the C \s-1API\s0 +will work fine. +.PP +Proper exception specifications might have to be added to callbacks passed +to libev: exceptions may be thrown only from watcher callbacks, all other +callbacks (allocator, syserr, loop acquire/release and periodic reschedule +callbacks) must not throw exceptions, and might need a \f(CW\*(C`noexcept\*(C'\fR +specification. If you have code that needs to be compiled as both C and +\&\*(C+ you can use the \f(CW\*(C`EV_NOEXCEPT\*(C'\fR macro for this: +.PP +.Vb 6 +\& static void +\& fatal_error (const char *msg) EV_NOEXCEPT +\& { +\& perror (msg); +\& abort (); +\& } +\& +\& ... +\& ev_set_syserr_cb (fatal_error); +.Ve +.PP +The only \s-1API\s0 functions that can currently throw exceptions are \f(CW\*(C`ev_run\*(C'\fR, +\&\f(CW\*(C`ev_invoke\*(C'\fR, \f(CW\*(C`ev_invoke_pending\*(C'\fR and \f(CW\*(C`ev_loop_destroy\*(C'\fR (the latter +because it runs cleanup watchers). +.PP +Throwing exceptions in watcher callbacks is only supported if libev itself +is compiled with a \*(C+ compiler or your C and \*(C+ environments allow +throwing exceptions through C libraries (most do). +.SS "\*(C+ \s-1API\s0" +.IX Subsection " API" +Libev comes with some simplistic wrapper classes for \*(C+ that mainly allow +you to use some convenience methods to start/stop watchers and also change +the callback model to a model using method callbacks on objects. +.PP +To use it, +.PP +.Vb 1 +\& #include +.Ve +.PP +This automatically includes \fIev.h\fR and puts all of its definitions (many +of them macros) into the global namespace. All \*(C+ specific things are +put into the \f(CW\*(C`ev\*(C'\fR namespace. It should support all the same embedding +options as \fIev.h\fR, most notably \f(CW\*(C`EV_MULTIPLICITY\*(C'\fR. +.PP +Care has been taken to keep the overhead low. The only data member the \*(C+ +classes add (compared to plain C\-style watchers) is the event loop pointer +that the watcher is associated with (or no additional members at all if +you disable \f(CW\*(C`EV_MULTIPLICITY\*(C'\fR when embedding libev). +.PP +Currently, functions, static and non-static member functions and classes +with \f(CW\*(C`operator ()\*(C'\fR can be used as callbacks. Other types should be easy +to add as long as they only need one additional pointer for context. If +you need support for other types of functors please contact the author +(preferably after implementing it). +.PP +For all this to work, your \*(C+ compiler either has to use the same calling +conventions as your C compiler (for static member functions), or you have +to embed libev and compile libev itself as \*(C+. +.PP +Here is a list of things available in the \f(CW\*(C`ev\*(C'\fR namespace: +.ie n .IP """ev::READ"", ""ev::WRITE"" etc." 4 +.el .IP "\f(CWev::READ\fR, \f(CWev::WRITE\fR etc." 4 +.IX Item "ev::READ, ev::WRITE etc." +These are just enum values with the same values as the \f(CW\*(C`EV_READ\*(C'\fR etc. +macros from \fIev.h\fR. +.ie n .IP """ev::tstamp"", ""ev::now""" 4 +.el .IP "\f(CWev::tstamp\fR, \f(CWev::now\fR" 4 +.IX Item "ev::tstamp, ev::now" +Aliases to the same types/functions as with the \f(CW\*(C`ev_\*(C'\fR prefix. +.ie n .IP """ev::io"", ""ev::timer"", ""ev::periodic"", ""ev::idle"", ""ev::sig"" etc." 4 +.el .IP "\f(CWev::io\fR, \f(CWev::timer\fR, \f(CWev::periodic\fR, \f(CWev::idle\fR, \f(CWev::sig\fR etc." 4 +.IX Item "ev::io, ev::timer, ev::periodic, ev::idle, ev::sig etc." +For each \f(CW\*(C`ev_TYPE\*(C'\fR watcher in \fIev.h\fR there is a corresponding class of +the same name in the \f(CW\*(C`ev\*(C'\fR namespace, with the exception of \f(CW\*(C`ev_signal\*(C'\fR +which is called \f(CW\*(C`ev::sig\*(C'\fR to avoid clashes with the \f(CW\*(C`signal\*(C'\fR macro +defined by many implementations. +.Sp +All of those classes have these methods: +.RS 4 +.IP "ev::TYPE::TYPE ()" 4 +.IX Item "ev::TYPE::TYPE ()" +.PD 0 +.IP "ev::TYPE::TYPE (loop)" 4 +.IX Item "ev::TYPE::TYPE (loop)" +.IP "ev::TYPE::~TYPE" 4 +.IX Item "ev::TYPE::~TYPE" +.PD +The constructor (optionally) takes an event loop to associate the watcher +with. If it is omitted, it will use \f(CW\*(C`EV_DEFAULT\*(C'\fR. +.Sp +The constructor calls \f(CW\*(C`ev_init\*(C'\fR for you, which means you have to call the +\&\f(CW\*(C`set\*(C'\fR method before starting it. +.Sp +It will not set a callback, however: You have to call the templated \f(CW\*(C`set\*(C'\fR +method to set a callback before you can start the watcher. +.Sp +(The reason why you have to use a method is a limitation in \*(C+ which does +not allow explicit template arguments for constructors). +.Sp +The destructor automatically stops the watcher if it is active. +.IP "w\->set (object *)" 4 +.IX Item "w->set (object *)" +This method sets the callback method to call. The method has to have a +signature of \f(CW\*(C`void (*)(ev_TYPE &, int)\*(C'\fR, it receives the watcher as +first argument and the \f(CW\*(C`revents\*(C'\fR as second. The object must be given as +parameter and is stored in the \f(CW\*(C`data\*(C'\fR member of the watcher. +.Sp +This method synthesizes efficient thunking code to call your method from +the C callback that libev requires. If your compiler can inline your +callback (i.e. it is visible to it at the place of the \f(CW\*(C`set\*(C'\fR call and +your compiler is good :), then the method will be fully inlined into the +thunking function, making it as fast as a direct C callback. +.Sp +Example: simple class declaration and watcher initialisation +.Sp +.Vb 4 +\& struct myclass +\& { +\& void io_cb (ev::io &w, int revents) { } +\& } +\& +\& myclass obj; +\& ev::io iow; +\& iow.set (&obj); +.Ve +.IP "w\->set (object *)" 4 +.IX Item "w->set (object *)" +This is a variation of a method callback \- leaving out the method to call +will default the method to \f(CW\*(C`operator ()\*(C'\fR, which makes it possible to use +functor objects without having to manually specify the \f(CW\*(C`operator ()\*(C'\fR all +the time. Incidentally, you can then also leave out the template argument +list. +.Sp +The \f(CW\*(C`operator ()\*(C'\fR method prototype must be \f(CW\*(C`void operator ()(watcher &w, +int revents)\*(C'\fR. +.Sp +See the method\-\f(CW\*(C`set\*(C'\fR above for more details. +.Sp +Example: use a functor object as callback. +.Sp +.Vb 7 +\& struct myfunctor +\& { +\& void operator() (ev::io &w, int revents) +\& { +\& ... +\& } +\& } +\& +\& myfunctor f; +\& +\& ev::io w; +\& w.set (&f); +.Ve +.IP "w\->set (void *data = 0)" 4 +.IX Item "w->set (void *data = 0)" +Also sets a callback, but uses a static method or plain function as +callback. The optional \f(CW\*(C`data\*(C'\fR argument will be stored in the watcher's +\&\f(CW\*(C`data\*(C'\fR member and is free for you to use. +.Sp +The prototype of the \f(CW\*(C`function\*(C'\fR must be \f(CW\*(C`void (*)(ev::TYPE &w, int)\*(C'\fR. +.Sp +See the method\-\f(CW\*(C`set\*(C'\fR above for more details. +.Sp +Example: Use a plain function as callback. +.Sp +.Vb 2 +\& static void io_cb (ev::io &w, int revents) { } +\& iow.set (); +.Ve +.IP "w\->set (loop)" 4 +.IX Item "w->set (loop)" +Associates a different \f(CW\*(C`struct ev_loop\*(C'\fR with this watcher. You can only +do this when the watcher is inactive (and not pending either). +.IP "w\->set ([arguments])" 4 +.IX Item "w->set ([arguments])" +Basically the same as \f(CW\*(C`ev_TYPE_set\*(C'\fR (except for \f(CW\*(C`ev::embed\*(C'\fR watchers>), +with the same arguments. Either this method or a suitable start method +must be called at least once. Unlike the C counterpart, an active watcher +gets automatically stopped and restarted when reconfiguring it with this +method. +.Sp +For \f(CW\*(C`ev::embed\*(C'\fR watchers this method is called \f(CW\*(C`set_embed\*(C'\fR, to avoid +clashing with the \f(CW\*(C`set (loop)\*(C'\fR method. +.Sp +For \f(CW\*(C`ev::io\*(C'\fR watchers there is an additional \f(CW\*(C`set\*(C'\fR method that acepts a +new event mask only, and internally calls \f(CW\*(C`ev_io_modfify\*(C'\fR. +.IP "w\->start ()" 4 +.IX Item "w->start ()" +Starts the watcher. Note that there is no \f(CW\*(C`loop\*(C'\fR argument, as the +constructor already stores the event loop. +.IP "w\->start ([arguments])" 4 +.IX Item "w->start ([arguments])" +Instead of calling \f(CW\*(C`set\*(C'\fR and \f(CW\*(C`start\*(C'\fR methods separately, it is often +convenient to wrap them in one call. Uses the same type of arguments as +the configure \f(CW\*(C`set\*(C'\fR method of the watcher. +.IP "w\->stop ()" 4 +.IX Item "w->stop ()" +Stops the watcher if it is active. Again, no \f(CW\*(C`loop\*(C'\fR argument. +.ie n .IP "w\->again () (""ev::timer"", ""ev::periodic"" only)" 4 +.el .IP "w\->again () (\f(CWev::timer\fR, \f(CWev::periodic\fR only)" 4 +.IX Item "w->again () (ev::timer, ev::periodic only)" +For \f(CW\*(C`ev::timer\*(C'\fR and \f(CW\*(C`ev::periodic\*(C'\fR, this invokes the corresponding +\&\f(CW\*(C`ev_TYPE_again\*(C'\fR function. +.ie n .IP "w\->sweep () (""ev::embed"" only)" 4 +.el .IP "w\->sweep () (\f(CWev::embed\fR only)" 4 +.IX Item "w->sweep () (ev::embed only)" +Invokes \f(CW\*(C`ev_embed_sweep\*(C'\fR. +.ie n .IP "w\->update () (""ev::stat"" only)" 4 +.el .IP "w\->update () (\f(CWev::stat\fR only)" 4 +.IX Item "w->update () (ev::stat only)" +Invokes \f(CW\*(C`ev_stat_stat\*(C'\fR. +.RE +.RS 4 +.RE +.PP +Example: Define a class with two I/O and idle watchers, start the I/O +watchers in the constructor. +.PP +.Vb 5 +\& class myclass +\& { +\& ev::io io ; void io_cb (ev::io &w, int revents); +\& ev::io io2 ; void io2_cb (ev::io &w, int revents); +\& ev::idle idle; void idle_cb (ev::idle &w, int revents); +\& +\& myclass (int fd) +\& { +\& io .set (this); +\& io2 .set (this); +\& idle.set (this); +\& +\& io.set (fd, ev::WRITE); // configure the watcher +\& io.start (); // start it whenever convenient +\& +\& io2.start (fd, ev::READ); // set + start in one call +\& } +\& }; +.Ve +.SH "OTHER LANGUAGE BINDINGS" +.IX Header "OTHER LANGUAGE BINDINGS" +Libev does not offer other language bindings itself, but bindings for a +number of languages exist in the form of third-party packages. If you know +any interesting language binding in addition to the ones listed here, drop +me a note. +.IP "Perl" 4 +.IX Item "Perl" +The \s-1EV\s0 module implements the full libev \s-1API\s0 and is actually used to test +libev. \s-1EV\s0 is developed together with libev. Apart from the \s-1EV\s0 core module, +there are additional modules that implement libev-compatible interfaces +to \f(CW\*(C`libadns\*(C'\fR (\f(CW\*(C`EV::ADNS\*(C'\fR, but \f(CW\*(C`AnyEvent::DNS\*(C'\fR is preferred nowadays), +\&\f(CW\*(C`Net::SNMP\*(C'\fR (\f(CW\*(C`Net::SNMP::EV\*(C'\fR) and the \f(CW\*(C`libglib\*(C'\fR event core (\f(CW\*(C`Glib::EV\*(C'\fR +and \f(CW\*(C`EV::Glib\*(C'\fR). +.Sp +It can be found and installed via \s-1CPAN,\s0 its homepage is at +. +.IP "Python" 4 +.IX Item "Python" +Python bindings can be found at . It +seems to be quite complete and well-documented. +.IP "Ruby" 4 +.IX Item "Ruby" +Tony Arcieri has written a ruby extension that offers access to a subset +of the libev \s-1API\s0 and adds file handle abstractions, asynchronous \s-1DNS\s0 and +more on top of it. It can be found via gem servers. Its homepage is at +. +.Sp +Roger Pack reports that using the link order \f(CW\*(C`\-lws2_32 \-lmsvcrt\-ruby\-190\*(C'\fR +makes rev work even on mingw. +.IP "Haskell" 4 +.IX Item "Haskell" +A haskell binding to libev is available at +. +.IP "D" 4 +.IX Item "D" +Leandro Lucarella has written a D language binding (\fIev.d\fR) for libev, to +be found at . +.IP "Ocaml" 4 +.IX Item "Ocaml" +Erkki Seppala has written Ocaml bindings for libev, to be found at +. +.IP "Lua" 4 +.IX Item "Lua" +Brian Maher has written a partial interface to libev for lua (at the +time of this writing, only \f(CW\*(C`ev_io\*(C'\fR and \f(CW\*(C`ev_timer\*(C'\fR), to be found at +. +.IP "Javascript" 4 +.IX Item "Javascript" +Node.js () uses libev as the underlying event library. +.IP "Others" 4 +.IX Item "Others" +There are others, and I stopped counting. +.SH "MACRO MAGIC" +.IX Header "MACRO MAGIC" +Libev can be compiled with a variety of options, the most fundamental +of which is \f(CW\*(C`EV_MULTIPLICITY\*(C'\fR. This option determines whether (most) +functions and callbacks have an initial \f(CW\*(C`struct ev_loop *\*(C'\fR argument. +.PP +To make it easier to write programs that cope with either variant, the +following macros are defined: +.ie n .IP """EV_A"", ""EV_A_""" 4 +.el .IP "\f(CWEV_A\fR, \f(CWEV_A_\fR" 4 +.IX Item "EV_A, EV_A_" +This provides the loop \fIargument\fR for functions, if one is required (\*(L"ev +loop argument\*(R"). The \f(CW\*(C`EV_A\*(C'\fR form is used when this is the sole argument, +\&\f(CW\*(C`EV_A_\*(C'\fR is used when other arguments are following. Example: +.Sp +.Vb 3 +\& ev_unref (EV_A); +\& ev_timer_add (EV_A_ watcher); +\& ev_run (EV_A_ 0); +.Ve +.Sp +It assumes the variable \f(CW\*(C`loop\*(C'\fR of type \f(CW\*(C`struct ev_loop *\*(C'\fR is in scope, +which is often provided by the following macro. +.ie n .IP """EV_P"", ""EV_P_""" 4 +.el .IP "\f(CWEV_P\fR, \f(CWEV_P_\fR" 4 +.IX Item "EV_P, EV_P_" +This provides the loop \fIparameter\fR for functions, if one is required (\*(L"ev +loop parameter\*(R"). The \f(CW\*(C`EV_P\*(C'\fR form is used when this is the sole parameter, +\&\f(CW\*(C`EV_P_\*(C'\fR is used when other parameters are following. Example: +.Sp +.Vb 2 +\& // this is how ev_unref is being declared +\& static void ev_unref (EV_P); +\& +\& // this is how you can declare your typical callback +\& static void cb (EV_P_ ev_timer *w, int revents) +.Ve +.Sp +It declares a parameter \f(CW\*(C`loop\*(C'\fR of type \f(CW\*(C`struct ev_loop *\*(C'\fR, quite +suitable for use with \f(CW\*(C`EV_A\*(C'\fR. +.ie n .IP """EV_DEFAULT"", ""EV_DEFAULT_""" 4 +.el .IP "\f(CWEV_DEFAULT\fR, \f(CWEV_DEFAULT_\fR" 4 +.IX Item "EV_DEFAULT, EV_DEFAULT_" +Similar to the other two macros, this gives you the value of the default +loop, if multiple loops are supported (\*(L"ev loop default\*(R"). The default loop +will be initialised if it isn't already initialised. +.Sp +For non-multiplicity builds, these macros do nothing, so you always have +to initialise the loop somewhere. +.ie n .IP """EV_DEFAULT_UC"", ""EV_DEFAULT_UC_""" 4 +.el .IP "\f(CWEV_DEFAULT_UC\fR, \f(CWEV_DEFAULT_UC_\fR" 4 +.IX Item "EV_DEFAULT_UC, EV_DEFAULT_UC_" +Usage identical to \f(CW\*(C`EV_DEFAULT\*(C'\fR and \f(CW\*(C`EV_DEFAULT_\*(C'\fR, but requires that the +default loop has been initialised (\f(CW\*(C`UC\*(C'\fR == unchecked). Their behaviour +is undefined when the default loop has not been initialised by a previous +execution of \f(CW\*(C`EV_DEFAULT\*(C'\fR, \f(CW\*(C`EV_DEFAULT_\*(C'\fR or \f(CW\*(C`ev_default_init (...)\*(C'\fR. +.Sp +It is often prudent to use \f(CW\*(C`EV_DEFAULT\*(C'\fR when initialising the first +watcher in a function but use \f(CW\*(C`EV_DEFAULT_UC\*(C'\fR afterwards. +.PP +Example: Declare and initialise a check watcher, utilising the above +macros so it will work regardless of whether multiple loops are supported +or not. +.PP +.Vb 5 +\& static void +\& check_cb (EV_P_ ev_timer *w, int revents) +\& { +\& ev_check_stop (EV_A_ w); +\& } +\& +\& ev_check check; +\& ev_check_init (&check, check_cb); +\& ev_check_start (EV_DEFAULT_ &check); +\& ev_run (EV_DEFAULT_ 0); +.Ve +.SH "EMBEDDING" +.IX Header "EMBEDDING" +Libev can (and often is) directly embedded into host +applications. Examples of applications that embed it include the Deliantra +Game Server, the \s-1EV\s0 perl module, the \s-1GNU\s0 Virtual Private Ethernet (gvpe) +and rxvt-unicode. +.PP +The goal is to enable you to just copy the necessary files into your +source directory without having to change even a single line in them, so +you can easily upgrade by simply copying (or having a checked-out copy of +libev somewhere in your source tree). +.SS "\s-1FILESETS\s0" +.IX Subsection "FILESETS" +Depending on what features you need you need to include one or more sets of files +in your application. +.PP +\fI\s-1CORE EVENT LOOP\s0\fR +.IX Subsection "CORE EVENT LOOP" +.PP +To include only the libev core (all the \f(CW\*(C`ev_*\*(C'\fR functions), with manual +configuration (no autoconf): +.PP +.Vb 2 +\& #define EV_STANDALONE 1 +\& #include "ev.c" +.Ve +.PP +This will automatically include \fIev.h\fR, too, and should be done in a +single C source file only to provide the function implementations. To use +it, do the same for \fIev.h\fR in all files wishing to use this \s-1API\s0 (best +done by writing a wrapper around \fIev.h\fR that you can include instead and +where you can put other configuration options): +.PP +.Vb 2 +\& #define EV_STANDALONE 1 +\& #include "ev.h" +.Ve +.PP +Both header files and implementation files can be compiled with a \*(C+ +compiler (at least, that's a stated goal, and breakage will be treated +as a bug). +.PP +You need the following files in your source tree, or in a directory +in your include path (e.g. in libev/ when using \-Ilibev): +.PP +.Vb 4 +\& ev.h +\& ev.c +\& ev_vars.h +\& ev_wrap.h +\& +\& ev_win32.c required on win32 platforms only +\& +\& ev_select.c only when select backend is enabled +\& ev_poll.c only when poll backend is enabled +\& ev_epoll.c only when the epoll backend is enabled +\& ev_linuxaio.c only when the linux aio backend is enabled +\& ev_iouring.c only when the linux io_uring backend is enabled +\& ev_kqueue.c only when the kqueue backend is enabled +\& ev_port.c only when the solaris port backend is enabled +.Ve +.PP +\&\fIev.c\fR includes the backend files directly when enabled, so you only need +to compile this single file. +.PP +\fI\s-1LIBEVENT COMPATIBILITY API\s0\fR +.IX Subsection "LIBEVENT COMPATIBILITY API" +.PP +To include the libevent compatibility \s-1API,\s0 also include: +.PP +.Vb 1 +\& #include "event.c" +.Ve +.PP +in the file including \fIev.c\fR, and: +.PP +.Vb 1 +\& #include "event.h" +.Ve +.PP +in the files that want to use the libevent \s-1API.\s0 This also includes \fIev.h\fR. +.PP +You need the following additional files for this: +.PP +.Vb 2 +\& event.h +\& event.c +.Ve +.PP +\fI\s-1AUTOCONF SUPPORT\s0\fR +.IX Subsection "AUTOCONF SUPPORT" +.PP +Instead of using \f(CW\*(C`EV_STANDALONE=1\*(C'\fR and providing your configuration in +whatever way you want, you can also \f(CW\*(C`m4_include([libev.m4])\*(C'\fR in your +\&\fIconfigure.ac\fR and leave \f(CW\*(C`EV_STANDALONE\*(C'\fR undefined. \fIev.c\fR will then +include \fIconfig.h\fR and configure itself accordingly. +.PP +For this of course you need the m4 file: +.PP +.Vb 1 +\& libev.m4 +.Ve +.SS "\s-1PREPROCESSOR SYMBOLS/MACROS\s0" +.IX Subsection "PREPROCESSOR SYMBOLS/MACROS" +Libev can be configured via a variety of preprocessor symbols you have to +define before including (or compiling) any of its files. The default in +the absence of autoconf is documented for every option. +.PP +Symbols marked with \*(L"(h)\*(R" do not change the \s-1ABI,\s0 and can have different +values when compiling libev vs. including \fIev.h\fR, so it is permissible +to redefine them before including \fIev.h\fR without breaking compatibility +to a compiled library. All other symbols change the \s-1ABI,\s0 which means all +users of libev and the libev code itself must be compiled with compatible +settings. +.IP "\s-1EV_COMPAT3\s0 (h)" 4 +.IX Item "EV_COMPAT3 (h)" +Backwards compatibility is a major concern for libev. This is why this +release of libev comes with wrappers for the functions and symbols that +have been renamed between libev version 3 and 4. +.Sp +You can disable these wrappers (to test compatibility with future +versions) by defining \f(CW\*(C`EV_COMPAT3\*(C'\fR to \f(CW0\fR when compiling your +sources. This has the additional advantage that you can drop the \f(CW\*(C`struct\*(C'\fR +from \f(CW\*(C`struct ev_loop\*(C'\fR declarations, as libev will provide an \f(CW\*(C`ev_loop\*(C'\fR +typedef in that case. +.Sp +In some future version, the default for \f(CW\*(C`EV_COMPAT3\*(C'\fR will become \f(CW0\fR, +and in some even more future version the compatibility code will be +removed completely. +.IP "\s-1EV_STANDALONE\s0 (h)" 4 +.IX Item "EV_STANDALONE (h)" +Must always be \f(CW1\fR if you do not use autoconf configuration, which +keeps libev from including \fIconfig.h\fR, and it also defines dummy +implementations for some libevent functions (such as logging, which is not +supported). It will also not define any of the structs usually found in +\&\fIevent.h\fR that are not directly supported by the libev core alone. +.Sp +In standalone mode, libev will still try to automatically deduce the +configuration, but has to be more conservative. +.IP "\s-1EV_USE_FLOOR\s0" 4 +.IX Item "EV_USE_FLOOR" +If defined to be \f(CW1\fR, libev will use the \f(CW\*(C`floor ()\*(C'\fR function for its +periodic reschedule calculations, otherwise libev will fall back on a +portable (slower) implementation. If you enable this, you usually have to +link against libm or something equivalent. Enabling this when the \f(CW\*(C`floor\*(C'\fR +function is not available will fail, so the safe default is to not enable +this. +.IP "\s-1EV_USE_MONOTONIC\s0" 4 +.IX Item "EV_USE_MONOTONIC" +If defined to be \f(CW1\fR, libev will try to detect the availability of the +monotonic clock option at both compile time and runtime. Otherwise no +use of the monotonic clock option will be attempted. If you enable this, +you usually have to link against librt or something similar. Enabling it +when the functionality isn't available is safe, though, although you have +to make sure you link against any libraries where the \f(CW\*(C`clock_gettime\*(C'\fR +function is hiding in (often \fI\-lrt\fR). See also \f(CW\*(C`EV_USE_CLOCK_SYSCALL\*(C'\fR. +.IP "\s-1EV_USE_REALTIME\s0" 4 +.IX Item "EV_USE_REALTIME" +If defined to be \f(CW1\fR, libev will try to detect the availability of the +real-time clock option at compile time (and assume its availability +at runtime if successful). Otherwise no use of the real-time clock +option will be attempted. This effectively replaces \f(CW\*(C`gettimeofday\*(C'\fR +by \f(CW\*(C`clock_get (CLOCK_REALTIME, ...)\*(C'\fR and will not normally affect +correctness. See the note about libraries in the description of +\&\f(CW\*(C`EV_USE_MONOTONIC\*(C'\fR, though. Defaults to the opposite value of +\&\f(CW\*(C`EV_USE_CLOCK_SYSCALL\*(C'\fR. +.IP "\s-1EV_USE_CLOCK_SYSCALL\s0" 4 +.IX Item "EV_USE_CLOCK_SYSCALL" +If defined to be \f(CW1\fR, libev will try to use a direct syscall instead +of calling the system-provided \f(CW\*(C`clock_gettime\*(C'\fR function. This option +exists because on GNU/Linux, \f(CW\*(C`clock_gettime\*(C'\fR is in \f(CW\*(C`librt\*(C'\fR, but \f(CW\*(C`librt\*(C'\fR +unconditionally pulls in \f(CW\*(C`libpthread\*(C'\fR, slowing down single-threaded +programs needlessly. Using a direct syscall is slightly slower (in +theory), because no optimised vdso implementation can be used, but avoids +the pthread dependency. Defaults to \f(CW1\fR on GNU/Linux with glibc 2.x or +higher, as it simplifies linking (no need for \f(CW\*(C`\-lrt\*(C'\fR). +.IP "\s-1EV_USE_NANOSLEEP\s0" 4 +.IX Item "EV_USE_NANOSLEEP" +If defined to be \f(CW1\fR, libev will assume that \f(CW\*(C`nanosleep ()\*(C'\fR is available +and will use it for delays. Otherwise it will use \f(CW\*(C`select ()\*(C'\fR. +.IP "\s-1EV_USE_EVENTFD\s0" 4 +.IX Item "EV_USE_EVENTFD" +If defined to be \f(CW1\fR, then libev will assume that \f(CW\*(C`eventfd ()\*(C'\fR is +available and will probe for kernel support at runtime. This will improve +\&\f(CW\*(C`ev_signal\*(C'\fR and \f(CW\*(C`ev_async\*(C'\fR performance and reduce resource consumption. +If undefined, it will be enabled if the headers indicate GNU/Linux + Glibc +2.7 or newer, otherwise disabled. +.IP "\s-1EV_USE_SIGNALFD\s0" 4 +.IX Item "EV_USE_SIGNALFD" +If defined to be \f(CW1\fR, then libev will assume that \f(CW\*(C`signalfd ()\*(C'\fR is +available and will probe for kernel support at runtime. This enables +the use of \s-1EVFLAG_SIGNALFD\s0 for faster and simpler signal handling. If +undefined, it will be enabled if the headers indicate GNU/Linux + Glibc +2.7 or newer, otherwise disabled. +.IP "\s-1EV_USE_TIMERFD\s0" 4 +.IX Item "EV_USE_TIMERFD" +If defined to be \f(CW1\fR, then libev will assume that \f(CW\*(C`timerfd ()\*(C'\fR is +available and will probe for kernel support at runtime. This allows +libev to detect time jumps accurately. If undefined, it will be enabled +if the headers indicate GNU/Linux + Glibc 2.8 or newer and define +\&\f(CW\*(C`TFD_TIMER_CANCEL_ON_SET\*(C'\fR, otherwise disabled. +.IP "\s-1EV_USE_EVENTFD\s0" 4 +.IX Item "EV_USE_EVENTFD" +If defined to be \f(CW1\fR, then libev will assume that \f(CW\*(C`eventfd ()\*(C'\fR is +available and will probe for kernel support at runtime. This will improve +\&\f(CW\*(C`ev_signal\*(C'\fR and \f(CW\*(C`ev_async\*(C'\fR performance and reduce resource consumption. +If undefined, it will be enabled if the headers indicate GNU/Linux + Glibc +2.7 or newer, otherwise disabled. +.IP "\s-1EV_USE_SELECT\s0" 4 +.IX Item "EV_USE_SELECT" +If undefined or defined to be \f(CW1\fR, libev will compile in support for the +\&\f(CW\*(C`select\*(C'\fR(2) backend. No attempt at auto-detection will be done: if no +other method takes over, select will be it. Otherwise the select backend +will not be compiled in. +.IP "\s-1EV_SELECT_USE_FD_SET\s0" 4 +.IX Item "EV_SELECT_USE_FD_SET" +If defined to \f(CW1\fR, then the select backend will use the system \f(CW\*(C`fd_set\*(C'\fR +structure. This is useful if libev doesn't compile due to a missing +\&\f(CW\*(C`NFDBITS\*(C'\fR or \f(CW\*(C`fd_mask\*(C'\fR definition or it mis-guesses the bitset layout +on exotic systems. This usually limits the range of file descriptors to +some low limit such as 1024 or might have other limitations (winsocket +only allows 64 sockets). The \f(CW\*(C`FD_SETSIZE\*(C'\fR macro, set before compilation, +configures the maximum size of the \f(CW\*(C`fd_set\*(C'\fR. +.IP "\s-1EV_SELECT_IS_WINSOCKET\s0" 4 +.IX Item "EV_SELECT_IS_WINSOCKET" +When defined to \f(CW1\fR, the select backend will assume that +select/socket/connect etc. don't understand file descriptors but +wants osf handles on win32 (this is the case when the select to +be used is the winsock select). This means that it will call +\&\f(CW\*(C`_get_osfhandle\*(C'\fR on the fd to convert it to an \s-1OS\s0 handle. Otherwise, +it is assumed that all these functions actually work on fds, even +on win32. Should not be defined on non\-win32 platforms. +.IP "\s-1EV_FD_TO_WIN32_HANDLE\s0(fd)" 4 +.IX Item "EV_FD_TO_WIN32_HANDLE(fd)" +If \f(CW\*(C`EV_SELECT_IS_WINSOCKET\*(C'\fR is enabled, then libev needs a way to map +file descriptors to socket handles. When not defining this symbol (the +default), then libev will call \f(CW\*(C`_get_osfhandle\*(C'\fR, which is usually +correct. In some cases, programs use their own file descriptor management, +in which case they can provide this function to map fds to socket handles. +.IP "\s-1EV_WIN32_HANDLE_TO_FD\s0(handle)" 4 +.IX Item "EV_WIN32_HANDLE_TO_FD(handle)" +If \f(CW\*(C`EV_SELECT_IS_WINSOCKET\*(C'\fR then libev maps handles to file descriptors +using the standard \f(CW\*(C`_open_osfhandle\*(C'\fR function. For programs implementing +their own fd to handle mapping, overwriting this function makes it easier +to do so. This can be done by defining this macro to an appropriate value. +.IP "\s-1EV_WIN32_CLOSE_FD\s0(fd)" 4 +.IX Item "EV_WIN32_CLOSE_FD(fd)" +If programs implement their own fd to handle mapping on win32, then this +macro can be used to override the \f(CW\*(C`close\*(C'\fR function, useful to unregister +file descriptors again. Note that the replacement function has to close +the underlying \s-1OS\s0 handle. +.IP "\s-1EV_USE_WSASOCKET\s0" 4 +.IX Item "EV_USE_WSASOCKET" +If defined to be \f(CW1\fR, libev will use \f(CW\*(C`WSASocket\*(C'\fR to create its internal +communication socket, which works better in some environments. Otherwise, +the normal \f(CW\*(C`socket\*(C'\fR function will be used, which works better in other +environments. +.IP "\s-1EV_USE_POLL\s0" 4 +.IX Item "EV_USE_POLL" +If defined to be \f(CW1\fR, libev will compile in support for the \f(CW\*(C`poll\*(C'\fR(2) +backend. Otherwise it will be enabled on non\-win32 platforms. It +takes precedence over select. +.IP "\s-1EV_USE_EPOLL\s0" 4 +.IX Item "EV_USE_EPOLL" +If defined to be \f(CW1\fR, libev will compile in support for the Linux +\&\f(CW\*(C`epoll\*(C'\fR(7) backend. Its availability will be detected at runtime, +otherwise another method will be used as fallback. This is the preferred +backend for GNU/Linux systems. If undefined, it will be enabled if the +headers indicate GNU/Linux + Glibc 2.4 or newer, otherwise disabled. +.IP "\s-1EV_USE_LINUXAIO\s0" 4 +.IX Item "EV_USE_LINUXAIO" +If defined to be \f(CW1\fR, libev will compile in support for the Linux aio +backend (\f(CW\*(C`EV_USE_EPOLL\*(C'\fR must also be enabled). If undefined, it will be +enabled on linux, otherwise disabled. +.IP "\s-1EV_USE_IOURING\s0" 4 +.IX Item "EV_USE_IOURING" +If defined to be \f(CW1\fR, libev will compile in support for the Linux +io_uring backend (\f(CW\*(C`EV_USE_EPOLL\*(C'\fR must also be enabled). Due to it's +current limitations it has to be requested explicitly. If undefined, it +will be enabled on linux, otherwise disabled. +.IP "\s-1EV_USE_KQUEUE\s0" 4 +.IX Item "EV_USE_KQUEUE" +If defined to be \f(CW1\fR, libev will compile in support for the \s-1BSD\s0 style +\&\f(CW\*(C`kqueue\*(C'\fR(2) backend. Its actual availability will be detected at runtime, +otherwise another method will be used as fallback. This is the preferred +backend for \s-1BSD\s0 and BSD-like systems, although on most BSDs kqueue only +supports some types of fds correctly (the only platform we found that +supports ptys for example was NetBSD), so kqueue might be compiled in, but +not be used unless explicitly requested. The best way to use it is to find +out whether kqueue supports your type of fd properly and use an embedded +kqueue loop. +.IP "\s-1EV_USE_PORT\s0" 4 +.IX Item "EV_USE_PORT" +If defined to be \f(CW1\fR, libev will compile in support for the Solaris +10 port style backend. Its availability will be detected at runtime, +otherwise another method will be used as fallback. This is the preferred +backend for Solaris 10 systems. +.IP "\s-1EV_USE_DEVPOLL\s0" 4 +.IX Item "EV_USE_DEVPOLL" +Reserved for future expansion, works like the \s-1USE\s0 symbols above. +.IP "\s-1EV_USE_INOTIFY\s0" 4 +.IX Item "EV_USE_INOTIFY" +If defined to be \f(CW1\fR, libev will compile in support for the Linux inotify +interface to speed up \f(CW\*(C`ev_stat\*(C'\fR watchers. Its actual availability will +be detected at runtime. If undefined, it will be enabled if the headers +indicate GNU/Linux + Glibc 2.4 or newer, otherwise disabled. +.IP "\s-1EV_NO_SMP\s0" 4 +.IX Item "EV_NO_SMP" +If defined to be \f(CW1\fR, libev will assume that memory is always coherent +between threads, that is, threads can be used, but threads never run on +different cpus (or different cpu cores). This reduces dependencies +and makes libev faster. +.IP "\s-1EV_NO_THREADS\s0" 4 +.IX Item "EV_NO_THREADS" +If defined to be \f(CW1\fR, libev will assume that it will never be called from +different threads (that includes signal handlers), which is a stronger +assumption than \f(CW\*(C`EV_NO_SMP\*(C'\fR, above. This reduces dependencies and makes +libev faster. +.IP "\s-1EV_ATOMIC_T\s0" 4 +.IX Item "EV_ATOMIC_T" +Libev requires an integer type (suitable for storing \f(CW0\fR or \f(CW1\fR) whose +access is atomic with respect to other threads or signal contexts. No +such type is easily found in the C language, so you can provide your own +type that you know is safe for your purposes. It is used both for signal +handler \*(L"locking\*(R" as well as for signal and thread safety in \f(CW\*(C`ev_async\*(C'\fR +watchers. +.Sp +In the absence of this define, libev will use \f(CW\*(C`sig_atomic_t volatile\*(C'\fR +(from \fIsignal.h\fR), which is usually good enough on most platforms. +.IP "\s-1EV_H\s0 (h)" 4 +.IX Item "EV_H (h)" +The name of the \fIev.h\fR header file used to include it. The default if +undefined is \f(CW"ev.h"\fR in \fIevent.h\fR, \fIev.c\fR and \fIev++.h\fR. This can be +used to virtually rename the \fIev.h\fR header file in case of conflicts. +.IP "\s-1EV_CONFIG_H\s0 (h)" 4 +.IX Item "EV_CONFIG_H (h)" +If \f(CW\*(C`EV_STANDALONE\*(C'\fR isn't \f(CW1\fR, this variable can be used to override +\&\fIev.c\fR's idea of where to find the \fIconfig.h\fR file, similarly to +\&\f(CW\*(C`EV_H\*(C'\fR, above. +.IP "\s-1EV_EVENT_H\s0 (h)" 4 +.IX Item "EV_EVENT_H (h)" +Similarly to \f(CW\*(C`EV_H\*(C'\fR, this macro can be used to override \fIevent.c\fR's idea +of how the \fIevent.h\fR header can be found, the default is \f(CW"event.h"\fR. +.IP "\s-1EV_PROTOTYPES\s0 (h)" 4 +.IX Item "EV_PROTOTYPES (h)" +If defined to be \f(CW0\fR, then \fIev.h\fR will not define any function +prototypes, but still define all the structs and other symbols. This is +occasionally useful if you want to provide your own wrapper functions +around libev functions. +.IP "\s-1EV_MULTIPLICITY\s0" 4 +.IX Item "EV_MULTIPLICITY" +If undefined or defined to \f(CW1\fR, then all event-loop-specific functions +will have the \f(CW\*(C`struct ev_loop *\*(C'\fR as first argument, and you can create +additional independent event loops. Otherwise there will be no support +for multiple event loops and there is no first event loop pointer +argument. Instead, all functions act on the single default loop. +.Sp +Note that \f(CW\*(C`EV_DEFAULT\*(C'\fR and \f(CW\*(C`EV_DEFAULT_\*(C'\fR will no longer provide a +default loop when multiplicity is switched off \- you always have to +initialise the loop manually in this case. +.IP "\s-1EV_MINPRI\s0" 4 +.IX Item "EV_MINPRI" +.PD 0 +.IP "\s-1EV_MAXPRI\s0" 4 +.IX Item "EV_MAXPRI" +.PD +The range of allowed priorities. \f(CW\*(C`EV_MINPRI\*(C'\fR must be smaller or equal to +\&\f(CW\*(C`EV_MAXPRI\*(C'\fR, but otherwise there are no non-obvious limitations. You can +provide for more priorities by overriding those symbols (usually defined +to be \f(CW\*(C`\-2\*(C'\fR and \f(CW2\fR, respectively). +.Sp +When doing priority-based operations, libev usually has to linearly search +all the priorities, so having many of them (hundreds) uses a lot of space +and time, so using the defaults of five priorities (\-2 .. +2) is usually +fine. +.Sp +If your embedding application does not need any priorities, defining these +both to \f(CW0\fR will save some memory and \s-1CPU.\s0 +.IP "\s-1EV_PERIODIC_ENABLE, EV_IDLE_ENABLE, EV_EMBED_ENABLE, EV_STAT_ENABLE, EV_PREPARE_ENABLE, EV_CHECK_ENABLE, EV_FORK_ENABLE, EV_SIGNAL_ENABLE, EV_ASYNC_ENABLE, EV_CHILD_ENABLE.\s0" 4 +.IX Item "EV_PERIODIC_ENABLE, EV_IDLE_ENABLE, EV_EMBED_ENABLE, EV_STAT_ENABLE, EV_PREPARE_ENABLE, EV_CHECK_ENABLE, EV_FORK_ENABLE, EV_SIGNAL_ENABLE, EV_ASYNC_ENABLE, EV_CHILD_ENABLE." +If undefined or defined to be \f(CW1\fR (and the platform supports it), then +the respective watcher type is supported. If defined to be \f(CW0\fR, then it +is not. Disabling watcher types mainly saves code size. +.IP "\s-1EV_FEATURES\s0" 4 +.IX Item "EV_FEATURES" +If you need to shave off some kilobytes of code at the expense of some +speed (but with the full \s-1API\s0), you can define this symbol to request +certain subsets of functionality. The default is to enable all features +that can be enabled on the platform. +.Sp +A typical way to use this symbol is to define it to \f(CW0\fR (or to a bitset +with some broad features you want) and then selectively re-enable +additional parts you want, for example if you want everything minimal, +but multiple event loop support, async and child watchers and the poll +backend, use this: +.Sp +.Vb 5 +\& #define EV_FEATURES 0 +\& #define EV_MULTIPLICITY 1 +\& #define EV_USE_POLL 1 +\& #define EV_CHILD_ENABLE 1 +\& #define EV_ASYNC_ENABLE 1 +.Ve +.Sp +The actual value is a bitset, it can be a combination of the following +values (by default, all of these are enabled): +.RS 4 +.ie n .IP "1 \- faster/larger code" 4 +.el .IP "\f(CW1\fR \- faster/larger code" 4 +.IX Item "1 - faster/larger code" +Use larger code to speed up some operations. +.Sp +Currently this is used to override some inlining decisions (enlarging the +code size by roughly 30% on amd64). +.Sp +When optimising for size, use of compiler flags such as \f(CW\*(C`\-Os\*(C'\fR with +gcc is recommended, as well as \f(CW\*(C`\-DNDEBUG\*(C'\fR, as libev contains a number of +assertions. +.Sp +The default is off when \f(CW\*(C`_\|_OPTIMIZE_SIZE_\|_\*(C'\fR is defined by your compiler +(e.g. gcc with \f(CW\*(C`\-Os\*(C'\fR). +.ie n .IP "2 \- faster/larger data structures" 4 +.el .IP "\f(CW2\fR \- faster/larger data structures" 4 +.IX Item "2 - faster/larger data structures" +Replaces the small 2\-heap for timer management by a faster 4\-heap, larger +hash table sizes and so on. This will usually further increase code size +and can additionally have an effect on the size of data structures at +runtime. +.Sp +The default is off when \f(CW\*(C`_\|_OPTIMIZE_SIZE_\|_\*(C'\fR is defined by your compiler +(e.g. gcc with \f(CW\*(C`\-Os\*(C'\fR). +.ie n .IP "4 \- full \s-1API\s0 configuration" 4 +.el .IP "\f(CW4\fR \- full \s-1API\s0 configuration" 4 +.IX Item "4 - full API configuration" +This enables priorities (sets \f(CW\*(C`EV_MAXPRI\*(C'\fR=2 and \f(CW\*(C`EV_MINPRI\*(C'\fR=\-2), and +enables multiplicity (\f(CW\*(C`EV_MULTIPLICITY\*(C'\fR=1). +.ie n .IP "8 \- full \s-1API\s0" 4 +.el .IP "\f(CW8\fR \- full \s-1API\s0" 4 +.IX Item "8 - full API" +This enables a lot of the \*(L"lesser used\*(R" \s-1API\s0 functions. See \f(CW\*(C`ev.h\*(C'\fR for +details on which parts of the \s-1API\s0 are still available without this +feature, and do not complain if this subset changes over time. +.ie n .IP "16 \- enable all optional watcher types" 4 +.el .IP "\f(CW16\fR \- enable all optional watcher types" 4 +.IX Item "16 - enable all optional watcher types" +Enables all optional watcher types. If you want to selectively enable +only some watcher types other than I/O and timers (e.g. prepare, +embed, async, child...) you can enable them manually by defining +\&\f(CW\*(C`EV_watchertype_ENABLE\*(C'\fR to \f(CW1\fR instead. +.ie n .IP "32 \- enable all backends" 4 +.el .IP "\f(CW32\fR \- enable all backends" 4 +.IX Item "32 - enable all backends" +This enables all backends \- without this feature, you need to enable at +least one backend manually (\f(CW\*(C`EV_USE_SELECT\*(C'\fR is a good choice). +.ie n .IP "64 \- enable OS-specific ""helper"" APIs" 4 +.el .IP "\f(CW64\fR \- enable OS-specific ``helper'' APIs" 4 +.IX Item "64 - enable OS-specific helper APIs" +Enable inotify, eventfd, signalfd and similar OS-specific helper APIs by +default. +.RE +.RS 4 +.Sp +Compiling with \f(CW\*(C`gcc \-Os \-DEV_STANDALONE \-DEV_USE_EPOLL=1 \-DEV_FEATURES=0\*(C'\fR +reduces the compiled size of libev from 24.7Kb code/2.8Kb data to 6.5Kb +code/0.3Kb data on my GNU/Linux amd64 system, while still giving you I/O +watchers, timers and monotonic clock support. +.Sp +With an intelligent-enough linker (gcc+binutils are intelligent enough +when you use \f(CW\*(C`\-Wl,\-\-gc\-sections \-ffunction\-sections\*(C'\fR) functions unused by +your program might be left out as well \- a binary starting a timer and an +I/O watcher then might come out at only 5Kb. +.RE +.IP "\s-1EV_API_STATIC\s0" 4 +.IX Item "EV_API_STATIC" +If this symbol is defined (by default it is not), then all identifiers +will have static linkage. This means that libev will not export any +identifiers, and you cannot link against libev anymore. This can be useful +when you embed libev, only want to use libev functions in a single file, +and do not want its identifiers to be visible. +.Sp +To use this, define \f(CW\*(C`EV_API_STATIC\*(C'\fR and include \fIev.c\fR in the file that +wants to use libev. +.Sp +This option only works when libev is compiled with a C compiler, as \*(C+ +doesn't support the required declaration syntax. +.IP "\s-1EV_AVOID_STDIO\s0" 4 +.IX Item "EV_AVOID_STDIO" +If this is set to \f(CW1\fR at compiletime, then libev will avoid using stdio +functions (printf, scanf, perror etc.). This will increase the code size +somewhat, but if your program doesn't otherwise depend on stdio and your +libc allows it, this avoids linking in the stdio library which is quite +big. +.Sp +Note that error messages might become less precise when this option is +enabled. +.IP "\s-1EV_NSIG\s0" 4 +.IX Item "EV_NSIG" +The highest supported signal number, +1 (or, the number of +signals): Normally, libev tries to deduce the maximum number of signals +automatically, but sometimes this fails, in which case it can be +specified. Also, using a lower number than detected (\f(CW32\fR should be +good for about any system in existence) can save some memory, as libev +statically allocates some 12\-24 bytes per signal number. +.IP "\s-1EV_PID_HASHSIZE\s0" 4 +.IX Item "EV_PID_HASHSIZE" +\&\f(CW\*(C`ev_child\*(C'\fR watchers use a small hash table to distribute workload by +pid. The default size is \f(CW16\fR (or \f(CW1\fR with \f(CW\*(C`EV_FEATURES\*(C'\fR disabled), +usually more than enough. If you need to manage thousands of children you +might want to increase this value (\fImust\fR be a power of two). +.IP "\s-1EV_INOTIFY_HASHSIZE\s0" 4 +.IX Item "EV_INOTIFY_HASHSIZE" +\&\f(CW\*(C`ev_stat\*(C'\fR watchers use a small hash table to distribute workload by +inotify watch id. The default size is \f(CW16\fR (or \f(CW1\fR with \f(CW\*(C`EV_FEATURES\*(C'\fR +disabled), usually more than enough. If you need to manage thousands of +\&\f(CW\*(C`ev_stat\*(C'\fR watchers you might want to increase this value (\fImust\fR be a +power of two). +.IP "\s-1EV_USE_4HEAP\s0" 4 +.IX Item "EV_USE_4HEAP" +Heaps are not very cache-efficient. To improve the cache-efficiency of the +timer and periodics heaps, libev uses a 4\-heap when this symbol is defined +to \f(CW1\fR. The 4\-heap uses more complicated (longer) code but has noticeably +faster performance with many (thousands) of watchers. +.Sp +The default is \f(CW1\fR, unless \f(CW\*(C`EV_FEATURES\*(C'\fR overrides it, in which case it +will be \f(CW0\fR. +.IP "\s-1EV_HEAP_CACHE_AT\s0" 4 +.IX Item "EV_HEAP_CACHE_AT" +Heaps are not very cache-efficient. To improve the cache-efficiency of the +timer and periodics heaps, libev can cache the timestamp (\fIat\fR) within +the heap structure (selected by defining \f(CW\*(C`EV_HEAP_CACHE_AT\*(C'\fR to \f(CW1\fR), +which uses 8\-12 bytes more per watcher and a few hundred bytes more code, +but avoids random read accesses on heap changes. This improves performance +noticeably with many (hundreds) of watchers. +.Sp +The default is \f(CW1\fR, unless \f(CW\*(C`EV_FEATURES\*(C'\fR overrides it, in which case it +will be \f(CW0\fR. +.IP "\s-1EV_VERIFY\s0" 4 +.IX Item "EV_VERIFY" +Controls how much internal verification (see \f(CW\*(C`ev_verify ()\*(C'\fR) will +be done: If set to \f(CW0\fR, no internal verification code will be compiled +in. If set to \f(CW1\fR, then verification code will be compiled in, but not +called. If set to \f(CW2\fR, then the internal verification code will be +called once per loop, which can slow down libev. If set to \f(CW3\fR, then the +verification code will be called very frequently, which will slow down +libev considerably. +.Sp +Verification errors are reported via C's \f(CW\*(C`assert\*(C'\fR mechanism, so if you +disable that (e.g. by defining \f(CW\*(C`NDEBUG\*(C'\fR) then no errors will be reported. +.Sp +The default is \f(CW1\fR, unless \f(CW\*(C`EV_FEATURES\*(C'\fR overrides it, in which case it +will be \f(CW0\fR. +.IP "\s-1EV_COMMON\s0" 4 +.IX Item "EV_COMMON" +By default, all watchers have a \f(CW\*(C`void *data\*(C'\fR member. By redefining +this macro to something else you can include more and other types of +members. You have to define it each time you include one of the files, +though, and it must be identical each time. +.Sp +For example, the perl \s-1EV\s0 module uses something like this: +.Sp +.Vb 3 +\& #define EV_COMMON \e +\& SV *self; /* contains this struct */ \e +\& SV *cb_sv, *fh /* note no trailing ";" */ +.Ve +.IP "\s-1EV_CB_DECLARE\s0 (type)" 4 +.IX Item "EV_CB_DECLARE (type)" +.PD 0 +.IP "\s-1EV_CB_INVOKE\s0 (watcher, revents)" 4 +.IX Item "EV_CB_INVOKE (watcher, revents)" +.IP "ev_set_cb (ev, cb)" 4 +.IX Item "ev_set_cb (ev, cb)" +.PD +Can be used to change the callback member declaration in each watcher, +and the way callbacks are invoked and set. Must expand to a struct member +definition and a statement, respectively. See the \fIev.h\fR header file for +their default definitions. One possible use for overriding these is to +avoid the \f(CW\*(C`struct ev_loop *\*(C'\fR as first argument in all cases, or to use +method calls instead of plain function calls in \*(C+. +.SS "\s-1EXPORTED API SYMBOLS\s0" +.IX Subsection "EXPORTED API SYMBOLS" +If you need to re-export the \s-1API\s0 (e.g. via a \s-1DLL\s0) and you need a list of +exported symbols, you can use the provided \fISymbol.*\fR files which list +all public symbols, one per line: +.PP +.Vb 2 +\& Symbols.ev for libev proper +\& Symbols.event for the libevent emulation +.Ve +.PP +This can also be used to rename all public symbols to avoid clashes with +multiple versions of libev linked together (which is obviously bad in +itself, but sometimes it is inconvenient to avoid this). +.PP +A sed command like this will create wrapper \f(CW\*(C`#define\*(C'\fR's that you need to +include before including \fIev.h\fR: +.PP +.Vb 1 +\& wrap.h +.Ve +.PP +This would create a file \fIwrap.h\fR which essentially looks like this: +.PP +.Vb 4 +\& #define ev_backend myprefix_ev_backend +\& #define ev_check_start myprefix_ev_check_start +\& #define ev_check_stop myprefix_ev_check_stop +\& ... +.Ve +.SS "\s-1EXAMPLES\s0" +.IX Subsection "EXAMPLES" +For a real-world example of a program the includes libev +verbatim, you can have a look at the \s-1EV\s0 perl module +(). It has the libev files in +the \fIlibev/\fR subdirectory and includes them in the \fI\s-1EV/EVAPI\s0.h\fR (public +interface) and \fI\s-1EV\s0.xs\fR (implementation) files. Only the \fI\s-1EV\s0.xs\fR file +will be compiled. It is pretty complex because it provides its own header +file. +.PP +The usage in rxvt-unicode is simpler. It has a \fIev_cpp.h\fR header file +that everybody includes and which overrides some configure choices: +.PP +.Vb 8 +\& #define EV_FEATURES 8 +\& #define EV_USE_SELECT 1 +\& #define EV_PREPARE_ENABLE 1 +\& #define EV_IDLE_ENABLE 1 +\& #define EV_SIGNAL_ENABLE 1 +\& #define EV_CHILD_ENABLE 1 +\& #define EV_USE_STDEXCEPT 0 +\& #define EV_CONFIG_H +\& +\& #include "ev++.h" +.Ve +.PP +And a \fIev_cpp.C\fR implementation file that contains libev proper and is compiled: +.PP +.Vb 2 +\& #include "ev_cpp.h" +\& #include "ev.c" +.Ve +.SH "INTERACTION WITH OTHER PROGRAMS, LIBRARIES OR THE ENVIRONMENT" +.IX Header "INTERACTION WITH OTHER PROGRAMS, LIBRARIES OR THE ENVIRONMENT" +.SS "\s-1THREADS AND COROUTINES\s0" +.IX Subsection "THREADS AND COROUTINES" +\fI\s-1THREADS\s0\fR +.IX Subsection "THREADS" +.PP +All libev functions are reentrant and thread-safe unless explicitly +documented otherwise, but libev implements no locking itself. This means +that you can use as many loops as you want in parallel, as long as there +are no concurrent calls into any libev function with the same loop +parameter (\f(CW\*(C`ev_default_*\*(C'\fR calls have an implicit default loop parameter, +of course): libev guarantees that different event loops share no data +structures that need any locking. +.PP +Or to put it differently: calls with different loop parameters can be done +concurrently from multiple threads, calls with the same loop parameter +must be done serially (but can be done from different threads, as long as +only one thread ever is inside a call at any point in time, e.g. by using +a mutex per loop). +.PP +Specifically to support threads (and signal handlers), libev implements +so-called \f(CW\*(C`ev_async\*(C'\fR watchers, which allow some limited form of +concurrency on the same event loop, namely waking it up \*(L"from the +outside\*(R". +.PP +If you want to know which design (one loop, locking, or multiple loops +without or something else still) is best for your problem, then I cannot +help you, but here is some generic advice: +.IP "\(bu" 4 +most applications have a main thread: use the default libev loop +in that thread, or create a separate thread running only the default loop. +.Sp +This helps integrating other libraries or software modules that use libev +themselves and don't care/know about threading. +.IP "\(bu" 4 +one loop per thread is usually a good model. +.Sp +Doing this is almost never wrong, sometimes a better-performance model +exists, but it is always a good start. +.IP "\(bu" 4 +other models exist, such as the leader/follower pattern, where one +loop is handed through multiple threads in a kind of round-robin fashion. +.Sp +Choosing a model is hard \- look around, learn, know that usually you can do +better than you currently do :\-) +.IP "\(bu" 4 +often you need to talk to some other thread which blocks in the +event loop. +.Sp +\&\f(CW\*(C`ev_async\*(C'\fR watchers can be used to wake them up from other threads safely +(or from signal contexts...). +.Sp +An example use would be to communicate signals or other events that only +work in the default loop by registering the signal watcher with the +default loop and triggering an \f(CW\*(C`ev_async\*(C'\fR watcher from the default loop +watcher callback into the event loop interested in the signal. +.PP +See also \*(L"\s-1THREAD LOCKING EXAMPLE\*(R"\s0. +.PP +\fI\s-1COROUTINES\s0\fR +.IX Subsection "COROUTINES" +.PP +Libev is very accommodating to coroutines (\*(L"cooperative threads\*(R"): +libev fully supports nesting calls to its functions from different +coroutines (e.g. you can call \f(CW\*(C`ev_run\*(C'\fR on the same loop from two +different coroutines, and switch freely between both coroutines running +the loop, as long as you don't confuse yourself). The only exception is +that you must not do this from \f(CW\*(C`ev_periodic\*(C'\fR reschedule callbacks. +.PP +Care has been taken to ensure that libev does not keep local state inside +\&\f(CW\*(C`ev_run\*(C'\fR, and other calls do not usually allow for coroutine switches as +they do not call any callbacks. +.SS "\s-1COMPILER WARNINGS\s0" +.IX Subsection "COMPILER WARNINGS" +Depending on your compiler and compiler settings, you might get no or a +lot of warnings when compiling libev code. Some people are apparently +scared by this. +.PP +However, these are unavoidable for many reasons. For one, each compiler +has different warnings, and each user has different tastes regarding +warning options. \*(L"Warn-free\*(R" code therefore cannot be a goal except when +targeting a specific compiler and compiler-version. +.PP +Another reason is that some compiler warnings require elaborate +workarounds, or other changes to the code that make it less clear and less +maintainable. +.PP +And of course, some compiler warnings are just plain stupid, or simply +wrong (because they don't actually warn about the condition their message +seems to warn about). For example, certain older gcc versions had some +warnings that resulted in an extreme number of false positives. These have +been fixed, but some people still insist on making code warn-free with +such buggy versions. +.PP +While libev is written to generate as few warnings as possible, +\&\*(L"warn-free\*(R" code is not a goal, and it is recommended not to build libev +with any compiler warnings enabled unless you are prepared to cope with +them (e.g. by ignoring them). Remember that warnings are just that: +warnings, not errors, or proof of bugs. +.SS "\s-1VALGRIND\s0" +.IX Subsection "VALGRIND" +Valgrind has a special section here because it is a popular tool that is +highly useful. Unfortunately, valgrind reports are very hard to interpret. +.PP +If you think you found a bug (memory leak, uninitialised data access etc.) +in libev, then check twice: If valgrind reports something like: +.PP +.Vb 3 +\& ==2274== definitely lost: 0 bytes in 0 blocks. +\& ==2274== possibly lost: 0 bytes in 0 blocks. +\& ==2274== still reachable: 256 bytes in 1 blocks. +.Ve +.PP +Then there is no memory leak, just as memory accounted to global variables +is not a memleak \- the memory is still being referenced, and didn't leak. +.PP +Similarly, under some circumstances, valgrind might report kernel bugs +as if it were a bug in libev (e.g. in realloc or in the poll backend, +although an acceptable workaround has been found here), or it might be +confused. +.PP +Keep in mind that valgrind is a very good tool, but only a tool. Don't +make it into some kind of religion. +.PP +If you are unsure about something, feel free to contact the mailing list +with the full valgrind report and an explanation on why you think this +is a bug in libev (best check the archives, too :). However, don't be +annoyed when you get a brisk \*(L"this is no bug\*(R" answer and take the chance +of learning how to interpret valgrind properly. +.PP +If you need, for some reason, empty reports from valgrind for your project +I suggest using suppression lists. +.SH "PORTABILITY NOTES" +.IX Header "PORTABILITY NOTES" +.SS "\s-1GNU/LINUX 32 BIT LIMITATIONS\s0" +.IX Subsection "GNU/LINUX 32 BIT LIMITATIONS" +GNU/Linux is the only common platform that supports 64 bit file/large file +interfaces but \fIdisables\fR them by default. +.PP +That means that libev compiled in the default environment doesn't support +files larger than 2GiB or so, which mainly affects \f(CW\*(C`ev_stat\*(C'\fR watchers. +.PP +Unfortunately, many programs try to work around this GNU/Linux issue +by enabling the large file \s-1API,\s0 which makes them incompatible with the +standard libev compiled for their system. +.PP +Likewise, libev cannot enable the large file \s-1API\s0 itself as this would +suddenly make it incompatible to the default compile time environment, +i.e. all programs not using special compile switches. +.SS "\s-1OS/X AND DARWIN BUGS\s0" +.IX Subsection "OS/X AND DARWIN BUGS" +The whole thing is a bug if you ask me \- basically any system interface +you touch is broken, whether it is locales, poll, kqueue or even the +OpenGL drivers. +.PP +\fI\f(CI\*(C`kqueue\*(C'\fI is buggy\fR +.IX Subsection "kqueue is buggy" +.PP +The kqueue syscall is broken in all known versions \- most versions support +only sockets, many support pipes. +.PP +Libev tries to work around this by not using \f(CW\*(C`kqueue\*(C'\fR by default on this +rotten platform, but of course you can still ask for it when creating a +loop \- embedding a socket-only kqueue loop into a select-based one is +probably going to work well. +.PP +\fI\f(CI\*(C`poll\*(C'\fI is buggy\fR +.IX Subsection "poll is buggy" +.PP +Instead of fixing \f(CW\*(C`kqueue\*(C'\fR, Apple replaced their (working) \f(CW\*(C`poll\*(C'\fR +implementation by something calling \f(CW\*(C`kqueue\*(C'\fR internally around the 10.5.6 +release, so now \f(CW\*(C`kqueue\*(C'\fR \fIand\fR \f(CW\*(C`poll\*(C'\fR are broken. +.PP +Libev tries to work around this by not using \f(CW\*(C`poll\*(C'\fR by default on +this rotten platform, but of course you can still ask for it when creating +a loop. +.PP +\fI\f(CI\*(C`select\*(C'\fI is buggy\fR +.IX Subsection "select is buggy" +.PP +All that's left is \f(CW\*(C`select\*(C'\fR, and of course Apple found a way to fuck this +one up as well: On \s-1OS/X,\s0 \f(CW\*(C`select\*(C'\fR actively limits the number of file +descriptors you can pass in to 1024 \- your program suddenly crashes when +you use more. +.PP +There is an undocumented \*(L"workaround\*(R" for this \- defining +\&\f(CW\*(C`_DARWIN_UNLIMITED_SELECT\*(C'\fR, which libev tries to use, so select \fIshould\fR +work on \s-1OS/X.\s0 +.SS "\s-1SOLARIS PROBLEMS AND WORKAROUNDS\s0" +.IX Subsection "SOLARIS PROBLEMS AND WORKAROUNDS" +\fI\f(CI\*(C`errno\*(C'\fI reentrancy\fR +.IX Subsection "errno reentrancy" +.PP +The default compile environment on Solaris is unfortunately so +thread-unsafe that you can't even use components/libraries compiled +without \f(CW\*(C`\-D_REENTRANT\*(C'\fR in a threaded program, which, of course, isn't +defined by default. A valid, if stupid, implementation choice. +.PP +If you want to use libev in threaded environments you have to make sure +it's compiled with \f(CW\*(C`_REENTRANT\*(C'\fR defined. +.PP +\fIEvent port backend\fR +.IX Subsection "Event port backend" +.PP +The scalable event interface for Solaris is called \*(L"event +ports\*(R". Unfortunately, this mechanism is very buggy in all major +releases. If you run into high \s-1CPU\s0 usage, your program freezes or you get +a large number of spurious wakeups, make sure you have all the relevant +and latest kernel patches applied. No, I don't know which ones, but there +are multiple ones to apply, and afterwards, event ports actually work +great. +.PP +If you can't get it to work, you can try running the program by setting +the environment variable \f(CW\*(C`LIBEV_FLAGS=3\*(C'\fR to only allow \f(CW\*(C`poll\*(C'\fR and +\&\f(CW\*(C`select\*(C'\fR backends. +.SS "\s-1AIX POLL BUG\s0" +.IX Subsection "AIX POLL BUG" +\&\s-1AIX\s0 unfortunately has a broken \f(CW\*(C`poll.h\*(C'\fR header. Libev works around +this by trying to avoid the poll backend altogether (i.e. it's not even +compiled in), which normally isn't a big problem as \f(CW\*(C`select\*(C'\fR works fine +with large bitsets on \s-1AIX,\s0 and \s-1AIX\s0 is dead anyway. +.SS "\s-1WIN32 PLATFORM LIMITATIONS AND WORKAROUNDS\s0" +.IX Subsection "WIN32 PLATFORM LIMITATIONS AND WORKAROUNDS" +\fIGeneral issues\fR +.IX Subsection "General issues" +.PP +Win32 doesn't support any of the standards (e.g. \s-1POSIX\s0) that libev +requires, and its I/O model is fundamentally incompatible with the \s-1POSIX\s0 +model. Libev still offers limited functionality on this platform in +the form of the \f(CW\*(C`EVBACKEND_SELECT\*(C'\fR backend, and only supports socket +descriptors. This only applies when using Win32 natively, not when using +e.g. cygwin. Actually, it only applies to the microsofts own compilers, +as every compiler comes with a slightly differently broken/incompatible +environment. +.PP +Lifting these limitations would basically require the full +re-implementation of the I/O system. If you are into this kind of thing, +then note that glib does exactly that for you in a very portable way (note +also that glib is the slowest event library known to man). +.PP +There is no supported compilation method available on windows except +embedding it into other applications. +.PP +Sensible signal handling is officially unsupported by Microsoft \- libev +tries its best, but under most conditions, signals will simply not work. +.PP +Not a libev limitation but worth mentioning: windows apparently doesn't +accept large writes: instead of resulting in a partial write, windows will +either accept everything or return \f(CW\*(C`ENOBUFS\*(C'\fR if the buffer is too large, +so make sure you only write small amounts into your sockets (less than a +megabyte seems safe, but this apparently depends on the amount of memory +available). +.PP +Due to the many, low, and arbitrary limits on the win32 platform and +the abysmal performance of winsockets, using a large number of sockets +is not recommended (and not reasonable). If your program needs to use +more than a hundred or so sockets, then likely it needs to use a totally +different implementation for windows, as libev offers the \s-1POSIX\s0 readiness +notification model, which cannot be implemented efficiently on windows +(due to Microsoft monopoly games). +.PP +A typical way to use libev under windows is to embed it (see the embedding +section for details) and use the following \fIevwrap.h\fR header file instead +of \fIev.h\fR: +.PP +.Vb 2 +\& #define EV_STANDALONE /* keeps ev from requiring config.h */ +\& #define EV_SELECT_IS_WINSOCKET 1 /* configure libev for windows select */ +\& +\& #include "ev.h" +.Ve +.PP +And compile the following \fIevwrap.c\fR file into your project (make sure +you do \fInot\fR compile the \fIev.c\fR or any other embedded source files!): +.PP +.Vb 2 +\& #include "evwrap.h" +\& #include "ev.c" +.Ve +.PP +\fIThe winsocket \f(CI\*(C`select\*(C'\fI function\fR +.IX Subsection "The winsocket select function" +.PP +The winsocket \f(CW\*(C`select\*(C'\fR function doesn't follow \s-1POSIX\s0 in that it +requires socket \fIhandles\fR and not socket \fIfile descriptors\fR (it is +also extremely buggy). This makes select very inefficient, and also +requires a mapping from file descriptors to socket handles (the Microsoft +C runtime provides the function \f(CW\*(C`_open_osfhandle\*(C'\fR for this). See the +discussion of the \f(CW\*(C`EV_SELECT_USE_FD_SET\*(C'\fR, \f(CW\*(C`EV_SELECT_IS_WINSOCKET\*(C'\fR and +\&\f(CW\*(C`EV_FD_TO_WIN32_HANDLE\*(C'\fR preprocessor symbols for more info. +.PP +The configuration for a \*(L"naked\*(R" win32 using the Microsoft runtime +libraries and raw winsocket select is: +.PP +.Vb 2 +\& #define EV_USE_SELECT 1 +\& #define EV_SELECT_IS_WINSOCKET 1 /* forces EV_SELECT_USE_FD_SET, too */ +.Ve +.PP +Note that winsockets handling of fd sets is O(n), so you can easily get a +complexity in the O(nX) range when using win32. +.PP +\fILimited number of file descriptors\fR +.IX Subsection "Limited number of file descriptors" +.PP +Windows has numerous arbitrary (and low) limits on things. +.PP +Early versions of winsocket's select only supported waiting for a maximum +of \f(CW64\fR handles (probably owning to the fact that all windows kernels +can only wait for \f(CW64\fR things at the same time internally; Microsoft +recommends spawning a chain of threads and wait for 63 handles and the +previous thread in each. Sounds great!). +.PP +Newer versions support more handles, but you need to define \f(CW\*(C`FD_SETSIZE\*(C'\fR +to some high number (e.g. \f(CW2048\fR) before compiling the winsocket select +call (which might be in libev or elsewhere, for example, perl and many +other interpreters do their own select emulation on windows). +.PP +Another limit is the number of file descriptors in the Microsoft runtime +libraries, which by default is \f(CW64\fR (there must be a hidden \fI64\fR +fetish or something like this inside Microsoft). You can increase this +by calling \f(CW\*(C`_setmaxstdio\*(C'\fR, which can increase this limit to \f(CW2048\fR +(another arbitrary limit), but is broken in many versions of the Microsoft +runtime libraries. This might get you to about \f(CW512\fR or \f(CW2048\fR sockets +(depending on windows version and/or the phase of the moon). To get more, +you need to wrap all I/O functions and provide your own fd management, but +the cost of calling select (O(nX)) will likely make this unworkable. +.SS "\s-1PORTABILITY REQUIREMENTS\s0" +.IX Subsection "PORTABILITY REQUIREMENTS" +In addition to a working ISO-C implementation and of course the +backend-specific APIs, libev relies on a few additional extensions: +.ie n .IP """void (*)(ev_watcher_type *, int revents)"" must have compatible calling conventions regardless of ""ev_watcher_type *""." 4 +.el .IP "\f(CWvoid (*)(ev_watcher_type *, int revents)\fR must have compatible calling conventions regardless of \f(CWev_watcher_type *\fR." 4 +.IX Item "void (*)(ev_watcher_type *, int revents) must have compatible calling conventions regardless of ev_watcher_type *." +Libev assumes not only that all watcher pointers have the same internal +structure (guaranteed by \s-1POSIX\s0 but not by \s-1ISO C\s0 for example), but it also +assumes that the same (machine) code can be used to call any watcher +callback: The watcher callbacks have different type signatures, but libev +calls them using an \f(CW\*(C`ev_watcher *\*(C'\fR internally. +.IP "null pointers and integer zero are represented by 0 bytes" 4 +.IX Item "null pointers and integer zero are represented by 0 bytes" +Libev uses \f(CW\*(C`memset\*(C'\fR to initialise structs and arrays to \f(CW0\fR bytes, and +relies on this setting pointers and integers to null. +.IP "pointer accesses must be thread-atomic" 4 +.IX Item "pointer accesses must be thread-atomic" +Accessing a pointer value must be atomic, it must both be readable and +writable in one piece \- this is the case on all current architectures. +.ie n .IP """sig_atomic_t volatile"" must be thread-atomic as well" 4 +.el .IP "\f(CWsig_atomic_t volatile\fR must be thread-atomic as well" 4 +.IX Item "sig_atomic_t volatile must be thread-atomic as well" +The type \f(CW\*(C`sig_atomic_t volatile\*(C'\fR (or whatever is defined as +\&\f(CW\*(C`EV_ATOMIC_T\*(C'\fR) must be atomic with respect to accesses from different +threads. This is not part of the specification for \f(CW\*(C`sig_atomic_t\*(C'\fR, but is +believed to be sufficiently portable. +.ie n .IP """sigprocmask"" must work in a threaded environment" 4 +.el .IP "\f(CWsigprocmask\fR must work in a threaded environment" 4 +.IX Item "sigprocmask must work in a threaded environment" +Libev uses \f(CW\*(C`sigprocmask\*(C'\fR to temporarily block signals. This is not +allowed in a threaded program (\f(CW\*(C`pthread_sigmask\*(C'\fR has to be used). Typical +pthread implementations will either allow \f(CW\*(C`sigprocmask\*(C'\fR in the \*(L"main +thread\*(R" or will block signals process-wide, both behaviours would +be compatible with libev. Interaction between \f(CW\*(C`sigprocmask\*(C'\fR and +\&\f(CW\*(C`pthread_sigmask\*(C'\fR could complicate things, however. +.Sp +The most portable way to handle signals is to block signals in all threads +except the initial one, and run the signal handling loop in the initial +thread as well. +.ie n .IP """long"" must be large enough for common memory allocation sizes" 4 +.el .IP "\f(CWlong\fR must be large enough for common memory allocation sizes" 4 +.IX Item "long must be large enough for common memory allocation sizes" +To improve portability and simplify its \s-1API,\s0 libev uses \f(CW\*(C`long\*(C'\fR internally +instead of \f(CW\*(C`size_t\*(C'\fR when allocating its data structures. On non-POSIX +systems (Microsoft...) this might be unexpectedly low, but is still at +least 31 bits everywhere, which is enough for hundreds of millions of +watchers. +.ie n .IP """double"" must hold a time value in seconds with enough accuracy" 4 +.el .IP "\f(CWdouble\fR must hold a time value in seconds with enough accuracy" 4 +.IX Item "double must hold a time value in seconds with enough accuracy" +The type \f(CW\*(C`double\*(C'\fR is used to represent timestamps. It is required to +have at least 51 bits of mantissa (and 9 bits of exponent), which is +good enough for at least into the year 4000 with millisecond accuracy +(the design goal for libev). This requirement is overfulfilled by +implementations using \s-1IEEE 754,\s0 which is basically all existing ones. +.Sp +With \s-1IEEE 754\s0 doubles, you get microsecond accuracy until at least the +year 2255 (and millisecond accuracy till the year 287396 \- by then, libev +is either obsolete or somebody patched it to use \f(CW\*(C`long double\*(C'\fR or +something like that, just kidding). +.PP +If you know of other additional requirements drop me a note. +.SH "ALGORITHMIC COMPLEXITIES" +.IX Header "ALGORITHMIC COMPLEXITIES" +In this section the complexities of (many of) the algorithms used inside +libev will be documented. For complexity discussions about backends see +the documentation for \f(CW\*(C`ev_default_init\*(C'\fR. +.PP +All of the following are about amortised time: If an array needs to be +extended, libev needs to realloc and move the whole array, but this +happens asymptotically rarer with higher number of elements, so O(1) might +mean that libev does a lengthy realloc operation in rare cases, but on +average it is much faster and asymptotically approaches constant time. +.IP "Starting and stopping timer/periodic watchers: O(log skipped_other_timers)" 4 +.IX Item "Starting and stopping timer/periodic watchers: O(log skipped_other_timers)" +This means that, when you have a watcher that triggers in one hour and +there are 100 watchers that would trigger before that, then inserting will +have to skip roughly seven (\f(CW\*(C`ld 100\*(C'\fR) of these watchers. +.IP "Changing timer/periodic watchers (by autorepeat or calling again): O(log skipped_other_timers)" 4 +.IX Item "Changing timer/periodic watchers (by autorepeat or calling again): O(log skipped_other_timers)" +That means that changing a timer costs less than removing/adding them, +as only the relative motion in the event queue has to be paid for. +.IP "Starting io/check/prepare/idle/signal/child/fork/async watchers: O(1)" 4 +.IX Item "Starting io/check/prepare/idle/signal/child/fork/async watchers: O(1)" +These just add the watcher into an array or at the head of a list. +.IP "Stopping check/prepare/idle/fork/async watchers: O(1)" 4 +.IX Item "Stopping check/prepare/idle/fork/async watchers: O(1)" +.PD 0 +.IP "Stopping an io/signal/child watcher: O(number_of_watchers_for_this_(fd/signal/pid % \s-1EV_PID_HASHSIZE\s0))" 4 +.IX Item "Stopping an io/signal/child watcher: O(number_of_watchers_for_this_(fd/signal/pid % EV_PID_HASHSIZE))" +.PD +These watchers are stored in lists, so they need to be walked to find the +correct watcher to remove. The lists are usually short (you don't usually +have many watchers waiting for the same fd or signal: one is typical, two +is rare). +.IP "Finding the next timer in each loop iteration: O(1)" 4 +.IX Item "Finding the next timer in each loop iteration: O(1)" +By virtue of using a binary or 4\-heap, the next timer is always found at a +fixed position in the storage array. +.IP "Each change on a file descriptor per loop iteration: O(number_of_watchers_for_this_fd)" 4 +.IX Item "Each change on a file descriptor per loop iteration: O(number_of_watchers_for_this_fd)" +A change means an I/O watcher gets started or stopped, which requires +libev to recalculate its status (and possibly tell the kernel, depending +on backend and whether \f(CW\*(C`ev_io_set\*(C'\fR was used). +.IP "Activating one watcher (putting it into the pending state): O(1)" 4 +.IX Item "Activating one watcher (putting it into the pending state): O(1)" +.PD 0 +.IP "Priority handling: O(number_of_priorities)" 4 +.IX Item "Priority handling: O(number_of_priorities)" +.PD +Priorities are implemented by allocating some space for each +priority. When doing priority-based operations, libev usually has to +linearly search all the priorities, but starting/stopping and activating +watchers becomes O(1) with respect to priority handling. +.IP "Sending an ev_async: O(1)" 4 +.IX Item "Sending an ev_async: O(1)" +.PD 0 +.IP "Processing ev_async_send: O(number_of_async_watchers)" 4 +.IX Item "Processing ev_async_send: O(number_of_async_watchers)" +.IP "Processing signals: O(max_signal_number)" 4 +.IX Item "Processing signals: O(max_signal_number)" +.PD +Sending involves a system call \fIiff\fR there were no other \f(CW\*(C`ev_async_send\*(C'\fR +calls in the current loop iteration and the loop is currently +blocked. Checking for async and signal events involves iterating over all +running async watchers or all signal numbers. +.SH "PORTING FROM LIBEV 3.X TO 4.X" +.IX Header "PORTING FROM LIBEV 3.X TO 4.X" +The major version 4 introduced some incompatible changes to the \s-1API.\s0 +.PP +At the moment, the \f(CW\*(C`ev.h\*(C'\fR header file provides compatibility definitions +for all changes, so most programs should still compile. The compatibility +layer might be removed in later versions of libev, so better update to the +new \s-1API\s0 early than late. +.ie n .IP """EV_COMPAT3"" backwards compatibility mechanism" 4 +.el .IP "\f(CWEV_COMPAT3\fR backwards compatibility mechanism" 4 +.IX Item "EV_COMPAT3 backwards compatibility mechanism" +The backward compatibility mechanism can be controlled by +\&\f(CW\*(C`EV_COMPAT3\*(C'\fR. See \*(L"\s-1PREPROCESSOR SYMBOLS/MACROS\*(R"\s0 in the \*(L"\s-1EMBEDDING\*(R"\s0 +section. +.ie n .IP """ev_default_destroy"" and ""ev_default_fork"" have been removed" 4 +.el .IP "\f(CWev_default_destroy\fR and \f(CWev_default_fork\fR have been removed" 4 +.IX Item "ev_default_destroy and ev_default_fork have been removed" +These calls can be replaced easily by their \f(CW\*(C`ev_loop_xxx\*(C'\fR counterparts: +.Sp +.Vb 2 +\& ev_loop_destroy (EV_DEFAULT_UC); +\& ev_loop_fork (EV_DEFAULT); +.Ve +.IP "function/symbol renames" 4 +.IX Item "function/symbol renames" +A number of functions and symbols have been renamed: +.Sp +.Vb 3 +\& ev_loop => ev_run +\& EVLOOP_NONBLOCK => EVRUN_NOWAIT +\& EVLOOP_ONESHOT => EVRUN_ONCE +\& +\& ev_unloop => ev_break +\& EVUNLOOP_CANCEL => EVBREAK_CANCEL +\& EVUNLOOP_ONE => EVBREAK_ONE +\& EVUNLOOP_ALL => EVBREAK_ALL +\& +\& EV_TIMEOUT => EV_TIMER +\& +\& ev_loop_count => ev_iteration +\& ev_loop_depth => ev_depth +\& ev_loop_verify => ev_verify +.Ve +.Sp +Most functions working on \f(CW\*(C`struct ev_loop\*(C'\fR objects don't have an +\&\f(CW\*(C`ev_loop_\*(C'\fR prefix, so it was removed; \f(CW\*(C`ev_loop\*(C'\fR, \f(CW\*(C`ev_unloop\*(C'\fR and +associated constants have been renamed to not collide with the \f(CW\*(C`struct +ev_loop\*(C'\fR anymore and \f(CW\*(C`EV_TIMER\*(C'\fR now follows the same naming scheme +as all other watcher types. Note that \f(CW\*(C`ev_loop_fork\*(C'\fR is still called +\&\f(CW\*(C`ev_loop_fork\*(C'\fR because it would otherwise clash with the \f(CW\*(C`ev_fork\*(C'\fR +typedef. +.ie n .IP """EV_MINIMAL"" mechanism replaced by ""EV_FEATURES""" 4 +.el .IP "\f(CWEV_MINIMAL\fR mechanism replaced by \f(CWEV_FEATURES\fR" 4 +.IX Item "EV_MINIMAL mechanism replaced by EV_FEATURES" +The preprocessor symbol \f(CW\*(C`EV_MINIMAL\*(C'\fR has been replaced by a different +mechanism, \f(CW\*(C`EV_FEATURES\*(C'\fR. Programs using \f(CW\*(C`EV_MINIMAL\*(C'\fR usually compile +and work, but the library code will of course be larger. +.SH "GLOSSARY" +.IX Header "GLOSSARY" +.IP "active" 4 +.IX Item "active" +A watcher is active as long as it has been started and not yet stopped. +See \*(L"\s-1WATCHER STATES\*(R"\s0 for details. +.IP "application" 4 +.IX Item "application" +In this document, an application is whatever is using libev. +.IP "backend" 4 +.IX Item "backend" +The part of the code dealing with the operating system interfaces. +.IP "callback" 4 +.IX Item "callback" +The address of a function that is called when some event has been +detected. Callbacks are being passed the event loop, the watcher that +received the event, and the actual event bitset. +.IP "callback/watcher invocation" 4 +.IX Item "callback/watcher invocation" +The act of calling the callback associated with a watcher. +.IP "event" 4 +.IX Item "event" +A change of state of some external event, such as data now being available +for reading on a file descriptor, time having passed or simply not having +any other events happening anymore. +.Sp +In libev, events are represented as single bits (such as \f(CW\*(C`EV_READ\*(C'\fR or +\&\f(CW\*(C`EV_TIMER\*(C'\fR). +.IP "event library" 4 +.IX Item "event library" +A software package implementing an event model and loop. +.IP "event loop" 4 +.IX Item "event loop" +An entity that handles and processes external events and converts them +into callback invocations. +.IP "event model" 4 +.IX Item "event model" +The model used to describe how an event loop handles and processes +watchers and events. +.IP "pending" 4 +.IX Item "pending" +A watcher is pending as soon as the corresponding event has been +detected. See \*(L"\s-1WATCHER STATES\*(R"\s0 for details. +.IP "real time" 4 +.IX Item "real time" +The physical time that is observed. It is apparently strictly monotonic :) +.IP "wall-clock time" 4 +.IX Item "wall-clock time" +The time and date as shown on clocks. Unlike real time, it can actually +be wrong and jump forwards and backwards, e.g. when you adjust your +clock. +.IP "watcher" 4 +.IX Item "watcher" +A data structure that describes interest in certain events. Watchers need +to be started (attached to an event loop) before they can receive events. +.SH "AUTHOR" +.IX Header "AUTHOR" +Marc Lehmann , with repeated corrections by Mikael +Magnusson and Emanuele Giaquinta, and minor corrections by many others. diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev.c b/deps/ngtcp2/ngtcp2/third-party/libev/ev.c new file mode 100644 index 00000000000000..ec212a1f0d21bc --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev.c @@ -0,0 +1,5627 @@ +/* + * libev event processing core, watcher management + * + * Copyright (c) 2007-2019 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +/* this big block deduces configuration from config.h */ +#ifndef EV_STANDALONE +# ifdef EV_CONFIG_H +# include EV_CONFIG_H +# else +# include "config.h" +# endif + +# if HAVE_FLOOR +# ifndef EV_USE_FLOOR +# define EV_USE_FLOOR 1 +# endif +# endif + +# if HAVE_CLOCK_SYSCALL +# ifndef EV_USE_CLOCK_SYSCALL +# define EV_USE_CLOCK_SYSCALL 1 +# ifndef EV_USE_REALTIME +# define EV_USE_REALTIME 0 +# endif +# ifndef EV_USE_MONOTONIC +# define EV_USE_MONOTONIC 1 +# endif +# endif +# elif !defined EV_USE_CLOCK_SYSCALL +# define EV_USE_CLOCK_SYSCALL 0 +# endif + +# if HAVE_CLOCK_GETTIME +# ifndef EV_USE_MONOTONIC +# define EV_USE_MONOTONIC 1 +# endif +# ifndef EV_USE_REALTIME +# define EV_USE_REALTIME 0 +# endif +# else +# ifndef EV_USE_MONOTONIC +# define EV_USE_MONOTONIC 0 +# endif +# ifndef EV_USE_REALTIME +# define EV_USE_REALTIME 0 +# endif +# endif + +# if HAVE_NANOSLEEP +# ifndef EV_USE_NANOSLEEP +# define EV_USE_NANOSLEEP EV_FEATURE_OS +# endif +# else +# undef EV_USE_NANOSLEEP +# define EV_USE_NANOSLEEP 0 +# endif + +# if HAVE_SELECT && HAVE_SYS_SELECT_H +# ifndef EV_USE_SELECT +# define EV_USE_SELECT EV_FEATURE_BACKENDS +# endif +# else +# undef EV_USE_SELECT +# define EV_USE_SELECT 0 +# endif + +# if HAVE_POLL && HAVE_POLL_H +# ifndef EV_USE_POLL +# define EV_USE_POLL EV_FEATURE_BACKENDS +# endif +# else +# undef EV_USE_POLL +# define EV_USE_POLL 0 +# endif + +# if HAVE_EPOLL_CTL && HAVE_SYS_EPOLL_H +# ifndef EV_USE_EPOLL +# define EV_USE_EPOLL EV_FEATURE_BACKENDS +# endif +# else +# undef EV_USE_EPOLL +# define EV_USE_EPOLL 0 +# endif + +# if HAVE_LINUX_AIO_ABI_H +# ifndef EV_USE_LINUXAIO +# define EV_USE_LINUXAIO 0 /* was: EV_FEATURE_BACKENDS, always off by default */ +# endif +# else +# undef EV_USE_LINUXAIO +# define EV_USE_LINUXAIO 0 +# endif + +# if HAVE_LINUX_FS_H && HAVE_SYS_TIMERFD_H && HAVE_KERNEL_RWF_T +# ifndef EV_USE_IOURING +# define EV_USE_IOURING EV_FEATURE_BACKENDS +# endif +# else +# undef EV_USE_IOURING +# define EV_USE_IOURING 0 +# endif + +# if HAVE_KQUEUE && HAVE_SYS_EVENT_H +# ifndef EV_USE_KQUEUE +# define EV_USE_KQUEUE EV_FEATURE_BACKENDS +# endif +# else +# undef EV_USE_KQUEUE +# define EV_USE_KQUEUE 0 +# endif + +# if HAVE_PORT_H && HAVE_PORT_CREATE +# ifndef EV_USE_PORT +# define EV_USE_PORT EV_FEATURE_BACKENDS +# endif +# else +# undef EV_USE_PORT +# define EV_USE_PORT 0 +# endif + +# if HAVE_INOTIFY_INIT && HAVE_SYS_INOTIFY_H +# ifndef EV_USE_INOTIFY +# define EV_USE_INOTIFY EV_FEATURE_OS +# endif +# else +# undef EV_USE_INOTIFY +# define EV_USE_INOTIFY 0 +# endif + +# if HAVE_SIGNALFD && HAVE_SYS_SIGNALFD_H +# ifndef EV_USE_SIGNALFD +# define EV_USE_SIGNALFD EV_FEATURE_OS +# endif +# else +# undef EV_USE_SIGNALFD +# define EV_USE_SIGNALFD 0 +# endif + +# if HAVE_EVENTFD +# ifndef EV_USE_EVENTFD +# define EV_USE_EVENTFD EV_FEATURE_OS +# endif +# else +# undef EV_USE_EVENTFD +# define EV_USE_EVENTFD 0 +# endif + +# if HAVE_SYS_TIMERFD_H +# ifndef EV_USE_TIMERFD +# define EV_USE_TIMERFD EV_FEATURE_OS +# endif +# else +# undef EV_USE_TIMERFD +# define EV_USE_TIMERFD 0 +# endif + +#endif + +/* OS X, in its infinite idiocy, actually HARDCODES + * a limit of 1024 into their select. Where people have brains, + * OS X engineers apparently have a vacuum. Or maybe they were + * ordered to have a vacuum, or they do anything for money. + * This might help. Or not. + * Note that this must be defined early, as other include files + * will rely on this define as well. + */ +#define _DARWIN_UNLIMITED_SELECT 1 + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +#ifdef EV_H +# include EV_H +#else +# include "ev.h" +#endif + +#if EV_NO_THREADS +# undef EV_NO_SMP +# define EV_NO_SMP 1 +# undef ECB_NO_THREADS +# define ECB_NO_THREADS 1 +#endif +#if EV_NO_SMP +# undef EV_NO_SMP +# define ECB_NO_SMP 1 +#endif + +#ifndef _WIN32 +# include +# include +# include +#else +# include +# define WIN32_LEAN_AND_MEAN +# include +# include +# ifndef EV_SELECT_IS_WINSOCKET +# define EV_SELECT_IS_WINSOCKET 1 +# endif +# undef EV_AVOID_STDIO +#endif + +/* this block tries to deduce configuration from header-defined symbols and defaults */ + +/* try to deduce the maximum number of signals on this platform */ +#if defined EV_NSIG +/* use what's provided */ +#elif defined NSIG +# define EV_NSIG (NSIG) +#elif defined _NSIG +# define EV_NSIG (_NSIG) +#elif defined SIGMAX +# define EV_NSIG (SIGMAX+1) +#elif defined SIG_MAX +# define EV_NSIG (SIG_MAX+1) +#elif defined _SIG_MAX +# define EV_NSIG (_SIG_MAX+1) +#elif defined MAXSIG +# define EV_NSIG (MAXSIG+1) +#elif defined MAX_SIG +# define EV_NSIG (MAX_SIG+1) +#elif defined SIGARRAYSIZE +# define EV_NSIG (SIGARRAYSIZE) /* Assume ary[SIGARRAYSIZE] */ +#elif defined _sys_nsig +# define EV_NSIG (_sys_nsig) /* Solaris 2.5 */ +#else +# define EV_NSIG (8 * sizeof (sigset_t) + 1) +#endif + +#ifndef EV_USE_FLOOR +# define EV_USE_FLOOR 0 +#endif + +#ifndef EV_USE_CLOCK_SYSCALL +# if __linux && __GLIBC__ == 2 && __GLIBC_MINOR__ < 17 +# define EV_USE_CLOCK_SYSCALL EV_FEATURE_OS +# else +# define EV_USE_CLOCK_SYSCALL 0 +# endif +#endif + +#if !(_POSIX_TIMERS > 0) +# ifndef EV_USE_MONOTONIC +# define EV_USE_MONOTONIC 0 +# endif +# ifndef EV_USE_REALTIME +# define EV_USE_REALTIME 0 +# endif +#endif + +#ifndef EV_USE_MONOTONIC +# if defined _POSIX_MONOTONIC_CLOCK && _POSIX_MONOTONIC_CLOCK >= 0 +# define EV_USE_MONOTONIC EV_FEATURE_OS +# else +# define EV_USE_MONOTONIC 0 +# endif +#endif + +#ifndef EV_USE_REALTIME +# define EV_USE_REALTIME !EV_USE_CLOCK_SYSCALL +#endif + +#ifndef EV_USE_NANOSLEEP +# if _POSIX_C_SOURCE >= 199309L +# define EV_USE_NANOSLEEP EV_FEATURE_OS +# else +# define EV_USE_NANOSLEEP 0 +# endif +#endif + +#ifndef EV_USE_SELECT +# define EV_USE_SELECT EV_FEATURE_BACKENDS +#endif + +#ifndef EV_USE_POLL +# ifdef _WIN32 +# define EV_USE_POLL 0 +# else +# define EV_USE_POLL EV_FEATURE_BACKENDS +# endif +#endif + +#ifndef EV_USE_EPOLL +# if __linux && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 4)) +# define EV_USE_EPOLL EV_FEATURE_BACKENDS +# else +# define EV_USE_EPOLL 0 +# endif +#endif + +#ifndef EV_USE_KQUEUE +# define EV_USE_KQUEUE 0 +#endif + +#ifndef EV_USE_PORT +# define EV_USE_PORT 0 +#endif + +#ifndef EV_USE_LINUXAIO +# if __linux /* libev currently assumes linux/aio_abi.h is always available on linux */ +# define EV_USE_LINUXAIO 0 /* was: 1, always off by default */ +# else +# define EV_USE_LINUXAIO 0 +# endif +#endif + +#ifndef EV_USE_IOURING +# if __linux /* later checks might disable again */ +# define EV_USE_IOURING 1 +# else +# define EV_USE_IOURING 0 +# endif +#endif + +#ifndef EV_USE_INOTIFY +# if __linux && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 4)) +# define EV_USE_INOTIFY EV_FEATURE_OS +# else +# define EV_USE_INOTIFY 0 +# endif +#endif + +#ifndef EV_PID_HASHSIZE +# define EV_PID_HASHSIZE EV_FEATURE_DATA ? 16 : 1 +#endif + +#ifndef EV_INOTIFY_HASHSIZE +# define EV_INOTIFY_HASHSIZE EV_FEATURE_DATA ? 16 : 1 +#endif + +#ifndef EV_USE_EVENTFD +# if __linux && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 7)) +# define EV_USE_EVENTFD EV_FEATURE_OS +# else +# define EV_USE_EVENTFD 0 +# endif +#endif + +#ifndef EV_USE_SIGNALFD +# if __linux && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 7)) +# define EV_USE_SIGNALFD EV_FEATURE_OS +# else +# define EV_USE_SIGNALFD 0 +# endif +#endif + +#ifndef EV_USE_TIMERFD +# if __linux && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 8)) +# define EV_USE_TIMERFD EV_FEATURE_OS +# else +# define EV_USE_TIMERFD 0 +# endif +#endif + +#if 0 /* debugging */ +# define EV_VERIFY 3 +# define EV_USE_4HEAP 1 +# define EV_HEAP_CACHE_AT 1 +#endif + +#ifndef EV_VERIFY +# define EV_VERIFY (EV_FEATURE_API ? 1 : 0) +#endif + +#ifndef EV_USE_4HEAP +# define EV_USE_4HEAP EV_FEATURE_DATA +#endif + +#ifndef EV_HEAP_CACHE_AT +# define EV_HEAP_CACHE_AT EV_FEATURE_DATA +#endif + +#ifdef __ANDROID__ +/* supposedly, android doesn't typedef fd_mask */ +# undef EV_USE_SELECT +# define EV_USE_SELECT 0 +/* supposedly, we need to include syscall.h, not sys/syscall.h, so just disable */ +# undef EV_USE_CLOCK_SYSCALL +# define EV_USE_CLOCK_SYSCALL 0 +#endif + +/* aix's poll.h seems to cause lots of trouble */ +#ifdef _AIX +/* AIX has a completely broken poll.h header */ +# undef EV_USE_POLL +# define EV_USE_POLL 0 +#endif + +/* on linux, we can use a (slow) syscall to avoid a dependency on pthread, */ +/* which makes programs even slower. might work on other unices, too. */ +#if EV_USE_CLOCK_SYSCALL +# include +# ifdef SYS_clock_gettime +# define clock_gettime(id, ts) syscall (SYS_clock_gettime, (id), (ts)) +# undef EV_USE_MONOTONIC +# define EV_USE_MONOTONIC 1 +# define EV_NEED_SYSCALL 1 +# else +# undef EV_USE_CLOCK_SYSCALL +# define EV_USE_CLOCK_SYSCALL 0 +# endif +#endif + +/* this block fixes any misconfiguration where we know we run into trouble otherwise */ + +#ifndef CLOCK_MONOTONIC +# undef EV_USE_MONOTONIC +# define EV_USE_MONOTONIC 0 +#endif + +#ifndef CLOCK_REALTIME +# undef EV_USE_REALTIME +# define EV_USE_REALTIME 0 +#endif + +#if !EV_STAT_ENABLE +# undef EV_USE_INOTIFY +# define EV_USE_INOTIFY 0 +#endif + +#if __linux && EV_USE_IOURING +# include +# if LINUX_VERSION_CODE < KERNEL_VERSION(4,14,0) +# undef EV_USE_IOURING +# define EV_USE_IOURING 0 +# endif +#endif + +#if !EV_USE_NANOSLEEP +/* hp-ux has it in sys/time.h, which we unconditionally include above */ +# if !defined _WIN32 && !defined __hpux +# include +# endif +#endif + +#if EV_USE_LINUXAIO +# include +# if SYS_io_getevents && EV_USE_EPOLL /* linuxaio backend requires epoll backend */ +# define EV_NEED_SYSCALL 1 +# else +# undef EV_USE_LINUXAIO +# define EV_USE_LINUXAIO 0 +# endif +#endif + +#if EV_USE_IOURING +# include +# if !SYS_io_uring_setup && __linux && !__alpha +# define SYS_io_uring_setup 425 +# define SYS_io_uring_enter 426 +# define SYS_io_uring_wregister 427 +# endif +# if SYS_io_uring_setup && EV_USE_EPOLL /* iouring backend requires epoll backend */ +# define EV_NEED_SYSCALL 1 +# else +# undef EV_USE_IOURING +# define EV_USE_IOURING 0 +# endif +#endif + +#if EV_USE_INOTIFY +# include +# include +/* some very old inotify.h headers don't have IN_DONT_FOLLOW */ +# ifndef IN_DONT_FOLLOW +# undef EV_USE_INOTIFY +# define EV_USE_INOTIFY 0 +# endif +#endif + +#if EV_USE_EVENTFD +/* our minimum requirement is glibc 2.7 which has the stub, but not the full header */ +# include +# ifndef EFD_NONBLOCK +# define EFD_NONBLOCK O_NONBLOCK +# endif +# ifndef EFD_CLOEXEC +# ifdef O_CLOEXEC +# define EFD_CLOEXEC O_CLOEXEC +# else +# define EFD_CLOEXEC 02000000 +# endif +# endif +EV_CPP(extern "C") int (eventfd) (unsigned int initval, int flags); +#endif + +#if EV_USE_SIGNALFD +/* our minimum requirement is glibc 2.7 which has the stub, but not the full header */ +# include +# ifndef SFD_NONBLOCK +# define SFD_NONBLOCK O_NONBLOCK +# endif +# ifndef SFD_CLOEXEC +# ifdef O_CLOEXEC +# define SFD_CLOEXEC O_CLOEXEC +# else +# define SFD_CLOEXEC 02000000 +# endif +# endif +EV_CPP (extern "C") int (signalfd) (int fd, const sigset_t *mask, int flags); + +struct signalfd_siginfo +{ + uint32_t ssi_signo; + char pad[128 - sizeof (uint32_t)]; +}; +#endif + +/* for timerfd, libev core requires TFD_TIMER_CANCEL_ON_SET &c */ +#if EV_USE_TIMERFD +# include +/* timerfd is only used for periodics */ +# if !(defined (TFD_TIMER_CANCEL_ON_SET) && defined (TFD_CLOEXEC) && defined (TFD_NONBLOCK)) || !EV_PERIODIC_ENABLE +# undef EV_USE_TIMERFD +# define EV_USE_TIMERFD 0 +# endif +#endif + +/*****************************************************************************/ + +#if EV_VERIFY >= 3 +# define EV_FREQUENT_CHECK ev_verify (EV_A) +#else +# define EV_FREQUENT_CHECK do { } while (0) +#endif + +/* + * This is used to work around floating point rounding problems. + * This value is good at least till the year 4000. + */ +#define MIN_INTERVAL 0.0001220703125 /* 1/2**13, good till 4000 */ +/*#define MIN_INTERVAL 0.00000095367431640625 /* 1/2**20, good till 2200 */ + +#define MIN_TIMEJUMP 1. /* minimum timejump that gets detected (if monotonic clock available) */ +#define MAX_BLOCKTIME 59.743 /* never wait longer than this time (to detect time jumps) */ +#define MAX_BLOCKTIME2 1500001.07 /* same, but when timerfd is used to detect jumps, also safe delay to not overflow */ + +/* find a portable timestamp that is "always" in the future but fits into time_t. + * this is quite hard, and we are mostly guessing - we handle 32 bit signed/unsigned time_t, + * and sizes larger than 32 bit, and maybe the unlikely floating point time_t */ +#define EV_TSTAMP_HUGE \ + (sizeof (time_t) >= 8 ? 10000000000000. \ + : 0 < (time_t)4294967295 ? 4294967295. \ + : 2147483647.) \ + +#ifndef EV_TS_CONST +# define EV_TS_CONST(nv) nv +# define EV_TS_TO_MSEC(a) a * 1e3 + 0.9999 +# define EV_TS_FROM_USEC(us) us * 1e-6 +# define EV_TV_SET(tv,t) do { tv.tv_sec = (long)t; tv.tv_usec = (long)((t - tv.tv_sec) * 1e6); } while (0) +# define EV_TS_SET(ts,t) do { ts.tv_sec = (long)t; ts.tv_nsec = (long)((t - ts.tv_sec) * 1e9); } while (0) +# define EV_TV_GET(tv) ((tv).tv_sec + (tv).tv_usec * 1e-6) +# define EV_TS_GET(ts) ((ts).tv_sec + (ts).tv_nsec * 1e-9) +#endif + +/* the following is ecb.h embedded into libev - use update_ev_c to update from an external copy */ +/* ECB.H BEGIN */ +/* + * libecb - http://software.schmorp.de/pkg/libecb + * + * Copyright (©) 2009-2015,2018-2020 Marc Alexander Lehmann + * Copyright (©) 2011 Emanuele Giaquinta + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +#ifndef ECB_H +#define ECB_H + +/* 16 bits major, 16 bits minor */ +#define ECB_VERSION 0x00010008 + +#include /* for memcpy */ + +#if defined (_WIN32) && !defined (__MINGW32__) + typedef signed char int8_t; + typedef unsigned char uint8_t; + typedef signed char int_fast8_t; + typedef unsigned char uint_fast8_t; + typedef signed short int16_t; + typedef unsigned short uint16_t; + typedef signed int int_fast16_t; + typedef unsigned int uint_fast16_t; + typedef signed int int32_t; + typedef unsigned int uint32_t; + typedef signed int int_fast32_t; + typedef unsigned int uint_fast32_t; + #if __GNUC__ + typedef signed long long int64_t; + typedef unsigned long long uint64_t; + #else /* _MSC_VER || __BORLANDC__ */ + typedef signed __int64 int64_t; + typedef unsigned __int64 uint64_t; + #endif + typedef int64_t int_fast64_t; + typedef uint64_t uint_fast64_t; + #ifdef _WIN64 + #define ECB_PTRSIZE 8 + typedef uint64_t uintptr_t; + typedef int64_t intptr_t; + #else + #define ECB_PTRSIZE 4 + typedef uint32_t uintptr_t; + typedef int32_t intptr_t; + #endif +#else + #include + #if (defined INTPTR_MAX ? INTPTR_MAX : ULONG_MAX) > 0xffffffffU + #define ECB_PTRSIZE 8 + #else + #define ECB_PTRSIZE 4 + #endif +#endif + +#define ECB_GCC_AMD64 (__amd64 || __amd64__ || __x86_64 || __x86_64__) +#define ECB_MSVC_AMD64 (_M_AMD64 || _M_X64) + +#ifndef ECB_OPTIMIZE_SIZE + #if __OPTIMIZE_SIZE__ + #define ECB_OPTIMIZE_SIZE 1 + #else + #define ECB_OPTIMIZE_SIZE 0 + #endif +#endif + +/* work around x32 idiocy by defining proper macros */ +#if ECB_GCC_AMD64 || ECB_MSVC_AMD64 + #if _ILP32 + #define ECB_AMD64_X32 1 + #else + #define ECB_AMD64 1 + #endif +#endif + +/* many compilers define _GNUC_ to some versions but then only implement + * what their idiot authors think are the "more important" extensions, + * causing enormous grief in return for some better fake benchmark numbers. + * or so. + * we try to detect these and simply assume they are not gcc - if they have + * an issue with that they should have done it right in the first place. + */ +#if !defined __GNUC_MINOR__ || defined __INTEL_COMPILER || defined __SUNPRO_C || defined __SUNPRO_CC || defined __llvm__ || defined __clang__ + #define ECB_GCC_VERSION(major,minor) 0 +#else + #define ECB_GCC_VERSION(major,minor) (__GNUC__ > (major) || (__GNUC__ == (major) && __GNUC_MINOR__ >= (minor))) +#endif + +#define ECB_CLANG_VERSION(major,minor) (__clang_major__ > (major) || (__clang_major__ == (major) && __clang_minor__ >= (minor))) + +#if __clang__ && defined __has_builtin + #define ECB_CLANG_BUILTIN(x) __has_builtin (x) +#else + #define ECB_CLANG_BUILTIN(x) 0 +#endif + +#if __clang__ && defined __has_extension + #define ECB_CLANG_EXTENSION(x) __has_extension (x) +#else + #define ECB_CLANG_EXTENSION(x) 0 +#endif + +#define ECB_CPP (__cplusplus+0) +#define ECB_CPP11 (__cplusplus >= 201103L) +#define ECB_CPP14 (__cplusplus >= 201402L) +#define ECB_CPP17 (__cplusplus >= 201703L) + +#if ECB_CPP + #define ECB_C 0 + #define ECB_STDC_VERSION 0 +#else + #define ECB_C 1 + #define ECB_STDC_VERSION __STDC_VERSION__ +#endif + +#define ECB_C99 (ECB_STDC_VERSION >= 199901L) +#define ECB_C11 (ECB_STDC_VERSION >= 201112L) +#define ECB_C17 (ECB_STDC_VERSION >= 201710L) + +#if ECB_CPP + #define ECB_EXTERN_C extern "C" + #define ECB_EXTERN_C_BEG ECB_EXTERN_C { + #define ECB_EXTERN_C_END } +#else + #define ECB_EXTERN_C extern + #define ECB_EXTERN_C_BEG + #define ECB_EXTERN_C_END +#endif + +/*****************************************************************************/ + +/* ECB_NO_THREADS - ecb is not used by multiple threads, ever */ +/* ECB_NO_SMP - ecb might be used in multiple threads, but only on a single cpu */ + +#if ECB_NO_THREADS + #define ECB_NO_SMP 1 +#endif + +#if ECB_NO_SMP + #define ECB_MEMORY_FENCE do { } while (0) +#endif + +/* http://www-01.ibm.com/support/knowledgecenter/SSGH3R_13.1.0/com.ibm.xlcpp131.aix.doc/compiler_ref/compiler_builtins.html */ +#if __xlC__ && ECB_CPP + #include +#endif + +#if 1400 <= _MSC_VER + #include /* fence functions _ReadBarrier, also bit search functions _BitScanReverse */ +#endif + +#ifndef ECB_MEMORY_FENCE + #if ECB_GCC_VERSION(2,5) || defined __INTEL_COMPILER || (__llvm__ && __GNUC__) || __SUNPRO_C >= 0x5110 || __SUNPRO_CC >= 0x5110 + #define ECB_MEMORY_FENCE_RELAXED __asm__ __volatile__ ("" : : : "memory") + #if __i386 || __i386__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("lock; orb $0, -1(%%esp)" : : : "memory") + #define ECB_MEMORY_FENCE_ACQUIRE __asm__ __volatile__ ("" : : : "memory") + #define ECB_MEMORY_FENCE_RELEASE __asm__ __volatile__ ("" : : : "memory") + #elif ECB_GCC_AMD64 + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("mfence" : : : "memory") + #define ECB_MEMORY_FENCE_ACQUIRE __asm__ __volatile__ ("" : : : "memory") + #define ECB_MEMORY_FENCE_RELEASE __asm__ __volatile__ ("" : : : "memory") + #elif __powerpc__ || __ppc__ || __powerpc64__ || __ppc64__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("sync" : : : "memory") + #elif defined __ARM_ARCH_2__ \ + || defined __ARM_ARCH_3__ || defined __ARM_ARCH_3M__ \ + || defined __ARM_ARCH_4__ || defined __ARM_ARCH_4T__ \ + || defined __ARM_ARCH_5__ || defined __ARM_ARCH_5E__ \ + || defined __ARM_ARCH_5T__ || defined __ARM_ARCH_5TE__ \ + || defined __ARM_ARCH_5TEJ__ + /* should not need any, unless running old code on newer cpu - arm doesn't support that */ + #elif defined __ARM_ARCH_6__ || defined __ARM_ARCH_6J__ \ + || defined __ARM_ARCH_6K__ || defined __ARM_ARCH_6ZK__ \ + || defined __ARM_ARCH_6T2__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("mcr p15,0,%0,c7,c10,5" : : "r" (0) : "memory") + #elif defined __ARM_ARCH_7__ || defined __ARM_ARCH_7A__ \ + || defined __ARM_ARCH_7R__ || defined __ARM_ARCH_7M__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("dmb" : : : "memory") + #elif __aarch64__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("dmb ish" : : : "memory") + #elif (__sparc || __sparc__) && !(__sparc_v8__ || defined __sparcv8) + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("membar #LoadStore | #LoadLoad | #StoreStore | #StoreLoad" : : : "memory") + #define ECB_MEMORY_FENCE_ACQUIRE __asm__ __volatile__ ("membar #LoadStore | #LoadLoad" : : : "memory") + #define ECB_MEMORY_FENCE_RELEASE __asm__ __volatile__ ("membar #LoadStore | #StoreStore") + #elif defined __s390__ || defined __s390x__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("bcr 15,0" : : : "memory") + #elif defined __mips__ + /* GNU/Linux emulates sync on mips1 architectures, so we force its use */ + /* anybody else who still uses mips1 is supposed to send in their version, with detection code. */ + #define ECB_MEMORY_FENCE __asm__ __volatile__ (".set mips2; sync; .set mips0" : : : "memory") + #elif defined __alpha__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("mb" : : : "memory") + #elif defined __hppa__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("" : : : "memory") + #define ECB_MEMORY_FENCE_RELEASE __asm__ __volatile__ ("") + #elif defined __ia64__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("mf" : : : "memory") + #elif defined __m68k__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("" : : : "memory") + #elif defined __m88k__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("tb1 0,%%r0,128" : : : "memory") + #elif defined __sh__ + #define ECB_MEMORY_FENCE __asm__ __volatile__ ("" : : : "memory") + #endif + #endif +#endif + +#ifndef ECB_MEMORY_FENCE + #if ECB_GCC_VERSION(4,7) + /* see comment below (stdatomic.h) about the C11 memory model. */ + #define ECB_MEMORY_FENCE __atomic_thread_fence (__ATOMIC_SEQ_CST) + #define ECB_MEMORY_FENCE_ACQUIRE __atomic_thread_fence (__ATOMIC_ACQUIRE) + #define ECB_MEMORY_FENCE_RELEASE __atomic_thread_fence (__ATOMIC_RELEASE) + #define ECB_MEMORY_FENCE_RELAXED __atomic_thread_fence (__ATOMIC_RELAXED) + + #elif ECB_CLANG_EXTENSION(c_atomic) + /* see comment below (stdatomic.h) about the C11 memory model. */ + #define ECB_MEMORY_FENCE __c11_atomic_thread_fence (__ATOMIC_SEQ_CST) + #define ECB_MEMORY_FENCE_ACQUIRE __c11_atomic_thread_fence (__ATOMIC_ACQUIRE) + #define ECB_MEMORY_FENCE_RELEASE __c11_atomic_thread_fence (__ATOMIC_RELEASE) + #define ECB_MEMORY_FENCE_RELAXED __c11_atomic_thread_fence (__ATOMIC_RELAXED) + + #elif ECB_GCC_VERSION(4,4) || defined __INTEL_COMPILER || defined __clang__ + #define ECB_MEMORY_FENCE __sync_synchronize () + #elif _MSC_VER >= 1500 /* VC++ 2008 */ + /* apparently, microsoft broke all the memory barrier stuff in Visual Studio 2008... */ + #pragma intrinsic(_ReadBarrier,_WriteBarrier,_ReadWriteBarrier) + #define ECB_MEMORY_FENCE _ReadWriteBarrier (); MemoryBarrier() + #define ECB_MEMORY_FENCE_ACQUIRE _ReadWriteBarrier (); MemoryBarrier() /* according to msdn, _ReadBarrier is not a load fence */ + #define ECB_MEMORY_FENCE_RELEASE _WriteBarrier (); MemoryBarrier() + #elif _MSC_VER >= 1400 /* VC++ 2005 */ + #pragma intrinsic(_ReadBarrier,_WriteBarrier,_ReadWriteBarrier) + #define ECB_MEMORY_FENCE _ReadWriteBarrier () + #define ECB_MEMORY_FENCE_ACQUIRE _ReadWriteBarrier () /* according to msdn, _ReadBarrier is not a load fence */ + #define ECB_MEMORY_FENCE_RELEASE _WriteBarrier () + #elif defined _WIN32 + #include + #define ECB_MEMORY_FENCE MemoryBarrier () /* actually just xchg on x86... scary */ + #elif __SUNPRO_C >= 0x5110 || __SUNPRO_CC >= 0x5110 + #include + #define ECB_MEMORY_FENCE __machine_rw_barrier () + #define ECB_MEMORY_FENCE_ACQUIRE __machine_acq_barrier () + #define ECB_MEMORY_FENCE_RELEASE __machine_rel_barrier () + #define ECB_MEMORY_FENCE_RELAXED __compiler_barrier () + #elif __xlC__ + #define ECB_MEMORY_FENCE __sync () + #endif +#endif + +#ifndef ECB_MEMORY_FENCE + #if ECB_C11 && !defined __STDC_NO_ATOMICS__ + /* we assume that these memory fences work on all variables/all memory accesses, */ + /* not just C11 atomics and atomic accesses */ + #include + #define ECB_MEMORY_FENCE atomic_thread_fence (memory_order_seq_cst) + #define ECB_MEMORY_FENCE_ACQUIRE atomic_thread_fence (memory_order_acquire) + #define ECB_MEMORY_FENCE_RELEASE atomic_thread_fence (memory_order_release) + #endif +#endif + +#ifndef ECB_MEMORY_FENCE + #if !ECB_AVOID_PTHREADS + /* + * if you get undefined symbol references to pthread_mutex_lock, + * or failure to find pthread.h, then you should implement + * the ECB_MEMORY_FENCE operations for your cpu/compiler + * OR provide pthread.h and link against the posix thread library + * of your system. + */ + #include + #define ECB_NEEDS_PTHREADS 1 + #define ECB_MEMORY_FENCE_NEEDS_PTHREADS 1 + + static pthread_mutex_t ecb_mf_lock = PTHREAD_MUTEX_INITIALIZER; + #define ECB_MEMORY_FENCE do { pthread_mutex_lock (&ecb_mf_lock); pthread_mutex_unlock (&ecb_mf_lock); } while (0) + #endif +#endif + +#if !defined ECB_MEMORY_FENCE_ACQUIRE && defined ECB_MEMORY_FENCE + #define ECB_MEMORY_FENCE_ACQUIRE ECB_MEMORY_FENCE +#endif + +#if !defined ECB_MEMORY_FENCE_RELEASE && defined ECB_MEMORY_FENCE + #define ECB_MEMORY_FENCE_RELEASE ECB_MEMORY_FENCE +#endif + +#if !defined ECB_MEMORY_FENCE_RELAXED && defined ECB_MEMORY_FENCE + #define ECB_MEMORY_FENCE_RELAXED ECB_MEMORY_FENCE /* very heavy-handed */ +#endif + +/*****************************************************************************/ + +#if ECB_CPP + #define ecb_inline static inline +#elif ECB_GCC_VERSION(2,5) + #define ecb_inline static __inline__ +#elif ECB_C99 + #define ecb_inline static inline +#else + #define ecb_inline static +#endif + +#if ECB_GCC_VERSION(3,3) + #define ecb_restrict __restrict__ +#elif ECB_C99 + #define ecb_restrict restrict +#else + #define ecb_restrict +#endif + +typedef int ecb_bool; + +#define ECB_CONCAT_(a, b) a ## b +#define ECB_CONCAT(a, b) ECB_CONCAT_(a, b) +#define ECB_STRINGIFY_(a) # a +#define ECB_STRINGIFY(a) ECB_STRINGIFY_(a) +#define ECB_STRINGIFY_EXPR(expr) ((expr), ECB_STRINGIFY_ (expr)) + +#define ecb_function_ ecb_inline + +#if ECB_GCC_VERSION(3,1) || ECB_CLANG_VERSION(2,8) + #define ecb_attribute(attrlist) __attribute__ (attrlist) +#else + #define ecb_attribute(attrlist) +#endif + +#if ECB_GCC_VERSION(3,1) || ECB_CLANG_BUILTIN(__builtin_constant_p) + #define ecb_is_constant(expr) __builtin_constant_p (expr) +#else + /* possible C11 impl for integral types + typedef struct ecb_is_constant_struct ecb_is_constant_struct; + #define ecb_is_constant(expr) _Generic ((1 ? (struct ecb_is_constant_struct *)0 : (void *)((expr) - (expr)), ecb_is_constant_struct *: 0, default: 1)) */ + + #define ecb_is_constant(expr) 0 +#endif + +#if ECB_GCC_VERSION(3,1) || ECB_CLANG_BUILTIN(__builtin_expect) + #define ecb_expect(expr,value) __builtin_expect ((expr),(value)) +#else + #define ecb_expect(expr,value) (expr) +#endif + +#if ECB_GCC_VERSION(3,1) || ECB_CLANG_BUILTIN(__builtin_prefetch) + #define ecb_prefetch(addr,rw,locality) __builtin_prefetch (addr, rw, locality) +#else + #define ecb_prefetch(addr,rw,locality) +#endif + +/* no emulation for ecb_decltype */ +#if ECB_CPP11 + // older implementations might have problems with decltype(x)::type, work around it + template struct ecb_decltype_t { typedef T type; }; + #define ecb_decltype(x) ecb_decltype_t::type +#elif ECB_GCC_VERSION(3,0) || ECB_CLANG_VERSION(2,8) + #define ecb_decltype(x) __typeof__ (x) +#endif + +#if _MSC_VER >= 1300 + #define ecb_deprecated __declspec (deprecated) +#else + #define ecb_deprecated ecb_attribute ((__deprecated__)) +#endif + +#if _MSC_VER >= 1500 + #define ecb_deprecated_message(msg) __declspec (deprecated (msg)) +#elif ECB_GCC_VERSION(4,5) + #define ecb_deprecated_message(msg) ecb_attribute ((__deprecated__ (msg)) +#else + #define ecb_deprecated_message(msg) ecb_deprecated +#endif + +#if _MSC_VER >= 1400 + #define ecb_noinline __declspec (noinline) +#else + #define ecb_noinline ecb_attribute ((__noinline__)) +#endif + +#define ecb_unused ecb_attribute ((__unused__)) +#define ecb_const ecb_attribute ((__const__)) +#define ecb_pure ecb_attribute ((__pure__)) + +#if ECB_C11 || __IBMC_NORETURN + /* http://www-01.ibm.com/support/knowledgecenter/SSGH3R_13.1.0/com.ibm.xlcpp131.aix.doc/language_ref/noreturn.html */ + #define ecb_noreturn _Noreturn +#elif ECB_CPP11 + #define ecb_noreturn [[noreturn]] +#elif _MSC_VER >= 1200 + /* http://msdn.microsoft.com/en-us/library/k6ktzx3s.aspx */ + #define ecb_noreturn __declspec (noreturn) +#else + #define ecb_noreturn ecb_attribute ((__noreturn__)) +#endif + +#if ECB_GCC_VERSION(4,3) + #define ecb_artificial ecb_attribute ((__artificial__)) + #define ecb_hot ecb_attribute ((__hot__)) + #define ecb_cold ecb_attribute ((__cold__)) +#else + #define ecb_artificial + #define ecb_hot + #define ecb_cold +#endif + +/* put around conditional expressions if you are very sure that the */ +/* expression is mostly true or mostly false. note that these return */ +/* booleans, not the expression. */ +#define ecb_expect_false(expr) ecb_expect (!!(expr), 0) +#define ecb_expect_true(expr) ecb_expect (!!(expr), 1) +/* for compatibility to the rest of the world */ +#define ecb_likely(expr) ecb_expect_true (expr) +#define ecb_unlikely(expr) ecb_expect_false (expr) + +/* count trailing zero bits and count # of one bits */ +#if ECB_GCC_VERSION(3,4) \ + || (ECB_CLANG_BUILTIN(__builtin_clz) && ECB_CLANG_BUILTIN(__builtin_clzll) \ + && ECB_CLANG_BUILTIN(__builtin_ctz) && ECB_CLANG_BUILTIN(__builtin_ctzll) \ + && ECB_CLANG_BUILTIN(__builtin_popcount)) + /* we assume int == 32 bit, long == 32 or 64 bit and long long == 64 bit */ + #define ecb_ld32(x) (__builtin_clz (x) ^ 31) + #define ecb_ld64(x) (__builtin_clzll (x) ^ 63) + #define ecb_ctz32(x) __builtin_ctz (x) + #define ecb_ctz64(x) __builtin_ctzll (x) + #define ecb_popcount32(x) __builtin_popcount (x) + /* no popcountll */ +#else + ecb_function_ ecb_const int ecb_ctz32 (uint32_t x); + ecb_function_ ecb_const int + ecb_ctz32 (uint32_t x) + { +#if 1400 <= _MSC_VER && (_M_IX86 || _M_X64 || _M_IA64 || _M_ARM) + unsigned long r; + _BitScanForward (&r, x); + return (int)r; +#else + int r = 0; + + x &= ~x + 1; /* this isolates the lowest bit */ + +#if ECB_branchless_on_i386 + r += !!(x & 0xaaaaaaaa) << 0; + r += !!(x & 0xcccccccc) << 1; + r += !!(x & 0xf0f0f0f0) << 2; + r += !!(x & 0xff00ff00) << 3; + r += !!(x & 0xffff0000) << 4; +#else + if (x & 0xaaaaaaaa) r += 1; + if (x & 0xcccccccc) r += 2; + if (x & 0xf0f0f0f0) r += 4; + if (x & 0xff00ff00) r += 8; + if (x & 0xffff0000) r += 16; +#endif + + return r; +#endif + } + + ecb_function_ ecb_const int ecb_ctz64 (uint64_t x); + ecb_function_ ecb_const int + ecb_ctz64 (uint64_t x) + { +#if 1400 <= _MSC_VER && (_M_X64 || _M_IA64 || _M_ARM) + unsigned long r; + _BitScanForward64 (&r, x); + return (int)r; +#else + int shift = x & 0xffffffff ? 0 : 32; + return ecb_ctz32 (x >> shift) + shift; +#endif + } + + ecb_function_ ecb_const int ecb_popcount32 (uint32_t x); + ecb_function_ ecb_const int + ecb_popcount32 (uint32_t x) + { + x -= (x >> 1) & 0x55555555; + x = ((x >> 2) & 0x33333333) + (x & 0x33333333); + x = ((x >> 4) + x) & 0x0f0f0f0f; + x *= 0x01010101; + + return x >> 24; + } + + ecb_function_ ecb_const int ecb_ld32 (uint32_t x); + ecb_function_ ecb_const int ecb_ld32 (uint32_t x) + { +#if 1400 <= _MSC_VER && (_M_IX86 || _M_X64 || _M_IA64 || _M_ARM) + unsigned long r; + _BitScanReverse (&r, x); + return (int)r; +#else + int r = 0; + + if (x >> 16) { x >>= 16; r += 16; } + if (x >> 8) { x >>= 8; r += 8; } + if (x >> 4) { x >>= 4; r += 4; } + if (x >> 2) { x >>= 2; r += 2; } + if (x >> 1) { r += 1; } + + return r; +#endif + } + + ecb_function_ ecb_const int ecb_ld64 (uint64_t x); + ecb_function_ ecb_const int ecb_ld64 (uint64_t x) + { +#if 1400 <= _MSC_VER && (_M_X64 || _M_IA64 || _M_ARM) + unsigned long r; + _BitScanReverse64 (&r, x); + return (int)r; +#else + int r = 0; + + if (x >> 32) { x >>= 32; r += 32; } + + return r + ecb_ld32 (x); +#endif + } +#endif + +ecb_function_ ecb_const ecb_bool ecb_is_pot32 (uint32_t x); +ecb_function_ ecb_const ecb_bool ecb_is_pot32 (uint32_t x) { return !(x & (x - 1)); } +ecb_function_ ecb_const ecb_bool ecb_is_pot64 (uint64_t x); +ecb_function_ ecb_const ecb_bool ecb_is_pot64 (uint64_t x) { return !(x & (x - 1)); } + +ecb_function_ ecb_const uint8_t ecb_bitrev8 (uint8_t x); +ecb_function_ ecb_const uint8_t ecb_bitrev8 (uint8_t x) +{ + return ( (x * 0x0802U & 0x22110U) + | (x * 0x8020U & 0x88440U)) * 0x10101U >> 16; +} + +ecb_function_ ecb_const uint16_t ecb_bitrev16 (uint16_t x); +ecb_function_ ecb_const uint16_t ecb_bitrev16 (uint16_t x) +{ + x = ((x >> 1) & 0x5555) | ((x & 0x5555) << 1); + x = ((x >> 2) & 0x3333) | ((x & 0x3333) << 2); + x = ((x >> 4) & 0x0f0f) | ((x & 0x0f0f) << 4); + x = ( x >> 8 ) | ( x << 8); + + return x; +} + +ecb_function_ ecb_const uint32_t ecb_bitrev32 (uint32_t x); +ecb_function_ ecb_const uint32_t ecb_bitrev32 (uint32_t x) +{ + x = ((x >> 1) & 0x55555555) | ((x & 0x55555555) << 1); + x = ((x >> 2) & 0x33333333) | ((x & 0x33333333) << 2); + x = ((x >> 4) & 0x0f0f0f0f) | ((x & 0x0f0f0f0f) << 4); + x = ((x >> 8) & 0x00ff00ff) | ((x & 0x00ff00ff) << 8); + x = ( x >> 16 ) | ( x << 16); + + return x; +} + +/* popcount64 is only available on 64 bit cpus as gcc builtin */ +/* so for this version we are lazy */ +ecb_function_ ecb_const int ecb_popcount64 (uint64_t x); +ecb_function_ ecb_const int +ecb_popcount64 (uint64_t x) +{ + return ecb_popcount32 (x) + ecb_popcount32 (x >> 32); +} + +ecb_inline ecb_const uint8_t ecb_rotl8 (uint8_t x, unsigned int count); +ecb_inline ecb_const uint8_t ecb_rotr8 (uint8_t x, unsigned int count); +ecb_inline ecb_const uint16_t ecb_rotl16 (uint16_t x, unsigned int count); +ecb_inline ecb_const uint16_t ecb_rotr16 (uint16_t x, unsigned int count); +ecb_inline ecb_const uint32_t ecb_rotl32 (uint32_t x, unsigned int count); +ecb_inline ecb_const uint32_t ecb_rotr32 (uint32_t x, unsigned int count); +ecb_inline ecb_const uint64_t ecb_rotl64 (uint64_t x, unsigned int count); +ecb_inline ecb_const uint64_t ecb_rotr64 (uint64_t x, unsigned int count); + +ecb_inline ecb_const uint8_t ecb_rotl8 (uint8_t x, unsigned int count) { return (x >> ( 8 - count)) | (x << count); } +ecb_inline ecb_const uint8_t ecb_rotr8 (uint8_t x, unsigned int count) { return (x << ( 8 - count)) | (x >> count); } +ecb_inline ecb_const uint16_t ecb_rotl16 (uint16_t x, unsigned int count) { return (x >> (16 - count)) | (x << count); } +ecb_inline ecb_const uint16_t ecb_rotr16 (uint16_t x, unsigned int count) { return (x << (16 - count)) | (x >> count); } +ecb_inline ecb_const uint32_t ecb_rotl32 (uint32_t x, unsigned int count) { return (x >> (32 - count)) | (x << count); } +ecb_inline ecb_const uint32_t ecb_rotr32 (uint32_t x, unsigned int count) { return (x << (32 - count)) | (x >> count); } +ecb_inline ecb_const uint64_t ecb_rotl64 (uint64_t x, unsigned int count) { return (x >> (64 - count)) | (x << count); } +ecb_inline ecb_const uint64_t ecb_rotr64 (uint64_t x, unsigned int count) { return (x << (64 - count)) | (x >> count); } + +#if ECB_CPP + +inline uint8_t ecb_ctz (uint8_t v) { return ecb_ctz32 (v); } +inline uint16_t ecb_ctz (uint16_t v) { return ecb_ctz32 (v); } +inline uint32_t ecb_ctz (uint32_t v) { return ecb_ctz32 (v); } +inline uint64_t ecb_ctz (uint64_t v) { return ecb_ctz64 (v); } + +inline bool ecb_is_pot (uint8_t v) { return ecb_is_pot32 (v); } +inline bool ecb_is_pot (uint16_t v) { return ecb_is_pot32 (v); } +inline bool ecb_is_pot (uint32_t v) { return ecb_is_pot32 (v); } +inline bool ecb_is_pot (uint64_t v) { return ecb_is_pot64 (v); } + +inline int ecb_ld (uint8_t v) { return ecb_ld32 (v); } +inline int ecb_ld (uint16_t v) { return ecb_ld32 (v); } +inline int ecb_ld (uint32_t v) { return ecb_ld32 (v); } +inline int ecb_ld (uint64_t v) { return ecb_ld64 (v); } + +inline int ecb_popcount (uint8_t v) { return ecb_popcount32 (v); } +inline int ecb_popcount (uint16_t v) { return ecb_popcount32 (v); } +inline int ecb_popcount (uint32_t v) { return ecb_popcount32 (v); } +inline int ecb_popcount (uint64_t v) { return ecb_popcount64 (v); } + +inline uint8_t ecb_bitrev (uint8_t v) { return ecb_bitrev8 (v); } +inline uint16_t ecb_bitrev (uint16_t v) { return ecb_bitrev16 (v); } +inline uint32_t ecb_bitrev (uint32_t v) { return ecb_bitrev32 (v); } + +inline uint8_t ecb_rotl (uint8_t v, unsigned int count) { return ecb_rotl8 (v, count); } +inline uint16_t ecb_rotl (uint16_t v, unsigned int count) { return ecb_rotl16 (v, count); } +inline uint32_t ecb_rotl (uint32_t v, unsigned int count) { return ecb_rotl32 (v, count); } +inline uint64_t ecb_rotl (uint64_t v, unsigned int count) { return ecb_rotl64 (v, count); } + +inline uint8_t ecb_rotr (uint8_t v, unsigned int count) { return ecb_rotr8 (v, count); } +inline uint16_t ecb_rotr (uint16_t v, unsigned int count) { return ecb_rotr16 (v, count); } +inline uint32_t ecb_rotr (uint32_t v, unsigned int count) { return ecb_rotr32 (v, count); } +inline uint64_t ecb_rotr (uint64_t v, unsigned int count) { return ecb_rotr64 (v, count); } + +#endif + +#if ECB_GCC_VERSION(4,3) || (ECB_CLANG_BUILTIN(__builtin_bswap32) && ECB_CLANG_BUILTIN(__builtin_bswap64)) + #if ECB_GCC_VERSION(4,8) || ECB_CLANG_BUILTIN(__builtin_bswap16) + #define ecb_bswap16(x) __builtin_bswap16 (x) + #else + #define ecb_bswap16(x) (__builtin_bswap32 (x) >> 16) + #endif + #define ecb_bswap32(x) __builtin_bswap32 (x) + #define ecb_bswap64(x) __builtin_bswap64 (x) +#elif _MSC_VER + #include + #define ecb_bswap16(x) ((uint16_t)_byteswap_ushort ((uint16_t)(x))) + #define ecb_bswap32(x) ((uint32_t)_byteswap_ulong ((uint32_t)(x))) + #define ecb_bswap64(x) ((uint64_t)_byteswap_uint64 ((uint64_t)(x))) +#else + ecb_function_ ecb_const uint16_t ecb_bswap16 (uint16_t x); + ecb_function_ ecb_const uint16_t + ecb_bswap16 (uint16_t x) + { + return ecb_rotl16 (x, 8); + } + + ecb_function_ ecb_const uint32_t ecb_bswap32 (uint32_t x); + ecb_function_ ecb_const uint32_t + ecb_bswap32 (uint32_t x) + { + return (((uint32_t)ecb_bswap16 (x)) << 16) | ecb_bswap16 (x >> 16); + } + + ecb_function_ ecb_const uint64_t ecb_bswap64 (uint64_t x); + ecb_function_ ecb_const uint64_t + ecb_bswap64 (uint64_t x) + { + return (((uint64_t)ecb_bswap32 (x)) << 32) | ecb_bswap32 (x >> 32); + } +#endif + +#if ECB_GCC_VERSION(4,5) || ECB_CLANG_BUILTIN(__builtin_unreachable) + #define ecb_unreachable() __builtin_unreachable () +#else + /* this seems to work fine, but gcc always emits a warning for it :/ */ + ecb_inline ecb_noreturn void ecb_unreachable (void); + ecb_inline ecb_noreturn void ecb_unreachable (void) { } +#endif + +/* try to tell the compiler that some condition is definitely true */ +#define ecb_assume(cond) if (!(cond)) ecb_unreachable (); else 0 + +ecb_inline ecb_const uint32_t ecb_byteorder_helper (void); +ecb_inline ecb_const uint32_t +ecb_byteorder_helper (void) +{ + /* the union code still generates code under pressure in gcc, */ + /* but less than using pointers, and always seems to */ + /* successfully return a constant. */ + /* the reason why we have this horrible preprocessor mess */ + /* is to avoid it in all cases, at least on common architectures */ + /* or when using a recent enough gcc version (>= 4.6) */ +#if (defined __BYTE_ORDER__ && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) \ + || ((__i386 || __i386__ || _M_IX86 || ECB_GCC_AMD64 || ECB_MSVC_AMD64) && !__VOS__) + #define ECB_LITTLE_ENDIAN 1 + return 0x44332211; +#elif (defined __BYTE_ORDER__ && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) \ + || ((__AARCH64EB__ || __MIPSEB__ || __ARMEB__) && !__VOS__) + #define ECB_BIG_ENDIAN 1 + return 0x11223344; +#else + union + { + uint8_t c[4]; + uint32_t u; + } u = { 0x11, 0x22, 0x33, 0x44 }; + return u.u; +#endif +} + +ecb_inline ecb_const ecb_bool ecb_big_endian (void); +ecb_inline ecb_const ecb_bool ecb_big_endian (void) { return ecb_byteorder_helper () == 0x11223344; } +ecb_inline ecb_const ecb_bool ecb_little_endian (void); +ecb_inline ecb_const ecb_bool ecb_little_endian (void) { return ecb_byteorder_helper () == 0x44332211; } + +/*****************************************************************************/ +/* unaligned load/store */ + +ecb_inline uint_fast16_t ecb_be_u16_to_host (uint_fast16_t v) { return ecb_little_endian () ? ecb_bswap16 (v) : v; } +ecb_inline uint_fast32_t ecb_be_u32_to_host (uint_fast32_t v) { return ecb_little_endian () ? ecb_bswap32 (v) : v; } +ecb_inline uint_fast64_t ecb_be_u64_to_host (uint_fast64_t v) { return ecb_little_endian () ? ecb_bswap64 (v) : v; } + +ecb_inline uint_fast16_t ecb_le_u16_to_host (uint_fast16_t v) { return ecb_big_endian () ? ecb_bswap16 (v) : v; } +ecb_inline uint_fast32_t ecb_le_u32_to_host (uint_fast32_t v) { return ecb_big_endian () ? ecb_bswap32 (v) : v; } +ecb_inline uint_fast64_t ecb_le_u64_to_host (uint_fast64_t v) { return ecb_big_endian () ? ecb_bswap64 (v) : v; } + +ecb_inline uint_fast16_t ecb_peek_u16_u (const void *ptr) { uint16_t v; memcpy (&v, ptr, sizeof (v)); return v; } +ecb_inline uint_fast32_t ecb_peek_u32_u (const void *ptr) { uint32_t v; memcpy (&v, ptr, sizeof (v)); return v; } +ecb_inline uint_fast64_t ecb_peek_u64_u (const void *ptr) { uint64_t v; memcpy (&v, ptr, sizeof (v)); return v; } + +ecb_inline uint_fast16_t ecb_peek_be_u16_u (const void *ptr) { return ecb_be_u16_to_host (ecb_peek_u16_u (ptr)); } +ecb_inline uint_fast32_t ecb_peek_be_u32_u (const void *ptr) { return ecb_be_u32_to_host (ecb_peek_u32_u (ptr)); } +ecb_inline uint_fast64_t ecb_peek_be_u64_u (const void *ptr) { return ecb_be_u64_to_host (ecb_peek_u64_u (ptr)); } + +ecb_inline uint_fast16_t ecb_peek_le_u16_u (const void *ptr) { return ecb_le_u16_to_host (ecb_peek_u16_u (ptr)); } +ecb_inline uint_fast32_t ecb_peek_le_u32_u (const void *ptr) { return ecb_le_u32_to_host (ecb_peek_u32_u (ptr)); } +ecb_inline uint_fast64_t ecb_peek_le_u64_u (const void *ptr) { return ecb_le_u64_to_host (ecb_peek_u64_u (ptr)); } + +ecb_inline uint_fast16_t ecb_host_to_be_u16 (uint_fast16_t v) { return ecb_little_endian () ? ecb_bswap16 (v) : v; } +ecb_inline uint_fast32_t ecb_host_to_be_u32 (uint_fast32_t v) { return ecb_little_endian () ? ecb_bswap32 (v) : v; } +ecb_inline uint_fast64_t ecb_host_to_be_u64 (uint_fast64_t v) { return ecb_little_endian () ? ecb_bswap64 (v) : v; } + +ecb_inline uint_fast16_t ecb_host_to_le_u16 (uint_fast16_t v) { return ecb_big_endian () ? ecb_bswap16 (v) : v; } +ecb_inline uint_fast32_t ecb_host_to_le_u32 (uint_fast32_t v) { return ecb_big_endian () ? ecb_bswap32 (v) : v; } +ecb_inline uint_fast64_t ecb_host_to_le_u64 (uint_fast64_t v) { return ecb_big_endian () ? ecb_bswap64 (v) : v; } + +ecb_inline void ecb_poke_u16_u (void *ptr, uint16_t v) { memcpy (ptr, &v, sizeof (v)); } +ecb_inline void ecb_poke_u32_u (void *ptr, uint32_t v) { memcpy (ptr, &v, sizeof (v)); } +ecb_inline void ecb_poke_u64_u (void *ptr, uint64_t v) { memcpy (ptr, &v, sizeof (v)); } + +ecb_inline void ecb_poke_be_u16_u (void *ptr, uint_fast16_t v) { ecb_poke_u16_u (ptr, ecb_host_to_be_u16 (v)); } +ecb_inline void ecb_poke_be_u32_u (void *ptr, uint_fast32_t v) { ecb_poke_u32_u (ptr, ecb_host_to_be_u32 (v)); } +ecb_inline void ecb_poke_be_u64_u (void *ptr, uint_fast64_t v) { ecb_poke_u64_u (ptr, ecb_host_to_be_u64 (v)); } + +ecb_inline void ecb_poke_le_u16_u (void *ptr, uint_fast16_t v) { ecb_poke_u16_u (ptr, ecb_host_to_le_u16 (v)); } +ecb_inline void ecb_poke_le_u32_u (void *ptr, uint_fast32_t v) { ecb_poke_u32_u (ptr, ecb_host_to_le_u32 (v)); } +ecb_inline void ecb_poke_le_u64_u (void *ptr, uint_fast64_t v) { ecb_poke_u64_u (ptr, ecb_host_to_le_u64 (v)); } + +#if ECB_CPP + +inline uint8_t ecb_bswap (uint8_t v) { return v; } +inline uint16_t ecb_bswap (uint16_t v) { return ecb_bswap16 (v); } +inline uint32_t ecb_bswap (uint32_t v) { return ecb_bswap32 (v); } +inline uint64_t ecb_bswap (uint64_t v) { return ecb_bswap64 (v); } + +template inline T ecb_be_to_host (T v) { return ecb_little_endian () ? ecb_bswap (v) : v; } +template inline T ecb_le_to_host (T v) { return ecb_big_endian () ? ecb_bswap (v) : v; } +template inline T ecb_peek (const void *ptr) { return *(const T *)ptr; } +template inline T ecb_peek_be (const void *ptr) { return ecb_be_to_host (ecb_peek (ptr)); } +template inline T ecb_peek_le (const void *ptr) { return ecb_le_to_host (ecb_peek (ptr)); } +template inline T ecb_peek_u (const void *ptr) { T v; memcpy (&v, ptr, sizeof (v)); return v; } +template inline T ecb_peek_be_u (const void *ptr) { return ecb_be_to_host (ecb_peek_u (ptr)); } +template inline T ecb_peek_le_u (const void *ptr) { return ecb_le_to_host (ecb_peek_u (ptr)); } + +template inline T ecb_host_to_be (T v) { return ecb_little_endian () ? ecb_bswap (v) : v; } +template inline T ecb_host_to_le (T v) { return ecb_big_endian () ? ecb_bswap (v) : v; } +template inline void ecb_poke (void *ptr, T v) { *(T *)ptr = v; } +template inline void ecb_poke_be (void *ptr, T v) { return ecb_poke (ptr, ecb_host_to_be (v)); } +template inline void ecb_poke_le (void *ptr, T v) { return ecb_poke (ptr, ecb_host_to_le (v)); } +template inline void ecb_poke_u (void *ptr, T v) { memcpy (ptr, &v, sizeof (v)); } +template inline void ecb_poke_be_u (void *ptr, T v) { return ecb_poke_u (ptr, ecb_host_to_be (v)); } +template inline void ecb_poke_le_u (void *ptr, T v) { return ecb_poke_u (ptr, ecb_host_to_le (v)); } + +#endif + +/*****************************************************************************/ + +#if ECB_GCC_VERSION(3,0) || ECB_C99 + #define ecb_mod(m,n) ((m) % (n) + ((m) % (n) < 0 ? (n) : 0)) +#else + #define ecb_mod(m,n) ((m) < 0 ? ((n) - 1 - ((-1 - (m)) % (n))) : ((m) % (n))) +#endif + +#if ECB_CPP + template + static inline T ecb_div_rd (T val, T div) + { + return val < 0 ? - ((-val + div - 1) / div) : (val ) / div; + } + template + static inline T ecb_div_ru (T val, T div) + { + return val < 0 ? - ((-val ) / div) : (val + div - 1) / div; + } +#else + #define ecb_div_rd(val,div) ((val) < 0 ? - ((-(val) + (div) - 1) / (div)) : ((val) ) / (div)) + #define ecb_div_ru(val,div) ((val) < 0 ? - ((-(val) ) / (div)) : ((val) + (div) - 1) / (div)) +#endif + +#if ecb_cplusplus_does_not_suck + /* does not work for local types (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2657.htm) */ + template + static inline int ecb_array_length (const T (&arr)[N]) + { + return N; + } +#else + #define ecb_array_length(name) (sizeof (name) / sizeof (name [0])) +#endif + +/*****************************************************************************/ + +ecb_function_ ecb_const uint32_t ecb_binary16_to_binary32 (uint32_t x); +ecb_function_ ecb_const uint32_t +ecb_binary16_to_binary32 (uint32_t x) +{ + unsigned int s = (x & 0x8000) << (31 - 15); + int e = (x >> 10) & 0x001f; + unsigned int m = x & 0x03ff; + + if (ecb_expect_false (e == 31)) + /* infinity or NaN */ + e = 255 - (127 - 15); + else if (ecb_expect_false (!e)) + { + if (ecb_expect_true (!m)) + /* zero, handled by code below by forcing e to 0 */ + e = 0 - (127 - 15); + else + { + /* subnormal, renormalise */ + unsigned int s = 10 - ecb_ld32 (m); + + m = (m << s) & 0x3ff; /* mask implicit bit */ + e -= s - 1; + } + } + + /* e and m now are normalised, or zero, (or inf or nan) */ + e += 127 - 15; + + return s | (e << 23) | (m << (23 - 10)); +} + +ecb_function_ ecb_const uint16_t ecb_binary32_to_binary16 (uint32_t x); +ecb_function_ ecb_const uint16_t +ecb_binary32_to_binary16 (uint32_t x) +{ + unsigned int s = (x >> 16) & 0x00008000; /* sign bit, the easy part */ + unsigned int e = ((x >> 23) & 0x000000ff) - (127 - 15); /* the desired exponent */ + unsigned int m = x & 0x007fffff; + + x &= 0x7fffffff; + + /* if it's within range of binary16 normals, use fast path */ + if (ecb_expect_true (0x38800000 <= x && x <= 0x477fefff)) + { + /* mantissa round-to-even */ + m += 0x00000fff + ((m >> (23 - 10)) & 1); + + /* handle overflow */ + if (ecb_expect_false (m >= 0x00800000)) + { + m >>= 1; + e += 1; + } + + return s | (e << 10) | (m >> (23 - 10)); + } + + /* handle large numbers and infinity */ + if (ecb_expect_true (0x477fefff < x && x <= 0x7f800000)) + return s | 0x7c00; + + /* handle zero, subnormals and small numbers */ + if (ecb_expect_true (x < 0x38800000)) + { + /* zero */ + if (ecb_expect_true (!x)) + return s; + + /* handle subnormals */ + + /* too small, will be zero */ + if (e < (14 - 24)) /* might not be sharp, but is good enough */ + return s; + + m |= 0x00800000; /* make implicit bit explicit */ + + /* very tricky - we need to round to the nearest e (+10) bit value */ + { + unsigned int bits = 14 - e; + unsigned int half = (1 << (bits - 1)) - 1; + unsigned int even = (m >> bits) & 1; + + /* if this overflows, we will end up with a normalised number */ + m = (m + half + even) >> bits; + } + + return s | m; + } + + /* handle NaNs, preserve leftmost nan bits, but make sure we don't turn them into infinities */ + m >>= 13; + + return s | 0x7c00 | m | !m; +} + +/*******************************************************************************/ +/* floating point stuff, can be disabled by defining ECB_NO_LIBM */ + +/* basically, everything uses "ieee pure-endian" floating point numbers */ +/* the only noteworthy exception is ancient armle, which uses order 43218765 */ +#if 0 \ + || __i386 || __i386__ \ + || ECB_GCC_AMD64 \ + || __powerpc__ || __ppc__ || __powerpc64__ || __ppc64__ \ + || defined __s390__ || defined __s390x__ \ + || defined __mips__ \ + || defined __alpha__ \ + || defined __hppa__ \ + || defined __ia64__ \ + || defined __m68k__ \ + || defined __m88k__ \ + || defined __sh__ \ + || defined _M_IX86 || defined ECB_MSVC_AMD64 || defined _M_IA64 \ + || (defined __arm__ && (defined __ARM_EABI__ || defined __EABI__ || defined __VFP_FP__ || defined _WIN32_WCE || defined __ANDROID__)) \ + || defined __aarch64__ + #define ECB_STDFP 1 +#else + #define ECB_STDFP 0 +#endif + +#ifndef ECB_NO_LIBM + + #include /* for frexp*, ldexp*, INFINITY, NAN */ + + /* only the oldest of old doesn't have this one. solaris. */ + #ifdef INFINITY + #define ECB_INFINITY INFINITY + #else + #define ECB_INFINITY HUGE_VAL + #endif + + #ifdef NAN + #define ECB_NAN NAN + #else + #define ECB_NAN ECB_INFINITY + #endif + + #if ECB_C99 || _XOPEN_VERSION >= 600 || _POSIX_VERSION >= 200112L + #define ecb_ldexpf(x,e) ldexpf ((x), (e)) + #define ecb_frexpf(x,e) frexpf ((x), (e)) + #else + #define ecb_ldexpf(x,e) (float) ldexp ((double) (x), (e)) + #define ecb_frexpf(x,e) (float) frexp ((double) (x), (e)) + #endif + + /* convert a float to ieee single/binary32 */ + ecb_function_ ecb_const uint32_t ecb_float_to_binary32 (float x); + ecb_function_ ecb_const uint32_t + ecb_float_to_binary32 (float x) + { + uint32_t r; + + #if ECB_STDFP + memcpy (&r, &x, 4); + #else + /* slow emulation, works for anything but -0 */ + uint32_t m; + int e; + + if (x == 0e0f ) return 0x00000000U; + if (x > +3.40282346638528860e+38f) return 0x7f800000U; + if (x < -3.40282346638528860e+38f) return 0xff800000U; + if (x != x ) return 0x7fbfffffU; + + m = ecb_frexpf (x, &e) * 0x1000000U; + + r = m & 0x80000000U; + + if (r) + m = -m; + + if (e <= -126) + { + m &= 0xffffffU; + m >>= (-125 - e); + e = -126; + } + + r |= (e + 126) << 23; + r |= m & 0x7fffffU; + #endif + + return r; + } + + /* converts an ieee single/binary32 to a float */ + ecb_function_ ecb_const float ecb_binary32_to_float (uint32_t x); + ecb_function_ ecb_const float + ecb_binary32_to_float (uint32_t x) + { + float r; + + #if ECB_STDFP + memcpy (&r, &x, 4); + #else + /* emulation, only works for normals and subnormals and +0 */ + int neg = x >> 31; + int e = (x >> 23) & 0xffU; + + x &= 0x7fffffU; + + if (e) + x |= 0x800000U; + else + e = 1; + + /* we distrust ldexpf a bit and do the 2**-24 scaling by an extra multiply */ + r = ecb_ldexpf (x * (0.5f / 0x800000U), e - 126); + + r = neg ? -r : r; + #endif + + return r; + } + + /* convert a double to ieee double/binary64 */ + ecb_function_ ecb_const uint64_t ecb_double_to_binary64 (double x); + ecb_function_ ecb_const uint64_t + ecb_double_to_binary64 (double x) + { + uint64_t r; + + #if ECB_STDFP + memcpy (&r, &x, 8); + #else + /* slow emulation, works for anything but -0 */ + uint64_t m; + int e; + + if (x == 0e0 ) return 0x0000000000000000U; + if (x > +1.79769313486231470e+308) return 0x7ff0000000000000U; + if (x < -1.79769313486231470e+308) return 0xfff0000000000000U; + if (x != x ) return 0X7ff7ffffffffffffU; + + m = frexp (x, &e) * 0x20000000000000U; + + r = m & 0x8000000000000000;; + + if (r) + m = -m; + + if (e <= -1022) + { + m &= 0x1fffffffffffffU; + m >>= (-1021 - e); + e = -1022; + } + + r |= ((uint64_t)(e + 1022)) << 52; + r |= m & 0xfffffffffffffU; + #endif + + return r; + } + + /* converts an ieee double/binary64 to a double */ + ecb_function_ ecb_const double ecb_binary64_to_double (uint64_t x); + ecb_function_ ecb_const double + ecb_binary64_to_double (uint64_t x) + { + double r; + + #if ECB_STDFP + memcpy (&r, &x, 8); + #else + /* emulation, only works for normals and subnormals and +0 */ + int neg = x >> 63; + int e = (x >> 52) & 0x7ffU; + + x &= 0xfffffffffffffU; + + if (e) + x |= 0x10000000000000U; + else + e = 1; + + /* we distrust ldexp a bit and do the 2**-53 scaling by an extra multiply */ + r = ldexp (x * (0.5 / 0x10000000000000U), e - 1022); + + r = neg ? -r : r; + #endif + + return r; + } + + /* convert a float to ieee half/binary16 */ + ecb_function_ ecb_const uint16_t ecb_float_to_binary16 (float x); + ecb_function_ ecb_const uint16_t + ecb_float_to_binary16 (float x) + { + return ecb_binary32_to_binary16 (ecb_float_to_binary32 (x)); + } + + /* convert an ieee half/binary16 to float */ + ecb_function_ ecb_const float ecb_binary16_to_float (uint16_t x); + ecb_function_ ecb_const float + ecb_binary16_to_float (uint16_t x) + { + return ecb_binary32_to_float (ecb_binary16_to_binary32 (x)); + } + +#endif + +#endif + +/* ECB.H END */ + +#if ECB_MEMORY_FENCE_NEEDS_PTHREADS +/* if your architecture doesn't need memory fences, e.g. because it is + * single-cpu/core, or if you use libev in a project that doesn't use libev + * from multiple threads, then you can define ECB_NO_THREADS when compiling + * libev, in which cases the memory fences become nops. + * alternatively, you can remove this #error and link against libpthread, + * which will then provide the memory fences. + */ +# error "memory fences not defined for your architecture, please report" +#endif + +#ifndef ECB_MEMORY_FENCE +# define ECB_MEMORY_FENCE do { } while (0) +# define ECB_MEMORY_FENCE_ACQUIRE ECB_MEMORY_FENCE +# define ECB_MEMORY_FENCE_RELEASE ECB_MEMORY_FENCE +#endif + +#define inline_size ecb_inline + +#if EV_FEATURE_CODE +# define inline_speed ecb_inline +#else +# define inline_speed ecb_noinline static +#endif + +/*****************************************************************************/ +/* raw syscall wrappers */ + +#if EV_NEED_SYSCALL + +#include + +/* + * define some syscall wrappers for common architectures + * this is mostly for nice looks during debugging, not performance. + * our syscalls return < 0, not == -1, on error. which is good + * enough for linux aio. + * TODO: arm is also common nowadays, maybe even mips and x86 + * TODO: after implementing this, it suddenly looks like overkill, but its hard to remove... + */ +#if __GNUC__ && __linux && ECB_AMD64 && !EV_FEATURE_CODE + /* the costly errno access probably kills this for size optimisation */ + + #define ev_syscall(nr,narg,arg1,arg2,arg3,arg4,arg5,arg6) \ + ({ \ + long res; \ + register unsigned long r6 __asm__ ("r9" ); \ + register unsigned long r5 __asm__ ("r8" ); \ + register unsigned long r4 __asm__ ("r10"); \ + register unsigned long r3 __asm__ ("rdx"); \ + register unsigned long r2 __asm__ ("rsi"); \ + register unsigned long r1 __asm__ ("rdi"); \ + if (narg >= 6) r6 = (unsigned long)(arg6); \ + if (narg >= 5) r5 = (unsigned long)(arg5); \ + if (narg >= 4) r4 = (unsigned long)(arg4); \ + if (narg >= 3) r3 = (unsigned long)(arg3); \ + if (narg >= 2) r2 = (unsigned long)(arg2); \ + if (narg >= 1) r1 = (unsigned long)(arg1); \ + __asm__ __volatile__ ( \ + "syscall\n\t" \ + : "=a" (res) \ + : "0" (nr), "r" (r1), "r" (r2), "r" (r3), "r" (r4), "r" (r5) \ + : "cc", "r11", "cx", "memory"); \ + errno = -res; \ + res; \ + }) + +#endif + +#ifdef ev_syscall + #define ev_syscall0(nr) ev_syscall (nr, 0, 0, 0, 0, 0, 0, 0) + #define ev_syscall1(nr,arg1) ev_syscall (nr, 1, arg1, 0, 0, 0, 0, 0) + #define ev_syscall2(nr,arg1,arg2) ev_syscall (nr, 2, arg1, arg2, 0, 0, 0, 0) + #define ev_syscall3(nr,arg1,arg2,arg3) ev_syscall (nr, 3, arg1, arg2, arg3, 0, 0, 0) + #define ev_syscall4(nr,arg1,arg2,arg3,arg4) ev_syscall (nr, 3, arg1, arg2, arg3, arg4, 0, 0) + #define ev_syscall5(nr,arg1,arg2,arg3,arg4,arg5) ev_syscall (nr, 5, arg1, arg2, arg3, arg4, arg5, 0) + #define ev_syscall6(nr,arg1,arg2,arg3,arg4,arg5,arg6) ev_syscall (nr, 6, arg1, arg2, arg3, arg4, arg5,arg6) +#else + #define ev_syscall0(nr) syscall (nr) + #define ev_syscall1(nr,arg1) syscall (nr, arg1) + #define ev_syscall2(nr,arg1,arg2) syscall (nr, arg1, arg2) + #define ev_syscall3(nr,arg1,arg2,arg3) syscall (nr, arg1, arg2, arg3) + #define ev_syscall4(nr,arg1,arg2,arg3,arg4) syscall (nr, arg1, arg2, arg3, arg4) + #define ev_syscall5(nr,arg1,arg2,arg3,arg4,arg5) syscall (nr, arg1, arg2, arg3, arg4, arg5) + #define ev_syscall6(nr,arg1,arg2,arg3,arg4,arg5,arg6) syscall (nr, arg1, arg2, arg3, arg4, arg5,arg6) +#endif + +#endif + +/*****************************************************************************/ + +#define NUMPRI (EV_MAXPRI - EV_MINPRI + 1) + +#if EV_MINPRI == EV_MAXPRI +# define ABSPRI(w) (((W)w), 0) +#else +# define ABSPRI(w) (((W)w)->priority - EV_MINPRI) +#endif + +#define EMPTY /* required for microsofts broken pseudo-c compiler */ + +typedef ev_watcher *W; +typedef ev_watcher_list *WL; +typedef ev_watcher_time *WT; + +#define ev_active(w) ((W)(w))->active +#define ev_at(w) ((WT)(w))->at + +#if EV_USE_REALTIME +/* sig_atomic_t is used to avoid per-thread variables or locking but still */ +/* giving it a reasonably high chance of working on typical architectures */ +static EV_ATOMIC_T have_realtime; /* did clock_gettime (CLOCK_REALTIME) work? */ +#endif + +#if EV_USE_MONOTONIC +static EV_ATOMIC_T have_monotonic; /* did clock_gettime (CLOCK_MONOTONIC) work? */ +#endif + +#ifndef EV_FD_TO_WIN32_HANDLE +# define EV_FD_TO_WIN32_HANDLE(fd) _get_osfhandle (fd) +#endif +#ifndef EV_WIN32_HANDLE_TO_FD +# define EV_WIN32_HANDLE_TO_FD(handle) _open_osfhandle (handle, 0) +#endif +#ifndef EV_WIN32_CLOSE_FD +# define EV_WIN32_CLOSE_FD(fd) close (fd) +#endif + +#ifdef _WIN32 +# include "ev_win32.c" +#endif + +/*****************************************************************************/ + +#if EV_USE_LINUXAIO +# include /* probably only needed for aio_context_t */ +#endif + +/* define a suitable floor function (only used by periodics atm) */ + +#if EV_USE_FLOOR +# include +# define ev_floor(v) floor (v) +#else + +#include + +/* a floor() replacement function, should be independent of ev_tstamp type */ +ecb_noinline +static ev_tstamp +ev_floor (ev_tstamp v) +{ + /* the choice of shift factor is not terribly important */ +#if FLT_RADIX != 2 /* assume FLT_RADIX == 10 */ + const ev_tstamp shift = sizeof (unsigned long) >= 8 ? 10000000000000000000. : 1000000000.; +#else + const ev_tstamp shift = sizeof (unsigned long) >= 8 ? 18446744073709551616. : 4294967296.; +#endif + + /* special treatment for negative arguments */ + if (ecb_expect_false (v < 0.)) + { + ev_tstamp f = -ev_floor (-v); + + return f - (f == v ? 0 : 1); + } + + /* argument too large for an unsigned long? then reduce it */ + if (ecb_expect_false (v >= shift)) + { + ev_tstamp f; + + if (v == v - 1.) + return v; /* very large numbers are assumed to be integer */ + + f = shift * ev_floor (v * (1. / shift)); + return f + ev_floor (v - f); + } + + /* fits into an unsigned long */ + return (unsigned long)v; +} + +#endif + +/*****************************************************************************/ + +#ifdef __linux +# include +#endif + +ecb_noinline ecb_cold +static unsigned int +ev_linux_version (void) +{ +#ifdef __linux + unsigned int v = 0; + struct utsname buf; + int i; + char *p = buf.release; + + if (uname (&buf)) + return 0; + + for (i = 3+1; --i; ) + { + unsigned int c = 0; + + for (;;) + { + if (*p >= '0' && *p <= '9') + c = c * 10 + *p++ - '0'; + else + { + p += *p == '.'; + break; + } + } + + v = (v << 8) | c; + } + + return v; +#else + return 0; +#endif +} + +/*****************************************************************************/ + +#if EV_AVOID_STDIO +ecb_noinline ecb_cold +static void +ev_printerr (const char *msg) +{ + write (STDERR_FILENO, msg, strlen (msg)); +} +#endif + +static void (*syserr_cb)(const char *msg) EV_NOEXCEPT; + +ecb_cold +void +ev_set_syserr_cb (void (*cb)(const char *msg) EV_NOEXCEPT) EV_NOEXCEPT +{ + syserr_cb = cb; +} + +ecb_noinline ecb_cold +static void +ev_syserr (const char *msg) +{ + if (!msg) + msg = "(libev) system error"; + + if (syserr_cb) + syserr_cb (msg); + else + { +#if EV_AVOID_STDIO + ev_printerr (msg); + ev_printerr (": "); + ev_printerr (strerror (errno)); + ev_printerr ("\n"); +#else + perror (msg); +#endif + abort (); + } +} + +static void * +ev_realloc_emul (void *ptr, long size) EV_NOEXCEPT +{ + /* some systems, notably openbsd and darwin, fail to properly + * implement realloc (x, 0) (as required by both ansi c-89 and + * the single unix specification, so work around them here. + * recently, also (at least) fedora and debian started breaking it, + * despite documenting it otherwise. + */ + + if (size) + return realloc (ptr, size); + + free (ptr); + return 0; +} + +static void *(*alloc)(void *ptr, long size) EV_NOEXCEPT = ev_realloc_emul; + +ecb_cold +void +ev_set_allocator (void *(*cb)(void *ptr, long size) EV_NOEXCEPT) EV_NOEXCEPT +{ + alloc = cb; +} + +inline_speed void * +ev_realloc (void *ptr, long size) +{ + ptr = alloc (ptr, size); + + if (!ptr && size) + { +#if EV_AVOID_STDIO + ev_printerr ("(libev) memory allocation failed, aborting.\n"); +#else + fprintf (stderr, "(libev) cannot allocate %ld bytes, aborting.", size); +#endif + abort (); + } + + return ptr; +} + +#define ev_malloc(size) ev_realloc (0, (size)) +#define ev_free(ptr) ev_realloc ((ptr), 0) + +/*****************************************************************************/ + +/* set in reify when reification needed */ +#define EV_ANFD_REIFY 1 + +/* file descriptor info structure */ +typedef struct +{ + WL head; + unsigned char events; /* the events watched for */ + unsigned char reify; /* flag set when this ANFD needs reification (EV_ANFD_REIFY, EV__IOFDSET) */ + unsigned char emask; /* some backends store the actual kernel mask in here */ + unsigned char eflags; /* flags field for use by backends */ +#if EV_USE_EPOLL + unsigned int egen; /* generation counter to counter epoll bugs */ +#endif +#if EV_SELECT_IS_WINSOCKET || EV_USE_IOCP + SOCKET handle; +#endif +#if EV_USE_IOCP + OVERLAPPED or, ow; +#endif +} ANFD; + +/* stores the pending event set for a given watcher */ +typedef struct +{ + W w; + int events; /* the pending event set for the given watcher */ +} ANPENDING; + +#if EV_USE_INOTIFY +/* hash table entry per inotify-id */ +typedef struct +{ + WL head; +} ANFS; +#endif + +/* Heap Entry */ +#if EV_HEAP_CACHE_AT + /* a heap element */ + typedef struct { + ev_tstamp at; + WT w; + } ANHE; + + #define ANHE_w(he) (he).w /* access watcher, read-write */ + #define ANHE_at(he) (he).at /* access cached at, read-only */ + #define ANHE_at_cache(he) (he).at = (he).w->at /* update at from watcher */ +#else + /* a heap element */ + typedef WT ANHE; + + #define ANHE_w(he) (he) + #define ANHE_at(he) (he)->at + #define ANHE_at_cache(he) +#endif + +#if EV_MULTIPLICITY + + struct ev_loop + { + ev_tstamp ev_rt_now; + #define ev_rt_now ((loop)->ev_rt_now) + #define VAR(name,decl) decl; + #include "ev_vars.h" + #undef VAR + }; + #include "ev_wrap.h" + + static struct ev_loop default_loop_struct; + EV_API_DECL struct ev_loop *ev_default_loop_ptr = 0; /* needs to be initialised to make it a definition despite extern */ + +#else + + EV_API_DECL ev_tstamp ev_rt_now = EV_TS_CONST (0.); /* needs to be initialised to make it a definition despite extern */ + #define VAR(name,decl) static decl; + #include "ev_vars.h" + #undef VAR + + static int ev_default_loop_ptr; + +#endif + +#if EV_FEATURE_API +# define EV_RELEASE_CB if (ecb_expect_false (release_cb)) release_cb (EV_A) +# define EV_ACQUIRE_CB if (ecb_expect_false (acquire_cb)) acquire_cb (EV_A) +# define EV_INVOKE_PENDING invoke_cb (EV_A) +#else +# define EV_RELEASE_CB (void)0 +# define EV_ACQUIRE_CB (void)0 +# define EV_INVOKE_PENDING ev_invoke_pending (EV_A) +#endif + +#define EVBREAK_RECURSE 0x80 + +/*****************************************************************************/ + +#ifndef EV_HAVE_EV_TIME +ev_tstamp +ev_time (void) EV_NOEXCEPT +{ +#if EV_USE_REALTIME + if (ecb_expect_true (have_realtime)) + { + struct timespec ts; + clock_gettime (CLOCK_REALTIME, &ts); + return EV_TS_GET (ts); + } +#endif + + { + struct timeval tv; + gettimeofday (&tv, 0); + return EV_TV_GET (tv); + } +} +#endif + +inline_size ev_tstamp +get_clock (void) +{ +#if EV_USE_MONOTONIC + if (ecb_expect_true (have_monotonic)) + { + struct timespec ts; + clock_gettime (CLOCK_MONOTONIC, &ts); + return EV_TS_GET (ts); + } +#endif + + return ev_time (); +} + +#if EV_MULTIPLICITY +ev_tstamp +ev_now (EV_P) EV_NOEXCEPT +{ + return ev_rt_now; +} +#endif + +void +ev_sleep (ev_tstamp delay) EV_NOEXCEPT +{ + if (delay > EV_TS_CONST (0.)) + { +#if EV_USE_NANOSLEEP + struct timespec ts; + + EV_TS_SET (ts, delay); + nanosleep (&ts, 0); +#elif defined _WIN32 + /* maybe this should round up, as ms is very low resolution */ + /* compared to select (µs) or nanosleep (ns) */ + Sleep ((unsigned long)(EV_TS_TO_MSEC (delay))); +#else + struct timeval tv; + + /* here we rely on sys/time.h + sys/types.h + unistd.h providing select */ + /* something not guaranteed by newer posix versions, but guaranteed */ + /* by older ones */ + EV_TV_SET (tv, delay); + select (0, 0, 0, 0, &tv); +#endif + } +} + +/*****************************************************************************/ + +#define MALLOC_ROUND 4096 /* prefer to allocate in chunks of this size, must be 2**n and >> 4 longs */ + +/* find a suitable new size for the given array, */ +/* hopefully by rounding to a nice-to-malloc size */ +inline_size int +array_nextsize (int elem, int cur, int cnt) +{ + int ncur = cur + 1; + + do + ncur <<= 1; + while (cnt > ncur); + + /* if size is large, round to MALLOC_ROUND - 4 * longs to accommodate malloc overhead */ + if (elem * ncur > MALLOC_ROUND - sizeof (void *) * 4) + { + ncur *= elem; + ncur = (ncur + elem + (MALLOC_ROUND - 1) + sizeof (void *) * 4) & ~(MALLOC_ROUND - 1); + ncur = ncur - sizeof (void *) * 4; + ncur /= elem; + } + + return ncur; +} + +ecb_noinline ecb_cold +static void * +array_realloc (int elem, void *base, int *cur, int cnt) +{ + *cur = array_nextsize (elem, *cur, cnt); + return ev_realloc (base, elem * *cur); +} + +#define array_needsize_noinit(base,offset,count) + +#define array_needsize_zerofill(base,offset,count) \ + memset ((void *)(base + offset), 0, sizeof (*(base)) * (count)) + +#define array_needsize(type,base,cur,cnt,init) \ + if (ecb_expect_false ((cnt) > (cur))) \ + { \ + ecb_unused int ocur_ = (cur); \ + (base) = (type *)array_realloc \ + (sizeof (type), (base), &(cur), (cnt)); \ + init ((base), ocur_, ((cur) - ocur_)); \ + } + +#if 0 +#define array_slim(type,stem) \ + if (stem ## max < array_roundsize (stem ## cnt >> 2)) \ + { \ + stem ## max = array_roundsize (stem ## cnt >> 1); \ + base = (type *)ev_realloc (base, sizeof (type) * (stem ## max));\ + fprintf (stderr, "slimmed down " # stem " to %d\n", stem ## max);/*D*/\ + } +#endif + +#define array_free(stem, idx) \ + ev_free (stem ## s idx); stem ## cnt idx = stem ## max idx = 0; stem ## s idx = 0 + +/*****************************************************************************/ + +/* dummy callback for pending events */ +ecb_noinline +static void +pendingcb (EV_P_ ev_prepare *w, int revents) +{ +} + +ecb_noinline +void +ev_feed_event (EV_P_ void *w, int revents) EV_NOEXCEPT +{ + W w_ = (W)w; + int pri = ABSPRI (w_); + + if (ecb_expect_false (w_->pending)) + pendings [pri][w_->pending - 1].events |= revents; + else + { + w_->pending = ++pendingcnt [pri]; + array_needsize (ANPENDING, pendings [pri], pendingmax [pri], w_->pending, array_needsize_noinit); + pendings [pri][w_->pending - 1].w = w_; + pendings [pri][w_->pending - 1].events = revents; + } + + pendingpri = NUMPRI - 1; +} + +inline_speed void +feed_reverse (EV_P_ W w) +{ + array_needsize (W, rfeeds, rfeedmax, rfeedcnt + 1, array_needsize_noinit); + rfeeds [rfeedcnt++] = w; +} + +inline_size void +feed_reverse_done (EV_P_ int revents) +{ + do + ev_feed_event (EV_A_ rfeeds [--rfeedcnt], revents); + while (rfeedcnt); +} + +inline_speed void +queue_events (EV_P_ W *events, int eventcnt, int type) +{ + int i; + + for (i = 0; i < eventcnt; ++i) + ev_feed_event (EV_A_ events [i], type); +} + +/*****************************************************************************/ + +inline_speed void +fd_event_nocheck (EV_P_ int fd, int revents) +{ + ANFD *anfd = anfds + fd; + ev_io *w; + + for (w = (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next) + { + int ev = w->events & revents; + + if (ev) + ev_feed_event (EV_A_ (W)w, ev); + } +} + +/* do not submit kernel events for fds that have reify set */ +/* because that means they changed while we were polling for new events */ +inline_speed void +fd_event (EV_P_ int fd, int revents) +{ + ANFD *anfd = anfds + fd; + + if (ecb_expect_true (!anfd->reify)) + fd_event_nocheck (EV_A_ fd, revents); +} + +void +ev_feed_fd_event (EV_P_ int fd, int revents) EV_NOEXCEPT +{ + if (fd >= 0 && fd < anfdmax) + fd_event_nocheck (EV_A_ fd, revents); +} + +/* make sure the external fd watch events are in-sync */ +/* with the kernel/libev internal state */ +inline_size void +fd_reify (EV_P) +{ + int i; + + /* most backends do not modify the fdchanges list in backend_modfiy. + * except io_uring, which has fixed-size buffers which might force us + * to handle events in backend_modify, causing fdchanges to be amended, + * which could result in an endless loop. + * to avoid this, we do not dynamically handle fds that were added + * during fd_reify. that means that for those backends, fdchangecnt + * might be non-zero during poll, which must cause them to not block. + * to not put too much of a burden on other backends, this detail + * needs to be handled in the backend. + */ + int changecnt = fdchangecnt; + +#if EV_SELECT_IS_WINSOCKET || EV_USE_IOCP + for (i = 0; i < changecnt; ++i) + { + int fd = fdchanges [i]; + ANFD *anfd = anfds + fd; + + if (anfd->reify & EV__IOFDSET && anfd->head) + { + SOCKET handle = EV_FD_TO_WIN32_HANDLE (fd); + + if (handle != anfd->handle) + { + unsigned long arg; + + assert (("libev: only socket fds supported in this configuration", ioctlsocket (handle, FIONREAD, &arg) == 0)); + + /* handle changed, but fd didn't - we need to do it in two steps */ + backend_modify (EV_A_ fd, anfd->events, 0); + anfd->events = 0; + anfd->handle = handle; + } + } + } +#endif + + for (i = 0; i < changecnt; ++i) + { + int fd = fdchanges [i]; + ANFD *anfd = anfds + fd; + ev_io *w; + + unsigned char o_events = anfd->events; + unsigned char o_reify = anfd->reify; + + anfd->reify = 0; + + /*if (ecb_expect_true (o_reify & EV_ANFD_REIFY)) probably a deoptimisation */ + { + anfd->events = 0; + + for (w = (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next) + anfd->events |= (unsigned char)w->events; + + if (o_events != anfd->events) + o_reify = EV__IOFDSET; /* actually |= */ + } + + if (o_reify & EV__IOFDSET) + backend_modify (EV_A_ fd, o_events, anfd->events); + } + + /* normally, fdchangecnt hasn't changed. if it has, then new fds have been added. + * this is a rare case (see beginning comment in this function), so we copy them to the + * front and hope the backend handles this case. + */ + if (ecb_expect_false (fdchangecnt != changecnt)) + memmove (fdchanges, fdchanges + changecnt, (fdchangecnt - changecnt) * sizeof (*fdchanges)); + + fdchangecnt -= changecnt; +} + +/* something about the given fd changed */ +inline_size +void +fd_change (EV_P_ int fd, int flags) +{ + unsigned char reify = anfds [fd].reify; + anfds [fd].reify = reify | flags; + + if (ecb_expect_true (!reify)) + { + ++fdchangecnt; + array_needsize (int, fdchanges, fdchangemax, fdchangecnt, array_needsize_noinit); + fdchanges [fdchangecnt - 1] = fd; + } +} + +/* the given fd is invalid/unusable, so make sure it doesn't hurt us anymore */ +inline_speed ecb_cold void +fd_kill (EV_P_ int fd) +{ + ev_io *w; + + while ((w = (ev_io *)anfds [fd].head)) + { + ev_io_stop (EV_A_ w); + ev_feed_event (EV_A_ (W)w, EV_ERROR | EV_READ | EV_WRITE); + } +} + +/* check whether the given fd is actually valid, for error recovery */ +inline_size ecb_cold int +fd_valid (int fd) +{ +#ifdef _WIN32 + return EV_FD_TO_WIN32_HANDLE (fd) != -1; +#else + return fcntl (fd, F_GETFD) != -1; +#endif +} + +/* called on EBADF to verify fds */ +ecb_noinline ecb_cold +static void +fd_ebadf (EV_P) +{ + int fd; + + for (fd = 0; fd < anfdmax; ++fd) + if (anfds [fd].events) + if (!fd_valid (fd) && errno == EBADF) + fd_kill (EV_A_ fd); +} + +/* called on ENOMEM in select/poll to kill some fds and retry */ +ecb_noinline ecb_cold +static void +fd_enomem (EV_P) +{ + int fd; + + for (fd = anfdmax; fd--; ) + if (anfds [fd].events) + { + fd_kill (EV_A_ fd); + break; + } +} + +/* usually called after fork if backend needs to re-arm all fds from scratch */ +ecb_noinline +static void +fd_rearm_all (EV_P) +{ + int fd; + + for (fd = 0; fd < anfdmax; ++fd) + if (anfds [fd].events) + { + anfds [fd].events = 0; + anfds [fd].emask = 0; + fd_change (EV_A_ fd, EV__IOFDSET | EV_ANFD_REIFY); + } +} + +/* used to prepare libev internal fd's */ +/* this is not fork-safe */ +inline_speed void +fd_intern (int fd) +{ +#ifdef _WIN32 + unsigned long arg = 1; + ioctlsocket (EV_FD_TO_WIN32_HANDLE (fd), FIONBIO, &arg); +#else + fcntl (fd, F_SETFD, FD_CLOEXEC); + fcntl (fd, F_SETFL, O_NONBLOCK); +#endif +} + +/*****************************************************************************/ + +/* + * the heap functions want a real array index. array index 0 is guaranteed to not + * be in-use at any time. the first heap entry is at array [HEAP0]. DHEAP gives + * the branching factor of the d-tree. + */ + +/* + * at the moment we allow libev the luxury of two heaps, + * a small-code-size 2-heap one and a ~1.5kb larger 4-heap + * which is more cache-efficient. + * the difference is about 5% with 50000+ watchers. + */ +#if EV_USE_4HEAP + +#define DHEAP 4 +#define HEAP0 (DHEAP - 1) /* index of first element in heap */ +#define HPARENT(k) ((((k) - HEAP0 - 1) / DHEAP) + HEAP0) +#define UPHEAP_DONE(p,k) ((p) == (k)) + +/* away from the root */ +inline_speed void +downheap (ANHE *heap, int N, int k) +{ + ANHE he = heap [k]; + ANHE *E = heap + N + HEAP0; + + for (;;) + { + ev_tstamp minat; + ANHE *minpos; + ANHE *pos = heap + DHEAP * (k - HEAP0) + HEAP0 + 1; + + /* find minimum child */ + if (ecb_expect_true (pos + DHEAP - 1 < E)) + { + /* fast path */ (minpos = pos + 0), (minat = ANHE_at (*minpos)); + if ( minat > ANHE_at (pos [1])) (minpos = pos + 1), (minat = ANHE_at (*minpos)); + if ( minat > ANHE_at (pos [2])) (minpos = pos + 2), (minat = ANHE_at (*minpos)); + if ( minat > ANHE_at (pos [3])) (minpos = pos + 3), (minat = ANHE_at (*minpos)); + } + else if (pos < E) + { + /* slow path */ (minpos = pos + 0), (minat = ANHE_at (*minpos)); + if (pos + 1 < E && minat > ANHE_at (pos [1])) (minpos = pos + 1), (minat = ANHE_at (*minpos)); + if (pos + 2 < E && minat > ANHE_at (pos [2])) (minpos = pos + 2), (minat = ANHE_at (*minpos)); + if (pos + 3 < E && minat > ANHE_at (pos [3])) (minpos = pos + 3), (minat = ANHE_at (*minpos)); + } + else + break; + + if (ANHE_at (he) <= minat) + break; + + heap [k] = *minpos; + ev_active (ANHE_w (*minpos)) = k; + + k = minpos - heap; + } + + heap [k] = he; + ev_active (ANHE_w (he)) = k; +} + +#else /* not 4HEAP */ + +#define HEAP0 1 +#define HPARENT(k) ((k) >> 1) +#define UPHEAP_DONE(p,k) (!(p)) + +/* away from the root */ +inline_speed void +downheap (ANHE *heap, int N, int k) +{ + ANHE he = heap [k]; + + for (;;) + { + int c = k << 1; + + if (c >= N + HEAP0) + break; + + c += c + 1 < N + HEAP0 && ANHE_at (heap [c]) > ANHE_at (heap [c + 1]) + ? 1 : 0; + + if (ANHE_at (he) <= ANHE_at (heap [c])) + break; + + heap [k] = heap [c]; + ev_active (ANHE_w (heap [k])) = k; + + k = c; + } + + heap [k] = he; + ev_active (ANHE_w (he)) = k; +} +#endif + +/* towards the root */ +inline_speed void +upheap (ANHE *heap, int k) +{ + ANHE he = heap [k]; + + for (;;) + { + int p = HPARENT (k); + + if (UPHEAP_DONE (p, k) || ANHE_at (heap [p]) <= ANHE_at (he)) + break; + + heap [k] = heap [p]; + ev_active (ANHE_w (heap [k])) = k; + k = p; + } + + heap [k] = he; + ev_active (ANHE_w (he)) = k; +} + +/* move an element suitably so it is in a correct place */ +inline_size void +adjustheap (ANHE *heap, int N, int k) +{ + if (k > HEAP0 && ANHE_at (heap [k]) <= ANHE_at (heap [HPARENT (k)])) + upheap (heap, k); + else + downheap (heap, N, k); +} + +/* rebuild the heap: this function is used only once and executed rarely */ +inline_size void +reheap (ANHE *heap, int N) +{ + int i; + + /* we don't use floyds algorithm, upheap is simpler and is more cache-efficient */ + /* also, this is easy to implement and correct for both 2-heaps and 4-heaps */ + for (i = 0; i < N; ++i) + upheap (heap, i + HEAP0); +} + +/*****************************************************************************/ + +/* associate signal watchers to a signal */ +typedef struct +{ + EV_ATOMIC_T pending; +#if EV_MULTIPLICITY + EV_P; +#endif + WL head; +} ANSIG; + +static ANSIG signals [EV_NSIG - 1]; + +/*****************************************************************************/ + +#if EV_SIGNAL_ENABLE || EV_ASYNC_ENABLE + +ecb_noinline ecb_cold +static void +evpipe_init (EV_P) +{ + if (!ev_is_active (&pipe_w)) + { + int fds [2]; + +# if EV_USE_EVENTFD + fds [0] = -1; + fds [1] = eventfd (0, EFD_NONBLOCK | EFD_CLOEXEC); + if (fds [1] < 0 && errno == EINVAL) + fds [1] = eventfd (0, 0); + + if (fds [1] < 0) +# endif + { + while (pipe (fds)) + ev_syserr ("(libev) error creating signal/async pipe"); + + fd_intern (fds [0]); + } + + evpipe [0] = fds [0]; + + if (evpipe [1] < 0) + evpipe [1] = fds [1]; /* first call, set write fd */ + else + { + /* on subsequent calls, do not change evpipe [1] */ + /* so that evpipe_write can always rely on its value. */ + /* this branch does not do anything sensible on windows, */ + /* so must not be executed on windows */ + + dup2 (fds [1], evpipe [1]); + close (fds [1]); + } + + fd_intern (evpipe [1]); + + ev_io_set (&pipe_w, evpipe [0] < 0 ? evpipe [1] : evpipe [0], EV_READ); + ev_io_start (EV_A_ &pipe_w); + ev_unref (EV_A); /* watcher should not keep loop alive */ + } +} + +inline_speed void +evpipe_write (EV_P_ EV_ATOMIC_T *flag) +{ + ECB_MEMORY_FENCE; /* push out the write before this function was called, acquire flag */ + + if (ecb_expect_true (*flag)) + return; + + *flag = 1; + ECB_MEMORY_FENCE_RELEASE; /* make sure flag is visible before the wakeup */ + + pipe_write_skipped = 1; + + ECB_MEMORY_FENCE; /* make sure pipe_write_skipped is visible before we check pipe_write_wanted */ + + if (pipe_write_wanted) + { + int old_errno; + + pipe_write_skipped = 0; + ECB_MEMORY_FENCE_RELEASE; + + old_errno = errno; /* save errno because write will clobber it */ + +#if EV_USE_EVENTFD + if (evpipe [0] < 0) + { + uint64_t counter = 1; + write (evpipe [1], &counter, sizeof (uint64_t)); + } + else +#endif + { +#ifdef _WIN32 + WSABUF buf; + DWORD sent; + buf.buf = (char *)&buf; + buf.len = 1; + WSASend (EV_FD_TO_WIN32_HANDLE (evpipe [1]), &buf, 1, &sent, 0, 0, 0); +#else + write (evpipe [1], &(evpipe [1]), 1); +#endif + } + + errno = old_errno; + } +} + +/* called whenever the libev signal pipe */ +/* got some events (signal, async) */ +static void +pipecb (EV_P_ ev_io *iow, int revents) +{ + int i; + + if (revents & EV_READ) + { +#if EV_USE_EVENTFD + if (evpipe [0] < 0) + { + uint64_t counter; + read (evpipe [1], &counter, sizeof (uint64_t)); + } + else +#endif + { + char dummy[4]; +#ifdef _WIN32 + WSABUF buf; + DWORD recvd; + DWORD flags = 0; + buf.buf = dummy; + buf.len = sizeof (dummy); + WSARecv (EV_FD_TO_WIN32_HANDLE (evpipe [0]), &buf, 1, &recvd, &flags, 0, 0); +#else + read (evpipe [0], &dummy, sizeof (dummy)); +#endif + } + } + + pipe_write_skipped = 0; + + ECB_MEMORY_FENCE; /* push out skipped, acquire flags */ + +#if EV_SIGNAL_ENABLE + if (sig_pending) + { + sig_pending = 0; + + ECB_MEMORY_FENCE; + + for (i = EV_NSIG - 1; i--; ) + if (ecb_expect_false (signals [i].pending)) + ev_feed_signal_event (EV_A_ i + 1); + } +#endif + +#if EV_ASYNC_ENABLE + if (async_pending) + { + async_pending = 0; + + ECB_MEMORY_FENCE; + + for (i = asynccnt; i--; ) + if (asyncs [i]->sent) + { + asyncs [i]->sent = 0; + ECB_MEMORY_FENCE_RELEASE; + ev_feed_event (EV_A_ asyncs [i], EV_ASYNC); + } + } +#endif +} + +/*****************************************************************************/ + +void +ev_feed_signal (int signum) EV_NOEXCEPT +{ +#if EV_MULTIPLICITY + EV_P; + ECB_MEMORY_FENCE_ACQUIRE; + EV_A = signals [signum - 1].loop; + + if (!EV_A) + return; +#endif + + signals [signum - 1].pending = 1; + evpipe_write (EV_A_ &sig_pending); +} + +static void +ev_sighandler (int signum) +{ +#ifdef _WIN32 + signal (signum, ev_sighandler); +#endif + + ev_feed_signal (signum); +} + +ecb_noinline +void +ev_feed_signal_event (EV_P_ int signum) EV_NOEXCEPT +{ + WL w; + + if (ecb_expect_false (signum <= 0 || signum >= EV_NSIG)) + return; + + --signum; + +#if EV_MULTIPLICITY + /* it is permissible to try to feed a signal to the wrong loop */ + /* or, likely more useful, feeding a signal nobody is waiting for */ + + if (ecb_expect_false (signals [signum].loop != EV_A)) + return; +#endif + + signals [signum].pending = 0; + ECB_MEMORY_FENCE_RELEASE; + + for (w = signals [signum].head; w; w = w->next) + ev_feed_event (EV_A_ (W)w, EV_SIGNAL); +} + +#if EV_USE_SIGNALFD +static void +sigfdcb (EV_P_ ev_io *iow, int revents) +{ + struct signalfd_siginfo si[2], *sip; /* these structs are big */ + + for (;;) + { + ssize_t res = read (sigfd, si, sizeof (si)); + + /* not ISO-C, as res might be -1, but works with SuS */ + for (sip = si; (char *)sip < (char *)si + res; ++sip) + ev_feed_signal_event (EV_A_ sip->ssi_signo); + + if (res < (ssize_t)sizeof (si)) + break; + } +} +#endif + +#endif + +/*****************************************************************************/ + +#if EV_CHILD_ENABLE +static WL childs [EV_PID_HASHSIZE]; + +static ev_signal childev; + +#ifndef WIFCONTINUED +# define WIFCONTINUED(status) 0 +#endif + +/* handle a single child status event */ +inline_speed void +child_reap (EV_P_ int chain, int pid, int status) +{ + ev_child *w; + int traced = WIFSTOPPED (status) || WIFCONTINUED (status); + + for (w = (ev_child *)childs [chain & ((EV_PID_HASHSIZE) - 1)]; w; w = (ev_child *)((WL)w)->next) + { + if ((w->pid == pid || !w->pid) + && (!traced || (w->flags & 1))) + { + ev_set_priority (w, EV_MAXPRI); /* need to do it *now*, this *must* be the same prio as the signal watcher itself */ + w->rpid = pid; + w->rstatus = status; + ev_feed_event (EV_A_ (W)w, EV_CHILD); + } + } +} + +#ifndef WCONTINUED +# define WCONTINUED 0 +#endif + +/* called on sigchld etc., calls waitpid */ +static void +childcb (EV_P_ ev_signal *sw, int revents) +{ + int pid, status; + + /* some systems define WCONTINUED but then fail to support it (linux 2.4) */ + if (0 >= (pid = waitpid (-1, &status, WNOHANG | WUNTRACED | WCONTINUED))) + if (!WCONTINUED + || errno != EINVAL + || 0 >= (pid = waitpid (-1, &status, WNOHANG | WUNTRACED))) + return; + + /* make sure we are called again until all children have been reaped */ + /* we need to do it this way so that the callback gets called before we continue */ + ev_feed_event (EV_A_ (W)sw, EV_SIGNAL); + + child_reap (EV_A_ pid, pid, status); + if ((EV_PID_HASHSIZE) > 1) + child_reap (EV_A_ 0, pid, status); /* this might trigger a watcher twice, but feed_event catches that */ +} + +#endif + +/*****************************************************************************/ + +#if EV_USE_TIMERFD + +static void periodics_reschedule (EV_P); + +static void +timerfdcb (EV_P_ ev_io *iow, int revents) +{ + struct itimerspec its = { 0 }; + + its.it_value.tv_sec = ev_rt_now + (int)MAX_BLOCKTIME2; + timerfd_settime (timerfd, TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &its, 0); + + ev_rt_now = ev_time (); + /* periodics_reschedule only needs ev_rt_now */ + /* but maybe in the future we want the full treatment. */ + /* + now_floor = EV_TS_CONST (0.); + time_update (EV_A_ EV_TSTAMP_HUGE); + */ +#if EV_PERIODIC_ENABLE + periodics_reschedule (EV_A); +#endif +} + +ecb_noinline ecb_cold +static void +evtimerfd_init (EV_P) +{ + if (!ev_is_active (&timerfd_w)) + { + timerfd = timerfd_create (CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC); + + if (timerfd >= 0) + { + fd_intern (timerfd); /* just to be sure */ + + ev_io_init (&timerfd_w, timerfdcb, timerfd, EV_READ); + ev_set_priority (&timerfd_w, EV_MINPRI); + ev_io_start (EV_A_ &timerfd_w); + ev_unref (EV_A); /* watcher should not keep loop alive */ + + /* (re-) arm timer */ + timerfdcb (EV_A_ 0, 0); + } + } +} + +#endif + +/*****************************************************************************/ + +#if EV_USE_IOCP +# include "ev_iocp.c" +#endif +#if EV_USE_PORT +# include "ev_port.c" +#endif +#if EV_USE_KQUEUE +# include "ev_kqueue.c" +#endif +#if EV_USE_EPOLL +# include "ev_epoll.c" +#endif +#if EV_USE_LINUXAIO +# include "ev_linuxaio.c" +#endif +#if EV_USE_IOURING +# include "ev_iouring.c" +#endif +#if EV_USE_POLL +# include "ev_poll.c" +#endif +#if EV_USE_SELECT +# include "ev_select.c" +#endif + +ecb_cold int +ev_version_major (void) EV_NOEXCEPT +{ + return EV_VERSION_MAJOR; +} + +ecb_cold int +ev_version_minor (void) EV_NOEXCEPT +{ + return EV_VERSION_MINOR; +} + +/* return true if we are running with elevated privileges and should ignore env variables */ +inline_size ecb_cold int +enable_secure (void) +{ +#ifdef _WIN32 + return 0; +#else + return getuid () != geteuid () + || getgid () != getegid (); +#endif +} + +ecb_cold +unsigned int +ev_supported_backends (void) EV_NOEXCEPT +{ + unsigned int flags = 0; + + if (EV_USE_PORT ) flags |= EVBACKEND_PORT; + if (EV_USE_KQUEUE ) flags |= EVBACKEND_KQUEUE; + if (EV_USE_EPOLL ) flags |= EVBACKEND_EPOLL; + if (EV_USE_LINUXAIO ) flags |= EVBACKEND_LINUXAIO; + if (EV_USE_IOURING && ev_linux_version () >= 0x050601) flags |= EVBACKEND_IOURING; /* 5.6.1+ */ + if (EV_USE_POLL ) flags |= EVBACKEND_POLL; + if (EV_USE_SELECT ) flags |= EVBACKEND_SELECT; + + return flags; +} + +ecb_cold +unsigned int +ev_recommended_backends (void) EV_NOEXCEPT +{ + unsigned int flags = ev_supported_backends (); + +#ifndef __NetBSD__ + /* kqueue is borked on everything but netbsd apparently */ + /* it usually doesn't work correctly on anything but sockets and pipes */ + flags &= ~EVBACKEND_KQUEUE; +#endif +#ifdef __APPLE__ + /* only select works correctly on that "unix-certified" platform */ + flags &= ~EVBACKEND_KQUEUE; /* horribly broken, even for sockets */ + flags &= ~EVBACKEND_POLL; /* poll is based on kqueue from 10.5 onwards */ +#endif +#ifdef __FreeBSD__ + flags &= ~EVBACKEND_POLL; /* poll return value is unusable (http://forums.freebsd.org/archive/index.php/t-10270.html) */ +#endif + + /* TODO: linuxaio is very experimental */ +#if !EV_RECOMMEND_LINUXAIO + flags &= ~EVBACKEND_LINUXAIO; +#endif + /* TODO: linuxaio is super experimental */ +#if !EV_RECOMMEND_IOURING + flags &= ~EVBACKEND_IOURING; +#endif + + return flags; +} + +ecb_cold +unsigned int +ev_embeddable_backends (void) EV_NOEXCEPT +{ + int flags = EVBACKEND_EPOLL | EVBACKEND_KQUEUE | EVBACKEND_PORT | EVBACKEND_IOURING; + + /* epoll embeddability broken on all linux versions up to at least 2.6.23 */ + if (ev_linux_version () < 0x020620) /* disable it on linux < 2.6.32 */ + flags &= ~EVBACKEND_EPOLL; + + /* EVBACKEND_LINUXAIO is theoretically embeddable, but suffers from a performance overhead */ + + return flags; +} + +unsigned int +ev_backend (EV_P) EV_NOEXCEPT +{ + return backend; +} + +#if EV_FEATURE_API +unsigned int +ev_iteration (EV_P) EV_NOEXCEPT +{ + return loop_count; +} + +unsigned int +ev_depth (EV_P) EV_NOEXCEPT +{ + return loop_depth; +} + +void +ev_set_io_collect_interval (EV_P_ ev_tstamp interval) EV_NOEXCEPT +{ + io_blocktime = interval; +} + +void +ev_set_timeout_collect_interval (EV_P_ ev_tstamp interval) EV_NOEXCEPT +{ + timeout_blocktime = interval; +} + +void +ev_set_userdata (EV_P_ void *data) EV_NOEXCEPT +{ + userdata = data; +} + +void * +ev_userdata (EV_P) EV_NOEXCEPT +{ + return userdata; +} + +void +ev_set_invoke_pending_cb (EV_P_ ev_loop_callback invoke_pending_cb) EV_NOEXCEPT +{ + invoke_cb = invoke_pending_cb; +} + +void +ev_set_loop_release_cb (EV_P_ void (*release)(EV_P) EV_NOEXCEPT, void (*acquire)(EV_P) EV_NOEXCEPT) EV_NOEXCEPT +{ + release_cb = release; + acquire_cb = acquire; +} +#endif + +/* initialise a loop structure, must be zero-initialised */ +ecb_noinline ecb_cold +static void +loop_init (EV_P_ unsigned int flags) EV_NOEXCEPT +{ + if (!backend) + { + origflags = flags; + +#if EV_USE_REALTIME + if (!have_realtime) + { + struct timespec ts; + + if (!clock_gettime (CLOCK_REALTIME, &ts)) + have_realtime = 1; + } +#endif + +#if EV_USE_MONOTONIC + if (!have_monotonic) + { + struct timespec ts; + + if (!clock_gettime (CLOCK_MONOTONIC, &ts)) + have_monotonic = 1; + } +#endif + + /* pid check not overridable via env */ +#ifndef _WIN32 + if (flags & EVFLAG_FORKCHECK) + curpid = getpid (); +#endif + + if (!(flags & EVFLAG_NOENV) + && !enable_secure () + && getenv ("LIBEV_FLAGS")) + flags = atoi (getenv ("LIBEV_FLAGS")); + + ev_rt_now = ev_time (); + mn_now = get_clock (); + now_floor = mn_now; + rtmn_diff = ev_rt_now - mn_now; +#if EV_FEATURE_API + invoke_cb = ev_invoke_pending; +#endif + + io_blocktime = 0.; + timeout_blocktime = 0.; + backend = 0; + backend_fd = -1; + sig_pending = 0; +#if EV_ASYNC_ENABLE + async_pending = 0; +#endif + pipe_write_skipped = 0; + pipe_write_wanted = 0; + evpipe [0] = -1; + evpipe [1] = -1; +#if EV_USE_INOTIFY + fs_fd = flags & EVFLAG_NOINOTIFY ? -1 : -2; +#endif +#if EV_USE_SIGNALFD + sigfd = flags & EVFLAG_SIGNALFD ? -2 : -1; +#endif +#if EV_USE_TIMERFD + timerfd = flags & EVFLAG_NOTIMERFD ? -1 : -2; +#endif + + if (!(flags & EVBACKEND_MASK)) + flags |= ev_recommended_backends (); + +#if EV_USE_IOCP + if (!backend && (flags & EVBACKEND_IOCP )) backend = iocp_init (EV_A_ flags); +#endif +#if EV_USE_PORT + if (!backend && (flags & EVBACKEND_PORT )) backend = port_init (EV_A_ flags); +#endif +#if EV_USE_KQUEUE + if (!backend && (flags & EVBACKEND_KQUEUE )) backend = kqueue_init (EV_A_ flags); +#endif +#if EV_USE_IOURING + if (!backend && (flags & EVBACKEND_IOURING )) backend = iouring_init (EV_A_ flags); +#endif +#if EV_USE_LINUXAIO + if (!backend && (flags & EVBACKEND_LINUXAIO)) backend = linuxaio_init (EV_A_ flags); +#endif +#if EV_USE_EPOLL + if (!backend && (flags & EVBACKEND_EPOLL )) backend = epoll_init (EV_A_ flags); +#endif +#if EV_USE_POLL + if (!backend && (flags & EVBACKEND_POLL )) backend = poll_init (EV_A_ flags); +#endif +#if EV_USE_SELECT + if (!backend && (flags & EVBACKEND_SELECT )) backend = select_init (EV_A_ flags); +#endif + + ev_prepare_init (&pending_w, pendingcb); + +#if EV_SIGNAL_ENABLE || EV_ASYNC_ENABLE + ev_init (&pipe_w, pipecb); + ev_set_priority (&pipe_w, EV_MAXPRI); +#endif + } +} + +/* free up a loop structure */ +ecb_cold +void +ev_loop_destroy (EV_P) +{ + int i; + +#if EV_MULTIPLICITY + /* mimic free (0) */ + if (!EV_A) + return; +#endif + +#if EV_CLEANUP_ENABLE + /* queue cleanup watchers (and execute them) */ + if (ecb_expect_false (cleanupcnt)) + { + queue_events (EV_A_ (W *)cleanups, cleanupcnt, EV_CLEANUP); + EV_INVOKE_PENDING; + } +#endif + +#if EV_CHILD_ENABLE + if (ev_is_default_loop (EV_A) && ev_is_active (&childev)) + { + ev_ref (EV_A); /* child watcher */ + ev_signal_stop (EV_A_ &childev); + } +#endif + + if (ev_is_active (&pipe_w)) + { + /*ev_ref (EV_A);*/ + /*ev_io_stop (EV_A_ &pipe_w);*/ + + if (evpipe [0] >= 0) EV_WIN32_CLOSE_FD (evpipe [0]); + if (evpipe [1] >= 0) EV_WIN32_CLOSE_FD (evpipe [1]); + } + +#if EV_USE_SIGNALFD + if (ev_is_active (&sigfd_w)) + close (sigfd); +#endif + +#if EV_USE_TIMERFD + if (ev_is_active (&timerfd_w)) + close (timerfd); +#endif + +#if EV_USE_INOTIFY + if (fs_fd >= 0) + close (fs_fd); +#endif + + if (backend_fd >= 0) + close (backend_fd); + +#if EV_USE_IOCP + if (backend == EVBACKEND_IOCP ) iocp_destroy (EV_A); +#endif +#if EV_USE_PORT + if (backend == EVBACKEND_PORT ) port_destroy (EV_A); +#endif +#if EV_USE_KQUEUE + if (backend == EVBACKEND_KQUEUE ) kqueue_destroy (EV_A); +#endif +#if EV_USE_IOURING + if (backend == EVBACKEND_IOURING ) iouring_destroy (EV_A); +#endif +#if EV_USE_LINUXAIO + if (backend == EVBACKEND_LINUXAIO) linuxaio_destroy (EV_A); +#endif +#if EV_USE_EPOLL + if (backend == EVBACKEND_EPOLL ) epoll_destroy (EV_A); +#endif +#if EV_USE_POLL + if (backend == EVBACKEND_POLL ) poll_destroy (EV_A); +#endif +#if EV_USE_SELECT + if (backend == EVBACKEND_SELECT ) select_destroy (EV_A); +#endif + + for (i = NUMPRI; i--; ) + { + array_free (pending, [i]); +#if EV_IDLE_ENABLE + array_free (idle, [i]); +#endif + } + + ev_free (anfds); anfds = 0; anfdmax = 0; + + /* have to use the microsoft-never-gets-it-right macro */ + array_free (rfeed, EMPTY); + array_free (fdchange, EMPTY); + array_free (timer, EMPTY); +#if EV_PERIODIC_ENABLE + array_free (periodic, EMPTY); +#endif +#if EV_FORK_ENABLE + array_free (fork, EMPTY); +#endif +#if EV_CLEANUP_ENABLE + array_free (cleanup, EMPTY); +#endif + array_free (prepare, EMPTY); + array_free (check, EMPTY); +#if EV_ASYNC_ENABLE + array_free (async, EMPTY); +#endif + + backend = 0; + +#if EV_MULTIPLICITY + if (ev_is_default_loop (EV_A)) +#endif + ev_default_loop_ptr = 0; +#if EV_MULTIPLICITY + else + ev_free (EV_A); +#endif +} + +#if EV_USE_INOTIFY +inline_size void infy_fork (EV_P); +#endif + +inline_size void +loop_fork (EV_P) +{ +#if EV_USE_PORT + if (backend == EVBACKEND_PORT ) port_fork (EV_A); +#endif +#if EV_USE_KQUEUE + if (backend == EVBACKEND_KQUEUE ) kqueue_fork (EV_A); +#endif +#if EV_USE_IOURING + if (backend == EVBACKEND_IOURING ) iouring_fork (EV_A); +#endif +#if EV_USE_LINUXAIO + if (backend == EVBACKEND_LINUXAIO) linuxaio_fork (EV_A); +#endif +#if EV_USE_EPOLL + if (backend == EVBACKEND_EPOLL ) epoll_fork (EV_A); +#endif +#if EV_USE_INOTIFY + infy_fork (EV_A); +#endif + + if (postfork != 2) + { + #if EV_USE_SIGNALFD + /* surprisingly, nothing needs to be done for signalfd, accoridng to docs, it does the right thing on fork */ + #endif + + #if EV_USE_TIMERFD + if (ev_is_active (&timerfd_w)) + { + ev_ref (EV_A); + ev_io_stop (EV_A_ &timerfd_w); + + close (timerfd); + timerfd = -2; + + evtimerfd_init (EV_A); + /* reschedule periodics, in case we missed something */ + ev_feed_event (EV_A_ &timerfd_w, EV_CUSTOM); + } + #endif + + #if EV_SIGNAL_ENABLE || EV_ASYNC_ENABLE + if (ev_is_active (&pipe_w)) + { + /* pipe_write_wanted must be false now, so modifying fd vars should be safe */ + + ev_ref (EV_A); + ev_io_stop (EV_A_ &pipe_w); + + if (evpipe [0] >= 0) + EV_WIN32_CLOSE_FD (evpipe [0]); + + evpipe_init (EV_A); + /* iterate over everything, in case we missed something before */ + ev_feed_event (EV_A_ &pipe_w, EV_CUSTOM); + } + #endif + } + + postfork = 0; +} + +#if EV_MULTIPLICITY + +ecb_cold +struct ev_loop * +ev_loop_new (unsigned int flags) EV_NOEXCEPT +{ + EV_P = (struct ev_loop *)ev_malloc (sizeof (struct ev_loop)); + + memset (EV_A, 0, sizeof (struct ev_loop)); + loop_init (EV_A_ flags); + + if (ev_backend (EV_A)) + return EV_A; + + ev_free (EV_A); + return 0; +} + +#endif /* multiplicity */ + +#if EV_VERIFY +ecb_noinline ecb_cold +static void +verify_watcher (EV_P_ W w) +{ + assert (("libev: watcher has invalid priority", ABSPRI (w) >= 0 && ABSPRI (w) < NUMPRI)); + + if (w->pending) + assert (("libev: pending watcher not on pending queue", pendings [ABSPRI (w)][w->pending - 1].w == w)); +} + +ecb_noinline ecb_cold +static void +verify_heap (EV_P_ ANHE *heap, int N) +{ + int i; + + for (i = HEAP0; i < N + HEAP0; ++i) + { + assert (("libev: active index mismatch in heap", ev_active (ANHE_w (heap [i])) == i)); + assert (("libev: heap condition violated", i == HEAP0 || ANHE_at (heap [HPARENT (i)]) <= ANHE_at (heap [i]))); + assert (("libev: heap at cache mismatch", ANHE_at (heap [i]) == ev_at (ANHE_w (heap [i])))); + + verify_watcher (EV_A_ (W)ANHE_w (heap [i])); + } +} + +ecb_noinline ecb_cold +static void +array_verify (EV_P_ W *ws, int cnt) +{ + while (cnt--) + { + assert (("libev: active index mismatch", ev_active (ws [cnt]) == cnt + 1)); + verify_watcher (EV_A_ ws [cnt]); + } +} +#endif + +#if EV_FEATURE_API +void ecb_cold +ev_verify (EV_P) EV_NOEXCEPT +{ +#if EV_VERIFY + int i; + WL w, w2; + + assert (activecnt >= -1); + + assert (fdchangemax >= fdchangecnt); + for (i = 0; i < fdchangecnt; ++i) + assert (("libev: negative fd in fdchanges", fdchanges [i] >= 0)); + + assert (anfdmax >= 0); + for (i = 0; i < anfdmax; ++i) + { + int j = 0; + + for (w = w2 = anfds [i].head; w; w = w->next) + { + verify_watcher (EV_A_ (W)w); + + if (j++ & 1) + { + assert (("libev: io watcher list contains a loop", w != w2)); + w2 = w2->next; + } + + assert (("libev: inactive fd watcher on anfd list", ev_active (w) == 1)); + assert (("libev: fd mismatch between watcher and anfd", ((ev_io *)w)->fd == i)); + } + } + + assert (timermax >= timercnt); + verify_heap (EV_A_ timers, timercnt); + +#if EV_PERIODIC_ENABLE + assert (periodicmax >= periodiccnt); + verify_heap (EV_A_ periodics, periodiccnt); +#endif + + for (i = NUMPRI; i--; ) + { + assert (pendingmax [i] >= pendingcnt [i]); +#if EV_IDLE_ENABLE + assert (idleall >= 0); + assert (idlemax [i] >= idlecnt [i]); + array_verify (EV_A_ (W *)idles [i], idlecnt [i]); +#endif + } + +#if EV_FORK_ENABLE + assert (forkmax >= forkcnt); + array_verify (EV_A_ (W *)forks, forkcnt); +#endif + +#if EV_CLEANUP_ENABLE + assert (cleanupmax >= cleanupcnt); + array_verify (EV_A_ (W *)cleanups, cleanupcnt); +#endif + +#if EV_ASYNC_ENABLE + assert (asyncmax >= asynccnt); + array_verify (EV_A_ (W *)asyncs, asynccnt); +#endif + +#if EV_PREPARE_ENABLE + assert (preparemax >= preparecnt); + array_verify (EV_A_ (W *)prepares, preparecnt); +#endif + +#if EV_CHECK_ENABLE + assert (checkmax >= checkcnt); + array_verify (EV_A_ (W *)checks, checkcnt); +#endif + +# if 0 +#if EV_CHILD_ENABLE + for (w = (ev_child *)childs [chain & ((EV_PID_HASHSIZE) - 1)]; w; w = (ev_child *)((WL)w)->next) + for (signum = EV_NSIG; signum--; ) if (signals [signum].pending) +#endif +# endif +#endif +} +#endif + +#if EV_MULTIPLICITY +ecb_cold +struct ev_loop * +#else +int +#endif +ev_default_loop (unsigned int flags) EV_NOEXCEPT +{ + if (!ev_default_loop_ptr) + { +#if EV_MULTIPLICITY + EV_P = ev_default_loop_ptr = &default_loop_struct; +#else + ev_default_loop_ptr = 1; +#endif + + loop_init (EV_A_ flags); + + if (ev_backend (EV_A)) + { +#if EV_CHILD_ENABLE + ev_signal_init (&childev, childcb, SIGCHLD); + ev_set_priority (&childev, EV_MAXPRI); + ev_signal_start (EV_A_ &childev); + ev_unref (EV_A); /* child watcher should not keep loop alive */ +#endif + } + else + ev_default_loop_ptr = 0; + } + + return ev_default_loop_ptr; +} + +void +ev_loop_fork (EV_P) EV_NOEXCEPT +{ + postfork = 1; +} + +/*****************************************************************************/ + +void +ev_invoke (EV_P_ void *w, int revents) +{ + EV_CB_INVOKE ((W)w, revents); +} + +unsigned int +ev_pending_count (EV_P) EV_NOEXCEPT +{ + int pri; + unsigned int count = 0; + + for (pri = NUMPRI; pri--; ) + count += pendingcnt [pri]; + + return count; +} + +ecb_noinline +void +ev_invoke_pending (EV_P) +{ + pendingpri = NUMPRI; + + do + { + --pendingpri; + + /* pendingpri possibly gets modified in the inner loop */ + while (pendingcnt [pendingpri]) + { + ANPENDING *p = pendings [pendingpri] + --pendingcnt [pendingpri]; + + p->w->pending = 0; + EV_CB_INVOKE (p->w, p->events); + EV_FREQUENT_CHECK; + } + } + while (pendingpri); +} + +#if EV_IDLE_ENABLE +/* make idle watchers pending. this handles the "call-idle */ +/* only when higher priorities are idle" logic */ +inline_size void +idle_reify (EV_P) +{ + if (ecb_expect_false (idleall)) + { + int pri; + + for (pri = NUMPRI; pri--; ) + { + if (pendingcnt [pri]) + break; + + if (idlecnt [pri]) + { + queue_events (EV_A_ (W *)idles [pri], idlecnt [pri], EV_IDLE); + break; + } + } + } +} +#endif + +/* make timers pending */ +inline_size void +timers_reify (EV_P) +{ + EV_FREQUENT_CHECK; + + if (timercnt && ANHE_at (timers [HEAP0]) < mn_now) + { + do + { + ev_timer *w = (ev_timer *)ANHE_w (timers [HEAP0]); + + /*assert (("libev: inactive timer on timer heap detected", ev_is_active (w)));*/ + + /* first reschedule or stop timer */ + if (w->repeat) + { + ev_at (w) += w->repeat; + if (ev_at (w) < mn_now) + ev_at (w) = mn_now; + + assert (("libev: negative ev_timer repeat value found while processing timers", w->repeat > EV_TS_CONST (0.))); + + ANHE_at_cache (timers [HEAP0]); + downheap (timers, timercnt, HEAP0); + } + else + ev_timer_stop (EV_A_ w); /* nonrepeating: stop timer */ + + EV_FREQUENT_CHECK; + feed_reverse (EV_A_ (W)w); + } + while (timercnt && ANHE_at (timers [HEAP0]) < mn_now); + + feed_reverse_done (EV_A_ EV_TIMER); + } +} + +#if EV_PERIODIC_ENABLE + +ecb_noinline +static void +periodic_recalc (EV_P_ ev_periodic *w) +{ + ev_tstamp interval = w->interval > MIN_INTERVAL ? w->interval : MIN_INTERVAL; + ev_tstamp at = w->offset + interval * ev_floor ((ev_rt_now - w->offset) / interval); + + /* the above almost always errs on the low side */ + while (at <= ev_rt_now) + { + ev_tstamp nat = at + w->interval; + + /* when resolution fails us, we use ev_rt_now */ + if (ecb_expect_false (nat == at)) + { + at = ev_rt_now; + break; + } + + at = nat; + } + + ev_at (w) = at; +} + +/* make periodics pending */ +inline_size void +periodics_reify (EV_P) +{ + EV_FREQUENT_CHECK; + + while (periodiccnt && ANHE_at (periodics [HEAP0]) < ev_rt_now) + { + do + { + ev_periodic *w = (ev_periodic *)ANHE_w (periodics [HEAP0]); + + /*assert (("libev: inactive timer on periodic heap detected", ev_is_active (w)));*/ + + /* first reschedule or stop timer */ + if (w->reschedule_cb) + { + ev_at (w) = w->reschedule_cb (w, ev_rt_now); + + assert (("libev: ev_periodic reschedule callback returned time in the past", ev_at (w) >= ev_rt_now)); + + ANHE_at_cache (periodics [HEAP0]); + downheap (periodics, periodiccnt, HEAP0); + } + else if (w->interval) + { + periodic_recalc (EV_A_ w); + ANHE_at_cache (periodics [HEAP0]); + downheap (periodics, periodiccnt, HEAP0); + } + else + ev_periodic_stop (EV_A_ w); /* nonrepeating: stop timer */ + + EV_FREQUENT_CHECK; + feed_reverse (EV_A_ (W)w); + } + while (periodiccnt && ANHE_at (periodics [HEAP0]) < ev_rt_now); + + feed_reverse_done (EV_A_ EV_PERIODIC); + } +} + +/* simply recalculate all periodics */ +/* TODO: maybe ensure that at least one event happens when jumping forward? */ +ecb_noinline ecb_cold +static void +periodics_reschedule (EV_P) +{ + int i; + + /* adjust periodics after time jump */ + for (i = HEAP0; i < periodiccnt + HEAP0; ++i) + { + ev_periodic *w = (ev_periodic *)ANHE_w (periodics [i]); + + if (w->reschedule_cb) + ev_at (w) = w->reschedule_cb (w, ev_rt_now); + else if (w->interval) + periodic_recalc (EV_A_ w); + + ANHE_at_cache (periodics [i]); + } + + reheap (periodics, periodiccnt); +} +#endif + +/* adjust all timers by a given offset */ +ecb_noinline ecb_cold +static void +timers_reschedule (EV_P_ ev_tstamp adjust) +{ + int i; + + for (i = 0; i < timercnt; ++i) + { + ANHE *he = timers + i + HEAP0; + ANHE_w (*he)->at += adjust; + ANHE_at_cache (*he); + } +} + +/* fetch new monotonic and realtime times from the kernel */ +/* also detect if there was a timejump, and act accordingly */ +inline_speed void +time_update (EV_P_ ev_tstamp max_block) +{ +#if EV_USE_MONOTONIC + if (ecb_expect_true (have_monotonic)) + { + int i; + ev_tstamp odiff = rtmn_diff; + + mn_now = get_clock (); + + /* only fetch the realtime clock every 0.5*MIN_TIMEJUMP seconds */ + /* interpolate in the meantime */ + if (ecb_expect_true (mn_now - now_floor < EV_TS_CONST (MIN_TIMEJUMP * .5))) + { + ev_rt_now = rtmn_diff + mn_now; + return; + } + + now_floor = mn_now; + ev_rt_now = ev_time (); + + /* loop a few times, before making important decisions. + * on the choice of "4": one iteration isn't enough, + * in case we get preempted during the calls to + * ev_time and get_clock. a second call is almost guaranteed + * to succeed in that case, though. and looping a few more times + * doesn't hurt either as we only do this on time-jumps or + * in the unlikely event of having been preempted here. + */ + for (i = 4; --i; ) + { + ev_tstamp diff; + rtmn_diff = ev_rt_now - mn_now; + + diff = odiff - rtmn_diff; + + if (ecb_expect_true ((diff < EV_TS_CONST (0.) ? -diff : diff) < EV_TS_CONST (MIN_TIMEJUMP))) + return; /* all is well */ + + ev_rt_now = ev_time (); + mn_now = get_clock (); + now_floor = mn_now; + } + + /* no timer adjustment, as the monotonic clock doesn't jump */ + /* timers_reschedule (EV_A_ rtmn_diff - odiff) */ +# if EV_PERIODIC_ENABLE + periodics_reschedule (EV_A); +# endif + } + else +#endif + { + ev_rt_now = ev_time (); + + if (ecb_expect_false (mn_now > ev_rt_now || ev_rt_now > mn_now + max_block + EV_TS_CONST (MIN_TIMEJUMP))) + { + /* adjust timers. this is easy, as the offset is the same for all of them */ + timers_reschedule (EV_A_ ev_rt_now - mn_now); +#if EV_PERIODIC_ENABLE + periodics_reschedule (EV_A); +#endif + } + + mn_now = ev_rt_now; + } +} + +int +ev_run (EV_P_ int flags) +{ +#if EV_FEATURE_API + ++loop_depth; +#endif + + assert (("libev: ev_loop recursion during release detected", loop_done != EVBREAK_RECURSE)); + + loop_done = EVBREAK_CANCEL; + + EV_INVOKE_PENDING; /* in case we recurse, ensure ordering stays nice and clean */ + + do + { +#if EV_VERIFY >= 2 + ev_verify (EV_A); +#endif + +#ifndef _WIN32 + if (ecb_expect_false (curpid)) /* penalise the forking check even more */ + if (ecb_expect_false (getpid () != curpid)) + { + curpid = getpid (); + postfork = 1; + } +#endif + +#if EV_FORK_ENABLE + /* we might have forked, so queue fork handlers */ + if (ecb_expect_false (postfork)) + if (forkcnt) + { + queue_events (EV_A_ (W *)forks, forkcnt, EV_FORK); + EV_INVOKE_PENDING; + } +#endif + +#if EV_PREPARE_ENABLE + /* queue prepare watchers (and execute them) */ + if (ecb_expect_false (preparecnt)) + { + queue_events (EV_A_ (W *)prepares, preparecnt, EV_PREPARE); + EV_INVOKE_PENDING; + } +#endif + + if (ecb_expect_false (loop_done)) + break; + + /* we might have forked, so reify kernel state if necessary */ + if (ecb_expect_false (postfork)) + loop_fork (EV_A); + + /* update fd-related kernel structures */ + fd_reify (EV_A); + + /* calculate blocking time */ + { + ev_tstamp waittime = 0.; + ev_tstamp sleeptime = 0.; + + /* remember old timestamp for io_blocktime calculation */ + ev_tstamp prev_mn_now = mn_now; + + /* update time to cancel out callback processing overhead */ + time_update (EV_A_ EV_TS_CONST (EV_TSTAMP_HUGE)); + + /* from now on, we want a pipe-wake-up */ + pipe_write_wanted = 1; + + ECB_MEMORY_FENCE; /* make sure pipe_write_wanted is visible before we check for potential skips */ + + if (ecb_expect_true (!(flags & EVRUN_NOWAIT || idleall || !activecnt || pipe_write_skipped))) + { + waittime = EV_TS_CONST (MAX_BLOCKTIME); + +#if EV_USE_TIMERFD + /* sleep a lot longer when we can reliably detect timejumps */ + if (ecb_expect_true (timerfd >= 0)) + waittime = EV_TS_CONST (MAX_BLOCKTIME2); +#endif +#if !EV_PERIODIC_ENABLE + /* without periodics but with monotonic clock there is no need */ + /* for any time jump detection, so sleep longer */ + if (ecb_expect_true (have_monotonic)) + waittime = EV_TS_CONST (MAX_BLOCKTIME2); +#endif + + if (timercnt) + { + ev_tstamp to = ANHE_at (timers [HEAP0]) - mn_now; + if (waittime > to) waittime = to; + } + +#if EV_PERIODIC_ENABLE + if (periodiccnt) + { + ev_tstamp to = ANHE_at (periodics [HEAP0]) - ev_rt_now; + if (waittime > to) waittime = to; + } +#endif + + /* don't let timeouts decrease the waittime below timeout_blocktime */ + if (ecb_expect_false (waittime < timeout_blocktime)) + waittime = timeout_blocktime; + + /* now there are two more special cases left, either we have + * already-expired timers, so we should not sleep, or we have timers + * that expire very soon, in which case we need to wait for a minimum + * amount of time for some event loop backends. + */ + if (ecb_expect_false (waittime < backend_mintime)) + waittime = waittime <= EV_TS_CONST (0.) + ? EV_TS_CONST (0.) + : backend_mintime; + + /* extra check because io_blocktime is commonly 0 */ + if (ecb_expect_false (io_blocktime)) + { + sleeptime = io_blocktime - (mn_now - prev_mn_now); + + if (sleeptime > waittime - backend_mintime) + sleeptime = waittime - backend_mintime; + + if (ecb_expect_true (sleeptime > EV_TS_CONST (0.))) + { + ev_sleep (sleeptime); + waittime -= sleeptime; + } + } + } + +#if EV_FEATURE_API + ++loop_count; +#endif + assert ((loop_done = EVBREAK_RECURSE, 1)); /* assert for side effect */ + backend_poll (EV_A_ waittime); + assert ((loop_done = EVBREAK_CANCEL, 1)); /* assert for side effect */ + + pipe_write_wanted = 0; /* just an optimisation, no fence needed */ + + ECB_MEMORY_FENCE_ACQUIRE; + if (pipe_write_skipped) + { + assert (("libev: pipe_w not active, but pipe not written", ev_is_active (&pipe_w))); + ev_feed_event (EV_A_ &pipe_w, EV_CUSTOM); + } + + /* update ev_rt_now, do magic */ + time_update (EV_A_ waittime + sleeptime); + } + + /* queue pending timers and reschedule them */ + timers_reify (EV_A); /* relative timers called last */ +#if EV_PERIODIC_ENABLE + periodics_reify (EV_A); /* absolute timers called first */ +#endif + +#if EV_IDLE_ENABLE + /* queue idle watchers unless other events are pending */ + idle_reify (EV_A); +#endif + +#if EV_CHECK_ENABLE + /* queue check watchers, to be executed first */ + if (ecb_expect_false (checkcnt)) + queue_events (EV_A_ (W *)checks, checkcnt, EV_CHECK); +#endif + + EV_INVOKE_PENDING; + } + while (ecb_expect_true ( + activecnt + && !loop_done + && !(flags & (EVRUN_ONCE | EVRUN_NOWAIT)) + )); + + if (loop_done == EVBREAK_ONE) + loop_done = EVBREAK_CANCEL; + +#if EV_FEATURE_API + --loop_depth; +#endif + + return activecnt; +} + +void +ev_break (EV_P_ int how) EV_NOEXCEPT +{ + loop_done = how; +} + +void +ev_ref (EV_P) EV_NOEXCEPT +{ + ++activecnt; +} + +void +ev_unref (EV_P) EV_NOEXCEPT +{ + --activecnt; +} + +void +ev_now_update (EV_P) EV_NOEXCEPT +{ + time_update (EV_A_ EV_TSTAMP_HUGE); +} + +void +ev_suspend (EV_P) EV_NOEXCEPT +{ + ev_now_update (EV_A); +} + +void +ev_resume (EV_P) EV_NOEXCEPT +{ + ev_tstamp mn_prev = mn_now; + + ev_now_update (EV_A); + timers_reschedule (EV_A_ mn_now - mn_prev); +#if EV_PERIODIC_ENABLE + /* TODO: really do this? */ + periodics_reschedule (EV_A); +#endif +} + +/*****************************************************************************/ +/* singly-linked list management, used when the expected list length is short */ + +inline_size void +wlist_add (WL *head, WL elem) +{ + elem->next = *head; + *head = elem; +} + +inline_size void +wlist_del (WL *head, WL elem) +{ + while (*head) + { + if (ecb_expect_true (*head == elem)) + { + *head = elem->next; + break; + } + + head = &(*head)->next; + } +} + +/* internal, faster, version of ev_clear_pending */ +inline_speed void +clear_pending (EV_P_ W w) +{ + if (w->pending) + { + pendings [ABSPRI (w)][w->pending - 1].w = (W)&pending_w; + w->pending = 0; + } +} + +int +ev_clear_pending (EV_P_ void *w) EV_NOEXCEPT +{ + W w_ = (W)w; + int pending = w_->pending; + + if (ecb_expect_true (pending)) + { + ANPENDING *p = pendings [ABSPRI (w_)] + pending - 1; + p->w = (W)&pending_w; + w_->pending = 0; + return p->events; + } + else + return 0; +} + +inline_size void +pri_adjust (EV_P_ W w) +{ + int pri = ev_priority (w); + pri = pri < EV_MINPRI ? EV_MINPRI : pri; + pri = pri > EV_MAXPRI ? EV_MAXPRI : pri; + ev_set_priority (w, pri); +} + +inline_speed void +ev_start (EV_P_ W w, int active) +{ + pri_adjust (EV_A_ w); + w->active = active; + ev_ref (EV_A); +} + +inline_size void +ev_stop (EV_P_ W w) +{ + ev_unref (EV_A); + w->active = 0; +} + +/*****************************************************************************/ + +ecb_noinline +void +ev_io_start (EV_P_ ev_io *w) EV_NOEXCEPT +{ + int fd = w->fd; + + if (ecb_expect_false (ev_is_active (w))) + return; + + assert (("libev: ev_io_start called with negative fd", fd >= 0)); + assert (("libev: ev_io_start called with illegal event mask", !(w->events & ~(EV__IOFDSET | EV_READ | EV_WRITE)))); + +#if EV_VERIFY >= 2 + assert (("libev: ev_io_start called on watcher with invalid fd", fd_valid (fd))); +#endif + EV_FREQUENT_CHECK; + + ev_start (EV_A_ (W)w, 1); + array_needsize (ANFD, anfds, anfdmax, fd + 1, array_needsize_zerofill); + wlist_add (&anfds[fd].head, (WL)w); + + /* common bug, apparently */ + assert (("libev: ev_io_start called with corrupted watcher", ((WL)w)->next != (WL)w)); + + fd_change (EV_A_ fd, w->events & EV__IOFDSET | EV_ANFD_REIFY); + w->events &= ~EV__IOFDSET; + + EV_FREQUENT_CHECK; +} + +ecb_noinline +void +ev_io_stop (EV_P_ ev_io *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + assert (("libev: ev_io_stop called with illegal fd (must stay constant after start!)", w->fd >= 0 && w->fd < anfdmax)); + +#if EV_VERIFY >= 2 + assert (("libev: ev_io_stop called on watcher with invalid fd", fd_valid (w->fd))); +#endif + EV_FREQUENT_CHECK; + + wlist_del (&anfds[w->fd].head, (WL)w); + ev_stop (EV_A_ (W)w); + + fd_change (EV_A_ w->fd, EV_ANFD_REIFY); + + EV_FREQUENT_CHECK; +} + +ecb_noinline +void +ev_timer_start (EV_P_ ev_timer *w) EV_NOEXCEPT +{ + if (ecb_expect_false (ev_is_active (w))) + return; + + ev_at (w) += mn_now; + + assert (("libev: ev_timer_start called with negative timer repeat value", w->repeat >= 0.)); + + EV_FREQUENT_CHECK; + + ++timercnt; + ev_start (EV_A_ (W)w, timercnt + HEAP0 - 1); + array_needsize (ANHE, timers, timermax, ev_active (w) + 1, array_needsize_noinit); + ANHE_w (timers [ev_active (w)]) = (WT)w; + ANHE_at_cache (timers [ev_active (w)]); + upheap (timers, ev_active (w)); + + EV_FREQUENT_CHECK; + + /*assert (("libev: internal timer heap corruption", timers [ev_active (w)] == (WT)w));*/ +} + +ecb_noinline +void +ev_timer_stop (EV_P_ ev_timer *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + { + int active = ev_active (w); + + assert (("libev: internal timer heap corruption", ANHE_w (timers [active]) == (WT)w)); + + --timercnt; + + if (ecb_expect_true (active < timercnt + HEAP0)) + { + timers [active] = timers [timercnt + HEAP0]; + adjustheap (timers, timercnt, active); + } + } + + ev_at (w) -= mn_now; + + ev_stop (EV_A_ (W)w); + + EV_FREQUENT_CHECK; +} + +ecb_noinline +void +ev_timer_again (EV_P_ ev_timer *w) EV_NOEXCEPT +{ + EV_FREQUENT_CHECK; + + clear_pending (EV_A_ (W)w); + + if (ev_is_active (w)) + { + if (w->repeat) + { + ev_at (w) = mn_now + w->repeat; + ANHE_at_cache (timers [ev_active (w)]); + adjustheap (timers, timercnt, ev_active (w)); + } + else + ev_timer_stop (EV_A_ w); + } + else if (w->repeat) + { + ev_at (w) = w->repeat; + ev_timer_start (EV_A_ w); + } + + EV_FREQUENT_CHECK; +} + +ev_tstamp +ev_timer_remaining (EV_P_ ev_timer *w) EV_NOEXCEPT +{ + return ev_at (w) - (ev_is_active (w) ? mn_now : EV_TS_CONST (0.)); +} + +#if EV_PERIODIC_ENABLE +ecb_noinline +void +ev_periodic_start (EV_P_ ev_periodic *w) EV_NOEXCEPT +{ + if (ecb_expect_false (ev_is_active (w))) + return; + +#if EV_USE_TIMERFD + if (timerfd == -2) + evtimerfd_init (EV_A); +#endif + + if (w->reschedule_cb) + ev_at (w) = w->reschedule_cb (w, ev_rt_now); + else if (w->interval) + { + assert (("libev: ev_periodic_start called with negative interval value", w->interval >= 0.)); + periodic_recalc (EV_A_ w); + } + else + ev_at (w) = w->offset; + + EV_FREQUENT_CHECK; + + ++periodiccnt; + ev_start (EV_A_ (W)w, periodiccnt + HEAP0 - 1); + array_needsize (ANHE, periodics, periodicmax, ev_active (w) + 1, array_needsize_noinit); + ANHE_w (periodics [ev_active (w)]) = (WT)w; + ANHE_at_cache (periodics [ev_active (w)]); + upheap (periodics, ev_active (w)); + + EV_FREQUENT_CHECK; + + /*assert (("libev: internal periodic heap corruption", ANHE_w (periodics [ev_active (w)]) == (WT)w));*/ +} + +ecb_noinline +void +ev_periodic_stop (EV_P_ ev_periodic *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + { + int active = ev_active (w); + + assert (("libev: internal periodic heap corruption", ANHE_w (periodics [active]) == (WT)w)); + + --periodiccnt; + + if (ecb_expect_true (active < periodiccnt + HEAP0)) + { + periodics [active] = periodics [periodiccnt + HEAP0]; + adjustheap (periodics, periodiccnt, active); + } + } + + ev_stop (EV_A_ (W)w); + + EV_FREQUENT_CHECK; +} + +ecb_noinline +void +ev_periodic_again (EV_P_ ev_periodic *w) EV_NOEXCEPT +{ + /* TODO: use adjustheap and recalculation */ + ev_periodic_stop (EV_A_ w); + ev_periodic_start (EV_A_ w); +} +#endif + +#ifndef SA_RESTART +# define SA_RESTART 0 +#endif + +#if EV_SIGNAL_ENABLE + +ecb_noinline +void +ev_signal_start (EV_P_ ev_signal *w) EV_NOEXCEPT +{ + if (ecb_expect_false (ev_is_active (w))) + return; + + assert (("libev: ev_signal_start called with illegal signal number", w->signum > 0 && w->signum < EV_NSIG)); + +#if EV_MULTIPLICITY + assert (("libev: a signal must not be attached to two different loops", + !signals [w->signum - 1].loop || signals [w->signum - 1].loop == loop)); + + signals [w->signum - 1].loop = EV_A; + ECB_MEMORY_FENCE_RELEASE; +#endif + + EV_FREQUENT_CHECK; + +#if EV_USE_SIGNALFD + if (sigfd == -2) + { + sigfd = signalfd (-1, &sigfd_set, SFD_NONBLOCK | SFD_CLOEXEC); + if (sigfd < 0 && errno == EINVAL) + sigfd = signalfd (-1, &sigfd_set, 0); /* retry without flags */ + + if (sigfd >= 0) + { + fd_intern (sigfd); /* doing it twice will not hurt */ + + sigemptyset (&sigfd_set); + + ev_io_init (&sigfd_w, sigfdcb, sigfd, EV_READ); + ev_set_priority (&sigfd_w, EV_MAXPRI); + ev_io_start (EV_A_ &sigfd_w); + ev_unref (EV_A); /* signalfd watcher should not keep loop alive */ + } + } + + if (sigfd >= 0) + { + /* TODO: check .head */ + sigaddset (&sigfd_set, w->signum); + sigprocmask (SIG_BLOCK, &sigfd_set, 0); + + signalfd (sigfd, &sigfd_set, 0); + } +#endif + + ev_start (EV_A_ (W)w, 1); + wlist_add (&signals [w->signum - 1].head, (WL)w); + + if (!((WL)w)->next) +# if EV_USE_SIGNALFD + if (sigfd < 0) /*TODO*/ +# endif + { +# ifdef _WIN32 + evpipe_init (EV_A); + + signal (w->signum, ev_sighandler); +# else + struct sigaction sa; + + evpipe_init (EV_A); + + sa.sa_handler = ev_sighandler; + sigfillset (&sa.sa_mask); + sa.sa_flags = SA_RESTART; /* if restarting works we save one iteration */ + sigaction (w->signum, &sa, 0); + + if (origflags & EVFLAG_NOSIGMASK) + { + sigemptyset (&sa.sa_mask); + sigaddset (&sa.sa_mask, w->signum); + sigprocmask (SIG_UNBLOCK, &sa.sa_mask, 0); + } +#endif + } + + EV_FREQUENT_CHECK; +} + +ecb_noinline +void +ev_signal_stop (EV_P_ ev_signal *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + wlist_del (&signals [w->signum - 1].head, (WL)w); + ev_stop (EV_A_ (W)w); + + if (!signals [w->signum - 1].head) + { +#if EV_MULTIPLICITY + signals [w->signum - 1].loop = 0; /* unattach from signal */ +#endif +#if EV_USE_SIGNALFD + if (sigfd >= 0) + { + sigset_t ss; + + sigemptyset (&ss); + sigaddset (&ss, w->signum); + sigdelset (&sigfd_set, w->signum); + + signalfd (sigfd, &sigfd_set, 0); + sigprocmask (SIG_UNBLOCK, &ss, 0); + } + else +#endif + signal (w->signum, SIG_DFL); + } + + EV_FREQUENT_CHECK; +} + +#endif + +#if EV_CHILD_ENABLE + +void +ev_child_start (EV_P_ ev_child *w) EV_NOEXCEPT +{ +#if EV_MULTIPLICITY + assert (("libev: child watchers are only supported in the default loop", loop == ev_default_loop_ptr)); +#endif + if (ecb_expect_false (ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + ev_start (EV_A_ (W)w, 1); + wlist_add (&childs [w->pid & ((EV_PID_HASHSIZE) - 1)], (WL)w); + + EV_FREQUENT_CHECK; +} + +void +ev_child_stop (EV_P_ ev_child *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + wlist_del (&childs [w->pid & ((EV_PID_HASHSIZE) - 1)], (WL)w); + ev_stop (EV_A_ (W)w); + + EV_FREQUENT_CHECK; +} + +#endif + +#if EV_STAT_ENABLE + +# ifdef _WIN32 +# undef lstat +# define lstat(a,b) _stati64 (a,b) +# endif + +#define DEF_STAT_INTERVAL 5.0074891 +#define NFS_STAT_INTERVAL 30.1074891 /* for filesystems potentially failing inotify */ +#define MIN_STAT_INTERVAL 0.1074891 + +ecb_noinline static void stat_timer_cb (EV_P_ ev_timer *w_, int revents); + +#if EV_USE_INOTIFY + +/* the * 2 is to allow for alignment padding, which for some reason is >> 8 */ +# define EV_INOTIFY_BUFSIZE (sizeof (struct inotify_event) * 2 + NAME_MAX) + +ecb_noinline +static void +infy_add (EV_P_ ev_stat *w) +{ + w->wd = inotify_add_watch (fs_fd, w->path, + IN_ATTRIB | IN_DELETE_SELF | IN_MOVE_SELF | IN_MODIFY + | IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO + | IN_DONT_FOLLOW | IN_MASK_ADD); + + if (w->wd >= 0) + { + struct statfs sfs; + + /* now local changes will be tracked by inotify, but remote changes won't */ + /* unless the filesystem is known to be local, we therefore still poll */ + /* also do poll on <2.6.25, but with normal frequency */ + + if (!fs_2625) + w->timer.repeat = w->interval ? w->interval : DEF_STAT_INTERVAL; + else if (!statfs (w->path, &sfs) + && (sfs.f_type == 0x1373 /* devfs */ + || sfs.f_type == 0x4006 /* fat */ + || sfs.f_type == 0x4d44 /* msdos */ + || sfs.f_type == 0xEF53 /* ext2/3 */ + || sfs.f_type == 0x72b6 /* jffs2 */ + || sfs.f_type == 0x858458f6 /* ramfs */ + || sfs.f_type == 0x5346544e /* ntfs */ + || sfs.f_type == 0x3153464a /* jfs */ + || sfs.f_type == 0x9123683e /* btrfs */ + || sfs.f_type == 0x52654973 /* reiser3 */ + || sfs.f_type == 0x01021994 /* tmpfs */ + || sfs.f_type == 0x58465342 /* xfs */)) + w->timer.repeat = 0.; /* filesystem is local, kernel new enough */ + else + w->timer.repeat = w->interval ? w->interval : NFS_STAT_INTERVAL; /* remote, use reduced frequency */ + } + else + { + /* can't use inotify, continue to stat */ + w->timer.repeat = w->interval ? w->interval : DEF_STAT_INTERVAL; + + /* if path is not there, monitor some parent directory for speedup hints */ + /* note that exceeding the hardcoded path limit is not a correctness issue, */ + /* but an efficiency issue only */ + if ((errno == ENOENT || errno == EACCES) && strlen (w->path) < 4096) + { + char path [4096]; + strcpy (path, w->path); + + do + { + int mask = IN_MASK_ADD | IN_DELETE_SELF | IN_MOVE_SELF + | (errno == EACCES ? IN_ATTRIB : IN_CREATE | IN_MOVED_TO); + + char *pend = strrchr (path, '/'); + + if (!pend || pend == path) + break; + + *pend = 0; + w->wd = inotify_add_watch (fs_fd, path, mask); + } + while (w->wd < 0 && (errno == ENOENT || errno == EACCES)); + } + } + + if (w->wd >= 0) + wlist_add (&fs_hash [w->wd & ((EV_INOTIFY_HASHSIZE) - 1)].head, (WL)w); + + /* now re-arm timer, if required */ + if (ev_is_active (&w->timer)) ev_ref (EV_A); + ev_timer_again (EV_A_ &w->timer); + if (ev_is_active (&w->timer)) ev_unref (EV_A); +} + +ecb_noinline +static void +infy_del (EV_P_ ev_stat *w) +{ + int slot; + int wd = w->wd; + + if (wd < 0) + return; + + w->wd = -2; + slot = wd & ((EV_INOTIFY_HASHSIZE) - 1); + wlist_del (&fs_hash [slot].head, (WL)w); + + /* remove this watcher, if others are watching it, they will rearm */ + inotify_rm_watch (fs_fd, wd); +} + +ecb_noinline +static void +infy_wd (EV_P_ int slot, int wd, struct inotify_event *ev) +{ + if (slot < 0) + /* overflow, need to check for all hash slots */ + for (slot = 0; slot < (EV_INOTIFY_HASHSIZE); ++slot) + infy_wd (EV_A_ slot, wd, ev); + else + { + WL w_; + + for (w_ = fs_hash [slot & ((EV_INOTIFY_HASHSIZE) - 1)].head; w_; ) + { + ev_stat *w = (ev_stat *)w_; + w_ = w_->next; /* lets us remove this watcher and all before it */ + + if (w->wd == wd || wd == -1) + { + if (ev->mask & (IN_IGNORED | IN_UNMOUNT | IN_DELETE_SELF)) + { + wlist_del (&fs_hash [slot & ((EV_INOTIFY_HASHSIZE) - 1)].head, (WL)w); + w->wd = -1; + infy_add (EV_A_ w); /* re-add, no matter what */ + } + + stat_timer_cb (EV_A_ &w->timer, 0); + } + } + } +} + +static void +infy_cb (EV_P_ ev_io *w, int revents) +{ + char buf [EV_INOTIFY_BUFSIZE]; + int ofs; + int len = read (fs_fd, buf, sizeof (buf)); + + for (ofs = 0; ofs < len; ) + { + struct inotify_event *ev = (struct inotify_event *)(buf + ofs); + infy_wd (EV_A_ ev->wd, ev->wd, ev); + ofs += sizeof (struct inotify_event) + ev->len; + } +} + +inline_size ecb_cold +void +ev_check_2625 (EV_P) +{ + /* kernels < 2.6.25 are borked + * http://www.ussg.indiana.edu/hypermail/linux/kernel/0711.3/1208.html + */ + if (ev_linux_version () < 0x020619) + return; + + fs_2625 = 1; +} + +inline_size int +infy_newfd (void) +{ +#if defined IN_CLOEXEC && defined IN_NONBLOCK + int fd = inotify_init1 (IN_CLOEXEC | IN_NONBLOCK); + if (fd >= 0) + return fd; +#endif + return inotify_init (); +} + +inline_size void +infy_init (EV_P) +{ + if (fs_fd != -2) + return; + + fs_fd = -1; + + ev_check_2625 (EV_A); + + fs_fd = infy_newfd (); + + if (fs_fd >= 0) + { + fd_intern (fs_fd); + ev_io_init (&fs_w, infy_cb, fs_fd, EV_READ); + ev_set_priority (&fs_w, EV_MAXPRI); + ev_io_start (EV_A_ &fs_w); + ev_unref (EV_A); + } +} + +inline_size void +infy_fork (EV_P) +{ + int slot; + + if (fs_fd < 0) + return; + + ev_ref (EV_A); + ev_io_stop (EV_A_ &fs_w); + close (fs_fd); + fs_fd = infy_newfd (); + + if (fs_fd >= 0) + { + fd_intern (fs_fd); + ev_io_set (&fs_w, fs_fd, EV_READ); + ev_io_start (EV_A_ &fs_w); + ev_unref (EV_A); + } + + for (slot = 0; slot < (EV_INOTIFY_HASHSIZE); ++slot) + { + WL w_ = fs_hash [slot].head; + fs_hash [slot].head = 0; + + while (w_) + { + ev_stat *w = (ev_stat *)w_; + w_ = w_->next; /* lets us add this watcher */ + + w->wd = -1; + + if (fs_fd >= 0) + infy_add (EV_A_ w); /* re-add, no matter what */ + else + { + w->timer.repeat = w->interval ? w->interval : DEF_STAT_INTERVAL; + if (ev_is_active (&w->timer)) ev_ref (EV_A); + ev_timer_again (EV_A_ &w->timer); + if (ev_is_active (&w->timer)) ev_unref (EV_A); + } + } + } +} + +#endif + +#ifdef _WIN32 +# define EV_LSTAT(p,b) _stati64 (p, b) +#else +# define EV_LSTAT(p,b) lstat (p, b) +#endif + +void +ev_stat_stat (EV_P_ ev_stat *w) EV_NOEXCEPT +{ + if (lstat (w->path, &w->attr) < 0) + w->attr.st_nlink = 0; + else if (!w->attr.st_nlink) + w->attr.st_nlink = 1; +} + +ecb_noinline +static void +stat_timer_cb (EV_P_ ev_timer *w_, int revents) +{ + ev_stat *w = (ev_stat *)(((char *)w_) - offsetof (ev_stat, timer)); + + ev_statdata prev = w->attr; + ev_stat_stat (EV_A_ w); + + /* memcmp doesn't work on netbsd, they.... do stuff to their struct stat */ + if ( + prev.st_dev != w->attr.st_dev + || prev.st_ino != w->attr.st_ino + || prev.st_mode != w->attr.st_mode + || prev.st_nlink != w->attr.st_nlink + || prev.st_uid != w->attr.st_uid + || prev.st_gid != w->attr.st_gid + || prev.st_rdev != w->attr.st_rdev + || prev.st_size != w->attr.st_size + || prev.st_atime != w->attr.st_atime + || prev.st_mtime != w->attr.st_mtime + || prev.st_ctime != w->attr.st_ctime + ) { + /* we only update w->prev on actual differences */ + /* in case we test more often than invoke the callback, */ + /* to ensure that prev is always different to attr */ + w->prev = prev; + + #if EV_USE_INOTIFY + if (fs_fd >= 0) + { + infy_del (EV_A_ w); + infy_add (EV_A_ w); + ev_stat_stat (EV_A_ w); /* avoid race... */ + } + #endif + + ev_feed_event (EV_A_ w, EV_STAT); + } +} + +void +ev_stat_start (EV_P_ ev_stat *w) EV_NOEXCEPT +{ + if (ecb_expect_false (ev_is_active (w))) + return; + + ev_stat_stat (EV_A_ w); + + if (w->interval < MIN_STAT_INTERVAL && w->interval) + w->interval = MIN_STAT_INTERVAL; + + ev_timer_init (&w->timer, stat_timer_cb, 0., w->interval ? w->interval : DEF_STAT_INTERVAL); + ev_set_priority (&w->timer, ev_priority (w)); + +#if EV_USE_INOTIFY + infy_init (EV_A); + + if (fs_fd >= 0) + infy_add (EV_A_ w); + else +#endif + { + ev_timer_again (EV_A_ &w->timer); + ev_unref (EV_A); + } + + ev_start (EV_A_ (W)w, 1); + + EV_FREQUENT_CHECK; +} + +void +ev_stat_stop (EV_P_ ev_stat *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + +#if EV_USE_INOTIFY + infy_del (EV_A_ w); +#endif + + if (ev_is_active (&w->timer)) + { + ev_ref (EV_A); + ev_timer_stop (EV_A_ &w->timer); + } + + ev_stop (EV_A_ (W)w); + + EV_FREQUENT_CHECK; +} +#endif + +#if EV_IDLE_ENABLE +void +ev_idle_start (EV_P_ ev_idle *w) EV_NOEXCEPT +{ + if (ecb_expect_false (ev_is_active (w))) + return; + + pri_adjust (EV_A_ (W)w); + + EV_FREQUENT_CHECK; + + { + int active = ++idlecnt [ABSPRI (w)]; + + ++idleall; + ev_start (EV_A_ (W)w, active); + + array_needsize (ev_idle *, idles [ABSPRI (w)], idlemax [ABSPRI (w)], active, array_needsize_noinit); + idles [ABSPRI (w)][active - 1] = w; + } + + EV_FREQUENT_CHECK; +} + +void +ev_idle_stop (EV_P_ ev_idle *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + { + int active = ev_active (w); + + idles [ABSPRI (w)][active - 1] = idles [ABSPRI (w)][--idlecnt [ABSPRI (w)]]; + ev_active (idles [ABSPRI (w)][active - 1]) = active; + + ev_stop (EV_A_ (W)w); + --idleall; + } + + EV_FREQUENT_CHECK; +} +#endif + +#if EV_PREPARE_ENABLE +void +ev_prepare_start (EV_P_ ev_prepare *w) EV_NOEXCEPT +{ + if (ecb_expect_false (ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + ev_start (EV_A_ (W)w, ++preparecnt); + array_needsize (ev_prepare *, prepares, preparemax, preparecnt, array_needsize_noinit); + prepares [preparecnt - 1] = w; + + EV_FREQUENT_CHECK; +} + +void +ev_prepare_stop (EV_P_ ev_prepare *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + { + int active = ev_active (w); + + prepares [active - 1] = prepares [--preparecnt]; + ev_active (prepares [active - 1]) = active; + } + + ev_stop (EV_A_ (W)w); + + EV_FREQUENT_CHECK; +} +#endif + +#if EV_CHECK_ENABLE +void +ev_check_start (EV_P_ ev_check *w) EV_NOEXCEPT +{ + if (ecb_expect_false (ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + ev_start (EV_A_ (W)w, ++checkcnt); + array_needsize (ev_check *, checks, checkmax, checkcnt, array_needsize_noinit); + checks [checkcnt - 1] = w; + + EV_FREQUENT_CHECK; +} + +void +ev_check_stop (EV_P_ ev_check *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + { + int active = ev_active (w); + + checks [active - 1] = checks [--checkcnt]; + ev_active (checks [active - 1]) = active; + } + + ev_stop (EV_A_ (W)w); + + EV_FREQUENT_CHECK; +} +#endif + +#if EV_EMBED_ENABLE +ecb_noinline +void +ev_embed_sweep (EV_P_ ev_embed *w) EV_NOEXCEPT +{ + ev_run (w->other, EVRUN_NOWAIT); +} + +static void +embed_io_cb (EV_P_ ev_io *io, int revents) +{ + ev_embed *w = (ev_embed *)(((char *)io) - offsetof (ev_embed, io)); + + if (ev_cb (w)) + ev_feed_event (EV_A_ (W)w, EV_EMBED); + else + ev_run (w->other, EVRUN_NOWAIT); +} + +static void +embed_prepare_cb (EV_P_ ev_prepare *prepare, int revents) +{ + ev_embed *w = (ev_embed *)(((char *)prepare) - offsetof (ev_embed, prepare)); + + { + EV_P = w->other; + + while (fdchangecnt) + { + fd_reify (EV_A); + ev_run (EV_A_ EVRUN_NOWAIT); + } + } +} + +#if EV_FORK_ENABLE +static void +embed_fork_cb (EV_P_ ev_fork *fork_w, int revents) +{ + ev_embed *w = (ev_embed *)(((char *)fork_w) - offsetof (ev_embed, fork)); + + ev_embed_stop (EV_A_ w); + + { + EV_P = w->other; + + ev_loop_fork (EV_A); + ev_run (EV_A_ EVRUN_NOWAIT); + } + + ev_embed_start (EV_A_ w); +} +#endif + +#if 0 +static void +embed_idle_cb (EV_P_ ev_idle *idle, int revents) +{ + ev_idle_stop (EV_A_ idle); +} +#endif + +void +ev_embed_start (EV_P_ ev_embed *w) EV_NOEXCEPT +{ + if (ecb_expect_false (ev_is_active (w))) + return; + + { + EV_P = w->other; + assert (("libev: loop to be embedded is not embeddable", backend & ev_embeddable_backends ())); + ev_io_init (&w->io, embed_io_cb, backend_fd, EV_READ); + } + + EV_FREQUENT_CHECK; + + ev_set_priority (&w->io, ev_priority (w)); + ev_io_start (EV_A_ &w->io); + + ev_prepare_init (&w->prepare, embed_prepare_cb); + ev_set_priority (&w->prepare, EV_MINPRI); + ev_prepare_start (EV_A_ &w->prepare); + +#if EV_FORK_ENABLE + ev_fork_init (&w->fork, embed_fork_cb); + ev_fork_start (EV_A_ &w->fork); +#endif + + /*ev_idle_init (&w->idle, e,bed_idle_cb);*/ + + ev_start (EV_A_ (W)w, 1); + + EV_FREQUENT_CHECK; +} + +void +ev_embed_stop (EV_P_ ev_embed *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + ev_io_stop (EV_A_ &w->io); + ev_prepare_stop (EV_A_ &w->prepare); +#if EV_FORK_ENABLE + ev_fork_stop (EV_A_ &w->fork); +#endif + + ev_stop (EV_A_ (W)w); + + EV_FREQUENT_CHECK; +} +#endif + +#if EV_FORK_ENABLE +void +ev_fork_start (EV_P_ ev_fork *w) EV_NOEXCEPT +{ + if (ecb_expect_false (ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + ev_start (EV_A_ (W)w, ++forkcnt); + array_needsize (ev_fork *, forks, forkmax, forkcnt, array_needsize_noinit); + forks [forkcnt - 1] = w; + + EV_FREQUENT_CHECK; +} + +void +ev_fork_stop (EV_P_ ev_fork *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + { + int active = ev_active (w); + + forks [active - 1] = forks [--forkcnt]; + ev_active (forks [active - 1]) = active; + } + + ev_stop (EV_A_ (W)w); + + EV_FREQUENT_CHECK; +} +#endif + +#if EV_CLEANUP_ENABLE +void +ev_cleanup_start (EV_P_ ev_cleanup *w) EV_NOEXCEPT +{ + if (ecb_expect_false (ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + ev_start (EV_A_ (W)w, ++cleanupcnt); + array_needsize (ev_cleanup *, cleanups, cleanupmax, cleanupcnt, array_needsize_noinit); + cleanups [cleanupcnt - 1] = w; + + /* cleanup watchers should never keep a refcount on the loop */ + ev_unref (EV_A); + EV_FREQUENT_CHECK; +} + +void +ev_cleanup_stop (EV_P_ ev_cleanup *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + ev_ref (EV_A); + + { + int active = ev_active (w); + + cleanups [active - 1] = cleanups [--cleanupcnt]; + ev_active (cleanups [active - 1]) = active; + } + + ev_stop (EV_A_ (W)w); + + EV_FREQUENT_CHECK; +} +#endif + +#if EV_ASYNC_ENABLE +void +ev_async_start (EV_P_ ev_async *w) EV_NOEXCEPT +{ + if (ecb_expect_false (ev_is_active (w))) + return; + + w->sent = 0; + + evpipe_init (EV_A); + + EV_FREQUENT_CHECK; + + ev_start (EV_A_ (W)w, ++asynccnt); + array_needsize (ev_async *, asyncs, asyncmax, asynccnt, array_needsize_noinit); + asyncs [asynccnt - 1] = w; + + EV_FREQUENT_CHECK; +} + +void +ev_async_stop (EV_P_ ev_async *w) EV_NOEXCEPT +{ + clear_pending (EV_A_ (W)w); + if (ecb_expect_false (!ev_is_active (w))) + return; + + EV_FREQUENT_CHECK; + + { + int active = ev_active (w); + + asyncs [active - 1] = asyncs [--asynccnt]; + ev_active (asyncs [active - 1]) = active; + } + + ev_stop (EV_A_ (W)w); + + EV_FREQUENT_CHECK; +} + +void +ev_async_send (EV_P_ ev_async *w) EV_NOEXCEPT +{ + w->sent = 1; + evpipe_write (EV_A_ &async_pending); +} +#endif + +/*****************************************************************************/ + +struct ev_once +{ + ev_io io; + ev_timer to; + void (*cb)(int revents, void *arg); + void *arg; +}; + +static void +once_cb (EV_P_ struct ev_once *once, int revents) +{ + void (*cb)(int revents, void *arg) = once->cb; + void *arg = once->arg; + + ev_io_stop (EV_A_ &once->io); + ev_timer_stop (EV_A_ &once->to); + ev_free (once); + + cb (revents, arg); +} + +static void +once_cb_io (EV_P_ ev_io *w, int revents) +{ + struct ev_once *once = (struct ev_once *)(((char *)w) - offsetof (struct ev_once, io)); + + once_cb (EV_A_ once, revents | ev_clear_pending (EV_A_ &once->to)); +} + +static void +once_cb_to (EV_P_ ev_timer *w, int revents) +{ + struct ev_once *once = (struct ev_once *)(((char *)w) - offsetof (struct ev_once, to)); + + once_cb (EV_A_ once, revents | ev_clear_pending (EV_A_ &once->io)); +} + +void +ev_once (EV_P_ int fd, int events, ev_tstamp timeout, void (*cb)(int revents, void *arg), void *arg) EV_NOEXCEPT +{ + struct ev_once *once = (struct ev_once *)ev_malloc (sizeof (struct ev_once)); + + once->cb = cb; + once->arg = arg; + + ev_init (&once->io, once_cb_io); + if (fd >= 0) + { + ev_io_set (&once->io, fd, events); + ev_io_start (EV_A_ &once->io); + } + + ev_init (&once->to, once_cb_to); + if (timeout >= 0.) + { + ev_timer_set (&once->to, timeout, 0.); + ev_timer_start (EV_A_ &once->to); + } +} + +/*****************************************************************************/ + +#if EV_WALK_ENABLE +ecb_cold +void +ev_walk (EV_P_ int types, void (*cb)(EV_P_ int type, void *w)) EV_NOEXCEPT +{ + int i, j; + ev_watcher_list *wl, *wn; + + if (types & (EV_IO | EV_EMBED)) + for (i = 0; i < anfdmax; ++i) + for (wl = anfds [i].head; wl; ) + { + wn = wl->next; + +#if EV_EMBED_ENABLE + if (ev_cb ((ev_io *)wl) == embed_io_cb) + { + if (types & EV_EMBED) + cb (EV_A_ EV_EMBED, ((char *)wl) - offsetof (struct ev_embed, io)); + } + else +#endif +#if EV_USE_INOTIFY + if (ev_cb ((ev_io *)wl) == infy_cb) + ; + else +#endif + if ((ev_io *)wl != &pipe_w) + if (types & EV_IO) + cb (EV_A_ EV_IO, wl); + + wl = wn; + } + + if (types & (EV_TIMER | EV_STAT)) + for (i = timercnt + HEAP0; i-- > HEAP0; ) +#if EV_STAT_ENABLE + /*TODO: timer is not always active*/ + if (ev_cb ((ev_timer *)ANHE_w (timers [i])) == stat_timer_cb) + { + if (types & EV_STAT) + cb (EV_A_ EV_STAT, ((char *)ANHE_w (timers [i])) - offsetof (struct ev_stat, timer)); + } + else +#endif + if (types & EV_TIMER) + cb (EV_A_ EV_TIMER, ANHE_w (timers [i])); + +#if EV_PERIODIC_ENABLE + if (types & EV_PERIODIC) + for (i = periodiccnt + HEAP0; i-- > HEAP0; ) + cb (EV_A_ EV_PERIODIC, ANHE_w (periodics [i])); +#endif + +#if EV_IDLE_ENABLE + if (types & EV_IDLE) + for (j = NUMPRI; j--; ) + for (i = idlecnt [j]; i--; ) + cb (EV_A_ EV_IDLE, idles [j][i]); +#endif + +#if EV_FORK_ENABLE + if (types & EV_FORK) + for (i = forkcnt; i--; ) + if (ev_cb (forks [i]) != embed_fork_cb) + cb (EV_A_ EV_FORK, forks [i]); +#endif + +#if EV_ASYNC_ENABLE + if (types & EV_ASYNC) + for (i = asynccnt; i--; ) + cb (EV_A_ EV_ASYNC, asyncs [i]); +#endif + +#if EV_PREPARE_ENABLE + if (types & EV_PREPARE) + for (i = preparecnt; i--; ) +# if EV_EMBED_ENABLE + if (ev_cb (prepares [i]) != embed_prepare_cb) +# endif + cb (EV_A_ EV_PREPARE, prepares [i]); +#endif + +#if EV_CHECK_ENABLE + if (types & EV_CHECK) + for (i = checkcnt; i--; ) + cb (EV_A_ EV_CHECK, checks [i]); +#endif + +#if EV_SIGNAL_ENABLE + if (types & EV_SIGNAL) + for (i = 0; i < EV_NSIG - 1; ++i) + for (wl = signals [i].head; wl; ) + { + wn = wl->next; + cb (EV_A_ EV_SIGNAL, wl); + wl = wn; + } +#endif + +#if EV_CHILD_ENABLE + if (types & EV_CHILD) + for (i = (EV_PID_HASHSIZE); i--; ) + for (wl = childs [i]; wl; ) + { + wn = wl->next; + cb (EV_A_ EV_CHILD, wl); + wl = wn; + } +#endif +/* EV_STAT 0x00001000 /* stat data changed */ +/* EV_EMBED 0x00010000 /* embedded event loop needs sweep */ +} +#endif + +#if EV_MULTIPLICITY + #include "ev_wrap.h" +#endif + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev.h b/deps/ngtcp2/ngtcp2/third-party/libev/ev.h new file mode 100644 index 00000000000000..4669c39b61458c --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev.h @@ -0,0 +1,860 @@ +/* + * libev native API header + * + * Copyright (c) 2007-2020 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +#ifndef EV_H_ +#define EV_H_ + +#ifdef __cplusplus +# define EV_CPP(x) x +# if __cplusplus >= 201103L +# define EV_NOEXCEPT noexcept +# else +# define EV_NOEXCEPT +# endif +#else +# define EV_CPP(x) +# define EV_NOEXCEPT +#endif +#define EV_THROW EV_NOEXCEPT /* pre-4.25, do not use in new code */ + +EV_CPP(extern "C" {) + +/*****************************************************************************/ + +/* pre-4.0 compatibility */ +#ifndef EV_COMPAT3 +# define EV_COMPAT3 1 +#endif + +#ifndef EV_FEATURES +# if defined __OPTIMIZE_SIZE__ +# define EV_FEATURES 0x7c +# else +# define EV_FEATURES 0x7f +# endif +#endif + +#define EV_FEATURE_CODE ((EV_FEATURES) & 1) +#define EV_FEATURE_DATA ((EV_FEATURES) & 2) +#define EV_FEATURE_CONFIG ((EV_FEATURES) & 4) +#define EV_FEATURE_API ((EV_FEATURES) & 8) +#define EV_FEATURE_WATCHERS ((EV_FEATURES) & 16) +#define EV_FEATURE_BACKENDS ((EV_FEATURES) & 32) +#define EV_FEATURE_OS ((EV_FEATURES) & 64) + +/* these priorities are inclusive, higher priorities will be invoked earlier */ +#ifndef EV_MINPRI +# define EV_MINPRI (EV_FEATURE_CONFIG ? -2 : 0) +#endif +#ifndef EV_MAXPRI +# define EV_MAXPRI (EV_FEATURE_CONFIG ? +2 : 0) +#endif + +#ifndef EV_MULTIPLICITY +# define EV_MULTIPLICITY EV_FEATURE_CONFIG +#endif + +#ifndef EV_PERIODIC_ENABLE +# define EV_PERIODIC_ENABLE EV_FEATURE_WATCHERS +#endif + +#ifndef EV_STAT_ENABLE +# define EV_STAT_ENABLE EV_FEATURE_WATCHERS +#endif + +#ifndef EV_PREPARE_ENABLE +# define EV_PREPARE_ENABLE EV_FEATURE_WATCHERS +#endif + +#ifndef EV_CHECK_ENABLE +# define EV_CHECK_ENABLE EV_FEATURE_WATCHERS +#endif + +#ifndef EV_IDLE_ENABLE +# define EV_IDLE_ENABLE EV_FEATURE_WATCHERS +#endif + +#ifndef EV_FORK_ENABLE +# define EV_FORK_ENABLE EV_FEATURE_WATCHERS +#endif + +#ifndef EV_CLEANUP_ENABLE +# define EV_CLEANUP_ENABLE EV_FEATURE_WATCHERS +#endif + +#ifndef EV_SIGNAL_ENABLE +# define EV_SIGNAL_ENABLE EV_FEATURE_WATCHERS +#endif + +#ifndef EV_CHILD_ENABLE +# ifdef _WIN32 +# define EV_CHILD_ENABLE 0 +# else +# define EV_CHILD_ENABLE EV_FEATURE_WATCHERS +#endif +#endif + +#ifndef EV_ASYNC_ENABLE +# define EV_ASYNC_ENABLE EV_FEATURE_WATCHERS +#endif + +#ifndef EV_EMBED_ENABLE +# define EV_EMBED_ENABLE EV_FEATURE_WATCHERS +#endif + +#ifndef EV_WALK_ENABLE +# define EV_WALK_ENABLE 0 /* not yet */ +#endif + +/*****************************************************************************/ + +#if EV_CHILD_ENABLE && !EV_SIGNAL_ENABLE +# undef EV_SIGNAL_ENABLE +# define EV_SIGNAL_ENABLE 1 +#endif + +/*****************************************************************************/ + +#ifndef EV_TSTAMP_T +# define EV_TSTAMP_T double +#endif +typedef EV_TSTAMP_T ev_tstamp; + +#include /* for memmove */ + +#ifndef EV_ATOMIC_T +# include +# define EV_ATOMIC_T sig_atomic_t volatile +#endif + +#if EV_STAT_ENABLE +# ifdef _WIN32 +# include +# include +# endif +# include +#endif + +/* support multiple event loops? */ +#if EV_MULTIPLICITY +struct ev_loop; +# define EV_P struct ev_loop *loop /* a loop as sole parameter in a declaration */ +# define EV_P_ EV_P, /* a loop as first of multiple parameters */ +# define EV_A loop /* a loop as sole argument to a function call */ +# define EV_A_ EV_A, /* a loop as first of multiple arguments */ +# define EV_DEFAULT_UC ev_default_loop_uc_ () /* the default loop, if initialised, as sole arg */ +# define EV_DEFAULT_UC_ EV_DEFAULT_UC, /* the default loop as first of multiple arguments */ +# define EV_DEFAULT ev_default_loop (0) /* the default loop as sole arg */ +# define EV_DEFAULT_ EV_DEFAULT, /* the default loop as first of multiple arguments */ +#else +# define EV_P void +# define EV_P_ +# define EV_A +# define EV_A_ +# define EV_DEFAULT +# define EV_DEFAULT_ +# define EV_DEFAULT_UC +# define EV_DEFAULT_UC_ +# undef EV_EMBED_ENABLE +#endif + +/* EV_INLINE is used for functions in header files */ +#if __STDC_VERSION__ >= 199901L || __GNUC__ >= 3 +# define EV_INLINE static inline +#else +# define EV_INLINE static +#endif + +#ifdef EV_API_STATIC +# define EV_API_DECL static +#else +# define EV_API_DECL extern +#endif + +/* EV_PROTOTYPES can be used to switch of prototype declarations */ +#ifndef EV_PROTOTYPES +# define EV_PROTOTYPES 1 +#endif + +/*****************************************************************************/ + +#define EV_VERSION_MAJOR 4 +#define EV_VERSION_MINOR 33 + +/* eventmask, revents, events... */ +enum { + EV_UNDEF = (int)0xFFFFFFFF, /* guaranteed to be invalid */ + EV_NONE = 0x00, /* no events */ + EV_READ = 0x01, /* ev_io detected read will not block */ + EV_WRITE = 0x02, /* ev_io detected write will not block */ + EV__IOFDSET = 0x80, /* internal use only */ + EV_IO = EV_READ, /* alias for type-detection */ + EV_TIMER = 0x00000100, /* timer timed out */ +#if EV_COMPAT3 + EV_TIMEOUT = EV_TIMER, /* pre 4.0 API compatibility */ +#endif + EV_PERIODIC = 0x00000200, /* periodic timer timed out */ + EV_SIGNAL = 0x00000400, /* signal was received */ + EV_CHILD = 0x00000800, /* child/pid had status change */ + EV_STAT = 0x00001000, /* stat data changed */ + EV_IDLE = 0x00002000, /* event loop is idling */ + EV_PREPARE = 0x00004000, /* event loop about to poll */ + EV_CHECK = 0x00008000, /* event loop finished poll */ + EV_EMBED = 0x00010000, /* embedded event loop needs sweep */ + EV_FORK = 0x00020000, /* event loop resumed in child */ + EV_CLEANUP = 0x00040000, /* event loop resumed in child */ + EV_ASYNC = 0x00080000, /* async intra-loop signal */ + EV_CUSTOM = 0x01000000, /* for use by user code */ + EV_ERROR = (int)0x80000000 /* sent when an error occurs */ +}; + +/* can be used to add custom fields to all watchers, while losing binary compatibility */ +#ifndef EV_COMMON +# define EV_COMMON void *data; +#endif + +#ifndef EV_CB_DECLARE +# define EV_CB_DECLARE(type) void (*cb)(EV_P_ struct type *w, int revents); +#endif +#ifndef EV_CB_INVOKE +# define EV_CB_INVOKE(watcher,revents) (watcher)->cb (EV_A_ (watcher), (revents)) +#endif + +/* not official, do not use */ +#define EV_CB(type,name) void name (EV_P_ struct ev_ ## type *w, int revents) + +/* + * struct member types: + * private: you may look at them, but not change them, + * and they might not mean anything to you. + * ro: can be read anytime, but only changed when the watcher isn't active. + * rw: can be read and modified anytime, even when the watcher is active. + * + * some internal details that might be helpful for debugging: + * + * active is either 0, which means the watcher is not active, + * or the array index of the watcher (periodics, timers) + * or the array index + 1 (most other watchers) + * or simply 1 for watchers that aren't in some array. + * pending is either 0, in which case the watcher isn't, + * or the array index + 1 in the pendings array. + */ + +#if EV_MINPRI == EV_MAXPRI +# define EV_DECL_PRIORITY +#elif !defined (EV_DECL_PRIORITY) +# define EV_DECL_PRIORITY int priority; +#endif + +/* shared by all watchers */ +#define EV_WATCHER(type) \ + int active; /* private */ \ + int pending; /* private */ \ + EV_DECL_PRIORITY /* private */ \ + EV_COMMON /* rw */ \ + EV_CB_DECLARE (type) /* private */ + +#define EV_WATCHER_LIST(type) \ + EV_WATCHER (type) \ + struct ev_watcher_list *next; /* private */ + +#define EV_WATCHER_TIME(type) \ + EV_WATCHER (type) \ + ev_tstamp at; /* private */ + +/* base class, nothing to see here unless you subclass */ +typedef struct ev_watcher +{ + EV_WATCHER (ev_watcher) +} ev_watcher; + +/* base class, nothing to see here unless you subclass */ +typedef struct ev_watcher_list +{ + EV_WATCHER_LIST (ev_watcher_list) +} ev_watcher_list; + +/* base class, nothing to see here unless you subclass */ +typedef struct ev_watcher_time +{ + EV_WATCHER_TIME (ev_watcher_time) +} ev_watcher_time; + +/* invoked when fd is either EV_READable or EV_WRITEable */ +/* revent EV_READ, EV_WRITE */ +typedef struct ev_io +{ + EV_WATCHER_LIST (ev_io) + + int fd; /* ro */ + int events; /* ro */ +} ev_io; + +/* invoked after a specific time, repeatable (based on monotonic clock) */ +/* revent EV_TIMEOUT */ +typedef struct ev_timer +{ + EV_WATCHER_TIME (ev_timer) + + ev_tstamp repeat; /* rw */ +} ev_timer; + +/* invoked at some specific time, possibly repeating at regular intervals (based on UTC) */ +/* revent EV_PERIODIC */ +typedef struct ev_periodic +{ + EV_WATCHER_TIME (ev_periodic) + + ev_tstamp offset; /* rw */ + ev_tstamp interval; /* rw */ + ev_tstamp (*reschedule_cb)(struct ev_periodic *w, ev_tstamp now) EV_NOEXCEPT; /* rw */ +} ev_periodic; + +/* invoked when the given signal has been received */ +/* revent EV_SIGNAL */ +typedef struct ev_signal +{ + EV_WATCHER_LIST (ev_signal) + + int signum; /* ro */ +} ev_signal; + +/* invoked when sigchld is received and waitpid indicates the given pid */ +/* revent EV_CHILD */ +/* does not support priorities */ +typedef struct ev_child +{ + EV_WATCHER_LIST (ev_child) + + int flags; /* private */ + int pid; /* ro */ + int rpid; /* rw, holds the received pid */ + int rstatus; /* rw, holds the exit status, use the macros from sys/wait.h */ +} ev_child; + +#if EV_STAT_ENABLE +/* st_nlink = 0 means missing file or other error */ +# ifdef _WIN32 +typedef struct _stati64 ev_statdata; +# else +typedef struct stat ev_statdata; +# endif + +/* invoked each time the stat data changes for a given path */ +/* revent EV_STAT */ +typedef struct ev_stat +{ + EV_WATCHER_LIST (ev_stat) + + ev_timer timer; /* private */ + ev_tstamp interval; /* ro */ + const char *path; /* ro */ + ev_statdata prev; /* ro */ + ev_statdata attr; /* ro */ + + int wd; /* wd for inotify, fd for kqueue */ +} ev_stat; +#endif + +/* invoked when the nothing else needs to be done, keeps the process from blocking */ +/* revent EV_IDLE */ +typedef struct ev_idle +{ + EV_WATCHER (ev_idle) +} ev_idle; + +/* invoked for each run of the mainloop, just before the blocking call */ +/* you can still change events in any way you like */ +/* revent EV_PREPARE */ +typedef struct ev_prepare +{ + EV_WATCHER (ev_prepare) +} ev_prepare; + +/* invoked for each run of the mainloop, just after the blocking call */ +/* revent EV_CHECK */ +typedef struct ev_check +{ + EV_WATCHER (ev_check) +} ev_check; + +/* the callback gets invoked before check in the child process when a fork was detected */ +/* revent EV_FORK */ +typedef struct ev_fork +{ + EV_WATCHER (ev_fork) +} ev_fork; + +/* is invoked just before the loop gets destroyed */ +/* revent EV_CLEANUP */ +typedef struct ev_cleanup +{ + EV_WATCHER (ev_cleanup) +} ev_cleanup; + +#if EV_EMBED_ENABLE +/* used to embed an event loop inside another */ +/* the callback gets invoked when the event loop has handled events, and can be 0 */ +typedef struct ev_embed +{ + EV_WATCHER (ev_embed) + + struct ev_loop *other; /* ro */ +#undef EV_IO_ENABLE +#define EV_IO_ENABLE 1 + ev_io io; /* private */ +#undef EV_PREPARE_ENABLE +#define EV_PREPARE_ENABLE 1 + ev_prepare prepare; /* private */ + ev_check check; /* unused */ + ev_timer timer; /* unused */ + ev_periodic periodic; /* unused */ + ev_idle idle; /* unused */ + ev_fork fork; /* private */ + ev_cleanup cleanup; /* unused */ +} ev_embed; +#endif + +#if EV_ASYNC_ENABLE +/* invoked when somebody calls ev_async_send on the watcher */ +/* revent EV_ASYNC */ +typedef struct ev_async +{ + EV_WATCHER (ev_async) + + EV_ATOMIC_T sent; /* private */ +} ev_async; + +# define ev_async_pending(w) (+(w)->sent) +#endif + +/* the presence of this union forces similar struct layout */ +union ev_any_watcher +{ + struct ev_watcher w; + struct ev_watcher_list wl; + + struct ev_io io; + struct ev_timer timer; + struct ev_periodic periodic; + struct ev_signal signal; + struct ev_child child; +#if EV_STAT_ENABLE + struct ev_stat stat; +#endif +#if EV_IDLE_ENABLE + struct ev_idle idle; +#endif + struct ev_prepare prepare; + struct ev_check check; +#if EV_FORK_ENABLE + struct ev_fork fork; +#endif +#if EV_CLEANUP_ENABLE + struct ev_cleanup cleanup; +#endif +#if EV_EMBED_ENABLE + struct ev_embed embed; +#endif +#if EV_ASYNC_ENABLE + struct ev_async async; +#endif +}; + +/* flag bits for ev_default_loop and ev_loop_new */ +enum { + /* the default */ + EVFLAG_AUTO = 0x00000000U, /* not quite a mask */ + /* flag bits */ + EVFLAG_NOENV = 0x01000000U, /* do NOT consult environment */ + EVFLAG_FORKCHECK = 0x02000000U, /* check for a fork in each iteration */ + /* debugging/feature disable */ + EVFLAG_NOINOTIFY = 0x00100000U, /* do not attempt to use inotify */ +#if EV_COMPAT3 + EVFLAG_NOSIGFD = 0, /* compatibility to pre-3.9 */ +#endif + EVFLAG_SIGNALFD = 0x00200000U, /* attempt to use signalfd */ + EVFLAG_NOSIGMASK = 0x00400000U, /* avoid modifying the signal mask */ + EVFLAG_NOTIMERFD = 0x00800000U /* avoid creating a timerfd */ +}; + +/* method bits to be ored together */ +enum { + EVBACKEND_SELECT = 0x00000001U, /* available just about anywhere */ + EVBACKEND_POLL = 0x00000002U, /* !win, !aix, broken on osx */ + EVBACKEND_EPOLL = 0x00000004U, /* linux */ + EVBACKEND_KQUEUE = 0x00000008U, /* bsd, broken on osx */ + EVBACKEND_DEVPOLL = 0x00000010U, /* solaris 8 */ /* NYI */ + EVBACKEND_PORT = 0x00000020U, /* solaris 10 */ + EVBACKEND_LINUXAIO = 0x00000040U, /* linux AIO, 4.19+ */ + EVBACKEND_IOURING = 0x00000080U, /* linux io_uring, 5.1+ */ + EVBACKEND_ALL = 0x000000FFU, /* all known backends */ + EVBACKEND_MASK = 0x0000FFFFU /* all future backends */ +}; + +#if EV_PROTOTYPES +EV_API_DECL int ev_version_major (void) EV_NOEXCEPT; +EV_API_DECL int ev_version_minor (void) EV_NOEXCEPT; + +EV_API_DECL unsigned int ev_supported_backends (void) EV_NOEXCEPT; +EV_API_DECL unsigned int ev_recommended_backends (void) EV_NOEXCEPT; +EV_API_DECL unsigned int ev_embeddable_backends (void) EV_NOEXCEPT; + +EV_API_DECL ev_tstamp ev_time (void) EV_NOEXCEPT; +EV_API_DECL void ev_sleep (ev_tstamp delay) EV_NOEXCEPT; /* sleep for a while */ + +/* Sets the allocation function to use, works like realloc. + * It is used to allocate and free memory. + * If it returns zero when memory needs to be allocated, the library might abort + * or take some potentially destructive action. + * The default is your system realloc function. + */ +EV_API_DECL void ev_set_allocator (void *(*cb)(void *ptr, long size) EV_NOEXCEPT) EV_NOEXCEPT; + +/* set the callback function to call on a + * retryable syscall error + * (such as failed select, poll, epoll_wait) + */ +EV_API_DECL void ev_set_syserr_cb (void (*cb)(const char *msg) EV_NOEXCEPT) EV_NOEXCEPT; + +#if EV_MULTIPLICITY + +/* the default loop is the only one that handles signals and child watchers */ +/* you can call this as often as you like */ +EV_API_DECL struct ev_loop *ev_default_loop (unsigned int flags EV_CPP (= 0)) EV_NOEXCEPT; + +#ifdef EV_API_STATIC +EV_API_DECL struct ev_loop *ev_default_loop_ptr; +#endif + +EV_INLINE struct ev_loop * +ev_default_loop_uc_ (void) EV_NOEXCEPT +{ + extern struct ev_loop *ev_default_loop_ptr; + + return ev_default_loop_ptr; +} + +EV_INLINE int +ev_is_default_loop (EV_P) EV_NOEXCEPT +{ + return EV_A == EV_DEFAULT_UC; +} + +/* create and destroy alternative loops that don't handle signals */ +EV_API_DECL struct ev_loop *ev_loop_new (unsigned int flags EV_CPP (= 0)) EV_NOEXCEPT; + +EV_API_DECL ev_tstamp ev_now (EV_P) EV_NOEXCEPT; /* time w.r.t. timers and the eventloop, updated after each poll */ + +#else + +EV_API_DECL int ev_default_loop (unsigned int flags EV_CPP (= 0)) EV_NOEXCEPT; /* returns true when successful */ + +EV_API_DECL ev_tstamp ev_rt_now; + +EV_INLINE ev_tstamp +ev_now (void) EV_NOEXCEPT +{ + return ev_rt_now; +} + +/* looks weird, but ev_is_default_loop (EV_A) still works if this exists */ +EV_INLINE int +ev_is_default_loop (void) EV_NOEXCEPT +{ + return 1; +} + +#endif /* multiplicity */ + +/* destroy event loops, also works for the default loop */ +EV_API_DECL void ev_loop_destroy (EV_P); + +/* this needs to be called after fork, to duplicate the loop */ +/* when you want to re-use it in the child */ +/* you can call it in either the parent or the child */ +/* you can actually call it at any time, anywhere :) */ +EV_API_DECL void ev_loop_fork (EV_P) EV_NOEXCEPT; + +EV_API_DECL unsigned int ev_backend (EV_P) EV_NOEXCEPT; /* backend in use by loop */ + +EV_API_DECL void ev_now_update (EV_P) EV_NOEXCEPT; /* update event loop time */ + +#if EV_WALK_ENABLE +/* walk (almost) all watchers in the loop of a given type, invoking the */ +/* callback on every such watcher. The callback might stop the watcher, */ +/* but do nothing else with the loop */ +EV_API_DECL void ev_walk (EV_P_ int types, void (*cb)(EV_P_ int type, void *w)) EV_NOEXCEPT; +#endif + +#endif /* prototypes */ + +/* ev_run flags values */ +enum { + EVRUN_NOWAIT = 1, /* do not block/wait */ + EVRUN_ONCE = 2 /* block *once* only */ +}; + +/* ev_break how values */ +enum { + EVBREAK_CANCEL = 0, /* undo unloop */ + EVBREAK_ONE = 1, /* unloop once */ + EVBREAK_ALL = 2 /* unloop all loops */ +}; + +#if EV_PROTOTYPES +EV_API_DECL int ev_run (EV_P_ int flags EV_CPP (= 0)); +EV_API_DECL void ev_break (EV_P_ int how EV_CPP (= EVBREAK_ONE)) EV_NOEXCEPT; /* break out of the loop */ + +/* + * ref/unref can be used to add or remove a refcount on the mainloop. every watcher + * keeps one reference. if you have a long-running watcher you never unregister that + * should not keep ev_loop from running, unref() after starting, and ref() before stopping. + */ +EV_API_DECL void ev_ref (EV_P) EV_NOEXCEPT; +EV_API_DECL void ev_unref (EV_P) EV_NOEXCEPT; + +/* + * convenience function, wait for a single event, without registering an event watcher + * if timeout is < 0, do wait indefinitely + */ +EV_API_DECL void ev_once (EV_P_ int fd, int events, ev_tstamp timeout, void (*cb)(int revents, void *arg), void *arg) EV_NOEXCEPT; + +EV_API_DECL void ev_invoke_pending (EV_P); /* invoke all pending watchers */ + +# if EV_FEATURE_API +EV_API_DECL unsigned int ev_iteration (EV_P) EV_NOEXCEPT; /* number of loop iterations */ +EV_API_DECL unsigned int ev_depth (EV_P) EV_NOEXCEPT; /* #ev_loop enters - #ev_loop leaves */ +EV_API_DECL void ev_verify (EV_P) EV_NOEXCEPT; /* abort if loop data corrupted */ + +EV_API_DECL void ev_set_io_collect_interval (EV_P_ ev_tstamp interval) EV_NOEXCEPT; /* sleep at least this time, default 0 */ +EV_API_DECL void ev_set_timeout_collect_interval (EV_P_ ev_tstamp interval) EV_NOEXCEPT; /* sleep at least this time, default 0 */ + +/* advanced stuff for threading etc. support, see docs */ +EV_API_DECL void ev_set_userdata (EV_P_ void *data) EV_NOEXCEPT; +EV_API_DECL void *ev_userdata (EV_P) EV_NOEXCEPT; +typedef void (*ev_loop_callback)(EV_P); +EV_API_DECL void ev_set_invoke_pending_cb (EV_P_ ev_loop_callback invoke_pending_cb) EV_NOEXCEPT; +/* C++ doesn't allow the use of the ev_loop_callback typedef here, so we need to spell it out */ +EV_API_DECL void ev_set_loop_release_cb (EV_P_ void (*release)(EV_P) EV_NOEXCEPT, void (*acquire)(EV_P) EV_NOEXCEPT) EV_NOEXCEPT; + +EV_API_DECL unsigned int ev_pending_count (EV_P) EV_NOEXCEPT; /* number of pending events, if any */ + +/* + * stop/start the timer handling. + */ +EV_API_DECL void ev_suspend (EV_P) EV_NOEXCEPT; +EV_API_DECL void ev_resume (EV_P) EV_NOEXCEPT; +#endif + +#endif + +/* these may evaluate ev multiple times, and the other arguments at most once */ +/* either use ev_init + ev_TYPE_set, or the ev_TYPE_init macro, below, to first initialise a watcher */ +#define ev_init(ev,cb_) do { \ + ((ev_watcher *)(void *)(ev))->active = \ + ((ev_watcher *)(void *)(ev))->pending = 0; \ + ev_set_priority ((ev), 0); \ + ev_set_cb ((ev), cb_); \ +} while (0) + +#define ev_io_modify(ev,events_) do { (ev)->events = (ev)->events & EV__IOFDSET | (events_); } while (0) +#define ev_io_set(ev,fd_,events_) do { (ev)->fd = (fd_); (ev)->events = (events_) | EV__IOFDSET; } while (0) +#define ev_timer_set(ev,after_,repeat_) do { ((ev_watcher_time *)(ev))->at = (after_); (ev)->repeat = (repeat_); } while (0) +#define ev_periodic_set(ev,ofs_,ival_,rcb_) do { (ev)->offset = (ofs_); (ev)->interval = (ival_); (ev)->reschedule_cb = (rcb_); } while (0) +#define ev_signal_set(ev,signum_) do { (ev)->signum = (signum_); } while (0) +#define ev_child_set(ev,pid_,trace_) do { (ev)->pid = (pid_); (ev)->flags = !!(trace_); } while (0) +#define ev_stat_set(ev,path_,interval_) do { (ev)->path = (path_); (ev)->interval = (interval_); (ev)->wd = -2; } while (0) +#define ev_idle_set(ev) /* nop, yes, this is a serious in-joke */ +#define ev_prepare_set(ev) /* nop, yes, this is a serious in-joke */ +#define ev_check_set(ev) /* nop, yes, this is a serious in-joke */ +#define ev_embed_set(ev,other_) do { (ev)->other = (other_); } while (0) +#define ev_fork_set(ev) /* nop, yes, this is a serious in-joke */ +#define ev_cleanup_set(ev) /* nop, yes, this is a serious in-joke */ +#define ev_async_set(ev) /* nop, yes, this is a serious in-joke */ + +#define ev_io_init(ev,cb,fd,events) do { ev_init ((ev), (cb)); ev_io_set ((ev),(fd),(events)); } while (0) +#define ev_timer_init(ev,cb,after,repeat) do { ev_init ((ev), (cb)); ev_timer_set ((ev),(after),(repeat)); } while (0) +#define ev_periodic_init(ev,cb,ofs,ival,rcb) do { ev_init ((ev), (cb)); ev_periodic_set ((ev),(ofs),(ival),(rcb)); } while (0) +#define ev_signal_init(ev,cb,signum) do { ev_init ((ev), (cb)); ev_signal_set ((ev), (signum)); } while (0) +#define ev_child_init(ev,cb,pid,trace) do { ev_init ((ev), (cb)); ev_child_set ((ev),(pid),(trace)); } while (0) +#define ev_stat_init(ev,cb,path,interval) do { ev_init ((ev), (cb)); ev_stat_set ((ev),(path),(interval)); } while (0) +#define ev_idle_init(ev,cb) do { ev_init ((ev), (cb)); ev_idle_set ((ev)); } while (0) +#define ev_prepare_init(ev,cb) do { ev_init ((ev), (cb)); ev_prepare_set ((ev)); } while (0) +#define ev_check_init(ev,cb) do { ev_init ((ev), (cb)); ev_check_set ((ev)); } while (0) +#define ev_embed_init(ev,cb,other) do { ev_init ((ev), (cb)); ev_embed_set ((ev),(other)); } while (0) +#define ev_fork_init(ev,cb) do { ev_init ((ev), (cb)); ev_fork_set ((ev)); } while (0) +#define ev_cleanup_init(ev,cb) do { ev_init ((ev), (cb)); ev_cleanup_set ((ev)); } while (0) +#define ev_async_init(ev,cb) do { ev_init ((ev), (cb)); ev_async_set ((ev)); } while (0) + +#define ev_is_pending(ev) (0 + ((ev_watcher *)(void *)(ev))->pending) /* ro, true when watcher is waiting for callback invocation */ +#define ev_is_active(ev) (0 + ((ev_watcher *)(void *)(ev))->active) /* ro, true when the watcher has been started */ + +#define ev_cb_(ev) (ev)->cb /* rw */ +#define ev_cb(ev) (memmove (&ev_cb_ (ev), &((ev_watcher *)(ev))->cb, sizeof (ev_cb_ (ev))), (ev)->cb) + +#if EV_MINPRI == EV_MAXPRI +# define ev_priority(ev) ((ev), EV_MINPRI) +# define ev_set_priority(ev,pri) ((ev), (pri)) +#else +# define ev_priority(ev) (+(((ev_watcher *)(void *)(ev))->priority)) +# define ev_set_priority(ev,pri) ( (ev_watcher *)(void *)(ev))->priority = (pri) +#endif + +#define ev_periodic_at(ev) (+((ev_watcher_time *)(ev))->at) + +#ifndef ev_set_cb +/* memmove is used here to avoid strict aliasing violations, and hopefully is optimized out by any reasonable compiler */ +# define ev_set_cb(ev,cb_) (ev_cb_ (ev) = (cb_), memmove (&((ev_watcher *)(ev))->cb, &ev_cb_ (ev), sizeof (ev_cb_ (ev)))) +#endif + +/* stopping (enabling, adding) a watcher does nothing if it is already running */ +/* stopping (disabling, deleting) a watcher does nothing unless it's already running */ +#if EV_PROTOTYPES + +/* feeds an event into a watcher as if the event actually occurred */ +/* accepts any ev_watcher type */ +EV_API_DECL void ev_feed_event (EV_P_ void *w, int revents) EV_NOEXCEPT; +EV_API_DECL void ev_feed_fd_event (EV_P_ int fd, int revents) EV_NOEXCEPT; +#if EV_SIGNAL_ENABLE +EV_API_DECL void ev_feed_signal (int signum) EV_NOEXCEPT; +EV_API_DECL void ev_feed_signal_event (EV_P_ int signum) EV_NOEXCEPT; +#endif +EV_API_DECL void ev_invoke (EV_P_ void *w, int revents); +EV_API_DECL int ev_clear_pending (EV_P_ void *w) EV_NOEXCEPT; + +EV_API_DECL void ev_io_start (EV_P_ ev_io *w) EV_NOEXCEPT; +EV_API_DECL void ev_io_stop (EV_P_ ev_io *w) EV_NOEXCEPT; + +EV_API_DECL void ev_timer_start (EV_P_ ev_timer *w) EV_NOEXCEPT; +EV_API_DECL void ev_timer_stop (EV_P_ ev_timer *w) EV_NOEXCEPT; +/* stops if active and no repeat, restarts if active and repeating, starts if inactive and repeating */ +EV_API_DECL void ev_timer_again (EV_P_ ev_timer *w) EV_NOEXCEPT; +/* return remaining time */ +EV_API_DECL ev_tstamp ev_timer_remaining (EV_P_ ev_timer *w) EV_NOEXCEPT; + +#if EV_PERIODIC_ENABLE +EV_API_DECL void ev_periodic_start (EV_P_ ev_periodic *w) EV_NOEXCEPT; +EV_API_DECL void ev_periodic_stop (EV_P_ ev_periodic *w) EV_NOEXCEPT; +EV_API_DECL void ev_periodic_again (EV_P_ ev_periodic *w) EV_NOEXCEPT; +#endif + +/* only supported in the default loop */ +#if EV_SIGNAL_ENABLE +EV_API_DECL void ev_signal_start (EV_P_ ev_signal *w) EV_NOEXCEPT; +EV_API_DECL void ev_signal_stop (EV_P_ ev_signal *w) EV_NOEXCEPT; +#endif + +/* only supported in the default loop */ +# if EV_CHILD_ENABLE +EV_API_DECL void ev_child_start (EV_P_ ev_child *w) EV_NOEXCEPT; +EV_API_DECL void ev_child_stop (EV_P_ ev_child *w) EV_NOEXCEPT; +# endif + +# if EV_STAT_ENABLE +EV_API_DECL void ev_stat_start (EV_P_ ev_stat *w) EV_NOEXCEPT; +EV_API_DECL void ev_stat_stop (EV_P_ ev_stat *w) EV_NOEXCEPT; +EV_API_DECL void ev_stat_stat (EV_P_ ev_stat *w) EV_NOEXCEPT; +# endif + +# if EV_IDLE_ENABLE +EV_API_DECL void ev_idle_start (EV_P_ ev_idle *w) EV_NOEXCEPT; +EV_API_DECL void ev_idle_stop (EV_P_ ev_idle *w) EV_NOEXCEPT; +# endif + +#if EV_PREPARE_ENABLE +EV_API_DECL void ev_prepare_start (EV_P_ ev_prepare *w) EV_NOEXCEPT; +EV_API_DECL void ev_prepare_stop (EV_P_ ev_prepare *w) EV_NOEXCEPT; +#endif + +#if EV_CHECK_ENABLE +EV_API_DECL void ev_check_start (EV_P_ ev_check *w) EV_NOEXCEPT; +EV_API_DECL void ev_check_stop (EV_P_ ev_check *w) EV_NOEXCEPT; +#endif + +# if EV_FORK_ENABLE +EV_API_DECL void ev_fork_start (EV_P_ ev_fork *w) EV_NOEXCEPT; +EV_API_DECL void ev_fork_stop (EV_P_ ev_fork *w) EV_NOEXCEPT; +# endif + +# if EV_CLEANUP_ENABLE +EV_API_DECL void ev_cleanup_start (EV_P_ ev_cleanup *w) EV_NOEXCEPT; +EV_API_DECL void ev_cleanup_stop (EV_P_ ev_cleanup *w) EV_NOEXCEPT; +# endif + +# if EV_EMBED_ENABLE +/* only supported when loop to be embedded is in fact embeddable */ +EV_API_DECL void ev_embed_start (EV_P_ ev_embed *w) EV_NOEXCEPT; +EV_API_DECL void ev_embed_stop (EV_P_ ev_embed *w) EV_NOEXCEPT; +EV_API_DECL void ev_embed_sweep (EV_P_ ev_embed *w) EV_NOEXCEPT; +# endif + +# if EV_ASYNC_ENABLE +EV_API_DECL void ev_async_start (EV_P_ ev_async *w) EV_NOEXCEPT; +EV_API_DECL void ev_async_stop (EV_P_ ev_async *w) EV_NOEXCEPT; +EV_API_DECL void ev_async_send (EV_P_ ev_async *w) EV_NOEXCEPT; +# endif + +#if EV_COMPAT3 + #define EVLOOP_NONBLOCK EVRUN_NOWAIT + #define EVLOOP_ONESHOT EVRUN_ONCE + #define EVUNLOOP_CANCEL EVBREAK_CANCEL + #define EVUNLOOP_ONE EVBREAK_ONE + #define EVUNLOOP_ALL EVBREAK_ALL + #if EV_PROTOTYPES + EV_INLINE void ev_loop (EV_P_ int flags) { ev_run (EV_A_ flags); } + EV_INLINE void ev_unloop (EV_P_ int how ) { ev_break (EV_A_ how ); } + EV_INLINE void ev_default_destroy (void) { ev_loop_destroy (EV_DEFAULT); } + EV_INLINE void ev_default_fork (void) { ev_loop_fork (EV_DEFAULT); } + #if EV_FEATURE_API + EV_INLINE unsigned int ev_loop_count (EV_P) { return ev_iteration (EV_A); } + EV_INLINE unsigned int ev_loop_depth (EV_P) { return ev_depth (EV_A); } + EV_INLINE void ev_loop_verify (EV_P) { ev_verify (EV_A); } + #endif + #endif +#else + typedef struct ev_loop ev_loop; +#endif + +#endif + +EV_CPP(}) + +#endif + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev.pod b/deps/ngtcp2/ngtcp2/third-party/libev/ev.pod new file mode 100644 index 00000000000000..e4eeb5073fe509 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev.pod @@ -0,0 +1,5741 @@ +=encoding utf-8 + +=head1 NAME + +libev - a high performance full-featured event loop written in C + +=head1 SYNOPSIS + + #include + +=head2 EXAMPLE PROGRAM + + // a single header file is required + #include + + #include // for puts + + // every watcher type has its own typedef'd struct + // with the name ev_TYPE + ev_io stdin_watcher; + ev_timer timeout_watcher; + + // all watcher callbacks have a similar signature + // this callback is called when data is readable on stdin + static void + stdin_cb (EV_P_ ev_io *w, int revents) + { + puts ("stdin ready"); + // for one-shot events, one must manually stop the watcher + // with its corresponding stop function. + ev_io_stop (EV_A_ w); + + // this causes all nested ev_run's to stop iterating + ev_break (EV_A_ EVBREAK_ALL); + } + + // another callback, this time for a time-out + static void + timeout_cb (EV_P_ ev_timer *w, int revents) + { + puts ("timeout"); + // this causes the innermost ev_run to stop iterating + ev_break (EV_A_ EVBREAK_ONE); + } + + int + main (void) + { + // use the default event loop unless you have special needs + struct ev_loop *loop = EV_DEFAULT; + + // initialise an io watcher, then start it + // this one will watch for stdin to become readable + ev_io_init (&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ); + ev_io_start (loop, &stdin_watcher); + + // initialise a timer watcher, then start it + // simple non-repeating 5.5 second timeout + ev_timer_init (&timeout_watcher, timeout_cb, 5.5, 0.); + ev_timer_start (loop, &timeout_watcher); + + // now wait for events to arrive + ev_run (loop, 0); + + // break was called, so exit + return 0; + } + +=head1 ABOUT THIS DOCUMENT + +This document documents the libev software package. + +The newest version of this document is also available as an html-formatted +web page you might find easier to navigate when reading it for the first +time: L. + +While this document tries to be as complete as possible in documenting +libev, its usage and the rationale behind its design, it is not a tutorial +on event-based programming, nor will it introduce event-based programming +with libev. + +Familiarity with event based programming techniques in general is assumed +throughout this document. + +=head1 WHAT TO READ WHEN IN A HURRY + +This manual tries to be very detailed, but unfortunately, this also makes +it very long. If you just want to know the basics of libev, I suggest +reading L, then the L above and +look up the missing functions in L and the C and +C sections in L. + +=head1 ABOUT LIBEV + +Libev is an event loop: you register interest in certain events (such as a +file descriptor being readable or a timeout occurring), and it will manage +these event sources and provide your program with events. + +To do this, it must take more or less complete control over your process +(or thread) by executing the I handler, and will then +communicate events via a callback mechanism. + +You register interest in certain events by registering so-called I, which are relatively small C structures you initialise with the +details of the event, and then hand it over to libev by I the +watcher. + +=head2 FEATURES + +Libev supports C (files, many character devices...). + +Epoll is truly the train wreck among event poll mechanisms, a frankenpoll, +cobbled together in a hurry, no thought to design or interaction with +others. Oh, the pain, will it ever stop... + +While stopping, setting and starting an I/O watcher in the same iteration +will result in some caching, there is still a system call per such +incident (because the same I could point to a different +I now), so its best to avoid that. Also, C'ed +file descriptors might not work very well if you register events for both +file descriptors. + +Best performance from this backend is achieved by not unregistering all +watchers for a file descriptor until it has been closed, if possible, +i.e. keep at least one watcher active per fd at all times. Stopping and +starting a watcher (without re-setting it) also usually doesn't cause +extra overhead. A fork can both result in spurious notifications as well +as in libev having to destroy and recreate the epoll object, which can +take considerable time and thus should be avoided. + +All this means that, in practice, C can be as fast or +faster than epoll for maybe up to a hundred file descriptors, depending on +the usage. So sad. + +While nominally embeddable in other event loops, this feature is broken in +a lot of kernel revisions, but probably(!) works in current versions. + +This backend maps C and C in the same way as +C. + +=item C (value 64, Linux) + +Use the Linux-specific Linux AIO (I C<< aio(7) >> but C<< +io_submit(2) >>) event interface available in post-4.18 kernels (but libev +only tries to use it in 4.19+). + +This is another Linux train wreck of an event interface. + +If this backend works for you (as of this writing, it was very +experimental), it is the best event interface available on Linux and might +be well worth enabling it - if it isn't available in your kernel this will +be detected and this backend will be skipped. + +This backend can batch oneshot requests and supports a user-space ring +buffer to receive events. It also doesn't suffer from most of the design +problems of epoll (such as not being able to remove event sources from +the epoll set), and generally sounds too good to be true. Because, this +being the Linux kernel, of course it suffers from a whole new set of +limitations, forcing you to fall back to epoll, inheriting all its design +issues. + +For one, it is not easily embeddable (but probably could be done using +an event fd at some extra overhead). It also is subject to a system wide +limit that can be configured in F. If no AIO +requests are left, this backend will be skipped during initialisation, and +will switch to epoll when the loop is active. + +Most problematic in practice, however, is that not all file descriptors +work with it. For example, in Linux 5.1, TCP sockets, pipes, event fds, +files, F and many others are supported, but ttys do not work +properly (a known bug that the kernel developers don't care about, see +L), so this is not +(yet?) a generic event polling interface. + +Overall, it seems the Linux developers just don't want it to have a +generic event handling mechanism other than C which have a high +overhead for the actual polling but can deliver many events at once. + +By setting a higher I you allow libev to spend more +time collecting I/O events, so you can handle more events per iteration, +at the cost of increasing latency. Timeouts (both C and +C) will not be affected. Setting this to a non-null value will +introduce an additional C call into most loop iterations. The +sleep time ensures that libev will not poll for I/O events more often then +once per this interval, on average (as long as the host time resolution is +good enough). + +Likewise, by setting a higher I you allow libev +to spend more time collecting timeouts, at the expense of increased +latency/jitter/inexactness (the watcher callback will be called +later). C watchers will not be affected. Setting this to a non-null +value will not introduce any overhead in libev. + +Many (busy) programs can usually benefit by setting the I/O collect +interval to a value near C<0.1> or so, which is often enough for +interactive servers (of course not for games), likewise for timeouts. It +usually doesn't make much sense to set it to a lower value than C<0.01>, +as this approaches the timing granularity of most systems. Note that if +you do transactions with the outside world and you can't increase the +parallelity, then this setting will limit your transaction rate (if you +need to poll once per transaction and the I/O collect interval is 0.01, +then you can't do more than 100 transactions per second). + +Setting the I can improve the opportunity for +saving power, as the program will "bundle" timer callback invocations that +are "near" in time together, by delaying some, thus reducing the number of +times the process sleeps and wakes up again. Another useful technique to +reduce iterations/wake-ups is to use C watchers and make sure +they fire on, say, one-second boundaries only. + +Example: we only need 0.1s timeout granularity, and we wish not to poll +more often than 100 times per second: + + ev_set_timeout_collect_interval (EV_DEFAULT_UC_ 0.1); + ev_set_io_collect_interval (EV_DEFAULT_UC_ 0.01); + +=item ev_invoke_pending (loop) + +This call will simply invoke all pending watchers while resetting their +pending state. Normally, C does this automatically when required, +but when overriding the invoke callback this call comes handy. This +function can be invoked from a watcher - this can be useful for example +when you want to do some lengthy calculation and want to pass further +event handling to another thread (you still have to make sure only one +thread executes within C or C of course). + +=item int ev_pending_count (loop) + +Returns the number of pending watchers - zero indicates that no watchers +are pending. + +=item ev_set_invoke_pending_cb (loop, void (*invoke_pending_cb)(EV_P)) + +This overrides the invoke pending functionality of the loop: Instead of +invoking all pending watchers when there are any, C will call +this callback instead. This is useful, for example, when you want to +invoke the actual watchers inside another context (another thread etc.). + +If you want to reset the callback, use C as new +callback. + +=item ev_set_loop_release_cb (loop, void (*release)(EV_P) throw (), void (*acquire)(EV_P) throw ()) + +Sometimes you want to share the same loop between multiple threads. This +can be done relatively simply by putting mutex_lock/unlock calls around +each call to a libev function. + +However, C can run an indefinite time, so it is not feasible +to wait for it to return. One way around this is to wake up the event +loop via C and C, another way is to set these +I and I callbacks on the loop. + +When set, then C will be called just before the thread is +suspended waiting for new events, and C is called just +afterwards. + +Ideally, C will just call your mutex_unlock function, and +C will just call the mutex_lock function again. + +While event loop modifications are allowed between invocations of +C and C (that's their only purpose after all), no +modifications done will affect the event loop, i.e. adding watchers will +have no effect on the set of file descriptors being watched, or the time +waited. Use an C watcher to wake up C when you want it +to take note of any changes you made. + +In theory, threads executing C will be async-cancel safe between +invocations of C and C. + +See also the locking example in the C section later in this +document. + +=item ev_set_userdata (loop, void *data) + +=item void *ev_userdata (loop) + +Set and retrieve a single C associated with a loop. When +C has never been called, then C returns +C<0>. + +These two functions can be used to associate arbitrary data with a loop, +and are intended solely for the C, C and +C callbacks described above, but of course can be (ab-)used for +any other purpose as well. + +=item ev_verify (loop) + +This function only does something when C support has been +compiled in, which is the default for non-minimal builds. It tries to go +through all internal structures and checks them for validity. If anything +is found to be inconsistent, it will print an error message to standard +error and call C. + +This can be used to catch bugs inside libev itself: under normal +circumstances, this function will never abort as of course libev keeps its +data structures consistent. + +=back + + +=head1 ANATOMY OF A WATCHER + +In the following description, uppercase C in names stands for the +watcher type, e.g. C can mean C for timer +watchers and C for I/O watchers. + +A watcher is an opaque structure that you allocate and register to record +your interest in some event. To make a concrete example, imagine you want +to wait for STDIN to become readable, you would create an C watcher +for that: + + static void my_cb (struct ev_loop *loop, ev_io *w, int revents) + { + ev_io_stop (w); + ev_break (loop, EVBREAK_ALL); + } + + struct ev_loop *loop = ev_default_loop (0); + + ev_io stdin_watcher; + + ev_init (&stdin_watcher, my_cb); + ev_io_set (&stdin_watcher, STDIN_FILENO, EV_READ); + ev_io_start (loop, &stdin_watcher); + + ev_run (loop, 0); + +As you can see, you are responsible for allocating the memory for your +watcher structures (and it is I a bad idea to do this on the +stack). + +Each watcher has an associated watcher structure (called C +or simply C, as typedefs are provided for all watcher structs). + +Each watcher structure must be initialised by a call to C, which expects a callback to be provided. This callback is +invoked each time the event occurs (or, in the case of I/O watchers, each +time the event loop detects that the file descriptor given is readable +and/or writable). + +Each watcher type further has its own C<< ev_TYPE_set (watcher *, ...) >> +macro to configure it, with arguments specific to the watcher type. There +is also a macro to combine initialisation and setting in one call: C<< +ev_TYPE_init (watcher *, callback, ...) >>. + +To make the watcher actually watch out for events, you have to start it +with a watcher-specific start function (C<< ev_TYPE_start (loop, watcher +*) >>), and you can stop watching for events at any time by calling the +corresponding stop function (C<< ev_TYPE_stop (loop, watcher *) >>. + +As long as your watcher is active (has been started but not stopped) you +must not touch the values stored in it except when explicitly documented +otherwise. Most specifically you must never reinitialise it or call its +C macro. + +Each and every callback receives the event loop pointer as first, the +registered watcher structure as second, and a bitset of received events as +third argument. + +The received events usually include a single bit per event type received +(you can receive multiple events at the same time). The possible bit masks +are: + +=over 4 + +=item C + +=item C + +The file descriptor in the C watcher has become readable and/or +writable. + +=item C + +The C watcher has timed out. + +=item C + +The C watcher has timed out. + +=item C + +The signal specified in the C watcher has been received by a thread. + +=item C + +The pid specified in the C watcher has received a status change. + +=item C + +The path specified in the C watcher changed its attributes somehow. + +=item C + +The C watcher has determined that you have nothing better to do. + +=item C + +=item C + +All C watchers are invoked just I C starts to +gather new events, and all C watchers are queued (not invoked) +just after C has gathered them, but before it queues any callbacks +for any received events. That means C watchers are the last +watchers invoked before the event loop sleeps or polls for new events, and +C watchers will be invoked before any other watchers of the same +or lower priority within an event loop iteration. + +Callbacks of both watcher types can start and stop as many watchers as +they want, and all of them will be taken into account (for example, a +C watcher might start an idle watcher to keep C from +blocking). + +=item C + +The embedded event loop specified in the C watcher needs attention. + +=item C + +The event loop has been resumed in the child process after fork (see +C). + +=item C + +The event loop is about to be destroyed (see C). + +=item C + +The given async watcher has been asynchronously notified (see C). + +=item C + +Not ever sent (or otherwise used) by libev itself, but can be freely used +by libev users to signal watchers (e.g. via C). + +=item C + +An unspecified error has occurred, the watcher has been stopped. This might +happen because the watcher could not be properly started because libev +ran out of memory, a file descriptor was found to be closed or any other +problem. Libev considers these application bugs. + +You best act on it by reporting the problem and somehow coping with the +watcher being stopped. Note that well-written programs should not receive +an error ever, so when your watcher receives it, this usually indicates a +bug in your program. + +Libev will usually signal a few "dummy" events together with an error, for +example it might indicate that a fd is readable or writable, and if your +callbacks is well-written it can just attempt the operation and cope with +the error from read() or write(). This will not work in multi-threaded +programs, though, as the fd could already be closed and reused for another +thing, so beware. + +=back + +=head2 GENERIC WATCHER FUNCTIONS + +=over 4 + +=item C (ev_TYPE *watcher, callback) + +This macro initialises the generic portion of a watcher. The contents +of the watcher object can be arbitrary (so C will do). Only +the generic parts of the watcher are initialised, you I to call +the type-specific C macro afterwards to initialise the +type-specific parts. For each type there is also a C macro +which rolls both calls into one. + +You can reinitialise a watcher at any time as long as it has been stopped +(or never started) and there are no pending events outstanding. + +The callback is always of type C. + +Example: Initialise an C watcher in two steps. + + ev_io w; + ev_init (&w, my_cb); + ev_io_set (&w, STDIN_FILENO, EV_READ); + +=item C (ev_TYPE *watcher, [args]) + +This macro initialises the type-specific parts of a watcher. You need to +call C at least once before you call this macro, but you can +call C any number of times. You must not, however, call this +macro on a watcher that is active (it can be pending, however, which is a +difference to the C macro). + +Although some watcher types do not have type-specific arguments +(e.g. C) you still need to call its C macro. + +See C, above, for an example. + +=item C (ev_TYPE *watcher, callback, [args]) + +This convenience macro rolls both C and C macro +calls into a single call. This is the most convenient method to initialise +a watcher. The same limitations apply, of course. + +Example: Initialise and set an C watcher in one step. + + ev_io_init (&w, my_cb, STDIN_FILENO, EV_READ); + +=item C (loop, ev_TYPE *watcher) + +Starts (activates) the given watcher. Only active watchers will receive +events. If the watcher is already active nothing will happen. + +Example: Start the C watcher that is being abused as example in this +whole section. + + ev_io_start (EV_DEFAULT_UC, &w); + +=item C (loop, ev_TYPE *watcher) + +Stops the given watcher if active, and clears the pending status (whether +the watcher was active or not). + +It is possible that stopped watchers are pending - for example, +non-repeating timers are being stopped when they become pending - but +calling C ensures that the watcher is neither active nor +pending. If you want to free or reuse the memory used by the watcher it is +therefore a good idea to always call its C function. + +=item bool ev_is_active (ev_TYPE *watcher) + +Returns a true value iff the watcher is active (i.e. it has been started +and not yet been stopped). As long as a watcher is active you must not modify +it. + +=item bool ev_is_pending (ev_TYPE *watcher) + +Returns a true value iff the watcher is pending, (i.e. it has outstanding +events but its callback has not yet been invoked). As long as a watcher +is pending (but not active) you must not call an init function on it (but +C is safe), you must not change its priority, and you must +make sure the watcher is available to libev (e.g. you cannot C +it). + +=item callback ev_cb (ev_TYPE *watcher) + +Returns the callback currently set on the watcher. + +=item ev_set_cb (ev_TYPE *watcher, callback) + +Change the callback. You can change the callback at virtually any time +(modulo threads). + +=item ev_set_priority (ev_TYPE *watcher, int priority) + +=item int ev_priority (ev_TYPE *watcher) + +Set and query the priority of the watcher. The priority is a small +integer between C (default: C<2>) and C +(default: C<-2>). Pending watchers with higher priority will be invoked +before watchers with lower priority, but priority will not keep watchers +from being executed (except for C watchers). + +If you need to suppress invocation when higher priority events are pending +you need to look at C watchers, which provide this functionality. + +You I change the priority of a watcher as long as it is active or +pending. + +Setting a priority outside the range of C to C is +fine, as long as you do not mind that the priority value you query might +or might not have been clamped to the valid range. + +The default priority used by watchers when no priority has been set is +always C<0>, which is supposed to not be too high and not be too low :). + +See L, below, for a more thorough treatment of +priorities. + +=item ev_invoke (loop, ev_TYPE *watcher, int revents) + +Invoke the C with the given C and C. Neither +C nor C need to be valid as long as the watcher callback +can deal with that fact, as both are simply passed through to the +callback. + +=item int ev_clear_pending (loop, ev_TYPE *watcher) + +If the watcher is pending, this function clears its pending status and +returns its C bitset (as if its callback was invoked). If the +watcher isn't pending it does nothing and returns C<0>. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its +callback to be invoked, which can be accomplished with this function. + +=item ev_feed_event (loop, ev_TYPE *watcher, int revents) + +Feeds the given event set into the event loop, as if the specified event +had happened for the specified watcher (which must be a pointer to an +initialised but not necessarily started event watcher). Obviously you must +not free the watcher as long as it has pending events. + +Stopping the watcher, letting libev invoke it, or calling +C will clear the pending event, even if the watcher was +not started in the first place. + +See also C and C for related +functions that do not need a watcher. + +=back + +See also the L and L idioms. + +=head2 WATCHER STATES + +There are various watcher states mentioned throughout this manual - +active, pending and so on. In this section these states and the rules to +transition between them will be described in more detail - and while these +rules might look complicated, they usually do "the right thing". + +=over 4 + +=item initialised + +Before a watcher can be registered with the event loop it has to be +initialised. This can be done with a call to C, or calls to +C followed by the watcher-specific C function. + +In this state it is simply some block of memory that is suitable for +use in an event loop. It can be moved around, freed, reused etc. at +will - as long as you either keep the memory contents intact, or call +C again. + +=item started/running/active + +Once a watcher has been started with a call to C it becomes +property of the event loop, and is actively waiting for events. While in +this state it cannot be accessed (except in a few documented ways), moved, +freed or anything else - the only legal thing is to keep a pointer to it, +and call libev functions on it that are documented to work on active watchers. + +=item pending + +If a watcher is active and libev determines that an event it is interested +in has occurred (such as a timer expiring), it will become pending. It will +stay in this pending state until either it is stopped or its callback is +about to be invoked, so it is not normally pending inside the watcher +callback. + +The watcher might or might not be active while it is pending (for example, +an expired non-repeating timer can be pending but no longer active). If it +is stopped, it can be freely accessed (e.g. by calling C), +but it is still property of the event loop at this time, so cannot be +moved, freed or reused. And if it is active the rules described in the +previous item still apply. + +It is also possible to feed an event on a watcher that is not active (e.g. +via C), in which case it becomes pending without being +active. + +=item stopped + +A watcher can be stopped implicitly by libev (in which case it might still +be pending), or explicitly by calling its C function. The +latter will clear any pending state the watcher might be in, regardless +of whether it was active or not, so stopping a watcher explicitly before +freeing it is often a good idea. + +While stopped (and not pending) the watcher is essentially in the +initialised state, that is, it can be reused, moved, modified in any way +you wish (but when you trash the memory block, you need to C +it again). + +=back + +=head2 WATCHER PRIORITY MODELS + +Many event loops support I, which are usually small +integers that influence the ordering of event callback invocation +between watchers in some way, all else being equal. + +In libev, watcher priorities can be set using C. See its +description for the more technical details such as the actual priority +range. + +There are two common ways how these these priorities are being interpreted +by event loops: + +In the more common lock-out model, higher priorities "lock out" invocation +of lower priority watchers, which means as long as higher priority +watchers receive events, lower priority watchers are not being invoked. + +The less common only-for-ordering model uses priorities solely to order +callback invocation within a single event loop iteration: Higher priority +watchers are invoked before lower priority ones, but they all get invoked +before polling for new events. + +Libev uses the second (only-for-ordering) model for all its watchers +except for idle watchers (which use the lock-out model). + +The rationale behind this is that implementing the lock-out model for +watchers is not well supported by most kernel interfaces, and most event +libraries will just poll for the same events again and again as long as +their callbacks have not been executed, which is very inefficient in the +common case of one high-priority watcher locking out a mass of lower +priority ones. + +Static (ordering) priorities are most useful when you have two or more +watchers handling the same resource: a typical usage example is having an +C watcher to receive data, and an associated C to handle +timeouts. Under load, data might be received while the program handles +other jobs, but since timers normally get invoked first, the timeout +handler will be executed before checking for data. In that case, giving +the timer a lower priority than the I/O watcher ensures that I/O will be +handled first even under adverse conditions (which is usually, but not +always, what you want). + +Since idle watchers use the "lock-out" model, meaning that idle watchers +will only be executed when no same or higher priority watchers have +received events, they can be used to implement the "lock-out" model when +required. + +For example, to emulate how many other event libraries handle priorities, +you can associate an C watcher to each such watcher, and in +the normal watcher callback, you just start the idle watcher. The real +processing is done in the idle watcher callback. This causes libev to +continuously poll and process kernel event data for the watcher, but when +the lock-out case is known to be rare (which in turn is rare :), this is +workable. + +Usually, however, the lock-out model implemented that way will perform +miserably under the type of load it was designed to handle. In that case, +it might be preferable to stop the real watcher before starting the +idle watcher, so the kernel will not have to process the event in case +the actual processing will be delayed for considerable time. + +Here is an example of an I/O watcher that should run at a strictly lower +priority than the default, and which should only process data when no +other events are pending: + + ev_idle idle; // actual processing watcher + ev_io io; // actual event watcher + + static void + io_cb (EV_P_ ev_io *w, int revents) + { + // stop the I/O watcher, we received the event, but + // are not yet ready to handle it. + ev_io_stop (EV_A_ w); + + // start the idle watcher to handle the actual event. + // it will not be executed as long as other watchers + // with the default priority are receiving events. + ev_idle_start (EV_A_ &idle); + } + + static void + idle_cb (EV_P_ ev_idle *w, int revents) + { + // actual processing + read (STDIN_FILENO, ...); + + // have to start the I/O watcher again, as + // we have handled the event + ev_io_start (EV_P_ &io); + } + + // initialisation + ev_idle_init (&idle, idle_cb); + ev_io_init (&io, io_cb, STDIN_FILENO, EV_READ); + ev_io_start (EV_DEFAULT_ &io); + +In the "real" world, it might also be beneficial to start a timer, so that +low-priority connections can not be locked out forever under load. This +enables your program to keep a lower latency for important connections +during short periods of high load, while not completely locking out less +important ones. + + +=head1 WATCHER TYPES + +This section describes each watcher in detail, but will not repeat +information given in the last section. Any initialisation/set macros, +functions and members specific to the watcher type are explained. + +Most members are additionally marked with either I<[read-only]>, meaning +that, while the watcher is active, you can look at the member and expect +some sensible content, but you must not modify it (you can modify it while +the watcher is stopped to your hearts content), or I<[read-write]>, which +means you can expect it to have some sensible content while the watcher is +active, but you can also modify it (within the same thread as the event +loop, i.e. without creating data races). Modifying it may not do something +sensible or take immediate effect (or do anything at all), but libev will +not crash or malfunction in any way. + +In any case, the documentation for each member will explain what the +effects are, and if there are any additional access restrictions. + +=head2 C - is this file descriptor readable or writable? + +I/O watchers check whether a file descriptor is readable or writable +in each iteration of the event loop, or, more precisely, when reading +would not block the process and writing would at least be able to write +some data. This behaviour is called level-triggering because you keep +receiving events as long as the condition persists. Remember you can stop +the watcher if you don't want to act on the event and neither want to +receive future events. + +In general you can register as many read and/or write event watchers per +fd as you want (as long as you don't confuse yourself). Setting all file +descriptors to non-blocking mode is also usually a good idea (but not +required if you know what you are doing). + +Another thing you have to watch out for is that it is quite easy to +receive "spurious" readiness notifications, that is, your callback might +be called with C but a subsequent C(2) will actually block +because there is no data. It is very easy to get into this situation even +with a relatively standard program structure. Thus it is best to always +use non-blocking I/O: An extra C(2) returning C is far +preferable to a program hanging until some data arrives. + +If you cannot run the fd in non-blocking mode (for example you should +not play around with an Xlib connection), then you have to separately +re-test whether a file descriptor is really ready with a known-to-be good +interface such as poll (fortunately in the case of Xlib, it already does +this on its own, so its quite safe to use). Some people additionally +use C and an interval timer, just to be sure you won't block +indefinitely. + +But really, best use non-blocking mode. + +=head3 The special problem of disappearing file descriptors + +Some backends (e.g. kqueue, epoll, linuxaio) need to be told about closing +a file descriptor (either due to calling C explicitly or any other +means, such as C). The reason is that you register interest in some +file descriptor, but when it goes away, the operating system will silently +drop this interest. If another file descriptor with the same number then +is registered with libev, there is no efficient way to see that this is, +in fact, a different file descriptor. + +To avoid having to explicitly tell libev about such cases, libev follows +the following policy: Each time C is being called, libev +will assume that this is potentially a new file descriptor, otherwise +it is assumed that the file descriptor stays the same. That means that +you I to call C (or C) when you change the +descriptor even if the file descriptor number itself did not change. + +This is how one would do it normally anyway, the important point is that +the libev application should not optimise around libev but should leave +optimisations to libev. + +=head3 The special problem of dup'ed file descriptors + +Some backends (e.g. epoll), cannot register events for file descriptors, +but only events for the underlying file descriptions. That means when you +have C'ed file descriptors or weirder constellations, and register +events for them, only one file descriptor might actually receive events. + +There is no workaround possible except not registering events +for potentially C'ed file descriptors, or to resort to +C or C. + +=head3 The special problem of files + +Many people try to use C. + +=item EV_USE_EVENTFD + +If defined to be C<1>, then libev will assume that C is +available and will probe for kernel support at runtime. This will improve +C and C performance and reduce resource consumption. +If undefined, it will be enabled if the headers indicate GNU/Linux + Glibc +2.7 or newer, otherwise disabled. + +=item EV_USE_SIGNALFD + +If defined to be C<1>, then libev will assume that C is +available and will probe for kernel support at runtime. This enables +the use of EVFLAG_SIGNALFD for faster and simpler signal handling. If +undefined, it will be enabled if the headers indicate GNU/Linux + Glibc +2.7 or newer, otherwise disabled. + +=item EV_USE_TIMERFD + +If defined to be C<1>, then libev will assume that C is +available and will probe for kernel support at runtime. This allows +libev to detect time jumps accurately. If undefined, it will be enabled +if the headers indicate GNU/Linux + Glibc 2.8 or newer and define +C, otherwise disabled. + +=item EV_USE_EVENTFD + +If defined to be C<1>, then libev will assume that C is +available and will probe for kernel support at runtime. This will improve +C and C performance and reduce resource consumption. +If undefined, it will be enabled if the headers indicate GNU/Linux + Glibc +2.7 or newer, otherwise disabled. + +=item EV_USE_SELECT + +If undefined or defined to be C<1>, libev will compile in support for the +C is buggy + +All that's left is C actively limits the number of file +descriptors you can pass in to 1024 - your program suddenly crashes when +you use more. + +There is an undocumented "workaround" for this - defining +C<_DARWIN_UNLIMITED_SELECT>, which libev tries to use, so select I +work on OS/X. + +=head2 SOLARIS PROBLEMS AND WORKAROUNDS + +=head3 C reentrancy + +The default compile environment on Solaris is unfortunately so +thread-unsafe that you can't even use components/libraries compiled +without C<-D_REENTRANT> in a threaded program, which, of course, isn't +defined by default. A valid, if stupid, implementation choice. + +If you want to use libev in threaded environments you have to make sure +it's compiled with C<_REENTRANT> defined. + +=head3 Event port backend + +The scalable event interface for Solaris is called "event +ports". Unfortunately, this mechanism is very buggy in all major +releases. If you run into high CPU usage, your program freezes or you get +a large number of spurious wakeups, make sure you have all the relevant +and latest kernel patches applied. No, I don't know which ones, but there +are multiple ones to apply, and afterwards, event ports actually work +great. + +If you can't get it to work, you can try running the program by setting +the environment variable C to only allow C and +C works fine +with large bitsets on AIX, and AIX is dead anyway. + +=head2 WIN32 PLATFORM LIMITATIONS AND WORKAROUNDS + +=head3 General issues + +Win32 doesn't support any of the standards (e.g. POSIX) that libev +requires, and its I/O model is fundamentally incompatible with the POSIX +model. Libev still offers limited functionality on this platform in +the form of the C backend, and only supports socket +descriptors. This only applies when using Win32 natively, not when using +e.g. cygwin. Actually, it only applies to the microsofts own compilers, +as every compiler comes with a slightly differently broken/incompatible +environment. + +Lifting these limitations would basically require the full +re-implementation of the I/O system. If you are into this kind of thing, +then note that glib does exactly that for you in a very portable way (note +also that glib is the slowest event library known to man). + +There is no supported compilation method available on windows except +embedding it into other applications. + +Sensible signal handling is officially unsupported by Microsoft - libev +tries its best, but under most conditions, signals will simply not work. + +Not a libev limitation but worth mentioning: windows apparently doesn't +accept large writes: instead of resulting in a partial write, windows will +either accept everything or return C if the buffer is too large, +so make sure you only write small amounts into your sockets (less than a +megabyte seems safe, but this apparently depends on the amount of memory +available). + +Due to the many, low, and arbitrary limits on the win32 platform and +the abysmal performance of winsockets, using a large number of sockets +is not recommended (and not reasonable). If your program needs to use +more than a hundred or so sockets, then likely it needs to use a totally +different implementation for windows, as libev offers the POSIX readiness +notification model, which cannot be implemented efficiently on windows +(due to Microsoft monopoly games). + +A typical way to use libev under windows is to embed it (see the embedding +section for details) and use the following F header file instead +of F: + + #define EV_STANDALONE /* keeps ev from requiring config.h */ + #define EV_SELECT_IS_WINSOCKET 1 /* configure libev for windows select */ + + #include "ev.h" + +And compile the following F file into your project (make sure +you do I compile the F or any other embedded source files!): + + #include "evwrap.h" + #include "ev.c" + +=head3 The winsocket C function doesn't follow POSIX in that it +requires socket I and not socket I (it is +also extremely buggy). This makes select very inefficient, and also +requires a mapping from file descriptors to socket handles (the Microsoft +C runtime provides the function C<_open_osfhandle> for this). See the +discussion of the C, C and +C preprocessor symbols for more info. + +The configuration for a "naked" win32 using the Microsoft runtime +libraries and raw winsocket select is: + + #define EV_USE_SELECT 1 + #define EV_SELECT_IS_WINSOCKET 1 /* forces EV_SELECT_USE_FD_SET, too */ + +Note that winsockets handling of fd sets is O(n), so you can easily get a +complexity in the O(n²) range when using win32. + +=head3 Limited number of file descriptors + +Windows has numerous arbitrary (and low) limits on things. + +Early versions of winsocket's select only supported waiting for a maximum +of C<64> handles (probably owning to the fact that all windows kernels +can only wait for C<64> things at the same time internally; Microsoft +recommends spawning a chain of threads and wait for 63 handles and the +previous thread in each. Sounds great!). + +Newer versions support more handles, but you need to define C +to some high number (e.g. C<2048>) before compiling the winsocket select +call (which might be in libev or elsewhere, for example, perl and many +other interpreters do their own select emulation on windows). + +Another limit is the number of file descriptors in the Microsoft runtime +libraries, which by default is C<64> (there must be a hidden I<64> +fetish or something like this inside Microsoft). You can increase this +by calling C<_setmaxstdio>, which can increase this limit to C<2048> +(another arbitrary limit), but is broken in many versions of the Microsoft +runtime libraries. This might get you to about C<512> or C<2048> sockets +(depending on windows version and/or the phase of the moon). To get more, +you need to wrap all I/O functions and provide your own fd management, but +the cost of calling select (O(n²)) will likely make this unworkable. + +=head2 PORTABILITY REQUIREMENTS + +In addition to a working ISO-C implementation and of course the +backend-specific APIs, libev relies on a few additional extensions: + +=over 4 + +=item C must have compatible +calling conventions regardless of C. + +Libev assumes not only that all watcher pointers have the same internal +structure (guaranteed by POSIX but not by ISO C for example), but it also +assumes that the same (machine) code can be used to call any watcher +callback: The watcher callbacks have different type signatures, but libev +calls them using an C internally. + +=item null pointers and integer zero are represented by 0 bytes + +Libev uses C to initialise structs and arrays to C<0> bytes, and +relies on this setting pointers and integers to null. + +=item pointer accesses must be thread-atomic + +Accessing a pointer value must be atomic, it must both be readable and +writable in one piece - this is the case on all current architectures. + +=item C must be thread-atomic as well + +The type C (or whatever is defined as +C) must be atomic with respect to accesses from different +threads. This is not part of the specification for C, but is +believed to be sufficiently portable. + +=item C must work in a threaded environment + +Libev uses C to temporarily block signals. This is not +allowed in a threaded program (C has to be used). Typical +pthread implementations will either allow C in the "main +thread" or will block signals process-wide, both behaviours would +be compatible with libev. Interaction between C and +C could complicate things, however. + +The most portable way to handle signals is to block signals in all threads +except the initial one, and run the signal handling loop in the initial +thread as well. + +=item C must be large enough for common memory allocation sizes + +To improve portability and simplify its API, libev uses C internally +instead of C when allocating its data structures. On non-POSIX +systems (Microsoft...) this might be unexpectedly low, but is still at +least 31 bits everywhere, which is enough for hundreds of millions of +watchers. + +=item C must hold a time value in seconds with enough accuracy + +The type C is used to represent timestamps. It is required to +have at least 51 bits of mantissa (and 9 bits of exponent), which is +good enough for at least into the year 4000 with millisecond accuracy +(the design goal for libev). This requirement is overfulfilled by +implementations using IEEE 754, which is basically all existing ones. + +With IEEE 754 doubles, you get microsecond accuracy until at least the +year 2255 (and millisecond accuracy till the year 287396 - by then, libev +is either obsolete or somebody patched it to use C or +something like that, just kidding). + +=back + +If you know of other additional requirements drop me a note. + + +=head1 ALGORITHMIC COMPLEXITIES + +In this section the complexities of (many of) the algorithms used inside +libev will be documented. For complexity discussions about backends see +the documentation for C. + +All of the following are about amortised time: If an array needs to be +extended, libev needs to realloc and move the whole array, but this +happens asymptotically rarer with higher number of elements, so O(1) might +mean that libev does a lengthy realloc operation in rare cases, but on +average it is much faster and asymptotically approaches constant time. + +=over 4 + +=item Starting and stopping timer/periodic watchers: O(log skipped_other_timers) + +This means that, when you have a watcher that triggers in one hour and +there are 100 watchers that would trigger before that, then inserting will +have to skip roughly seven (C) of these watchers. + +=item Changing timer/periodic watchers (by autorepeat or calling again): O(log skipped_other_timers) + +That means that changing a timer costs less than removing/adding them, +as only the relative motion in the event queue has to be paid for. + +=item Starting io/check/prepare/idle/signal/child/fork/async watchers: O(1) + +These just add the watcher into an array or at the head of a list. + +=item Stopping check/prepare/idle/fork/async watchers: O(1) + +=item Stopping an io/signal/child watcher: O(number_of_watchers_for_this_(fd/signal/pid % EV_PID_HASHSIZE)) + +These watchers are stored in lists, so they need to be walked to find the +correct watcher to remove. The lists are usually short (you don't usually +have many watchers waiting for the same fd or signal: one is typical, two +is rare). + +=item Finding the next timer in each loop iteration: O(1) + +By virtue of using a binary or 4-heap, the next timer is always found at a +fixed position in the storage array. + +=item Each change on a file descriptor per loop iteration: O(number_of_watchers_for_this_fd) + +A change means an I/O watcher gets started or stopped, which requires +libev to recalculate its status (and possibly tell the kernel, depending +on backend and whether C was used). + +=item Activating one watcher (putting it into the pending state): O(1) + +=item Priority handling: O(number_of_priorities) + +Priorities are implemented by allocating some space for each +priority. When doing priority-based operations, libev usually has to +linearly search all the priorities, but starting/stopping and activating +watchers becomes O(1) with respect to priority handling. + +=item Sending an ev_async: O(1) + +=item Processing ev_async_send: O(number_of_async_watchers) + +=item Processing signals: O(max_signal_number) + +Sending involves a system call I there were no other C +calls in the current loop iteration and the loop is currently +blocked. Checking for async and signal events involves iterating over all +running async watchers or all signal numbers. + +=back + + +=head1 PORTING FROM LIBEV 3.X TO 4.X + +The major version 4 introduced some incompatible changes to the API. + +At the moment, the C header file provides compatibility definitions +for all changes, so most programs should still compile. The compatibility +layer might be removed in later versions of libev, so better update to the +new API early than late. + +=over 4 + +=item C backwards compatibility mechanism + +The backward compatibility mechanism can be controlled by +C. See L in the L +section. + +=item C and C have been removed + +These calls can be replaced easily by their C counterparts: + + ev_loop_destroy (EV_DEFAULT_UC); + ev_loop_fork (EV_DEFAULT); + +=item function/symbol renames + +A number of functions and symbols have been renamed: + + ev_loop => ev_run + EVLOOP_NONBLOCK => EVRUN_NOWAIT + EVLOOP_ONESHOT => EVRUN_ONCE + + ev_unloop => ev_break + EVUNLOOP_CANCEL => EVBREAK_CANCEL + EVUNLOOP_ONE => EVBREAK_ONE + EVUNLOOP_ALL => EVBREAK_ALL + + EV_TIMEOUT => EV_TIMER + + ev_loop_count => ev_iteration + ev_loop_depth => ev_depth + ev_loop_verify => ev_verify + +Most functions working on C objects don't have an +C prefix, so it was removed; C, C and +associated constants have been renamed to not collide with the C anymore and C now follows the same naming scheme +as all other watcher types. Note that C is still called +C because it would otherwise clash with the C +typedef. + +=item C mechanism replaced by C + +The preprocessor symbol C has been replaced by a different +mechanism, C. Programs using C usually compile +and work, but the library code will of course be larger. + +=back + + +=head1 GLOSSARY + +=over 4 + +=item active + +A watcher is active as long as it has been started and not yet stopped. +See L for details. + +=item application + +In this document, an application is whatever is using libev. + +=item backend + +The part of the code dealing with the operating system interfaces. + +=item callback + +The address of a function that is called when some event has been +detected. Callbacks are being passed the event loop, the watcher that +received the event, and the actual event bitset. + +=item callback/watcher invocation + +The act of calling the callback associated with a watcher. + +=item event + +A change of state of some external event, such as data now being available +for reading on a file descriptor, time having passed or simply not having +any other events happening anymore. + +In libev, events are represented as single bits (such as C or +C). + +=item event library + +A software package implementing an event model and loop. + +=item event loop + +An entity that handles and processes external events and converts them +into callback invocations. + +=item event model + +The model used to describe how an event loop handles and processes +watchers and events. + +=item pending + +A watcher is pending as soon as the corresponding event has been +detected. See L for details. + +=item real time + +The physical time that is observed. It is apparently strictly monotonic :) + +=item wall-clock time + +The time and date as shown on clocks. Unlike real time, it can actually +be wrong and jump forwards and backwards, e.g. when you adjust your +clock. + +=item watcher + +A data structure that describes interest in certain events. Watchers need +to be started (attached to an event loop) before they can receive events. + +=back + +=head1 AUTHOR + +Marc Lehmann , with repeated corrections by Mikael +Magnusson and Emanuele Giaquinta, and minor corrections by many others. + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev_epoll.c b/deps/ngtcp2/ngtcp2/third-party/libev/ev_epoll.c new file mode 100644 index 00000000000000..58cfa684d5139b --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev_epoll.c @@ -0,0 +1,298 @@ +/* + * libev epoll fd activity backend + * + * Copyright (c) 2007,2008,2009,2010,2011,2016,2017,2019 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +/* + * general notes about epoll: + * + * a) epoll silently removes fds from the fd set. as nothing tells us + * that an fd has been removed otherwise, we have to continually + * "rearm" fds that we suspect *might* have changed (same + * problem with kqueue, but much less costly there). + * b) the fact that ADD != MOD creates a lot of extra syscalls due to a) + * and seems not to have any advantage. + * c) the inability to handle fork or file descriptors (think dup) + * limits the applicability over poll, so this is not a generic + * poll replacement. + * d) epoll doesn't work the same as select with many file descriptors + * (such as files). while not critical, no other advanced interface + * seems to share this (rather non-unixy) limitation. + * e) epoll claims to be embeddable, but in practise you never get + * a ready event for the epoll fd (broken: <=2.6.26, working: >=2.6.32). + * f) epoll_ctl returning EPERM means the fd is always ready. + * + * lots of "weird code" and complication handling in this file is due + * to these design problems with epoll, as we try very hard to avoid + * epoll_ctl syscalls for common usage patterns and handle the breakage + * ensuing from receiving events for closed and otherwise long gone + * file descriptors. + */ + +#include + +#define EV_EMASK_EPERM 0x80 + +static void +epoll_modify (EV_P_ int fd, int oev, int nev) +{ + struct epoll_event ev; + unsigned char oldmask; + + /* + * we handle EPOLL_CTL_DEL by ignoring it here + * on the assumption that the fd is gone anyways + * if that is wrong, we have to handle the spurious + * event in epoll_poll. + * if the fd is added again, we try to ADD it, and, if that + * fails, we assume it still has the same eventmask. + */ + if (!nev) + return; + + oldmask = anfds [fd].emask; + anfds [fd].emask = nev; + + /* store the generation counter in the upper 32 bits, the fd in the lower 32 bits */ + ev.data.u64 = (uint64_t)(uint32_t)fd + | ((uint64_t)(uint32_t)++anfds [fd].egen << 32); + ev.events = (nev & EV_READ ? EPOLLIN : 0) + | (nev & EV_WRITE ? EPOLLOUT : 0); + + if (ecb_expect_true (!epoll_ctl (backend_fd, oev && oldmask != nev ? EPOLL_CTL_MOD : EPOLL_CTL_ADD, fd, &ev))) + return; + + if (ecb_expect_true (errno == ENOENT)) + { + /* if ENOENT then the fd went away, so try to do the right thing */ + if (!nev) + goto dec_egen; + + if (!epoll_ctl (backend_fd, EPOLL_CTL_ADD, fd, &ev)) + return; + } + else if (ecb_expect_true (errno == EEXIST)) + { + /* EEXIST means we ignored a previous DEL, but the fd is still active */ + /* if the kernel mask is the same as the new mask, we assume it hasn't changed */ + if (oldmask == nev) + goto dec_egen; + + if (!epoll_ctl (backend_fd, EPOLL_CTL_MOD, fd, &ev)) + return; + } + else if (ecb_expect_true (errno == EPERM)) + { + /* EPERM means the fd is always ready, but epoll is too snobbish */ + /* to handle it, unlike select or poll. */ + anfds [fd].emask = EV_EMASK_EPERM; + + /* add fd to epoll_eperms, if not already inside */ + if (!(oldmask & EV_EMASK_EPERM)) + { + array_needsize (int, epoll_eperms, epoll_epermmax, epoll_epermcnt + 1, array_needsize_noinit); + epoll_eperms [epoll_epermcnt++] = fd; + } + + return; + } + else + assert (("libev: I/O watcher with invalid fd found in epoll_ctl", errno != EBADF && errno != ELOOP && errno != EINVAL)); + + fd_kill (EV_A_ fd); + +dec_egen: + /* we didn't successfully call epoll_ctl, so decrement the generation counter again */ + --anfds [fd].egen; +} + +static void +epoll_poll (EV_P_ ev_tstamp timeout) +{ + int i; + int eventcnt; + + if (ecb_expect_false (epoll_epermcnt)) + timeout = EV_TS_CONST (0.); + + /* epoll wait times cannot be larger than (LONG_MAX - 999UL) / HZ msecs, which is below */ + /* the default libev max wait time, however. */ + EV_RELEASE_CB; + eventcnt = epoll_wait (backend_fd, epoll_events, epoll_eventmax, EV_TS_TO_MSEC (timeout)); + EV_ACQUIRE_CB; + + if (ecb_expect_false (eventcnt < 0)) + { + if (errno != EINTR) + ev_syserr ("(libev) epoll_wait"); + + return; + } + + for (i = 0; i < eventcnt; ++i) + { + struct epoll_event *ev = epoll_events + i; + + int fd = (uint32_t)ev->data.u64; /* mask out the lower 32 bits */ + int want = anfds [fd].events; + int got = (ev->events & (EPOLLOUT | EPOLLERR | EPOLLHUP) ? EV_WRITE : 0) + | (ev->events & (EPOLLIN | EPOLLERR | EPOLLHUP) ? EV_READ : 0); + + /* + * check for spurious notification. + * this only finds spurious notifications on egen updates + * other spurious notifications will be found by epoll_ctl, below + * we assume that fd is always in range, as we never shrink the anfds array + */ + if (ecb_expect_false ((uint32_t)anfds [fd].egen != (uint32_t)(ev->data.u64 >> 32))) + { + /* recreate kernel state */ + postfork |= 2; + continue; + } + + if (ecb_expect_false (got & ~want)) + { + anfds [fd].emask = want; + + /* + * we received an event but are not interested in it, try mod or del + * this often happens because we optimistically do not unregister fds + * when we are no longer interested in them, but also when we get spurious + * notifications for fds from another process. this is partially handled + * above with the gencounter check (== our fd is not the event fd), and + * partially here, when epoll_ctl returns an error (== a child has the fd + * but we closed it). + * note: for events such as POLLHUP, where we can't know whether it refers + * to EV_READ or EV_WRITE, we might issue redundant EPOLL_CTL_MOD calls. + */ + ev->events = (want & EV_READ ? EPOLLIN : 0) + | (want & EV_WRITE ? EPOLLOUT : 0); + + /* pre-2.6.9 kernels require a non-null pointer with EPOLL_CTL_DEL, */ + /* which is fortunately easy to do for us. */ + if (epoll_ctl (backend_fd, want ? EPOLL_CTL_MOD : EPOLL_CTL_DEL, fd, ev)) + { + postfork |= 2; /* an error occurred, recreate kernel state */ + continue; + } + } + + fd_event (EV_A_ fd, got); + } + + /* if the receive array was full, increase its size */ + if (ecb_expect_false (eventcnt == epoll_eventmax)) + { + ev_free (epoll_events); + epoll_eventmax = array_nextsize (sizeof (struct epoll_event), epoll_eventmax, epoll_eventmax + 1); + epoll_events = (struct epoll_event *)ev_malloc (sizeof (struct epoll_event) * epoll_eventmax); + } + + /* now synthesize events for all fds where epoll fails, while select works... */ + for (i = epoll_epermcnt; i--; ) + { + int fd = epoll_eperms [i]; + unsigned char events = anfds [fd].events & (EV_READ | EV_WRITE); + + if (anfds [fd].emask & EV_EMASK_EPERM && events) + fd_event (EV_A_ fd, events); + else + { + epoll_eperms [i] = epoll_eperms [--epoll_epermcnt]; + anfds [fd].emask = 0; + } + } +} + +static int +epoll_epoll_create (void) +{ + int fd; + +#if defined EPOLL_CLOEXEC && !defined __ANDROID__ + fd = epoll_create1 (EPOLL_CLOEXEC); + + if (fd < 0 && (errno == EINVAL || errno == ENOSYS)) +#endif + { + fd = epoll_create (256); + + if (fd >= 0) + fcntl (fd, F_SETFD, FD_CLOEXEC); + } + + return fd; +} + +inline_size +int +epoll_init (EV_P_ int flags) +{ + if ((backend_fd = epoll_epoll_create ()) < 0) + return 0; + + backend_mintime = EV_TS_CONST (1e-3); /* epoll does sometimes return early, this is just to avoid the worst */ + backend_modify = epoll_modify; + backend_poll = epoll_poll; + + epoll_eventmax = 64; /* initial number of events receivable per poll */ + epoll_events = (struct epoll_event *)ev_malloc (sizeof (struct epoll_event) * epoll_eventmax); + + return EVBACKEND_EPOLL; +} + +inline_size +void +epoll_destroy (EV_P) +{ + ev_free (epoll_events); + array_free (epoll_eperm, EMPTY); +} + +ecb_cold +static void +epoll_fork (EV_P) +{ + close (backend_fd); + + while ((backend_fd = epoll_epoll_create ()) < 0) + ev_syserr ("(libev) epoll_create"); + + fd_rearm_all (EV_A); +} + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev_iouring.c b/deps/ngtcp2/ngtcp2/third-party/libev/ev_iouring.c new file mode 100644 index 00000000000000..bfd3de65f14e0e --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev_iouring.c @@ -0,0 +1,694 @@ +/* + * libev linux io_uring fd activity backend + * + * Copyright (c) 2019-2020 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +/* + * general notes about linux io_uring: + * + * a) it's the best interface I have seen so far. on linux. + * b) best is not necessarily very good. + * c) it's better than the aio mess, doesn't suffer from the fork problems + * of linux aio or epoll and so on and so on. and you could do event stuff + * without any syscalls. what's not to like? + * d) ok, it's vastly more complex, but that's ok, really. + * e) why two mmaps instead of one? one would be more space-efficient, + * and I can't see what benefit two would have (other than being + * somehow resizable/relocatable, but that's apparently not possible). + * f) hmm, it's practically undebuggable (gdb can't access the memory, and + * the bizarre way structure offsets are communicated makes it hard to + * just print the ring buffer heads, even *iff* the memory were visible + * in gdb. but then, that's also ok, really. + * g) well, you cannot specify a timeout when waiting for events. no, + * seriously, the interface doesn't support a timeout. never seen _that_ + * before. sure, you can use a timerfd, but that's another syscall + * you could have avoided. overall, this bizarre omission smells + * like a µ-optimisation by the io_uring author for his personal + * applications, to the detriment of everybody else who just wants + * an event loop. but, umm, ok, if that's all, it could be worse. + * (from what I gather from the author Jens Axboe, it simply didn't + * occur to him, and he made good on it by adding an unlimited nuber + * of timeouts later :). + * h) initially there was a hardcoded limit of 4096 outstanding events. + * later versions not only bump this to 32k, but also can handle + * an unlimited amount of events, so this only affects the batch size. + * i) unlike linux aio, you *can* register more then the limit + * of fd events. while early verisons of io_uring signalled an overflow + * and you ended up getting wet. 5.5+ does not do this anymore. + * j) but, oh my! it had exactly the same bugs as the linux aio backend, + * where some undocumented poll combinations just fail. fortunately, + * after finally reaching the author, he was more than willing to fix + * this probably in 5.6+. + * k) overall, the *API* itself is, I dare to say, not a total trainwreck. + * once the bugs ae fixed (probably in 5.6+), it will be without + * competition. + */ + +/* TODO: use internal TIMEOUT */ +/* TODO: take advantage of single mmap, NODROP etc. */ +/* TODO: resize cq/sq size independently */ + +#include +#include +#include +#include + +#define IOURING_INIT_ENTRIES 32 + +/*****************************************************************************/ +/* syscall wrapdadoop - this section has the raw api/abi definitions */ + +#include +#include + +/* mostly directly taken from the kernel or documentation */ + +struct io_uring_sqe +{ + __u8 opcode; + __u8 flags; + __u16 ioprio; + __s32 fd; + union { + __u64 off; + __u64 addr2; + }; + __u64 addr; + __u32 len; + union { + __kernel_rwf_t rw_flags; + __u32 fsync_flags; + __u16 poll_events; + __u32 sync_range_flags; + __u32 msg_flags; + __u32 timeout_flags; + __u32 accept_flags; + __u32 cancel_flags; + __u32 open_flags; + __u32 statx_flags; + }; + __u64 user_data; + union { + __u16 buf_index; + __u64 __pad2[3]; + }; +}; + +struct io_uring_cqe +{ + __u64 user_data; + __s32 res; + __u32 flags; +}; + +struct io_sqring_offsets +{ + __u32 head; + __u32 tail; + __u32 ring_mask; + __u32 ring_entries; + __u32 flags; + __u32 dropped; + __u32 array; + __u32 resv1; + __u64 resv2; +}; + +struct io_cqring_offsets +{ + __u32 head; + __u32 tail; + __u32 ring_mask; + __u32 ring_entries; + __u32 overflow; + __u32 cqes; + __u64 resv[2]; +}; + +struct io_uring_params +{ + __u32 sq_entries; + __u32 cq_entries; + __u32 flags; + __u32 sq_thread_cpu; + __u32 sq_thread_idle; + __u32 features; + __u32 resv[4]; + struct io_sqring_offsets sq_off; + struct io_cqring_offsets cq_off; +}; + +#define IORING_SETUP_CQSIZE 0x00000008 + +#define IORING_OP_POLL_ADD 6 +#define IORING_OP_POLL_REMOVE 7 +#define IORING_OP_TIMEOUT 11 +#define IORING_OP_TIMEOUT_REMOVE 12 + +/* relative or absolute, reference clock is CLOCK_MONOTONIC */ +struct iouring_kernel_timespec +{ + int64_t tv_sec; + long long tv_nsec; +}; + +#define IORING_TIMEOUT_ABS 0x00000001 + +#define IORING_ENTER_GETEVENTS 0x01 + +#define IORING_OFF_SQ_RING 0x00000000ULL +#define IORING_OFF_CQ_RING 0x08000000ULL +#define IORING_OFF_SQES 0x10000000ULL + +#define IORING_FEAT_SINGLE_MMAP 0x00000001 +#define IORING_FEAT_NODROP 0x00000002 +#define IORING_FEAT_SUBMIT_STABLE 0x00000004 + +inline_size +int +evsys_io_uring_setup (unsigned entries, struct io_uring_params *params) +{ + return ev_syscall2 (SYS_io_uring_setup, entries, params); +} + +inline_size +int +evsys_io_uring_enter (int fd, unsigned to_submit, unsigned min_complete, unsigned flags, const sigset_t *sig, size_t sigsz) +{ + return ev_syscall6 (SYS_io_uring_enter, fd, to_submit, min_complete, flags, sig, sigsz); +} + +/*****************************************************************************/ +/* actual backed implementation */ + +/* we hope that volatile will make the compiler access this variables only once */ +#define EV_SQ_VAR(name) *(volatile unsigned *)((char *)iouring_sq_ring + iouring_sq_ ## name) +#define EV_CQ_VAR(name) *(volatile unsigned *)((char *)iouring_cq_ring + iouring_cq_ ## name) + +/* the index array */ +#define EV_SQ_ARRAY ((unsigned *)((char *)iouring_sq_ring + iouring_sq_array)) + +/* the submit/completion queue entries */ +#define EV_SQES ((struct io_uring_sqe *) iouring_sqes) +#define EV_CQES ((struct io_uring_cqe *)((char *)iouring_cq_ring + iouring_cq_cqes)) + +inline_speed +int +iouring_enter (EV_P_ ev_tstamp timeout) +{ + int res; + + EV_RELEASE_CB; + + res = evsys_io_uring_enter (iouring_fd, iouring_to_submit, 1, + timeout > EV_TS_CONST (0.) ? IORING_ENTER_GETEVENTS : 0, 0, 0); + + assert (("libev: io_uring_enter did not consume all sqes", (res < 0 || res == iouring_to_submit))); + + iouring_to_submit = 0; + + EV_ACQUIRE_CB; + + return res; +} + +/* TODO: can we move things around so we don't need this forward-reference? */ +static void +iouring_poll (EV_P_ ev_tstamp timeout); + +static +struct io_uring_sqe * +iouring_sqe_get (EV_P) +{ + unsigned tail; + + for (;;) + { + tail = EV_SQ_VAR (tail); + + if (ecb_expect_true (tail + 1 - EV_SQ_VAR (head) <= EV_SQ_VAR (ring_entries))) + break; /* whats the problem, we have free sqes */ + + /* queue full, need to flush and possibly handle some events */ + +#if EV_FEATURE_CODE + /* first we ask the kernel nicely, most often this frees up some sqes */ + int res = iouring_enter (EV_A_ EV_TS_CONST (0.)); + + ECB_MEMORY_FENCE_ACQUIRE; /* better safe than sorry */ + + if (res >= 0) + continue; /* yes, it worked, try again */ +#endif + + /* some problem, possibly EBUSY - do the full poll and let it handle any issues */ + + iouring_poll (EV_A_ EV_TS_CONST (0.)); + /* iouring_poll should have done ECB_MEMORY_FENCE_ACQUIRE for us */ + } + + /*assert (("libev: io_uring queue full after flush", tail + 1 - EV_SQ_VAR (head) <= EV_SQ_VAR (ring_entries)));*/ + + return EV_SQES + (tail & EV_SQ_VAR (ring_mask)); +} + +inline_size +struct io_uring_sqe * +iouring_sqe_submit (EV_P_ struct io_uring_sqe *sqe) +{ + unsigned idx = sqe - EV_SQES; + + EV_SQ_ARRAY [idx] = idx; + ECB_MEMORY_FENCE_RELEASE; + ++EV_SQ_VAR (tail); + /*ECB_MEMORY_FENCE_RELEASE; /* for the time being we assume this is not needed */ + ++iouring_to_submit; +} + +/*****************************************************************************/ + +/* when the timerfd expires we simply note the fact, + * as the purpose of the timerfd is to wake us up, nothing else. + * the next iteration should re-set it. + */ +static void +iouring_tfd_cb (EV_P_ struct ev_io *w, int revents) +{ + iouring_tfd_to = EV_TSTAMP_HUGE; +} + +/* called for full and partial cleanup */ +ecb_cold +static int +iouring_internal_destroy (EV_P) +{ + close (iouring_tfd); + close (iouring_fd); + + if (iouring_sq_ring != MAP_FAILED) munmap (iouring_sq_ring, iouring_sq_ring_size); + if (iouring_cq_ring != MAP_FAILED) munmap (iouring_cq_ring, iouring_cq_ring_size); + if (iouring_sqes != MAP_FAILED) munmap (iouring_sqes , iouring_sqes_size ); + + if (ev_is_active (&iouring_tfd_w)) + { + ev_ref (EV_A); + ev_io_stop (EV_A_ &iouring_tfd_w); + } +} + +ecb_cold +static int +iouring_internal_init (EV_P) +{ + struct io_uring_params params = { 0 }; + + iouring_to_submit = 0; + + iouring_tfd = -1; + iouring_sq_ring = MAP_FAILED; + iouring_cq_ring = MAP_FAILED; + iouring_sqes = MAP_FAILED; + + if (!have_monotonic) /* cannot really happen, but what if11 */ + return -1; + + for (;;) + { + iouring_fd = evsys_io_uring_setup (iouring_entries, ¶ms); + + if (iouring_fd >= 0) + break; /* yippie */ + + if (errno != EINVAL) + return -1; /* we failed */ + +#if TODO + if ((~params.features) & (IORING_FEAT_NODROP | IORING_FEATURE_SINGLE_MMAP | IORING_FEAT_SUBMIT_STABLE)) + return -1; /* we require the above features */ +#endif + + /* EINVAL: lots of possible reasons, but maybe + * it is because we hit the unqueryable hardcoded size limit + */ + + /* we hit the limit already, give up */ + if (iouring_max_entries) + return -1; + + /* first time we hit EINVAL? assume we hit the limit, so go back and retry */ + iouring_entries >>= 1; + iouring_max_entries = iouring_entries; + } + + iouring_sq_ring_size = params.sq_off.array + params.sq_entries * sizeof (unsigned); + iouring_cq_ring_size = params.cq_off.cqes + params.cq_entries * sizeof (struct io_uring_cqe); + iouring_sqes_size = params.sq_entries * sizeof (struct io_uring_sqe); + + iouring_sq_ring = mmap (0, iouring_sq_ring_size, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_POPULATE, iouring_fd, IORING_OFF_SQ_RING); + iouring_cq_ring = mmap (0, iouring_cq_ring_size, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_POPULATE, iouring_fd, IORING_OFF_CQ_RING); + iouring_sqes = mmap (0, iouring_sqes_size, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_POPULATE, iouring_fd, IORING_OFF_SQES); + + if (iouring_sq_ring == MAP_FAILED || iouring_cq_ring == MAP_FAILED || iouring_sqes == MAP_FAILED) + return -1; + + iouring_sq_head = params.sq_off.head; + iouring_sq_tail = params.sq_off.tail; + iouring_sq_ring_mask = params.sq_off.ring_mask; + iouring_sq_ring_entries = params.sq_off.ring_entries; + iouring_sq_flags = params.sq_off.flags; + iouring_sq_dropped = params.sq_off.dropped; + iouring_sq_array = params.sq_off.array; + + iouring_cq_head = params.cq_off.head; + iouring_cq_tail = params.cq_off.tail; + iouring_cq_ring_mask = params.cq_off.ring_mask; + iouring_cq_ring_entries = params.cq_off.ring_entries; + iouring_cq_overflow = params.cq_off.overflow; + iouring_cq_cqes = params.cq_off.cqes; + + iouring_tfd = timerfd_create (CLOCK_MONOTONIC, TFD_CLOEXEC); + + if (iouring_tfd < 0) + return iouring_tfd; + + iouring_tfd_to = EV_TSTAMP_HUGE; + + return 0; +} + +ecb_cold +static void +iouring_fork (EV_P) +{ + iouring_internal_destroy (EV_A); + + while (iouring_internal_init (EV_A) < 0) + ev_syserr ("(libev) io_uring_setup"); + + fd_rearm_all (EV_A); + + ev_io_stop (EV_A_ &iouring_tfd_w); + ev_io_set (EV_A_ &iouring_tfd_w, iouring_tfd, EV_READ); + ev_io_start (EV_A_ &iouring_tfd_w); +} + +/*****************************************************************************/ + +static void +iouring_modify (EV_P_ int fd, int oev, int nev) +{ + if (oev) + { + /* we assume the sqe's are all "properly" initialised */ + struct io_uring_sqe *sqe = iouring_sqe_get (EV_A); + sqe->opcode = IORING_OP_POLL_REMOVE; + sqe->fd = fd; + /* Jens Axboe notified me that user_data is not what is documented, but is + * some kind of unique ID that has to match, otherwise the request cannot + * be removed. Since we don't *really* have that, we pass in the old + * generation counter - if that fails, too bad, it will hopefully be removed + * at close time and then be ignored. */ + sqe->addr = (uint32_t)fd | ((__u64)(uint32_t)anfds [fd].egen << 32); + sqe->user_data = (uint64_t)-1; + iouring_sqe_submit (EV_A_ sqe); + + /* increment generation counter to avoid handling old events */ + ++anfds [fd].egen; + } + + if (nev) + { + struct io_uring_sqe *sqe = iouring_sqe_get (EV_A); + sqe->opcode = IORING_OP_POLL_ADD; + sqe->fd = fd; + sqe->addr = 0; + sqe->user_data = (uint32_t)fd | ((__u64)(uint32_t)anfds [fd].egen << 32); + sqe->poll_events = + (nev & EV_READ ? POLLIN : 0) + | (nev & EV_WRITE ? POLLOUT : 0); + iouring_sqe_submit (EV_A_ sqe); + } +} + +inline_size +void +iouring_tfd_update (EV_P_ ev_tstamp timeout) +{ + ev_tstamp tfd_to = mn_now + timeout; + + /* we assume there will be many iterations per timer change, so + * we only re-set the timerfd when we have to because its expiry + * is too late. + */ + if (ecb_expect_false (tfd_to < iouring_tfd_to)) + { + struct itimerspec its; + + iouring_tfd_to = tfd_to; + EV_TS_SET (its.it_interval, 0.); + EV_TS_SET (its.it_value, tfd_to); + + if (timerfd_settime (iouring_tfd, TFD_TIMER_ABSTIME, &its, 0) < 0) + assert (("libev: iouring timerfd_settime failed", 0)); + } +} + +inline_size +void +iouring_process_cqe (EV_P_ struct io_uring_cqe *cqe) +{ + int fd = cqe->user_data & 0xffffffffU; + uint32_t gen = cqe->user_data >> 32; + int res = cqe->res; + + /* user_data -1 is a remove that we are not atm. interested in */ + if (cqe->user_data == (uint64_t)-1) + return; + + assert (("libev: io_uring fd must be in-bounds", fd >= 0 && fd < anfdmax)); + + /* documentation lies, of course. the result value is NOT like + * normal syscalls, but like linux raw syscalls, i.e. negative + * error numbers. fortunate, as otherwise there would be no way + * to get error codes at all. still, why not document this? + */ + + /* ignore event if generation doesn't match */ + /* other than skipping removal events, */ + /* this should actually be very rare */ + if (ecb_expect_false (gen != (uint32_t)anfds [fd].egen)) + return; + + if (ecb_expect_false (res < 0)) + { + /*TODO: EINVAL handling (was something failed with this fd)*/ + + if (res == -EBADF) + { + assert (("libev: event loop rejected bad fd", res != -EBADF)); + fd_kill (EV_A_ fd); + } + else + { + errno = -res; + ev_syserr ("(libev) IORING_OP_POLL_ADD"); + } + + return; + } + + /* feed events, we do not expect or handle POLLNVAL */ + fd_event ( + EV_A_ + fd, + (res & (POLLOUT | POLLERR | POLLHUP) ? EV_WRITE : 0) + | (res & (POLLIN | POLLERR | POLLHUP) ? EV_READ : 0) + ); + + /* io_uring is oneshot, so we need to re-arm the fd next iteration */ + /* this also means we usually have to do at least one syscall per iteration */ + anfds [fd].events = 0; + fd_change (EV_A_ fd, EV_ANFD_REIFY); +} + +/* called when the event queue overflows */ +ecb_cold +static void +iouring_overflow (EV_P) +{ + /* we have two options, resize the queue (by tearing down + * everything and recreating it, or living with it + * and polling. + * we implement this by resizing the queue, and, if that fails, + * we just recreate the state on every failure, which + * kind of is a very inefficient poll. + * one danger is, due to the bios toward lower fds, + * we will only really get events for those, so + * maybe we need a poll() fallback, after all. + */ + /*EV_CQ_VAR (overflow) = 0;*/ /* need to do this if we keep the state and poll manually */ + + fd_rearm_all (EV_A); + + /* we double the size until we hit the hard-to-probe maximum */ + if (!iouring_max_entries) + { + iouring_entries <<= 1; + iouring_fork (EV_A); + } + else + { + /* we hit the kernel limit, we should fall back to something else. + * we can either poll() a few times and hope for the best, + * poll always, or switch to epoll. + * TODO: is this necessary with newer kernels? + */ + + iouring_internal_destroy (EV_A); + + /* this should make it so that on return, we don't call any uring functions */ + iouring_to_submit = 0; + + for (;;) + { + backend = epoll_init (EV_A_ 0); + + if (backend) + break; + + ev_syserr ("(libev) iouring switch to epoll"); + } + } +} + +/* handle any events in the completion queue, return true if there were any */ +static int +iouring_handle_cq (EV_P) +{ + unsigned head, tail, mask; + + head = EV_CQ_VAR (head); + ECB_MEMORY_FENCE_ACQUIRE; + tail = EV_CQ_VAR (tail); + + if (head == tail) + return 0; + + /* it can only overflow if we have events, yes, yes? */ + if (ecb_expect_false (EV_CQ_VAR (overflow))) + { + iouring_overflow (EV_A); + return 1; + } + + mask = EV_CQ_VAR (ring_mask); + + do + iouring_process_cqe (EV_A_ &EV_CQES [head++ & mask]); + while (head != tail); + + EV_CQ_VAR (head) = head; + ECB_MEMORY_FENCE_RELEASE; + + return 1; +} + +static void +iouring_poll (EV_P_ ev_tstamp timeout) +{ + /* if we have events, no need for extra syscalls, but we might have to queue events */ + /* we also clar the timeout if there are outstanding fdchanges */ + /* the latter should only happen if both the sq and cq are full, most likely */ + /* because we have a lot of event sources that immediately complete */ + /* TODO: fdchacngecnt is always 0 because fd_reify does not have two buffers yet */ + if (iouring_handle_cq (EV_A) || fdchangecnt) + timeout = EV_TS_CONST (0.); + else + /* no events, so maybe wait for some */ + iouring_tfd_update (EV_A_ timeout); + + /* only enter the kernel if we have something to submit, or we need to wait */ + if (timeout || iouring_to_submit) + { + int res = iouring_enter (EV_A_ timeout); + + if (ecb_expect_false (res < 0)) + if (errno == EINTR) + /* ignore */; + else if (errno == EBUSY) + /* cq full, cannot submit - should be rare because we flush the cq first, so simply ignore */; + else + ev_syserr ("(libev) iouring setup"); + else + iouring_handle_cq (EV_A); + } +} + +inline_size +int +iouring_init (EV_P_ int flags) +{ + iouring_entries = IOURING_INIT_ENTRIES; + iouring_max_entries = 0; + + if (iouring_internal_init (EV_A) < 0) + { + iouring_internal_destroy (EV_A); + return 0; + } + + ev_io_init (&iouring_tfd_w, iouring_tfd_cb, iouring_tfd, EV_READ); + ev_set_priority (&iouring_tfd_w, EV_MINPRI); + ev_io_start (EV_A_ &iouring_tfd_w); + ev_unref (EV_A); /* watcher should not keep loop alive */ + + backend_modify = iouring_modify; + backend_poll = iouring_poll; + + return EVBACKEND_IOURING; +} + +inline_size +void +iouring_destroy (EV_P) +{ + iouring_internal_destroy (EV_A); +} + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev_kqueue.c b/deps/ngtcp2/ngtcp2/third-party/libev/ev_kqueue.c new file mode 100644 index 00000000000000..69c5147f138884 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev_kqueue.c @@ -0,0 +1,224 @@ +/* + * libev kqueue backend + * + * Copyright (c) 2007,2008,2009,2010,2011,2012,2013,2016,2019 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +#include +#include +#include +#include +#include + +inline_speed +void +kqueue_change (EV_P_ int fd, int filter, int flags, int fflags) +{ + ++kqueue_changecnt; + array_needsize (struct kevent, kqueue_changes, kqueue_changemax, kqueue_changecnt, array_needsize_noinit); + + EV_SET (&kqueue_changes [kqueue_changecnt - 1], fd, filter, flags, fflags, 0, 0); +} + +/* OS X at least needs this */ +#ifndef EV_ENABLE +# define EV_ENABLE 0 +#endif +#ifndef NOTE_EOF +# define NOTE_EOF 0 +#endif + +static void +kqueue_modify (EV_P_ int fd, int oev, int nev) +{ + if (oev != nev) + { + if (oev & EV_READ) + kqueue_change (EV_A_ fd, EVFILT_READ , EV_DELETE, 0); + + if (oev & EV_WRITE) + kqueue_change (EV_A_ fd, EVFILT_WRITE, EV_DELETE, 0); + } + + /* to detect close/reopen reliably, we have to re-add */ + /* event requests even when oev == nev */ + + if (nev & EV_READ) + kqueue_change (EV_A_ fd, EVFILT_READ , EV_ADD | EV_ENABLE, NOTE_EOF); + + if (nev & EV_WRITE) + kqueue_change (EV_A_ fd, EVFILT_WRITE, EV_ADD | EV_ENABLE, NOTE_EOF); +} + +static void +kqueue_poll (EV_P_ ev_tstamp timeout) +{ + int res, i; + struct timespec ts; + + /* need to resize so there is enough space for errors */ + if (kqueue_changecnt > kqueue_eventmax) + { + ev_free (kqueue_events); + kqueue_eventmax = array_nextsize (sizeof (struct kevent), kqueue_eventmax, kqueue_changecnt); + kqueue_events = (struct kevent *)ev_malloc (sizeof (struct kevent) * kqueue_eventmax); + } + + EV_RELEASE_CB; + EV_TS_SET (ts, timeout); + res = kevent (backend_fd, kqueue_changes, kqueue_changecnt, kqueue_events, kqueue_eventmax, &ts); + EV_ACQUIRE_CB; + kqueue_changecnt = 0; + + if (ecb_expect_false (res < 0)) + { + if (errno != EINTR) + ev_syserr ("(libev) kqueue kevent"); + + return; + } + + for (i = 0; i < res; ++i) + { + int fd = kqueue_events [i].ident; + + if (ecb_expect_false (kqueue_events [i].flags & EV_ERROR)) + { + int err = kqueue_events [i].data; + + /* we are only interested in errors for fds that we are interested in :) */ + if (anfds [fd].events) + { + if (err == ENOENT) /* resubmit changes on ENOENT */ + kqueue_modify (EV_A_ fd, 0, anfds [fd].events); + else if (err == EBADF) /* on EBADF, we re-check the fd */ + { + if (fd_valid (fd)) + kqueue_modify (EV_A_ fd, 0, anfds [fd].events); + else + { + assert (("libev: kqueue found invalid fd", 0)); + fd_kill (EV_A_ fd); + } + } + else /* on all other errors, we error out on the fd */ + { + assert (("libev: kqueue found invalid fd", 0)); + fd_kill (EV_A_ fd); + } + } + } + else + fd_event ( + EV_A_ + fd, + kqueue_events [i].filter == EVFILT_READ ? EV_READ + : kqueue_events [i].filter == EVFILT_WRITE ? EV_WRITE + : 0 + ); + } + + if (ecb_expect_false (res == kqueue_eventmax)) + { + ev_free (kqueue_events); + kqueue_eventmax = array_nextsize (sizeof (struct kevent), kqueue_eventmax, kqueue_eventmax + 1); + kqueue_events = (struct kevent *)ev_malloc (sizeof (struct kevent) * kqueue_eventmax); + } +} + +inline_size +int +kqueue_init (EV_P_ int flags) +{ + /* initialize the kernel queue */ + kqueue_fd_pid = getpid (); + if ((backend_fd = kqueue ()) < 0) + return 0; + + fcntl (backend_fd, F_SETFD, FD_CLOEXEC); /* not sure if necessary, hopefully doesn't hurt */ + + backend_mintime = EV_TS_CONST (1e-9); /* apparently, they did the right thing in freebsd */ + backend_modify = kqueue_modify; + backend_poll = kqueue_poll; + + kqueue_eventmax = 64; /* initial number of events receivable per poll */ + kqueue_events = (struct kevent *)ev_malloc (sizeof (struct kevent) * kqueue_eventmax); + + kqueue_changes = 0; + kqueue_changemax = 0; + kqueue_changecnt = 0; + + return EVBACKEND_KQUEUE; +} + +inline_size +void +kqueue_destroy (EV_P) +{ + ev_free (kqueue_events); + ev_free (kqueue_changes); +} + +inline_size +void +kqueue_fork (EV_P) +{ + /* some BSD kernels don't just destroy the kqueue itself, + * but also close the fd, which isn't documented, and + * impossible to support properly. + * we remember the pid of the kqueue call and only close + * the fd if the pid is still the same. + * this leaks fds on sane kernels, but BSD interfaces are + * notoriously buggy and rarely get fixed. + */ + pid_t newpid = getpid (); + + if (newpid == kqueue_fd_pid) + close (backend_fd); + + kqueue_fd_pid = newpid; + while ((backend_fd = kqueue ()) < 0) + ev_syserr ("(libev) kqueue"); + + fcntl (backend_fd, F_SETFD, FD_CLOEXEC); + + /* re-register interest in fds */ + fd_rearm_all (EV_A); +} + +/* sys/event.h defines EV_ERROR */ +#undef EV_ERROR + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev_linuxaio.c b/deps/ngtcp2/ngtcp2/third-party/libev/ev_linuxaio.c new file mode 100644 index 00000000000000..4687a703e8d592 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev_linuxaio.c @@ -0,0 +1,620 @@ +/* + * libev linux aio fd activity backend + * + * Copyright (c) 2019 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +/* + * general notes about linux aio: + * + * a) at first, the linux aio IOCB_CMD_POLL functionality introduced in + * 4.18 looks too good to be true: both watchers and events can be + * batched, and events can even be handled in userspace using + * a ring buffer shared with the kernel. watchers can be canceled + * regardless of whether the fd has been closed. no problems with fork. + * ok, the ring buffer is 200% undocumented (there isn't even a + * header file), but otherwise, it's pure bliss! + * b) ok, watchers are one-shot, so you have to re-arm active ones + * on every iteration. so much for syscall-less event handling, + * but at least these re-arms can be batched, no big deal, right? + * c) well, linux as usual: the documentation lies to you: io_submit + * sometimes returns EINVAL because the kernel doesn't feel like + * handling your poll mask - ttys can be polled for POLLOUT, + * POLLOUT|POLLIN, but polling for POLLIN fails. just great, + * so we have to fall back to something else (hello, epoll), + * but at least the fallback can be slow, because these are + * exceptional cases, right? + * d) hmm, you have to tell the kernel the maximum number of watchers + * you want to queue when initialising the aio context. but of + * course the real limit is magically calculated in the kernel, and + * is often higher then we asked for. so we just have to destroy + * the aio context and re-create it a bit larger if we hit the limit. + * (starts to remind you of epoll? well, it's a bit more deterministic + * and less gambling, but still ugly as hell). + * e) that's when you find out you can also hit an arbitrary system-wide + * limit. or the kernel simply doesn't want to handle your watchers. + * what the fuck do we do then? you guessed it, in the middle + * of event handling we have to switch to 100% epoll polling. and + * that better is as fast as normal epoll polling, so you practically + * have to use the normal epoll backend with all its quirks. + * f) end result of this train wreck: it inherits all the disadvantages + * from epoll, while adding a number on its own. why even bother to use + * it? because if conditions are right and your fds are supported and you + * don't hit a limit, this backend is actually faster, doesn't gamble with + * your fds, batches watchers and events and doesn't require costly state + * recreates. well, until it does. + * g) all of this makes this backend use almost twice as much code as epoll. + * which in turn uses twice as much code as poll. and that#s not counting + * the fact that this backend also depends on the epoll backend, making + * it three times as much code as poll, or kqueue. + * h) bleah. why can't linux just do kqueue. sure kqueue is ugly, but by now + * it's clear that whatever linux comes up with is far, far, far worse. + */ + +#include /* actually linux/time.h, but we must assume they are compatible */ +#include +#include + +/*****************************************************************************/ +/* syscall wrapdadoop - this section has the raw api/abi definitions */ + +#include /* no glibc wrappers */ + +/* aio_abi.h is not versioned in any way, so we cannot test for its existance */ +#define IOCB_CMD_POLL 5 + +/* taken from linux/fs/aio.c. yup, that's a .c file. + * not only is this totally undocumented, not even the source code + * can tell you what the future semantics of compat_features and + * incompat_features are, or what header_length actually is for. + */ +#define AIO_RING_MAGIC 0xa10a10a1 +#define EV_AIO_RING_INCOMPAT_FEATURES 0 +struct aio_ring +{ + unsigned id; /* kernel internal index number */ + unsigned nr; /* number of io_events */ + unsigned head; /* Written to by userland or by kernel. */ + unsigned tail; + + unsigned magic; + unsigned compat_features; + unsigned incompat_features; + unsigned header_length; /* size of aio_ring */ + + struct io_event io_events[0]; +}; + +inline_size +int +evsys_io_setup (unsigned nr_events, aio_context_t *ctx_idp) +{ + return ev_syscall2 (SYS_io_setup, nr_events, ctx_idp); +} + +inline_size +int +evsys_io_destroy (aio_context_t ctx_id) +{ + return ev_syscall1 (SYS_io_destroy, ctx_id); +} + +inline_size +int +evsys_io_submit (aio_context_t ctx_id, long nr, struct iocb *cbp[]) +{ + return ev_syscall3 (SYS_io_submit, ctx_id, nr, cbp); +} + +inline_size +int +evsys_io_cancel (aio_context_t ctx_id, struct iocb *cbp, struct io_event *result) +{ + return ev_syscall3 (SYS_io_cancel, ctx_id, cbp, result); +} + +inline_size +int +evsys_io_getevents (aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout) +{ + return ev_syscall5 (SYS_io_getevents, ctx_id, min_nr, nr, events, timeout); +} + +/*****************************************************************************/ +/* actual backed implementation */ + +ecb_cold +static int +linuxaio_nr_events (EV_P) +{ + /* we start with 16 iocbs and incraese from there + * that's tiny, but the kernel has a rather low system-wide + * limit that can be reached quickly, so let's be parsimonious + * with this resource. + * Rest assured, the kernel generously rounds up small and big numbers + * in different ways (but doesn't seem to charge you for it). + * The 15 here is because the kernel usually has a power of two as aio-max-nr, + * and this helps to take advantage of that limit. + */ + + /* we try to fill 4kB pages exactly. + * the ring buffer header is 32 bytes, every io event is 32 bytes. + * the kernel takes the io requests number, doubles it, adds 2 + * and adds the ring buffer. + * the way we use this is by starting low, and then roughly doubling the + * size each time we hit a limit. + */ + + int requests = 15 << linuxaio_iteration; + int one_page = (4096 + / sizeof (struct io_event) ) / 2; /* how many fit into one page */ + int first_page = ((4096 - sizeof (struct aio_ring)) + / sizeof (struct io_event) - 2) / 2; /* how many fit into the first page */ + + /* if everything fits into one page, use count exactly */ + if (requests > first_page) + /* otherwise, round down to full pages and add the first page */ + requests = requests / one_page * one_page + first_page; + + return requests; +} + +/* we use out own wrapper structure in case we ever want to do something "clever" */ +typedef struct aniocb +{ + struct iocb io; + /*int inuse;*/ +} *ANIOCBP; + +inline_size +void +linuxaio_array_needsize_iocbp (ANIOCBP *base, int offset, int count) +{ + while (count--) + { + /* TODO: quite the overhead to allocate every iocb separately, maybe use our own allocator? */ + ANIOCBP iocb = (ANIOCBP)ev_malloc (sizeof (*iocb)); + + /* full zero initialise is probably not required at the moment, but + * this is not well documented, so we better do it. + */ + memset (iocb, 0, sizeof (*iocb)); + + iocb->io.aio_lio_opcode = IOCB_CMD_POLL; + iocb->io.aio_fildes = offset; + + base [offset++] = iocb; + } +} + +ecb_cold +static void +linuxaio_free_iocbp (EV_P) +{ + while (linuxaio_iocbpmax--) + ev_free (linuxaio_iocbps [linuxaio_iocbpmax]); + + linuxaio_iocbpmax = 0; /* next resize will completely reallocate the array, at some overhead */ +} + +static void +linuxaio_modify (EV_P_ int fd, int oev, int nev) +{ + array_needsize (ANIOCBP, linuxaio_iocbps, linuxaio_iocbpmax, fd + 1, linuxaio_array_needsize_iocbp); + ANIOCBP iocb = linuxaio_iocbps [fd]; + ANFD *anfd = &anfds [fd]; + + if (ecb_expect_false (iocb->io.aio_reqprio < 0)) + { + /* we handed this fd over to epoll, so undo this first */ + /* we do it manually because the optimisations on epoll_modify won't do us any good */ + epoll_ctl (backend_fd, EPOLL_CTL_DEL, fd, 0); + anfd->emask = 0; + iocb->io.aio_reqprio = 0; + } + else if (ecb_expect_false (iocb->io.aio_buf)) + { + /* iocb active, so cancel it first before resubmit */ + /* this assumes we only ever get one call per fd per loop iteration */ + for (;;) + { + /* on all relevant kernels, io_cancel fails with EINPROGRESS on "success" */ + if (ecb_expect_false (evsys_io_cancel (linuxaio_ctx, &iocb->io, (struct io_event *)0) == 0)) + break; + + if (ecb_expect_true (errno == EINPROGRESS)) + break; + + /* the EINPROGRESS test is for nicer error message. clumsy. */ + if (errno != EINTR) + { + assert (("libev: linuxaio unexpected io_cancel failed", errno != EINTR && errno != EINPROGRESS)); + break; + } + } + + /* increment generation counter to avoid handling old events */ + ++anfd->egen; + } + + iocb->io.aio_buf = (nev & EV_READ ? POLLIN : 0) + | (nev & EV_WRITE ? POLLOUT : 0); + + if (nev) + { + iocb->io.aio_data = (uint32_t)fd | ((__u64)(uint32_t)anfd->egen << 32); + + /* queue iocb up for io_submit */ + /* this assumes we only ever get one call per fd per loop iteration */ + ++linuxaio_submitcnt; + array_needsize (struct iocb *, linuxaio_submits, linuxaio_submitmax, linuxaio_submitcnt, array_needsize_noinit); + linuxaio_submits [linuxaio_submitcnt - 1] = &iocb->io; + } +} + +static void +linuxaio_epoll_cb (EV_P_ struct ev_io *w, int revents) +{ + epoll_poll (EV_A_ 0); +} + +inline_speed +void +linuxaio_fd_rearm (EV_P_ int fd) +{ + anfds [fd].events = 0; + linuxaio_iocbps [fd]->io.aio_buf = 0; + fd_change (EV_A_ fd, EV_ANFD_REIFY); +} + +static void +linuxaio_parse_events (EV_P_ struct io_event *ev, int nr) +{ + while (nr) + { + int fd = ev->data & 0xffffffff; + uint32_t gen = ev->data >> 32; + int res = ev->res; + + assert (("libev: iocb fd must be in-bounds", fd >= 0 && fd < anfdmax)); + + /* only accept events if generation counter matches */ + if (ecb_expect_true (gen == (uint32_t)anfds [fd].egen)) + { + /* feed events, we do not expect or handle POLLNVAL */ + fd_event ( + EV_A_ + fd, + (res & (POLLOUT | POLLERR | POLLHUP) ? EV_WRITE : 0) + | (res & (POLLIN | POLLERR | POLLHUP) ? EV_READ : 0) + ); + + /* linux aio is oneshot: rearm fd. TODO: this does more work than strictly needed */ + linuxaio_fd_rearm (EV_A_ fd); + } + + --nr; + ++ev; + } +} + +/* get any events from ring buffer, return true if any were handled */ +static int +linuxaio_get_events_from_ring (EV_P) +{ + struct aio_ring *ring = (struct aio_ring *)linuxaio_ctx; + unsigned head, tail; + + /* the kernel reads and writes both of these variables, */ + /* as a C extension, we assume that volatile use here */ + /* both makes reads atomic and once-only */ + head = *(volatile unsigned *)&ring->head; + ECB_MEMORY_FENCE_ACQUIRE; + tail = *(volatile unsigned *)&ring->tail; + + if (head == tail) + return 0; + + /* parse all available events, but only once, to avoid starvation */ + if (ecb_expect_true (tail > head)) /* normal case around */ + linuxaio_parse_events (EV_A_ ring->io_events + head, tail - head); + else /* wrapped around */ + { + linuxaio_parse_events (EV_A_ ring->io_events + head, ring->nr - head); + linuxaio_parse_events (EV_A_ ring->io_events, tail); + } + + ECB_MEMORY_FENCE_RELEASE; + /* as an extension to C, we hope that the volatile will make this atomic and once-only */ + *(volatile unsigned *)&ring->head = tail; + + return 1; +} + +inline_size +int +linuxaio_ringbuf_valid (EV_P) +{ + struct aio_ring *ring = (struct aio_ring *)linuxaio_ctx; + + return ecb_expect_true (ring->magic == AIO_RING_MAGIC) + && ring->incompat_features == EV_AIO_RING_INCOMPAT_FEATURES + && ring->header_length == sizeof (struct aio_ring); /* TODO: or use it to find io_event[0]? */ +} + +/* read at least one event from kernel, or timeout */ +inline_size +void +linuxaio_get_events (EV_P_ ev_tstamp timeout) +{ + struct timespec ts; + struct io_event ioev[8]; /* 256 octet stack space */ + int want = 1; /* how many events to request */ + int ringbuf_valid = linuxaio_ringbuf_valid (EV_A); + + if (ecb_expect_true (ringbuf_valid)) + { + /* if the ring buffer has any events, we don't wait or call the kernel at all */ + if (linuxaio_get_events_from_ring (EV_A)) + return; + + /* if the ring buffer is empty, and we don't have a timeout, then don't call the kernel */ + if (!timeout) + return; + } + else + /* no ringbuffer, request slightly larger batch */ + want = sizeof (ioev) / sizeof (ioev [0]); + + /* no events, so wait for some + * for fairness reasons, we do this in a loop, to fetch all events + */ + for (;;) + { + int res; + + EV_RELEASE_CB; + + EV_TS_SET (ts, timeout); + res = evsys_io_getevents (linuxaio_ctx, 1, want, ioev, &ts); + + EV_ACQUIRE_CB; + + if (res < 0) + if (errno == EINTR) + /* ignored, retry */; + else + ev_syserr ("(libev) linuxaio io_getevents"); + else if (res) + { + /* at least one event available, handle them */ + linuxaio_parse_events (EV_A_ ioev, res); + + if (ecb_expect_true (ringbuf_valid)) + { + /* if we have a ring buffer, handle any remaining events in it */ + linuxaio_get_events_from_ring (EV_A); + + /* at this point, we should have handled all outstanding events */ + break; + } + else if (res < want) + /* otherwise, if there were fewere events than we wanted, we assume there are no more */ + break; + } + else + break; /* no events from the kernel, we are done */ + + timeout = EV_TS_CONST (0.); /* only wait in the first iteration */ + } +} + +inline_size +int +linuxaio_io_setup (EV_P) +{ + linuxaio_ctx = 0; + return evsys_io_setup (linuxaio_nr_events (EV_A), &linuxaio_ctx); +} + +static void +linuxaio_poll (EV_P_ ev_tstamp timeout) +{ + int submitted; + + /* first phase: submit new iocbs */ + + /* io_submit might return less than the requested number of iocbs */ + /* this is, afaics, only because of errors, but we go by the book and use a loop, */ + /* which allows us to pinpoint the erroneous iocb */ + for (submitted = 0; submitted < linuxaio_submitcnt; ) + { + int res = evsys_io_submit (linuxaio_ctx, linuxaio_submitcnt - submitted, linuxaio_submits + submitted); + + if (ecb_expect_false (res < 0)) + if (errno == EINVAL) + { + /* This happens for unsupported fds, officially, but in my testing, + * also randomly happens for supported fds. We fall back to good old + * poll() here, under the assumption that this is a very rare case. + * See https://lore.kernel.org/patchwork/patch/1047453/ to see + * discussion about such a case (ttys) where polling for POLLIN + * fails but POLLIN|POLLOUT works. + */ + struct iocb *iocb = linuxaio_submits [submitted]; + epoll_modify (EV_A_ iocb->aio_fildes, 0, anfds [iocb->aio_fildes].events); + iocb->aio_reqprio = -1; /* mark iocb as epoll */ + + res = 1; /* skip this iocb - another iocb, another chance */ + } + else if (errno == EAGAIN) + { + /* This happens when the ring buffer is full, or some other shit we + * don't know and isn't documented. Most likely because we have too + * many requests and linux aio can't be assed to handle them. + * In this case, we try to allocate a larger ring buffer, freeing + * ours first. This might fail, in which case we have to fall back to 100% + * epoll. + * God, how I hate linux not getting its act together. Ever. + */ + evsys_io_destroy (linuxaio_ctx); + linuxaio_submitcnt = 0; + + /* rearm all fds with active iocbs */ + { + int fd; + for (fd = 0; fd < linuxaio_iocbpmax; ++fd) + if (linuxaio_iocbps [fd]->io.aio_buf) + linuxaio_fd_rearm (EV_A_ fd); + } + + ++linuxaio_iteration; + if (linuxaio_io_setup (EV_A) < 0) + { + /* TODO: rearm all and recreate epoll backend from scratch */ + /* TODO: might be more prudent? */ + + /* to bad, we can't get a new aio context, go 100% epoll */ + linuxaio_free_iocbp (EV_A); + ev_io_stop (EV_A_ &linuxaio_epoll_w); + ev_ref (EV_A); + linuxaio_ctx = 0; + + backend = EVBACKEND_EPOLL; + backend_modify = epoll_modify; + backend_poll = epoll_poll; + } + + timeout = EV_TS_CONST (0.); + /* it's easiest to handle this mess in another iteration */ + return; + } + else if (errno == EBADF) + { + assert (("libev: event loop rejected bad fd", errno != EBADF)); + fd_kill (EV_A_ linuxaio_submits [submitted]->aio_fildes); + + res = 1; /* skip this iocb */ + } + else if (errno == EINTR) /* not seen in reality, not documented */ + res = 0; /* silently ignore and retry */ + else + { + ev_syserr ("(libev) linuxaio io_submit"); + res = 0; + } + + submitted += res; + } + + linuxaio_submitcnt = 0; + + /* second phase: fetch and parse events */ + + linuxaio_get_events (EV_A_ timeout); +} + +inline_size +int +linuxaio_init (EV_P_ int flags) +{ + /* would be great to have a nice test for IOCB_CMD_POLL instead */ + /* also: test some semi-common fd types, such as files and ttys in recommended_backends */ + /* 4.18 introduced IOCB_CMD_POLL, 4.19 made epoll work, and we need that */ + if (ev_linux_version () < 0x041300) + return 0; + + if (!epoll_init (EV_A_ 0)) + return 0; + + linuxaio_iteration = 0; + + if (linuxaio_io_setup (EV_A) < 0) + { + epoll_destroy (EV_A); + return 0; + } + + ev_io_init (&linuxaio_epoll_w, linuxaio_epoll_cb, backend_fd, EV_READ); + ev_set_priority (&linuxaio_epoll_w, EV_MAXPRI); + ev_io_start (EV_A_ &linuxaio_epoll_w); + ev_unref (EV_A); /* watcher should not keep loop alive */ + + backend_modify = linuxaio_modify; + backend_poll = linuxaio_poll; + + linuxaio_iocbpmax = 0; + linuxaio_iocbps = 0; + + linuxaio_submits = 0; + linuxaio_submitmax = 0; + linuxaio_submitcnt = 0; + + return EVBACKEND_LINUXAIO; +} + +inline_size +void +linuxaio_destroy (EV_P) +{ + epoll_destroy (EV_A); + linuxaio_free_iocbp (EV_A); + evsys_io_destroy (linuxaio_ctx); /* fails in child, aio context is destroyed */ +} + +ecb_cold +static void +linuxaio_fork (EV_P) +{ + linuxaio_submitcnt = 0; /* all pointers were invalidated */ + linuxaio_free_iocbp (EV_A); /* this frees all iocbs, which is very heavy-handed */ + evsys_io_destroy (linuxaio_ctx); /* fails in child, aio context is destroyed */ + + linuxaio_iteration = 0; /* we start over in the child */ + + while (linuxaio_io_setup (EV_A) < 0) + ev_syserr ("(libev) linuxaio io_setup"); + + /* forking epoll should also effectively unregister all fds from the backend */ + epoll_fork (EV_A); + /* epoll_fork already did this. hopefully */ + /*fd_rearm_all (EV_A);*/ + + ev_io_stop (EV_A_ &linuxaio_epoll_w); + ev_io_set (EV_A_ &linuxaio_epoll_w, backend_fd, EV_READ); + ev_io_start (EV_A_ &linuxaio_epoll_w); +} + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev_poll.c b/deps/ngtcp2/ngtcp2/third-party/libev/ev_poll.c new file mode 100644 index 00000000000000..e5508ddb0c25ec --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev_poll.c @@ -0,0 +1,156 @@ +/* + * libev poll fd activity backend + * + * Copyright (c) 2007,2008,2009,2010,2011,2016,2019 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +#include + +inline_size +void +array_needsize_pollidx (int *base, int offset, int count) +{ + /* using memset (.., -1, ...) is tempting, we we try + * to be ultraportable + */ + base += offset; + while (count--) + *base++ = -1; +} + +static void +poll_modify (EV_P_ int fd, int oev, int nev) +{ + int idx; + + if (oev == nev) + return; + + array_needsize (int, pollidxs, pollidxmax, fd + 1, array_needsize_pollidx); + + idx = pollidxs [fd]; + + if (idx < 0) /* need to allocate a new pollfd */ + { + pollidxs [fd] = idx = pollcnt++; + array_needsize (struct pollfd, polls, pollmax, pollcnt, array_needsize_noinit); + polls [idx].fd = fd; + } + + assert (polls [idx].fd == fd); + + if (nev) + polls [idx].events = + (nev & EV_READ ? POLLIN : 0) + | (nev & EV_WRITE ? POLLOUT : 0); + else /* remove pollfd */ + { + pollidxs [fd] = -1; + + if (ecb_expect_true (idx < --pollcnt)) + { + polls [idx] = polls [pollcnt]; + pollidxs [polls [idx].fd] = idx; + } + } +} + +static void +poll_poll (EV_P_ ev_tstamp timeout) +{ + struct pollfd *p; + int res; + + EV_RELEASE_CB; + res = poll (polls, pollcnt, EV_TS_TO_MSEC (timeout)); + EV_ACQUIRE_CB; + + if (ecb_expect_false (res < 0)) + { + if (errno == EBADF) + fd_ebadf (EV_A); + else if (errno == ENOMEM && !syserr_cb) + fd_enomem (EV_A); + else if (errno != EINTR) + ev_syserr ("(libev) poll"); + } + else + for (p = polls; res; ++p) + { + assert (("libev: poll returned illegal result, broken BSD kernel?", p < polls + pollcnt)); + + if (ecb_expect_false (p->revents)) /* this expect is debatable */ + { + --res; + + if (ecb_expect_false (p->revents & POLLNVAL)) + { + assert (("libev: poll found invalid fd in poll set", 0)); + fd_kill (EV_A_ p->fd); + } + else + fd_event ( + EV_A_ + p->fd, + (p->revents & (POLLOUT | POLLERR | POLLHUP) ? EV_WRITE : 0) + | (p->revents & (POLLIN | POLLERR | POLLHUP) ? EV_READ : 0) + ); + } + } +} + +inline_size +int +poll_init (EV_P_ int flags) +{ + backend_mintime = EV_TS_CONST (1e-3); + backend_modify = poll_modify; + backend_poll = poll_poll; + + pollidxs = 0; pollidxmax = 0; + polls = 0; pollmax = 0; pollcnt = 0; + + return EVBACKEND_POLL; +} + +inline_size +void +poll_destroy (EV_P) +{ + ev_free (pollidxs); + ev_free (polls); +} + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev_port.c b/deps/ngtcp2/ngtcp2/third-party/libev/ev_port.c new file mode 100644 index 00000000000000..f4cd9d99cd39ba --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev_port.c @@ -0,0 +1,192 @@ +/* + * libev solaris event port backend + * + * Copyright (c) 2007,2008,2009,2010,2011,2019 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +/* useful reading: + * + * http://bugs.opensolaris.org/view_bug.do?bug_id=6268715 (random results) + * http://bugs.opensolaris.org/view_bug.do?bug_id=6455223 (just totally broken) + * http://bugs.opensolaris.org/view_bug.do?bug_id=6873782 (manpage ETIME) + * http://bugs.opensolaris.org/view_bug.do?bug_id=6874410 (implementation ETIME) + * http://www.mail-archive.com/networking-discuss@opensolaris.org/msg11898.html ETIME vs. nget + * http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/lib/libc/port/gen/event_port.c (libc) + * http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/fs/portfs/port.c#1325 (kernel) + */ + +#include +#include +#include +#include +#include +#include + +inline_speed +void +port_associate_and_check (EV_P_ int fd, int ev) +{ + if (0 > + port_associate ( + backend_fd, PORT_SOURCE_FD, fd, + (ev & EV_READ ? POLLIN : 0) + | (ev & EV_WRITE ? POLLOUT : 0), + 0 + ) + ) + { + if (errno == EBADFD) + { + assert (("libev: port_associate found invalid fd", errno != EBADFD)); + fd_kill (EV_A_ fd); + } + else + ev_syserr ("(libev) port_associate"); + } +} + +static void +port_modify (EV_P_ int fd, int oev, int nev) +{ + /* we need to reassociate no matter what, as closes are + * once more silently being discarded. + */ + if (!nev) + { + if (oev) + port_dissociate (backend_fd, PORT_SOURCE_FD, fd); + } + else + port_associate_and_check (EV_A_ fd, nev); +} + +static void +port_poll (EV_P_ ev_tstamp timeout) +{ + int res, i; + struct timespec ts; + uint_t nget = 1; + + /* we initialise this to something we will skip in the loop, as */ + /* port_getn can return with nget unchanged, but no indication */ + /* whether it was the original value or has been updated :/ */ + port_events [0].portev_source = 0; + + EV_RELEASE_CB; + EV_TS_SET (ts, timeout); + res = port_getn (backend_fd, port_events, port_eventmax, &nget, &ts); + EV_ACQUIRE_CB; + + /* port_getn may or may not set nget on error */ + /* so we rely on port_events [0].portev_source not being updated */ + if (res == -1 && errno != ETIME && errno != EINTR) + ev_syserr ("(libev) port_getn (see http://bugs.opensolaris.org/view_bug.do?bug_id=6268715, try LIBEV_FLAGS=3 env variable)"); + + for (i = 0; i < nget; ++i) + { + if (port_events [i].portev_source == PORT_SOURCE_FD) + { + int fd = port_events [i].portev_object; + + fd_event ( + EV_A_ + fd, + (port_events [i].portev_events & (POLLOUT | POLLERR | POLLHUP) ? EV_WRITE : 0) + | (port_events [i].portev_events & (POLLIN | POLLERR | POLLHUP) ? EV_READ : 0) + ); + + fd_change (EV_A_ fd, EV__IOFDSET); + } + } + + if (ecb_expect_false (nget == port_eventmax)) + { + ev_free (port_events); + port_eventmax = array_nextsize (sizeof (port_event_t), port_eventmax, port_eventmax + 1); + port_events = (port_event_t *)ev_malloc (sizeof (port_event_t) * port_eventmax); + } +} + +inline_size +int +port_init (EV_P_ int flags) +{ + /* Initialize the kernel queue */ + if ((backend_fd = port_create ()) < 0) + return 0; + + assert (("libev: PORT_SOURCE_FD must not be zero", PORT_SOURCE_FD)); + + fcntl (backend_fd, F_SETFD, FD_CLOEXEC); /* not sure if necessary, hopefully doesn't hurt */ + + /* if my reading of the opensolaris kernel sources are correct, then + * opensolaris does something very stupid: it checks if the time has already + * elapsed and doesn't round up if that is the case, otherwise it DOES round + * up. Since we can't know what the case is, we need to guess by using a + * "large enough" timeout. Normally, 1e-9 would be correct. + */ + backend_mintime = EV_TS_CONST (1e-3); /* needed to compensate for port_getn returning early */ + backend_modify = port_modify; + backend_poll = port_poll; + + port_eventmax = 64; /* initial number of events receivable per poll */ + port_events = (port_event_t *)ev_malloc (sizeof (port_event_t) * port_eventmax); + + return EVBACKEND_PORT; +} + +inline_size +void +port_destroy (EV_P) +{ + ev_free (port_events); +} + +inline_size +void +port_fork (EV_P) +{ + close (backend_fd); + + while ((backend_fd = port_create ()) < 0) + ev_syserr ("(libev) port"); + + fcntl (backend_fd, F_SETFD, FD_CLOEXEC); + + /* re-register interest in fds */ + fd_rearm_all (EV_A); +} + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev_select.c b/deps/ngtcp2/ngtcp2/third-party/libev/ev_select.c new file mode 100644 index 00000000000000..b862c8113783b5 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev_select.c @@ -0,0 +1,316 @@ +/* + * libev select fd activity backend + * + * Copyright (c) 2007,2008,2009,2010,2011 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +#ifndef _WIN32 +/* for unix systems */ +# include +# ifndef __hpux +/* for REAL unix systems */ +# include +# endif +#endif + +#ifndef EV_SELECT_USE_FD_SET +# ifdef NFDBITS +# define EV_SELECT_USE_FD_SET 0 +# else +# define EV_SELECT_USE_FD_SET 1 +# endif +#endif + +#if EV_SELECT_IS_WINSOCKET +# undef EV_SELECT_USE_FD_SET +# define EV_SELECT_USE_FD_SET 1 +# undef NFDBITS +# define NFDBITS 0 +#endif + +#if !EV_SELECT_USE_FD_SET +# define NFDBYTES (NFDBITS / 8) +#endif + +#include + +static void +select_modify (EV_P_ int fd, int oev, int nev) +{ + if (oev == nev) + return; + + { +#if EV_SELECT_USE_FD_SET + + #if EV_SELECT_IS_WINSOCKET + SOCKET handle = anfds [fd].handle; + #else + int handle = fd; + #endif + + assert (("libev: fd >= FD_SETSIZE passed to fd_set-based select backend", fd < FD_SETSIZE)); + + /* FD_SET is broken on windows (it adds the fd to a set twice or more, + * which eventually leads to overflows). Need to call it only on changes. + */ + #if EV_SELECT_IS_WINSOCKET + if ((oev ^ nev) & EV_READ) + #endif + if (nev & EV_READ) + FD_SET (handle, (fd_set *)vec_ri); + else + FD_CLR (handle, (fd_set *)vec_ri); + + #if EV_SELECT_IS_WINSOCKET + if ((oev ^ nev) & EV_WRITE) + #endif + if (nev & EV_WRITE) + FD_SET (handle, (fd_set *)vec_wi); + else + FD_CLR (handle, (fd_set *)vec_wi); + +#else + + int word = fd / NFDBITS; + fd_mask mask = 1UL << (fd % NFDBITS); + + if (ecb_expect_false (vec_max <= word)) + { + int new_max = word + 1; + + vec_ri = ev_realloc (vec_ri, new_max * NFDBYTES); + vec_ro = ev_realloc (vec_ro, new_max * NFDBYTES); /* could free/malloc */ + vec_wi = ev_realloc (vec_wi, new_max * NFDBYTES); + vec_wo = ev_realloc (vec_wo, new_max * NFDBYTES); /* could free/malloc */ + #ifdef _WIN32 + vec_eo = ev_realloc (vec_eo, new_max * NFDBYTES); /* could free/malloc */ + #endif + + for (; vec_max < new_max; ++vec_max) + ((fd_mask *)vec_ri) [vec_max] = + ((fd_mask *)vec_wi) [vec_max] = 0; + } + + ((fd_mask *)vec_ri) [word] |= mask; + if (!(nev & EV_READ)) + ((fd_mask *)vec_ri) [word] &= ~mask; + + ((fd_mask *)vec_wi) [word] |= mask; + if (!(nev & EV_WRITE)) + ((fd_mask *)vec_wi) [word] &= ~mask; +#endif + } +} + +static void +select_poll (EV_P_ ev_tstamp timeout) +{ + struct timeval tv; + int res; + int fd_setsize; + + EV_RELEASE_CB; + EV_TV_SET (tv, timeout); + +#if EV_SELECT_USE_FD_SET + fd_setsize = sizeof (fd_set); +#else + fd_setsize = vec_max * NFDBYTES; +#endif + + memcpy (vec_ro, vec_ri, fd_setsize); + memcpy (vec_wo, vec_wi, fd_setsize); + +#ifdef _WIN32 + /* pass in the write set as except set. + * the idea behind this is to work around a windows bug that causes + * errors to be reported as an exception and not by setting + * the writable bit. this is so uncontrollably lame. + */ + memcpy (vec_eo, vec_wi, fd_setsize); + res = select (vec_max * NFDBITS, (fd_set *)vec_ro, (fd_set *)vec_wo, (fd_set *)vec_eo, &tv); +#elif EV_SELECT_USE_FD_SET + fd_setsize = anfdmax < FD_SETSIZE ? anfdmax : FD_SETSIZE; + res = select (fd_setsize, (fd_set *)vec_ro, (fd_set *)vec_wo, 0, &tv); +#else + res = select (vec_max * NFDBITS, (fd_set *)vec_ro, (fd_set *)vec_wo, 0, &tv); +#endif + EV_ACQUIRE_CB; + + if (ecb_expect_false (res < 0)) + { + #if EV_SELECT_IS_WINSOCKET + errno = WSAGetLastError (); + #endif + #ifdef WSABASEERR + /* on windows, select returns incompatible error codes, fix this */ + if (errno >= WSABASEERR && errno < WSABASEERR + 1000) + if (errno == WSAENOTSOCK) + errno = EBADF; + else + errno -= WSABASEERR; + #endif + + #ifdef _WIN32 + /* select on windows erroneously returns EINVAL when no fd sets have been + * provided (this is documented). what microsoft doesn't tell you that this bug + * exists even when the fd sets _are_ provided, so we have to check for this bug + * here and emulate by sleeping manually. + * we also get EINVAL when the timeout is invalid, but we ignore this case here + * and assume that EINVAL always means: you have to wait manually. + */ + if (errno == EINVAL) + { + if (timeout) + { + unsigned long ms = EV_TS_TO_MSEC (timeout); + Sleep (ms ? ms : 1); + } + + return; + } + #endif + + if (errno == EBADF) + fd_ebadf (EV_A); + else if (errno == ENOMEM && !syserr_cb) + fd_enomem (EV_A); + else if (errno != EINTR) + ev_syserr ("(libev) select"); + + return; + } + +#if EV_SELECT_USE_FD_SET + + { + int fd; + + for (fd = 0; fd < anfdmax; ++fd) + if (anfds [fd].events) + { + int events = 0; + #if EV_SELECT_IS_WINSOCKET + SOCKET handle = anfds [fd].handle; + #else + int handle = fd; + #endif + + if (FD_ISSET (handle, (fd_set *)vec_ro)) events |= EV_READ; + if (FD_ISSET (handle, (fd_set *)vec_wo)) events |= EV_WRITE; + #ifdef _WIN32 + if (FD_ISSET (handle, (fd_set *)vec_eo)) events |= EV_WRITE; + #endif + + if (ecb_expect_true (events)) + fd_event (EV_A_ fd, events); + } + } + +#else + + { + int word, bit; + for (word = vec_max; word--; ) + { + fd_mask word_r = ((fd_mask *)vec_ro) [word]; + fd_mask word_w = ((fd_mask *)vec_wo) [word]; + #ifdef _WIN32 + word_w |= ((fd_mask *)vec_eo) [word]; + #endif + + if (word_r || word_w) + for (bit = NFDBITS; bit--; ) + { + fd_mask mask = 1UL << bit; + int events = 0; + + events |= word_r & mask ? EV_READ : 0; + events |= word_w & mask ? EV_WRITE : 0; + + if (ecb_expect_true (events)) + fd_event (EV_A_ word * NFDBITS + bit, events); + } + } + } + +#endif +} + +inline_size +int +select_init (EV_P_ int flags) +{ + backend_mintime = EV_TS_CONST (1e-6); + backend_modify = select_modify; + backend_poll = select_poll; + +#if EV_SELECT_USE_FD_SET + vec_ri = ev_malloc (sizeof (fd_set)); FD_ZERO ((fd_set *)vec_ri); + vec_ro = ev_malloc (sizeof (fd_set)); + vec_wi = ev_malloc (sizeof (fd_set)); FD_ZERO ((fd_set *)vec_wi); + vec_wo = ev_malloc (sizeof (fd_set)); + #ifdef _WIN32 + vec_eo = ev_malloc (sizeof (fd_set)); + #endif +#else + vec_max = 0; + vec_ri = 0; + vec_ro = 0; + vec_wi = 0; + vec_wo = 0; + #ifdef _WIN32 + vec_eo = 0; + #endif +#endif + + return EVBACKEND_SELECT; +} + +inline_size +void +select_destroy (EV_P) +{ + ev_free (vec_ri); + ev_free (vec_ro); + ev_free (vec_wi); + ev_free (vec_wo); + #ifdef _WIN32 + ev_free (vec_eo); + #endif +} + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev_vars.h b/deps/ngtcp2/ngtcp2/third-party/libev/ev_vars.h new file mode 100644 index 00000000000000..fb0c5831624559 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev_vars.h @@ -0,0 +1,249 @@ +/* + * loop member variable declarations + * + * Copyright (c) 2007,2008,2009,2010,2011,2012,2013,2019 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +#define VARx(type,name) VAR(name, type name) + +VARx(ev_tstamp, now_floor) /* last time we refreshed rt_time */ +VARx(ev_tstamp, mn_now) /* monotonic clock "now" */ +VARx(ev_tstamp, rtmn_diff) /* difference realtime - monotonic time */ + +/* for reverse feeding of events */ +VARx(W *, rfeeds) +VARx(int, rfeedmax) +VARx(int, rfeedcnt) + +VAR (pendings, ANPENDING *pendings [NUMPRI]) +VAR (pendingmax, int pendingmax [NUMPRI]) +VAR (pendingcnt, int pendingcnt [NUMPRI]) +VARx(int, pendingpri) /* highest priority currently pending */ +VARx(ev_prepare, pending_w) /* dummy pending watcher */ + +VARx(ev_tstamp, io_blocktime) +VARx(ev_tstamp, timeout_blocktime) + +VARx(int, backend) +VARx(int, activecnt) /* total number of active events ("refcount") */ +VARx(EV_ATOMIC_T, loop_done) /* signal by ev_break */ + +VARx(int, backend_fd) +VARx(ev_tstamp, backend_mintime) /* assumed typical timer resolution */ +VAR (backend_modify, void (*backend_modify)(EV_P_ int fd, int oev, int nev)) +VAR (backend_poll , void (*backend_poll)(EV_P_ ev_tstamp timeout)) + +VARx(ANFD *, anfds) +VARx(int, anfdmax) + +VAR (evpipe, int evpipe [2]) +VARx(ev_io, pipe_w) +VARx(EV_ATOMIC_T, pipe_write_wanted) +VARx(EV_ATOMIC_T, pipe_write_skipped) + +#if !defined(_WIN32) || EV_GENWRAP +VARx(pid_t, curpid) +#endif + +VARx(char, postfork) /* true if we need to recreate kernel state after fork */ + +#if EV_USE_SELECT || EV_GENWRAP +VARx(void *, vec_ri) +VARx(void *, vec_ro) +VARx(void *, vec_wi) +VARx(void *, vec_wo) +#if defined(_WIN32) || EV_GENWRAP +VARx(void *, vec_eo) +#endif +VARx(int, vec_max) +#endif + +#if EV_USE_POLL || EV_GENWRAP +VARx(struct pollfd *, polls) +VARx(int, pollmax) +VARx(int, pollcnt) +VARx(int *, pollidxs) /* maps fds into structure indices */ +VARx(int, pollidxmax) +#endif + +#if EV_USE_EPOLL || EV_GENWRAP +VARx(struct epoll_event *, epoll_events) +VARx(int, epoll_eventmax) +VARx(int *, epoll_eperms) +VARx(int, epoll_epermcnt) +VARx(int, epoll_epermmax) +#endif + +#if EV_USE_LINUXAIO || EV_GENWRAP +VARx(aio_context_t, linuxaio_ctx) +VARx(int, linuxaio_iteration) +VARx(struct aniocb **, linuxaio_iocbps) +VARx(int, linuxaio_iocbpmax) +VARx(struct iocb **, linuxaio_submits) +VARx(int, linuxaio_submitcnt) +VARx(int, linuxaio_submitmax) +VARx(ev_io, linuxaio_epoll_w) +#endif + +#if EV_USE_IOURING || EV_GENWRAP +VARx(int, iouring_fd) +VARx(unsigned, iouring_to_submit); +VARx(int, iouring_entries) +VARx(int, iouring_max_entries) +VARx(void *, iouring_sq_ring) +VARx(void *, iouring_cq_ring) +VARx(void *, iouring_sqes) +VARx(uint32_t, iouring_sq_ring_size) +VARx(uint32_t, iouring_cq_ring_size) +VARx(uint32_t, iouring_sqes_size) +VARx(uint32_t, iouring_sq_head) +VARx(uint32_t, iouring_sq_tail) +VARx(uint32_t, iouring_sq_ring_mask) +VARx(uint32_t, iouring_sq_ring_entries) +VARx(uint32_t, iouring_sq_flags) +VARx(uint32_t, iouring_sq_dropped) +VARx(uint32_t, iouring_sq_array) +VARx(uint32_t, iouring_cq_head) +VARx(uint32_t, iouring_cq_tail) +VARx(uint32_t, iouring_cq_ring_mask) +VARx(uint32_t, iouring_cq_ring_entries) +VARx(uint32_t, iouring_cq_overflow) +VARx(uint32_t, iouring_cq_cqes) +VARx(ev_tstamp, iouring_tfd_to) +VARx(int, iouring_tfd) +VARx(ev_io, iouring_tfd_w) +#endif + +#if EV_USE_KQUEUE || EV_GENWRAP +VARx(pid_t, kqueue_fd_pid) +VARx(struct kevent *, kqueue_changes) +VARx(int, kqueue_changemax) +VARx(int, kqueue_changecnt) +VARx(struct kevent *, kqueue_events) +VARx(int, kqueue_eventmax) +#endif + +#if EV_USE_PORT || EV_GENWRAP +VARx(struct port_event *, port_events) +VARx(int, port_eventmax) +#endif + +#if EV_USE_IOCP || EV_GENWRAP +VARx(HANDLE, iocp) +#endif + +VARx(int *, fdchanges) +VARx(int, fdchangemax) +VARx(int, fdchangecnt) + +VARx(ANHE *, timers) +VARx(int, timermax) +VARx(int, timercnt) + +#if EV_PERIODIC_ENABLE || EV_GENWRAP +VARx(ANHE *, periodics) +VARx(int, periodicmax) +VARx(int, periodiccnt) +#endif + +#if EV_IDLE_ENABLE || EV_GENWRAP +VAR (idles, ev_idle **idles [NUMPRI]) +VAR (idlemax, int idlemax [NUMPRI]) +VAR (idlecnt, int idlecnt [NUMPRI]) +#endif +VARx(int, idleall) /* total number */ + +VARx(struct ev_prepare **, prepares) +VARx(int, preparemax) +VARx(int, preparecnt) + +VARx(struct ev_check **, checks) +VARx(int, checkmax) +VARx(int, checkcnt) + +#if EV_FORK_ENABLE || EV_GENWRAP +VARx(struct ev_fork **, forks) +VARx(int, forkmax) +VARx(int, forkcnt) +#endif + +#if EV_CLEANUP_ENABLE || EV_GENWRAP +VARx(struct ev_cleanup **, cleanups) +VARx(int, cleanupmax) +VARx(int, cleanupcnt) +#endif + +#if EV_ASYNC_ENABLE || EV_GENWRAP +VARx(EV_ATOMIC_T, async_pending) +VARx(struct ev_async **, asyncs) +VARx(int, asyncmax) +VARx(int, asynccnt) +#endif + +#if EV_USE_INOTIFY || EV_GENWRAP +VARx(int, fs_fd) +VARx(ev_io, fs_w) +VARx(char, fs_2625) /* whether we are running in linux 2.6.25 or newer */ +VAR (fs_hash, ANFS fs_hash [EV_INOTIFY_HASHSIZE]) +#endif + +VARx(EV_ATOMIC_T, sig_pending) +#if EV_USE_SIGNALFD || EV_GENWRAP +VARx(int, sigfd) +VARx(ev_io, sigfd_w) +VARx(sigset_t, sigfd_set) +#endif + +#if EV_USE_TIMERFD || EV_GENWRAP +VARx(int, timerfd) /* timerfd for time jump detection */ +VARx(ev_io, timerfd_w) +#endif + +VARx(unsigned int, origflags) /* original loop flags */ + +#if EV_FEATURE_API || EV_GENWRAP +VARx(unsigned int, loop_count) /* total number of loop iterations/blocks */ +VARx(unsigned int, loop_depth) /* #ev_run enters - #ev_run leaves */ + +VARx(void *, userdata) +/* C++ doesn't support the ev_loop_callback typedef here. stinks. */ +VAR (release_cb, void (*release_cb)(EV_P) EV_NOEXCEPT) +VAR (acquire_cb, void (*acquire_cb)(EV_P) EV_NOEXCEPT) +VAR (invoke_cb , ev_loop_callback invoke_cb) +#endif + +#undef VARx + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev_win32.c b/deps/ngtcp2/ngtcp2/third-party/libev/ev_win32.c new file mode 100644 index 00000000000000..97344c3e1cd4d2 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev_win32.c @@ -0,0 +1,162 @@ +/* + * libev win32 compatibility cruft (_not_ a backend) + * + * Copyright (c) 2007,2008,2009 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +#ifdef _WIN32 + +/* note: the comment below could not be substantiated, but what would I care */ +/* MSDN says this is required to handle SIGFPE */ +/* my wild guess would be that using something floating-pointy is required */ +/* for the crt to do something about it */ +volatile double SIGFPE_REQ = 0.0f; + +static SOCKET +ev_tcp_socket (void) +{ +#if EV_USE_WSASOCKET + return WSASocket (AF_INET, SOCK_STREAM, 0, 0, 0, 0); +#else + return socket (AF_INET, SOCK_STREAM, 0); +#endif +} + +/* oh, the humanity! */ +static int +ev_pipe (int filedes [2]) +{ + struct sockaddr_in addr = { 0 }; + int addr_size = sizeof (addr); + struct sockaddr_in adr2; + int adr2_size = sizeof (adr2); + SOCKET listener; + SOCKET sock [2] = { -1, -1 }; + + if ((listener = ev_tcp_socket ()) == INVALID_SOCKET) + return -1; + + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl (INADDR_LOOPBACK); + addr.sin_port = 0; + + if (bind (listener, (struct sockaddr *)&addr, addr_size)) + goto fail; + + if (getsockname (listener, (struct sockaddr *)&addr, &addr_size)) + goto fail; + + if (listen (listener, 1)) + goto fail; + + if ((sock [0] = ev_tcp_socket ()) == INVALID_SOCKET) + goto fail; + + if (connect (sock [0], (struct sockaddr *)&addr, addr_size)) + goto fail; + + /* TODO: returns INVALID_SOCKET on winsock accept, not < 0. fix it */ + /* when convenient, probably by just removing error checking altogether? */ + if ((sock [1] = accept (listener, 0, 0)) < 0) + goto fail; + + /* windows vista returns fantasy port numbers for sockets: + * example for two interconnected tcp sockets: + * + * (Socket::unpack_sockaddr_in getsockname $sock0)[0] == 53364 + * (Socket::unpack_sockaddr_in getpeername $sock0)[0] == 53363 + * (Socket::unpack_sockaddr_in getsockname $sock1)[0] == 53363 + * (Socket::unpack_sockaddr_in getpeername $sock1)[0] == 53365 + * + * wow! tridirectional sockets! + * + * this way of checking ports seems to work: + */ + if (getpeername (sock [0], (struct sockaddr *)&addr, &addr_size)) + goto fail; + + if (getsockname (sock [1], (struct sockaddr *)&adr2, &adr2_size)) + goto fail; + + errno = WSAEINVAL; + if (addr_size != adr2_size + || addr.sin_addr.s_addr != adr2.sin_addr.s_addr /* just to be sure, I mean, it's windows */ + || addr.sin_port != adr2.sin_port) + goto fail; + + closesocket (listener); + +#if EV_SELECT_IS_WINSOCKET + filedes [0] = EV_WIN32_HANDLE_TO_FD (sock [0]); + filedes [1] = EV_WIN32_HANDLE_TO_FD (sock [1]); +#else + /* when select isn't winsocket, we also expect socket, connect, accept etc. + * to work on fds */ + filedes [0] = sock [0]; + filedes [1] = sock [1]; +#endif + + return 0; + +fail: + closesocket (listener); + + if (sock [0] != INVALID_SOCKET) closesocket (sock [0]); + if (sock [1] != INVALID_SOCKET) closesocket (sock [1]); + + return -1; +} + +#undef pipe +#define pipe(filedes) ev_pipe (filedes) + +#define EV_HAVE_EV_TIME 1 +ev_tstamp +ev_time (void) +{ + FILETIME ft; + ULARGE_INTEGER ui; + + GetSystemTimeAsFileTime (&ft); + ui.u.LowPart = ft.dwLowDateTime; + ui.u.HighPart = ft.dwHighDateTime; + + /* also, msvc cannot convert ulonglong to double... yes, it is that sucky */ + return EV_TS_FROM_USEC (((LONGLONG)(ui.QuadPart - 116444736000000000) * 1e-1)); +} + +#endif + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ev_wrap.h b/deps/ngtcp2/ngtcp2/third-party/libev/ev_wrap.h new file mode 100644 index 00000000000000..45d793cedb8e24 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ev_wrap.h @@ -0,0 +1,272 @@ +/* DO NOT EDIT, automatically generated by update_ev_wrap */ +#ifndef EV_WRAP_H +#define EV_WRAP_H +#define acquire_cb ((loop)->acquire_cb) +#define activecnt ((loop)->activecnt) +#define anfdmax ((loop)->anfdmax) +#define anfds ((loop)->anfds) +#define async_pending ((loop)->async_pending) +#define asynccnt ((loop)->asynccnt) +#define asyncmax ((loop)->asyncmax) +#define asyncs ((loop)->asyncs) +#define backend ((loop)->backend) +#define backend_fd ((loop)->backend_fd) +#define backend_mintime ((loop)->backend_mintime) +#define backend_modify ((loop)->backend_modify) +#define backend_poll ((loop)->backend_poll) +#define checkcnt ((loop)->checkcnt) +#define checkmax ((loop)->checkmax) +#define checks ((loop)->checks) +#define cleanupcnt ((loop)->cleanupcnt) +#define cleanupmax ((loop)->cleanupmax) +#define cleanups ((loop)->cleanups) +#define curpid ((loop)->curpid) +#define epoll_epermcnt ((loop)->epoll_epermcnt) +#define epoll_epermmax ((loop)->epoll_epermmax) +#define epoll_eperms ((loop)->epoll_eperms) +#define epoll_eventmax ((loop)->epoll_eventmax) +#define epoll_events ((loop)->epoll_events) +#define evpipe ((loop)->evpipe) +#define fdchangecnt ((loop)->fdchangecnt) +#define fdchangemax ((loop)->fdchangemax) +#define fdchanges ((loop)->fdchanges) +#define forkcnt ((loop)->forkcnt) +#define forkmax ((loop)->forkmax) +#define forks ((loop)->forks) +#define fs_2625 ((loop)->fs_2625) +#define fs_fd ((loop)->fs_fd) +#define fs_hash ((loop)->fs_hash) +#define fs_w ((loop)->fs_w) +#define idleall ((loop)->idleall) +#define idlecnt ((loop)->idlecnt) +#define idlemax ((loop)->idlemax) +#define idles ((loop)->idles) +#define invoke_cb ((loop)->invoke_cb) +#define io_blocktime ((loop)->io_blocktime) +#define iocp ((loop)->iocp) +#define iouring_cq_cqes ((loop)->iouring_cq_cqes) +#define iouring_cq_head ((loop)->iouring_cq_head) +#define iouring_cq_overflow ((loop)->iouring_cq_overflow) +#define iouring_cq_ring ((loop)->iouring_cq_ring) +#define iouring_cq_ring_entries ((loop)->iouring_cq_ring_entries) +#define iouring_cq_ring_mask ((loop)->iouring_cq_ring_mask) +#define iouring_cq_ring_size ((loop)->iouring_cq_ring_size) +#define iouring_cq_tail ((loop)->iouring_cq_tail) +#define iouring_entries ((loop)->iouring_entries) +#define iouring_fd ((loop)->iouring_fd) +#define iouring_max_entries ((loop)->iouring_max_entries) +#define iouring_sq_array ((loop)->iouring_sq_array) +#define iouring_sq_dropped ((loop)->iouring_sq_dropped) +#define iouring_sq_flags ((loop)->iouring_sq_flags) +#define iouring_sq_head ((loop)->iouring_sq_head) +#define iouring_sq_ring ((loop)->iouring_sq_ring) +#define iouring_sq_ring_entries ((loop)->iouring_sq_ring_entries) +#define iouring_sq_ring_mask ((loop)->iouring_sq_ring_mask) +#define iouring_sq_ring_size ((loop)->iouring_sq_ring_size) +#define iouring_sq_tail ((loop)->iouring_sq_tail) +#define iouring_sqes ((loop)->iouring_sqes) +#define iouring_sqes_size ((loop)->iouring_sqes_size) +#define iouring_tfd ((loop)->iouring_tfd) +#define iouring_tfd_to ((loop)->iouring_tfd_to) +#define iouring_tfd_w ((loop)->iouring_tfd_w) +#define iouring_to_submit ((loop)->iouring_to_submit) +#define kqueue_changecnt ((loop)->kqueue_changecnt) +#define kqueue_changemax ((loop)->kqueue_changemax) +#define kqueue_changes ((loop)->kqueue_changes) +#define kqueue_eventmax ((loop)->kqueue_eventmax) +#define kqueue_events ((loop)->kqueue_events) +#define kqueue_fd_pid ((loop)->kqueue_fd_pid) +#define linuxaio_ctx ((loop)->linuxaio_ctx) +#define linuxaio_epoll_w ((loop)->linuxaio_epoll_w) +#define linuxaio_iocbpmax ((loop)->linuxaio_iocbpmax) +#define linuxaio_iocbps ((loop)->linuxaio_iocbps) +#define linuxaio_iteration ((loop)->linuxaio_iteration) +#define linuxaio_submitcnt ((loop)->linuxaio_submitcnt) +#define linuxaio_submitmax ((loop)->linuxaio_submitmax) +#define linuxaio_submits ((loop)->linuxaio_submits) +#define loop_count ((loop)->loop_count) +#define loop_depth ((loop)->loop_depth) +#define loop_done ((loop)->loop_done) +#define mn_now ((loop)->mn_now) +#define now_floor ((loop)->now_floor) +#define origflags ((loop)->origflags) +#define pending_w ((loop)->pending_w) +#define pendingcnt ((loop)->pendingcnt) +#define pendingmax ((loop)->pendingmax) +#define pendingpri ((loop)->pendingpri) +#define pendings ((loop)->pendings) +#define periodiccnt ((loop)->periodiccnt) +#define periodicmax ((loop)->periodicmax) +#define periodics ((loop)->periodics) +#define pipe_w ((loop)->pipe_w) +#define pipe_write_skipped ((loop)->pipe_write_skipped) +#define pipe_write_wanted ((loop)->pipe_write_wanted) +#define pollcnt ((loop)->pollcnt) +#define pollidxmax ((loop)->pollidxmax) +#define pollidxs ((loop)->pollidxs) +#define pollmax ((loop)->pollmax) +#define polls ((loop)->polls) +#define port_eventmax ((loop)->port_eventmax) +#define port_events ((loop)->port_events) +#define postfork ((loop)->postfork) +#define preparecnt ((loop)->preparecnt) +#define preparemax ((loop)->preparemax) +#define prepares ((loop)->prepares) +#define release_cb ((loop)->release_cb) +#define rfeedcnt ((loop)->rfeedcnt) +#define rfeedmax ((loop)->rfeedmax) +#define rfeeds ((loop)->rfeeds) +#define rtmn_diff ((loop)->rtmn_diff) +#define sig_pending ((loop)->sig_pending) +#define sigfd ((loop)->sigfd) +#define sigfd_set ((loop)->sigfd_set) +#define sigfd_w ((loop)->sigfd_w) +#define timeout_blocktime ((loop)->timeout_blocktime) +#define timercnt ((loop)->timercnt) +#define timerfd ((loop)->timerfd) +#define timerfd_w ((loop)->timerfd_w) +#define timermax ((loop)->timermax) +#define timers ((loop)->timers) +#define userdata ((loop)->userdata) +#define vec_eo ((loop)->vec_eo) +#define vec_max ((loop)->vec_max) +#define vec_ri ((loop)->vec_ri) +#define vec_ro ((loop)->vec_ro) +#define vec_wi ((loop)->vec_wi) +#define vec_wo ((loop)->vec_wo) +#else +#undef EV_WRAP_H +#undef acquire_cb +#undef activecnt +#undef anfdmax +#undef anfds +#undef async_pending +#undef asynccnt +#undef asyncmax +#undef asyncs +#undef backend +#undef backend_fd +#undef backend_mintime +#undef backend_modify +#undef backend_poll +#undef checkcnt +#undef checkmax +#undef checks +#undef cleanupcnt +#undef cleanupmax +#undef cleanups +#undef curpid +#undef epoll_epermcnt +#undef epoll_epermmax +#undef epoll_eperms +#undef epoll_eventmax +#undef epoll_events +#undef evpipe +#undef fdchangecnt +#undef fdchangemax +#undef fdchanges +#undef forkcnt +#undef forkmax +#undef forks +#undef fs_2625 +#undef fs_fd +#undef fs_hash +#undef fs_w +#undef idleall +#undef idlecnt +#undef idlemax +#undef idles +#undef invoke_cb +#undef io_blocktime +#undef iocp +#undef iouring_cq_cqes +#undef iouring_cq_head +#undef iouring_cq_overflow +#undef iouring_cq_ring +#undef iouring_cq_ring_entries +#undef iouring_cq_ring_mask +#undef iouring_cq_ring_size +#undef iouring_cq_tail +#undef iouring_entries +#undef iouring_fd +#undef iouring_max_entries +#undef iouring_sq_array +#undef iouring_sq_dropped +#undef iouring_sq_flags +#undef iouring_sq_head +#undef iouring_sq_ring +#undef iouring_sq_ring_entries +#undef iouring_sq_ring_mask +#undef iouring_sq_ring_size +#undef iouring_sq_tail +#undef iouring_sqes +#undef iouring_sqes_size +#undef iouring_tfd +#undef iouring_tfd_to +#undef iouring_tfd_w +#undef iouring_to_submit +#undef kqueue_changecnt +#undef kqueue_changemax +#undef kqueue_changes +#undef kqueue_eventmax +#undef kqueue_events +#undef kqueue_fd_pid +#undef linuxaio_ctx +#undef linuxaio_epoll_w +#undef linuxaio_iocbpmax +#undef linuxaio_iocbps +#undef linuxaio_iteration +#undef linuxaio_submitcnt +#undef linuxaio_submitmax +#undef linuxaio_submits +#undef loop_count +#undef loop_depth +#undef loop_done +#undef mn_now +#undef now_floor +#undef origflags +#undef pending_w +#undef pendingcnt +#undef pendingmax +#undef pendingpri +#undef pendings +#undef periodiccnt +#undef periodicmax +#undef periodics +#undef pipe_w +#undef pipe_write_skipped +#undef pipe_write_wanted +#undef pollcnt +#undef pollidxmax +#undef pollidxs +#undef pollmax +#undef polls +#undef port_eventmax +#undef port_events +#undef postfork +#undef preparecnt +#undef preparemax +#undef prepares +#undef release_cb +#undef rfeedcnt +#undef rfeedmax +#undef rfeeds +#undef rtmn_diff +#undef sig_pending +#undef sigfd +#undef sigfd_set +#undef sigfd_w +#undef timeout_blocktime +#undef timercnt +#undef timerfd +#undef timerfd_w +#undef timermax +#undef timers +#undef userdata +#undef vec_eo +#undef vec_max +#undef vec_ri +#undef vec_ro +#undef vec_wi +#undef vec_wo +#endif diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/event.c b/deps/ngtcp2/ngtcp2/third-party/libev/event.c new file mode 100644 index 00000000000000..5586cd353835dc --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/event.c @@ -0,0 +1,425 @@ +/* + * libevent compatibility layer + * + * Copyright (c) 2007,2008,2009,2010,2012 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +#include +#include +#include + +#ifdef EV_EVENT_H +# include EV_EVENT_H +#else +# include "event.h" +#endif + +#if EV_MULTIPLICITY +# define dLOOPev struct ev_loop *loop = (struct ev_loop *)ev->ev_base +# define dLOOPbase struct ev_loop *loop = (struct ev_loop *)base +#else +# define dLOOPev +# define dLOOPbase +#endif + +/* never accessed, will always be cast from/to ev_loop */ +struct event_base +{ + int dummy; +}; + +static struct event_base *ev_x_cur; + +static ev_tstamp +ev_tv_get (struct timeval *tv) +{ + if (tv) + { + ev_tstamp after = tv->tv_sec + tv->tv_usec * 1e-6; + return after ? after : 1e-6; + } + else + return -1.; +} + +#define EVENT_STRINGIFY(s) # s +#define EVENT_VERSION(a,b) EVENT_STRINGIFY (a) "." EVENT_STRINGIFY (b) + +const char * +event_get_version (void) +{ + /* returns ABI, not API or library, version */ + return EVENT_VERSION (EV_VERSION_MAJOR, EV_VERSION_MINOR); +} + +const char * +event_get_method (void) +{ + return "libev"; +} + +void *event_init (void) +{ +#if EV_MULTIPLICITY + if (ev_x_cur) + ev_x_cur = (struct event_base *)ev_loop_new (EVFLAG_AUTO); + else + ev_x_cur = (struct event_base *)ev_default_loop (EVFLAG_AUTO); +#else + assert (("libev: multiple event bases not supported when not compiled with EV_MULTIPLICITY", !ev_x_cur)); + + ev_x_cur = (struct event_base *)(long)ev_default_loop (EVFLAG_AUTO); +#endif + + return ev_x_cur; +} + +const char * +event_base_get_method (const struct event_base *base) +{ + return "libev"; +} + +struct event_base * +event_base_new (void) +{ +#if EV_MULTIPLICITY + return (struct event_base *)ev_loop_new (EVFLAG_AUTO); +#else + assert (("libev: multiple event bases not supported when not compiled with EV_MULTIPLICITY")); + return NULL; +#endif +} + +void event_base_free (struct event_base *base) +{ + dLOOPbase; + +#if EV_MULTIPLICITY + if (!ev_is_default_loop (loop)) + ev_loop_destroy (loop); +#endif +} + +int event_dispatch (void) +{ + return event_base_dispatch (ev_x_cur); +} + +#ifdef EV_STANDALONE +void event_set_log_callback (event_log_cb cb) +{ + /* nop */ +} +#endif + +int event_loop (int flags) +{ + return event_base_loop (ev_x_cur, flags); +} + +int event_loopexit (struct timeval *tv) +{ + return event_base_loopexit (ev_x_cur, tv); +} + +event_callback_fn event_get_callback +(const struct event *ev) +{ + return ev->ev_callback; +} + +static void +ev_x_cb (struct event *ev, int revents) +{ + revents &= EV_READ | EV_WRITE | EV_TIMER | EV_SIGNAL; + + ev->ev_res = revents; + ev->ev_callback (ev->ev_fd, (short)revents, ev->ev_arg); +} + +static void +ev_x_cb_sig (EV_P_ struct ev_signal *w, int revents) +{ + struct event *ev = (struct event *)(((char *)w) - offsetof (struct event, iosig.sig)); + + if (revents & EV_ERROR) + event_del (ev); + + ev_x_cb (ev, revents); +} + +static void +ev_x_cb_io (EV_P_ struct ev_io *w, int revents) +{ + struct event *ev = (struct event *)(((char *)w) - offsetof (struct event, iosig.io)); + + if ((revents & EV_ERROR) || !(ev->ev_events & EV_PERSIST)) + event_del (ev); + + ev_x_cb (ev, revents); +} + +static void +ev_x_cb_to (EV_P_ struct ev_timer *w, int revents) +{ + struct event *ev = (struct event *)(((char *)w) - offsetof (struct event, to)); + + event_del (ev); + + ev_x_cb (ev, revents); +} + +void event_set (struct event *ev, int fd, short events, void (*cb)(int, short, void *), void *arg) +{ + if (events & EV_SIGNAL) + ev_init (&ev->iosig.sig, ev_x_cb_sig); + else + ev_init (&ev->iosig.io, ev_x_cb_io); + + ev_init (&ev->to, ev_x_cb_to); + + ev->ev_base = ev_x_cur; /* not threadsafe, but it's how libevent works */ + ev->ev_fd = fd; + ev->ev_events = events; + ev->ev_pri = 0; + ev->ev_callback = cb; + ev->ev_arg = arg; + ev->ev_res = 0; + ev->ev_flags = EVLIST_INIT; +} + +int event_once (int fd, short events, void (*cb)(int, short, void *), void *arg, struct timeval *tv) +{ + return event_base_once (ev_x_cur, fd, events, cb, arg, tv); +} + +int event_add (struct event *ev, struct timeval *tv) +{ + dLOOPev; + + if (ev->ev_events & EV_SIGNAL) + { + if (!ev_is_active (&ev->iosig.sig)) + { + ev_signal_set (&ev->iosig.sig, ev->ev_fd); + ev_signal_start (EV_A_ &ev->iosig.sig); + + ev->ev_flags |= EVLIST_SIGNAL; + } + } + else if (ev->ev_events & (EV_READ | EV_WRITE)) + { + if (!ev_is_active (&ev->iosig.io)) + { + ev_io_set (&ev->iosig.io, ev->ev_fd, ev->ev_events & (EV_READ | EV_WRITE)); + ev_io_start (EV_A_ &ev->iosig.io); + + ev->ev_flags |= EVLIST_INSERTED; + } + } + + if (tv) + { + ev->to.repeat = ev_tv_get (tv); + ev_timer_again (EV_A_ &ev->to); + ev->ev_flags |= EVLIST_TIMEOUT; + } + else + { + ev_timer_stop (EV_A_ &ev->to); + ev->ev_flags &= ~EVLIST_TIMEOUT; + } + + ev->ev_flags |= EVLIST_ACTIVE; + + return 0; +} + +int event_del (struct event *ev) +{ + dLOOPev; + + if (ev->ev_events & EV_SIGNAL) + ev_signal_stop (EV_A_ &ev->iosig.sig); + else if (ev->ev_events & (EV_READ | EV_WRITE)) + ev_io_stop (EV_A_ &ev->iosig.io); + + if (ev_is_active (&ev->to)) + ev_timer_stop (EV_A_ &ev->to); + + ev->ev_flags = EVLIST_INIT; + + return 0; +} + +void event_active (struct event *ev, int res, short ncalls) +{ + dLOOPev; + + if (res & EV_TIMEOUT) + ev_feed_event (EV_A_ &ev->to, res & EV_TIMEOUT); + + if (res & EV_SIGNAL) + ev_feed_event (EV_A_ &ev->iosig.sig, res & EV_SIGNAL); + + if (res & (EV_READ | EV_WRITE)) + ev_feed_event (EV_A_ &ev->iosig.io, res & (EV_READ | EV_WRITE)); +} + +int event_pending (struct event *ev, short events, struct timeval *tv) +{ + short revents = 0; + dLOOPev; + + if (ev->ev_events & EV_SIGNAL) + { + /* sig */ + if (ev_is_active (&ev->iosig.sig) || ev_is_pending (&ev->iosig.sig)) + revents |= EV_SIGNAL; + } + else if (ev->ev_events & (EV_READ | EV_WRITE)) + { + /* io */ + if (ev_is_active (&ev->iosig.io) || ev_is_pending (&ev->iosig.io)) + revents |= ev->ev_events & (EV_READ | EV_WRITE); + } + + if (ev->ev_events & EV_TIMEOUT || ev_is_active (&ev->to) || ev_is_pending (&ev->to)) + { + revents |= EV_TIMEOUT; + + if (tv) + { + ev_tstamp at = ev_now (EV_A); + + tv->tv_sec = (long)at; + tv->tv_usec = (long)((at - (ev_tstamp)tv->tv_sec) * 1e6); + } + } + + return events & revents; +} + +int event_priority_init (int npri) +{ + return event_base_priority_init (ev_x_cur, npri); +} + +int event_priority_set (struct event *ev, int pri) +{ + ev->ev_pri = pri; + + return 0; +} + +int event_base_set (struct event_base *base, struct event *ev) +{ + ev->ev_base = base; + + return 0; +} + +int event_base_loop (struct event_base *base, int flags) +{ + dLOOPbase; + + return !ev_run (EV_A_ flags); +} + +int event_base_dispatch (struct event_base *base) +{ + return event_base_loop (base, 0); +} + +static void +ev_x_loopexit_cb (int revents, void *base) +{ + dLOOPbase; + + ev_break (EV_A_ EVBREAK_ONE); +} + +int event_base_loopexit (struct event_base *base, struct timeval *tv) +{ + ev_tstamp after = ev_tv_get (tv); + dLOOPbase; + + ev_once (EV_A_ -1, 0, after >= 0. ? after : 0., ev_x_loopexit_cb, (void *)base); + + return 0; +} + +struct ev_x_once +{ + int fd; + void (*cb)(int, short, void *); + void *arg; +}; + +static void +ev_x_once_cb (int revents, void *arg) +{ + struct ev_x_once *once = (struct ev_x_once *)arg; + + once->cb (once->fd, (short)revents, once->arg); + free (once); +} + +int event_base_once (struct event_base *base, int fd, short events, void (*cb)(int, short, void *), void *arg, struct timeval *tv) +{ + struct ev_x_once *once = (struct ev_x_once *)malloc (sizeof (struct ev_x_once)); + dLOOPbase; + + if (!once) + return -1; + + once->fd = fd; + once->cb = cb; + once->arg = arg; + + ev_once (EV_A_ fd, events & (EV_READ | EV_WRITE), ev_tv_get (tv), ev_x_once_cb, (void *)once); + + return 0; +} + +int event_base_priority_init (struct event_base *base, int npri) +{ + /*dLOOPbase;*/ + + return 0; +} + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/event.h b/deps/ngtcp2/ngtcp2/third-party/libev/event.h new file mode 100644 index 00000000000000..aa81928f35649a --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/event.h @@ -0,0 +1,177 @@ +/* + * libevent compatibility header, only core events supported + * + * Copyright (c) 2007,2008,2010,2012 Marc Alexander Lehmann + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modifica- + * tion, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MER- + * CHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPE- + * CIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTH- + * ERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Alternatively, the contents of this file may be used under the terms of + * the GNU General Public License ("GPL") version 2 or any later version, + * in which case the provisions of the GPL are applicable instead of + * the above. If you wish to allow the use of your version of this file + * only under the terms of the GPL and not to allow others to use your + * version of this file under the BSD license, indicate your decision + * by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL. If you do not delete the + * provisions above, a recipient may use your version of this file under + * either the BSD or the GPL. + */ + +#ifndef EVENT_H_ +#define EVENT_H_ + +#ifdef EV_H +# include EV_H +#else +# include "ev.h" +#endif + +#ifndef EVLOOP_NONBLOCK +# define EVLOOP_NONBLOCK EVRUN_NOWAIT +#endif +#ifndef EVLOOP_ONESHOT +# define EVLOOP_ONESHOT EVRUN_ONCE +#endif +#ifndef EV_TIMEOUT +# define EV_TIMEOUT EV_TIMER +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* we need sys/time.h for struct timeval only */ +#if !defined (WIN32) || defined (__MINGW32__) +# include /* mingw seems to need this, for whatever reason */ +# include +#endif + +struct event_base; + +#define EVLIST_TIMEOUT 0x01 +#define EVLIST_INSERTED 0x02 +#define EVLIST_SIGNAL 0x04 +#define EVLIST_ACTIVE 0x08 +#define EVLIST_INTERNAL 0x10 +#define EVLIST_INIT 0x80 + +typedef void (*event_callback_fn)(int, short, void *); + +struct event +{ + /* libev watchers we map onto */ + union { + struct ev_io io; + struct ev_signal sig; + } iosig; + struct ev_timer to; + + /* compatibility slots */ + struct event_base *ev_base; + event_callback_fn ev_callback; + void *ev_arg; + int ev_fd; + int ev_pri; + int ev_res; + int ev_flags; + short ev_events; +}; + +event_callback_fn event_get_callback (const struct event *ev); + +#define EV_READ EV_READ +#define EV_WRITE EV_WRITE +#define EV_PERSIST 0x10 +#define EV_ET 0x20 /* nop */ + +#define EVENT_SIGNAL(ev) ((int) (ev)->ev_fd) +#define EVENT_FD(ev) ((int) (ev)->ev_fd) + +#define event_initialized(ev) ((ev)->ev_flags & EVLIST_INIT) + +#define evtimer_add(ev,tv) event_add (ev, tv) +#define evtimer_set(ev,cb,data) event_set (ev, -1, 0, cb, data) +#define evtimer_del(ev) event_del (ev) +#define evtimer_pending(ev,tv) event_pending (ev, EV_TIMEOUT, tv) +#define evtimer_initialized(ev) event_initialized (ev) + +#define timeout_add(ev,tv) evtimer_add (ev, tv) +#define timeout_set(ev,cb,data) evtimer_set (ev, cb, data) +#define timeout_del(ev) evtimer_del (ev) +#define timeout_pending(ev,tv) evtimer_pending (ev, tv) +#define timeout_initialized(ev) evtimer_initialized (ev) + +#define signal_add(ev,tv) event_add (ev, tv) +#define signal_set(ev,sig,cb,data) event_set (ev, sig, EV_SIGNAL | EV_PERSIST, cb, data) +#define signal_del(ev) event_del (ev) +#define signal_pending(ev,tv) event_pending (ev, EV_SIGNAL, tv) +#define signal_initialized(ev) event_initialized (ev) + +const char *event_get_version (void); +const char *event_get_method (void); + +void *event_init (void); +void event_base_free (struct event_base *base); + +#define EVLOOP_ONCE EVLOOP_ONESHOT +int event_loop (int); +int event_loopexit (struct timeval *tv); +int event_dispatch (void); + +#define _EVENT_LOG_DEBUG 0 +#define _EVENT_LOG_MSG 1 +#define _EVENT_LOG_WARN 2 +#define _EVENT_LOG_ERR 3 +typedef void (*event_log_cb)(int severity, const char *msg); +void event_set_log_callback(event_log_cb cb); + +void event_set (struct event *ev, int fd, short events, void (*cb)(int, short, void *), void *arg); +int event_once (int fd, short events, void (*cb)(int, short, void *), void *arg, struct timeval *tv); + +int event_add (struct event *ev, struct timeval *tv); +int event_del (struct event *ev); +void event_active (struct event *ev, int res, short ncalls); /* ncalls is being ignored */ + +int event_pending (struct event *ev, short, struct timeval *tv); + +int event_priority_init (int npri); +int event_priority_set (struct event *ev, int pri); + +struct event_base *event_base_new (void); +const char *event_base_get_method (const struct event_base *); +int event_base_set (struct event_base *base, struct event *ev); +int event_base_loop (struct event_base *base, int); +int event_base_loopexit (struct event_base *base, struct timeval *tv); +int event_base_dispatch (struct event_base *base); +int event_base_once (struct event_base *base, int fd, short events, void (*cb)(int, short, void *), void *arg, struct timeval *tv); +int event_base_priority_init (struct event_base *base, int fd); + +/* next line is different in the libevent+libev version */ +/*libevent-include*/ + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/install-sh b/deps/ngtcp2/ngtcp2/third-party/libev/install-sh new file mode 100755 index 00000000000000..8175c640fe6288 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/install-sh @@ -0,0 +1,518 @@ +#!/bin/sh +# install - install a program, script, or datafile + +scriptversion=2018-03-11.20; # UTC + +# This originates from X11R5 (mit/util/scripts/install.sh), which was +# later released in X11R6 (xc/config/util/install.sh) with the +# following copyright and license. +# +# Copyright (C) 1994 X Consortium +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC- +# TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Except as contained in this notice, the name of the X Consortium shall not +# be used in advertising or otherwise to promote the sale, use or other deal- +# ings in this Software without prior written authorization from the X Consor- +# tium. +# +# +# FSF changes to this file are in the public domain. +# +# Calling this script install-sh is preferred over install.sh, to prevent +# 'make' implicit rules from creating a file called install from it +# when there is no Makefile. +# +# This script is compatible with the BSD install script, but was written +# from scratch. + +tab=' ' +nl=' +' +IFS=" $tab$nl" + +# Set DOITPROG to "echo" to test this script. + +doit=${DOITPROG-} +doit_exec=${doit:-exec} + +# Put in absolute file names if you don't have them in your path; +# or use environment vars. + +chgrpprog=${CHGRPPROG-chgrp} +chmodprog=${CHMODPROG-chmod} +chownprog=${CHOWNPROG-chown} +cmpprog=${CMPPROG-cmp} +cpprog=${CPPROG-cp} +mkdirprog=${MKDIRPROG-mkdir} +mvprog=${MVPROG-mv} +rmprog=${RMPROG-rm} +stripprog=${STRIPPROG-strip} + +posix_mkdir= + +# Desired mode of installed file. +mode=0755 + +chgrpcmd= +chmodcmd=$chmodprog +chowncmd= +mvcmd=$mvprog +rmcmd="$rmprog -f" +stripcmd= + +src= +dst= +dir_arg= +dst_arg= + +copy_on_change=false +is_target_a_directory=possibly + +usage="\ +Usage: $0 [OPTION]... [-T] SRCFILE DSTFILE + or: $0 [OPTION]... SRCFILES... DIRECTORY + or: $0 [OPTION]... -t DIRECTORY SRCFILES... + or: $0 [OPTION]... -d DIRECTORIES... + +In the 1st form, copy SRCFILE to DSTFILE. +In the 2nd and 3rd, copy all SRCFILES to DIRECTORY. +In the 4th, create DIRECTORIES. + +Options: + --help display this help and exit. + --version display version info and exit. + + -c (ignored) + -C install only if different (preserve the last data modification time) + -d create directories instead of installing files. + -g GROUP $chgrpprog installed files to GROUP. + -m MODE $chmodprog installed files to MODE. + -o USER $chownprog installed files to USER. + -s $stripprog installed files. + -t DIRECTORY install into DIRECTORY. + -T report an error if DSTFILE is a directory. + +Environment variables override the default commands: + CHGRPPROG CHMODPROG CHOWNPROG CMPPROG CPPROG MKDIRPROG MVPROG + RMPROG STRIPPROG +" + +while test $# -ne 0; do + case $1 in + -c) ;; + + -C) copy_on_change=true;; + + -d) dir_arg=true;; + + -g) chgrpcmd="$chgrpprog $2" + shift;; + + --help) echo "$usage"; exit $?;; + + -m) mode=$2 + case $mode in + *' '* | *"$tab"* | *"$nl"* | *'*'* | *'?'* | *'['*) + echo "$0: invalid mode: $mode" >&2 + exit 1;; + esac + shift;; + + -o) chowncmd="$chownprog $2" + shift;; + + -s) stripcmd=$stripprog;; + + -t) + is_target_a_directory=always + dst_arg=$2 + # Protect names problematic for 'test' and other utilities. + case $dst_arg in + -* | [=\(\)!]) dst_arg=./$dst_arg;; + esac + shift;; + + -T) is_target_a_directory=never;; + + --version) echo "$0 $scriptversion"; exit $?;; + + --) shift + break;; + + -*) echo "$0: invalid option: $1" >&2 + exit 1;; + + *) break;; + esac + shift +done + +# We allow the use of options -d and -T together, by making -d +# take the precedence; this is for compatibility with GNU install. + +if test -n "$dir_arg"; then + if test -n "$dst_arg"; then + echo "$0: target directory not allowed when installing a directory." >&2 + exit 1 + fi +fi + +if test $# -ne 0 && test -z "$dir_arg$dst_arg"; then + # When -d is used, all remaining arguments are directories to create. + # When -t is used, the destination is already specified. + # Otherwise, the last argument is the destination. Remove it from $@. + for arg + do + if test -n "$dst_arg"; then + # $@ is not empty: it contains at least $arg. + set fnord "$@" "$dst_arg" + shift # fnord + fi + shift # arg + dst_arg=$arg + # Protect names problematic for 'test' and other utilities. + case $dst_arg in + -* | [=\(\)!]) dst_arg=./$dst_arg;; + esac + done +fi + +if test $# -eq 0; then + if test -z "$dir_arg"; then + echo "$0: no input file specified." >&2 + exit 1 + fi + # It's OK to call 'install-sh -d' without argument. + # This can happen when creating conditional directories. + exit 0 +fi + +if test -z "$dir_arg"; then + if test $# -gt 1 || test "$is_target_a_directory" = always; then + if test ! -d "$dst_arg"; then + echo "$0: $dst_arg: Is not a directory." >&2 + exit 1 + fi + fi +fi + +if test -z "$dir_arg"; then + do_exit='(exit $ret); exit $ret' + trap "ret=129; $do_exit" 1 + trap "ret=130; $do_exit" 2 + trap "ret=141; $do_exit" 13 + trap "ret=143; $do_exit" 15 + + # Set umask so as not to create temps with too-generous modes. + # However, 'strip' requires both read and write access to temps. + case $mode in + # Optimize common cases. + *644) cp_umask=133;; + *755) cp_umask=22;; + + *[0-7]) + if test -z "$stripcmd"; then + u_plus_rw= + else + u_plus_rw='% 200' + fi + cp_umask=`expr '(' 777 - $mode % 1000 ')' $u_plus_rw`;; + *) + if test -z "$stripcmd"; then + u_plus_rw= + else + u_plus_rw=,u+rw + fi + cp_umask=$mode$u_plus_rw;; + esac +fi + +for src +do + # Protect names problematic for 'test' and other utilities. + case $src in + -* | [=\(\)!]) src=./$src;; + esac + + if test -n "$dir_arg"; then + dst=$src + dstdir=$dst + test -d "$dstdir" + dstdir_status=$? + else + + # Waiting for this to be detected by the "$cpprog $src $dsttmp" command + # might cause directories to be created, which would be especially bad + # if $src (and thus $dsttmp) contains '*'. + if test ! -f "$src" && test ! -d "$src"; then + echo "$0: $src does not exist." >&2 + exit 1 + fi + + if test -z "$dst_arg"; then + echo "$0: no destination specified." >&2 + exit 1 + fi + dst=$dst_arg + + # If destination is a directory, append the input filename. + if test -d "$dst"; then + if test "$is_target_a_directory" = never; then + echo "$0: $dst_arg: Is a directory" >&2 + exit 1 + fi + dstdir=$dst + dstbase=`basename "$src"` + case $dst in + */) dst=$dst$dstbase;; + *) dst=$dst/$dstbase;; + esac + dstdir_status=0 + else + dstdir=`dirname "$dst"` + test -d "$dstdir" + dstdir_status=$? + fi + fi + + case $dstdir in + */) dstdirslash=$dstdir;; + *) dstdirslash=$dstdir/;; + esac + + obsolete_mkdir_used=false + + if test $dstdir_status != 0; then + case $posix_mkdir in + '') + # Create intermediate dirs using mode 755 as modified by the umask. + # This is like FreeBSD 'install' as of 1997-10-28. + umask=`umask` + case $stripcmd.$umask in + # Optimize common cases. + *[2367][2367]) mkdir_umask=$umask;; + .*0[02][02] | .[02][02] | .[02]) mkdir_umask=22;; + + *[0-7]) + mkdir_umask=`expr $umask + 22 \ + - $umask % 100 % 40 + $umask % 20 \ + - $umask % 10 % 4 + $umask % 2 + `;; + *) mkdir_umask=$umask,go-w;; + esac + + # With -d, create the new directory with the user-specified mode. + # Otherwise, rely on $mkdir_umask. + if test -n "$dir_arg"; then + mkdir_mode=-m$mode + else + mkdir_mode= + fi + + posix_mkdir=false + case $umask in + *[123567][0-7][0-7]) + # POSIX mkdir -p sets u+wx bits regardless of umask, which + # is incompatible with FreeBSD 'install' when (umask & 300) != 0. + ;; + *) + # Note that $RANDOM variable is not portable (e.g. dash); Use it + # here however when possible just to lower collision chance. + tmpdir=${TMPDIR-/tmp}/ins$RANDOM-$$ + + trap 'ret=$?; rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir" 2>/dev/null; exit $ret' 0 + + # Because "mkdir -p" follows existing symlinks and we likely work + # directly in world-writeable /tmp, make sure that the '$tmpdir' + # directory is successfully created first before we actually test + # 'mkdir -p' feature. + if (umask $mkdir_umask && + $mkdirprog $mkdir_mode "$tmpdir" && + exec $mkdirprog $mkdir_mode -p -- "$tmpdir/a/b") >/dev/null 2>&1 + then + if test -z "$dir_arg" || { + # Check for POSIX incompatibilities with -m. + # HP-UX 11.23 and IRIX 6.5 mkdir -m -p sets group- or + # other-writable bit of parent directory when it shouldn't. + # FreeBSD 6.1 mkdir -m -p sets mode of existing directory. + test_tmpdir="$tmpdir/a" + ls_ld_tmpdir=`ls -ld "$test_tmpdir"` + case $ls_ld_tmpdir in + d????-?r-*) different_mode=700;; + d????-?--*) different_mode=755;; + *) false;; + esac && + $mkdirprog -m$different_mode -p -- "$test_tmpdir" && { + ls_ld_tmpdir_1=`ls -ld "$test_tmpdir"` + test "$ls_ld_tmpdir" = "$ls_ld_tmpdir_1" + } + } + then posix_mkdir=: + fi + rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir" + else + # Remove any dirs left behind by ancient mkdir implementations. + rmdir ./$mkdir_mode ./-p ./-- "$tmpdir" 2>/dev/null + fi + trap '' 0;; + esac;; + esac + + if + $posix_mkdir && ( + umask $mkdir_umask && + $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir" + ) + then : + else + + # The umask is ridiculous, or mkdir does not conform to POSIX, + # or it failed possibly due to a race condition. Create the + # directory the slow way, step by step, checking for races as we go. + + case $dstdir in + /*) prefix='/';; + [-=\(\)!]*) prefix='./';; + *) prefix='';; + esac + + oIFS=$IFS + IFS=/ + set -f + set fnord $dstdir + shift + set +f + IFS=$oIFS + + prefixes= + + for d + do + test X"$d" = X && continue + + prefix=$prefix$d + if test -d "$prefix"; then + prefixes= + else + if $posix_mkdir; then + (umask=$mkdir_umask && + $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir") && break + # Don't fail if two instances are running concurrently. + test -d "$prefix" || exit 1 + else + case $prefix in + *\'*) qprefix=`echo "$prefix" | sed "s/'/'\\\\\\\\''/g"`;; + *) qprefix=$prefix;; + esac + prefixes="$prefixes '$qprefix'" + fi + fi + prefix=$prefix/ + done + + if test -n "$prefixes"; then + # Don't fail if two instances are running concurrently. + (umask $mkdir_umask && + eval "\$doit_exec \$mkdirprog $prefixes") || + test -d "$dstdir" || exit 1 + obsolete_mkdir_used=true + fi + fi + fi + + if test -n "$dir_arg"; then + { test -z "$chowncmd" || $doit $chowncmd "$dst"; } && + { test -z "$chgrpcmd" || $doit $chgrpcmd "$dst"; } && + { test "$obsolete_mkdir_used$chowncmd$chgrpcmd" = false || + test -z "$chmodcmd" || $doit $chmodcmd $mode "$dst"; } || exit 1 + else + + # Make a couple of temp file names in the proper directory. + dsttmp=${dstdirslash}_inst.$$_ + rmtmp=${dstdirslash}_rm.$$_ + + # Trap to clean up those temp files at exit. + trap 'ret=$?; rm -f "$dsttmp" "$rmtmp" && exit $ret' 0 + + # Copy the file name to the temp name. + (umask $cp_umask && $doit_exec $cpprog "$src" "$dsttmp") && + + # and set any options; do chmod last to preserve setuid bits. + # + # If any of these fail, we abort the whole thing. If we want to + # ignore errors from any of these, just make sure not to ignore + # errors from the above "$doit $cpprog $src $dsttmp" command. + # + { test -z "$chowncmd" || $doit $chowncmd "$dsttmp"; } && + { test -z "$chgrpcmd" || $doit $chgrpcmd "$dsttmp"; } && + { test -z "$stripcmd" || $doit $stripcmd "$dsttmp"; } && + { test -z "$chmodcmd" || $doit $chmodcmd $mode "$dsttmp"; } && + + # If -C, don't bother to copy if it wouldn't change the file. + if $copy_on_change && + old=`LC_ALL=C ls -dlL "$dst" 2>/dev/null` && + new=`LC_ALL=C ls -dlL "$dsttmp" 2>/dev/null` && + set -f && + set X $old && old=:$2:$4:$5:$6 && + set X $new && new=:$2:$4:$5:$6 && + set +f && + test "$old" = "$new" && + $cmpprog "$dst" "$dsttmp" >/dev/null 2>&1 + then + rm -f "$dsttmp" + else + # Rename the file to the real destination. + $doit $mvcmd -f "$dsttmp" "$dst" 2>/dev/null || + + # The rename failed, perhaps because mv can't rename something else + # to itself, or perhaps because mv is so ancient that it does not + # support -f. + { + # Now remove or move aside any old file at destination location. + # We try this two ways since rm can't unlink itself on some + # systems and the destination file might be busy for other + # reasons. In this case, the final cleanup might fail but the new + # file should still install successfully. + { + test ! -f "$dst" || + $doit $rmcmd -f "$dst" 2>/dev/null || + { $doit $mvcmd -f "$dst" "$rmtmp" 2>/dev/null && + { $doit $rmcmd -f "$rmtmp" 2>/dev/null; :; } + } || + { echo "$0: cannot unlink or rename $dst" >&2 + (exit 1); exit 1 + } + } && + + # Now rename the file to the real destination. + $doit $mvcmd "$dsttmp" "$dst" + } + fi || exit 1 + + trap '' 0 + fi +done + +# Local variables: +# eval: (add-hook 'before-save-hook 'time-stamp) +# time-stamp-start: "scriptversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-time-zone: "UTC0" +# time-stamp-end: "; # UTC" +# End: diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/libev.m4 b/deps/ngtcp2/ngtcp2/third-party/libev/libev.m4 new file mode 100644 index 00000000000000..f859eff27c1910 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/libev.m4 @@ -0,0 +1,47 @@ +dnl this file is part of libev, do not make local modifications +dnl http://software.schmorp.de/pkg/libev + +dnl libev support +AC_CHECK_HEADERS(sys/inotify.h sys/epoll.h sys/event.h port.h poll.h sys/timerfd.h) +AC_CHECK_HEADERS(sys/select.h sys/eventfd.h sys/signalfd.h linux/aio_abi.h linux/fs.h) + +AC_CHECK_FUNCS(inotify_init epoll_ctl kqueue port_create poll select eventfd signalfd) + +AC_CHECK_FUNCS(clock_gettime, [], [ + dnl on linux, try syscall wrapper first + if test $(uname) = Linux; then + AC_MSG_CHECKING(for clock_gettime syscall) + AC_LINK_IFELSE([AC_LANG_PROGRAM( + [#include + #include + #include ], + [struct timespec ts; int status = syscall (SYS_clock_gettime, CLOCK_REALTIME, &ts)])], + [ac_have_clock_syscall=1 + AC_DEFINE(HAVE_CLOCK_SYSCALL, 1, Define to 1 to use the syscall interface for clock_gettime) + AC_MSG_RESULT(yes)], + [AC_MSG_RESULT(no)]) + fi + if test -z "$LIBEV_M4_AVOID_LIBRT" && test -z "$ac_have_clock_syscall"; then + AC_CHECK_LIB(rt, clock_gettime) + unset ac_cv_func_clock_gettime + AC_CHECK_FUNCS(clock_gettime) + fi +]) + +AC_CHECK_FUNCS(nanosleep, [], [ + if test -z "$LIBEV_M4_AVOID_LIBRT"; then + AC_CHECK_LIB(rt, nanosleep) + unset ac_cv_func_nanosleep + AC_CHECK_FUNCS(nanosleep) + fi +]) + +AC_CHECK_TYPE(__kernel_rwf_t, [ + AC_DEFINE(HAVE_KERNEL_RWF_T, 1, Define to 1 if linux/fs.h defined kernel_rwf_t) +], [], [#include ]) + +if test -z "$LIBEV_M4_AVOID_LIBM"; then + LIBM=m +fi +AC_SEARCH_LIBS(floor, $LIBM, [AC_DEFINE(HAVE_FLOOR, 1, Define to 1 if the floor function is available)]) + diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/ltmain.sh b/deps/ngtcp2/ngtcp2/third-party/libev/ltmain.sh new file mode 100644 index 00000000000000..c12c197be78798 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/ltmain.sh @@ -0,0 +1,11249 @@ +#! /bin/sh +## DO NOT EDIT - This file generated from ./build-aux/ltmain.in +## by inline-source v2014-01-03.01 + +# libtool (GNU libtool) 2.4.6 +# Provide generalized library-building support services. +# Written by Gordon Matzigkeit , 1996 + +# Copyright (C) 1996-2015 Free Software Foundation, Inc. +# This is free software; see the source for copying conditions. There is NO +# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +# GNU Libtool is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# As a special exception to the GNU General Public License, +# if you distribute this file as part of a program or library that +# is built using GNU Libtool, you may include this file under the +# same distribution terms that you use for the rest of that program. +# +# GNU Libtool is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +PROGRAM=libtool +PACKAGE=libtool +VERSION="2.4.6 Debian-2.4.6-9" +package_revision=2.4.6 + + +## ------ ## +## Usage. ## +## ------ ## + +# Run './libtool --help' for help with using this script from the +# command line. + + +## ------------------------------- ## +## User overridable command paths. ## +## ------------------------------- ## + +# After configure completes, it has a better idea of some of the +# shell tools we need than the defaults used by the functions shared +# with bootstrap, so set those here where they can still be over- +# ridden by the user, but otherwise take precedence. + +: ${AUTOCONF="autoconf"} +: ${AUTOMAKE="automake"} + + +## -------------------------- ## +## Source external libraries. ## +## -------------------------- ## + +# Much of our low-level functionality needs to be sourced from external +# libraries, which are installed to $pkgauxdir. + +# Set a version string for this script. +scriptversion=2015-01-20.17; # UTC + +# General shell script boiler plate, and helper functions. +# Written by Gary V. Vaughan, 2004 + +# Copyright (C) 2004-2015 Free Software Foundation, Inc. +# This is free software; see the source for copying conditions. There is NO +# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. + +# As a special exception to the GNU General Public License, if you distribute +# this file as part of a program or library that is built using GNU Libtool, +# you may include this file under the same distribution terms that you use +# for the rest of that program. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNES FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Please report bugs or propose patches to gary@gnu.org. + + +## ------ ## +## Usage. ## +## ------ ## + +# Evaluate this file near the top of your script to gain access to +# the functions and variables defined here: +# +# . `echo "$0" | ${SED-sed} 's|[^/]*$||'`/build-aux/funclib.sh +# +# If you need to override any of the default environment variable +# settings, do that before evaluating this file. + + +## -------------------- ## +## Shell normalisation. ## +## -------------------- ## + +# Some shells need a little help to be as Bourne compatible as possible. +# Before doing anything else, make sure all that help has been provided! + +DUALCASE=1; export DUALCASE # for MKS sh +if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then : + emulate sh + NULLCMD=: + # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which + # is contrary to our usage. Disable this feature. + alias -g '${1+"$@"}'='"$@"' + setopt NO_GLOB_SUBST +else + case `(set -o) 2>/dev/null` in *posix*) set -o posix ;; esac +fi + +# NLS nuisances: We save the old values in case they are required later. +_G_user_locale= +_G_safe_locale= +for _G_var in LANG LANGUAGE LC_ALL LC_CTYPE LC_COLLATE LC_MESSAGES +do + eval "if test set = \"\${$_G_var+set}\"; then + save_$_G_var=\$$_G_var + $_G_var=C + export $_G_var + _G_user_locale=\"$_G_var=\\\$save_\$_G_var; \$_G_user_locale\" + _G_safe_locale=\"$_G_var=C; \$_G_safe_locale\" + fi" +done + +# CDPATH. +(unset CDPATH) >/dev/null 2>&1 && unset CDPATH + +# Make sure IFS has a sensible default +sp=' ' +nl=' +' +IFS="$sp $nl" + +# There are apparently some retarded systems that use ';' as a PATH separator! +if test "${PATH_SEPARATOR+set}" != set; then + PATH_SEPARATOR=: + (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { + (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || + PATH_SEPARATOR=';' + } +fi + + + +## ------------------------- ## +## Locate command utilities. ## +## ------------------------- ## + + +# func_executable_p FILE +# ---------------------- +# Check that FILE is an executable regular file. +func_executable_p () +{ + test -f "$1" && test -x "$1" +} + + +# func_path_progs PROGS_LIST CHECK_FUNC [PATH] +# -------------------------------------------- +# Search for either a program that responds to --version with output +# containing "GNU", or else returned by CHECK_FUNC otherwise, by +# trying all the directories in PATH with each of the elements of +# PROGS_LIST. +# +# CHECK_FUNC should accept the path to a candidate program, and +# set $func_check_prog_result if it truncates its output less than +# $_G_path_prog_max characters. +func_path_progs () +{ + _G_progs_list=$1 + _G_check_func=$2 + _G_PATH=${3-"$PATH"} + + _G_path_prog_max=0 + _G_path_prog_found=false + _G_save_IFS=$IFS; IFS=${PATH_SEPARATOR-:} + for _G_dir in $_G_PATH; do + IFS=$_G_save_IFS + test -z "$_G_dir" && _G_dir=. + for _G_prog_name in $_G_progs_list; do + for _exeext in '' .EXE; do + _G_path_prog=$_G_dir/$_G_prog_name$_exeext + func_executable_p "$_G_path_prog" || continue + case `"$_G_path_prog" --version 2>&1` in + *GNU*) func_path_progs_result=$_G_path_prog _G_path_prog_found=: ;; + *) $_G_check_func $_G_path_prog + func_path_progs_result=$func_check_prog_result + ;; + esac + $_G_path_prog_found && break 3 + done + done + done + IFS=$_G_save_IFS + test -z "$func_path_progs_result" && { + echo "no acceptable sed could be found in \$PATH" >&2 + exit 1 + } +} + + +# We want to be able to use the functions in this file before configure +# has figured out where the best binaries are kept, which means we have +# to search for them ourselves - except when the results are already set +# where we skip the searches. + +# Unless the user overrides by setting SED, search the path for either GNU +# sed, or the sed that truncates its output the least. +test -z "$SED" && { + _G_sed_script=s/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/ + for _G_i in 1 2 3 4 5 6 7; do + _G_sed_script=$_G_sed_script$nl$_G_sed_script + done + echo "$_G_sed_script" 2>/dev/null | sed 99q >conftest.sed + _G_sed_script= + + func_check_prog_sed () + { + _G_path_prog=$1 + + _G_count=0 + printf 0123456789 >conftest.in + while : + do + cat conftest.in conftest.in >conftest.tmp + mv conftest.tmp conftest.in + cp conftest.in conftest.nl + echo '' >> conftest.nl + "$_G_path_prog" -f conftest.sed conftest.out 2>/dev/null || break + diff conftest.out conftest.nl >/dev/null 2>&1 || break + _G_count=`expr $_G_count + 1` + if test "$_G_count" -gt "$_G_path_prog_max"; then + # Best one so far, save it but keep looking for a better one + func_check_prog_result=$_G_path_prog + _G_path_prog_max=$_G_count + fi + # 10*(2^10) chars as input seems more than enough + test 10 -lt "$_G_count" && break + done + rm -f conftest.in conftest.tmp conftest.nl conftest.out + } + + func_path_progs "sed gsed" func_check_prog_sed $PATH:/usr/xpg4/bin + rm -f conftest.sed + SED=$func_path_progs_result +} + + +# Unless the user overrides by setting GREP, search the path for either GNU +# grep, or the grep that truncates its output the least. +test -z "$GREP" && { + func_check_prog_grep () + { + _G_path_prog=$1 + + _G_count=0 + _G_path_prog_max=0 + printf 0123456789 >conftest.in + while : + do + cat conftest.in conftest.in >conftest.tmp + mv conftest.tmp conftest.in + cp conftest.in conftest.nl + echo 'GREP' >> conftest.nl + "$_G_path_prog" -e 'GREP$' -e '-(cannot match)-' conftest.out 2>/dev/null || break + diff conftest.out conftest.nl >/dev/null 2>&1 || break + _G_count=`expr $_G_count + 1` + if test "$_G_count" -gt "$_G_path_prog_max"; then + # Best one so far, save it but keep looking for a better one + func_check_prog_result=$_G_path_prog + _G_path_prog_max=$_G_count + fi + # 10*(2^10) chars as input seems more than enough + test 10 -lt "$_G_count" && break + done + rm -f conftest.in conftest.tmp conftest.nl conftest.out + } + + func_path_progs "grep ggrep" func_check_prog_grep $PATH:/usr/xpg4/bin + GREP=$func_path_progs_result +} + + +## ------------------------------- ## +## User overridable command paths. ## +## ------------------------------- ## + +# All uppercase variable names are used for environment variables. These +# variables can be overridden by the user before calling a script that +# uses them if a suitable command of that name is not already available +# in the command search PATH. + +: ${CP="cp -f"} +: ${ECHO="printf %s\n"} +: ${EGREP="$GREP -E"} +: ${FGREP="$GREP -F"} +: ${LN_S="ln -s"} +: ${MAKE="make"} +: ${MKDIR="mkdir"} +: ${MV="mv -f"} +: ${RM="rm -f"} +: ${SHELL="${CONFIG_SHELL-/bin/sh}"} + + +## -------------------- ## +## Useful sed snippets. ## +## -------------------- ## + +sed_dirname='s|/[^/]*$||' +sed_basename='s|^.*/||' + +# Sed substitution that helps us do robust quoting. It backslashifies +# metacharacters that are still active within double-quoted strings. +sed_quote_subst='s|\([`"$\\]\)|\\\1|g' + +# Same as above, but do not quote variable references. +sed_double_quote_subst='s/\(["`\\]\)/\\\1/g' + +# Sed substitution that turns a string into a regex matching for the +# string literally. +sed_make_literal_regex='s|[].[^$\\*\/]|\\&|g' + +# Sed substitution that converts a w32 file name or path +# that contains forward slashes, into one that contains +# (escaped) backslashes. A very naive implementation. +sed_naive_backslashify='s|\\\\*|\\|g;s|/|\\|g;s|\\|\\\\|g' + +# Re-'\' parameter expansions in output of sed_double_quote_subst that +# were '\'-ed in input to the same. If an odd number of '\' preceded a +# '$' in input to sed_double_quote_subst, that '$' was protected from +# expansion. Since each input '\' is now two '\'s, look for any number +# of runs of four '\'s followed by two '\'s and then a '$'. '\' that '$'. +_G_bs='\\' +_G_bs2='\\\\' +_G_bs4='\\\\\\\\' +_G_dollar='\$' +sed_double_backslash="\ + s/$_G_bs4/&\\ +/g + s/^$_G_bs2$_G_dollar/$_G_bs&/ + s/\\([^$_G_bs]\\)$_G_bs2$_G_dollar/\\1$_G_bs2$_G_bs$_G_dollar/g + s/\n//g" + + +## ----------------- ## +## Global variables. ## +## ----------------- ## + +# Except for the global variables explicitly listed below, the following +# functions in the '^func_' namespace, and the '^require_' namespace +# variables initialised in the 'Resource management' section, sourcing +# this file will not pollute your global namespace with anything +# else. There's no portable way to scope variables in Bourne shell +# though, so actually running these functions will sometimes place +# results into a variable named after the function, and often use +# temporary variables in the '^_G_' namespace. If you are careful to +# avoid using those namespaces casually in your sourcing script, things +# should continue to work as you expect. And, of course, you can freely +# overwrite any of the functions or variables defined here before +# calling anything to customize them. + +EXIT_SUCCESS=0 +EXIT_FAILURE=1 +EXIT_MISMATCH=63 # $? = 63 is used to indicate version mismatch to missing. +EXIT_SKIP=77 # $? = 77 is used to indicate a skipped test to automake. + +# Allow overriding, eg assuming that you follow the convention of +# putting '$debug_cmd' at the start of all your functions, you can get +# bash to show function call trace with: +# +# debug_cmd='eval echo "${FUNCNAME[0]} $*" >&2' bash your-script-name +debug_cmd=${debug_cmd-":"} +exit_cmd=: + +# By convention, finish your script with: +# +# exit $exit_status +# +# so that you can set exit_status to non-zero if you want to indicate +# something went wrong during execution without actually bailing out at +# the point of failure. +exit_status=$EXIT_SUCCESS + +# Work around backward compatibility issue on IRIX 6.5. On IRIX 6.4+, sh +# is ksh but when the shell is invoked as "sh" and the current value of +# the _XPG environment variable is not equal to 1 (one), the special +# positional parameter $0, within a function call, is the name of the +# function. +progpath=$0 + +# The name of this program. +progname=`$ECHO "$progpath" |$SED "$sed_basename"` + +# Make sure we have an absolute progpath for reexecution: +case $progpath in + [\\/]*|[A-Za-z]:\\*) ;; + *[\\/]*) + progdir=`$ECHO "$progpath" |$SED "$sed_dirname"` + progdir=`cd "$progdir" && pwd` + progpath=$progdir/$progname + ;; + *) + _G_IFS=$IFS + IFS=${PATH_SEPARATOR-:} + for progdir in $PATH; do + IFS=$_G_IFS + test -x "$progdir/$progname" && break + done + IFS=$_G_IFS + test -n "$progdir" || progdir=`pwd` + progpath=$progdir/$progname + ;; +esac + + +## ----------------- ## +## Standard options. ## +## ----------------- ## + +# The following options affect the operation of the functions defined +# below, and should be set appropriately depending on run-time para- +# meters passed on the command line. + +opt_dry_run=false +opt_quiet=false +opt_verbose=false + +# Categories 'all' and 'none' are always available. Append any others +# you will pass as the first argument to func_warning from your own +# code. +warning_categories= + +# By default, display warnings according to 'opt_warning_types'. Set +# 'warning_func' to ':' to elide all warnings, or func_fatal_error to +# treat the next displayed warning as a fatal error. +warning_func=func_warn_and_continue + +# Set to 'all' to display all warnings, 'none' to suppress all +# warnings, or a space delimited list of some subset of +# 'warning_categories' to display only the listed warnings. +opt_warning_types=all + + +## -------------------- ## +## Resource management. ## +## -------------------- ## + +# This section contains definitions for functions that each ensure a +# particular resource (a file, or a non-empty configuration variable for +# example) is available, and if appropriate to extract default values +# from pertinent package files. Call them using their associated +# 'require_*' variable to ensure that they are executed, at most, once. +# +# It's entirely deliberate that calling these functions can set +# variables that don't obey the namespace limitations obeyed by the rest +# of this file, in order that that they be as useful as possible to +# callers. + + +# require_term_colors +# ------------------- +# Allow display of bold text on terminals that support it. +require_term_colors=func_require_term_colors +func_require_term_colors () +{ + $debug_cmd + + test -t 1 && { + # COLORTERM and USE_ANSI_COLORS environment variables take + # precedence, because most terminfo databases neglect to describe + # whether color sequences are supported. + test -n "${COLORTERM+set}" && : ${USE_ANSI_COLORS="1"} + + if test 1 = "$USE_ANSI_COLORS"; then + # Standard ANSI escape sequences + tc_reset='' + tc_bold=''; tc_standout='' + tc_red=''; tc_green='' + tc_blue=''; tc_cyan='' + else + # Otherwise trust the terminfo database after all. + test -n "`tput sgr0 2>/dev/null`" && { + tc_reset=`tput sgr0` + test -n "`tput bold 2>/dev/null`" && tc_bold=`tput bold` + tc_standout=$tc_bold + test -n "`tput smso 2>/dev/null`" && tc_standout=`tput smso` + test -n "`tput setaf 1 2>/dev/null`" && tc_red=`tput setaf 1` + test -n "`tput setaf 2 2>/dev/null`" && tc_green=`tput setaf 2` + test -n "`tput setaf 4 2>/dev/null`" && tc_blue=`tput setaf 4` + test -n "`tput setaf 5 2>/dev/null`" && tc_cyan=`tput setaf 5` + } + fi + } + + require_term_colors=: +} + + +## ----------------- ## +## Function library. ## +## ----------------- ## + +# This section contains a variety of useful functions to call in your +# scripts. Take note of the portable wrappers for features provided by +# some modern shells, which will fall back to slower equivalents on +# less featureful shells. + + +# func_append VAR VALUE +# --------------------- +# Append VALUE onto the existing contents of VAR. + + # We should try to minimise forks, especially on Windows where they are + # unreasonably slow, so skip the feature probes when bash or zsh are + # being used: + if test set = "${BASH_VERSION+set}${ZSH_VERSION+set}"; then + : ${_G_HAVE_ARITH_OP="yes"} + : ${_G_HAVE_XSI_OPS="yes"} + # The += operator was introduced in bash 3.1 + case $BASH_VERSION in + [12].* | 3.0 | 3.0*) ;; + *) + : ${_G_HAVE_PLUSEQ_OP="yes"} + ;; + esac + fi + + # _G_HAVE_PLUSEQ_OP + # Can be empty, in which case the shell is probed, "yes" if += is + # useable or anything else if it does not work. + test -z "$_G_HAVE_PLUSEQ_OP" \ + && (eval 'x=a; x+=" b"; test "a b" = "$x"') 2>/dev/null \ + && _G_HAVE_PLUSEQ_OP=yes + +if test yes = "$_G_HAVE_PLUSEQ_OP" +then + # This is an XSI compatible shell, allowing a faster implementation... + eval 'func_append () + { + $debug_cmd + + eval "$1+=\$2" + }' +else + # ...otherwise fall back to using expr, which is often a shell builtin. + func_append () + { + $debug_cmd + + eval "$1=\$$1\$2" + } +fi + + +# func_append_quoted VAR VALUE +# ---------------------------- +# Quote VALUE and append to the end of shell variable VAR, separated +# by a space. +if test yes = "$_G_HAVE_PLUSEQ_OP"; then + eval 'func_append_quoted () + { + $debug_cmd + + func_quote_for_eval "$2" + eval "$1+=\\ \$func_quote_for_eval_result" + }' +else + func_append_quoted () + { + $debug_cmd + + func_quote_for_eval "$2" + eval "$1=\$$1\\ \$func_quote_for_eval_result" + } +fi + + +# func_append_uniq VAR VALUE +# -------------------------- +# Append unique VALUE onto the existing contents of VAR, assuming +# entries are delimited by the first character of VALUE. For example: +# +# func_append_uniq options " --another-option option-argument" +# +# will only append to $options if " --another-option option-argument " +# is not already present somewhere in $options already (note spaces at +# each end implied by leading space in second argument). +func_append_uniq () +{ + $debug_cmd + + eval _G_current_value='`$ECHO $'$1'`' + _G_delim=`expr "$2" : '\(.\)'` + + case $_G_delim$_G_current_value$_G_delim in + *"$2$_G_delim"*) ;; + *) func_append "$@" ;; + esac +} + + +# func_arith TERM... +# ------------------ +# Set func_arith_result to the result of evaluating TERMs. + test -z "$_G_HAVE_ARITH_OP" \ + && (eval 'test 2 = $(( 1 + 1 ))') 2>/dev/null \ + && _G_HAVE_ARITH_OP=yes + +if test yes = "$_G_HAVE_ARITH_OP"; then + eval 'func_arith () + { + $debug_cmd + + func_arith_result=$(( $* )) + }' +else + func_arith () + { + $debug_cmd + + func_arith_result=`expr "$@"` + } +fi + + +# func_basename FILE +# ------------------ +# Set func_basename_result to FILE with everything up to and including +# the last / stripped. +if test yes = "$_G_HAVE_XSI_OPS"; then + # If this shell supports suffix pattern removal, then use it to avoid + # forking. Hide the definitions single quotes in case the shell chokes + # on unsupported syntax... + _b='func_basename_result=${1##*/}' + _d='case $1 in + */*) func_dirname_result=${1%/*}$2 ;; + * ) func_dirname_result=$3 ;; + esac' + +else + # ...otherwise fall back to using sed. + _b='func_basename_result=`$ECHO "$1" |$SED "$sed_basename"`' + _d='func_dirname_result=`$ECHO "$1" |$SED "$sed_dirname"` + if test "X$func_dirname_result" = "X$1"; then + func_dirname_result=$3 + else + func_append func_dirname_result "$2" + fi' +fi + +eval 'func_basename () +{ + $debug_cmd + + '"$_b"' +}' + + +# func_dirname FILE APPEND NONDIR_REPLACEMENT +# ------------------------------------------- +# Compute the dirname of FILE. If nonempty, add APPEND to the result, +# otherwise set result to NONDIR_REPLACEMENT. +eval 'func_dirname () +{ + $debug_cmd + + '"$_d"' +}' + + +# func_dirname_and_basename FILE APPEND NONDIR_REPLACEMENT +# -------------------------------------------------------- +# Perform func_basename and func_dirname in a single function +# call: +# dirname: Compute the dirname of FILE. If nonempty, +# add APPEND to the result, otherwise set result +# to NONDIR_REPLACEMENT. +# value returned in "$func_dirname_result" +# basename: Compute filename of FILE. +# value retuned in "$func_basename_result" +# For efficiency, we do not delegate to the functions above but instead +# duplicate the functionality here. +eval 'func_dirname_and_basename () +{ + $debug_cmd + + '"$_b"' + '"$_d"' +}' + + +# func_echo ARG... +# ---------------- +# Echo program name prefixed message. +func_echo () +{ + $debug_cmd + + _G_message=$* + + func_echo_IFS=$IFS + IFS=$nl + for _G_line in $_G_message; do + IFS=$func_echo_IFS + $ECHO "$progname: $_G_line" + done + IFS=$func_echo_IFS +} + + +# func_echo_all ARG... +# -------------------- +# Invoke $ECHO with all args, space-separated. +func_echo_all () +{ + $ECHO "$*" +} + + +# func_echo_infix_1 INFIX ARG... +# ------------------------------ +# Echo program name, followed by INFIX on the first line, with any +# additional lines not showing INFIX. +func_echo_infix_1 () +{ + $debug_cmd + + $require_term_colors + + _G_infix=$1; shift + _G_indent=$_G_infix + _G_prefix="$progname: $_G_infix: " + _G_message=$* + + # Strip color escape sequences before counting printable length + for _G_tc in "$tc_reset" "$tc_bold" "$tc_standout" "$tc_red" "$tc_green" "$tc_blue" "$tc_cyan" + do + test -n "$_G_tc" && { + _G_esc_tc=`$ECHO "$_G_tc" | $SED "$sed_make_literal_regex"` + _G_indent=`$ECHO "$_G_indent" | $SED "s|$_G_esc_tc||g"` + } + done + _G_indent="$progname: "`echo "$_G_indent" | $SED 's|.| |g'`" " ## exclude from sc_prohibit_nested_quotes + + func_echo_infix_1_IFS=$IFS + IFS=$nl + for _G_line in $_G_message; do + IFS=$func_echo_infix_1_IFS + $ECHO "$_G_prefix$tc_bold$_G_line$tc_reset" >&2 + _G_prefix=$_G_indent + done + IFS=$func_echo_infix_1_IFS +} + + +# func_error ARG... +# ----------------- +# Echo program name prefixed message to standard error. +func_error () +{ + $debug_cmd + + $require_term_colors + + func_echo_infix_1 " $tc_standout${tc_red}error$tc_reset" "$*" >&2 +} + + +# func_fatal_error ARG... +# ----------------------- +# Echo program name prefixed message to standard error, and exit. +func_fatal_error () +{ + $debug_cmd + + func_error "$*" + exit $EXIT_FAILURE +} + + +# func_grep EXPRESSION FILENAME +# ----------------------------- +# Check whether EXPRESSION matches any line of FILENAME, without output. +func_grep () +{ + $debug_cmd + + $GREP "$1" "$2" >/dev/null 2>&1 +} + + +# func_len STRING +# --------------- +# Set func_len_result to the length of STRING. STRING may not +# start with a hyphen. + test -z "$_G_HAVE_XSI_OPS" \ + && (eval 'x=a/b/c; + test 5aa/bb/cc = "${#x}${x%%/*}${x%/*}${x#*/}${x##*/}"') 2>/dev/null \ + && _G_HAVE_XSI_OPS=yes + +if test yes = "$_G_HAVE_XSI_OPS"; then + eval 'func_len () + { + $debug_cmd + + func_len_result=${#1} + }' +else + func_len () + { + $debug_cmd + + func_len_result=`expr "$1" : ".*" 2>/dev/null || echo $max_cmd_len` + } +fi + + +# func_mkdir_p DIRECTORY-PATH +# --------------------------- +# Make sure the entire path to DIRECTORY-PATH is available. +func_mkdir_p () +{ + $debug_cmd + + _G_directory_path=$1 + _G_dir_list= + + if test -n "$_G_directory_path" && test : != "$opt_dry_run"; then + + # Protect directory names starting with '-' + case $_G_directory_path in + -*) _G_directory_path=./$_G_directory_path ;; + esac + + # While some portion of DIR does not yet exist... + while test ! -d "$_G_directory_path"; do + # ...make a list in topmost first order. Use a colon delimited + # list incase some portion of path contains whitespace. + _G_dir_list=$_G_directory_path:$_G_dir_list + + # If the last portion added has no slash in it, the list is done + case $_G_directory_path in */*) ;; *) break ;; esac + + # ...otherwise throw away the child directory and loop + _G_directory_path=`$ECHO "$_G_directory_path" | $SED -e "$sed_dirname"` + done + _G_dir_list=`$ECHO "$_G_dir_list" | $SED 's|:*$||'` + + func_mkdir_p_IFS=$IFS; IFS=: + for _G_dir in $_G_dir_list; do + IFS=$func_mkdir_p_IFS + # mkdir can fail with a 'File exist' error if two processes + # try to create one of the directories concurrently. Don't + # stop in that case! + $MKDIR "$_G_dir" 2>/dev/null || : + done + IFS=$func_mkdir_p_IFS + + # Bail out if we (or some other process) failed to create a directory. + test -d "$_G_directory_path" || \ + func_fatal_error "Failed to create '$1'" + fi +} + + +# func_mktempdir [BASENAME] +# ------------------------- +# Make a temporary directory that won't clash with other running +# libtool processes, and avoids race conditions if possible. If +# given, BASENAME is the basename for that directory. +func_mktempdir () +{ + $debug_cmd + + _G_template=${TMPDIR-/tmp}/${1-$progname} + + if test : = "$opt_dry_run"; then + # Return a directory name, but don't create it in dry-run mode + _G_tmpdir=$_G_template-$$ + else + + # If mktemp works, use that first and foremost + _G_tmpdir=`mktemp -d "$_G_template-XXXXXXXX" 2>/dev/null` + + if test ! -d "$_G_tmpdir"; then + # Failing that, at least try and use $RANDOM to avoid a race + _G_tmpdir=$_G_template-${RANDOM-0}$$ + + func_mktempdir_umask=`umask` + umask 0077 + $MKDIR "$_G_tmpdir" + umask $func_mktempdir_umask + fi + + # If we're not in dry-run mode, bomb out on failure + test -d "$_G_tmpdir" || \ + func_fatal_error "cannot create temporary directory '$_G_tmpdir'" + fi + + $ECHO "$_G_tmpdir" +} + + +# func_normal_abspath PATH +# ------------------------ +# Remove doubled-up and trailing slashes, "." path components, +# and cancel out any ".." path components in PATH after making +# it an absolute path. +func_normal_abspath () +{ + $debug_cmd + + # These SED scripts presuppose an absolute path with a trailing slash. + _G_pathcar='s|^/\([^/]*\).*$|\1|' + _G_pathcdr='s|^/[^/]*||' + _G_removedotparts=':dotsl + s|/\./|/|g + t dotsl + s|/\.$|/|' + _G_collapseslashes='s|/\{1,\}|/|g' + _G_finalslash='s|/*$|/|' + + # Start from root dir and reassemble the path. + func_normal_abspath_result= + func_normal_abspath_tpath=$1 + func_normal_abspath_altnamespace= + case $func_normal_abspath_tpath in + "") + # Empty path, that just means $cwd. + func_stripname '' '/' "`pwd`" + func_normal_abspath_result=$func_stripname_result + return + ;; + # The next three entries are used to spot a run of precisely + # two leading slashes without using negated character classes; + # we take advantage of case's first-match behaviour. + ///*) + # Unusual form of absolute path, do nothing. + ;; + //*) + # Not necessarily an ordinary path; POSIX reserves leading '//' + # and for example Cygwin uses it to access remote file shares + # over CIFS/SMB, so we conserve a leading double slash if found. + func_normal_abspath_altnamespace=/ + ;; + /*) + # Absolute path, do nothing. + ;; + *) + # Relative path, prepend $cwd. + func_normal_abspath_tpath=`pwd`/$func_normal_abspath_tpath + ;; + esac + + # Cancel out all the simple stuff to save iterations. We also want + # the path to end with a slash for ease of parsing, so make sure + # there is one (and only one) here. + func_normal_abspath_tpath=`$ECHO "$func_normal_abspath_tpath" | $SED \ + -e "$_G_removedotparts" -e "$_G_collapseslashes" -e "$_G_finalslash"` + while :; do + # Processed it all yet? + if test / = "$func_normal_abspath_tpath"; then + # If we ascended to the root using ".." the result may be empty now. + if test -z "$func_normal_abspath_result"; then + func_normal_abspath_result=/ + fi + break + fi + func_normal_abspath_tcomponent=`$ECHO "$func_normal_abspath_tpath" | $SED \ + -e "$_G_pathcar"` + func_normal_abspath_tpath=`$ECHO "$func_normal_abspath_tpath" | $SED \ + -e "$_G_pathcdr"` + # Figure out what to do with it + case $func_normal_abspath_tcomponent in + "") + # Trailing empty path component, ignore it. + ;; + ..) + # Parent dir; strip last assembled component from result. + func_dirname "$func_normal_abspath_result" + func_normal_abspath_result=$func_dirname_result + ;; + *) + # Actual path component, append it. + func_append func_normal_abspath_result "/$func_normal_abspath_tcomponent" + ;; + esac + done + # Restore leading double-slash if one was found on entry. + func_normal_abspath_result=$func_normal_abspath_altnamespace$func_normal_abspath_result +} + + +# func_notquiet ARG... +# -------------------- +# Echo program name prefixed message only when not in quiet mode. +func_notquiet () +{ + $debug_cmd + + $opt_quiet || func_echo ${1+"$@"} + + # A bug in bash halts the script if the last line of a function + # fails when set -e is in force, so we need another command to + # work around that: + : +} + + +# func_relative_path SRCDIR DSTDIR +# -------------------------------- +# Set func_relative_path_result to the relative path from SRCDIR to DSTDIR. +func_relative_path () +{ + $debug_cmd + + func_relative_path_result= + func_normal_abspath "$1" + func_relative_path_tlibdir=$func_normal_abspath_result + func_normal_abspath "$2" + func_relative_path_tbindir=$func_normal_abspath_result + + # Ascend the tree starting from libdir + while :; do + # check if we have found a prefix of bindir + case $func_relative_path_tbindir in + $func_relative_path_tlibdir) + # found an exact match + func_relative_path_tcancelled= + break + ;; + $func_relative_path_tlibdir*) + # found a matching prefix + func_stripname "$func_relative_path_tlibdir" '' "$func_relative_path_tbindir" + func_relative_path_tcancelled=$func_stripname_result + if test -z "$func_relative_path_result"; then + func_relative_path_result=. + fi + break + ;; + *) + func_dirname $func_relative_path_tlibdir + func_relative_path_tlibdir=$func_dirname_result + if test -z "$func_relative_path_tlibdir"; then + # Have to descend all the way to the root! + func_relative_path_result=../$func_relative_path_result + func_relative_path_tcancelled=$func_relative_path_tbindir + break + fi + func_relative_path_result=../$func_relative_path_result + ;; + esac + done + + # Now calculate path; take care to avoid doubling-up slashes. + func_stripname '' '/' "$func_relative_path_result" + func_relative_path_result=$func_stripname_result + func_stripname '/' '/' "$func_relative_path_tcancelled" + if test -n "$func_stripname_result"; then + func_append func_relative_path_result "/$func_stripname_result" + fi + + # Normalisation. If bindir is libdir, return '.' else relative path. + if test -n "$func_relative_path_result"; then + func_stripname './' '' "$func_relative_path_result" + func_relative_path_result=$func_stripname_result + fi + + test -n "$func_relative_path_result" || func_relative_path_result=. + + : +} + + +# func_quote_for_eval ARG... +# -------------------------- +# Aesthetically quote ARGs to be evaled later. +# This function returns two values: +# i) func_quote_for_eval_result +# double-quoted, suitable for a subsequent eval +# ii) func_quote_for_eval_unquoted_result +# has all characters that are still active within double +# quotes backslashified. +func_quote_for_eval () +{ + $debug_cmd + + func_quote_for_eval_unquoted_result= + func_quote_for_eval_result= + while test 0 -lt $#; do + case $1 in + *[\\\`\"\$]*) + _G_unquoted_arg=`printf '%s\n' "$1" |$SED "$sed_quote_subst"` ;; + *) + _G_unquoted_arg=$1 ;; + esac + if test -n "$func_quote_for_eval_unquoted_result"; then + func_append func_quote_for_eval_unquoted_result " $_G_unquoted_arg" + else + func_append func_quote_for_eval_unquoted_result "$_G_unquoted_arg" + fi + + case $_G_unquoted_arg in + # Double-quote args containing shell metacharacters to delay + # word splitting, command substitution and variable expansion + # for a subsequent eval. + # Many Bourne shells cannot handle close brackets correctly + # in scan sets, so we specify it separately. + *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \ ]*|*]*|"") + _G_quoted_arg=\"$_G_unquoted_arg\" + ;; + *) + _G_quoted_arg=$_G_unquoted_arg + ;; + esac + + if test -n "$func_quote_for_eval_result"; then + func_append func_quote_for_eval_result " $_G_quoted_arg" + else + func_append func_quote_for_eval_result "$_G_quoted_arg" + fi + shift + done +} + + +# func_quote_for_expand ARG +# ------------------------- +# Aesthetically quote ARG to be evaled later; same as above, +# but do not quote variable references. +func_quote_for_expand () +{ + $debug_cmd + + case $1 in + *[\\\`\"]*) + _G_arg=`$ECHO "$1" | $SED \ + -e "$sed_double_quote_subst" -e "$sed_double_backslash"` ;; + *) + _G_arg=$1 ;; + esac + + case $_G_arg in + # Double-quote args containing shell metacharacters to delay + # word splitting and command substitution for a subsequent eval. + # Many Bourne shells cannot handle close brackets correctly + # in scan sets, so we specify it separately. + *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \ ]*|*]*|"") + _G_arg=\"$_G_arg\" + ;; + esac + + func_quote_for_expand_result=$_G_arg +} + + +# func_stripname PREFIX SUFFIX NAME +# --------------------------------- +# strip PREFIX and SUFFIX from NAME, and store in func_stripname_result. +# PREFIX and SUFFIX must not contain globbing or regex special +# characters, hashes, percent signs, but SUFFIX may contain a leading +# dot (in which case that matches only a dot). +if test yes = "$_G_HAVE_XSI_OPS"; then + eval 'func_stripname () + { + $debug_cmd + + # pdksh 5.2.14 does not do ${X%$Y} correctly if both X and Y are + # positional parameters, so assign one to ordinary variable first. + func_stripname_result=$3 + func_stripname_result=${func_stripname_result#"$1"} + func_stripname_result=${func_stripname_result%"$2"} + }' +else + func_stripname () + { + $debug_cmd + + case $2 in + .*) func_stripname_result=`$ECHO "$3" | $SED -e "s%^$1%%" -e "s%\\\\$2\$%%"`;; + *) func_stripname_result=`$ECHO "$3" | $SED -e "s%^$1%%" -e "s%$2\$%%"`;; + esac + } +fi + + +# func_show_eval CMD [FAIL_EXP] +# ----------------------------- +# Unless opt_quiet is true, then output CMD. Then, if opt_dryrun is +# not true, evaluate CMD. If the evaluation of CMD fails, and FAIL_EXP +# is given, then evaluate it. +func_show_eval () +{ + $debug_cmd + + _G_cmd=$1 + _G_fail_exp=${2-':'} + + func_quote_for_expand "$_G_cmd" + eval "func_notquiet $func_quote_for_expand_result" + + $opt_dry_run || { + eval "$_G_cmd" + _G_status=$? + if test 0 -ne "$_G_status"; then + eval "(exit $_G_status); $_G_fail_exp" + fi + } +} + + +# func_show_eval_locale CMD [FAIL_EXP] +# ------------------------------------ +# Unless opt_quiet is true, then output CMD. Then, if opt_dryrun is +# not true, evaluate CMD. If the evaluation of CMD fails, and FAIL_EXP +# is given, then evaluate it. Use the saved locale for evaluation. +func_show_eval_locale () +{ + $debug_cmd + + _G_cmd=$1 + _G_fail_exp=${2-':'} + + $opt_quiet || { + func_quote_for_expand "$_G_cmd" + eval "func_echo $func_quote_for_expand_result" + } + + $opt_dry_run || { + eval "$_G_user_locale + $_G_cmd" + _G_status=$? + eval "$_G_safe_locale" + if test 0 -ne "$_G_status"; then + eval "(exit $_G_status); $_G_fail_exp" + fi + } +} + + +# func_tr_sh +# ---------- +# Turn $1 into a string suitable for a shell variable name. +# Result is stored in $func_tr_sh_result. All characters +# not in the set a-zA-Z0-9_ are replaced with '_'. Further, +# if $1 begins with a digit, a '_' is prepended as well. +func_tr_sh () +{ + $debug_cmd + + case $1 in + [0-9]* | *[!a-zA-Z0-9_]*) + func_tr_sh_result=`$ECHO "$1" | $SED -e 's/^\([0-9]\)/_\1/' -e 's/[^a-zA-Z0-9_]/_/g'` + ;; + * ) + func_tr_sh_result=$1 + ;; + esac +} + + +# func_verbose ARG... +# ------------------- +# Echo program name prefixed message in verbose mode only. +func_verbose () +{ + $debug_cmd + + $opt_verbose && func_echo "$*" + + : +} + + +# func_warn_and_continue ARG... +# ----------------------------- +# Echo program name prefixed warning message to standard error. +func_warn_and_continue () +{ + $debug_cmd + + $require_term_colors + + func_echo_infix_1 "${tc_red}warning$tc_reset" "$*" >&2 +} + + +# func_warning CATEGORY ARG... +# ---------------------------- +# Echo program name prefixed warning message to standard error. Warning +# messages can be filtered according to CATEGORY, where this function +# elides messages where CATEGORY is not listed in the global variable +# 'opt_warning_types'. +func_warning () +{ + $debug_cmd + + # CATEGORY must be in the warning_categories list! + case " $warning_categories " in + *" $1 "*) ;; + *) func_internal_error "invalid warning category '$1'" ;; + esac + + _G_category=$1 + shift + + case " $opt_warning_types " in + *" $_G_category "*) $warning_func ${1+"$@"} ;; + esac +} + + +# func_sort_ver VER1 VER2 +# ----------------------- +# 'sort -V' is not generally available. +# Note this deviates from the version comparison in automake +# in that it treats 1.5 < 1.5.0, and treats 1.4.4a < 1.4-p3a +# but this should suffice as we won't be specifying old +# version formats or redundant trailing .0 in bootstrap.conf. +# If we did want full compatibility then we should probably +# use m4_version_compare from autoconf. +func_sort_ver () +{ + $debug_cmd + + printf '%s\n%s\n' "$1" "$2" \ + | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n -k 5,5n -k 6,6n -k 7,7n -k 8,8n -k 9,9n +} + +# func_lt_ver PREV CURR +# --------------------- +# Return true if PREV and CURR are in the correct order according to +# func_sort_ver, otherwise false. Use it like this: +# +# func_lt_ver "$prev_ver" "$proposed_ver" || func_fatal_error "..." +func_lt_ver () +{ + $debug_cmd + + test "x$1" = x`func_sort_ver "$1" "$2" | $SED 1q` +} + + +# Local variables: +# mode: shell-script +# sh-indentation: 2 +# eval: (add-hook 'before-save-hook 'time-stamp) +# time-stamp-pattern: "10/scriptversion=%:y-%02m-%02d.%02H; # UTC" +# time-stamp-time-zone: "UTC" +# End: +#! /bin/sh + +# Set a version string for this script. +scriptversion=2015-10-07.11; # UTC + +# A portable, pluggable option parser for Bourne shell. +# Written by Gary V. Vaughan, 2010 + +# Copyright (C) 2010-2015 Free Software Foundation, Inc. +# This is free software; see the source for copying conditions. There is NO +# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Please report bugs or propose patches to gary@gnu.org. + + +## ------ ## +## Usage. ## +## ------ ## + +# This file is a library for parsing options in your shell scripts along +# with assorted other useful supporting features that you can make use +# of too. +# +# For the simplest scripts you might need only: +# +# #!/bin/sh +# . relative/path/to/funclib.sh +# . relative/path/to/options-parser +# scriptversion=1.0 +# func_options ${1+"$@"} +# eval set dummy "$func_options_result"; shift +# ...rest of your script... +# +# In order for the '--version' option to work, you will need to have a +# suitably formatted comment like the one at the top of this file +# starting with '# Written by ' and ending with '# warranty; '. +# +# For '-h' and '--help' to work, you will also need a one line +# description of your script's purpose in a comment directly above the +# '# Written by ' line, like the one at the top of this file. +# +# The default options also support '--debug', which will turn on shell +# execution tracing (see the comment above debug_cmd below for another +# use), and '--verbose' and the func_verbose function to allow your script +# to display verbose messages only when your user has specified +# '--verbose'. +# +# After sourcing this file, you can plug processing for additional +# options by amending the variables from the 'Configuration' section +# below, and following the instructions in the 'Option parsing' +# section further down. + +## -------------- ## +## Configuration. ## +## -------------- ## + +# You should override these variables in your script after sourcing this +# file so that they reflect the customisations you have added to the +# option parser. + +# The usage line for option parsing errors and the start of '-h' and +# '--help' output messages. You can embed shell variables for delayed +# expansion at the time the message is displayed, but you will need to +# quote other shell meta-characters carefully to prevent them being +# expanded when the contents are evaled. +usage='$progpath [OPTION]...' + +# Short help message in response to '-h' and '--help'. Add to this or +# override it after sourcing this library to reflect the full set of +# options your script accepts. +usage_message="\ + --debug enable verbose shell tracing + -W, --warnings=CATEGORY + report the warnings falling in CATEGORY [all] + -v, --verbose verbosely report processing + --version print version information and exit + -h, --help print short or long help message and exit +" + +# Additional text appended to 'usage_message' in response to '--help'. +long_help_message=" +Warning categories include: + 'all' show all warnings + 'none' turn off all the warnings + 'error' warnings are treated as fatal errors" + +# Help message printed before fatal option parsing errors. +fatal_help="Try '\$progname --help' for more information." + + + +## ------------------------- ## +## Hook function management. ## +## ------------------------- ## + +# This section contains functions for adding, removing, and running hooks +# to the main code. A hook is just a named list of of function, that can +# be run in order later on. + +# func_hookable FUNC_NAME +# ----------------------- +# Declare that FUNC_NAME will run hooks added with +# 'func_add_hook FUNC_NAME ...'. +func_hookable () +{ + $debug_cmd + + func_append hookable_fns " $1" +} + + +# func_add_hook FUNC_NAME HOOK_FUNC +# --------------------------------- +# Request that FUNC_NAME call HOOK_FUNC before it returns. FUNC_NAME must +# first have been declared "hookable" by a call to 'func_hookable'. +func_add_hook () +{ + $debug_cmd + + case " $hookable_fns " in + *" $1 "*) ;; + *) func_fatal_error "'$1' does not accept hook functions." ;; + esac + + eval func_append ${1}_hooks '" $2"' +} + + +# func_remove_hook FUNC_NAME HOOK_FUNC +# ------------------------------------ +# Remove HOOK_FUNC from the list of functions called by FUNC_NAME. +func_remove_hook () +{ + $debug_cmd + + eval ${1}_hooks='`$ECHO "\$'$1'_hooks" |$SED "s| '$2'||"`' +} + + +# func_run_hooks FUNC_NAME [ARG]... +# --------------------------------- +# Run all hook functions registered to FUNC_NAME. +# It is assumed that the list of hook functions contains nothing more +# than a whitespace-delimited list of legal shell function names, and +# no effort is wasted trying to catch shell meta-characters or preserve +# whitespace. +func_run_hooks () +{ + $debug_cmd + + _G_rc_run_hooks=false + + case " $hookable_fns " in + *" $1 "*) ;; + *) func_fatal_error "'$1' does not support hook funcions.n" ;; + esac + + eval _G_hook_fns=\$$1_hooks; shift + + for _G_hook in $_G_hook_fns; do + if eval $_G_hook '"$@"'; then + # store returned options list back into positional + # parameters for next 'cmd' execution. + eval _G_hook_result=\$${_G_hook}_result + eval set dummy "$_G_hook_result"; shift + _G_rc_run_hooks=: + fi + done + + $_G_rc_run_hooks && func_run_hooks_result=$_G_hook_result +} + + + +## --------------- ## +## Option parsing. ## +## --------------- ## + +# In order to add your own option parsing hooks, you must accept the +# full positional parameter list in your hook function, you may remove/edit +# any options that you action, and then pass back the remaining unprocessed +# options in '_result', escaped suitably for +# 'eval'. In this case you also must return $EXIT_SUCCESS to let the +# hook's caller know that it should pay attention to +# '_result'. Returning $EXIT_FAILURE signalizes that +# arguments are left untouched by the hook and therefore caller will ignore the +# result variable. +# +# Like this: +# +# my_options_prep () +# { +# $debug_cmd +# +# # Extend the existing usage message. +# usage_message=$usage_message' +# -s, --silent don'\''t print informational messages +# ' +# # No change in '$@' (ignored completely by this hook). There is +# # no need to do the equivalent (but slower) action: +# # func_quote_for_eval ${1+"$@"} +# # my_options_prep_result=$func_quote_for_eval_result +# false +# } +# func_add_hook func_options_prep my_options_prep +# +# +# my_silent_option () +# { +# $debug_cmd +# +# args_changed=false +# +# # Note that for efficiency, we parse as many options as we can +# # recognise in a loop before passing the remainder back to the +# # caller on the first unrecognised argument we encounter. +# while test $# -gt 0; do +# opt=$1; shift +# case $opt in +# --silent|-s) opt_silent=: +# args_changed=: +# ;; +# # Separate non-argument short options: +# -s*) func_split_short_opt "$_G_opt" +# set dummy "$func_split_short_opt_name" \ +# "-$func_split_short_opt_arg" ${1+"$@"} +# shift +# args_changed=: +# ;; +# *) # Make sure the first unrecognised option "$_G_opt" +# # is added back to "$@", we could need that later +# # if $args_changed is true. +# set dummy "$_G_opt" ${1+"$@"}; shift; break ;; +# esac +# done +# +# if $args_changed; then +# func_quote_for_eval ${1+"$@"} +# my_silent_option_result=$func_quote_for_eval_result +# fi +# +# $args_changed +# } +# func_add_hook func_parse_options my_silent_option +# +# +# my_option_validation () +# { +# $debug_cmd +# +# $opt_silent && $opt_verbose && func_fatal_help "\ +# '--silent' and '--verbose' options are mutually exclusive." +# +# false +# } +# func_add_hook func_validate_options my_option_validation +# +# You'll also need to manually amend $usage_message to reflect the extra +# options you parse. It's preferable to append if you can, so that +# multiple option parsing hooks can be added safely. + + +# func_options_finish [ARG]... +# ---------------------------- +# Finishing the option parse loop (call 'func_options' hooks ATM). +func_options_finish () +{ + $debug_cmd + + _G_func_options_finish_exit=false + if func_run_hooks func_options ${1+"$@"}; then + func_options_finish_result=$func_run_hooks_result + _G_func_options_finish_exit=: + fi + + $_G_func_options_finish_exit +} + + +# func_options [ARG]... +# --------------------- +# All the functions called inside func_options are hookable. See the +# individual implementations for details. +func_hookable func_options +func_options () +{ + $debug_cmd + + _G_rc_options=false + + for my_func in options_prep parse_options validate_options options_finish + do + if eval func_$my_func '${1+"$@"}'; then + eval _G_res_var='$'"func_${my_func}_result" + eval set dummy "$_G_res_var" ; shift + _G_rc_options=: + fi + done + + # Save modified positional parameters for caller. As a top-level + # options-parser function we always need to set the 'func_options_result' + # variable (regardless the $_G_rc_options value). + if $_G_rc_options; then + func_options_result=$_G_res_var + else + func_quote_for_eval ${1+"$@"} + func_options_result=$func_quote_for_eval_result + fi + + $_G_rc_options +} + + +# func_options_prep [ARG]... +# -------------------------- +# All initialisations required before starting the option parse loop. +# Note that when calling hook functions, we pass through the list of +# positional parameters. If a hook function modifies that list, and +# needs to propagate that back to rest of this script, then the complete +# modified list must be put in 'func_run_hooks_result' before +# returning $EXIT_SUCCESS (otherwise $EXIT_FAILURE is returned). +func_hookable func_options_prep +func_options_prep () +{ + $debug_cmd + + # Option defaults: + opt_verbose=false + opt_warning_types= + + _G_rc_options_prep=false + if func_run_hooks func_options_prep ${1+"$@"}; then + _G_rc_options_prep=: + # save modified positional parameters for caller + func_options_prep_result=$func_run_hooks_result + fi + + $_G_rc_options_prep +} + + +# func_parse_options [ARG]... +# --------------------------- +# The main option parsing loop. +func_hookable func_parse_options +func_parse_options () +{ + $debug_cmd + + func_parse_options_result= + + _G_rc_parse_options=false + # this just eases exit handling + while test $# -gt 0; do + # Defer to hook functions for initial option parsing, so they + # get priority in the event of reusing an option name. + if func_run_hooks func_parse_options ${1+"$@"}; then + eval set dummy "$func_run_hooks_result"; shift + _G_rc_parse_options=: + fi + + # Break out of the loop if we already parsed every option. + test $# -gt 0 || break + + _G_match_parse_options=: + _G_opt=$1 + shift + case $_G_opt in + --debug|-x) debug_cmd='set -x' + func_echo "enabling shell trace mode" + $debug_cmd + ;; + + --no-warnings|--no-warning|--no-warn) + set dummy --warnings none ${1+"$@"} + shift + ;; + + --warnings|--warning|-W) + if test $# = 0 && func_missing_arg $_G_opt; then + _G_rc_parse_options=: + break + fi + case " $warning_categories $1" in + *" $1 "*) + # trailing space prevents matching last $1 above + func_append_uniq opt_warning_types " $1" + ;; + *all) + opt_warning_types=$warning_categories + ;; + *none) + opt_warning_types=none + warning_func=: + ;; + *error) + opt_warning_types=$warning_categories + warning_func=func_fatal_error + ;; + *) + func_fatal_error \ + "unsupported warning category: '$1'" + ;; + esac + shift + ;; + + --verbose|-v) opt_verbose=: ;; + --version) func_version ;; + -\?|-h) func_usage ;; + --help) func_help ;; + + # Separate optargs to long options (plugins may need this): + --*=*) func_split_equals "$_G_opt" + set dummy "$func_split_equals_lhs" \ + "$func_split_equals_rhs" ${1+"$@"} + shift + ;; + + # Separate optargs to short options: + -W*) + func_split_short_opt "$_G_opt" + set dummy "$func_split_short_opt_name" \ + "$func_split_short_opt_arg" ${1+"$@"} + shift + ;; + + # Separate non-argument short options: + -\?*|-h*|-v*|-x*) + func_split_short_opt "$_G_opt" + set dummy "$func_split_short_opt_name" \ + "-$func_split_short_opt_arg" ${1+"$@"} + shift + ;; + + --) _G_rc_parse_options=: ; break ;; + -*) func_fatal_help "unrecognised option: '$_G_opt'" ;; + *) set dummy "$_G_opt" ${1+"$@"}; shift + _G_match_parse_options=false + break + ;; + esac + + $_G_match_parse_options && _G_rc_parse_options=: + done + + + if $_G_rc_parse_options; then + # save modified positional parameters for caller + func_quote_for_eval ${1+"$@"} + func_parse_options_result=$func_quote_for_eval_result + fi + + $_G_rc_parse_options +} + + +# func_validate_options [ARG]... +# ------------------------------ +# Perform any sanity checks on option settings and/or unconsumed +# arguments. +func_hookable func_validate_options +func_validate_options () +{ + $debug_cmd + + _G_rc_validate_options=false + + # Display all warnings if -W was not given. + test -n "$opt_warning_types" || opt_warning_types=" $warning_categories" + + if func_run_hooks func_validate_options ${1+"$@"}; then + # save modified positional parameters for caller + func_validate_options_result=$func_run_hooks_result + _G_rc_validate_options=: + fi + + # Bail if the options were screwed! + $exit_cmd $EXIT_FAILURE + + $_G_rc_validate_options +} + + + +## ----------------- ## +## Helper functions. ## +## ----------------- ## + +# This section contains the helper functions used by the rest of the +# hookable option parser framework in ascii-betical order. + + +# func_fatal_help ARG... +# ---------------------- +# Echo program name prefixed message to standard error, followed by +# a help hint, and exit. +func_fatal_help () +{ + $debug_cmd + + eval \$ECHO \""Usage: $usage"\" + eval \$ECHO \""$fatal_help"\" + func_error ${1+"$@"} + exit $EXIT_FAILURE +} + + +# func_help +# --------- +# Echo long help message to standard output and exit. +func_help () +{ + $debug_cmd + + func_usage_message + $ECHO "$long_help_message" + exit 0 +} + + +# func_missing_arg ARGNAME +# ------------------------ +# Echo program name prefixed message to standard error and set global +# exit_cmd. +func_missing_arg () +{ + $debug_cmd + + func_error "Missing argument for '$1'." + exit_cmd=exit +} + + +# func_split_equals STRING +# ------------------------ +# Set func_split_equals_lhs and func_split_equals_rhs shell variables after +# splitting STRING at the '=' sign. +test -z "$_G_HAVE_XSI_OPS" \ + && (eval 'x=a/b/c; + test 5aa/bb/cc = "${#x}${x%%/*}${x%/*}${x#*/}${x##*/}"') 2>/dev/null \ + && _G_HAVE_XSI_OPS=yes + +if test yes = "$_G_HAVE_XSI_OPS" +then + # This is an XSI compatible shell, allowing a faster implementation... + eval 'func_split_equals () + { + $debug_cmd + + func_split_equals_lhs=${1%%=*} + func_split_equals_rhs=${1#*=} + test "x$func_split_equals_lhs" = "x$1" \ + && func_split_equals_rhs= + }' +else + # ...otherwise fall back to using expr, which is often a shell builtin. + func_split_equals () + { + $debug_cmd + + func_split_equals_lhs=`expr "x$1" : 'x\([^=]*\)'` + func_split_equals_rhs= + test "x$func_split_equals_lhs" = "x$1" \ + || func_split_equals_rhs=`expr "x$1" : 'x[^=]*=\(.*\)$'` + } +fi #func_split_equals + + +# func_split_short_opt SHORTOPT +# ----------------------------- +# Set func_split_short_opt_name and func_split_short_opt_arg shell +# variables after splitting SHORTOPT after the 2nd character. +if test yes = "$_G_HAVE_XSI_OPS" +then + # This is an XSI compatible shell, allowing a faster implementation... + eval 'func_split_short_opt () + { + $debug_cmd + + func_split_short_opt_arg=${1#??} + func_split_short_opt_name=${1%"$func_split_short_opt_arg"} + }' +else + # ...otherwise fall back to using expr, which is often a shell builtin. + func_split_short_opt () + { + $debug_cmd + + func_split_short_opt_name=`expr "x$1" : 'x-\(.\)'` + func_split_short_opt_arg=`expr "x$1" : 'x-.\(.*\)$'` + } +fi #func_split_short_opt + + +# func_usage +# ---------- +# Echo short help message to standard output and exit. +func_usage () +{ + $debug_cmd + + func_usage_message + $ECHO "Run '$progname --help |${PAGER-more}' for full usage" + exit 0 +} + + +# func_usage_message +# ------------------ +# Echo short help message to standard output. +func_usage_message () +{ + $debug_cmd + + eval \$ECHO \""Usage: $usage"\" + echo + $SED -n 's|^# || + /^Written by/{ + x;p;x + } + h + /^Written by/q' < "$progpath" + echo + eval \$ECHO \""$usage_message"\" +} + + +# func_version +# ------------ +# Echo version message to standard output and exit. +func_version () +{ + $debug_cmd + + printf '%s\n' "$progname $scriptversion" + $SED -n ' + /(C)/!b go + :more + /\./!{ + N + s|\n# | | + b more + } + :go + /^# Written by /,/# warranty; / { + s|^# || + s|^# *$|| + s|\((C)\)[ 0-9,-]*[ ,-]\([1-9][0-9]* \)|\1 \2| + p + } + /^# Written by / { + s|^# || + p + } + /^warranty; /q' < "$progpath" + + exit $? +} + + +# Local variables: +# mode: shell-script +# sh-indentation: 2 +# eval: (add-hook 'before-save-hook 'time-stamp) +# time-stamp-pattern: "10/scriptversion=%:y-%02m-%02d.%02H; # UTC" +# time-stamp-time-zone: "UTC" +# End: + +# Set a version string. +scriptversion='(GNU libtool) 2.4.6' + + +# func_echo ARG... +# ---------------- +# Libtool also displays the current mode in messages, so override +# funclib.sh func_echo with this custom definition. +func_echo () +{ + $debug_cmd + + _G_message=$* + + func_echo_IFS=$IFS + IFS=$nl + for _G_line in $_G_message; do + IFS=$func_echo_IFS + $ECHO "$progname${opt_mode+: $opt_mode}: $_G_line" + done + IFS=$func_echo_IFS +} + + +# func_warning ARG... +# ------------------- +# Libtool warnings are not categorized, so override funclib.sh +# func_warning with this simpler definition. +func_warning () +{ + $debug_cmd + + $warning_func ${1+"$@"} +} + + +## ---------------- ## +## Options parsing. ## +## ---------------- ## + +# Hook in the functions to make sure our own options are parsed during +# the option parsing loop. + +usage='$progpath [OPTION]... [MODE-ARG]...' + +# Short help message in response to '-h'. +usage_message="Options: + --config show all configuration variables + --debug enable verbose shell tracing + -n, --dry-run display commands without modifying any files + --features display basic configuration information and exit + --mode=MODE use operation mode MODE + --no-warnings equivalent to '-Wnone' + --preserve-dup-deps don't remove duplicate dependency libraries + --quiet, --silent don't print informational messages + --tag=TAG use configuration variables from tag TAG + -v, --verbose print more informational messages than default + --version print version information + -W, --warnings=CATEGORY report the warnings falling in CATEGORY [all] + -h, --help, --help-all print short, long, or detailed help message +" + +# Additional text appended to 'usage_message' in response to '--help'. +func_help () +{ + $debug_cmd + + func_usage_message + $ECHO "$long_help_message + +MODE must be one of the following: + + clean remove files from the build directory + compile compile a source file into a libtool object + execute automatically set library path, then run a program + finish complete the installation of libtool libraries + install install libraries or executables + link create a library or an executable + uninstall remove libraries from an installed directory + +MODE-ARGS vary depending on the MODE. When passed as first option, +'--mode=MODE' may be abbreviated as 'MODE' or a unique abbreviation of that. +Try '$progname --help --mode=MODE' for a more detailed description of MODE. + +When reporting a bug, please describe a test case to reproduce it and +include the following information: + + host-triplet: $host + shell: $SHELL + compiler: $LTCC + compiler flags: $LTCFLAGS + linker: $LD (gnu? $with_gnu_ld) + version: $progname $scriptversion Debian-2.4.6-9 + automake: `($AUTOMAKE --version) 2>/dev/null |$SED 1q` + autoconf: `($AUTOCONF --version) 2>/dev/null |$SED 1q` + +Report bugs to . +GNU libtool home page: . +General help using GNU software: ." + exit 0 +} + + +# func_lo2o OBJECT-NAME +# --------------------- +# Transform OBJECT-NAME from a '.lo' suffix to the platform specific +# object suffix. + +lo2o=s/\\.lo\$/.$objext/ +o2lo=s/\\.$objext\$/.lo/ + +if test yes = "$_G_HAVE_XSI_OPS"; then + eval 'func_lo2o () + { + case $1 in + *.lo) func_lo2o_result=${1%.lo}.$objext ;; + * ) func_lo2o_result=$1 ;; + esac + }' + + # func_xform LIBOBJ-OR-SOURCE + # --------------------------- + # Transform LIBOBJ-OR-SOURCE from a '.o' or '.c' (or otherwise) + # suffix to a '.lo' libtool-object suffix. + eval 'func_xform () + { + func_xform_result=${1%.*}.lo + }' +else + # ...otherwise fall back to using sed. + func_lo2o () + { + func_lo2o_result=`$ECHO "$1" | $SED "$lo2o"` + } + + func_xform () + { + func_xform_result=`$ECHO "$1" | $SED 's|\.[^.]*$|.lo|'` + } +fi + + +# func_fatal_configuration ARG... +# ------------------------------- +# Echo program name prefixed message to standard error, followed by +# a configuration failure hint, and exit. +func_fatal_configuration () +{ + func__fatal_error ${1+"$@"} \ + "See the $PACKAGE documentation for more information." \ + "Fatal configuration error." +} + + +# func_config +# ----------- +# Display the configuration for all the tags in this script. +func_config () +{ + re_begincf='^# ### BEGIN LIBTOOL' + re_endcf='^# ### END LIBTOOL' + + # Default configuration. + $SED "1,/$re_begincf CONFIG/d;/$re_endcf CONFIG/,\$d" < "$progpath" + + # Now print the configurations for the tags. + for tagname in $taglist; do + $SED -n "/$re_begincf TAG CONFIG: $tagname\$/,/$re_endcf TAG CONFIG: $tagname\$/p" < "$progpath" + done + + exit $? +} + + +# func_features +# ------------- +# Display the features supported by this script. +func_features () +{ + echo "host: $host" + if test yes = "$build_libtool_libs"; then + echo "enable shared libraries" + else + echo "disable shared libraries" + fi + if test yes = "$build_old_libs"; then + echo "enable static libraries" + else + echo "disable static libraries" + fi + + exit $? +} + + +# func_enable_tag TAGNAME +# ----------------------- +# Verify that TAGNAME is valid, and either flag an error and exit, or +# enable the TAGNAME tag. We also add TAGNAME to the global $taglist +# variable here. +func_enable_tag () +{ + # Global variable: + tagname=$1 + + re_begincf="^# ### BEGIN LIBTOOL TAG CONFIG: $tagname\$" + re_endcf="^# ### END LIBTOOL TAG CONFIG: $tagname\$" + sed_extractcf=/$re_begincf/,/$re_endcf/p + + # Validate tagname. + case $tagname in + *[!-_A-Za-z0-9,/]*) + func_fatal_error "invalid tag name: $tagname" + ;; + esac + + # Don't test for the "default" C tag, as we know it's + # there but not specially marked. + case $tagname in + CC) ;; + *) + if $GREP "$re_begincf" "$progpath" >/dev/null 2>&1; then + taglist="$taglist $tagname" + + # Evaluate the configuration. Be careful to quote the path + # and the sed script, to avoid splitting on whitespace, but + # also don't use non-portable quotes within backquotes within + # quotes we have to do it in 2 steps: + extractedcf=`$SED -n -e "$sed_extractcf" < "$progpath"` + eval "$extractedcf" + else + func_error "ignoring unknown tag $tagname" + fi + ;; + esac +} + + +# func_check_version_match +# ------------------------ +# Ensure that we are using m4 macros, and libtool script from the same +# release of libtool. +func_check_version_match () +{ + if test "$package_revision" != "$macro_revision"; then + if test "$VERSION" != "$macro_version"; then + if test -z "$macro_version"; then + cat >&2 <<_LT_EOF +$progname: Version mismatch error. This is $PACKAGE $VERSION, but the +$progname: definition of this LT_INIT comes from an older release. +$progname: You should recreate aclocal.m4 with macros from $PACKAGE $VERSION +$progname: and run autoconf again. +_LT_EOF + else + cat >&2 <<_LT_EOF +$progname: Version mismatch error. This is $PACKAGE $VERSION, but the +$progname: definition of this LT_INIT comes from $PACKAGE $macro_version. +$progname: You should recreate aclocal.m4 with macros from $PACKAGE $VERSION +$progname: and run autoconf again. +_LT_EOF + fi + else + cat >&2 <<_LT_EOF +$progname: Version mismatch error. This is $PACKAGE $VERSION, revision $package_revision, +$progname: but the definition of this LT_INIT comes from revision $macro_revision. +$progname: You should recreate aclocal.m4 with macros from revision $package_revision +$progname: of $PACKAGE $VERSION and run autoconf again. +_LT_EOF + fi + + exit $EXIT_MISMATCH + fi +} + + +# libtool_options_prep [ARG]... +# ----------------------------- +# Preparation for options parsed by libtool. +libtool_options_prep () +{ + $debug_mode + + # Option defaults: + opt_config=false + opt_dlopen= + opt_dry_run=false + opt_help=false + opt_mode= + opt_preserve_dup_deps=false + opt_quiet=false + + nonopt= + preserve_args= + + _G_rc_lt_options_prep=: + + # Shorthand for --mode=foo, only valid as the first argument + case $1 in + clean|clea|cle|cl) + shift; set dummy --mode clean ${1+"$@"}; shift + ;; + compile|compil|compi|comp|com|co|c) + shift; set dummy --mode compile ${1+"$@"}; shift + ;; + execute|execut|execu|exec|exe|ex|e) + shift; set dummy --mode execute ${1+"$@"}; shift + ;; + finish|finis|fini|fin|fi|f) + shift; set dummy --mode finish ${1+"$@"}; shift + ;; + install|instal|insta|inst|ins|in|i) + shift; set dummy --mode install ${1+"$@"}; shift + ;; + link|lin|li|l) + shift; set dummy --mode link ${1+"$@"}; shift + ;; + uninstall|uninstal|uninsta|uninst|unins|unin|uni|un|u) + shift; set dummy --mode uninstall ${1+"$@"}; shift + ;; + *) + _G_rc_lt_options_prep=false + ;; + esac + + if $_G_rc_lt_options_prep; then + # Pass back the list of options. + func_quote_for_eval ${1+"$@"} + libtool_options_prep_result=$func_quote_for_eval_result + fi + + $_G_rc_lt_options_prep +} +func_add_hook func_options_prep libtool_options_prep + + +# libtool_parse_options [ARG]... +# --------------------------------- +# Provide handling for libtool specific options. +libtool_parse_options () +{ + $debug_cmd + + _G_rc_lt_parse_options=false + + # Perform our own loop to consume as many options as possible in + # each iteration. + while test $# -gt 0; do + _G_match_lt_parse_options=: + _G_opt=$1 + shift + case $_G_opt in + --dry-run|--dryrun|-n) + opt_dry_run=: + ;; + + --config) func_config ;; + + --dlopen|-dlopen) + opt_dlopen="${opt_dlopen+$opt_dlopen +}$1" + shift + ;; + + --preserve-dup-deps) + opt_preserve_dup_deps=: ;; + + --features) func_features ;; + + --finish) set dummy --mode finish ${1+"$@"}; shift ;; + + --help) opt_help=: ;; + + --help-all) opt_help=': help-all' ;; + + --mode) test $# = 0 && func_missing_arg $_G_opt && break + opt_mode=$1 + case $1 in + # Valid mode arguments: + clean|compile|execute|finish|install|link|relink|uninstall) ;; + + # Catch anything else as an error + *) func_error "invalid argument for $_G_opt" + exit_cmd=exit + break + ;; + esac + shift + ;; + + --no-silent|--no-quiet) + opt_quiet=false + func_append preserve_args " $_G_opt" + ;; + + --no-warnings|--no-warning|--no-warn) + opt_warning=false + func_append preserve_args " $_G_opt" + ;; + + --no-verbose) + opt_verbose=false + func_append preserve_args " $_G_opt" + ;; + + --silent|--quiet) + opt_quiet=: + opt_verbose=false + func_append preserve_args " $_G_opt" + ;; + + --tag) test $# = 0 && func_missing_arg $_G_opt && break + opt_tag=$1 + func_append preserve_args " $_G_opt $1" + func_enable_tag "$1" + shift + ;; + + --verbose|-v) opt_quiet=false + opt_verbose=: + func_append preserve_args " $_G_opt" + ;; + + # An option not handled by this hook function: + *) set dummy "$_G_opt" ${1+"$@"} ; shift + _G_match_lt_parse_options=false + break + ;; + esac + $_G_match_lt_parse_options && _G_rc_lt_parse_options=: + done + + if $_G_rc_lt_parse_options; then + # save modified positional parameters for caller + func_quote_for_eval ${1+"$@"} + libtool_parse_options_result=$func_quote_for_eval_result + fi + + $_G_rc_lt_parse_options +} +func_add_hook func_parse_options libtool_parse_options + + + +# libtool_validate_options [ARG]... +# --------------------------------- +# Perform any sanity checks on option settings and/or unconsumed +# arguments. +libtool_validate_options () +{ + # save first non-option argument + if test 0 -lt $#; then + nonopt=$1 + shift + fi + + # preserve --debug + test : = "$debug_cmd" || func_append preserve_args " --debug" + + case $host in + # Solaris2 added to fix http://debbugs.gnu.org/cgi/bugreport.cgi?bug=16452 + # see also: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=59788 + *cygwin* | *mingw* | *pw32* | *cegcc* | *solaris2* | *os2*) + # don't eliminate duplications in $postdeps and $predeps + opt_duplicate_compiler_generated_deps=: + ;; + *) + opt_duplicate_compiler_generated_deps=$opt_preserve_dup_deps + ;; + esac + + $opt_help || { + # Sanity checks first: + func_check_version_match + + test yes != "$build_libtool_libs" \ + && test yes != "$build_old_libs" \ + && func_fatal_configuration "not configured to build any kind of library" + + # Darwin sucks + eval std_shrext=\"$shrext_cmds\" + + # Only execute mode is allowed to have -dlopen flags. + if test -n "$opt_dlopen" && test execute != "$opt_mode"; then + func_error "unrecognized option '-dlopen'" + $ECHO "$help" 1>&2 + exit $EXIT_FAILURE + fi + + # Change the help message to a mode-specific one. + generic_help=$help + help="Try '$progname --help --mode=$opt_mode' for more information." + } + + # Pass back the unparsed argument list + func_quote_for_eval ${1+"$@"} + libtool_validate_options_result=$func_quote_for_eval_result +} +func_add_hook func_validate_options libtool_validate_options + + +# Process options as early as possible so that --help and --version +# can return quickly. +func_options ${1+"$@"} +eval set dummy "$func_options_result"; shift + + + +## ----------- ## +## Main. ## +## ----------- ## + +magic='%%%MAGIC variable%%%' +magic_exe='%%%MAGIC EXE variable%%%' + +# Global variables. +extracted_archives= +extracted_serial=0 + +# If this variable is set in any of the actions, the command in it +# will be execed at the end. This prevents here-documents from being +# left over by shells. +exec_cmd= + + +# A function that is used when there is no print builtin or printf. +func_fallback_echo () +{ + eval 'cat <<_LTECHO_EOF +$1 +_LTECHO_EOF' +} + +# func_generated_by_libtool +# True iff stdin has been generated by Libtool. This function is only +# a basic sanity check; it will hardly flush out determined imposters. +func_generated_by_libtool_p () +{ + $GREP "^# Generated by .*$PACKAGE" > /dev/null 2>&1 +} + +# func_lalib_p file +# True iff FILE is a libtool '.la' library or '.lo' object file. +# This function is only a basic sanity check; it will hardly flush out +# determined imposters. +func_lalib_p () +{ + test -f "$1" && + $SED -e 4q "$1" 2>/dev/null | func_generated_by_libtool_p +} + +# func_lalib_unsafe_p file +# True iff FILE is a libtool '.la' library or '.lo' object file. +# This function implements the same check as func_lalib_p without +# resorting to external programs. To this end, it redirects stdin and +# closes it afterwards, without saving the original file descriptor. +# As a safety measure, use it only where a negative result would be +# fatal anyway. Works if 'file' does not exist. +func_lalib_unsafe_p () +{ + lalib_p=no + if test -f "$1" && test -r "$1" && exec 5<&0 <"$1"; then + for lalib_p_l in 1 2 3 4 + do + read lalib_p_line + case $lalib_p_line in + \#\ Generated\ by\ *$PACKAGE* ) lalib_p=yes; break;; + esac + done + exec 0<&5 5<&- + fi + test yes = "$lalib_p" +} + +# func_ltwrapper_script_p file +# True iff FILE is a libtool wrapper script +# This function is only a basic sanity check; it will hardly flush out +# determined imposters. +func_ltwrapper_script_p () +{ + test -f "$1" && + $lt_truncate_bin < "$1" 2>/dev/null | func_generated_by_libtool_p +} + +# func_ltwrapper_executable_p file +# True iff FILE is a libtool wrapper executable +# This function is only a basic sanity check; it will hardly flush out +# determined imposters. +func_ltwrapper_executable_p () +{ + func_ltwrapper_exec_suffix= + case $1 in + *.exe) ;; + *) func_ltwrapper_exec_suffix=.exe ;; + esac + $GREP "$magic_exe" "$1$func_ltwrapper_exec_suffix" >/dev/null 2>&1 +} + +# func_ltwrapper_scriptname file +# Assumes file is an ltwrapper_executable +# uses $file to determine the appropriate filename for a +# temporary ltwrapper_script. +func_ltwrapper_scriptname () +{ + func_dirname_and_basename "$1" "" "." + func_stripname '' '.exe' "$func_basename_result" + func_ltwrapper_scriptname_result=$func_dirname_result/$objdir/${func_stripname_result}_ltshwrapper +} + +# func_ltwrapper_p file +# True iff FILE is a libtool wrapper script or wrapper executable +# This function is only a basic sanity check; it will hardly flush out +# determined imposters. +func_ltwrapper_p () +{ + func_ltwrapper_script_p "$1" || func_ltwrapper_executable_p "$1" +} + + +# func_execute_cmds commands fail_cmd +# Execute tilde-delimited COMMANDS. +# If FAIL_CMD is given, eval that upon failure. +# FAIL_CMD may read-access the current command in variable CMD! +func_execute_cmds () +{ + $debug_cmd + + save_ifs=$IFS; IFS='~' + for cmd in $1; do + IFS=$sp$nl + eval cmd=\"$cmd\" + IFS=$save_ifs + func_show_eval "$cmd" "${2-:}" + done + IFS=$save_ifs +} + + +# func_source file +# Source FILE, adding directory component if necessary. +# Note that it is not necessary on cygwin/mingw to append a dot to +# FILE even if both FILE and FILE.exe exist: automatic-append-.exe +# behavior happens only for exec(3), not for open(2)! Also, sourcing +# 'FILE.' does not work on cygwin managed mounts. +func_source () +{ + $debug_cmd + + case $1 in + */* | *\\*) . "$1" ;; + *) . "./$1" ;; + esac +} + + +# func_resolve_sysroot PATH +# Replace a leading = in PATH with a sysroot. Store the result into +# func_resolve_sysroot_result +func_resolve_sysroot () +{ + func_resolve_sysroot_result=$1 + case $func_resolve_sysroot_result in + =*) + func_stripname '=' '' "$func_resolve_sysroot_result" + func_resolve_sysroot_result=$lt_sysroot$func_stripname_result + ;; + esac +} + +# func_replace_sysroot PATH +# If PATH begins with the sysroot, replace it with = and +# store the result into func_replace_sysroot_result. +func_replace_sysroot () +{ + case $lt_sysroot:$1 in + ?*:"$lt_sysroot"*) + func_stripname "$lt_sysroot" '' "$1" + func_replace_sysroot_result='='$func_stripname_result + ;; + *) + # Including no sysroot. + func_replace_sysroot_result=$1 + ;; + esac +} + +# func_infer_tag arg +# Infer tagged configuration to use if any are available and +# if one wasn't chosen via the "--tag" command line option. +# Only attempt this if the compiler in the base compile +# command doesn't match the default compiler. +# arg is usually of the form 'gcc ...' +func_infer_tag () +{ + $debug_cmd + + if test -n "$available_tags" && test -z "$tagname"; then + CC_quoted= + for arg in $CC; do + func_append_quoted CC_quoted "$arg" + done + CC_expanded=`func_echo_all $CC` + CC_quoted_expanded=`func_echo_all $CC_quoted` + case $@ in + # Blanks in the command may have been stripped by the calling shell, + # but not from the CC environment variable when configure was run. + " $CC "* | "$CC "* | " $CC_expanded "* | "$CC_expanded "* | \ + " $CC_quoted"* | "$CC_quoted "* | " $CC_quoted_expanded "* | "$CC_quoted_expanded "*) ;; + # Blanks at the start of $base_compile will cause this to fail + # if we don't check for them as well. + *) + for z in $available_tags; do + if $GREP "^# ### BEGIN LIBTOOL TAG CONFIG: $z$" < "$progpath" > /dev/null; then + # Evaluate the configuration. + eval "`$SED -n -e '/^# ### BEGIN LIBTOOL TAG CONFIG: '$z'$/,/^# ### END LIBTOOL TAG CONFIG: '$z'$/p' < $progpath`" + CC_quoted= + for arg in $CC; do + # Double-quote args containing other shell metacharacters. + func_append_quoted CC_quoted "$arg" + done + CC_expanded=`func_echo_all $CC` + CC_quoted_expanded=`func_echo_all $CC_quoted` + case "$@ " in + " $CC "* | "$CC "* | " $CC_expanded "* | "$CC_expanded "* | \ + " $CC_quoted"* | "$CC_quoted "* | " $CC_quoted_expanded "* | "$CC_quoted_expanded "*) + # The compiler in the base compile command matches + # the one in the tagged configuration. + # Assume this is the tagged configuration we want. + tagname=$z + break + ;; + esac + fi + done + # If $tagname still isn't set, then no tagged configuration + # was found and let the user know that the "--tag" command + # line option must be used. + if test -z "$tagname"; then + func_echo "unable to infer tagged configuration" + func_fatal_error "specify a tag with '--tag'" +# else +# func_verbose "using $tagname tagged configuration" + fi + ;; + esac + fi +} + + + +# func_write_libtool_object output_name pic_name nonpic_name +# Create a libtool object file (analogous to a ".la" file), +# but don't create it if we're doing a dry run. +func_write_libtool_object () +{ + write_libobj=$1 + if test yes = "$build_libtool_libs"; then + write_lobj=\'$2\' + else + write_lobj=none + fi + + if test yes = "$build_old_libs"; then + write_oldobj=\'$3\' + else + write_oldobj=none + fi + + $opt_dry_run || { + cat >${write_libobj}T </dev/null` + if test "$?" -eq 0 && test -n "$func_convert_core_file_wine_to_w32_tmp"; then + func_convert_core_file_wine_to_w32_result=`$ECHO "$func_convert_core_file_wine_to_w32_tmp" | + $SED -e "$sed_naive_backslashify"` + else + func_convert_core_file_wine_to_w32_result= + fi + fi +} +# end: func_convert_core_file_wine_to_w32 + + +# func_convert_core_path_wine_to_w32 ARG +# Helper function used by path conversion functions when $build is *nix, and +# $host is mingw, cygwin, or some other w32 environment. Relies on a correctly +# configured wine environment available, with the winepath program in $build's +# $PATH. Assumes ARG has no leading or trailing path separator characters. +# +# ARG is path to be converted from $build format to win32. +# Result is available in $func_convert_core_path_wine_to_w32_result. +# Unconvertible file (directory) names in ARG are skipped; if no directory names +# are convertible, then the result may be empty. +func_convert_core_path_wine_to_w32 () +{ + $debug_cmd + + # unfortunately, winepath doesn't convert paths, only file names + func_convert_core_path_wine_to_w32_result= + if test -n "$1"; then + oldIFS=$IFS + IFS=: + for func_convert_core_path_wine_to_w32_f in $1; do + IFS=$oldIFS + func_convert_core_file_wine_to_w32 "$func_convert_core_path_wine_to_w32_f" + if test -n "$func_convert_core_file_wine_to_w32_result"; then + if test -z "$func_convert_core_path_wine_to_w32_result"; then + func_convert_core_path_wine_to_w32_result=$func_convert_core_file_wine_to_w32_result + else + func_append func_convert_core_path_wine_to_w32_result ";$func_convert_core_file_wine_to_w32_result" + fi + fi + done + IFS=$oldIFS + fi +} +# end: func_convert_core_path_wine_to_w32 + + +# func_cygpath ARGS... +# Wrapper around calling the cygpath program via LT_CYGPATH. This is used when +# when (1) $build is *nix and Cygwin is hosted via a wine environment; or (2) +# $build is MSYS and $host is Cygwin, or (3) $build is Cygwin. In case (1) or +# (2), returns the Cygwin file name or path in func_cygpath_result (input +# file name or path is assumed to be in w32 format, as previously converted +# from $build's *nix or MSYS format). In case (3), returns the w32 file name +# or path in func_cygpath_result (input file name or path is assumed to be in +# Cygwin format). Returns an empty string on error. +# +# ARGS are passed to cygpath, with the last one being the file name or path to +# be converted. +# +# Specify the absolute *nix (or w32) name to cygpath in the LT_CYGPATH +# environment variable; do not put it in $PATH. +func_cygpath () +{ + $debug_cmd + + if test -n "$LT_CYGPATH" && test -f "$LT_CYGPATH"; then + func_cygpath_result=`$LT_CYGPATH "$@" 2>/dev/null` + if test "$?" -ne 0; then + # on failure, ensure result is empty + func_cygpath_result= + fi + else + func_cygpath_result= + func_error "LT_CYGPATH is empty or specifies non-existent file: '$LT_CYGPATH'" + fi +} +#end: func_cygpath + + +# func_convert_core_msys_to_w32 ARG +# Convert file name or path ARG from MSYS format to w32 format. Return +# result in func_convert_core_msys_to_w32_result. +func_convert_core_msys_to_w32 () +{ + $debug_cmd + + # awkward: cmd appends spaces to result + func_convert_core_msys_to_w32_result=`( cmd //c echo "$1" ) 2>/dev/null | + $SED -e 's/[ ]*$//' -e "$sed_naive_backslashify"` +} +#end: func_convert_core_msys_to_w32 + + +# func_convert_file_check ARG1 ARG2 +# Verify that ARG1 (a file name in $build format) was converted to $host +# format in ARG2. Otherwise, emit an error message, but continue (resetting +# func_to_host_file_result to ARG1). +func_convert_file_check () +{ + $debug_cmd + + if test -z "$2" && test -n "$1"; then + func_error "Could not determine host file name corresponding to" + func_error " '$1'" + func_error "Continuing, but uninstalled executables may not work." + # Fallback: + func_to_host_file_result=$1 + fi +} +# end func_convert_file_check + + +# func_convert_path_check FROM_PATHSEP TO_PATHSEP FROM_PATH TO_PATH +# Verify that FROM_PATH (a path in $build format) was converted to $host +# format in TO_PATH. Otherwise, emit an error message, but continue, resetting +# func_to_host_file_result to a simplistic fallback value (see below). +func_convert_path_check () +{ + $debug_cmd + + if test -z "$4" && test -n "$3"; then + func_error "Could not determine the host path corresponding to" + func_error " '$3'" + func_error "Continuing, but uninstalled executables may not work." + # Fallback. This is a deliberately simplistic "conversion" and + # should not be "improved". See libtool.info. + if test "x$1" != "x$2"; then + lt_replace_pathsep_chars="s|$1|$2|g" + func_to_host_path_result=`echo "$3" | + $SED -e "$lt_replace_pathsep_chars"` + else + func_to_host_path_result=$3 + fi + fi +} +# end func_convert_path_check + + +# func_convert_path_front_back_pathsep FRONTPAT BACKPAT REPL ORIG +# Modifies func_to_host_path_result by prepending REPL if ORIG matches FRONTPAT +# and appending REPL if ORIG matches BACKPAT. +func_convert_path_front_back_pathsep () +{ + $debug_cmd + + case $4 in + $1 ) func_to_host_path_result=$3$func_to_host_path_result + ;; + esac + case $4 in + $2 ) func_append func_to_host_path_result "$3" + ;; + esac +} +# end func_convert_path_front_back_pathsep + + +################################################## +# $build to $host FILE NAME CONVERSION FUNCTIONS # +################################################## +# invoked via '$to_host_file_cmd ARG' +# +# In each case, ARG is the path to be converted from $build to $host format. +# Result will be available in $func_to_host_file_result. + + +# func_to_host_file ARG +# Converts the file name ARG from $build format to $host format. Return result +# in func_to_host_file_result. +func_to_host_file () +{ + $debug_cmd + + $to_host_file_cmd "$1" +} +# end func_to_host_file + + +# func_to_tool_file ARG LAZY +# converts the file name ARG from $build format to toolchain format. Return +# result in func_to_tool_file_result. If the conversion in use is listed +# in (the comma separated) LAZY, no conversion takes place. +func_to_tool_file () +{ + $debug_cmd + + case ,$2, in + *,"$to_tool_file_cmd",*) + func_to_tool_file_result=$1 + ;; + *) + $to_tool_file_cmd "$1" + func_to_tool_file_result=$func_to_host_file_result + ;; + esac +} +# end func_to_tool_file + + +# func_convert_file_noop ARG +# Copy ARG to func_to_host_file_result. +func_convert_file_noop () +{ + func_to_host_file_result=$1 +} +# end func_convert_file_noop + + +# func_convert_file_msys_to_w32 ARG +# Convert file name ARG from (mingw) MSYS to (mingw) w32 format; automatic +# conversion to w32 is not available inside the cwrapper. Returns result in +# func_to_host_file_result. +func_convert_file_msys_to_w32 () +{ + $debug_cmd + + func_to_host_file_result=$1 + if test -n "$1"; then + func_convert_core_msys_to_w32 "$1" + func_to_host_file_result=$func_convert_core_msys_to_w32_result + fi + func_convert_file_check "$1" "$func_to_host_file_result" +} +# end func_convert_file_msys_to_w32 + + +# func_convert_file_cygwin_to_w32 ARG +# Convert file name ARG from Cygwin to w32 format. Returns result in +# func_to_host_file_result. +func_convert_file_cygwin_to_w32 () +{ + $debug_cmd + + func_to_host_file_result=$1 + if test -n "$1"; then + # because $build is cygwin, we call "the" cygpath in $PATH; no need to use + # LT_CYGPATH in this case. + func_to_host_file_result=`cygpath -m "$1"` + fi + func_convert_file_check "$1" "$func_to_host_file_result" +} +# end func_convert_file_cygwin_to_w32 + + +# func_convert_file_nix_to_w32 ARG +# Convert file name ARG from *nix to w32 format. Requires a wine environment +# and a working winepath. Returns result in func_to_host_file_result. +func_convert_file_nix_to_w32 () +{ + $debug_cmd + + func_to_host_file_result=$1 + if test -n "$1"; then + func_convert_core_file_wine_to_w32 "$1" + func_to_host_file_result=$func_convert_core_file_wine_to_w32_result + fi + func_convert_file_check "$1" "$func_to_host_file_result" +} +# end func_convert_file_nix_to_w32 + + +# func_convert_file_msys_to_cygwin ARG +# Convert file name ARG from MSYS to Cygwin format. Requires LT_CYGPATH set. +# Returns result in func_to_host_file_result. +func_convert_file_msys_to_cygwin () +{ + $debug_cmd + + func_to_host_file_result=$1 + if test -n "$1"; then + func_convert_core_msys_to_w32 "$1" + func_cygpath -u "$func_convert_core_msys_to_w32_result" + func_to_host_file_result=$func_cygpath_result + fi + func_convert_file_check "$1" "$func_to_host_file_result" +} +# end func_convert_file_msys_to_cygwin + + +# func_convert_file_nix_to_cygwin ARG +# Convert file name ARG from *nix to Cygwin format. Requires Cygwin installed +# in a wine environment, working winepath, and LT_CYGPATH set. Returns result +# in func_to_host_file_result. +func_convert_file_nix_to_cygwin () +{ + $debug_cmd + + func_to_host_file_result=$1 + if test -n "$1"; then + # convert from *nix to w32, then use cygpath to convert from w32 to cygwin. + func_convert_core_file_wine_to_w32 "$1" + func_cygpath -u "$func_convert_core_file_wine_to_w32_result" + func_to_host_file_result=$func_cygpath_result + fi + func_convert_file_check "$1" "$func_to_host_file_result" +} +# end func_convert_file_nix_to_cygwin + + +############################################# +# $build to $host PATH CONVERSION FUNCTIONS # +############################################# +# invoked via '$to_host_path_cmd ARG' +# +# In each case, ARG is the path to be converted from $build to $host format. +# The result will be available in $func_to_host_path_result. +# +# Path separators are also converted from $build format to $host format. If +# ARG begins or ends with a path separator character, it is preserved (but +# converted to $host format) on output. +# +# All path conversion functions are named using the following convention: +# file name conversion function : func_convert_file_X_to_Y () +# path conversion function : func_convert_path_X_to_Y () +# where, for any given $build/$host combination the 'X_to_Y' value is the +# same. If conversion functions are added for new $build/$host combinations, +# the two new functions must follow this pattern, or func_init_to_host_path_cmd +# will break. + + +# func_init_to_host_path_cmd +# Ensures that function "pointer" variable $to_host_path_cmd is set to the +# appropriate value, based on the value of $to_host_file_cmd. +to_host_path_cmd= +func_init_to_host_path_cmd () +{ + $debug_cmd + + if test -z "$to_host_path_cmd"; then + func_stripname 'func_convert_file_' '' "$to_host_file_cmd" + to_host_path_cmd=func_convert_path_$func_stripname_result + fi +} + + +# func_to_host_path ARG +# Converts the path ARG from $build format to $host format. Return result +# in func_to_host_path_result. +func_to_host_path () +{ + $debug_cmd + + func_init_to_host_path_cmd + $to_host_path_cmd "$1" +} +# end func_to_host_path + + +# func_convert_path_noop ARG +# Copy ARG to func_to_host_path_result. +func_convert_path_noop () +{ + func_to_host_path_result=$1 +} +# end func_convert_path_noop + + +# func_convert_path_msys_to_w32 ARG +# Convert path ARG from (mingw) MSYS to (mingw) w32 format; automatic +# conversion to w32 is not available inside the cwrapper. Returns result in +# func_to_host_path_result. +func_convert_path_msys_to_w32 () +{ + $debug_cmd + + func_to_host_path_result=$1 + if test -n "$1"; then + # Remove leading and trailing path separator characters from ARG. MSYS + # behavior is inconsistent here; cygpath turns them into '.;' and ';.'; + # and winepath ignores them completely. + func_stripname : : "$1" + func_to_host_path_tmp1=$func_stripname_result + func_convert_core_msys_to_w32 "$func_to_host_path_tmp1" + func_to_host_path_result=$func_convert_core_msys_to_w32_result + func_convert_path_check : ";" \ + "$func_to_host_path_tmp1" "$func_to_host_path_result" + func_convert_path_front_back_pathsep ":*" "*:" ";" "$1" + fi +} +# end func_convert_path_msys_to_w32 + + +# func_convert_path_cygwin_to_w32 ARG +# Convert path ARG from Cygwin to w32 format. Returns result in +# func_to_host_file_result. +func_convert_path_cygwin_to_w32 () +{ + $debug_cmd + + func_to_host_path_result=$1 + if test -n "$1"; then + # See func_convert_path_msys_to_w32: + func_stripname : : "$1" + func_to_host_path_tmp1=$func_stripname_result + func_to_host_path_result=`cygpath -m -p "$func_to_host_path_tmp1"` + func_convert_path_check : ";" \ + "$func_to_host_path_tmp1" "$func_to_host_path_result" + func_convert_path_front_back_pathsep ":*" "*:" ";" "$1" + fi +} +# end func_convert_path_cygwin_to_w32 + + +# func_convert_path_nix_to_w32 ARG +# Convert path ARG from *nix to w32 format. Requires a wine environment and +# a working winepath. Returns result in func_to_host_file_result. +func_convert_path_nix_to_w32 () +{ + $debug_cmd + + func_to_host_path_result=$1 + if test -n "$1"; then + # See func_convert_path_msys_to_w32: + func_stripname : : "$1" + func_to_host_path_tmp1=$func_stripname_result + func_convert_core_path_wine_to_w32 "$func_to_host_path_tmp1" + func_to_host_path_result=$func_convert_core_path_wine_to_w32_result + func_convert_path_check : ";" \ + "$func_to_host_path_tmp1" "$func_to_host_path_result" + func_convert_path_front_back_pathsep ":*" "*:" ";" "$1" + fi +} +# end func_convert_path_nix_to_w32 + + +# func_convert_path_msys_to_cygwin ARG +# Convert path ARG from MSYS to Cygwin format. Requires LT_CYGPATH set. +# Returns result in func_to_host_file_result. +func_convert_path_msys_to_cygwin () +{ + $debug_cmd + + func_to_host_path_result=$1 + if test -n "$1"; then + # See func_convert_path_msys_to_w32: + func_stripname : : "$1" + func_to_host_path_tmp1=$func_stripname_result + func_convert_core_msys_to_w32 "$func_to_host_path_tmp1" + func_cygpath -u -p "$func_convert_core_msys_to_w32_result" + func_to_host_path_result=$func_cygpath_result + func_convert_path_check : : \ + "$func_to_host_path_tmp1" "$func_to_host_path_result" + func_convert_path_front_back_pathsep ":*" "*:" : "$1" + fi +} +# end func_convert_path_msys_to_cygwin + + +# func_convert_path_nix_to_cygwin ARG +# Convert path ARG from *nix to Cygwin format. Requires Cygwin installed in a +# a wine environment, working winepath, and LT_CYGPATH set. Returns result in +# func_to_host_file_result. +func_convert_path_nix_to_cygwin () +{ + $debug_cmd + + func_to_host_path_result=$1 + if test -n "$1"; then + # Remove leading and trailing path separator characters from + # ARG. msys behavior is inconsistent here, cygpath turns them + # into '.;' and ';.', and winepath ignores them completely. + func_stripname : : "$1" + func_to_host_path_tmp1=$func_stripname_result + func_convert_core_path_wine_to_w32 "$func_to_host_path_tmp1" + func_cygpath -u -p "$func_convert_core_path_wine_to_w32_result" + func_to_host_path_result=$func_cygpath_result + func_convert_path_check : : \ + "$func_to_host_path_tmp1" "$func_to_host_path_result" + func_convert_path_front_back_pathsep ":*" "*:" : "$1" + fi +} +# end func_convert_path_nix_to_cygwin + + +# func_dll_def_p FILE +# True iff FILE is a Windows DLL '.def' file. +# Keep in sync with _LT_DLL_DEF_P in libtool.m4 +func_dll_def_p () +{ + $debug_cmd + + func_dll_def_p_tmp=`$SED -n \ + -e 's/^[ ]*//' \ + -e '/^\(;.*\)*$/d' \ + -e 's/^\(EXPORTS\|LIBRARY\)\([ ].*\)*$/DEF/p' \ + -e q \ + "$1"` + test DEF = "$func_dll_def_p_tmp" +} + + +# func_mode_compile arg... +func_mode_compile () +{ + $debug_cmd + + # Get the compilation command and the source file. + base_compile= + srcfile=$nonopt # always keep a non-empty value in "srcfile" + suppress_opt=yes + suppress_output= + arg_mode=normal + libobj= + later= + pie_flag= + + for arg + do + case $arg_mode in + arg ) + # do not "continue". Instead, add this to base_compile + lastarg=$arg + arg_mode=normal + ;; + + target ) + libobj=$arg + arg_mode=normal + continue + ;; + + normal ) + # Accept any command-line options. + case $arg in + -o) + test -n "$libobj" && \ + func_fatal_error "you cannot specify '-o' more than once" + arg_mode=target + continue + ;; + + -pie | -fpie | -fPIE) + func_append pie_flag " $arg" + continue + ;; + + -shared | -static | -prefer-pic | -prefer-non-pic) + func_append later " $arg" + continue + ;; + + -no-suppress) + suppress_opt=no + continue + ;; + + -Xcompiler) + arg_mode=arg # the next one goes into the "base_compile" arg list + continue # The current "srcfile" will either be retained or + ;; # replaced later. I would guess that would be a bug. + + -Wc,*) + func_stripname '-Wc,' '' "$arg" + args=$func_stripname_result + lastarg= + save_ifs=$IFS; IFS=, + for arg in $args; do + IFS=$save_ifs + func_append_quoted lastarg "$arg" + done + IFS=$save_ifs + func_stripname ' ' '' "$lastarg" + lastarg=$func_stripname_result + + # Add the arguments to base_compile. + func_append base_compile " $lastarg" + continue + ;; + + *) + # Accept the current argument as the source file. + # The previous "srcfile" becomes the current argument. + # + lastarg=$srcfile + srcfile=$arg + ;; + esac # case $arg + ;; + esac # case $arg_mode + + # Aesthetically quote the previous argument. + func_append_quoted base_compile "$lastarg" + done # for arg + + case $arg_mode in + arg) + func_fatal_error "you must specify an argument for -Xcompile" + ;; + target) + func_fatal_error "you must specify a target with '-o'" + ;; + *) + # Get the name of the library object. + test -z "$libobj" && { + func_basename "$srcfile" + libobj=$func_basename_result + } + ;; + esac + + # Recognize several different file suffixes. + # If the user specifies -o file.o, it is replaced with file.lo + case $libobj in + *.[cCFSifmso] | \ + *.ada | *.adb | *.ads | *.asm | \ + *.c++ | *.cc | *.ii | *.class | *.cpp | *.cxx | \ + *.[fF][09]? | *.for | *.java | *.go | *.obj | *.sx | *.cu | *.cup) + func_xform "$libobj" + libobj=$func_xform_result + ;; + esac + + case $libobj in + *.lo) func_lo2o "$libobj"; obj=$func_lo2o_result ;; + *) + func_fatal_error "cannot determine name of library object from '$libobj'" + ;; + esac + + func_infer_tag $base_compile + + for arg in $later; do + case $arg in + -shared) + test yes = "$build_libtool_libs" \ + || func_fatal_configuration "cannot build a shared library" + build_old_libs=no + continue + ;; + + -static) + build_libtool_libs=no + build_old_libs=yes + continue + ;; + + -prefer-pic) + pic_mode=yes + continue + ;; + + -prefer-non-pic) + pic_mode=no + continue + ;; + esac + done + + func_quote_for_eval "$libobj" + test "X$libobj" != "X$func_quote_for_eval_result" \ + && $ECHO "X$libobj" | $GREP '[]~#^*{};<>?"'"'"' &()|`$[]' \ + && func_warning "libobj name '$libobj' may not contain shell special characters." + func_dirname_and_basename "$obj" "/" "" + objname=$func_basename_result + xdir=$func_dirname_result + lobj=$xdir$objdir/$objname + + test -z "$base_compile" && \ + func_fatal_help "you must specify a compilation command" + + # Delete any leftover library objects. + if test yes = "$build_old_libs"; then + removelist="$obj $lobj $libobj ${libobj}T" + else + removelist="$lobj $libobj ${libobj}T" + fi + + # On Cygwin there's no "real" PIC flag so we must build both object types + case $host_os in + cygwin* | mingw* | pw32* | os2* | cegcc*) + pic_mode=default + ;; + esac + if test no = "$pic_mode" && test pass_all != "$deplibs_check_method"; then + # non-PIC code in shared libraries is not supported + pic_mode=default + fi + + # Calculate the filename of the output object if compiler does + # not support -o with -c + if test no = "$compiler_c_o"; then + output_obj=`$ECHO "$srcfile" | $SED 's%^.*/%%; s%\.[^.]*$%%'`.$objext + lockfile=$output_obj.lock + else + output_obj= + need_locks=no + lockfile= + fi + + # Lock this critical section if it is needed + # We use this script file to make the link, it avoids creating a new file + if test yes = "$need_locks"; then + until $opt_dry_run || ln "$progpath" "$lockfile" 2>/dev/null; do + func_echo "Waiting for $lockfile to be removed" + sleep 2 + done + elif test warn = "$need_locks"; then + if test -f "$lockfile"; then + $ECHO "\ +*** ERROR, $lockfile exists and contains: +`cat $lockfile 2>/dev/null` + +This indicates that another process is trying to use the same +temporary object file, and libtool could not work around it because +your compiler does not support '-c' and '-o' together. If you +repeat this compilation, it may succeed, by chance, but you had better +avoid parallel builds (make -j) in this platform, or get a better +compiler." + + $opt_dry_run || $RM $removelist + exit $EXIT_FAILURE + fi + func_append removelist " $output_obj" + $ECHO "$srcfile" > "$lockfile" + fi + + $opt_dry_run || $RM $removelist + func_append removelist " $lockfile" + trap '$opt_dry_run || $RM $removelist; exit $EXIT_FAILURE' 1 2 15 + + func_to_tool_file "$srcfile" func_convert_file_msys_to_w32 + srcfile=$func_to_tool_file_result + func_quote_for_eval "$srcfile" + qsrcfile=$func_quote_for_eval_result + + # Only build a PIC object if we are building libtool libraries. + if test yes = "$build_libtool_libs"; then + # Without this assignment, base_compile gets emptied. + fbsd_hideous_sh_bug=$base_compile + + if test no != "$pic_mode"; then + command="$base_compile $qsrcfile $pic_flag" + else + # Don't build PIC code + command="$base_compile $qsrcfile" + fi + + func_mkdir_p "$xdir$objdir" + + if test -z "$output_obj"; then + # Place PIC objects in $objdir + func_append command " -o $lobj" + fi + + func_show_eval_locale "$command" \ + 'test -n "$output_obj" && $RM $removelist; exit $EXIT_FAILURE' + + if test warn = "$need_locks" && + test "X`cat $lockfile 2>/dev/null`" != "X$srcfile"; then + $ECHO "\ +*** ERROR, $lockfile contains: +`cat $lockfile 2>/dev/null` + +but it should contain: +$srcfile + +This indicates that another process is trying to use the same +temporary object file, and libtool could not work around it because +your compiler does not support '-c' and '-o' together. If you +repeat this compilation, it may succeed, by chance, but you had better +avoid parallel builds (make -j) in this platform, or get a better +compiler." + + $opt_dry_run || $RM $removelist + exit $EXIT_FAILURE + fi + + # Just move the object if needed, then go on to compile the next one + if test -n "$output_obj" && test "X$output_obj" != "X$lobj"; then + func_show_eval '$MV "$output_obj" "$lobj"' \ + 'error=$?; $opt_dry_run || $RM $removelist; exit $error' + fi + + # Allow error messages only from the first compilation. + if test yes = "$suppress_opt"; then + suppress_output=' >/dev/null 2>&1' + fi + fi + + # Only build a position-dependent object if we build old libraries. + if test yes = "$build_old_libs"; then + if test yes != "$pic_mode"; then + # Don't build PIC code + command="$base_compile $qsrcfile$pie_flag" + else + command="$base_compile $qsrcfile $pic_flag" + fi + if test yes = "$compiler_c_o"; then + func_append command " -o $obj" + fi + + # Suppress compiler output if we already did a PIC compilation. + func_append command "$suppress_output" + func_show_eval_locale "$command" \ + '$opt_dry_run || $RM $removelist; exit $EXIT_FAILURE' + + if test warn = "$need_locks" && + test "X`cat $lockfile 2>/dev/null`" != "X$srcfile"; then + $ECHO "\ +*** ERROR, $lockfile contains: +`cat $lockfile 2>/dev/null` + +but it should contain: +$srcfile + +This indicates that another process is trying to use the same +temporary object file, and libtool could not work around it because +your compiler does not support '-c' and '-o' together. If you +repeat this compilation, it may succeed, by chance, but you had better +avoid parallel builds (make -j) in this platform, or get a better +compiler." + + $opt_dry_run || $RM $removelist + exit $EXIT_FAILURE + fi + + # Just move the object if needed + if test -n "$output_obj" && test "X$output_obj" != "X$obj"; then + func_show_eval '$MV "$output_obj" "$obj"' \ + 'error=$?; $opt_dry_run || $RM $removelist; exit $error' + fi + fi + + $opt_dry_run || { + func_write_libtool_object "$libobj" "$objdir/$objname" "$objname" + + # Unlock the critical section if it was locked + if test no != "$need_locks"; then + removelist=$lockfile + $RM "$lockfile" + fi + } + + exit $EXIT_SUCCESS +} + +$opt_help || { + test compile = "$opt_mode" && func_mode_compile ${1+"$@"} +} + +func_mode_help () +{ + # We need to display help for each of the modes. + case $opt_mode in + "") + # Generic help is extracted from the usage comments + # at the start of this file. + func_help + ;; + + clean) + $ECHO \ +"Usage: $progname [OPTION]... --mode=clean RM [RM-OPTION]... FILE... + +Remove files from the build directory. + +RM is the name of the program to use to delete files associated with each FILE +(typically '/bin/rm'). RM-OPTIONS are options (such as '-f') to be passed +to RM. + +If FILE is a libtool library, object or program, all the files associated +with it are deleted. Otherwise, only FILE itself is deleted using RM." + ;; + + compile) + $ECHO \ +"Usage: $progname [OPTION]... --mode=compile COMPILE-COMMAND... SOURCEFILE + +Compile a source file into a libtool library object. + +This mode accepts the following additional options: + + -o OUTPUT-FILE set the output file name to OUTPUT-FILE + -no-suppress do not suppress compiler output for multiple passes + -prefer-pic try to build PIC objects only + -prefer-non-pic try to build non-PIC objects only + -shared do not build a '.o' file suitable for static linking + -static only build a '.o' file suitable for static linking + -Wc,FLAG pass FLAG directly to the compiler + +COMPILE-COMMAND is a command to be used in creating a 'standard' object file +from the given SOURCEFILE. + +The output file name is determined by removing the directory component from +SOURCEFILE, then substituting the C source code suffix '.c' with the +library object suffix, '.lo'." + ;; + + execute) + $ECHO \ +"Usage: $progname [OPTION]... --mode=execute COMMAND [ARGS]... + +Automatically set library path, then run a program. + +This mode accepts the following additional options: + + -dlopen FILE add the directory containing FILE to the library path + +This mode sets the library path environment variable according to '-dlopen' +flags. + +If any of the ARGS are libtool executable wrappers, then they are translated +into their corresponding uninstalled binary, and any of their required library +directories are added to the library path. + +Then, COMMAND is executed, with ARGS as arguments." + ;; + + finish) + $ECHO \ +"Usage: $progname [OPTION]... --mode=finish [LIBDIR]... + +Complete the installation of libtool libraries. + +Each LIBDIR is a directory that contains libtool libraries. + +The commands that this mode executes may require superuser privileges. Use +the '--dry-run' option if you just want to see what would be executed." + ;; + + install) + $ECHO \ +"Usage: $progname [OPTION]... --mode=install INSTALL-COMMAND... + +Install executables or libraries. + +INSTALL-COMMAND is the installation command. The first component should be +either the 'install' or 'cp' program. + +The following components of INSTALL-COMMAND are treated specially: + + -inst-prefix-dir PREFIX-DIR Use PREFIX-DIR as a staging area for installation + +The rest of the components are interpreted as arguments to that command (only +BSD-compatible install options are recognized)." + ;; + + link) + $ECHO \ +"Usage: $progname [OPTION]... --mode=link LINK-COMMAND... + +Link object files or libraries together to form another library, or to +create an executable program. + +LINK-COMMAND is a command using the C compiler that you would use to create +a program from several object files. + +The following components of LINK-COMMAND are treated specially: + + -all-static do not do any dynamic linking at all + -avoid-version do not add a version suffix if possible + -bindir BINDIR specify path to binaries directory (for systems where + libraries must be found in the PATH setting at runtime) + -dlopen FILE '-dlpreopen' FILE if it cannot be dlopened at runtime + -dlpreopen FILE link in FILE and add its symbols to lt_preloaded_symbols + -export-dynamic allow symbols from OUTPUT-FILE to be resolved with dlsym(3) + -export-symbols SYMFILE + try to export only the symbols listed in SYMFILE + -export-symbols-regex REGEX + try to export only the symbols matching REGEX + -LLIBDIR search LIBDIR for required installed libraries + -lNAME OUTPUT-FILE requires the installed library libNAME + -module build a library that can dlopened + -no-fast-install disable the fast-install mode + -no-install link a not-installable executable + -no-undefined declare that a library does not refer to external symbols + -o OUTPUT-FILE create OUTPUT-FILE from the specified objects + -objectlist FILE use a list of object files found in FILE to specify objects + -os2dllname NAME force a short DLL name on OS/2 (no effect on other OSes) + -precious-files-regex REGEX + don't remove output files matching REGEX + -release RELEASE specify package release information + -rpath LIBDIR the created library will eventually be installed in LIBDIR + -R[ ]LIBDIR add LIBDIR to the runtime path of programs and libraries + -shared only do dynamic linking of libtool libraries + -shrext SUFFIX override the standard shared library file extension + -static do not do any dynamic linking of uninstalled libtool libraries + -static-libtool-libs + do not do any dynamic linking of libtool libraries + -version-info CURRENT[:REVISION[:AGE]] + specify library version info [each variable defaults to 0] + -weak LIBNAME declare that the target provides the LIBNAME interface + -Wc,FLAG + -Xcompiler FLAG pass linker-specific FLAG directly to the compiler + -Wl,FLAG + -Xlinker FLAG pass linker-specific FLAG directly to the linker + -XCClinker FLAG pass link-specific FLAG to the compiler driver (CC) + +All other options (arguments beginning with '-') are ignored. + +Every other argument is treated as a filename. Files ending in '.la' are +treated as uninstalled libtool libraries, other files are standard or library +object files. + +If the OUTPUT-FILE ends in '.la', then a libtool library is created, +only library objects ('.lo' files) may be specified, and '-rpath' is +required, except when creating a convenience library. + +If OUTPUT-FILE ends in '.a' or '.lib', then a standard library is created +using 'ar' and 'ranlib', or on Windows using 'lib'. + +If OUTPUT-FILE ends in '.lo' or '.$objext', then a reloadable object file +is created, otherwise an executable program is created." + ;; + + uninstall) + $ECHO \ +"Usage: $progname [OPTION]... --mode=uninstall RM [RM-OPTION]... FILE... + +Remove libraries from an installation directory. + +RM is the name of the program to use to delete files associated with each FILE +(typically '/bin/rm'). RM-OPTIONS are options (such as '-f') to be passed +to RM. + +If FILE is a libtool library, all the files associated with it are deleted. +Otherwise, only FILE itself is deleted using RM." + ;; + + *) + func_fatal_help "invalid operation mode '$opt_mode'" + ;; + esac + + echo + $ECHO "Try '$progname --help' for more information about other modes." +} + +# Now that we've collected a possible --mode arg, show help if necessary +if $opt_help; then + if test : = "$opt_help"; then + func_mode_help + else + { + func_help noexit + for opt_mode in compile link execute install finish uninstall clean; do + func_mode_help + done + } | $SED -n '1p; 2,$s/^Usage:/ or: /p' + { + func_help noexit + for opt_mode in compile link execute install finish uninstall clean; do + echo + func_mode_help + done + } | + $SED '1d + /^When reporting/,/^Report/{ + H + d + } + $x + /information about other modes/d + /more detailed .*MODE/d + s/^Usage:.*--mode=\([^ ]*\) .*/Description of \1 mode:/' + fi + exit $? +fi + + +# func_mode_execute arg... +func_mode_execute () +{ + $debug_cmd + + # The first argument is the command name. + cmd=$nonopt + test -z "$cmd" && \ + func_fatal_help "you must specify a COMMAND" + + # Handle -dlopen flags immediately. + for file in $opt_dlopen; do + test -f "$file" \ + || func_fatal_help "'$file' is not a file" + + dir= + case $file in + *.la) + func_resolve_sysroot "$file" + file=$func_resolve_sysroot_result + + # Check to see that this really is a libtool archive. + func_lalib_unsafe_p "$file" \ + || func_fatal_help "'$lib' is not a valid libtool archive" + + # Read the libtool library. + dlname= + library_names= + func_source "$file" + + # Skip this library if it cannot be dlopened. + if test -z "$dlname"; then + # Warn if it was a shared library. + test -n "$library_names" && \ + func_warning "'$file' was not linked with '-export-dynamic'" + continue + fi + + func_dirname "$file" "" "." + dir=$func_dirname_result + + if test -f "$dir/$objdir/$dlname"; then + func_append dir "/$objdir" + else + if test ! -f "$dir/$dlname"; then + func_fatal_error "cannot find '$dlname' in '$dir' or '$dir/$objdir'" + fi + fi + ;; + + *.lo) + # Just add the directory containing the .lo file. + func_dirname "$file" "" "." + dir=$func_dirname_result + ;; + + *) + func_warning "'-dlopen' is ignored for non-libtool libraries and objects" + continue + ;; + esac + + # Get the absolute pathname. + absdir=`cd "$dir" && pwd` + test -n "$absdir" && dir=$absdir + + # Now add the directory to shlibpath_var. + if eval "test -z \"\$$shlibpath_var\""; then + eval "$shlibpath_var=\"\$dir\"" + else + eval "$shlibpath_var=\"\$dir:\$$shlibpath_var\"" + fi + done + + # This variable tells wrapper scripts just to set shlibpath_var + # rather than running their programs. + libtool_execute_magic=$magic + + # Check if any of the arguments is a wrapper script. + args= + for file + do + case $file in + -* | *.la | *.lo ) ;; + *) + # Do a test to see if this is really a libtool program. + if func_ltwrapper_script_p "$file"; then + func_source "$file" + # Transform arg to wrapped name. + file=$progdir/$program + elif func_ltwrapper_executable_p "$file"; then + func_ltwrapper_scriptname "$file" + func_source "$func_ltwrapper_scriptname_result" + # Transform arg to wrapped name. + file=$progdir/$program + fi + ;; + esac + # Quote arguments (to preserve shell metacharacters). + func_append_quoted args "$file" + done + + if $opt_dry_run; then + # Display what would be done. + if test -n "$shlibpath_var"; then + eval "\$ECHO \"\$shlibpath_var=\$$shlibpath_var\"" + echo "export $shlibpath_var" + fi + $ECHO "$cmd$args" + exit $EXIT_SUCCESS + else + if test -n "$shlibpath_var"; then + # Export the shlibpath_var. + eval "export $shlibpath_var" + fi + + # Restore saved environment variables + for lt_var in LANG LANGUAGE LC_ALL LC_CTYPE LC_COLLATE LC_MESSAGES + do + eval "if test \"\${save_$lt_var+set}\" = set; then + $lt_var=\$save_$lt_var; export $lt_var + else + $lt_unset $lt_var + fi" + done + + # Now prepare to actually exec the command. + exec_cmd=\$cmd$args + fi +} + +test execute = "$opt_mode" && func_mode_execute ${1+"$@"} + + +# func_mode_finish arg... +func_mode_finish () +{ + $debug_cmd + + libs= + libdirs= + admincmds= + + for opt in "$nonopt" ${1+"$@"} + do + if test -d "$opt"; then + func_append libdirs " $opt" + + elif test -f "$opt"; then + if func_lalib_unsafe_p "$opt"; then + func_append libs " $opt" + else + func_warning "'$opt' is not a valid libtool archive" + fi + + else + func_fatal_error "invalid argument '$opt'" + fi + done + + if test -n "$libs"; then + if test -n "$lt_sysroot"; then + sysroot_regex=`$ECHO "$lt_sysroot" | $SED "$sed_make_literal_regex"` + sysroot_cmd="s/\([ ']\)$sysroot_regex/\1/g;" + else + sysroot_cmd= + fi + + # Remove sysroot references + if $opt_dry_run; then + for lib in $libs; do + echo "removing references to $lt_sysroot and '=' prefixes from $lib" + done + else + tmpdir=`func_mktempdir` + for lib in $libs; do + $SED -e "$sysroot_cmd s/\([ ']-[LR]\)=/\1/g; s/\([ ']\)=/\1/g" $lib \ + > $tmpdir/tmp-la + mv -f $tmpdir/tmp-la $lib + done + ${RM}r "$tmpdir" + fi + fi + + if test -n "$finish_cmds$finish_eval" && test -n "$libdirs"; then + for libdir in $libdirs; do + if test -n "$finish_cmds"; then + # Do each command in the finish commands. + func_execute_cmds "$finish_cmds" 'admincmds="$admincmds +'"$cmd"'"' + fi + if test -n "$finish_eval"; then + # Do the single finish_eval. + eval cmds=\"$finish_eval\" + $opt_dry_run || eval "$cmds" || func_append admincmds " + $cmds" + fi + done + fi + + # Exit here if they wanted silent mode. + $opt_quiet && exit $EXIT_SUCCESS + + if test -n "$finish_cmds$finish_eval" && test -n "$libdirs"; then + echo "----------------------------------------------------------------------" + echo "Libraries have been installed in:" + for libdir in $libdirs; do + $ECHO " $libdir" + done + echo + echo "If you ever happen to want to link against installed libraries" + echo "in a given directory, LIBDIR, you must either use libtool, and" + echo "specify the full pathname of the library, or use the '-LLIBDIR'" + echo "flag during linking and do at least one of the following:" + if test -n "$shlibpath_var"; then + echo " - add LIBDIR to the '$shlibpath_var' environment variable" + echo " during execution" + fi + if test -n "$runpath_var"; then + echo " - add LIBDIR to the '$runpath_var' environment variable" + echo " during linking" + fi + if test -n "$hardcode_libdir_flag_spec"; then + libdir=LIBDIR + eval flag=\"$hardcode_libdir_flag_spec\" + + $ECHO " - use the '$flag' linker flag" + fi + if test -n "$admincmds"; then + $ECHO " - have your system administrator run these commands:$admincmds" + fi + if test -f /etc/ld.so.conf; then + echo " - have your system administrator add LIBDIR to '/etc/ld.so.conf'" + fi + echo + + echo "See any operating system documentation about shared libraries for" + case $host in + solaris2.[6789]|solaris2.1[0-9]) + echo "more information, such as the ld(1), crle(1) and ld.so(8) manual" + echo "pages." + ;; + *) + echo "more information, such as the ld(1) and ld.so(8) manual pages." + ;; + esac + echo "----------------------------------------------------------------------" + fi + exit $EXIT_SUCCESS +} + +test finish = "$opt_mode" && func_mode_finish ${1+"$@"} + + +# func_mode_install arg... +func_mode_install () +{ + $debug_cmd + + # There may be an optional sh(1) argument at the beginning of + # install_prog (especially on Windows NT). + if test "$SHELL" = "$nonopt" || test /bin/sh = "$nonopt" || + # Allow the use of GNU shtool's install command. + case $nonopt in *shtool*) :;; *) false;; esac + then + # Aesthetically quote it. + func_quote_for_eval "$nonopt" + install_prog="$func_quote_for_eval_result " + arg=$1 + shift + else + install_prog= + arg=$nonopt + fi + + # The real first argument should be the name of the installation program. + # Aesthetically quote it. + func_quote_for_eval "$arg" + func_append install_prog "$func_quote_for_eval_result" + install_shared_prog=$install_prog + case " $install_prog " in + *[\\\ /]cp\ *) install_cp=: ;; + *) install_cp=false ;; + esac + + # We need to accept at least all the BSD install flags. + dest= + files= + opts= + prev= + install_type= + isdir=false + stripme= + no_mode=: + for arg + do + arg2= + if test -n "$dest"; then + func_append files " $dest" + dest=$arg + continue + fi + + case $arg in + -d) isdir=: ;; + -f) + if $install_cp; then :; else + prev=$arg + fi + ;; + -g | -m | -o) + prev=$arg + ;; + -s) + stripme=" -s" + continue + ;; + -*) + ;; + *) + # If the previous option needed an argument, then skip it. + if test -n "$prev"; then + if test X-m = "X$prev" && test -n "$install_override_mode"; then + arg2=$install_override_mode + no_mode=false + fi + prev= + else + dest=$arg + continue + fi + ;; + esac + + # Aesthetically quote the argument. + func_quote_for_eval "$arg" + func_append install_prog " $func_quote_for_eval_result" + if test -n "$arg2"; then + func_quote_for_eval "$arg2" + fi + func_append install_shared_prog " $func_quote_for_eval_result" + done + + test -z "$install_prog" && \ + func_fatal_help "you must specify an install program" + + test -n "$prev" && \ + func_fatal_help "the '$prev' option requires an argument" + + if test -n "$install_override_mode" && $no_mode; then + if $install_cp; then :; else + func_quote_for_eval "$install_override_mode" + func_append install_shared_prog " -m $func_quote_for_eval_result" + fi + fi + + if test -z "$files"; then + if test -z "$dest"; then + func_fatal_help "no file or destination specified" + else + func_fatal_help "you must specify a destination" + fi + fi + + # Strip any trailing slash from the destination. + func_stripname '' '/' "$dest" + dest=$func_stripname_result + + # Check to see that the destination is a directory. + test -d "$dest" && isdir=: + if $isdir; then + destdir=$dest + destname= + else + func_dirname_and_basename "$dest" "" "." + destdir=$func_dirname_result + destname=$func_basename_result + + # Not a directory, so check to see that there is only one file specified. + set dummy $files; shift + test "$#" -gt 1 && \ + func_fatal_help "'$dest' is not a directory" + fi + case $destdir in + [\\/]* | [A-Za-z]:[\\/]*) ;; + *) + for file in $files; do + case $file in + *.lo) ;; + *) + func_fatal_help "'$destdir' must be an absolute directory name" + ;; + esac + done + ;; + esac + + # This variable tells wrapper scripts just to set variables rather + # than running their programs. + libtool_install_magic=$magic + + staticlibs= + future_libdirs= + current_libdirs= + for file in $files; do + + # Do each installation. + case $file in + *.$libext) + # Do the static libraries later. + func_append staticlibs " $file" + ;; + + *.la) + func_resolve_sysroot "$file" + file=$func_resolve_sysroot_result + + # Check to see that this really is a libtool archive. + func_lalib_unsafe_p "$file" \ + || func_fatal_help "'$file' is not a valid libtool archive" + + library_names= + old_library= + relink_command= + func_source "$file" + + # Add the libdir to current_libdirs if it is the destination. + if test "X$destdir" = "X$libdir"; then + case "$current_libdirs " in + *" $libdir "*) ;; + *) func_append current_libdirs " $libdir" ;; + esac + else + # Note the libdir as a future libdir. + case "$future_libdirs " in + *" $libdir "*) ;; + *) func_append future_libdirs " $libdir" ;; + esac + fi + + func_dirname "$file" "/" "" + dir=$func_dirname_result + func_append dir "$objdir" + + if test -n "$relink_command"; then + # Determine the prefix the user has applied to our future dir. + inst_prefix_dir=`$ECHO "$destdir" | $SED -e "s%$libdir\$%%"` + + # Don't allow the user to place us outside of our expected + # location b/c this prevents finding dependent libraries that + # are installed to the same prefix. + # At present, this check doesn't affect windows .dll's that + # are installed into $libdir/../bin (currently, that works fine) + # but it's something to keep an eye on. + test "$inst_prefix_dir" = "$destdir" && \ + func_fatal_error "error: cannot install '$file' to a directory not ending in $libdir" + + if test -n "$inst_prefix_dir"; then + # Stick the inst_prefix_dir data into the link command. + relink_command=`$ECHO "$relink_command" | $SED "s%@inst_prefix_dir@%-inst-prefix-dir $inst_prefix_dir%"` + else + relink_command=`$ECHO "$relink_command" | $SED "s%@inst_prefix_dir@%%"` + fi + + func_warning "relinking '$file'" + func_show_eval "$relink_command" \ + 'func_fatal_error "error: relink '\''$file'\'' with the above command before installing it"' + fi + + # See the names of the shared library. + set dummy $library_names; shift + if test -n "$1"; then + realname=$1 + shift + + srcname=$realname + test -n "$relink_command" && srcname=${realname}T + + # Install the shared library and build the symlinks. + func_show_eval "$install_shared_prog $dir/$srcname $destdir/$realname" \ + 'exit $?' + tstripme=$stripme + case $host_os in + cygwin* | mingw* | pw32* | cegcc*) + case $realname in + *.dll.a) + tstripme= + ;; + esac + ;; + os2*) + case $realname in + *_dll.a) + tstripme= + ;; + esac + ;; + esac + if test -n "$tstripme" && test -n "$striplib"; then + func_show_eval "$striplib $destdir/$realname" 'exit $?' + fi + + if test "$#" -gt 0; then + # Delete the old symlinks, and create new ones. + # Try 'ln -sf' first, because the 'ln' binary might depend on + # the symlink we replace! Solaris /bin/ln does not understand -f, + # so we also need to try rm && ln -s. + for linkname + do + test "$linkname" != "$realname" \ + && func_show_eval "(cd $destdir && { $LN_S -f $realname $linkname || { $RM $linkname && $LN_S $realname $linkname; }; })" + done + fi + + # Do each command in the postinstall commands. + lib=$destdir/$realname + func_execute_cmds "$postinstall_cmds" 'exit $?' + fi + + # Install the pseudo-library for information purposes. + func_basename "$file" + name=$func_basename_result + instname=$dir/${name}i + func_show_eval "$install_prog $instname $destdir/$name" 'exit $?' + + # Maybe install the static library, too. + test -n "$old_library" && func_append staticlibs " $dir/$old_library" + ;; + + *.lo) + # Install (i.e. copy) a libtool object. + + # Figure out destination file name, if it wasn't already specified. + if test -n "$destname"; then + destfile=$destdir/$destname + else + func_basename "$file" + destfile=$func_basename_result + destfile=$destdir/$destfile + fi + + # Deduce the name of the destination old-style object file. + case $destfile in + *.lo) + func_lo2o "$destfile" + staticdest=$func_lo2o_result + ;; + *.$objext) + staticdest=$destfile + destfile= + ;; + *) + func_fatal_help "cannot copy a libtool object to '$destfile'" + ;; + esac + + # Install the libtool object if requested. + test -n "$destfile" && \ + func_show_eval "$install_prog $file $destfile" 'exit $?' + + # Install the old object if enabled. + if test yes = "$build_old_libs"; then + # Deduce the name of the old-style object file. + func_lo2o "$file" + staticobj=$func_lo2o_result + func_show_eval "$install_prog \$staticobj \$staticdest" 'exit $?' + fi + exit $EXIT_SUCCESS + ;; + + *) + # Figure out destination file name, if it wasn't already specified. + if test -n "$destname"; then + destfile=$destdir/$destname + else + func_basename "$file" + destfile=$func_basename_result + destfile=$destdir/$destfile + fi + + # If the file is missing, and there is a .exe on the end, strip it + # because it is most likely a libtool script we actually want to + # install + stripped_ext= + case $file in + *.exe) + if test ! -f "$file"; then + func_stripname '' '.exe' "$file" + file=$func_stripname_result + stripped_ext=.exe + fi + ;; + esac + + # Do a test to see if this is really a libtool program. + case $host in + *cygwin* | *mingw*) + if func_ltwrapper_executable_p "$file"; then + func_ltwrapper_scriptname "$file" + wrapper=$func_ltwrapper_scriptname_result + else + func_stripname '' '.exe' "$file" + wrapper=$func_stripname_result + fi + ;; + *) + wrapper=$file + ;; + esac + if func_ltwrapper_script_p "$wrapper"; then + notinst_deplibs= + relink_command= + + func_source "$wrapper" + + # Check the variables that should have been set. + test -z "$generated_by_libtool_version" && \ + func_fatal_error "invalid libtool wrapper script '$wrapper'" + + finalize=: + for lib in $notinst_deplibs; do + # Check to see that each library is installed. + libdir= + if test -f "$lib"; then + func_source "$lib" + fi + libfile=$libdir/`$ECHO "$lib" | $SED 's%^.*/%%g'` + if test -n "$libdir" && test ! -f "$libfile"; then + func_warning "'$lib' has not been installed in '$libdir'" + finalize=false + fi + done + + relink_command= + func_source "$wrapper" + + outputname= + if test no = "$fast_install" && test -n "$relink_command"; then + $opt_dry_run || { + if $finalize; then + tmpdir=`func_mktempdir` + func_basename "$file$stripped_ext" + file=$func_basename_result + outputname=$tmpdir/$file + # Replace the output file specification. + relink_command=`$ECHO "$relink_command" | $SED 's%@OUTPUT@%'"$outputname"'%g'` + + $opt_quiet || { + func_quote_for_expand "$relink_command" + eval "func_echo $func_quote_for_expand_result" + } + if eval "$relink_command"; then : + else + func_error "error: relink '$file' with the above command before installing it" + $opt_dry_run || ${RM}r "$tmpdir" + continue + fi + file=$outputname + else + func_warning "cannot relink '$file'" + fi + } + else + # Install the binary that we compiled earlier. + file=`$ECHO "$file$stripped_ext" | $SED "s%\([^/]*\)$%$objdir/\1%"` + fi + fi + + # remove .exe since cygwin /usr/bin/install will append another + # one anyway + case $install_prog,$host in + */usr/bin/install*,*cygwin*) + case $file:$destfile in + *.exe:*.exe) + # this is ok + ;; + *.exe:*) + destfile=$destfile.exe + ;; + *:*.exe) + func_stripname '' '.exe' "$destfile" + destfile=$func_stripname_result + ;; + esac + ;; + esac + func_show_eval "$install_prog\$stripme \$file \$destfile" 'exit $?' + $opt_dry_run || if test -n "$outputname"; then + ${RM}r "$tmpdir" + fi + ;; + esac + done + + for file in $staticlibs; do + func_basename "$file" + name=$func_basename_result + + # Set up the ranlib parameters. + oldlib=$destdir/$name + func_to_tool_file "$oldlib" func_convert_file_msys_to_w32 + tool_oldlib=$func_to_tool_file_result + + func_show_eval "$install_prog \$file \$oldlib" 'exit $?' + + if test -n "$stripme" && test -n "$old_striplib"; then + func_show_eval "$old_striplib $tool_oldlib" 'exit $?' + fi + + # Do each command in the postinstall commands. + func_execute_cmds "$old_postinstall_cmds" 'exit $?' + done + + test -n "$future_libdirs" && \ + func_warning "remember to run '$progname --finish$future_libdirs'" + + if test -n "$current_libdirs"; then + # Maybe just do a dry run. + $opt_dry_run && current_libdirs=" -n$current_libdirs" + exec_cmd='$SHELL "$progpath" $preserve_args --finish$current_libdirs' + else + exit $EXIT_SUCCESS + fi +} + +test install = "$opt_mode" && func_mode_install ${1+"$@"} + + +# func_generate_dlsyms outputname originator pic_p +# Extract symbols from dlprefiles and create ${outputname}S.o with +# a dlpreopen symbol table. +func_generate_dlsyms () +{ + $debug_cmd + + my_outputname=$1 + my_originator=$2 + my_pic_p=${3-false} + my_prefix=`$ECHO "$my_originator" | $SED 's%[^a-zA-Z0-9]%_%g'` + my_dlsyms= + + if test -n "$dlfiles$dlprefiles" || test no != "$dlself"; then + if test -n "$NM" && test -n "$global_symbol_pipe"; then + my_dlsyms=${my_outputname}S.c + else + func_error "not configured to extract global symbols from dlpreopened files" + fi + fi + + if test -n "$my_dlsyms"; then + case $my_dlsyms in + "") ;; + *.c) + # Discover the nlist of each of the dlfiles. + nlist=$output_objdir/$my_outputname.nm + + func_show_eval "$RM $nlist ${nlist}S ${nlist}T" + + # Parse the name list into a source file. + func_verbose "creating $output_objdir/$my_dlsyms" + + $opt_dry_run || $ECHO > "$output_objdir/$my_dlsyms" "\ +/* $my_dlsyms - symbol resolution table for '$my_outputname' dlsym emulation. */ +/* Generated by $PROGRAM (GNU $PACKAGE) $VERSION */ + +#ifdef __cplusplus +extern \"C\" { +#endif + +#if defined __GNUC__ && (((__GNUC__ == 4) && (__GNUC_MINOR__ >= 4)) || (__GNUC__ > 4)) +#pragma GCC diagnostic ignored \"-Wstrict-prototypes\" +#endif + +/* Keep this code in sync between libtool.m4, ltmain, lt_system.h, and tests. */ +#if defined _WIN32 || defined __CYGWIN__ || defined _WIN32_WCE +/* DATA imports from DLLs on WIN32 can't be const, because runtime + relocations are performed -- see ld's documentation on pseudo-relocs. */ +# define LT_DLSYM_CONST +#elif defined __osf__ +/* This system does not cope well with relocations in const data. */ +# define LT_DLSYM_CONST +#else +# define LT_DLSYM_CONST const +#endif + +#define STREQ(s1, s2) (strcmp ((s1), (s2)) == 0) + +/* External symbol declarations for the compiler. */\ +" + + if test yes = "$dlself"; then + func_verbose "generating symbol list for '$output'" + + $opt_dry_run || echo ': @PROGRAM@ ' > "$nlist" + + # Add our own program objects to the symbol list. + progfiles=`$ECHO "$objs$old_deplibs" | $SP2NL | $SED "$lo2o" | $NL2SP` + for progfile in $progfiles; do + func_to_tool_file "$progfile" func_convert_file_msys_to_w32 + func_verbose "extracting global C symbols from '$func_to_tool_file_result'" + $opt_dry_run || eval "$NM $func_to_tool_file_result | $global_symbol_pipe >> '$nlist'" + done + + if test -n "$exclude_expsyms"; then + $opt_dry_run || { + eval '$EGREP -v " ($exclude_expsyms)$" "$nlist" > "$nlist"T' + eval '$MV "$nlist"T "$nlist"' + } + fi + + if test -n "$export_symbols_regex"; then + $opt_dry_run || { + eval '$EGREP -e "$export_symbols_regex" "$nlist" > "$nlist"T' + eval '$MV "$nlist"T "$nlist"' + } + fi + + # Prepare the list of exported symbols + if test -z "$export_symbols"; then + export_symbols=$output_objdir/$outputname.exp + $opt_dry_run || { + $RM $export_symbols + eval "$SED -n -e '/^: @PROGRAM@ $/d' -e 's/^.* \(.*\)$/\1/p' "'< "$nlist" > "$export_symbols"' + case $host in + *cygwin* | *mingw* | *cegcc* ) + eval "echo EXPORTS "'> "$output_objdir/$outputname.def"' + eval 'cat "$export_symbols" >> "$output_objdir/$outputname.def"' + ;; + esac + } + else + $opt_dry_run || { + eval "$SED -e 's/\([].[*^$]\)/\\\\\1/g' -e 's/^/ /' -e 's/$/$/'"' < "$export_symbols" > "$output_objdir/$outputname.exp"' + eval '$GREP -f "$output_objdir/$outputname.exp" < "$nlist" > "$nlist"T' + eval '$MV "$nlist"T "$nlist"' + case $host in + *cygwin* | *mingw* | *cegcc* ) + eval "echo EXPORTS "'> "$output_objdir/$outputname.def"' + eval 'cat "$nlist" >> "$output_objdir/$outputname.def"' + ;; + esac + } + fi + fi + + for dlprefile in $dlprefiles; do + func_verbose "extracting global C symbols from '$dlprefile'" + func_basename "$dlprefile" + name=$func_basename_result + case $host in + *cygwin* | *mingw* | *cegcc* ) + # if an import library, we need to obtain dlname + if func_win32_import_lib_p "$dlprefile"; then + func_tr_sh "$dlprefile" + eval "curr_lafile=\$libfile_$func_tr_sh_result" + dlprefile_dlbasename= + if test -n "$curr_lafile" && func_lalib_p "$curr_lafile"; then + # Use subshell, to avoid clobbering current variable values + dlprefile_dlname=`source "$curr_lafile" && echo "$dlname"` + if test -n "$dlprefile_dlname"; then + func_basename "$dlprefile_dlname" + dlprefile_dlbasename=$func_basename_result + else + # no lafile. user explicitly requested -dlpreopen . + $sharedlib_from_linklib_cmd "$dlprefile" + dlprefile_dlbasename=$sharedlib_from_linklib_result + fi + fi + $opt_dry_run || { + if test -n "$dlprefile_dlbasename"; then + eval '$ECHO ": $dlprefile_dlbasename" >> "$nlist"' + else + func_warning "Could not compute DLL name from $name" + eval '$ECHO ": $name " >> "$nlist"' + fi + func_to_tool_file "$dlprefile" func_convert_file_msys_to_w32 + eval "$NM \"$func_to_tool_file_result\" 2>/dev/null | $global_symbol_pipe | + $SED -e '/I __imp/d' -e 's/I __nm_/D /;s/_nm__//' >> '$nlist'" + } + else # not an import lib + $opt_dry_run || { + eval '$ECHO ": $name " >> "$nlist"' + func_to_tool_file "$dlprefile" func_convert_file_msys_to_w32 + eval "$NM \"$func_to_tool_file_result\" 2>/dev/null | $global_symbol_pipe >> '$nlist'" + } + fi + ;; + *) + $opt_dry_run || { + eval '$ECHO ": $name " >> "$nlist"' + func_to_tool_file "$dlprefile" func_convert_file_msys_to_w32 + eval "$NM \"$func_to_tool_file_result\" 2>/dev/null | $global_symbol_pipe >> '$nlist'" + } + ;; + esac + done + + $opt_dry_run || { + # Make sure we have at least an empty file. + test -f "$nlist" || : > "$nlist" + + if test -n "$exclude_expsyms"; then + $EGREP -v " ($exclude_expsyms)$" "$nlist" > "$nlist"T + $MV "$nlist"T "$nlist" + fi + + # Try sorting and uniquifying the output. + if $GREP -v "^: " < "$nlist" | + if sort -k 3 /dev/null 2>&1; then + sort -k 3 + else + sort +2 + fi | + uniq > "$nlist"S; then + : + else + $GREP -v "^: " < "$nlist" > "$nlist"S + fi + + if test -f "$nlist"S; then + eval "$global_symbol_to_cdecl"' < "$nlist"S >> "$output_objdir/$my_dlsyms"' + else + echo '/* NONE */' >> "$output_objdir/$my_dlsyms" + fi + + func_show_eval '$RM "${nlist}I"' + if test -n "$global_symbol_to_import"; then + eval "$global_symbol_to_import"' < "$nlist"S > "$nlist"I' + fi + + echo >> "$output_objdir/$my_dlsyms" "\ + +/* The mapping between symbol names and symbols. */ +typedef struct { + const char *name; + void *address; +} lt_dlsymlist; +extern LT_DLSYM_CONST lt_dlsymlist +lt_${my_prefix}_LTX_preloaded_symbols[];\ +" + + if test -s "$nlist"I; then + echo >> "$output_objdir/$my_dlsyms" "\ +static void lt_syminit(void) +{ + LT_DLSYM_CONST lt_dlsymlist *symbol = lt_${my_prefix}_LTX_preloaded_symbols; + for (; symbol->name; ++symbol) + {" + $SED 's/.*/ if (STREQ (symbol->name, \"&\")) symbol->address = (void *) \&&;/' < "$nlist"I >> "$output_objdir/$my_dlsyms" + echo >> "$output_objdir/$my_dlsyms" "\ + } +}" + fi + echo >> "$output_objdir/$my_dlsyms" "\ +LT_DLSYM_CONST lt_dlsymlist +lt_${my_prefix}_LTX_preloaded_symbols[] = +{ {\"$my_originator\", (void *) 0}," + + if test -s "$nlist"I; then + echo >> "$output_objdir/$my_dlsyms" "\ + {\"@INIT@\", (void *) <_syminit}," + fi + + case $need_lib_prefix in + no) + eval "$global_symbol_to_c_name_address" < "$nlist" >> "$output_objdir/$my_dlsyms" + ;; + *) + eval "$global_symbol_to_c_name_address_lib_prefix" < "$nlist" >> "$output_objdir/$my_dlsyms" + ;; + esac + echo >> "$output_objdir/$my_dlsyms" "\ + {0, (void *) 0} +}; + +/* This works around a problem in FreeBSD linker */ +#ifdef FREEBSD_WORKAROUND +static const void *lt_preloaded_setup() { + return lt_${my_prefix}_LTX_preloaded_symbols; +} +#endif + +#ifdef __cplusplus +} +#endif\ +" + } # !$opt_dry_run + + pic_flag_for_symtable= + case "$compile_command " in + *" -static "*) ;; + *) + case $host in + # compiling the symbol table file with pic_flag works around + # a FreeBSD bug that causes programs to crash when -lm is + # linked before any other PIC object. But we must not use + # pic_flag when linking with -static. The problem exists in + # FreeBSD 2.2.6 and is fixed in FreeBSD 3.1. + *-*-freebsd2.*|*-*-freebsd3.0*|*-*-freebsdelf3.0*) + pic_flag_for_symtable=" $pic_flag -DFREEBSD_WORKAROUND" ;; + *-*-hpux*) + pic_flag_for_symtable=" $pic_flag" ;; + *) + $my_pic_p && pic_flag_for_symtable=" $pic_flag" + ;; + esac + ;; + esac + symtab_cflags= + for arg in $LTCFLAGS; do + case $arg in + -pie | -fpie | -fPIE) ;; + *) func_append symtab_cflags " $arg" ;; + esac + done + + # Now compile the dynamic symbol file. + func_show_eval '(cd $output_objdir && $LTCC$symtab_cflags -c$no_builtin_flag$pic_flag_for_symtable "$my_dlsyms")' 'exit $?' + + # Clean up the generated files. + func_show_eval '$RM "$output_objdir/$my_dlsyms" "$nlist" "${nlist}S" "${nlist}T" "${nlist}I"' + + # Transform the symbol file into the correct name. + symfileobj=$output_objdir/${my_outputname}S.$objext + case $host in + *cygwin* | *mingw* | *cegcc* ) + if test -f "$output_objdir/$my_outputname.def"; then + compile_command=`$ECHO "$compile_command" | $SED "s%@SYMFILE@%$output_objdir/$my_outputname.def $symfileobj%"` + finalize_command=`$ECHO "$finalize_command" | $SED "s%@SYMFILE@%$output_objdir/$my_outputname.def $symfileobj%"` + else + compile_command=`$ECHO "$compile_command" | $SED "s%@SYMFILE@%$symfileobj%"` + finalize_command=`$ECHO "$finalize_command" | $SED "s%@SYMFILE@%$symfileobj%"` + fi + ;; + *) + compile_command=`$ECHO "$compile_command" | $SED "s%@SYMFILE@%$symfileobj%"` + finalize_command=`$ECHO "$finalize_command" | $SED "s%@SYMFILE@%$symfileobj%"` + ;; + esac + ;; + *) + func_fatal_error "unknown suffix for '$my_dlsyms'" + ;; + esac + else + # We keep going just in case the user didn't refer to + # lt_preloaded_symbols. The linker will fail if global_symbol_pipe + # really was required. + + # Nullify the symbol file. + compile_command=`$ECHO "$compile_command" | $SED "s% @SYMFILE@%%"` + finalize_command=`$ECHO "$finalize_command" | $SED "s% @SYMFILE@%%"` + fi +} + +# func_cygming_gnu_implib_p ARG +# This predicate returns with zero status (TRUE) if +# ARG is a GNU/binutils-style import library. Returns +# with nonzero status (FALSE) otherwise. +func_cygming_gnu_implib_p () +{ + $debug_cmd + + func_to_tool_file "$1" func_convert_file_msys_to_w32 + func_cygming_gnu_implib_tmp=`$NM "$func_to_tool_file_result" | eval "$global_symbol_pipe" | $EGREP ' (_head_[A-Za-z0-9_]+_[ad]l*|[A-Za-z0-9_]+_[ad]l*_iname)$'` + test -n "$func_cygming_gnu_implib_tmp" +} + +# func_cygming_ms_implib_p ARG +# This predicate returns with zero status (TRUE) if +# ARG is an MS-style import library. Returns +# with nonzero status (FALSE) otherwise. +func_cygming_ms_implib_p () +{ + $debug_cmd + + func_to_tool_file "$1" func_convert_file_msys_to_w32 + func_cygming_ms_implib_tmp=`$NM "$func_to_tool_file_result" | eval "$global_symbol_pipe" | $GREP '_NULL_IMPORT_DESCRIPTOR'` + test -n "$func_cygming_ms_implib_tmp" +} + +# func_win32_libid arg +# return the library type of file 'arg' +# +# Need a lot of goo to handle *both* DLLs and import libs +# Has to be a shell function in order to 'eat' the argument +# that is supplied when $file_magic_command is called. +# Despite the name, also deal with 64 bit binaries. +func_win32_libid () +{ + $debug_cmd + + win32_libid_type=unknown + win32_fileres=`file -L $1 2>/dev/null` + case $win32_fileres in + *ar\ archive\ import\ library*) # definitely import + win32_libid_type="x86 archive import" + ;; + *ar\ archive*) # could be an import, or static + # Keep the egrep pattern in sync with the one in _LT_CHECK_MAGIC_METHOD. + if eval $OBJDUMP -f $1 | $SED -e '10q' 2>/dev/null | + $EGREP 'file format (pei*-i386(.*architecture: i386)?|pe-arm-wince|pe-x86-64)' >/dev/null; then + case $nm_interface in + "MS dumpbin") + if func_cygming_ms_implib_p "$1" || + func_cygming_gnu_implib_p "$1" + then + win32_nmres=import + else + win32_nmres= + fi + ;; + *) + func_to_tool_file "$1" func_convert_file_msys_to_w32 + win32_nmres=`eval $NM -f posix -A \"$func_to_tool_file_result\" | + $SED -n -e ' + 1,100{ + / I /{ + s|.*|import| + p + q + } + }'` + ;; + esac + case $win32_nmres in + import*) win32_libid_type="x86 archive import";; + *) win32_libid_type="x86 archive static";; + esac + fi + ;; + *DLL*) + win32_libid_type="x86 DLL" + ;; + *executable*) # but shell scripts are "executable" too... + case $win32_fileres in + *MS\ Windows\ PE\ Intel*) + win32_libid_type="x86 DLL" + ;; + esac + ;; + esac + $ECHO "$win32_libid_type" +} + +# func_cygming_dll_for_implib ARG +# +# Platform-specific function to extract the +# name of the DLL associated with the specified +# import library ARG. +# Invoked by eval'ing the libtool variable +# $sharedlib_from_linklib_cmd +# Result is available in the variable +# $sharedlib_from_linklib_result +func_cygming_dll_for_implib () +{ + $debug_cmd + + sharedlib_from_linklib_result=`$DLLTOOL --identify-strict --identify "$1"` +} + +# func_cygming_dll_for_implib_fallback_core SECTION_NAME LIBNAMEs +# +# The is the core of a fallback implementation of a +# platform-specific function to extract the name of the +# DLL associated with the specified import library LIBNAME. +# +# SECTION_NAME is either .idata$6 or .idata$7, depending +# on the platform and compiler that created the implib. +# +# Echos the name of the DLL associated with the +# specified import library. +func_cygming_dll_for_implib_fallback_core () +{ + $debug_cmd + + match_literal=`$ECHO "$1" | $SED "$sed_make_literal_regex"` + $OBJDUMP -s --section "$1" "$2" 2>/dev/null | + $SED '/^Contents of section '"$match_literal"':/{ + # Place marker at beginning of archive member dllname section + s/.*/====MARK====/ + p + d + } + # These lines can sometimes be longer than 43 characters, but + # are always uninteresting + /:[ ]*file format pe[i]\{,1\}-/d + /^In archive [^:]*:/d + # Ensure marker is printed + /^====MARK====/p + # Remove all lines with less than 43 characters + /^.\{43\}/!d + # From remaining lines, remove first 43 characters + s/^.\{43\}//' | + $SED -n ' + # Join marker and all lines until next marker into a single line + /^====MARK====/ b para + H + $ b para + b + :para + x + s/\n//g + # Remove the marker + s/^====MARK====// + # Remove trailing dots and whitespace + s/[\. \t]*$// + # Print + /./p' | + # we now have a list, one entry per line, of the stringified + # contents of the appropriate section of all members of the + # archive that possess that section. Heuristic: eliminate + # all those that have a first or second character that is + # a '.' (that is, objdump's representation of an unprintable + # character.) This should work for all archives with less than + # 0x302f exports -- but will fail for DLLs whose name actually + # begins with a literal '.' or a single character followed by + # a '.'. + # + # Of those that remain, print the first one. + $SED -e '/^\./d;/^.\./d;q' +} + +# func_cygming_dll_for_implib_fallback ARG +# Platform-specific function to extract the +# name of the DLL associated with the specified +# import library ARG. +# +# This fallback implementation is for use when $DLLTOOL +# does not support the --identify-strict option. +# Invoked by eval'ing the libtool variable +# $sharedlib_from_linklib_cmd +# Result is available in the variable +# $sharedlib_from_linklib_result +func_cygming_dll_for_implib_fallback () +{ + $debug_cmd + + if func_cygming_gnu_implib_p "$1"; then + # binutils import library + sharedlib_from_linklib_result=`func_cygming_dll_for_implib_fallback_core '.idata$7' "$1"` + elif func_cygming_ms_implib_p "$1"; then + # ms-generated import library + sharedlib_from_linklib_result=`func_cygming_dll_for_implib_fallback_core '.idata$6' "$1"` + else + # unknown + sharedlib_from_linklib_result= + fi +} + + +# func_extract_an_archive dir oldlib +func_extract_an_archive () +{ + $debug_cmd + + f_ex_an_ar_dir=$1; shift + f_ex_an_ar_oldlib=$1 + if test yes = "$lock_old_archive_extraction"; then + lockfile=$f_ex_an_ar_oldlib.lock + until $opt_dry_run || ln "$progpath" "$lockfile" 2>/dev/null; do + func_echo "Waiting for $lockfile to be removed" + sleep 2 + done + fi + func_show_eval "(cd \$f_ex_an_ar_dir && $AR x \"\$f_ex_an_ar_oldlib\")" \ + 'stat=$?; rm -f "$lockfile"; exit $stat' + if test yes = "$lock_old_archive_extraction"; then + $opt_dry_run || rm -f "$lockfile" + fi + if ($AR t "$f_ex_an_ar_oldlib" | sort | sort -uc >/dev/null 2>&1); then + : + else + func_fatal_error "object name conflicts in archive: $f_ex_an_ar_dir/$f_ex_an_ar_oldlib" + fi +} + + +# func_extract_archives gentop oldlib ... +func_extract_archives () +{ + $debug_cmd + + my_gentop=$1; shift + my_oldlibs=${1+"$@"} + my_oldobjs= + my_xlib= + my_xabs= + my_xdir= + + for my_xlib in $my_oldlibs; do + # Extract the objects. + case $my_xlib in + [\\/]* | [A-Za-z]:[\\/]*) my_xabs=$my_xlib ;; + *) my_xabs=`pwd`"/$my_xlib" ;; + esac + func_basename "$my_xlib" + my_xlib=$func_basename_result + my_xlib_u=$my_xlib + while :; do + case " $extracted_archives " in + *" $my_xlib_u "*) + func_arith $extracted_serial + 1 + extracted_serial=$func_arith_result + my_xlib_u=lt$extracted_serial-$my_xlib ;; + *) break ;; + esac + done + extracted_archives="$extracted_archives $my_xlib_u" + my_xdir=$my_gentop/$my_xlib_u + + func_mkdir_p "$my_xdir" + + case $host in + *-darwin*) + func_verbose "Extracting $my_xabs" + # Do not bother doing anything if just a dry run + $opt_dry_run || { + darwin_orig_dir=`pwd` + cd $my_xdir || exit $? + darwin_archive=$my_xabs + darwin_curdir=`pwd` + func_basename "$darwin_archive" + darwin_base_archive=$func_basename_result + darwin_arches=`$LIPO -info "$darwin_archive" 2>/dev/null | $GREP Architectures 2>/dev/null || true` + if test -n "$darwin_arches"; then + darwin_arches=`$ECHO "$darwin_arches" | $SED -e 's/.*are://'` + darwin_arch= + func_verbose "$darwin_base_archive has multiple architectures $darwin_arches" + for darwin_arch in $darwin_arches; do + func_mkdir_p "unfat-$$/$darwin_base_archive-$darwin_arch" + $LIPO -thin $darwin_arch -output "unfat-$$/$darwin_base_archive-$darwin_arch/$darwin_base_archive" "$darwin_archive" + cd "unfat-$$/$darwin_base_archive-$darwin_arch" + func_extract_an_archive "`pwd`" "$darwin_base_archive" + cd "$darwin_curdir" + $RM "unfat-$$/$darwin_base_archive-$darwin_arch/$darwin_base_archive" + done # $darwin_arches + ## Okay now we've a bunch of thin objects, gotta fatten them up :) + darwin_filelist=`find unfat-$$ -type f -name \*.o -print -o -name \*.lo -print | $SED -e "$sed_basename" | sort -u` + darwin_file= + darwin_files= + for darwin_file in $darwin_filelist; do + darwin_files=`find unfat-$$ -name $darwin_file -print | sort | $NL2SP` + $LIPO -create -output "$darwin_file" $darwin_files + done # $darwin_filelist + $RM -rf unfat-$$ + cd "$darwin_orig_dir" + else + cd $darwin_orig_dir + func_extract_an_archive "$my_xdir" "$my_xabs" + fi # $darwin_arches + } # !$opt_dry_run + ;; + *) + func_extract_an_archive "$my_xdir" "$my_xabs" + ;; + esac + my_oldobjs="$my_oldobjs "`find $my_xdir -name \*.$objext -print -o -name \*.lo -print | sort | $NL2SP` + done + + func_extract_archives_result=$my_oldobjs +} + + +# func_emit_wrapper [arg=no] +# +# Emit a libtool wrapper script on stdout. +# Don't directly open a file because we may want to +# incorporate the script contents within a cygwin/mingw +# wrapper executable. Must ONLY be called from within +# func_mode_link because it depends on a number of variables +# set therein. +# +# ARG is the value that the WRAPPER_SCRIPT_BELONGS_IN_OBJDIR +# variable will take. If 'yes', then the emitted script +# will assume that the directory where it is stored is +# the $objdir directory. This is a cygwin/mingw-specific +# behavior. +func_emit_wrapper () +{ + func_emit_wrapper_arg1=${1-no} + + $ECHO "\ +#! $SHELL + +# $output - temporary wrapper script for $objdir/$outputname +# Generated by $PROGRAM (GNU $PACKAGE) $VERSION +# +# The $output program cannot be directly executed until all the libtool +# libraries that it depends on are installed. +# +# This wrapper script should never be moved out of the build directory. +# If it is, it will not operate correctly. + +# Sed substitution that helps us do robust quoting. It backslashifies +# metacharacters that are still active within double-quoted strings. +sed_quote_subst='$sed_quote_subst' + +# Be Bourne compatible +if test -n \"\${ZSH_VERSION+set}\" && (emulate sh) >/dev/null 2>&1; then + emulate sh + NULLCMD=: + # Zsh 3.x and 4.x performs word splitting on \${1+\"\$@\"}, which + # is contrary to our usage. Disable this feature. + alias -g '\${1+\"\$@\"}'='\"\$@\"' + setopt NO_GLOB_SUBST +else + case \`(set -o) 2>/dev/null\` in *posix*) set -o posix;; esac +fi +BIN_SH=xpg4; export BIN_SH # for Tru64 +DUALCASE=1; export DUALCASE # for MKS sh + +# The HP-UX ksh and POSIX shell print the target directory to stdout +# if CDPATH is set. +(unset CDPATH) >/dev/null 2>&1 && unset CDPATH + +relink_command=\"$relink_command\" + +# This environment variable determines our operation mode. +if test \"\$libtool_install_magic\" = \"$magic\"; then + # install mode needs the following variables: + generated_by_libtool_version='$macro_version' + notinst_deplibs='$notinst_deplibs' +else + # When we are sourced in execute mode, \$file and \$ECHO are already set. + if test \"\$libtool_execute_magic\" != \"$magic\"; then + file=\"\$0\"" + + qECHO=`$ECHO "$ECHO" | $SED "$sed_quote_subst"` + $ECHO "\ + +# A function that is used when there is no print builtin or printf. +func_fallback_echo () +{ + eval 'cat <<_LTECHO_EOF +\$1 +_LTECHO_EOF' +} + ECHO=\"$qECHO\" + fi + +# Very basic option parsing. These options are (a) specific to +# the libtool wrapper, (b) are identical between the wrapper +# /script/ and the wrapper /executable/ that is used only on +# windows platforms, and (c) all begin with the string "--lt-" +# (application programs are unlikely to have options that match +# this pattern). +# +# There are only two supported options: --lt-debug and +# --lt-dump-script. There is, deliberately, no --lt-help. +# +# The first argument to this parsing function should be the +# script's $0 value, followed by "$@". +lt_option_debug= +func_parse_lt_options () +{ + lt_script_arg0=\$0 + shift + for lt_opt + do + case \"\$lt_opt\" in + --lt-debug) lt_option_debug=1 ;; + --lt-dump-script) + lt_dump_D=\`\$ECHO \"X\$lt_script_arg0\" | $SED -e 's/^X//' -e 's%/[^/]*$%%'\` + test \"X\$lt_dump_D\" = \"X\$lt_script_arg0\" && lt_dump_D=. + lt_dump_F=\`\$ECHO \"X\$lt_script_arg0\" | $SED -e 's/^X//' -e 's%^.*/%%'\` + cat \"\$lt_dump_D/\$lt_dump_F\" + exit 0 + ;; + --lt-*) + \$ECHO \"Unrecognized --lt- option: '\$lt_opt'\" 1>&2 + exit 1 + ;; + esac + done + + # Print the debug banner immediately: + if test -n \"\$lt_option_debug\"; then + echo \"$outputname:$output:\$LINENO: libtool wrapper (GNU $PACKAGE) $VERSION\" 1>&2 + fi +} + +# Used when --lt-debug. Prints its arguments to stdout +# (redirection is the responsibility of the caller) +func_lt_dump_args () +{ + lt_dump_args_N=1; + for lt_arg + do + \$ECHO \"$outputname:$output:\$LINENO: newargv[\$lt_dump_args_N]: \$lt_arg\" + lt_dump_args_N=\`expr \$lt_dump_args_N + 1\` + done +} + +# Core function for launching the target application +func_exec_program_core () +{ +" + case $host in + # Backslashes separate directories on plain windows + *-*-mingw | *-*-os2* | *-cegcc*) + $ECHO "\ + if test -n \"\$lt_option_debug\"; then + \$ECHO \"$outputname:$output:\$LINENO: newargv[0]: \$progdir\\\\\$program\" 1>&2 + func_lt_dump_args \${1+\"\$@\"} 1>&2 + fi + exec \"\$progdir\\\\\$program\" \${1+\"\$@\"} +" + ;; + + *) + $ECHO "\ + if test -n \"\$lt_option_debug\"; then + \$ECHO \"$outputname:$output:\$LINENO: newargv[0]: \$progdir/\$program\" 1>&2 + func_lt_dump_args \${1+\"\$@\"} 1>&2 + fi + exec \"\$progdir/\$program\" \${1+\"\$@\"} +" + ;; + esac + $ECHO "\ + \$ECHO \"\$0: cannot exec \$program \$*\" 1>&2 + exit 1 +} + +# A function to encapsulate launching the target application +# Strips options in the --lt-* namespace from \$@ and +# launches target application with the remaining arguments. +func_exec_program () +{ + case \" \$* \" in + *\\ --lt-*) + for lt_wr_arg + do + case \$lt_wr_arg in + --lt-*) ;; + *) set x \"\$@\" \"\$lt_wr_arg\"; shift;; + esac + shift + done ;; + esac + func_exec_program_core \${1+\"\$@\"} +} + + # Parse options + func_parse_lt_options \"\$0\" \${1+\"\$@\"} + + # Find the directory that this script lives in. + thisdir=\`\$ECHO \"\$file\" | $SED 's%/[^/]*$%%'\` + test \"x\$thisdir\" = \"x\$file\" && thisdir=. + + # Follow symbolic links until we get to the real thisdir. + file=\`ls -ld \"\$file\" | $SED -n 's/.*-> //p'\` + while test -n \"\$file\"; do + destdir=\`\$ECHO \"\$file\" | $SED 's%/[^/]*\$%%'\` + + # If there was a directory component, then change thisdir. + if test \"x\$destdir\" != \"x\$file\"; then + case \"\$destdir\" in + [\\\\/]* | [A-Za-z]:[\\\\/]*) thisdir=\"\$destdir\" ;; + *) thisdir=\"\$thisdir/\$destdir\" ;; + esac + fi + + file=\`\$ECHO \"\$file\" | $SED 's%^.*/%%'\` + file=\`ls -ld \"\$thisdir/\$file\" | $SED -n 's/.*-> //p'\` + done + + # Usually 'no', except on cygwin/mingw when embedded into + # the cwrapper. + WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=$func_emit_wrapper_arg1 + if test \"\$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR\" = \"yes\"; then + # special case for '.' + if test \"\$thisdir\" = \".\"; then + thisdir=\`pwd\` + fi + # remove .libs from thisdir + case \"\$thisdir\" in + *[\\\\/]$objdir ) thisdir=\`\$ECHO \"\$thisdir\" | $SED 's%[\\\\/][^\\\\/]*$%%'\` ;; + $objdir ) thisdir=. ;; + esac + fi + + # Try to get the absolute directory name. + absdir=\`cd \"\$thisdir\" && pwd\` + test -n \"\$absdir\" && thisdir=\"\$absdir\" +" + + if test yes = "$fast_install"; then + $ECHO "\ + program=lt-'$outputname'$exeext + progdir=\"\$thisdir/$objdir\" + + if test ! -f \"\$progdir/\$program\" || + { file=\`ls -1dt \"\$progdir/\$program\" \"\$progdir/../\$program\" 2>/dev/null | $SED 1q\`; \\ + test \"X\$file\" != \"X\$progdir/\$program\"; }; then + + file=\"\$\$-\$program\" + + if test ! -d \"\$progdir\"; then + $MKDIR \"\$progdir\" + else + $RM \"\$progdir/\$file\" + fi" + + $ECHO "\ + + # relink executable if necessary + if test -n \"\$relink_command\"; then + if relink_command_output=\`eval \$relink_command 2>&1\`; then : + else + \$ECHO \"\$relink_command_output\" >&2 + $RM \"\$progdir/\$file\" + exit 1 + fi + fi + + $MV \"\$progdir/\$file\" \"\$progdir/\$program\" 2>/dev/null || + { $RM \"\$progdir/\$program\"; + $MV \"\$progdir/\$file\" \"\$progdir/\$program\"; } + $RM \"\$progdir/\$file\" + fi" + else + $ECHO "\ + program='$outputname' + progdir=\"\$thisdir/$objdir\" +" + fi + + $ECHO "\ + + if test -f \"\$progdir/\$program\"; then" + + # fixup the dll searchpath if we need to. + # + # Fix the DLL searchpath if we need to. Do this before prepending + # to shlibpath, because on Windows, both are PATH and uninstalled + # libraries must come first. + if test -n "$dllsearchpath"; then + $ECHO "\ + # Add the dll search path components to the executable PATH + PATH=$dllsearchpath:\$PATH +" + fi + + # Export our shlibpath_var if we have one. + if test yes = "$shlibpath_overrides_runpath" && test -n "$shlibpath_var" && test -n "$temp_rpath"; then + $ECHO "\ + # Add our own library path to $shlibpath_var + $shlibpath_var=\"$temp_rpath\$$shlibpath_var\" + + # Some systems cannot cope with colon-terminated $shlibpath_var + # The second colon is a workaround for a bug in BeOS R4 sed + $shlibpath_var=\`\$ECHO \"\$$shlibpath_var\" | $SED 's/::*\$//'\` + + export $shlibpath_var +" + fi + + $ECHO "\ + if test \"\$libtool_execute_magic\" != \"$magic\"; then + # Run the actual program with our arguments. + func_exec_program \${1+\"\$@\"} + fi + else + # The program doesn't exist. + \$ECHO \"\$0: error: '\$progdir/\$program' does not exist\" 1>&2 + \$ECHO \"This script is just a wrapper for \$program.\" 1>&2 + \$ECHO \"See the $PACKAGE documentation for more information.\" 1>&2 + exit 1 + fi +fi\ +" +} + + +# func_emit_cwrapperexe_src +# emit the source code for a wrapper executable on stdout +# Must ONLY be called from within func_mode_link because +# it depends on a number of variable set therein. +func_emit_cwrapperexe_src () +{ + cat < +#include +#ifdef _MSC_VER +# include +# include +# include +#else +# include +# include +# ifdef __CYGWIN__ +# include +# endif +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#define STREQ(s1, s2) (strcmp ((s1), (s2)) == 0) + +/* declarations of non-ANSI functions */ +#if defined __MINGW32__ +# ifdef __STRICT_ANSI__ +int _putenv (const char *); +# endif +#elif defined __CYGWIN__ +# ifdef __STRICT_ANSI__ +char *realpath (const char *, char *); +int putenv (char *); +int setenv (const char *, const char *, int); +# endif +/* #elif defined other_platform || defined ... */ +#endif + +/* portability defines, excluding path handling macros */ +#if defined _MSC_VER +# define setmode _setmode +# define stat _stat +# define chmod _chmod +# define getcwd _getcwd +# define putenv _putenv +# define S_IXUSR _S_IEXEC +#elif defined __MINGW32__ +# define setmode _setmode +# define stat _stat +# define chmod _chmod +# define getcwd _getcwd +# define putenv _putenv +#elif defined __CYGWIN__ +# define HAVE_SETENV +# define FOPEN_WB "wb" +/* #elif defined other platforms ... */ +#endif + +#if defined PATH_MAX +# define LT_PATHMAX PATH_MAX +#elif defined MAXPATHLEN +# define LT_PATHMAX MAXPATHLEN +#else +# define LT_PATHMAX 1024 +#endif + +#ifndef S_IXOTH +# define S_IXOTH 0 +#endif +#ifndef S_IXGRP +# define S_IXGRP 0 +#endif + +/* path handling portability macros */ +#ifndef DIR_SEPARATOR +# define DIR_SEPARATOR '/' +# define PATH_SEPARATOR ':' +#endif + +#if defined _WIN32 || defined __MSDOS__ || defined __DJGPP__ || \ + defined __OS2__ +# define HAVE_DOS_BASED_FILE_SYSTEM +# define FOPEN_WB "wb" +# ifndef DIR_SEPARATOR_2 +# define DIR_SEPARATOR_2 '\\' +# endif +# ifndef PATH_SEPARATOR_2 +# define PATH_SEPARATOR_2 ';' +# endif +#endif + +#ifndef DIR_SEPARATOR_2 +# define IS_DIR_SEPARATOR(ch) ((ch) == DIR_SEPARATOR) +#else /* DIR_SEPARATOR_2 */ +# define IS_DIR_SEPARATOR(ch) \ + (((ch) == DIR_SEPARATOR) || ((ch) == DIR_SEPARATOR_2)) +#endif /* DIR_SEPARATOR_2 */ + +#ifndef PATH_SEPARATOR_2 +# define IS_PATH_SEPARATOR(ch) ((ch) == PATH_SEPARATOR) +#else /* PATH_SEPARATOR_2 */ +# define IS_PATH_SEPARATOR(ch) ((ch) == PATH_SEPARATOR_2) +#endif /* PATH_SEPARATOR_2 */ + +#ifndef FOPEN_WB +# define FOPEN_WB "w" +#endif +#ifndef _O_BINARY +# define _O_BINARY 0 +#endif + +#define XMALLOC(type, num) ((type *) xmalloc ((num) * sizeof(type))) +#define XFREE(stale) do { \ + if (stale) { free (stale); stale = 0; } \ +} while (0) + +#if defined LT_DEBUGWRAPPER +static int lt_debug = 1; +#else +static int lt_debug = 0; +#endif + +const char *program_name = "libtool-wrapper"; /* in case xstrdup fails */ + +void *xmalloc (size_t num); +char *xstrdup (const char *string); +const char *base_name (const char *name); +char *find_executable (const char *wrapper); +char *chase_symlinks (const char *pathspec); +int make_executable (const char *path); +int check_executable (const char *path); +char *strendzap (char *str, const char *pat); +void lt_debugprintf (const char *file, int line, const char *fmt, ...); +void lt_fatal (const char *file, int line, const char *message, ...); +static const char *nonnull (const char *s); +static const char *nonempty (const char *s); +void lt_setenv (const char *name, const char *value); +char *lt_extend_str (const char *orig_value, const char *add, int to_end); +void lt_update_exe_path (const char *name, const char *value); +void lt_update_lib_path (const char *name, const char *value); +char **prepare_spawn (char **argv); +void lt_dump_script (FILE *f); +EOF + + cat <= 0) + && (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) + return 1; + else + return 0; +} + +int +make_executable (const char *path) +{ + int rval = 0; + struct stat st; + + lt_debugprintf (__FILE__, __LINE__, "(make_executable): %s\n", + nonempty (path)); + if ((!path) || (!*path)) + return 0; + + if (stat (path, &st) >= 0) + { + rval = chmod (path, st.st_mode | S_IXOTH | S_IXGRP | S_IXUSR); + } + return rval; +} + +/* Searches for the full path of the wrapper. Returns + newly allocated full path name if found, NULL otherwise + Does not chase symlinks, even on platforms that support them. +*/ +char * +find_executable (const char *wrapper) +{ + int has_slash = 0; + const char *p; + const char *p_next; + /* static buffer for getcwd */ + char tmp[LT_PATHMAX + 1]; + size_t tmp_len; + char *concat_name; + + lt_debugprintf (__FILE__, __LINE__, "(find_executable): %s\n", + nonempty (wrapper)); + + if ((wrapper == NULL) || (*wrapper == '\0')) + return NULL; + + /* Absolute path? */ +#if defined HAVE_DOS_BASED_FILE_SYSTEM + if (isalpha ((unsigned char) wrapper[0]) && wrapper[1] == ':') + { + concat_name = xstrdup (wrapper); + if (check_executable (concat_name)) + return concat_name; + XFREE (concat_name); + } + else + { +#endif + if (IS_DIR_SEPARATOR (wrapper[0])) + { + concat_name = xstrdup (wrapper); + if (check_executable (concat_name)) + return concat_name; + XFREE (concat_name); + } +#if defined HAVE_DOS_BASED_FILE_SYSTEM + } +#endif + + for (p = wrapper; *p; p++) + if (*p == '/') + { + has_slash = 1; + break; + } + if (!has_slash) + { + /* no slashes; search PATH */ + const char *path = getenv ("PATH"); + if (path != NULL) + { + for (p = path; *p; p = p_next) + { + const char *q; + size_t p_len; + for (q = p; *q; q++) + if (IS_PATH_SEPARATOR (*q)) + break; + p_len = (size_t) (q - p); + p_next = (*q == '\0' ? q : q + 1); + if (p_len == 0) + { + /* empty path: current directory */ + if (getcwd (tmp, LT_PATHMAX) == NULL) + lt_fatal (__FILE__, __LINE__, "getcwd failed: %s", + nonnull (strerror (errno))); + tmp_len = strlen (tmp); + concat_name = + XMALLOC (char, tmp_len + 1 + strlen (wrapper) + 1); + memcpy (concat_name, tmp, tmp_len); + concat_name[tmp_len] = '/'; + strcpy (concat_name + tmp_len + 1, wrapper); + } + else + { + concat_name = + XMALLOC (char, p_len + 1 + strlen (wrapper) + 1); + memcpy (concat_name, p, p_len); + concat_name[p_len] = '/'; + strcpy (concat_name + p_len + 1, wrapper); + } + if (check_executable (concat_name)) + return concat_name; + XFREE (concat_name); + } + } + /* not found in PATH; assume curdir */ + } + /* Relative path | not found in path: prepend cwd */ + if (getcwd (tmp, LT_PATHMAX) == NULL) + lt_fatal (__FILE__, __LINE__, "getcwd failed: %s", + nonnull (strerror (errno))); + tmp_len = strlen (tmp); + concat_name = XMALLOC (char, tmp_len + 1 + strlen (wrapper) + 1); + memcpy (concat_name, tmp, tmp_len); + concat_name[tmp_len] = '/'; + strcpy (concat_name + tmp_len + 1, wrapper); + + if (check_executable (concat_name)) + return concat_name; + XFREE (concat_name); + return NULL; +} + +char * +chase_symlinks (const char *pathspec) +{ +#ifndef S_ISLNK + return xstrdup (pathspec); +#else + char buf[LT_PATHMAX]; + struct stat s; + char *tmp_pathspec = xstrdup (pathspec); + char *p; + int has_symlinks = 0; + while (strlen (tmp_pathspec) && !has_symlinks) + { + lt_debugprintf (__FILE__, __LINE__, + "checking path component for symlinks: %s\n", + tmp_pathspec); + if (lstat (tmp_pathspec, &s) == 0) + { + if (S_ISLNK (s.st_mode) != 0) + { + has_symlinks = 1; + break; + } + + /* search backwards for last DIR_SEPARATOR */ + p = tmp_pathspec + strlen (tmp_pathspec) - 1; + while ((p > tmp_pathspec) && (!IS_DIR_SEPARATOR (*p))) + p--; + if ((p == tmp_pathspec) && (!IS_DIR_SEPARATOR (*p))) + { + /* no more DIR_SEPARATORS left */ + break; + } + *p = '\0'; + } + else + { + lt_fatal (__FILE__, __LINE__, + "error accessing file \"%s\": %s", + tmp_pathspec, nonnull (strerror (errno))); + } + } + XFREE (tmp_pathspec); + + if (!has_symlinks) + { + return xstrdup (pathspec); + } + + tmp_pathspec = realpath (pathspec, buf); + if (tmp_pathspec == 0) + { + lt_fatal (__FILE__, __LINE__, + "could not follow symlinks for %s", pathspec); + } + return xstrdup (tmp_pathspec); +#endif +} + +char * +strendzap (char *str, const char *pat) +{ + size_t len, patlen; + + assert (str != NULL); + assert (pat != NULL); + + len = strlen (str); + patlen = strlen (pat); + + if (patlen <= len) + { + str += len - patlen; + if (STREQ (str, pat)) + *str = '\0'; + } + return str; +} + +void +lt_debugprintf (const char *file, int line, const char *fmt, ...) +{ + va_list args; + if (lt_debug) + { + (void) fprintf (stderr, "%s:%s:%d: ", program_name, file, line); + va_start (args, fmt); + (void) vfprintf (stderr, fmt, args); + va_end (args); + } +} + +static void +lt_error_core (int exit_status, const char *file, + int line, const char *mode, + const char *message, va_list ap) +{ + fprintf (stderr, "%s:%s:%d: %s: ", program_name, file, line, mode); + vfprintf (stderr, message, ap); + fprintf (stderr, ".\n"); + + if (exit_status >= 0) + exit (exit_status); +} + +void +lt_fatal (const char *file, int line, const char *message, ...) +{ + va_list ap; + va_start (ap, message); + lt_error_core (EXIT_FAILURE, file, line, "FATAL", message, ap); + va_end (ap); +} + +static const char * +nonnull (const char *s) +{ + return s ? s : "(null)"; +} + +static const char * +nonempty (const char *s) +{ + return (s && !*s) ? "(empty)" : nonnull (s); +} + +void +lt_setenv (const char *name, const char *value) +{ + lt_debugprintf (__FILE__, __LINE__, + "(lt_setenv) setting '%s' to '%s'\n", + nonnull (name), nonnull (value)); + { +#ifdef HAVE_SETENV + /* always make a copy, for consistency with !HAVE_SETENV */ + char *str = xstrdup (value); + setenv (name, str, 1); +#else + size_t len = strlen (name) + 1 + strlen (value) + 1; + char *str = XMALLOC (char, len); + sprintf (str, "%s=%s", name, value); + if (putenv (str) != EXIT_SUCCESS) + { + XFREE (str); + } +#endif + } +} + +char * +lt_extend_str (const char *orig_value, const char *add, int to_end) +{ + char *new_value; + if (orig_value && *orig_value) + { + size_t orig_value_len = strlen (orig_value); + size_t add_len = strlen (add); + new_value = XMALLOC (char, add_len + orig_value_len + 1); + if (to_end) + { + strcpy (new_value, orig_value); + strcpy (new_value + orig_value_len, add); + } + else + { + strcpy (new_value, add); + strcpy (new_value + add_len, orig_value); + } + } + else + { + new_value = xstrdup (add); + } + return new_value; +} + +void +lt_update_exe_path (const char *name, const char *value) +{ + lt_debugprintf (__FILE__, __LINE__, + "(lt_update_exe_path) modifying '%s' by prepending '%s'\n", + nonnull (name), nonnull (value)); + + if (name && *name && value && *value) + { + char *new_value = lt_extend_str (getenv (name), value, 0); + /* some systems can't cope with a ':'-terminated path #' */ + size_t len = strlen (new_value); + while ((len > 0) && IS_PATH_SEPARATOR (new_value[len-1])) + { + new_value[--len] = '\0'; + } + lt_setenv (name, new_value); + XFREE (new_value); + } +} + +void +lt_update_lib_path (const char *name, const char *value) +{ + lt_debugprintf (__FILE__, __LINE__, + "(lt_update_lib_path) modifying '%s' by prepending '%s'\n", + nonnull (name), nonnull (value)); + + if (name && *name && value && *value) + { + char *new_value = lt_extend_str (getenv (name), value, 0); + lt_setenv (name, new_value); + XFREE (new_value); + } +} + +EOF + case $host_os in + mingw*) + cat <<"EOF" + +/* Prepares an argument vector before calling spawn(). + Note that spawn() does not by itself call the command interpreter + (getenv ("COMSPEC") != NULL ? getenv ("COMSPEC") : + ({ OSVERSIONINFO v; v.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); + GetVersionEx(&v); + v.dwPlatformId == VER_PLATFORM_WIN32_NT; + }) ? "cmd.exe" : "command.com"). + Instead it simply concatenates the arguments, separated by ' ', and calls + CreateProcess(). We must quote the arguments since Win32 CreateProcess() + interprets characters like ' ', '\t', '\\', '"' (but not '<' and '>') in a + special way: + - Space and tab are interpreted as delimiters. They are not treated as + delimiters if they are surrounded by double quotes: "...". + - Unescaped double quotes are removed from the input. Their only effect is + that within double quotes, space and tab are treated like normal + characters. + - Backslashes not followed by double quotes are not special. + - But 2*n+1 backslashes followed by a double quote become + n backslashes followed by a double quote (n >= 0): + \" -> " + \\\" -> \" + \\\\\" -> \\" + */ +#define SHELL_SPECIAL_CHARS "\"\\ \001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037" +#define SHELL_SPACE_CHARS " \001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037" +char ** +prepare_spawn (char **argv) +{ + size_t argc; + char **new_argv; + size_t i; + + /* Count number of arguments. */ + for (argc = 0; argv[argc] != NULL; argc++) + ; + + /* Allocate new argument vector. */ + new_argv = XMALLOC (char *, argc + 1); + + /* Put quoted arguments into the new argument vector. */ + for (i = 0; i < argc; i++) + { + const char *string = argv[i]; + + if (string[0] == '\0') + new_argv[i] = xstrdup ("\"\""); + else if (strpbrk (string, SHELL_SPECIAL_CHARS) != NULL) + { + int quote_around = (strpbrk (string, SHELL_SPACE_CHARS) != NULL); + size_t length; + unsigned int backslashes; + const char *s; + char *quoted_string; + char *p; + + length = 0; + backslashes = 0; + if (quote_around) + length++; + for (s = string; *s != '\0'; s++) + { + char c = *s; + if (c == '"') + length += backslashes + 1; + length++; + if (c == '\\') + backslashes++; + else + backslashes = 0; + } + if (quote_around) + length += backslashes + 1; + + quoted_string = XMALLOC (char, length + 1); + + p = quoted_string; + backslashes = 0; + if (quote_around) + *p++ = '"'; + for (s = string; *s != '\0'; s++) + { + char c = *s; + if (c == '"') + { + unsigned int j; + for (j = backslashes + 1; j > 0; j--) + *p++ = '\\'; + } + *p++ = c; + if (c == '\\') + backslashes++; + else + backslashes = 0; + } + if (quote_around) + { + unsigned int j; + for (j = backslashes; j > 0; j--) + *p++ = '\\'; + *p++ = '"'; + } + *p = '\0'; + + new_argv[i] = quoted_string; + } + else + new_argv[i] = (char *) string; + } + new_argv[argc] = NULL; + + return new_argv; +} +EOF + ;; + esac + + cat <<"EOF" +void lt_dump_script (FILE* f) +{ +EOF + func_emit_wrapper yes | + $SED -n -e ' +s/^\(.\{79\}\)\(..*\)/\1\ +\2/ +h +s/\([\\"]\)/\\\1/g +s/$/\\n/ +s/\([^\n]*\).*/ fputs ("\1", f);/p +g +D' + cat <<"EOF" +} +EOF +} +# end: func_emit_cwrapperexe_src + +# func_win32_import_lib_p ARG +# True if ARG is an import lib, as indicated by $file_magic_cmd +func_win32_import_lib_p () +{ + $debug_cmd + + case `eval $file_magic_cmd \"\$1\" 2>/dev/null | $SED -e 10q` in + *import*) : ;; + *) false ;; + esac +} + +# func_suncc_cstd_abi +# !!ONLY CALL THIS FOR SUN CC AFTER $compile_command IS FULLY EXPANDED!! +# Several compiler flags select an ABI that is incompatible with the +# Cstd library. Avoid specifying it if any are in CXXFLAGS. +func_suncc_cstd_abi () +{ + $debug_cmd + + case " $compile_command " in + *" -compat=g "*|*\ -std=c++[0-9][0-9]\ *|*" -library=stdcxx4 "*|*" -library=stlport4 "*) + suncc_use_cstd_abi=no + ;; + *) + suncc_use_cstd_abi=yes + ;; + esac +} + +# func_mode_link arg... +func_mode_link () +{ + $debug_cmd + + case $host in + *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2* | *-cegcc*) + # It is impossible to link a dll without this setting, and + # we shouldn't force the makefile maintainer to figure out + # what system we are compiling for in order to pass an extra + # flag for every libtool invocation. + # allow_undefined=no + + # FIXME: Unfortunately, there are problems with the above when trying + # to make a dll that has undefined symbols, in which case not + # even a static library is built. For now, we need to specify + # -no-undefined on the libtool link line when we can be certain + # that all symbols are satisfied, otherwise we get a static library. + allow_undefined=yes + ;; + *) + allow_undefined=yes + ;; + esac + libtool_args=$nonopt + base_compile="$nonopt $@" + compile_command=$nonopt + finalize_command=$nonopt + + compile_rpath= + finalize_rpath= + compile_shlibpath= + finalize_shlibpath= + convenience= + old_convenience= + deplibs= + old_deplibs= + compiler_flags= + linker_flags= + dllsearchpath= + lib_search_path=`pwd` + inst_prefix_dir= + new_inherited_linker_flags= + + avoid_version=no + bindir= + dlfiles= + dlprefiles= + dlself=no + export_dynamic=no + export_symbols= + export_symbols_regex= + generated= + libobjs= + ltlibs= + module=no + no_install=no + objs= + os2dllname= + non_pic_objects= + precious_files_regex= + prefer_static_libs=no + preload=false + prev= + prevarg= + release= + rpath= + xrpath= + perm_rpath= + temp_rpath= + thread_safe=no + vinfo= + vinfo_number=no + weak_libs= + single_module=$wl-single_module + func_infer_tag $base_compile + + # We need to know -static, to get the right output filenames. + for arg + do + case $arg in + -shared) + test yes != "$build_libtool_libs" \ + && func_fatal_configuration "cannot build a shared library" + build_old_libs=no + break + ;; + -all-static | -static | -static-libtool-libs) + case $arg in + -all-static) + if test yes = "$build_libtool_libs" && test -z "$link_static_flag"; then + func_warning "complete static linking is impossible in this configuration" + fi + if test -n "$link_static_flag"; then + dlopen_self=$dlopen_self_static + fi + prefer_static_libs=yes + ;; + -static) + if test -z "$pic_flag" && test -n "$link_static_flag"; then + dlopen_self=$dlopen_self_static + fi + prefer_static_libs=built + ;; + -static-libtool-libs) + if test -z "$pic_flag" && test -n "$link_static_flag"; then + dlopen_self=$dlopen_self_static + fi + prefer_static_libs=yes + ;; + esac + build_libtool_libs=no + build_old_libs=yes + break + ;; + esac + done + + # See if our shared archives depend on static archives. + test -n "$old_archive_from_new_cmds" && build_old_libs=yes + + # Go through the arguments, transforming them on the way. + while test "$#" -gt 0; do + arg=$1 + shift + func_quote_for_eval "$arg" + qarg=$func_quote_for_eval_unquoted_result + func_append libtool_args " $func_quote_for_eval_result" + + # If the previous option needs an argument, assign it. + if test -n "$prev"; then + case $prev in + output) + func_append compile_command " @OUTPUT@" + func_append finalize_command " @OUTPUT@" + ;; + esac + + case $prev in + bindir) + bindir=$arg + prev= + continue + ;; + dlfiles|dlprefiles) + $preload || { + # Add the symbol object into the linking commands. + func_append compile_command " @SYMFILE@" + func_append finalize_command " @SYMFILE@" + preload=: + } + case $arg in + *.la | *.lo) ;; # We handle these cases below. + force) + if test no = "$dlself"; then + dlself=needless + export_dynamic=yes + fi + prev= + continue + ;; + self) + if test dlprefiles = "$prev"; then + dlself=yes + elif test dlfiles = "$prev" && test yes != "$dlopen_self"; then + dlself=yes + else + dlself=needless + export_dynamic=yes + fi + prev= + continue + ;; + *) + if test dlfiles = "$prev"; then + func_append dlfiles " $arg" + else + func_append dlprefiles " $arg" + fi + prev= + continue + ;; + esac + ;; + expsyms) + export_symbols=$arg + test -f "$arg" \ + || func_fatal_error "symbol file '$arg' does not exist" + prev= + continue + ;; + expsyms_regex) + export_symbols_regex=$arg + prev= + continue + ;; + framework) + case $host in + *-*-darwin*) + case "$deplibs " in + *" $qarg.ltframework "*) ;; + *) func_append deplibs " $qarg.ltframework" # this is fixed later + ;; + esac + ;; + esac + prev= + continue + ;; + inst_prefix) + inst_prefix_dir=$arg + prev= + continue + ;; + mllvm) + # Clang does not use LLVM to link, so we can simply discard any + # '-mllvm $arg' options when doing the link step. + prev= + continue + ;; + objectlist) + if test -f "$arg"; then + save_arg=$arg + moreargs= + for fil in `cat "$save_arg"` + do +# func_append moreargs " $fil" + arg=$fil + # A libtool-controlled object. + + # Check to see that this really is a libtool object. + if func_lalib_unsafe_p "$arg"; then + pic_object= + non_pic_object= + + # Read the .lo file + func_source "$arg" + + if test -z "$pic_object" || + test -z "$non_pic_object" || + test none = "$pic_object" && + test none = "$non_pic_object"; then + func_fatal_error "cannot find name of object for '$arg'" + fi + + # Extract subdirectory from the argument. + func_dirname "$arg" "/" "" + xdir=$func_dirname_result + + if test none != "$pic_object"; then + # Prepend the subdirectory the object is found in. + pic_object=$xdir$pic_object + + if test dlfiles = "$prev"; then + if test yes = "$build_libtool_libs" && test yes = "$dlopen_support"; then + func_append dlfiles " $pic_object" + prev= + continue + else + # If libtool objects are unsupported, then we need to preload. + prev=dlprefiles + fi + fi + + # CHECK ME: I think I busted this. -Ossama + if test dlprefiles = "$prev"; then + # Preload the old-style object. + func_append dlprefiles " $pic_object" + prev= + fi + + # A PIC object. + func_append libobjs " $pic_object" + arg=$pic_object + fi + + # Non-PIC object. + if test none != "$non_pic_object"; then + # Prepend the subdirectory the object is found in. + non_pic_object=$xdir$non_pic_object + + # A standard non-PIC object + func_append non_pic_objects " $non_pic_object" + if test -z "$pic_object" || test none = "$pic_object"; then + arg=$non_pic_object + fi + else + # If the PIC object exists, use it instead. + # $xdir was prepended to $pic_object above. + non_pic_object=$pic_object + func_append non_pic_objects " $non_pic_object" + fi + else + # Only an error if not doing a dry-run. + if $opt_dry_run; then + # Extract subdirectory from the argument. + func_dirname "$arg" "/" "" + xdir=$func_dirname_result + + func_lo2o "$arg" + pic_object=$xdir$objdir/$func_lo2o_result + non_pic_object=$xdir$func_lo2o_result + func_append libobjs " $pic_object" + func_append non_pic_objects " $non_pic_object" + else + func_fatal_error "'$arg' is not a valid libtool object" + fi + fi + done + else + func_fatal_error "link input file '$arg' does not exist" + fi + arg=$save_arg + prev= + continue + ;; + os2dllname) + os2dllname=$arg + prev= + continue + ;; + precious_regex) + precious_files_regex=$arg + prev= + continue + ;; + release) + release=-$arg + prev= + continue + ;; + rpath | xrpath) + # We need an absolute path. + case $arg in + [\\/]* | [A-Za-z]:[\\/]*) ;; + *) + func_fatal_error "only absolute run-paths are allowed" + ;; + esac + if test rpath = "$prev"; then + case "$rpath " in + *" $arg "*) ;; + *) func_append rpath " $arg" ;; + esac + else + case "$xrpath " in + *" $arg "*) ;; + *) func_append xrpath " $arg" ;; + esac + fi + prev= + continue + ;; + shrext) + shrext_cmds=$arg + prev= + continue + ;; + weak) + func_append weak_libs " $arg" + prev= + continue + ;; + xcclinker) + func_append linker_flags " $qarg" + func_append compiler_flags " $qarg" + prev= + func_append compile_command " $qarg" + func_append finalize_command " $qarg" + continue + ;; + xcompiler) + func_append compiler_flags " $qarg" + prev= + func_append compile_command " $qarg" + func_append finalize_command " $qarg" + continue + ;; + xlinker) + func_append linker_flags " $qarg" + func_append compiler_flags " $wl$qarg" + prev= + func_append compile_command " $wl$qarg" + func_append finalize_command " $wl$qarg" + continue + ;; + *) + eval "$prev=\"\$arg\"" + prev= + continue + ;; + esac + fi # test -n "$prev" + + prevarg=$arg + + case $arg in + -all-static) + if test -n "$link_static_flag"; then + # See comment for -static flag below, for more details. + func_append compile_command " $link_static_flag" + func_append finalize_command " $link_static_flag" + fi + continue + ;; + + -allow-undefined) + # FIXME: remove this flag sometime in the future. + func_fatal_error "'-allow-undefined' must not be used because it is the default" + ;; + + -avoid-version) + avoid_version=yes + continue + ;; + + -bindir) + prev=bindir + continue + ;; + + -dlopen) + prev=dlfiles + continue + ;; + + -dlpreopen) + prev=dlprefiles + continue + ;; + + -export-dynamic) + export_dynamic=yes + continue + ;; + + -export-symbols | -export-symbols-regex) + if test -n "$export_symbols" || test -n "$export_symbols_regex"; then + func_fatal_error "more than one -exported-symbols argument is not allowed" + fi + if test X-export-symbols = "X$arg"; then + prev=expsyms + else + prev=expsyms_regex + fi + continue + ;; + + -framework) + prev=framework + continue + ;; + + -inst-prefix-dir) + prev=inst_prefix + continue + ;; + + # The native IRIX linker understands -LANG:*, -LIST:* and -LNO:* + # so, if we see these flags be careful not to treat them like -L + -L[A-Z][A-Z]*:*) + case $with_gcc/$host in + no/*-*-irix* | /*-*-irix*) + func_append compile_command " $arg" + func_append finalize_command " $arg" + ;; + esac + continue + ;; + + -L*) + func_stripname "-L" '' "$arg" + if test -z "$func_stripname_result"; then + if test "$#" -gt 0; then + func_fatal_error "require no space between '-L' and '$1'" + else + func_fatal_error "need path for '-L' option" + fi + fi + func_resolve_sysroot "$func_stripname_result" + dir=$func_resolve_sysroot_result + # We need an absolute path. + case $dir in + [\\/]* | [A-Za-z]:[\\/]*) ;; + *) + absdir=`cd "$dir" && pwd` + test -z "$absdir" && \ + func_fatal_error "cannot determine absolute directory name of '$dir'" + dir=$absdir + ;; + esac + case "$deplibs " in + *" -L$dir "* | *" $arg "*) + # Will only happen for absolute or sysroot arguments + ;; + *) + # Preserve sysroot, but never include relative directories + case $dir in + [\\/]* | [A-Za-z]:[\\/]* | =*) func_append deplibs " $arg" ;; + *) func_append deplibs " -L$dir" ;; + esac + func_append lib_search_path " $dir" + ;; + esac + case $host in + *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2* | *-cegcc*) + testbindir=`$ECHO "$dir" | $SED 's*/lib$*/bin*'` + case :$dllsearchpath: in + *":$dir:"*) ;; + ::) dllsearchpath=$dir;; + *) func_append dllsearchpath ":$dir";; + esac + case :$dllsearchpath: in + *":$testbindir:"*) ;; + ::) dllsearchpath=$testbindir;; + *) func_append dllsearchpath ":$testbindir";; + esac + ;; + esac + continue + ;; + + -l*) + if test X-lc = "X$arg" || test X-lm = "X$arg"; then + case $host in + *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-beos* | *-cegcc* | *-*-haiku*) + # These systems don't actually have a C or math library (as such) + continue + ;; + *-*-os2*) + # These systems don't actually have a C library (as such) + test X-lc = "X$arg" && continue + ;; + *-*-openbsd* | *-*-freebsd* | *-*-dragonfly* | *-*-bitrig*) + # Do not include libc due to us having libc/libc_r. + test X-lc = "X$arg" && continue + ;; + *-*-rhapsody* | *-*-darwin1.[012]) + # Rhapsody C and math libraries are in the System framework + func_append deplibs " System.ltframework" + continue + ;; + *-*-sco3.2v5* | *-*-sco5v6*) + # Causes problems with __ctype + test X-lc = "X$arg" && continue + ;; + *-*-sysv4.2uw2* | *-*-sysv5* | *-*-unixware* | *-*-OpenUNIX*) + # Compiler inserts libc in the correct place for threads to work + test X-lc = "X$arg" && continue + ;; + esac + elif test X-lc_r = "X$arg"; then + case $host in + *-*-openbsd* | *-*-freebsd* | *-*-dragonfly* | *-*-bitrig*) + # Do not include libc_r directly, use -pthread flag. + continue + ;; + esac + fi + func_append deplibs " $arg" + continue + ;; + + -mllvm) + prev=mllvm + continue + ;; + + -module) + module=yes + continue + ;; + + # Tru64 UNIX uses -model [arg] to determine the layout of C++ + # classes, name mangling, and exception handling. + # Darwin uses the -arch flag to determine output architecture. + -model|-arch|-isysroot|--sysroot) + func_append compiler_flags " $arg" + func_append compile_command " $arg" + func_append finalize_command " $arg" + prev=xcompiler + continue + ;; + + -mt|-mthreads|-kthread|-Kthread|-pthread|-pthreads|--thread-safe \ + |-threads|-fopenmp|-openmp|-mp|-xopenmp|-omp|-qsmp=*) + func_append compiler_flags " $arg" + func_append compile_command " $arg" + func_append finalize_command " $arg" + case "$new_inherited_linker_flags " in + *" $arg "*) ;; + * ) func_append new_inherited_linker_flags " $arg" ;; + esac + continue + ;; + + -multi_module) + single_module=$wl-multi_module + continue + ;; + + -no-fast-install) + fast_install=no + continue + ;; + + -no-install) + case $host in + *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2* | *-*-darwin* | *-cegcc*) + # The PATH hackery in wrapper scripts is required on Windows + # and Darwin in order for the loader to find any dlls it needs. + func_warning "'-no-install' is ignored for $host" + func_warning "assuming '-no-fast-install' instead" + fast_install=no + ;; + *) no_install=yes ;; + esac + continue + ;; + + -no-undefined) + allow_undefined=no + continue + ;; + + -objectlist) + prev=objectlist + continue + ;; + + -os2dllname) + prev=os2dllname + continue + ;; + + -o) prev=output ;; + + -precious-files-regex) + prev=precious_regex + continue + ;; + + -release) + prev=release + continue + ;; + + -rpath) + prev=rpath + continue + ;; + + -R) + prev=xrpath + continue + ;; + + -R*) + func_stripname '-R' '' "$arg" + dir=$func_stripname_result + # We need an absolute path. + case $dir in + [\\/]* | [A-Za-z]:[\\/]*) ;; + =*) + func_stripname '=' '' "$dir" + dir=$lt_sysroot$func_stripname_result + ;; + *) + func_fatal_error "only absolute run-paths are allowed" + ;; + esac + case "$xrpath " in + *" $dir "*) ;; + *) func_append xrpath " $dir" ;; + esac + continue + ;; + + -shared) + # The effects of -shared are defined in a previous loop. + continue + ;; + + -shrext) + prev=shrext + continue + ;; + + -static | -static-libtool-libs) + # The effects of -static are defined in a previous loop. + # We used to do the same as -all-static on platforms that + # didn't have a PIC flag, but the assumption that the effects + # would be equivalent was wrong. It would break on at least + # Digital Unix and AIX. + continue + ;; + + -thread-safe) + thread_safe=yes + continue + ;; + + -version-info) + prev=vinfo + continue + ;; + + -version-number) + prev=vinfo + vinfo_number=yes + continue + ;; + + -weak) + prev=weak + continue + ;; + + -Wc,*) + func_stripname '-Wc,' '' "$arg" + args=$func_stripname_result + arg= + save_ifs=$IFS; IFS=, + for flag in $args; do + IFS=$save_ifs + func_quote_for_eval "$flag" + func_append arg " $func_quote_for_eval_result" + func_append compiler_flags " $func_quote_for_eval_result" + done + IFS=$save_ifs + func_stripname ' ' '' "$arg" + arg=$func_stripname_result + ;; + + -Wl,*) + func_stripname '-Wl,' '' "$arg" + args=$func_stripname_result + arg= + save_ifs=$IFS; IFS=, + for flag in $args; do + IFS=$save_ifs + func_quote_for_eval "$flag" + func_append arg " $wl$func_quote_for_eval_result" + func_append compiler_flags " $wl$func_quote_for_eval_result" + func_append linker_flags " $func_quote_for_eval_result" + done + IFS=$save_ifs + func_stripname ' ' '' "$arg" + arg=$func_stripname_result + ;; + + -Xcompiler) + prev=xcompiler + continue + ;; + + -Xlinker) + prev=xlinker + continue + ;; + + -XCClinker) + prev=xcclinker + continue + ;; + + # -msg_* for osf cc + -msg_*) + func_quote_for_eval "$arg" + arg=$func_quote_for_eval_result + ;; + + # Flags to be passed through unchanged, with rationale: + # -64, -mips[0-9] enable 64-bit mode for the SGI compiler + # -r[0-9][0-9]* specify processor for the SGI compiler + # -xarch=*, -xtarget=* enable 64-bit mode for the Sun compiler + # +DA*, +DD* enable 64-bit mode for the HP compiler + # -q* compiler args for the IBM compiler + # -m*, -t[45]*, -txscale* architecture-specific flags for GCC + # -F/path path to uninstalled frameworks, gcc on darwin + # -p, -pg, --coverage, -fprofile-* profiling flags for GCC + # -fstack-protector* stack protector flags for GCC + # @file GCC response files + # -tp=* Portland pgcc target processor selection + # --sysroot=* for sysroot support + # -O*, -g*, -flto*, -fwhopr*, -fuse-linker-plugin GCC link-time optimization + # -specs=* GCC specs files + # -stdlib=* select c++ std lib with clang + # -fsanitize=* Clang/GCC memory and address sanitizer + # -fuse-ld=* Linker select flags for GCC + -64|-mips[0-9]|-r[0-9][0-9]*|-xarch=*|-xtarget=*|+DA*|+DD*|-q*|-m*| \ + -t[45]*|-txscale*|-p|-pg|--coverage|-fprofile-*|-F*|@*|-tp=*|--sysroot=*| \ + -O*|-g*|-flto*|-fwhopr*|-fuse-linker-plugin|-fstack-protector*|-stdlib=*| \ + -specs=*|-fsanitize=*|-fuse-ld=*) + func_quote_for_eval "$arg" + arg=$func_quote_for_eval_result + func_append compile_command " $arg" + func_append finalize_command " $arg" + func_append compiler_flags " $arg" + continue + ;; + + -Z*) + if test os2 = "`expr $host : '.*\(os2\)'`"; then + # OS/2 uses -Zxxx to specify OS/2-specific options + compiler_flags="$compiler_flags $arg" + func_append compile_command " $arg" + func_append finalize_command " $arg" + case $arg in + -Zlinker | -Zstack) + prev=xcompiler + ;; + esac + continue + else + # Otherwise treat like 'Some other compiler flag' below + func_quote_for_eval "$arg" + arg=$func_quote_for_eval_result + fi + ;; + + # Some other compiler flag. + -* | +*) + func_quote_for_eval "$arg" + arg=$func_quote_for_eval_result + ;; + + *.$objext) + # A standard object. + func_append objs " $arg" + ;; + + *.lo) + # A libtool-controlled object. + + # Check to see that this really is a libtool object. + if func_lalib_unsafe_p "$arg"; then + pic_object= + non_pic_object= + + # Read the .lo file + func_source "$arg" + + if test -z "$pic_object" || + test -z "$non_pic_object" || + test none = "$pic_object" && + test none = "$non_pic_object"; then + func_fatal_error "cannot find name of object for '$arg'" + fi + + # Extract subdirectory from the argument. + func_dirname "$arg" "/" "" + xdir=$func_dirname_result + + test none = "$pic_object" || { + # Prepend the subdirectory the object is found in. + pic_object=$xdir$pic_object + + if test dlfiles = "$prev"; then + if test yes = "$build_libtool_libs" && test yes = "$dlopen_support"; then + func_append dlfiles " $pic_object" + prev= + continue + else + # If libtool objects are unsupported, then we need to preload. + prev=dlprefiles + fi + fi + + # CHECK ME: I think I busted this. -Ossama + if test dlprefiles = "$prev"; then + # Preload the old-style object. + func_append dlprefiles " $pic_object" + prev= + fi + + # A PIC object. + func_append libobjs " $pic_object" + arg=$pic_object + } + + # Non-PIC object. + if test none != "$non_pic_object"; then + # Prepend the subdirectory the object is found in. + non_pic_object=$xdir$non_pic_object + + # A standard non-PIC object + func_append non_pic_objects " $non_pic_object" + if test -z "$pic_object" || test none = "$pic_object"; then + arg=$non_pic_object + fi + else + # If the PIC object exists, use it instead. + # $xdir was prepended to $pic_object above. + non_pic_object=$pic_object + func_append non_pic_objects " $non_pic_object" + fi + else + # Only an error if not doing a dry-run. + if $opt_dry_run; then + # Extract subdirectory from the argument. + func_dirname "$arg" "/" "" + xdir=$func_dirname_result + + func_lo2o "$arg" + pic_object=$xdir$objdir/$func_lo2o_result + non_pic_object=$xdir$func_lo2o_result + func_append libobjs " $pic_object" + func_append non_pic_objects " $non_pic_object" + else + func_fatal_error "'$arg' is not a valid libtool object" + fi + fi + ;; + + *.$libext) + # An archive. + func_append deplibs " $arg" + func_append old_deplibs " $arg" + continue + ;; + + *.la) + # A libtool-controlled library. + + func_resolve_sysroot "$arg" + if test dlfiles = "$prev"; then + # This library was specified with -dlopen. + func_append dlfiles " $func_resolve_sysroot_result" + prev= + elif test dlprefiles = "$prev"; then + # The library was specified with -dlpreopen. + func_append dlprefiles " $func_resolve_sysroot_result" + prev= + else + func_append deplibs " $func_resolve_sysroot_result" + fi + continue + ;; + + # Some other compiler argument. + *) + # Unknown arguments in both finalize_command and compile_command need + # to be aesthetically quoted because they are evaled later. + func_quote_for_eval "$arg" + arg=$func_quote_for_eval_result + ;; + esac # arg + + # Now actually substitute the argument into the commands. + if test -n "$arg"; then + func_append compile_command " $arg" + func_append finalize_command " $arg" + fi + done # argument parsing loop + + test -n "$prev" && \ + func_fatal_help "the '$prevarg' option requires an argument" + + if test yes = "$export_dynamic" && test -n "$export_dynamic_flag_spec"; then + eval arg=\"$export_dynamic_flag_spec\" + func_append compile_command " $arg" + func_append finalize_command " $arg" + fi + + oldlibs= + # calculate the name of the file, without its directory + func_basename "$output" + outputname=$func_basename_result + libobjs_save=$libobjs + + if test -n "$shlibpath_var"; then + # get the directories listed in $shlibpath_var + eval shlib_search_path=\`\$ECHO \"\$$shlibpath_var\" \| \$SED \'s/:/ /g\'\` + else + shlib_search_path= + fi + eval sys_lib_search_path=\"$sys_lib_search_path_spec\" + eval sys_lib_dlsearch_path=\"$sys_lib_dlsearch_path_spec\" + + # Definition is injected by LT_CONFIG during libtool generation. + func_munge_path_list sys_lib_dlsearch_path "$LT_SYS_LIBRARY_PATH" + + func_dirname "$output" "/" "" + output_objdir=$func_dirname_result$objdir + func_to_tool_file "$output_objdir/" + tool_output_objdir=$func_to_tool_file_result + # Create the object directory. + func_mkdir_p "$output_objdir" + + # Determine the type of output + case $output in + "") + func_fatal_help "you must specify an output file" + ;; + *.$libext) linkmode=oldlib ;; + *.lo | *.$objext) linkmode=obj ;; + *.la) linkmode=lib ;; + *) linkmode=prog ;; # Anything else should be a program. + esac + + specialdeplibs= + + libs= + # Find all interdependent deplibs by searching for libraries + # that are linked more than once (e.g. -la -lb -la) + for deplib in $deplibs; do + if $opt_preserve_dup_deps; then + case "$libs " in + *" $deplib "*) func_append specialdeplibs " $deplib" ;; + esac + fi + func_append libs " $deplib" + done + + if test lib = "$linkmode"; then + libs="$predeps $libs $compiler_lib_search_path $postdeps" + + # Compute libraries that are listed more than once in $predeps + # $postdeps and mark them as special (i.e., whose duplicates are + # not to be eliminated). + pre_post_deps= + if $opt_duplicate_compiler_generated_deps; then + for pre_post_dep in $predeps $postdeps; do + case "$pre_post_deps " in + *" $pre_post_dep "*) func_append specialdeplibs " $pre_post_deps" ;; + esac + func_append pre_post_deps " $pre_post_dep" + done + fi + pre_post_deps= + fi + + deplibs= + newdependency_libs= + newlib_search_path= + need_relink=no # whether we're linking any uninstalled libtool libraries + notinst_deplibs= # not-installed libtool libraries + notinst_path= # paths that contain not-installed libtool libraries + + case $linkmode in + lib) + passes="conv dlpreopen link" + for file in $dlfiles $dlprefiles; do + case $file in + *.la) ;; + *) + func_fatal_help "libraries can '-dlopen' only libtool libraries: $file" + ;; + esac + done + ;; + prog) + compile_deplibs= + finalize_deplibs= + alldeplibs=false + newdlfiles= + newdlprefiles= + passes="conv scan dlopen dlpreopen link" + ;; + *) passes="conv" + ;; + esac + + for pass in $passes; do + # The preopen pass in lib mode reverses $deplibs; put it back here + # so that -L comes before libs that need it for instance... + if test lib,link = "$linkmode,$pass"; then + ## FIXME: Find the place where the list is rebuilt in the wrong + ## order, and fix it there properly + tmp_deplibs= + for deplib in $deplibs; do + tmp_deplibs="$deplib $tmp_deplibs" + done + deplibs=$tmp_deplibs + fi + + if test lib,link = "$linkmode,$pass" || + test prog,scan = "$linkmode,$pass"; then + libs=$deplibs + deplibs= + fi + if test prog = "$linkmode"; then + case $pass in + dlopen) libs=$dlfiles ;; + dlpreopen) libs=$dlprefiles ;; + link) + libs="$deplibs %DEPLIBS%" + test "X$link_all_deplibs" != Xno && libs="$libs $dependency_libs" + ;; + esac + fi + if test lib,dlpreopen = "$linkmode,$pass"; then + # Collect and forward deplibs of preopened libtool libs + for lib in $dlprefiles; do + # Ignore non-libtool-libs + dependency_libs= + func_resolve_sysroot "$lib" + case $lib in + *.la) func_source "$func_resolve_sysroot_result" ;; + esac + + # Collect preopened libtool deplibs, except any this library + # has declared as weak libs + for deplib in $dependency_libs; do + func_basename "$deplib" + deplib_base=$func_basename_result + case " $weak_libs " in + *" $deplib_base "*) ;; + *) func_append deplibs " $deplib" ;; + esac + done + done + libs=$dlprefiles + fi + if test dlopen = "$pass"; then + # Collect dlpreopened libraries + save_deplibs=$deplibs + deplibs= + fi + + for deplib in $libs; do + lib= + found=false + case $deplib in + -mt|-mthreads|-kthread|-Kthread|-pthread|-pthreads|--thread-safe \ + |-threads|-fopenmp|-openmp|-mp|-xopenmp|-omp|-qsmp=*) + if test prog,link = "$linkmode,$pass"; then + compile_deplibs="$deplib $compile_deplibs" + finalize_deplibs="$deplib $finalize_deplibs" + else + func_append compiler_flags " $deplib" + if test lib = "$linkmode"; then + case "$new_inherited_linker_flags " in + *" $deplib "*) ;; + * ) func_append new_inherited_linker_flags " $deplib" ;; + esac + fi + fi + continue + ;; + -l*) + if test lib != "$linkmode" && test prog != "$linkmode"; then + func_warning "'-l' is ignored for archives/objects" + continue + fi + func_stripname '-l' '' "$deplib" + name=$func_stripname_result + if test lib = "$linkmode"; then + searchdirs="$newlib_search_path $lib_search_path $compiler_lib_search_dirs $sys_lib_search_path $shlib_search_path" + else + searchdirs="$newlib_search_path $lib_search_path $sys_lib_search_path $shlib_search_path" + fi + for searchdir in $searchdirs; do + for search_ext in .la $std_shrext .so .a; do + # Search the libtool library + lib=$searchdir/lib$name$search_ext + if test -f "$lib"; then + if test .la = "$search_ext"; then + found=: + else + found=false + fi + break 2 + fi + done + done + if $found; then + # deplib is a libtool library + # If $allow_libtool_libs_with_static_runtimes && $deplib is a stdlib, + # We need to do some special things here, and not later. + if test yes = "$allow_libtool_libs_with_static_runtimes"; then + case " $predeps $postdeps " in + *" $deplib "*) + if func_lalib_p "$lib"; then + library_names= + old_library= + func_source "$lib" + for l in $old_library $library_names; do + ll=$l + done + if test "X$ll" = "X$old_library"; then # only static version available + found=false + func_dirname "$lib" "" "." + ladir=$func_dirname_result + lib=$ladir/$old_library + if test prog,link = "$linkmode,$pass"; then + compile_deplibs="$deplib $compile_deplibs" + finalize_deplibs="$deplib $finalize_deplibs" + else + deplibs="$deplib $deplibs" + test lib = "$linkmode" && newdependency_libs="$deplib $newdependency_libs" + fi + continue + fi + fi + ;; + *) ;; + esac + fi + else + # deplib doesn't seem to be a libtool library + if test prog,link = "$linkmode,$pass"; then + compile_deplibs="$deplib $compile_deplibs" + finalize_deplibs="$deplib $finalize_deplibs" + else + deplibs="$deplib $deplibs" + test lib = "$linkmode" && newdependency_libs="$deplib $newdependency_libs" + fi + continue + fi + ;; # -l + *.ltframework) + if test prog,link = "$linkmode,$pass"; then + compile_deplibs="$deplib $compile_deplibs" + finalize_deplibs="$deplib $finalize_deplibs" + else + deplibs="$deplib $deplibs" + if test lib = "$linkmode"; then + case "$new_inherited_linker_flags " in + *" $deplib "*) ;; + * ) func_append new_inherited_linker_flags " $deplib" ;; + esac + fi + fi + continue + ;; + -L*) + case $linkmode in + lib) + deplibs="$deplib $deplibs" + test conv = "$pass" && continue + newdependency_libs="$deplib $newdependency_libs" + func_stripname '-L' '' "$deplib" + func_resolve_sysroot "$func_stripname_result" + func_append newlib_search_path " $func_resolve_sysroot_result" + ;; + prog) + if test conv = "$pass"; then + deplibs="$deplib $deplibs" + continue + fi + if test scan = "$pass"; then + deplibs="$deplib $deplibs" + else + compile_deplibs="$deplib $compile_deplibs" + finalize_deplibs="$deplib $finalize_deplibs" + fi + func_stripname '-L' '' "$deplib" + func_resolve_sysroot "$func_stripname_result" + func_append newlib_search_path " $func_resolve_sysroot_result" + ;; + *) + func_warning "'-L' is ignored for archives/objects" + ;; + esac # linkmode + continue + ;; # -L + -R*) + if test link = "$pass"; then + func_stripname '-R' '' "$deplib" + func_resolve_sysroot "$func_stripname_result" + dir=$func_resolve_sysroot_result + # Make sure the xrpath contains only unique directories. + case "$xrpath " in + *" $dir "*) ;; + *) func_append xrpath " $dir" ;; + esac + fi + deplibs="$deplib $deplibs" + continue + ;; + *.la) + func_resolve_sysroot "$deplib" + lib=$func_resolve_sysroot_result + ;; + *.$libext) + if test conv = "$pass"; then + deplibs="$deplib $deplibs" + continue + fi + case $linkmode in + lib) + # Linking convenience modules into shared libraries is allowed, + # but linking other static libraries is non-portable. + case " $dlpreconveniencelibs " in + *" $deplib "*) ;; + *) + valid_a_lib=false + case $deplibs_check_method in + match_pattern*) + set dummy $deplibs_check_method; shift + match_pattern_regex=`expr "$deplibs_check_method" : "$1 \(.*\)"` + if eval "\$ECHO \"$deplib\"" 2>/dev/null | $SED 10q \ + | $EGREP "$match_pattern_regex" > /dev/null; then + valid_a_lib=: + fi + ;; + pass_all) + valid_a_lib=: + ;; + esac + if $valid_a_lib; then + echo + $ECHO "*** Warning: Linking the shared library $output against the" + $ECHO "*** static library $deplib is not portable!" + deplibs="$deplib $deplibs" + else + echo + $ECHO "*** Warning: Trying to link with static lib archive $deplib." + echo "*** I have the capability to make that library automatically link in when" + echo "*** you link to this library. But I can only do this if you have a" + echo "*** shared version of the library, which you do not appear to have" + echo "*** because the file extensions .$libext of this argument makes me believe" + echo "*** that it is just a static archive that I should not use here." + fi + ;; + esac + continue + ;; + prog) + if test link != "$pass"; then + deplibs="$deplib $deplibs" + else + compile_deplibs="$deplib $compile_deplibs" + finalize_deplibs="$deplib $finalize_deplibs" + fi + continue + ;; + esac # linkmode + ;; # *.$libext + *.lo | *.$objext) + if test conv = "$pass"; then + deplibs="$deplib $deplibs" + elif test prog = "$linkmode"; then + if test dlpreopen = "$pass" || test yes != "$dlopen_support" || test no = "$build_libtool_libs"; then + # If there is no dlopen support or we're linking statically, + # we need to preload. + func_append newdlprefiles " $deplib" + compile_deplibs="$deplib $compile_deplibs" + finalize_deplibs="$deplib $finalize_deplibs" + else + func_append newdlfiles " $deplib" + fi + fi + continue + ;; + %DEPLIBS%) + alldeplibs=: + continue + ;; + esac # case $deplib + + $found || test -f "$lib" \ + || func_fatal_error "cannot find the library '$lib' or unhandled argument '$deplib'" + + # Check to see that this really is a libtool archive. + func_lalib_unsafe_p "$lib" \ + || func_fatal_error "'$lib' is not a valid libtool archive" + + func_dirname "$lib" "" "." + ladir=$func_dirname_result + + dlname= + dlopen= + dlpreopen= + libdir= + library_names= + old_library= + inherited_linker_flags= + # If the library was installed with an old release of libtool, + # it will not redefine variables installed, or shouldnotlink + installed=yes + shouldnotlink=no + avoidtemprpath= + + + # Read the .la file + func_source "$lib" + + # Convert "-framework foo" to "foo.ltframework" + if test -n "$inherited_linker_flags"; then + tmp_inherited_linker_flags=`$ECHO "$inherited_linker_flags" | $SED 's/-framework \([^ $]*\)/\1.ltframework/g'` + for tmp_inherited_linker_flag in $tmp_inherited_linker_flags; do + case " $new_inherited_linker_flags " in + *" $tmp_inherited_linker_flag "*) ;; + *) func_append new_inherited_linker_flags " $tmp_inherited_linker_flag";; + esac + done + fi + dependency_libs=`$ECHO " $dependency_libs" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'` + if test lib,link = "$linkmode,$pass" || + test prog,scan = "$linkmode,$pass" || + { test prog != "$linkmode" && test lib != "$linkmode"; }; then + test -n "$dlopen" && func_append dlfiles " $dlopen" + test -n "$dlpreopen" && func_append dlprefiles " $dlpreopen" + fi + + if test conv = "$pass"; then + # Only check for convenience libraries + deplibs="$lib $deplibs" + if test -z "$libdir"; then + if test -z "$old_library"; then + func_fatal_error "cannot find name of link library for '$lib'" + fi + # It is a libtool convenience library, so add in its objects. + func_append convenience " $ladir/$objdir/$old_library" + func_append old_convenience " $ladir/$objdir/$old_library" + tmp_libs= + for deplib in $dependency_libs; do + deplibs="$deplib $deplibs" + if $opt_preserve_dup_deps; then + case "$tmp_libs " in + *" $deplib "*) func_append specialdeplibs " $deplib" ;; + esac + fi + func_append tmp_libs " $deplib" + done + elif test prog != "$linkmode" && test lib != "$linkmode"; then + func_fatal_error "'$lib' is not a convenience library" + fi + continue + fi # $pass = conv + + + # Get the name of the library we link against. + linklib= + if test -n "$old_library" && + { test yes = "$prefer_static_libs" || + test built,no = "$prefer_static_libs,$installed"; }; then + linklib=$old_library + else + for l in $old_library $library_names; do + linklib=$l + done + fi + if test -z "$linklib"; then + func_fatal_error "cannot find name of link library for '$lib'" + fi + + # This library was specified with -dlopen. + if test dlopen = "$pass"; then + test -z "$libdir" \ + && func_fatal_error "cannot -dlopen a convenience library: '$lib'" + if test -z "$dlname" || + test yes != "$dlopen_support" || + test no = "$build_libtool_libs" + then + # If there is no dlname, no dlopen support or we're linking + # statically, we need to preload. We also need to preload any + # dependent libraries so libltdl's deplib preloader doesn't + # bomb out in the load deplibs phase. + func_append dlprefiles " $lib $dependency_libs" + else + func_append newdlfiles " $lib" + fi + continue + fi # $pass = dlopen + + # We need an absolute path. + case $ladir in + [\\/]* | [A-Za-z]:[\\/]*) abs_ladir=$ladir ;; + *) + abs_ladir=`cd "$ladir" && pwd` + if test -z "$abs_ladir"; then + func_warning "cannot determine absolute directory name of '$ladir'" + func_warning "passing it literally to the linker, although it might fail" + abs_ladir=$ladir + fi + ;; + esac + func_basename "$lib" + laname=$func_basename_result + + # Find the relevant object directory and library name. + if test yes = "$installed"; then + if test ! -f "$lt_sysroot$libdir/$linklib" && test -f "$abs_ladir/$linklib"; then + func_warning "library '$lib' was moved." + dir=$ladir + absdir=$abs_ladir + libdir=$abs_ladir + else + dir=$lt_sysroot$libdir + absdir=$lt_sysroot$libdir + fi + test yes = "$hardcode_automatic" && avoidtemprpath=yes + else + if test ! -f "$ladir/$objdir/$linklib" && test -f "$abs_ladir/$linklib"; then + dir=$ladir + absdir=$abs_ladir + # Remove this search path later + func_append notinst_path " $abs_ladir" + else + dir=$ladir/$objdir + absdir=$abs_ladir/$objdir + # Remove this search path later + func_append notinst_path " $abs_ladir" + fi + fi # $installed = yes + func_stripname 'lib' '.la' "$laname" + name=$func_stripname_result + + # This library was specified with -dlpreopen. + if test dlpreopen = "$pass"; then + if test -z "$libdir" && test prog = "$linkmode"; then + func_fatal_error "only libraries may -dlpreopen a convenience library: '$lib'" + fi + case $host in + # special handling for platforms with PE-DLLs. + *cygwin* | *mingw* | *cegcc* ) + # Linker will automatically link against shared library if both + # static and shared are present. Therefore, ensure we extract + # symbols from the import library if a shared library is present + # (otherwise, the dlopen module name will be incorrect). We do + # this by putting the import library name into $newdlprefiles. + # We recover the dlopen module name by 'saving' the la file + # name in a special purpose variable, and (later) extracting the + # dlname from the la file. + if test -n "$dlname"; then + func_tr_sh "$dir/$linklib" + eval "libfile_$func_tr_sh_result=\$abs_ladir/\$laname" + func_append newdlprefiles " $dir/$linklib" + else + func_append newdlprefiles " $dir/$old_library" + # Keep a list of preopened convenience libraries to check + # that they are being used correctly in the link pass. + test -z "$libdir" && \ + func_append dlpreconveniencelibs " $dir/$old_library" + fi + ;; + * ) + # Prefer using a static library (so that no silly _DYNAMIC symbols + # are required to link). + if test -n "$old_library"; then + func_append newdlprefiles " $dir/$old_library" + # Keep a list of preopened convenience libraries to check + # that they are being used correctly in the link pass. + test -z "$libdir" && \ + func_append dlpreconveniencelibs " $dir/$old_library" + # Otherwise, use the dlname, so that lt_dlopen finds it. + elif test -n "$dlname"; then + func_append newdlprefiles " $dir/$dlname" + else + func_append newdlprefiles " $dir/$linklib" + fi + ;; + esac + fi # $pass = dlpreopen + + if test -z "$libdir"; then + # Link the convenience library + if test lib = "$linkmode"; then + deplibs="$dir/$old_library $deplibs" + elif test prog,link = "$linkmode,$pass"; then + compile_deplibs="$dir/$old_library $compile_deplibs" + finalize_deplibs="$dir/$old_library $finalize_deplibs" + else + deplibs="$lib $deplibs" # used for prog,scan pass + fi + continue + fi + + + if test prog = "$linkmode" && test link != "$pass"; then + func_append newlib_search_path " $ladir" + deplibs="$lib $deplibs" + + linkalldeplibs=false + if test no != "$link_all_deplibs" || test -z "$library_names" || + test no = "$build_libtool_libs"; then + linkalldeplibs=: + fi + + tmp_libs= + for deplib in $dependency_libs; do + case $deplib in + -L*) func_stripname '-L' '' "$deplib" + func_resolve_sysroot "$func_stripname_result" + func_append newlib_search_path " $func_resolve_sysroot_result" + ;; + esac + # Need to link against all dependency_libs? + if $linkalldeplibs; then + deplibs="$deplib $deplibs" + else + # Need to hardcode shared library paths + # or/and link against static libraries + newdependency_libs="$deplib $newdependency_libs" + fi + if $opt_preserve_dup_deps; then + case "$tmp_libs " in + *" $deplib "*) func_append specialdeplibs " $deplib" ;; + esac + fi + func_append tmp_libs " $deplib" + done # for deplib + continue + fi # $linkmode = prog... + + if test prog,link = "$linkmode,$pass"; then + if test -n "$library_names" && + { { test no = "$prefer_static_libs" || + test built,yes = "$prefer_static_libs,$installed"; } || + test -z "$old_library"; }; then + # We need to hardcode the library path + if test -n "$shlibpath_var" && test -z "$avoidtemprpath"; then + # Make sure the rpath contains only unique directories. + case $temp_rpath: in + *"$absdir:"*) ;; + *) func_append temp_rpath "$absdir:" ;; + esac + fi + + # Hardcode the library path. + # Skip directories that are in the system default run-time + # search path. + case " $sys_lib_dlsearch_path " in + *" $absdir "*) ;; + *) + case "$compile_rpath " in + *" $absdir "*) ;; + *) func_append compile_rpath " $absdir" ;; + esac + ;; + esac + case " $sys_lib_dlsearch_path " in + *" $libdir "*) ;; + *) + case "$finalize_rpath " in + *" $libdir "*) ;; + *) func_append finalize_rpath " $libdir" ;; + esac + ;; + esac + fi # $linkmode,$pass = prog,link... + + if $alldeplibs && + { test pass_all = "$deplibs_check_method" || + { test yes = "$build_libtool_libs" && + test -n "$library_names"; }; }; then + # We only need to search for static libraries + continue + fi + fi + + link_static=no # Whether the deplib will be linked statically + use_static_libs=$prefer_static_libs + if test built = "$use_static_libs" && test yes = "$installed"; then + use_static_libs=no + fi + if test -n "$library_names" && + { test no = "$use_static_libs" || test -z "$old_library"; }; then + case $host in + *cygwin* | *mingw* | *cegcc* | *os2*) + # No point in relinking DLLs because paths are not encoded + func_append notinst_deplibs " $lib" + need_relink=no + ;; + *) + if test no = "$installed"; then + func_append notinst_deplibs " $lib" + need_relink=yes + fi + ;; + esac + # This is a shared library + + # Warn about portability, can't link against -module's on some + # systems (darwin). Don't bleat about dlopened modules though! + dlopenmodule= + for dlpremoduletest in $dlprefiles; do + if test "X$dlpremoduletest" = "X$lib"; then + dlopenmodule=$dlpremoduletest + break + fi + done + if test -z "$dlopenmodule" && test yes = "$shouldnotlink" && test link = "$pass"; then + echo + if test prog = "$linkmode"; then + $ECHO "*** Warning: Linking the executable $output against the loadable module" + else + $ECHO "*** Warning: Linking the shared library $output against the loadable module" + fi + $ECHO "*** $linklib is not portable!" + fi + if test lib = "$linkmode" && + test yes = "$hardcode_into_libs"; then + # Hardcode the library path. + # Skip directories that are in the system default run-time + # search path. + case " $sys_lib_dlsearch_path " in + *" $absdir "*) ;; + *) + case "$compile_rpath " in + *" $absdir "*) ;; + *) func_append compile_rpath " $absdir" ;; + esac + ;; + esac + case " $sys_lib_dlsearch_path " in + *" $libdir "*) ;; + *) + case "$finalize_rpath " in + *" $libdir "*) ;; + *) func_append finalize_rpath " $libdir" ;; + esac + ;; + esac + fi + + if test -n "$old_archive_from_expsyms_cmds"; then + # figure out the soname + set dummy $library_names + shift + realname=$1 + shift + libname=`eval "\\$ECHO \"$libname_spec\""` + # use dlname if we got it. it's perfectly good, no? + if test -n "$dlname"; then + soname=$dlname + elif test -n "$soname_spec"; then + # bleh windows + case $host in + *cygwin* | mingw* | *cegcc* | *os2*) + func_arith $current - $age + major=$func_arith_result + versuffix=-$major + ;; + esac + eval soname=\"$soname_spec\" + else + soname=$realname + fi + + # Make a new name for the extract_expsyms_cmds to use + soroot=$soname + func_basename "$soroot" + soname=$func_basename_result + func_stripname 'lib' '.dll' "$soname" + newlib=libimp-$func_stripname_result.a + + # If the library has no export list, then create one now + if test -f "$output_objdir/$soname-def"; then : + else + func_verbose "extracting exported symbol list from '$soname'" + func_execute_cmds "$extract_expsyms_cmds" 'exit $?' + fi + + # Create $newlib + if test -f "$output_objdir/$newlib"; then :; else + func_verbose "generating import library for '$soname'" + func_execute_cmds "$old_archive_from_expsyms_cmds" 'exit $?' + fi + # make sure the library variables are pointing to the new library + dir=$output_objdir + linklib=$newlib + fi # test -n "$old_archive_from_expsyms_cmds" + + if test prog = "$linkmode" || test relink != "$opt_mode"; then + add_shlibpath= + add_dir= + add= + lib_linked=yes + case $hardcode_action in + immediate | unsupported) + if test no = "$hardcode_direct"; then + add=$dir/$linklib + case $host in + *-*-sco3.2v5.0.[024]*) add_dir=-L$dir ;; + *-*-sysv4*uw2*) add_dir=-L$dir ;; + *-*-sysv5OpenUNIX* | *-*-sysv5UnixWare7.[01].[10]* | \ + *-*-unixware7*) add_dir=-L$dir ;; + *-*-darwin* ) + # if the lib is a (non-dlopened) module then we cannot + # link against it, someone is ignoring the earlier warnings + if /usr/bin/file -L $add 2> /dev/null | + $GREP ": [^:]* bundle" >/dev/null; then + if test "X$dlopenmodule" != "X$lib"; then + $ECHO "*** Warning: lib $linklib is a module, not a shared library" + if test -z "$old_library"; then + echo + echo "*** And there doesn't seem to be a static archive available" + echo "*** The link will probably fail, sorry" + else + add=$dir/$old_library + fi + elif test -n "$old_library"; then + add=$dir/$old_library + fi + fi + esac + elif test no = "$hardcode_minus_L"; then + case $host in + *-*-sunos*) add_shlibpath=$dir ;; + esac + add_dir=-L$dir + add=-l$name + elif test no = "$hardcode_shlibpath_var"; then + add_shlibpath=$dir + add=-l$name + else + lib_linked=no + fi + ;; + relink) + if test yes = "$hardcode_direct" && + test no = "$hardcode_direct_absolute"; then + add=$dir/$linklib + elif test yes = "$hardcode_minus_L"; then + add_dir=-L$absdir + # Try looking first in the location we're being installed to. + if test -n "$inst_prefix_dir"; then + case $libdir in + [\\/]*) + func_append add_dir " -L$inst_prefix_dir$libdir" + ;; + esac + fi + add=-l$name + elif test yes = "$hardcode_shlibpath_var"; then + add_shlibpath=$dir + add=-l$name + else + lib_linked=no + fi + ;; + *) lib_linked=no ;; + esac + + if test yes != "$lib_linked"; then + func_fatal_configuration "unsupported hardcode properties" + fi + + if test -n "$add_shlibpath"; then + case :$compile_shlibpath: in + *":$add_shlibpath:"*) ;; + *) func_append compile_shlibpath "$add_shlibpath:" ;; + esac + fi + if test prog = "$linkmode"; then + test -n "$add_dir" && compile_deplibs="$add_dir $compile_deplibs" + test -n "$add" && compile_deplibs="$add $compile_deplibs" + else + test -n "$add_dir" && deplibs="$add_dir $deplibs" + test -n "$add" && deplibs="$add $deplibs" + if test yes != "$hardcode_direct" && + test yes != "$hardcode_minus_L" && + test yes = "$hardcode_shlibpath_var"; then + case :$finalize_shlibpath: in + *":$libdir:"*) ;; + *) func_append finalize_shlibpath "$libdir:" ;; + esac + fi + fi + fi + + if test prog = "$linkmode" || test relink = "$opt_mode"; then + add_shlibpath= + add_dir= + add= + # Finalize command for both is simple: just hardcode it. + if test yes = "$hardcode_direct" && + test no = "$hardcode_direct_absolute"; then + add=$libdir/$linklib + elif test yes = "$hardcode_minus_L"; then + add_dir=-L$libdir + add=-l$name + elif test yes = "$hardcode_shlibpath_var"; then + case :$finalize_shlibpath: in + *":$libdir:"*) ;; + *) func_append finalize_shlibpath "$libdir:" ;; + esac + add=-l$name + elif test yes = "$hardcode_automatic"; then + if test -n "$inst_prefix_dir" && + test -f "$inst_prefix_dir$libdir/$linklib"; then + add=$inst_prefix_dir$libdir/$linklib + else + add=$libdir/$linklib + fi + else + # We cannot seem to hardcode it, guess we'll fake it. + add_dir=-L$libdir + # Try looking first in the location we're being installed to. + if test -n "$inst_prefix_dir"; then + case $libdir in + [\\/]*) + func_append add_dir " -L$inst_prefix_dir$libdir" + ;; + esac + fi + add=-l$name + fi + + if test prog = "$linkmode"; then + test -n "$add_dir" && finalize_deplibs="$add_dir $finalize_deplibs" + test -n "$add" && finalize_deplibs="$add $finalize_deplibs" + else + test -n "$add_dir" && deplibs="$add_dir $deplibs" + test -n "$add" && deplibs="$add $deplibs" + fi + fi + elif test prog = "$linkmode"; then + # Here we assume that one of hardcode_direct or hardcode_minus_L + # is not unsupported. This is valid on all known static and + # shared platforms. + if test unsupported != "$hardcode_direct"; then + test -n "$old_library" && linklib=$old_library + compile_deplibs="$dir/$linklib $compile_deplibs" + finalize_deplibs="$dir/$linklib $finalize_deplibs" + else + compile_deplibs="-l$name -L$dir $compile_deplibs" + finalize_deplibs="-l$name -L$dir $finalize_deplibs" + fi + elif test yes = "$build_libtool_libs"; then + # Not a shared library + if test pass_all != "$deplibs_check_method"; then + # We're trying link a shared library against a static one + # but the system doesn't support it. + + # Just print a warning and add the library to dependency_libs so + # that the program can be linked against the static library. + echo + $ECHO "*** Warning: This system cannot link to static lib archive $lib." + echo "*** I have the capability to make that library automatically link in when" + echo "*** you link to this library. But I can only do this if you have a" + echo "*** shared version of the library, which you do not appear to have." + if test yes = "$module"; then + echo "*** But as you try to build a module library, libtool will still create " + echo "*** a static module, that should work as long as the dlopening application" + echo "*** is linked with the -dlopen flag to resolve symbols at runtime." + if test -z "$global_symbol_pipe"; then + echo + echo "*** However, this would only work if libtool was able to extract symbol" + echo "*** lists from a program, using 'nm' or equivalent, but libtool could" + echo "*** not find such a program. So, this module is probably useless." + echo "*** 'nm' from GNU binutils and a full rebuild may help." + fi + if test no = "$build_old_libs"; then + build_libtool_libs=module + build_old_libs=yes + else + build_libtool_libs=no + fi + fi + else + deplibs="$dir/$old_library $deplibs" + link_static=yes + fi + fi # link shared/static library? + + if test lib = "$linkmode"; then + if test -n "$dependency_libs" && + { test yes != "$hardcode_into_libs" || + test yes = "$build_old_libs" || + test yes = "$link_static"; }; then + # Extract -R from dependency_libs + temp_deplibs= + for libdir in $dependency_libs; do + case $libdir in + -R*) func_stripname '-R' '' "$libdir" + temp_xrpath=$func_stripname_result + case " $xrpath " in + *" $temp_xrpath "*) ;; + *) func_append xrpath " $temp_xrpath";; + esac;; + *) func_append temp_deplibs " $libdir";; + esac + done + dependency_libs=$temp_deplibs + fi + + func_append newlib_search_path " $absdir" + # Link against this library + test no = "$link_static" && newdependency_libs="$abs_ladir/$laname $newdependency_libs" + # ... and its dependency_libs + tmp_libs= + for deplib in $dependency_libs; do + newdependency_libs="$deplib $newdependency_libs" + case $deplib in + -L*) func_stripname '-L' '' "$deplib" + func_resolve_sysroot "$func_stripname_result";; + *) func_resolve_sysroot "$deplib" ;; + esac + if $opt_preserve_dup_deps; then + case "$tmp_libs " in + *" $func_resolve_sysroot_result "*) + func_append specialdeplibs " $func_resolve_sysroot_result" ;; + esac + fi + func_append tmp_libs " $func_resolve_sysroot_result" + done + + if test no != "$link_all_deplibs"; then + # Add the search paths of all dependency libraries + for deplib in $dependency_libs; do + path= + case $deplib in + -L*) path=$deplib ;; + *.la) + func_resolve_sysroot "$deplib" + deplib=$func_resolve_sysroot_result + func_dirname "$deplib" "" "." + dir=$func_dirname_result + # We need an absolute path. + case $dir in + [\\/]* | [A-Za-z]:[\\/]*) absdir=$dir ;; + *) + absdir=`cd "$dir" && pwd` + if test -z "$absdir"; then + func_warning "cannot determine absolute directory name of '$dir'" + absdir=$dir + fi + ;; + esac + if $GREP "^installed=no" $deplib > /dev/null; then + case $host in + *-*-darwin*) + depdepl= + eval deplibrary_names=`$SED -n -e 's/^library_names=\(.*\)$/\1/p' $deplib` + if test -n "$deplibrary_names"; then + for tmp in $deplibrary_names; do + depdepl=$tmp + done + if test -f "$absdir/$objdir/$depdepl"; then + depdepl=$absdir/$objdir/$depdepl + darwin_install_name=`$OTOOL -L $depdepl | awk '{if (NR == 2) {print $1;exit}}'` + if test -z "$darwin_install_name"; then + darwin_install_name=`$OTOOL64 -L $depdepl | awk '{if (NR == 2) {print $1;exit}}'` + fi + func_append compiler_flags " $wl-dylib_file $wl$darwin_install_name:$depdepl" + func_append linker_flags " -dylib_file $darwin_install_name:$depdepl" + path= + fi + fi + ;; + *) + path=-L$absdir/$objdir + ;; + esac + else + eval libdir=`$SED -n -e 's/^libdir=\(.*\)$/\1/p' $deplib` + test -z "$libdir" && \ + func_fatal_error "'$deplib' is not a valid libtool archive" + test "$absdir" != "$libdir" && \ + func_warning "'$deplib' seems to be moved" + + path=-L$absdir + fi + ;; + esac + case " $deplibs " in + *" $path "*) ;; + *) deplibs="$path $deplibs" ;; + esac + done + fi # link_all_deplibs != no + fi # linkmode = lib + done # for deplib in $libs + if test link = "$pass"; then + if test prog = "$linkmode"; then + compile_deplibs="$new_inherited_linker_flags $compile_deplibs" + finalize_deplibs="$new_inherited_linker_flags $finalize_deplibs" + else + compiler_flags="$compiler_flags "`$ECHO " $new_inherited_linker_flags" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'` + fi + fi + dependency_libs=$newdependency_libs + if test dlpreopen = "$pass"; then + # Link the dlpreopened libraries before other libraries + for deplib in $save_deplibs; do + deplibs="$deplib $deplibs" + done + fi + if test dlopen != "$pass"; then + test conv = "$pass" || { + # Make sure lib_search_path contains only unique directories. + lib_search_path= + for dir in $newlib_search_path; do + case "$lib_search_path " in + *" $dir "*) ;; + *) func_append lib_search_path " $dir" ;; + esac + done + newlib_search_path= + } + + if test prog,link = "$linkmode,$pass"; then + vars="compile_deplibs finalize_deplibs" + else + vars=deplibs + fi + for var in $vars dependency_libs; do + # Add libraries to $var in reverse order + eval tmp_libs=\"\$$var\" + new_libs= + for deplib in $tmp_libs; do + # FIXME: Pedantically, this is the right thing to do, so + # that some nasty dependency loop isn't accidentally + # broken: + #new_libs="$deplib $new_libs" + # Pragmatically, this seems to cause very few problems in + # practice: + case $deplib in + -L*) new_libs="$deplib $new_libs" ;; + -R*) ;; + *) + # And here is the reason: when a library appears more + # than once as an explicit dependence of a library, or + # is implicitly linked in more than once by the + # compiler, it is considered special, and multiple + # occurrences thereof are not removed. Compare this + # with having the same library being listed as a + # dependency of multiple other libraries: in this case, + # we know (pedantically, we assume) the library does not + # need to be listed more than once, so we keep only the + # last copy. This is not always right, but it is rare + # enough that we require users that really mean to play + # such unportable linking tricks to link the library + # using -Wl,-lname, so that libtool does not consider it + # for duplicate removal. + case " $specialdeplibs " in + *" $deplib "*) new_libs="$deplib $new_libs" ;; + *) + case " $new_libs " in + *" $deplib "*) ;; + *) new_libs="$deplib $new_libs" ;; + esac + ;; + esac + ;; + esac + done + tmp_libs= + for deplib in $new_libs; do + case $deplib in + -L*) + case " $tmp_libs " in + *" $deplib "*) ;; + *) func_append tmp_libs " $deplib" ;; + esac + ;; + *) func_append tmp_libs " $deplib" ;; + esac + done + eval $var=\"$tmp_libs\" + done # for var + fi + + # Add Sun CC postdeps if required: + test CXX = "$tagname" && { + case $host_os in + linux*) + case `$CC -V 2>&1 | sed 5q` in + *Sun\ C*) # Sun C++ 5.9 + func_suncc_cstd_abi + + if test no != "$suncc_use_cstd_abi"; then + func_append postdeps ' -library=Cstd -library=Crun' + fi + ;; + esac + ;; + + solaris*) + func_cc_basename "$CC" + case $func_cc_basename_result in + CC* | sunCC*) + func_suncc_cstd_abi + + if test no != "$suncc_use_cstd_abi"; then + func_append postdeps ' -library=Cstd -library=Crun' + fi + ;; + esac + ;; + esac + } + + # Last step: remove runtime libs from dependency_libs + # (they stay in deplibs) + tmp_libs= + for i in $dependency_libs; do + case " $predeps $postdeps $compiler_lib_search_path " in + *" $i "*) + i= + ;; + esac + if test -n "$i"; then + func_append tmp_libs " $i" + fi + done + dependency_libs=$tmp_libs + done # for pass + if test prog = "$linkmode"; then + dlfiles=$newdlfiles + fi + if test prog = "$linkmode" || test lib = "$linkmode"; then + dlprefiles=$newdlprefiles + fi + + case $linkmode in + oldlib) + if test -n "$dlfiles$dlprefiles" || test no != "$dlself"; then + func_warning "'-dlopen' is ignored for archives" + fi + + case " $deplibs" in + *\ -l* | *\ -L*) + func_warning "'-l' and '-L' are ignored for archives" ;; + esac + + test -n "$rpath" && \ + func_warning "'-rpath' is ignored for archives" + + test -n "$xrpath" && \ + func_warning "'-R' is ignored for archives" + + test -n "$vinfo" && \ + func_warning "'-version-info/-version-number' is ignored for archives" + + test -n "$release" && \ + func_warning "'-release' is ignored for archives" + + test -n "$export_symbols$export_symbols_regex" && \ + func_warning "'-export-symbols' is ignored for archives" + + # Now set the variables for building old libraries. + build_libtool_libs=no + oldlibs=$output + func_append objs "$old_deplibs" + ;; + + lib) + # Make sure we only generate libraries of the form 'libNAME.la'. + case $outputname in + lib*) + func_stripname 'lib' '.la' "$outputname" + name=$func_stripname_result + eval shared_ext=\"$shrext_cmds\" + eval libname=\"$libname_spec\" + ;; + *) + test no = "$module" \ + && func_fatal_help "libtool library '$output' must begin with 'lib'" + + if test no != "$need_lib_prefix"; then + # Add the "lib" prefix for modules if required + func_stripname '' '.la' "$outputname" + name=$func_stripname_result + eval shared_ext=\"$shrext_cmds\" + eval libname=\"$libname_spec\" + else + func_stripname '' '.la' "$outputname" + libname=$func_stripname_result + fi + ;; + esac + + if test -n "$objs"; then + if test pass_all != "$deplibs_check_method"; then + func_fatal_error "cannot build libtool library '$output' from non-libtool objects on this host:$objs" + else + echo + $ECHO "*** Warning: Linking the shared library $output against the non-libtool" + $ECHO "*** objects $objs is not portable!" + func_append libobjs " $objs" + fi + fi + + test no = "$dlself" \ + || func_warning "'-dlopen self' is ignored for libtool libraries" + + set dummy $rpath + shift + test 1 -lt "$#" \ + && func_warning "ignoring multiple '-rpath's for a libtool library" + + install_libdir=$1 + + oldlibs= + if test -z "$rpath"; then + if test yes = "$build_libtool_libs"; then + # Building a libtool convenience library. + # Some compilers have problems with a '.al' extension so + # convenience libraries should have the same extension an + # archive normally would. + oldlibs="$output_objdir/$libname.$libext $oldlibs" + build_libtool_libs=convenience + build_old_libs=yes + fi + + test -n "$vinfo" && \ + func_warning "'-version-info/-version-number' is ignored for convenience libraries" + + test -n "$release" && \ + func_warning "'-release' is ignored for convenience libraries" + else + + # Parse the version information argument. + save_ifs=$IFS; IFS=: + set dummy $vinfo 0 0 0 + shift + IFS=$save_ifs + + test -n "$7" && \ + func_fatal_help "too many parameters to '-version-info'" + + # convert absolute version numbers to libtool ages + # this retains compatibility with .la files and attempts + # to make the code below a bit more comprehensible + + case $vinfo_number in + yes) + number_major=$1 + number_minor=$2 + number_revision=$3 + # + # There are really only two kinds -- those that + # use the current revision as the major version + # and those that subtract age and use age as + # a minor version. But, then there is irix + # that has an extra 1 added just for fun + # + case $version_type in + # correct linux to gnu/linux during the next big refactor + darwin|freebsd-elf|linux|osf|windows|none) + func_arith $number_major + $number_minor + current=$func_arith_result + age=$number_minor + revision=$number_revision + ;; + freebsd-aout|qnx|sunos) + current=$number_major + revision=$number_minor + age=0 + ;; + irix|nonstopux) + func_arith $number_major + $number_minor + current=$func_arith_result + age=$number_minor + revision=$number_minor + lt_irix_increment=no + ;; + *) + func_fatal_configuration "$modename: unknown library version type '$version_type'" + ;; + esac + ;; + no) + current=$1 + revision=$2 + age=$3 + ;; + esac + + # Check that each of the things are valid numbers. + case $current in + 0|[1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-9][0-9][0-9][0-9]|[1-9][0-9][0-9][0-9][0-9]) ;; + *) + func_error "CURRENT '$current' must be a nonnegative integer" + func_fatal_error "'$vinfo' is not valid version information" + ;; + esac + + case $revision in + 0|[1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-9][0-9][0-9][0-9]|[1-9][0-9][0-9][0-9][0-9]) ;; + *) + func_error "REVISION '$revision' must be a nonnegative integer" + func_fatal_error "'$vinfo' is not valid version information" + ;; + esac + + case $age in + 0|[1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-9][0-9][0-9][0-9]|[1-9][0-9][0-9][0-9][0-9]) ;; + *) + func_error "AGE '$age' must be a nonnegative integer" + func_fatal_error "'$vinfo' is not valid version information" + ;; + esac + + if test "$age" -gt "$current"; then + func_error "AGE '$age' is greater than the current interface number '$current'" + func_fatal_error "'$vinfo' is not valid version information" + fi + + # Calculate the version variables. + major= + versuffix= + verstring= + case $version_type in + none) ;; + + darwin) + # Like Linux, but with the current version available in + # verstring for coding it into the library header + func_arith $current - $age + major=.$func_arith_result + versuffix=$major.$age.$revision + # Darwin ld doesn't like 0 for these options... + func_arith $current + 1 + minor_current=$func_arith_result + xlcverstring="$wl-compatibility_version $wl$minor_current $wl-current_version $wl$minor_current.$revision" + verstring="-compatibility_version $minor_current -current_version $minor_current.$revision" + # On Darwin other compilers + case $CC in + nagfor*) + verstring="$wl-compatibility_version $wl$minor_current $wl-current_version $wl$minor_current.$revision" + ;; + *) + verstring="-compatibility_version $minor_current -current_version $minor_current.$revision" + ;; + esac + ;; + + freebsd-aout) + major=.$current + versuffix=.$current.$revision + ;; + + freebsd-elf) + func_arith $current - $age + major=.$func_arith_result + versuffix=$major.$age.$revision + ;; + + irix | nonstopux) + if test no = "$lt_irix_increment"; then + func_arith $current - $age + else + func_arith $current - $age + 1 + fi + major=$func_arith_result + + case $version_type in + nonstopux) verstring_prefix=nonstopux ;; + *) verstring_prefix=sgi ;; + esac + verstring=$verstring_prefix$major.$revision + + # Add in all the interfaces that we are compatible with. + loop=$revision + while test 0 -ne "$loop"; do + func_arith $revision - $loop + iface=$func_arith_result + func_arith $loop - 1 + loop=$func_arith_result + verstring=$verstring_prefix$major.$iface:$verstring + done + + # Before this point, $major must not contain '.'. + major=.$major + versuffix=$major.$revision + ;; + + linux) # correct to gnu/linux during the next big refactor + func_arith $current - $age + major=.$func_arith_result + versuffix=$major.$age.$revision + ;; + + osf) + func_arith $current - $age + major=.$func_arith_result + versuffix=.$current.$age.$revision + verstring=$current.$age.$revision + + # Add in all the interfaces that we are compatible with. + loop=$age + while test 0 -ne "$loop"; do + func_arith $current - $loop + iface=$func_arith_result + func_arith $loop - 1 + loop=$func_arith_result + verstring=$verstring:$iface.0 + done + + # Make executables depend on our current version. + func_append verstring ":$current.0" + ;; + + qnx) + major=.$current + versuffix=.$current + ;; + + sco) + major=.$current + versuffix=.$current + ;; + + sunos) + major=.$current + versuffix=.$current.$revision + ;; + + windows) + # Use '-' rather than '.', since we only want one + # extension on DOS 8.3 file systems. + func_arith $current - $age + major=$func_arith_result + versuffix=-$major + ;; + + *) + func_fatal_configuration "unknown library version type '$version_type'" + ;; + esac + + # Clear the version info if we defaulted, and they specified a release. + if test -z "$vinfo" && test -n "$release"; then + major= + case $version_type in + darwin) + # we can't check for "0.0" in archive_cmds due to quoting + # problems, so we reset it completely + verstring= + ;; + *) + verstring=0.0 + ;; + esac + if test no = "$need_version"; then + versuffix= + else + versuffix=.0.0 + fi + fi + + # Remove version info from name if versioning should be avoided + if test yes,no = "$avoid_version,$need_version"; then + major= + versuffix= + verstring= + fi + + # Check to see if the archive will have undefined symbols. + if test yes = "$allow_undefined"; then + if test unsupported = "$allow_undefined_flag"; then + if test yes = "$build_old_libs"; then + func_warning "undefined symbols not allowed in $host shared libraries; building static only" + build_libtool_libs=no + else + func_fatal_error "can't build $host shared library unless -no-undefined is specified" + fi + fi + else + # Don't allow undefined symbols. + allow_undefined_flag=$no_undefined_flag + fi + + fi + + func_generate_dlsyms "$libname" "$libname" : + func_append libobjs " $symfileobj" + test " " = "$libobjs" && libobjs= + + if test relink != "$opt_mode"; then + # Remove our outputs, but don't remove object files since they + # may have been created when compiling PIC objects. + removelist= + tempremovelist=`$ECHO "$output_objdir/*"` + for p in $tempremovelist; do + case $p in + *.$objext | *.gcno) + ;; + $output_objdir/$outputname | $output_objdir/$libname.* | $output_objdir/$libname$release.*) + if test -n "$precious_files_regex"; then + if $ECHO "$p" | $EGREP -e "$precious_files_regex" >/dev/null 2>&1 + then + continue + fi + fi + func_append removelist " $p" + ;; + *) ;; + esac + done + test -n "$removelist" && \ + func_show_eval "${RM}r \$removelist" + fi + + # Now set the variables for building old libraries. + if test yes = "$build_old_libs" && test convenience != "$build_libtool_libs"; then + func_append oldlibs " $output_objdir/$libname.$libext" + + # Transform .lo files to .o files. + oldobjs="$objs "`$ECHO "$libobjs" | $SP2NL | $SED "/\.$libext$/d; $lo2o" | $NL2SP` + fi + + # Eliminate all temporary directories. + #for path in $notinst_path; do + # lib_search_path=`$ECHO "$lib_search_path " | $SED "s% $path % %g"` + # deplibs=`$ECHO "$deplibs " | $SED "s% -L$path % %g"` + # dependency_libs=`$ECHO "$dependency_libs " | $SED "s% -L$path % %g"` + #done + + if test -n "$xrpath"; then + # If the user specified any rpath flags, then add them. + temp_xrpath= + for libdir in $xrpath; do + func_replace_sysroot "$libdir" + func_append temp_xrpath " -R$func_replace_sysroot_result" + case "$finalize_rpath " in + *" $libdir "*) ;; + *) func_append finalize_rpath " $libdir" ;; + esac + done + if test yes != "$hardcode_into_libs" || test yes = "$build_old_libs"; then + dependency_libs="$temp_xrpath $dependency_libs" + fi + fi + + # Make sure dlfiles contains only unique files that won't be dlpreopened + old_dlfiles=$dlfiles + dlfiles= + for lib in $old_dlfiles; do + case " $dlprefiles $dlfiles " in + *" $lib "*) ;; + *) func_append dlfiles " $lib" ;; + esac + done + + # Make sure dlprefiles contains only unique files + old_dlprefiles=$dlprefiles + dlprefiles= + for lib in $old_dlprefiles; do + case "$dlprefiles " in + *" $lib "*) ;; + *) func_append dlprefiles " $lib" ;; + esac + done + + if test yes = "$build_libtool_libs"; then + if test -n "$rpath"; then + case $host in + *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2* | *-*-beos* | *-cegcc* | *-*-haiku*) + # these systems don't actually have a c library (as such)! + ;; + *-*-rhapsody* | *-*-darwin1.[012]) + # Rhapsody C library is in the System framework + func_append deplibs " System.ltframework" + ;; + *-*-netbsd*) + # Don't link with libc until the a.out ld.so is fixed. + ;; + *-*-openbsd* | *-*-freebsd* | *-*-dragonfly*) + # Do not include libc due to us having libc/libc_r. + ;; + *-*-sco3.2v5* | *-*-sco5v6*) + # Causes problems with __ctype + ;; + *-*-sysv4.2uw2* | *-*-sysv5* | *-*-unixware* | *-*-OpenUNIX*) + # Compiler inserts libc in the correct place for threads to work + ;; + *) + # Add libc to deplibs on all other systems if necessary. + if test yes = "$build_libtool_need_lc"; then + func_append deplibs " -lc" + fi + ;; + esac + fi + + # Transform deplibs into only deplibs that can be linked in shared. + name_save=$name + libname_save=$libname + release_save=$release + versuffix_save=$versuffix + major_save=$major + # I'm not sure if I'm treating the release correctly. I think + # release should show up in the -l (ie -lgmp5) so we don't want to + # add it in twice. Is that correct? + release= + versuffix= + major= + newdeplibs= + droppeddeps=no + case $deplibs_check_method in + pass_all) + # Don't check for shared/static. Everything works. + # This might be a little naive. We might want to check + # whether the library exists or not. But this is on + # osf3 & osf4 and I'm not really sure... Just + # implementing what was already the behavior. + newdeplibs=$deplibs + ;; + test_compile) + # This code stresses the "libraries are programs" paradigm to its + # limits. Maybe even breaks it. We compile a program, linking it + # against the deplibs as a proxy for the library. Then we can check + # whether they linked in statically or dynamically with ldd. + $opt_dry_run || $RM conftest.c + cat > conftest.c </dev/null` + $nocaseglob + else + potential_libs=`ls $i/$libnameglob[.-]* 2>/dev/null` + fi + for potent_lib in $potential_libs; do + # Follow soft links. + if ls -lLd "$potent_lib" 2>/dev/null | + $GREP " -> " >/dev/null; then + continue + fi + # The statement above tries to avoid entering an + # endless loop below, in case of cyclic links. + # We might still enter an endless loop, since a link + # loop can be closed while we follow links, + # but so what? + potlib=$potent_lib + while test -h "$potlib" 2>/dev/null; do + potliblink=`ls -ld $potlib | $SED 's/.* -> //'` + case $potliblink in + [\\/]* | [A-Za-z]:[\\/]*) potlib=$potliblink;; + *) potlib=`$ECHO "$potlib" | $SED 's|[^/]*$||'`"$potliblink";; + esac + done + if eval $file_magic_cmd \"\$potlib\" 2>/dev/null | + $SED -e 10q | + $EGREP "$file_magic_regex" > /dev/null; then + func_append newdeplibs " $a_deplib" + a_deplib= + break 2 + fi + done + done + fi + if test -n "$a_deplib"; then + droppeddeps=yes + echo + $ECHO "*** Warning: linker path does not have real file for library $a_deplib." + echo "*** I have the capability to make that library automatically link in when" + echo "*** you link to this library. But I can only do this if you have a" + echo "*** shared version of the library, which you do not appear to have" + echo "*** because I did check the linker path looking for a file starting" + if test -z "$potlib"; then + $ECHO "*** with $libname but no candidates were found. (...for file magic test)" + else + $ECHO "*** with $libname and none of the candidates passed a file format test" + $ECHO "*** using a file magic. Last file checked: $potlib" + fi + fi + ;; + *) + # Add a -L argument. + func_append newdeplibs " $a_deplib" + ;; + esac + done # Gone through all deplibs. + ;; + match_pattern*) + set dummy $deplibs_check_method; shift + match_pattern_regex=`expr "$deplibs_check_method" : "$1 \(.*\)"` + for a_deplib in $deplibs; do + case $a_deplib in + -l*) + func_stripname -l '' "$a_deplib" + name=$func_stripname_result + if test yes = "$allow_libtool_libs_with_static_runtimes"; then + case " $predeps $postdeps " in + *" $a_deplib "*) + func_append newdeplibs " $a_deplib" + a_deplib= + ;; + esac + fi + if test -n "$a_deplib"; then + libname=`eval "\\$ECHO \"$libname_spec\""` + for i in $lib_search_path $sys_lib_search_path $shlib_search_path; do + potential_libs=`ls $i/$libname[.-]* 2>/dev/null` + for potent_lib in $potential_libs; do + potlib=$potent_lib # see symlink-check above in file_magic test + if eval "\$ECHO \"$potent_lib\"" 2>/dev/null | $SED 10q | \ + $EGREP "$match_pattern_regex" > /dev/null; then + func_append newdeplibs " $a_deplib" + a_deplib= + break 2 + fi + done + done + fi + if test -n "$a_deplib"; then + droppeddeps=yes + echo + $ECHO "*** Warning: linker path does not have real file for library $a_deplib." + echo "*** I have the capability to make that library automatically link in when" + echo "*** you link to this library. But I can only do this if you have a" + echo "*** shared version of the library, which you do not appear to have" + echo "*** because I did check the linker path looking for a file starting" + if test -z "$potlib"; then + $ECHO "*** with $libname but no candidates were found. (...for regex pattern test)" + else + $ECHO "*** with $libname and none of the candidates passed a file format test" + $ECHO "*** using a regex pattern. Last file checked: $potlib" + fi + fi + ;; + *) + # Add a -L argument. + func_append newdeplibs " $a_deplib" + ;; + esac + done # Gone through all deplibs. + ;; + none | unknown | *) + newdeplibs= + tmp_deplibs=`$ECHO " $deplibs" | $SED 's/ -lc$//; s/ -[LR][^ ]*//g'` + if test yes = "$allow_libtool_libs_with_static_runtimes"; then + for i in $predeps $postdeps; do + # can't use Xsed below, because $i might contain '/' + tmp_deplibs=`$ECHO " $tmp_deplibs" | $SED "s|$i||"` + done + fi + case $tmp_deplibs in + *[!\ \ ]*) + echo + if test none = "$deplibs_check_method"; then + echo "*** Warning: inter-library dependencies are not supported in this platform." + else + echo "*** Warning: inter-library dependencies are not known to be supported." + fi + echo "*** All declared inter-library dependencies are being dropped." + droppeddeps=yes + ;; + esac + ;; + esac + versuffix=$versuffix_save + major=$major_save + release=$release_save + libname=$libname_save + name=$name_save + + case $host in + *-*-rhapsody* | *-*-darwin1.[012]) + # On Rhapsody replace the C library with the System framework + newdeplibs=`$ECHO " $newdeplibs" | $SED 's/ -lc / System.ltframework /'` + ;; + esac + + if test yes = "$droppeddeps"; then + if test yes = "$module"; then + echo + echo "*** Warning: libtool could not satisfy all declared inter-library" + $ECHO "*** dependencies of module $libname. Therefore, libtool will create" + echo "*** a static module, that should work as long as the dlopening" + echo "*** application is linked with the -dlopen flag." + if test -z "$global_symbol_pipe"; then + echo + echo "*** However, this would only work if libtool was able to extract symbol" + echo "*** lists from a program, using 'nm' or equivalent, but libtool could" + echo "*** not find such a program. So, this module is probably useless." + echo "*** 'nm' from GNU binutils and a full rebuild may help." + fi + if test no = "$build_old_libs"; then + oldlibs=$output_objdir/$libname.$libext + build_libtool_libs=module + build_old_libs=yes + else + build_libtool_libs=no + fi + else + echo "*** The inter-library dependencies that have been dropped here will be" + echo "*** automatically added whenever a program is linked with this library" + echo "*** or is declared to -dlopen it." + + if test no = "$allow_undefined"; then + echo + echo "*** Since this library must not contain undefined symbols," + echo "*** because either the platform does not support them or" + echo "*** it was explicitly requested with -no-undefined," + echo "*** libtool will only create a static version of it." + if test no = "$build_old_libs"; then + oldlibs=$output_objdir/$libname.$libext + build_libtool_libs=module + build_old_libs=yes + else + build_libtool_libs=no + fi + fi + fi + fi + # Done checking deplibs! + deplibs=$newdeplibs + fi + # Time to change all our "foo.ltframework" stuff back to "-framework foo" + case $host in + *-*-darwin*) + newdeplibs=`$ECHO " $newdeplibs" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'` + new_inherited_linker_flags=`$ECHO " $new_inherited_linker_flags" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'` + deplibs=`$ECHO " $deplibs" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'` + ;; + esac + + # move library search paths that coincide with paths to not yet + # installed libraries to the beginning of the library search list + new_libs= + for path in $notinst_path; do + case " $new_libs " in + *" -L$path/$objdir "*) ;; + *) + case " $deplibs " in + *" -L$path/$objdir "*) + func_append new_libs " -L$path/$objdir" ;; + esac + ;; + esac + done + for deplib in $deplibs; do + case $deplib in + -L*) + case " $new_libs " in + *" $deplib "*) ;; + *) func_append new_libs " $deplib" ;; + esac + ;; + *) func_append new_libs " $deplib" ;; + esac + done + deplibs=$new_libs + + # All the library-specific variables (install_libdir is set above). + library_names= + old_library= + dlname= + + # Test again, we may have decided not to build it any more + if test yes = "$build_libtool_libs"; then + # Remove $wl instances when linking with ld. + # FIXME: should test the right _cmds variable. + case $archive_cmds in + *\$LD\ *) wl= ;; + esac + if test yes = "$hardcode_into_libs"; then + # Hardcode the library paths + hardcode_libdirs= + dep_rpath= + rpath=$finalize_rpath + test relink = "$opt_mode" || rpath=$compile_rpath$rpath + for libdir in $rpath; do + if test -n "$hardcode_libdir_flag_spec"; then + if test -n "$hardcode_libdir_separator"; then + func_replace_sysroot "$libdir" + libdir=$func_replace_sysroot_result + if test -z "$hardcode_libdirs"; then + hardcode_libdirs=$libdir + else + # Just accumulate the unique libdirs. + case $hardcode_libdir_separator$hardcode_libdirs$hardcode_libdir_separator in + *"$hardcode_libdir_separator$libdir$hardcode_libdir_separator"*) + ;; + *) + func_append hardcode_libdirs "$hardcode_libdir_separator$libdir" + ;; + esac + fi + else + eval flag=\"$hardcode_libdir_flag_spec\" + func_append dep_rpath " $flag" + fi + elif test -n "$runpath_var"; then + case "$perm_rpath " in + *" $libdir "*) ;; + *) func_append perm_rpath " $libdir" ;; + esac + fi + done + # Substitute the hardcoded libdirs into the rpath. + if test -n "$hardcode_libdir_separator" && + test -n "$hardcode_libdirs"; then + libdir=$hardcode_libdirs + eval "dep_rpath=\"$hardcode_libdir_flag_spec\"" + fi + if test -n "$runpath_var" && test -n "$perm_rpath"; then + # We should set the runpath_var. + rpath= + for dir in $perm_rpath; do + func_append rpath "$dir:" + done + eval "$runpath_var='$rpath\$$runpath_var'; export $runpath_var" + fi + test -n "$dep_rpath" && deplibs="$dep_rpath $deplibs" + fi + + shlibpath=$finalize_shlibpath + test relink = "$opt_mode" || shlibpath=$compile_shlibpath$shlibpath + if test -n "$shlibpath"; then + eval "$shlibpath_var='$shlibpath\$$shlibpath_var'; export $shlibpath_var" + fi + + # Get the real and link names of the library. + eval shared_ext=\"$shrext_cmds\" + eval library_names=\"$library_names_spec\" + set dummy $library_names + shift + realname=$1 + shift + + if test -n "$soname_spec"; then + eval soname=\"$soname_spec\" + else + soname=$realname + fi + if test -z "$dlname"; then + dlname=$soname + fi + + lib=$output_objdir/$realname + linknames= + for link + do + func_append linknames " $link" + done + + # Use standard objects if they are pic + test -z "$pic_flag" && libobjs=`$ECHO "$libobjs" | $SP2NL | $SED "$lo2o" | $NL2SP` + test "X$libobjs" = "X " && libobjs= + + delfiles= + if test -n "$export_symbols" && test -n "$include_expsyms"; then + $opt_dry_run || cp "$export_symbols" "$output_objdir/$libname.uexp" + export_symbols=$output_objdir/$libname.uexp + func_append delfiles " $export_symbols" + fi + + orig_export_symbols= + case $host_os in + cygwin* | mingw* | cegcc*) + if test -n "$export_symbols" && test -z "$export_symbols_regex"; then + # exporting using user supplied symfile + func_dll_def_p "$export_symbols" || { + # and it's NOT already a .def file. Must figure out + # which of the given symbols are data symbols and tag + # them as such. So, trigger use of export_symbols_cmds. + # export_symbols gets reassigned inside the "prepare + # the list of exported symbols" if statement, so the + # include_expsyms logic still works. + orig_export_symbols=$export_symbols + export_symbols= + always_export_symbols=yes + } + fi + ;; + esac + + # Prepare the list of exported symbols + if test -z "$export_symbols"; then + if test yes = "$always_export_symbols" || test -n "$export_symbols_regex"; then + func_verbose "generating symbol list for '$libname.la'" + export_symbols=$output_objdir/$libname.exp + $opt_dry_run || $RM $export_symbols + cmds=$export_symbols_cmds + save_ifs=$IFS; IFS='~' + for cmd1 in $cmds; do + IFS=$save_ifs + # Take the normal branch if the nm_file_list_spec branch + # doesn't work or if tool conversion is not needed. + case $nm_file_list_spec~$to_tool_file_cmd in + *~func_convert_file_noop | *~func_convert_file_msys_to_w32 | ~*) + try_normal_branch=yes + eval cmd=\"$cmd1\" + func_len " $cmd" + len=$func_len_result + ;; + *) + try_normal_branch=no + ;; + esac + if test yes = "$try_normal_branch" \ + && { test "$len" -lt "$max_cmd_len" \ + || test "$max_cmd_len" -le -1; } + then + func_show_eval "$cmd" 'exit $?' + skipped_export=false + elif test -n "$nm_file_list_spec"; then + func_basename "$output" + output_la=$func_basename_result + save_libobjs=$libobjs + save_output=$output + output=$output_objdir/$output_la.nm + func_to_tool_file "$output" + libobjs=$nm_file_list_spec$func_to_tool_file_result + func_append delfiles " $output" + func_verbose "creating $NM input file list: $output" + for obj in $save_libobjs; do + func_to_tool_file "$obj" + $ECHO "$func_to_tool_file_result" + done > "$output" + eval cmd=\"$cmd1\" + func_show_eval "$cmd" 'exit $?' + output=$save_output + libobjs=$save_libobjs + skipped_export=false + else + # The command line is too long to execute in one step. + func_verbose "using reloadable object file for export list..." + skipped_export=: + # Break out early, otherwise skipped_export may be + # set to false by a later but shorter cmd. + break + fi + done + IFS=$save_ifs + if test -n "$export_symbols_regex" && test : != "$skipped_export"; then + func_show_eval '$EGREP -e "$export_symbols_regex" "$export_symbols" > "${export_symbols}T"' + func_show_eval '$MV "${export_symbols}T" "$export_symbols"' + fi + fi + fi + + if test -n "$export_symbols" && test -n "$include_expsyms"; then + tmp_export_symbols=$export_symbols + test -n "$orig_export_symbols" && tmp_export_symbols=$orig_export_symbols + $opt_dry_run || eval '$ECHO "$include_expsyms" | $SP2NL >> "$tmp_export_symbols"' + fi + + if test : != "$skipped_export" && test -n "$orig_export_symbols"; then + # The given exports_symbols file has to be filtered, so filter it. + func_verbose "filter symbol list for '$libname.la' to tag DATA exports" + # FIXME: $output_objdir/$libname.filter potentially contains lots of + # 's' commands, which not all seds can handle. GNU sed should be fine + # though. Also, the filter scales superlinearly with the number of + # global variables. join(1) would be nice here, but unfortunately + # isn't a blessed tool. + $opt_dry_run || $SED -e '/[ ,]DATA/!d;s,\(.*\)\([ \,].*\),s|^\1$|\1\2|,' < $export_symbols > $output_objdir/$libname.filter + func_append delfiles " $export_symbols $output_objdir/$libname.filter" + export_symbols=$output_objdir/$libname.def + $opt_dry_run || $SED -f $output_objdir/$libname.filter < $orig_export_symbols > $export_symbols + fi + + tmp_deplibs= + for test_deplib in $deplibs; do + case " $convenience " in + *" $test_deplib "*) ;; + *) + func_append tmp_deplibs " $test_deplib" + ;; + esac + done + deplibs=$tmp_deplibs + + if test -n "$convenience"; then + if test -n "$whole_archive_flag_spec" && + test yes = "$compiler_needs_object" && + test -z "$libobjs"; then + # extract the archives, so we have objects to list. + # TODO: could optimize this to just extract one archive. + whole_archive_flag_spec= + fi + if test -n "$whole_archive_flag_spec"; then + save_libobjs=$libobjs + eval libobjs=\"\$libobjs $whole_archive_flag_spec\" + test "X$libobjs" = "X " && libobjs= + else + gentop=$output_objdir/${outputname}x + func_append generated " $gentop" + + func_extract_archives $gentop $convenience + func_append libobjs " $func_extract_archives_result" + test "X$libobjs" = "X " && libobjs= + fi + fi + + if test yes = "$thread_safe" && test -n "$thread_safe_flag_spec"; then + eval flag=\"$thread_safe_flag_spec\" + func_append linker_flags " $flag" + fi + + # Make a backup of the uninstalled library when relinking + if test relink = "$opt_mode"; then + $opt_dry_run || eval '(cd $output_objdir && $RM ${realname}U && $MV $realname ${realname}U)' || exit $? + fi + + # Do each of the archive commands. + if test yes = "$module" && test -n "$module_cmds"; then + if test -n "$export_symbols" && test -n "$module_expsym_cmds"; then + eval test_cmds=\"$module_expsym_cmds\" + cmds=$module_expsym_cmds + else + eval test_cmds=\"$module_cmds\" + cmds=$module_cmds + fi + else + if test -n "$export_symbols" && test -n "$archive_expsym_cmds"; then + eval test_cmds=\"$archive_expsym_cmds\" + cmds=$archive_expsym_cmds + else + eval test_cmds=\"$archive_cmds\" + cmds=$archive_cmds + fi + fi + + if test : != "$skipped_export" && + func_len " $test_cmds" && + len=$func_len_result && + test "$len" -lt "$max_cmd_len" || test "$max_cmd_len" -le -1; then + : + else + # The command line is too long to link in one step, link piecewise + # or, if using GNU ld and skipped_export is not :, use a linker + # script. + + # Save the value of $output and $libobjs because we want to + # use them later. If we have whole_archive_flag_spec, we + # want to use save_libobjs as it was before + # whole_archive_flag_spec was expanded, because we can't + # assume the linker understands whole_archive_flag_spec. + # This may have to be revisited, in case too many + # convenience libraries get linked in and end up exceeding + # the spec. + if test -z "$convenience" || test -z "$whole_archive_flag_spec"; then + save_libobjs=$libobjs + fi + save_output=$output + func_basename "$output" + output_la=$func_basename_result + + # Clear the reloadable object creation command queue and + # initialize k to one. + test_cmds= + concat_cmds= + objlist= + last_robj= + k=1 + + if test -n "$save_libobjs" && test : != "$skipped_export" && test yes = "$with_gnu_ld"; then + output=$output_objdir/$output_la.lnkscript + func_verbose "creating GNU ld script: $output" + echo 'INPUT (' > $output + for obj in $save_libobjs + do + func_to_tool_file "$obj" + $ECHO "$func_to_tool_file_result" >> $output + done + echo ')' >> $output + func_append delfiles " $output" + func_to_tool_file "$output" + output=$func_to_tool_file_result + elif test -n "$save_libobjs" && test : != "$skipped_export" && test -n "$file_list_spec"; then + output=$output_objdir/$output_la.lnk + func_verbose "creating linker input file list: $output" + : > $output + set x $save_libobjs + shift + firstobj= + if test yes = "$compiler_needs_object"; then + firstobj="$1 " + shift + fi + for obj + do + func_to_tool_file "$obj" + $ECHO "$func_to_tool_file_result" >> $output + done + func_append delfiles " $output" + func_to_tool_file "$output" + output=$firstobj\"$file_list_spec$func_to_tool_file_result\" + else + if test -n "$save_libobjs"; then + func_verbose "creating reloadable object files..." + output=$output_objdir/$output_la-$k.$objext + eval test_cmds=\"$reload_cmds\" + func_len " $test_cmds" + len0=$func_len_result + len=$len0 + + # Loop over the list of objects to be linked. + for obj in $save_libobjs + do + func_len " $obj" + func_arith $len + $func_len_result + len=$func_arith_result + if test -z "$objlist" || + test "$len" -lt "$max_cmd_len"; then + func_append objlist " $obj" + else + # The command $test_cmds is almost too long, add a + # command to the queue. + if test 1 -eq "$k"; then + # The first file doesn't have a previous command to add. + reload_objs=$objlist + eval concat_cmds=\"$reload_cmds\" + else + # All subsequent reloadable object files will link in + # the last one created. + reload_objs="$objlist $last_robj" + eval concat_cmds=\"\$concat_cmds~$reload_cmds~\$RM $last_robj\" + fi + last_robj=$output_objdir/$output_la-$k.$objext + func_arith $k + 1 + k=$func_arith_result + output=$output_objdir/$output_la-$k.$objext + objlist=" $obj" + func_len " $last_robj" + func_arith $len0 + $func_len_result + len=$func_arith_result + fi + done + # Handle the remaining objects by creating one last + # reloadable object file. All subsequent reloadable object + # files will link in the last one created. + test -z "$concat_cmds" || concat_cmds=$concat_cmds~ + reload_objs="$objlist $last_robj" + eval concat_cmds=\"\$concat_cmds$reload_cmds\" + if test -n "$last_robj"; then + eval concat_cmds=\"\$concat_cmds~\$RM $last_robj\" + fi + func_append delfiles " $output" + + else + output= + fi + + ${skipped_export-false} && { + func_verbose "generating symbol list for '$libname.la'" + export_symbols=$output_objdir/$libname.exp + $opt_dry_run || $RM $export_symbols + libobjs=$output + # Append the command to create the export file. + test -z "$concat_cmds" || concat_cmds=$concat_cmds~ + eval concat_cmds=\"\$concat_cmds$export_symbols_cmds\" + if test -n "$last_robj"; then + eval concat_cmds=\"\$concat_cmds~\$RM $last_robj\" + fi + } + + test -n "$save_libobjs" && + func_verbose "creating a temporary reloadable object file: $output" + + # Loop through the commands generated above and execute them. + save_ifs=$IFS; IFS='~' + for cmd in $concat_cmds; do + IFS=$save_ifs + $opt_quiet || { + func_quote_for_expand "$cmd" + eval "func_echo $func_quote_for_expand_result" + } + $opt_dry_run || eval "$cmd" || { + lt_exit=$? + + # Restore the uninstalled library and exit + if test relink = "$opt_mode"; then + ( cd "$output_objdir" && \ + $RM "${realname}T" && \ + $MV "${realname}U" "$realname" ) + fi + + exit $lt_exit + } + done + IFS=$save_ifs + + if test -n "$export_symbols_regex" && ${skipped_export-false}; then + func_show_eval '$EGREP -e "$export_symbols_regex" "$export_symbols" > "${export_symbols}T"' + func_show_eval '$MV "${export_symbols}T" "$export_symbols"' + fi + fi + + ${skipped_export-false} && { + if test -n "$export_symbols" && test -n "$include_expsyms"; then + tmp_export_symbols=$export_symbols + test -n "$orig_export_symbols" && tmp_export_symbols=$orig_export_symbols + $opt_dry_run || eval '$ECHO "$include_expsyms" | $SP2NL >> "$tmp_export_symbols"' + fi + + if test -n "$orig_export_symbols"; then + # The given exports_symbols file has to be filtered, so filter it. + func_verbose "filter symbol list for '$libname.la' to tag DATA exports" + # FIXME: $output_objdir/$libname.filter potentially contains lots of + # 's' commands, which not all seds can handle. GNU sed should be fine + # though. Also, the filter scales superlinearly with the number of + # global variables. join(1) would be nice here, but unfortunately + # isn't a blessed tool. + $opt_dry_run || $SED -e '/[ ,]DATA/!d;s,\(.*\)\([ \,].*\),s|^\1$|\1\2|,' < $export_symbols > $output_objdir/$libname.filter + func_append delfiles " $export_symbols $output_objdir/$libname.filter" + export_symbols=$output_objdir/$libname.def + $opt_dry_run || $SED -f $output_objdir/$libname.filter < $orig_export_symbols > $export_symbols + fi + } + + libobjs=$output + # Restore the value of output. + output=$save_output + + if test -n "$convenience" && test -n "$whole_archive_flag_spec"; then + eval libobjs=\"\$libobjs $whole_archive_flag_spec\" + test "X$libobjs" = "X " && libobjs= + fi + # Expand the library linking commands again to reset the + # value of $libobjs for piecewise linking. + + # Do each of the archive commands. + if test yes = "$module" && test -n "$module_cmds"; then + if test -n "$export_symbols" && test -n "$module_expsym_cmds"; then + cmds=$module_expsym_cmds + else + cmds=$module_cmds + fi + else + if test -n "$export_symbols" && test -n "$archive_expsym_cmds"; then + cmds=$archive_expsym_cmds + else + cmds=$archive_cmds + fi + fi + fi + + if test -n "$delfiles"; then + # Append the command to remove temporary files to $cmds. + eval cmds=\"\$cmds~\$RM $delfiles\" + fi + + # Add any objects from preloaded convenience libraries + if test -n "$dlprefiles"; then + gentop=$output_objdir/${outputname}x + func_append generated " $gentop" + + func_extract_archives $gentop $dlprefiles + func_append libobjs " $func_extract_archives_result" + test "X$libobjs" = "X " && libobjs= + fi + + save_ifs=$IFS; IFS='~' + for cmd in $cmds; do + IFS=$sp$nl + eval cmd=\"$cmd\" + IFS=$save_ifs + $opt_quiet || { + func_quote_for_expand "$cmd" + eval "func_echo $func_quote_for_expand_result" + } + $opt_dry_run || eval "$cmd" || { + lt_exit=$? + + # Restore the uninstalled library and exit + if test relink = "$opt_mode"; then + ( cd "$output_objdir" && \ + $RM "${realname}T" && \ + $MV "${realname}U" "$realname" ) + fi + + exit $lt_exit + } + done + IFS=$save_ifs + + # Restore the uninstalled library and exit + if test relink = "$opt_mode"; then + $opt_dry_run || eval '(cd $output_objdir && $RM ${realname}T && $MV $realname ${realname}T && $MV ${realname}U $realname)' || exit $? + + if test -n "$convenience"; then + if test -z "$whole_archive_flag_spec"; then + func_show_eval '${RM}r "$gentop"' + fi + fi + + exit $EXIT_SUCCESS + fi + + # Create links to the real library. + for linkname in $linknames; do + if test "$realname" != "$linkname"; then + func_show_eval '(cd "$output_objdir" && $RM "$linkname" && $LN_S "$realname" "$linkname")' 'exit $?' + fi + done + + # If -module or -export-dynamic was specified, set the dlname. + if test yes = "$module" || test yes = "$export_dynamic"; then + # On all known operating systems, these are identical. + dlname=$soname + fi + fi + ;; + + obj) + if test -n "$dlfiles$dlprefiles" || test no != "$dlself"; then + func_warning "'-dlopen' is ignored for objects" + fi + + case " $deplibs" in + *\ -l* | *\ -L*) + func_warning "'-l' and '-L' are ignored for objects" ;; + esac + + test -n "$rpath" && \ + func_warning "'-rpath' is ignored for objects" + + test -n "$xrpath" && \ + func_warning "'-R' is ignored for objects" + + test -n "$vinfo" && \ + func_warning "'-version-info' is ignored for objects" + + test -n "$release" && \ + func_warning "'-release' is ignored for objects" + + case $output in + *.lo) + test -n "$objs$old_deplibs" && \ + func_fatal_error "cannot build library object '$output' from non-libtool objects" + + libobj=$output + func_lo2o "$libobj" + obj=$func_lo2o_result + ;; + *) + libobj= + obj=$output + ;; + esac + + # Delete the old objects. + $opt_dry_run || $RM $obj $libobj + + # Objects from convenience libraries. This assumes + # single-version convenience libraries. Whenever we create + # different ones for PIC/non-PIC, this we'll have to duplicate + # the extraction. + reload_conv_objs= + gentop= + # if reload_cmds runs $LD directly, get rid of -Wl from + # whole_archive_flag_spec and hope we can get by with turning comma + # into space. + case $reload_cmds in + *\$LD[\ \$]*) wl= ;; + esac + if test -n "$convenience"; then + if test -n "$whole_archive_flag_spec"; then + eval tmp_whole_archive_flags=\"$whole_archive_flag_spec\" + test -n "$wl" || tmp_whole_archive_flags=`$ECHO "$tmp_whole_archive_flags" | $SED 's|,| |g'` + reload_conv_objs=$reload_objs\ $tmp_whole_archive_flags + else + gentop=$output_objdir/${obj}x + func_append generated " $gentop" + + func_extract_archives $gentop $convenience + reload_conv_objs="$reload_objs $func_extract_archives_result" + fi + fi + + # If we're not building shared, we need to use non_pic_objs + test yes = "$build_libtool_libs" || libobjs=$non_pic_objects + + # Create the old-style object. + reload_objs=$objs$old_deplibs' '`$ECHO "$libobjs" | $SP2NL | $SED "/\.$libext$/d; /\.lib$/d; $lo2o" | $NL2SP`' '$reload_conv_objs + + output=$obj + func_execute_cmds "$reload_cmds" 'exit $?' + + # Exit if we aren't doing a library object file. + if test -z "$libobj"; then + if test -n "$gentop"; then + func_show_eval '${RM}r "$gentop"' + fi + + exit $EXIT_SUCCESS + fi + + test yes = "$build_libtool_libs" || { + if test -n "$gentop"; then + func_show_eval '${RM}r "$gentop"' + fi + + # Create an invalid libtool object if no PIC, so that we don't + # accidentally link it into a program. + # $show "echo timestamp > $libobj" + # $opt_dry_run || eval "echo timestamp > $libobj" || exit $? + exit $EXIT_SUCCESS + } + + if test -n "$pic_flag" || test default != "$pic_mode"; then + # Only do commands if we really have different PIC objects. + reload_objs="$libobjs $reload_conv_objs" + output=$libobj + func_execute_cmds "$reload_cmds" 'exit $?' + fi + + if test -n "$gentop"; then + func_show_eval '${RM}r "$gentop"' + fi + + exit $EXIT_SUCCESS + ;; + + prog) + case $host in + *cygwin*) func_stripname '' '.exe' "$output" + output=$func_stripname_result.exe;; + esac + test -n "$vinfo" && \ + func_warning "'-version-info' is ignored for programs" + + test -n "$release" && \ + func_warning "'-release' is ignored for programs" + + $preload \ + && test unknown,unknown,unknown = "$dlopen_support,$dlopen_self,$dlopen_self_static" \ + && func_warning "'LT_INIT([dlopen])' not used. Assuming no dlopen support." + + case $host in + *-*-rhapsody* | *-*-darwin1.[012]) + # On Rhapsody replace the C library is the System framework + compile_deplibs=`$ECHO " $compile_deplibs" | $SED 's/ -lc / System.ltframework /'` + finalize_deplibs=`$ECHO " $finalize_deplibs" | $SED 's/ -lc / System.ltframework /'` + ;; + esac + + case $host in + *-*-darwin*) + # Don't allow lazy linking, it breaks C++ global constructors + # But is supposedly fixed on 10.4 or later (yay!). + if test CXX = "$tagname"; then + case ${MACOSX_DEPLOYMENT_TARGET-10.0} in + 10.[0123]) + func_append compile_command " $wl-bind_at_load" + func_append finalize_command " $wl-bind_at_load" + ;; + esac + fi + # Time to change all our "foo.ltframework" stuff back to "-framework foo" + compile_deplibs=`$ECHO " $compile_deplibs" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'` + finalize_deplibs=`$ECHO " $finalize_deplibs" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'` + ;; + esac + + + # move library search paths that coincide with paths to not yet + # installed libraries to the beginning of the library search list + new_libs= + for path in $notinst_path; do + case " $new_libs " in + *" -L$path/$objdir "*) ;; + *) + case " $compile_deplibs " in + *" -L$path/$objdir "*) + func_append new_libs " -L$path/$objdir" ;; + esac + ;; + esac + done + for deplib in $compile_deplibs; do + case $deplib in + -L*) + case " $new_libs " in + *" $deplib "*) ;; + *) func_append new_libs " $deplib" ;; + esac + ;; + *) func_append new_libs " $deplib" ;; + esac + done + compile_deplibs=$new_libs + + + func_append compile_command " $compile_deplibs" + func_append finalize_command " $finalize_deplibs" + + if test -n "$rpath$xrpath"; then + # If the user specified any rpath flags, then add them. + for libdir in $rpath $xrpath; do + # This is the magic to use -rpath. + case "$finalize_rpath " in + *" $libdir "*) ;; + *) func_append finalize_rpath " $libdir" ;; + esac + done + fi + + # Now hardcode the library paths + rpath= + hardcode_libdirs= + for libdir in $compile_rpath $finalize_rpath; do + if test -n "$hardcode_libdir_flag_spec"; then + if test -n "$hardcode_libdir_separator"; then + if test -z "$hardcode_libdirs"; then + hardcode_libdirs=$libdir + else + # Just accumulate the unique libdirs. + case $hardcode_libdir_separator$hardcode_libdirs$hardcode_libdir_separator in + *"$hardcode_libdir_separator$libdir$hardcode_libdir_separator"*) + ;; + *) + func_append hardcode_libdirs "$hardcode_libdir_separator$libdir" + ;; + esac + fi + else + eval flag=\"$hardcode_libdir_flag_spec\" + func_append rpath " $flag" + fi + elif test -n "$runpath_var"; then + case "$perm_rpath " in + *" $libdir "*) ;; + *) func_append perm_rpath " $libdir" ;; + esac + fi + case $host in + *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2* | *-cegcc*) + testbindir=`$ECHO "$libdir" | $SED -e 's*/lib$*/bin*'` + case :$dllsearchpath: in + *":$libdir:"*) ;; + ::) dllsearchpath=$libdir;; + *) func_append dllsearchpath ":$libdir";; + esac + case :$dllsearchpath: in + *":$testbindir:"*) ;; + ::) dllsearchpath=$testbindir;; + *) func_append dllsearchpath ":$testbindir";; + esac + ;; + esac + done + # Substitute the hardcoded libdirs into the rpath. + if test -n "$hardcode_libdir_separator" && + test -n "$hardcode_libdirs"; then + libdir=$hardcode_libdirs + eval rpath=\" $hardcode_libdir_flag_spec\" + fi + compile_rpath=$rpath + + rpath= + hardcode_libdirs= + for libdir in $finalize_rpath; do + if test -n "$hardcode_libdir_flag_spec"; then + if test -n "$hardcode_libdir_separator"; then + if test -z "$hardcode_libdirs"; then + hardcode_libdirs=$libdir + else + # Just accumulate the unique libdirs. + case $hardcode_libdir_separator$hardcode_libdirs$hardcode_libdir_separator in + *"$hardcode_libdir_separator$libdir$hardcode_libdir_separator"*) + ;; + *) + func_append hardcode_libdirs "$hardcode_libdir_separator$libdir" + ;; + esac + fi + else + eval flag=\"$hardcode_libdir_flag_spec\" + func_append rpath " $flag" + fi + elif test -n "$runpath_var"; then + case "$finalize_perm_rpath " in + *" $libdir "*) ;; + *) func_append finalize_perm_rpath " $libdir" ;; + esac + fi + done + # Substitute the hardcoded libdirs into the rpath. + if test -n "$hardcode_libdir_separator" && + test -n "$hardcode_libdirs"; then + libdir=$hardcode_libdirs + eval rpath=\" $hardcode_libdir_flag_spec\" + fi + finalize_rpath=$rpath + + if test -n "$libobjs" && test yes = "$build_old_libs"; then + # Transform all the library objects into standard objects. + compile_command=`$ECHO "$compile_command" | $SP2NL | $SED "$lo2o" | $NL2SP` + finalize_command=`$ECHO "$finalize_command" | $SP2NL | $SED "$lo2o" | $NL2SP` + fi + + func_generate_dlsyms "$outputname" "@PROGRAM@" false + + # template prelinking step + if test -n "$prelink_cmds"; then + func_execute_cmds "$prelink_cmds" 'exit $?' + fi + + wrappers_required=: + case $host in + *cegcc* | *mingw32ce*) + # Disable wrappers for cegcc and mingw32ce hosts, we are cross compiling anyway. + wrappers_required=false + ;; + *cygwin* | *mingw* ) + test yes = "$build_libtool_libs" || wrappers_required=false + ;; + *) + if test no = "$need_relink" || test yes != "$build_libtool_libs"; then + wrappers_required=false + fi + ;; + esac + $wrappers_required || { + # Replace the output file specification. + compile_command=`$ECHO "$compile_command" | $SED 's%@OUTPUT@%'"$output"'%g'` + link_command=$compile_command$compile_rpath + + # We have no uninstalled library dependencies, so finalize right now. + exit_status=0 + func_show_eval "$link_command" 'exit_status=$?' + + if test -n "$postlink_cmds"; then + func_to_tool_file "$output" + postlink_cmds=`func_echo_all "$postlink_cmds" | $SED -e 's%@OUTPUT@%'"$output"'%g' -e 's%@TOOL_OUTPUT@%'"$func_to_tool_file_result"'%g'` + func_execute_cmds "$postlink_cmds" 'exit $?' + fi + + # Delete the generated files. + if test -f "$output_objdir/${outputname}S.$objext"; then + func_show_eval '$RM "$output_objdir/${outputname}S.$objext"' + fi + + exit $exit_status + } + + if test -n "$compile_shlibpath$finalize_shlibpath"; then + compile_command="$shlibpath_var=\"$compile_shlibpath$finalize_shlibpath\$$shlibpath_var\" $compile_command" + fi + if test -n "$finalize_shlibpath"; then + finalize_command="$shlibpath_var=\"$finalize_shlibpath\$$shlibpath_var\" $finalize_command" + fi + + compile_var= + finalize_var= + if test -n "$runpath_var"; then + if test -n "$perm_rpath"; then + # We should set the runpath_var. + rpath= + for dir in $perm_rpath; do + func_append rpath "$dir:" + done + compile_var="$runpath_var=\"$rpath\$$runpath_var\" " + fi + if test -n "$finalize_perm_rpath"; then + # We should set the runpath_var. + rpath= + for dir in $finalize_perm_rpath; do + func_append rpath "$dir:" + done + finalize_var="$runpath_var=\"$rpath\$$runpath_var\" " + fi + fi + + if test yes = "$no_install"; then + # We don't need to create a wrapper script. + link_command=$compile_var$compile_command$compile_rpath + # Replace the output file specification. + link_command=`$ECHO "$link_command" | $SED 's%@OUTPUT@%'"$output"'%g'` + # Delete the old output file. + $opt_dry_run || $RM $output + # Link the executable and exit + func_show_eval "$link_command" 'exit $?' + + if test -n "$postlink_cmds"; then + func_to_tool_file "$output" + postlink_cmds=`func_echo_all "$postlink_cmds" | $SED -e 's%@OUTPUT@%'"$output"'%g' -e 's%@TOOL_OUTPUT@%'"$func_to_tool_file_result"'%g'` + func_execute_cmds "$postlink_cmds" 'exit $?' + fi + + exit $EXIT_SUCCESS + fi + + case $hardcode_action,$fast_install in + relink,*) + # Fast installation is not supported + link_command=$compile_var$compile_command$compile_rpath + relink_command=$finalize_var$finalize_command$finalize_rpath + + func_warning "this platform does not like uninstalled shared libraries" + func_warning "'$output' will be relinked during installation" + ;; + *,yes) + link_command=$finalize_var$compile_command$finalize_rpath + relink_command=`$ECHO "$compile_var$compile_command$compile_rpath" | $SED 's%@OUTPUT@%\$progdir/\$file%g'` + ;; + *,no) + link_command=$compile_var$compile_command$compile_rpath + relink_command=$finalize_var$finalize_command$finalize_rpath + ;; + *,needless) + link_command=$finalize_var$compile_command$finalize_rpath + relink_command= + ;; + esac + + # Replace the output file specification. + link_command=`$ECHO "$link_command" | $SED 's%@OUTPUT@%'"$output_objdir/$outputname"'%g'` + + # Delete the old output files. + $opt_dry_run || $RM $output $output_objdir/$outputname $output_objdir/lt-$outputname + + func_show_eval "$link_command" 'exit $?' + + if test -n "$postlink_cmds"; then + func_to_tool_file "$output_objdir/$outputname" + postlink_cmds=`func_echo_all "$postlink_cmds" | $SED -e 's%@OUTPUT@%'"$output_objdir/$outputname"'%g' -e 's%@TOOL_OUTPUT@%'"$func_to_tool_file_result"'%g'` + func_execute_cmds "$postlink_cmds" 'exit $?' + fi + + # Now create the wrapper script. + func_verbose "creating $output" + + # Quote the relink command for shipping. + if test -n "$relink_command"; then + # Preserve any variables that may affect compiler behavior + for var in $variables_saved_for_relink; do + if eval test -z \"\${$var+set}\"; then + relink_command="{ test -z \"\${$var+set}\" || $lt_unset $var || { $var=; export $var; }; }; $relink_command" + elif eval var_value=\$$var; test -z "$var_value"; then + relink_command="$var=; export $var; $relink_command" + else + func_quote_for_eval "$var_value" + relink_command="$var=$func_quote_for_eval_result; export $var; $relink_command" + fi + done + relink_command="(cd `pwd`; $relink_command)" + relink_command=`$ECHO "$relink_command" | $SED "$sed_quote_subst"` + fi + + # Only actually do things if not in dry run mode. + $opt_dry_run || { + # win32 will think the script is a binary if it has + # a .exe suffix, so we strip it off here. + case $output in + *.exe) func_stripname '' '.exe' "$output" + output=$func_stripname_result ;; + esac + # test for cygwin because mv fails w/o .exe extensions + case $host in + *cygwin*) + exeext=.exe + func_stripname '' '.exe' "$outputname" + outputname=$func_stripname_result ;; + *) exeext= ;; + esac + case $host in + *cygwin* | *mingw* ) + func_dirname_and_basename "$output" "" "." + output_name=$func_basename_result + output_path=$func_dirname_result + cwrappersource=$output_path/$objdir/lt-$output_name.c + cwrapper=$output_path/$output_name.exe + $RM $cwrappersource $cwrapper + trap "$RM $cwrappersource $cwrapper; exit $EXIT_FAILURE" 1 2 15 + + func_emit_cwrapperexe_src > $cwrappersource + + # The wrapper executable is built using the $host compiler, + # because it contains $host paths and files. If cross- + # compiling, it, like the target executable, must be + # executed on the $host or under an emulation environment. + $opt_dry_run || { + $LTCC $LTCFLAGS -o $cwrapper $cwrappersource + $STRIP $cwrapper + } + + # Now, create the wrapper script for func_source use: + func_ltwrapper_scriptname $cwrapper + $RM $func_ltwrapper_scriptname_result + trap "$RM $func_ltwrapper_scriptname_result; exit $EXIT_FAILURE" 1 2 15 + $opt_dry_run || { + # note: this script will not be executed, so do not chmod. + if test "x$build" = "x$host"; then + $cwrapper --lt-dump-script > $func_ltwrapper_scriptname_result + else + func_emit_wrapper no > $func_ltwrapper_scriptname_result + fi + } + ;; + * ) + $RM $output + trap "$RM $output; exit $EXIT_FAILURE" 1 2 15 + + func_emit_wrapper no > $output + chmod +x $output + ;; + esac + } + exit $EXIT_SUCCESS + ;; + esac + + # See if we need to build an old-fashioned archive. + for oldlib in $oldlibs; do + + case $build_libtool_libs in + convenience) + oldobjs="$libobjs_save $symfileobj" + addlibs=$convenience + build_libtool_libs=no + ;; + module) + oldobjs=$libobjs_save + addlibs=$old_convenience + build_libtool_libs=no + ;; + *) + oldobjs="$old_deplibs $non_pic_objects" + $preload && test -f "$symfileobj" \ + && func_append oldobjs " $symfileobj" + addlibs=$old_convenience + ;; + esac + + if test -n "$addlibs"; then + gentop=$output_objdir/${outputname}x + func_append generated " $gentop" + + func_extract_archives $gentop $addlibs + func_append oldobjs " $func_extract_archives_result" + fi + + # Do each command in the archive commands. + if test -n "$old_archive_from_new_cmds" && test yes = "$build_libtool_libs"; then + cmds=$old_archive_from_new_cmds + else + + # Add any objects from preloaded convenience libraries + if test -n "$dlprefiles"; then + gentop=$output_objdir/${outputname}x + func_append generated " $gentop" + + func_extract_archives $gentop $dlprefiles + func_append oldobjs " $func_extract_archives_result" + fi + + # POSIX demands no paths to be encoded in archives. We have + # to avoid creating archives with duplicate basenames if we + # might have to extract them afterwards, e.g., when creating a + # static archive out of a convenience library, or when linking + # the entirety of a libtool archive into another (currently + # not supported by libtool). + if (for obj in $oldobjs + do + func_basename "$obj" + $ECHO "$func_basename_result" + done | sort | sort -uc >/dev/null 2>&1); then + : + else + echo "copying selected object files to avoid basename conflicts..." + gentop=$output_objdir/${outputname}x + func_append generated " $gentop" + func_mkdir_p "$gentop" + save_oldobjs=$oldobjs + oldobjs= + counter=1 + for obj in $save_oldobjs + do + func_basename "$obj" + objbase=$func_basename_result + case " $oldobjs " in + " ") oldobjs=$obj ;; + *[\ /]"$objbase "*) + while :; do + # Make sure we don't pick an alternate name that also + # overlaps. + newobj=lt$counter-$objbase + func_arith $counter + 1 + counter=$func_arith_result + case " $oldobjs " in + *[\ /]"$newobj "*) ;; + *) if test ! -f "$gentop/$newobj"; then break; fi ;; + esac + done + func_show_eval "ln $obj $gentop/$newobj || cp $obj $gentop/$newobj" + func_append oldobjs " $gentop/$newobj" + ;; + *) func_append oldobjs " $obj" ;; + esac + done + fi + func_to_tool_file "$oldlib" func_convert_file_msys_to_w32 + tool_oldlib=$func_to_tool_file_result + eval cmds=\"$old_archive_cmds\" + + func_len " $cmds" + len=$func_len_result + if test "$len" -lt "$max_cmd_len" || test "$max_cmd_len" -le -1; then + cmds=$old_archive_cmds + elif test -n "$archiver_list_spec"; then + func_verbose "using command file archive linking..." + for obj in $oldobjs + do + func_to_tool_file "$obj" + $ECHO "$func_to_tool_file_result" + done > $output_objdir/$libname.libcmd + func_to_tool_file "$output_objdir/$libname.libcmd" + oldobjs=" $archiver_list_spec$func_to_tool_file_result" + cmds=$old_archive_cmds + else + # the command line is too long to link in one step, link in parts + func_verbose "using piecewise archive linking..." + save_RANLIB=$RANLIB + RANLIB=: + objlist= + concat_cmds= + save_oldobjs=$oldobjs + oldobjs= + # Is there a better way of finding the last object in the list? + for obj in $save_oldobjs + do + last_oldobj=$obj + done + eval test_cmds=\"$old_archive_cmds\" + func_len " $test_cmds" + len0=$func_len_result + len=$len0 + for obj in $save_oldobjs + do + func_len " $obj" + func_arith $len + $func_len_result + len=$func_arith_result + func_append objlist " $obj" + if test "$len" -lt "$max_cmd_len"; then + : + else + # the above command should be used before it gets too long + oldobjs=$objlist + if test "$obj" = "$last_oldobj"; then + RANLIB=$save_RANLIB + fi + test -z "$concat_cmds" || concat_cmds=$concat_cmds~ + eval concat_cmds=\"\$concat_cmds$old_archive_cmds\" + objlist= + len=$len0 + fi + done + RANLIB=$save_RANLIB + oldobjs=$objlist + if test -z "$oldobjs"; then + eval cmds=\"\$concat_cmds\" + else + eval cmds=\"\$concat_cmds~\$old_archive_cmds\" + fi + fi + fi + func_execute_cmds "$cmds" 'exit $?' + done + + test -n "$generated" && \ + func_show_eval "${RM}r$generated" + + # Now create the libtool archive. + case $output in + *.la) + old_library= + test yes = "$build_old_libs" && old_library=$libname.$libext + func_verbose "creating $output" + + # Preserve any variables that may affect compiler behavior + for var in $variables_saved_for_relink; do + if eval test -z \"\${$var+set}\"; then + relink_command="{ test -z \"\${$var+set}\" || $lt_unset $var || { $var=; export $var; }; }; $relink_command" + elif eval var_value=\$$var; test -z "$var_value"; then + relink_command="$var=; export $var; $relink_command" + else + func_quote_for_eval "$var_value" + relink_command="$var=$func_quote_for_eval_result; export $var; $relink_command" + fi + done + # Quote the link command for shipping. + relink_command="(cd `pwd`; $SHELL \"$progpath\" $preserve_args --mode=relink $libtool_args @inst_prefix_dir@)" + relink_command=`$ECHO "$relink_command" | $SED "$sed_quote_subst"` + if test yes = "$hardcode_automatic"; then + relink_command= + fi + + # Only create the output if not a dry run. + $opt_dry_run || { + for installed in no yes; do + if test yes = "$installed"; then + if test -z "$install_libdir"; then + break + fi + output=$output_objdir/${outputname}i + # Replace all uninstalled libtool libraries with the installed ones + newdependency_libs= + for deplib in $dependency_libs; do + case $deplib in + *.la) + func_basename "$deplib" + name=$func_basename_result + func_resolve_sysroot "$deplib" + eval libdir=`$SED -n -e 's/^libdir=\(.*\)$/\1/p' $func_resolve_sysroot_result` + test -z "$libdir" && \ + func_fatal_error "'$deplib' is not a valid libtool archive" + func_append newdependency_libs " ${lt_sysroot:+=}$libdir/$name" + ;; + -L*) + func_stripname -L '' "$deplib" + func_replace_sysroot "$func_stripname_result" + func_append newdependency_libs " -L$func_replace_sysroot_result" + ;; + -R*) + func_stripname -R '' "$deplib" + func_replace_sysroot "$func_stripname_result" + func_append newdependency_libs " -R$func_replace_sysroot_result" + ;; + *) func_append newdependency_libs " $deplib" ;; + esac + done + dependency_libs=$newdependency_libs + newdlfiles= + + for lib in $dlfiles; do + case $lib in + *.la) + func_basename "$lib" + name=$func_basename_result + eval libdir=`$SED -n -e 's/^libdir=\(.*\)$/\1/p' $lib` + test -z "$libdir" && \ + func_fatal_error "'$lib' is not a valid libtool archive" + func_append newdlfiles " ${lt_sysroot:+=}$libdir/$name" + ;; + *) func_append newdlfiles " $lib" ;; + esac + done + dlfiles=$newdlfiles + newdlprefiles= + for lib in $dlprefiles; do + case $lib in + *.la) + # Only pass preopened files to the pseudo-archive (for + # eventual linking with the app. that links it) if we + # didn't already link the preopened objects directly into + # the library: + func_basename "$lib" + name=$func_basename_result + eval libdir=`$SED -n -e 's/^libdir=\(.*\)$/\1/p' $lib` + test -z "$libdir" && \ + func_fatal_error "'$lib' is not a valid libtool archive" + func_append newdlprefiles " ${lt_sysroot:+=}$libdir/$name" + ;; + esac + done + dlprefiles=$newdlprefiles + else + newdlfiles= + for lib in $dlfiles; do + case $lib in + [\\/]* | [A-Za-z]:[\\/]*) abs=$lib ;; + *) abs=`pwd`"/$lib" ;; + esac + func_append newdlfiles " $abs" + done + dlfiles=$newdlfiles + newdlprefiles= + for lib in $dlprefiles; do + case $lib in + [\\/]* | [A-Za-z]:[\\/]*) abs=$lib ;; + *) abs=`pwd`"/$lib" ;; + esac + func_append newdlprefiles " $abs" + done + dlprefiles=$newdlprefiles + fi + $RM $output + # place dlname in correct position for cygwin + # In fact, it would be nice if we could use this code for all target + # systems that can't hard-code library paths into their executables + # and that have no shared library path variable independent of PATH, + # but it turns out we can't easily determine that from inspecting + # libtool variables, so we have to hard-code the OSs to which it + # applies here; at the moment, that means platforms that use the PE + # object format with DLL files. See the long comment at the top of + # tests/bindir.at for full details. + tdlname=$dlname + case $host,$output,$installed,$module,$dlname in + *cygwin*,*lai,yes,no,*.dll | *mingw*,*lai,yes,no,*.dll | *cegcc*,*lai,yes,no,*.dll) + # If a -bindir argument was supplied, place the dll there. + if test -n "$bindir"; then + func_relative_path "$install_libdir" "$bindir" + tdlname=$func_relative_path_result/$dlname + else + # Otherwise fall back on heuristic. + tdlname=../bin/$dlname + fi + ;; + esac + $ECHO > $output "\ +# $outputname - a libtool library file +# Generated by $PROGRAM (GNU $PACKAGE) $VERSION +# +# Please DO NOT delete this file! +# It is necessary for linking the library. + +# The name that we can dlopen(3). +dlname='$tdlname' + +# Names of this library. +library_names='$library_names' + +# The name of the static archive. +old_library='$old_library' + +# Linker flags that cannot go in dependency_libs. +inherited_linker_flags='$new_inherited_linker_flags' + +# Libraries that this one depends upon. +dependency_libs='$dependency_libs' + +# Names of additional weak libraries provided by this library +weak_library_names='$weak_libs' + +# Version information for $libname. +current=$current +age=$age +revision=$revision + +# Is this an already installed library? +installed=$installed + +# Should we warn about portability when linking against -modules? +shouldnotlink=$module + +# Files to dlopen/dlpreopen +dlopen='$dlfiles' +dlpreopen='$dlprefiles' + +# Directory that this library needs to be installed in: +libdir='$install_libdir'" + if test no,yes = "$installed,$need_relink"; then + $ECHO >> $output "\ +relink_command=\"$relink_command\"" + fi + done + } + + # Do a symbolic link so that the libtool archive can be found in + # LD_LIBRARY_PATH before the program is installed. + func_show_eval '( cd "$output_objdir" && $RM "$outputname" && $LN_S "../$outputname" "$outputname" )' 'exit $?' + ;; + esac + exit $EXIT_SUCCESS +} + +if test link = "$opt_mode" || test relink = "$opt_mode"; then + func_mode_link ${1+"$@"} +fi + + +# func_mode_uninstall arg... +func_mode_uninstall () +{ + $debug_cmd + + RM=$nonopt + files= + rmforce=false + exit_status=0 + + # This variable tells wrapper scripts just to set variables rather + # than running their programs. + libtool_install_magic=$magic + + for arg + do + case $arg in + -f) func_append RM " $arg"; rmforce=: ;; + -*) func_append RM " $arg" ;; + *) func_append files " $arg" ;; + esac + done + + test -z "$RM" && \ + func_fatal_help "you must specify an RM program" + + rmdirs= + + for file in $files; do + func_dirname "$file" "" "." + dir=$func_dirname_result + if test . = "$dir"; then + odir=$objdir + else + odir=$dir/$objdir + fi + func_basename "$file" + name=$func_basename_result + test uninstall = "$opt_mode" && odir=$dir + + # Remember odir for removal later, being careful to avoid duplicates + if test clean = "$opt_mode"; then + case " $rmdirs " in + *" $odir "*) ;; + *) func_append rmdirs " $odir" ;; + esac + fi + + # Don't error if the file doesn't exist and rm -f was used. + if { test -L "$file"; } >/dev/null 2>&1 || + { test -h "$file"; } >/dev/null 2>&1 || + test -f "$file"; then + : + elif test -d "$file"; then + exit_status=1 + continue + elif $rmforce; then + continue + fi + + rmfiles=$file + + case $name in + *.la) + # Possibly a libtool archive, so verify it. + if func_lalib_p "$file"; then + func_source $dir/$name + + # Delete the libtool libraries and symlinks. + for n in $library_names; do + func_append rmfiles " $odir/$n" + done + test -n "$old_library" && func_append rmfiles " $odir/$old_library" + + case $opt_mode in + clean) + case " $library_names " in + *" $dlname "*) ;; + *) test -n "$dlname" && func_append rmfiles " $odir/$dlname" ;; + esac + test -n "$libdir" && func_append rmfiles " $odir/$name $odir/${name}i" + ;; + uninstall) + if test -n "$library_names"; then + # Do each command in the postuninstall commands. + func_execute_cmds "$postuninstall_cmds" '$rmforce || exit_status=1' + fi + + if test -n "$old_library"; then + # Do each command in the old_postuninstall commands. + func_execute_cmds "$old_postuninstall_cmds" '$rmforce || exit_status=1' + fi + # FIXME: should reinstall the best remaining shared library. + ;; + esac + fi + ;; + + *.lo) + # Possibly a libtool object, so verify it. + if func_lalib_p "$file"; then + + # Read the .lo file + func_source $dir/$name + + # Add PIC object to the list of files to remove. + if test -n "$pic_object" && test none != "$pic_object"; then + func_append rmfiles " $dir/$pic_object" + fi + + # Add non-PIC object to the list of files to remove. + if test -n "$non_pic_object" && test none != "$non_pic_object"; then + func_append rmfiles " $dir/$non_pic_object" + fi + fi + ;; + + *) + if test clean = "$opt_mode"; then + noexename=$name + case $file in + *.exe) + func_stripname '' '.exe' "$file" + file=$func_stripname_result + func_stripname '' '.exe' "$name" + noexename=$func_stripname_result + # $file with .exe has already been added to rmfiles, + # add $file without .exe + func_append rmfiles " $file" + ;; + esac + # Do a test to see if this is a libtool program. + if func_ltwrapper_p "$file"; then + if func_ltwrapper_executable_p "$file"; then + func_ltwrapper_scriptname "$file" + relink_command= + func_source $func_ltwrapper_scriptname_result + func_append rmfiles " $func_ltwrapper_scriptname_result" + else + relink_command= + func_source $dir/$noexename + fi + + # note $name still contains .exe if it was in $file originally + # as does the version of $file that was added into $rmfiles + func_append rmfiles " $odir/$name $odir/${name}S.$objext" + if test yes = "$fast_install" && test -n "$relink_command"; then + func_append rmfiles " $odir/lt-$name" + fi + if test "X$noexename" != "X$name"; then + func_append rmfiles " $odir/lt-$noexename.c" + fi + fi + fi + ;; + esac + func_show_eval "$RM $rmfiles" 'exit_status=1' + done + + # Try to remove the $objdir's in the directories where we deleted files + for dir in $rmdirs; do + if test -d "$dir"; then + func_show_eval "rmdir $dir >/dev/null 2>&1" + fi + done + + exit $exit_status +} + +if test uninstall = "$opt_mode" || test clean = "$opt_mode"; then + func_mode_uninstall ${1+"$@"} +fi + +test -z "$opt_mode" && { + help=$generic_help + func_fatal_help "you must specify a MODE" +} + +test -z "$exec_cmd" && \ + func_fatal_help "invalid operation mode '$opt_mode'" + +if test -n "$exec_cmd"; then + eval exec "$exec_cmd" + exit $EXIT_FAILURE +fi + +exit $exit_status + + +# The TAGs below are defined such that we never get into a situation +# where we disable both kinds of libraries. Given conflicting +# choices, we go for a static library, that is the most portable, +# since we can't tell whether shared libraries were disabled because +# the user asked for that or because the platform doesn't support +# them. This is particularly important on AIX, because we don't +# support having both static and shared libraries enabled at the same +# time on that platform, so we default to a shared-only configuration. +# If a disable-shared tag is given, we'll fallback to a static-only +# configuration. But we'll never go from static-only to shared-only. + +# ### BEGIN LIBTOOL TAG CONFIG: disable-shared +build_libtool_libs=no +build_old_libs=yes +# ### END LIBTOOL TAG CONFIG: disable-shared + +# ### BEGIN LIBTOOL TAG CONFIG: disable-static +build_old_libs=`case $build_libtool_libs in yes) echo no;; *) echo yes;; esac` +# ### END LIBTOOL TAG CONFIG: disable-static + +# Local Variables: +# mode:shell-script +# sh-indentation:2 +# End: diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/missing b/deps/ngtcp2/ngtcp2/third-party/libev/missing new file mode 100755 index 00000000000000..625aeb11897a22 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/missing @@ -0,0 +1,215 @@ +#! /bin/sh +# Common wrapper for a few potentially missing GNU programs. + +scriptversion=2018-03-07.03; # UTC + +# Copyright (C) 1996-2018 Free Software Foundation, Inc. +# Originally written by Fran,cois Pinard , 1996. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program that contains a +# configuration script generated by Autoconf, you may include it under +# the same distribution terms that you use for the rest of that program. + +if test $# -eq 0; then + echo 1>&2 "Try '$0 --help' for more information" + exit 1 +fi + +case $1 in + + --is-lightweight) + # Used by our autoconf macros to check whether the available missing + # script is modern enough. + exit 0 + ;; + + --run) + # Back-compat with the calling convention used by older automake. + shift + ;; + + -h|--h|--he|--hel|--help) + echo "\ +$0 [OPTION]... PROGRAM [ARGUMENT]... + +Run 'PROGRAM [ARGUMENT]...', returning a proper advice when this fails due +to PROGRAM being missing or too old. + +Options: + -h, --help display this help and exit + -v, --version output version information and exit + +Supported PROGRAM values: + aclocal autoconf autoheader autom4te automake makeinfo + bison yacc flex lex help2man + +Version suffixes to PROGRAM as well as the prefixes 'gnu-', 'gnu', and +'g' are ignored when checking the name. + +Send bug reports to ." + exit $? + ;; + + -v|--v|--ve|--ver|--vers|--versi|--versio|--version) + echo "missing $scriptversion (GNU Automake)" + exit $? + ;; + + -*) + echo 1>&2 "$0: unknown '$1' option" + echo 1>&2 "Try '$0 --help' for more information" + exit 1 + ;; + +esac + +# Run the given program, remember its exit status. +"$@"; st=$? + +# If it succeeded, we are done. +test $st -eq 0 && exit 0 + +# Also exit now if we it failed (or wasn't found), and '--version' was +# passed; such an option is passed most likely to detect whether the +# program is present and works. +case $2 in --version|--help) exit $st;; esac + +# Exit code 63 means version mismatch. This often happens when the user +# tries to use an ancient version of a tool on a file that requires a +# minimum version. +if test $st -eq 63; then + msg="probably too old" +elif test $st -eq 127; then + # Program was missing. + msg="missing on your system" +else + # Program was found and executed, but failed. Give up. + exit $st +fi + +perl_URL=https://www.perl.org/ +flex_URL=https://github.com/westes/flex +gnu_software_URL=https://www.gnu.org/software + +program_details () +{ + case $1 in + aclocal|automake) + echo "The '$1' program is part of the GNU Automake package:" + echo "<$gnu_software_URL/automake>" + echo "It also requires GNU Autoconf, GNU m4 and Perl in order to run:" + echo "<$gnu_software_URL/autoconf>" + echo "<$gnu_software_URL/m4/>" + echo "<$perl_URL>" + ;; + autoconf|autom4te|autoheader) + echo "The '$1' program is part of the GNU Autoconf package:" + echo "<$gnu_software_URL/autoconf/>" + echo "It also requires GNU m4 and Perl in order to run:" + echo "<$gnu_software_URL/m4/>" + echo "<$perl_URL>" + ;; + esac +} + +give_advice () +{ + # Normalize program name to check for. + normalized_program=`echo "$1" | sed ' + s/^gnu-//; t + s/^gnu//; t + s/^g//; t'` + + printf '%s\n' "'$1' is $msg." + + configure_deps="'configure.ac' or m4 files included by 'configure.ac'" + case $normalized_program in + autoconf*) + echo "You should only need it if you modified 'configure.ac'," + echo "or m4 files included by it." + program_details 'autoconf' + ;; + autoheader*) + echo "You should only need it if you modified 'acconfig.h' or" + echo "$configure_deps." + program_details 'autoheader' + ;; + automake*) + echo "You should only need it if you modified 'Makefile.am' or" + echo "$configure_deps." + program_details 'automake' + ;; + aclocal*) + echo "You should only need it if you modified 'acinclude.m4' or" + echo "$configure_deps." + program_details 'aclocal' + ;; + autom4te*) + echo "You might have modified some maintainer files that require" + echo "the 'autom4te' program to be rebuilt." + program_details 'autom4te' + ;; + bison*|yacc*) + echo "You should only need it if you modified a '.y' file." + echo "You may want to install the GNU Bison package:" + echo "<$gnu_software_URL/bison/>" + ;; + lex*|flex*) + echo "You should only need it if you modified a '.l' file." + echo "You may want to install the Fast Lexical Analyzer package:" + echo "<$flex_URL>" + ;; + help2man*) + echo "You should only need it if you modified a dependency" \ + "of a man page." + echo "You may want to install the GNU Help2man package:" + echo "<$gnu_software_URL/help2man/>" + ;; + makeinfo*) + echo "You should only need it if you modified a '.texi' file, or" + echo "any other file indirectly affecting the aspect of the manual." + echo "You might want to install the Texinfo package:" + echo "<$gnu_software_URL/texinfo/>" + echo "The spurious makeinfo call might also be the consequence of" + echo "using a buggy 'make' (AIX, DU, IRIX), in which case you might" + echo "want to install GNU make:" + echo "<$gnu_software_URL/make/>" + ;; + *) + echo "You might have modified some files without having the proper" + echo "tools for further handling them. Check the 'README' file, it" + echo "often tells you about the needed prerequisites for installing" + echo "this package. You may also peek at any GNU archive site, in" + echo "case some other package contains this missing '$1' program." + ;; + esac +} + +give_advice "$1" | sed -e '1s/^/WARNING: /' \ + -e '2,$s/^/ /' >&2 + +# Propagate the correct exit status (expected to be 127 for a program +# not found, 63 for a program that failed due to version mismatch). +exit $st + +# Local variables: +# eval: (add-hook 'before-save-hook 'time-stamp) +# time-stamp-start: "scriptversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-time-zone: "UTC0" +# time-stamp-end: "; # UTC" +# End: diff --git a/deps/ngtcp2/ngtcp2/third-party/libev/mkinstalldirs b/deps/ngtcp2/ngtcp2/third-party/libev/mkinstalldirs new file mode 100755 index 00000000000000..36aa90953b3e72 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/libev/mkinstalldirs @@ -0,0 +1,162 @@ +#! /bin/sh +# mkinstalldirs --- make directory hierarchy + +scriptversion=2018-03-07.03; # UTC + +# Original author: Noah Friedman +# Created: 1993-05-16 +# Public domain. +# +# This file is maintained in Automake, please report +# bugs to or send patches to +# . + +nl=' +' +IFS=" "" $nl" +errstatus=0 +dirmode= + +usage="\ +Usage: mkinstalldirs [-h] [--help] [--version] [-m MODE] DIR ... + +Create each directory DIR (with mode MODE, if specified), including all +leading file name components. + +Report bugs to ." + +# process command line arguments +while test $# -gt 0 ; do + case $1 in + -h | --help | --h*) # -h for help + echo "$usage" + exit $? + ;; + -m) # -m PERM arg + shift + test $# -eq 0 && { echo "$usage" 1>&2; exit 1; } + dirmode=$1 + shift + ;; + --version) + echo "$0 $scriptversion" + exit $? + ;; + --) # stop option processing + shift + break + ;; + -*) # unknown option + echo "$usage" 1>&2 + exit 1 + ;; + *) # first non-opt arg + break + ;; + esac +done + +for file +do + if test -d "$file"; then + shift + else + break + fi +done + +case $# in + 0) exit 0 ;; +esac + +# Solaris 8's mkdir -p isn't thread-safe. If you mkdir -p a/b and +# mkdir -p a/c at the same time, both will detect that a is missing, +# one will create a, then the other will try to create a and die with +# a "File exists" error. This is a problem when calling mkinstalldirs +# from a parallel make. We use --version in the probe to restrict +# ourselves to GNU mkdir, which is thread-safe. +case $dirmode in + '') + if mkdir -p --version . >/dev/null 2>&1 && test ! -d ./--version; then + echo "mkdir -p -- $*" + exec mkdir -p -- "$@" + else + # On NextStep and OpenStep, the 'mkdir' command does not + # recognize any option. It will interpret all options as + # directories to create, and then abort because '.' already + # exists. + test -d ./-p && rmdir ./-p + test -d ./--version && rmdir ./--version + fi + ;; + *) + if mkdir -m "$dirmode" -p --version . >/dev/null 2>&1 && + test ! -d ./--version; then + echo "mkdir -m $dirmode -p -- $*" + exec mkdir -m "$dirmode" -p -- "$@" + else + # Clean up after NextStep and OpenStep mkdir. + for d in ./-m ./-p ./--version "./$dirmode"; + do + test -d $d && rmdir $d + done + fi + ;; +esac + +for file +do + case $file in + /*) pathcomp=/ ;; + *) pathcomp= ;; + esac + oIFS=$IFS + IFS=/ + set fnord $file + shift + IFS=$oIFS + + for d + do + test "x$d" = x && continue + + pathcomp=$pathcomp$d + case $pathcomp in + -*) pathcomp=./$pathcomp ;; + esac + + if test ! -d "$pathcomp"; then + echo "mkdir $pathcomp" + + mkdir "$pathcomp" || lasterr=$? + + if test ! -d "$pathcomp"; then + errstatus=$lasterr + else + if test ! -z "$dirmode"; then + echo "chmod $dirmode $pathcomp" + lasterr= + chmod "$dirmode" "$pathcomp" || lasterr=$? + + if test ! -z "$lasterr"; then + errstatus=$lasterr + fi + fi + fi + fi + + pathcomp=$pathcomp/ + done +done + +exit $errstatus + +# Local Variables: +# mode: shell-script +# sh-indentation: 2 +# eval: (add-hook 'before-save-hook 'time-stamp) +# time-stamp-start: "scriptversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-time-zone: "UTC0" +# time-stamp-end: "; # UTC" +# End: diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/.clang-format b/deps/ngtcp2/ngtcp2/third-party/urlparse/.clang-format new file mode 100644 index 00000000000000..4a4552bdb278b0 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/.clang-format @@ -0,0 +1,246 @@ +--- +Language: Cpp +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignArrayOfStructures: None +AlignConsecutiveAssignments: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: true +AlignConsecutiveBitFields: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: true +AlignConsecutiveDeclarations: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: true +AlignConsecutiveMacros: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: true +AlignConsecutiveShortCaseStatements: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCaseColons: false +AlignEscapedNewlines: Right +AlignOperands: Align +AlignTrailingComments: + Kind: Always + OverEmptyLines: 0 +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowBreakBeforeNoexceptSpecifier: Never +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortCompoundRequirementOnASingleLine: true +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: true +BitFieldColonSpacing: Both +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterExternBlock: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakAdjacentStringLiterals: true +BreakAfterAttributes: Leave +BreakAfterJavaFieldAnnotations: false +BreakArrays: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: Always +BreakBeforeBraces: Attach +BreakBeforeInlineASMColon: OnlyMultiline +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerIndentWidth: 2 +ContinuationIndentWidth: 2 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: false +IndentExternBlock: AfterExternBlock +IndentGotoLabels: true +IndentPPDirectives: AfterHash +IndentRequiresClause: false +IndentWidth: 2 +IndentWrappedFunctionNames: false +InsertBraces: false +InsertNewlineAtEOF: false +InsertTrailingCommas: None +IntegerLiteralSeparator: + Binary: 0 + BinaryMinDigits: 0 + Decimal: 0 + DecimalMinDigits: 0 + Hex: 0 + HexMinDigits: 0 +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +KeepEmptyLinesAtEOF: false +LambdaBodyIndentation: Signature +LineEnding: DeriveLF +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PackConstructorInitializers: NextLine +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakScopeResolution: 500 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyIndentedWhitespace: 0 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Right +PPIndentWidth: -1 +QualifierAlignment: Leave +ReferenceAlignment: Pointer +ReflowComments: true +RemoveBracesLLVM: false +RemoveParentheses: Leave +RemoveSemicolon: false +RequiresClausePosition: OwnLine +RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Leave +ShortNamespaceLines: 1 +SkipMacroDefinitionBody: false +SortIncludes: Never +SortJavaStaticImport: Before +SortUsingDeclarations: LexicographicNumeric +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeJsonColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDefinitionName: false + AfterFunctionDeclarationName: false + AfterIfMacros: true + AfterOverloadedOperator: false + AfterPlacementOperator: true + AfterRequiresInClause: false + AfterRequiresInExpression: false + BeforeNonEmptyParentheses: false +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInContainerLiterals: true +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParens: Never +SpacesInParensOptions: + InCStyleCasts: false + InConditionalStatements: false + InEmptyParentheses: false + Other: false +SpacesInSquareBrackets: false +Standard: Latest +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION + - munit_void_test_decl +TabWidth: 8 +UseTab: Never +VerilogBreakBetweenInstancePorts: true +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME +... + diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/.clusterfuzzlite/Dockerfile b/deps/ngtcp2/ngtcp2/third-party/urlparse/.clusterfuzzlite/Dockerfile new file mode 100644 index 00000000000000..a2763c87d75671 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/.clusterfuzzlite/Dockerfile @@ -0,0 +1,5 @@ +FROM gcr.io/oss-fuzz-base/base-builder:v1 +RUN apt-get update && apt-get install -y make autoconf automake libtool pkg-config +COPY . $SRC/urlparse +WORKDIR urlparse +COPY .clusterfuzzlite/build.sh $SRC/ diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/.clusterfuzzlite/build.sh b/deps/ngtcp2/ngtcp2/third-party/urlparse/.clusterfuzzlite/build.sh new file mode 100755 index 00000000000000..6f33867031568a --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/.clusterfuzzlite/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash -eu + +autoreconf -i +./configure --disable-dependency-tracking +make -j$(nproc) check TESTS="" + +$CXX $CXXFLAGS -std=c++17 -I. \ + fuzz/parser.cc -o $OUT/parser \ + $LIB_FUZZING_ENGINE .libs/liburlparse.a .libs/libhttp-parser.a + +zip -j $OUT/parser_seed_corpus.zip fuzz/corpus/parser/* diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/.clusterfuzzlite/project.yaml b/deps/ngtcp2/ngtcp2/third-party/urlparse/.clusterfuzzlite/project.yaml new file mode 100644 index 00000000000000..b4788012b1024a --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/.clusterfuzzlite/project.yaml @@ -0,0 +1 @@ +language: c++ diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/.gitmodules b/deps/ngtcp2/ngtcp2/third-party/urlparse/.gitmodules new file mode 100644 index 00000000000000..19a1abdfd49a25 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/.gitmodules @@ -0,0 +1,6 @@ +[submodule "munit"] + path = munit + url = https://github.com/ngtcp2/munit +[submodule "http-parser"] + path = http-parser + url = https://github.com/nodejs/http-parser diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/AUTHORS b/deps/ngtcp2/ngtcp2/third-party/urlparse/AUTHORS new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/COPYING b/deps/ngtcp2/ngtcp2/third-party/urlparse/COPYING new file mode 100644 index 00000000000000..06af8be1d9d53f --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/COPYING @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2024 urlparse contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/ChangeLog b/deps/ngtcp2/ngtcp2/third-party/urlparse/ChangeLog new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/NEWS b/deps/ngtcp2/ngtcp2/third-party/urlparse/NEWS new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/README b/deps/ngtcp2/ngtcp2/third-party/urlparse/README new file mode 100644 index 00000000000000..5ccc0ea36b545a --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/README @@ -0,0 +1 @@ +See README.rst diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/README.rst b/deps/ngtcp2/ngtcp2/third-party/urlparse/README.rst new file mode 100644 index 00000000000000..4e8c470bcdebdc --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/README.rst @@ -0,0 +1,38 @@ +urlparse +======== + +urlparse is a URL parser compatible to `url_parser_parse_url()` from +`nodejs/http-parser `_ which +has been archived since 2022. urlparse only implements the strict +mode. + +There is a slight difference in a return code when they fail. +`url_parser_parse_url()` returns nonzero if it fails. +`urlparse_parse_url()` returns the negative error code +``URLPARSE_ERR_PARSE`` if it fails. + +A caller needs to call `http_parser_url_init()` before +`http_parser_parse_url()`. urlparse does not need a similar function +because `urlparse_parser_url()` initializes ``urlparse_url`` before +its use. + +`url_parser_parse_url()` historically does not follow any standards +like RFC 3986. Here is the allowed characters in each URL component: + +- scheme: ``A-Za-z`` +- userinfo: ``A-Za-z!$%&'()*+,-.:;=_~`` +- host: ``a-zA-Z0-9-.`` +- IPv6 host: ``A-Fa-f0-9.:`` + + - optionally followed by zone info which starts ``%`` and can + contain: ``A-Za-z0-9%-._~`` + + - and IPv6 host must be enclosed by ``[`` and ``]`` + +- port: ``0-9`` +- path: ``A-Za-z0-9!"$%&'()*+,-./:;<=>@[\]^_`{|}~`` +- query: ``A-Za-z0-9!"$%&'()*+,-./:;<=>?@[\]^_`{|}~`` +- fragment: ``A-Za-z0-9!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~`` + + - all consecutive ``#`` characters that precede a fragment are + treated as a single ``#``. A fragment cannot start with ``#``. diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/configure.ac b/deps/ngtcp2/ngtcp2/third-party/urlparse/configure.ac new file mode 100644 index 00000000000000..16818ca8fa04d2 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/configure.ac @@ -0,0 +1,183 @@ +# urlparse +# +# Copyright (c) 2024 urlparse contributors +# Copyright (c) 2023 sfparse contributors +# Copyright (c) 2019 nghttp3 contributors +# Copyright (c) 2016 ngtcp2 contributors +# Copyright (c) 2012 nghttp2 contributors +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +AC_PREREQ(2.61) +AC_INIT([urlparse], [0.0.0-DEV], [t-tujikawa@users.sourceforge.net]) +AC_CONFIG_AUX_DIR([.]) +AC_CONFIG_MACRO_DIR([m4]) +AC_CONFIG_HEADERS([config.h]) +AC_USE_SYSTEM_EXTENSIONS + +LT_PREREQ([2.2.6]) +LT_INIT() + +AC_CANONICAL_BUILD +AC_CANONICAL_HOST +AC_CANONICAL_TARGET + +AM_INIT_AUTOMAKE([subdir-objects]) + +m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) + +# See versioning rule: +# https://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html +AC_SUBST(LT_CURRENT, 0) +AC_SUBST(LT_REVISION, 0) +AC_SUBST(LT_AGE, 0) + +# Checks for command-line options +AC_ARG_ENABLE([werror], + [AS_HELP_STRING([--enable-werror], + [Turn on compile time warnings])], + [werror=$enableval], [werror=no]) + +# Checks for programs +AC_PROG_CC +AC_PROG_CPP +AC_PROG_INSTALL +AC_PROG_LN_S +AC_PROG_MAKE_SET +AC_PROG_MKDIR_P + +PKG_PROG_PKG_CONFIG([0.20]) + +# Checks for libraries. + +# Checks for header files. +AC_CHECK_HEADERS([ \ + assert.h \ + inttypes.h \ + stddef.h \ + stdint.h \ + stdlib.h \ + string.h \ + sys/types.h +]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_TYPE_SIZE_T +AC_TYPE_UINT8_T +AC_TYPE_UINT16_T +AC_TYPE_UINT32_T +AC_TYPE_UINT64_T + +# More compiler flags from nghttp2. +save_CFLAGS=$CFLAGS + +CFLAGS= + +if test "x$werror" != "xno"; then + # For C compiler + AX_CHECK_COMPILE_FLAG([-Wall], [CFLAGS="$CFLAGS -Wall"]) + AX_CHECK_COMPILE_FLAG([-Wextra], [CFLAGS="$CFLAGS -Wextra"]) + AX_CHECK_COMPILE_FLAG([-Werror], [CFLAGS="$CFLAGS -Werror"]) + AX_CHECK_COMPILE_FLAG([-Wmissing-prototypes], [CFLAGS="$CFLAGS -Wmissing-prototypes"]) + AX_CHECK_COMPILE_FLAG([-Wstrict-prototypes], [CFLAGS="$CFLAGS -Wstrict-prototypes"]) + AX_CHECK_COMPILE_FLAG([-Wmissing-declarations], [CFLAGS="$CFLAGS -Wmissing-declarations"]) + AX_CHECK_COMPILE_FLAG([-Wpointer-arith], [CFLAGS="$CFLAGS -Wpointer-arith"]) + AX_CHECK_COMPILE_FLAG([-Wdeclaration-after-statement], [CFLAGS="$CFLAGS -Wdeclaration-after-statement"]) + AX_CHECK_COMPILE_FLAG([-Wformat-security], [CFLAGS="$CFLAGS -Wformat-security"]) + AX_CHECK_COMPILE_FLAG([-Wwrite-strings], [CFLAGS="$CFLAGS -Wwrite-strings"]) + AX_CHECK_COMPILE_FLAG([-Wshadow], [CFLAGS="$CFLAGS -Wshadow"]) + AX_CHECK_COMPILE_FLAG([-Winline], [CFLAGS="$CFLAGS -Winline"]) + AX_CHECK_COMPILE_FLAG([-Wnested-externs], [CFLAGS="$CFLAGS -Wnested-externs"]) + AX_CHECK_COMPILE_FLAG([-Wfloat-equal], [CFLAGS="$CFLAGS -Wfloat-equal"]) + AX_CHECK_COMPILE_FLAG([-Wundef], [CFLAGS="$CFLAGS -Wundef"]) + AX_CHECK_COMPILE_FLAG([-Wendif-labels], [CFLAGS="$CFLAGS -Wendif-labels"]) + AX_CHECK_COMPILE_FLAG([-Wempty-body], [CFLAGS="$CFLAGS -Wempty-body"]) + AX_CHECK_COMPILE_FLAG([-Wcast-align], [CFLAGS="$CFLAGS -Wcast-align"]) + AX_CHECK_COMPILE_FLAG([-Wclobbered], [CFLAGS="$CFLAGS -Wclobbered"]) + AX_CHECK_COMPILE_FLAG([-Wvla], [CFLAGS="$CFLAGS -Wvla"]) + AX_CHECK_COMPILE_FLAG([-Wpragmas], [CFLAGS="$CFLAGS -Wpragmas"]) + AX_CHECK_COMPILE_FLAG([-Wunreachable-code], [CFLAGS="$CFLAGS -Wunreachable-code"]) + AX_CHECK_COMPILE_FLAG([-Waddress], [CFLAGS="$CFLAGS -Waddress"]) + AX_CHECK_COMPILE_FLAG([-Wattributes], [CFLAGS="$CFLAGS -Wattributes"]) + AX_CHECK_COMPILE_FLAG([-Wdiv-by-zero], [CFLAGS="$CFLAGS -Wdiv-by-zero"]) + AX_CHECK_COMPILE_FLAG([-Wshorten-64-to-32], [CFLAGS="$CFLAGS -Wshorten-64-to-32"]) + + AX_CHECK_COMPILE_FLAG([-Wconversion], [CFLAGS="$CFLAGS -Wconversion"]) + AX_CHECK_COMPILE_FLAG([-Wextended-offsetof], [CFLAGS="$CFLAGS -Wextended-offsetof"]) + AX_CHECK_COMPILE_FLAG([-Wformat-nonliteral], [CFLAGS="$CFLAGS -Wformat-nonliteral"]) + AX_CHECK_COMPILE_FLAG([-Wlanguage-extension-token], [CFLAGS="$CFLAGS -Wlanguage-extension-token"]) + AX_CHECK_COMPILE_FLAG([-Wmissing-field-initializers], [CFLAGS="$CFLAGS -Wmissing-field-initializers"]) + AX_CHECK_COMPILE_FLAG([-Wmissing-noreturn], [CFLAGS="$CFLAGS -Wmissing-noreturn"]) + AX_CHECK_COMPILE_FLAG([-Wmissing-variable-declarations], [CFLAGS="$CFLAGS -Wmissing-variable-declarations"]) + # Not used because we cannot change public structs + # AX_CHECK_COMPILE_FLAG([-Wpadded], [CFLAGS="$CFLAGS -Wpadded"]) + AX_CHECK_COMPILE_FLAG([-Wsign-conversion], [CFLAGS="$CFLAGS -Wsign-conversion"]) + # Not used because this basically disallows default case + # AX_CHECK_COMPILE_FLAG([-Wswitch-enum], [CFLAGS="$CFLAGS -Wswitch-enum"]) + AX_CHECK_COMPILE_FLAG([-Wunreachable-code-break], [CFLAGS="$CFLAGS -Wunreachable-code-break"]) + AX_CHECK_COMPILE_FLAG([-Wunused-macros], [CFLAGS="$CFLAGS -Wunused-macros"]) + AX_CHECK_COMPILE_FLAG([-Wunused-parameter], [CFLAGS="$CFLAGS -Wunused-parameter"]) + AX_CHECK_COMPILE_FLAG([-Wredundant-decls], [CFLAGS="$CFLAGS -Wredundant-decls"]) + # Only work with Clang for the moment + AX_CHECK_COMPILE_FLAG([-Wheader-guard], [CFLAGS="$CFLAGS -Wheader-guard"]) + AX_CHECK_COMPILE_FLAG([-Wsometimes-uninitialized], [CFLAGS="$CFLAGS -Wsometimes-uninitialized"]) + AX_CHECK_COMPILE_FLAG([-Wextra-semi], [CFLAGS="$CFLAGS -Wextra-semi"]) + + # Only work with gcc7 for the moment + AX_CHECK_COMPILE_FLAG([-Wduplicated-branches], [CFLAGS="$CFLAGS -Wduplicated-branches"]) + + # This is required because we pass format string as "const char*. + AX_CHECK_COMPILE_FLAG([-Wno-format-nonliteral], [CFLAGS="$CFLAGS -Wno-format-nonliteral"]) +fi + +WARNCFLAGS=$CFLAGS + +CFLAGS=$save_CFLAGS + +AC_SUBST([WARNCFLAGS]) + +AC_CONFIG_FILES([ + Makefile + doc/Makefile + doc/source/conf.py +]) +AC_OUTPUT + +AC_MSG_NOTICE([summary of build options: + + Package version: ${VERSION} + Library version: $LT_CURRENT:$LT_REVISION:$LT_AGE + Install prefix: ${prefix} + System types: + Build: ${build} + Host: ${host} + Target: ${target} + Compiler: + C preprocessor: ${CPP} + CPPFLAGS: ${CPPFLAGS} + C compiler: ${CC} + CFLAGS: ${CFLAGS} + LDFLAGS: ${LDFLAGS} + WARNCFLAGS: ${WARNCFLAGS} + LIBS: ${LIBS} + Library: + Shared: ${enable_shared} + Static: ${enable_static} +}') +]) diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/doc/mkapiref.py b/deps/ngtcp2/ngtcp2/third-party/urlparse/doc/mkapiref.py new file mode 100755 index 00000000000000..6ac4d58add8bb8 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/doc/mkapiref.py @@ -0,0 +1,396 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# urlparse +# +# Copyright (c) 2024 urlparse contributors +# Copyright (c) 2023 sfparse contributors +# Copyright (c) 2020 nghttp3 contributors +# Copyright (c) 2020 ngtcp2 contributors +# Copyright (c) 2012 Tatsuhiro Tsujikawa +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Generates API reference from C source code. + +import re, sys, argparse, os.path + +class FunctionDoc: + def __init__(self, name, content, domain, filename): + self.name = name + self.content = content + self.domain = domain + if self.domain == 'function': + self.funcname = re.search(r'(urlparse_[^ )]+)\(', self.name).group(1) + self.filename = filename + + def write(self, out): + out.write('.. {}:: {}\n'.format(self.domain, self.name)) + out.write('\n') + for line in self.content: + out.write(' {}\n'.format(line)) + +class StructDoc: + def __init__(self, name, content, members, member_domain): + self.name = name + self.content = content + self.members = members + self.member_domain = member_domain + + def write(self, out): + indents = ' ' + if self.name: + out.write('.. type:: {}\n'.format(self.name)) + out.write('\n') + for line in self.content: + out.write(' {}\n'.format(line)) + out.write('\n') + for name, content, indent in self.members: + if name.startswith('@union_'): + out.write('{} .. union:: {}\n'.format(indents * indent, name)) + if name.startswith('@struct_'): + out.write('{} .. {}:: struct {}\n'.format(indents * indent, self.member_domain, name)) + else: + out.write('{} .. {}:: {}\n'.format(indents * indent, self.member_domain, name)) + out.write('\n') + for line in content: + out.write('{} {}\n'.format(indents * indent, line)) + if name.startswith('@struct_'): + out.write('\n') + out.write('\n') + +class EnumDoc: + def __init__(self, name, content, members): + self.name = name + self.content = content + self.members = members + + def write(self, out): + if self.name: + out.write('.. type:: {}\n'.format(self.name)) + out.write('\n') + for line in self.content: + out.write(' {}\n'.format(line)) + out.write('\n') + for name, content in self.members: + out.write(' .. enum:: {}\n'.format(name)) + out.write('\n') + for line in content: + out.write(' {}\n'.format(line)) + out.write('\n') + +class MacroDoc: + def __init__(self, name, content): + self.name = name + self.content = content + + def write(self, out): + out.write('''.. macro:: {}\n'''.format(self.name)) + out.write('\n') + for line in self.content: + out.write(' {}\n'.format(line)) + +class MacroSectionDoc: + def __init__(self, content): + self.content = content + + def write(self, out): + out.write('\n') + c = ' '.join(self.content).strip() + out.write(c) + out.write('\n') + out.write('-' * len(c)) + out.write('\n\n') + +class TypedefDoc: + def __init__(self, name, content): + self.name = name + self.content = content + + def write(self, out): + out.write('''.. type:: {}\n'''.format(self.name)) + out.write('\n') + for line in self.content: + out.write(' {}\n'.format(line)) + +def make_api_ref(infile): + macros = [] + enums = [] + types = [] + functions = [] + while True: + line = infile.readline() + if not line: + break + elif line == '/**\n': + line = infile.readline() + doctype = line.split()[1] + if doctype == '@function': + functions.append(process_function('function', infile)) + elif doctype == '@functypedef': + types.append(process_function('type', infile)) + elif doctype == '@struct' or doctype == '@union': + types.append(process_struct(infile)) + elif doctype == '@enum': + enums.append(process_enum(infile)) + elif doctype == '@macro': + macros.append(process_macro(infile)) + elif doctype == '@macrosection': + macros.append(process_macrosection(infile)) + elif doctype == '@typedef': + types.append(process_typedef(infile)) + return macros, enums, types, functions + +def output( + title, indexfile, macrosfile, enumsfile, typesfile, funcsdir, + macros, enums, types, functions): + indexfile.write(''' +{title} +{titledecoration} + +.. toctree:: + :maxdepth: 1 + + {macros} + {enums} + {types} +'''.format( + title=title, titledecoration='='*len(title), + macros=os.path.splitext(os.path.basename(macrosfile.name))[0], + enums=os.path.splitext(os.path.basename(enumsfile.name))[0], + types=os.path.splitext(os.path.basename(typesfile.name))[0], +)) + + for doc in functions: + indexfile.write(' {}\n'.format(doc.funcname)) + + macrosfile.write(''' +Macros +====== +''') + for doc in macros: + doc.write(macrosfile) + + enumsfile.write(''' +Enums +===== +''') + for doc in enums: + doc.write(enumsfile) + + typesfile.write(''' +Types (structs, unions and typedefs) +==================================== +''') + for doc in types: + doc.write(typesfile) + + for doc in functions: + with open(os.path.join(funcsdir, doc.funcname + '.rst'), 'w') as f: + f.write(''' +{funcname} +{secul} + +Synopsis +-------- + +*#include <{filename}>* + +'''.format(funcname=doc.funcname, secul='='*len(doc.funcname), + filename=doc.filename)) + doc.write(f) + +def process_macro(infile): + content = read_content(infile) + lines = [] + while True: + line = infile.readline() + if not line: + break + line = line.rstrip() + lines.append(line.rstrip('\\')) + if not line.endswith('\\'): + break + + macro_name = re.sub(r'#define ', '', ''.join(lines)) + m = re.match(r'^[^( ]+(:?\(.*?\))?', macro_name) + macro_name = m.group(0) + return MacroDoc(macro_name, content) + +def process_macrosection(infile): + content = read_content(infile) + return MacroSectionDoc(content) + +def process_typedef(infile): + content = read_content(infile) + typedef = infile.readline() + typedef = re.sub(r';\n$', '', typedef) + typedef = re.sub(r'typedef ', '', typedef) + return TypedefDoc(typedef, content) + +def process_enum(infile): + members = [] + enum_name = None + content = read_content(infile) + while True: + line = infile.readline() + if not line: + break + elif re.match(r'\s*/\*\*\n', line): + member_content = read_content(infile) + line = infile.readline() + items = line.split() + member_name = items[0].rstrip(',') + if len(items) >= 3: + member_content.insert(0, '(``{}``) '\ + .format(' '.join(items[2:]).rstrip(','))) + members.append((member_name, member_content)) + elif line.startswith('}'): + enum_name = line.rstrip().split()[1] + enum_name = re.sub(r';$', '', enum_name) + break + return EnumDoc(enum_name, content, members) + +def process_struct(infile): + members = [] + struct_name = None + content = read_content(infile) + indent = 0 + anonstruct_name = None + anonstruct_member_name = None + anonstruct_member_content = None + anonstruct_members = [] + while True: + line = infile.readline() + if not line: + break + elif re.match(r'\s*/\*\*\n', line): + member_content = read_content(infile) + if '@anonunion_start' in member_content: + member_name = member_content[2].strip() + members.append((member_name, '', indent)) + indent += 1 + elif '@anonunion_end' in member_content: + indent -= 1 + elif '@anonstruct_start' in member_content: + anonstruct_name = member_content[2].strip() + anonstruct_member_content = member_content[4:] + indent += 1 + elif '@anonstruct_end' in member_content: + line = infile.readline() + anonstruct_member_name = re.sub(r'\s*}\s*', '', line).rstrip().rstrip(';') + indent -= 1 + members.append((anonstruct_name + ' ' + anonstruct_member_name, + anonstruct_member_content, indent)) + members.extend(anonstruct_members) + anonstruct_name = None + anonstruct_member_name = None + anonstruct_member_content = None + anonstruct_members = [] + else: + line = infile.readline() + member_name = line.rstrip().rstrip(';') + if anonstruct_name is None: + members.append((member_name, member_content, indent)) + else: + anonstruct_members.append((member_name, member_content, indent)) + elif line.startswith('}') or\ + (line.startswith('typedef ') and line.endswith(';\n')): + if line.startswith('}'): + index = 1 + else: + index = 3 + struct_name = line.rstrip().split()[index] + struct_name = re.sub(r';$', '', struct_name) + break + return StructDoc(struct_name, content, members, 'member') + +def process_function(domain, infile): + content = read_content(infile) + func_proto = [] + while True: + line = infile.readline() + if not line: + break + elif line == '\n': + break + else: + func_proto.append(line) + func_proto = ''.join(func_proto) + func_proto = re.sub(r'int (settings|callbacks)_version,', + '', func_proto) + func_proto = re.sub(r'_versioned\(', '(', func_proto) + func_proto = re.sub(r';\n$', '', func_proto) + func_proto = re.sub(r'\s+', ' ', func_proto) + func_proto = re.sub(r'typedef ', '', func_proto) + filename = os.path.basename(infile.name) + return FunctionDoc(func_proto, content, domain, filename) + +def read_content(infile): + content = [] + while True: + line = infile.readline() + if not line: + break + if re.match(r'\s*\*/\n', line): + break + else: + content.append(transform_content(line.rstrip())) + return content + +def arg_repl(matchobj): + return '*{}*'.format(matchobj.group(1).replace('*', '\\*')) + +def transform_content(content): + content = re.sub(r'^\s+\* ?', '', content) + content = re.sub(r'\|([^\s|]+)\|', arg_repl, content) + return content + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="Generate API reference") + parser.add_argument('--title', default='API Reference', + help='title of index page') + parser.add_argument('index', type=argparse.FileType('w'), + help='index output file') + parser.add_argument('macros', type=argparse.FileType('w'), + help='macros section output file. The filename should be macros.rst') + parser.add_argument('enums', type=argparse.FileType('w'), + help='enums section output file. The filename should be enums.rst') + parser.add_argument('types', type=argparse.FileType('w'), + help='types section output file. The filename should be types.rst') + parser.add_argument('funcsdir', + help='functions doc output dir') + parser.add_argument('files', nargs='+', type=argparse.FileType('r'), + help='source file') + args = parser.parse_args() + macros = [] + enums = [] + types = [] + funcs = [] + for infile in args.files: + m, e, t, f = make_api_ref(infile) + macros.extend(m) + enums.extend(e) + types.extend(t) + funcs.extend(f) + funcs.sort(key=lambda x: x.funcname) + output( + args.title, + args.index, args.macros, args.enums, args.types, args.funcsdir, + macros, enums, types, funcs) diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/doc/source/index.rst b/deps/ngtcp2/ngtcp2/third-party/urlparse/doc/source/index.rst new file mode 100644 index 00000000000000..0bf657a2749762 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/doc/source/index.rst @@ -0,0 +1,15 @@ +Welcome to urlparse's documentation! +==================================== + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + apiref + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/examples.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/examples.c new file mode 100644 index 00000000000000..78f2de106c92b0 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/examples.c @@ -0,0 +1 @@ +int main(void) { return 0; } diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/connect b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/connect new file mode 100644 index 00000000000000..cc56d45bb4bd77 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/connect @@ -0,0 +1 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()*+,;=:@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-:12345 \ No newline at end of file diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/connect-ipv6 b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/connect-ipv6 new file mode 100644 index 00000000000000..f5cd1c4b68cfd1 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/connect-ipv6 @@ -0,0 +1 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()*+,;=:@[abcdefABCDEF0123456789:.%abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%.-_~]:12345 \ No newline at end of file diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/example b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/example new file mode 100644 index 00000000000000..a364175574b437 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/example @@ -0,0 +1 @@ +https://example.com/ \ No newline at end of file diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/ipv6 b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/ipv6 new file mode 100644 index 00000000000000..4d03e8809575b5 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/ipv6 @@ -0,0 +1 @@ +https://abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()*+,;=:@[abcdefABCDEF0123456789:.%abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%.-_~]:12345/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()*+,;=:@/"<>[\]^`{|}?abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()*+,;=:@/?"<>[\]^`{|}#abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()*+,;=:@/?"<>[\]^`{|} \ No newline at end of file diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/long b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/long new file mode 100644 index 00000000000000..a9026edba99c20 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/corpus/parser/long @@ -0,0 +1 @@ +https://abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()*+,;=:@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-:12345/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()*+,;=:@/"<>[\]^`{|}?abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()*+,;=:@/?"<>[\]^`{|}#abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()*+,;=:@/?"<>[\]^`{|} \ No newline at end of file diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/parser.cc b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/parser.cc new file mode 100644 index 00000000000000..57e5256a9d1b25 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/fuzz/parser.cc @@ -0,0 +1,72 @@ +#include +#include +#include + +#include "urlparse.h" +#include "http-parser/http_parser.h" + +namespace { +/* Copied from https://github.com/nodejs/http-parser */ +void dump_url(const uint8_t *url, const urlparse_url *u) { + size_t i; + + fprintf(stderr, "\tfield_set: 0x%x, port: %u\n", u->field_set, u->port); + for (i = 0; i < URLPARSE_MAX; i++) { + if ((u->field_set & (1 << i)) == 0) { + fprintf(stderr, "\tfield_data[%zu]: unset\n", i); + continue; + } + + fprintf(stderr, "\tfield_data[%zu]: off: %u len: %u part: \"%.*s\"\n", i, + u->field_data[i].off, u->field_data[i].len, u->field_data[i].len, + url + u->field_data[i].off); + } +} +} // namespace + +namespace { +int fuzz(const uint8_t *data, size_t size, int is_connect) { + urlparse_url u; + struct http_parser_url hu; + int rv1, rv2; + + rv1 = urlparse_parse_url(reinterpret_cast(data), size, + is_connect, &u); + memset(&hu, 0, sizeof(hu)); + rv2 = http_parser_parse_url(reinterpret_cast(data), size, + is_connect, &hu); + if (rv2 != 0) { + rv2 = URLPARSE_ERR_PARSE; + } + + if (rv1 != rv2) { + fprintf( + stderr, + "urlparse_parse_url(%d) and http_parser_parse_url(%d) disagree with " + "is_connect = %d\n", + rv1, rv2, is_connect); + return -1; + } + + if (rv1 == 0 && memcmp(&hu, &u, sizeof(u)) != 0) { + fprintf(stderr, "is_connect = %d\n", is_connect); + fprintf(stderr, "target http_parser_url:\n"); + dump_url(data, reinterpret_cast(&hu)); + fprintf(stderr, "result urlparse_url:\n"); + dump_url(data, &u); + + return -1; + } + + return 0; +} +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (fuzz(data, size, /* is_connect = */ 0) != 0 || + fuzz(data, size, /* is_connect = */ 1) != 0) { + abort(); + } + + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/.mailmap b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/.mailmap new file mode 100644 index 00000000000000..278d1412637240 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/.mailmap @@ -0,0 +1,8 @@ +# update AUTHORS with: +# git log --all --reverse --format='%aN <%aE>' | perl -ne 'BEGIN{print "# Authors ordered by first contribution.\n"} print unless $h{$_}; $h{$_} = 1' > AUTHORS +Ryan Dahl +Salman Haq +Simon Zimmermann +Thomas LE ROUX LE ROUX Thomas +Thomas LE ROUX Thomas LE ROUX +Fedor Indutny diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/.travis.yml b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/.travis.yml new file mode 100644 index 00000000000000..4b038e6e62d638 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/.travis.yml @@ -0,0 +1,13 @@ +language: c + +compiler: + - clang + - gcc + +script: + - "make" + +notifications: + email: false + irc: + - "irc.freenode.net#node-ci" diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/AUTHORS b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/AUTHORS new file mode 100644 index 00000000000000..5323b685caefb9 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/AUTHORS @@ -0,0 +1,68 @@ +# Authors ordered by first contribution. +Ryan Dahl +Jeremy Hinegardner +Sergey Shepelev +Joe Damato +tomika +Phoenix Sol +Cliff Frey +Ewen Cheslack-Postava +Santiago Gala +Tim Becker +Jeff Terrace +Ben Noordhuis +Nathan Rajlich +Mark Nottingham +Aman Gupta +Tim Becker +Sean Cunningham +Peter Griess +Salman Haq +Cliff Frey +Jon Kolb +Fouad Mardini +Paul Querna +Felix Geisendörfer +koichik +Andre Caron +Ivo Raisr +James McLaughlin +David Gwynne +Thomas LE ROUX +Randy Rizun +Andre Louis Caron +Simon Zimmermann +Erik Dubbelboer +Martell Malone +Bertrand Paquet +BogDan Vatra +Peter Faiman +Corey Richardson +Tóth Tamás +Cam Swords +Chris Dickinson +Uli Köhler +Charlie Somerville +Patrik Stutz +Fedor Indutny +runner +Alexis Campailla +David Wragg +Vinnie Falco +Alex Butum +Rex Feng +Alex Kocharin +Mark Koopman +Helge Heß +Alexis La Goutte +George Miroshnykov +Maciej Małecki +Marc O'Morain +Jeff Pinner +Timothy J Fontaine +Akagi201 +Romain Giraud +Jay Satiro +Arne Steen +Kjell Schubert +Olivier Mengué diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/LICENSE-MIT b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/LICENSE-MIT new file mode 100644 index 00000000000000..1ec0ab4e1746be --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright Joyent, Inc. and other Node contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/README.md b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/README.md new file mode 100644 index 00000000000000..e38d3a57881a6a --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/README.md @@ -0,0 +1,249 @@ +HTTP Parser +=========== + +http-parser is [**not** actively maintained](https://github.com/nodejs/http-parser/issues/522). +New projects and projects looking to migrate should consider [llhttp](https://github.com/nodejs/llhttp). + +[![Build Status](https://api.travis-ci.org/nodejs/http-parser.svg?branch=master)](https://travis-ci.org/nodejs/http-parser) + +This is a parser for HTTP messages written in C. It parses both requests and +responses. The parser is designed to be used in performance HTTP +applications. It does not make any syscalls nor allocations, it does not +buffer data, it can be interrupted at anytime. Depending on your +architecture, it only requires about 40 bytes of data per message +stream (in a web server that is per connection). + +Features: + + * No dependencies + * Handles persistent streams (keep-alive). + * Decodes chunked encoding. + * Upgrade support + * Defends against buffer overflow attacks. + +The parser extracts the following information from HTTP messages: + + * Header fields and values + * Content-Length + * Request method + * Response status code + * Transfer-Encoding + * HTTP version + * Request URL + * Message body + + +Usage +----- + +One `http_parser` object is used per TCP connection. Initialize the struct +using `http_parser_init()` and set the callbacks. That might look something +like this for a request parser: +```c +http_parser_settings settings; +settings.on_url = my_url_callback; +settings.on_header_field = my_header_field_callback; +/* ... */ + +http_parser *parser = malloc(sizeof(http_parser)); +http_parser_init(parser, HTTP_REQUEST); +parser->data = my_socket; +``` + +When data is received on the socket execute the parser and check for errors. + +```c +size_t len = 80*1024, nparsed; +char buf[len]; +ssize_t recved; + +recved = recv(fd, buf, len, 0); + +if (recved < 0) { + /* Handle error. */ +} + +/* Start up / continue the parser. + * Note we pass recved==0 to signal that EOF has been received. + */ +nparsed = http_parser_execute(parser, &settings, buf, recved); + +if (parser->upgrade) { + /* handle new protocol */ +} else if (nparsed != recved) { + /* Handle error. Usually just close the connection. */ +} +``` + +`http_parser` needs to know where the end of the stream is. For example, sometimes +servers send responses without Content-Length and expect the client to +consume input (for the body) until EOF. To tell `http_parser` about EOF, give +`0` as the fourth parameter to `http_parser_execute()`. Callbacks and errors +can still be encountered during an EOF, so one must still be prepared +to receive them. + +Scalar valued message information such as `status_code`, `method`, and the +HTTP version are stored in the parser structure. This data is only +temporally stored in `http_parser` and gets reset on each new message. If +this information is needed later, copy it out of the structure during the +`headers_complete` callback. + +The parser decodes the transfer-encoding for both requests and responses +transparently. That is, a chunked encoding is decoded before being sent to +the on_body callback. + + +The Special Problem of Upgrade +------------------------------ + +`http_parser` supports upgrading the connection to a different protocol. An +increasingly common example of this is the WebSocket protocol which sends +a request like + + GET /demo HTTP/1.1 + Upgrade: WebSocket + Connection: Upgrade + Host: example.com + Origin: http://example.com + WebSocket-Protocol: sample + +followed by non-HTTP data. + +(See [RFC6455](https://tools.ietf.org/html/rfc6455) for more information the +WebSocket protocol.) + +To support this, the parser will treat this as a normal HTTP message without a +body, issuing both on_headers_complete and on_message_complete callbacks. However +http_parser_execute() will stop parsing at the end of the headers and return. + +The user is expected to check if `parser->upgrade` has been set to 1 after +`http_parser_execute()` returns. Non-HTTP data begins at the buffer supplied +offset by the return value of `http_parser_execute()`. + + +Callbacks +--------- + +During the `http_parser_execute()` call, the callbacks set in +`http_parser_settings` will be executed. The parser maintains state and +never looks behind, so buffering the data is not necessary. If you need to +save certain data for later usage, you can do that from the callbacks. + +There are two types of callbacks: + +* notification `typedef int (*http_cb) (http_parser*);` + Callbacks: on_message_begin, on_headers_complete, on_message_complete. +* data `typedef int (*http_data_cb) (http_parser*, const char *at, size_t length);` + Callbacks: (requests only) on_url, + (common) on_header_field, on_header_value, on_body; + +Callbacks must return 0 on success. Returning a non-zero value indicates +error to the parser, making it exit immediately. + +For cases where it is necessary to pass local information to/from a callback, +the `http_parser` object's `data` field can be used. +An example of such a case is when using threads to handle a socket connection, +parse a request, and then give a response over that socket. By instantiation +of a thread-local struct containing relevant data (e.g. accepted socket, +allocated memory for callbacks to write into, etc), a parser's callbacks are +able to communicate data between the scope of the thread and the scope of the +callback in a threadsafe manner. This allows `http_parser` to be used in +multi-threaded contexts. + +Example: +```c + typedef struct { + socket_t sock; + void* buffer; + int buf_len; + } custom_data_t; + + +int my_url_callback(http_parser* parser, const char *at, size_t length) { + /* access to thread local custom_data_t struct. + Use this access save parsed data for later use into thread local + buffer, or communicate over socket + */ + parser->data; + ... + return 0; +} + +... + +void http_parser_thread(socket_t sock) { + int nparsed = 0; + /* allocate memory for user data */ + custom_data_t *my_data = malloc(sizeof(custom_data_t)); + + /* some information for use by callbacks. + * achieves thread -> callback information flow */ + my_data->sock = sock; + + /* instantiate a thread-local parser */ + http_parser *parser = malloc(sizeof(http_parser)); + http_parser_init(parser, HTTP_REQUEST); /* initialise parser */ + /* this custom data reference is accessible through the reference to the + parser supplied to callback functions */ + parser->data = my_data; + + http_parser_settings settings; /* set up callbacks */ + settings.on_url = my_url_callback; + + /* execute parser */ + nparsed = http_parser_execute(parser, &settings, buf, recved); + + ... + /* parsed information copied from callback. + can now perform action on data copied into thread-local memory from callbacks. + achieves callback -> thread information flow */ + my_data->buffer; + ... +} + +``` + +In case you parse HTTP message in chunks (i.e. `read()` request line +from socket, parse, read half headers, parse, etc) your data callbacks +may be called more than once. `http_parser` guarantees that data pointer is only +valid for the lifetime of callback. You can also `read()` into a heap allocated +buffer to avoid copying memory around if this fits your application. + +Reading headers may be a tricky task if you read/parse headers partially. +Basically, you need to remember whether last header callback was field or value +and apply the following logic: + + (on_header_field and on_header_value shortened to on_h_*) + ------------------------ ------------ -------------------------------------------- + | State (prev. callback) | Callback | Description/action | + ------------------------ ------------ -------------------------------------------- + | nothing (first call) | on_h_field | Allocate new buffer and copy callback data | + | | | into it | + ------------------------ ------------ -------------------------------------------- + | value | on_h_field | New header started. | + | | | Copy current name,value buffers to headers | + | | | list and allocate new buffer for new name | + ------------------------ ------------ -------------------------------------------- + | field | on_h_field | Previous name continues. Reallocate name | + | | | buffer and append callback data to it | + ------------------------ ------------ -------------------------------------------- + | field | on_h_value | Value for current header started. Allocate | + | | | new buffer and copy callback data to it | + ------------------------ ------------ -------------------------------------------- + | value | on_h_value | Value continues. Reallocate value buffer | + | | | and append callback data to it | + ------------------------ ------------ -------------------------------------------- + + +Parsing URLs +------------ + +A simplistic zero-copy URL parser is provided as `http_parser_parse_url()`. +Users of this library may wish to use it to parse URLs constructed from +consecutive `on_url` callbacks. + +See examples of reading in headers: + +* [partial example](http://gist.github.com/155877) in C +* [from http-parser tests](http://github.com/joyent/http-parser/blob/37a0ff8/test.c#L403) in C +* [from Node library](http://github.com/joyent/node/blob/842eaf4/src/http.js#L284) in Javascript diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/bench.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/bench.c new file mode 100644 index 00000000000000..678f5556c59880 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/bench.c @@ -0,0 +1,128 @@ +/* Copyright Fedor Indutny. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include "http_parser.h" +#include +#include +#include +#include +#include + +/* 8 gb */ +static const int64_t kBytes = 8LL << 30; + +static const char data[] = + "POST /joyent/http-parser HTTP/1.1\r\n" + "Host: github.com\r\n" + "DNT: 1\r\n" + "Accept-Encoding: gzip, deflate, sdch\r\n" + "Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4\r\n" + "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/39.0.2171.65 Safari/537.36\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9," + "image/webp,*/*;q=0.8\r\n" + "Referer: https://github.com/joyent/http-parser\r\n" + "Connection: keep-alive\r\n" + "Transfer-Encoding: chunked\r\n" + "Cache-Control: max-age=0\r\n\r\nb\r\nhello world\r\n0\r\n"; +static const size_t data_len = sizeof(data) - 1; + +static int on_info(http_parser* p) { + return 0; +} + + +static int on_data(http_parser* p, const char *at, size_t length) { + return 0; +} + +static http_parser_settings settings = { + .on_message_begin = on_info, + .on_headers_complete = on_info, + .on_message_complete = on_info, + .on_header_field = on_data, + .on_header_value = on_data, + .on_url = on_data, + .on_status = on_data, + .on_body = on_data +}; + +int bench(int iter_count, int silent) { + struct http_parser parser; + int i; + int err; + struct timeval start; + struct timeval end; + + if (!silent) { + err = gettimeofday(&start, NULL); + assert(err == 0); + } + + fprintf(stderr, "req_len=%d\n", (int) data_len); + for (i = 0; i < iter_count; i++) { + size_t parsed; + http_parser_init(&parser, HTTP_REQUEST); + + parsed = http_parser_execute(&parser, &settings, data, data_len); + assert(parsed == data_len); + } + + if (!silent) { + double elapsed; + double bw; + double total; + + err = gettimeofday(&end, NULL); + assert(err == 0); + + fprintf(stdout, "Benchmark result:\n"); + + elapsed = (double) (end.tv_sec - start.tv_sec) + + (end.tv_usec - start.tv_usec) * 1e-6f; + + total = (double) iter_count * data_len; + bw = (double) total / elapsed; + + fprintf(stdout, "%.2f mb | %.2f mb/s | %.2f req/sec | %.2f s\n", + (double) total / (1024 * 1024), + bw / (1024 * 1024), + (double) iter_count / elapsed, + elapsed); + + fflush(stdout); + } + + return 0; +} + +int main(int argc, char** argv) { + int64_t iterations; + + iterations = kBytes / (int64_t) data_len; + if (argc == 2 && strcmp(argv[1], "infinite") == 0) { + for (;;) + bench(iterations, 1); + return 0; + } else { + return bench(iterations, 0); + } +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/contrib/parsertrace.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/contrib/parsertrace.c new file mode 100644 index 00000000000000..3daa7f46a19370 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/contrib/parsertrace.c @@ -0,0 +1,157 @@ +/* Copyright Joyent, Inc. and other Node contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* Dump what the parser finds to stdout as it happen */ + +#include "http_parser.h" +#include +#include +#include + +int on_message_begin(http_parser* _) { + (void)_; + printf("\n***MESSAGE BEGIN***\n\n"); + return 0; +} + +int on_headers_complete(http_parser* _) { + (void)_; + printf("\n***HEADERS COMPLETE***\n\n"); + return 0; +} + +int on_message_complete(http_parser* _) { + (void)_; + printf("\n***MESSAGE COMPLETE***\n\n"); + return 0; +} + +int on_url(http_parser* _, const char* at, size_t length) { + (void)_; + printf("Url: %.*s\n", (int)length, at); + return 0; +} + +int on_header_field(http_parser* _, const char* at, size_t length) { + (void)_; + printf("Header field: %.*s\n", (int)length, at); + return 0; +} + +int on_header_value(http_parser* _, const char* at, size_t length) { + (void)_; + printf("Header value: %.*s\n", (int)length, at); + return 0; +} + +int on_body(http_parser* _, const char* at, size_t length) { + (void)_; + printf("Body: %.*s\n", (int)length, at); + return 0; +} + +void usage(const char* name) { + fprintf(stderr, + "Usage: %s $type $filename\n" + " type: -x, where x is one of {r,b,q}\n" + " parses file as a Response, reQuest, or Both\n", + name); + exit(EXIT_FAILURE); +} + +int main(int argc, char* argv[]) { + enum http_parser_type file_type; + + if (argc != 3) { + usage(argv[0]); + } + + char* type = argv[1]; + if (type[0] != '-') { + usage(argv[0]); + } + + switch (type[1]) { + /* in the case of "-", type[1] will be NUL */ + case 'r': + file_type = HTTP_RESPONSE; + break; + case 'q': + file_type = HTTP_REQUEST; + break; + case 'b': + file_type = HTTP_BOTH; + break; + default: + usage(argv[0]); + } + + char* filename = argv[2]; + FILE* file = fopen(filename, "r"); + if (file == NULL) { + perror("fopen"); + goto fail; + } + + fseek(file, 0, SEEK_END); + long file_length = ftell(file); + if (file_length == -1) { + perror("ftell"); + goto fail; + } + fseek(file, 0, SEEK_SET); + + char* data = malloc(file_length); + if (fread(data, 1, file_length, file) != (size_t)file_length) { + fprintf(stderr, "couldn't read entire file\n"); + free(data); + goto fail; + } + + http_parser_settings settings; + memset(&settings, 0, sizeof(settings)); + settings.on_message_begin = on_message_begin; + settings.on_url = on_url; + settings.on_header_field = on_header_field; + settings.on_header_value = on_header_value; + settings.on_headers_complete = on_headers_complete; + settings.on_body = on_body; + settings.on_message_complete = on_message_complete; + + http_parser parser; + http_parser_init(&parser, file_type); + size_t nparsed = http_parser_execute(&parser, &settings, data, file_length); + free(data); + + if (nparsed != (size_t)file_length) { + fprintf(stderr, + "Error: %s (%s)\n", + http_errno_description(HTTP_PARSER_ERRNO(&parser)), + http_errno_name(HTTP_PARSER_ERRNO(&parser))); + goto fail; + } + + return EXIT_SUCCESS; + +fail: + fclose(file); + return EXIT_FAILURE; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/contrib/url_parser.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/contrib/url_parser.c new file mode 100644 index 00000000000000..f235bed9e483dc --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/contrib/url_parser.c @@ -0,0 +1,47 @@ +#include "http_parser.h" +#include +#include + +void +dump_url (const char *url, const struct http_parser_url *u) +{ + unsigned int i; + + printf("\tfield_set: 0x%x, port: %u\n", u->field_set, u->port); + for (i = 0; i < UF_MAX; i++) { + if ((u->field_set & (1 << i)) == 0) { + printf("\tfield_data[%u]: unset\n", i); + continue; + } + + printf("\tfield_data[%u]: off: %u, len: %u, part: %.*s\n", + i, + u->field_data[i].off, + u->field_data[i].len, + u->field_data[i].len, + url + u->field_data[i].off); + } +} + +int main(int argc, char ** argv) { + struct http_parser_url u; + int len, connect, result; + + if (argc != 3) { + printf("Syntax : %s connect|get url\n", argv[0]); + return 1; + } + len = strlen(argv[2]); + connect = strcmp("connect", argv[1]) == 0 ? 1 : 0; + printf("Parsing %s, connect %d\n", argv[2], connect); + + http_parser_url_init(&u); + result = http_parser_parse_url(argv[2], len, connect, &u); + if (result != 0) { + printf("Parse error : %d\n", result); + return result; + } + printf("Parse ok, result : \n"); + dump_url(argv[2], &u); + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/fuzzers/fuzz_parser.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/fuzzers/fuzz_parser.c new file mode 100644 index 00000000000000..1a8442c9b637a6 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/fuzzers/fuzz_parser.c @@ -0,0 +1,26 @@ +#include +#include +#include +#include "http_parser.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + static const http_parser_settings settings_null = { + .on_message_begin = 0 + , .on_header_field = 0 + ,.on_header_value = 0 + ,.on_url = 0 + ,.on_status = 0 + ,.on_body = 0 + ,.on_headers_complete = 0 + ,.on_message_complete = 0 + ,.on_chunk_header = 0 + ,.on_chunk_complete = 0 + }; + + http_parser parser; + http_parser_init(&parser, HTTP_BOTH); + http_parser_execute(&parser, &settings_null, (char*)data, size); + + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/fuzzers/fuzz_url.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/fuzzers/fuzz_url.c new file mode 100644 index 00000000000000..eca11a2edce552 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/fuzzers/fuzz_url.c @@ -0,0 +1,14 @@ +#include +#include +#include +#include "http_parser.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + struct http_parser_url u; + http_parser_url_init(&u); + http_parser_parse_url((char*)data, size, 0, &u); + http_parser_parse_url((char*)data, size, 1, &u); + + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/http_parser.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/http_parser.c new file mode 100644 index 00000000000000..9be003e7322eec --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/http_parser.c @@ -0,0 +1,2575 @@ +/* Copyright Joyent, Inc. and other Node contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include "http_parser.h" +#include +#include +#include +#include +#include + +static uint32_t max_header_size = HTTP_MAX_HEADER_SIZE; + +#ifndef ULLONG_MAX +# define ULLONG_MAX ((uint64_t) -1) /* 2^64-1 */ +#endif + +#ifndef MIN +# define MIN(a,b) ((a) < (b) ? (a) : (b)) +#endif + +#ifndef ARRAY_SIZE +# define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#endif + +#ifndef BIT_AT +# define BIT_AT(a, i) \ + (!!((unsigned int) (a)[(unsigned int) (i) >> 3] & \ + (1 << ((unsigned int) (i) & 7)))) +#endif + +#ifndef ELEM_AT +# define ELEM_AT(a, i, v) ((unsigned int) (i) < ARRAY_SIZE(a) ? (a)[(i)] : (v)) +#endif + +#define SET_ERRNO(e) \ +do { \ + parser->nread = nread; \ + parser->http_errno = (e); \ +} while(0) + +#define CURRENT_STATE() p_state +#define UPDATE_STATE(V) p_state = (enum state) (V); +#define RETURN(V) \ +do { \ + parser->nread = nread; \ + parser->state = CURRENT_STATE(); \ + return (V); \ +} while (0); +#define REEXECUTE() \ + goto reexecute; \ + + +#ifdef __GNUC__ +# define LIKELY(X) __builtin_expect(!!(X), 1) +# define UNLIKELY(X) __builtin_expect(!!(X), 0) +#else +# define LIKELY(X) (X) +# define UNLIKELY(X) (X) +#endif + + +/* Run the notify callback FOR, returning ER if it fails */ +#define CALLBACK_NOTIFY_(FOR, ER) \ +do { \ + assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ + \ + if (LIKELY(settings->on_##FOR)) { \ + parser->state = CURRENT_STATE(); \ + if (UNLIKELY(0 != settings->on_##FOR(parser))) { \ + SET_ERRNO(HPE_CB_##FOR); \ + } \ + UPDATE_STATE(parser->state); \ + \ + /* We either errored above or got paused; get out */ \ + if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \ + return (ER); \ + } \ + } \ +} while (0) + +/* Run the notify callback FOR and consume the current byte */ +#define CALLBACK_NOTIFY(FOR) CALLBACK_NOTIFY_(FOR, p - data + 1) + +/* Run the notify callback FOR and don't consume the current byte */ +#define CALLBACK_NOTIFY_NOADVANCE(FOR) CALLBACK_NOTIFY_(FOR, p - data) + +/* Run data callback FOR with LEN bytes, returning ER if it fails */ +#define CALLBACK_DATA_(FOR, LEN, ER) \ +do { \ + assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ + \ + if (FOR##_mark) { \ + if (LIKELY(settings->on_##FOR)) { \ + parser->state = CURRENT_STATE(); \ + if (UNLIKELY(0 != \ + settings->on_##FOR(parser, FOR##_mark, (LEN)))) { \ + SET_ERRNO(HPE_CB_##FOR); \ + } \ + UPDATE_STATE(parser->state); \ + \ + /* We either errored above or got paused; get out */ \ + if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \ + return (ER); \ + } \ + } \ + FOR##_mark = NULL; \ + } \ +} while (0) + +/* Run the data callback FOR and consume the current byte */ +#define CALLBACK_DATA(FOR) \ + CALLBACK_DATA_(FOR, p - FOR##_mark, p - data + 1) + +/* Run the data callback FOR and don't consume the current byte */ +#define CALLBACK_DATA_NOADVANCE(FOR) \ + CALLBACK_DATA_(FOR, p - FOR##_mark, p - data) + +/* Set the mark FOR; non-destructive if mark is already set */ +#define MARK(FOR) \ +do { \ + if (!FOR##_mark) { \ + FOR##_mark = p; \ + } \ +} while (0) + +/* Don't allow the total size of the HTTP headers (including the status + * line) to exceed max_header_size. This check is here to protect + * embedders against denial-of-service attacks where the attacker feeds + * us a never-ending header that the embedder keeps buffering. + * + * This check is arguably the responsibility of embedders but we're doing + * it on the embedder's behalf because most won't bother and this way we + * make the web a little safer. max_header_size is still far bigger + * than any reasonable request or response so this should never affect + * day-to-day operation. + */ +#define COUNT_HEADER_SIZE(V) \ +do { \ + nread += (uint32_t)(V); \ + if (UNLIKELY(nread > max_header_size)) { \ + SET_ERRNO(HPE_HEADER_OVERFLOW); \ + goto error; \ + } \ +} while (0) + + +#define PROXY_CONNECTION "proxy-connection" +#define CONNECTION "connection" +#define CONTENT_LENGTH "content-length" +#define TRANSFER_ENCODING "transfer-encoding" +#define UPGRADE "upgrade" +#define CHUNKED "chunked" +#define KEEP_ALIVE "keep-alive" +#define CLOSE "close" + + +static const char *method_strings[] = + { +#define XX(num, name, string) #string, + HTTP_METHOD_MAP(XX) +#undef XX + }; + + +/* Tokens as defined by rfc 2616. Also lowercases them. + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + */ +static const char tokens[256] = { +/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ + ' ', '!', 0, '#', '$', '%', '&', '\'', +/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ + 0, 0, '*', '+', 0, '-', '.', 0, +/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ + '0', '1', '2', '3', '4', '5', '6', '7', +/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ + '8', '9', 0, 0, 0, 0, 0, 0, +/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ + 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', +/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', +/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', +/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ + 'x', 'y', 'z', 0, 0, 0, '^', '_', +/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ + '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', +/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', +/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', +/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ + 'x', 'y', 'z', 0, '|', 0, '~', 0 }; + + +static const int8_t unhex[256] = + {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + , 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1,-1,-1,-1,-1 + ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + }; + + +#if HTTP_PARSER_STRICT +# define T(v) 0 +#else +# define T(v) v +#endif + + +static const uint8_t normal_url_char[32] = { +/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ + 0 | T(2) | 0 | 0 | T(16) | 0 | 0 | 0, +/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ + 0 | 2 | 4 | 0 | 16 | 32 | 64 | 128, +/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, +/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, }; + +#undef T + +enum state + { s_dead = 1 /* important that this is > 0 */ + + , s_start_req_or_res + , s_res_or_resp_H + , s_start_res + , s_res_H + , s_res_HT + , s_res_HTT + , s_res_HTTP + , s_res_http_major + , s_res_http_dot + , s_res_http_minor + , s_res_http_end + , s_res_first_status_code + , s_res_status_code + , s_res_status_start + , s_res_status + , s_res_line_almost_done + + , s_start_req + + , s_req_method + , s_req_spaces_before_url + , s_req_schema + , s_req_schema_slash + , s_req_schema_slash_slash + , s_req_server_start + , s_req_server + , s_req_server_with_at + , s_req_path + , s_req_query_string_start + , s_req_query_string + , s_req_fragment_start + , s_req_fragment + , s_req_http_start + , s_req_http_H + , s_req_http_HT + , s_req_http_HTT + , s_req_http_HTTP + , s_req_http_I + , s_req_http_IC + , s_req_http_major + , s_req_http_dot + , s_req_http_minor + , s_req_http_end + , s_req_line_almost_done + + , s_header_field_start + , s_header_field + , s_header_value_discard_ws + , s_header_value_discard_ws_almost_done + , s_header_value_discard_lws + , s_header_value_start + , s_header_value + , s_header_value_lws + + , s_header_almost_done + + , s_chunk_size_start + , s_chunk_size + , s_chunk_parameters + , s_chunk_size_almost_done + + , s_headers_almost_done + , s_headers_done + + /* Important: 's_headers_done' must be the last 'header' state. All + * states beyond this must be 'body' states. It is used for overflow + * checking. See the PARSING_HEADER() macro. + */ + + , s_chunk_data + , s_chunk_data_almost_done + , s_chunk_data_done + + , s_body_identity + , s_body_identity_eof + + , s_message_done + }; + + +#define PARSING_HEADER(state) (state <= s_headers_done) + + +enum header_states + { h_general = 0 + , h_C + , h_CO + , h_CON + + , h_matching_connection + , h_matching_proxy_connection + , h_matching_content_length + , h_matching_transfer_encoding + , h_matching_upgrade + + , h_connection + , h_content_length + , h_content_length_num + , h_content_length_ws + , h_transfer_encoding + , h_upgrade + + , h_matching_transfer_encoding_token_start + , h_matching_transfer_encoding_chunked + , h_matching_transfer_encoding_token + + , h_matching_connection_token_start + , h_matching_connection_keep_alive + , h_matching_connection_close + , h_matching_connection_upgrade + , h_matching_connection_token + + , h_transfer_encoding_chunked + , h_connection_keep_alive + , h_connection_close + , h_connection_upgrade + }; + +enum http_host_state + { + s_http_host_dead = 1 + , s_http_userinfo_start + , s_http_userinfo + , s_http_host_start + , s_http_host_v6_start + , s_http_host + , s_http_host_v6 + , s_http_host_v6_end + , s_http_host_v6_zone_start + , s_http_host_v6_zone + , s_http_host_port_start + , s_http_host_port +}; + +/* Macros for character classes; depends on strict-mode */ +#define CR '\r' +#define LF '\n' +#define LOWER(c) (unsigned char)(c | 0x20) +#define IS_ALPHA(c) (LOWER(c) >= 'a' && LOWER(c) <= 'z') +#define IS_NUM(c) ((c) >= '0' && (c) <= '9') +#define IS_ALPHANUM(c) (IS_ALPHA(c) || IS_NUM(c)) +#define IS_HEX(c) (IS_NUM(c) || (LOWER(c) >= 'a' && LOWER(c) <= 'f')) +#define IS_MARK(c) ((c) == '-' || (c) == '_' || (c) == '.' || \ + (c) == '!' || (c) == '~' || (c) == '*' || (c) == '\'' || (c) == '(' || \ + (c) == ')') +#define IS_USERINFO_CHAR(c) (IS_ALPHANUM(c) || IS_MARK(c) || (c) == '%' || \ + (c) == ';' || (c) == ':' || (c) == '&' || (c) == '=' || (c) == '+' || \ + (c) == '$' || (c) == ',') + +#define STRICT_TOKEN(c) ((c == ' ') ? 0 : tokens[(unsigned char)c]) + +#if HTTP_PARSER_STRICT +#define TOKEN(c) STRICT_TOKEN(c) +#define IS_URL_CHAR(c) (BIT_AT(normal_url_char, (unsigned char)c)) +#define IS_HOST_CHAR(c) (IS_ALPHANUM(c) || (c) == '.' || (c) == '-') +#else +#define TOKEN(c) tokens[(unsigned char)c] +#define IS_URL_CHAR(c) \ + (BIT_AT(normal_url_char, (unsigned char)c) || ((c) & 0x80)) +#define IS_HOST_CHAR(c) \ + (IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_') +#endif + +/** + * Verify that a char is a valid visible (printable) US-ASCII + * character or %x80-FF + **/ +#define IS_HEADER_CHAR(ch) \ + (ch == CR || ch == LF || ch == 9 || ((unsigned char)ch > 31 && ch != 127)) + +#define start_state (parser->type == HTTP_REQUEST ? s_start_req : s_start_res) + + +#if HTTP_PARSER_STRICT +# define STRICT_CHECK(cond) \ +do { \ + if (cond) { \ + SET_ERRNO(HPE_STRICT); \ + goto error; \ + } \ +} while (0) +# define NEW_MESSAGE() (http_should_keep_alive(parser) ? start_state : s_dead) +#else +# define STRICT_CHECK(cond) +# define NEW_MESSAGE() start_state +#endif + + +/* Map errno values to strings for human-readable output */ +#define HTTP_STRERROR_GEN(n, s) { "HPE_" #n, s }, +static struct { + const char *name; + const char *description; +} http_strerror_tab[] = { + HTTP_ERRNO_MAP(HTTP_STRERROR_GEN) +}; +#undef HTTP_STRERROR_GEN + +int http_message_needs_eof(const http_parser *parser); + +/* Our URL parser. + * + * This is designed to be shared by http_parser_execute() for URL validation, + * hence it has a state transition + byte-for-byte interface. In addition, it + * is meant to be embedded in http_parser_parse_url(), which does the dirty + * work of turning state transitions URL components for its API. + * + * This function should only be invoked with non-space characters. It is + * assumed that the caller cares about (and can detect) the transition between + * URL and non-URL states by looking for these. + */ +static enum state +parse_url_char(enum state s, const char ch) +{ + if (ch == ' ' || ch == '\r' || ch == '\n') { + return s_dead; + } + +#if HTTP_PARSER_STRICT + if (ch == '\t' || ch == '\f') { + return s_dead; + } +#endif + + switch (s) { + case s_req_spaces_before_url: + /* Proxied requests are followed by scheme of an absolute URI (alpha). + * All methods except CONNECT are followed by '/' or '*'. + */ + + if (ch == '/' || ch == '*') { + return s_req_path; + } + + if (IS_ALPHA(ch)) { + return s_req_schema; + } + + break; + + case s_req_schema: + if (IS_ALPHA(ch)) { + return s; + } + + if (ch == ':') { + return s_req_schema_slash; + } + + break; + + case s_req_schema_slash: + if (ch == '/') { + return s_req_schema_slash_slash; + } + + break; + + case s_req_schema_slash_slash: + if (ch == '/') { + return s_req_server_start; + } + + break; + + case s_req_server_with_at: + if (ch == '@') { + return s_dead; + } + + /* fall through */ + case s_req_server_start: + case s_req_server: + if (ch == '/') { + return s_req_path; + } + + if (ch == '?') { + return s_req_query_string_start; + } + + if (ch == '@') { + return s_req_server_with_at; + } + + if (IS_USERINFO_CHAR(ch) || ch == '[' || ch == ']') { + return s_req_server; + } + + break; + + case s_req_path: + if (IS_URL_CHAR(ch)) { + return s; + } + + switch (ch) { + case '?': + return s_req_query_string_start; + + case '#': + return s_req_fragment_start; + } + + break; + + case s_req_query_string_start: + case s_req_query_string: + if (IS_URL_CHAR(ch)) { + return s_req_query_string; + } + + switch (ch) { + case '?': + /* allow extra '?' in query string */ + return s_req_query_string; + + case '#': + return s_req_fragment_start; + } + + break; + + case s_req_fragment_start: + if (IS_URL_CHAR(ch)) { + return s_req_fragment; + } + + switch (ch) { + case '?': + return s_req_fragment; + + case '#': + return s; + } + + break; + + case s_req_fragment: + if (IS_URL_CHAR(ch)) { + return s; + } + + switch (ch) { + case '?': + case '#': + return s; + } + + break; + + default: + break; + } + + /* We should never fall out of the switch above unless there's an error */ + return s_dead; +} + +size_t http_parser_execute (http_parser *parser, + const http_parser_settings *settings, + const char *data, + size_t len) +{ + char c, ch; + int8_t unhex_val; + const char *p = data; + const char *header_field_mark = 0; + const char *header_value_mark = 0; + const char *url_mark = 0; + const char *body_mark = 0; + const char *status_mark = 0; + enum state p_state = (enum state) parser->state; + const unsigned int lenient = parser->lenient_http_headers; + const unsigned int allow_chunked_length = parser->allow_chunked_length; + + uint32_t nread = parser->nread; + + /* We're in an error state. Don't bother doing anything. */ + if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { + return 0; + } + + if (len == 0) { + switch (CURRENT_STATE()) { + case s_body_identity_eof: + /* Use of CALLBACK_NOTIFY() here would erroneously return 1 byte read if + * we got paused. + */ + CALLBACK_NOTIFY_NOADVANCE(message_complete); + return 0; + + case s_dead: + case s_start_req_or_res: + case s_start_res: + case s_start_req: + return 0; + + default: + SET_ERRNO(HPE_INVALID_EOF_STATE); + return 1; + } + } + + + if (CURRENT_STATE() == s_header_field) + header_field_mark = data; + if (CURRENT_STATE() == s_header_value) + header_value_mark = data; + switch (CURRENT_STATE()) { + case s_req_path: + case s_req_schema: + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + case s_req_server: + case s_req_server_with_at: + case s_req_query_string_start: + case s_req_query_string: + case s_req_fragment_start: + case s_req_fragment: + url_mark = data; + break; + case s_res_status: + status_mark = data; + break; + default: + break; + } + + for (p=data; p != data + len; p++) { + ch = *p; + + if (PARSING_HEADER(CURRENT_STATE())) + COUNT_HEADER_SIZE(1); + +reexecute: + switch (CURRENT_STATE()) { + + case s_dead: + /* this state is used after a 'Connection: close' message + * the parser will error out if it reads another message + */ + if (LIKELY(ch == CR || ch == LF)) + break; + + SET_ERRNO(HPE_CLOSED_CONNECTION); + goto error; + + case s_start_req_or_res: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->uses_transfer_encoding = 0; + parser->content_length = ULLONG_MAX; + + if (ch == 'H') { + UPDATE_STATE(s_res_or_resp_H); + + CALLBACK_NOTIFY(message_begin); + } else { + parser->type = HTTP_REQUEST; + UPDATE_STATE(s_start_req); + REEXECUTE(); + } + + break; + } + + case s_res_or_resp_H: + if (ch == 'T') { + parser->type = HTTP_RESPONSE; + UPDATE_STATE(s_res_HT); + } else { + if (UNLIKELY(ch != 'E')) { + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + + parser->type = HTTP_REQUEST; + parser->method = HTTP_HEAD; + parser->index = 2; + UPDATE_STATE(s_req_method); + } + break; + + case s_start_res: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->uses_transfer_encoding = 0; + parser->content_length = ULLONG_MAX; + + if (ch == 'H') { + UPDATE_STATE(s_res_H); + } else { + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + + CALLBACK_NOTIFY(message_begin); + break; + } + + case s_res_H: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_res_HT); + break; + + case s_res_HT: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_res_HTT); + break; + + case s_res_HTT: + STRICT_CHECK(ch != 'P'); + UPDATE_STATE(s_res_HTTP); + break; + + case s_res_HTTP: + STRICT_CHECK(ch != '/'); + UPDATE_STATE(s_res_http_major); + break; + + case s_res_http_major: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_major = ch - '0'; + UPDATE_STATE(s_res_http_dot); + break; + + case s_res_http_dot: + { + if (UNLIKELY(ch != '.')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_res_http_minor); + break; + } + + case s_res_http_minor: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_minor = ch - '0'; + UPDATE_STATE(s_res_http_end); + break; + + case s_res_http_end: + { + if (UNLIKELY(ch != ' ')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_res_first_status_code); + break; + } + + case s_res_first_status_code: + { + if (!IS_NUM(ch)) { + if (ch == ' ') { + break; + } + + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + parser->status_code = ch - '0'; + UPDATE_STATE(s_res_status_code); + break; + } + + case s_res_status_code: + { + if (!IS_NUM(ch)) { + switch (ch) { + case ' ': + UPDATE_STATE(s_res_status_start); + break; + case CR: + case LF: + UPDATE_STATE(s_res_status_start); + REEXECUTE(); + break; + default: + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + break; + } + + parser->status_code *= 10; + parser->status_code += ch - '0'; + + if (UNLIKELY(parser->status_code > 999)) { + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + + break; + } + + case s_res_status_start: + { + MARK(status); + UPDATE_STATE(s_res_status); + parser->index = 0; + + if (ch == CR || ch == LF) + REEXECUTE(); + + break; + } + + case s_res_status: + if (ch == CR) { + UPDATE_STATE(s_res_line_almost_done); + CALLBACK_DATA(status); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_field_start); + CALLBACK_DATA(status); + break; + } + + break; + + case s_res_line_almost_done: + STRICT_CHECK(ch != LF); + UPDATE_STATE(s_header_field_start); + break; + + case s_start_req: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->uses_transfer_encoding = 0; + parser->content_length = ULLONG_MAX; + + if (UNLIKELY(!IS_ALPHA(ch))) { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + parser->method = (enum http_method) 0; + parser->index = 1; + switch (ch) { + case 'A': parser->method = HTTP_ACL; break; + case 'B': parser->method = HTTP_BIND; break; + case 'C': parser->method = HTTP_CONNECT; /* or COPY, CHECKOUT */ break; + case 'D': parser->method = HTTP_DELETE; break; + case 'G': parser->method = HTTP_GET; break; + case 'H': parser->method = HTTP_HEAD; break; + case 'L': parser->method = HTTP_LOCK; /* or LINK */ break; + case 'M': parser->method = HTTP_MKCOL; /* or MOVE, MKACTIVITY, MERGE, M-SEARCH, MKCALENDAR */ break; + case 'N': parser->method = HTTP_NOTIFY; break; + case 'O': parser->method = HTTP_OPTIONS; break; + case 'P': parser->method = HTTP_POST; + /* or PROPFIND|PROPPATCH|PUT|PATCH|PURGE */ + break; + case 'R': parser->method = HTTP_REPORT; /* or REBIND */ break; + case 'S': parser->method = HTTP_SUBSCRIBE; /* or SEARCH, SOURCE */ break; + case 'T': parser->method = HTTP_TRACE; break; + case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE, UNBIND, UNLINK */ break; + default: + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + UPDATE_STATE(s_req_method); + + CALLBACK_NOTIFY(message_begin); + + break; + } + + case s_req_method: + { + const char *matcher; + if (UNLIKELY(ch == '\0')) { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + matcher = method_strings[parser->method]; + if (ch == ' ' && matcher[parser->index] == '\0') { + UPDATE_STATE(s_req_spaces_before_url); + } else if (ch == matcher[parser->index]) { + ; /* nada */ + } else if ((ch >= 'A' && ch <= 'Z') || ch == '-') { + + switch (parser->method << 16 | parser->index << 8 | ch) { +#define XX(meth, pos, ch, new_meth) \ + case (HTTP_##meth << 16 | pos << 8 | ch): \ + parser->method = HTTP_##new_meth; break; + + XX(POST, 1, 'U', PUT) + XX(POST, 1, 'A', PATCH) + XX(POST, 1, 'R', PROPFIND) + XX(PUT, 2, 'R', PURGE) + XX(CONNECT, 1, 'H', CHECKOUT) + XX(CONNECT, 2, 'P', COPY) + XX(MKCOL, 1, 'O', MOVE) + XX(MKCOL, 1, 'E', MERGE) + XX(MKCOL, 1, '-', MSEARCH) + XX(MKCOL, 2, 'A', MKACTIVITY) + XX(MKCOL, 3, 'A', MKCALENDAR) + XX(SUBSCRIBE, 1, 'E', SEARCH) + XX(SUBSCRIBE, 1, 'O', SOURCE) + XX(REPORT, 2, 'B', REBIND) + XX(PROPFIND, 4, 'P', PROPPATCH) + XX(LOCK, 1, 'I', LINK) + XX(UNLOCK, 2, 'S', UNSUBSCRIBE) + XX(UNLOCK, 2, 'B', UNBIND) + XX(UNLOCK, 3, 'I', UNLINK) +#undef XX + default: + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + } else { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + ++parser->index; + break; + } + + case s_req_spaces_before_url: + { + if (ch == ' ') break; + + MARK(url); + if (parser->method == HTTP_CONNECT) { + UPDATE_STATE(s_req_server_start); + } + + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + + break; + } + + case s_req_schema: + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + { + switch (ch) { + /* No whitespace allowed here */ + case ' ': + case CR: + case LF: + SET_ERRNO(HPE_INVALID_URL); + goto error; + default: + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + } + + break; + } + + case s_req_server: + case s_req_server_with_at: + case s_req_path: + case s_req_query_string_start: + case s_req_query_string: + case s_req_fragment_start: + case s_req_fragment: + { + switch (ch) { + case ' ': + UPDATE_STATE(s_req_http_start); + CALLBACK_DATA(url); + break; + case CR: + case LF: + parser->http_major = 0; + parser->http_minor = 9; + UPDATE_STATE((ch == CR) ? + s_req_line_almost_done : + s_header_field_start); + CALLBACK_DATA(url); + break; + default: + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + } + break; + } + + case s_req_http_start: + switch (ch) { + case ' ': + break; + case 'H': + UPDATE_STATE(s_req_http_H); + break; + case 'I': + if (parser->method == HTTP_SOURCE) { + UPDATE_STATE(s_req_http_I); + break; + } + /* fall through */ + default: + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + break; + + case s_req_http_H: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_req_http_HT); + break; + + case s_req_http_HT: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_req_http_HTT); + break; + + case s_req_http_HTT: + STRICT_CHECK(ch != 'P'); + UPDATE_STATE(s_req_http_HTTP); + break; + + case s_req_http_I: + STRICT_CHECK(ch != 'C'); + UPDATE_STATE(s_req_http_IC); + break; + + case s_req_http_IC: + STRICT_CHECK(ch != 'E'); + UPDATE_STATE(s_req_http_HTTP); /* Treat "ICE" as "HTTP". */ + break; + + case s_req_http_HTTP: + STRICT_CHECK(ch != '/'); + UPDATE_STATE(s_req_http_major); + break; + + case s_req_http_major: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_major = ch - '0'; + UPDATE_STATE(s_req_http_dot); + break; + + case s_req_http_dot: + { + if (UNLIKELY(ch != '.')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_req_http_minor); + break; + } + + case s_req_http_minor: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_minor = ch - '0'; + UPDATE_STATE(s_req_http_end); + break; + + case s_req_http_end: + { + if (ch == CR) { + UPDATE_STATE(s_req_line_almost_done); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_field_start); + break; + } + + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + break; + } + + /* end of request line */ + case s_req_line_almost_done: + { + if (UNLIKELY(ch != LF)) { + SET_ERRNO(HPE_LF_EXPECTED); + goto error; + } + + UPDATE_STATE(s_header_field_start); + break; + } + + case s_header_field_start: + { + if (ch == CR) { + UPDATE_STATE(s_headers_almost_done); + break; + } + + if (ch == LF) { + /* they might be just sending \n instead of \r\n so this would be + * the second \n to denote the end of headers*/ + UPDATE_STATE(s_headers_almost_done); + REEXECUTE(); + } + + c = TOKEN(ch); + + if (UNLIKELY(!c)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + MARK(header_field); + + parser->index = 0; + UPDATE_STATE(s_header_field); + + switch (c) { + case 'c': + parser->header_state = h_C; + break; + + case 'p': + parser->header_state = h_matching_proxy_connection; + break; + + case 't': + parser->header_state = h_matching_transfer_encoding; + break; + + case 'u': + parser->header_state = h_matching_upgrade; + break; + + default: + parser->header_state = h_general; + break; + } + break; + } + + case s_header_field: + { + const char* start = p; + for (; p != data + len; p++) { + ch = *p; + c = TOKEN(ch); + + if (!c) + break; + + switch (parser->header_state) { + case h_general: { + size_t left = data + len - p; + const char* pe = p + MIN(left, max_header_size); + while (p+1 < pe && TOKEN(p[1])) { + p++; + } + break; + } + + case h_C: + parser->index++; + parser->header_state = (c == 'o' ? h_CO : h_general); + break; + + case h_CO: + parser->index++; + parser->header_state = (c == 'n' ? h_CON : h_general); + break; + + case h_CON: + parser->index++; + switch (c) { + case 'n': + parser->header_state = h_matching_connection; + break; + case 't': + parser->header_state = h_matching_content_length; + break; + default: + parser->header_state = h_general; + break; + } + break; + + /* connection */ + + case h_matching_connection: + parser->index++; + if (parser->index > sizeof(CONNECTION)-1 + || c != CONNECTION[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(CONNECTION)-2) { + parser->header_state = h_connection; + } + break; + + /* proxy-connection */ + + case h_matching_proxy_connection: + parser->index++; + if (parser->index > sizeof(PROXY_CONNECTION)-1 + || c != PROXY_CONNECTION[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(PROXY_CONNECTION)-2) { + parser->header_state = h_connection; + } + break; + + /* content-length */ + + case h_matching_content_length: + parser->index++; + if (parser->index > sizeof(CONTENT_LENGTH)-1 + || c != CONTENT_LENGTH[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(CONTENT_LENGTH)-2) { + parser->header_state = h_content_length; + } + break; + + /* transfer-encoding */ + + case h_matching_transfer_encoding: + parser->index++; + if (parser->index > sizeof(TRANSFER_ENCODING)-1 + || c != TRANSFER_ENCODING[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(TRANSFER_ENCODING)-2) { + parser->header_state = h_transfer_encoding; + parser->uses_transfer_encoding = 1; + } + break; + + /* upgrade */ + + case h_matching_upgrade: + parser->index++; + if (parser->index > sizeof(UPGRADE)-1 + || c != UPGRADE[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(UPGRADE)-2) { + parser->header_state = h_upgrade; + } + break; + + case h_connection: + case h_content_length: + case h_transfer_encoding: + case h_upgrade: + if (ch != ' ') parser->header_state = h_general; + break; + + default: + assert(0 && "Unknown header_state"); + break; + } + } + + if (p == data + len) { + --p; + COUNT_HEADER_SIZE(p - start); + break; + } + + COUNT_HEADER_SIZE(p - start); + + if (ch == ':') { + UPDATE_STATE(s_header_value_discard_ws); + CALLBACK_DATA(header_field); + break; + } + + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + case s_header_value_discard_ws: + if (ch == ' ' || ch == '\t') break; + + if (ch == CR) { + UPDATE_STATE(s_header_value_discard_ws_almost_done); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_value_discard_lws); + break; + } + + /* fall through */ + + case s_header_value_start: + { + MARK(header_value); + + UPDATE_STATE(s_header_value); + parser->index = 0; + + c = LOWER(ch); + + switch (parser->header_state) { + case h_upgrade: + parser->flags |= F_UPGRADE; + parser->header_state = h_general; + break; + + case h_transfer_encoding: + /* looking for 'Transfer-Encoding: chunked' */ + if ('c' == c) { + parser->header_state = h_matching_transfer_encoding_chunked; + } else { + parser->header_state = h_matching_transfer_encoding_token; + } + break; + + /* Multi-value `Transfer-Encoding` header */ + case h_matching_transfer_encoding_token_start: + break; + + case h_content_length: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + } + + if (parser->flags & F_CONTENTLENGTH) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + + parser->flags |= F_CONTENTLENGTH; + parser->content_length = ch - '0'; + parser->header_state = h_content_length_num; + break; + + /* when obsolete line folding is encountered for content length + * continue to the s_header_value state */ + case h_content_length_ws: + break; + + case h_connection: + /* looking for 'Connection: keep-alive' */ + if (c == 'k') { + parser->header_state = h_matching_connection_keep_alive; + /* looking for 'Connection: close' */ + } else if (c == 'c') { + parser->header_state = h_matching_connection_close; + } else if (c == 'u') { + parser->header_state = h_matching_connection_upgrade; + } else { + parser->header_state = h_matching_connection_token; + } + break; + + /* Multi-value `Connection` header */ + case h_matching_connection_token_start: + break; + + default: + parser->header_state = h_general; + break; + } + break; + } + + case s_header_value: + { + const char* start = p; + enum header_states h_state = (enum header_states) parser->header_state; + for (; p != data + len; p++) { + ch = *p; + if (ch == CR) { + UPDATE_STATE(s_header_almost_done); + parser->header_state = h_state; + CALLBACK_DATA(header_value); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_almost_done); + COUNT_HEADER_SIZE(p - start); + parser->header_state = h_state; + CALLBACK_DATA_NOADVANCE(header_value); + REEXECUTE(); + } + + if (!lenient && !IS_HEADER_CHAR(ch)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + c = LOWER(ch); + + switch (h_state) { + case h_general: + { + size_t left = data + len - p; + const char* pe = p + MIN(left, max_header_size); + + for (; p != pe; p++) { + ch = *p; + if (ch == CR || ch == LF) { + --p; + break; + } + if (!lenient && !IS_HEADER_CHAR(ch)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + } + if (p == data + len) + --p; + break; + } + + case h_connection: + case h_transfer_encoding: + assert(0 && "Shouldn't get here."); + break; + + case h_content_length: + if (ch == ' ') break; + h_state = h_content_length_num; + /* fall through */ + + case h_content_length_num: + { + uint64_t t; + + if (ch == ' ') { + h_state = h_content_length_ws; + break; + } + + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + } + + t = parser->content_length; + t *= 10; + t += ch - '0'; + + /* Overflow? Test against a conservative limit for simplicity. */ + if (UNLIKELY((ULLONG_MAX - 10) / 10 < parser->content_length)) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + } + + parser->content_length = t; + break; + } + + case h_content_length_ws: + if (ch == ' ') break; + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + + /* Transfer-Encoding: chunked */ + case h_matching_transfer_encoding_token_start: + /* looking for 'Transfer-Encoding: chunked' */ + if ('c' == c) { + h_state = h_matching_transfer_encoding_chunked; + } else if (STRICT_TOKEN(c)) { + /* TODO(indutny): similar code below does this, but why? + * At the very least it seems to be inconsistent given that + * h_matching_transfer_encoding_token does not check for + * `STRICT_TOKEN` + */ + h_state = h_matching_transfer_encoding_token; + } else if (c == ' ' || c == '\t') { + /* Skip lws */ + } else { + h_state = h_general; + } + break; + + case h_matching_transfer_encoding_chunked: + parser->index++; + if (parser->index > sizeof(CHUNKED)-1 + || c != CHUNKED[parser->index]) { + h_state = h_matching_transfer_encoding_token; + } else if (parser->index == sizeof(CHUNKED)-2) { + h_state = h_transfer_encoding_chunked; + } + break; + + case h_matching_transfer_encoding_token: + if (ch == ',') { + h_state = h_matching_transfer_encoding_token_start; + parser->index = 0; + } + break; + + case h_matching_connection_token_start: + /* looking for 'Connection: keep-alive' */ + if (c == 'k') { + h_state = h_matching_connection_keep_alive; + /* looking for 'Connection: close' */ + } else if (c == 'c') { + h_state = h_matching_connection_close; + } else if (c == 'u') { + h_state = h_matching_connection_upgrade; + } else if (STRICT_TOKEN(c)) { + h_state = h_matching_connection_token; + } else if (c == ' ' || c == '\t') { + /* Skip lws */ + } else { + h_state = h_general; + } + break; + + /* looking for 'Connection: keep-alive' */ + case h_matching_connection_keep_alive: + parser->index++; + if (parser->index > sizeof(KEEP_ALIVE)-1 + || c != KEEP_ALIVE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(KEEP_ALIVE)-2) { + h_state = h_connection_keep_alive; + } + break; + + /* looking for 'Connection: close' */ + case h_matching_connection_close: + parser->index++; + if (parser->index > sizeof(CLOSE)-1 || c != CLOSE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(CLOSE)-2) { + h_state = h_connection_close; + } + break; + + /* looking for 'Connection: upgrade' */ + case h_matching_connection_upgrade: + parser->index++; + if (parser->index > sizeof(UPGRADE) - 1 || + c != UPGRADE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(UPGRADE)-2) { + h_state = h_connection_upgrade; + } + break; + + case h_matching_connection_token: + if (ch == ',') { + h_state = h_matching_connection_token_start; + parser->index = 0; + } + break; + + case h_transfer_encoding_chunked: + if (ch != ' ') h_state = h_matching_transfer_encoding_token; + break; + + case h_connection_keep_alive: + case h_connection_close: + case h_connection_upgrade: + if (ch == ',') { + if (h_state == h_connection_keep_alive) { + parser->flags |= F_CONNECTION_KEEP_ALIVE; + } else if (h_state == h_connection_close) { + parser->flags |= F_CONNECTION_CLOSE; + } else if (h_state == h_connection_upgrade) { + parser->flags |= F_CONNECTION_UPGRADE; + } + h_state = h_matching_connection_token_start; + parser->index = 0; + } else if (ch != ' ') { + h_state = h_matching_connection_token; + } + break; + + default: + UPDATE_STATE(s_header_value); + h_state = h_general; + break; + } + } + parser->header_state = h_state; + + if (p == data + len) + --p; + + COUNT_HEADER_SIZE(p - start); + break; + } + + case s_header_almost_done: + { + if (UNLIKELY(ch != LF)) { + SET_ERRNO(HPE_LF_EXPECTED); + goto error; + } + + UPDATE_STATE(s_header_value_lws); + break; + } + + case s_header_value_lws: + { + if (ch == ' ' || ch == '\t') { + if (parser->header_state == h_content_length_num) { + /* treat obsolete line folding as space */ + parser->header_state = h_content_length_ws; + } + UPDATE_STATE(s_header_value_start); + REEXECUTE(); + } + + /* finished the header */ + switch (parser->header_state) { + case h_connection_keep_alive: + parser->flags |= F_CONNECTION_KEEP_ALIVE; + break; + case h_connection_close: + parser->flags |= F_CONNECTION_CLOSE; + break; + case h_transfer_encoding_chunked: + parser->flags |= F_CHUNKED; + break; + case h_connection_upgrade: + parser->flags |= F_CONNECTION_UPGRADE; + break; + default: + break; + } + + UPDATE_STATE(s_header_field_start); + REEXECUTE(); + } + + case s_header_value_discard_ws_almost_done: + { + STRICT_CHECK(ch != LF); + UPDATE_STATE(s_header_value_discard_lws); + break; + } + + case s_header_value_discard_lws: + { + if (ch == ' ' || ch == '\t') { + UPDATE_STATE(s_header_value_discard_ws); + break; + } else { + switch (parser->header_state) { + case h_connection_keep_alive: + parser->flags |= F_CONNECTION_KEEP_ALIVE; + break; + case h_connection_close: + parser->flags |= F_CONNECTION_CLOSE; + break; + case h_connection_upgrade: + parser->flags |= F_CONNECTION_UPGRADE; + break; + case h_transfer_encoding_chunked: + parser->flags |= F_CHUNKED; + break; + case h_content_length: + /* do not allow empty content length */ + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + break; + default: + break; + } + + /* header value was empty */ + MARK(header_value); + UPDATE_STATE(s_header_field_start); + CALLBACK_DATA_NOADVANCE(header_value); + REEXECUTE(); + } + } + + case s_headers_almost_done: + { + STRICT_CHECK(ch != LF); + + if (parser->flags & F_TRAILING) { + /* End of a chunked request */ + UPDATE_STATE(s_message_done); + CALLBACK_NOTIFY_NOADVANCE(chunk_complete); + REEXECUTE(); + } + + /* Cannot use transfer-encoding and a content-length header together + per the HTTP specification. (RFC 7230 Section 3.3.3) */ + if ((parser->uses_transfer_encoding == 1) && + (parser->flags & F_CONTENTLENGTH)) { + /* Allow it for lenient parsing as long as `Transfer-Encoding` is + * not `chunked` or allow_length_with_encoding is set + */ + if (parser->flags & F_CHUNKED) { + if (!allow_chunked_length) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + } else if (!lenient) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + } + + UPDATE_STATE(s_headers_done); + + /* Set this here so that on_headers_complete() callbacks can see it */ + if ((parser->flags & F_UPGRADE) && + (parser->flags & F_CONNECTION_UPGRADE)) { + /* For responses, "Upgrade: foo" and "Connection: upgrade" are + * mandatory only when it is a 101 Switching Protocols response, + * otherwise it is purely informational, to announce support. + */ + parser->upgrade = + (parser->type == HTTP_REQUEST || parser->status_code == 101); + } else { + parser->upgrade = (parser->method == HTTP_CONNECT); + } + + /* Here we call the headers_complete callback. This is somewhat + * different than other callbacks because if the user returns 1, we + * will interpret that as saying that this message has no body. This + * is needed for the annoying case of recieving a response to a HEAD + * request. + * + * We'd like to use CALLBACK_NOTIFY_NOADVANCE() here but we cannot, so + * we have to simulate it by handling a change in errno below. + */ + if (settings->on_headers_complete) { + switch (settings->on_headers_complete(parser)) { + case 0: + break; + + case 2: + parser->upgrade = 1; + + /* fall through */ + case 1: + parser->flags |= F_SKIPBODY; + break; + + default: + SET_ERRNO(HPE_CB_headers_complete); + RETURN(p - data); /* Error */ + } + } + + if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { + RETURN(p - data); + } + + REEXECUTE(); + } + + case s_headers_done: + { + int hasBody; + STRICT_CHECK(ch != LF); + + parser->nread = 0; + nread = 0; + + hasBody = parser->flags & F_CHUNKED || + (parser->content_length > 0 && parser->content_length != ULLONG_MAX); + if (parser->upgrade && (parser->method == HTTP_CONNECT || + (parser->flags & F_SKIPBODY) || !hasBody)) { + /* Exit, the rest of the message is in a different protocol. */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + RETURN((p - data) + 1); + } + + if (parser->flags & F_SKIPBODY) { + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else if (parser->flags & F_CHUNKED) { + /* chunked encoding - ignore Content-Length header, + * prepare for a chunk */ + UPDATE_STATE(s_chunk_size_start); + } else if (parser->uses_transfer_encoding == 1) { + if (parser->type == HTTP_REQUEST && !lenient) { + /* RFC 7230 3.3.3 */ + + /* If a Transfer-Encoding header field + * is present in a request and the chunked transfer coding is not + * the final encoding, the message body length cannot be determined + * reliably; the server MUST respond with the 400 (Bad Request) + * status code and then close the connection. + */ + SET_ERRNO(HPE_INVALID_TRANSFER_ENCODING); + RETURN(p - data); /* Error */ + } else { + /* RFC 7230 3.3.3 */ + + /* If a Transfer-Encoding header field is present in a response and + * the chunked transfer coding is not the final encoding, the + * message body length is determined by reading the connection until + * it is closed by the server. + */ + UPDATE_STATE(s_body_identity_eof); + } + } else { + if (parser->content_length == 0) { + /* Content-Length header given but zero: Content-Length: 0\r\n */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else if (parser->content_length != ULLONG_MAX) { + /* Content-Length header given and non-zero */ + UPDATE_STATE(s_body_identity); + } else { + if (!http_message_needs_eof(parser)) { + /* Assume content-length 0 - read the next */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else { + /* Read body until EOF */ + UPDATE_STATE(s_body_identity_eof); + } + } + } + + break; + } + + case s_body_identity: + { + uint64_t to_read = MIN(parser->content_length, + (uint64_t) ((data + len) - p)); + + assert(parser->content_length != 0 + && parser->content_length != ULLONG_MAX); + + /* The difference between advancing content_length and p is because + * the latter will automaticaly advance on the next loop iteration. + * Further, if content_length ends up at 0, we want to see the last + * byte again for our message complete callback. + */ + MARK(body); + parser->content_length -= to_read; + p += to_read - 1; + + if (parser->content_length == 0) { + UPDATE_STATE(s_message_done); + + /* Mimic CALLBACK_DATA_NOADVANCE() but with one extra byte. + * + * The alternative to doing this is to wait for the next byte to + * trigger the data callback, just as in every other case. The + * problem with this is that this makes it difficult for the test + * harness to distinguish between complete-on-EOF and + * complete-on-length. It's not clear that this distinction is + * important for applications, but let's keep it for now. + */ + CALLBACK_DATA_(body, p - body_mark + 1, p - data); + REEXECUTE(); + } + + break; + } + + /* read until EOF */ + case s_body_identity_eof: + MARK(body); + p = data + len - 1; + + break; + + case s_message_done: + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + if (parser->upgrade) { + /* Exit, the rest of the message is in a different protocol. */ + RETURN((p - data) + 1); + } + break; + + case s_chunk_size_start: + { + assert(nread == 1); + assert(parser->flags & F_CHUNKED); + + unhex_val = unhex[(unsigned char)ch]; + if (UNLIKELY(unhex_val == -1)) { + SET_ERRNO(HPE_INVALID_CHUNK_SIZE); + goto error; + } + + parser->content_length = unhex_val; + UPDATE_STATE(s_chunk_size); + break; + } + + case s_chunk_size: + { + uint64_t t; + + assert(parser->flags & F_CHUNKED); + + if (ch == CR) { + UPDATE_STATE(s_chunk_size_almost_done); + break; + } + + unhex_val = unhex[(unsigned char)ch]; + + if (unhex_val == -1) { + if (ch == ';' || ch == ' ') { + UPDATE_STATE(s_chunk_parameters); + break; + } + + SET_ERRNO(HPE_INVALID_CHUNK_SIZE); + goto error; + } + + t = parser->content_length; + t *= 16; + t += unhex_val; + + /* Overflow? Test against a conservative limit for simplicity. */ + if (UNLIKELY((ULLONG_MAX - 16) / 16 < parser->content_length)) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + } + + parser->content_length = t; + break; + } + + case s_chunk_parameters: + { + assert(parser->flags & F_CHUNKED); + /* just ignore this shit. TODO check for overflow */ + if (ch == CR) { + UPDATE_STATE(s_chunk_size_almost_done); + break; + } + break; + } + + case s_chunk_size_almost_done: + { + assert(parser->flags & F_CHUNKED); + STRICT_CHECK(ch != LF); + + parser->nread = 0; + nread = 0; + + if (parser->content_length == 0) { + parser->flags |= F_TRAILING; + UPDATE_STATE(s_header_field_start); + } else { + UPDATE_STATE(s_chunk_data); + } + CALLBACK_NOTIFY(chunk_header); + break; + } + + case s_chunk_data: + { + uint64_t to_read = MIN(parser->content_length, + (uint64_t) ((data + len) - p)); + + assert(parser->flags & F_CHUNKED); + assert(parser->content_length != 0 + && parser->content_length != ULLONG_MAX); + + /* See the explanation in s_body_identity for why the content + * length and data pointers are managed this way. + */ + MARK(body); + parser->content_length -= to_read; + p += to_read - 1; + + if (parser->content_length == 0) { + UPDATE_STATE(s_chunk_data_almost_done); + } + + break; + } + + case s_chunk_data_almost_done: + assert(parser->flags & F_CHUNKED); + assert(parser->content_length == 0); + STRICT_CHECK(ch != CR); + UPDATE_STATE(s_chunk_data_done); + CALLBACK_DATA(body); + break; + + case s_chunk_data_done: + assert(parser->flags & F_CHUNKED); + STRICT_CHECK(ch != LF); + parser->nread = 0; + nread = 0; + UPDATE_STATE(s_chunk_size_start); + CALLBACK_NOTIFY(chunk_complete); + break; + + default: + assert(0 && "unhandled state"); + SET_ERRNO(HPE_INVALID_INTERNAL_STATE); + goto error; + } + } + + /* Run callbacks for any marks that we have leftover after we ran out of + * bytes. There should be at most one of these set, so it's OK to invoke + * them in series (unset marks will not result in callbacks). + * + * We use the NOADVANCE() variety of callbacks here because 'p' has already + * overflowed 'data' and this allows us to correct for the off-by-one that + * we'd otherwise have (since CALLBACK_DATA() is meant to be run with a 'p' + * value that's in-bounds). + */ + + assert(((header_field_mark ? 1 : 0) + + (header_value_mark ? 1 : 0) + + (url_mark ? 1 : 0) + + (body_mark ? 1 : 0) + + (status_mark ? 1 : 0)) <= 1); + + CALLBACK_DATA_NOADVANCE(header_field); + CALLBACK_DATA_NOADVANCE(header_value); + CALLBACK_DATA_NOADVANCE(url); + CALLBACK_DATA_NOADVANCE(body); + CALLBACK_DATA_NOADVANCE(status); + + RETURN(len); + +error: + if (HTTP_PARSER_ERRNO(parser) == HPE_OK) { + SET_ERRNO(HPE_UNKNOWN); + } + + RETURN(p - data); +} + + +/* Does the parser need to see an EOF to find the end of the message? */ +int +http_message_needs_eof (const http_parser *parser) +{ + if (parser->type == HTTP_REQUEST) { + return 0; + } + + /* See RFC 2616 section 4.4 */ + if (parser->status_code / 100 == 1 || /* 1xx e.g. Continue */ + parser->status_code == 204 || /* No Content */ + parser->status_code == 304 || /* Not Modified */ + parser->flags & F_SKIPBODY) { /* response to a HEAD request */ + return 0; + } + + /* RFC 7230 3.3.3, see `s_headers_almost_done` */ + if ((parser->uses_transfer_encoding == 1) && + (parser->flags & F_CHUNKED) == 0) { + return 1; + } + + if ((parser->flags & F_CHUNKED) || parser->content_length != ULLONG_MAX) { + return 0; + } + + return 1; +} + + +int +http_should_keep_alive (const http_parser *parser) +{ + if (parser->http_major > 0 && parser->http_minor > 0) { + /* HTTP/1.1 */ + if (parser->flags & F_CONNECTION_CLOSE) { + return 0; + } + } else { + /* HTTP/1.0 or earlier */ + if (!(parser->flags & F_CONNECTION_KEEP_ALIVE)) { + return 0; + } + } + + return !http_message_needs_eof(parser); +} + + +const char * +http_method_str (enum http_method m) +{ + return ELEM_AT(method_strings, m, ""); +} + +const char * +http_status_str (enum http_status s) +{ + switch (s) { +#define XX(num, name, string) case HTTP_STATUS_##name: return #string; + HTTP_STATUS_MAP(XX) +#undef XX + default: return ""; + } +} + +void +http_parser_init (http_parser *parser, enum http_parser_type t) +{ + void *data = parser->data; /* preserve application data */ + memset(parser, 0, sizeof(*parser)); + parser->data = data; + parser->type = t; + parser->state = (t == HTTP_REQUEST ? s_start_req : (t == HTTP_RESPONSE ? s_start_res : s_start_req_or_res)); + parser->http_errno = HPE_OK; +} + +void +http_parser_settings_init(http_parser_settings *settings) +{ + memset(settings, 0, sizeof(*settings)); +} + +const char * +http_errno_name(enum http_errno err) { + assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); + return http_strerror_tab[err].name; +} + +const char * +http_errno_description(enum http_errno err) { + assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); + return http_strerror_tab[err].description; +} + +static enum http_host_state +http_parse_host_char(enum http_host_state s, const char ch) { + switch(s) { + case s_http_userinfo: + case s_http_userinfo_start: + if (ch == '@') { + return s_http_host_start; + } + + if (IS_USERINFO_CHAR(ch)) { + return s_http_userinfo; + } + break; + + case s_http_host_start: + if (ch == '[') { + return s_http_host_v6_start; + } + + if (IS_HOST_CHAR(ch)) { + return s_http_host; + } + + break; + + case s_http_host: + if (IS_HOST_CHAR(ch)) { + return s_http_host; + } + + /* fall through */ + case s_http_host_v6_end: + if (ch == ':') { + return s_http_host_port_start; + } + + break; + + case s_http_host_v6: + if (ch == ']') { + return s_http_host_v6_end; + } + + /* fall through */ + case s_http_host_v6_start: + if (IS_HEX(ch) || ch == ':' || ch == '.') { + return s_http_host_v6; + } + + if (s == s_http_host_v6 && ch == '%') { + return s_http_host_v6_zone_start; + } + break; + + case s_http_host_v6_zone: + if (ch == ']') { + return s_http_host_v6_end; + } + + /* fall through */ + case s_http_host_v6_zone_start: + /* RFC 6874 Zone ID consists of 1*( unreserved / pct-encoded) */ + if (IS_ALPHANUM(ch) || ch == '%' || ch == '.' || ch == '-' || ch == '_' || + ch == '~') { + return s_http_host_v6_zone; + } + break; + + case s_http_host_port: + case s_http_host_port_start: + if (IS_NUM(ch)) { + return s_http_host_port; + } + + break; + + default: + break; + } + return s_http_host_dead; +} + +static int +http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { + enum http_host_state s; + + const char *p; + size_t buflen = u->field_data[UF_HOST].off + u->field_data[UF_HOST].len; + + assert(u->field_set & (1 << UF_HOST)); + + u->field_data[UF_HOST].len = 0; + + s = found_at ? s_http_userinfo_start : s_http_host_start; + + for (p = buf + u->field_data[UF_HOST].off; p < buf + buflen; p++) { + enum http_host_state new_s = http_parse_host_char(s, *p); + + if (new_s == s_http_host_dead) { + return 1; + } + + switch(new_s) { + case s_http_host: + if (s != s_http_host) { + u->field_data[UF_HOST].off = (uint16_t)(p - buf); + } + u->field_data[UF_HOST].len++; + break; + + case s_http_host_v6: + if (s != s_http_host_v6) { + u->field_data[UF_HOST].off = (uint16_t)(p - buf); + } + u->field_data[UF_HOST].len++; + break; + + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: + u->field_data[UF_HOST].len++; + break; + + case s_http_host_port: + if (s != s_http_host_port) { + u->field_data[UF_PORT].off = (uint16_t)(p - buf); + u->field_data[UF_PORT].len = 0; + u->field_set |= (1 << UF_PORT); + } + u->field_data[UF_PORT].len++; + break; + + case s_http_userinfo: + if (s != s_http_userinfo) { + u->field_data[UF_USERINFO].off = (uint16_t)(p - buf); + u->field_data[UF_USERINFO].len = 0; + u->field_set |= (1 << UF_USERINFO); + } + u->field_data[UF_USERINFO].len++; + break; + + default: + break; + } + s = new_s; + } + + /* Make sure we don't end somewhere unexpected */ + switch (s) { + case s_http_host_start: + case s_http_host_v6_start: + case s_http_host_v6: + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: + case s_http_host_port_start: + case s_http_userinfo: + case s_http_userinfo_start: + return 1; + default: + break; + } + + return 0; +} + +void +http_parser_url_init(struct http_parser_url *u) { + memset(u, 0, sizeof(*u)); +} + +int +http_parser_parse_url(const char *buf, size_t buflen, int is_connect, + struct http_parser_url *u) +{ + enum state s; + const char *p; + enum http_parser_url_fields uf, old_uf; + int found_at = 0; + + if (buflen == 0) { + return 1; + } + + u->port = u->field_set = 0; + s = is_connect ? s_req_server_start : s_req_spaces_before_url; + old_uf = UF_MAX; + + for (p = buf; p < buf + buflen; p++) { + s = parse_url_char(s, *p); + + /* Figure out the next field that we're operating on */ + switch (s) { + case s_dead: + return 1; + + /* Skip delimeters */ + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + case s_req_query_string_start: + case s_req_fragment_start: + continue; + + case s_req_schema: + uf = UF_SCHEMA; + break; + + case s_req_server_with_at: + found_at = 1; + + /* fall through */ + case s_req_server: + uf = UF_HOST; + break; + + case s_req_path: + uf = UF_PATH; + break; + + case s_req_query_string: + uf = UF_QUERY; + break; + + case s_req_fragment: + uf = UF_FRAGMENT; + break; + + default: + assert(!"Unexpected state"); + return 1; + } + + /* Nothing's changed; soldier on */ + if (uf == old_uf) { + u->field_data[uf].len++; + continue; + } + + u->field_data[uf].off = (uint16_t)(p - buf); + u->field_data[uf].len = 1; + + u->field_set |= (1 << uf); + old_uf = uf; + } + + /* host must be present if there is a schema */ + /* parsing http:///toto will fail */ + if ((u->field_set & (1 << UF_SCHEMA)) && + (u->field_set & (1 << UF_HOST)) == 0) { + return 1; + } + + if (u->field_set & (1 << UF_HOST)) { + if (http_parse_host(buf, u, found_at) != 0) { + return 1; + } + } + + /* CONNECT requests can only contain "hostname:port" */ + if (is_connect && u->field_set != ((1 << UF_HOST)|(1 << UF_PORT))) { + return 1; + } + + if (u->field_set & (1 << UF_PORT)) { + uint16_t off; + uint16_t len; + const char* p; + const char* end; + unsigned long v; + + off = u->field_data[UF_PORT].off; + len = u->field_data[UF_PORT].len; + end = buf + off + len; + + /* NOTE: The characters are already validated and are in the [0-9] range */ + assert((size_t) (off + len) <= buflen && "Port number overflow"); + v = 0; + for (p = buf + off; p < end; p++) { + v *= 10; + v += *p - '0'; + + /* Ports have a max value of 2^16 */ + if (v > 0xffff) { + return 1; + } + } + + u->port = (uint16_t) v; + } + + return 0; +} + +void +http_parser_pause(http_parser *parser, int paused) { + /* Users should only be pausing/unpausing a parser that is not in an error + * state. In non-debug builds, there's not much that we can do about this + * other than ignore it. + */ + if (HTTP_PARSER_ERRNO(parser) == HPE_OK || + HTTP_PARSER_ERRNO(parser) == HPE_PAUSED) { + uint32_t nread = parser->nread; /* used by the SET_ERRNO macro */ + SET_ERRNO((paused) ? HPE_PAUSED : HPE_OK); + } else { + assert(0 && "Attempting to pause parser in error state"); + } +} + +int +http_body_is_final(const struct http_parser *parser) { + return parser->state == s_message_done; +} + +unsigned long +http_parser_version(void) { + return HTTP_PARSER_VERSION_MAJOR * 0x10000 | + HTTP_PARSER_VERSION_MINOR * 0x00100 | + HTTP_PARSER_VERSION_PATCH * 0x00001; +} + +void +http_parser_set_max_header_size(uint32_t size) { + max_header_size = size; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/http_parser.gyp b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/http_parser.gyp new file mode 100644 index 00000000000000..ef34ecaeaeab45 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/http_parser.gyp @@ -0,0 +1,111 @@ +# This file is used with the GYP meta build system. +# http://code.google.com/p/gyp/ +# To build try this: +# svn co http://gyp.googlecode.com/svn/trunk gyp +# ./gyp/gyp -f make --depth=`pwd` http_parser.gyp +# ./out/Debug/test +{ + 'target_defaults': { + 'default_configuration': 'Debug', + 'configurations': { + # TODO: hoist these out and put them somewhere common, because + # RuntimeLibrary MUST MATCH across the entire project + 'Debug': { + 'defines': [ 'DEBUG', '_DEBUG' ], + 'cflags': [ '-Wall', '-Wextra', '-O0', '-g', '-ftrapv' ], + 'msvs_settings': { + 'VCCLCompilerTool': { + 'RuntimeLibrary': 1, # static debug + }, + }, + }, + 'Release': { + 'defines': [ 'NDEBUG' ], + 'cflags': [ '-Wall', '-Wextra', '-O3' ], + 'msvs_settings': { + 'VCCLCompilerTool': { + 'RuntimeLibrary': 0, # static release + }, + }, + } + }, + 'msvs_settings': { + 'VCCLCompilerTool': { + }, + 'VCLibrarianTool': { + }, + 'VCLinkerTool': { + 'GenerateDebugInformation': 'true', + }, + }, + 'conditions': [ + ['OS == "win"', { + 'defines': [ + 'WIN32' + ], + }] + ], + }, + + 'targets': [ + { + 'target_name': 'http_parser', + 'type': 'static_library', + 'include_dirs': [ '.' ], + 'direct_dependent_settings': { + 'defines': [ 'HTTP_PARSER_STRICT=0' ], + 'include_dirs': [ '.' ], + }, + 'defines': [ 'HTTP_PARSER_STRICT=0' ], + 'sources': [ './http_parser.c', ], + 'conditions': [ + ['OS=="win"', { + 'msvs_settings': { + 'VCCLCompilerTool': { + # Compile as C++. http_parser.c is actually C99, but C++ is + # close enough in this case. + 'CompileAs': 2, + }, + }, + }] + ], + }, + + { + 'target_name': 'http_parser_strict', + 'type': 'static_library', + 'include_dirs': [ '.' ], + 'direct_dependent_settings': { + 'defines': [ 'HTTP_PARSER_STRICT=1' ], + 'include_dirs': [ '.' ], + }, + 'defines': [ 'HTTP_PARSER_STRICT=1' ], + 'sources': [ './http_parser.c', ], + 'conditions': [ + ['OS=="win"', { + 'msvs_settings': { + 'VCCLCompilerTool': { + # Compile as C++. http_parser.c is actually C99, but C++ is + # close enough in this case. + 'CompileAs': 2, + }, + }, + }] + ], + }, + + { + 'target_name': 'test-nonstrict', + 'type': 'executable', + 'dependencies': [ 'http_parser' ], + 'sources': [ 'test.c' ] + }, + + { + 'target_name': 'test-strict', + 'type': 'executable', + 'dependencies': [ 'http_parser_strict' ], + 'sources': [ 'test.c' ] + } + ] +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/http_parser.h b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/http_parser.h new file mode 100644 index 00000000000000..3772b3994fe4ae --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/http_parser.h @@ -0,0 +1,449 @@ +/* Copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#ifndef http_parser_h +#define http_parser_h +#ifdef __cplusplus +extern "C" { +#endif + +/* Also update SONAME in the Makefile whenever you change these. */ +#define HTTP_PARSER_VERSION_MAJOR 2 +#define HTTP_PARSER_VERSION_MINOR 9 +#define HTTP_PARSER_VERSION_PATCH 4 + +#include +#if defined(_WIN32) && !defined(__MINGW32__) && \ + (!defined(_MSC_VER) || _MSC_VER<1600) && !defined(__WINE__) +#include +typedef __int8 int8_t; +typedef unsigned __int8 uint8_t; +typedef __int16 int16_t; +typedef unsigned __int16 uint16_t; +typedef __int32 int32_t; +typedef unsigned __int32 uint32_t; +typedef __int64 int64_t; +typedef unsigned __int64 uint64_t; +#elif (defined(__sun) || defined(__sun__)) && defined(__SunOS_5_9) +#include +#else +#include +#endif + +/* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run + * faster + */ +#ifndef HTTP_PARSER_STRICT +# define HTTP_PARSER_STRICT 1 +#endif + +/* Maximium header size allowed. If the macro is not defined + * before including this header then the default is used. To + * change the maximum header size, define the macro in the build + * environment (e.g. -DHTTP_MAX_HEADER_SIZE=). To remove + * the effective limit on the size of the header, define the macro + * to a very large number (e.g. -DHTTP_MAX_HEADER_SIZE=0x7fffffff) + */ +#ifndef HTTP_MAX_HEADER_SIZE +# define HTTP_MAX_HEADER_SIZE (80*1024) +#endif + +typedef struct http_parser http_parser; +typedef struct http_parser_settings http_parser_settings; + + +/* Callbacks should return non-zero to indicate an error. The parser will + * then halt execution. + * + * The one exception is on_headers_complete. In a HTTP_RESPONSE parser + * returning '1' from on_headers_complete will tell the parser that it + * should not expect a body. This is used when receiving a response to a + * HEAD request which may contain 'Content-Length' or 'Transfer-Encoding: + * chunked' headers that indicate the presence of a body. + * + * Returning `2` from on_headers_complete will tell parser that it should not + * expect neither a body nor any futher responses on this connection. This is + * useful for handling responses to a CONNECT request which may not contain + * `Upgrade` or `Connection: upgrade` headers. + * + * http_data_cb does not return data chunks. It will be called arbitrarily + * many times for each string. E.G. you might get 10 callbacks for "on_url" + * each providing just a few characters more data. + */ +typedef int (*http_data_cb) (http_parser*, const char *at, size_t length); +typedef int (*http_cb) (http_parser*); + + +/* Status Codes */ +#define HTTP_STATUS_MAP(XX) \ + XX(100, CONTINUE, Continue) \ + XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \ + XX(102, PROCESSING, Processing) \ + XX(200, OK, OK) \ + XX(201, CREATED, Created) \ + XX(202, ACCEPTED, Accepted) \ + XX(203, NON_AUTHORITATIVE_INFORMATION, Non-Authoritative Information) \ + XX(204, NO_CONTENT, No Content) \ + XX(205, RESET_CONTENT, Reset Content) \ + XX(206, PARTIAL_CONTENT, Partial Content) \ + XX(207, MULTI_STATUS, Multi-Status) \ + XX(208, ALREADY_REPORTED, Already Reported) \ + XX(226, IM_USED, IM Used) \ + XX(300, MULTIPLE_CHOICES, Multiple Choices) \ + XX(301, MOVED_PERMANENTLY, Moved Permanently) \ + XX(302, FOUND, Found) \ + XX(303, SEE_OTHER, See Other) \ + XX(304, NOT_MODIFIED, Not Modified) \ + XX(305, USE_PROXY, Use Proxy) \ + XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \ + XX(308, PERMANENT_REDIRECT, Permanent Redirect) \ + XX(400, BAD_REQUEST, Bad Request) \ + XX(401, UNAUTHORIZED, Unauthorized) \ + XX(402, PAYMENT_REQUIRED, Payment Required) \ + XX(403, FORBIDDEN, Forbidden) \ + XX(404, NOT_FOUND, Not Found) \ + XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \ + XX(406, NOT_ACCEPTABLE, Not Acceptable) \ + XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \ + XX(408, REQUEST_TIMEOUT, Request Timeout) \ + XX(409, CONFLICT, Conflict) \ + XX(410, GONE, Gone) \ + XX(411, LENGTH_REQUIRED, Length Required) \ + XX(412, PRECONDITION_FAILED, Precondition Failed) \ + XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \ + XX(414, URI_TOO_LONG, URI Too Long) \ + XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \ + XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \ + XX(417, EXPECTATION_FAILED, Expectation Failed) \ + XX(421, MISDIRECTED_REQUEST, Misdirected Request) \ + XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \ + XX(423, LOCKED, Locked) \ + XX(424, FAILED_DEPENDENCY, Failed Dependency) \ + XX(426, UPGRADE_REQUIRED, Upgrade Required) \ + XX(428, PRECONDITION_REQUIRED, Precondition Required) \ + XX(429, TOO_MANY_REQUESTS, Too Many Requests) \ + XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \ + XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \ + XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \ + XX(501, NOT_IMPLEMENTED, Not Implemented) \ + XX(502, BAD_GATEWAY, Bad Gateway) \ + XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \ + XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \ + XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \ + XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \ + XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \ + XX(508, LOOP_DETECTED, Loop Detected) \ + XX(510, NOT_EXTENDED, Not Extended) \ + XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) \ + +enum http_status + { +#define XX(num, name, string) HTTP_STATUS_##name = num, + HTTP_STATUS_MAP(XX) +#undef XX + }; + + +/* Request Methods */ +#define HTTP_METHOD_MAP(XX) \ + XX(0, DELETE, DELETE) \ + XX(1, GET, GET) \ + XX(2, HEAD, HEAD) \ + XX(3, POST, POST) \ + XX(4, PUT, PUT) \ + /* pathological */ \ + XX(5, CONNECT, CONNECT) \ + XX(6, OPTIONS, OPTIONS) \ + XX(7, TRACE, TRACE) \ + /* WebDAV */ \ + XX(8, COPY, COPY) \ + XX(9, LOCK, LOCK) \ + XX(10, MKCOL, MKCOL) \ + XX(11, MOVE, MOVE) \ + XX(12, PROPFIND, PROPFIND) \ + XX(13, PROPPATCH, PROPPATCH) \ + XX(14, SEARCH, SEARCH) \ + XX(15, UNLOCK, UNLOCK) \ + XX(16, BIND, BIND) \ + XX(17, REBIND, REBIND) \ + XX(18, UNBIND, UNBIND) \ + XX(19, ACL, ACL) \ + /* subversion */ \ + XX(20, REPORT, REPORT) \ + XX(21, MKACTIVITY, MKACTIVITY) \ + XX(22, CHECKOUT, CHECKOUT) \ + XX(23, MERGE, MERGE) \ + /* upnp */ \ + XX(24, MSEARCH, M-SEARCH) \ + XX(25, NOTIFY, NOTIFY) \ + XX(26, SUBSCRIBE, SUBSCRIBE) \ + XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \ + /* RFC-5789 */ \ + XX(28, PATCH, PATCH) \ + XX(29, PURGE, PURGE) \ + /* CalDAV */ \ + XX(30, MKCALENDAR, MKCALENDAR) \ + /* RFC-2068, section 19.6.1.2 */ \ + XX(31, LINK, LINK) \ + XX(32, UNLINK, UNLINK) \ + /* icecast */ \ + XX(33, SOURCE, SOURCE) \ + +enum http_method + { +#define XX(num, name, string) HTTP_##name = num, + HTTP_METHOD_MAP(XX) +#undef XX + }; + + +enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH }; + + +/* Flag values for http_parser.flags field */ +enum flags + { F_CHUNKED = 1 << 0 + , F_CONNECTION_KEEP_ALIVE = 1 << 1 + , F_CONNECTION_CLOSE = 1 << 2 + , F_CONNECTION_UPGRADE = 1 << 3 + , F_TRAILING = 1 << 4 + , F_UPGRADE = 1 << 5 + , F_SKIPBODY = 1 << 6 + , F_CONTENTLENGTH = 1 << 7 + }; + + +/* Map for errno-related constants + * + * The provided argument should be a macro that takes 2 arguments. + */ +#define HTTP_ERRNO_MAP(XX) \ + /* No error */ \ + XX(OK, "success") \ + \ + /* Callback-related errors */ \ + XX(CB_message_begin, "the on_message_begin callback failed") \ + XX(CB_url, "the on_url callback failed") \ + XX(CB_header_field, "the on_header_field callback failed") \ + XX(CB_header_value, "the on_header_value callback failed") \ + XX(CB_headers_complete, "the on_headers_complete callback failed") \ + XX(CB_body, "the on_body callback failed") \ + XX(CB_message_complete, "the on_message_complete callback failed") \ + XX(CB_status, "the on_status callback failed") \ + XX(CB_chunk_header, "the on_chunk_header callback failed") \ + XX(CB_chunk_complete, "the on_chunk_complete callback failed") \ + \ + /* Parsing-related errors */ \ + XX(INVALID_EOF_STATE, "stream ended at an unexpected time") \ + XX(HEADER_OVERFLOW, \ + "too many header bytes seen; overflow detected") \ + XX(CLOSED_CONNECTION, \ + "data received after completed connection: close message") \ + XX(INVALID_VERSION, "invalid HTTP version") \ + XX(INVALID_STATUS, "invalid HTTP status code") \ + XX(INVALID_METHOD, "invalid HTTP method") \ + XX(INVALID_URL, "invalid URL") \ + XX(INVALID_HOST, "invalid host") \ + XX(INVALID_PORT, "invalid port") \ + XX(INVALID_PATH, "invalid path") \ + XX(INVALID_QUERY_STRING, "invalid query string") \ + XX(INVALID_FRAGMENT, "invalid fragment") \ + XX(LF_EXPECTED, "LF character expected") \ + XX(INVALID_HEADER_TOKEN, "invalid character in header") \ + XX(INVALID_CONTENT_LENGTH, \ + "invalid character in content-length header") \ + XX(UNEXPECTED_CONTENT_LENGTH, \ + "unexpected content-length header") \ + XX(INVALID_CHUNK_SIZE, \ + "invalid character in chunk size header") \ + XX(INVALID_CONSTANT, "invalid constant string") \ + XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\ + XX(STRICT, "strict mode assertion failed") \ + XX(PAUSED, "parser is paused") \ + XX(UNKNOWN, "an unknown error occurred") \ + XX(INVALID_TRANSFER_ENCODING, \ + "request has invalid transfer-encoding") \ + + +/* Define HPE_* values for each errno value above */ +#define HTTP_ERRNO_GEN(n, s) HPE_##n, +enum http_errno { + HTTP_ERRNO_MAP(HTTP_ERRNO_GEN) +}; +#undef HTTP_ERRNO_GEN + + +/* Get an http_errno value from an http_parser */ +#define HTTP_PARSER_ERRNO(p) ((enum http_errno) (p)->http_errno) + + +struct http_parser { + /** PRIVATE **/ + unsigned int type : 2; /* enum http_parser_type */ + unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */ + unsigned int state : 7; /* enum state from http_parser.c */ + unsigned int header_state : 7; /* enum header_state from http_parser.c */ + unsigned int index : 5; /* index into current matcher */ + unsigned int uses_transfer_encoding : 1; /* Transfer-Encoding header is present */ + unsigned int allow_chunked_length : 1; /* Allow headers with both + * `Content-Length` and + * `Transfer-Encoding: chunked` set */ + unsigned int lenient_http_headers : 1; + + uint32_t nread; /* # bytes read in various scenarios */ + uint64_t content_length; /* # bytes in body. `(uint64_t) -1` (all bits one) + * if no Content-Length header. + */ + + /** READ-ONLY **/ + unsigned short http_major; + unsigned short http_minor; + unsigned int status_code : 16; /* responses only */ + unsigned int method : 8; /* requests only */ + unsigned int http_errno : 7; + + /* 1 = Upgrade header was present and the parser has exited because of that. + * 0 = No upgrade header present. + * Should be checked when http_parser_execute() returns in addition to + * error checking. + */ + unsigned int upgrade : 1; + + /** PUBLIC **/ + void *data; /* A pointer to get hook to the "connection" or "socket" object */ +}; + + +struct http_parser_settings { + http_cb on_message_begin; + http_data_cb on_url; + http_data_cb on_status; + http_data_cb on_header_field; + http_data_cb on_header_value; + http_cb on_headers_complete; + http_data_cb on_body; + http_cb on_message_complete; + /* When on_chunk_header is called, the current chunk length is stored + * in parser->content_length. + */ + http_cb on_chunk_header; + http_cb on_chunk_complete; +}; + + +enum http_parser_url_fields + { UF_SCHEMA = 0 + , UF_HOST = 1 + , UF_PORT = 2 + , UF_PATH = 3 + , UF_QUERY = 4 + , UF_FRAGMENT = 5 + , UF_USERINFO = 6 + , UF_MAX = 7 + }; + + +/* Result structure for http_parser_parse_url(). + * + * Callers should index into field_data[] with UF_* values iff field_set + * has the relevant (1 << UF_*) bit set. As a courtesy to clients (and + * because we probably have padding left over), we convert any port to + * a uint16_t. + */ +struct http_parser_url { + uint16_t field_set; /* Bitmask of (1 << UF_*) values */ + uint16_t port; /* Converted UF_PORT string */ + + struct { + uint16_t off; /* Offset into buffer in which field starts */ + uint16_t len; /* Length of run in buffer */ + } field_data[UF_MAX]; +}; + + +/* Returns the library version. Bits 16-23 contain the major version number, + * bits 8-15 the minor version number and bits 0-7 the patch level. + * Usage example: + * + * unsigned long version = http_parser_version(); + * unsigned major = (version >> 16) & 255; + * unsigned minor = (version >> 8) & 255; + * unsigned patch = version & 255; + * printf("http_parser v%u.%u.%u\n", major, minor, patch); + */ +unsigned long http_parser_version(void); + +void http_parser_init(http_parser *parser, enum http_parser_type type); + + +/* Initialize http_parser_settings members to 0 + */ +void http_parser_settings_init(http_parser_settings *settings); + + +/* Executes the parser. Returns number of parsed bytes. Sets + * `parser->http_errno` on error. */ +size_t http_parser_execute(http_parser *parser, + const http_parser_settings *settings, + const char *data, + size_t len); + + +/* If http_should_keep_alive() in the on_headers_complete or + * on_message_complete callback returns 0, then this should be + * the last message on the connection. + * If you are the server, respond with the "Connection: close" header. + * If you are the client, close the connection. + */ +int http_should_keep_alive(const http_parser *parser); + +/* Returns a string version of the HTTP method. */ +const char *http_method_str(enum http_method m); + +/* Returns a string version of the HTTP status code. */ +const char *http_status_str(enum http_status s); + +/* Return a string name of the given error */ +const char *http_errno_name(enum http_errno err); + +/* Return a string description of the given error */ +const char *http_errno_description(enum http_errno err); + +/* Initialize all http_parser_url members to 0 */ +void http_parser_url_init(struct http_parser_url *u); + +/* Parse a URL; return nonzero on failure */ +int http_parser_parse_url(const char *buf, size_t buflen, + int is_connect, + struct http_parser_url *u); + +/* Pause or un-pause the parser; a nonzero value pauses */ +void http_parser_pause(http_parser *parser, int paused); + +/* Checks if this is the final chunk of the body. */ +int http_body_is_final(const http_parser *parser); + +/* Change the maximum header size provided at compile time. */ +void http_parser_set_max_header_size(uint32_t size); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/test.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/test.c new file mode 100644 index 00000000000000..3f7c77b34944e5 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http-parser/test.c @@ -0,0 +1,4729 @@ +/* Copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include "http_parser.h" +#include +#include +#include +#include /* rand */ +#include +#include + +#if defined(__APPLE__) +# undef strlncpy +#endif /* defined(__APPLE__) */ + +#undef TRUE +#define TRUE 1 +#undef FALSE +#define FALSE 0 + +#define MAX_HEADERS 13 +#define MAX_ELEMENT_SIZE 2048 +#define MAX_CHUNKS 16 + +#define MIN(a,b) ((a) < (b) ? (a) : (b)) + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof(*x)) + +static http_parser parser; + +struct message { + const char *name; // for debugging purposes + const char *raw; + enum http_parser_type type; + enum http_method method; + int status_code; + char response_status[MAX_ELEMENT_SIZE]; + char request_path[MAX_ELEMENT_SIZE]; + char request_url[MAX_ELEMENT_SIZE]; + char fragment[MAX_ELEMENT_SIZE]; + char query_string[MAX_ELEMENT_SIZE]; + char body[MAX_ELEMENT_SIZE]; + size_t body_size; + const char *host; + const char *userinfo; + uint16_t port; + int num_headers; + enum { NONE=0, FIELD, VALUE } last_header_element; + char headers [MAX_HEADERS][2][MAX_ELEMENT_SIZE]; + int should_keep_alive; + + int num_chunks; + int num_chunks_complete; + int chunk_lengths[MAX_CHUNKS]; + + const char *upgrade; // upgraded body + + unsigned short http_major; + unsigned short http_minor; + uint64_t content_length; + + int message_begin_cb_called; + int headers_complete_cb_called; + int message_complete_cb_called; + int status_cb_called; + int message_complete_on_eof; + int body_is_final; + int allow_chunked_length; +}; + +static int currently_parsing_eof; + +static struct message messages[5]; +static int num_messages; +static http_parser_settings *current_pause_parser; + +/* * R E Q U E S T S * */ +const struct message requests[] = +#define CURL_GET 0 +{ {.name= "curl get" + ,.type= HTTP_REQUEST + ,.raw= "GET /test HTTP/1.1\r\n" + "User-Agent: curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1\r\n" + "Host: 0.0.0.0=5000\r\n" + "Accept: */*\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/test" + ,.request_url= "/test" + ,.content_length= -1 + ,.num_headers= 3 + ,.headers= + { { "User-Agent", "curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1" } + , { "Host", "0.0.0.0=5000" } + , { "Accept", "*/*" } + } + ,.body= "" + } + +#define FIREFOX_GET 1 +, {.name= "firefox get" + ,.type= HTTP_REQUEST + ,.raw= "GET /favicon.ico HTTP/1.1\r\n" + "Host: 0.0.0.0=5000\r\n" + "User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "Accept-Language: en-us,en;q=0.5\r\n" + "Accept-Encoding: gzip,deflate\r\n" + "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + "Keep-Alive: 300\r\n" + "Connection: keep-alive\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/favicon.ico" + ,.request_url= "/favicon.ico" + ,.content_length= -1 + ,.num_headers= 8 + ,.headers= + { { "Host", "0.0.0.0=5000" } + , { "User-Agent", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0" } + , { "Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } + , { "Accept-Language", "en-us,en;q=0.5" } + , { "Accept-Encoding", "gzip,deflate" } + , { "Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7" } + , { "Keep-Alive", "300" } + , { "Connection", "keep-alive" } + } + ,.body= "" + } + +#define DUMBLUCK 2 +, {.name= "dumbluck" + ,.type= HTTP_REQUEST + ,.raw= "GET /dumbluck HTTP/1.1\r\n" + "aaaaaaaaaaaaa:++++++++++\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/dumbluck" + ,.request_url= "/dumbluck" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "aaaaaaaaaaaaa", "++++++++++" } + } + ,.body= "" + } + +#define FRAGMENT_IN_URI 3 +, {.name= "fragment in url" + ,.type= HTTP_REQUEST + ,.raw= "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "page=1" + ,.fragment= "posts-17408" + ,.request_path= "/forums/1/topics/2375" + /* XXX request url does include fragment? */ + ,.request_url= "/forums/1/topics/2375?page=1#posts-17408" + ,.content_length= -1 + ,.num_headers= 0 + ,.body= "" + } + +#define GET_NO_HEADERS_NO_BODY 4 +, {.name= "get no headers no body" + ,.type= HTTP_REQUEST + ,.raw= "GET /get_no_headers_no_body/world HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE /* would need Connection: close */ + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/get_no_headers_no_body/world" + ,.request_url= "/get_no_headers_no_body/world" + ,.content_length= -1 + ,.num_headers= 0 + ,.body= "" + } + +#define GET_ONE_HEADER_NO_BODY 5 +, {.name= "get one header no body" + ,.type= HTTP_REQUEST + ,.raw= "GET /get_one_header_no_body HTTP/1.1\r\n" + "Accept: */*\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE /* would need Connection: close */ + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/get_one_header_no_body" + ,.request_url= "/get_one_header_no_body" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "Accept" , "*/*" } + } + ,.body= "" + } + +#define GET_FUNKY_CONTENT_LENGTH 6 +, {.name= "get funky content length body hello" + ,.type= HTTP_REQUEST + ,.raw= "GET /get_funky_content_length_body_hello HTTP/1.0\r\n" + "conTENT-Length: 5\r\n" + "\r\n" + "HELLO" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/get_funky_content_length_body_hello" + ,.request_url= "/get_funky_content_length_body_hello" + ,.content_length= 5 + ,.num_headers= 1 + ,.headers= + { { "conTENT-Length" , "5" } + } + ,.body= "HELLO" + } + +#define POST_IDENTITY_BODY_WORLD 7 +, {.name= "post identity body world" + ,.type= HTTP_REQUEST + ,.raw= "POST /post_identity_body_world?q=search#hey HTTP/1.1\r\n" + "Accept: */*\r\n" + "Content-Length: 5\r\n" + "\r\n" + "World" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "q=search" + ,.fragment= "hey" + ,.request_path= "/post_identity_body_world" + ,.request_url= "/post_identity_body_world?q=search#hey" + ,.content_length= 5 + ,.num_headers= 2 + ,.headers= + { { "Accept", "*/*" } + , { "Content-Length", "5" } + } + ,.body= "World" + } + +#define POST_CHUNKED_ALL_YOUR_BASE 8 +, {.name= "post - chunked body: all your base are belong to us" + ,.type= HTTP_REQUEST + ,.raw= "POST /post_chunked_all_your_base HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "1e\r\nall your base are belong to us\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/post_chunked_all_your_base" + ,.request_url= "/post_chunked_all_your_base" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "Transfer-Encoding" , "chunked" } + } + ,.body= "all your base are belong to us" + ,.num_chunks_complete= 2 + ,.chunk_lengths= { 0x1e } + } + +#define TWO_CHUNKS_MULT_ZERO_END 9 +, {.name= "two chunks ; triple zero ending" + ,.type= HTTP_REQUEST + ,.raw= "POST /two_chunks_mult_zero_end HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "6\r\n world\r\n" + "000\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/two_chunks_mult_zero_end" + ,.request_url= "/two_chunks_mult_zero_end" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "Transfer-Encoding", "chunked" } + } + ,.body= "hello world" + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 5, 6 } + } + +#define CHUNKED_W_TRAILING_HEADERS 10 +, {.name= "chunked with trailing headers. blech." + ,.type= HTTP_REQUEST + ,.raw= "POST /chunked_w_trailing_headers HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "6\r\n world\r\n" + "0\r\n" + "Vary: *\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/chunked_w_trailing_headers" + ,.request_url= "/chunked_w_trailing_headers" + ,.content_length= -1 + ,.num_headers= 3 + ,.headers= + { { "Transfer-Encoding", "chunked" } + , { "Vary", "*" } + , { "Content-Type", "text/plain" } + } + ,.body= "hello world" + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 5, 6 } + } + +#define CHUNKED_W_NONSENSE_AFTER_LENGTH 11 +, {.name= "with nonsense after the length" + ,.type= HTTP_REQUEST + ,.raw= "POST /chunked_w_nonsense_after_length HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5; ilovew3;whattheluck=aretheseparametersfor\r\nhello\r\n" + "6; blahblah; blah\r\n world\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/chunked_w_nonsense_after_length" + ,.request_url= "/chunked_w_nonsense_after_length" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "Transfer-Encoding", "chunked" } + } + ,.body= "hello world" + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 5, 6 } + } + +#define WITH_QUOTES 12 +, {.name= "with quotes" + ,.type= HTTP_REQUEST + ,.raw= "GET /with_\"stupid\"_quotes?foo=\"bar\" HTTP/1.1\r\n\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "foo=\"bar\"" + ,.fragment= "" + ,.request_path= "/with_\"stupid\"_quotes" + ,.request_url= "/with_\"stupid\"_quotes?foo=\"bar\"" + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define APACHEBENCH_GET 13 +/* The server receiving this request SHOULD NOT wait for EOF + * to know that content-length == 0. + * How to represent this in a unit test? message_complete_on_eof + * Compare with NO_CONTENT_LENGTH_RESPONSE. + */ +, {.name = "apachebench get" + ,.type= HTTP_REQUEST + ,.raw= "GET /test HTTP/1.0\r\n" + "Host: 0.0.0.0:5000\r\n" + "User-Agent: ApacheBench/2.3\r\n" + "Accept: */*\r\n\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/test" + ,.request_url= "/test" + ,.content_length= -1 + ,.num_headers= 3 + ,.headers= { { "Host", "0.0.0.0:5000" } + , { "User-Agent", "ApacheBench/2.3" } + , { "Accept", "*/*" } + } + ,.body= "" + } + +#define QUERY_URL_WITH_QUESTION_MARK_GET 14 +/* Some clients include '?' characters in query strings. + */ +, {.name = "query url with question mark" + ,.type= HTTP_REQUEST + ,.raw= "GET /test.cgi?foo=bar?baz HTTP/1.1\r\n\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "foo=bar?baz" + ,.fragment= "" + ,.request_path= "/test.cgi" + ,.request_url= "/test.cgi?foo=bar?baz" + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= {} + ,.body= "" + } + +#define PREFIX_NEWLINE_GET 15 +/* Some clients, especially after a POST in a keep-alive connection, + * will send an extra CRLF before the next request + */ +, {.name = "newline prefix get" + ,.type= HTTP_REQUEST + ,.raw= "\r\nGET /test HTTP/1.1\r\n\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/test" + ,.request_url= "/test" + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define UPGRADE_REQUEST 16 +, {.name = "upgrade request" + ,.type= HTTP_REQUEST + ,.raw= "GET /demo HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\n" + "Sec-WebSocket-Protocol: sample\r\n" + "Upgrade: WebSocket\r\n" + "Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\n" + "Origin: http://example.com\r\n" + "\r\n" + "Hot diggity dogg" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/demo" + ,.request_url= "/demo" + ,.content_length= -1 + ,.num_headers= 7 + ,.upgrade="Hot diggity dogg" + ,.headers= { { "Host", "example.com" } + , { "Connection", "Upgrade" } + , { "Sec-WebSocket-Key2", "12998 5 Y3 1 .P00" } + , { "Sec-WebSocket-Protocol", "sample" } + , { "Upgrade", "WebSocket" } + , { "Sec-WebSocket-Key1", "4 @1 46546xW%0l 1 5" } + , { "Origin", "http://example.com" } + } + ,.body= "" + } + +#define CONNECT_REQUEST 17 +, {.name = "connect request" + ,.type= HTTP_REQUEST + ,.raw= "CONNECT 0-home0.netscape.com:443 HTTP/1.0\r\n" + "User-agent: Mozilla/1.1N\r\n" + "Proxy-authorization: basic aGVsbG86d29ybGQ=\r\n" + "\r\n" + "some data\r\n" + "and yet even more data" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_CONNECT + ,.query_string= "" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "0-home0.netscape.com:443" + ,.content_length= -1 + ,.num_headers= 2 + ,.upgrade="some data\r\nand yet even more data" + ,.headers= { { "User-agent", "Mozilla/1.1N" } + , { "Proxy-authorization", "basic aGVsbG86d29ybGQ=" } + } + ,.body= "" + } + +#define REPORT_REQ 18 +, {.name= "report request" + ,.type= HTTP_REQUEST + ,.raw= "REPORT /test HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_REPORT + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/test" + ,.request_url= "/test" + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= {} + ,.body= "" + } + +#define NO_HTTP_VERSION 19 +, {.name= "request with no http version" + ,.type= HTTP_REQUEST + ,.raw= "GET /\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 0 + ,.http_minor= 9 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= {} + ,.body= "" + } + +#define MSEARCH_REQ 20 +, {.name= "m-search request" + ,.type= HTTP_REQUEST + ,.raw= "M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "MAN: \"ssdp:discover\"\r\n" + "ST: \"ssdp:all\"\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_MSEARCH + ,.query_string= "" + ,.fragment= "" + ,.request_path= "*" + ,.request_url= "*" + ,.content_length= -1 + ,.num_headers= 3 + ,.headers= { { "HOST", "239.255.255.250:1900" } + , { "MAN", "\"ssdp:discover\"" } + , { "ST", "\"ssdp:all\"" } + } + ,.body= "" + } + +#define LINE_FOLDING_IN_HEADER 21 +, {.name= "line folding in header value" + ,.type= HTTP_REQUEST + ,.raw= "GET / HTTP/1.1\r\n" + "Line1: abc\r\n" + "\tdef\r\n" + " ghi\r\n" + "\t\tjkl\r\n" + " mno \r\n" + "\t \tqrs\r\n" + "Line2: \t line2\t\r\n" + "Line3:\r\n" + " line3\r\n" + "Line4: \r\n" + " \r\n" + "Connection:\r\n" + " close\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.content_length= -1 + ,.num_headers= 5 + ,.headers= { { "Line1", "abc\tdef ghi\t\tjkl mno \t \tqrs" } + , { "Line2", "line2\t" } + , { "Line3", "line3" } + , { "Line4", "" } + , { "Connection", "close" }, + } + ,.body= "" + } + + +#define QUERY_TERMINATED_HOST 22 +, {.name= "host terminated by a query string" + ,.type= HTTP_REQUEST + ,.raw= "GET http://hypnotoad.org?hail=all HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "hail=all" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "http://hypnotoad.org?hail=all" + ,.host= "hypnotoad.org" + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define QUERY_TERMINATED_HOSTPORT 23 +, {.name= "host:port terminated by a query string" + ,.type= HTTP_REQUEST + ,.raw= "GET http://hypnotoad.org:1234?hail=all HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "hail=all" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "http://hypnotoad.org:1234?hail=all" + ,.host= "hypnotoad.org" + ,.port= 1234 + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define SPACE_TERMINATED_HOSTPORT 24 +, {.name= "host:port terminated by a space" + ,.type= HTTP_REQUEST + ,.raw= "GET http://hypnotoad.org:1234 HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "http://hypnotoad.org:1234" + ,.host= "hypnotoad.org" + ,.port= 1234 + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define PATCH_REQ 25 +, {.name = "PATCH request" + ,.type= HTTP_REQUEST + ,.raw= "PATCH /file.txt HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Type: application/example\r\n" + "If-Match: \"e0023aa4e\"\r\n" + "Content-Length: 10\r\n" + "\r\n" + "cccccccccc" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_PATCH + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/file.txt" + ,.request_url= "/file.txt" + ,.content_length= 10 + ,.num_headers= 4 + ,.headers= { { "Host", "www.example.com" } + , { "Content-Type", "application/example" } + , { "If-Match", "\"e0023aa4e\"" } + , { "Content-Length", "10" } + } + ,.body= "cccccccccc" + } + +#define CONNECT_CAPS_REQUEST 26 +, {.name = "connect caps request" + ,.type= HTTP_REQUEST + ,.raw= "CONNECT HOME0.NETSCAPE.COM:443 HTTP/1.0\r\n" + "User-agent: Mozilla/1.1N\r\n" + "Proxy-authorization: basic aGVsbG86d29ybGQ=\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_CONNECT + ,.query_string= "" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "HOME0.NETSCAPE.COM:443" + ,.content_length= -1 + ,.num_headers= 2 + ,.upgrade="" + ,.headers= { { "User-agent", "Mozilla/1.1N" } + , { "Proxy-authorization", "basic aGVsbG86d29ybGQ=" } + } + ,.body= "" + } + +#if !HTTP_PARSER_STRICT +#define UTF8_PATH_REQ 27 +, {.name= "utf-8 path request" + ,.type= HTTP_REQUEST + ,.raw= "GET /δ¶/δt/pope?q=1#narf HTTP/1.1\r\n" + "Host: github.com\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "q=1" + ,.fragment= "narf" + ,.request_path= "/δ¶/δt/pope" + ,.request_url= "/δ¶/δt/pope?q=1#narf" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= { {"Host", "github.com" } + } + ,.body= "" + } + +#define HOSTNAME_UNDERSCORE 28 +, {.name = "hostname underscore" + ,.type= HTTP_REQUEST + ,.raw= "CONNECT home_0.netscape.com:443 HTTP/1.0\r\n" + "User-agent: Mozilla/1.1N\r\n" + "Proxy-authorization: basic aGVsbG86d29ybGQ=\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_CONNECT + ,.query_string= "" + ,.fragment= "" + ,.request_path= "" + ,.request_url= "home_0.netscape.com:443" + ,.content_length= -1 + ,.num_headers= 2 + ,.upgrade="" + ,.headers= { { "User-agent", "Mozilla/1.1N" } + , { "Proxy-authorization", "basic aGVsbG86d29ybGQ=" } + } + ,.body= "" + } +#endif /* !HTTP_PARSER_STRICT */ + +/* see https://github.com/ry/http-parser/issues/47 */ +#define EAT_TRAILING_CRLF_NO_CONNECTION_CLOSE 29 +, {.name = "eat CRLF between requests, no \"Connection: close\" header" + ,.raw= "POST / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: 4\r\n" + "\r\n" + "q=42\r\n" /* note the trailing CRLF */ + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.content_length= 4 + ,.num_headers= 3 + ,.upgrade= 0 + ,.headers= { { "Host", "www.example.com" } + , { "Content-Type", "application/x-www-form-urlencoded" } + , { "Content-Length", "4" } + } + ,.body= "q=42" + } + +/* see https://github.com/ry/http-parser/issues/47 */ +#define EAT_TRAILING_CRLF_WITH_CONNECTION_CLOSE 30 +, {.name = "eat CRLF between requests even if \"Connection: close\" is set" + ,.raw= "POST / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: 4\r\n" + "Connection: close\r\n" + "\r\n" + "q=42\r\n" /* note the trailing CRLF */ + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE /* input buffer isn't empty when on_message_complete is called */ + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.content_length= 4 + ,.num_headers= 4 + ,.upgrade= 0 + ,.headers= { { "Host", "www.example.com" } + , { "Content-Type", "application/x-www-form-urlencoded" } + , { "Content-Length", "4" } + , { "Connection", "close" } + } + ,.body= "q=42" + } + +#define PURGE_REQ 31 +, {.name = "PURGE request" + ,.type= HTTP_REQUEST + ,.raw= "PURGE /file.txt HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_PURGE + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/file.txt" + ,.request_url= "/file.txt" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= { { "Host", "www.example.com" } } + ,.body= "" + } + +#define SEARCH_REQ 32 +, {.name = "SEARCH request" + ,.type= HTTP_REQUEST + ,.raw= "SEARCH / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_SEARCH + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= { { "Host", "www.example.com" } } + ,.body= "" + } + +#define PROXY_WITH_BASIC_AUTH 33 +, {.name= "host:port and basic_auth" + ,.type= HTTP_REQUEST + ,.raw= "GET http://a%12:b!&*$@hypnotoad.org:1234/toto HTTP/1.1\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.fragment= "" + ,.request_path= "/toto" + ,.request_url= "http://a%12:b!&*$@hypnotoad.org:1234/toto" + ,.host= "hypnotoad.org" + ,.userinfo= "a%12:b!&*$" + ,.port= 1234 + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= { } + ,.body= "" + } + +#define LINE_FOLDING_IN_HEADER_WITH_LF 34 +, {.name= "line folding in header value" + ,.type= HTTP_REQUEST + ,.raw= "GET / HTTP/1.1\n" + "Line1: abc\n" + "\tdef\n" + " ghi\n" + "\t\tjkl\n" + " mno \n" + "\t \tqrs\n" + "Line2: \t line2\t\n" + "Line3:\n" + " line3\n" + "Line4: \n" + " \n" + "Connection:\n" + " close\n" + "\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.content_length= -1 + ,.num_headers= 5 + ,.headers= { { "Line1", "abc\tdef ghi\t\tjkl mno \t \tqrs" } + , { "Line2", "line2\t" } + , { "Line3", "line3" } + , { "Line4", "" } + , { "Connection", "close" }, + } + ,.body= "" + } + +#define CONNECTION_MULTI 35 +, {.name = "multiple connection header values with folding" + ,.type= HTTP_REQUEST + ,.raw= "GET /demo HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: Something,\r\n" + " Upgrade, ,Keep-Alive\r\n" + "Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\n" + "Sec-WebSocket-Protocol: sample\r\n" + "Upgrade: WebSocket\r\n" + "Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\n" + "Origin: http://example.com\r\n" + "\r\n" + "Hot diggity dogg" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/demo" + ,.request_url= "/demo" + ,.content_length= -1 + ,.num_headers= 7 + ,.upgrade="Hot diggity dogg" + ,.headers= { { "Host", "example.com" } + , { "Connection", "Something, Upgrade, ,Keep-Alive" } + , { "Sec-WebSocket-Key2", "12998 5 Y3 1 .P00" } + , { "Sec-WebSocket-Protocol", "sample" } + , { "Upgrade", "WebSocket" } + , { "Sec-WebSocket-Key1", "4 @1 46546xW%0l 1 5" } + , { "Origin", "http://example.com" } + } + ,.body= "" + } + +#define CONNECTION_MULTI_LWS 36 +, {.name = "multiple connection header values with folding and lws" + ,.type= HTTP_REQUEST + ,.raw= "GET /demo HTTP/1.1\r\n" + "Connection: keep-alive, upgrade\r\n" + "Upgrade: WebSocket\r\n" + "\r\n" + "Hot diggity dogg" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/demo" + ,.request_url= "/demo" + ,.content_length= -1 + ,.num_headers= 2 + ,.upgrade="Hot diggity dogg" + ,.headers= { { "Connection", "keep-alive, upgrade" } + , { "Upgrade", "WebSocket" } + } + ,.body= "" + } + +#define CONNECTION_MULTI_LWS_CRLF 37 +, {.name = "multiple connection header values with folding and lws" + ,.type= HTTP_REQUEST + ,.raw= "GET /demo HTTP/1.1\r\n" + "Connection: keep-alive, \r\n upgrade\r\n" + "Upgrade: WebSocket\r\n" + "\r\n" + "Hot diggity dogg" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_GET + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/demo" + ,.request_url= "/demo" + ,.content_length= -1 + ,.num_headers= 2 + ,.upgrade="Hot diggity dogg" + ,.headers= { { "Connection", "keep-alive, upgrade" } + , { "Upgrade", "WebSocket" } + } + ,.body= "" + } + +#define UPGRADE_POST_REQUEST 38 +, {.name = "upgrade post request" + ,.type= HTTP_REQUEST + ,.raw= "POST /demo HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: Upgrade\r\n" + "Upgrade: HTTP/2.0\r\n" + "Content-Length: 15\r\n" + "\r\n" + "sweet post body" + "Hot diggity dogg" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.request_path= "/demo" + ,.request_url= "/demo" + ,.content_length= 15 + ,.num_headers= 4 + ,.upgrade="Hot diggity dogg" + ,.headers= { { "Host", "example.com" } + , { "Connection", "Upgrade" } + , { "Upgrade", "HTTP/2.0" } + , { "Content-Length", "15" } + } + ,.body= "sweet post body" + } + +#define CONNECT_WITH_BODY_REQUEST 39 +, {.name = "connect with body request" + ,.type= HTTP_REQUEST + ,.raw= "CONNECT foo.bar.com:443 HTTP/1.0\r\n" + "User-agent: Mozilla/1.1N\r\n" + "Proxy-authorization: basic aGVsbG86d29ybGQ=\r\n" + "Content-Length: 10\r\n" + "\r\n" + "blarfcicle" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_CONNECT + ,.request_url= "foo.bar.com:443" + ,.content_length= 10 + ,.num_headers= 3 + ,.upgrade="blarfcicle" + ,.headers= { { "User-agent", "Mozilla/1.1N" } + , { "Proxy-authorization", "basic aGVsbG86d29ybGQ=" } + , { "Content-Length", "10" } + } + ,.body= "" + } + +/* Examples from the Internet draft for LINK/UNLINK methods: + * https://tools.ietf.org/id/draft-snell-link-method-01.html#rfc.section.5 + */ + +#define LINK_REQUEST 40 +, {.name = "link request" + ,.type= HTTP_REQUEST + ,.raw= "LINK /images/my_dog.jpg HTTP/1.1\r\n" + "Host: example.com\r\n" + "Link: ; rel=\"tag\"\r\n" + "Link: ; rel=\"tag\"\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_LINK + ,.request_path= "/images/my_dog.jpg" + ,.request_url= "/images/my_dog.jpg" + ,.query_string= "" + ,.fragment= "" + ,.content_length= -1 + ,.num_headers= 3 + ,.headers= { { "Host", "example.com" } + , { "Link", "; rel=\"tag\"" } + , { "Link", "; rel=\"tag\"" } + } + ,.body= "" + } + +#define UNLINK_REQUEST 41 +, {.name = "unlink request" + ,.type= HTTP_REQUEST + ,.raw= "UNLINK /images/my_dog.jpg HTTP/1.1\r\n" + "Host: example.com\r\n" + "Link: ; rel=\"tag\"\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_UNLINK + ,.request_path= "/images/my_dog.jpg" + ,.request_url= "/images/my_dog.jpg" + ,.query_string= "" + ,.fragment= "" + ,.content_length= -1 + ,.num_headers= 2 + ,.headers= { { "Host", "example.com" } + , { "Link", "; rel=\"tag\"" } + } + ,.body= "" + } + +#define SOURCE_REQUEST 42 +, {.name = "source request" + ,.type= HTTP_REQUEST + ,.raw= "SOURCE /music/sweet/music HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_SOURCE + ,.request_path= "/music/sweet/music" + ,.request_url= "/music/sweet/music" + ,.query_string= "" + ,.fragment= "" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= { { "Host", "example.com" } } + ,.body= "" + } + +#define SOURCE_ICE_REQUEST 43 +, {.name = "source request" + ,.type= HTTP_REQUEST + ,.raw= "SOURCE /music/sweet/music ICE/1.0\r\n" + "Host: example.com\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_SOURCE + ,.request_path= "/music/sweet/music" + ,.request_url= "/music/sweet/music" + ,.query_string= "" + ,.fragment= "" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= { { "Host", "example.com" } } + ,.body= "" + } + +#define POST_MULTI_TE_LAST_CHUNKED 44 +, {.name= "post - multi coding transfer-encoding chunked body" + ,.type= HTTP_REQUEST + ,.raw= "POST / HTTP/1.1\r\n" + "Transfer-Encoding: deflate, chunked\r\n" + "\r\n" + "1e\r\nall your base are belong to us\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "Transfer-Encoding" , "deflate, chunked" } + } + ,.body= "all your base are belong to us" + ,.num_chunks_complete= 2 + ,.chunk_lengths= { 0x1e } + } + +#define POST_MULTI_LINE_TE_LAST_CHUNKED 45 +, {.name= "post - multi line coding transfer-encoding chunked body" + ,.type= HTTP_REQUEST + ,.raw= "POST / HTTP/1.1\r\n" + "Transfer-Encoding: deflate,\r\n" + " chunked\r\n" + "\r\n" + "1e\r\nall your base are belong to us\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/" + ,.request_url= "/" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "Transfer-Encoding" , "deflate, chunked" } + } + ,.body= "all your base are belong to us" + ,.num_chunks_complete= 2 + ,.chunk_lengths= { 0x1e } + } + +#define CHUNKED_CONTENT_LENGTH 46 +, {.name= "chunked with content-length set, allow_chunked_length flag is set" + ,.type= HTTP_REQUEST + ,.raw= "POST /chunked_w_content_length HTTP/1.1\r\n" + "Content-Length: 10\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5; ilovew3;whattheluck=aretheseparametersfor\r\nhello\r\n" + "6; blahblah; blah\r\n world\r\n" + "0\r\n" + "\r\n" + ,.allow_chunked_length = 1 + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_POST + ,.query_string= "" + ,.fragment= "" + ,.request_path= "/chunked_w_content_length" + ,.request_url= "/chunked_w_content_length" + ,.content_length= 10 + ,.num_headers= 2 + ,.headers={ { "Content-Length", "10"} + , { "Transfer-Encoding", "chunked" } + } + ,.body= "hello world" + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 5, 6 } + } +}; + +/* * R E S P O N S E S * */ +const struct message responses[] = +#define GOOGLE_301 0 +{ {.name= "google 301" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 301 Moved Permanently\r\n" + "Location: http://www.google.com/\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "Date: Sun, 26 Apr 2009 11:11:49 GMT\r\n" + "Expires: Tue, 26 May 2009 11:11:49 GMT\r\n" + "X-$PrototypeBI-Version: 1.6.0.3\r\n" /* $ char in header field */ + "Cache-Control: public, max-age=2592000\r\n" + "Server: gws\r\n" + "Content-Length: 219 \r\n" + "\r\n" + "\n" + "301 Moved\n" + "

301 Moved

\n" + "The document has moved\n" + "here.\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 301 + ,.response_status= "Moved Permanently" + ,.content_length= 219 + ,.num_headers= 8 + ,.headers= + { { "Location", "http://www.google.com/" } + , { "Content-Type", "text/html; charset=UTF-8" } + , { "Date", "Sun, 26 Apr 2009 11:11:49 GMT" } + , { "Expires", "Tue, 26 May 2009 11:11:49 GMT" } + , { "X-$PrototypeBI-Version", "1.6.0.3" } + , { "Cache-Control", "public, max-age=2592000" } + , { "Server", "gws" } + , { "Content-Length", "219 " } + } + ,.body= "\n" + "301 Moved\n" + "

301 Moved

\n" + "The document has moved\n" + "here.\r\n" + "\r\n" + } + +#define NO_CONTENT_LENGTH_RESPONSE 1 +/* The client should wait for the server's EOF. That is, when content-length + * is not specified, and "Connection: close", the end of body is specified + * by the EOF. + * Compare with APACHEBENCH_GET + */ +, {.name= "no content-length response" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Date: Tue, 04 Aug 2009 07:59:32 GMT\r\n" + "Server: Apache\r\n" + "X-Powered-By: Servlet/2.5 JSP/2.1\r\n" + "Content-Type: text/xml; charset=utf-8\r\n" + "Connection: close\r\n" + "\r\n" + "\n" + "\n" + " \n" + " \n" + " SOAP-ENV:Client\n" + " Client Error\n" + " \n" + " \n" + "" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 5 + ,.headers= + { { "Date", "Tue, 04 Aug 2009 07:59:32 GMT" } + , { "Server", "Apache" } + , { "X-Powered-By", "Servlet/2.5 JSP/2.1" } + , { "Content-Type", "text/xml; charset=utf-8" } + , { "Connection", "close" } + } + ,.body= "\n" + "\n" + " \n" + " \n" + " SOAP-ENV:Client\n" + " Client Error\n" + " \n" + " \n" + "" + } + +#define NO_HEADERS_NO_BODY_404 2 +, {.name= "404 no headers no body" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 404 Not Found\r\n\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 404 + ,.response_status= "Not Found" + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= {} + ,.body_size= 0 + ,.body= "" + } + +#define NO_REASON_PHRASE 3 +, {.name= "301 no response phrase" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 301\r\n\r\n" + ,.should_keep_alive = FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 301 + ,.content_length= -1 + ,.response_status= "" + ,.num_headers= 0 + ,.headers= {} + ,.body= "" + } + +#define TRAILING_SPACE_ON_CHUNKED_BODY 4 +, {.name="200 trailing space on chunked body" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "25 \r\n" + "This is the data in the first chunk\r\n" + "\r\n" + "1C\r\n" + "and this is the second one\r\n" + "\r\n" + "0 \r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 2 + ,.headers= + { {"Content-Type", "text/plain" } + , {"Transfer-Encoding", "chunked" } + } + ,.body_size = 37+28 + ,.body = + "This is the data in the first chunk\r\n" + "and this is the second one\r\n" + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 0x25, 0x1c } + } + +#define NO_CARRIAGE_RET 5 +, {.name="no carriage ret" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\n" + "Content-Type: text/html; charset=utf-8\n" + "Connection: close\n" + "\n" + "these headers are from http://news.ycombinator.com/" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 2 + ,.headers= + { {"Content-Type", "text/html; charset=utf-8" } + , {"Connection", "close" } + } + ,.body= "these headers are from http://news.ycombinator.com/" + } + +#define PROXY_CONNECTION 6 +, {.name="proxy connection" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "Content-Length: 11\r\n" + "Proxy-Connection: close\r\n" + "Date: Thu, 31 Dec 2009 20:55:48 +0000\r\n" + "\r\n" + "hello world" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= 11 + ,.num_headers= 4 + ,.headers= + { {"Content-Type", "text/html; charset=UTF-8" } + , {"Content-Length", "11" } + , {"Proxy-Connection", "close" } + , {"Date", "Thu, 31 Dec 2009 20:55:48 +0000"} + } + ,.body= "hello world" + } + +#define UNDERSTORE_HEADER_KEY 7 + // shown by + // curl -o /dev/null -v "http://ad.doubleclick.net/pfadx/DARTSHELLCONFIGXML;dcmt=text/xml;" +, {.name="underscore header key" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Server: DCLK-AdSvr\r\n" + "Content-Type: text/xml\r\n" + "Content-Length: 0\r\n" + "DCLK_imp: v7;x;114750856;0-0;0;17820020;0/0;21603567/21621457/1;;~okv=;dcmt=text/xml;;~cs=o\r\n\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= 0 + ,.num_headers= 4 + ,.headers= + { {"Server", "DCLK-AdSvr" } + , {"Content-Type", "text/xml" } + , {"Content-Length", "0" } + , {"DCLK_imp", "v7;x;114750856;0-0;0;17820020;0/0;21603567/21621457/1;;~okv=;dcmt=text/xml;;~cs=o" } + } + ,.body= "" + } + +#define BONJOUR_MADAME_FR 8 +/* The client should not merge two headers fields when the first one doesn't + * have a value. + */ +, {.name= "bonjourmadame.fr" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.0 301 Moved Permanently\r\n" + "Date: Thu, 03 Jun 2010 09:56:32 GMT\r\n" + "Server: Apache/2.2.3 (Red Hat)\r\n" + "Cache-Control: public\r\n" + "Pragma: \r\n" + "Location: http://www.bonjourmadame.fr/\r\n" + "Vary: Accept-Encoding\r\n" + "Content-Length: 0\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "Connection: keep-alive\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.status_code= 301 + ,.response_status= "Moved Permanently" + ,.content_length= 0 + ,.num_headers= 9 + ,.headers= + { { "Date", "Thu, 03 Jun 2010 09:56:32 GMT" } + , { "Server", "Apache/2.2.3 (Red Hat)" } + , { "Cache-Control", "public" } + , { "Pragma", "" } + , { "Location", "http://www.bonjourmadame.fr/" } + , { "Vary", "Accept-Encoding" } + , { "Content-Length", "0" } + , { "Content-Type", "text/html; charset=UTF-8" } + , { "Connection", "keep-alive" } + } + ,.body= "" + } + +#define RES_FIELD_UNDERSCORE 9 +/* Should handle spaces in header fields */ +, {.name= "field underscore" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Date: Tue, 28 Sep 2010 01:14:13 GMT\r\n" + "Server: Apache\r\n" + "Cache-Control: no-cache, must-revalidate\r\n" + "Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n" + ".et-Cookie: PlaxoCS=1274804622353690521; path=/; domain=.plaxo.com\r\n" + "Vary: Accept-Encoding\r\n" + "_eep-Alive: timeout=45\r\n" /* semantic value ignored */ + "_onnection: Keep-Alive\r\n" /* semantic value ignored */ + "Transfer-Encoding: chunked\r\n" + "Content-Type: text/html\r\n" + "Connection: close\r\n" + "\r\n" + "0\r\n\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 11 + ,.headers= + { { "Date", "Tue, 28 Sep 2010 01:14:13 GMT" } + , { "Server", "Apache" } + , { "Cache-Control", "no-cache, must-revalidate" } + , { "Expires", "Mon, 26 Jul 1997 05:00:00 GMT" } + , { ".et-Cookie", "PlaxoCS=1274804622353690521; path=/; domain=.plaxo.com" } + , { "Vary", "Accept-Encoding" } + , { "_eep-Alive", "timeout=45" } + , { "_onnection", "Keep-Alive" } + , { "Transfer-Encoding", "chunked" } + , { "Content-Type", "text/html" } + , { "Connection", "close" } + } + ,.body= "" + ,.num_chunks_complete= 1 + ,.chunk_lengths= {} + } + +#define NON_ASCII_IN_STATUS_LINE 10 +/* Should handle non-ASCII in status line */ +, {.name= "non-ASCII in status line" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 500 Oriëntatieprobleem\r\n" + "Date: Fri, 5 Nov 2010 23:07:12 GMT+2\r\n" + "Content-Length: 0\r\n" + "Connection: close\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 500 + ,.response_status= "Oriëntatieprobleem" + ,.content_length= 0 + ,.num_headers= 3 + ,.headers= + { { "Date", "Fri, 5 Nov 2010 23:07:12 GMT+2" } + , { "Content-Length", "0" } + , { "Connection", "close" } + } + ,.body= "" + } + +#define HTTP_VERSION_0_9 11 +/* Should handle HTTP/0.9 */ +, {.name= "http version 0.9" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/0.9 200 OK\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 0 + ,.http_minor= 9 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= + {} + ,.body= "" + } + +#define NO_CONTENT_LENGTH_NO_TRANSFER_ENCODING_RESPONSE 12 +/* The client should wait for the server's EOF. That is, when neither + * content-length nor transfer-encoding is specified, the end of body + * is specified by the EOF. + */ +, {.name= "neither content-length nor transfer-encoding response" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + "hello world" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "Content-Type", "text/plain" } + } + ,.body= "hello world" + } + +#define NO_BODY_HTTP10_KA_200 13 +, {.name= "HTTP/1.0 with keep-alive and EOF-terminated 200 status" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.0 200 OK\r\n" + "Connection: keep-alive\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 0 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "Connection", "keep-alive" } + } + ,.body_size= 0 + ,.body= "" + } + +#define NO_BODY_HTTP10_KA_204 14 +, {.name= "HTTP/1.0 with keep-alive and a 204 status" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.0 204 No content\r\n" + "Connection: keep-alive\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.status_code= 204 + ,.response_status= "No content" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "Connection", "keep-alive" } + } + ,.body_size= 0 + ,.body= "" + } + +#define NO_BODY_HTTP11_KA_200 15 +, {.name= "HTTP/1.1 with an EOF-terminated 200 status" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 0 + ,.headers={} + ,.body_size= 0 + ,.body= "" + } + +#define NO_BODY_HTTP11_KA_204 16 +, {.name= "HTTP/1.1 with a 204 status" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 204 No content\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 204 + ,.response_status= "No content" + ,.content_length= -1 + ,.num_headers= 0 + ,.headers={} + ,.body_size= 0 + ,.body= "" + } + +#define NO_BODY_HTTP11_NOKA_204 17 +, {.name= "HTTP/1.1 with a 204 status and keep-alive disabled" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 204 No content\r\n" + "Connection: close\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 204 + ,.response_status= "No content" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "Connection", "close" } + } + ,.body_size= 0 + ,.body= "" + } + +#define NO_BODY_HTTP11_KA_CHUNKED_200 18 +, {.name= "HTTP/1.1 with chunked endocing and a 200 response" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= + { { "Transfer-Encoding", "chunked" } + } + ,.body_size= 0 + ,.body= "" + ,.num_chunks_complete= 1 + } + +#if !HTTP_PARSER_STRICT +#define SPACE_IN_FIELD_RES 19 +/* Should handle spaces in header fields */ +, {.name= "field space" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Server: Microsoft-IIS/6.0\r\n" + "X-Powered-By: ASP.NET\r\n" + "en-US Content-Type: text/xml\r\n" /* this is the problem */ + "Content-Type: text/xml\r\n" + "Content-Length: 16\r\n" + "Date: Fri, 23 Jul 2010 18:45:38 GMT\r\n" + "Connection: keep-alive\r\n" + "\r\n" + "hello" /* fake body */ + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= 16 + ,.num_headers= 7 + ,.headers= + { { "Server", "Microsoft-IIS/6.0" } + , { "X-Powered-By", "ASP.NET" } + , { "en-US Content-Type", "text/xml" } + , { "Content-Type", "text/xml" } + , { "Content-Length", "16" } + , { "Date", "Fri, 23 Jul 2010 18:45:38 GMT" } + , { "Connection", "keep-alive" } + } + ,.body= "hello" + } +#endif /* !HTTP_PARSER_STRICT */ + +#define AMAZON_COM 20 +, {.name= "amazon.com" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 301 MovedPermanently\r\n" + "Date: Wed, 15 May 2013 17:06:33 GMT\r\n" + "Server: Server\r\n" + "x-amz-id-1: 0GPHKXSJQ826RK7GZEB2\r\n" + "p3p: policyref=\"http://www.amazon.com/w3c/p3p.xml\",CP=\"CAO DSP LAW CUR ADM IVAo IVDo CONo OTPo OUR DELi PUBi OTRi BUS PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA HEA PRE LOC GOV OTC \"\r\n" + "x-amz-id-2: STN69VZxIFSz9YJLbz1GDbxpbjG6Qjmmq5E3DxRhOUw+Et0p4hr7c/Q8qNcx4oAD\r\n" + "Location: http://www.amazon.com/Dan-Brown/e/B000AP9DSU/ref=s9_pop_gw_al1?_encoding=UTF8&refinementId=618073011&pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-2&pf_rd_r=0SHYY5BZXN3KR20BNFAY&pf_rd_t=101&pf_rd_p=1263340922&pf_rd_i=507846\r\n" + "Vary: Accept-Encoding,User-Agent\r\n" + "Content-Type: text/html; charset=ISO-8859-1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "1\r\n" + "\n\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 301 + ,.response_status= "MovedPermanently" + ,.content_length= -1 + ,.num_headers= 9 + ,.headers= { { "Date", "Wed, 15 May 2013 17:06:33 GMT" } + , { "Server", "Server" } + , { "x-amz-id-1", "0GPHKXSJQ826RK7GZEB2" } + , { "p3p", "policyref=\"http://www.amazon.com/w3c/p3p.xml\",CP=\"CAO DSP LAW CUR ADM IVAo IVDo CONo OTPo OUR DELi PUBi OTRi BUS PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA HEA PRE LOC GOV OTC \"" } + , { "x-amz-id-2", "STN69VZxIFSz9YJLbz1GDbxpbjG6Qjmmq5E3DxRhOUw+Et0p4hr7c/Q8qNcx4oAD" } + , { "Location", "http://www.amazon.com/Dan-Brown/e/B000AP9DSU/ref=s9_pop_gw_al1?_encoding=UTF8&refinementId=618073011&pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-2&pf_rd_r=0SHYY5BZXN3KR20BNFAY&pf_rd_t=101&pf_rd_p=1263340922&pf_rd_i=507846" } + , { "Vary", "Accept-Encoding,User-Agent" } + , { "Content-Type", "text/html; charset=ISO-8859-1" } + , { "Transfer-Encoding", "chunked" } + } + ,.body= "\n" + ,.num_chunks_complete= 2 + ,.chunk_lengths= { 1 } + } + +#define EMPTY_REASON_PHRASE_AFTER_SPACE 21 +, {.name= "empty reason phrase after space" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 \r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "" + ,.content_length= -1 + ,.num_headers= 0 + ,.headers= {} + ,.body= "" + } + +#define CONTENT_LENGTH_X 22 +, {.name= "Content-Length-X" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Content-Length-X: 0\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\n" + "OK\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 2 + ,.headers= { { "Content-Length-X", "0" } + , { "Transfer-Encoding", "chunked" } + } + ,.body= "OK" + ,.num_chunks_complete= 2 + ,.chunk_lengths= { 2 } + } + +#define HTTP_101_RESPONSE_WITH_UPGRADE_HEADER 23 +, {.name= "HTTP 101 response with Upgrade header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "\r\n" + "proto" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 101 + ,.response_status= "Switching Protocols" + ,.upgrade= "proto" + ,.content_length= -1 + ,.num_headers= 2 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + } + } + +#define HTTP_101_RESPONSE_WITH_UPGRADE_HEADER_AND_CONTENT_LENGTH 24 +, {.name= "HTTP 101 response with Upgrade and Content-Length header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Content-Length: 4\r\n" + "\r\n" + "body" + "proto" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 101 + ,.response_status= "Switching Protocols" + ,.body= "body" + ,.upgrade= "proto" + ,.content_length= 4 + ,.num_headers= 3 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Content-Length", "4" } + } + } + +#define HTTP_101_RESPONSE_WITH_UPGRADE_HEADER_AND_TRANSFER_ENCODING 25 +, {.name= "HTTP 101 response with Upgrade and Transfer-Encoding header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\n" + "bo\r\n" + "2\r\n" + "dy\r\n" + "0\r\n" + "\r\n" + "proto" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 101 + ,.response_status= "Switching Protocols" + ,.body= "body" + ,.upgrade= "proto" + ,.content_length= -1 + ,.num_headers= 3 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Transfer-Encoding", "chunked" } + } + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 2, 2 } + } + +#define HTTP_200_RESPONSE_WITH_UPGRADE_HEADER 26 +, {.name= "HTTP 200 response with Upgrade header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "\r\n" + "body" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.body= "body" + ,.upgrade= NULL + ,.content_length= -1 + ,.num_headers= 2 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + } + } + +#define HTTP_200_RESPONSE_WITH_UPGRADE_HEADER_AND_CONTENT_LENGTH 27 +, {.name= "HTTP 200 response with Upgrade and Content-Length header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Content-Length: 4\r\n" + "\r\n" + "body" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= 4 + ,.num_headers= 3 + ,.body= "body" + ,.upgrade= NULL + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Content-Length", "4" } + } + } + +#define HTTP_200_RESPONSE_WITH_UPGRADE_HEADER_AND_TRANSFER_ENCODING 28 +, {.name= "HTTP 200 response with Upgrade and Transfer-Encoding header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\n" + "bo\r\n" + "2\r\n" + "dy\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 3 + ,.body= "body" + ,.upgrade= NULL + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Transfer-Encoding", "chunked" } + } + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 2, 2 } + } +#define HTTP_200_MULTI_TE_NOT_LAST_CHUNKED 29 +, {.name= "HTTP 200 response with `chunked` being *not last* Transfer-Encoding" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked, identity\r\n" + "\r\n" + "2\r\n" + "OK\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 1 + ,.headers= { { "Transfer-Encoding", "chunked, identity" } + } + ,.body= "2\r\nOK\r\n0\r\n\r\n" + ,.num_chunks_complete= 0 + } +}; + +/* strnlen() is a POSIX.2008 addition. Can't rely on it being available so + * define it ourselves. + */ +size_t +strnlen(const char *s, size_t maxlen) +{ + const char *p; + + p = memchr(s, '\0', maxlen); + if (p == NULL) + return maxlen; + + return p - s; +} + +size_t +strlncat(char *dst, size_t len, const char *src, size_t n) +{ + size_t slen; + size_t dlen; + size_t rlen; + size_t ncpy; + + slen = strnlen(src, n); + dlen = strnlen(dst, len); + + if (dlen < len) { + rlen = len - dlen; + ncpy = slen < rlen ? slen : (rlen - 1); + memcpy(dst + dlen, src, ncpy); + dst[dlen + ncpy] = '\0'; + } + + assert(len > slen + dlen); + return slen + dlen; +} + +size_t +strlncpy(char *dst, size_t len, const char *src, size_t n) +{ + size_t slen; + size_t ncpy; + + slen = strnlen(src, n); + + if (len > 0) { + ncpy = slen < len ? slen : (len - 1); + memcpy(dst, src, ncpy); + dst[ncpy] = '\0'; + } + + assert(len > slen); + return slen; +} + +int +request_url_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == &parser); + strlncat(messages[num_messages].request_url, + sizeof(messages[num_messages].request_url), + buf, + len); + return 0; +} + +int +header_field_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == &parser); + struct message *m = &messages[num_messages]; + + if (m->last_header_element != FIELD) + m->num_headers++; + + strlncat(m->headers[m->num_headers-1][0], + sizeof(m->headers[m->num_headers-1][0]), + buf, + len); + + m->last_header_element = FIELD; + + return 0; +} + +int +header_value_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == &parser); + struct message *m = &messages[num_messages]; + + strlncat(m->headers[m->num_headers-1][1], + sizeof(m->headers[m->num_headers-1][1]), + buf, + len); + + m->last_header_element = VALUE; + + return 0; +} + +void +check_body_is_final (const http_parser *p) +{ + if (messages[num_messages].body_is_final) { + fprintf(stderr, "\n\n *** Error http_body_is_final() should return 1 " + "on last on_body callback call " + "but it doesn't! ***\n\n"); + assert(0); + abort(); + } + messages[num_messages].body_is_final = http_body_is_final(p); +} + +int +body_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == &parser); + strlncat(messages[num_messages].body, + sizeof(messages[num_messages].body), + buf, + len); + messages[num_messages].body_size += len; + check_body_is_final(p); + // printf("body_cb: '%s'\n", requests[num_messages].body); + return 0; +} + +int +count_body_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == &parser); + assert(buf); + messages[num_messages].body_size += len; + check_body_is_final(p); + return 0; +} + +int +message_begin_cb (http_parser *p) +{ + assert(p == &parser); + assert(!messages[num_messages].message_begin_cb_called); + messages[num_messages].message_begin_cb_called = TRUE; + return 0; +} + +int +headers_complete_cb (http_parser *p) +{ + assert(p == &parser); + messages[num_messages].method = parser.method; + messages[num_messages].status_code = parser.status_code; + messages[num_messages].http_major = parser.http_major; + messages[num_messages].http_minor = parser.http_minor; + messages[num_messages].content_length = parser.content_length; + messages[num_messages].headers_complete_cb_called = TRUE; + messages[num_messages].should_keep_alive = http_should_keep_alive(&parser); + return 0; +} + +int +message_complete_cb (http_parser *p) +{ + assert(p == &parser); + if (messages[num_messages].should_keep_alive != + http_should_keep_alive(&parser)) + { + fprintf(stderr, "\n\n *** Error http_should_keep_alive() should have same " + "value in both on_message_complete and on_headers_complete " + "but it doesn't! ***\n\n"); + assert(0); + abort(); + } + + if (messages[num_messages].body_size && + http_body_is_final(p) && + !messages[num_messages].body_is_final) + { + fprintf(stderr, "\n\n *** Error http_body_is_final() should return 1 " + "on last on_body callback call " + "but it doesn't! ***\n\n"); + assert(0); + abort(); + } + + messages[num_messages].message_complete_cb_called = TRUE; + + messages[num_messages].message_complete_on_eof = currently_parsing_eof; + + num_messages++; + return 0; +} + +int +response_status_cb (http_parser *p, const char *buf, size_t len) +{ + assert(p == &parser); + + messages[num_messages].status_cb_called = TRUE; + + strlncat(messages[num_messages].response_status, + sizeof(messages[num_messages].response_status), + buf, + len); + return 0; +} + +int +chunk_header_cb (http_parser *p) +{ + assert(p == &parser); + int chunk_idx = messages[num_messages].num_chunks; + messages[num_messages].num_chunks++; + if (chunk_idx < MAX_CHUNKS) { + messages[num_messages].chunk_lengths[chunk_idx] = p->content_length; + } + + return 0; +} + +int +chunk_complete_cb (http_parser *p) +{ + assert(p == &parser); + + /* Here we want to verify that each chunk_header_cb is matched by a + * chunk_complete_cb, so not only should the total number of calls to + * both callbacks be the same, but they also should be interleaved + * properly */ + assert(messages[num_messages].num_chunks == + messages[num_messages].num_chunks_complete + 1); + + messages[num_messages].num_chunks_complete++; + return 0; +} + +/* These dontcall_* callbacks exist so that we can verify that when we're + * paused, no additional callbacks are invoked */ +int +dontcall_message_begin_cb (http_parser *p) +{ + if (p) { } // gcc + fprintf(stderr, "\n\n*** on_message_begin() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_header_field_cb (http_parser *p, const char *buf, size_t len) +{ + if (p || buf || len) { } // gcc + fprintf(stderr, "\n\n*** on_header_field() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_header_value_cb (http_parser *p, const char *buf, size_t len) +{ + if (p || buf || len) { } // gcc + fprintf(stderr, "\n\n*** on_header_value() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_request_url_cb (http_parser *p, const char *buf, size_t len) +{ + if (p || buf || len) { } // gcc + fprintf(stderr, "\n\n*** on_request_url() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_body_cb (http_parser *p, const char *buf, size_t len) +{ + if (p || buf || len) { } // gcc + fprintf(stderr, "\n\n*** on_body_cb() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_headers_complete_cb (http_parser *p) +{ + if (p) { } // gcc + fprintf(stderr, "\n\n*** on_headers_complete() called on paused " + "parser ***\n\n"); + abort(); +} + +int +dontcall_message_complete_cb (http_parser *p) +{ + if (p) { } // gcc + fprintf(stderr, "\n\n*** on_message_complete() called on paused " + "parser ***\n\n"); + abort(); +} + +int +dontcall_response_status_cb (http_parser *p, const char *buf, size_t len) +{ + if (p || buf || len) { } // gcc + fprintf(stderr, "\n\n*** on_status() called on paused parser ***\n\n"); + abort(); +} + +int +dontcall_chunk_header_cb (http_parser *p) +{ + if (p) { } // gcc + fprintf(stderr, "\n\n*** on_chunk_header() called on paused parser ***\n\n"); + exit(1); +} + +int +dontcall_chunk_complete_cb (http_parser *p) +{ + if (p) { } // gcc + fprintf(stderr, "\n\n*** on_chunk_complete() " + "called on paused parser ***\n\n"); + exit(1); +} + +static http_parser_settings settings_dontcall = + {.on_message_begin = dontcall_message_begin_cb + ,.on_header_field = dontcall_header_field_cb + ,.on_header_value = dontcall_header_value_cb + ,.on_url = dontcall_request_url_cb + ,.on_status = dontcall_response_status_cb + ,.on_body = dontcall_body_cb + ,.on_headers_complete = dontcall_headers_complete_cb + ,.on_message_complete = dontcall_message_complete_cb + ,.on_chunk_header = dontcall_chunk_header_cb + ,.on_chunk_complete = dontcall_chunk_complete_cb + }; + +/* These pause_* callbacks always pause the parser and just invoke the regular + * callback that tracks content. Before returning, we overwrite the parser + * settings to point to the _dontcall variety so that we can verify that + * the pause actually did, you know, pause. */ +int +pause_message_begin_cb (http_parser *p) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return message_begin_cb(p); +} + +int +pause_header_field_cb (http_parser *p, const char *buf, size_t len) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return header_field_cb(p, buf, len); +} + +int +pause_header_value_cb (http_parser *p, const char *buf, size_t len) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return header_value_cb(p, buf, len); +} + +int +pause_request_url_cb (http_parser *p, const char *buf, size_t len) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return request_url_cb(p, buf, len); +} + +int +pause_body_cb (http_parser *p, const char *buf, size_t len) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return body_cb(p, buf, len); +} + +int +pause_headers_complete_cb (http_parser *p) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return headers_complete_cb(p); +} + +int +pause_message_complete_cb (http_parser *p) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return message_complete_cb(p); +} + +int +pause_response_status_cb (http_parser *p, const char *buf, size_t len) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return response_status_cb(p, buf, len); +} + +int +pause_chunk_header_cb (http_parser *p) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return chunk_header_cb(p); +} + +int +pause_chunk_complete_cb (http_parser *p) +{ + http_parser_pause(p, 1); + *current_pause_parser = settings_dontcall; + return chunk_complete_cb(p); +} + +int +connect_headers_complete_cb (http_parser *p) +{ + headers_complete_cb(p); + return 1; +} + +int +connect_message_complete_cb (http_parser *p) +{ + messages[num_messages].should_keep_alive = http_should_keep_alive(&parser); + return message_complete_cb(p); +} + +static http_parser_settings settings_pause = + {.on_message_begin = pause_message_begin_cb + ,.on_header_field = pause_header_field_cb + ,.on_header_value = pause_header_value_cb + ,.on_url = pause_request_url_cb + ,.on_status = pause_response_status_cb + ,.on_body = pause_body_cb + ,.on_headers_complete = pause_headers_complete_cb + ,.on_message_complete = pause_message_complete_cb + ,.on_chunk_header = pause_chunk_header_cb + ,.on_chunk_complete = pause_chunk_complete_cb + }; + +static http_parser_settings settings = + {.on_message_begin = message_begin_cb + ,.on_header_field = header_field_cb + ,.on_header_value = header_value_cb + ,.on_url = request_url_cb + ,.on_status = response_status_cb + ,.on_body = body_cb + ,.on_headers_complete = headers_complete_cb + ,.on_message_complete = message_complete_cb + ,.on_chunk_header = chunk_header_cb + ,.on_chunk_complete = chunk_complete_cb + }; + +static http_parser_settings settings_count_body = + {.on_message_begin = message_begin_cb + ,.on_header_field = header_field_cb + ,.on_header_value = header_value_cb + ,.on_url = request_url_cb + ,.on_status = response_status_cb + ,.on_body = count_body_cb + ,.on_headers_complete = headers_complete_cb + ,.on_message_complete = message_complete_cb + ,.on_chunk_header = chunk_header_cb + ,.on_chunk_complete = chunk_complete_cb + }; + +static http_parser_settings settings_connect = + {.on_message_begin = message_begin_cb + ,.on_header_field = header_field_cb + ,.on_header_value = header_value_cb + ,.on_url = request_url_cb + ,.on_status = response_status_cb + ,.on_body = dontcall_body_cb + ,.on_headers_complete = connect_headers_complete_cb + ,.on_message_complete = connect_message_complete_cb + ,.on_chunk_header = chunk_header_cb + ,.on_chunk_complete = chunk_complete_cb + }; + +static http_parser_settings settings_null = + {.on_message_begin = 0 + ,.on_header_field = 0 + ,.on_header_value = 0 + ,.on_url = 0 + ,.on_status = 0 + ,.on_body = 0 + ,.on_headers_complete = 0 + ,.on_message_complete = 0 + ,.on_chunk_header = 0 + ,.on_chunk_complete = 0 + }; + +void +parser_init (enum http_parser_type type) +{ + num_messages = 0; + http_parser_init(&parser, type); + memset(&messages, 0, sizeof messages); +} + +size_t parse (const char *buf, size_t len) +{ + size_t nparsed; + currently_parsing_eof = (len == 0); + nparsed = http_parser_execute(&parser, &settings, buf, len); + return nparsed; +} + +size_t parse_count_body (const char *buf, size_t len) +{ + size_t nparsed; + currently_parsing_eof = (len == 0); + nparsed = http_parser_execute(&parser, &settings_count_body, buf, len); + return nparsed; +} + +size_t parse_pause (const char *buf, size_t len) +{ + size_t nparsed; + http_parser_settings s = settings_pause; + + currently_parsing_eof = (len == 0); + current_pause_parser = &s; + nparsed = http_parser_execute(&parser, current_pause_parser, buf, len); + return nparsed; +} + +size_t parse_connect (const char *buf, size_t len) +{ + size_t nparsed; + currently_parsing_eof = (len == 0); + nparsed = http_parser_execute(&parser, &settings_connect, buf, len); + return nparsed; +} + +static inline int +check_str_eq (const struct message *m, + const char *prop, + const char *expected, + const char *found) { + if ((expected == NULL) != (found == NULL)) { + printf("\n*** Error: %s in '%s' ***\n\n", prop, m->name); + printf("expected %s\n", (expected == NULL) ? "NULL" : expected); + printf(" found %s\n", (found == NULL) ? "NULL" : found); + return 0; + } + if (expected != NULL && 0 != strcmp(expected, found)) { + printf("\n*** Error: %s in '%s' ***\n\n", prop, m->name); + printf("expected '%s'\n", expected); + printf(" found '%s'\n", found); + return 0; + } + return 1; +} + +static inline int +check_num_eq (const struct message *m, + const char *prop, + int expected, + int found) { + if (expected != found) { + printf("\n*** Error: %s in '%s' ***\n\n", prop, m->name); + printf("expected %d\n", expected); + printf(" found %d\n", found); + return 0; + } + return 1; +} + +#define MESSAGE_CHECK_STR_EQ(expected, found, prop) \ + if (!check_str_eq(expected, #prop, expected->prop, found->prop)) return 0 + +#define MESSAGE_CHECK_NUM_EQ(expected, found, prop) \ + if (!check_num_eq(expected, #prop, expected->prop, found->prop)) return 0 + +#define MESSAGE_CHECK_URL_EQ(u, expected, found, prop, fn) \ +do { \ + char ubuf[256]; \ + \ + if ((u)->field_set & (1 << (fn))) { \ + memcpy(ubuf, (found)->request_url + (u)->field_data[(fn)].off, \ + (u)->field_data[(fn)].len); \ + ubuf[(u)->field_data[(fn)].len] = '\0'; \ + } else { \ + ubuf[0] = '\0'; \ + } \ + \ + check_str_eq(expected, #prop, expected->prop, ubuf); \ +} while(0) + +int +message_eq (int index, int connect, const struct message *expected) +{ + int i; + struct message *m = &messages[index]; + + MESSAGE_CHECK_NUM_EQ(expected, m, http_major); + MESSAGE_CHECK_NUM_EQ(expected, m, http_minor); + MESSAGE_CHECK_NUM_EQ(expected, m, content_length); + + if (expected->type == HTTP_REQUEST) { + MESSAGE_CHECK_NUM_EQ(expected, m, method); + } else { + MESSAGE_CHECK_NUM_EQ(expected, m, status_code); + MESSAGE_CHECK_STR_EQ(expected, m, response_status); + assert(m->status_cb_called); + } + + if (!connect) { + MESSAGE_CHECK_NUM_EQ(expected, m, should_keep_alive); + MESSAGE_CHECK_NUM_EQ(expected, m, message_complete_on_eof); + } + + assert(m->message_begin_cb_called); + assert(m->headers_complete_cb_called); + assert(m->message_complete_cb_called); + + + MESSAGE_CHECK_STR_EQ(expected, m, request_url); + + /* Check URL components; we can't do this w/ CONNECT since it doesn't + * send us a well-formed URL. + */ + if (*m->request_url && m->method != HTTP_CONNECT) { + struct http_parser_url u; + + if (http_parser_parse_url(m->request_url, strlen(m->request_url), 0, &u)) { + fprintf(stderr, "\n\n*** failed to parse URL %s ***\n\n", + m->request_url); + abort(); + } + + if (expected->host) { + MESSAGE_CHECK_URL_EQ(&u, expected, m, host, UF_HOST); + } + + if (expected->userinfo) { + MESSAGE_CHECK_URL_EQ(&u, expected, m, userinfo, UF_USERINFO); + } + + m->port = (u.field_set & (1 << UF_PORT)) ? + u.port : 0; + + MESSAGE_CHECK_URL_EQ(&u, expected, m, query_string, UF_QUERY); + MESSAGE_CHECK_URL_EQ(&u, expected, m, fragment, UF_FRAGMENT); + MESSAGE_CHECK_URL_EQ(&u, expected, m, request_path, UF_PATH); + MESSAGE_CHECK_NUM_EQ(expected, m, port); + } + + if (connect) { + check_num_eq(m, "body_size", 0, m->body_size); + } else if (expected->body_size) { + MESSAGE_CHECK_NUM_EQ(expected, m, body_size); + } else { + MESSAGE_CHECK_STR_EQ(expected, m, body); + } + + if (connect) { + check_num_eq(m, "num_chunks_complete", 0, m->num_chunks_complete); + } else { + assert(m->num_chunks == m->num_chunks_complete); + MESSAGE_CHECK_NUM_EQ(expected, m, num_chunks_complete); + for (i = 0; i < m->num_chunks && i < MAX_CHUNKS; i++) { + MESSAGE_CHECK_NUM_EQ(expected, m, chunk_lengths[i]); + } + } + + MESSAGE_CHECK_NUM_EQ(expected, m, num_headers); + + int r; + for (i = 0; i < m->num_headers; i++) { + r = check_str_eq(expected, "header field", expected->headers[i][0], m->headers[i][0]); + if (!r) return 0; + r = check_str_eq(expected, "header value", expected->headers[i][1], m->headers[i][1]); + if (!r) return 0; + } + + if (!connect) { + MESSAGE_CHECK_STR_EQ(expected, m, upgrade); + } + + return 1; +} + +/* Given a sequence of varargs messages, return the number of them that the + * parser should successfully parse, taking into account that upgraded + * messages prevent all subsequent messages from being parsed. + */ +size_t +count_parsed_messages(const size_t nmsgs, ...) { + size_t i; + va_list ap; + + va_start(ap, nmsgs); + + for (i = 0; i < nmsgs; i++) { + struct message *m = va_arg(ap, struct message *); + + if (m->upgrade) { + va_end(ap); + return i + 1; + } + } + + va_end(ap); + return nmsgs; +} + +/* Given a sequence of bytes and the number of these that we were able to + * parse, verify that upgrade bodies are correct. + */ +void +upgrade_message_fix(char *body, const size_t nread, const size_t nmsgs, ...) { + va_list ap; + size_t i; + size_t off = 0; + + va_start(ap, nmsgs); + + for (i = 0; i < nmsgs; i++) { + struct message *m = va_arg(ap, struct message *); + + off += strlen(m->raw); + + if (m->upgrade) { + off -= strlen(m->upgrade); + + /* Check the portion of the response after its specified upgrade */ + if (!check_str_eq(m, "upgrade", body + off, body + nread)) { + abort(); + } + + /* Fix up the response so that message_eq() will verify the beginning + * of the upgrade */ + *(body + nread + strlen(m->upgrade)) = '\0'; + messages[num_messages -1 ].upgrade = body + nread; + + va_end(ap); + return; + } + } + + va_end(ap); + printf("\n\n*** Error: expected a message with upgrade ***\n"); + + abort(); +} + +static void +print_error (const char *raw, size_t error_location) +{ + fprintf(stderr, "\n*** %s ***\n\n", + http_errno_description(HTTP_PARSER_ERRNO(&parser))); + + int this_line = 0, char_len = 0; + size_t i, j, len = strlen(raw), error_location_line = 0; + for (i = 0; i < len; i++) { + if (i == error_location) this_line = 1; + switch (raw[i]) { + case '\r': + char_len = 2; + fprintf(stderr, "\\r"); + break; + + case '\n': + fprintf(stderr, "\\n\n"); + + if (this_line) goto print; + + error_location_line = 0; + continue; + + default: + char_len = 1; + fputc(raw[i], stderr); + break; + } + if (!this_line) error_location_line += char_len; + } + + fprintf(stderr, "[eof]\n"); + + print: + for (j = 0; j < error_location_line; j++) { + fputc(' ', stderr); + } + fprintf(stderr, "^\n\nerror location: %u\n", (unsigned int)error_location); +} + +void +test_preserve_data (void) +{ + char my_data[] = "application-specific data"; + http_parser parser; + parser.data = my_data; + http_parser_init(&parser, HTTP_REQUEST); + if (parser.data != my_data) { + printf("\n*** parser.data not preserved accross http_parser_init ***\n\n"); + abort(); + } +} + +struct url_test { + const char *name; + const char *url; + int is_connect; + struct http_parser_url u; + int rv; +}; + +const struct url_test url_tests[] = +{ {.name="proxy request" + ,.url="http://hostname/" + ,.is_connect=0 + ,.u= + {.field_set=(1 << UF_SCHEMA) | (1 << UF_HOST) | (1 << UF_PATH) + ,.port=0 + ,.field_data= + {{ 0, 4 } /* UF_SCHEMA */ + ,{ 7, 8 } /* UF_HOST */ + ,{ 0, 0 } /* UF_PORT */ + ,{ 15, 1 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="proxy request with port" + ,.url="http://hostname:444/" + ,.is_connect=0 + ,.u= + {.field_set=(1 << UF_SCHEMA) | (1 << UF_HOST) | (1 << UF_PORT) | (1 << UF_PATH) + ,.port=444 + ,.field_data= + {{ 0, 4 } /* UF_SCHEMA */ + ,{ 7, 8 } /* UF_HOST */ + ,{ 16, 3 } /* UF_PORT */ + ,{ 19, 1 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="CONNECT request" + ,.url="hostname:443" + ,.is_connect=1 + ,.u= + {.field_set=(1 << UF_HOST) | (1 << UF_PORT) + ,.port=443 + ,.field_data= + {{ 0, 0 } /* UF_SCHEMA */ + ,{ 0, 8 } /* UF_HOST */ + ,{ 9, 3 } /* UF_PORT */ + ,{ 0, 0 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="CONNECT request but not connect" + ,.url="hostname:443" + ,.is_connect=0 + ,.rv=1 + } + +, {.name="proxy ipv6 request" + ,.url="http://[1:2::3:4]/" + ,.is_connect=0 + ,.u= + {.field_set=(1 << UF_SCHEMA) | (1 << UF_HOST) | (1 << UF_PATH) + ,.port=0 + ,.field_data= + {{ 0, 4 } /* UF_SCHEMA */ + ,{ 8, 8 } /* UF_HOST */ + ,{ 0, 0 } /* UF_PORT */ + ,{ 17, 1 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="proxy ipv6 request with port" + ,.url="http://[1:2::3:4]:67/" + ,.is_connect=0 + ,.u= + {.field_set=(1 << UF_SCHEMA) | (1 << UF_HOST) | (1 << UF_PORT) | (1 << UF_PATH) + ,.port=67 + ,.field_data= + {{ 0, 4 } /* UF_SCHEMA */ + ,{ 8, 8 } /* UF_HOST */ + ,{ 18, 2 } /* UF_PORT */ + ,{ 20, 1 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="CONNECT ipv6 address" + ,.url="[1:2::3:4]:443" + ,.is_connect=1 + ,.u= + {.field_set=(1 << UF_HOST) | (1 << UF_PORT) + ,.port=443 + ,.field_data= + {{ 0, 0 } /* UF_SCHEMA */ + ,{ 1, 8 } /* UF_HOST */ + ,{ 11, 3 } /* UF_PORT */ + ,{ 0, 0 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="ipv4 in ipv6 address" + ,.url="http://[2001:0000:0000:0000:0000:0000:1.9.1.1]/" + ,.is_connect=0 + ,.u= + {.field_set=(1 << UF_SCHEMA) | (1 << UF_HOST) | (1 << UF_PATH) + ,.port=0 + ,.field_data= + {{ 0, 4 } /* UF_SCHEMA */ + ,{ 8, 37 } /* UF_HOST */ + ,{ 0, 0 } /* UF_PORT */ + ,{ 46, 1 } /* UF_PATH */ + ,{ 0, 0 } /* UF_QUERY */ + ,{ 0, 0 } /* UF_FRAGMENT */ + ,{ 0, 0 } /* UF_USERINFO */ + } + } + ,.rv=0 + } + +, {.name="extra ? in query string" + ,.url="http://a.tbcdn.cn/p/fp/2010c/??fp-header-min.css,fp-base-min.css," + "fp-channel-min.css,fp-product-min.css,fp-mall-min.css,fp-category-min.css," + "fp-sub-min.css,fp-gdp4p-min.css,fp-css3-min.css,fp-misc-min.css?t=20101022.css" + ,.is_connect=0 + ,.u= + {.field_set=(1<field_set, u->port); + for (i = 0; i < UF_MAX; i++) { + if ((u->field_set & (1 << i)) == 0) { + printf("\tfield_data[%u]: unset\n", i); + continue; + } + + printf("\tfield_data[%u]: off: %u len: %u part: \"%.*s\n\"", + i, + u->field_data[i].off, + u->field_data[i].len, + u->field_data[i].len, + url + u->field_data[i].off); + } +} + +void +test_parse_url (void) +{ + struct http_parser_url u; + const struct url_test *test; + unsigned int i; + int rv; + + for (i = 0; i < (sizeof(url_tests) / sizeof(url_tests[0])); i++) { + test = &url_tests[i]; + memset(&u, 0, sizeof(u)); + + rv = http_parser_parse_url(test->url, + test->url ? strlen(test->url) : 0, + test->is_connect, + &u); + + if (test->rv == 0) { + if (rv != 0) { + printf("\n*** http_parser_parse_url(\"%s\") \"%s\" test failed, " + "unexpected rv %d ***\n\n", test->url, test->name, rv); + abort(); + } + + if (memcmp(&u, &test->u, sizeof(u)) != 0) { + printf("\n*** http_parser_parse_url(\"%s\") \"%s\" failed ***\n", + test->url, test->name); + + printf("target http_parser_url:\n"); + dump_url(test->url, &test->u); + printf("result http_parser_url:\n"); + dump_url(test->url, &u); + + abort(); + } + } else { + /* test->rv != 0 */ + if (rv == 0) { + printf("\n*** http_parser_parse_url(\"%s\") \"%s\" test failed, " + "unexpected rv %d ***\n\n", test->url, test->name, rv); + abort(); + } + } + } +} + +void +test_method_str (void) +{ + assert(0 == strcmp("GET", http_method_str(HTTP_GET))); + assert(0 == strcmp("", http_method_str(1337))); +} + +void +test_status_str (void) +{ + assert(0 == strcmp("OK", http_status_str(HTTP_STATUS_OK))); + assert(0 == strcmp("Not Found", http_status_str(HTTP_STATUS_NOT_FOUND))); + assert(0 == strcmp("", http_status_str(1337))); +} + +void +test_message (const struct message *message) +{ + size_t raw_len = strlen(message->raw); + size_t msg1len; + for (msg1len = 0; msg1len < raw_len; msg1len++) { + parser_init(message->type); + if (message->allow_chunked_length) { + parser.allow_chunked_length = 1; + } + + size_t read; + const char *msg1 = message->raw; + const char *msg2 = msg1 + msg1len; + size_t msg2len = raw_len - msg1len; + + if (msg1len) { + assert(num_messages == 0); + messages[0].headers_complete_cb_called = FALSE; + + read = parse(msg1, msg1len); + + if (!messages[0].headers_complete_cb_called && parser.nread != read) { + assert(parser.nread == read); + print_error(msg1, read); + abort(); + } + + if (message->upgrade && parser.upgrade && num_messages > 0) { + messages[num_messages - 1].upgrade = msg1 + read; + goto test; + } + + if (read != msg1len) { + print_error(msg1, read); + abort(); + } + } + + + read = parse(msg2, msg2len); + + if (message->upgrade && parser.upgrade) { + messages[num_messages - 1].upgrade = msg2 + read; + goto test; + } + + if (read != msg2len) { + print_error(msg2, read); + abort(); + } + + read = parse(NULL, 0); + + if (read != 0) { + print_error(message->raw, read); + abort(); + } + + test: + + if (num_messages != 1) { + printf("\n*** num_messages != 1 after testing '%s' ***\n\n", message->name); + abort(); + } + + if(!message_eq(0, 0, message)) abort(); + } +} + +void +test_message_count_body (const struct message *message) +{ + parser_init(message->type); + + size_t read; + size_t l = strlen(message->raw); + size_t i, toread; + size_t chunk = 4024; + + for (i = 0; i < l; i+= chunk) { + toread = MIN(l-i, chunk); + read = parse_count_body(message->raw + i, toread); + if (read != toread) { + print_error(message->raw, read); + abort(); + } + } + + + read = parse_count_body(NULL, 0); + if (read != 0) { + print_error(message->raw, read); + abort(); + } + + if (num_messages != 1) { + printf("\n*** num_messages != 1 after testing '%s' ***\n\n", message->name); + abort(); + } + + if(!message_eq(0, 0, message)) abort(); +} + +void +test_simple_type (const char *buf, + enum http_errno err_expected, + enum http_parser_type type) +{ + parser_init(type); + + enum http_errno err; + + parse(buf, strlen(buf)); + err = HTTP_PARSER_ERRNO(&parser); + parse(NULL, 0); + + /* In strict mode, allow us to pass with an unexpected HPE_STRICT as + * long as the caller isn't expecting success. + */ +#if HTTP_PARSER_STRICT + if (err_expected != err && err_expected != HPE_OK && err != HPE_STRICT) { +#else + if (err_expected != err) { +#endif + fprintf(stderr, "\n*** test_simple expected %s, but saw %s ***\n\n%s\n", + http_errno_name(err_expected), http_errno_name(err), buf); + abort(); + } +} + +void +test_simple (const char *buf, enum http_errno err_expected) +{ + test_simple_type(buf, err_expected, HTTP_REQUEST); +} + +void +test_invalid_header_content (int req, const char* str) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = str; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_INVALID_HEADER_TOKEN); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in invalid header content test ***\n"); + abort(); +} + +void +test_invalid_header_field_content_error (int req) +{ + test_invalid_header_content(req, "Foo: F\01ailure"); + test_invalid_header_content(req, "Foo: B\02ar"); +} + +void +test_invalid_header_field (int req, const char* str) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = str; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_INVALID_HEADER_TOKEN); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in invalid header token test ***\n"); + abort(); +} + +void +test_invalid_header_field_token_error (int req) +{ + test_invalid_header_field(req, "Fo@: Failure"); + test_invalid_header_field(req, "Foo\01\test: Bar"); +} + +void +test_double_content_length_error (int req) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = "Content-Length: 0\r\nContent-Length: 1\r\n\r\n"; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_UNEXPECTED_CONTENT_LENGTH); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in double content-length test ***\n"); + abort(); +} + +void +test_chunked_content_length_error (int req) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = "Transfer-Encoding: anything\r\nContent-Length: 1\r\n\r\n"; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_UNEXPECTED_CONTENT_LENGTH); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in chunked content-length test ***\n"); + abort(); +} + +void +test_header_cr_no_lf_error (int req) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = "Foo: 1\rBar: 1\r\n\r\n"; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_LF_EXPECTED); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in header whitespace test ***\n"); + abort(); +} + +void +test_no_overflow_parse_url (void) +{ + int rv; + struct http_parser_url u; + + http_parser_url_init(&u); + rv = http_parser_parse_url("http://example.com:8001", 22, 0, &u); + + if (rv != 0) { + fprintf(stderr, + "\n*** test_no_overflow_parse_url invalid return value=%d\n", + rv); + abort(); + } + + if (u.port != 800) { + fprintf(stderr, + "\n*** test_no_overflow_parse_url invalid port number=%d\n", + u.port); + abort(); + } +} + +void +test_header_overflow_error (int req) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? "GET / HTTP/1.1\r\n" : "HTTP/1.0 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = "header-key: header-value\r\n"; + size_t buflen = strlen(buf); + + int i; + for (i = 0; i < 10000; i++) { + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + //fprintf(stderr, "error found on iter %d\n", i); + assert(HTTP_PARSER_ERRNO(&parser) == HPE_HEADER_OVERFLOW); + return; + } + } + + fprintf(stderr, "\n*** Error expected but none in header overflow test ***\n"); + abort(); +} + + +void +test_header_nread_value () +{ + http_parser parser; + http_parser_init(&parser, HTTP_REQUEST); + size_t parsed; + const char *buf; + buf = "GET / HTTP/1.1\r\nheader: value\nhdr: value\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + assert(parser.nread == strlen(buf)); +} + + +static void +test_content_length_overflow (const char *buf, size_t buflen, int expect_ok) +{ + http_parser parser; + http_parser_init(&parser, HTTP_RESPONSE); + http_parser_execute(&parser, &settings_null, buf, buflen); + + if (expect_ok) + assert(HTTP_PARSER_ERRNO(&parser) == HPE_OK); + else + assert(HTTP_PARSER_ERRNO(&parser) == HPE_INVALID_CONTENT_LENGTH); +} + +void +test_header_content_length_overflow_error (void) +{ +#define X(size) \ + "HTTP/1.1 200 OK\r\n" \ + "Content-Length: " #size "\r\n" \ + "\r\n" + const char a[] = X(1844674407370955160); /* 2^64 / 10 - 1 */ + const char b[] = X(18446744073709551615); /* 2^64-1 */ + const char c[] = X(18446744073709551616); /* 2^64 */ +#undef X + test_content_length_overflow(a, sizeof(a) - 1, 1); /* expect ok */ + test_content_length_overflow(b, sizeof(b) - 1, 0); /* expect failure */ + test_content_length_overflow(c, sizeof(c) - 1, 0); /* expect failure */ +} + +void +test_chunk_content_length_overflow_error (void) +{ +#define X(size) \ + "HTTP/1.1 200 OK\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "\r\n" \ + #size "\r\n" \ + "..." + const char a[] = X(FFFFFFFFFFFFFFE); /* 2^64 / 16 - 1 */ + const char b[] = X(FFFFFFFFFFFFFFFF); /* 2^64-1 */ + const char c[] = X(10000000000000000); /* 2^64 */ +#undef X + test_content_length_overflow(a, sizeof(a) - 1, 1); /* expect ok */ + test_content_length_overflow(b, sizeof(b) - 1, 0); /* expect failure */ + test_content_length_overflow(c, sizeof(c) - 1, 0); /* expect failure */ +} + +void +test_no_overflow_long_body (int req, size_t length) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + size_t i; + char buf1[3000]; + size_t buf1len = sprintf(buf1, "%s\r\nConnection: Keep-Alive\r\nContent-Length: %lu\r\n\r\n", + req ? "POST / HTTP/1.0" : "HTTP/1.0 200 OK", (unsigned long)length); + parsed = http_parser_execute(&parser, &settings_null, buf1, buf1len); + if (parsed != buf1len) + goto err; + + for (i = 0; i < length; i++) { + char foo = 'a'; + parsed = http_parser_execute(&parser, &settings_null, &foo, 1); + if (parsed != 1) + goto err; + } + + parsed = http_parser_execute(&parser, &settings_null, buf1, buf1len); + if (parsed != buf1len) goto err; + return; + + err: + fprintf(stderr, + "\n*** error in test_no_overflow_long_body %s of length %lu ***\n", + req ? "REQUEST" : "RESPONSE", + (unsigned long)length); + abort(); +} + +void +test_multiple3 (const struct message *r1, const struct message *r2, const struct message *r3) +{ + int message_count = count_parsed_messages(3, r1, r2, r3); + + char total[ strlen(r1->raw) + + strlen(r2->raw) + + strlen(r3->raw) + + 1 + ]; + total[0] = '\0'; + + strcat(total, r1->raw); + strcat(total, r2->raw); + strcat(total, r3->raw); + + parser_init(r1->type); + if (r1->allow_chunked_length || + r2->allow_chunked_length || + r3->allow_chunked_length) { + parser.allow_chunked_length = 1; + } + + size_t read; + + read = parse(total, strlen(total)); + + if (parser.upgrade) { + upgrade_message_fix(total, read, 3, r1, r2, r3); + goto test; + } + + if (read != strlen(total)) { + print_error(total, read); + abort(); + } + + read = parse(NULL, 0); + + if (read != 0) { + print_error(total, read); + abort(); + } + +test: + + if (message_count != num_messages) { + fprintf(stderr, "\n\n*** Parser didn't see 3 messages only %d *** \n", num_messages); + abort(); + } + + if (!message_eq(0, 0, r1)) abort(); + if (message_count > 1 && !message_eq(1, 0, r2)) abort(); + if (message_count > 2 && !message_eq(2, 0, r3)) abort(); +} + +/* SCAN through every possible breaking to make sure the + * parser can handle getting the content in any chunks that + * might come from the socket + */ +void +test_scan (const struct message *r1, const struct message *r2, const struct message *r3) +{ + char total[80*1024] = "\0"; + char buf1[80*1024] = "\0"; + char buf2[80*1024] = "\0"; + char buf3[80*1024] = "\0"; + + strcat(total, r1->raw); + strcat(total, r2->raw); + strcat(total, r3->raw); + + size_t read; + + int total_len = strlen(total); + + int total_ops = 2 * (total_len - 1) * (total_len - 2) / 2; + int ops = 0 ; + + size_t buf1_len, buf2_len, buf3_len; + int message_count = count_parsed_messages(3, r1, r2, r3); + + int i,j,type_both; + for (type_both = 0; type_both < 2; type_both ++ ) { + for (j = 2; j < total_len; j ++ ) { + for (i = 1; i < j; i ++ ) { + + if (ops % 1000 == 0) { + printf("\b\b\b\b%3.0f%%", 100 * (float)ops /(float)total_ops); + fflush(stdout); + } + ops += 1; + + parser_init(type_both ? HTTP_BOTH : r1->type); + + buf1_len = i; + strlncpy(buf1, sizeof(buf1), total, buf1_len); + buf1[buf1_len] = 0; + + buf2_len = j - i; + strlncpy(buf2, sizeof(buf1), total+i, buf2_len); + buf2[buf2_len] = 0; + + buf3_len = total_len - j; + strlncpy(buf3, sizeof(buf1), total+j, buf3_len); + buf3[buf3_len] = 0; + + assert(num_messages == 0); + messages[0].headers_complete_cb_called = FALSE; + + read = parse(buf1, buf1_len); + + if (!messages[0].headers_complete_cb_called && parser.nread != read) { + print_error(buf1, read); + goto error; + } + + if (parser.upgrade) goto test; + + if (read != buf1_len) { + print_error(buf1, read); + goto error; + } + + read += parse(buf2, buf2_len); + + if (parser.upgrade) goto test; + + if (read != buf1_len + buf2_len) { + print_error(buf2, read); + goto error; + } + + read += parse(buf3, buf3_len); + + if (parser.upgrade) goto test; + + if (read != buf1_len + buf2_len + buf3_len) { + print_error(buf3, read); + goto error; + } + + parse(NULL, 0); + +test: + if (parser.upgrade) { + upgrade_message_fix(total, read, 3, r1, r2, r3); + } + + if (message_count != num_messages) { + fprintf(stderr, "\n\nParser didn't see %d messages only %d\n", + message_count, num_messages); + goto error; + } + + if (!message_eq(0, 0, r1)) { + fprintf(stderr, "\n\nError matching messages[0] in test_scan.\n"); + goto error; + } + + if (message_count > 1 && !message_eq(1, 0, r2)) { + fprintf(stderr, "\n\nError matching messages[1] in test_scan.\n"); + goto error; + } + + if (message_count > 2 && !message_eq(2, 0, r3)) { + fprintf(stderr, "\n\nError matching messages[2] in test_scan.\n"); + goto error; + } + } + } + } + puts("\b\b\b\b100%"); + return; + + error: + fprintf(stderr, "i=%d j=%d\n", i, j); + fprintf(stderr, "buf1 (%u) %s\n\n", (unsigned int)buf1_len, buf1); + fprintf(stderr, "buf2 (%u) %s\n\n", (unsigned int)buf2_len , buf2); + fprintf(stderr, "buf3 (%u) %s\n", (unsigned int)buf3_len, buf3); + abort(); +} + +// user required to free the result +// string terminated by \0 +char * +create_large_chunked_message (int body_size_in_kb, const char* headers) +{ + int i; + size_t wrote = 0; + size_t headers_len = strlen(headers); + size_t bufsize = headers_len + (5+1024+2)*body_size_in_kb + 6; + char * buf = malloc(bufsize); + + memcpy(buf, headers, headers_len); + wrote += headers_len; + + for (i = 0; i < body_size_in_kb; i++) { + // write 1kb chunk into the body. + memcpy(buf + wrote, "400\r\n", 5); + wrote += 5; + memset(buf + wrote, 'C', 1024); + wrote += 1024; + strcpy(buf + wrote, "\r\n"); + wrote += 2; + } + + memcpy(buf + wrote, "0\r\n\r\n", 6); + wrote += 6; + assert(wrote == bufsize); + + return buf; +} + +/* Verify that we can pause parsing at any of the bytes in the + * message and still get the result that we're expecting. */ +void +test_message_pause (const struct message *msg) +{ + char *buf = (char*) msg->raw; + size_t buflen = strlen(msg->raw); + size_t nread; + + parser_init(msg->type); + if (msg->allow_chunked_length) { + parser.allow_chunked_length = 1; + } + + do { + nread = parse_pause(buf, buflen); + + // We can only set the upgrade buffer once we've gotten our message + // completion callback. + if (messages[0].message_complete_cb_called && + msg->upgrade && + parser.upgrade) { + messages[0].upgrade = buf + nread; + goto test; + } + + if (nread < buflen) { + + // Not much do to if we failed a strict-mode check + if (HTTP_PARSER_ERRNO(&parser) == HPE_STRICT) { + return; + } + + assert (HTTP_PARSER_ERRNO(&parser) == HPE_PAUSED); + } + + buf += nread; + buflen -= nread; + http_parser_pause(&parser, 0); + } while (buflen > 0); + + nread = parse_pause(NULL, 0); + assert (nread == 0); + +test: + if (num_messages != 1) { + printf("\n*** num_messages != 1 after testing '%s' ***\n\n", msg->name); + abort(); + } + + if(!message_eq(0, 0, msg)) abort(); +} + +/* Verify that body and next message won't be parsed in responses to CONNECT */ +void +test_message_connect (const struct message *msg) +{ + char *buf = (char*) msg->raw; + size_t buflen = strlen(msg->raw); + + parser_init(msg->type); + + parse_connect(buf, buflen); + + if (num_messages != 1) { + printf("\n*** num_messages != 1 after testing '%s' ***\n\n", msg->name); + abort(); + } + + if(!message_eq(0, 1, msg)) abort(); +} + +int +main (void) +{ + unsigned i, j, k; + unsigned long version; + unsigned major; + unsigned minor; + unsigned patch; + + version = http_parser_version(); + major = (version >> 16) & 255; + minor = (version >> 8) & 255; + patch = version & 255; + printf("http_parser v%u.%u.%u (0x%06lx)\n", major, minor, patch, version); + + printf("sizeof(http_parser) = %u\n", (unsigned int)sizeof(http_parser)); + +#if defined(__i386__) || defined(__x86_64__) + /* Should be 32 on both 32 bits and 64 bits x86 because of struct padding, + * see https://github.com/nodejs/http-parser/issues/507. + */ + assert(sizeof(http_parser) == 32); +#endif + + //// API + test_preserve_data(); + test_parse_url(); + test_method_str(); + test_status_str(); + + //// NREAD + test_header_nread_value(); + + //// OVERFLOW CONDITIONS + test_no_overflow_parse_url(); + + test_header_overflow_error(HTTP_REQUEST); + test_no_overflow_long_body(HTTP_REQUEST, 1000); + test_no_overflow_long_body(HTTP_REQUEST, 100000); + + test_header_overflow_error(HTTP_RESPONSE); + test_no_overflow_long_body(HTTP_RESPONSE, 1000); + test_no_overflow_long_body(HTTP_RESPONSE, 100000); + + test_header_content_length_overflow_error(); + test_chunk_content_length_overflow_error(); + + //// HEADER FIELD CONDITIONS + test_double_content_length_error(HTTP_REQUEST); + test_chunked_content_length_error(HTTP_REQUEST); + test_header_cr_no_lf_error(HTTP_REQUEST); + test_invalid_header_field_token_error(HTTP_REQUEST); + test_invalid_header_field_content_error(HTTP_REQUEST); + test_double_content_length_error(HTTP_RESPONSE); + test_chunked_content_length_error(HTTP_RESPONSE); + test_header_cr_no_lf_error(HTTP_RESPONSE); + test_invalid_header_field_token_error(HTTP_RESPONSE); + test_invalid_header_field_content_error(HTTP_RESPONSE); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length:\r\n" // empty + "\r\n", + HPE_INVALID_CONTENT_LENGTH, + HTTP_REQUEST); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length: 42 \r\n" // Note the surrounding whitespace. + "\r\n", + HPE_OK, + HTTP_REQUEST); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length: 4 2\r\n" + "\r\n", + HPE_INVALID_CONTENT_LENGTH, + HTTP_REQUEST); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length: 13 37\r\n" + "\r\n", + HPE_INVALID_CONTENT_LENGTH, + HTTP_REQUEST); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length: 42\r\n" + " Hello world!\r\n", + HPE_INVALID_CONTENT_LENGTH, + HTTP_REQUEST); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length: 42\r\n" + " \r\n", + HPE_OK, + HTTP_REQUEST); + + //// RESPONSES + + test_simple_type("HTP/1.1 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/01.1 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/11.1 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/1.01 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/1.1\t200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("\rHTTP/1.1\t200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + + for (i = 0; i < ARRAY_SIZE(responses); i++) { + test_message(&responses[i]); + } + + for (i = 0; i < ARRAY_SIZE(responses); i++) { + test_message_pause(&responses[i]); + } + + for (i = 0; i < ARRAY_SIZE(responses); i++) { + test_message_connect(&responses[i]); + } + + for (i = 0; i < ARRAY_SIZE(responses); i++) { + if (!responses[i].should_keep_alive) continue; + for (j = 0; j < ARRAY_SIZE(responses); j++) { + if (!responses[j].should_keep_alive) continue; + for (k = 0; k < ARRAY_SIZE(responses); k++) { + test_multiple3(&responses[i], &responses[j], &responses[k]); + } + } + } + + test_message_count_body(&responses[NO_HEADERS_NO_BODY_404]); + test_message_count_body(&responses[TRAILING_SPACE_ON_CHUNKED_BODY]); + + // test very large chunked response + { + char * msg = create_large_chunked_message(31337, + "HTTP/1.0 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Type: text/plain\r\n" + "\r\n"); + struct message large_chunked = + {.name= "large chunked" + ,.type= HTTP_RESPONSE + ,.raw= msg + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.status_code= 200 + ,.response_status= "OK" + ,.content_length= -1 + ,.num_headers= 2 + ,.headers= + { { "Transfer-Encoding", "chunked" } + , { "Content-Type", "text/plain" } + } + ,.body_size= 31337*1024 + ,.num_chunks_complete= 31338 + }; + for (i = 0; i < MAX_CHUNKS; i++) { + large_chunked.chunk_lengths[i] = 1024; + } + test_message_count_body(&large_chunked); + free(msg); + } + + + + printf("response scan 1/2 "); + test_scan( &responses[TRAILING_SPACE_ON_CHUNKED_BODY] + , &responses[NO_BODY_HTTP10_KA_204] + , &responses[NO_REASON_PHRASE] + ); + + printf("response scan 2/2 "); + test_scan( &responses[BONJOUR_MADAME_FR] + , &responses[UNDERSTORE_HEADER_KEY] + , &responses[NO_CARRIAGE_RET] + ); + + puts("responses okay"); + + + /// REQUESTS + + test_simple("GET / IHTTP/1.0\r\n\r\n", HPE_INVALID_CONSTANT); + test_simple("GET / ICE/1.0\r\n\r\n", HPE_INVALID_CONSTANT); + test_simple("GET / HTP/1.1\r\n\r\n", HPE_INVALID_VERSION); + test_simple("GET / HTTP/01.1\r\n\r\n", HPE_INVALID_VERSION); + test_simple("GET / HTTP/11.1\r\n\r\n", HPE_INVALID_VERSION); + test_simple("GET / HTTP/1.01\r\n\r\n", HPE_INVALID_VERSION); + + test_simple("GET / HTTP/1.0\r\nHello: w\1rld\r\n\r\n", HPE_INVALID_HEADER_TOKEN); + test_simple("GET / HTTP/1.0\r\nHello: woooo\2rld\r\n\r\n", HPE_INVALID_HEADER_TOKEN); + + // Extended characters - see nodejs/test/parallel/test-http-headers-obstext.js + test_simple("GET / HTTP/1.1\r\n" + "Test: Düsseldorf\r\n", + HPE_OK); + + // Well-formed but incomplete + test_simple("GET / HTTP/1.1\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: 6\r\n" + "\r\n" + "fooba", + HPE_OK); + + // Unknown Transfer-Encoding in request + test_simple("GET / HTTP/1.1\r\n" + "Transfer-Encoding: unknown\r\n" + "\r\n", + HPE_INVALID_TRANSFER_ENCODING); + + static const char *all_methods[] = { + "DELETE", + "GET", + "HEAD", + "POST", + "PUT", + //"CONNECT", //CONNECT can't be tested like other methods, it's a tunnel + "OPTIONS", + "TRACE", + "COPY", + "LOCK", + "MKCOL", + "MOVE", + "PROPFIND", + "PROPPATCH", + "SEARCH", + "UNLOCK", + "BIND", + "REBIND", + "UNBIND", + "ACL", + "REPORT", + "MKACTIVITY", + "CHECKOUT", + "MERGE", + "M-SEARCH", + "NOTIFY", + "SUBSCRIBE", + "UNSUBSCRIBE", + "PATCH", + "PURGE", + "MKCALENDAR", + "LINK", + "UNLINK", + 0 }; + const char **this_method; + for (this_method = all_methods; *this_method; this_method++) { + char buf[200]; + sprintf(buf, "%s / HTTP/1.1\r\n\r\n", *this_method); + test_simple(buf, HPE_OK); + } + + static const char *bad_methods[] = { + "ASDF", + "C******", + "COLA", + "GEM", + "GETA", + "M****", + "MKCOLA", + "PROPPATCHA", + "PUN", + "PX", + "SA", + "hello world", + 0 }; + for (this_method = bad_methods; *this_method; this_method++) { + char buf[200]; + sprintf(buf, "%s / HTTP/1.1\r\n\r\n", *this_method); + test_simple(buf, HPE_INVALID_METHOD); + } + + // illegal header field name line folding + test_simple("GET / HTTP/1.1\r\n" + "name\r\n" + " : value\r\n" + "\r\n", + HPE_INVALID_HEADER_TOKEN); + + const char *dumbluck2 = + "GET / HTTP/1.1\r\n" + "X-SSL-Nonsense: -----BEGIN CERTIFICATE-----\r\n" + "\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n" + "\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n" + "\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n" + "\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n" + "\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n" + "\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n" + "\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n" + "\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n" + "\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n" + "\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n" + "\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n" + "\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n" + "\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n" + "\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n" + "\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n" + "\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n" + "\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n" + "\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n" + "\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n" + "\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n" + "\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n" + "\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n" + "\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n" + "\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n" + "\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n" + "\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n" + "\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n" + "\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n" + "\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n" + "\tRA==\r\n" + "\t-----END CERTIFICATE-----\r\n" + "\r\n"; + test_simple(dumbluck2, HPE_OK); + + const char *corrupted_connection = + "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Connection\r\033\065\325eep-Alive\r\n" + "Accept-Encoding: gzip\r\n" + "\r\n"; + test_simple(corrupted_connection, HPE_INVALID_HEADER_TOKEN); + + const char *corrupted_header_name = + "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "X-Some-Header\r\033\065\325eep-Alive\r\n" + "Accept-Encoding: gzip\r\n" + "\r\n"; + test_simple(corrupted_header_name, HPE_INVALID_HEADER_TOKEN); + +#if 0 + // NOTE(Wed Nov 18 11:57:27 CET 2009) this seems okay. we just read body + // until EOF. + // + // no content-length + // error if there is a body without content length + const char *bad_get_no_headers_no_body = "GET /bad_get_no_headers_no_body/world HTTP/1.1\r\n" + "Accept: */*\r\n" + "\r\n" + "HELLO"; + test_simple(bad_get_no_headers_no_body, 0); +#endif + /* TODO sending junk and large headers gets rejected */ + + + /* check to make sure our predefined requests are okay */ + for (i = 0; i < ARRAY_SIZE(requests); i++) { + test_message(&requests[i]); + } + + for (i = 0; i < ARRAY_SIZE(requests); i++) { + test_message_pause(&requests[i]); + } + + for (i = 0; i < ARRAY_SIZE(requests); i++) { + if (!requests[i].should_keep_alive) continue; + for (j = 0; j < ARRAY_SIZE(requests); j++) { + if (!requests[j].should_keep_alive) continue; + for (k = 0; k < ARRAY_SIZE(requests); k++) { + test_multiple3(&requests[i], &requests[j], &requests[k]); + } + } + } + + printf("request scan 1/4 "); + test_scan( &requests[GET_NO_HEADERS_NO_BODY] + , &requests[GET_ONE_HEADER_NO_BODY] + , &requests[GET_NO_HEADERS_NO_BODY] + ); + + printf("request scan 2/4 "); + test_scan( &requests[POST_CHUNKED_ALL_YOUR_BASE] + , &requests[POST_IDENTITY_BODY_WORLD] + , &requests[GET_FUNKY_CONTENT_LENGTH] + ); + + printf("request scan 3/4 "); + test_scan( &requests[TWO_CHUNKS_MULT_ZERO_END] + , &requests[CHUNKED_W_TRAILING_HEADERS] + , &requests[CHUNKED_W_NONSENSE_AFTER_LENGTH] + ); + + printf("request scan 4/4 "); + test_scan( &requests[QUERY_URL_WITH_QUESTION_MARK_GET] + , &requests[PREFIX_NEWLINE_GET ] + , &requests[CONNECT_REQUEST] + ); + + puts("requests okay"); + + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http_parser_compat_test.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/http_parser_compat_test.c new file mode 100644 index 00000000000000..f39cf7630a61af --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http_parser_compat_test.c @@ -0,0 +1,689 @@ +/* + * urlparse + * + * Copyright (c) 2024 urlparse contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "http_parser_compat_test.h" + +#include +#include +#include + +#include "urlparse.h" + +static const MunitTest tests[] = { + munit_void_test(test_http_parser_compat), + munit_test_end(), +}; + +const MunitSuite http_parser_compat_suite = { + "/http_parser_compat", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE, +}; + +/* Copied from + https://github.com/nodejs/http-parser/blob/ec8b5ee63f0e51191ea43bb0c6eac7bfbff3141d/test.c + */ + +struct url_test { + const char *name; + const char *url; + int is_connect; + urlparse_url u; + int rv; +}; + +static const struct url_test url_tests[] = + { + { + .name = "proxy request", + .url = "http://hostname/", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PATH), + .port = 0, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {7, 8} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {15, 1} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "proxy request with port", + .url = "http://hostname:444/", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PORT) | (1 << URLPARSE_PATH), + .port = 444, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {7, 8} /* URLPARSE_HOST */, + {16, 3} /* URLPARSE_PORT */, + {19, 1} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "CONNECT request", + .url = "hostname:443", + .is_connect = 1, + .u = + { + .field_set = (1 << URLPARSE_HOST) | (1 << URLPARSE_PORT), + .port = 443, + .field_data = + { + {0, 0} /* URLPARSE_SCHEMA */, + {0, 8} /* URLPARSE_HOST */, + {9, 3} /* URLPARSE_PORT */, + {0, 0} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "CONNECT request but not connect", + .url = "hostname:443", + .is_connect = 0, + .rv = URLPARSE_ERR_PARSE, + }, + { + .name = "proxy ipv6 request", + .url = "http://[1:2::3:4]/", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PATH), + .port = 0, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {8, 8} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {17, 1} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "proxy ipv6 request with port", + .url = "http://[1:2::3:4]:67/", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PORT) | (1 << URLPARSE_PATH), + .port = 67, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {8, 8} /* URLPARSE_HOST */, + {18, 2} /* URLPARSE_PORT */, + {20, 1} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "CONNECT ipv6 address", + .url = "[1:2::3:4]:443", + .is_connect = 1, + .u = + { + .field_set = (1 << URLPARSE_HOST) | (1 << URLPARSE_PORT), + .port = 443, + .field_data = + { + {0, 0} /* URLPARSE_SCHEMA */, + {1, 8} /* URLPARSE_HOST */, + {11, 3} /* URLPARSE_PORT */, + {0, 0} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "ipv4 in ipv6 address", + .url = "http://[2001:0000:0000:0000:0000:0000:1.9.1.1]/", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PATH), + .port = 0, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {8, 37} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {46, 1} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "extra ? in query string", + .url = "http://a.tbcdn.cn/p/fp/2010c/??fp-header-min.css,fp-base-min.css," + "fp-channel-min.css,fp-product-min.css,fp-mall-min.css,fp-" + "category-min.css," + "fp-sub-min.css,fp-gdp4p-min.css,fp-css3-min.css,fp-misc-min.css?" + "t=20101022.css", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PATH) | (1 << URLPARSE_QUERY), + .port = 0, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {7, 10} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {17, 12} /* URLPARSE_PATH */, + {30, 187} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "space URL encoded", + .url = "/toto.html?toto=a%20b", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_PATH) | (1 << URLPARSE_QUERY), + .port = 0, + .field_data = + { + {0, 0} /* URLPARSE_SCHEMA */, + {0, 0} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {0, 10} /* URLPARSE_PATH */, + {11, 10} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "URL fragment", + .url = "/toto.html#titi", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_PATH) | (1 << URLPARSE_FRAGMENT), + .port = 0, + .field_data = + { + {0, 0} /* URLPARSE_SCHEMA */, + {0, 0} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {0, 10} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {11, 4} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "complex URL fragment", + .url = "http://www.webmasterworld.com/r.cgi?f=21&d=8405&url=" + "http://www.example.com/index.html?foo=bar&hello=world#midpage", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PATH) | (1 << URLPARSE_QUERY) | + (1 << URLPARSE_FRAGMENT), + .port = 0, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {7, 22} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {29, 6} /* URLPARSE_PATH */, + {36, 69} /* URLPARSE_QUERY */, + {106, 7} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "complex URL from node js url parser doc", + .url = "http://host.com:8080/p/a/t/h?query=string#hash", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PORT) | (1 << URLPARSE_PATH) | + (1 << URLPARSE_QUERY) | (1 << URLPARSE_FRAGMENT), + .port = 8080, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {7, 8} /* URLPARSE_HOST */, + {16, 4} /* URLPARSE_PORT */, + {20, 8} /* URLPARSE_PATH */, + {29, 12} /* URLPARSE_QUERY */, + {42, 4} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "complex URL with basic auth from node js url parser doc", + .url = "http://a:b@host.com:8080/p/a/t/h?query=string#hash", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PORT) | (1 << URLPARSE_PATH) | + (1 << URLPARSE_QUERY) | (1 << URLPARSE_FRAGMENT) | + (1 << URLPARSE_USERINFO), + .port = 8080, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {11, 8} /* URLPARSE_HOST */, + {20, 4} /* URLPARSE_PORT */, + {24, 8} /* URLPARSE_PATH */, + {33, 12} /* URLPARSE_QUERY */, + {46, 4} /* URLPARSE_FRAGMENT */, + {7, 3} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "double @", + .url = "http://a:b@@hostname:443/", + .is_connect = 0, + .rv = URLPARSE_ERR_PARSE, + }, + { + .name = "proxy empty host", + .url = "http://:443/", + .is_connect = 0, + .rv = URLPARSE_ERR_PARSE, + }, + { + .name = "proxy empty port", + .url = "http://hostname:/", + .is_connect = 0, + .rv = URLPARSE_ERR_PARSE, + }, + { + .name = "CONNECT with basic auth", + .url = "a:b@hostname:443", + .is_connect = 1, + .rv = URLPARSE_ERR_PARSE, + }, + { + .name = "CONNECT empty host", + .url = ":443", + .is_connect = 1, + .rv = URLPARSE_ERR_PARSE, + }, + { + .name = "CONNECT empty port", + .url = "hostname:", + .is_connect = 1, + .rv = URLPARSE_ERR_PARSE, + }, + { + .name = "CONNECT with extra bits", + .url = "hostname:443/", + .is_connect = 1, + .rv = URLPARSE_ERR_PARSE, + }, + { + .name = "space in URL", + .url = "/foo bar/", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "proxy basic auth with space url encoded", + .url = "http://a%20:b@host.com/", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PATH) | (1 << URLPARSE_USERINFO), + .port = 0, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {14, 8} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {22, 1} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {7, 6} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "carriage return in URL", + .url = "/foo\rbar/", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "proxy double : in URL", + .url = "http://hostname::443/", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "proxy basic auth with double :", + .url = "http://a::b@host.com/", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PATH) | (1 << URLPARSE_USERINFO), + .port = 0, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {12, 8} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {20, 1} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {7, 4} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "line feed in URL", + .url = "/foo\nbar/", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "proxy empty basic auth", + .url = "http://@hostname/fo", + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PATH), + .port = 0, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {8, 8} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {16, 3} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "proxy line feed in hostname", + .url = "http://host\name/fo", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "proxy % in hostname", + .url = "http://host%name/fo", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "proxy ; in hostname", + .url = "http://host;ame/fo", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "proxy basic auth with unreservedchars", + .url = "http://a!;-_!=+$@host.com/", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PATH) | (1 << URLPARSE_USERINFO), + .port = 0, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {17, 8} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {25, 1} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {7, 9} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "proxy only empty basic auth", + .url = "http://@/fo", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "proxy only basic auth", + .url = "http://toto@/fo", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "proxy emtpy hostname", + .url = "http:///fo", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "proxy = in URL", + .url = "http://host=ame/fo", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "ipv6 address with Zone ID", + .url = "http://[fe80::a%25eth0]/", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PATH), + .port = 0, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {8, 14} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {23, 1} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "ipv6 address with Zone ID, but '%' is not percent-encoded", + .url = "http://[fe80::a%eth0]/", + .is_connect = 0, + .u = + { + .field_set = (1 << URLPARSE_SCHEMA) | (1 << URLPARSE_HOST) | + (1 << URLPARSE_PATH), + .port = 0, + .field_data = + { + {0, 4} /* URLPARSE_SCHEMA */, + {8, 12} /* URLPARSE_HOST */, + {0, 0} /* URLPARSE_PORT */, + {21, 1} /* URLPARSE_PATH */, + {0, 0} /* URLPARSE_QUERY */, + {0, 0} /* URLPARSE_FRAGMENT */, + {0, 0} /* URLPARSE_USERINFO */, + }, + }, + .rv = 0, + }, + { + .name = "ipv6 address ending with '%'", + .url = "http://[fe80::a%]/", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "ipv6 address with Zone ID including bad character", + .url = "http://[fe80::a%$HOME]/", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "just ipv6 Zone ID", + .url = "http://[%eth0]/", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "empty url", + .url = "", + .is_connect = 0, + .rv = URLPARSE_ERR_PARSE, + }, + { + .name = "NULL url", + .url = NULL, + .is_connect = 0, + .rv = URLPARSE_ERR_PARSE, + }, + { + .name = "full of spaces url", + .url = " ", + .is_connect = 0, + .rv = URLPARSE_ERR_PARSE, + }, + { + .name = "tab in URL", + .url = "/foo\tbar/", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, + { + .name = "form feed in URL", + .url = "/foo\fbar/", + .rv = URLPARSE_ERR_PARSE /* s_dead */, + }, +}; + +static void dump_url(const char *url, const urlparse_url *u) { + size_t i; + + fprintf(stderr, "\tfield_set: 0x%x, port: %u\n", u->field_set, u->port); + for (i = 0; i < URLPARSE_MAX; i++) { + if ((u->field_set & (1 << i)) == 0) { + fprintf(stderr, "\tfield_data[%zu]: unset\n", i); + continue; + } + + fprintf(stderr, "\tfield_data[%zu]: off: %u len: %u part: \"%.*s\"\n", i, + u->field_data[i].off, u->field_data[i].len, u->field_data[i].len, + url + u->field_data[i].off); + } +} + +void test_http_parser_compat(void) { + urlparse_url u; + const struct url_test *test; + size_t i; + int rv; + + for (i = 0; i < (sizeof(url_tests) / sizeof(url_tests[0])); ++i) { + test = &url_tests[i]; + memset(&u, 0, sizeof(u)); + + rv = urlparse_parse_url(test->url, test->url ? strlen(test->url) : 0, + test->is_connect, &u); + + if (test->rv == 0) { + if (rv != 0) { + fprintf(stderr, + "\n*** http_parser_parse_url(\"%s\") \"%s\" test failed, " + "unexpected rv %d ***\n\n", + test->url, test->name, rv); + + assert_int(0, ==, rv); + } + + if (memcmp(&u, &test->u, sizeof(u)) != 0) { + fprintf(stderr, + "\n*** http_parser_parse_url(\"%s\") \"%s\" failed ***\n", + test->url, test->name); + + fprintf(stderr, "target http_parser_url:\n"); + dump_url(test->url, &test->u); + fprintf(stderr, "result http_parser_url:\n"); + dump_url(test->url, &u); + + assert_memory_equal(sizeof(u), &test->u, &u); + } + } else { + /* test->rv != 0 */ + if (rv == 0) { + fprintf(stderr, + "\n*** http_parser_parse_url(\"%s\") \"%s\" test failed, " + "unexpected rv %d ***\n\n", + test->url, test->name, rv); + + assert_int(test->rv, ==, rv); + } + } + } +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/http_parser_compat_test.h b/deps/ngtcp2/ngtcp2/third-party/urlparse/http_parser_compat_test.h new file mode 100644 index 00000000000000..f97f5593928cd1 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/http_parser_compat_test.h @@ -0,0 +1,36 @@ +/* + * urlparse + * + * Copyright (c) 2024 urlparse contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef HTTP_PARSER_COMPAT_TEST_H +#define HTTP_PARSER_COMPAT_TEST_H + +#define MUNIT_ENABLE_ASSERT_ALIASES + +#include "munit.h" + +extern const MunitSuite http_parser_compat_suite; + +munit_void_test_decl(test_http_parser_compat) + +#endif /* !defined(HTTP_PARSER_COMPAT_TEST_H) */ diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.appveyor.yml b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.appveyor.yml new file mode 100644 index 00000000000000..55720926e46a4c --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.appveyor.yml @@ -0,0 +1,34 @@ +version: "{build}" + +environment: + matrix: + - ARCHITECTURE: x64 + MSVC_VER: 14 + - ARCHITECTURE: x86 + MSVC_VER: 14 + - ARCHITECTURE: x64 + MSVC_VER: 12 + - ARCHITECTURE: x86 + MSVC_VER: 12 + - ARCHITECTURE: x86 + MSVC_VER: 11 + - ARCHITECTURE: x86 + MSVC_VER: 10 + - ARCHITECTURE: x86 + MSVC_VER: 9 + +branches: + except: + - master + - /^(wip\/)?(travis|osx|mingw|ipp)(\-.+)?$/ + +configuration: Debug + +install: + +before_build: + - call "C:\Program Files (x86)\Microsoft Visual Studio %MSVC_VER%.0\VC\vcvarsall.bat" %ARCHITECTURE% + +build_script: cl.exe /W4 /WX /Feexample munit.c example.c + +test_script: example.exe --color always diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.clang-format b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.clang-format new file mode 100644 index 00000000000000..0604933b9738f2 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.clang-format @@ -0,0 +1,245 @@ +--- +Language: Cpp +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignArrayOfStructures: None +AlignConsecutiveAssignments: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: true +AlignConsecutiveBitFields: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: true +AlignConsecutiveDeclarations: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: true +AlignConsecutiveMacros: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: true +AlignConsecutiveShortCaseStatements: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCaseColons: false +AlignEscapedNewlines: Right +AlignOperands: Align +AlignTrailingComments: + Kind: Always + OverEmptyLines: 0 +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowBreakBeforeNoexceptSpecifier: Never +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortCompoundRequirementOnASingleLine: true +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: true +BitFieldColonSpacing: Both +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterExternBlock: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakAdjacentStringLiterals: true +BreakAfterAttributes: Leave +BreakAfterJavaFieldAnnotations: false +BreakArrays: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: Always +BreakBeforeBraces: Attach +BreakBeforeInlineASMColon: OnlyMultiline +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerIndentWidth: 2 +ContinuationIndentWidth: 2 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: false +IndentExternBlock: AfterExternBlock +IndentGotoLabels: true +IndentPPDirectives: AfterHash +IndentRequiresClause: false +IndentWidth: 2 +IndentWrappedFunctionNames: false +InsertBraces: false +InsertNewlineAtEOF: false +InsertTrailingCommas: None +IntegerLiteralSeparator: + Binary: 0 + BinaryMinDigits: 0 + Decimal: 0 + DecimalMinDigits: 0 + Hex: 0 + HexMinDigits: 0 +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +KeepEmptyLinesAtEOF: false +LambdaBodyIndentation: Signature +LineEnding: DeriveLF +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PackConstructorInitializers: NextLine +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakScopeResolution: 500 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyIndentedWhitespace: 0 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Right +PPIndentWidth: -1 +QualifierAlignment: Leave +ReferenceAlignment: Pointer +ReflowComments: true +RemoveBracesLLVM: false +RemoveParentheses: Leave +RemoveSemicolon: false +RequiresClausePosition: OwnLine +RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Leave +ShortNamespaceLines: 1 +SkipMacroDefinitionBody: false +SortIncludes: Never +SortJavaStaticImport: Before +SortUsingDeclarations: LexicographicNumeric +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeJsonColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDefinitionName: false + AfterFunctionDeclarationName: false + AfterIfMacros: true + AfterOverloadedOperator: false + AfterPlacementOperator: true + AfterRequiresInClause: false + AfterRequiresInExpression: false + BeforeNonEmptyParentheses: false +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInContainerLiterals: true +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParens: Never +SpacesInParensOptions: + InCStyleCasts: false + InConditionalStatements: false + InEmptyParentheses: false + Other: false +SpacesInSquareBrackets: false +Standard: Latest +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseTab: Never +VerilogBreakBetweenInstancePorts: true +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME +... + diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.dir-locals.el b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.dir-locals.el new file mode 100644 index 00000000000000..4cc5d8496910fa --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.dir-locals.el @@ -0,0 +1,2 @@ +((nil . ((indent-tabs-mode . nil) + (c-basic-offset . 2)))) diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.travis.yml b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.travis.yml new file mode 100644 index 00000000000000..6f221a8ba00a7b --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/.travis.yml @@ -0,0 +1,149 @@ +language: c +sudo: false +dist: trusty +branches: + except: + - /^(wip\/)?(appveyor|msvc|mingw|windows)(\-.+)?$/ + # https://github.com/travis-ci/travis-ci/issues/6632 + # - /^master$/ +matrix: + include: + ### + ## Linux builds using various versions of GCC. + ### + - env: C_COMPILER=gcc-6 + addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-6 + - g++-6 + - env: C_COMPILER=gcc-5 + addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-5 + - g++-5 + # - env: C_COMPILER=gcc-4.9 + # addons: + # apt: + # sources: + # - ubuntu-toolchain-r-test + # packages: + # - gcc-4.9 + # - g++-4.9 + - env: C_COMPILER=gcc-4.8 + addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-4.8 + - g++-4.8 + # - env: C_COMPILER=gcc-4.7 + # addons: + # apt: + # sources: + # - ubuntu-toolchain-r-test + # packages: + # - gcc-4.7 + # - g++-4.7 + - env: C_COMPILER=gcc-4.6 + addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-4.6 + - g++-4.6 + # - os: linux + # env: C_COMPILER=gcc-4.5 + # addons: + # apt: + # sources: + # - ubuntu-toolchain-r-test + # packages: + # - gcc-4.5 + # - g++-4.5 + - env: C_COMPILER=gcc-4.4 + addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-4.4 + - g++-4.4 + + ### + ## clang on Linux + ### + - env: C_COMPILER=clang-3.9 + addons: + apt: + sources: + - llvm-toolchain-precise-3.9 + - ubuntu-toolchain-r-test + packages: + - clang-3.9 + # - env: C_COMPILER=clang-3.8 + # addons: + # apt: + # sources: + # - llvm-toolchain-precise-3.8 + # - ubuntu-toolchain-r-test + # packages: + # - clang-3.8 + - env: C_COMPILER=clang-3.7 + addons: + apt: + sources: + - llvm-toolchain-precise-3.7 + - ubuntu-toolchain-r-test + packages: + - clang-3.7 + # - env: C_COMPILER=clang-3.6 + # addons: + # apt: + # sources: + # - llvm-toolchain-precise-3.6 + # - ubuntu-toolchain-r-test + # packages: + # - clang-3.6 + - env: C_COMPILER=clang-3.5 + addons: + apt: + sources: + - llvm-toolchain-precise-3.5 + - ubuntu-toolchain-r-test + packages: + - clang-3.5 + + ### + ## PGI + ### + - env: C_COMPILER=pgcc OPENMP=y + + ### + ## OS X + ### + - os: osx + + ### + ## Meson + ### + - env: BUILD_SYSTEM=meson + +before_install: +- if [ -n "${C_COMPILER}" ]; then export CC="${C_COMPILER}"; fi +- if [ "${C_COMPILER}" = "pgcc" ]; then wget -q -O /dev/stdout 'https://raw.githubusercontent.com/nemequ/pgi-travis/master/install-pgi.sh' | /bin/sh; fi +- if [ "${BUILD_SYSTEM}" = "meson" ]; then wget -O /tmp/ninja-linux.zip $(curl -s https://api.github.com/repos/ninja-build/ninja/releases/latest | grep -oP 'https://github.com/ninja-build/ninja/releases/download/v[0-9\.]+/ninja-linux.zip') && unzip -q /tmp/ninja-linux.zip -d ~/bin && pyenv local 3.6 && pip3 install meson; fi + +script: + - if [ "${BUILD_SYSTEM}" = "meson" ]; then meson build && ninja -Cbuild; else make CC="${CC}" AGGRESSIVE_WARNINGS=y EXTENSION="${EXTENSION}" OPENMP="${OPENMP}" ASAN="${ASAN}" UBSAN="${UBSAN}"; fi + - if [ "${BUILD_SYSTEM}" = "meson" ]; then ninja -Cbuild test; else make test; fi + +notifications: + email: false diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/COPYING b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/COPYING new file mode 100644 index 00000000000000..2991ac706dd3fd --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/COPYING @@ -0,0 +1,21 @@ +µnit Testing Framework +Copyright (c) 2013-2016 Evan Nemerson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/README.md b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/README.md new file mode 100644 index 00000000000000..9f861f2a1d9032 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/README.md @@ -0,0 +1,54 @@ +# µnit + +µnit is a small but full-featured unit testing framework for C. It has +no dependencies (beyond libc), is permissively licensed (MIT), and is +easy to include into any project. + +For more information, see +[the µnit web site](https://nemequ.github.io/munit). + +[![Build status](https://travis-ci.org/nemequ/munit.svg?branch=master)](https://travis-ci.org/nemequ/munit) +[![Windows build status](https://ci.appveyor.com/api/projects/status/db515g5ifcwjohq7/branch/master?svg=true)](https://ci.appveyor.com/project/quixdb/munit/branch/master) + +## Features + +Features µnit currently includes include: + + * Handy assertion macros which make for nice error messages. + * Reproducible cross-platform random number generation, including + support for supplying a seed via CLI. + * Timing of both wall-clock and CPU time. + * Parameterized tests. + * Nested test suites. + * Flexible CLI. + * Forking + ([except on Windows](https://github.com/nemequ/munit/issues/2)). + * Hiding output of successful tests. + +Features µnit does not currently include, but some day may include +(a.k.a., if you file a PR…), include: + + * [TAP](http://testanything.org/) support; feel free to discuss in + [issue #1](https://github.com/nemequ/munit/issues/1) + +### Include into your project with meson + +In your `subprojects` folder put a `munit.wrap` file containing: + +``` +[wrap-git] +directory=munit +url=https://github.com/nemequ/munit/ +revision=head +``` + +Then you can use a subproject fallback when you include munit as a +dependency to your project: `dependency('munit', fallback: ['munit', 'munit_dep'])` + +## Documentation + +See [the µnit web site](https://nemequ.github.io/munit). + +Additionally, there is a heavily-commented +[example.c](https://github.com/nemequ/munit/blob/master/example.c) in +the repository. diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/example.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/example.c new file mode 100644 index 00000000000000..8a2bf1d36bd587 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/example.c @@ -0,0 +1,341 @@ +/* Example file for using µnit. + * + * µnit is MIT-licensed, but for this file and this file alone: + * + * To the extent possible under law, the author(s) of this file have + * waived all copyright and related or neighboring rights to this + * work. See for + * details. + *********************************************************************/ + +#include "munit.h" + +/* This is just to disable an MSVC warning about conditional + * expressions being constant, which you shouldn't have to do for your + * code. It's only here because we want to be able to do silly things + * like assert that 0 != 1 for our demo. */ +#if defined(_MSC_VER) +# pragma warning(disable : 4127) +#endif + +/* Tests are functions that return void, and take a single void* + * parameter. We'll get to what that parameter is later. */ +static MunitResult test_compare(const MunitParameter params[], void *data) { + /* We'll use these later */ + const unsigned char val_uchar = 'b'; + const short val_short = 1729; + double pi = 3.141592654; + char *stewardesses = "stewardesses"; + char *most_fun_word_to_type; + + /* These are just to silence compiler warnings about the parameters + * being unused. */ + (void)params; + (void)data; + + /* Let's start with the basics. */ + munit_assert(0 != 1); + + /* There is also the more verbose, though slightly more descriptive + munit_assert_true/false: */ + munit_assert_false(0); + + /* You can also call munit_error and munit_errorf yourself. We + * won't do it is used to indicate a failure, but here is what it + * would look like: */ + /* munit_error("FAIL"); */ + /* munit_errorf("Goodbye, cruel %s", "world"); */ + + /* There are macros for comparing lots of types. */ + munit_assert_char('a', ==, 'a'); + + /* Sure, you could just assert('a' == 'a'), but if you did that, a + * failed assertion would just say something like "assertion failed: + * val_uchar == 'b'". µnit will tell you the actual values, so a + * failure here would result in something like "assertion failed: + * val_uchar == 'b' ('X' == 'b')." */ + munit_assert_uchar(val_uchar, ==, 'b'); + + /* Obviously we can handle values larger than 'char' and 'uchar'. + * There are versions for char, short, int, long, long long, + * int8/16/32/64_t, as well as the unsigned versions of them all. */ + munit_assert_short(42, <, val_short); + + /* There is also support for size_t. + * + * The longest word in English without repeating any letters is + * "uncopyrightables", which has uncopyrightable (and + * dermatoglyphics, which is the study of fingerprints) beat by a + * character */ + munit_assert_size(strlen("uncopyrightables"), >, strlen("dermatoglyphics")); + + /* Of course there is also support for doubles and floats. */ + munit_assert_double(pi, ==, 3.141592654); + + /* If you want to compare two doubles for equality, you might want + * to consider using munit_assert_double_equal. It compares two + * doubles for equality within a precison of 1.0 x 10^-(precision). + * Note that precision (the third argument to the macro) needs to be + * fully evaluated to an integer by the preprocessor so µnit doesn't + * have to depend pow, which is often in libm not libc. */ + munit_assert_double_equal(3.141592654, 3.141592653589793, 9); + + /* And if you want to check strings for equality (or inequality), + * there is munit_assert_string_equal/not_equal. + * + * "stewardesses" is the longest word you can type on a QWERTY + * keyboard with only one hand, which makes it loads of fun to type. + * If I'm going to have to type a string repeatedly, let's make it a + * good one! */ + munit_assert_string_equal(stewardesses, "stewardesses"); + + /* A personal favorite macro which is fantastic if you're working + * with binary data, is the one which naïvely checks two blobs of + * memory for equality. If this fails it will tell you the offset + * of the first differing byte. */ + munit_assert_memory_equal(7, stewardesses, "steward"); + + /* You can also make sure that two blobs differ *somewhere*: */ + munit_assert_memory_not_equal(8, stewardesses, "steward"); + + /* There are equal/not_equal macros for pointers, too: */ + most_fun_word_to_type = stewardesses; + munit_assert_ptr_equal(most_fun_word_to_type, stewardesses); + + /* And null/not_null */ + munit_assert_null(NULL); + munit_assert_not_null(most_fun_word_to_type); + + /* Lets verify that the data parameter is what we expected. We'll + * see where this comes from in a bit. + * + * Note that the casting isn't usually required; if you give this + * function a real pointer (instead of a number like 0xdeadbeef) it + * would work as expected. */ + munit_assert_ptr_equal(data, (void *)(uintptr_t)0xdeadbeef); + + return MUNIT_OK; +} + +static MunitResult test_rand(const MunitParameter params[], void *user_data) { + int random_int; + double random_dbl; + munit_uint8_t data[5]; + + (void)params; + (void)user_data; + + /* One thing missing from a lot of unit testing frameworks is a + * random number generator. You can't just use srand/rand because + * the implementation varies across different platforms, and it's + * important to be able to look at the seed used in a failing test + * to see if you can reproduce it. Some randomness is a fantastic + * thing to have in your tests, I don't know why more people don't + * do it... + * + * µnit's PRNG is re-seeded with the same value for each iteration + * of each test. The seed is retrieved from the MUNIT_SEED + * envirnment variable or, if none is provided, one will be + * (pseudo-)randomly generated. */ + + /* If you need an integer in a given range */ + random_int = munit_rand_int_range(128, 4096); + munit_assert_int(random_int, >=, 128); + munit_assert_int(random_int, <=, 4096); + + /* Or maybe you want a double, between 0 and 1: */ + random_dbl = munit_rand_double(); + munit_assert_double(random_dbl, >=, 0.0); + munit_assert_double(random_dbl, <=, 1.0); + + /* Of course, you want to be able to reproduce bugs discovered + * during testing, so every time the tests are run they print the + * random seed used. When you want to reproduce a result, just put + * that random seed in the MUNIT_SEED environment variable; it even + * works on different platforms. + * + * If you want this to pass, use 0xdeadbeef as the random seed and + * uncomment the next line of code. Note that the PRNG is not + * re-seeded between iterations of the same test, so this will only + * work on the first iteration. */ + /* munit_assert_uint32(munit_rand_uint32(), ==, 1306447409); */ + + /* You can also get blobs of random memory: */ + munit_rand_memory(sizeof(data), data); + + return MUNIT_OK; +} + +/* This test case shows how to accept parameters. We'll see how to + * specify them soon. + * + * By default, every possible variation of a parameterized test is + * run, but you can specify parameters manually if you want to only + * run specific test(s), or you can pass the --single argument to the + * CLI to have the harness simply choose one variation at random + * instead of running them all. */ +static MunitResult test_parameters(const MunitParameter params[], + void *user_data) { + const char *foo; + const char *bar; + + (void)user_data; + + /* The "foo" parameter is specified as one of the following values: + * "one", "two", or "three". */ + foo = munit_parameters_get(params, "foo"); + /* Similarly, "bar" is one of "four", "five", or "six". */ + bar = munit_parameters_get(params, "bar"); + /* "baz" is a bit more complicated. We don't actually specify a + * list of valid values, so by default NULL is passed. However, the + * CLI will accept any value. This is a good way to have a value + * that is usually selected randomly by the test, but can be + * overridden on the command line if desired. */ + /* const char* baz = munit_parameters_get(params, "baz"); */ + + /* Notice that we're returning MUNIT_FAIL instead of writing an + * error message. Error messages are generally preferable, since + * they make it easier to diagnose the issue, but this is an + * option. + * + * Possible values are: + * - MUNIT_OK: Sucess + * - MUNIT_FAIL: Failure + * - MUNIT_SKIP: The test was skipped; usually this happens when a + * particular feature isn't in use. For example, if you're + * writing a test which uses a Wayland-only feature, but your + * application is running on X11. + * - MUNIT_ERROR: The test failed, but not because of anything you + * wanted to test. For example, maybe your test downloads a + * remote resource and tries to parse it, but the network was + * down. + */ + + if (strcmp(foo, "one") != 0 && strcmp(foo, "two") != 0 && + strcmp(foo, "three") != 0) + return MUNIT_FAIL; + + if (strcmp(bar, "red") != 0 && strcmp(bar, "green") != 0 && + strcmp(bar, "blue") != 0) + return MUNIT_FAIL; + + return MUNIT_OK; +} + +/* The setup function, if you provide one, for a test will be run + * before the test, and the return value will be passed as the sole + * parameter to the test function. */ +static void *test_compare_setup(const MunitParameter params[], + void *user_data) { + (void)params; + + munit_assert_string_equal(user_data, "µnit"); + return (void *)(uintptr_t)0xdeadbeef; +} + +/* To clean up after a test, you can use a tear down function. The + * fixture argument is the value returned by the setup function + * above. */ +static void test_compare_tear_down(void *fixture) { + munit_assert_ptr_equal(fixture, (void *)(uintptr_t)0xdeadbeef); +} + +static char *foo_params[] = {(char *)"one", (char *)"two", (char *)"three", + NULL}; + +static char *bar_params[] = {(char *)"red", (char *)"green", (char *)"blue", + NULL}; + +static MunitParameterEnum test_params[] = { + {(char *)"foo", foo_params}, + {(char *)"bar", bar_params}, + {(char *)"baz", NULL}, + {NULL, NULL}, +}; + +/* Creating a test suite is pretty simple. First, you'll need an + * array of tests: */ +static MunitTest test_suite_tests[] = { + {/* The name is just a unique human-readable way to identify the + * test. You can use it to run a specific test if you want, but + * usually it's mostly decorative. */ + (char *)"/example/compare", + /* You probably won't be surprised to learn that the tests are + * functions. */ + test_compare, + /* If you want, you can supply a function to set up a fixture. If + * you supply NULL, the user_data parameter from munit_suite_main + * will be used directly. If, however, you provide a callback + * here the user_data parameter will be passed to this callback, + * and the return value from this callback will be passed to the + * test function. + * + * For our example we don't really need a fixture, but lets + * provide one anyways. */ + test_compare_setup, + /* If you passed a callback for the fixture setup function, you + * may want to pass a corresponding callback here to reverse the + * operation. */ + test_compare_tear_down, + /* Finally, there is a bitmask for options you can pass here. You + * can provide either MUNIT_TEST_OPTION_NONE or 0 here to use the + * defaults. */ + MUNIT_TEST_OPTION_NONE, NULL}, + /* Usually this is written in a much more compact format; all these + * comments kind of ruin that, though. Here is how you'll usually + * see entries written: */ + {(char *)"/example/rand", test_rand, NULL, NULL, MUNIT_TEST_OPTION_NONE, + NULL}, + /* To tell the test runner when the array is over, just add a NULL + * entry at the end. */ + {(char *)"/example/parameters", test_parameters, NULL, NULL, + MUNIT_TEST_OPTION_NONE, test_params}, + {NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}}; + +/* If you wanted to have your test suite run other test suites you + * could declare an array of them. Of course each sub-suite can + * contain more suites, etc. */ +/* static const MunitSuite other_suites[] = { */ +/* { "/second", test_suite_tests, NULL, 1, MUNIT_SUITE_OPTION_NONE }, */ +/* { NULL, NULL, NULL, 0, MUNIT_SUITE_OPTION_NONE } */ +/* }; */ + +/* Now we'll actually declare the test suite. You could do this in + * the main function, or on the heap, or whatever you want. */ +static const MunitSuite test_suite = { + /* This string will be prepended to all test names in this suite; + * for example, "/example/rand" will become "/µnit/example/rand". + * Note that, while it doesn't really matter for the top-level + * suite, NULL signal the end of an array of tests; you should use + * an empty string ("") instead. */ + (char *)"", + /* The first parameter is the array of test suites. */ + test_suite_tests, + /* In addition to containing test cases, suites can contain other + * test suites. This isn't necessary in this example, but it can be + * a great help to projects with lots of tests by making it easier + * to spread the tests across many files. This is where you would + * put "other_suites" (which is commented out above). */ + NULL, + /* An interesting feature of µnit is that it supports automatically + * running multiple iterations of the tests. This is usually only + * interesting if you make use of the PRNG to randomize your tests + * cases a bit, or if you are doing performance testing and want to + * average multiple runs. 0 is an alias for 1. */ + 1, + /* Just like MUNIT_TEST_OPTION_NONE, you can provide + * MUNIT_SUITE_OPTION_NONE or 0 to use the default settings. */ + MUNIT_SUITE_OPTION_NONE}; + +/* This is only necessary for EXIT_SUCCESS and EXIT_FAILURE, which you + * *should* be using but probably aren't (no, zero and non-zero don't + * always mean success and failure). I guess my point is that nothing + * about µnit requires it. */ +#include + +int main(int argc, char *argv[]) { + /* Finally, we'll actually run our test suite! That second argument + * is the user_data parameter which will be passed either to the + * test or (if provided) the fixture setup function. */ + return munit_suite_main(&test_suite, (void *)"µnit", argc, argv); +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/meson.build b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/meson.build new file mode 100644 index 00000000000000..c15b405a831a16 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/meson.build @@ -0,0 +1,37 @@ +project('munit', 'c') + +conf_data = configuration_data() +conf_data.set('version', '0.2.0') + +add_project_arguments('-std=c99', language : 'c') + +cc = meson.get_compiler('c') + +root_include = include_directories('.') + +munit = library('munit', + ['munit.c'], + install: meson.is_subproject()) + +if meson.is_subproject() + munit_dep = declare_dependency( + include_directories : root_include, + link_with : munit) +else + # standalone install + install_headers('munit.h') + + pkg = import('pkgconfig') + pkg.generate(name: 'munit', + description: 'µnit Testing Library for C', + version: conf_data.get('version'), + libraries: munit) + + # compile the demo project + munit_example_src = files('example.c') + munit_example = executable('munit_example', munit_example_src, + include_directories: root_include, + link_with: munit) + + test('munit example test', munit_example) +endif diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/munit.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/munit.c new file mode 100644 index 00000000000000..c5faec09b11162 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/munit.c @@ -0,0 +1,2447 @@ +/* Copyright (c) 2013-2018 Evan Nemerson + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/*** Configuration ***/ + +/* This is just where the output from the test goes. It's really just + * meant to let you choose stdout or stderr, but if anyone really want + * to direct it to a file let me know, it would be fairly easy to + * support. */ +#if !defined(MUNIT_OUTPUT_FILE) +# define MUNIT_OUTPUT_FILE stdout +#endif + +/* This is a bit more useful; it tells µnit how to format the seconds in + * timed tests. If your tests run for longer you might want to reduce + * it, and if your computer is really fast and your tests are tiny you + * can increase it. */ +#if !defined(MUNIT_TEST_TIME_FORMAT) +# define MUNIT_TEST_TIME_FORMAT "0.8f" +#endif + +/* If you have long test names you might want to consider bumping + * this. The result information takes 43 characters. */ +#if !defined(MUNIT_TEST_NAME_LEN) +# define MUNIT_TEST_NAME_LEN 37 +#endif + +/* If you don't like the timing information, you can disable it by + * defining MUNIT_DISABLE_TIMING. */ +#if !defined(MUNIT_DISABLE_TIMING) +# define MUNIT_ENABLE_TIMING +#endif + +/*** End configuration ***/ + +#if defined(_POSIX_C_SOURCE) && (_POSIX_C_SOURCE < 200809L) +# undef _POSIX_C_SOURCE +#endif +#if !defined(_POSIX_C_SOURCE) +# define _POSIX_C_SOURCE 200809L +#endif + +/* Solaris freaks out if you try to use a POSIX or SUS standard without + * the "right" C standard. */ +#if defined(_XOPEN_SOURCE) +# undef _XOPEN_SOURCE +#endif + +#if defined(__STDC_VERSION__) +# if __STDC_VERSION__ >= 201112L +# define _XOPEN_SOURCE 700 +# elif __STDC_VERSION__ >= 199901L +# define _XOPEN_SOURCE 600 +# endif +#endif + +/* Because, according to Microsoft, POSIX is deprecated. You've got + * to appreciate the chutzpah. */ +#if defined(_MSC_VER) && !defined(_CRT_NONSTDC_NO_DEPRECATE) +# define _CRT_NONSTDC_NO_DEPRECATE +#endif + +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) +# include +#elif defined(_WIN32) +/* https://msdn.microsoft.com/en-us/library/tf4dy80a.aspx */ +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(MUNIT_NO_NL_LANGINFO) && !defined(_WIN32) +# define MUNIT_NL_LANGINFO +# include +# include +# include +#endif + +#if !defined(_WIN32) +# include +# include +# include +#else +# include +# include +# include +# if !defined(STDERR_FILENO) +# define STDERR_FILENO _fileno(stderr) +# endif +#endif + +#include "munit.h" + +#define MUNIT_STRINGIFY(x) #x +#define MUNIT_XSTRINGIFY(x) MUNIT_STRINGIFY(x) + +#if defined(__GNUC__) || defined(__INTEL_COMPILER) || defined(__SUNPRO_CC) || \ + defined(__IBMCPP__) +# define MUNIT_THREAD_LOCAL __thread +#elif (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201102L)) || \ + defined(_Thread_local) +# define MUNIT_THREAD_LOCAL _Thread_local +#elif defined(_WIN32) +# define MUNIT_THREAD_LOCAL __declspec(thread) +#endif + +/* MSVC 12.0 will emit a warning at /W4 for code like 'do { ... } + * while (0)', or 'do { ... } while (1)'. I'm pretty sure nobody + * at Microsoft compiles with /W4. */ +#if defined(_MSC_VER) && (_MSC_VER <= 1800) +# pragma warning(disable : 4127) +#endif + +#if defined(_WIN32) || defined(__EMSCRIPTEN__) +# define MUNIT_NO_FORK +#endif + +#if defined(__EMSCRIPTEN__) +# define MUNIT_NO_BUFFER +#endif + +/*** Logging ***/ + +static MunitLogLevel munit_log_level_visible = MUNIT_LOG_INFO; +static MunitLogLevel munit_log_level_fatal = MUNIT_LOG_ERROR; + +#if defined(MUNIT_THREAD_LOCAL) +static MUNIT_THREAD_LOCAL munit_bool munit_error_jmp_buf_valid = 0; +static MUNIT_THREAD_LOCAL jmp_buf munit_error_jmp_buf; +#endif + +/* At certain warning levels, mingw will trigger warnings about + * suggesting the format attribute, which we've explicity *not* set + * because it will then choke on our attempts to use the MS-specific + * I64 modifier for size_t (which we have to use since MSVC doesn't + * support the C99 z modifier). */ + +#if defined(__MINGW32__) || defined(__MINGW64__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wsuggest-attribute=format" +#endif + +MUNIT_PRINTF(5, 0) +static void munit_logf_exv(MunitLogLevel level, FILE *fp, const char *filename, + int line, const char *format, va_list ap) { + if (level < munit_log_level_visible) + return; + + switch (level) { + case MUNIT_LOG_DEBUG: + fputs("Debug", fp); + break; + case MUNIT_LOG_INFO: + fputs("Info", fp); + break; + case MUNIT_LOG_WARNING: + fputs("Warning", fp); + break; + case MUNIT_LOG_ERROR: + fputs("Error", fp); + break; + default: + munit_logf_ex(MUNIT_LOG_ERROR, filename, line, "Invalid log level (%d)", + level); + return; + } + + fputs(": ", fp); + if (filename != NULL) + fprintf(fp, "%s:%d: ", filename, line); + vfprintf(fp, format, ap); + fputc('\n', fp); +} + +MUNIT_PRINTF(3, 4) +static void munit_logf_internal(MunitLogLevel level, FILE *fp, + const char *format, ...) { + va_list ap; + + va_start(ap, format); + munit_logf_exv(level, fp, NULL, 0, format, ap); + va_end(ap); +} + +static void munit_log_internal(MunitLogLevel level, FILE *fp, + const char *message) { + munit_logf_internal(level, fp, "%s", message); +} + +void munit_logf_ex(MunitLogLevel level, const char *filename, int line, + const char *format, ...) { + va_list ap; + + va_start(ap, format); + munit_logf_exv(level, stderr, filename, line, format, ap); + va_end(ap); + + if (level >= munit_log_level_fatal) { +#if defined(MUNIT_THREAD_LOCAL) + if (munit_error_jmp_buf_valid) + longjmp(munit_error_jmp_buf, 1); +#endif + abort(); + } +} + +void munit_errorf_ex(const char *filename, int line, const char *format, ...) { + va_list ap; + + va_start(ap, format); + munit_logf_exv(MUNIT_LOG_ERROR, stderr, filename, line, format, ap); + va_end(ap); + +#if defined(MUNIT_THREAD_LOCAL) + if (munit_error_jmp_buf_valid) + longjmp(munit_error_jmp_buf, 1); +#endif + abort(); +} + +#if defined(__MINGW32__) || defined(__MINGW64__) +# pragma GCC diagnostic pop +#endif + +#if !defined(MUNIT_STRERROR_LEN) +# define MUNIT_STRERROR_LEN 80 +#endif + +static void munit_log_errno(MunitLogLevel level, FILE *fp, const char *msg) { +#if defined(MUNIT_NO_STRERROR_R) || \ + (defined(__MINGW32__) && !defined(MINGW_HAS_SECURE_API)) + munit_logf_internal(level, fp, "%s: %s (%d)", msg, strerror(errno), errno); +#else + char munit_error_str[MUNIT_STRERROR_LEN]; + munit_error_str[0] = '\0'; + +# if !defined(_WIN32) + strerror_r(errno, munit_error_str, MUNIT_STRERROR_LEN); +# else + strerror_s(munit_error_str, MUNIT_STRERROR_LEN, errno); +# endif + + munit_logf_internal(level, fp, "%s: %s (%d)", msg, munit_error_str, errno); +#endif +} + +/*** Memory allocation ***/ + +void *munit_malloc_ex(const char *filename, int line, size_t size) { + void *ptr; + + if (size == 0) + return NULL; + + ptr = calloc(1, size); + if (MUNIT_UNLIKELY(ptr == NULL)) { + munit_logf_ex(MUNIT_LOG_ERROR, filename, line, + "Failed to allocate %" MUNIT_SIZE_MODIFIER "u bytes.", size); + } + + return ptr; +} + +/*** Timer code ***/ + +#if defined(MUNIT_ENABLE_TIMING) + +# define psnip_uint64_t munit_uint64_t +# define psnip_uint32_t munit_uint32_t + +/* Code copied from portable-snippets + * . If you need to + * change something, please do it there so we can keep the code in + * sync. */ + +/* Clocks (v1) + * Portable Snippets - https://gitub.com/nemequ/portable-snippets + * Created by Evan Nemerson + * + * To the extent possible under law, the authors have waived all + * copyright and related or neighboring rights to this code. For + * details, see the Creative Commons Zero 1.0 Universal license at + * https://creativecommons.org/publicdomain/zero/1.0/ + */ + +# if !defined(PSNIP_CLOCK_H) +# define PSNIP_CLOCK_H + +# if !defined(psnip_uint64_t) +# include "../exact-int/exact-int.h" +# endif + +# if !defined(PSNIP_CLOCK_STATIC_INLINE) +# if defined(__GNUC__) +# define PSNIP_CLOCK__COMPILER_ATTRIBUTES __attribute__((__unused__)) +# else +# define PSNIP_CLOCK__COMPILER_ATTRIBUTES +# endif + +# define PSNIP_CLOCK__FUNCTION PSNIP_CLOCK__COMPILER_ATTRIBUTES static +# endif + +enum PsnipClockType { + /* This clock provides the current time, in units since 1970-01-01 + * 00:00:00 UTC not including leap seconds. In other words, UNIX + * time. Keep in mind that this clock doesn't account for leap + * seconds, and can go backwards (think NTP adjustments). */ + PSNIP_CLOCK_TYPE_WALL = 1, + /* The CPU time is a clock which increases only when the current + * process is active (i.e., it doesn't increment while blocking on + * I/O). */ + PSNIP_CLOCK_TYPE_CPU = 2, + /* Monotonic time is always running (unlike CPU time), but it only + ever moves forward unless you reboot the system. Things like NTP + adjustments have no effect on this clock. */ + PSNIP_CLOCK_TYPE_MONOTONIC = 3 +}; + +struct PsnipClockTimespec { + psnip_uint64_t seconds; + psnip_uint64_t nanoseconds; +}; + +/* Methods we support: */ + +# define PSNIP_CLOCK_METHOD_CLOCK_GETTIME 1 +# define PSNIP_CLOCK_METHOD_TIME 2 +# define PSNIP_CLOCK_METHOD_GETTIMEOFDAY 3 +# define PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER 4 +# define PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME 5 +# define PSNIP_CLOCK_METHOD_CLOCK 6 +# define PSNIP_CLOCK_METHOD_GETPROCESSTIMES 7 +# define PSNIP_CLOCK_METHOD_GETRUSAGE 8 +# define PSNIP_CLOCK_METHOD_GETSYSTEMTIMEPRECISEASFILETIME 9 +# define PSNIP_CLOCK_METHOD_GETTICKCOUNT64 10 + +# include + +# if defined(HEDLEY_UNREACHABLE) +# define PSNIP_CLOCK_UNREACHABLE() HEDLEY_UNREACHABLE() +# else +# define PSNIP_CLOCK_UNREACHABLE() assert(0) +# endif + +/* Choose an implementation */ + +/* #undef PSNIP_CLOCK_WALL_METHOD */ +/* #undef PSNIP_CLOCK_CPU_METHOD */ +/* #undef PSNIP_CLOCK_MONOTONIC_METHOD */ + +/* We want to be able to detect the libc implementation, so we include + ( isn't available everywhere). */ + +# if defined(__unix__) || defined(__unix) || defined(__linux__) +# include +# include +# endif + +# if defined(_POSIX_TIMERS) && (_POSIX_TIMERS > 0) +/* These are known to work without librt. If you know of others + * please let us know so we can add them. */ +# if (defined(__GLIBC__) && \ + (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 17))) || \ + (defined(__FreeBSD__)) +# define PSNIP_CLOCK_HAVE_CLOCK_GETTIME +# elif !defined(PSNIP_CLOCK_NO_LIBRT) +# define PSNIP_CLOCK_HAVE_CLOCK_GETTIME +# endif +# endif + +# if defined(_WIN32) +# if !defined(PSNIP_CLOCK_CPU_METHOD) +# define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_GETPROCESSTIMES +# endif +# if !defined(PSNIP_CLOCK_MONOTONIC_METHOD) +# define PSNIP_CLOCK_MONOTONIC_METHOD \ + PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER +# endif +# endif + +# if defined(__MACH__) && !defined(__gnu_hurd__) +# if !defined(PSNIP_CLOCK_MONOTONIC_METHOD) +# define PSNIP_CLOCK_MONOTONIC_METHOD \ + PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME +# endif +# endif + +# if defined(PSNIP_CLOCK_HAVE_CLOCK_GETTIME) +# include +# if !defined(PSNIP_CLOCK_WALL_METHOD) +# if defined(CLOCK_REALTIME_PRECISE) +# define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME +# define PSNIP_CLOCK_CLOCK_GETTIME_WALL CLOCK_REALTIME_PRECISE +# elif !defined(__sun) +# define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME +# define PSNIP_CLOCK_CLOCK_GETTIME_WALL CLOCK_REALTIME +# endif +# endif +# if !defined(PSNIP_CLOCK_CPU_METHOD) +# if defined(_POSIX_CPUTIME) || defined(CLOCK_PROCESS_CPUTIME_ID) +# define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME +# define PSNIP_CLOCK_CLOCK_GETTIME_CPU CLOCK_PROCESS_CPUTIME_ID +# elif defined(CLOCK_VIRTUAL) +# define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME +# define PSNIP_CLOCK_CLOCK_GETTIME_CPU CLOCK_VIRTUAL +# endif +# endif +# if !defined(PSNIP_CLOCK_MONOTONIC_METHOD) +# if defined(CLOCK_MONOTONIC_RAW) +# define PSNIP_CLOCK_MONOTONIC_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME +# define PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC CLOCK_MONOTONIC +# elif defined(CLOCK_MONOTONIC_PRECISE) +# define PSNIP_CLOCK_MONOTONIC_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME +# define PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC CLOCK_MONOTONIC_PRECISE +# elif defined(_POSIX_MONOTONIC_CLOCK) || defined(CLOCK_MONOTONIC) +# define PSNIP_CLOCK_MONOTONIC_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME +# define PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC CLOCK_MONOTONIC +# endif +# endif +# endif + +# if defined(_POSIX_VERSION) && (_POSIX_VERSION >= 200112L) +# if !defined(PSNIP_CLOCK_WALL_METHOD) +# define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_GETTIMEOFDAY +# endif +# endif + +# if !defined(PSNIP_CLOCK_WALL_METHOD) +# define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_TIME +# endif + +# if !defined(PSNIP_CLOCK_CPU_METHOD) +# define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_CLOCK +# endif + +/* Primarily here for testing. */ +# if !defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + defined(PSNIP_CLOCK_REQUIRE_MONOTONIC) +# error No monotonic clock found. +# endif + +/* Implementations */ + +# if (defined(PSNIP_CLOCK_CPU_METHOD) && \ + (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \ + (defined(PSNIP_CLOCK_WALL_METHOD) && \ + (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \ + (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \ + (defined(PSNIP_CLOCK_CPU_METHOD) && \ + (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK)) || \ + (defined(PSNIP_CLOCK_WALL_METHOD) && \ + (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK)) || \ + (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK)) || \ + (defined(PSNIP_CLOCK_CPU_METHOD) && \ + (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_TIME)) || \ + (defined(PSNIP_CLOCK_WALL_METHOD) && \ + (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_TIME)) || \ + (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_TIME)) +# include +# endif + +# if (defined(PSNIP_CLOCK_CPU_METHOD) && \ + (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY)) || \ + (defined(PSNIP_CLOCK_WALL_METHOD) && \ + (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY)) || \ + (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY)) +# include +# endif + +# if (defined(PSNIP_CLOCK_CPU_METHOD) && \ + (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES)) || \ + (defined(PSNIP_CLOCK_WALL_METHOD) && \ + (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES)) || \ + (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + (PSNIP_CLOCK_MONOTONIC_METHOD == \ + PSNIP_CLOCK_METHOD_GETPROCESSTIMES)) || \ + (defined(PSNIP_CLOCK_CPU_METHOD) && \ + (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64)) || \ + (defined(PSNIP_CLOCK_WALL_METHOD) && \ + (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64)) || \ + (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64)) +# include +# endif + +# if (defined(PSNIP_CLOCK_CPU_METHOD) && \ + (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETRUSAGE)) || \ + (defined(PSNIP_CLOCK_WALL_METHOD) && \ + (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETRUSAGE)) || \ + (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETRUSAGE)) +# include +# include +# endif + +# if (defined(PSNIP_CLOCK_CPU_METHOD) && \ + (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME)) || \ + (defined(PSNIP_CLOCK_WALL_METHOD) && \ + (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME)) || \ + (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + (PSNIP_CLOCK_MONOTONIC_METHOD == \ + PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME)) +# include +# include +# include +# endif + +/*** Implementations ***/ + +# define PSNIP_CLOCK_NSEC_PER_SEC ((psnip_uint32_t)(1000000000ULL)) + +# if (defined(PSNIP_CLOCK_CPU_METHOD) && \ + (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \ + (defined(PSNIP_CLOCK_WALL_METHOD) && \ + (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \ + (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) +PSNIP_CLOCK__FUNCTION psnip_uint32_t +psnip_clock__clock_getres(clockid_t clk_id) { + struct timespec res; + int r; + + r = clock_getres(clk_id, &res); + if (r != 0) + return 0; + + return (psnip_uint32_t)(PSNIP_CLOCK_NSEC_PER_SEC / + (psnip_uint64_t)res.tv_nsec); +} + +PSNIP_CLOCK__FUNCTION int +psnip_clock__clock_gettime(clockid_t clk_id, struct PsnipClockTimespec *res) { + struct timespec ts; + + if (clock_gettime(clk_id, &ts) != 0) + return -10; + + res->seconds = (psnip_uint64_t)(ts.tv_sec); + res->nanoseconds = (psnip_uint64_t)(ts.tv_nsec); + + return 0; +} +# endif + +PSNIP_CLOCK__FUNCTION psnip_uint32_t psnip_clock_wall_get_precision(void) { +# if !defined(PSNIP_CLOCK_WALL_METHOD) + return 0; +# elif defined(PSNIP_CLOCK_WALL_METHOD) && \ + PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME + return psnip_clock__clock_getres(PSNIP_CLOCK_CLOCK_GETTIME_WALL); +# elif defined(PSNIP_CLOCK_WALL_METHOD) && \ + PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY + return 1000000; +# elif defined(PSNIP_CLOCK_WALL_METHOD) && \ + PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_TIME + return 1; +# else + return 0; +# endif +} + +PSNIP_CLOCK__FUNCTION int +psnip_clock_wall_get_time(struct PsnipClockTimespec *res) { +# if !defined(PSNIP_CLOCK_WALL_METHOD) + (void)res; + + return -2; +# elif defined(PSNIP_CLOCK_WALL_METHOD) && \ + PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME + return psnip_clock__clock_gettime(PSNIP_CLOCK_CLOCK_GETTIME_WALL, res); +# elif defined(PSNIP_CLOCK_WALL_METHOD) && \ + PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_TIME + res->seconds = time(NULL); + res->nanoseconds = 0; +# elif defined(PSNIP_CLOCK_WALL_METHOD) && \ + PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY + struct timeval tv; + + if (gettimeofday(&tv, NULL) != 0) + return -6; + + res->seconds = (psnip_uint64_t)tv.tv_sec; + res->nanoseconds = (psnip_uint64_t)tv.tv_usec * 1000; +# else + (void)res; + + return -2; +# endif + + return 0; +} + +PSNIP_CLOCK__FUNCTION psnip_uint32_t psnip_clock_cpu_get_precision(void) { +# if !defined(PSNIP_CLOCK_CPU_METHOD) + return 0; +# elif defined(PSNIP_CLOCK_CPU_METHOD) && \ + PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME + return psnip_clock__clock_getres(PSNIP_CLOCK_CLOCK_GETTIME_CPU); +# elif defined(PSNIP_CLOCK_CPU_METHOD) && \ + PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK + return CLOCKS_PER_SEC; +# elif defined(PSNIP_CLOCK_CPU_METHOD) && \ + PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES + return PSNIP_CLOCK_NSEC_PER_SEC / 100; +# else + return 0; +# endif +} + +PSNIP_CLOCK__FUNCTION int +psnip_clock_cpu_get_time(struct PsnipClockTimespec *res) { +# if !defined(PSNIP_CLOCK_CPU_METHOD) + (void)res; + return -2; +# elif defined(PSNIP_CLOCK_CPU_METHOD) && \ + PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME + return psnip_clock__clock_gettime(PSNIP_CLOCK_CLOCK_GETTIME_CPU, res); +# elif defined(PSNIP_CLOCK_CPU_METHOD) && \ + PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK + clock_t t = clock(); + if (t == ((clock_t)-1)) + return -5; + res->seconds = t / CLOCKS_PER_SEC; + res->nanoseconds = + (t % CLOCKS_PER_SEC) * (PSNIP_CLOCK_NSEC_PER_SEC / CLOCKS_PER_SEC); +# elif defined(PSNIP_CLOCK_CPU_METHOD) && \ + PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES + FILETIME CreationTime, ExitTime, KernelTime, UserTime; + LARGE_INTEGER date, adjust; + + if (!GetProcessTimes(GetCurrentProcess(), &CreationTime, &ExitTime, + &KernelTime, &UserTime)) + return -7; + + /* http://www.frenk.com/2009/12/convert-filetime-to-unix-timestamp/ */ + date.HighPart = (LONG)UserTime.dwHighDateTime; + date.LowPart = UserTime.dwLowDateTime; + adjust.QuadPart = 11644473600000 * 10000; + date.QuadPart -= adjust.QuadPart; + + res->seconds = (psnip_uint64_t)(date.QuadPart / 10000000); + res->nanoseconds = (psnip_uint64_t)(date.QuadPart % 10000000) * + (PSNIP_CLOCK_NSEC_PER_SEC / 100); +# elif PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETRUSAGE + struct rusage usage; + if (getrusage(RUSAGE_SELF, &usage) != 0) + return -8; + + res->seconds = usage.ru_utime.tv_sec; + res->nanoseconds = tv.tv_usec * 1000; +# else + (void)res; + return -2; +# endif + + return 0; +} + +PSNIP_CLOCK__FUNCTION psnip_uint32_t psnip_clock_monotonic_get_precision(void) { +# if !defined(PSNIP_CLOCK_MONOTONIC_METHOD) + return 0; +# elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME + return psnip_clock__clock_getres(PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC); +# elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME + static mach_timebase_info_data_t tbi = { + 0, + }; + if (tbi.denom == 0) + mach_timebase_info(&tbi); + return (psnip_uint32_t)(tbi.numer / tbi.denom); +# elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64 + return 1000; +# elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + PSNIP_CLOCK_MONOTONIC_METHOD == \ + PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER + LARGE_INTEGER Frequency; + QueryPerformanceFrequency(&Frequency); + return (psnip_uint32_t)((Frequency.QuadPart > PSNIP_CLOCK_NSEC_PER_SEC) + ? PSNIP_CLOCK_NSEC_PER_SEC + : Frequency.QuadPart); +# else + return 0; +# endif +} + +PSNIP_CLOCK__FUNCTION int +psnip_clock_monotonic_get_time(struct PsnipClockTimespec *res) { +# if !defined(PSNIP_CLOCK_MONOTONIC_METHOD) + (void)res; + return -2; +# elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME + return psnip_clock__clock_gettime(PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC, res); +# elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME + psnip_uint64_t nsec = mach_absolute_time(); + static mach_timebase_info_data_t tbi = { + 0, + }; + if (tbi.denom == 0) + mach_timebase_info(&tbi); + nsec *= ((psnip_uint64_t)tbi.numer) / ((psnip_uint64_t)tbi.denom); + res->seconds = nsec / PSNIP_CLOCK_NSEC_PER_SEC; + res->nanoseconds = nsec % PSNIP_CLOCK_NSEC_PER_SEC; +# elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + PSNIP_CLOCK_MONOTONIC_METHOD == \ + PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER + LARGE_INTEGER t, f; + if (QueryPerformanceCounter(&t) == 0) + return -12; + + QueryPerformanceFrequency(&f); + res->seconds = (psnip_uint64_t)(t.QuadPart / f.QuadPart); + res->nanoseconds = (psnip_uint64_t)(t.QuadPart % f.QuadPart); + if (f.QuadPart > PSNIP_CLOCK_NSEC_PER_SEC) + res->nanoseconds /= (psnip_uint64_t)f.QuadPart / PSNIP_CLOCK_NSEC_PER_SEC; + else + res->nanoseconds *= PSNIP_CLOCK_NSEC_PER_SEC / (psnip_uint64_t)f.QuadPart; +# elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && \ + PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64 + const ULONGLONG msec = GetTickCount64(); + res->seconds = msec / 1000; + res->nanoseconds = sec % 1000; +# else + return -2; +# endif + + return 0; +} + +/* Returns the number of ticks per second for the specified clock. + * For example, a clock with millisecond precision would return 1000, + * and a clock with 1 second (such as the time() function) would + * return 1. + * + * If the requested clock isn't available, it will return 0. + * Hopefully this will be rare, but if it happens to you please let us + * know so we can work on finding a way to support your system. + * + * Note that different clocks on the same system often have a + * different precisions. + */ +PSNIP_CLOCK__FUNCTION psnip_uint32_t +psnip_clock_get_precision(enum PsnipClockType clock_type) { + switch (clock_type) { + case PSNIP_CLOCK_TYPE_MONOTONIC: + return psnip_clock_monotonic_get_precision(); + case PSNIP_CLOCK_TYPE_CPU: + return psnip_clock_cpu_get_precision(); + case PSNIP_CLOCK_TYPE_WALL: + return psnip_clock_wall_get_precision(); + } + + PSNIP_CLOCK_UNREACHABLE(); + return 0; +} + +/* Set the provided timespec to the requested time. Returns 0 on + * success, or a negative value on failure. */ +PSNIP_CLOCK__FUNCTION int psnip_clock_get_time(enum PsnipClockType clock_type, + struct PsnipClockTimespec *res) { + assert(res != NULL); + + switch (clock_type) { + case PSNIP_CLOCK_TYPE_MONOTONIC: + return psnip_clock_monotonic_get_time(res); + case PSNIP_CLOCK_TYPE_CPU: + return psnip_clock_cpu_get_time(res); + case PSNIP_CLOCK_TYPE_WALL: + return psnip_clock_wall_get_time(res); + } + + return -1; +} + +# endif /* !defined(PSNIP_CLOCK_H) */ + +static psnip_uint64_t munit_clock_get_elapsed(struct PsnipClockTimespec *start, + struct PsnipClockTimespec *end) { + psnip_uint64_t r = (end->seconds - start->seconds) * PSNIP_CLOCK_NSEC_PER_SEC; + if (end->nanoseconds < start->nanoseconds) { + return r - (start->nanoseconds - end->nanoseconds); + } + + return r + (end->nanoseconds - start->nanoseconds); +} + +#else +# include +#endif /* defined(MUNIT_ENABLE_TIMING) */ + +/*** PRNG stuff ***/ + +/* This is (unless I screwed up, which is entirely possible) the + * version of PCG with 32-bit state. It was chosen because it has a + * small enough state that we should reliably be able to use CAS + * instead of requiring a lock for thread-safety. + * + * If I did screw up, I probably will not bother changing it unless + * there is a significant bias. It's really not important this be + * particularly strong, as long as it is fairly random it's much more + * important that it be reproducible, so bug reports have a better + * chance of being reproducible. */ + +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \ + !defined(__STDC_NO_ATOMICS__) && !defined(__EMSCRIPTEN__) && \ + (!defined(__GNUC_MINOR__) || (__GNUC__ > 4) || \ + (__GNUC__ == 4 && __GNUC_MINOR__ > 8)) +# define HAVE_STDATOMIC +#elif defined(__clang__) +# if __has_extension(c_atomic) +# define HAVE_CLANG_ATOMICS +# endif +#endif + +/* Workaround for http://llvm.org/bugs/show_bug.cgi?id=26911 */ +#if defined(__clang__) && defined(_WIN32) +# undef HAVE_STDATOMIC +# if defined(__c2__) +# undef HAVE_CLANG_ATOMICS +# endif +#endif + +#if defined(_OPENMP) +# define ATOMIC_UINT32_T uint32_t +#elif defined(HAVE_STDATOMIC) +# include +# define ATOMIC_UINT32_T _Atomic uint32_t +#elif defined(HAVE_CLANG_ATOMICS) +# define ATOMIC_UINT32_T _Atomic uint32_t +#elif defined(_WIN32) +# define ATOMIC_UINT32_T volatile LONG +#else +# define ATOMIC_UINT32_T volatile uint32_t +#endif + +static ATOMIC_UINT32_T munit_rand_state = 42; + +#if defined(_OPENMP) +static inline void munit_atomic_store(ATOMIC_UINT32_T *dest, + ATOMIC_UINT32_T value) { +# pragma omp critical(munit_atomics) + *dest = value; +} + +static inline uint32_t munit_atomic_load(ATOMIC_UINT32_T *src) { + int ret; +# pragma omp critical(munit_atomics) + ret = *src; + return ret; +} + +static inline uint32_t munit_atomic_cas(ATOMIC_UINT32_T *dest, + ATOMIC_UINT32_T *expected, + ATOMIC_UINT32_T desired) { + munit_bool ret; + +# pragma omp critical(munit_atomics) + { + if (*dest == *expected) { + *dest = desired; + ret = 1; + } else { + ret = 0; + } + } + + return ret; +} +#elif defined(HAVE_STDATOMIC) +# define munit_atomic_store(dest, value) atomic_store(dest, value) +# define munit_atomic_load(src) atomic_load(src) +# define munit_atomic_cas(dest, expected, value) \ + atomic_compare_exchange_weak(dest, expected, value) +#elif defined(HAVE_CLANG_ATOMICS) +# define munit_atomic_store(dest, value) \ + __c11_atomic_store(dest, value, __ATOMIC_SEQ_CST) +# define munit_atomic_load(src) __c11_atomic_load(src, __ATOMIC_SEQ_CST) +# define munit_atomic_cas(dest, expected, value) \ + __c11_atomic_compare_exchange_weak(dest, expected, value, \ + __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST) +#elif defined(__GNUC__) && (__GNUC__ > 4) || \ + (__GNUC__ == 4 && __GNUC_MINOR__ >= 7) +# define munit_atomic_store(dest, value) \ + __atomic_store_n(dest, value, __ATOMIC_SEQ_CST) +# define munit_atomic_load(src) __atomic_load_n(src, __ATOMIC_SEQ_CST) +# define munit_atomic_cas(dest, expected, value) \ + __atomic_compare_exchange_n(dest, expected, value, 1, __ATOMIC_SEQ_CST, \ + __ATOMIC_SEQ_CST) +#elif defined(__GNUC__) && (__GNUC__ >= 4) +# define munit_atomic_store(dest, value) \ + do { \ + *(dest) = (value); \ + } while (0) +# define munit_atomic_load(src) (*(src)) +# define munit_atomic_cas(dest, expected, value) \ + __sync_bool_compare_and_swap(dest, *expected, value) +#elif defined(_WIN32) /* Untested */ +# define munit_atomic_store(dest, value) \ + do { \ + *(dest) = (value); \ + } while (0) +# define munit_atomic_load(src) (*(src)) +# define munit_atomic_cas(dest, expected, value) \ + InterlockedCompareExchange((dest), (value), *(expected)) +#else +# warning No atomic implementation, PRNG will not be thread-safe +# define munit_atomic_store(dest, value) \ + do { \ + *(dest) = (value); \ + } while (0) +# define munit_atomic_load(src) (*(src)) +static inline munit_bool munit_atomic_cas(ATOMIC_UINT32_T *dest, + ATOMIC_UINT32_T *expected, + ATOMIC_UINT32_T desired) { + if (*dest == *expected) { + *dest = desired; + return 1; + } else { + return 0; + } +} +#endif + +#define MUNIT_PRNG_MULTIPLIER (747796405U) +#define MUNIT_PRNG_INCREMENT (1729U) + +static munit_uint32_t munit_rand_next_state(munit_uint32_t state) { + return state * MUNIT_PRNG_MULTIPLIER + MUNIT_PRNG_INCREMENT; +} + +static munit_uint32_t munit_rand_from_state(munit_uint32_t state) { + munit_uint32_t res = ((state >> ((state >> 28) + 4)) ^ state) * (277803737U); + res ^= res >> 22; + return res; +} + +void munit_rand_seed(munit_uint32_t seed) { + munit_uint32_t state = munit_rand_next_state(seed + MUNIT_PRNG_INCREMENT); + munit_atomic_store(&munit_rand_state, state); +} + +static munit_uint32_t munit_rand_generate_seed(void) { + munit_uint32_t seed, state; +#if defined(MUNIT_ENABLE_TIMING) + struct PsnipClockTimespec wc = { + 0, + }; + + psnip_clock_get_time(PSNIP_CLOCK_TYPE_WALL, &wc); + seed = (munit_uint32_t)wc.nanoseconds; +#else + seed = (munit_uint32_t)time(NULL); +#endif + + state = munit_rand_next_state(seed + MUNIT_PRNG_INCREMENT); + return munit_rand_from_state(state); +} + +static munit_uint32_t munit_rand_state_uint32(munit_uint32_t *state) { + const munit_uint32_t old = *state; + *state = munit_rand_next_state(old); + return munit_rand_from_state(old); +} + +munit_uint32_t munit_rand_uint32(void) { + munit_uint32_t old, state; + + do { + old = munit_atomic_load(&munit_rand_state); + state = munit_rand_next_state(old); + } while (!munit_atomic_cas(&munit_rand_state, &old, state)); + + return munit_rand_from_state(old); +} + +static void munit_rand_state_memory(munit_uint32_t *state, size_t size, + munit_uint8_t *data) { + size_t members_remaining = size / sizeof(munit_uint32_t); + size_t bytes_remaining = size % sizeof(munit_uint32_t); + munit_uint8_t *b = data; + munit_uint32_t rv; + while (members_remaining-- > 0) { + rv = munit_rand_state_uint32(state); + memcpy(b, &rv, sizeof(munit_uint32_t)); + b += sizeof(munit_uint32_t); + } + if (bytes_remaining != 0) { + rv = munit_rand_state_uint32(state); + memcpy(b, &rv, bytes_remaining); + } +} + +void munit_rand_memory(size_t size, munit_uint8_t *data) { + munit_uint32_t old, state; + + do { + state = old = munit_atomic_load(&munit_rand_state); + munit_rand_state_memory(&state, size, data); + } while (!munit_atomic_cas(&munit_rand_state, &old, state)); +} + +static munit_uint32_t munit_rand_state_at_most(munit_uint32_t *state, + munit_uint32_t salt, + munit_uint32_t max) { + /* We want (UINT32_MAX + 1) % max, which in unsigned arithmetic is the same + * as (UINT32_MAX + 1 - max) % max = -max % max. We compute -max using not + * to avoid compiler warnings. + */ + const munit_uint32_t min = (~max + 1U) % max; + munit_uint32_t x; + + if (max == (~((munit_uint32_t)0U))) + return munit_rand_state_uint32(state) ^ salt; + + max++; + + do { + x = munit_rand_state_uint32(state) ^ salt; + } while (x < min); + + return x % max; +} + +static munit_uint32_t munit_rand_at_most(munit_uint32_t salt, + munit_uint32_t max) { + munit_uint32_t old, state; + munit_uint32_t retval; + + do { + state = old = munit_atomic_load(&munit_rand_state); + retval = munit_rand_state_at_most(&state, salt, max); + } while (!munit_atomic_cas(&munit_rand_state, &old, state)); + + return retval; +} + +int munit_rand_int_range(int min, int max) { + munit_uint64_t range = (munit_uint64_t)max - (munit_uint64_t)min; + + if (min > max) + return munit_rand_int_range(max, min); + + if (range > (~((munit_uint32_t)0U))) + range = (~((munit_uint32_t)0U)); + + return min + (int)munit_rand_at_most(0, (munit_uint32_t)range); +} + +double munit_rand_double(void) { + munit_uint32_t old, state; + double retval = 0.0; + + do { + state = old = munit_atomic_load(&munit_rand_state); + + /* See http://mumble.net/~campbell/tmp/random_real.c for how to do + * this right. Patches welcome if you feel that this is too + * biased. */ + retval = munit_rand_state_uint32(&state) / ((~((munit_uint32_t)0U)) + 1.0); + } while (!munit_atomic_cas(&munit_rand_state, &old, state)); + + return retval; +} + +/*** Test suite handling ***/ + +typedef struct { + unsigned int successful; + unsigned int skipped; + unsigned int failed; + unsigned int errored; +#if defined(MUNIT_ENABLE_TIMING) + munit_uint64_t cpu_clock; + munit_uint64_t wall_clock; +#endif +} MunitReport; + +typedef struct { + const char *prefix; + const MunitSuite *suite; + const char **tests; + munit_uint32_t seed; + unsigned int iterations; + MunitParameter *parameters; + munit_bool single_parameter_mode; + void *user_data; + MunitReport report; + munit_bool colorize; + munit_bool fork; + munit_bool show_stderr; + munit_bool fatal_failures; +} MunitTestRunner; + +const char *munit_parameters_get(const MunitParameter params[], + const char *key) { + const MunitParameter *param; + + for (param = params; param != NULL && param->name != NULL; param++) + if (strcmp(param->name, key) == 0) + return param->value; + return NULL; +} + +#if defined(MUNIT_ENABLE_TIMING) +static void munit_print_time(FILE *fp, munit_uint64_t nanoseconds) { + fprintf(fp, "%" MUNIT_TEST_TIME_FORMAT, + ((double)nanoseconds) / ((double)PSNIP_CLOCK_NSEC_PER_SEC)); +} +#endif + +/* Add a paramter to an array of parameters. */ +static MunitResult munit_parameters_add(size_t *params_size, + MunitParameter **params, char *name, + char *value) { + *params = realloc(*params, sizeof(MunitParameter) * (*params_size + 2)); + if (*params == NULL) + return MUNIT_ERROR; + + (*params)[*params_size].name = name; + (*params)[*params_size].value = value; + (*params_size)++; + (*params)[*params_size].name = NULL; + (*params)[*params_size].value = NULL; + + return MUNIT_OK; +} + +/* Concatenate two strings, but just return one of the components + * unaltered if the other is NULL or "". */ +static char *munit_maybe_concat(size_t *len, char *prefix, char *suffix) { + char *res; + size_t res_l; + const size_t prefix_l = prefix != NULL ? strlen(prefix) : 0; + const size_t suffix_l = suffix != NULL ? strlen(suffix) : 0; + if (prefix_l == 0 && suffix_l == 0) { + res = NULL; + res_l = 0; + } else if (prefix_l == 0 && suffix_l != 0) { + res = suffix; + res_l = suffix_l; + } else if (prefix_l != 0 && suffix_l == 0) { + res = prefix; + res_l = prefix_l; + } else { + res_l = prefix_l + suffix_l; + res = malloc(res_l + 1); + memcpy(res, prefix, prefix_l); + memcpy(res + prefix_l, suffix, suffix_l); + res[res_l] = 0; + } + + if (len != NULL) + *len = res_l; + + return res; +} + +/* Possbily free a string returned by munit_maybe_concat. */ +static void munit_maybe_free_concat(char *s, const char *prefix, + const char *suffix) { + if (prefix != s && suffix != s) + free(s); +} + +/* Cheap string hash function, just used to salt the PRNG. */ +static munit_uint32_t munit_str_hash(const char *name) { + const char *p; + munit_uint32_t h = 5381U; + + for (p = name; *p != '\0'; p++) + h = (munit_uint32_t)(h << 5) + h + (munit_uint32_t)*p; + + return h; +} + +static void munit_splice(int from, int to) { + munit_uint8_t buf[1024]; +#if !defined(_WIN32) + ssize_t len; + ssize_t bytes_written; + ssize_t write_res; +#else + int len; + int bytes_written; + int write_res; +#endif + do { + len = read(from, buf, sizeof(buf)); + if (len > 0) { + bytes_written = 0; + do { + write_res = write(to, buf + bytes_written, +#if !defined(_WIN32) + (size_t) +#else + (unsigned int) +#endif + (len - bytes_written)); + if (write_res < 0) + break; + bytes_written += write_res; + } while (bytes_written < len); + } else + break; + } while (1); +} + +/* This is the part that should be handled in the child process */ +static MunitResult munit_test_runner_exec(MunitTestRunner *runner, + const MunitTest *test, + const MunitParameter params[], + MunitReport *report) { + unsigned int iterations = runner->iterations; + MunitResult result = MUNIT_FAIL; +#if defined(MUNIT_ENABLE_TIMING) + struct PsnipClockTimespec wall_clock_begin = + { + 0, + }, + wall_clock_end = { + 0, + }; + struct PsnipClockTimespec cpu_clock_begin = + { + 0, + }, + cpu_clock_end = { + 0, + }; +#endif + unsigned int i = 0; + + if ((test->options & MUNIT_TEST_OPTION_SINGLE_ITERATION) == + MUNIT_TEST_OPTION_SINGLE_ITERATION) + iterations = 1; + else if (iterations == 0) + iterations = runner->suite->iterations; + + munit_rand_seed(runner->seed); + + do { + void *data = (test->setup == NULL) ? runner->user_data + : test->setup(params, runner->user_data); + +#if defined(MUNIT_ENABLE_TIMING) + psnip_clock_get_time(PSNIP_CLOCK_TYPE_WALL, &wall_clock_begin); + psnip_clock_get_time(PSNIP_CLOCK_TYPE_CPU, &cpu_clock_begin); +#endif + + result = test->test(params, data); + +#if defined(MUNIT_ENABLE_TIMING) + psnip_clock_get_time(PSNIP_CLOCK_TYPE_WALL, &wall_clock_end); + psnip_clock_get_time(PSNIP_CLOCK_TYPE_CPU, &cpu_clock_end); +#endif + + if (test->tear_down != NULL) + test->tear_down(data); + + if (MUNIT_LIKELY(result == MUNIT_OK)) { + report->successful++; +#if defined(MUNIT_ENABLE_TIMING) + report->wall_clock += + munit_clock_get_elapsed(&wall_clock_begin, &wall_clock_end); + report->cpu_clock += + munit_clock_get_elapsed(&cpu_clock_begin, &cpu_clock_end); +#endif + } else { + switch ((int)result) { + case MUNIT_SKIP: + report->skipped++; + break; + case MUNIT_FAIL: + report->failed++; + break; + case MUNIT_ERROR: + report->errored++; + break; + default: + break; + } + break; + } + } while (++i < iterations); + + return result; +} + +#if defined(MUNIT_EMOTICON) +# define MUNIT_RESULT_STRING_OK ":)" +# define MUNIT_RESULT_STRING_SKIP ":|" +# define MUNIT_RESULT_STRING_FAIL ":(" +# define MUNIT_RESULT_STRING_ERROR ":o" +# define MUNIT_RESULT_STRING_TODO ":/" +#else +# define MUNIT_RESULT_STRING_OK "OK " +# define MUNIT_RESULT_STRING_SKIP "SKIP " +# define MUNIT_RESULT_STRING_FAIL "FAIL " +# define MUNIT_RESULT_STRING_ERROR "ERROR" +# define MUNIT_RESULT_STRING_TODO "TODO " +#endif + +static void munit_test_runner_print_color(const MunitTestRunner *runner, + const char *string, char color) { + if (runner->colorize) + fprintf(MUNIT_OUTPUT_FILE, "\x1b[3%cm%s\x1b[39m", color, string); + else + fputs(string, MUNIT_OUTPUT_FILE); +} + +#if !defined(MUNIT_NO_BUFFER) +static int munit_replace_stderr(FILE *stderr_buf) { + if (stderr_buf != NULL) { + const int orig_stderr = dup(STDERR_FILENO); + + int errfd = fileno(stderr_buf); + if (MUNIT_UNLIKELY(errfd == -1)) { + exit(EXIT_FAILURE); + } + + dup2(errfd, STDERR_FILENO); + + return orig_stderr; + } + + return -1; +} + +static void munit_restore_stderr(int orig_stderr) { + if (orig_stderr != -1) { + dup2(orig_stderr, STDERR_FILENO); + close(orig_stderr); + } +} +#endif /* !defined(MUNIT_NO_BUFFER) */ + +/* Run a test with the specified parameters. */ +static void +munit_test_runner_run_test_with_params(MunitTestRunner *runner, + const MunitTest *test, + const MunitParameter params[]) { + MunitResult result = MUNIT_OK; + MunitReport report = {0, 0, 0, 0, +#if defined(MUNIT_ENABLE_TIMING) + 0, 0 +#endif + }; + unsigned int output_l; + munit_bool first; + const MunitParameter *param; + FILE *stderr_buf; +#if !defined(MUNIT_NO_FORK) + int pipefd[2]; + pid_t fork_pid; + ssize_t bytes_written = 0; + ssize_t write_res; + ssize_t bytes_read = 0; + ssize_t read_res; + int status = 0; + pid_t changed_pid; +#endif + + if (params != NULL) { + output_l = 2; + fputs(" ", MUNIT_OUTPUT_FILE); + first = 1; + for (param = params; param != NULL && param->name != NULL; param++) { + if (!first) { + fputs(", ", MUNIT_OUTPUT_FILE); + output_l += 2; + } else { + first = 0; + } + + output_l += (unsigned int)fprintf(MUNIT_OUTPUT_FILE, "%s=%s", param->name, + param->value); + } + while (output_l++ < MUNIT_TEST_NAME_LEN) { + fputc(' ', MUNIT_OUTPUT_FILE); + } + } + + fflush(MUNIT_OUTPUT_FILE); + + stderr_buf = NULL; +#if !defined(_WIN32) || defined(__MINGW32__) + stderr_buf = tmpfile(); +#else + tmpfile_s(&stderr_buf); +#endif + if (stderr_buf == NULL) { + munit_log_errno(MUNIT_LOG_ERROR, stderr, + "unable to create buffer for stderr"); + result = MUNIT_ERROR; + goto print_result; + } + +#if !defined(MUNIT_NO_FORK) + if (runner->fork) { + pipefd[0] = -1; + pipefd[1] = -1; + if (pipe(pipefd) != 0) { + munit_log_errno(MUNIT_LOG_ERROR, stderr, "unable to create pipe"); + result = MUNIT_ERROR; + goto print_result; + } + + fork_pid = fork(); + if (fork_pid == 0) { + int orig_stderr; + + close(pipefd[0]); + + orig_stderr = munit_replace_stderr(stderr_buf); + munit_test_runner_exec(runner, test, params, &report); + + /* Note that we don't restore stderr. This is so we can buffer + * things written to stderr later on (such as by + * asan/tsan/ubsan, valgrind, etc.) */ + close(orig_stderr); + + do { + write_res = + write(pipefd[1], ((munit_uint8_t *)(&report)) + bytes_written, + sizeof(report) - (size_t)bytes_written); + if (write_res < 0) { + if (stderr_buf != NULL) { + munit_log_errno(MUNIT_LOG_ERROR, stderr, "unable to write to pipe"); + } + exit(EXIT_FAILURE); + } + bytes_written += write_res; + } while ((size_t)bytes_written < sizeof(report)); + + if (stderr_buf != NULL) + fclose(stderr_buf); + close(pipefd[1]); + + exit(EXIT_SUCCESS); + } else if (fork_pid == -1) { + close(pipefd[0]); + close(pipefd[1]); + if (stderr_buf != NULL) { + munit_log_errno(MUNIT_LOG_ERROR, stderr, "unable to fork"); + } + report.errored++; + result = MUNIT_ERROR; + } else { + close(pipefd[1]); + do { + read_res = read(pipefd[0], ((munit_uint8_t *)(&report)) + bytes_read, + sizeof(report) - (size_t)bytes_read); + if (read_res < 1) + break; + bytes_read += read_res; + } while (bytes_read < (ssize_t)sizeof(report)); + + changed_pid = waitpid(fork_pid, &status, 0); + + if (MUNIT_LIKELY(changed_pid == fork_pid) && + MUNIT_LIKELY(WIFEXITED(status))) { + if (bytes_read != sizeof(report)) { + munit_logf_internal(MUNIT_LOG_ERROR, stderr_buf, + "child exited unexpectedly with status %d", + WEXITSTATUS(status)); + report.errored++; + } else if (WEXITSTATUS(status) != EXIT_SUCCESS) { + munit_logf_internal(MUNIT_LOG_ERROR, stderr_buf, + "child exited with status %d", + WEXITSTATUS(status)); + report.errored++; + } + } else { + if (WIFSIGNALED(status)) { +# if defined(_XOPEN_VERSION) && (_XOPEN_VERSION >= 700) + munit_logf_internal(MUNIT_LOG_ERROR, stderr_buf, + "child killed by signal %d (%s)", + WTERMSIG(status), strsignal(WTERMSIG(status))); +# else + munit_logf_internal(MUNIT_LOG_ERROR, stderr_buf, + "child killed by signal %d", WTERMSIG(status)); +# endif + } else if (WIFSTOPPED(status)) { + munit_logf_internal(MUNIT_LOG_ERROR, stderr_buf, + "child stopped by signal %d", WSTOPSIG(status)); + } + report.errored++; + } + + close(pipefd[0]); + waitpid(fork_pid, NULL, 0); + } + } else +#endif + { +#if !defined(MUNIT_NO_BUFFER) + const volatile int orig_stderr = munit_replace_stderr(stderr_buf); +#endif + +#if defined(MUNIT_THREAD_LOCAL) + if (MUNIT_UNLIKELY(setjmp(munit_error_jmp_buf) != 0)) { + result = MUNIT_FAIL; + report.failed++; + } else { + munit_error_jmp_buf_valid = 1; + result = munit_test_runner_exec(runner, test, params, &report); + } +#else + result = munit_test_runner_exec(runner, test, params, &report); +#endif + +#if !defined(MUNIT_NO_BUFFER) + munit_restore_stderr(orig_stderr); +#endif + + /* Here just so that the label is used on Windows and we don't get + * a warning */ + goto print_result; + } + +print_result: + + fputs("[ ", MUNIT_OUTPUT_FILE); + if ((test->options & MUNIT_TEST_OPTION_TODO) == MUNIT_TEST_OPTION_TODO) { + if (report.failed != 0 || report.errored != 0 || report.skipped != 0) { + munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_TODO, '3'); + result = MUNIT_OK; + } else { + munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_ERROR, '1'); + if (MUNIT_LIKELY(stderr_buf != NULL)) + munit_log_internal(MUNIT_LOG_ERROR, stderr_buf, + "Test marked TODO, but was successful."); + runner->report.failed++; + result = MUNIT_ERROR; + } + } else if (report.failed > 0) { + munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_FAIL, '1'); + runner->report.failed++; + result = MUNIT_FAIL; + } else if (report.errored > 0) { + munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_ERROR, '1'); + runner->report.errored++; + result = MUNIT_ERROR; + } else if (report.skipped > 0) { + munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_SKIP, '3'); + runner->report.skipped++; + result = MUNIT_SKIP; + } else if (report.successful > 1) { + munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_OK, '2'); +#if defined(MUNIT_ENABLE_TIMING) + fputs(" ] [ ", MUNIT_OUTPUT_FILE); + munit_print_time(MUNIT_OUTPUT_FILE, report.wall_clock / report.successful); + fputs(" / ", MUNIT_OUTPUT_FILE); + munit_print_time(MUNIT_OUTPUT_FILE, report.cpu_clock / report.successful); + fprintf(MUNIT_OUTPUT_FILE, + " CPU ]\n %-" MUNIT_XSTRINGIFY(MUNIT_TEST_NAME_LEN) "s Total: [ ", + ""); + munit_print_time(MUNIT_OUTPUT_FILE, report.wall_clock); + fputs(" / ", MUNIT_OUTPUT_FILE); + munit_print_time(MUNIT_OUTPUT_FILE, report.cpu_clock); + fputs(" CPU", MUNIT_OUTPUT_FILE); +#endif + runner->report.successful++; + result = MUNIT_OK; + } else if (report.successful > 0) { + munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_OK, '2'); +#if defined(MUNIT_ENABLE_TIMING) + fputs(" ] [ ", MUNIT_OUTPUT_FILE); + munit_print_time(MUNIT_OUTPUT_FILE, report.wall_clock); + fputs(" / ", MUNIT_OUTPUT_FILE); + munit_print_time(MUNIT_OUTPUT_FILE, report.cpu_clock); + fputs(" CPU", MUNIT_OUTPUT_FILE); +#endif + runner->report.successful++; + result = MUNIT_OK; + } + fputs(" ]\n", MUNIT_OUTPUT_FILE); + + if (stderr_buf != NULL) { + if (result == MUNIT_FAIL || result == MUNIT_ERROR || runner->show_stderr) { + fflush(MUNIT_OUTPUT_FILE); + + rewind(stderr_buf); + munit_splice(fileno(stderr_buf), STDERR_FILENO); + + fflush(stderr); + } + + fclose(stderr_buf); + } +} + +static void munit_test_runner_run_test_wild(MunitTestRunner *runner, + const MunitTest *test, + const char *test_name, + MunitParameter *params, + MunitParameter *p) { + const MunitParameterEnum *pe; + char **values; + MunitParameter *next; + + for (pe = test->parameters; pe != NULL && pe->name != NULL; pe++) { + if (p->name == pe->name) + break; + } + + if (pe == NULL) + return; + + for (values = pe->values; *values != NULL; values++) { + next = p + 1; + p->value = *values; + if (next->name == NULL) { + munit_test_runner_run_test_with_params(runner, test, params); + } else { + munit_test_runner_run_test_wild(runner, test, test_name, params, next); + } + if (runner->fatal_failures && + (runner->report.failed != 0 || runner->report.errored != 0)) + break; + } +} + +/* Run a single test, with every combination of parameters + * requested. */ +static void munit_test_runner_run_test(MunitTestRunner *runner, + const MunitTest *test, + const char *prefix) { + char *test_name = + munit_maybe_concat(NULL, (char *)prefix, (char *)test->name); + /* The array of parameters to pass to + * munit_test_runner_run_test_with_params */ + MunitParameter *params = NULL; + size_t params_l = 0; + /* Wildcard parameters are parameters which have possible values + * specified in the test, but no specific value was passed to the + * CLI. That means we want to run the test once for every + * possible combination of parameter values or, if --single was + * passed to the CLI, a single time with a random set of + * parameters. */ + MunitParameter *wild_params = NULL; + size_t wild_params_l = 0; + const MunitParameterEnum *pe; + const MunitParameter *cli_p; + munit_bool filled; + unsigned int possible; + char **vals; + size_t first_wild; + const MunitParameter *wp; + int pidx; + + munit_rand_seed(runner->seed); + + fprintf(MUNIT_OUTPUT_FILE, "%-" MUNIT_XSTRINGIFY(MUNIT_TEST_NAME_LEN) "s", + test_name); + + if (test->parameters == NULL) { + /* No parameters. Simple, nice. */ + munit_test_runner_run_test_with_params(runner, test, NULL); + } else { + fputc('\n', MUNIT_OUTPUT_FILE); + + for (pe = test->parameters; pe != NULL && pe->name != NULL; pe++) { + /* Did we received a value for this parameter from the CLI? */ + filled = 0; + for (cli_p = runner->parameters; cli_p != NULL && cli_p->name != NULL; + cli_p++) { + if (strcmp(cli_p->name, pe->name) == 0) { + if (MUNIT_UNLIKELY(munit_parameters_add(¶ms_l, ¶ms, pe->name, + cli_p->value) != MUNIT_OK)) + goto cleanup; + filled = 1; + break; + } + } + if (filled) + continue; + + /* Nothing from CLI, is the enum NULL/empty? We're not a + * fuzzer… */ + if (pe->values == NULL || pe->values[0] == NULL) + continue; + + /* If --single was passed to the CLI, choose a value from the + * list of possibilities randomly. */ + if (runner->single_parameter_mode) { + possible = 0; + for (vals = pe->values; *vals != NULL; vals++) + possible++; + /* We want the tests to be reproducible, even if you're only + * running a single test, but we don't want every test with + * the same number of parameters to choose the same parameter + * number, so use the test name as a primitive salt. */ + pidx = (int)munit_rand_at_most(munit_str_hash(test_name), possible - 1); + if (MUNIT_UNLIKELY(munit_parameters_add(¶ms_l, ¶ms, pe->name, + pe->values[pidx]) != MUNIT_OK)) + goto cleanup; + } else { + /* We want to try every permutation. Put in a placeholder + * entry, we'll iterate through them later. */ + if (MUNIT_UNLIKELY(munit_parameters_add(&wild_params_l, &wild_params, + pe->name, NULL) != MUNIT_OK)) + goto cleanup; + } + } + + if (wild_params_l != 0) { + first_wild = params_l; + for (wp = wild_params; wp != NULL && wp->name != NULL; wp++) { + for (pe = test->parameters; + pe != NULL && pe->name != NULL && pe->values != NULL; pe++) { + if (strcmp(wp->name, pe->name) == 0) { + if (MUNIT_UNLIKELY(munit_parameters_add(¶ms_l, ¶ms, + pe->name, + pe->values[0]) != MUNIT_OK)) + goto cleanup; + } + } + } + + munit_test_runner_run_test_wild(runner, test, test_name, params, + params + first_wild); + } else { + munit_test_runner_run_test_with_params(runner, test, params); + } + + cleanup: + free(params); + free(wild_params); + } + + munit_maybe_free_concat(test_name, prefix, test->name); +} + +/* Recurse through the suite and run all the tests. If a list of + * tests to run was provied on the command line, run only those + * tests. */ +static void munit_test_runner_run_suite(MunitTestRunner *runner, + const MunitSuite *suite, + const char *prefix) { + size_t pre_l; + char *pre = munit_maybe_concat(&pre_l, (char *)prefix, (char *)suite->prefix); + const MunitTest *test; + const char **test_name; + const MunitSuite *child_suite; + + /* Run the tests. */ + for (test = suite->tests; test != NULL && test->test != NULL; test++) { + if (runner->tests != NULL) { /* Specific tests were requested on the CLI */ + for (test_name = runner->tests; test_name != NULL && *test_name != NULL; + test_name++) { + if ((pre_l == 0 || strncmp(pre, *test_name, pre_l) == 0) && + strncmp(test->name, *test_name + pre_l, + strlen(*test_name + pre_l)) == 0) { + munit_test_runner_run_test(runner, test, pre); + if (runner->fatal_failures && + (runner->report.failed != 0 || runner->report.errored != 0)) + goto cleanup; + } + } + } else { /* Run all tests */ + munit_test_runner_run_test(runner, test, pre); + } + } + + if (runner->fatal_failures && + (runner->report.failed != 0 || runner->report.errored != 0)) + goto cleanup; + + /* Run any child suites. */ + for (child_suite = suite->suites; + child_suite != NULL && child_suite->prefix != NULL; child_suite++) { + munit_test_runner_run_suite(runner, child_suite, pre); + } + +cleanup: + + munit_maybe_free_concat(pre, prefix, suite->prefix); +} + +static void munit_test_runner_run(MunitTestRunner *runner) { + munit_test_runner_run_suite(runner, runner->suite, NULL); +} + +static void munit_print_help(int argc, char *const *argv, void *user_data, + const MunitArgument arguments[]) { + const MunitArgument *arg; + (void)argc; + + printf("USAGE: %s [OPTIONS...] [TEST...]\n\n", argv[0]); + puts( + " --seed SEED\n" + " Value used to seed the PRNG. Must be a 32-bit integer in " + "decimal\n" + " notation with no separators (commas, decimals, spaces, " + "etc.), or\n" + " hexidecimal prefixed by \"0x\".\n" + " --iterations N\n" + " Run each test N times. 0 means the default number.\n" + " --param name value\n" + " A parameter key/value pair which will be passed to any test " + "with\n" + " takes a parameter of that name. If not provided, the test " + "will be\n" + " run once for each possible parameter value.\n" + " --list Write a list of all available tests.\n" + " --list-params\n" + " Write a list of all available tests and their possible " + "parameters.\n" + " --single Run each parameterized test in a single configuration " + "instead of\n" + " every possible combination\n" + " --log-visible debug|info|warning|error\n" + " --log-fatal debug|info|warning|error\n" + " Set the level at which messages of different severities are " + "visible,\n" + " or cause the test to terminate.\n" +#if !defined(MUNIT_NO_FORK) + " --no-fork Do not execute tests in a child process. If this option is " + "supplied\n" + " and a test crashes (including by failing an assertion), no " + "further\n" + " tests will be performed.\n" +#endif + " --fatal-failures\n" + " Stop executing tests as soon as a failure is found.\n" + " --show-stderr\n" + " Show data written to stderr by the tests, even if the test " + "succeeds.\n" + " --color auto|always|never\n" + " Colorize (or don't) the output.\n" + /* 12345678901234567890123456789012345678901234567890123456789012345678901234567890 + */ + " --help Print this help message and exit.\n"); +#if defined(MUNIT_NL_LANGINFO) + setlocale(LC_ALL, ""); + fputs((strcasecmp("UTF-8", nl_langinfo(CODESET)) == 0) ? "µnit" : "munit", + stdout); +#else + puts("munit"); +#endif + printf(" %d.%d.%d\n" + "Full documentation at: https://nemequ.github.io/munit/\n", + (MUNIT_CURRENT_VERSION >> 16) & 0xff, + (MUNIT_CURRENT_VERSION >> 8) & 0xff, + (MUNIT_CURRENT_VERSION >> 0) & 0xff); + for (arg = arguments; arg != NULL && arg->name != NULL; arg++) + arg->write_help(arg, user_data); +} + +static const MunitArgument * +munit_arguments_find(const MunitArgument arguments[], const char *name) { + const MunitArgument *arg; + + for (arg = arguments; arg != NULL && arg->name != NULL; arg++) + if (strcmp(arg->name, name) == 0) + return arg; + + return NULL; +} + +static void munit_suite_list_tests(const MunitSuite *suite, + munit_bool show_params, const char *prefix) { + size_t pre_l; + char *pre = munit_maybe_concat(&pre_l, (char *)prefix, (char *)suite->prefix); + const MunitTest *test; + const MunitParameterEnum *params; + munit_bool first; + char **val; + const MunitSuite *child_suite; + + for (test = suite->tests; test != NULL && test->name != NULL; test++) { + if (pre != NULL) + fputs(pre, stdout); + puts(test->name); + + if (show_params) { + for (params = test->parameters; params != NULL && params->name != NULL; + params++) { + fprintf(stdout, " - %s: ", params->name); + if (params->values == NULL) { + puts("Any"); + } else { + first = 1; + for (val = params->values; *val != NULL; val++) { + if (!first) { + fputs(", ", stdout); + } else { + first = 0; + } + fputs(*val, stdout); + } + putc('\n', stdout); + } + } + } + } + + for (child_suite = suite->suites; + child_suite != NULL && child_suite->prefix != NULL; child_suite++) { + munit_suite_list_tests(child_suite, show_params, pre); + } + + munit_maybe_free_concat(pre, prefix, suite->prefix); +} + +static munit_bool munit_stream_supports_ansi(FILE *stream) { +#if !defined(_WIN32) + return isatty(fileno(stream)); +#else + +# if !defined(__MINGW32__) + size_t ansicon_size = 0; +# endif + + if (isatty(fileno(stream))) { +# if !defined(__MINGW32__) + getenv_s(&ansicon_size, NULL, 0, "ANSICON"); + return ansicon_size != 0; +# else + return getenv("ANSICON") != NULL; +# endif + } + return 0; +#endif +} + +int munit_suite_main_custom(const MunitSuite *suite, void *user_data, int argc, + char *const *argv, + const MunitArgument arguments[]) { + int result = EXIT_FAILURE; + MunitTestRunner runner; + size_t parameters_size = 0; + size_t tests_size = 0; + int arg; + + char *envptr; + unsigned long ts; + char *endptr; + unsigned long long iterations; + MunitLogLevel level; + const MunitArgument *argument; + const char **runner_tests; + unsigned int tests_run; + unsigned int tests_total; + + runner.prefix = NULL; + runner.suite = NULL; + runner.tests = NULL; + runner.seed = 0; + runner.iterations = 0; + runner.parameters = NULL; + runner.single_parameter_mode = 0; + runner.user_data = NULL; + + runner.report.successful = 0; + runner.report.skipped = 0; + runner.report.failed = 0; + runner.report.errored = 0; +#if defined(MUNIT_ENABLE_TIMING) + runner.report.cpu_clock = 0; + runner.report.wall_clock = 0; +#endif + + runner.colorize = 0; +#if !defined(_WIN32) + runner.fork = 1; +#else + runner.fork = 0; +#endif + runner.show_stderr = 0; + runner.fatal_failures = 0; + runner.suite = suite; + runner.user_data = user_data; + runner.seed = munit_rand_generate_seed(); + runner.colorize = munit_stream_supports_ansi(MUNIT_OUTPUT_FILE); + + for (arg = 1; arg < argc; arg++) { + if (strncmp("--", argv[arg], 2) == 0) { + if (strcmp("seed", argv[arg] + 2) == 0) { + if (arg + 1 >= argc) { + munit_logf_internal(MUNIT_LOG_ERROR, stderr, + "%s requires an argument", argv[arg]); + goto cleanup; + } + + envptr = argv[arg + 1]; + ts = strtoul(argv[arg + 1], &envptr, 0); + if (*envptr != '\0' || ts > (~((munit_uint32_t)0U))) { + munit_logf_internal(MUNIT_LOG_ERROR, stderr, + "invalid value ('%s') passed to %s", + argv[arg + 1], argv[arg]); + goto cleanup; + } + runner.seed = (munit_uint32_t)ts; + + arg++; + } else if (strcmp("iterations", argv[arg] + 2) == 0) { + if (arg + 1 >= argc) { + munit_logf_internal(MUNIT_LOG_ERROR, stderr, + "%s requires an argument", argv[arg]); + goto cleanup; + } + + endptr = argv[arg + 1]; + iterations = strtoul(argv[arg + 1], &endptr, 0); + if (*endptr != '\0' || iterations > UINT_MAX) { + munit_logf_internal(MUNIT_LOG_ERROR, stderr, + "invalid value ('%s') passed to %s", + argv[arg + 1], argv[arg]); + goto cleanup; + } + + runner.iterations = (unsigned int)iterations; + + arg++; + } else if (strcmp("param", argv[arg] + 2) == 0) { + if (arg + 2 >= argc) { + munit_logf_internal(MUNIT_LOG_ERROR, stderr, + "%s requires two arguments", argv[arg]); + goto cleanup; + } + + runner.parameters = realloc(runner.parameters, sizeof(MunitParameter) * + (parameters_size + 2)); + if (runner.parameters == NULL) { + munit_log_internal(MUNIT_LOG_ERROR, stderr, + "failed to allocate memory"); + goto cleanup; + } + runner.parameters[parameters_size].name = (char *)argv[arg + 1]; + runner.parameters[parameters_size].value = (char *)argv[arg + 2]; + parameters_size++; + runner.parameters[parameters_size].name = NULL; + runner.parameters[parameters_size].value = NULL; + arg += 2; + } else if (strcmp("color", argv[arg] + 2) == 0) { + if (arg + 1 >= argc) { + munit_logf_internal(MUNIT_LOG_ERROR, stderr, + "%s requires an argument", argv[arg]); + goto cleanup; + } + + if (strcmp(argv[arg + 1], "always") == 0) + runner.colorize = 1; + else if (strcmp(argv[arg + 1], "never") == 0) + runner.colorize = 0; + else if (strcmp(argv[arg + 1], "auto") == 0) + runner.colorize = munit_stream_supports_ansi(MUNIT_OUTPUT_FILE); + else { + munit_logf_internal(MUNIT_LOG_ERROR, stderr, + "invalid value ('%s') passed to %s", + argv[arg + 1], argv[arg]); + goto cleanup; + } + + arg++; + } else if (strcmp("help", argv[arg] + 2) == 0) { + munit_print_help(argc, argv, user_data, arguments); + result = EXIT_SUCCESS; + goto cleanup; + } else if (strcmp("single", argv[arg] + 2) == 0) { + runner.single_parameter_mode = 1; + } else if (strcmp("show-stderr", argv[arg] + 2) == 0) { + runner.show_stderr = 1; +#if !defined(_WIN32) + } else if (strcmp("no-fork", argv[arg] + 2) == 0) { + runner.fork = 0; +#endif + } else if (strcmp("fatal-failures", argv[arg] + 2) == 0) { + runner.fatal_failures = 1; + } else if (strcmp("log-visible", argv[arg] + 2) == 0 || + strcmp("log-fatal", argv[arg] + 2) == 0) { + if (arg + 1 >= argc) { + munit_logf_internal(MUNIT_LOG_ERROR, stderr, + "%s requires an argument", argv[arg]); + goto cleanup; + } + + if (strcmp(argv[arg + 1], "debug") == 0) + level = MUNIT_LOG_DEBUG; + else if (strcmp(argv[arg + 1], "info") == 0) + level = MUNIT_LOG_INFO; + else if (strcmp(argv[arg + 1], "warning") == 0) + level = MUNIT_LOG_WARNING; + else if (strcmp(argv[arg + 1], "error") == 0) + level = MUNIT_LOG_ERROR; + else { + munit_logf_internal(MUNIT_LOG_ERROR, stderr, + "invalid value ('%s') passed to %s", + argv[arg + 1], argv[arg]); + goto cleanup; + } + + if (strcmp("log-visible", argv[arg] + 2) == 0) + munit_log_level_visible = level; + else + munit_log_level_fatal = level; + + arg++; + } else if (strcmp("list", argv[arg] + 2) == 0) { + munit_suite_list_tests(suite, 0, NULL); + result = EXIT_SUCCESS; + goto cleanup; + } else if (strcmp("list-params", argv[arg] + 2) == 0) { + munit_suite_list_tests(suite, 1, NULL); + result = EXIT_SUCCESS; + goto cleanup; + } else { + argument = munit_arguments_find(arguments, argv[arg] + 2); + if (argument == NULL) { + munit_logf_internal(MUNIT_LOG_ERROR, stderr, + "unknown argument ('%s')", argv[arg]); + goto cleanup; + } + + if (!argument->parse_argument(suite, user_data, &arg, argc, argv)) + goto cleanup; + } + } else { + runner_tests = + realloc((void *)runner.tests, sizeof(char *) * (tests_size + 2)); + if (runner_tests == NULL) { + munit_log_internal(MUNIT_LOG_ERROR, stderr, + "failed to allocate memory"); + goto cleanup; + } + runner.tests = runner_tests; + runner.tests[tests_size++] = argv[arg]; + runner.tests[tests_size] = NULL; + } + } + + fflush(stderr); + fprintf(MUNIT_OUTPUT_FILE, + "Running test suite with seed 0x%08" PRIx32 "...\n", runner.seed); + + munit_test_runner_run(&runner); + + tests_run = + runner.report.successful + runner.report.failed + runner.report.errored; + tests_total = tests_run + runner.report.skipped; + if (tests_run == 0) { + fprintf(stderr, "No tests run, %d (100%%) skipped.\n", + runner.report.skipped); + } else { + fprintf(MUNIT_OUTPUT_FILE, + "%d of %d (%0.0f%%) tests successful, %d (%0.0f%%) test skipped.\n", + runner.report.successful, tests_run, + (((double)runner.report.successful) / ((double)tests_run)) * 100.0, + runner.report.skipped, + (((double)runner.report.skipped) / ((double)tests_total)) * 100.0); + } + + if (runner.report.failed == 0 && runner.report.errored == 0) { + result = EXIT_SUCCESS; + } + +cleanup: + free(runner.parameters); + free((void *)runner.tests); + + return result; +} + +int munit_suite_main(const MunitSuite *suite, void *user_data, int argc, + char *const *argv) { + return munit_suite_main_custom(suite, user_data, argc, argv, NULL); +} + +static uint8_t hexchars[] = "0123456789abcdef"; + +static uint8_t *hexdump_addr(uint8_t *dest, size_t addr) { + size_t i; + uint8_t a; + + for (i = 0; i < 4; ++i) { + a = (addr >> (3 - i) * 8) & 0xff; + + *dest++ = hexchars[a >> 4]; + *dest++ = hexchars[a & 0xf]; + } + + return dest; +} + +static uint8_t *asciidump(uint8_t *dest, const uint8_t *data, size_t datalen) { + size_t i; + + *dest++ = '|'; + + for (i = 0; i < datalen; ++i) { + if (0x20 <= data[i] && data[i] <= 0x7e) { + *dest++ = data[i]; + } else { + *dest++ = '.'; + } + } + + *dest++ = '|'; + + return dest; +} + +static uint8_t *hexdump8(uint8_t *dest, const uint8_t *data, size_t datalen) { + size_t i; + + for (i = 0; i < datalen; ++i) { + *dest++ = hexchars[data[i] >> 4]; + *dest++ = hexchars[data[i] & 0xf]; + *dest++ = ' '; + } + + for (; i < 8; ++i) { + *dest++ = ' '; + *dest++ = ' '; + *dest++ = ' '; + } + + return dest; +} + +static uint8_t *hexdump16(uint8_t *dest, const uint8_t *data, size_t datalen) { + dest = hexdump8(dest, data, datalen < 8 ? datalen : 8); + *dest++ = ' '; + + if (datalen < 8) { + data = NULL; + datalen = 0; + } else { + data += 8; + datalen -= 8; + } + + dest = hexdump8(dest, data, datalen); + *dest++ = ' '; + + return dest; +} + +static uint8_t *hexdump_line(uint8_t *dest, const uint8_t *data, size_t datalen, + size_t addr) { + dest = hexdump_addr(dest, addr); + *dest++ = ' '; + *dest++ = ' '; + + dest = hexdump16(dest, data, datalen); + + dest = asciidump(dest, data, datalen); + + return dest; +} + +int munit_hexdump(FILE *fp, const void *data, size_t datalen) { + size_t offset = 0, n, len; + uint8_t buf[128], *p; + const uint8_t *s; + int repeated = 0; + + if (datalen == 0) { + return 0; + } + + for (; offset < datalen; offset += 16) { + n = datalen - offset; + s = (const uint8_t *)data + offset; + + if (n >= 16) { + n = 16; + + if (offset > 0) { + if (memcmp(s - 16, s, 16) == 0) { + if (repeated) { + continue; + } + + repeated = 1; + + if (fwrite("*\n", 1, 2, fp) < 2) { + return -1; + } + + continue; + } + + repeated = 0; + } + } + + p = hexdump_line(buf, s, n, offset); + *p++ = '\n'; + + len = (size_t)(p - buf); + + if (fwrite(buf, 1, len, fp) < len) { + return -1; + } + } + + p = hexdump_addr(buf, datalen); + *p++ = '\n'; + + len = (size_t)(p - buf); + + if (fwrite(buf, 1, len, fp) < len) { + return -1; + } + + return 0; +} + +int munit_hexdump_diff(FILE *fp, const void *a, size_t alen, const void *b, + size_t blen) { + size_t offset = 0, k, i, len, ncomp, maxlen, adoff = 0; + uint8_t buf[128], *p; + const uint8_t mk[2] = {'-', '+'}; + struct datasource { + const uint8_t *data; + size_t datalen; + const uint8_t *s; + size_t n; + } ds[] = {{a, alen, NULL, 0}, {b, blen, NULL, 0}}, *dp; + + maxlen = alen < blen ? blen : alen; + + for (; offset < maxlen; offset += 16) { + for (k = 0; k < 2; ++k) { + dp = &ds[k]; + + if (offset < dp->datalen) { + dp->s = (const uint8_t *)dp->data + offset; + dp->n = dp->datalen - offset; + + if (dp->n > 16) { + dp->n = 16; + } + } else { + dp->s = NULL; + dp->n = 0; + } + } + + if (ds[0].n == ds[1].n && memcmp(ds[0].s, ds[1].s, ds[0].n) == 0) { + continue; + } + + for (k = 0; k < 2; ++k) { + dp = &ds[k]; + + if (!dp->n) { + continue; + } + + p = buf; + *p++ = mk[k]; + *p++ = mk[k]; + *p++ = mk[k]; + *p++ = mk[k]; + + p = hexdump_line(p, dp->s, dp->n, offset); + *p++ = '\n'; + + len = (size_t)(p - buf); + + if (fwrite(buf, 1, len, fp) < len) { + return -1; + } + } + + if (!ds[0].n || !ds[1].n) { + continue; + } + + ncomp = ds[0].n < ds[1].n ? ds[0].n : ds[1].n; + + p = buf + 4 + 10; + + memset(buf, ' ', 4 + 78); + + for (i = 0; i < ncomp; ++i) { + if (ds[0].s[i] == ds[1].s[i]) { + *p++ = ' '; + *p++ = ' '; + } else { + adoff = 4 + 10 + 51 + i; + *(buf + adoff) = '^'; + + *p++ = '^'; + *p++ = '^'; + } + + *p++ = ' '; + + if (i == 7) { + *p++ = ' '; + } + } + + if (adoff) { + len = adoff + 1; + } else { + len = (size_t)(p - buf); + } + + buf[len++] = '\n'; + + if (fwrite(buf, 1, len, fp) < len) { + return -1; + } + } + + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/munit.h b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/munit.h new file mode 100644 index 00000000000000..e78a4801216b37 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/munit.h @@ -0,0 +1,574 @@ +/* µnit Testing Framework + * Copyright (c) 2013-2017 Evan Nemerson + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef MUNIT_H +#define MUNIT_H + +#include +#include +#include +#include + +#define MUNIT_VERSION(major, minor, revision) \ + (((major) << 16) | ((minor) << 8) | (revision)) + +#define MUNIT_CURRENT_VERSION MUNIT_VERSION(0, 4, 1) + +#if defined(_MSC_VER) && (_MSC_VER < 1600) +# define munit_int8_t __int8 +# define munit_uint8_t unsigned __int8 +# define munit_int16_t __int16 +# define munit_uint16_t unsigned __int16 +# define munit_int32_t __int32 +# define munit_uint32_t unsigned __int32 +# define munit_int64_t __int64 +# define munit_uint64_t unsigned __int64 +#else +# include +# define munit_int8_t int8_t +# define munit_uint8_t uint8_t +# define munit_int16_t int16_t +# define munit_uint16_t uint16_t +# define munit_int32_t int32_t +# define munit_uint32_t uint32_t +# define munit_int64_t int64_t +# define munit_uint64_t uint64_t +#endif + +#if defined(_MSC_VER) && (_MSC_VER < 1800) +# if !defined(PRIi8) +# define PRIi8 "i" +# endif +# if !defined(PRIi16) +# define PRIi16 "i" +# endif +# if !defined(PRIi32) +# define PRIi32 "i" +# endif +# if !defined(PRIi64) +# define PRIi64 "I64i" +# endif +# if !defined(PRId8) +# define PRId8 "d" +# endif +# if !defined(PRId16) +# define PRId16 "d" +# endif +# if !defined(PRId32) +# define PRId32 "d" +# endif +# if !defined(PRId64) +# define PRId64 "I64d" +# endif +# if !defined(PRIx8) +# define PRIx8 "x" +# endif +# if !defined(PRIx16) +# define PRIx16 "x" +# endif +# if !defined(PRIx32) +# define PRIx32 "x" +# endif +# if !defined(PRIx64) +# define PRIx64 "I64x" +# endif +# if !defined(PRIu8) +# define PRIu8 "u" +# endif +# if !defined(PRIu16) +# define PRIu16 "u" +# endif +# if !defined(PRIu32) +# define PRIu32 "u" +# endif +# if !defined(PRIu64) +# define PRIu64 "I64u" +# endif +#else +# include +#endif + +#if !defined(munit_bool) +# if defined(bool) +# define munit_bool bool +# elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) +# define munit_bool _Bool +# else +# define munit_bool int +# endif +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +#if defined(__GNUC__) +# define MUNIT_LIKELY(expr) (__builtin_expect((expr), 1)) +# define MUNIT_UNLIKELY(expr) (__builtin_expect((expr), 0)) +# define MUNIT_UNUSED __attribute__((__unused__)) +#else +# define MUNIT_LIKELY(expr) (expr) +# define MUNIT_UNLIKELY(expr) (expr) +# define MUNIT_UNUSED +#endif + +#if !defined(_WIN32) +# define MUNIT_SIZE_MODIFIER "z" +# define MUNIT_CHAR_MODIFIER "hh" +# define MUNIT_SHORT_MODIFIER "h" +#else +# if defined(_M_X64) || defined(__amd64__) +# define MUNIT_SIZE_MODIFIER "I64" +# else +# define MUNIT_SIZE_MODIFIER "" +# endif +# define MUNIT_CHAR_MODIFIER "" +# define MUNIT_SHORT_MODIFIER "" +#endif + +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +# define MUNIT_NO_RETURN _Noreturn +#elif defined(__GNUC__) +# define MUNIT_NO_RETURN __attribute__((__noreturn__)) +#elif defined(_MSC_VER) +# define MUNIT_NO_RETURN __declspec(noreturn) +#else +# define MUNIT_NO_RETURN +#endif + +#if defined(_MSC_VER) && (_MSC_VER >= 1500) +# define MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + __pragma(warning(push)) __pragma(warning(disable : 4127)) +# define MUNIT_POP_DISABLE_MSVC_C4127_ __pragma(warning(pop)) +#else +# define MUNIT_PUSH_DISABLE_MSVC_C4127_ +# define MUNIT_POP_DISABLE_MSVC_C4127_ +#endif + +typedef enum { + MUNIT_LOG_DEBUG, + MUNIT_LOG_INFO, + MUNIT_LOG_WARNING, + MUNIT_LOG_ERROR +} MunitLogLevel; + +#if defined(__GNUC__) && !defined(__MINGW32__) +# define MUNIT_PRINTF(string_index, first_to_check) \ + __attribute__((format(printf, string_index, first_to_check))) +#else +# define MUNIT_PRINTF(string_index, first_to_check) +#endif + +MUNIT_PRINTF(4, 5) +void munit_logf_ex(MunitLogLevel level, const char *filename, int line, + const char *format, ...); + +#define munit_logf(level, format, ...) \ + munit_logf_ex(level, __FILE__, __LINE__, format, __VA_ARGS__) + +#define munit_log(level, msg) munit_logf(level, "%s", msg) + +MUNIT_NO_RETURN +MUNIT_PRINTF(3, 4) +void munit_errorf_ex(const char *filename, int line, const char *format, ...); + +#define munit_errorf(format, ...) \ + munit_errorf_ex(__FILE__, __LINE__, format, __VA_ARGS__) + +#define munit_error(msg) munit_errorf("%s", msg) + +#define munit_assert(expr) \ + do { \ + if (!MUNIT_LIKELY(expr)) { \ + munit_error("assertion failed: " #expr); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#define munit_assert_true(expr) \ + do { \ + if (!MUNIT_LIKELY(expr)) { \ + munit_error("assertion failed: " #expr " is not true"); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#define munit_assert_false(expr) \ + do { \ + if (!MUNIT_LIKELY(!(expr))) { \ + munit_error("assertion failed: " #expr " is not false"); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#define munit_assert_type_full(prefix, suffix, T, fmt, a, op, b) \ + do { \ + T munit_tmp_a_ = (a); \ + T munit_tmp_b_ = (b); \ + if (!(munit_tmp_a_ op munit_tmp_b_)) { \ + munit_errorf("assertion failed: %s %s %s (" prefix "%" fmt suffix \ + " %s " prefix "%" fmt suffix ")", \ + #a, #op, #b, munit_tmp_a_, #op, munit_tmp_b_); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#define munit_assert_type(T, fmt, a, op, b) \ + munit_assert_type_full("", "", T, fmt, a, op, b) + +#define munit_assert_char(a, op, b) \ + munit_assert_type_full("'\\x", "'", char, "02" MUNIT_CHAR_MODIFIER "x", a, \ + op, b) +#define munit_assert_uchar(a, op, b) \ + munit_assert_type_full("'\\x", "'", unsigned char, \ + "02" MUNIT_CHAR_MODIFIER "x", a, op, b) +#define munit_assert_short(a, op, b) \ + munit_assert_type(short, MUNIT_SHORT_MODIFIER "d", a, op, b) +#define munit_assert_ushort(a, op, b) \ + munit_assert_type(unsigned short, MUNIT_SHORT_MODIFIER "u", a, op, b) +#define munit_assert_int(a, op, b) munit_assert_type(int, "d", a, op, b) +#define munit_assert_uint(a, op, b) \ + munit_assert_type(unsigned int, "u", a, op, b) +#define munit_assert_long(a, op, b) munit_assert_type(long int, "ld", a, op, b) +#define munit_assert_ulong(a, op, b) \ + munit_assert_type(unsigned long int, "lu", a, op, b) +#define munit_assert_llong(a, op, b) \ + munit_assert_type(long long int, "lld", a, op, b) +#define munit_assert_ullong(a, op, b) \ + munit_assert_type(unsigned long long int, "llu", a, op, b) + +#define munit_assert_size(a, op, b) \ + munit_assert_type(size_t, MUNIT_SIZE_MODIFIER "u", a, op, b) +#define munit_assert_ssize(a, op, b) \ + munit_assert_type(ssize_t, MUNIT_SIZE_MODIFIER "d", a, op, b) + +#define munit_assert_float(a, op, b) munit_assert_type(float, "f", a, op, b) +#define munit_assert_double(a, op, b) munit_assert_type(double, "g", a, op, b) +#define munit_assert_ptr(a, op, b) \ + munit_assert_type(const void *, "p", a, op, b) + +#define munit_assert_int8(a, op, b) \ + munit_assert_type(munit_int8_t, PRIi8, a, op, b) +#define munit_assert_uint8(a, op, b) \ + munit_assert_type(munit_uint8_t, PRIu8, a, op, b) +#define munit_assert_int16(a, op, b) \ + munit_assert_type(munit_int16_t, PRIi16, a, op, b) +#define munit_assert_uint16(a, op, b) \ + munit_assert_type(munit_uint16_t, PRIu16, a, op, b) +#define munit_assert_int32(a, op, b) \ + munit_assert_type(munit_int32_t, PRIi32, a, op, b) +#define munit_assert_uint32(a, op, b) \ + munit_assert_type(munit_uint32_t, PRIu32, a, op, b) +#define munit_assert_int64(a, op, b) \ + munit_assert_type(munit_int64_t, PRIi64, a, op, b) +#define munit_assert_uint64(a, op, b) \ + munit_assert_type(munit_uint64_t, PRIu64, a, op, b) + +#define munit_assert_ptrdiff(a, op, b) \ + munit_assert_type(ptrdiff_t, "td", a, op, b) + +#define munit_assert_enum(T, a, op, b) munit_assert_type(T, "d", a, op, b) + +#define munit_assert_double_equal(a, b, precision) \ + do { \ + const double munit_tmp_a_ = (a); \ + const double munit_tmp_b_ = (b); \ + const double munit_tmp_diff_ = ((munit_tmp_a_ - munit_tmp_b_) < 0) \ + ? -(munit_tmp_a_ - munit_tmp_b_) \ + : (munit_tmp_a_ - munit_tmp_b_); \ + if (MUNIT_UNLIKELY(munit_tmp_diff_ > 1e-##precision)) { \ + munit_errorf("assertion failed: %s == %s (%0." #precision \ + "g == %0." #precision "g)", \ + #a, #b, munit_tmp_a_, munit_tmp_b_); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#include +#define munit_assert_string_equal(a, b) \ + do { \ + const char *munit_tmp_a_ = (a); \ + const char *munit_tmp_b_ = (b); \ + if (MUNIT_UNLIKELY(strcmp(munit_tmp_a_, munit_tmp_b_) != 0)) { \ + munit_hexdump_diff(stderr, munit_tmp_a_, strlen(munit_tmp_a_), \ + munit_tmp_b_, strlen(munit_tmp_b_)); \ + munit_errorf("assertion failed: string %s == %s (\"%s\" == \"%s\")", #a, \ + #b, munit_tmp_a_, munit_tmp_b_); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#define munit_assert_string_not_equal(a, b) \ + do { \ + const char *munit_tmp_a_ = (a); \ + const char *munit_tmp_b_ = (b); \ + if (MUNIT_UNLIKELY(strcmp(munit_tmp_a_, munit_tmp_b_) == 0)) { \ + munit_errorf("assertion failed: string %s != %s (\"%s\" == \"%s\")", #a, \ + #b, munit_tmp_a_, munit_tmp_b_); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#define munit_assert_memory_equal(size, a, b) \ + do { \ + const unsigned char *munit_tmp_a_ = (const unsigned char *)(a); \ + const unsigned char *munit_tmp_b_ = (const unsigned char *)(b); \ + const size_t munit_tmp_size_ = (size); \ + if (MUNIT_UNLIKELY(memcmp(munit_tmp_a_, munit_tmp_b_, munit_tmp_size_)) != \ + 0) { \ + size_t munit_tmp_pos_; \ + for (munit_tmp_pos_ = 0; munit_tmp_pos_ < munit_tmp_size_; \ + munit_tmp_pos_++) { \ + if (munit_tmp_a_[munit_tmp_pos_] != munit_tmp_b_[munit_tmp_pos_]) { \ + munit_hexdump_diff(stderr, munit_tmp_a_, size, munit_tmp_b_, size); \ + munit_errorf("assertion failed: memory %s == %s, at offset " \ + "%" MUNIT_SIZE_MODIFIER "u", \ + #a, #b, munit_tmp_pos_); \ + break; \ + } \ + } \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#define munit_assert_memn_equal(a, a_size, b, b_size) \ + do { \ + const unsigned char *munit_tmp_a_ = (const unsigned char *)(a); \ + const unsigned char *munit_tmp_b_ = (const unsigned char *)(b); \ + const size_t munit_tmp_a_size_ = (a_size); \ + const size_t munit_tmp_b_size_ = (b_size); \ + if (MUNIT_UNLIKELY(munit_tmp_a_size_ != munit_tmp_b_size_) || \ + MUNIT_UNLIKELY(munit_tmp_a_size_ && memcmp(munit_tmp_a_, munit_tmp_b_, \ + munit_tmp_a_size_)) != 0) { \ + munit_hexdump_diff(stderr, munit_tmp_a_, munit_tmp_a_size_, \ + munit_tmp_b_, munit_tmp_b_size_); \ + munit_errorf("assertion failed: memory %s == %s", #a, #b); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#define munit_assert_memory_not_equal(size, a, b) \ + do { \ + const unsigned char *munit_tmp_a_ = (const unsigned char *)(a); \ + const unsigned char *munit_tmp_b_ = (const unsigned char *)(b); \ + const size_t munit_tmp_size_ = (size); \ + if (MUNIT_UNLIKELY(memcmp(munit_tmp_a_, munit_tmp_b_, munit_tmp_size_)) == \ + 0) { \ + munit_errorf("assertion failed: memory %s != %s (%zu bytes)", #a, #b, \ + munit_tmp_size_); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#define munit_assert_ptr_equal(a, b) munit_assert_ptr(a, ==, b) +#define munit_assert_ptr_not_equal(a, b) munit_assert_ptr(a, !=, b) +#define munit_assert_null(ptr) munit_assert_ptr(ptr, ==, NULL) +#define munit_assert_not_null(ptr) munit_assert_ptr(ptr, !=, NULL) +#define munit_assert_ptr_null(ptr) munit_assert_ptr(ptr, ==, NULL) +#define munit_assert_ptr_not_null(ptr) munit_assert_ptr(ptr, !=, NULL) + +/*** Memory allocation ***/ + +void *munit_malloc_ex(const char *filename, int line, size_t size); + +#define munit_malloc(size) munit_malloc_ex(__FILE__, __LINE__, (size)) + +#define munit_new(type) ((type *)munit_malloc(sizeof(type))) + +#define munit_calloc(nmemb, size) munit_malloc((nmemb) * (size)) + +#define munit_newa(type, nmemb) ((type *)munit_calloc((nmemb), sizeof(type))) + +/*** Random number generation ***/ + +void munit_rand_seed(munit_uint32_t seed); +munit_uint32_t munit_rand_uint32(void); +int munit_rand_int_range(int min, int max); +double munit_rand_double(void); +void munit_rand_memory(size_t size, munit_uint8_t *buffer); + +/*** Tests and Suites ***/ + +typedef enum { + /* Test successful */ + MUNIT_OK, + /* Test failed */ + MUNIT_FAIL, + /* Test was skipped */ + MUNIT_SKIP, + /* Test failed due to circumstances not intended to be tested + * (things like network errors, invalid parameter value, failure to + * allocate memory in the test harness, etc.). */ + MUNIT_ERROR +} MunitResult; + +typedef struct { + char *name; + char **values; +} MunitParameterEnum; + +typedef struct { + char *name; + char *value; +} MunitParameter; + +const char *munit_parameters_get(const MunitParameter params[], + const char *key); + +typedef enum { + MUNIT_TEST_OPTION_NONE = 0, + MUNIT_TEST_OPTION_SINGLE_ITERATION = 1 << 0, + MUNIT_TEST_OPTION_TODO = 1 << 1 +} MunitTestOptions; + +typedef MunitResult (*MunitTestFunc)(const MunitParameter params[], + void *user_data_or_fixture); +typedef void *(*MunitTestSetup)(const MunitParameter params[], void *user_data); +typedef void (*MunitTestTearDown)(void *fixture); + +typedef struct { + const char *name; + MunitTestFunc test; + MunitTestSetup setup; + MunitTestTearDown tear_down; + MunitTestOptions options; + MunitParameterEnum *parameters; +} MunitTest; + +typedef enum { MUNIT_SUITE_OPTION_NONE = 0 } MunitSuiteOptions; + +typedef struct MunitSuite_ MunitSuite; + +struct MunitSuite_ { + const char *prefix; + const MunitTest *tests; + const MunitSuite *suites; + unsigned int iterations; + MunitSuiteOptions options; +}; + +int munit_suite_main(const MunitSuite *suite, void *user_data, int argc, + char *const *argv); + +/* Note: I'm not very happy with this API; it's likely to change if I + * figure out something better. Suggestions welcome. */ + +typedef struct MunitArgument_ MunitArgument; + +struct MunitArgument_ { + char *name; + munit_bool (*parse_argument)(const MunitSuite *suite, void *user_data, + int *arg, int argc, char *const *argv); + void (*write_help)(const MunitArgument *argument, void *user_data); +}; + +int munit_suite_main_custom(const MunitSuite *suite, void *user_data, int argc, + char *const *argv, const MunitArgument arguments[]); + +#if defined(MUNIT_ENABLE_ASSERT_ALIASES) + +# define assert_true(expr) munit_assert_true(expr) +# define assert_false(expr) munit_assert_false(expr) +# define assert_char(a, op, b) munit_assert_char(a, op, b) +# define assert_uchar(a, op, b) munit_assert_uchar(a, op, b) +# define assert_short(a, op, b) munit_assert_short(a, op, b) +# define assert_ushort(a, op, b) munit_assert_ushort(a, op, b) +# define assert_int(a, op, b) munit_assert_int(a, op, b) +# define assert_uint(a, op, b) munit_assert_uint(a, op, b) +# define assert_long(a, op, b) munit_assert_long(a, op, b) +# define assert_ulong(a, op, b) munit_assert_ulong(a, op, b) +# define assert_llong(a, op, b) munit_assert_llong(a, op, b) +# define assert_ullong(a, op, b) munit_assert_ullong(a, op, b) +# define assert_size(a, op, b) munit_assert_size(a, op, b) +# define assert_ssize(a, op, b) munit_assert_ssize(a, op, b) +# define assert_float(a, op, b) munit_assert_float(a, op, b) +# define assert_double(a, op, b) munit_assert_double(a, op, b) +# define assert_ptr(a, op, b) munit_assert_ptr(a, op, b) + +# define assert_int8(a, op, b) munit_assert_int8(a, op, b) +# define assert_uint8(a, op, b) munit_assert_uint8(a, op, b) +# define assert_int16(a, op, b) munit_assert_int16(a, op, b) +# define assert_uint16(a, op, b) munit_assert_uint16(a, op, b) +# define assert_int32(a, op, b) munit_assert_int32(a, op, b) +# define assert_uint32(a, op, b) munit_assert_uint32(a, op, b) +# define assert_int64(a, op, b) munit_assert_int64(a, op, b) +# define assert_uint64(a, op, b) munit_assert_uint64(a, op, b) + +# define assert_ptrdiff(a, op, b) munit_assert_ptrdiff(a, op, b) + +# define assert_enum(T, a, op, b) munit_assert_enum(T, a, op, b) + +# define assert_double_equal(a, b, precision) \ + munit_assert_double_equal(a, b, precision) +# define assert_string_equal(a, b) munit_assert_string_equal(a, b) +# define assert_string_not_equal(a, b) munit_assert_string_not_equal(a, b) +# define assert_memory_equal(size, a, b) munit_assert_memory_equal(size, a, b) +# define assert_memn_equal(a, a_size, b, b_size) \ + munit_assert_memn_equal(a, a_size, b, b_size) +# define assert_memory_not_equal(size, a, b) \ + munit_assert_memory_not_equal(size, a, b) +# define assert_ptr_equal(a, b) munit_assert_ptr_equal(a, b) +# define assert_ptr_not_equal(a, b) munit_assert_ptr_not_equal(a, b) +# define assert_ptr_null(ptr) munit_assert_null_equal(ptr) +# define assert_ptr_not_null(ptr) munit_assert_not_null(ptr) + +# define assert_null(ptr) munit_assert_null(ptr) +# define assert_not_null(ptr) munit_assert_not_null(ptr) + +#endif /* defined(MUNIT_ENABLE_ASSERT_ALIASES) */ + +#define munit_void_test_decl(func) \ + void func(void); \ + \ + static inline MunitResult wrap_##func(const MunitParameter params[], \ + void *fixture) { \ + (void)params; \ + (void)fixture; \ + \ + func(); \ + return MUNIT_OK; \ + } + +#define munit_void_test(func) \ + {"/" #func, wrap_##func, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL} + +#define munit_test_end() {NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL} + +int munit_hexdump(FILE *fp, const void *data, size_t datalen); + +int munit_hexdump_diff(FILE *fp, const void *a, size_t alen, const void *b, + size_t blen); + +#if defined(__cplusplus) +} +#endif + +#endif /* !defined(MUNIT_H) */ + +#if defined(MUNIT_ENABLE_ASSERT_ALIASES) +#if defined(assert) +# undef assert +#endif +#define assert(expr) munit_assert(expr) +#endif diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/munitxx.h b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/munitxx.h new file mode 100644 index 00000000000000..8f9d7830c9c310 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/munit/munitxx.h @@ -0,0 +1,94 @@ +/* µnit Testing Framework + * Copyright (c) 2013-2017 Evan Nemerson + * Copyright (c) 2023 munit contributors + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef MUNITXX_H +#define MUNITXX_H + +#include + +#include +#include +#if __cplusplus >= 201703L +# include +#endif // __cplusplus >= 201703L + +#define munit_assert_stdstring_equal(a, b) \ + do { \ + const std::string munit_tmp_a_ = (a); \ + const std::string munit_tmp_b_ = (b); \ + if (MUNIT_UNLIKELY(munit_tmp_a_ != munit_tmp_b_)) { \ + munit_hexdump_diff(stderr, munit_tmp_a_.c_str(), munit_tmp_a_.size(), \ + munit_tmp_b_.c_str(), munit_tmp_b_.size()); \ + munit_errorf("assertion failed: string %s == %s (\"%s\" == \"%s\")", #a, \ + #b, munit_tmp_a_.c_str(), munit_tmp_b_.c_str()); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#if __cplusplus >= 201703L +# define munit_assert_stdsv_equal(a, b) \ + do { \ + const std::string_view munit_tmp_a_ = (a); \ + const std::string_view munit_tmp_b_ = (b); \ + if (MUNIT_UNLIKELY(munit_tmp_a_ != munit_tmp_b_)) { \ + munit_hexdump_diff(stderr, munit_tmp_a_.data(), munit_tmp_a_.size(), \ + munit_tmp_b_.data(), munit_tmp_b_.size()); \ + munit_errorf( \ + "assertion failed: string %s == %s (\"%.*s\" == \"%.*s\")", #a, #b, \ + (int)munit_tmp_a_.size(), munit_tmp_a_.data(), \ + (int)munit_tmp_b_.size(), munit_tmp_b_.data()); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ +#endif // __cplusplus >= 201703L + +#define munit_assert_enum_class(a, op, b) \ + do { \ + auto munit_tmp_a_ = (a); \ + auto munit_tmp_b_ = (b); \ + if (!(munit_tmp_a_ op munit_tmp_b_)) { \ + auto munit_tmp_a_str_ = std::to_string( \ + static_cast>( \ + munit_tmp_a_)); \ + auto munit_tmp_b_str_ = std::to_string( \ + static_cast>( \ + munit_tmp_b_)); \ + munit_errorf("assertion failed: %s %s %s (%s %s %s)", #a, #op, #b, \ + munit_tmp_a_str_.c_str(), #op, munit_tmp_b_str_.c_str()); \ + } \ + MUNIT_PUSH_DISABLE_MSVC_C4127_ \ + } while (0) MUNIT_POP_DISABLE_MSVC_C4127_ + +#if defined(MUNIT_ENABLE_ASSERT_ALIASES) + +# define assert_stdstring_equal(a, b) munit_assert_stdstring_equal(a, b) +# if __cplusplus >= 201703L +# define assert_stdsv_equal(a, b) munit_assert_stdsv_equal(a, b) +# endif // __cplusplus >= 201703L +# define assert_enum_class(a, op, b) munit_assert_enum_class(a, op, b) + +#endif /* defined(MUNIT_ENABLE_ASSERT_ALIASES) */ + +#endif // MUNITXX_H diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse.c new file mode 100644 index 00000000000000..3e00da5eb0cfbc --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse.c @@ -0,0 +1,635 @@ +/* + * urlparse + * + * Copyright (c) 2024 urlparse contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "urlparse.h" + +#include +#include + +#define ALPHAS \ + ['A'] = 1, ['B'] = 1, ['C'] = 1, ['D'] = 1, ['E'] = 1, ['F'] = 1, ['G'] = 1, \ + ['H'] = 1, ['I'] = 1, ['J'] = 1, ['K'] = 1, ['L'] = 1, ['M'] = 1, ['N'] = 1, \ + ['O'] = 1, ['P'] = 1, ['Q'] = 1, ['R'] = 1, ['S'] = 1, ['T'] = 1, ['U'] = 1, \ + ['V'] = 1, ['W'] = 1, ['X'] = 1, ['Y'] = 1, ['Z'] = 1, ['a'] = 1, ['b'] = 1, \ + ['c'] = 1, ['d'] = 1, ['e'] = 1, ['f'] = 1, ['g'] = 1, ['h'] = 1, ['i'] = 1, \ + ['j'] = 1, ['k'] = 1, ['l'] = 1, ['m'] = 1, ['n'] = 1, ['o'] = 1, ['p'] = 1, \ + ['q'] = 1, ['r'] = 1, ['s'] = 1, ['t'] = 1, ['u'] = 1, ['v'] = 1, ['w'] = 1, \ + ['x'] = 1, ['y'] = 1, ['z'] = 1 + +#define DIGITS \ + ['0'] = 1, ['1'] = 1, ['2'] = 1, ['3'] = 1, ['4'] = 1, ['5'] = 1, ['6'] = 1, \ + ['7'] = 1, ['8'] = 1, ['9'] = 1 + +#define HEXDIGITS \ + DIGITS, ['A'] = 1, ['B'] = 1, ['C'] = 1, ['D'] = 1, ['E'] = 1, ['F'] = 1, \ + ['a'] = 1, ['b'] = 1, ['c'] = 1, ['d'] = 1, ['e'] = 1, ['f'] = 1 + +typedef struct urlparse_parser { + const char *begin; + const char *pos; + const char *end; +} urlparse_parser; + +static int urlparse_parser_eof(urlparse_parser *up) { + return up->pos == up->end; +} + +static void urlparse_url_set_field_data(urlparse_url *dest, int field, + const char *data, const char *start, + const char *end) { + dest->field_set |= (uint16_t)(1 << field); + dest->field_data[field].off = (uint16_t)(start - data); + dest->field_data[field].len = (uint16_t)(end - start); +} + +static const uint8_t scheme_chars[256] = { + ALPHAS, + [':'] = 2, + ['/'] = 3, + ['*'] = 3, +}; + +static int parse_scheme(urlparse_parser *up, urlparse_url *dest) { + const char *start; + + if (urlparse_parser_eof(up)) { + return URLPARSE_ERR_PARSE; + } + + switch (scheme_chars[(uint8_t)*up->pos]) { + case 0: + case 2: + return URLPARSE_ERR_PARSE; + case 1: + break; + case 3: + return 0; + } + + start = up->pos; + ++up->pos; + + for (; !urlparse_parser_eof(up); ++up->pos) { + switch (scheme_chars[(uint8_t)*up->pos]) { + case 0: + case 3: + return URLPARSE_ERR_PARSE; + case 1: + continue; + case 2: + goto fin; + } + } + + return URLPARSE_ERR_PARSE; + +fin: + urlparse_url_set_field_data(dest, URLPARSE_SCHEMA, up->begin, start, up->pos); + + return 0; +} + +static const uint8_t path_chars[256] = { + /* unreserved */ + DIGITS, + ALPHAS, + ['-'] = 1, + ['.'] = 1, + ['_'] = 1, + ['~'] = 1, + /* pct-encoded */ + ['%'] = 1, + /* sub-delims */ + ['!'] = 1, + ['$'] = 1, + ['&'] = 1, + ['\''] = 1, + ['('] = 1, + [')'] = 1, + ['*'] = 1, + ['+'] = 1, + [','] = 1, + [';'] = 1, + ['='] = 1, + /* extra */ + [':'] = 1, + ['@'] = 1, + ['/'] = 1, + /* http-parser allows the following characters as well. */ + ['"'] = 1, + ['<'] = 1, + ['>'] = 1, + ['['] = 1, + ['\\'] = 1, + [']'] = 1, + ['^'] = 1, + ['`'] = 1, + ['{'] = 1, + ['|'] = 1, + ['}'] = 1, + /* query found */ + ['?'] = 2, + /* fragment found */ + ['#'] = 2, +}; + +static int parse_path_abempty(urlparse_parser *up, urlparse_url *dest) { + const char *start; + + if (urlparse_parser_eof(up)) { + return 0; + } + + switch (*up->pos) { + case '?': + return 0; + case '/': + break; + case '*': + if (dest->field_set & (1 << URLPARSE_HOST)) { + return URLPARSE_ERR_PARSE; + } + + break; + default: + return URLPARSE_ERR_PARSE; + } + + start = up->pos++; + + for (; !urlparse_parser_eof(up); ++up->pos) { + switch (path_chars[(uint8_t)*up->pos]) { + case 0: + return URLPARSE_ERR_PARSE; + case 1: + continue; + case 2: + goto fin; + } + } + +fin: + urlparse_url_set_field_data(dest, URLPARSE_PATH, up->begin, start, up->pos); + + return 0; +} + +static const uint8_t userinfo_chars[256] = { + /* unreserved */ + DIGITS, + ALPHAS, + ['-'] = 1, + ['.'] = 1, + ['_'] = 1, + ['~'] = 1, + /* pct-encoded */ + ['%'] = 1, + /* sub-delims */ + ['!'] = 1, + ['$'] = 1, + ['&'] = 1, + ['\''] = 1, + ['('] = 1, + [')'] = 1, + ['*'] = 1, + ['+'] = 1, + [','] = 1, + [';'] = 1, + ['='] = 1, + [':'] = 1, + ['@'] = 2, +}; + +static int parse_userinfo(urlparse_parser *up, urlparse_url *dest) { + const char *start; + + assert(!urlparse_parser_eof(up)); + + start = up->pos; + + for (; !urlparse_parser_eof(up); ++up->pos) { + switch (userinfo_chars[(uint8_t)*up->pos]) { + case 0: + return URLPARSE_ERR_PARSE; + case 1: + continue; + case 2: + goto fin; + } + } + + return URLPARSE_ERR_PARSE; + +fin: + if (start != up->pos) { + urlparse_url_set_field_data(dest, URLPARSE_USERINFO, up->begin, start, + up->pos); + } + + ++up->pos; + + return 0; +} + +static const uint8_t host_chars[256] = { + DIGITS, ALPHAS, ['.'] = 1, ['-'] = 1, [':'] = 2, ['/'] = 2, ['?'] = 2, +}; + +static int parse_host(urlparse_parser *up, urlparse_url *dest) { + const char *start; + + assert(!urlparse_parser_eof(up)); + + start = up->pos; + + for (; !urlparse_parser_eof(up); ++up->pos) { + switch (host_chars[(uint8_t)*up->pos]) { + case 0: + return URLPARSE_ERR_PARSE; + case 1: + continue; + case 2: + break; + } + + break; + } + + if (start == up->pos) { + return URLPARSE_ERR_PARSE; + } + + urlparse_url_set_field_data(dest, URLPARSE_HOST, up->begin, start, up->pos); + + return 0; +} + +static const uint8_t ipv6_host_chars[256] = { + HEXDIGITS, [':'] = 1, ['.'] = 1, ['%'] = 2, [']'] = 3, +}; + +static const uint8_t ipv6_zone_chars[256] = { + DIGITS, ALPHAS, ['%'] = 1, ['.'] = 1, + ['-'] = 1, ['_'] = 1, ['~'] = 1, [']'] = 2, +}; + +static int parse_ipv6_host(urlparse_parser *up, urlparse_url *dest) { + const char *start, *zone_start; + + if (urlparse_parser_eof(up)) { + return URLPARSE_ERR_PARSE; + } + + start = up->pos; + + for (; !urlparse_parser_eof(up); ++up->pos) { + switch (ipv6_host_chars[(uint8_t)*up->pos]) { + case 0: + return URLPARSE_ERR_PARSE; + case 1: + continue; + case 2: + if (start == up->pos) { + return URLPARSE_ERR_PARSE; + } + + zone_start = up->pos; + + ++up->pos; + if (urlparse_parser_eof(up)) { + return URLPARSE_ERR_PARSE; + } + + for (; !urlparse_parser_eof(up); ++up->pos) { + switch (ipv6_zone_chars[(uint8_t)*up->pos]) { + case 0: + return URLPARSE_ERR_PARSE; + case 1: + continue; + case 2: + if (zone_start + 1 == up->pos) { + return URLPARSE_ERR_PARSE; + } + + goto fin; + } + } + + return URLPARSE_ERR_PARSE; + case 3: + goto fin; + } + } + + return URLPARSE_ERR_PARSE; + +fin: + if (start == up->pos) { + return URLPARSE_ERR_PARSE; + } + + urlparse_url_set_field_data(dest, URLPARSE_HOST, up->begin, start, up->pos); + + ++up->pos; + + return 0; +} + +static const uint8_t port_chars[256] = { + DIGITS, + ['/'] = 2, + ['?'] = 2, +}; + +static int parse_port(urlparse_parser *up, urlparse_url *dest) { + const char *start; + uint16_t port, d; + + if (urlparse_parser_eof(up)) { + /* http_parser disallows empty port. */ + return URLPARSE_ERR_PARSE; + } + + start = up->pos; + port = 0; + + for (; !urlparse_parser_eof(up); ++up->pos) { + switch (port_chars[(uint8_t)*up->pos]) { + case 0: + return URLPARSE_ERR_PARSE; + case 1: + if (port > UINT16_MAX / 10) { + return URLPARSE_ERR_PARSE; + } + + port *= 10; + d = (uint16_t)(*up->pos - '0'); + + if (port > UINT16_MAX - d) { + return URLPARSE_ERR_PARSE; + } + + port += d; + + break; + case 2: + /* http_parser disallows empty port. */ + if (start == up->pos) { + return URLPARSE_ERR_PARSE; + } + + goto fin; + } + } + +fin: + urlparse_url_set_field_data(dest, URLPARSE_PORT, up->begin, start, up->pos); + dest->port = port; + + return 0; +} + +static int parse_authority(urlparse_parser *up, urlparse_url *dest) { + const char *start; + int rv; + + if (urlparse_parser_eof(up)) { + return URLPARSE_ERR_PARSE; + } + + if (*up->pos == '[') { + ++up->pos; + + rv = parse_ipv6_host(up, dest); + if (rv != 0) { + return rv; + } + } else { + start = up->pos; + + rv = parse_host(up, dest); + if (rv == 0) { + if (urlparse_parser_eof(up) || *up->pos != ':') { + return 0; + } + + ++up->pos; + + rv = parse_port(up, dest); + if (rv == 0) { + return 0; + } + } + + /* Rewind, and try parsing userinfo, and then host. */ + up->pos = start; + + rv = parse_userinfo(up, dest); + if (rv != 0) { + return rv; + } + + if (urlparse_parser_eof(up)) { + return URLPARSE_ERR_PARSE; + } + + if (*up->pos == '[') { + ++up->pos; + + rv = parse_ipv6_host(up, dest); + if (rv != 0) { + return rv; + } + } else { + rv = parse_host(up, dest); + if (rv != 0) { + return rv; + } + } + } + + if (urlparse_parser_eof(up) || *up->pos != ':') { + return 0; + } + + ++up->pos; + + return parse_port(up, dest); +} + +static const uint8_t extra_chars[256] = { + /* unreserved */ + DIGITS, + ALPHAS, + ['-'] = 1, + ['.'] = 1, + ['_'] = 1, + ['~'] = 1, + /* pct-encoded */ + ['%'] = 1, + /* sub-delims */ + ['!'] = 1, + ['$'] = 1, + ['&'] = 1, + ['\''] = 1, + ['('] = 1, + [')'] = 1, + ['*'] = 1, + ['+'] = 1, + [','] = 1, + [';'] = 1, + ['='] = 1, + /* extra */ + [':'] = 1, + ['@'] = 1, + /* query/fragment specific */ + ['/'] = 1, + ['?'] = 1, + /* http-parser allows the following characters as well. */ + ['"'] = 1, + ['<'] = 1, + ['>'] = 1, + ['['] = 1, + ['\\'] = 1, + [']'] = 1, + ['^'] = 1, + ['`'] = 1, + ['{'] = 1, + ['|'] = 1, + ['}'] = 1, + ['#'] = 2, +}; + +static int parse_extra(urlparse_parser *up, urlparse_url *dest, int field) { + const char *start; + + start = up->pos; + + for (; !urlparse_parser_eof(up); ++up->pos) { + switch (extra_chars[(uint8_t)*up->pos]) { + case 0: + return URLPARSE_ERR_PARSE; + case 1: + continue; + case 2: + if (field == URLPARSE_QUERY) { + goto fin; + } + + continue; + } + } + +fin: + if (start != up->pos) { + urlparse_url_set_field_data(dest, field, up->begin, start, up->pos); + } + + return 0; +} + +int urlparse_parse_url(const char *url, size_t urllen, int is_connect, + urlparse_url *dest) { + urlparse_parser up; + int rv; + + if (urllen == 0 || urllen > UINT16_MAX) { + return URLPARSE_ERR_PARSE; + } + + up.begin = url; + up.pos = url; + up.end = url + urllen; + + memset(dest, 0, sizeof(*dest)); + + if (is_connect) { + /* http_parser requires host:port when is_connect is nonzero. */ + rv = parse_authority(&up, dest); + if (rv != 0) { + return rv; + } + } else { + rv = parse_scheme(&up, dest); + if (rv != 0) { + return rv; + } + + if (dest->field_set & (1 << URLPARSE_SCHEMA)) { + if (up.end - up.pos < 3 || *up.pos != ':' || *(up.pos + 1) != '/' || + *(up.pos + 2) != '/') { + return URLPARSE_ERR_PARSE; + } + + up.pos += 3; + + rv = parse_authority(&up, dest); + if (rv != 0) { + return rv; + } + } + } + + rv = parse_path_abempty(&up, dest); + if (rv != 0) { + return rv; + } + + if (urlparse_parser_eof(&up)) { + goto fin; + } + + if (*up.pos == '?') { + ++up.pos; + + rv = parse_extra(&up, dest, URLPARSE_QUERY); + if (rv != 0) { + return rv; + } + } + + if (urlparse_parser_eof(&up)) { + goto fin; + } + + assert(*up.pos == '#'); + + ++up.pos; + + for (; !urlparse_parser_eof(&up) && *up.pos == '#'; ++up.pos) + ; + + rv = parse_extra(&up, dest, URLPARSE_FRAGMENT); + if (rv != 0) { + return rv; + } + +fin: + if (is_connect && + dest->field_set != ((1 << URLPARSE_HOST) | (1 << URLPARSE_PORT))) { + return URLPARSE_ERR_PARSE; + } + + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse.h b/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse.h new file mode 100644 index 00000000000000..8941ba27426ab9 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse.h @@ -0,0 +1,170 @@ +/* + * urlparse + * + * Copyright (c) 2024 urlparse contributors + * Copyright (c) 2023 sfparse contributors + * Copyright (c) 2019 nghttp3 contributors + * Copyright (c) 2015 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef URLPARSE_H +#define URLPARSE_H + +/* Define WIN32 when build target is Win32 API (borrowed from + libcurl) */ +#if (defined(_WIN32) || defined(__WIN32__)) && !defined(WIN32) +# define WIN32 +#endif /* (defined(_WIN32) || defined(__WIN32__)) && !defined(WIN32) */ + +#ifdef __cplusplus +extern "C" { +#endif /* defined(__cplusplus) */ + +#if defined(_MSC_VER) && (_MSC_VER < 1800) +/* MSVC < 2013 does not have inttypes.h because it is not C99 + compliant. See compiler macros and version number in + https://sourceforge.net/p/predef/wiki/Compilers/ */ +# include +#else /* !(defined(_MSC_VER) && (_MSC_VER < 1800)) */ +# include +#endif /* !(defined(_MSC_VER) && (_MSC_VER < 1800)) */ +#include +#include + +/** + * @macro + * + * :macro:`URLPARSE_ERR_PARSE` indicates that an error occurred while + * the parser is processing a URL. + */ +#define URLPARSE_ERR_PARSE -1 + +/** + * @enum + * + * :type:`urlparse_url_fields` defines URL component fields. + */ +typedef enum urlparse_url_fields { + /** + * :enum:`URLPARSE_SCHEMA` is a URL scheme. + */ + URLPARSE_SCHEMA = 0, + /** + * :enum:`URLPARSE_HOST` is a host. + */ + URLPARSE_HOST = 1, + /** + * :enum:`URLPARSE_PORT` is a port. + */ + URLPARSE_PORT = 2, + /** + * :enum:`URLPARSE_PATH` is a path. + */ + URLPARSE_PATH = 3, + /** + * :enum:`URLPARSE_QUERY` is a query. + */ + URLPARSE_QUERY = 4, + /** + * :enum:`URLPARSE_FRAGMENT` is a fragment. + */ + URLPARSE_FRAGMENT = 5, + /** + * :enum:`URLPARSE_USERINFO` is a userinfo. + */ + URLPARSE_USERINFO = 6, + /** + * :enum:`URLPARSE_MAX` is the number of fields. + */ + URLPARSE_MAX = 7 +} urlparse_url_fields; + +/** + * @struct + * + * :type:`urlparse_url` is a struct to store the result of parsing a + * URL. + */ +typedef struct urlparse_url { + /** + * :member:`field_set` is a bitmask of (1 << :type:`URLPARSE_* + * `) values. + */ + uint16_t field_set; + /** + * :member:`port` is the integer representation of + * :enum:`URLPARSE_PORT ` string. + * It is assigned only when (:member:`field_set` & (1 << + * :enum:`URLPARSE_PORT `)) is + * nonzero. + */ + uint16_t port; + + /** + * @anonstruct_start + * + * @struct_urlparse_field_data + * + * :member:`field_data` stores the position and its length of each + * URL component if the corresponding bit is set in + * :member:`field_set`. For example, + * field_data[:enum:`URLPARSE_HOST + * `] is assigned if + * (:member:`field_set` & (1 << :enum:`URLPARSE_HOST + * `)) is nonzero. + */ + struct { + /** + * :member:`off` is an offset into buffer in which field starts. + */ + uint16_t off; + /** + * :member:`len` is a length of run in buffer. + */ + uint16_t len; + /** + * @anonstruct_end + */ + } field_data[URLPARSE_MAX]; +} urlparse_url; + +/** + * @function + * + * `urlparse_parse_url` parses |url| of length |urllen| bytes, and + * stores the result in |u|. If |is_connect| is nonzero, it parses + * the URL as a request target that appears in CONNECT request, that + * is, consisting of only the host and port number. + * + * This function initializes |u| before its use. If this function + * returns nonzero, |u| might not be initialized. + * + * This function returns 0 if it succeeds, or + * :macro:`URLPARSE_ERR_PARSE`. + */ +int urlparse_parse_url(const char *url, size_t urllen, int is_connect, + urlparse_url *u); + +#ifdef __cplusplus +} +#endif /* defined(__cplusplus) */ + +#endif /* !defined(URLPARSE_H) */ diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse_test.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse_test.c new file mode 100644 index 00000000000000..7339a253d126fc --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse_test.c @@ -0,0 +1,1128 @@ +/* + * urlparse + * + * Copyright (c) 2024 urlparse contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "urlparse_test.h" + +#include +#include +#include + +#include "urlparse.h" +#include "http-parser/http_parser.h" + +static const MunitTest tests[] = { + munit_void_test(test_urlparse_parse_url), + munit_test_end(), +}; + +const MunitSuite urlparse_suite = { + "/urlparse", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE, +}; + +#define init_buffer(BUF, BUFLEN, S) \ + do { \ + (BUF) = malloc(sizeof(S) - 1); \ + memcpy((BUF), (S), sizeof(S) - 1); \ + (BUFLEN) = sizeof(S) - 1; \ + } while (0) + +#define assert_field_equal(URL, FIELD, BUF, EXPECTED) \ + do { \ + assert_true((URL).field_set & (1 << (FIELD))); \ + assert_memn_equal((EXPECTED), sizeof(EXPECTED) - 1, \ + (BUF) + (URL).field_data[(FIELD)].off, \ + (URL).field_data[(FIELD)].len); \ + } while (0) + +#define assert_field_unset(URL, FIELD) \ + assert_false((URL).field_set & (1 << (FIELD))) + +/* Copied from https://github.com/nodejs/http-parser */ +static void dump_url(const char *url, const urlparse_url *u) { + size_t i; + + fprintf(stderr, "\tfield_set: 0x%x, port: %u\n", u->field_set, u->port); + for (i = 0; i < URLPARSE_MAX; i++) { + if ((u->field_set & (1 << i)) == 0) { + fprintf(stderr, "\tfield_data[%zu]: unset\n", i); + continue; + } + + fprintf(stderr, "\tfield_data[%zu]: off: %u len: %u part: \"%.*s\"\n", i, + u->field_data[i].off, u->field_data[i].len, u->field_data[i].len, + url + u->field_data[i].off); + } +} + +#define assert_url(BUF, U1, U2) \ + do { \ + if (memcmp((U1), (U2), sizeof(urlparse_url)) == 0) { \ + break; \ + } \ + \ + dump_url((BUF), (urlparse_url *)(U1)); \ + dump_url((BUF), (U2)); \ + assert_memory_equal(sizeof(urlparse_url), (U1), (U2)); \ + } while (0) + +void test_urlparse_parse_url(void) { + char *b; + size_t blen; + urlparse_url u; + int rv; + struct http_parser_url hu; + + { + /* absempty */ + init_buffer(b, blen, "https://example.com/"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_PATH, b, "/"); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* absempty + port */ + init_buffer(b, blen, "https://example.com:8080/foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_PORT, b, "8080"); + assert_uint16(8080, ==, u.port); + assert_field_equal(u, URLPARSE_PATH, b, "/foo"); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* port too large */ + init_buffer(b, blen, "https://example.com:65536/foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* leading zeros in port */ + init_buffer(b, blen, "https://example.com:0000443/foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_PORT, b, "0000443"); + assert_uint16(443, ==, u.port); + assert_field_equal(u, URLPARSE_PATH, b, "/foo"); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* empty path */ + init_buffer(b, blen, "https://example.com"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_PATH); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* userinfo */ + init_buffer(b, blen, "https://foo:bar@example.com/"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_USERINFO, b, "foo:bar"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_PATH, b, "/"); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* host:port like userinfo */ + init_buffer(b, blen, "https://foo:111@example.com/"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_USERINFO, b, "foo:111"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_PATH, b, "/"); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* IPv6 host */ + init_buffer(b, blen, "https://[::1]/"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "::1"); + assert_field_equal(u, URLPARSE_PATH, b, "/"); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* query */ + init_buffer(b, blen, "https://example.com/?foo/??"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_PATH, b, "/"); + assert_field_equal(u, URLPARSE_QUERY, b, "foo/??"); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* fragment */ + init_buffer(b, blen, "https://example.com/#?foo/??"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_PATH, b, "/"); + assert_field_equal(u, URLPARSE_FRAGMENT, b, "?foo/??"); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* path + fragment */ + init_buffer(b, blen, "https://example.com/?32423?fdaff#?foo/??"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_PATH, b, "/"); + assert_field_equal(u, URLPARSE_QUERY, b, "32423?fdaff"); + assert_field_equal(u, URLPARSE_FRAGMENT, b, "?foo/??"); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PORT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* path_rootless */ + init_buffer(b, blen, "foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* path_absolute */ + init_buffer(b, blen, "/foo/bar"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_PATH, b, "/foo/bar"); + assert_field_unset(u, URLPARSE_SCHEMA); + assert_field_unset(u, URLPARSE_HOST); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* is_connect */ + init_buffer(b, blen, "example.com:443"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 1, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_PORT, b, "443"); + assert_uint16(443, ==, u.port); + assert_field_unset(u, URLPARSE_SCHEMA); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PATH); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 1, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* is_connect without port */ + init_buffer(b, blen, "example.com"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 1, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 1, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* is_connect with empty port */ + init_buffer(b, blen, "example.com:"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 1, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 1, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* is_connect with scheme */ + init_buffer(b, blen, "https://example.com:443"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 1, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 1, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* empty scheme */ + init_buffer(b, blen, "://example.com:443"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* starts with "//" */ + init_buffer(b, blen, "//example.com:443"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_PATH, b, "//example.com:443"); + assert_field_unset(u, URLPARSE_SCHEMA); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_HOST); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* no colon in first component */ + init_buffer(b, blen, "https/foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* '*' in scheme */ + init_buffer(b, blen, "https*://example.com"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* scheme contains a character that has the highest bit set, which + can make the char negative. */ + init_buffer(b, blen, + "https" + "\x80" + "://example.com"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* query starts with ?? */ + init_buffer(b, blen, "/foo??bar"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_PATH, b, "/foo"); + assert_field_equal(u, URLPARSE_QUERY, b, "?bar"); + assert_field_unset(u, URLPARSE_SCHEMA); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_HOST); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* fragment starts with ## */ + init_buffer(b, blen, "/foo##bar"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_PATH, b, "/foo"); + assert_field_equal(u, URLPARSE_FRAGMENT, b, "bar"); + assert_field_unset(u, URLPARSE_SCHEMA); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_HOST); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_QUERY); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* query after host */ + init_buffer(b, blen, "https://example.com?foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_QUERY, b, "foo"); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_PATH); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* query after port */ + init_buffer(b, blen, "https://example.com:8443?foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_PORT, b, "8443"); + assert_uint16(8443, ==, u.port); + assert_field_equal(u, URLPARSE_QUERY, b, "foo"); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PATH); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* fragment after host */ + init_buffer(b, blen, "https://example.com#foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* fragment after port */ + init_buffer(b, blen, "https://example.com:8443#foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* query only */ + init_buffer(b, blen, "?foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* fragment only */ + init_buffer(b, blen, "#foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* host all chars */ + init_buffer( + b, blen, + "https://" + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal( + u, URLPARSE_HOST, b, + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-"); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_PATH); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* IPv6 all chars */ + init_buffer( + b, blen, + "https://" + "[abcdefABCDEF0123456789:.%" + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%.-_~]"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal( + u, URLPARSE_HOST, b, + "abcdefABCDEF0123456789:.%" + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%.-_~"); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_PATH); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* userinfo all chars */ + init_buffer(b, blen, + "https://" + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~%!$&'()*+,;=:@example.com"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_USERINFO, b, + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012" + "3456789-._~%!$&'()*+,;=:"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_PATH); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* path all chars */ + init_buffer(b, blen, + "https://example.com/" + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~%!$&'()*+,;=:@/\"<>[\\]^`{|}"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal( + u, URLPARSE_PATH, b, + "/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~%!$&'()*+,;=:@/\"<>[\\]^`{|}"); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* query all chars */ + init_buffer(b, blen, + "https://" + "example.com?" + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~%!$&'()*+,;=:@/?\"<>[\\]^`{|}"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal( + u, URLPARSE_QUERY, b, + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~%!$&'()*+,;=:@/?\"<>[\\]^`{|}"); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_PATH); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* fragment all chars */ + init_buffer(b, blen, + "https://" + "example.com/#" + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~%!$&'()*+,;=:@/?\"<>[\\]^`{|}"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_SCHEMA, b, "https"); + assert_field_equal(u, URLPARSE_HOST, b, "example.com"); + assert_field_equal(u, URLPARSE_PATH, b, "/"); + assert_field_equal( + u, URLPARSE_FRAGMENT, b, + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~%!$&'()*+,;=:@/?\"<>[\\]^`{|}"); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_QUERY); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* just * */ + init_buffer(b, blen, "*"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_PATH, b, "*"); + assert_field_unset(u, URLPARSE_SCHEMA); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_HOST); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* just ** */ + init_buffer(b, blen, "**"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_PATH, b, "**"); + assert_field_unset(u, URLPARSE_SCHEMA); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_HOST); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* ** followed by ? */ + init_buffer(b, blen, "**?foo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_PATH, b, "**"); + assert_field_equal(u, URLPARSE_QUERY, b, "foo"); + assert_field_unset(u, URLPARSE_SCHEMA); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_HOST); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* just / */ + init_buffer(b, blen, "/"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_PATH, b, "/"); + assert_field_unset(u, URLPARSE_SCHEMA); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_HOST); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_QUERY); + assert_field_unset(u, URLPARSE_FRAGMENT); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* * flollowed by # */ + init_buffer(b, blen, "*#w#"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + assert_field_equal(u, URLPARSE_PATH, b, "*"); + assert_field_equal(u, URLPARSE_FRAGMENT, b, "w#"); + assert_field_unset(u, URLPARSE_SCHEMA); + assert_field_unset(u, URLPARSE_USERINFO); + assert_field_unset(u, URLPARSE_HOST); + assert_field_unset(u, URLPARSE_PORT); + assert_field_unset(u, URLPARSE_QUERY); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* bad IPv6 */ + init_buffer(b, blen, "mmm://[[[[[[[[)iomm://)mo"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* non-alpha scheme */ + init_buffer(b, blen, "c6://c:@N"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* unterminated IPv6 */ + init_buffer(b, blen, "Cc://[8"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* IPv6 followed by garbage */ + init_buffer(b, blen, "s://[F]*iiiiffy"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* empty IPv6 */ + init_buffer(b, blen, "h://@[]//2h:"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(URLPARSE_ERR_PARSE, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, !=, rv); + + free(b); + } + + { + /* is_connect with query */ + init_buffer(b, blen, "A:1?"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 1, &u); + + assert_int(0, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 1, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } + + { + /* long URL */ + init_buffer( + b, blen, + "https://" + "abcdefghijklmno2qrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'(" + ")*+,;=:@[abcdefABCDEF0123456789:.%" + "abcdefghijklmnopqrDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.~_%]:12345/" + "abcdefghijklmnopqrstuxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()*" + "+,;=:@[abcdefABCDEF0123456789:.%" + "abcdefghijklmnopqrDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.~_%]:12345/" + "abcdefghijklmnopqrstuvwx$&'()*+,;=:@/" + "?\"<>[\\]^`{|}#abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP<>[\\]^`{|}"); + + rv = urlparse_parse_url(b, blen, /* is_connect = */ 0, &u); + + assert_int(0, ==, rv); + + memset(&hu, 0, sizeof(hu)); + rv = http_parser_parse_url(b, blen, /* is_connect = */ 0, &hu); + + assert_int(0, ==, rv); + assert_url(b, &hu, &u); + + free(b); + } +} diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse_test.h b/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse_test.h new file mode 100644 index 00000000000000..4aa11605cf818d --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse_test.h @@ -0,0 +1,36 @@ +/* + * urlparse + * + * Copyright (c) 2024 urlparse contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef URLPARSE_TEST_H +#define URLPARSE_TEST_H + +#define MUNIT_ENABLE_ASSERT_ALIASES + +#include "munit.h" + +extern const MunitSuite urlparse_suite; + +munit_void_test_decl(test_urlparse_parse_url) + +#endif /* !defined(URLPARSE_TEST_H) */ diff --git a/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse_test_main.c b/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse_test_main.c new file mode 100644 index 00000000000000..5998d9ac94755e --- /dev/null +++ b/deps/ngtcp2/ngtcp2/third-party/urlparse/urlparse_test_main.c @@ -0,0 +1,46 @@ +/* + * urlparse + * + * Copyright (c) 2024 urlparse contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include +#endif /* defined(HAVE_CONFIG_H) */ + +#include "munit.h" + +/* include test cases' include files here */ +#include "urlparse_test.h" +#include "http_parser_compat_test.h" + +int main(int argc, char *argv[]) { + const MunitSuite suites[] = { + urlparse_suite, + http_parser_compat_suite, + {NULL, NULL, NULL, 0, MUNIT_SUITE_OPTION_NONE}, + }; + const MunitSuite suite = { + "", NULL, suites, 1, MUNIT_SUITE_OPTION_NONE, + }; + + return munit_suite_main(&suite, NULL, argc, argv); +} diff --git a/doc/api/cli.md b/doc/api/cli.md index 1d1a7e2ffea800..5153dbcc152b96 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1203,6 +1203,16 @@ added: Enable experimental support for storage inspection +### `--experimental-stream-iter` + + + +> Stability: 1 - Experimental + +Enable the experimental [`node:stream/iter`][] module. + ### `--experimental-test-coverage` + +> Stability: 1 - Experimental + +The Node.js error code for a [`QuicError`][] thrown to abort a QUIC stream +or session with an explicit application or transport error code. + + + +### `ERR_QUIC_STREAM_RESET` + + + +> Stability: 1 - Experimental + +A QUIC stream was reset by the peer. The error includes the reset code +provided by the peer. + ### `ERR_QUIC_TRANSPORT_ERROR` @@ -2906,6 +2932,13 @@ An attempt was made to call [`stream.pipe()`][] on a [`Writable`][] stream. A stream method was called that cannot complete because the stream was destroyed using `stream.destroy()`. + + +### `ERR_STREAM_ITER_MISSING_FLAG` + +A stream/iter API was used without the `--experimental-stream-iter` CLI flag +enabled. + ### `ERR_STREAM_NULL_VALUES` @@ -4430,6 +4463,7 @@ An error occurred trying to allocate memory. This should never happen. [`MessagePort`]: worker_threads.md#class-messageport [`Object.getPrototypeOf`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf [`Object.setPrototypeOf`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf +[`QuicError`]: quic.md#class-quicerror [`REPL`]: repl.md [`ServerResponse`]: http.md#class-httpserverresponse [`Writable`]: stream.md#class-streamwritable diff --git a/doc/api/fs.md b/doc/api/fs.md index 9916d5b0fb99aa..287662ebe3df37 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -377,6 +377,154 @@ added: v10.0.0 * Type: {number} The numeric file descriptor managed by the {FileHandle} object. +#### `filehandle.pull([...transforms][, options])` + + + +> Stability: 1 - Experimental + +* `...transforms` {Function|Object} Optional transforms to apply via + [`stream/iter pull()`][]. +* `options` {Object} + * `signal` {AbortSignal} + * `autoClose` {boolean} Close the file handle when the stream ends. + **Default:** `false`. + * `start` {number} Byte offset to begin reading from. When specified, + reads use explicit positioning (`pread` semantics). **Default:** current + file position. + * `limit` {number} Maximum number of bytes to read before ending the + iterator. Reads stop when `limit` bytes have been delivered or EOF is + reached, whichever comes first. **Default:** read until EOF. + * `chunkSize` {number} Size in bytes of the buffer allocated for each + read operation. **Default:** `131072` (128 KB). +* Returns: {AsyncIterable} whose chunks fulfill with {Uint8Array\[]} + +Return the file contents as an async iterable using the +[`node:stream/iter`][] pull model. Reads are performed in `chunkSize`-byte +chunks (default 128 KB). If transforms are provided, they are applied +via [`stream/iter pull()`][]. + +The file handle is locked while the iterable is being consumed and unlocked +when iteration completes, an error occurs, or the consumer breaks. + +This function is only available when the `--experimental-stream-iter` flag is +enabled. + +```mjs +import { open } from 'node:fs/promises'; +import { text } from 'node:stream/iter'; +import { compressGzip } from 'node:zlib/iter'; + +const fh = await open('input.txt', 'r'); + +// Read as text +console.log(await text(fh.pull({ autoClose: true }))); + +// Read 1 KB starting at byte 100 +const fh2 = await open('input.txt', 'r'); +console.log(await text(fh2.pull({ start: 100, limit: 1024, autoClose: true }))); + +// Read with compression +const fh3 = await open('input.txt', 'r'); +const compressed = fh3.pull(compressGzip(), { autoClose: true }); +``` + +```cjs +const { open } = require('node:fs/promises'); +const { text } = require('node:stream/iter'); +const { compressGzip } = require('node:zlib/iter'); + +async function run() { + const fh = await open('input.txt', 'r'); + + // Read as text + console.log(await text(fh.pull({ autoClose: true }))); + + // Read 1 KB starting at byte 100 + const fh2 = await open('input.txt', 'r'); + console.log(await text(fh2.pull({ start: 100, limit: 1024, autoClose: true }))); + + // Read with compression + const fh3 = await open('input.txt', 'r'); + const compressed = fh3.pull(compressGzip(), { autoClose: true }); +} + +run().catch(console.error); +``` + +#### `filehandle.pullSync([...transforms][, options])` + + + +> Stability: 1 - Experimental + +* `...transforms` {Function|Object} Optional transforms to apply via + [`stream/iter pullSync()`][]. +* `options` {Object} + * `autoClose` {boolean} Close the file handle when the stream ends. + **Default:** `false`. + * `start` {number} Byte offset to begin reading from. When specified, + reads use explicit positioning. **Default:** current file position. + * `limit` {number} Maximum number of bytes to read before ending the + iterator. **Default:** read until EOF. + * `chunkSize` {number} Size in bytes of the buffer allocated for each + read operation. **Default:** `131072` (128 KB). +* Returns: {Iterable} whose chunks return {Uint8Array\[]} + +Synchronous counterpart of [`filehandle.pull()`][]. Returns a sync iterable +that reads the file using synchronous I/O on the main thread. Reads are +performed in `chunkSize`-byte chunks (default 128 KB). + +The file handle is locked while the iterable is being consumed. Unlike the +async `pull()`, this method does not support `AbortSignal` since all +operations are synchronous. + +This function is only available when the `--experimental-stream-iter` flag is +enabled. + +```mjs +import { open } from 'node:fs/promises'; +import { textSync, pipeToSync } from 'node:stream/iter'; +import { compressGzipSync, decompressGzipSync } from 'node:zlib/iter'; + +const fh = await open('input.txt', 'r'); + +// Read as text (sync) +console.log(textSync(fh.pullSync({ autoClose: true }))); + +// Sync compress pipeline: file -> gzip -> file +const src = await open('input.txt', 'r'); +const dst = await open('output.gz', 'w'); +pipeToSync(src.pullSync(compressGzipSync(), { autoClose: true }), dst.writer({ autoClose: true })); +``` + +```cjs +const { open } = require('node:fs/promises'); +const { textSync, pipeToSync } = require('node:stream/iter'); +const { compressGzipSync, decompressGzipSync } = require('node:zlib/iter'); + +async function run() { + const fh = await open('input.txt', 'r'); + + // Read as text (sync) + console.log(textSync(fh.pullSync({ autoClose: true }))); + + // Sync compress pipeline: file -> gzip -> file + const src = await open('input.txt', 'r'); + const dst = await open('output.gz', 'w'); + pipeToSync( + src.pullSync(compressGzipSync(), { autoClose: true }), + dst.writer({ autoClose: true }), + ); +} + +run().catch(console.error); +``` + #### `filehandle.read(buffer, offset, length, position)` + +> Stability: 1 - Experimental + +* `options` {Object} + * `autoClose` {boolean} Close the file handle when the writer ends or + fails. **Default:** `false`. + * `start` {number} Byte offset to start writing at. When specified, + writes use explicit positioning. **Default:** current file position. + * `limit` {number} Maximum number of bytes the writer will accept. + Async writes (`write()`, `writev()`) that would exceed the limit reject + with `ERR_OUT_OF_RANGE`. Sync writes (`writeSync()`, `writevSync()`) + return `false`. **Default:** no limit. + * `chunkSize` {number} Maximum chunk size in bytes for synchronous write + operations. Writes larger than this threshold fall back to async I/O. + Set this to match the reader's `chunkSize` for optimal `pipeTo()` + performance. **Default:** `131072` (128 KB). +* Returns: {Object} + * `write(chunk[, options])` {Function} Returns {Promise}. + Accepts `Uint8Array`, `Buffer`, or string (UTF-8 encoded). + * `chunk` {Buffer|TypedArray|DataView|string} + * `options` {Object} + * `signal` {AbortSignal} If the signal is already aborted, the write + rejects with `AbortError` without performing I/O. + * `writev(chunks[, options])` {Function} Returns {Promise}. Uses + scatter/gather I/O via a single `writev()` syscall. Accepts mixed + `Uint8Array`/string arrays. + * `chunks` {Buffer\[]|TypedArray\[]|DataView\[]|string\[]} + * `options` {Object} + * `signal` {AbortSignal} If the signal is already aborted, the write + rejects with `AbortError` without performing I/O. + * `writeSync(chunk)` {Function} Returns {boolean}. Attempts a synchronous + write. Returns `true` if the write succeeded, `false` if the caller + should fall back to async `write()`. Returns `false` when: the writer + is closed/errored, an async operation is in flight, the chunk exceeds + `chunkSize`, or the write would exceed `limit`. + * `chunk` {Buffer|TypedArray|DataView|string} + * `writevSync(chunks)` {Function} Returns {boolean}. Synchronous batch + write. Same fallback semantics as `writeSync()`. + * `chunks` {Buffer\[]|TypedArray\[]|DataView\[]|string\[]} + * `end([options])` {Function} Returns {Promise}, fulfills with the total + number of bytes written. Idempotent: returns `totalBytesWritten` if already + closed, returns the pending promise if already closing. Rejects if the writer + is in an errored state. + * `options` {Object} + * `signal` {AbortSignal} If the signal is already aborted, `end()` + rejects with `AbortError` and the writer remains open. + * `endSync()` {Function} Returns {number|number} total bytes written on + success, `-1` if the writer is errored or an async operation is in + flight. Idempotent when already closed. + * `fail(reason)` {Function} Puts the writer into a terminal error state. + Synchronous. If the writer is already closed or errored, this is a + no-op. If `autoClose` is true, closes the file handle synchronously. + +Return a [`node:stream/iter`][] writer backed by this file handle. + +The writer supports both `Symbol.asyncDispose` and `Symbol.dispose`: + +* `await using w = fh.writer()` — if the writer is still open (no `end()` + called), `asyncDispose` calls `fail()`. If `end()` is pending, it waits + for it to complete. +* `using w = fh.writer()` — calls `fail()` unconditionally. + +The `writeSync()` and `writevSync()` methods enable the try-sync fast path +used by [`stream/iter pipeTo()`][]. When the reader's chunk size matches the +writer's `chunkSize`, all writes in a `pipeTo()` pipeline complete +synchronously with zero promise overhead. + +This function is only available when the `--experimental-stream-iter` flag is +enabled. + +```mjs +import { open } from 'node:fs/promises'; +import { from, pipeTo } from 'node:stream/iter'; +import { compressGzip } from 'node:zlib/iter'; + +// Async pipeline +const fh = await open('output.gz', 'w'); +await pipeTo(from('Hello!'), compressGzip(), fh.writer({ autoClose: true })); + +// Sync pipeline with limit +const src = await open('input.txt', 'r'); +const dst = await open('output.txt', 'w'); +const w = dst.writer({ limit: 1024 * 1024 }); // Max 1 MB +await pipeTo(src.pull({ autoClose: true }), w); +await w.end(); +await dst.close(); +``` + +```cjs +const { open } = require('node:fs/promises'); +const { from, pipeTo } = require('node:stream/iter'); +const { compressGzip } = require('node:zlib/iter'); + +async function run() { + // Async pipeline + const fh = await open('output.gz', 'w'); + await pipeTo(from('Hello!'), compressGzip(), fh.writer({ autoClose: true })); + + // Sync pipeline with limit + const src = await open('input.txt', 'r'); + const dst = await open('output.txt', 'w'); + const w = dst.writer({ limit: 1024 * 1024 }); // Max 1 MB + await pipeTo(src.pull({ autoClose: true }), w); + await w.end(); + await dst.close(); +} + +run().catch(console.error); +``` + #### `filehandle[Symbol.asyncDispose]()` + +* `options` {object} + * `active` {boolean} If `true` (the default), only returns endpoints that are + active (not destroyed, not closing, and not busy). If `false` returns all + endpoints. +* Returns: {quic.QuicEndpoint\[]} + +Returns the list of all `QuicEndpoint` instances. By default, only active +endpoints are returned. + +## `quic.constants` + + + +* {Object} + +An object containing commonly used constants for QUIC configuration. + +### `quic.constants.cc` + +* {Object} + +Congestion control algorithm identifiers, for use with the +[`sessionOptions.cc`][] option: + +* `quic.constants.cc.RENO` — Reno congestion control. +* `quic.constants.cc.CUBIC` — CUBIC congestion control. +* `quic.constants.cc.BBR` — BBR congestion control. + +### `quic.constants.DEFAULT_CIPHERS` + +* {string} + +The default TLS 1.3 cipher suite list used when [`sessionOptions.ciphers`][] +is not specified. + +### `quic.constants.DEFAULT_GROUPS` + +* {string} + +The default TLS 1.3 key-exchange group list used when +[`sessionOptions.groups`][] is not specified. + ## Class: `QuicEndpoint` A `QuicEndpoint` encapsulates the local UDP-port binding for QUIC. It can be @@ -206,10 +666,40 @@ True if `endpoint.destroy()` has been called. Read only. ### `endpoint.listening` + + * Type: {boolean} True if the endpoint is actively listening for incoming connections. Read only. +### `endpoint.maxConnectionsPerHost` + + + +* Type: {number} + +The maximum number of concurrent connections allowed per remote IP address. +`0` means unlimited (default). Can be set at construction time via the +`maxConnectionsPerHost` option and changed dynamically at any time. +The valid range is `0` to `65535`. + +### `endpoint.maxConnectionsTotal` + + + +* Type: {number} + +The maximum total number of concurrent connections across all remote +addresses. `0` means unlimited (default). Can be set at construction time via +the `maxConnectionsTotal` option and changed dynamically at any time. +The valid range is `0` to `65535`. + ### `endpoint.setSNIContexts(entries[, options])` -* Type: {bigint} The total number of QUIC retry attempts on this endpoint. Read only. +* Type: {bigint} The total number of retry packets sent by this endpoint. Read only. + +### `endpointStats.retryRateLimited` + +* Type: {bigint} The total number of retry packets dropped by the global rate + limiter. Read only. A non-zero value indicates the endpoint is under retry + flood pressure. ### `endpointStats.versionNegotiationCount` @@ -352,7 +848,13 @@ added: v23.8.0 added: v23.8.0 --> -* Type: {bigint} The total number sessions rejected due to QUIC version mismatch. Read only. +* Type: {bigint} The total number of version negotiation packets sent by this + endpoint. Read only. + +### `endpointStats.versionNegotiationRateLimited` + +* Type: {bigint} The total number of version negotiation packets dropped by + the global rate limiter. Read only. ### `endpointStats.statelessResetCount` @@ -360,7 +862,13 @@ added: v23.8.0 added: v23.8.0 --> -* Type: {bigint} The total number of stateless resets handled by this endpoint. Read only. +* Type: {bigint} The total number of stateless reset packets sent by this + endpoint. Read only. + +### `endpointStats.statelessResetRateLimited` + +* Type: {bigint} The total number of stateless reset packets dropped by the + global rate limiter. Read only. ### `endpointStats.immediateCloseCount` @@ -368,7 +876,24 @@ added: v23.8.0 added: v23.8.0 --> -* Type: {bigint} The total number of sessions that were closed before handshake completed. Read only. +* Type: {bigint} The total number of immediate connection close packets sent + by this endpoint. Read only. + +### `endpointStats.immediateCloseRateLimited` + +* Type: {bigint} The total number of immediate connection close packets + dropped by the global rate limiter. Read only. + +### `endpointStats.sessionCreationRateLimited` + +* Type: {bigint} The total number of session creation attempts dropped by the + per-host rate limiter. Read only. A non-zero value indicates one or more + remote addresses are creating sessions faster than the configured rate allows. + +### `endpointStats.packetsBlocked` + +* Type: {bigint} The total number of incoming packets dropped by the + block list filter. Read only. ## Class: `QuicSession` @@ -378,18 +903,75 @@ added: v23.8.0 A `QuicSession` represents the local side of a QUIC connection. -### `session.close()` +### `session.applicationOptions` + + + +* Type: {quic.ApplicationOptions} + +The current application-level options for this session. These include settings +that are specific to the negotiated application protocol (e.g. HTTP/3) and may +be negotiated separately from the transport parameters. Read only. +You can use the callback [`session.onapplication`][] to be informed, when settings +from the remote arrive. + +### `session.close([options])` +* `options` {Object} + * `code` {bigint|number} The error code to include in the `CONNECTION_CLOSE` + frame sent to the peer. **Default:** `0` (no error). + * `type` {string} Either `'transport'` or `'application'`. Determines the + error code namespace used in the `CONNECTION_CLOSE` frame. When `'transport'` + (the default), the frame type is `0x1c` and the code is interpreted as a QUIC + transport error. When `'application'`, the frame type is `0x1d` and the code + is application-specific. **Default:** `'transport'`. + * `reason` {string} An optional human-readable reason string included in + the `CONNECTION_CLOSE` frame. Per RFC 9000, this is for diagnostic purposes + only and should not be used for machine-readable error descriptions. * Returns: {Promise} Initiate a graceful close of the session. Existing streams will be allowed to complete but no new streams will be opened. Once all streams have closed, the session will be destroyed. The returned promise will be fulfilled once -the session has been destroyed. +the session has been destroyed. If a non-zero `code` is specified, the +promise will reject with an `ERR_QUIC_TRANSPORT_ERROR` or +`ERR_QUIC_APPLICATION_ERROR` depending on the `type`. + +### `session.opened` + + + +* Type: {Promise} for an {Object} + * `local` {net.SocketAddress} The local socket address. + * `remote` {net.SocketAddress} The remote socket address. + * `servername` {string} The SNI server name negotiated during the handshake. + * `protocol` {string} The ALPN protocol negotiated during the handshake. + * `cipher` {string} The name of the negotiated TLS cipher suite. + * `cipherVersion` {string} The TLS protocol version of the cipher suite + (e.g., `'TLSv1.3'`). + * `validationErrorReason` {string} If certificate validation failed, the + reason string. Empty string if validation succeeded. + * `validationErrorCode` {number} If certificate validation failed, the + error code. `0` if validation succeeded. + * `earlyDataAttempted` {boolean} Whether 0-RTT early data was attempted. + * `earlyDataAccepted` {boolean} Whether 0-RTT early data was accepted by + the server. + +A promise that is fulfilled once the TLS handshake completes successfully. +The resolved value contains information about the established session +including the negotiated protocol, cipher suite, certificate validation +status, and 0-RTT early data status. + +If the handshake fails or the session is destroyed before the handshake +completes, the promise will be rejected. ### `session.closed` @@ -401,16 +983,38 @@ added: v23.8.0 A promise that is fulfilled once the session is destroyed. -### `session.destroy([error])` +### `session.closing` + + + +* Type: {boolean} + +True if [`session.close()`][] has been called and the session has not yet +been destroyed. Read only. + +### `session.destroy([error[, options]])` * `error` {any} - -Immediately destroy the session. All streams will be destroys and the -session will be closed. +* `options` {Object} + * `code` {bigint|number} The error code to include in the `CONNECTION_CLOSE` + frame sent to the peer. **Default:** `0`. + * `type` {string} Either `'transport'` or `'application'`. **Default:** + `'transport'`. + * `reason` {string} An optional human-readable reason string included in + the `CONNECTION_CLOSE` frame. + +Immediately destroy the session. All streams will be destroyed and the +session will be closed. If `error` is provided and [`session.onerror`][] is +set, the `onerror` callback is invoked before destruction. The +`session.closed` promise will reject with the error. If `options` is +provided, the `CONNECTION_CLOSE` frame sent to the peer will include the +specified error code, type, and reason. ### `session.destroyed` @@ -422,15 +1026,55 @@ added: v23.8.0 True if `session.destroy()` has been called. Read only. +### `session.localTransportParams` + + + +* Type: {quic.TransportParams|null} + +The transport parameters advertised by the local endpoint during the handshake. +Returns `null` if the session has been destroyed. Read only. + ### `session.endpoint` -* Type: {quic.QuicEndpoint} +* Type: {quic.QuicEndpoint|null} + +The endpoint that created this session. Returns `null` if the session +has been destroyed. Read only. + +### `session.onapplication` + + + +* Type: {quic.OnApplicationCallback} + +The callback to invoke when new application options, e.g. HTTP/3 settings arrived. + +### `session.onerror` + + + +* Type: {Function|undefined} + +An optional callback invoked when the session is destroyed with an error. +This includes errors caused by user callbacks that throw or reject (see +[Callback error handling][]). The callback receives a single argument: the +error that triggered the destruction. If the `onerror` callback itself throws +or returns a promise that rejects, the error is surfaced as an uncaught +exception. Read/write. -The endpoint that created this session. Read only. +Can also be set via the `onerror` option in [`quic.connect()`][] or +[`quic.listen()`][]. ### `session.onstream` @@ -462,6 +1106,23 @@ added: v23.8.0 The callback to invoke when the status of a datagram is updated. Read/write. +### `session.onearlyrejected` + + + +* Type: {Function|undefined} + +The callback to invoke when the server rejects 0-RTT early data. When +this fires, all streams that were opened during the 0-RTT phase have +been destroyed. The application should re-open streams if needed. +Read/write. + +This callback only fires on the client side when the server rejects +the client's 0-RTT attempt. The connection falls back to 1-RTT and +continues normally. + ### `session.onpathvalidation` + +* Type: {quic.OnNewTokenCallback} + +The callback to invoke when a NEW\_TOKEN token is received from the server. +The token can be passed as the `token` option on a future connection to +the same server to skip address validation. Read/write. + +### `session.onorigin` + + + +* Type: {quic.OnOriginCallback} + +The callback to invoke when an ORIGIN frame (RFC 9412) is received from +the server, indicating which origins the server is authoritative for. +Read/write. + +### `session.ongoaway` + + + +* Type: {Function} + +The callback to invoke when the peer sends an HTTP/3 GOAWAY frame, +indicating it is initiating a graceful shutdown. The callback receives +`(lastStreamId)` where `lastStreamId` is a `{bigint}`: + +* When `lastStreamId` is `-1n`, the peer sent a shutdown notice (intent + to close) without specifying a stream boundary. All existing streams + may still be processed. +* When `lastStreamId` is `>= 0n`, it is the highest stream ID the peer + may have processed. Streams with IDs above this value were NOT + processed and can be safely retried on a new connection. + +After GOAWAY is received, `session.createBidirectionalStream()` will +throw `ERR_INVALID_STATE`. Existing streams continue until they +complete or the session closes. + +This callback is only relevant for HTTP/3 sessions. Read/write. + +### `session.onkeylog` + + + +* Type: {quic.OnKeylogCallback} + +The callback to invoke when TLS key material is available. Requires +[`sessionOptions.keylog`][] to be `true`. Each invocation receives a single +line of [NSS Key Log Format][] text (including a trailing newline). This is +useful for decrypting packet captures with tools like Wireshark. Read/write. + +Can also be set via the `onkeylog` option in [`quic.connect()`][] or +[`quic.listen()`][]. + +### `session.onqlog` + + + +* Type: {quic.OnQlogCallback} + +The callback to invoke when qlog data is available. Requires +[`sessionOptions.qlog`][] to be `true`. The callback receives a string +chunk of [JSON-SEQ][] formatted qlog data and a boolean `fin` flag. When +`fin` is `true`, the chunk is the final qlog output for this session and +the concatenated chunks form a complete qlog trace. Read/write. + +Qlog data arrives during the connection lifecycle. The first chunk contains +the qlog header with format metadata. Subsequent chunks contain trace +events. The final chunk (with `fin` set to `true`) is emitted during +session destruction and completes the JSON-SEQ output. + +Can also be set via the `onqlog` option in [`quic.connect()`][] or +[`quic.listen()`][]. + ### `session.createBidirectionalStream([options])` * `options` {Object} - * `body` {ArrayBuffer | ArrayBufferView | Blob} - * `sendOrder` {number} + * `body` {string | ArrayBuffer | SharedArrayBuffer | ArrayBufferView | + Blob | FileHandle | AsyncIterable | Iterable | Promise | null} + The outbound body source. See [`stream.setBody()`][] for details on + supported types. When omitted, the stream starts half-closed (writable + side open, no body queued). + * `headers` {Object} Initial request or response headers to send. Only + used when the session supports headers (e.g. HTTP/3). If `body` is not + specified and `headers` is provided, the stream is treated as + headers-only (terminal). + * `priority` {string} The priority level of the stream. One of `'high'`, + `'default'`, or `'low'`. **Default:** `'default'`. + * `incremental` {boolean} When `true`, data from this stream may be + interleaved with data from other streams of the same priority level. + When `false`, the stream should be completed before same-priority peers. + **Default:** `false`. + * `highWaterMark` {number} The maximum number of bytes that the writer + will buffer before `writeSync()` returns `false`. When the buffered + data exceeds this limit, the caller should wait for drain before + writing more. **Default:** `65536` (64 KB). + * `onheaders` {Function} Callback for received initial response headers. + Called with `(headers)`. + * `ontrailers` {Function} Callback for received trailing headers. + Called with `(trailers)`. + * `oninfo` {Function} Callback for received informational (1xx) headers. + Called with `(headers)`. + * `onwanttrailers` {Function} Callback when trailers should be sent. + Called with no arguments; use [`stream.sendTrailers()`][] within the + callback. * Returns: {Promise} for a {quic.QuicStream} Open a new bidirectional stream. If the `body` option is not specified, -the outgoing stream will be half-closed. +the outgoing stream will be half-closed. The `priority` and `incremental` +options are only used when the session supports priority (e.g. HTTP/3). +The `headers`, `onheaders`, `ontrailers`, `oninfo`, and `onwanttrailers` +options are only used when the session supports headers (e.g. HTTP/3). ### `session.createUnidirectionalStream([options])` @@ -523,12 +1300,33 @@ added: v23.8.0 --> * `options` {Object} - * `body` {ArrayBuffer | ArrayBufferView | Blob} - * `sendOrder` {number} + * `body` {string | ArrayBuffer | SharedArrayBuffer | ArrayBufferView | + Blob | FileHandle | AsyncIterable | Iterable | Promise | null} + The outbound body source. See [`stream.setBody()`][] for details on + supported types. When omitted, the stream is closed immediately. + * `headers` {Object} Initial request headers to send. + * `priority` {string} The priority level of the stream. One of `'high'`, + `'default'`, or `'low'`. **Default:** `'default'`. + * `incremental` {boolean} When `true`, data from this stream may be + interleaved with data from other streams of the same priority level. + When `false`, the stream should be completed before same-priority peers. + **Default:** `false`. + * `highWaterMark` {number} The maximum number of bytes that the writer + will buffer before `writeSync()` returns `false`. When the buffered + data exceeds this limit, the caller should wait for drain before + writing more. **Default:** `65536` (64 KB). + * `onheaders` {Function} Callback for received initial response headers. + Called with `(headers)`. + * `ontrailers` {Function} Callback for received trailing headers. + Called with `(trailers)`. + * `oninfo` {Function} Callback for received informational (1xx) headers. + Called with `(headers)`. + * `onwanttrailers` {Function} Callback when trailers should be sent. * Returns: {Promise} for a {quic.QuicStream} Open a new unidirectional stream. If the `body` option is not specified, -the outgoing stream will be closed. +the outgoing stream will be closed. The `priority` and `incremental` +options are only used when the session supports priority (e.g. HTTP/3). ### `session.path` @@ -542,18 +1340,135 @@ added: v23.8.0 The local and remote socket addresses associated with the session. Read only. -### `session.sendDatagram(datagram)` +### `session.remoteTransportParams` + + + +* Type: {quic.TransportParams|null|undefined} + +The transport parameters advertised by the remote peer during the handshake. +Returns `null` if the session has been destroyed, `undefined` if the handshake +has not yet completed and the remote parameters are not yet available. Read +only. + +### `session.sendDatagram(datagram[, encoding])` -* `datagram` {string|ArrayBufferView} -* Returns: {bigint} +* `datagram` {string|ArrayBufferView|Promise} +* `encoding` {string} The encoding to use if `datagram` is a string. + **Default:** `'utf8'`. +* Returns: {Promise} for a {bigint} datagram ID. + +Sends an unreliable datagram to the remote peer, returning a promise for +the datagram ID. + +If `datagram` is a string, it will be encoded using the specified `encoding`. + +If `datagram` is an `ArrayBufferView`, the bytes are copied into an +internal buffer; the caller's source buffer is unchanged and may be reused +or mutated immediately after the call returns. Callers that want to ensure +their source cannot be mutated after the call (for example, when handing +the buffer off to another async consumer) can call +`ArrayBuffer.prototype.transfer()` themselves before passing the buffer. + +If `datagram` is a `Promise`, it will be awaited before sending. If the +session closes while awaiting, `0n` is returned silently (datagrams are +inherently unreliable). + +If the datagram payload is zero-length (empty string after encoding, detached +buffer, or zero-length view), `0n` is returned and no datagram is sent. + +For HTTP/3 sessions, the peer must advertise `SETTINGS_H3_DATAGRAM=1` +(via `application: { enableDatagrams: true }`) for datagrams to be sent. +If the peer's setting is `0`, `sendDatagram()` returns `0n` (per RFC 9297 +§3, an endpoint MUST NOT send HTTP Datagrams unless the peer indicated +support). + +Datagrams cannot be fragmented — each must fit within a single QUIC packet. +The maximum datagram size is determined by the peer's +`maxDatagramFrameSize` transport parameter (which the peer advertises during +the handshake). If the peer sets this to `0`, datagrams are not supported +and `0n` will be returned. If the datagram exceeds the peer's limit, it +will be silently dropped and `0n` returned. The local +`maxDatagramFrameSize` transport parameter (default: `1200` bytes) controls +what this endpoint advertises to the peer as its own maximum. + +### `session.certificate` + + + +* Type: {crypto.X509Certificate|undefined} + +The local certificate as a [`crypto.X509Certificate`][] instance. Server +sessions return the certificate configured for the negotiated SNI host. +Client sessions return `undefined` unless a client certificate was sent. +Returns `undefined` if the session is destroyed. + +### `session.peerCertificate` + + + +* Type: {crypto.X509Certificate|undefined} + +The peer's certificate as a [`crypto.X509Certificate`][] instance. Returns +`undefined` if the peer did not present a certificate or the session is +destroyed. + +### `session.ephemeralKeyInfo` + + + +* Type: {Object|undefined} + +The ephemeral key information for the session, with properties such as +`type`, `name`, and `size`. Only available on client sessions. Returns +`undefined` for server sessions or if the session is destroyed. + +### `session.maxDatagramSize` + + + +* Type: {number} + +The maximum datagram payload size in bytes that the peer will accept. +This is derived from the peer's `maxDatagramFrameSize` transport +parameter minus the DATAGRAM frame overhead (type byte and variable-length +integer encoding). Returns `0` if the peer does not support datagrams or +if the handshake has not yet completed. Datagrams larger than this value +will not be sent. + +### `session.maxPendingDatagrams` + + + +* Type: {number} +* **Default:** `128` + +The maximum number of datagrams that can be queued for sending. Datagrams +are queued when `sendDatagram()` is called and sent opportunistically +alongside stream data by the packet serialization loop. When the queue +is full, the [`sessionOptions.datagramDropPolicy`][] determines whether +the oldest or newest datagram is dropped. Dropped datagrams are reported +as lost via the `ondatagramstatus` callback. -Sends an unreliable datagram to the remote peer, returning the datagram ID. -If the datagram payload is specified as an `ArrayBufferView`, then ownership of -that view will be transferred to the underlying stream. +This property can be changed dynamically to adjust queue capacity +based on application activity or memory pressure. The valid range +is `0` to `65535`. ### `session.stats` @@ -668,7 +1583,7 @@ added: v23.8.0 * Type: {bigint} -### `sessionStats.maxBytesInFlights` +### `sessionStats.maxBytesInFlight` +* Type: {bigint} The total number of peer-initiated streams destroyed by the + stream idle timeout. Read only. -### `stream.closed` +## Class: `QuicError` -* Type: {Promise} +> Stability: 1 - Experimental -A promise that is fulfilled when the stream is fully closed. +A `QuicError` is an `Error` subclass that carries an explicit numeric +QUIC error code. Use it to abort a QUIC stream or session with a +specific application-protocol-defined error code rather than letting +the implementation pick a generic fallback. -### `stream.destroy([error])` +The class is exported from `node:quic`: - +```mjs +import { QuicError } from 'node:quic'; +``` -* `error` {any} +```cjs +const { QuicError } = require('node:quic'); +``` -Immediately and abruptly destroys the stream. +When a `QuicError` is supplied to APIs that emit a wire frame +([`writer.fail()`][], [`stream.destroy()`][]), the QUIC stack uses +[`error.errorCode`][] as the wire code for the resulting frame. +When any other value is supplied (for example a plain `Error`), the +implementation falls back to the negotiated application protocol's +"internal error" code (`H3_INTERNAL_ERROR` (`0x102`) for HTTP/3, or +the QUIC transport-layer `INTERNAL_ERROR` (`0x1`) for raw QUIC). -### `stream.destroyed` +The Node.js error code (`error.code`) defaults to +`'ERR_QUIC_STREAM_ABORTED'`. Callers who need a more specific code +string can override it via `options.code` — the numeric QUIC code +is unaffected. + +The Node.js error code is fixed at `'ERR_QUIC_STREAM_ABORTED'` so that +catch blocks can distinguish a `QuicError` from other Node.js errors +without checking the prototype chain. The numeric QUIC code lives on +the separate [`error.errorCode`][] property to avoid colliding with +the Node.js convention that `error.code` is a string. + +### `new QuicError(message, options)` -* Type: {boolean} +* `message` {string} A human-readable description of the error. +* `options` {Object} + * `errorCode` {bigint | number} The numeric QUIC error code. Numbers + are coerced to `BigInt`. Must be a non-negative 62-bit unsigned + varint (`0n <= errorCode <= 2n ** 62n - 1n`). + * `code` {string} The Node.js-style error code string assigned to + `error.code`. Defaults to `'ERR_QUIC_STREAM_ABORTED'`. + * `type` {string} Either `'application'` (default) or `'transport'`. + Indicates whether the code is defined by the negotiated + application protocol (e.g. RFC 9114 for HTTP/3) or by the QUIC + transport layer (RFC 9000). Stream resets always carry application + codes, so the default is `'application'`. -True if `stream.destroy()` has been called. +```mjs +import { QuicError } from 'node:quic'; -### `stream.direction` +const err = new QuicError('rejecting stream', { errorCode: 0x10cn }); +console.log(err.code); // 'ERR_QUIC_STREAM_ABORTED' +console.log(err.errorCode); // 268n +console.log(err.type); // 'application' + +const custom = new QuicError('custom failure', { + errorCode: 0x10cn, + code: 'ERR_MY_QUIC_FAILURE', +}); +console.log(custom.code); // 'ERR_MY_QUIC_FAILURE' +``` + +### `error.errorCode` -* Type: {string} One of either `'bidi'` or `'uni'`. +* Type: {bigint} -The directionality of the stream. Read only. +The numeric QUIC error code carried by this error. -### `stream.id` +### `error.type` -* Type: {bigint} +* Type: {string} -The stream ID. Read only. +Either `'application'` or `'transport'`. Indicates the namespace of +[`error.errorCode`][]. -### `stream.onblocked` +## Class: `QuicStream` -* Type: {quic.OnBlockedCallback} - -The callback to invoke when the stream is blocked. Read/write. - -### `stream.onreset` +### `stream.closed` -* Type: {quic.OnStreamErrorCallback} +* Type: {Promise} -The callback to invoke when the stream is reset. Read/write. +A promise that is fulfilled when the stream is fully closed. It resolves +when the stream closes cleanly (including idle timeout). It rejects with +an `ERR_QUIC_APPLICATION_ERROR` or `ERR_QUIC_TRANSPORT_ERROR` when the +stream is closed due to a QUIC error (e.g., stream reset by the peer, +CONNECTION\_CLOSE with a non-zero error code). -### `stream.readable` +### `stream.destroy([error[, options]])` -* Type: {ReadableStream} +* `error` {any} +* `options` {Object} + * `code` {bigint|number} The application error code to include in the + `RESET_STREAM` and `STOP_SENDING` frames sent to the peer. Numbers are + coerced to `BigInt`. When omitted, the wire code is derived from `error` + (see below). + * `reason` {string} An optional human-readable reason string. Accepted for + symmetry with [`session.close()`][] and [`session.destroy()`][], but + **not transmitted on the wire** — neither `RESET_STREAM` nor + `STOP_SENDING` carry a reason field. Provided for application logging + and for use by the [`stream.onerror`][] callback. + +Immediately and abruptly destroys the stream. If `error` is provided and +[`stream.onerror`][] is set, the `onerror` callback is invoked before +destruction. The `stream.closed` promise rejects with the error. + +When the stream is destroyed with an `error` (or with an explicit +`options.code`), the QUIC stack signals the abort to the peer: + +* If the writable side is still open, a `RESET_STREAM` frame is sent. +* If the readable side is still open (a bidirectional stream, or a + remote-initiated unidirectional stream), a `STOP_SENDING` frame is sent. + +Both frames carry the same wire code, resolved with the following +precedence: + +1. `options.code`, when explicitly provided. +2. [`error.errorCode`][], when `error` is a [`QuicError`][]. +3. The negotiated application protocol's "internal error" code + (`H3_INTERNAL_ERROR` (`0x102`) for HTTP/3, or the QUIC transport-layer + `INTERNAL_ERROR` (`0x1`) for raw QUIC). + +A clean destroy — no `error` and no `options.code` — does not emit +`RESET_STREAM` or `STOP_SENDING`; the stream's existing close machinery +handles teardown. + +See [Aborting a stream][] for an overview of the available stream-abort +APIs. -### `stream.session` +### `stream.destroyed` -* Type: {quic.QuicSession} +* Type: {boolean} -The session that created this stream. Read only. +True if `stream.destroy()` has been called. -### `stream.stats` +### Aborting a stream - +A QuicStream can be aborted in three ways, each producing different +wire-frame side effects: -* Type: {quic.QuicStream.Stats} +* [`writer.fail(reason)`][] — Aborts only the writable side. Sends + `RESET_STREAM` to the peer. The readable side is unaffected; any data + already buffered for read remains available. +* [`stream.destroy()`][] with an `error` argument — Tears the stream + down completely. Sends `RESET_STREAM` on any still-open writable side + **and** `STOP_SENDING` on any still-open readable side. The wire code + is derived from `error` (see [`stream.destroy()`][] for the precedence + rules). +* [`stream.destroy()`][] with an explicit `options.code` — Same as the + previous form but with a caller-supplied wire code, which takes + precedence over any code carried by `error`. -The current statistics for the stream. Read only. +When `error` is a [`QuicError`][], its [`error.errorCode`][] is used as +the wire code for both `writer.fail()` and `stream.destroy()`. Otherwise +the implementation falls back to the negotiated application protocol's +"internal error" code (see [`QuicError`][]). -## Class: `QuicStream.Stats` +### `stream.early` -### `streamStats.ackedAt` +* Type: {boolean} - +True if any data on this stream was received as 0-RTT (early data) +before the TLS handshake completed. Early data is less secure and +could potentially be replayed by an attacker. Applications should +treat early data with appropriate caution. -* Type: {bigint} +This property is only meaningful on the server side. On the client +side, it is always `false`. -### `streamStats.bytesReceived` +### `stream.direction` -* Type: {bigint} +* Type: {string|null} One of `'bidi'`, `'uni'`, or `null`. -### `streamStats.bytesSent` +The directionality of the stream, or `null` if the stream has been destroyed +or is still pending. Read only. + +### `stream.highWaterMark` -* Type: {bigint} - -### `streamStats.createdAt` +* Type: {number} - +The maximum number of bytes that the writer will buffer before +`writeSync()` returns `false`. When the buffered data exceeds this limit, +the caller should wait for drain before writing more. -* Type: {bigint} +The value can be changed dynamically at any time. This is particularly +useful for streams received via the `onstream` callback, where the +default (65536) may need to be adjusted based on application needs. +The valid range is `0` to `4294967295`. -### `streamStats.destroyedAt` +### `stream.id` -* Type: {bigint} +* Type: {bigint|null} -### `streamStats.finalSize` +The stream ID, or `null` if the stream has been destroyed or is still +pending. Read only. + +### `stream.onerror` -* Type: {bigint} +* Type: {Function|undefined} -### `streamStats.isConnected` +An optional callback invoked when the stream is destroyed with an error. +This includes errors caused by user callbacks that throw or reject (see +[Callback error handling][]). The callback receives a single argument: the +error that triggered the destruction. If the `onerror` callback itself throws +or returns a promise that rejects, the error is surfaced as an uncaught +exception. Read/write. + +### `stream.onblocked` -* Type: {bigint} +* Type: {quic.OnBlockedCallback} -### `streamStats.maxOffset` +The callback to invoke when the stream is blocked. Read/write. + +### `stream.onreset` -* Type: {bigint} +* Type: {quic.OnStreamErrorCallback} -### `streamStats.maxOffsetAcknowledged` +The callback to invoke when the peer aborts a direction of the stream by +sending a `RESET_STREAM` frame (the peer abandons their writable side, so +no further data will arrive on our readable side) or a `STOP_SENDING` +frame (the peer asks us to stop writing on our writable side). - +The callback receives a Node.js error whose `errorCode` (`bigint`) +property carries the application error code from the wire frame. -* Type: {bigint} +The stream is **not** automatically destroyed when this callback fires — +the application chooses how to react. Common patterns are: ignore (and +continue using the still-active direction on a bidirectional stream), +abort the other direction with [`writer.fail()`][], or tear down the +whole stream with [`stream.destroy()`][]. Read/write. -### `streamStats.maxOffsetReceived` +### `stream.headers` -* Type: {bigint} - -### `streamStats.openedAt` +* Type: {Object|undefined} - +The buffered initial headers received on this stream, or `undefined` if the +application does not support headers or no headers have been received yet. +For server-side streams, this contains the request headers (e.g., `:method`, +`:path`, `:scheme`). For client-side streams, this contains the response +headers (e.g., `:status`). -* Type: {bigint} +Header names are lowercase strings. Multi-value headers are represented as +arrays. The object has `__proto__: null`. -### `streamStats.receivedAt` +### `stream.onheaders` -* Type: {bigint} +* Type: {Function} -## Types +The callback to invoke when initial headers are received on the stream. The +callback receives `(headers)` where `headers` is an object (same format as +`stream.headers`). For HTTP/3, this delivers request pseudo-headers on the +server side and response headers on the client side. Throws +`ERR_INVALID_STATE` if set on a session that does not support headers. +Read/write. -### Type: `EndpointOptions` +### `stream.ontrailers` -* Type: {Object} +* Type: {Function} -The endpoint configuration options passed when constructing a new `QuicEndpoint` instance. +The callback to invoke when trailing headers are received from the peer. +The callback receives `(trailers)` where `trailers` is an object in the +same format as `stream.headers`. Throws `ERR_INVALID_STATE` if set on a +session that does not support headers. Read/write. -#### `endpointOptions.address` +### `stream.oninfo` -* Type: {net.SocketAddress | string} The local UDP address and port the endpoint should bind to. +* Type: {Function} -If not specified the endpoint will bind to IPv4 `localhost` on a random port. +The callback to invoke when informational (1xx) headers are received from +the server. The callback receives `(headers)` where `headers` is an object +in the same format as `stream.headers`. Informational headers are sent +before the final response (e.g., 103 Early Hints). Throws +`ERR_INVALID_STATE` if set on a session that does not support headers. +Read/write. -#### `endpointOptions.addressLRUSize` +### `stream.onwanttrailers` -* Type: {bigint|number} +* Type: {Function} -The endpoint maintains an internal cache of validated socket addresses as a -performance optimization. This option sets the maximum number of addresses -that are cache. This is an advanced option that users typically won't have -need to specify. +The callback to invoke when the application is ready for trailing headers +to be sent. This is called synchronously — the user must call +[`stream.sendTrailers()`][] within this callback. Throws +`ERR_INVALID_STATE` if set on a session that does not support headers. +Read/write. -#### `endpointOptions.ipv6Only` +### `stream.pendingTrailers` -* Type: {boolean} +* Type: {Object|undefined} -When `true`, indicates that the endpoint should bind only to IPv6 addresses. +Set trailing headers to be sent automatically when the application requests +them. This is an alternative to the [`stream.onwanttrailers`][] callback +for cases where the trailers are known before the body completes. Throws +`ERR_INVALID_STATE` if set on a session that does not support headers. +Read/write. -#### `endpointOptions.reusePort` +### `stream.sendHeaders(headers[, options])` -* Type: {boolean} -* Default: `false` - -When `true`, allows multiple endpoints (across separate processes) to bind to -the same address and port. The kernel will load-balance incoming UDP datagrams -across all sockets bound with this option. This enables horizontal scaling of -QUIC servers by running multiple Node.js processes on the same port. +* `headers` {Object} Header object with string keys and string or + string-array values. Pseudo-headers (`:method`, `:path`, etc.) must + appear before regular headers. +* `options` {Object} + * `terminal` {boolean} If `true`, the stream is closed for sending + after the headers (no body will follow). **Default:** `false`. +* Returns: {boolean} -Supported on Linux 3.9+ and DragonFlyBSD 3.6+. On unsupported platforms, the -bind will fail with an error. +Sends initial or response headers on the stream. For client-side streams, +this sends request headers. For server-side streams, this sends response +headers. Throws `ERR_INVALID_STATE` if the session does not support headers. -#### `endpointOptions.maxConnectionsPerHost` +### `stream.sendInformationalHeaders(headers)` -* Type: {bigint|number} +* `headers` {Object} Header object. Must include `:status` with a 1xx + value (e.g., `{ ':status': '103', 'link': '; rel=preload' }`). +* Returns: {boolean} -Specifies the maximum number of concurrent sessions allowed per remote peer address. +Sends informational (1xx) response headers. Server only. Throws +`ERR_INVALID_STATE` if the session does not support headers. -#### `endpointOptions.maxConnectionsTotal` +### `stream.sendTrailers(headers)` -* Type: {bigint|number} +* `headers` {Object} Trailing header object. Pseudo-headers must not be + included in trailers. +* Returns: {boolean} -Specifies the maximum total number of concurrent sessions. +Sends trailing headers on the stream. Must be called synchronously during +the [`stream.onwanttrailers`][] callback, or set ahead of time via +[`stream.pendingTrailers`][]. Throws `ERR_INVALID_STATE` if the session +does not support headers. -#### `endpointOptions.maxRetries` +### `stream.priority` -* Type: {bigint|number} +* Type: {Object|null} + * `level` {string} One of `'high'`, `'default'`, or `'low'`. + * `incremental` {boolean} Whether the stream data should be interleaved + with other streams of the same priority level. -Specifies the maximum number of QUIC retry attempts allowed per remote peer address. +The current priority of the stream. Returns `null` if the session does not +support priority (e.g. non-HTTP/3) or if the stream has been destroyed. +Read only. Use [`stream.setPriority()`][] to change the priority. -#### `endpointOptions.maxStatelessResetsPerHost` +On client-side HTTP/3 sessions, the value reflects what was set via +[`stream.setPriority()`][]. On server-side HTTP/3 sessions, the value +reflects the peer's requested priority (e.g., from `PRIORITY_UPDATE` frames). + +### `stream.setPriority([options])` -* Type: {bigint|number} +* `options` {Object} + * `level` {string} The priority level. One of `'high'`, `'default'`, or + `'low'`. **Default:** `'default'`. + * `incremental` {boolean} When `true`, data from this stream may be + interleaved with data from other streams of the same priority level. + **Default:** `false`. -Specifies the maximum number of stateless resets that are allowed per remote peer address. +Sets the priority of the stream. Throws `ERR_INVALID_STATE` if the session +does not support priority (e.g. non-HTTP/3). Has no effect if the stream +has been destroyed. -#### `endpointOptions.retryTokenExpiration` +### `stream[Symbol.asyncIterator]()` -* Type: {bigint|number} +* Returns: {AsyncIterableIterator} yielding {Uint8Array\[]} -Specifies the length of time a QUIC retry token is considered valid. +The stream implements `Symbol.asyncIterator`, making it directly usable +in `for await...of` loops. Each iteration yields a batch of `Uint8Array` +chunks. -#### `endpointOptions.resetTokenSecret` +Only one async iterator can be obtained per stream. A second call throws +`ERR_INVALID_STATE`. Non-readable streams (outbound-only unidirectional +or closed) return an immediately-finished iterator. - +```mjs +for await (const chunks of stream) { + for (const chunk of chunks) { + // Process each Uint8Array chunk + } +} +``` -* Type: {ArrayBufferView} +Compatible with stream/iter utilities: -Specifies the 16-byte secret used to generate QUIC retry tokens. +```mjs +import Stream from 'node:stream/iter'; +const body = await Stream.bytes(stream); +const text = await Stream.text(stream); +await Stream.pipeTo(stream, someWriter); +``` -#### `endpointOptions.tokenExpiration` +### `stream.writer` -* Type: {bigint|number} +* Type: {Object} -Specifies the length of time a QUIC token is considered valid. +Returns a Writer object for pushing data to the stream incrementally. +The Writer implements the stream/iter Writer interface with the +try-sync-fallback-to-async pattern. + +Only available when no `body` source was provided at creation time or via +[`stream.setBody()`][]. Non-writable streams return an already-closed +Writer. Throws `ERR_INVALID_STATE` if the outbound is already configured. + +The Writer has the following methods: + +* `writeSync(chunk)` — Synchronous write. Returns `true` if accepted, + `false` if flow-controlled. Data is NOT accepted on `false`. +* `write(chunk[, options])` — Async write with drain wait. `options.signal` + is checked at entry but not observed during the write. +* `writevSync(chunks)` — Synchronous vectored write. All-or-nothing. +* `writev(chunks[, options])` — Async vectored write. +* `endSync()` — Synchronous close. Returns total bytes or `-1`. +* `end([options])` — Async close. +* `fail(reason)` — Errors the stream (sends `RESET_STREAM` to peer). + When `reason` is a [`QuicError`][], its [`error.errorCode`][] is used + as the wire code on the resulting `RESET_STREAM` frame; otherwise + the wire code falls back to the negotiated application protocol's + "internal error" code (`H3_INTERNAL_ERROR` (`0x102`) for HTTP/3, or + the QUIC transport-layer `INTERNAL_ERROR` (`0x1`) for raw QUIC). + See [`stream.destroy()`][] for a full-stream abort that also resets + the readable side via `STOP_SENDING`. +* `desiredSize` — Available capacity in bytes, or `null` if closed/errored. + +The bytes from each `writeSync()` / `writevSync()` / `write()` / `writev()` +input chunk are copied into an internal buffer, so the caller's source +buffer is unchanged and may be reused or mutated immediately after the +call returns. Callers that want to ensure a source buffer cannot be +mutated after handing it off can call `ArrayBuffer.prototype.transfer()` +themselves before passing the buffer. + +### `stream.setBody(body)` + + + +* `body` {string | ArrayBuffer | SharedArrayBuffer | ArrayBufferView | + Blob | FileHandle | AsyncIterable | Iterable | Promise | null} + +Sets the outbound body source for the stream. Can only be called once. +Mutually exclusive with [`stream.writer`][]. + +The following body source types are supported: + +* `null` — The writable side is closed immediately (FIN sent with no data). +* `string` — UTF-8 encoded and sent as a single chunk. +* `ArrayBuffer`, `SharedArrayBuffer`, `ArrayBufferView` — Sent as a single + chunk. The bytes are copied into an internal buffer, so the caller's + source buffer is unchanged and may be reused or mutated immediately + after the call returns. Callers wanting to ensure their source cannot + be mutated after handing it off can call + `ArrayBuffer.prototype.transfer()` themselves before passing the buffer. +* `Blob` — Sent from the Blob's underlying data queue. +* {FileHandle} — The file contents are read asynchronously via an + fd-backed data source. The `FileHandle` must be opened for reading + (e.g. via [`fs.promises.open(path, 'r')`][]). Once passed as a body, the + `FileHandle` is locked and cannot be used as a body for another stream. + The `FileHandle` is automatically closed when the stream finishes. +* `AsyncIterable`, `Iterable` — Each yielded chunk (string or + `Uint8Array`) is written incrementally in streaming mode. +* `Promise` — Awaited; the resolved value is used as the body (subject + to the same type rules). + +Throws `ERR_INVALID_STATE` if the outbound is already configured or if +the writer has been accessed. -#### `endpointOptions.tokenSecret` +### `stream.session` -* Type: {ArrayBufferView} +* Type: {quic.QuicSession|null} -Specifies the 16-byte secret used to generate QUIC tokens. +The session that created this stream, or `null` if the stream has been +destroyed. Read only. -#### `endpointOptions.udpReceiveBufferSize` +### `stream.stats` -* Type: {number} +* Type: {quic.QuicStream.Stats} -#### `endpointOptions.udpSendBufferSize` +The current statistics for the stream. Read only. + +## Class: `QuicStream.Stats` -* Type: {number} - -#### `endpointOptions.udpTTL` +### `streamStats.ackedAt` -* Type: {number} +* Type: {bigint} -#### `endpointOptions.validateAddress` +### `streamStats.bytesAccumulated` -* Type: {boolean} +* Type: {bigint} -When `true`, requires that the endpoint validate peer addresses using retry packets -while establishing a new connection. +The current number of bytes sitting in the stream's receive accumulation +buffer, awaiting delivery to the application. A value near zero indicates +the reader is keeping up with incoming data. A value near the stream's +flow control window indicates the application is not consuming data fast +enough. -### Type: `SessionOptions` +### `streamStats.bytesReceived` -#### `sessionOptions.alpn` +* Type: {bigint} + +### `streamStats.bytesSent` -* Type: {string} (client) | {string\[]} (server) - -The ALPN (Application-Layer Protocol Negotiation) identifier(s). - -For **client** sessions, this is a single string specifying the protocol -the client wants to use (e.g. `'h3'`). +* Type: {bigint} -For **server** sessions, this is an array of protocol names in preference -order that the server supports (e.g. `['h3', 'h3-29']`). During the TLS -handshake, the server selects the first protocol from its list that the -client also supports. +### `streamStats.createdAt` -The negotiated ALPN determines which Application implementation is used -for the session. `'h3'` and `'h3-*'` variants select the HTTP/3 -application; all other values select the default application. + -Default: `'h3'` +* Type: {bigint} -#### `sessionOptions.ca` (client only) +### `streamStats.destroyedAt` -* Type: {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} - -The CA certificates to use for client sessions. For server sessions, CA -certificates are specified per-identity in the [`sessionOptions.sni`][] map. +* Type: {bigint} -#### `sessionOptions.cc` +### `streamStats.finalSize` -* Type: {string} - -Specifies the congestion control algorithm that will be used -. Must be set to one of either `'reno'`, `'cubic'`, or `'bbr'`. - -This is an advanced option that users typically won't have need to specify. +* Type: {bigint} -#### `sessionOptions.certs` (client only) +### `streamStats.isConnected` -* Type: {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} - -The TLS certificates to use for client sessions. For server sessions, -certificates are specified per-identity in the [`sessionOptions.sni`][] map. +* Type: {bigint} -#### `sessionOptions.ciphers` +### `streamStats.maxBytesAccumulated` -* Type: {string} +* Type: {bigint} -The list of supported TLS 1.3 cipher algorithms. +The peak number of bytes that were accumulated in the stream's receive +buffer at any point during the stream's lifetime. This value only +increases monotonically. It is useful for diagnosing whether a stream +experienced backpressure episodes and whether the accumulation buffer +sizing is appropriate for the workload. -#### `sessionOptions.crl` (client only) +### `streamStats.maxOffset` -* Type: {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} - -The CRL to use for client sessions. For server sessions, CRLs are specified -per-identity in the [`sessionOptions.sni`][] map. +* Type: {bigint} -#### `sessionOptions.groups` +### `streamStats.maxOffsetAcknowledged` -* Type: {string} - -The list of support TLS 1.3 cipher groups. +* Type: {bigint} -#### `sessionOptions.keylog` +### `streamStats.maxOffsetReceived` -* Type: {boolean} - -True to enable TLS keylogging output. +* Type: {bigint} -#### `sessionOptions.keys` (client only) +### `streamStats.openedAt` -* Type: {KeyObject|KeyObject\[]} - -The TLS crypto keys to use for client sessions. For server sessions, -keys are specified per-identity in the [`sessionOptions.sni`][] map. +* Type: {bigint} -#### `sessionOptions.maxPayloadSize` +### `streamStats.receivedAt` -* Type: {bigint|number} +* Type: {bigint} -Specifies the maximum UDP packet payload size. +## Types -#### `sessionOptions.maxStreamWindow` +### type: `ApplicationOptions` -* Type: {bigint|number} +* Type: {Object} + +The application specific options. + +#### `applicationOptions.maxHeaderPairs` + +* Type: {bigint|number} + +Maximum number of header name-value pairs accepted per header block. +Headers beyond this limit are silently dropped. **Default:** `128` + +#### `applicationOptions.maxHeaderLength` + +* Type: {bigint|number} + +Maximum total byte length of all header names and values combined per header +block. Headers that would push the total over this limit are silently +dropped. **Default:** `8192` + +#### `applicationOptions.maxFieldSectionSize` + +* Type: {bigint|number} + +Maximum size of a compressed header field section (QPACK). `0` means +unlimited. **Default:** `0` + +#### `applicationOptions.qpackMaxDTableCapacity` + +* Type: {bigint|number} + +QPACK dynamic table capacity in bytes. Set to `0` to disable the dynamic +table. **Default:** `4096` + +#### `applicationOptions.qpackEncoderMaxDTableCapacity` + +* Type: {bigint|number} + +QPACK encoder maximum dynamic table capacity. **Default:** `4096` + +#### `applicationOptions.qpackBlockedStreams` + +* Type: {bigint|number} + +Maximum number of streams that can e blocked waiting for QPACK dynamic table +updates. **Default:** `100` + +#### `applicationOptions.enableConnectProtocol` + +* Type: {boolean} + +Enable the extended CONNECT protocol (RFC 9220). **Default:** `false` + +#### `applicationOptions.enableDatagrams` + +* Type: {boolean} + +Enable HTTP/3 datagrams (RFC 9297). **Default:** `false` + +### Type: `EndpointOptions` + + + +* Type: {Object} + +The endpoint configuration options passed when constructing a new `QuicEndpoint` instance. + +#### `endpointOptions.address` + + + +* Type: {net.SocketAddress | string} The local UDP address and port the endpoint should bind to. + +If not specified the endpoint will bind to IPv4 `localhost` on a random port. + +#### `endpointOptions.blockList` + +* Type: {net.BlockList} + +An optional [`net.BlockList`][] instance for filtering incoming packets by +source address. When configured, every received UDP packet is checked against +the block list before any QUIC processing occurs, minimizing resource +expenditure on blocked sources. The block list is evaluated live — rules +added to the `BlockList` object after the endpoint is created take effect +immediately. + +See [`endpointOptions.blockListPolicy`][] for how matches are interpreted. + +#### `endpointOptions.blockListPolicy` + +* Type: {string} One of `'deny'` or `'allow'`. +* **Default:** `'deny'` + +Controls how the [`endpointOptions.blockList`][] is interpreted: + +* `'deny'` — Packets from addresses matching the block list are dropped. + All other addresses are accepted. This is the typical blocklist mode. +* `'allow'` — Only packets from addresses matching the block list are + accepted. All other addresses are dropped. This is an allowlist mode + for restricting access to known clients. + +If no block list is configured, this option has no effect. + +#### `endpointOptions.addressLRUSize` + + + +* Type: {bigint|number} + +The endpoint maintains an internal cache of validated socket addresses as a +performance optimization. This option sets the maximum number of addresses +that are cached. This is an advanced option that users typically won't have +need to specify. + +#### `endpointOptions.disableStatelessReset` + + + +* Type: {boolean} + +When `true`, the endpoint will not send stateless reset packets in response +to packets from unknown connections. Stateless resets allow a peer to detect +that a connection has been lost even when the server has no state for it. +Disabling them may be useful in testing or when stateless resets are handled +at a different layer. + +#### `endpointOptions.idleTimeout` + + + +* Type: {number} +* Default: `0` + +The number of seconds an endpoint will remain alive after all sessions have +closed and it is no longer listening. A value of `0` (default) means the +endpoint is only destroyed when explicitly closed via `endpoint.close()` or +`endpoint.destroy()`. A positive value starts an idle timer when the endpoint +becomes idle; if no new sessions are created before the timer fires, the +endpoint is automatically destroyed. This is useful for connection pooling +where endpoints should linger briefly for reuse by future `connect()` calls. + +#### `endpointOptions.ipv6Only` + + + +* Type: {boolean} + +When `true`, indicates that the endpoint should bind only to IPv6 addresses. + +#### `endpointOptions.reusePort` + + + +* Type: {boolean} +* Default: `false` + +When `true`, allows multiple endpoints (across separate processes) to bind to +the same address and port. The kernel will load-balance incoming UDP datagrams +across all sockets bound with this option. This enables horizontal scaling of +QUIC servers by running multiple Node.js processes on the same port. + +Supported on Linux 3.9+ and DragonFlyBSD 3.6+. On unsupported platforms, the +bind will fail with an error. + +#### `endpointOptions.maxConnectionsPerHost` + + + +* Type: {number} +* Default: `0` (unlimited) + +Specifies the maximum number of concurrent sessions allowed per remote IP +address (ignoring port). When the limit is reached, new connections from the +same IP are refused with `CONNECTION_REFUSED`. A value of `0` disables the +limit. The maximum value is `65535`. + +This limit can also be changed dynamically after construction via +[`endpoint.maxConnectionsPerHost`][]. + +#### `endpointOptions.maxConnectionsTotal` + + + +* Type: {number} +* Default: `0` (unlimited) + +Specifies the maximum total number of concurrent sessions across all remote +addresses. When the limit is reached, new connections are refused with +`CONNECTION_REFUSED`. A value of `0` disables the limit. The maximum value is +`65535`. + +This limit can also be changed dynamically after construction via +[`endpoint.maxConnectionsTotal`][]. + +#### `endpointOptions.retryRate` + +* Type: {number} +* **Default:** `100` + +The maximum number of QUIC retry packets the endpoint will send per second. +This is a global rate limit (not per-host) that caps the total server-wide +retry response rate, preventing spoofed-source floods from consuming unbounded +resources. + +#### `endpointOptions.retryBurst` + +* Type: {number} +* **Default:** `200` + +The maximum burst of retry packets allowed before rate limiting takes effect. + +#### `endpointOptions.statelessResetRate` + +* Type: {number} +* **Default:** `100` + +The maximum number of stateless reset packets the endpoint will send per second. + +#### `endpointOptions.statelessResetBurst` + +* Type: {number} +* **Default:** `200` + +The maximum burst of stateless reset packets allowed before rate limiting +takes effect. + +#### `endpointOptions.versionNegotiationRate` + +* Type: {number} +* **Default:** `100` + +The maximum number of version negotiation packets the endpoint will send per +second. + +#### `endpointOptions.versionNegotiationBurst` + +* Type: {number} +* **Default:** `200` + +The maximum burst of version negotiation packets allowed before rate limiting +takes effect. + +#### `endpointOptions.immediateCloseRate` + +* Type: {number} +* **Default:** `100` + +The maximum number of immediate connection close packets the endpoint will +send per second. + +#### `endpointOptions.immediateCloseBurst` + +* Type: {number} +* **Default:** `200` + +The maximum burst of immediate connection close packets allowed before rate +limiting takes effect. + +#### `endpointOptions.sessionCreationRate` + +* Type: {number} +* **Default:** `50` + +The maximum number of new sessions that a single remote address can create per +second. This is a per-host rate limit tracked in the address validation LRU +cache. It prevents a validated remote address from churning through sessions +(rapidly opening and abandoning connections) faster than the server can handle. +For benchmarking where traffic comes from a single source, set this to a high +value. + +#### `endpointOptions.sessionCreationBurst` + +* Type: {number} +* **Default:** `100` + +The maximum burst of new session creations allowed from a single remote address +before rate limiting takes effect. + +#### `endpointOptions.retryTokenExpiration` + + + +* Type: {bigint|number} + +Specifies the length of time a QUIC retry token is considered valid. + +#### `endpointOptions.resetTokenSecret` + + + +* Type: {ArrayBufferView} + +Specifies the 16-byte secret used to generate QUIC retry tokens. + +#### `endpointOptions.tokenExpiration` + + + +* Type: {bigint|number} + +Specifies the length of time a QUIC token is considered valid. + +#### `endpointOptions.tokenSecret` + + + +* Type: {ArrayBufferView} + +Specifies the 16-byte secret used to generate QUIC tokens. + +#### `endpointOptions.udpReceiveBufferSize` + + + +* Type: {number} + +#### `endpointOptions.udpSendBufferSize` + + + +* Type: {number} + +#### `endpointOptions.udpTTL` + + + +* Type: {number} + +#### `endpointOptions.validateAddress` + + + +* Type: {boolean} + +When `true`, requires that the endpoint validate peer addresses using retry packets +while establishing a new connection. + +### Type: `SessionOptions` + + + +#### `sessionOptions.alpn` + + + +* Type: {string} (client) | {string\[]} (server) + +The ALPN (Application-Layer Protocol Negotiation) identifier(s). + +For **client** sessions, this is a single string specifying the protocol +the client wants to use (e.g. `'h3'`). + +For **server** sessions, this is an array of protocol names in preference +order that the server supports (e.g. `['h3', 'h3-29']`). During the TLS +handshake, the server selects the first protocol from its list that the +client also supports. + +The negotiated ALPN determines which Application implementation is used +for the session. `'h3'` and `'h3-*'` variants select the HTTP/3 +application; all other values select the default application. + +Default: `'h3'` + +#### `sessionOptions.application` + + + +* Type: {quic.ApplicationOptions} + +Application-specific options. + +```mjs +const { listen } = await import('node:quic'); + +await listen((session) => { /* ... */ }, { + application: { + maxHeaderPairs: 64, + qpackMaxDTableCapacity: 8192, + enableDatagrams: true, + }, + // ... other session options +}); +``` + +#### `sessionOptions.ca` (client only) + + + +* Type: {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} + +The CA certificates to use for client sessions. For server sessions, CA +certificates are specified per-identity in the [`sessionOptions.sni`][] map. + +#### `sessionOptions.cc` + + + +* Type: {string} + +Specifies the congestion control algorithm that will be used. +Must be set to one of either `'reno'`, `'cubic'`, or `'bbr'`. + +This is an advanced option that users typically won't have need to specify. + +#### `sessionOptions.certs` (client only) + + + +* Type: {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} + +The TLS certificates to use for client sessions. For server sessions, +certificates are specified per-identity in the [`sessionOptions.sni`][] map. + +#### `sessionOptions.ciphers` + + + +* Type: {string} + +The list of supported TLS 1.3 cipher algorithms. + +#### `sessionOptions.crl` (client only) + + + +* Type: {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} + +The CRL to use for client sessions. For server sessions, CRLs are specified +per-identity in the [`sessionOptions.sni`][] map. + +#### `sessionOptions.enableEarlyData` + + + +* Type: {boolean} **Default:** `true` + +When `true`, enables TLS 0-RTT early data for this session. Early data +allows the client to send application data before the TLS handshake +completes, reducing latency on reconnection when a valid session ticket +is available. Set to `false` to disable early data support. + +#### `sessionOptions.groups` + + + +* Type: {string} + +The list of supported TLS 1.3 cipher groups. + +#### `sessionOptions.keylog` + + + +* Type: {boolean} + +When `true`, enables TLS key logging for the session. Key material is +delivered to the [`session.onkeylog`][] callback in [NSS Key Log Format][]. +Each callback invocation receives a single line of key material. The output +can be used with tools such as Wireshark to decrypt captured QUIC traffic. + +#### `sessionOptions.keys` (client only) + + + +* Type: {KeyObject|KeyObject\[]} + +The TLS crypto keys to use for client sessions. For server sessions, +keys are specified per-identity in the [`sessionOptions.sni`][] map. + +#### `sessionOptions.maxPayloadSize` + + + +* Type: {bigint|number} + +Specifies the maximum UDP packet payload size. + +#### `sessionOptions.maxStreamWindow` + + + +* Type: {bigint|number} Specifies the maximum stream flow-control window size. @@ -1324,9 +3011,15 @@ added: v23.8.0 --> * Type: {string} One of `'use'`, `'ignore'`, or `'default'`. +* **Default:** `'ignore'` When the remote peer advertises a preferred address, this option specifies whether -to use it or ignore it. +to use it or ignore it. The default is `'ignore'` because honoring a server's +preferred address causes the client to migrate its connection to a different IP +address, which can be exploited for data exfiltration attacks that are +indistinguishable from legitimate QUIC connection migration at the network level. +Set to `'use'` only when connecting to trusted servers that require preferred +address migration. #### `sessionOptions.qlog` @@ -1336,7 +3029,10 @@ added: v23.8.0 * Type: {boolean} -True if qlog output should be enabled. +When `true`, enables [qlog][] diagnostic output for the session. Qlog data +is delivered to the [`session.onqlog`][] callback as chunks of [JSON-SEQ][] +formatted text. The output can be analyzed with qlog visualization tools +such as [qvis][]. #### `sessionOptions.sessionTicket` @@ -1346,6 +3042,67 @@ added: v23.8.0 * Type: {ArrayBufferView} A session ticket to use for 0RTT session resumption. +#### `sessionOptions.datagramDropPolicy` + + + +* Type: {string} +* **Default:** `'drop-oldest'` + +Controls which datagram to drop when the pending datagram queue +(sized by [`session.maxPendingDatagrams`][]) is full. Must be one of +`'drop-oldest'` (discard the oldest queued datagram to make room) or +`'drop-newest'` (reject the incoming datagram). Dropped datagrams are +reported as lost via the `ondatagramstatus` callback. + +This option is immutable after session creation. + +#### `sessionOptions.streamIdleTimeout` + +* Type: {bigint|number} +* **Default:** `30000` (30 seconds) + +The maximum time in milliseconds that a peer-initiated stream can be idle +(no data received) before it is automatically destroyed. This protects +against slowloris-style attacks where a remote peer opens streams but never +sends data, holding server resources indefinitely. Only peer-initiated +streams are checked — locally-initiated streams are the application's +responsibility. Set to `0` to disable. + +The idle check runs as part of the normal send processing loop, so it adds +no additional timers or event loop overhead. The +`session.stats.streamsIdleTimedOut` counter tracks how many streams have been +destroyed by this mechanism. + +#### `sessionOptions.maxDatagramSendAttempts` + +* Type: {number} +* **Default:** `5` + +The maximum number of `SendPendingData` cycles a datagram can survive +without being sent before it is abandoned. When a datagram cannot be +sent due to congestion control or packet size constraints, it remains +in the queue and the attempt counter increments. Once the limit is +reached, the datagram is dropped and reported as `'abandoned'` via the +`ondatagramstatus` callback. Valid range: `1` to `255`. + +#### `sessionOptions.drainingPeriodMultiplier` + + + +* Type: {number} +* **Default:** `3` + +A multiplier applied to the Probe Timeout (PTO) to compute the draining +period duration after receiving a `CONNECTION_CLOSE` frame from the peer. +RFC 9000 Section 10.2 requires the draining period to persist for at least +three times the current PTO. The valid range is `3` to `255`. Values below +`3` are clamped to `3`. + #### `sessionOptions.handshakeTimeout` + +* Type: {bigint|number} +* **Default:** `0` (use ngtcp2 default of 333ms) + +Specifies the initial round-trip time estimate in milliseconds. This value is +used for probe timeout (PTO) computation, initial pacing, and early loss +detection before the first actual RTT sample is collected from the connection. +The default of 333ms is appropriate for the general internet. For low-latency +environments such as loopback or same-rack deployments, setting a value closer +to the actual RTT (e.g., `1`) avoids unnecessarily conservative initial +behavior. + +#### `sessionOptions.keepAlive` + + + +* Type: {bigint|number} +* **Default:** `0` (disabled) + +Specifies the keep-alive timeout in milliseconds. When set to a non-zero +value, PING frames will be sent automatically to keep the connection alive +before the idle timeout fires. The value should be less than the effective +idle timeout (`maxIdleTimeout` transport parameter) to be useful. + +#### `sessionOptions.verifyPeer` (client only) + +* Type: {string} One of `'strict'`, `'auto'`, or `'manual'`. +* **Default:** `'auto'` + +Controls how the client handles server certificate validation: + +* `'strict'` — OpenSSL aborts the TLS handshake immediately if the server's + certificate fails validation. The `session.opened` promise rejects with a + TLS error. The application cannot inspect the certificate or the error + details. This is the most secure mode. + +* `'auto'` — The TLS handshake completes regardless of validation result. + If validation fails, the `session.opened` promise is rejected with an error + containing the validation reason, and the session is destroyed. The + `onhandshake` callback (if set) fires before rejection, allowing diagnostic + logging. This is the default and matches the behavior of `tls.connect()` + with `rejectUnauthorized: true`. + +* `'manual'` — The TLS handshake completes regardless of validation result. + The `session.opened` promise resolves with the handshake info, which includes + `validationErrorReason` and `validationErrorCode` if validation failed. The + application is responsible for checking these values and deciding whether to + continue. Use this mode for custom validation logic, certificate pinning, or + intentionally accepting self-signed certificates. + #### `sessionOptions.servername` (client only) + +* Type: {ArrayBufferView} + +An opaque address validation token previously received from the server +via the [`session.onnewtoken`][] callback. Providing a valid token on +reconnection allows the client to skip the server's address validation, +reducing handshake latency. + #### `sessionOptions.transportParams` + +* Type: {boolean} **Default:** `true` + +If `true`, the peer certificate is verified against the list of supplied CAs. +An error is emitted if verification fails; the error can be inspected via +the `validationErrorReason` and `validationErrorCode` fields in the +handshake callback. If `false`, peer certificate verification errors are +ignored. + +#### `sessionOptions.reuseEndpoint` + + + +* Type: {boolean} +* Default: `true` + +When `true` (the default), `connect()` will attempt to reuse an existing +endpoint rather than creating a new one for each session. This provides +connection pooling behavior — multiple sessions can share a single UDP +socket. The reuse logic will not return an endpoint that is listening on +the same address as the connect target (to prevent CID routing conflicts). + +Set to `false` to force creation of a new endpoint for the session. This +is useful when endpoint isolation is required (e.g., testing stateless +reset behavior where source port identity matters). + #### `sessionOptions.verifyClient` +The `TransportParams` type represents the QUIC transport parameters that are +negotiated during session establishment. These parameters are used when +creating a session. The negotiated values can be observed via the +`session.localTransportParams` and `session.remoteTransportParams` properties. + +#### `transportParams.initialSCID` + + + +* Type: {string} + +The initial source connection ID (SCID) specified. This field is ignored on +creation of the session and is provided for informational purposes only when +available in the `session.localTransportParams` and +`session.remoteTransportParams` properties. + +#### `transportParams.originalDCID` + + + +* Type: {string} + +The original destination connection ID (DCID) specified. This field is +ignored on creation of the session and is provided for informational +purposes only when available in the `session.localTransportParams` and +`session.remoteTransportParams` properties. + #### `transportParams.preferredAddressIpv4` -* Type: {net.SocketAddress} The preferred IPv4 address to advertise. +* Type: {net.SocketAddress} The preferred IPv4 address to advertise (only + used by servers). #### `transportParams.preferredAddressIpv6` @@ -1491,7 +3390,8 @@ added: v23.8.0 added: v23.8.0 --> -* Type: {net.SocketAddress} The preferred IPv6 address to advertise. +* Type: {net.SocketAddress} The preferred IPv6 address to advertise (only + used by servers) #### `transportParams.initialMaxStreamDataBidiLocal` @@ -1580,9 +3480,52 @@ added: v23.8.0 --> * Type: {bigint|number} +* **Default:** `1200` + +The maximum size in bytes of a DATAGRAM frame payload that this endpoint +is willing to receive. Set to `0` to disable datagram support. The peer +will not send datagrams larger than this value. The actual maximum size of +a datagram that can be _sent_ is determined by the peer's +`maxDatagramFrameSize`, not this endpoint's value. + +#### `transportParams.retrySCID` + + + +* Type: {string} + +The retry connection ID specified. This field is ignored on creation +of the session and is provided for informational purposes only when +available in the `session.localTransportParams` and +`session.remoteTransportParams` properties. ## Callbacks +### Callback error handling + +All session and stream callbacks may be synchronous functions or async +functions. If a callback throws synchronously or returns a promise that +rejects, the error is caught and the owning session or stream is destroyed +with that error: + +* Stream callbacks (`onblocked`, `onreset`, `onheaders`, `ontrailers`, + `oninfo`, `onwanttrailers`): the stream is destroyed. +* Session callbacks (`onapplication`, `onstream`, `ondatagram`, + `ondatagramstatus`, `onpathvalidation`, `onsessionticket`, + `onnewtoken`, `onversionnegotiation`, `onorigin`, `ongoaway`, + `onhandshake`, `onkeylog`, `onqlog`): the session is destroyed along + with all of its streams. + +Before destruction, the optional [`session.onerror`][] or +[`stream.onerror`][] callback is invoked (if set), giving the application a +chance to observe or log the error. The `session.closed` or `stream.closed` +promise will reject with the error. + +If the `onerror` callback itself throws or returns a promise that rejects, +the error from `onerror` is surfaced as an uncaught exception. + ### Callback: `OnSessionCallback` + +* `this` {quic.QuicSession} +* `applicationoption` {quic.QuicSession} + +The callback function that is invoked when application options change. +E.g. for http/3 settings are included in applications options and +may arrive after the connection is established. ### Callback: `OnPathValidationCallback` @@ -1631,11 +3591,15 @@ added: v23.8.0 * `this` {quic.QuicSession} * `result` {string} One of either `'success'`, `'failure'`, or `'aborted'`. -* `newLocalAddress` {net.SocketAddress} -* `newRemoteAddress` {net.SocketAddress} -* `oldLocalAddress` {net.SocketAddress} -* `oldRemoteAddress` {net.SocketAddress} -* `preferredAddress` {boolean} +* `newLocalAddress` {net.SocketAddress} The local address of the validated path. +* `newRemoteAddress` {net.SocketAddress} The remote address of the validated path. +* `oldLocalAddress` {net.SocketAddress | null} The local address of the previous + path, or `null` if this is the first path validation (e.g., preferred address + migration from the client's perspective). +* `oldRemoteAddress` {net.SocketAddress | null} The remote address of the previous + path, or `null`. +* `preferredAddress` {boolean} `true` if the path validation was triggered by + a preferred address migration on the client side. `undefined` on the server side. ### Callback: `OnSessionTicketCallback` @@ -1653,9 +3617,17 @@ added: v23.8.0 --> * `this` {quic.QuicSession} -* `version` {number} -* `requestedVersions` {number\[]} -* `supportedVersions` {number\[]} +* `version` {number} The QUIC version that was configured for this session + (the version that the server did not support). +* `requestedVersions` {number\[]} The versions advertised by the server in + the Version Negotiation packet. These are the versions the server supports. +* `supportedVersions` {number\[]} The versions supported locally, expressed + as a two-element array `[minVersion, maxVersion]`. + +Called when the server responds to the client's Initial packet with a +Version Negotiation packet, indicating that the version used by the client +is not supported. The session is always destroyed immediately after this +callback returns. ### Callback: `OnHandshakeCallback` @@ -1664,13 +3636,69 @@ added: v23.8.0 --> * `this` {quic.QuicSession} -* `sni` {string} -* `alpn` {string} -* `cipher` {string} -* `cipherVersion` {string} -* `validationErrorReason` {string} -* `validationErrorCode` {number} -* `earlyDataAccepted` {boolean} +* `info` {Object} The same object that `session.opened` resolves with. + * `local` {net.SocketAddress} The local socket address. + * `remote` {net.SocketAddress} The remote socket address. + * `servername` {string} The SNI server name negotiated during the handshake. + * `protocol` {string} The ALPN protocol negotiated during the handshake. + * `cipher` {string} The name of the negotiated TLS cipher suite. + * `cipherVersion` {string} The TLS protocol version of the cipher suite. + * `validationErrorReason` {string} If certificate validation failed, the + reason string. Empty string if validation succeeded. + * `validationErrorCode` {number} If certificate validation failed, the + error code. `0` if validation succeeded. + * `earlyDataAttempted` {boolean} Whether 0-RTT early data was attempted. + * `earlyDataAccepted` {boolean} Whether 0-RTT early data was accepted. + +### Callback: `OnNewTokenCallback` + + + +* `this` {quic.QuicSession} +* `token` {Buffer} The NEW\_TOKEN token data. +* `address` {SocketAddress} The remote address the token is associated with. + +### Callback: `OnOriginCallback` + + + +* `this` {quic.QuicSession} +* `origins` {string\[]} The list of origins the server is authoritative for. + +### Callback: `OnKeylogCallback` + + + +* `this` {quic.QuicSession} +* `line` {string} A single line of [NSS Key Log Format][] text, including + a trailing newline character. + +Called when TLS key material is available. Only fires when +[`sessionOptions.keylog`][] is `true`. Multiple lines are emitted during the +TLS 1.3 handshake, each containing a secret label, the client random, and +the secret value. + +### Callback: `OnQlogCallback` + + + +* `this` {quic.QuicSession} +* `data` {string} A chunk of [JSON-SEQ][] formatted [qlog][] data. +* `fin` {boolean} `true` if this is the final qlog chunk for the session. + +Called when qlog diagnostic data is available. Only fires when +[`sessionOptions.qlog`][] is `true`. The `data` chunks should be +concatenated in order to produce the complete qlog output. When `fin` is +`true`, no more chunks will be emitted and the concatenated result is a +complete JSON-SEQ document. ### Callback: `OnBlockedCallback` @@ -1689,6 +3717,264 @@ added: v23.8.0 * `this` {quic.QuicStream} * `error` {any} +### Callback: `OnHeadersCallback` + + + +* `this` {quic.QuicStream} +* `headers` {Object} Header object with lowercase string keys and + string or string-array values. + +Called when initial request or response headers are received. For HTTP/3, +this delivers request pseudo-headers on the server and response headers +on the client. + +### Callback: `OnTrailersCallback` + + + +* `this` {quic.QuicStream} +* `trailers` {Object} Trailing header object. + +Called when trailing headers are received from the peer. + +### Callback: `OnInfoCallback` + + + +* `this` {quic.QuicStream} +* `headers` {Object} Informational header object. + +Called when informational (1xx) headers are received from the server +(e.g., 103 Early Hints). + +## HTTP/3 support + + + +When the negotiated ALPN identifier is `'h3'` (or one of the `'h3-*'` +draft variants), the QUIC session runs the HTTP/3 application backed +by `nghttp3`. `'h3'` is the default ALPN for `quic.connect()` and +`quic.listen()`, so HTTP/3 is what you get unless you select a +different ALPN explicitly. + +Selecting the HTTP/3 application enables a number of stream- and +session-level capabilities that are not available to non-HTTP/3 +applications: + +* **Headers and trailers** — request and response header blocks + (including pseudo-headers such as `:method`, `:path`, `:scheme`, + `:authority`, and `:status`), trailing headers, and informational + (`1xx`) responses. See [`stream.sendHeaders()`][], + [`stream.sendTrailers()`][], and + [`stream.sendInformationalHeaders()`][]. +* **Stream priority (RFC 9218)** — per-stream urgency and + incremental flags. See [`stream.priority`][] and + [`stream.setPriority()`][]. +* **HTTP/3 datagrams (RFC 9297)** — unreliable application-layer + datagrams. The peer must advertise `SETTINGS_H3_DATAGRAM=1`, which + is enabled by setting [`application.enableDatagrams`][] to `true` + on both peers. See [`session.sendDatagram()`][] and + [`session.ondatagram`][]. +* **ORIGIN frame (RFC 9412)** — servers automatically advertise the + hostnames in their [`sessionOptions.sni`][] map (entries with + `authoritative: true`); clients receive the list via + [`session.onorigin`][]. +* **GOAWAY** — graceful shutdown. The server emits `GOAWAY` as part + of [`session.close()`][]; the client observes it via + [`session.ongoaway`][] and stops opening new bidirectional streams. +* **Extended CONNECT settings (RFC 9220)** — the + `SETTINGS_ENABLE_CONNECT_PROTOCOL` setting can be enabled via + [`application.enableConnectProtocol`][]. The setting is exchanged + but the application is responsible for handling the `:protocol` + pseudo-header and any payload framing on top. +* **QPACK tuning** — dynamic-table size and blocked-streams limits + via [`application.qpackMaxDTableCapacity`][] and friends. + +### Minimal HTTP/3 client + +```mjs +import { connect } from 'node:quic'; +import process from 'node:process'; + +const session = await connect('example.com:443', { + // ALPN defaults to 'h3'. + servername: 'example.com', +}); +await session.opened; + +const stream = await session.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/', + ':scheme': 'https', + ':authority': 'example.com', + }, + onheaders(headers) { + console.log('status:', headers[':status']); + }, +}); + +const decoder = new TextDecoder(); +for await (const chunks of stream) { + for (const chunk of chunks) { + process.stdout.write(decoder.decode(chunk, { stream: true })); + } +} + +await session.close(); +``` + +A few things to note: + +* `session.createBidirectionalStream({ headers })` automatically + marks the HEADERS frame as terminal when no `body` is provided — + the request is `HEADERS` followed by `END_STREAM`. +* The `onheaders` callback receives the response pseudo-headers and + regular headers in a single object with lowercase string keys. + After the callback returns, the same object is also accessible + via [`stream.headers`][]. +* Reading `for await (const chunks of stream)` consumes the response + body. Each iteration yields a `Uint8Array[]` batch of chunks. +* HTTP semantic helpers (URL parsing, method/status validation, + redirects, content negotiation, and so on) are intentionally not + built in. The caller is responsible for any HTTP-level handling + beyond the wire framing. + +### Minimal HTTP/3 server + +```mjs +import { listen } from 'node:quic'; + +const encoder = new TextEncoder(); + +const endpoint = await listen((session) => { + // The session.onstream callback fires for each new client-initiated stream. +}, { + sni: { '*': { keys: [defaultKey], certs: [defaultCert] } }, + // ALPN defaults to 'h3'. + onheaders(headers) { + // `this` is the QuicStream. Pseudo-headers are available on the + // request header block (`:method`, `:path`, `:scheme`, + // `:authority`). + if (headers[':path'] === '/health') { + this.sendHeaders({ ':status': '200', 'content-type': 'text/plain' }); + const w = this.writer; + w.writeSync(encoder.encode('ok\n')); + w.endSync(); + } else { + this.sendHeaders({ ':status': '404' }, { terminal: true }); + } + }, +}); + +console.log('listening on', endpoint.address); +``` + +Server-side notes: + +* Setting `onheaders` at the [`listen()`][`quic.listen()`] level + applies it to every incoming stream (it is wired up before + `onstream` fires). Setting it inside `onstream` is too late for + HTTP/3, where the request HEADERS frame is the first thing that + arrives on the stream. +* `this.sendHeaders(headers, { terminal: true })` marks the + response HEADERS frame as terminal (no body follows). +* For body responses, send headers first, then write to + `this.writer` and call `endSync()` to send the body and close the + stream cleanly. + +### What is not implemented + +* **Server push** — `PUSH_PROMISE` and the related push-stream + machinery are not implemented and are not on the near-term + roadmap. Server push has limited deployment in practice, and most + use cases are better served by Early Hints (`103`) or by direct + fetches from the client. +* **WebTransport / extended-CONNECT helpers** — the + `SETTINGS_ENABLE_CONNECT_PROTOCOL` setting can be negotiated but + there is no built-in support for the `:protocol` pseudo-header, + WebTransport datagram demultiplexing, or capsule framing. +* **Higher-level HTTP semantics** — there is no built-in + request/response router, URL parsing, content-encoding + negotiation, body-type coercion, redirect following, or + cookie handling. These are deliberately left to higher-level + libraries built on top of `node:quic`. + +## Performance measurement + + + +QUIC sessions, streams, and endpoints emit [`PerformanceEntry`][] objects +with `entryType` set to `'quic'`. These entries are only created when a +[`PerformanceObserver`][] is observing the `'quic'` entry type, ensuring +zero overhead when not in use. + +Each entry provides: + +* `name` {string} One of `'QuicEndpoint'`, `'QuicSession'`, or `'QuicStream'`. +* `entryType` {string} Always `'quic'`. +* `startTime` {number} High-resolution timestamp (ms) when the object was created. +* `duration` {number} Lifetime in milliseconds from creation to destruction. +* `detail` {Object} Entry-specific metadata (see below). + +### `QuicEndpoint` entries + +* `detail.stats` {QuicEndpointStats} The endpoint's statistics object + (frozen at destruction time). + +### `QuicSession` entries + +* `detail.stats` {QuicSessionStats} The session's statistics object + (frozen at destruction time). Includes bytes sent/received, RTT + measurements, congestion window, packet counts, and more. +* `detail.handshake` {Object|undefined} Timing-relevant handshake metadata, + or `undefined` if the handshake did not complete before destruction. + * `servername` {string} The negotiated SNI server name. + * `protocol` {string} The negotiated ALPN protocol. + * `earlyDataAttempted` {boolean} Whether 0-RTT early data was attempted. + * `earlyDataAccepted` {boolean} Whether 0-RTT early data was accepted. +* `detail.path` {Object|undefined} The session's network path, or + `undefined` if not yet established. + * `local` {net.SocketAddress} + * `remote` {net.SocketAddress} + +### `QuicStream` entries + +* `detail.stats` {QuicStreamStats} The stream's statistics object + (frozen at destruction time). Includes bytes sent/received, timing + timestamps, and offset tracking. +* `detail.direction` {string} Either `'bidi'` or `'uni'`. + +### Example + +```mjs +import { PerformanceObserver } from 'node:perf_hooks'; + +const obs = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + console.log(`${entry.name}: ${entry.duration.toFixed(1)}ms`); + if (entry.name === 'QuicSession') { + const { stats, handshake } = entry.detail; + console.log(` protocol: ${handshake?.protocol}`); + console.log(` bytes sent: ${stats.bytesSent}`); + console.log(` smoothed RTT: ${stats.smoothedRtt}ns`); + } + } +}); +obs.observe({ entryTypes: ['quic'] }); +``` + ## Diagnostic Channels ### Channel: `quic.endpoint.created` @@ -1700,6 +3986,8 @@ added: v23.8.0 * `endpoint` {quic.QuicEndpoint} * `config` {quic.EndpointOptions} +Published when a new endpoint is created. + ### Channel: `quic.endpoint.listen` * `endpoint` {quic.QuicEndpoint} -* `optoins` {quic.SessionOptions} +* `options` {quic.SessionOptions} + +Published when an endpoint begins listening for incoming connections. + +### Channel: `quic.endpoint.connect` + + + +* `endpoint` {quic.QuicEndpoint} +* `address` {net.SocketAddress} The target server address. +* `options` {quic.SessionOptions} + +Published when [`quic.connect()`][] is about to create a client session. +Fires before the ngtcp2 connection is established, allowing diagnostic +subscribers to observe the connection intent. ### Channel: `quic.endpoint.closing` @@ -1718,6 +4022,8 @@ added: v23.8.0 * `endpoint` {quic.QuicEndpoint} * `hasPendingError` {boolean} +Published when an endpoint begins gracefully closing. + ### Channel: `quic.endpoint.closed` * `endpoint` {quic.QuicEndpoint} +* `stats` {quic.QuicEndpoint.Stats} Final endpoint statistics. + +Published when an endpoint has finished closing and is destroyed. ### Channel: `quic.endpoint.error` @@ -1735,6 +4044,8 @@ added: v23.8.0 * `endpoint` {quic.QuicEndpoint} * `error` {any} +Published when an endpoint encounters an error that causes it to close. + ### Channel: `quic.endpoint.busy.change` + +* `applicationoptions` {quic.ApplicationOptions} Current application options. +* `session` {quic.QuicSession} + +Published when a locally-initiated stream is opened. + ### Channel: `quic.session.created.client` +* `endpoint` {quic.QuicEndpoint} +* `session` {quic.QuicSession} +* `address` {net.SocketAddress} The remote server address. +* `options` {quic.SessionOptions} + +Published when a client-initiated session is created. + ### Channel: `quic.session.created.server` +* `endpoint` {quic.QuicEndpoint} +* `session` {quic.QuicSession} +* `address` {net.SocketAddress|undefined} The remote peer address. + +Published when a server-side session is created for an incoming connection. + ### Channel: `quic.session.open.stream` +* `stream` {quic.QuicStream} +* `session` {quic.QuicSession} +* `direction` {string} Either `'bidi'` or `'uni'`. + +Published when a locally-initiated stream is opened. + ### Channel: `quic.session.received.stream` +* `stream` {quic.QuicStream} +* `session` {quic.QuicSession} +* `direction` {string} Either `'bidi'` or `'uni'`. + +Published when a remotely-initiated stream is received. + ### Channel: `quic.session.send.datagram` +* `id` {bigint} The datagram ID. +* `length` {number} The datagram payload size in bytes. +* `session` {quic.QuicSession} + +Published when a datagram is queued for sending. + ### Channel: `quic.session.update.key` +* `session` {quic.QuicSession} + +Published when a TLS key update is initiated. + ### Channel: `quic.session.closing` +* `session` {quic.QuicSession} + +Published when a session begins gracefully closing (including when a +GOAWAY frame is received from the peer). + ### Channel: `quic.session.closed` +* `session` {quic.QuicSession} +* `error` {any} The error that caused the close, or `undefined` if clean. +* `stats` {quic.QuicSession.Stats} Final session statistics. + +Published when a session is destroyed. The `stats` object is a snapshot +of the final statistics at the time of destruction. + +### Channel: `quic.session.error` + + + +* `session` {quic.QuicSession} +* `error` {any} The error that caused the session to be destroyed. + +Published when a session is destroyed due to an error. Fires before the +`onerror` callback and before streams are torn down. Unlike +`quic.session.closed` (which fires for both clean and error closes), this +channel fires only when an error is present, making it suitable for +error-only alerting. + ### Channel: `quic.session.receive.datagram` +* `length` {number} The datagram payload size in bytes. +* `early` {boolean} Whether the datagram was received as 0-RTT early data. +* `session` {quic.QuicSession} + +Published when a datagram is received from the remote peer. + ### Channel: `quic.session.receive.datagram.status` +* `id` {bigint} The datagram ID. +* `status` {string} One of `'acknowledged'`, `'lost'`, or `'abandoned'`. +* `session` {quic.QuicSession} + +Published when the delivery status of a sent datagram is updated. + ### Channel: `quic.session.path.validation` +* `result` {string} One of `'success'`, `'failure'`, or `'aborted'`. +* `newLocalAddress` {net.SocketAddress} +* `newRemoteAddress` {net.SocketAddress} +* `oldLocalAddress` {net.SocketAddress|null} +* `oldRemoteAddress` {net.SocketAddress|null} +* `preferredAddress` {boolean} +* `session` {quic.QuicSession} + +Published when a path validation attempt completes. + +### Channel: `quic.session.new.token` + + + +* `token` {Buffer} The NEW\_TOKEN token data. +* `address` {net.SocketAddress} The remote server address. +* `session` {quic.QuicSession} + +Published when a client session receives a NEW\_TOKEN frame from the +server. + ### Channel: `quic.session.ticket` +* `ticket` {Object} The opaque session ticket. +* `session` {quic.QuicSession} + +Published when a new TLS session ticket is received. + ### Channel: `quic.session.version.negotiation` +* `version` {number} The QUIC version that was configured for this session. +* `requestedVersions` {number\[]} The versions advertised by the server. +* `supportedVersions` {number\[]} The versions supported locally. +* `session` {quic.QuicSession} + +Published when the client receives a Version Negotiation packet from the +server. The session is always destroyed immediately after. + +### Channel: `quic.session.receive.origin` + + + +* `origins` {string\[]} The list of origins the server is authoritative for. +* `session` {quic.QuicSession} + +Published when the session receives an ORIGIN frame (RFC 9412) from +the peer. + ### Channel: `quic.session.handshake` +* `session` {quic.QuicSession} +* `servername` {string} +* `protocol` {string} +* `cipher` {string} +* `cipherVersion` {string} +* `validationErrorReason` {string} +* `validationErrorCode` {number} +* `earlyDataAttempted` {boolean} +* `earlyDataAccepted` {boolean} + +Published when the TLS handshake completes. + +### Channel: `quic.session.goaway` + + + +* `session` {quic.QuicSession} +* `lastStreamId` {bigint} The highest stream ID the peer may have processed. + +Published when the peer sends an HTTP/3 GOAWAY frame. Streams with IDs +above `lastStreamId` were not processed and can be retried on a new +connection. A `lastStreamId` of `-1n` indicates a shutdown notice without +a stream boundary. + +### Channel: `quic.session.early.rejected` + + + +* `session` {quic.QuicSession} + +Published when the server rejects 0-RTT early data. All streams that were +opened during the 0-RTT phase have been destroyed. Useful for diagnosing +latency regressions when 0-RTT is expected to succeed. + +### Channel: `quic.stream.closed` + + + +* `stream` {quic.QuicStream} +* `session` {quic.QuicSession} +* `error` {any} The error that caused the close, or `undefined` if clean. +* `stats` {quic.QuicStream.Stats} Final stream statistics. + +Published when a stream is destroyed. The `stats` object is a snapshot +of the final statistics at the time of destruction. + +### Channel: `quic.stream.headers` + + + +* `stream` {quic.QuicStream} +* `session` {quic.QuicSession} +* `headers` {Object} The initial request or response headers. + +Published when initial headers are received on a stream. For HTTP/3 +server-side streams, this contains request pseudo-headers (`:method`, +`:path`, etc.). For client-side streams, this contains response headers +(`:status`, etc.). + +### Channel: `quic.stream.trailers` + + + +* `stream` {quic.QuicStream} +* `session` {quic.QuicSession} +* `trailers` {Object} The trailing headers. + +Published when trailing headers are received on a stream. + +### Channel: `quic.stream.info` + + + +* `stream` {quic.QuicStream} +* `session` {quic.QuicSession} +* `headers` {Object} The informational headers. + +Published when informational (1xx) headers are received on a stream +(e.g., 103 Early Hints). + +### Channel: `quic.stream.reset` + + + +* `stream` {quic.QuicStream} +* `session` {quic.QuicSession} +* `error` {any} The QUIC error associated with the reset. + +Published when a stream receives a STOP\_SENDING or RESET\_STREAM frame +from the peer, indicating the peer has aborted the stream. This is a +key signal for diagnosing application-level issues such as cancelled +requests. + +### Channel: `quic.stream.blocked` + + + +* `stream` {quic.QuicStream} +* `session` {quic.QuicSession} + +Published when a stream is flow-control blocked and cannot send data +until the peer increases the flow control window. Useful for diagnosing +throughput issues caused by flow control. + +[Aborting a stream]: #aborting-a-stream +[Callback error handling]: #callback-error-handling +[JSON-SEQ]: https://www.rfc-editor.org/rfc/rfc7464 +[NSS Key Log Format]: https://udn.realityripple.com/docs/Mozilla/Projects/NSS/Key_Log_Format +[RFC 8879]: https://www.rfc-editor.org/rfc/rfc8879 +[RFC 8999]: https://www.rfc-editor.org/rfc/rfc8999 +[RFC 9000]: https://www.rfc-editor.org/rfc/rfc9000 +[RFC 9000 Section 8.1]: https://www.rfc-editor.org/rfc/rfc9000#section-8.1 +[RFC 9001]: https://www.rfc-editor.org/rfc/rfc9001 +[RFC 9002]: https://www.rfc-editor.org/rfc/rfc9002 +[RFC 9114]: https://www.rfc-editor.org/rfc/rfc9114 +[RFC 9204]: https://www.rfc-editor.org/rfc/rfc9204 +[RFC 9218]: https://www.rfc-editor.org/rfc/rfc9218 +[RFC 9220]: https://www.rfc-editor.org/rfc/rfc9220 +[RFC 9221]: https://www.rfc-editor.org/rfc/rfc9221 +[RFC 9287]: https://www.rfc-editor.org/rfc/rfc9287 +[RFC 9297]: https://www.rfc-editor.org/rfc/rfc9297 +[RFC 9308]: https://www.rfc-editor.org/rfc/rfc9308 +[RFC 9312]: https://www.rfc-editor.org/rfc/rfc9312 +[RFC 9368]: https://www.rfc-editor.org/rfc/rfc9368 +[RFC 9369]: https://www.rfc-editor.org/rfc/rfc9369 +[RFC 9412]: https://www.rfc-editor.org/rfc/rfc9412 +[RFC 9443]: https://www.rfc-editor.org/rfc/rfc9443 +[`PerformanceEntry`]: perf_hooks.md#class-performanceentry +[`PerformanceObserver`]: perf_hooks.md#class-performanceobserver +[`QuicError`]: #class-quicerror +[`application.enableConnectProtocol`]: #sessionoptionsapplication +[`application.enableDatagrams`]: #sessionoptionsapplication +[`application.qpackMaxDTableCapacity`]: #sessionoptionsapplication +[`crypto.X509Certificate`]: crypto.md#class-x509certificate +[`endpoint.busy`]: #endpointbusy +[`endpoint.maxConnectionsPerHost`]: #endpointmaxconnectionsperhost +[`endpoint.maxConnectionsTotal`]: #endpointmaxconnectionstotal +[`endpointOptions.blockListPolicy`]: #endpointoptionsblocklistpolicy +[`endpointOptions.blockList`]: #endpointoptionsblocklist +[`endpointOptions.immediateCloseBurst`]: #endpointoptionsimmediatecloseburst +[`endpointOptions.immediateCloseRate`]: #endpointoptionsimmediatecloserate +[`endpointOptions.retryBurst`]: #endpointoptionsretryburst +[`endpointOptions.retryRate`]: #endpointoptionsretryrate +[`endpointOptions.sessionCreationBurst`]: #endpointoptionssessioncreationburst +[`endpointOptions.sessionCreationRate`]: #endpointoptionssessioncreationrate +[`endpointOptions.statelessResetBurst`]: #endpointoptionsstatelessresetburst +[`endpointOptions.statelessResetRate`]: #endpointoptionsstatelessresetrate +[`endpointOptions.versionNegotiationBurst`]: #endpointoptionsversionnegotiationburst +[`endpointOptions.versionNegotiationRate`]: #endpointoptionsversionnegotiationrate +[`error.errorCode`]: #errorerrorcode +[`fs.promises.open(path, 'r')`]: fs.md#fspromisesopenpath-flags-mode +[`maxDatagramFrameSize`]: #transportparamsmaxdatagramframesize +[`net.BlockList`]: net.md#class-netblocklist +[`quic.connect()`]: #quicconnectaddress-options +[`quic.listen()`]: #quiclistenonsession-options +[`session.close()`]: #sessioncloseoptions +[`session.createBidirectionalStream()`]: #sessioncreatebidirectionalstreamoptions +[`session.createUnidirectionalStream()`]: #sessioncreateunidirectionalstreamoptions +[`session.destroy()`]: #sessiondestroyerror-options +[`session.maxPendingDatagrams`]: #sessionmaxpendingdatagrams +[`session.onapplication`]: #sessiononapplication +[`session.ondatagram`]: #sessionondatagram +[`session.ondatagramstatus`]: #sessionondatagramstatus +[`session.onearlyrejected`]: #sessiononearlyrejected +[`session.onerror`]: #sessiononerror +[`session.ongoaway`]: #sessionongoaway +[`session.onkeylog`]: #sessiononkeylog +[`session.onnewtoken`]: #sessiononnewtoken +[`session.onorigin`]: #sessiononorigin +[`session.onqlog`]: #sessiononqlog +[`session.onsessionticket`]: #sessiononsessionticket +[`session.onstream`]: #sessiononstream +[`session.sendDatagram()`]: #sessionsenddatagramdatagram-encoding +[`sessionOptions.cc`]: #sessionoptionscc +[`sessionOptions.ciphers`]: #sessionoptionsciphers +[`sessionOptions.datagramDropPolicy`]: #sessionoptionsdatagramdroppolicy +[`sessionOptions.groups`]: #sessionoptionsgroups +[`sessionOptions.keylog`]: #sessionoptionskeylog +[`sessionOptions.qlog`]: #sessionoptionsqlog +[`sessionOptions.sessionTicket`]: #sessionoptionssessionticket [`sessionOptions.sni`]: #sessionoptionssni-server-only +[`sessionOptions.token`]: #sessionoptionstoken-client-only +[`stream.destroy()`]: #streamdestroyerror-options +[`stream.headers`]: #streamheaders +[`stream.onerror`]: #streamonerror +[`stream.onwanttrailers`]: #streamonwanttrailers +[`stream.pendingTrailers`]: #streampendingtrailers +[`stream.priority`]: #streampriority +[`stream.sendHeaders()`]: #streamsendheadersheaders-options +[`stream.sendInformationalHeaders()`]: #streamsendinformationalheadersheaders +[`stream.sendTrailers()`]: #streamsendtrailersheaders +[`stream.setBody()`]: #streamsetbodybody +[`stream.setPriority()`]: #streamsetpriorityoptions +[`stream.writer`]: #streamwriter +[`writer.fail()`]: #streamwriter +[`writer.fail(reason)`]: #streamwriter +[qlog]: https://datatracker.ietf.org/doc/draft-ietf-quic-qlog-main-schema/ +[qvis]: https://qvis.quictools.info/ diff --git a/doc/api/stream.md b/doc/api/stream.md index d339090ce98b2d..879bf6c86a98a8 100644 --- a/doc/api/stream.md +++ b/doc/api/stream.md @@ -1998,6 +1998,61 @@ option. In the code example above, data will be in a single chunk if the file has less then 64 KiB of data because no `highWaterMark` option is provided to [`fs.createReadStream()`][]. +##### `readable[Symbol.for('Stream.toAsyncStreamable')]()` + + + +> Stability: 1 - Experimental + +* Returns: {AsyncIterable} An `AsyncIterable` that yields + batched chunks from the stream. + +When the `--experimental-stream-iter` flag is enabled, `Readable` streams +implement the [`Stream.toAsyncStreamable`][] protocol, enabling efficient +consumption by the [`stream/iter`][] API. + +This provides a batched async iterator that drains the stream's internal +buffer into `Uint8Array[]` batches, amortizing the per-chunk Promise overhead +of the standard `Symbol.asyncIterator` path. For byte-mode streams, chunks +are yielded directly as `Buffer` instances (which are `Uint8Array` subclasses). +For object-mode or encoded streams, each chunk is normalized to `Uint8Array` +before batching. + +The returned iterator is tagged as a validated source, so [`from()`][stream-iter-from] +passes it through without additional normalization. + +```mjs +import { Readable } from 'node:stream'; +import { text, from } from 'node:stream/iter'; + +const readable = new Readable({ + read() { this.push('hello'); this.push(null); }, +}); + +// Readable is automatically consumed via toAsyncStreamable +console.log(await text(from(readable))); // 'hello' +``` + +```cjs +const { Readable } = require('node:stream'); +const { text, from } = require('node:stream/iter'); + +async function run() { + const readable = new Readable({ + read() { this.push('hello'); this.push(null); }, + }); + + console.log(await text(from(readable))); // 'hello' +} + +run().catch(console.error); +``` + +Without the `--experimental-stream-iter` flag, calling this method throws +[`ERR_STREAM_ITER_MISSING_FLAG`][]. + ##### `readable[Symbol.asyncDispose]()` + +> Stability: 1 - Experimental – Enable this API with the [`--experimental-stream-iter`][] CLI flag. + + + +The `node:stream/iter` module provides a streaming API built on iterables +rather than the event-driven `Readable`/`Writable`/`Transform` class hierarchy, +or the Web Streams `ReadableStream`/`WritableStream`/`TransformStream` interfaces. + +Streams are represented as {AsyncIterable} (async) or {Iterable} (sync). There +are no base classes to extend -- any +object implementing the iterable protocol can participate. Transforms are plain +functions or objects with a `transform` method. + +Data flows in **batches** ({Uint8Array\[]} per iteration) to amortize the cost +of async operations. + +```mjs +import { from, pull, text } from 'node:stream/iter'; +import { compressGzip, decompressGzip } from 'node:zlib/iter'; + +// Compress and decompress a string +const compressed = pull(from('Hello, world!'), compressGzip()); +const result = await text(pull(compressed, decompressGzip())); +console.log(result); // 'Hello, world!' +``` + +```cjs +const { from, pull, text } = require('node:stream/iter'); +const { compressGzip, decompressGzip } = require('node:zlib/iter'); + +async function run() { + // Compress and decompress a string + const compressed = pull(from('Hello, world!'), compressGzip()); + const result = await text(pull(compressed, decompressGzip())); + console.log(result); // 'Hello, world!' +} + +run().catch(console.error); +``` + +```mjs +import { open } from 'node:fs/promises'; +import { text, pipeTo } from 'node:stream/iter'; +import { compressGzip, decompressGzip } from 'node:zlib/iter'; + +// Read a file, compress, write to another file +const src = await open('input.txt', 'r'); +const dst = await open('output.gz', 'w'); +await pipeTo(src.pull(), compressGzip(), dst.writer({ autoClose: true })); +await src.close(); + +// Read it back +const gz = await open('output.gz', 'r'); +console.log(await text(gz.pull(decompressGzip(), { autoClose: true }))); +``` + +```cjs +const { open } = require('node:fs/promises'); +const { text, pipeTo } = require('node:stream/iter'); +const { compressGzip, decompressGzip } = require('node:zlib/iter'); + +async function run() { + // Read a file, compress, write to another file + const src = await open('input.txt', 'r'); + const dst = await open('output.gz', 'w'); + await pipeTo(src.pull(), compressGzip(), dst.writer({ autoClose: true })); + await src.close(); + + // Read it back + const gz = await open('output.gz', 'r'); + console.log(await text(gz.pull(decompressGzip(), { autoClose: true }))); +} + +run().catch(console.error); +``` + +## Concepts + +### Byte streams + +All data in this API is represented as {Uint8Array} bytes. Strings +are automatically UTF-8 encoded when passed to `from()`, `push()`, or +`pipeTo()`. This removes ambiguity around encodings and enables zero-copy +transfers between streams and native code. + +### Batching + +Each iteration yields a **batch** -- an {Array} of {Uint8Array} chunks +({Uint8Array\[]}). Batching amortizes the cost of `await` and {Promise} creation +across multiple chunks. A consumer that processes one chunk at a time can +simply iterate the inner array: + +```mjs +for await (const batch of source) { + for (const chunk of batch) { + handle(chunk); + } +} +``` + +```cjs +async function run() { + for await (const batch of source) { + for (const chunk of batch) { + handle(chunk); + } + } +} +``` + +### Transforms + +Transforms come in two forms: + +* **Stateless** -- a function `(chunks, options) => result` called once per + batch. Receives `Uint8Array[]` (or `null` as the flush signal) and an + `options` object. Returns {Uint8Array\[]|null|Iterable}. + +* **Stateful** -- an object `{ transform(source, options) }` where `transform` + is a generator (sync or async) that receives the entire upstream iterable + and an `options` object, and yields output. This form is used for + compression, encryption, and any transform that needs to buffer across + batches. + +Both forms receive an `options` parameter with the following property: + +* `options.signal` {AbortSignal} An AbortSignal that fires when the pipeline + is cancelled, encounters an error, or the consumer stops reading. Transforms + can check `signal.aborted` or listen for the `'abort'` event to perform + early cleanup. + +The flush signal (`null`) is sent after the source ends, giving transforms +a chance to emit trailing data (e.g., compression footers). + +```js +// Stateless: uppercase transform +const upper = (chunks) => { + if (chunks === null) return null; // flush + return chunks.map((c) => new TextEncoder().encode( + new TextDecoder().decode(c).toUpperCase(), + )); +}; + +// Stateful: line splitter +const lines = { + transform: async function*(source) { + let partial = ''; + for await (const chunks of source) { + if (chunks === null) { + if (partial) yield [new TextEncoder().encode(partial)]; + continue; + } + for (const chunk of chunks) { + const str = partial + new TextDecoder().decode(chunk); + const parts = str.split('\n'); + partial = parts.pop(); + for (const line of parts) { + yield [new TextEncoder().encode(`${line}\n`)]; + } + } + } + }, +}; +``` + +### Pull vs. push + +The API supports two models: + +* **Pull** -- data flows on demand. `pull()` and `pullSync()` create lazy + pipelines that only read from the source when the consumer iterates. + +* **Push** -- data is written explicitly. `push()` creates a writer/readable + pair with backpressure. The writer pushes data in; the readable is consumed + as an async iterable. + +### Backpressure + +Pull streams have natural backpressure -- the consumer drives the pace, so +the source is never read faster than the consumer can process. Push streams +need explicit backpressure because the producer and consumer run +independently. The `highWaterMark` and `backpressure` options on `push()`, +`broadcast()`, and `share()` control how this works. + +#### The two-buffer model + +Push streams use a two-part buffering system. Think of it like a bucket +(slots) being filled through a hose (pending writes), with a float valve +that closes when the bucket is full: + +```text + highWaterMark (e.g., 3) + | + Producer v + | +---------+ + v | | + [ write() ] ----+ +--->| slots |---> Consumer pulls + [ write() ] | | | (bucket)| for await (...) + [ write() ] v | +---------+ + +--------+ ^ + | pending| | + | writes | float valve + | (hose) | (backpressure) + +--------+ + ^ + | + 'strict' mode limits this too! +``` + +* **Slots (the bucket)** -- data ready for the consumer, capped at + `highWaterMark`. When the consumer pulls, it drains all slots at once + into a single batch. + +* **Pending writes (the hose)** -- writes waiting for slot space. After + the consumer drains, pending writes are promoted into the now-empty + slots and their promises settle. + +How each policy uses these buffers: + +| Policy | Slots limit | Pending writes limit | +| --------------- | --------------- | -------------------- | +| `'strict'` | `highWaterMark` | `highWaterMark` | +| `'block'` | `highWaterMark` | Unbounded | +| `'drop-oldest'` | `highWaterMark` | N/A (never waits) | +| `'drop-newest'` | `highWaterMark` | N/A (never waits) | + +#### Strict (default) + +Strict mode catches "fire-and-forget" patterns where the producer calls +`write()` without awaiting, which would cause unbounded memory growth. +It limits both the slots buffer and the pending writes queue to +`highWaterMark`. + +If you properly await each write, you can only ever have one pending +write at a time (yours), so you never hit the pending writes limit. +Unawaited writes accumulate in the pending queue and throw once it +overflows: + +```mjs +import { push, text } from 'node:stream/iter'; + +const { writer, readable } = push({ highWaterMark: 16 }); + +// Consumer must run concurrently -- without it, the first write +// that fills the buffer blocks the producer forever. +const consuming = text(readable); + +// GOOD: awaited writes. The producer waits for the consumer to +// make room when the buffer is full. +for (const item of dataset) { + await writer.write(item); +} +await writer.end(); +console.log(await consuming); +``` + +```cjs +const { push, text } = require('node:stream/iter'); + +async function run() { + const { writer, readable } = push({ highWaterMark: 16 }); + + // Consumer must run concurrently -- without it, the first write + // that fills the buffer blocks the producer forever. + const consuming = text(readable); + + // GOOD: awaited writes. The producer waits for the consumer to + // make room when the buffer is full. + for (const item of dataset) { + await writer.write(item); + } + await writer.end(); + console.log(await consuming); +} + +run().catch(console.error); +``` + +Forgetting to `await` will eventually throw: + +```js +// BAD: fire-and-forget. Strict mode throws once both buffers fill. +for (const item of dataset) { + writer.write(item); // Not awaited -- queues without bound +} +// --> throws "Backpressure violation: too many pending writes" +``` + +#### Block + +Block mode caps slots at `highWaterMark` but places no limit on the +pending writes queue. Awaited writes block until the consumer makes room, +just like strict mode. The difference is that unawaited writes silently +queue forever instead of throwing -- a potential memory leak if the +producer forgets to `await`. + +This is the mode that existing Node.js classic streams and Web Streams +default to. Use it when you control the producer and know it awaits +properly, or when migrating code from those APIs. + +```mjs +import { push, text } from 'node:stream/iter'; + +const { writer, readable } = push({ + highWaterMark: 16, + backpressure: 'block', +}); + +const consuming = text(readable); + +// Safe -- awaited writes block until the consumer reads. +for (const item of dataset) { + await writer.write(item); +} +await writer.end(); +console.log(await consuming); +``` + +```cjs +const { push, text } = require('node:stream/iter'); + +async function run() { + const { writer, readable } = push({ + highWaterMark: 16, + backpressure: 'block', + }); + + const consuming = text(readable); + + // Safe -- awaited writes block until the consumer reads. + for (const item of dataset) { + await writer.write(item); + } + await writer.end(); + console.log(await consuming); +} + +run().catch(console.error); +``` + +#### Drop-oldest + +Writes never wait. When the slots buffer is full, the oldest buffered +chunk is evicted to make room for the incoming write. The consumer +always sees the most recent data. Useful for live feeds, telemetry, or +any scenario where stale data is less valuable than current data. + +```mjs +import { push } from 'node:stream/iter'; + +// Keep only the 5 most recent readings +const { writer, readable } = push({ + highWaterMark: 5, + backpressure: 'drop-oldest', +}); +``` + +```cjs +const { push } = require('node:stream/iter'); + +// Keep only the 5 most recent readings +const { writer, readable } = push({ + highWaterMark: 5, + backpressure: 'drop-oldest', +}); +``` + +#### Drop-newest + +Writes never wait. When the slots buffer is full, the incoming write is +silently discarded. The consumer processes what is already buffered +without being overwhelmed by new data. Useful for rate-limiting or +shedding load under pressure. + +```mjs +import { push } from 'node:stream/iter'; + +// Accept up to 10 buffered items; discard anything beyond that +const { writer, readable } = push({ + highWaterMark: 10, + backpressure: 'drop-newest', +}); +``` + +```cjs +const { push } = require('node:stream/iter'); + +// Accept up to 10 buffered items; discard anything beyond that +const { writer, readable } = push({ + highWaterMark: 10, + backpressure: 'drop-newest', +}); +``` + +### Writer interface + +A writer is any object conforming to the Writer interface. Only `write()` is +required; all other methods are optional. + +Each async method has a synchronous `*Sync` counterpart designed for a +try-fallback pattern: attempt the fast synchronous path first, and fall back +to the async version only when the synchronous call indicates it could not +complete: + +```mjs +if (!writer.writeSync(chunk)) await writer.write(chunk); +if (!writer.writevSync(chunks)) await writer.writev(chunks); +if (writer.endSync() < 0) await writer.end(); +writer.fail(err); // Always synchronous, no fallback needed +``` + +#### `writer.desiredSize` + +* {number|null} + +The number of buffer slots available before the high water mark is reached. +Returns `null` if the writer is closed or the consumer has disconnected. + +The value is always non-negative. + +#### `writer.end([options])` + +* `options` {Object} + * `signal` {AbortSignal} Cancel just this operation. The signal cancels only + the pending `end()` call; it does not fail the writer itself. +* Returns: {Promise} Fulfills with the total number of bytes written. + +Signal that no more data will be written. + +#### `writer.endSync()` + +* Returns: {number} Total bytes written, or `-1` if the writer is not open. + +Synchronous variant of `writer.end()`. Returns `-1` if the writer is already +closed or errored. Can be used as a try-fallback pattern: + +```cjs +const result = writer.endSync(); +if (result < 0) { + writer.end(); +} +``` + +#### `writer.fail(reason)` + +* `reason` {any} + +Put the writer into a terminal error state. If the writer is already closed +or errored, this is a no-op. Unlike `write()` and `end()`, `fail()` is +unconditionally synchronous because failing a writer is a pure state +transition with no async work to perform. + +#### `writer.write(chunk[, options])` + +* `chunk` {Uint8Array|string} +* `options` {Object} + * `signal` {AbortSignal} Cancel just this write operation. The signal cancels + only the pending `write()` call; it does not fail the writer itself. +* Returns: {Promise} Fulfills with `undefined` when buffer space is available. + +Write a chunk. + +#### `writer.writeSync(chunk)` + +* `chunk` {Uint8Array|string} +* Returns: {boolean} `true` if the write was accepted, `false` if the + buffer is full. + +Synchronous write. Does not block; returns `false` if backpressure is active. + +#### `writer.writev(chunks[, options])` + +* `chunks` {Uint8Array\[]|string\[]} +* `options` {Object} + * `signal` {AbortSignal} Cancel just this write operation. The signal cancels + only the pending `writev()` call; it does not fail the writer itself. +* Returns: {Promise} + +Write multiple chunks as a single batch. + +#### `writer.writevSync(chunks)` + +* `chunks` {Uint8Array\[]|string\[]} +* Returns: {boolean} `true` if the write was accepted, `false` if the + buffer is full. + +Synchronous batch write. + +## The `stream/iter` module + +All functions are available both as named exports and as properties of the +`Stream` namespace object: + +```mjs +// Named exports +import { from, pull, bytes, Stream } from 'node:stream/iter'; + +// Namespace access +Stream.from('hello'); +``` + +```cjs +// Named exports +const { from, pull, bytes, Stream } = require('node:stream/iter'); + +// Namespace access +Stream.from('hello'); +``` + +Including the `node:` prefix on the module specifier is optional. + +## Sources + +### `from(input)` + + + +* `input` {string|ArrayBuffer|ArrayBufferView|Iterable|AsyncIterable|Object} + Must not be `null` or `undefined`. +* Returns: {AsyncIterable} whose chunks fulfill with {Uint8Array\[]}. + +Create an async byte stream from the given input. Strings are UTF-8 encoded. +`ArrayBuffer` and `ArrayBufferView` values are wrapped as `Uint8Array`. Arrays +and iterables in `input` are recursively flattened and normalized. + +Objects implementing `Symbol.for('Stream.toAsyncStreamable')` or +`Symbol.for('Stream.toStreamable')` are converted via those protocols. The +`toAsyncStreamable` protocol takes precedence over `toStreamable`, which takes +precedence over the iteration protocols (`Symbol.asyncIterator`, +`Symbol.iterator`). + +```mjs +import { Buffer } from 'node:buffer'; +import { from, text } from 'node:stream/iter'; + +console.log(await text(from('hello'))); // 'hello' +console.log(await text(from(Buffer.from('hello')))); // 'hello' +``` + +```cjs +const { Buffer } = require('node:buffer'); +const { from, text } = require('node:stream/iter'); + +async function run() { + console.log(await text(from('hello'))); // 'hello' + console.log(await text(from(Buffer.from('hello')))); // 'hello' +} + +run().catch(console.error); +``` + +### `fromSync(input)` + + + +* `input` {string|ArrayBuffer|ArrayBufferView|Iterable|Object} + Must not be `null` or `undefined`. +* Returns: {Iterable} whose chunks return {Uint8Array\[]} + +Synchronous version of [`from()`][]. Returns a sync iterable. Cannot accept +async iterables or promises. Objects implementing +`Symbol.for('Stream.toStreamable')` are converted via that protocol (takes +precedence over `Symbol.iterator`). The `toAsyncStreamable` protocol is +ignored entirely. + +```mjs +import { fromSync, textSync } from 'node:stream/iter'; + +console.log(textSync(fromSync('hello'))); // 'hello' +``` + +```cjs +const { fromSync, textSync } = require('node:stream/iter'); + +console.log(textSync(fromSync('hello'))); // 'hello' +``` + +## Pipelines + +### `pipeTo(source[, ...transforms], writer[, options])` + + + +* `source` {AsyncIterable|Iterable} The data source. +* `...transforms` {Function|Object} Zero or more transforms to apply. +* `writer` {Object} Destination with `write(chunk)` method. +* `options` {Object} + * `signal` {AbortSignal} Abort the pipeline. + * `preventClose` {boolean} If `true`, do not call `writer.end()` when + the source ends. **Default:** `false`. + * `preventFail` {boolean} If `true`, do not call `writer.fail()` on + error. **Default:** `false`. +* Returns: {Promise} Fulfills with the total number of bytes written. + +Pipe a source through transforms into a writer. If the writer has a +`writev(chunks)` method, entire batches are passed in a single call (enabling +scatter/gather I/O). + +If the writer implements the optional `*Sync` methods (`writeSync`, `writevSync`, +`endSync`), `pipeTo()` will attempt to use the synchronous methods +first as a fast path, and fall back to the async versions only when the sync +methods indicate they cannot complete (e.g., backpressure or waiting for the +next tick). `fail()` is always called synchronously. + +```mjs +import { from, pipeTo } from 'node:stream/iter'; +import { compressGzip } from 'node:zlib/iter'; +import { open } from 'node:fs/promises'; + +const fh = await open('output.gz', 'w'); +const totalBytes = await pipeTo( + from('Hello, world!'), + compressGzip(), + fh.writer({ autoClose: true }), +); +``` + +```cjs +const { from, pipeTo } = require('node:stream/iter'); +const { compressGzip } = require('node:zlib/iter'); +const { open } = require('node:fs/promises'); + +async function run() { + const fh = await open('output.gz', 'w'); + const totalBytes = await pipeTo( + from('Hello, world!'), + compressGzip(), + fh.writer({ autoClose: true }), + ); +} + +run().catch(console.error); +``` + +### `pipeToSync(source[, ...transforms], writer[, options])` + + + +* `source` {Iterable} The sync data source. +* `...transforms` {Function|Object} Zero or more sync transforms. +* `writer` {Object} Destination with `write(chunk)` method. +* `options` {Object} + * `preventClose` {boolean} **Default:** `false`. + * `preventFail` {boolean} **Default:** `false`. +* Returns: {number} Total bytes written. + +Synchronous version of [`pipeTo()`][]. The `source`, all transforms, and the +`writer` must be synchronous. Cannot accept async iterables or promises. + +The `writer` must have the `*Sync` methods (`writeSync`, `writevSync`, +`endSync`) and `fail()` for this to work. + +### `pull(source[, ...transforms][, options])` + + + +* `source` {AsyncIterable|Iterable} The data source. +* `...transforms` {Function|Object} Zero or more transforms to apply. +* `options` {Object} + * `signal` {AbortSignal} Abort the pipeline. +* Returns: {AsyncIterable} whose chunks fulfill with {Uint8Array\[]} + +Create a lazy async pipeline. Data is not read from `source` until the +returned iterable is consumed. Transforms are applied in order. + +```mjs +import { from, pull, text } from 'node:stream/iter'; + +const asciiUpper = (chunks) => { + if (chunks === null) return null; + return chunks.map((c) => { + for (let i = 0; i < c.length; i++) { + c[i] -= (c[i] >= 97 && c[i] <= 122) * 32; + } + return c; + }); +}; + +const result = pull(from('hello'), asciiUpper); +console.log(await text(result)); // 'HELLO' +``` + +```cjs +const { from, pull, text } = require('node:stream/iter'); + +const asciiUpper = (chunks) => { + if (chunks === null) return null; + return chunks.map((c) => { + for (let i = 0; i < c.length; i++) { + c[i] -= (c[i] >= 97 && c[i] <= 122) * 32; + } + return c; + }); +}; + +async function run() { + const result = pull(from('hello'), asciiUpper); + console.log(await text(result)); // 'HELLO' +} + +run().catch(console.error); +``` + +Using an `AbortSignal`: + +```mjs +import { pull } from 'node:stream/iter'; + +const ac = new AbortController(); +const result = pull(source, transform, { signal: ac.signal }); +ac.abort(); // Pipeline throws AbortError on next iteration +``` + +```cjs +const { pull } = require('node:stream/iter'); + +const ac = new AbortController(); +const result = pull(source, transform, { signal: ac.signal }); +ac.abort(); // Pipeline throws AbortError on next iteration +``` + +### `pullSync(source[, ...transforms])` + + + +* `source` {Iterable} The sync data source. +* `...transforms` {Function|Object} Zero or more sync transforms. +* Returns: {Iterable} whose chunks return {Uint8Array\[]} + +Synchronous version of [`pull()`][]. All transforms must be synchronous. + +## Push streams + +### `push([...transforms][, options])` + + + +* `...transforms` {Function|Object} Optional transforms applied to the + readable side. +* `options` {Object} + * `highWaterMark` {number} Maximum number of buffered slots before + backpressure is applied. Must be >= 1; values below 1 are clamped to 1. + **Default:** `4`. + * `backpressure` {string} Backpressure policy: `'strict'`, `'block'`, + `'drop-oldest'`, or `'drop-newest'`. **Default:** `'strict'`. + * `signal` {AbortSignal} Abort the stream. +* Returns: {Object} + * `writer` {Writable} The writer side. + * `readable` {AsyncIterable} whose chunks fulfill with {Uint8Array\[]} + +Create a push stream with backpressure. The writer pushes data in; the +readable side is consumed as an async iterable. + +```mjs +import { push, text } from 'node:stream/iter'; + +const { writer, readable } = push(); + +// Producer and consumer must run concurrently. With strict backpressure +// (the default), awaited writes block until the consumer reads. +const producing = (async () => { + await writer.write('hello'); + await writer.write(' world'); + await writer.end(); +})(); + +console.log(await text(readable)); // 'hello world' +await producing; +``` + +```cjs +const { push, text } = require('node:stream/iter'); + +async function run() { + const { writer, readable } = push(); + + // Producer and consumer must run concurrently. With strict backpressure + // (the default), awaited writes block until the consumer reads. + const producing = (async () => { + await writer.write('hello'); + await writer.write(' world'); + await writer.end(); + })(); + + console.log(await text(readable)); // 'hello world' + await producing; +} + +run().catch(console.error); +``` + +The writer returned by `push()` conforms to the \[Writer interface]\[]. + +## Duplex channels + +### `duplex([options])` + + + +* `options` {Object} + * `highWaterMark` {number} Buffer size for both directions. + **Default:** `4`. + * `backpressure` {string} Policy for both directions. + **Default:** `'strict'`. + * `signal` {AbortSignal} Cancellation signal for both channels. + * `a` {Object} Options specific to the A-to-B direction. Overrides + shared options. + * `highWaterMark` {number} + * `backpressure` {string} + * `b` {Object} Options specific to the B-to-A direction. Overrides + shared options. + * `highWaterMark` {number} + * `backpressure` {string} +* Returns: {Array} A pair `[channelA, channelB]` of duplex channels. + +Create a pair of connected duplex channels for bidirectional communication, +similar to `socketpair()`. Data written to one channel's writer appears in +the other channel's readable. + +Each channel has: + +* `writer` — a \[Writer interface]\[] object for sending data to the peer. +* `readable` — an {AsyncIterable} for reading data from the peer. +* `close()` — close this end of the channel (idempotent). +* `[Symbol.asyncDispose]()` — async dispose support for `await using`. + +```mjs +import { duplex, text } from 'node:stream/iter'; + +const [client, server] = duplex(); + +// Server echoes back +const serving = (async () => { + for await (const chunks of server.readable) { + await server.writer.writev(chunks); + } +})(); + +await client.writer.write('hello'); +await client.writer.end(); + +console.log(await text(server.readable)); // handled by echo +await serving; +``` + +```cjs +const { duplex, text } = require('node:stream/iter'); + +async function run() { + const [client, server] = duplex(); + + // Server echoes back + const serving = (async () => { + for await (const chunks of server.readable) { + await server.writer.writev(chunks); + } + })(); + + await client.writer.write('hello'); + await client.writer.end(); + + console.log(await text(server.readable)); // handled by echo + await serving; +} + +run().catch(console.error); +``` + +## Consumers + +### `array(source[, options])` + + + +* `source` {AsyncIterable|Iterable} whose chunks must be {Uint8Array\[]} +* `options` {Object} + * `signal` {AbortSignal} + * `limit` {number} Maximum number of bytes to consume. If the total bytes + collected exceeds limit, an `ERR_OUT_OF_RANGE` error is thrown +* Returns: {Promise} Fulfills with an array of `Uint8Array` objects. + +Collect all chunks as an array of `Uint8Array` values (without concatenating). + +### `arrayBuffer(source[, options])` + + + +* `source` {AsyncIterable|Iterable} whose chunks must be {Uint8Array\[]} +* `options` {Object} + * `signal` {AbortSignal} + * `limit` {number} Maximum number of bytes to consume. If the total bytes + collected exceeds limit, an `ERR_OUT_OF_RANGE` error is thrown +* Returns: {Promise} Fulfills with an `ArrayBuffer` object. + +Collect all bytes into an `ArrayBuffer`. + +### `arrayBufferSync(source[, options])` + + + +* `source` {Iterable} whose chunks must be {Uint8Array\[]} +* `options` {Object} + * `limit` {number} Maximum number of bytes to consume. If the total bytes + collected exceeds limit, an `ERR_OUT_OF_RANGE` error is thrown +* Returns: {ArrayBuffer} + +Synchronous version of [`arrayBuffer()`][]. + +### `arraySync(source[, options])` + + + +* `source` {Iterable} whose chunks must be {Uint8Array\[]} +* `options` {Object} + * `limit` {number} Maximum number of bytes to consume. If the total bytes + collected exceeds limit, an `ERR_OUT_OF_RANGE` error is thrown +* Returns: {Uint8Array\[]} + +Synchronous version of [`array()`][]. + +### `bytes(source[, options])` + + + +* `source` {AsyncIterable|Iterable} whose chunks must be {Uint8Array\[]} +* `options` {Object} + * `signal` {AbortSignal} + * `limit` {number} Maximum number of bytes to consume. If the total bytes + collected exceeds limit, an `ERR_OUT_OF_RANGE` error is thrown +* Returns: {Promise} Fulfills with an `Uint8Array` object. + +Collect all bytes from a stream into a single `Uint8Array`. + +```mjs +import { from, bytes } from 'node:stream/iter'; + +const data = await bytes(from('hello')); +console.log(data); // Uint8Array(5) [ 104, 101, 108, 108, 111 ] +``` + +```cjs +const { from, bytes } = require('node:stream/iter'); + +async function run() { + const data = await bytes(from('hello')); + console.log(data); // Uint8Array(5) [ 104, 101, 108, 108, 111 ] +} + +run().catch(console.error); +``` + +### `bytesSync(source[, options])` + + + +* `source` {Iterable} whose chunks must be {Uint8Array\[]} +* `options` {Object} + * `limit` {number} Maximum number of bytes to consume. If the total bytes + collected exceeds limit, an `ERR_OUT_OF_RANGE` error is thrown +* Returns: {Uint8Array} + +Synchronous version of [`bytes()`][]. + +### `text(source[, options])` + + + +* `source` {AsyncIterable|Iterable} whose chunks must be {Uint8Array\[]} +* `options` {Object} + * `encoding` {string} Text encoding. **Default:** `'utf-8'`. + * `signal` {AbortSignal} + * `limit` {number} Maximum number of bytes to consume. If the total bytes + collected exceeds limit, an `ERR_OUT_OF_RANGE` error is thrown +* Returns: {Promise} Fulfills with a `string`. + +Collect all bytes and decode as text. + +```mjs +import { from, text } from 'node:stream/iter'; + +console.log(await text(from('hello'))); // 'hello' +``` + +```cjs +const { from, text } = require('node:stream/iter'); + +async function run() { + console.log(await text(from('hello'))); // 'hello' +} + +run().catch(console.error); +``` + +### `textSync(source[, options])` + + + +* `source` {Iterable} whose chunks must be {Uint8Array\[]} +* `options` {Object} + * `encoding` {string} **Default:** `'utf-8'`. + * `limit` {number} Maximum number of bytes to consume. If the total bytes + collected exceeds limit, an `ERR_OUT_OF_RANGE` error is thrown +* Returns: {string} + +Synchronous version of [`text()`][]. + +## Utilities + +### `ondrain(drainable)` + + + +* `drainable` {Object} An object implementing the drainable protocol. +* Returns: {Promise|null} + +Wait for a drainable writer's backpressure to clear. Returns `null` if +the object does not implement the drainable protocol, or a promise that +fulfills with `true` when the writer can accept more data. + +```mjs +import { push, ondrain, text } from 'node:stream/iter'; + +const { writer, readable } = push({ highWaterMark: 2 }); +writer.writeSync('a'); +writer.writeSync('b'); + +// Start consuming so the buffer can actually drain +const consuming = text(readable); + +// Buffer is full -- wait for drain +const canWrite = await ondrain(writer); +if (canWrite) { + await writer.write('c'); +} +await writer.end(); +await consuming; +``` + +```cjs +const { push, ondrain, text } = require('node:stream/iter'); + +async function run() { + const { writer, readable } = push({ highWaterMark: 2 }); + writer.writeSync('a'); + writer.writeSync('b'); + + // Start consuming so the buffer can actually drain + const consuming = text(readable); + + // Buffer is full -- wait for drain + const canWrite = await ondrain(writer); + if (canWrite) { + await writer.write('c'); + } + await writer.end(); + await consuming; +} + +run().catch(console.error); +``` + +### `merge(...sources[, options])` + + + +* `...sources` {AsyncIterable|Iterable} whose chunks must be {Uint8Array\[]} +* `options` {Object} + * `signal` {AbortSignal} +* Returns: {AsyncIterable} whose chunks fulfill with {Uint8Array\[]} + +Merge multiple async iterables by yielding batches in temporal order +(whichever source produces data first). All sources are consumed +concurrently. + +```mjs +import { from, merge, text } from 'node:stream/iter'; + +const merged = merge(from('hello '), from('world')); +console.log(await text(merged)); // Order depends on timing +``` + +```cjs +const { from, merge, text } = require('node:stream/iter'); + +async function run() { + const merged = merge(from('hello '), from('world')); + console.log(await text(merged)); // Order depends on timing +} + +run().catch(console.error); +``` + +### `tap(callback)` + + + +* `callback` {Function} `(chunks) => void` Called with each batch. +* Returns: {Function} A stateless transform. + +Create a pass-through transform that observes batches without modifying them. +Useful for logging, metrics, or debugging. + +```mjs +import { from, pull, text, tap } from 'node:stream/iter'; + +const result = pull( + from('hello'), + tap((chunks) => console.log('Batch size:', chunks.length)), +); +console.log(await text(result)); +``` + +```cjs +const { from, pull, text, tap } = require('node:stream/iter'); + +async function run() { + const result = pull( + from('hello'), + tap((chunks) => console.log('Batch size:', chunks.length)), + ); + console.log(await text(result)); +} + +run().catch(console.error); +``` + +`tap()` intentionally does not prevent in-place modification of the +chunks by the tapping callback; but return values are ignored. + +### `tapSync(callback)` + + + +* `callback` {Function} +* Returns: {Function} + +Synchronous version of [`tap()`][]. + +## Multi-consumer + +### `broadcast([options])` + + + +* `options` {Object} + * `highWaterMark` {number} Buffer size in slots. Must be >= 1; values + below 1 are clamped to 1. **Default:** `16`. + * `backpressure` {string} `'strict'`, `'block'`, `'drop-oldest'`, or + `'drop-newest'`. **Default:** `'strict'`. + * `signal` {AbortSignal} +* Returns: {Object} + * `writer` {Writable} + * `broadcast` {BroadcastChannel} + +Create a push-model multi-consumer broadcast channel. A single writer pushes +data to multiple consumers. Each consumer has an independent cursor into a +shared buffer. + +```mjs +import { broadcast, text } from 'node:stream/iter'; + +const { writer, broadcast: bc } = broadcast(); + +// Create consumers before writing +const c1 = bc.push(); // Consumer 1 +const c2 = bc.push(); // Consumer 2 + +// Producer and consumers must run concurrently. Awaited writes +// block when the buffer fills until consumers read. +const producing = (async () => { + await writer.write('hello'); + await writer.end(); +})(); + +const [r1, r2] = await Promise.all([text(c1), text(c2)]); +console.log(r1); // 'hello' +console.log(r2); // 'hello' +await producing; +``` + +```cjs +const { broadcast, text } = require('node:stream/iter'); + +async function run() { + const { writer, broadcast: bc } = broadcast(); + + // Create consumers before writing + const c1 = bc.push(); // Consumer 1 + const c2 = bc.push(); // Consumer 2 + + // Producer and consumers must run concurrently. Awaited writes + // block when the buffer fills until consumers read. + const producing = (async () => { + await writer.write('hello'); + await writer.end(); + })(); + + const [r1, r2] = await Promise.all([text(c1), text(c2)]); + console.log(r1); // 'hello' + console.log(r2); // 'hello' + await producing; +} + +run().catch(console.error); +``` + +#### `broadcast.bufferSize` + +* {number} + +The number of chunks currently buffered. + +#### `broadcast.cancel([reason])` + +* `reason` {Error} + +Cancel the broadcast. All consumers receive an error. + +#### `broadcast.consumerCount` + +* {number} + +The number of active consumers. + +#### `broadcast.push([...transforms][, options])` + +* `...transforms` {Function|Object} +* `options` {Object} + * `signal` {AbortSignal} +* Returns: {AsyncIterable} whose chunks fulfill with {Uint8Array\[]} + +Create a new consumer. Each consumer receives all data written to the +broadcast from the point of subscription onward. Optional transforms are +applied to this consumer's view of the data. + +#### `broadcast[Symbol.dispose]()` + +Alias for `broadcast.cancel()`. + +### `Broadcast.from(input[, options])` + + + +* `input` {AsyncIterable|Iterable|BroadcastChannel} +* `options` {Object} Same as `broadcast()`. +* Returns: {Object} `{ writer, broadcast }` + +Create a {BroadcastChannel} from an existing source. The source is consumed +automatically and pushed to all subscribers. + +### `share(source[, options])` + + + +* `source` {AsyncIterable} The source to share. +* `options` {Object} + * `highWaterMark` {number} Buffer size. Must be >= 1; values below 1 + are clamped to 1. **Default:** `16`. + * `backpressure` {string} `'strict'`, `'block'`, `'drop-oldest'`, or + `'drop-newest'`. **Default:** `'strict'`. +* Returns: {Share} + +Create a pull-model multi-consumer shared stream. Unlike `broadcast()`, the +source is only read when a consumer pulls. Multiple consumers share a single +buffer. + +```mjs +import { from, share, text } from 'node:stream/iter'; + +const shared = share(from('hello')); + +const c1 = shared.pull(); +const c2 = shared.pull(); + +// Consume concurrently to avoid deadlock with small buffers. +const [r1, r2] = await Promise.all([text(c1), text(c2)]); +console.log(r1); // 'hello' +console.log(r2); // 'hello' +``` + +```cjs +const { from, share, text } = require('node:stream/iter'); + +async function run() { + const shared = share(from('hello')); + + const c1 = shared.pull(); + const c2 = shared.pull(); + + // Consume concurrently to avoid deadlock with small buffers. + const [r1, r2] = await Promise.all([text(c1), text(c2)]); + console.log(r1); // 'hello' + console.log(r2); // 'hello' +} + +run().catch(console.error); +``` + +### Class: `Share` + +#### Static method: `Share.from(input[, options])` + + + +* `input` {AsyncIterable|Shareable} +* `options` {Object} Same as `share()`. +* Returns: {Share} + +Create a {Share} from an existing source. + +#### `share.bufferSize` + +* {number} + +The number of chunks currently buffered. + +#### `share.cancel([reason])` + +* `reason` {Error} + +Cancel the share. All consumers receive an error. + +#### `share.consumerCount` + +* {number} + +The number of active consumers. + +#### `share.pull([...transforms][, options])` + +* `...transforms` {Function|Object} +* `options` {Object} + * `signal` {AbortSignal} +* Returns: {AsyncIterable} whose chunks fulfill with {Uint8Array\[]} + +Create a new consumer of the shared source. + +#### `share[Symbol.dispose]()` + +Alias for `share.cancel()`. + +### Interface: `Shareable` + +#### `sharable[Symbol.for('Stream.shareProtocol')]` + +* {Function} that returns a {Share}. + +### Interface: `SyncShareable` + +#### `sharable[Symbol.for('Stream.shareSyncProtocol')]` + +* {Function} that returns a {SyncShare}. + +### `shareSync(source[, options])` + + + +* `source` {Iterable} The sync source to share. +* `options` {Object} + * `highWaterMark` {number} Must be >= 1; values below 1 are clamped + to 1. **Default:** `16`. + * `backpressure` {string} **Default:** `'strict'`. +* Returns: {SyncShare} + +Synchronous version of [`share()`][]. + +### Class: `SyncShare` + +#### Static method: `SyncShare.fromSync(input[, options])` + + + +* `input` {Iterable|SyncShareable} +* `options` {Object} +* Returns: {SyncShare} + +#### `share.bufferSize` + +* {number} + +The number of chunks currently buffered. + +#### `share.cancel([reason])` + +* `reason` {Error} + +Cancel the share. All consumers receive an error. + +#### `share.consumerCount` + +* {number} + +The number of active consumers. + +#### `share.pull([...transforms][, options])` + +* `...transforms` {Function|Object} +* `options` {Object} + * `signal` {AbortSignal} +* Returns: {Iterable} whose chunks return {Uint8Array\[]} + +Create a new consumer of the shared source. + +#### `share[Symbol.dispose]()` + +Alias for `share.cancel()`. + +## Compression and decompression transforms + +Compression and decompression transforms for use with `pull()`, `pullSync()`, +`pipeTo()`, and `pipeToSync()` are available via the [`node:zlib/iter`][] +module. See the [`node:zlib/iter` documentation][`node:zlib/iter`] for details. + +## Classic stream interop + +These utility functions bridge between classic +[`stream.Readable`][]/[`stream.Writable`][] streams and the `stream/iter` +API. + +Both `fromReadable()` and `fromWritable()` accept duck-typed objects -- they +do not require the input to extend `stream.Readable` or `stream.Writable` +directly. The minimum contract is described below for each function. + +### `fromReadable(readable)` + + + +> Stability: 1 - Experimental + +* `readable` {stream.Readable|Object} A classic Readable stream or any object + with `read()`, `on()`, and `off()` methods. +* Returns: {AsyncIterable} whose chunks fulfill with {Uint8Array\[]} + +Converts a classic Readable stream (or duck-typed equivalent) into a +stream/iter async iterable source that can be passed to [`from()`][], +[`pull()`][], [`text()`][], etc. + +If the object implements the [`toAsyncStreamable`][] protocol (as +`stream.Readable` does), that protocol is used. Otherwise, the function +duck-types on `read()`, `on()`, and `off()` (EventEmitter) and wraps the +stream with a batched async iterator. + +The result is cached per instance -- calling `fromReadable()` twice with the +same stream returns the same iterable. + +For object-mode or encoded Readable streams, chunks are automatically +normalized to `Uint8Array`. + +```mjs +import { Readable } from 'node:stream'; +import { fromReadable, text } from 'node:stream/iter'; + +const readable = new Readable({ + read() { this.push('hello world'); this.push(null); }, +}); + +const result = await text(fromReadable(readable)); +console.log(result); // 'hello world' +``` + +```cjs +const { Readable } = require('node:stream'); +const { fromReadable, text } = require('node:stream/iter'); + +const readable = new Readable({ + read() { this.push('hello world'); this.push(null); }, +}); + +async function run() { + const result = await text(fromReadable(readable)); + console.log(result); // 'hello world' +} +run(); +``` + +### `fromWritable(writable[, options])` + + + +> Stability: 1 - Experimental + +* `writable` {stream.Writable|Object} A classic Writable stream or any object + with `write()` and `on()` methods. +* `options` {Object} + * `backpressure` {string} Backpressure policy. **Default:** `'strict'`. + * `'strict'` -- writes are rejected when the buffer is full. Catches + callers that ignore backpressure. + * `'block'` -- writes wait for drain when the buffer is full. Recommended + for use with [`pipeTo()`][]. + * `'drop-newest'` -- writes are silently discarded when the buffer is full. + * `'drop-oldest'` -- **not supported**. Throws `ERR_INVALID_ARG_VALUE`. +* Returns: {Object} A stream/iter Writer adapter. + +Creates a stream/iter Writer adapter from a classic Writable stream (or +duck-typed equivalent). The adapter can be passed to [`pipeTo()`][] as a +destination. + +Since all writes on a classic Writable are fundamentally asynchronous, +the synchronous Writer methods (`writeSync`, `writevSync`, `endSync`) always +return `false` or `-1`, deferring to the async path. The per-write +`options.signal` parameter from the Writer interface is also ignored. + +The result is cached per instance and backpressure policy -- calling +`fromWritable()` twice with the same stream and `backpressure` option returns +the same Writer. + +For duck-typed streams that do not expose `writableHighWaterMark`, +`writableLength`, or similar properties, sensible defaults are used. +Object-mode writables (if detectable) are rejected since the Writer +interface is bytes-only. + +```mjs +import { Writable } from 'node:stream'; +import { from, fromWritable, pipeTo } from 'node:stream/iter'; + +const writable = new Writable({ + write(chunk, encoding, cb) { console.log(chunk.toString()); cb(); }, +}); + +await pipeTo(from('hello world'), + fromWritable(writable, { backpressure: 'block' })); +``` + +```cjs +const { Writable } = require('node:stream'); +const { from, fromWritable, pipeTo } = require('node:stream/iter'); + +async function run() { + const writable = new Writable({ + write(chunk, encoding, cb) { console.log(chunk.toString()); cb(); }, + }); + + await pipeTo(from('hello world'), + fromWritable(writable, { backpressure: 'block' })); +} +run(); +``` + +### `toReadable(source[, options])` + + + +> Stability: 1 - Experimental + +* `source` {AsyncIterable} whose chunks must fulfill with {Uint8Array\[]} + the return value of [`pull()`][] or [`from()`][]. +* `options` {Object} + * `highWaterMark` {number} The internal buffer size in bytes before + backpressure is applied. **Default:** `65536` (64 KB). + * `signal` {AbortSignal} An optional signal to abort the readable. +* Returns: {stream.Readable} + +Creates a byte-mode [`stream.Readable`][] from the `source` +(the native batch format used by the stream/iter API). Each `Uint8Array` in a +yielded batch is pushed as a separate chunk into the Readable. + +```mjs +import { createWriteStream } from 'node:fs'; +import { from, pull, toReadable } from 'node:stream/iter'; +import { compressGzip } from 'node:zlib/iter'; + +const source = pull(from('hello world'), compressGzip()); +const readable = toReadable(source); + +readable.pipe(createWriteStream('output.gz')); +``` + +```cjs +const { createWriteStream } = require('node:fs'); +const { from, pull, toReadable } = require('node:stream/iter'); +const { compressGzip } = require('node:zlib/iter'); + +const source = pull(from('hello world'), compressGzip()); +const readable = toReadable(source); + +readable.pipe(createWriteStream('output.gz')); +``` + +### `toReadableSync(source[, options])` + + + +> Stability: 1 - Experimental + +* `source` {Iterable} whose chunks must return {Uint8Array\[]}, such as the + return value of [`pullSync()`][] or [`fromSync()`][]. +* `options` {Object} + * `highWaterMark` {number} The internal buffer size in bytes before + backpressure is applied. **Default:** `65536` (64 KB). +* Returns: {stream.Readable} + +Creates a byte-mode [`stream.Readable`][] from the `source`. +The `_read()` method pulls from the iterator +synchronously, so data is available immediately via `readable.read()`. + +```mjs +import { fromSync, toReadableSync } from 'node:stream/iter'; + +const source = fromSync('hello world'); +const readable = toReadableSync(source); + +console.log(readable.read().toString()); // 'hello world' +``` + +```cjs +const { fromSync, toReadableSync } = require('node:stream/iter'); + +const source = fromSync('hello world'); +const readable = toReadableSync(source); + +console.log(readable.read().toString()); // 'hello world' +``` + +### `toWritable(writer)` + + + +> Stability: 1 - Experimental + +* `writer` {Object} A stream/iter Writer. Only the `write()` method is + required; `end()`, `fail()`, `writeSync()`, `writevSync()`, `endSync()`, + and `writev()` are optional. +* Returns: {stream.Writable} + +Creates a classic [`stream.Writable`][] backed by a stream/iter Writer. + +Each `_write()` / `_writev()` call attempts the Writer's synchronous method +first (`writeSync` / `writevSync`), falling back to the async method if the +sync path returns `false`. Similarly, `_final()` tries `endSync()` +before `end()`. When the sync path succeeds, the callback is deferred via +`queueMicrotask` to preserve the async resolution contract. + +The Writable's `highWaterMark` is set to `Number.MAX_SAFE_INTEGER` to +effectively disable its internal buffering, allowing the underlying Writer +to manage backpressure directly. + +```mjs +import { push, toWritable } from 'node:stream/iter'; + +const { writer, readable } = push(); +const writable = toWritable(writer); + +writable.write('hello'); +writable.end(); +``` + +```cjs +const { push, toWritable } = require('node:stream/iter'); + +const { writer, readable } = push(); +const writable = toWritable(writer); + +writable.write('hello'); +writable.end(); +``` + +## Protocol symbols + +These well-known symbols allow third-party objects to participate in the +streaming protocol without importing from `node:stream/iter` directly. + +### `Stream.broadcastProtocol` + +* Value: `Symbol.for('Stream.broadcastProtocol')` + +The value must be a function. When called by `Broadcast.from()`, it receives +the options passed to `Broadcast.from()` and must return an object conforming +to the {BroadcastChannel} interface. The implementation is fully custom -- it can +manage consumers, buffering, and backpressure however it wants. + +```mjs +import { Broadcast, text } from 'node:stream/iter'; + +// This example defers to the built-in Broadcast, but a custom +// implementation could use any mechanism. +class MessageBus { + #broadcast; + #writer; + + constructor() { + const { writer, broadcast } = Broadcast(); + this.#writer = writer; + this.#broadcast = broadcast; + } + + [Symbol.for('Stream.broadcastProtocol')](options) { + return this.#broadcast; + } + + send(data) { + this.#writer.write(new TextEncoder().encode(data)); + } + + close() { + this.#writer.end(); + } +} + +const bus = new MessageBus(); +const { broadcast } = Broadcast.from(bus); +const consumer = broadcast.push(); +bus.send('hello'); +bus.close(); +console.log(await text(consumer)); // 'hello' +``` + +```cjs +const { Broadcast, text } = require('node:stream/iter'); + +// This example defers to the built-in Broadcast, but a custom +// implementation could use any mechanism. +class MessageBus { + #broadcast; + #writer; + + constructor() { + const { writer, broadcast } = Broadcast(); + this.#writer = writer; + this.#broadcast = broadcast; + } + + [Symbol.for('Stream.broadcastProtocol')](options) { + return this.#broadcast; + } + + send(data) { + this.#writer.write(new TextEncoder().encode(data)); + } + + close() { + this.#writer.end(); + } +} + +const bus = new MessageBus(); +const { broadcast } = Broadcast.from(bus); +const consumer = broadcast.push(); +bus.send('hello'); +bus.close(); +text(consumer).then(console.log); // 'hello' +``` + +### `Stream.drainableProtocol` + +* Value: `Symbol.for('Stream.drainableProtocol')` + +Implement to make a writer compatible with `ondrain()`. The method should +return `null` if no backpressure, or a promise that fulfills with a truthy value +when backpressure clears. + +```mjs +import { ondrain } from 'node:stream/iter'; + +class CustomWriter { + #queue = []; + #drain = null; + #closed = false; + [Symbol.for('Stream.drainableProtocol')]() { + if (this.#closed) return null; + if (this.#queue.length < 3) return Promise.resolve(true); + this.#drain ??= Promise.withResolvers(); + return this.#drain.promise; + } + write(chunk) { + this.#queue.push(chunk); + } + flush() { + this.#queue.length = 0; + this.#drain?.resolve(true); + this.#drain = null; + } + close() { + this.#closed = true; + } +} +const writer = new CustomWriter(); +const ready = ondrain(writer); +console.log(ready); // Promise { true } -- no backpressure +``` + +```cjs +const { ondrain } = require('node:stream/iter'); + +class CustomWriter { + #queue = []; + #drain = null; + #closed = false; + + [Symbol.for('Stream.drainableProtocol')]() { + if (this.#closed) return null; + if (this.#queue.length < 3) return Promise.resolve(true); + this.#drain ??= Promise.withResolvers(); + return this.#drain.promise; + } + + write(chunk) { + this.#queue.push(chunk); + } + + flush() { + this.#queue.length = 0; + this.#drain?.resolve(true); + this.#drain = null; + } + + close() { + this.#closed = true; + } +} + +const writer = new CustomWriter(); +const ready = ondrain(writer); +console.log(ready); // Promise { true } -- no backpressure +``` + +### `Stream.shareProtocol` + +* Value: `Symbol.for('Stream.shareProtocol')` + +The value must be a function. When called by `Share.from()`, it receives the +options passed to `Share.from()` and must return an object conforming to the +{Share} interface. The implementation is fully custom -- it can manage the shared +source, consumers, buffering, and backpressure however it wants. + +```mjs +import { share, Share, text } from 'node:stream/iter'; + +// This example defers to the built-in share(), but a custom +// implementation could use any mechanism. +class DataPool { + #share; + + constructor(source) { + this.#share = share(source); + } + + [Symbol.for('Stream.shareProtocol')](options) { + return this.#share; + } +} + +const pool = new DataPool( + (async function* () { + yield 'hello'; + })(), +); + +const shared = Share.from(pool); +const consumer = shared.pull(); +console.log(await text(consumer)); // 'hello' +``` + +```cjs +const { share, Share, text } = require('node:stream/iter'); + +// This example defers to the built-in share(), but a custom +// implementation could use any mechanism. +class DataPool { + #share; + + constructor(source) { + this.#share = share(source); + } + + [Symbol.for('Stream.shareProtocol')](options) { + return this.#share; + } +} + +const pool = new DataPool( + (async function* () { + yield 'hello'; + })(), +); + +const shared = Share.from(pool); +const consumer = shared.pull(); +text(consumer).then(console.log); // 'hello' +``` + +### `Stream.shareSyncProtocol` + +* Value: `Symbol.for('Stream.shareSyncProtocol')` + +The value must be a function. When called by `SyncShare.fromSync()`, it receives +the options passed to `SyncShare.fromSync()` and must return an object conforming +to the {SyncShare} interface. The implementation is fully custom -- it can manage +the shared source, consumers, and buffering however it wants. + +```mjs +import { shareSync, SyncShare, textSync } from 'node:stream/iter'; + +// This example defers to the built-in shareSync(), but a custom +// implementation could use any mechanism. +class SyncDataPool { + #share; + + constructor(source) { + this.#share = shareSync(source); + } + + [Symbol.for('Stream.shareSyncProtocol')](options) { + return this.#share; + } +} + +const encoder = new TextEncoder(); +const pool = new SyncDataPool( + function* () { + yield [encoder.encode('hello')]; + }(), +); + +const shared = SyncShare.fromSync(pool); +const consumer = shared.pull(); +console.log(textSync(consumer)); // 'hello' +``` + +```cjs +const { shareSync, SyncShare, textSync } = require('node:stream/iter'); + +// This example defers to the built-in shareSync(), but a custom +// implementation could use any mechanism. +class SyncDataPool { + #share; + + constructor(source) { + this.#share = shareSync(source); + } + + [Symbol.for('Stream.shareSyncProtocol')](options) { + return this.#share; + } +} + +const encoder = new TextEncoder(); +const pool = new SyncDataPool( + function* () { + yield [encoder.encode('hello')]; + }(), +); + +const shared = SyncShare.fromSync(pool); +const consumer = shared.pull(); +console.log(textSync(consumer)); // 'hello' +``` + +### `Stream.toAsyncStreamable` + +* Value: `Symbol.for('Stream.toAsyncStreamable')` + +The value must be a function that converts the object into a streamable value. +When the object is encountered anywhere in the streaming pipeline (as a source +passed to `from()`, or as a value returned from a transform), this method is +called to produce the actual data. It may return any value that resolves to: +a string, `Uint8Array`, `AsyncIterable`, `Iterable`, or another streamable +object. + +```mjs +import { from, text } from 'node:stream/iter'; + +class Greeting { + #name; + + constructor(name) { + this.#name = name; + } + + [Symbol.for('Stream.toAsyncStreamable')]() { + return `hello ${this.#name}`; + } +} + +const stream = from(new Greeting('world')); +console.log(await text(stream)); // 'hello world' +``` + +```cjs +const { from, text } = require('node:stream/iter'); + +class Greeting { + #name; + + constructor(name) { + this.#name = name; + } + + [Symbol.for('Stream.toAsyncStreamable')]() { + return `hello ${this.#name}`; + } +} + +const stream = from(new Greeting('world')); +text(stream).then(console.log); // 'hello world' +``` + +### `Stream.toStreamable` + +* Value: `Symbol.for('Stream.toStreamable')` + +The value must be a function that synchronously converts the object into a +streamable value. When the object is encountered anywhere in the streaming +pipeline (as a source passed to `fromSync()`, or as a value returned from a +sync transform), this method is called to produce the actual data. It must +synchronously return a streamable value: a string, `Uint8Array`, or `Iterable`. + +```mjs +import { fromSync, textSync } from 'node:stream/iter'; + +class Greeting { + #name; + + constructor(name) { + this.#name = name; + } + + [Symbol.for('Stream.toStreamable')]() { + return `hello ${this.#name}`; + } +} + +const stream = fromSync(new Greeting('world')); +console.log(textSync(stream)); // 'hello world' +``` + +```cjs +const { fromSync, textSync } = require('node:stream/iter'); + +class Greeting { + #name; + + constructor(name) { + this.#name = name; + } + + [Symbol.for('Stream.toStreamable')]() { + return `hello ${this.#name}`; + } +} + +const stream = fromSync(new Greeting('world')); +console.log(textSync(stream)); // 'hello world' +``` + +[`--experimental-stream-iter`]: cli.md#--experimental-stream-iter +[`array()`]: #arraysource-options +[`arrayBuffer()`]: #arraybuffersource-options +[`bytes()`]: #bytessource-options +[`from()`]: #frominput +[`fromSync()`]: #fromsyncinput +[`node:zlib/iter`]: zlib.md#iterable-compression +[`pipeTo()`]: #pipetosource-transforms-writer-options +[`pull()`]: #pullsource-transforms-options +[`pullSync()`]: #pullsyncsource-transforms +[`share()`]: #sharesource-options +[`stream.Readable`]: stream.md#class-streamreadable +[`stream.Writable`]: stream.md#class-streamwritable +[`tap()`]: #tapcallback +[`text()`]: #textsource-options +[`toAsyncStreamable`]: #streamtoasyncstreamable diff --git a/doc/api/zlib.md b/doc/api/zlib.md index f6d9619040faff..d3513bfb938467 100644 --- a/doc/api/zlib.md +++ b/doc/api/zlib.md @@ -1760,11 +1760,261 @@ added: Decompress a chunk of data with [`ZstdDecompress`][]. +## Iterable Compression + + + +> Stability: 1 - Experimental + +The `node:zlib/iter` module provides compression and decompression transforms +for use with the [`node:stream/iter`][] iterable streams API. + +This module is available only when the `--experimental-stream-iter` CLI flag +is enabled. + +Each algorithm has both an async variant (stateful async generator, for use +with [`pull()`][] and [`pipeTo()`][]) and a sync variant (stateful sync +generator, for use with `pullSync()` and `pipeToSync()`). + +The async transforms run compression on the libuv threadpool, overlapping +I/O with JavaScript execution. The sync transforms run compression directly +on the main thread. + +> Note: The defaults for these transforms are tuned for streaming throughput, +> and differ from the defaults in `node:zlib`. In particular, gzip/deflate +> default to level 4 (not 6) and memLevel 9 (not 8), and Brotli defaults to +> quality 6 (not 11). These choices match common HTTP server configurations +> and provide significantly faster compression with only a small reduction in +> compression ratio. All defaults can be overridden via options. + +```mjs +import { from, pull, bytes, text } from 'node:stream/iter'; +import { compressGzip, decompressGzip } from 'node:zlib/iter'; + +// Async round-trip +const compressed = await bytes(pull(from('hello'), compressGzip())); +const original = await text(pull(from(compressed), decompressGzip())); +console.log(original); // 'hello' +``` + +```cjs +const { from, pull, bytes, text } = require('node:stream/iter'); +const { compressGzip, decompressGzip } = require('node:zlib/iter'); + +async function run() { + const compressed = await bytes(pull(from('hello'), compressGzip())); + const original = await text(pull(from(compressed), decompressGzip())); + console.log(original); // 'hello' +} + +run().catch(console.error); +``` + +```mjs +import { fromSync, pullSync, textSync } from 'node:stream/iter'; +import { compressGzipSync, decompressGzipSync } from 'node:zlib/iter'; + +// Sync round-trip +const compressed = pullSync(fromSync('hello'), compressGzipSync()); +const original = textSync(pullSync(compressed, decompressGzipSync())); +console.log(original); // 'hello' +``` + +```cjs +const { fromSync, pullSync, textSync } = require('node:stream/iter'); +const { compressGzipSync, decompressGzipSync } = require('node:zlib/iter'); + +const compressed = pullSync(fromSync('hello'), compressGzipSync()); +const original = textSync(pullSync(compressed, decompressGzipSync())); +console.log(original); // 'hello' +``` + +### `compressBrotli([options])` + +### `compressBrotliSync([options])` + + + +* `options` {Object} + * `chunkSize` {number} Output buffer size. **Default:** `65536` (64 KB). + * `params` {Object} Key-value object where keys and values are + `zlib.constants` entries. The most important compressor parameters are: + * `BROTLI_PARAM_MODE` -- `BROTLI_MODE_GENERIC` (default), + `BROTLI_MODE_TEXT`, or `BROTLI_MODE_FONT`. + * `BROTLI_PARAM_QUALITY` -- ranges from `BROTLI_MIN_QUALITY` to + `BROTLI_MAX_QUALITY`. **Default:** `6` (not `BROTLI_DEFAULT_QUALITY` + which is 11). Quality 6 is appropriate for streaming; quality 11 is + intended for offline/build-time compression. + * `BROTLI_PARAM_SIZE_HINT` -- expected input size. **Default:** `0` + (unknown). + * `BROTLI_PARAM_LGWIN` -- window size (log2). **Default:** `20` (1 MB). + The Brotli library default is 22 (4 MB); the reduced default saves + memory without significant compression impact for streaming workloads. + * `BROTLI_PARAM_LGBLOCK` -- input block size (log2). + See the [Brotli compressor options][] in the zlib documentation for the + full list. + * `dictionary` {Buffer|TypedArray|DataView} +* Returns: {Object} A stateful transform. + +Create a Brotli compression transform. Output is compatible with +`zlib.brotliDecompress()` and `decompressBrotli()`/`decompressBrotliSync()`. + +### `compressDeflate([options])` + +### `compressDeflateSync([options])` + + + +* `options` {Object} + * `chunkSize` {number} Output buffer size. **Default:** `65536` (64 KB). + * `level` {number} Compression level (`0`-`9`). **Default:** `4`. + * `windowBits` {number} **Default:** `Z_DEFAULT_WINDOWBITS` (15). + * `memLevel` {number} **Default:** `9`. + * `strategy` {number} **Default:** `Z_DEFAULT_STRATEGY`. + * `dictionary` {Buffer|TypedArray|DataView} +* Returns: {Object} A stateful transform. + +Create a deflate compression transform. Output is compatible with +`zlib.inflate()` and `decompressDeflate()`/`decompressDeflateSync()`. + +### `compressGzip([options])` + +### `compressGzipSync([options])` + + + +* `options` {Object} + * `chunkSize` {number} Output buffer size. **Default:** `65536` (64 KB). + * `level` {number} Compression level (`0`-`9`). **Default:** `4`. + * `windowBits` {number} **Default:** `Z_DEFAULT_WINDOWBITS` (15). + * `memLevel` {number} **Default:** `9`. + * `strategy` {number} **Default:** `Z_DEFAULT_STRATEGY`. + * `dictionary` {Buffer|TypedArray|DataView} +* Returns: {Object} A stateful transform. + +Create a gzip compression transform. Output is compatible with `zlib.gunzip()` +and `decompressGzip()`/`decompressGzipSync()`. + +### `compressZstd([options])` + +### `compressZstdSync([options])` + + + +* `options` {Object} + * `chunkSize` {number} Output buffer size. **Default:** `65536` (64 KB). + * `params` {Object} Key-value object where keys and values are + `zlib.constants` entries. The most important compressor parameters are: + * `ZSTD_c_compressionLevel` -- **Default:** `ZSTD_CLEVEL_DEFAULT` (3). + * `ZSTD_c_checksumFlag` -- generate a checksum. **Default:** `0`. + * `ZSTD_c_strategy` -- compression strategy. Values include + `ZSTD_fast`, `ZSTD_dfast`, `ZSTD_greedy`, `ZSTD_lazy`, + `ZSTD_lazy2`, `ZSTD_btlazy2`, `ZSTD_btopt`, `ZSTD_btultra`, + `ZSTD_btultra2`. + See the [Zstd compressor options][] in the zlib documentation for the + full list. + * `pledgedSrcSize` {number} Expected uncompressed size (optional hint). + * `dictionary` {Buffer|TypedArray|DataView} +* Returns: {Object} A stateful transform. + +Create a Zstandard compression transform. Output is compatible with +`zlib.zstdDecompress()` and `decompressZstd()`/`decompressZstdSync()`. + +### `decompressBrotli([options])` + +### `decompressBrotliSync([options])` + + + +* `options` {Object} + * `chunkSize` {number} Output buffer size. **Default:** `65536` (64 KB). + * `params` {Object} Key-value object where keys and values are + `zlib.constants` entries. Available decompressor parameters: + * `BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION` -- boolean + flag affecting internal memory allocation. + * `BROTLI_DECODER_PARAM_LARGE_WINDOW` -- boolean flag enabling "Large + Window Brotli" mode (not compatible with [RFC 7932][]). + See the [Brotli decompressor options][] in the zlib documentation for + details. + * `dictionary` {Buffer|TypedArray|DataView} +* Returns: {Object} A stateful transform. + +Create a Brotli decompression transform. + +### `decompressDeflate([options])` + +### `decompressDeflateSync([options])` + + + +* `options` {Object} + * `chunkSize` {number} Output buffer size. **Default:** `65536` (64 KB). + * `windowBits` {number} **Default:** `Z_DEFAULT_WINDOWBITS` (15). + * `dictionary` {Buffer|TypedArray|DataView} +* Returns: {Object} A stateful transform. + +Create a deflate decompression transform. + +### `decompressGzip([options])` + +### `decompressGzipSync([options])` + + + +* `options` {Object} + * `chunkSize` {number} Output buffer size. **Default:** `65536` (64 KB). + * `windowBits` {number} **Default:** `Z_DEFAULT_WINDOWBITS` (15). + * `dictionary` {Buffer|TypedArray|DataView} +* Returns: {Object} A stateful transform. + +Create a gzip decompression transform. + +### `decompressZstd([options])` + +### `decompressZstdSync([options])` + + + +* `options` {Object} + * `chunkSize` {number} Output buffer size. **Default:** `65536` (64 KB). + * `params` {Object} Key-value object where keys and values are + `zlib.constants` entries. Available decompressor parameters: + * `ZSTD_d_windowLogMax` -- maximum window size (log2) the decompressor + will allocate. Limits memory usage against malicious input. + See the [Zstd decompressor options][] in the zlib documentation for + details. + * `dictionary` {Buffer|TypedArray|DataView} +* Returns: {Object} A stateful transform. + +Create a Zstandard decompression transform. + +[Brotli compressor options]: #compressor-options +[Brotli decompressor options]: #decompressor-options [Brotli parameters]: #brotli-constants [Cyclic redundancy check]: https://en.wikipedia.org/wiki/Cyclic_redundancy_check [Memory usage tuning]: #memory-usage-tuning -[RFC 7932]: https://www.rfc-editor.org/rfc/rfc7932.txt +[RFC 7932]: https://www.rfc-editor.org/rfc/rfc7932.html [Streams API]: stream.md +[Zstd compressor options]: #compressor-options +[Zstd decompressor options]: #decompressor-options [Zstd parameters]: #zstd-constants [`.flush()`]: #zlibflushkind-callback [`Accept-Encoding`]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3 @@ -1783,6 +2033,9 @@ Decompress a chunk of data with [`ZstdDecompress`][]. [`ZstdDecompress`]: #class-zlibzstddecompress [`buffer.kMaxLength`]: buffer.md#bufferkmaxlength [`deflateInit2` and `inflateInit2`]: https://zlib.net/manual.html#Advanced +[`node:stream/iter`]: stream_iter.md +[`pipeTo()`]: stream_iter.md#pipetosource-transforms-writer-options +[`pull()`]: stream_iter.md#pullsource-transforms-options [`stream.Transform`]: stream.md#class-streamtransform [convenience methods]: #convenience-methods [zlib documentation]: https://zlib.net/manual.html#Constants diff --git a/doc/node.1 b/doc/node.1 index 7e3c0628fc3f44..f9d08a0f1a3f8d 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -253,6 +253,11 @@ to use as a custom module loader. .It Fl -permission Enable the permission model. . +.It Fl -experimental-stream-iter +Enable the experimental +.Sy node:stream/iter +module. +. .It Fl -experimental-shadow-realm Use this flag to enable ShadowRealm support. . diff --git a/lib/eslint.config_partial.mjs b/lib/eslint.config_partial.mjs index fe690a95f21a2d..956774cdcf766a 100644 --- a/lib/eslint.config_partial.mjs +++ b/lib/eslint.config_partial.mjs @@ -23,7 +23,7 @@ const noRestrictedSyntax = [ message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead", }, { - selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])', + selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError)$/])', message: "Use an error exported by 'internal/errors' instead.", }, { diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 41e4803917c3dc..02a46268c6c35c 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -1,6 +1,7 @@ 'use strict'; const { + ArrayPrototypePush, MathMax, MathMin, ObjectDefineProperties, @@ -550,13 +551,82 @@ function createBlobReaderStream(reader) { }, { highWaterMark: 0 }); } +// Maximum number of chunks to collect in a single batch to prevent +// unbounded memory growth when the DataQueue has a large burst of data. +const kMaxBatchChunks = 16; + +async function* createBlobReaderIterable(reader, options = {}) { + const { getReadError } = options; + let wakeup = PromiseWithResolvers(); + reader.setWakeup(wakeup.resolve); + + try { + while (true) { + const batch = []; + let blocked = false; + let eos = false; + let error = null; + + // Pull as many chunks as available synchronously. + // reader.pull(callback) calls the callback synchronously via + // MakeCallback, so we can collect multiple chunks per iteration + // step without any async overhead. + while (true) { + let pullResult; + reader.pull((status, buffer) => { + pullResult = { status, buffer }; + }); + + if (pullResult.status === 0) { + eos = true; + break; + } + if (pullResult.status < 0) { + error = typeof getReadError === 'function' ? + getReadError(pullResult.status) : + new ERR_INVALID_STATE('The reader is not readable'); + break; + } + if (pullResult.status === 2) { + blocked = true; + break; + } + ArrayPrototypePush(batch, new Uint8Array(pullResult.buffer)); + if (batch.length >= kMaxBatchChunks) break; + } + + if (batch.length > 0) { + yield batch; + } + + if (eos) return; + if (error) throw error; + + if (blocked) { + const fin = await wakeup.promise; + wakeup = PromiseWithResolvers(); + reader.setWakeup(wakeup.resolve); + // If the wakeup was triggered by FIN (EndReadable), the DataQueue + // is capped. Continue the loop to pull again -- the next pull will + // return EOS. Without this, a race between the data notification + // and the FIN notification can leave the iterator waiting for a + // wakeup that will never come. + if (fin) continue; + } + } + } finally { + reader.setWakeup(undefined); + } +} + module.exports = { Blob, createBlob, createBlobFromFilePath, + createBlobReaderIterable, + createBlobReaderStream, isBlob, kHandle, resolveObjectURL, TransferableBlob, - createBlobReaderStream, }; diff --git a/lib/internal/blocklist.js b/lib/internal/blocklist.js index b0840ed7fa462e..fd0e7667377b6a 100644 --- a/lib/internal/blocklist.js +++ b/lib/internal/blocklist.js @@ -298,4 +298,5 @@ ObjectSetPrototypeOf(InternalBlockList.prototype, BlockList.prototype); module.exports = { BlockList, InternalBlockList, + kHandle, }; diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js index f49f0814bbc687..2ccceb493e68bb 100644 --- a/lib/internal/bootstrap/realm.js +++ b/lib/internal/bootstrap/realm.js @@ -131,7 +131,7 @@ const schemelessBlockList = new SafeSet([ 'test/reporters', ]); // Modules that will only be enabled at run time. -const experimentalModuleList = new SafeSet(['sqlite', 'quic']); +const experimentalModuleList = new SafeSet(['sqlite', 'quic', 'stream/iter', 'zlib/iter']); // Set up process.binding() and process._linkedBinding(). { diff --git a/lib/internal/errors.js b/lib/internal/errors.js index ae5d308602c595..6e35811d0529c5 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1676,11 +1676,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP', E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError); E('ERR_PROXY_INVALID_CONFIG', '%s', Error); E('ERR_PROXY_TUNNEL', '%s', Error); -E('ERR_QUIC_APPLICATION_ERROR', 'A QUIC application error occurred. %d [%s]', Error); E('ERR_QUIC_CONNECTION_FAILED', 'QUIC connection failed', Error); E('ERR_QUIC_ENDPOINT_CLOSED', 'QUIC endpoint closed: %s (%d)', Error); E('ERR_QUIC_OPEN_STREAM_FAILED', 'Failed to open QUIC stream', Error); -E('ERR_QUIC_TRANSPORT_ERROR', 'A QUIC transport error occurred. %d [%s]', Error); +E('ERR_QUIC_STREAM_ABORTED', '%s', Error); +E('ERR_QUIC_STREAM_RESET', + 'The QUIC stream was reset by the peer with error code %d', Error); E('ERR_QUIC_VERSION_NEGOTIATION_ERROR', 'The QUIC session requires version negotiation', Error); E('ERR_REQUIRE_ASYNC_MODULE', function(filename, parentFilename) { let message = 'require() cannot be used on an ESM ' + @@ -1775,6 +1776,8 @@ E('ERR_STREAM_ALREADY_FINISHED', Error); E('ERR_STREAM_CANNOT_PIPE', 'Cannot pipe, not readable', Error); E('ERR_STREAM_DESTROYED', 'Cannot call %s after a stream was destroyed', Error); +E('ERR_STREAM_ITER_MISSING_FLAG', + 'The stream/iter API requires the --experimental-stream-iter flag', TypeError); E('ERR_STREAM_NULL_VALUES', 'May not write null values to stream', TypeError); E('ERR_STREAM_PREMATURE_CLOSE', 'Premature close', Error); E('ERR_STREAM_PUSH_AFTER_EOF', 'stream.push() after EOF', Error); diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index b65e7eb2ce3fc4..860251e71c96a4 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -16,6 +16,9 @@ const { SafePromisePrototypeFinally, Symbol, SymbolAsyncDispose, + SymbolAsyncIterator, + SymbolDispose, + SymbolIterator, Uint8Array, uncurryThis, } = primordials; @@ -43,6 +46,8 @@ const { ERR_INVALID_ARG_VALUE, ERR_INVALID_STATE, ERR_METHOD_NOT_IMPLEMENTED, + ERR_OPERATION_FAILED, + ERR_OUT_OF_RANGE, }, } = require('internal/errors'); const { isArrayBufferView } = require('internal/util/types'); @@ -68,6 +73,7 @@ const { stringToFlags, stringToSymlinkType, toUnixTimestamp, + handleErrorFromBinding: handleSyncErrorFromBinding, validateBufferArray, validateCpOptions, validateOffsetLengthRead, @@ -100,6 +106,7 @@ const { isWindows, isMacOS, } = require('internal/util'); +const { getOptionValue } = require('internal/options'); const EventEmitter = require('events'); const { StringDecoder } = require('string_decoder'); const { kFSWatchStart, watch } = require('internal/fs/watchers'); @@ -119,6 +126,7 @@ const kCloseReject = Symbol('kCloseReject'); const kRef = Symbol('kRef'); const kUnref = Symbol('kUnref'); const kLocked = Symbol('kLocked'); +const kCloseSync = Symbol('kCloseSync'); const { kUsePromises } = binding; const { Interface } = require('internal/readline/interface'); @@ -146,6 +154,24 @@ const lazyReadableStream = getLazy(() => require('internal/webstreams/readablestream').ReadableStream, ); +// Lazy loaded to avoid circular dependency with new streams. +let newStreamsPull; +let newStreamsPullSync; +let newStreamsParsePullArgs; +let newStreamsToUint8Array; +let newStreamsConvertChunks; +function lazyNewStreams() { + if (newStreamsPull === undefined) { + const pullModule = require('internal/streams/iter/pull'); + newStreamsPull = pullModule.pull; + newStreamsPullSync = pullModule.pullSync; + const utils = require('internal/streams/iter/utils'); + newStreamsParsePullArgs = utils.parsePullArgs; + newStreamsToUint8Array = utils.toUint8Array; + newStreamsConvertChunks = utils.convertChunks; + } +} + // By the time the C++ land creates an error for a promise rejection (likely from a // libuv callback), there is already no JS frames on the stack. So we need to // wait until V8 resumes execution back to JS land before we have enough information @@ -156,6 +182,12 @@ function handleErrorFromBinding(error) { } class FileHandle extends EventEmitter { + #brandCheck = undefined; + + static isFileHandle(value) { + return (value != null && typeof value === 'object' && #brandCheck in value); + } + /** * @param {InternalFSBinding.FileHandle | undefined} filehandle */ @@ -273,6 +305,16 @@ class FileHandle extends EventEmitter { return this[kClosePromise]; }; + [kCloseSync]() { + if (this[kFd] === -1) return; + if (this[kClosePromise]) { + throw new ERR_INVALID_STATE('The FileHandle is closing'); + } + this[kFd] = -1; + this[kHandle].closeSync(); + this.emit('close'); + } + async [SymbolAsyncDispose]() { await this.close(); } @@ -428,6 +470,612 @@ class FileHandle extends EventEmitter { } } +if (getOptionValue('--experimental-stream-iter')) { + const kNullPrototo = { __proto__: null }; + const kDefaultChunkSize = 131072; + const kNone = -1; + /** + * Return the file contents as an AsyncIterable using the + * new streams pull model. Optional transforms and options (including + * AbortSignal) may be provided as trailing arguments, mirroring the + * Stream.pull() signature. + * @param {...(Function|object)} args - Optional transforms and/or options + * @returns {AsyncIterable} + */ + FileHandle.prototype.pull = function pull(...args) { + if (this[kFd] === kNone) + throw new ERR_INVALID_STATE('The FileHandle is closed'); + if (this[kClosePromise]) + throw new ERR_INVALID_STATE('The FileHandle is closing'); + if (this[kLocked]) + throw new ERR_INVALID_STATE('The FileHandle is locked'); + + lazyNewStreams(); + const { transforms, options = kNullPrototo } = newStreamsParsePullArgs(args); + + const { + autoClose = false, + chunkSize: readSize = kDefaultChunkSize, + signal, + } = options; + let { + start: pos = kNone, + limit: remaining = kNone, + } = options; + + const handle = this; + const fd = this[kFd]; + + validateBoolean(autoClose, 'options.autoClose'); + + if (pos !== kNone) { + validateInteger(pos, 'options.start', 0); + } + if (remaining !== kNone) { + validateInteger(remaining, 'options.limit', 1); + } + if (readSize !== undefined) { + validateInteger(readSize, 'options.chunkSize', 1); + } + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + } + + this[kLocked] = true; + + const source = { + __proto__: null, + async *[SymbolAsyncIterator]() { + handle[kRef](); + try { + if (signal) { + // Signal-aware path + while (remaining !== 0) { + if (signal.aborted) { + throw signal.reason ?? + lazyDOMException('The operation was aborted', + 'AbortError'); + } + const toRead = remaining > 0 ? + MathMin(readSize, remaining) : readSize; + const buf = Buffer.allocUnsafe(toRead); + let bytesRead; + try { + bytesRead = + (await binding.read(fd, buf, 0, + toRead, pos, kUsePromises)) || 0; + } catch (err) { + ErrorCaptureStackTrace(err, handleErrorFromBinding); + throw err; + } + if (bytesRead === 0) break; + if (pos >= 0) pos += bytesRead; + if (remaining > 0) remaining -= bytesRead; + yield [bytesRead < toRead ? buf.subarray(0, bytesRead) : buf]; + } + } else { + // Fast path - no signal check per iteration + while (remaining !== 0) { + const toRead = remaining > 0 ? + MathMin(readSize, remaining) : readSize; + const buf = Buffer.allocUnsafe(toRead); + let bytesRead; + try { + bytesRead = + (await binding.read(fd, buf, 0, + toRead, pos, kUsePromises)) || 0; + } catch (err) { + ErrorCaptureStackTrace(err, handleErrorFromBinding); + throw err; + } + if (bytesRead === 0) break; + if (pos >= 0) pos += bytesRead; + if (remaining > 0) remaining -= bytesRead; + yield [bytesRead < toRead ? buf.subarray(0, bytesRead) : buf]; + } + } + } finally { + handle[kLocked] = false; + handle[kUnref](); + if (autoClose) { + await handle.close(); + } + } + }, + }; + + // If transforms provided, wrap with pull pipeline + if (transforms.length > 0) { + const pullArgs = [...transforms]; + if (options) { + ArrayPrototypePush(pullArgs, options); + } + return newStreamsPull(source, ...pullArgs); + } + return source; + }; + + /** + * Return the file contents as an Iterable using synchronous + * reads. Optional transforms and options may be provided as trailing + * arguments, mirroring the Stream.pullSync() signature. + * @param {...(Function|object)} args - Optional transforms and/or options + * @returns {Iterable} + */ + FileHandle.prototype.pullSync = function pullSync(...args) { + if (this[kFd] === kNone) + throw new ERR_INVALID_STATE('The FileHandle is closed'); + if (this[kClosePromise]) + throw new ERR_INVALID_STATE('The FileHandle is closing'); + if (this[kLocked]) + throw new ERR_INVALID_STATE('The FileHandle is locked'); + + lazyNewStreams(); + const { transforms, options = kNullPrototo } = newStreamsParsePullArgs(args); + + const { + autoClose = false, + chunkSize: readSize = kDefaultChunkSize, + } = options; + let { + start: pos = kNone, + limit: remaining = kNone, + } = options; + + const handle = this; + const fd = this[kFd]; + + validateBoolean(autoClose, 'options.autoClose'); + + if (pos !== kNone) { + validateInteger(pos, 'options.start', 0); + } + if (remaining !== kNone) { + validateInteger(remaining, 'options.limit', 1); + } + if (readSize !== undefined) { + validateInteger(readSize, 'options.chunkSize', 1); + } + + this[kLocked] = true; + + handle[kRef](); + + function cleanup() { + handle[kLocked] = false; + handle[kUnref](); + if (autoClose) { + handle[kCloseSync](); + } + } + + const source = { + __proto__: null, + [SymbolIterator]() { + let done = false; + return { + __proto__: null, + next() { + if (done || remaining === 0) { + if (!done) { + done = true; + cleanup(); + } + return { value: undefined, done: true }; + } + const toRead = remaining > 0 ? + MathMin(readSize, remaining) : readSize; + const buf = Buffer.allocUnsafe(toRead); + let bytesRead; + try { + bytesRead = binding.read(fd, buf, 0, toRead, pos) || 0; + } catch (err) { + done = true; + cleanup(); + throw err; + } + if (bytesRead === 0) { + done = true; + cleanup(); + return { value: undefined, done: true }; + } + if (pos >= 0) pos += bytesRead; + if (remaining > 0) remaining -= bytesRead; + const chunk = bytesRead < toRead ? + buf.subarray(0, bytesRead) : buf; + return { value: [chunk], done: false }; + }, + return() { + if (!done) { + done = true; + cleanup(); + } + return { value: undefined, done: true }; + }, + }; + }, + }; + + if (transforms.length > 0) { + return newStreamsPullSync(source, ...transforms); + } + return source; + }; + + /** + * Return a new-streams Writer backed by this file handle. + * The writer uses direct binding.writeBuffer / binding.writeBuffers + * calls, bypassing the FileHandle.write() validation chain. + * + * Supports writev() for batch writes (single syscall per batch). + * Handles EAGAIN with retry (up to 5 attempts), matching WriteStream. + * @param {{ + * autoClose?: boolean; + * start?: number; + * }} [options] + * @returns {{ write, writev, end, fail }} + */ + FileHandle.prototype.writer = function writer(options = kNullPrototo) { + if (this[kFd] === kNone) + throw new ERR_INVALID_STATE('The FileHandle is closed'); + if (this[kClosePromise]) + throw new ERR_INVALID_STATE('The FileHandle is closing'); + if (this[kLocked]) + throw new ERR_INVALID_STATE('The FileHandle is locked'); + + lazyNewStreams(); + + validateObject(options, 'options'); + const { + autoClose = false, + chunkSize: syncWriteThreshold = kDefaultChunkSize, + } = options; + let { + start: pos = kNone, + limit: bytesRemaining = kNone, + } = options; + + const handle = this; + const fd = this[kFd]; + let totalBytesWritten = 0; + let closed = false; + let closing = false; + let pendingEndPromise = null; + let error = null; + let asyncPending = false; + + validateBoolean(autoClose, 'options.autoClose'); + + if (pos !== kNone) { + validateInteger(pos, 'options.start', 0); + } + if (bytesRemaining !== kNone) { + validateInteger(bytesRemaining, 'options.limit', 1); + } + if (syncWriteThreshold !== undefined) { + validateInteger(syncWriteThreshold, 'options.chunkSize', 1); + } + + this[kLocked] = true; + handle[kRef](); + + // Write a single buffer with EAGAIN retry (up to 5 retries). + async function writeAll(buf, offset, length, position, signal) { + asyncPending = true; + try { + let retries = 0; + while (length > 0) { + const bytesWritten = (await PromisePrototypeThen( + binding.writeBuffer(fd, buf, offset, length, position, + kUsePromises), + undefined, + handleErrorFromBinding, + )) || 0; + + signal?.throwIfAborted(); + + if (bytesWritten === 0) { + if (++retries > 5) { + throw new ERR_OPERATION_FAILED('write failed after retries'); + } + } else { + retries = 0; + } + + totalBytesWritten += bytesWritten; + offset += bytesWritten; + length -= bytesWritten; + if (position >= 0) position += bytesWritten; + } + } finally { + asyncPending = false; + } + } + + // Writev with EAGAIN retry. On partial write, concatenates remaining + // buffers and falls back to writeAll (same approach as WriteStream). + async function writevAll(buffers, position, signal) { + asyncPending = true; + try { + let totalSize = 0; + for (let i = 0; i < buffers.length; i++) { + totalSize += buffers[i].byteLength; + } + + let retries = 0; + while (totalSize > 0) { + const bytesWritten = (await PromisePrototypeThen( + binding.writeBuffers(fd, buffers, position, kUsePromises), + undefined, + handleErrorFromBinding, + )) || 0; + + signal?.throwIfAborted(); + + if (bytesWritten === 0) { + if (++retries > 5) { + throw new ERR_OPERATION_FAILED('writev failed after retries'); + } + } else { + retries = 0; + } + + totalBytesWritten += bytesWritten; + totalSize -= bytesWritten; + if (position >= 0) position += bytesWritten; + + if (totalSize > 0) { + // Partial write - concatenate remaining and use writeAll. + const remaining = Buffer.concat(buffers); + const wrote = bytesWritten; + // writeAll is already inside asyncPending = true, but + // writeAll sets it again - that's fine (idempotent). + await writeAll(remaining, wrote, remaining.length - wrote, + position, signal); + return; + } + } + } finally { + asyncPending = false; + } + } + + // Synchronous write with EAGAIN retry. Throws on I/O error. + // Used by writeSync for the full write, and by writevSync for + // completing a partial writev. + function writeSyncAll(buf, offset, length, position) { + let retries = 0; + while (length > 0) { + const ctx = {}; + const bytesWritten = binding.writeBuffer( + fd, buf, offset, length, position, undefined, ctx) || 0; + if (ctx.errno !== undefined) { + handleSyncErrorFromBinding(ctx); + } + if (bytesWritten === 0) { + if (++retries > 5) { + throw new ERR_OPERATION_FAILED('write failed after retries'); + } + } else { + retries = 0; + } + totalBytesWritten += bytesWritten; + offset += bytesWritten; + length -= bytesWritten; + if (position >= 0) position += bytesWritten; + } + } + + async function cleanup() { + if (closed) return; + closed = true; + handle[kLocked] = false; + handle[kUnref](); + if (autoClose) { + await handle.close(); + } + } + + return { + __proto__: null, + write(chunk, options = kNullPrototo) { + if (error) { + return PromiseReject(error); + } + if (closed) { + return PromiseReject( + new ERR_INVALID_STATE.TypeError('The writer is closed')); + } + validateObject(options, 'options'); + const { + signal, + } = options; + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + if (signal.aborted) { + return PromiseReject(signal.reason); + } + } + chunk = newStreamsToUint8Array(chunk); + if (bytesRemaining >= 0 && chunk.byteLength > bytesRemaining) { + return PromiseReject( + new ERR_OUT_OF_RANGE('write', `<= ${bytesRemaining} bytes`, + chunk.byteLength)); + } + if (bytesRemaining > 0) bytesRemaining -= chunk.byteLength; + const position = pos; + if (pos >= 0) pos += chunk.byteLength; + return writeAll(chunk, 0, chunk.byteLength, position, signal); + }, + + writev(chunks, options = kNullPrototo) { + if (error) { + return PromiseReject(error); + } + if (closed) { + return PromiseReject( + new ERR_INVALID_STATE.TypeError('The writer is closed')); + } + validateObject(options, 'options'); + const { + signal, + } = options; + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + if (signal?.aborted) { + return PromiseReject(signal.reason); + } + } + chunks = newStreamsConvertChunks(chunks); + let totalSize = 0; + for (let i = 0; i < chunks.length; i++) { + totalSize += chunks[i].byteLength; + } + if (bytesRemaining >= 0 && totalSize > bytesRemaining) { + return PromiseReject( + new ERR_OUT_OF_RANGE('writev', `<= ${bytesRemaining} bytes`, + totalSize)); + } + if (bytesRemaining > 0) bytesRemaining -= totalSize; + const position = pos; + if (pos >= 0) pos += totalSize; + return writevAll(chunks, position, signal); + }, + + writeSync(chunk) { + if (error || closed || asyncPending) return false; + chunk = newStreamsToUint8Array(chunk); + const length = chunk.byteLength; + if (length > syncWriteThreshold) return false; + if (length === 0) return true; + if (bytesRemaining >= 0 && length > bytesRemaining) return false; + const position = pos; + // First attempt - if this fails with zero bytes written, + // return false so pipeTo can fall back to async write(). + const ctx = {}; + const bytesWritten = binding.writeBuffer( + fd, chunk, 0, length, position, undefined, ctx) || 0; + if (ctx.errno !== undefined) return false; + totalBytesWritten += bytesWritten; + if (position >= 0) { + pos = position + bytesWritten; + } + if (bytesWritten === length) { + if (bytesRemaining > 0) bytesRemaining -= length; + return true; + } + // Partial write - bytes are on disk. Must complete or throw. + // Cannot return false here because pipeTo would re-send the + // full chunk, causing duplicate data on disk. + writeSyncAll(chunk, bytesWritten, length - bytesWritten, + position >= 0 ? position + bytesWritten : -1); + if (bytesRemaining > 0) bytesRemaining -= length; + return true; + }, + + writevSync(chunks) { + if (error || closed || asyncPending) return false; + chunks = newStreamsConvertChunks(chunks); + let totalSize = 0; + for (let i = 0; i < chunks.length; i++) { + totalSize += chunks[i].byteLength; + } + if (totalSize > syncWriteThreshold) return false; + if (totalSize === 0) return true; + if (bytesRemaining >= 0 && totalSize > bytesRemaining) return false; + const position = pos; + // writeBuffers throws on error (zero bytes written) - safe + // to catch and return false for async fallback. + let bytesWritten; + try { + bytesWritten = binding.writeBuffers(fd, chunks, position) || 0; + } catch { + return false; + } + totalBytesWritten += bytesWritten; + if (position >= 0) { + pos = position + bytesWritten; + } + if (bytesWritten === totalSize) { + if (bytesRemaining > 0) bytesRemaining -= totalSize; + return true; + } + // Partial writev - bytes are on disk. Must complete or throw. + const rest = Buffer.concat(chunks); + writeSyncAll(rest, bytesWritten, + rest.byteLength - bytesWritten, + position >= 0 ? position + bytesWritten : -1); + if (bytesRemaining > 0) bytesRemaining -= totalSize; + return true; + }, + + end(options = kNullPrototo) { + if (error) { + return PromiseReject(error); + } + if (closed) { + return PromiseResolve(totalBytesWritten); + } + if (closing) { + return pendingEndPromise; + } + validateObject(options, 'options'); + const { + signal, + } = options; + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + if (signal.aborted) { + return PromiseReject(signal.reason); + } + } + closing = true; + pendingEndPromise = PromisePrototypeThen( + cleanup(), () => totalBytesWritten); + return pendingEndPromise; + }, + + endSync() { + if (error) return -1; + if (closed) return totalBytesWritten; + if (asyncPending) return -1; + closed = true; + handle[kLocked] = false; + handle[kUnref](); + if (autoClose) { + handle[kCloseSync](); + } + return totalBytesWritten; + }, + + fail(reason) { + if (closed || error) return; + error = reason ?? new ERR_INVALID_STATE('Failed'); + closed = true; + handle[kLocked] = false; + handle[kUnref](); + if (autoClose) { + handle[kCloseSync](); + } + }, + + [SymbolAsyncDispose]() { + if (closing) { + return pendingEndPromise ?? PromiseResolve(); + } + if (!closed && !error) { + this.fail(); + } + return PromiseResolve(); + }, + + [SymbolDispose]() { + this.fail(); + }, + }; + }; +} + async function handleFdClose(fileOpPromise, closeFunc) { return PromisePrototypeThen( fileOpPromise, @@ -1413,6 +2061,8 @@ module.exports = { }, FileHandle, + kHandle, + kLocked, kRef, kUnref, }; diff --git a/lib/internal/perf/observe.js b/lib/internal/perf/observe.js index 52499fa186151e..f35544328d430b 100644 --- a/lib/internal/perf/observe.js +++ b/lib/internal/perf/observe.js @@ -28,6 +28,7 @@ const { NODE_PERFORMANCE_ENTRY_TYPE_HTTP, NODE_PERFORMANCE_ENTRY_TYPE_NET, NODE_PERFORMANCE_ENTRY_TYPE_DNS, + NODE_PERFORMANCE_ENTRY_TYPE_QUIC, }, installGarbageCollectionTracking, observerCounts, @@ -94,6 +95,7 @@ const kSupportedEntryTypes = ObjectFreeze([ 'mark', 'measure', 'net', + 'quic', 'resource', ]); @@ -138,6 +140,7 @@ function getObserverType(type) { case 'http': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP; case 'net': return NODE_PERFORMANCE_ENTRY_TYPE_NET; case 'dns': return NODE_PERFORMANCE_ENTRY_TYPE_DNS; + case 'quic': return NODE_PERFORMANCE_ENTRY_TYPE_QUIC; } } diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 2716c2ae81b642..909b336e1874d4 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -115,6 +115,7 @@ function prepareExecution(options) { setupNavigator(); setupWarningHandler(); setupSQLite(); + setupStreamIter(); setupQuic(); setupWebStorage(); setupWebsocket(); @@ -391,6 +392,16 @@ function initializeConfigFileSupport() { } } +function setupStreamIter() { + if (!getOptionValue('--experimental-stream-iter')) { + return; + } + + const { BuiltinModule } = require('internal/bootstrap/realm'); + BuiltinModule.allowRequireByUsers('stream/iter'); + BuiltinModule.allowRequireByUsers('zlib/iter'); +} + function setupQuic() { if (!getOptionValue('--experimental-quic')) { return; diff --git a/lib/internal/quic/diagnostics.js b/lib/internal/quic/diagnostics.js new file mode 100644 index 00000000000000..7180f719bc09d3 --- /dev/null +++ b/lib/internal/quic/diagnostics.js @@ -0,0 +1,79 @@ +'use strict'; + +// TODO(@jasnell) Temporarily ignoring c8 covrerage for this file while tests +// are still being developed. +/* c8 ignore start */ + +const dc = require('diagnostics_channel'); + +const onEndpointCreatedChannel = dc.channel('quic.endpoint.created'); +const onEndpointListeningChannel = dc.channel('quic.endpoint.listen'); +const onEndpointClosingChannel = dc.channel('quic.endpoint.closing'); +const onEndpointClosedChannel = dc.channel('quic.endpoint.closed'); +const onEndpointErrorChannel = dc.channel('quic.endpoint.error'); +const onEndpointBusyChangeChannel = dc.channel('quic.endpoint.busy.change'); +const onEndpointClientSessionChannel = dc.channel('quic.session.created.client'); +const onEndpointServerSessionChannel = dc.channel('quic.session.created.server'); +const onSessionApplicationChannel = dc.channel('quic.session.application'); +const onSessionOpenStreamChannel = dc.channel('quic.session.open.stream'); +const onSessionReceivedStreamChannel = dc.channel('quic.session.received.stream'); +const onSessionSendDatagramChannel = dc.channel('quic.session.send.datagram'); +const onSessionUpdateKeyChannel = dc.channel('quic.session.update.key'); +const onSessionClosingChannel = dc.channel('quic.session.closing'); +const onSessionClosedChannel = dc.channel('quic.session.closed'); +const onSessionReceiveDatagramChannel = dc.channel('quic.session.receive.datagram'); +const onSessionReceiveDatagramStatusChannel = dc.channel('quic.session.receive.datagram.status'); +const onSessionPathValidationChannel = dc.channel('quic.session.path.validation'); +const onSessionNewTokenChannel = dc.channel('quic.session.new.token'); +const onSessionTicketChannel = dc.channel('quic.session.ticket'); +const onSessionVersionNegotiationChannel = dc.channel('quic.session.version.negotiation'); +const onSessionOriginChannel = dc.channel('quic.session.receive.origin'); +const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); +const onSessionGoawayChannel = dc.channel('quic.session.goaway'); +const onSessionEarlyRejectedChannel = dc.channel('quic.session.early.rejected'); +const onStreamClosedChannel = dc.channel('quic.stream.closed'); +const onStreamHeadersChannel = dc.channel('quic.stream.headers'); +const onStreamTrailersChannel = dc.channel('quic.stream.trailers'); +const onStreamInfoChannel = dc.channel('quic.stream.info'); +const onStreamResetChannel = dc.channel('quic.stream.reset'); +const onStreamBlockedChannel = dc.channel('quic.stream.blocked'); +const onSessionErrorChannel = dc.channel('quic.session.error'); +const onEndpointConnectChannel = dc.channel('quic.endpoint.connect'); + +module.exports = { + onEndpointCreatedChannel, + onEndpointListeningChannel, + onEndpointClosingChannel, + onEndpointClosedChannel, + onEndpointErrorChannel, + onEndpointBusyChangeChannel, + onEndpointClientSessionChannel, + onEndpointServerSessionChannel, + onSessionApplicationChannel, + onSessionOpenStreamChannel, + onSessionReceivedStreamChannel, + onSessionSendDatagramChannel, + onSessionUpdateKeyChannel, + onSessionClosingChannel, + onSessionClosedChannel, + onSessionReceiveDatagramChannel, + onSessionReceiveDatagramStatusChannel, + onSessionPathValidationChannel, + onSessionNewTokenChannel, + onSessionTicketChannel, + onSessionVersionNegotiationChannel, + onSessionOriginChannel, + onSessionHandshakeChannel, + onSessionGoawayChannel, + onSessionEarlyRejectedChannel, + onStreamClosedChannel, + onStreamHeadersChannel, + onStreamTrailersChannel, + onStreamInfoChannel, + onStreamResetChannel, + onStreamBlockedChannel, + onSessionErrorChannel, + onEndpointConnectChannel, +}; + +/* c8 ignore stop */ diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 5f5dc738a2f2bf..a7df1c4eb7d043 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -5,14 +5,26 @@ /* c8 ignore start */ const { - ArrayBufferPrototypeTransfer, + ArrayFrom, ArrayIsArray, ArrayPrototypePush, BigInt, + DataViewPrototypeGetByteLength, + ErrorCaptureStackTrace, + FunctionPrototypeBind, + Number, ObjectDefineProperties, ObjectKeys, + PromisePrototypeThen, + PromiseResolve, + PromiseWithResolvers, SafeSet, + Symbol, SymbolAsyncDispose, + SymbolAsyncIterator, + SymbolDispose, + SymbolIterator, + TypedArrayPrototypeGetByteLength, Uint8Array, } = primordials; @@ -26,6 +38,10 @@ if (!process.features.quic || !getOptionValue('--experimental-quic')) { } const { inspect } = require('internal/util/inspect'); +const { + BlockList, + kHandle: kBlockListHandle, +} = require('internal/blocklist'); let debug = require('internal/util/debuglog').debuglog('quic', (fn) => { debug = fn; @@ -36,30 +52,49 @@ const { setCallbacks, // The constants to be exposed to end users for various options. - CC_ALGO_RENO_STR, - CC_ALGO_CUBIC_STR, - CC_ALGO_BBR_STR, - PREFERRED_ADDRESS_IGNORE, - PREFERRED_ADDRESS_USE, - DEFAULT_PREFERRED_ADDRESS_POLICY, + CC_ALGO_RENO_STR: CC_ALGO_RENO, + CC_ALGO_CUBIC_STR: CC_ALGO_CUBIC, + CC_ALGO_BBR_STR: CC_ALGO_BBR, DEFAULT_CIPHERS, DEFAULT_GROUPS, - STREAM_DIRECTION_BIDIRECTIONAL, - STREAM_DIRECTION_UNIDIRECTIONAL, // Internal constants for use by the implementation. // These are not exposed to end users. + PREFERRED_ADDRESS_IGNORE: kPreferredAddressIgnore, + PREFERRED_ADDRESS_USE: kPreferredAddressUse, + DEFAULT_PREFERRED_ADDRESS_POLICY: kPreferredAddressDefault, + STREAM_DIRECTION_BIDIRECTIONAL: kStreamDirectionBidirectional, + STREAM_DIRECTION_UNIDIRECTIONAL: kStreamDirectionUnidirectional, CLOSECONTEXT_CLOSE: kCloseContextClose, CLOSECONTEXT_BIND_FAILURE: kCloseContextBindFailure, CLOSECONTEXT_LISTEN_FAILURE: kCloseContextListenFailure, CLOSECONTEXT_RECEIVE_FAILURE: kCloseContextReceiveFailure, CLOSECONTEXT_SEND_FAILURE: kCloseContextSendFailure, CLOSECONTEXT_START_FAILURE: kCloseContextStartFailure, + QUIC_STREAM_HEADERS_KIND_INITIAL: kHeadersKindInitial, + QUIC_STREAM_HEADERS_KIND_HINTS: kHeadersKindHints, + QUIC_STREAM_HEADERS_KIND_TRAILING: kHeadersKindTrailing, + QUIC_STREAM_HEADERS_FLAGS_NONE: kHeadersFlagsNone, + QUIC_STREAM_HEADERS_FLAGS_TERMINAL: kHeadersFlagsTerminal, } = internalBinding('quic'); +// Maps the numeric HeadersKind constants from C++ to user-facing strings. +// Indexed by the enum value (HINTS=0, INITIAL=1, TRAILING=2). +const kHeadersKindName = []; +kHeadersKindName[kHeadersKindHints] = 'hints'; +kHeadersKindName[kHeadersKindInitial] = 'initial'; +kHeadersKindName[kHeadersKindTrailing] = 'trailing'; + +const { + markPromiseAsHandled, +} = internalBinding('util'); + const { isArrayBuffer, isArrayBufferView, + isDataView, + isPromise, + isSharedArrayBuffer, } = require('util/types'); const { @@ -72,12 +107,14 @@ const { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_STATE, + ERR_INVALID_THIS, ERR_MISSING_ARGS, - ERR_QUIC_APPLICATION_ERROR, + ERR_OUT_OF_RANGE, ERR_QUIC_CONNECTION_FAILED, ERR_QUIC_ENDPOINT_CLOSED, ERR_QUIC_OPEN_STREAM_FAILED, - ERR_QUIC_TRANSPORT_ERROR, + ERR_QUIC_STREAM_ABORTED, + ERR_QUIC_STREAM_RESET, ERR_QUIC_VERSION_NEGOTIATION_ERROR, }, } = require('internal/errors'); @@ -89,11 +126,26 @@ const { } = require('internal/socketaddress'); const { - createBlobReaderStream, + createBlobReaderIterable, isBlob, kHandle: kBlobHandle, } = require('internal/blob'); +const { + drainableProtocol, + kValidatedSource, +} = require('internal/streams/iter/types'); + +const { + toUint8Array, + convertChunks, +} = require('internal/streams/iter/utils'); + +const { + from: streamFrom, + fromSync: streamFromSync, +} = require('internal/streams/iter/from'); + const { getKeyObjectHandle, getKeyObjectType, @@ -101,10 +153,22 @@ const { } = require('internal/crypto/keys'); const { + InternalX509Certificate, +} = require('internal/crypto/x509'); + +const { + FileHandle, + kHandle: kFileHandle, + kLocked: kFileLocked, +} = require('internal/fs/promises'); + +const { + validateAbortSignal, validateBoolean, validateFunction, - validateNumber, + validateInteger, validateObject, + validateOneOf, validateString, } = require('internal/validators'); @@ -116,38 +180,46 @@ const { const kEmptyObject = { __proto__: null }; const { + kAttachFileHandle, kBlocked, kConnect, kDatagram, kDatagramStatus, + kDrain, + kEarlyDataRejected, kFinishClose, + kGoaway, kHandshake, + kHandshakeCompleted, + kVerifyPeer, kHeaders, kOwner, kRemoveSession, + kKeylog, kListen, kNewSession, + kQlog, kRemoveStream, kNewStream, - kOnHeaders, - kOnTrailers, + kNewToken, + kOrigin, + kStreamCallbacks, kPathValidation, kPrivateConstructor, kReset, kSendHeaders, + kSessionApplication, kSessionTicket, - kState, kTrailers, kVersionNegotiation, kInspect, - kWantsHeaders, - kWantsTrailers, } = require('internal/quic/symbols'); const { QuicEndpointStats, QuicStreamStats, QuicSessionStats, + kCreateDisconnected, } = require('internal/quic/stats'); const { @@ -158,27 +230,56 @@ const { const assert = require('internal/assert'); -const dc = require('diagnostics_channel'); -const onEndpointCreatedChannel = dc.channel('quic.endpoint.created'); -const onEndpointListeningChannel = dc.channel('quic.endpoint.listen'); -const onEndpointClosingChannel = dc.channel('quic.endpoint.closing'); -const onEndpointClosedChannel = dc.channel('quic.endpoint.closed'); -const onEndpointErrorChannel = dc.channel('quic.endpoint.error'); -const onEndpointBusyChangeChannel = dc.channel('quic.endpoint.busy.change'); -const onEndpointClientSessionChannel = dc.channel('quic.session.created.client'); -const onEndpointServerSessionChannel = dc.channel('quic.session.created.server'); -const onSessionOpenStreamChannel = dc.channel('quic.session.open.stream'); -const onSessionReceivedStreamChannel = dc.channel('quic.session.received.stream'); -const onSessionSendDatagramChannel = dc.channel('quic.session.send.datagram'); -const onSessionUpdateKeyChannel = dc.channel('quic.session.update.key'); -const onSessionClosingChannel = dc.channel('quic.session.closing'); -const onSessionClosedChannel = dc.channel('quic.session.closed'); -const onSessionReceiveDatagramChannel = dc.channel('quic.session.receive.datagram'); -const onSessionReceiveDatagramStatusChannel = dc.channel('quic.session.receive.datagram.status'); -const onSessionPathValidationChannel = dc.channel('quic.session.path.validation'); -const onSessionTicketChannel = dc.channel('quic.session.ticket'); -const onSessionVersionNegotiationChannel = dc.channel('quic.session.version.negotiation'); -const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); +const { + hasObserver, + startPerf, + stopPerf, +} = require('internal/perf/observe'); + +const kPerfEntry = Symbol('kPerfEntry'); + +const { + onEndpointCreatedChannel, + onEndpointListeningChannel, + onEndpointClosingChannel, + onEndpointClosedChannel, + onEndpointErrorChannel, + onEndpointBusyChangeChannel, + onEndpointClientSessionChannel, + onEndpointServerSessionChannel, + onSessionOpenStreamChannel, + onSessionReceivedStreamChannel, + onSessionSendDatagramChannel, + onSessionUpdateKeyChannel, + onSessionClosingChannel, + onSessionClosedChannel, + onSessionReceiveDatagramChannel, + onSessionReceiveDatagramStatusChannel, + onSessionPathValidationChannel, + onSessionNewTokenChannel, + onSessionApplicationChannel, + onSessionTicketChannel, + onSessionVersionNegotiationChannel, + onSessionOriginChannel, + onSessionHandshakeChannel, + onSessionGoawayChannel, + onSessionEarlyRejectedChannel, + onStreamClosedChannel, + onStreamHeadersChannel, + onStreamTrailersChannel, + onStreamInfoChannel, + onStreamResetChannel, + onStreamBlockedChannel, + onSessionErrorChannel, + onEndpointConnectChannel, +} = require('internal/quic/diagnostics'); + +const kNilDatagramId = 0n; + +// Module-level registry of all live QuicEndpoint instances. Used by +// connect() and listen() to find existing endpoints for reuse instead +// of creating a new one per session. +const endpointRegistry = new SafeSet(); /** * @typedef {import('../socketaddress.js').SocketAddress} SocketAddress @@ -187,24 +288,54 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); /** * @typedef {object} OpenStreamOptions - * @property {ArrayBuffer|ArrayBufferView|Blob} [body] The outbound payload - * @property {number} [sendOrder] The ordering of this stream relative to others in the same session. + * @property {string|ArrayBuffer|SharedArrayBuffer|ArrayBufferView|Blob| + * FileHandle|AsyncIterable|Iterable|Promise|null} [body] The outbound + * body source. See the public docs for `stream.setBody()` for details + * on supported types. When omitted, the stream is closed immediately. + * @property {object} [headers] Initial request or response headers to + * send. Only used when the negotiated application supports headers + * (e.g. HTTP/3). + * @property {'high'|'default'|'low'} [priority] The priority level of the stream. + * @property {boolean} [incremental] Whether to interleave data with same-priority streams. + * @property {number} [highWaterMark] The high water mark for write + * backpressure, in bytes. **Default:** `65536`. + * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers + * @property {OnTrailersCallback} [ontrailers] Callback for incoming trailing headers + * @property {OnInfoCallback} [oninfo] Callback for informational (1xx) headers + * @property {OnWantTrailersCallback} [onwanttrailers] Callback fired when the + * transport is ready to send trailers for this stream. */ /** + * Provides the configuration options for a QuicEndpoint. * @typedef {object} EndpointOptions - * @property {string|SocketAddress} [address] The local address to bind to + * @property {SocketAddress|string} [address] The local address to bind to * @property {bigint|number} [addressLRUSize] The size of the address LRU cache + * @property {'reno'|'cubic'|'bbr'} [cc] The congestion control algorithm + * @property {boolean} [disableStatelessReset] When true, the endpoint will not send stateless resets + * @property {bigint|number} [idleTimeout] The default idle timeout for sessions on this endpoint * @property {boolean} [ipv6Only] Use IPv6 only * @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing * @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host * @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections - * @property {bigint|number} [maxRetries] The maximum number of retries - * @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host + * @property {number} [retryRate] Global rate limit for retry packets (per second) + * @property {number} [retryBurst] Burst capacity for retry rate limiter + * @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second) + * @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter + * @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second) + * @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter + * @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second) + * @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter + * @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second) + * @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter + * @property {net.BlockList} [blockList] Block list for filtering incoming packets by source address + * @property {'deny'|'allow'} [blockListPolicy='deny'] How to interpret the block list * @property {ArrayBufferView} [resetTokenSecret] The reset token secret * @property {bigint|number} [retryTokenExpiration] The retry token expiration + * @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0) * @property {bigint|number} [tokenExpiration] The token expiration * @property {ArrayBufferView} [tokenSecret] The token secret + * @property {number} [txDiagnosticLoss] The transmit diagnostic loss probability (range 0.0-1.0) * @property {number} [udpReceiveBufferSize] The UDP receive buffer size * @property {number} [udpSendBufferSize] The UDP send buffer size * @property {number} [udpTTL] The UDP TTL @@ -240,34 +371,100 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); * @property {boolean} [enableDatagrams] Enable datagrams */ +/** + * Per-identity TLS options. Used as the values in the `sni` map of + * `SessionOptions` for server endpoints. + * @typedef {object} IdentityOptions + * @property {KeyObject|KeyObject[]} keys The TLS private keys. + * @property {ArrayBuffer|ArrayBufferView|Array} certs The TLS certificates. + * @property {boolean} [verifyPrivateKey] Verify the private key. + * **Default:** `false`. + * @property {number} [port] The port to advertise in HTTP/3 ORIGIN frames + * for this host name. **Default:** `443`. + * @property {boolean} [authoritative] Whether to include this host name + * in HTTP/3 ORIGIN frames. **Default:** `true`. Wildcard (`'*'`) + * entries are always excluded regardless of this setting. + */ + /** * @typedef {object} SessionOptions * @property {EndpointOptions|QuicEndpoint} [endpoint] An endpoint to use. - * @property {number} [version] The version - * @property {number} [minVersion] The minimum version + * @property {boolean} [reuseEndpoint] When `true` (default), `connect()` + * will attempt to reuse an existing endpoint rather than create a new + * one. Has no effect for server sessions. + * @property {number} [version] The QUIC version + * @property {number} [minVersion] The minimum acceptable QUIC version * @property {'use'|'ignore'|'default'} [preferredAddressPolicy] The preferred address policy + * @property {'strict'|'auto'|'manual'} [verifyPeer='auto'] Peer certificate verification policy (client only) * @property {ApplicationOptions} [application] The application options * @property {TransportParams} [transportParams] The transport parameters - * @property {string} [servername] The server name identifier - * @property {string} [protocol] The application layer protocol negotiation - * @property {string} [ciphers] The ciphers - * @property {string} [groups] The groups - * @property {boolean} [keylog] Enable key logging - * @property {boolean} [verifyClient] Verify the client + * @property {string} [servername] The server name identifier (client only) + * @property {string|string[]} [alpn] The ALPN protocol identifier(s). + * For client sessions, a single string. For server sessions, an array + * of protocol names in preference order. + * @property {string} [ciphers] The TLS ciphers + * @property {string} [groups] The TLS key-exchange groups + * @property {boolean} [keylog] Enable TLS key logging + * @property {boolean} [verifyClient] Verify the client certificate (server only) * @property {boolean} [tlsTrace] Enable TLS tracing - * @property {boolean} [verifyPrivateKey] Verify the private key - * @property {KeyObject|KeyObject[]} [keys] The keys - * @property {ArrayBuffer|ArrayBufferView|Array} [certs] The certificates + * @property {boolean} [enableEarlyData] Enable 0-RTT early data. + * **Default:** `true`. + * @property {boolean} [rejectUnauthorized] Verify the peer certificate + * against the supplied CAs. **Default:** `true`. + * @property {boolean} [verifyPrivateKey] Verify the private key (client only) + * @property {KeyObject|KeyObject[]} [keys] The TLS private keys (client only) + * @property {ArrayBuffer|ArrayBufferView|Array} [certs] The TLS certificates (client only) * @property {ArrayBuffer|ArrayBufferView|Array} [ca] The certificate authority * @property {ArrayBuffer|ArrayBufferView|Array} [crl] The certificate revocation list + * @property {{[key: string]: IdentityOptions}} [sni] Map of host names to + * per-identity TLS options for Server Name Indication. Required for + * server sessions. The special key `'*'` specifies the optional + * default/fallback identity. * @property {boolean} [qlog] Enable qlog - * @property {ArrayBufferView} [sessionTicket] The session ticket + * @property {ArrayBufferView} [sessionTicket] A session ticket from a + * prior session, used to resume that session (client only). + * @property {ArrayBufferView} [token] An opaque address validation token + * previously received from the server via `onnewtoken` (client only). * @property {bigint|number} [handshakeTimeout] The handshake timeout + * @property {bigint|number} [initialRtt] The initial round-trip time estimate in milliseconds. + * Used for PTO computation and initial pacing before the first RTT sample. Default uses + * ngtcp2's built-in default of 333ms. Set lower for low-latency environments. + * @property {bigint|number} [keepAlive] The keep-alive timeout in milliseconds. When set, + * PING frames will be sent automatically to prevent idle timeout. * @property {bigint|number} [maxStreamWindow] The maximum stream window - * @property {bigint|number} [maxWindow] The maximum window + * @property {bigint|number} [maxWindow] The maximum connection window * @property {bigint|number} [maxPayloadSize] The maximum payload size * @property {bigint|number} [unacknowledgedPacketThreshold] The unacknowledged packet threshold * @property {'reno'|'cubic'|'bbr'} [cc] The congestion control algorithm + * @property {'drop-oldest'|'drop-newest'} [datagramDropPolicy] The + * policy used when the pending datagram queue is full. + * **Default:** `'drop-oldest'`. + * @property {number} [drainingPeriodMultiplier] Multiplier applied to the + * draining period (3 * PTO) used by ngtcp2. Range `3..255`. + * **Default:** `3`. + * @property {bigint|number} [streamIdleTimeout] Time in ms before idle peer-initiated streams are destroyed + * @property {number} [maxDatagramSendAttempts] Maximum number of times a + * datagram is retried before being abandoned. Range `1..255`. + * **Default:** `5`. + * @property {OnSessionErrorCallback} [onerror] Session error callback. + * @property {OnStreamCallback} [onstream] Incoming stream callback. + * @property {OnDatagramCallback} [ondatagram] Incoming datagram callback. + * @property {OnDatagramStatusCallback} [ondatagramstatus] Outgoing datagram status callback. + * @property {OnPathValidationCallback} [onpathvalidation] Path validation callback. + * @property {OnSessionTicketCallback} [onsessionticket] New session-ticket callback. + * @property {OnVersionNegotiationCallback} [onversionnegotiation] Version negotiation callback. + * @property {OnHandshakeCallback} [onhandshake] Handshake-completed callback. + * @property {OnNewTokenCallback} [onnewtoken] NEW_TOKEN frame callback (client only). + * @property {OnOriginCallback} [onorigin] ORIGIN frame callback (client only). + * @property {OnGoawayCallback} [ongoaway] GOAWAY frame callback. + * @property {OnKeylogCallback} [onkeylog] TLS key-log callback. + * @property {OnQlogCallback} [onqlog] qlog data callback. + * @property {OnApplicationCallback} [onapplication] application options callback. + * @property {OnHeadersCallback} [onheaders] Default per-stream initial-headers callback. + * @property {OnTrailersCallback} [ontrailers] Default per-stream trailing-headers callback. + * @property {OnInfoCallback} [oninfo] Default per-stream informational-headers callback. + * @property {OnWantTrailersCallback} [onwanttrailers] Default per-stream + * want-trailers callback. */ /** @@ -282,6 +479,63 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); * @property {SocketAddress} remote The remote address */ +/** + * @typedef {object} QuicSessionInfo + * @property {SocketAddress} local The local address + * @property {SocketAddress} remote The remote address + * @property {string} protocol The alpn protocol identifier negotiated for this session + * @property {string} servername The servername identifier for this session + * @property {string} cipher The cipher suite negotiated for this session + * @property {string} cipherVersion The version of the cipher suite negotiated for this session + * @property {string} [validationErrorReason] The reason the session failed validation (if any) + * @property {string} [validationErrorCode] The error code for the validation failure (if any) + */ + +/** + * @typedef {object} QuicStreamDestroyOptions + * @property {bigint|number} [code] An explicit application + * error code to send on the resulting `RESET_STREAM` / + * `STOP_SENDING` frames. Numbers are coerced to `BigInt`. When + * omitted, the code is derived from `error` per the precedence + * above. + * @property {string} [reason] Optional human-readable reason. + * Accepted for symmetry with `session.close()` / + * `session.destroy()`; QUIC `RESET_STREAM` and `STOP_SENDING` + * frames do not themselves carry a reason field over the wire. + */ + +/** + * @typedef {object} SendHeadersOptions + * @property {boolean} [terminal] When true, indicates that no body data will be + * sent after these headers. + */ + +/** + * @typedef {object} StreamPriority + * @property {'default' | 'low' | 'high'} level The priority level of the stream. + * @property {boolean} incremental Whether to interleave data with same-priority streams. + */ + +/** + * @typedef {object} QuicSessionPath + * @property {SocketAddress} local The local address for this path + * @property {SocketAddress} remote The remote address for this path + */ + +/** + * @typedef {object} SNIContextOptions + * @property {boolean} [replace] When `true`, the provided SNI context will replace + * the default context for the session. When `false` (default), the provided + * context will be merged with the default context, with precedence given to + * the provided context on any overlapping options. + */ + +/** + * @typedef {object} ProcessSessionOptions + * @property {boolean} forServer true if processing options for a server session + * @property {string} addressFamily the address family to use for validating + */ + /** * Called when the Endpoint receives a new server-side Session. * @callback OnSessionCallback @@ -290,6 +544,14 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); * @returns {void} */ +/** + * Called when a session is destroyed with an error. + * @callback OnSessionErrorCallback + * @this {QuicSession} + * @param {any} error + * @returns {void} + */ + /** * @callback OnStreamCallback * @this {QuicSession} @@ -305,6 +567,36 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); * @returns {void} */ +/** + * Called when the status of a previously sent datagram is reported. + * @callback OnDatagramStatusCallback + * @this {QuicSession} + * @param {bigint} id The datagram id + * @param {'acknowledged'|'lost'|'abandoned'} status + * @returns {void} + */ + +/** + * Called when QUIC path validation completes (or fails). + * @callback OnPathValidationCallback + * @this {QuicSession} + * @param {'success'|'failure'|'aborted'} result + * @param {SocketAddress} newLocalAddress + * @param {SocketAddress} newRemoteAddress + * @param {SocketAddress|null} oldLocalAddress + * @param {SocketAddress|null} oldRemoteAddress + * @param {boolean} [preferredAddress] `true` if the validation was triggered + * by a preferred-address migration on the client side. + * @returns {void} + */ + +/** + * @callback OnApplicationCallback + * @this {QuicSession} + * @param {ApplicationOptions} applicationoptions + * @returns {void} + */ + /** * @callback OnSessionTicketCallback * @this {QuicSession} @@ -312,6 +604,84 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); * @returns {void} */ +/** + * Called when the server responds with a Version Negotiation packet. + * The session is destroyed immediately after this returns. + * @callback OnVersionNegotiationCallback + * @this {QuicSession} + * @param {number} version The QUIC version configured for this session + * @param {number[]} requestedVersions The versions advertised by the server + * @param {number[]} supportedVersions A `[minVersion, maxVersion]` pair + * @returns {void} + */ + +/** + * Called when the TLS handshake completes successfully. + * @callback OnHandshakeCallback + * @this {QuicSession} + * @param {string} sni + * @param {string} alpn + * @param {string} cipher + * @param {string} cipherVersion + * @param {string} [validationErrorReason] + * @param {number} [validationErrorCode] + * @param {boolean} earlyDataAttempted + * @param {boolean} earlyDataAccepted + * @returns {void} + */ + +/** + * Called when the server issues a NEW_TOKEN frame to the client. + * @callback OnNewTokenCallback + * @this {QuicSession} + * @param {Buffer} token The opaque token data + * @param {SocketAddress} address The remote server address + * @returns {void} + */ + +/** + * Called when the server sends an ORIGIN frame. + * @callback OnOriginCallback + * @this {QuicSession} + * @param {string[]} origins The list of origins the server claims authority for + * @returns {void} + */ + +/** + * Called when the peer sends a GOAWAY frame (HTTP/3 only). + * @callback OnGoawayCallback + * @this {QuicSession} + * @param {bigint} lastStreamId The highest stream ID the peer may have processed + * @returns {void} + */ + +/** + * Called when TLS key-log material is available. Only fires when + * `sessionOptions.keylog` is `true`. + * @callback OnKeylogCallback + * @this {QuicSession} + * @param {string} line A single NSS Key Log Format line, including trailing newline. + * @returns {void} + */ + +/** + * Called when qlog diagnostic data is available. Only fires when + * `sessionOptions.qlog` is `true`. + * @callback OnQlogCallback + * @this {QuicSession} + * @param {string} data A chunk of JSON-SEQ formatted qlog data + * @param {boolean} fin `true` if this is the final qlog chunk for the session. + * @returns {void} + */ + +/** + * Called when `ApplicationOptions` are changed, e.g. HTTP/3 settings. + * @callback OnApplicationCallback + * @this {QuicSession} + * @param {ApplicationOptions} applicationoptions ApplicationOptions object + * @returns {void} + */ + /** * @callback OnBlockedCallback * @this {QuicStream} @@ -326,51 +696,38 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); */ /** + * Called when initial request or response headers are received. * @callback OnHeadersCallback * @this {QuicStream} - * @param {object} headers - * @param {string} kind + * @param {object} headers Header object with lowercase string keys and + * string or string-array values. * @returns {void} */ /** + * Called when trailing headers are received from the peer. * @callback OnTrailersCallback * @this {QuicStream} + * @param {object} trailers Trailing header object. * @returns {void} */ /** - * Provides the callback configuration for the Endpoint|undefined. - * @typedef {object} EndpointOptions - * @property {SocketAddress | string} [address] The local address to bind to - * @property {bigint|number} [retryTokenExpiration] The retry token expiration - * @property {bigint|number} [tokenExpiration] The token expiration - * @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host - * @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections - * @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host - * @property {bigint|number} [addressLRUSize] The size of the address LRU cache - * @property {bigint|number} [maxRetries] The maximum number of retriesw - * @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0) - * @property {number} [txDiagnosticLoss] The transmit diagnostic loss probability (range 0.0-1.0) - * @property {number} [udpReceiveBufferSize] The UDP receive buffer size - * @property {number} [udpSendBufferSize] The UDP send buffer size - * @property {number} [udpTTL] The UDP TTL - * @property {boolean} [validateAddress] Validate the address - * @property {boolean} [ipv6Only] Use IPv6 only - * @property {ArrayBufferView} [resetTokenSecret] The reset token secret - * @property {ArrayBufferView} [tokenSecret] The token secret + * Called when informational (1xx) headers are received from the server + * (e.g. 103 Early Hints). + * @callback OnInfoCallback + * @this {QuicStream} + * @param {object} headers Informational header object. + * @returns {void} */ /** - * @typedef {object} QuicSessionInfo - * @property {SocketAddress} local The local address - * @property {SocketAddress} remote The remote address - * @property {string} protocol The alpn protocol identifier negotiated for this session - * @property {string} servername The servername identifier for this session - * @property {string} cipher The cipher suite negotiated for this session - * @property {string} cipherVersion The version of the cipher suite negotiated for this session - * @property {string} [validationErrorReason] The reason the session failed validation (if any) - * @property {string} [validationErrorCode] The error code for the validation failure (if any) + * Called when the transport is ready to send trailers for this stream. + * The handler should call `stream.sendTrailers(...)` (or + * `stream.sendTrailers()` with previously-set trailers) to provide them. + * @callback OnWantTrailersCallback + * @this {QuicStream} + * @returns {void} */ setCallbacks({ @@ -388,7 +745,7 @@ setCallbacks({ }, /** * Called when the QuicEndpoint C++ handle receives a new server-side session - * @param {*} session The QuicSession C++ handle + * @param {object} session The QuicSession C++ handle */ onSessionNew(session) { debug('new server session callback', this[kOwner], session); @@ -403,10 +760,22 @@ setCallbacks({ * @param {number} errorType * @param {number} code * @param {string} [reason] + * @param {string} [errorName] Decoded TLS alert name when `code` is a + * CRYPTO_ERROR; otherwise undefined. + */ + onSessionClose(errorType, code, reason, errorName) { + debug('session close callback', errorType, code, reason, errorName); + this[kOwner][kFinishClose](errorType, code, reason, errorName); + }, + + /** + * Called when the peer sends a GOAWAY frame (HTTP/3 only). + * @param {bigint} lastStreamId The highest stream ID the peer may have + * processed. Streams above this ID were not processed and can be retried. */ - onSessionClose(errorType, code, reason) { - debug('session close callback', errorType, code, reason); - this[kOwner][kFinishClose](errorType, code, reason); + onSessionGoaway(lastStreamId) { + debug('session goaway callback', lastStreamId); + this[kOwner][kGoaway](lastStreamId); }, /** @@ -415,7 +784,7 @@ setCallbacks({ * @param {boolean} early */ onSessionDatagram(uint8Array, early) { - debug('session datagram callback', uint8Array.byteLength, early); + debug('session datagram callback', TypedArrayPrototypeGetByteLength(uint8Array), early); this[kOwner][kDatagram](uint8Array, early); }, @@ -437,14 +806,20 @@ setCallbacks({ * @param {string} cipherVersion * @param {string} validationErrorReason * @param {number} validationErrorCode + * @param {boolean} earlyDataAttempted + * @param {boolean} earlyDataAccepted */ onSessionHandshake(servername, protocol, cipher, cipherVersion, validationErrorReason, - validationErrorCode) { + validationErrorCode, + earlyDataAttempted, + earlyDataAccepted) { debug('session handshake callback', servername, protocol, cipher, cipherVersion, - validationErrorReason, validationErrorCode); + validationErrorReason, validationErrorCode, + earlyDataAttempted, earlyDataAccepted); this[kOwner][kHandshake](servername, protocol, cipher, cipherVersion, - validationErrorReason, validationErrorCode); + validationErrorReason, validationErrorCode, + earlyDataAttempted, earlyDataAccepted); }, /** @@ -459,14 +834,21 @@ setCallbacks({ onSessionPathValidation(result, newLocalAddress, newRemoteAddress, oldLocalAddress, oldRemoteAddress, preferredAddress) { debug('session path validation callback', this[kOwner]); - this[kOwner][kPathValidation](result, - new InternalSocketAddress(newLocalAddress), - new InternalSocketAddress(newRemoteAddress), - new InternalSocketAddress(oldLocalAddress), - new InternalSocketAddress(oldRemoteAddress), + this[kOwner][kPathValidation](result, newLocalAddress, newRemoteAddress, + oldLocalAddress, oldRemoteAddress, preferredAddress); }, + /** + * Called when the session's application object is updated + * E.g. http/3 session arrived. + * @param {ApplicationOptions} applicationoptions An application object + */ + onSessionApplication(applicationoptions) { + debug('session application callback', this[kOwner]); + this[kOwner][kSessionApplication](applicationoptions); + }, + /** * Called when the session generates a new TLS session ticket * @param {object} ticket An opaque session ticket @@ -485,14 +867,33 @@ setCallbacks({ */ onSessionNewToken(token, address) { debug('session new token callback', this[kOwner]); - // TODO(@jasnell): Emit to JS for storage and future reconnection use + this[kOwner][kNewToken](token, address); + }, + + /** + * Called when the server rejects 0-RTT early data. All streams + * opened during the 0-RTT phase have been destroyed. The + * application should re-open streams if needed. + */ + onSessionEarlyDataRejected() { + debug('session early data rejected callback', this[kOwner]); + this[kOwner][kEarlyDataRejected](); + }, + + /** + * Called when the session receives an ORIGIN frame from the peer (RFC 9412). + * @param {string[]} origins The list of origins the peer claims authority for + */ + onSessionOrigin(origins) { + debug('session origin callback', this[kOwner]); + this[kOwner][kOrigin](origins); }, /** * Called when the session receives a session version negotiation request - * @param {*} version - * @param {*} requestedVersions - * @param {*} supportedVersions + * @param {number} version + * @param {number[]} requestedVersions + * @param {number[]} supportedVersions */ onSessionVersionNegotiation(version, requestedVersions, @@ -504,6 +905,23 @@ setCallbacks({ // session will be destroyed. }, + onSessionKeyLog(line) { + debug('session key log callback', line, this[kOwner]); + this[kOwner][kKeylog](line); + }, + + onSessionQlog(data, fin) { + if (this[kOwner] === undefined) { + // Qlog data can arrive during ngtcp2_conn creation, before the + // QuicSession JS wrapper exists. Cache until the wrapper is ready. + this._pendingQlog ??= []; + this._pendingQlog.push(data, fin); + return; + } + debug('session qlog callback', this[kOwner]); + this[kOwner][kQlog](data, fin); + }, + /** * Called when a new stream has been received for the session * @param {object} stream The QuicStream C++ handle @@ -527,14 +945,43 @@ setCallbacks({ this[kOwner][kBlocked](); }, + onStreamDrain() { + // Called when the stream's outbound buffer has capacity for more data. + debug('stream drain callback', this[kOwner]); + this[kOwner][kDrain](); + }, + onStreamClose(error) { - // Called when the stream C++ handle has been closed. + // Called when the stream C++ handle has been closed. The error is + // either undefined (clean close) or a raw array [type, code, reason] + // from QuicError::ToV8Value. Convert to a proper Node.js Error. + if (error !== undefined) { + error = convertQuicError(error); + } else if (this[kOwner] && !this[kOwner].destroyed) { + // The stream is closing cleanly, but it may have been reset by the + // peer (ReceiveStreamReset) or locally (resetStream). The C++ side + // records the reset code in state.resetCode. If set, surface the + // reset as the close error so stream.closed rejects -- the reset + // was an abnormal termination even if the session closed cleanly. + const resetCode = getQuicStreamState(this[kOwner]).resetCode; + if (resetCode !== undefined && resetCode > 0n) { + error = makeQuicError( + 'ERR_QUIC_APPLICATION_ERROR', + 'QUIC application error', + 'application', + resetCode, + `stream reset with code ${resetCode}`); + } + } debug(`stream ${this[kOwner].id} closed callback with error: ${error}`); this[kOwner][kFinishClose](error); }, onStreamReset(error) { // Called when the stream C++ handle has received a stream reset. + if (error !== undefined) { + error = convertQuicError(error); + } debug('stream reset callback', this[kOwner], error); this[kOwner][kReset](error); }, @@ -552,245 +999,1473 @@ setCallbacks({ }, }); -function validateBody(body) { - // TODO(@jasnell): Support streaming sources - if (body === undefined) return body; - if (isArrayBuffer(body)) return ArrayBufferPrototypeTransfer(body); - if (isArrayBufferView(body)) { - const size = body.byteLength; - const offset = body.byteOffset; - return new Uint8Array(ArrayBufferPrototypeTransfer(body.buffer), offset, size); +function assertPrivateSymbol(privateSymbol) { + if (privateSymbol !== kPrivateConstructor) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); } - if (isBlob(body)) return body[kBlobHandle]; - - throw new ERR_INVALID_ARG_TYPE('options.body', [ - 'ArrayBuffer', - 'ArrayBufferView', - 'Blob', - ], body); } -class QuicStream { - /** @type {object} */ - #handle; - /** @type {QuicSession} */ - #session; - /** @type {QuicStreamStats} */ - #stats; - /** @type {QuicStreamState} */ - #state; - /** @type {number} */ - #direction = undefined; - /** @type {OnBlockedCallback|undefined} */ - #onblocked = undefined; - /** @type {OnStreamErrorCallback|undefined} */ - #onreset = undefined; - /** @type {OnHeadersCallback|undefined} */ - #onheaders = undefined; - /** @type {OnTrailersCallback|undefined} */ - #ontrailers = undefined; - /** @type {Promise} */ - #pendingClose = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials - #reader; - #readable; +// QUIC error codes are 62-bit varints (RFC 9000 section 16). The +// maximum representable code is 2**62 - 1. +const kMaxQuicErrorCode = (1n << 62n) - 1n; + +/** + * An Error subclass that carries an explicit numeric QUIC error code. + * Use this when destroying a stream or aborting an outbound writer to + * communicate a specific application-protocol-defined error code to + * the peer. When a `QuicError` is supplied, the QUIC stack uses + * `errorCode` as the wire code for the resulting RESET_STREAM / + * STOP_SENDING / CONNECTION_CLOSE frame; otherwise the negotiated + * application's "internal error" code is used (see + * `QuicSessionState.internalErrorCode`). + * + * The Node.js error code (`error.code`) defaults to + * `'ERR_QUIC_STREAM_ABORTED'` but can be overridden via + * `options.code`. The numeric QUIC code lives on the separate + * `errorCode` property to avoid colliding with Node.js's convention + * that `error.code` is a string. + */ +class QuicError extends Error { + /** @type {bigint} */ + #errorCode; + /** @type {'transport' | 'application'} */ + #type; + + static isQuicError(val) { + return val != null && typeof val === 'object' && #errorCode in val; + } /** - * @param {symbol} privateSymbol - * @param {object} handle - * @param {QuicSession} session - * @param {number} direction + * @param {string} message + * @param {object} options + * @param {bigint|number} options.errorCode The numeric QUIC error + * code. Numbers are coerced to BigInt. Must be a non-negative + * 62-bit unsigned varint + * (`0n <= errorCode <= 2n ** 62n - 1n`). + * @param {string} [options.code] The Node.js-style error code + * string assigned to `error.code`. Defaults to + * `'ERR_QUIC_STREAM_ABORTED'`. + * @param {'transport'|'application'} [options.type] Whether the + * code is a transport-layer code (defined by RFC 9000) or an + * application-layer code (defined by the negotiated ALPN, e.g. + * RFC 9114 for HTTP/3). Defaults to `'application'`. Stream + * resets always carry application codes; this option is exposed + * for use sites that may target either layer. */ - constructor(privateSymbol, handle, session, direction) { - if (privateSymbol !== kPrivateConstructor) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + constructor(message, options = kEmptyObject) { + validateString(message, 'message'); + validateObject(options, 'options'); + const { + errorCode, + code = 'ERR_QUIC_STREAM_ABORTED', + type = 'application', + } = options; + if (errorCode === undefined) { + throw new ERR_MISSING_ARGS('options.errorCode'); } - - this.#handle = handle; - this.#handle[kOwner] = this; - this.#session = session; - this.#direction = direction; - this.#stats = new QuicStreamStats(kPrivateConstructor, this.#handle.stats); - this.#state = new QuicStreamState(kPrivateConstructor, this.#handle.state); - this.#reader = this.#handle.getReader(); - - if (this.pending) { - debug(`pending ${this.direction} stream created`); - } else { - debug(`${this.direction} stream ${this.id} created`); + if (typeof errorCode !== 'bigint' && typeof errorCode !== 'number') { + throw new ERR_INVALID_ARG_TYPE('options.errorCode', + ['bigint', 'number'], errorCode); } - } - - get readable() { - if (this.#readable === undefined) { - assert(this.#reader); - this.#readable = createBlobReaderStream(this.#reader); + validateString(code, 'options.code'); + validateOneOf(type, 'options.type', ['transport', 'application']); + const numericCode = BigInt(errorCode); + if (numericCode < 0n || numericCode > kMaxQuicErrorCode) { + throw new ERR_OUT_OF_RANGE('options.errorCode', + `>= 0 and <= ${kMaxQuicErrorCode}`, + errorCode); } - return this.#readable; + super(message); + this.code = code; + this.#errorCode = numericCode; + this.#type = type; } - /** @type {boolean} */ - get pending() { return this.#state.pending; } - - /** @type {OnBlockedCallback} */ - get onblocked() { return this.#onblocked; } + /** @type {bigint} */ + get errorCode() { + return this.#errorCode; + } - set onblocked(fn) { - if (fn === undefined) { - this.#onblocked = undefined; - this.#state.wantsBlock = false; - } else { - validateFunction(fn, 'onblocked'); - this.#onblocked = fn.bind(this); - this.#state.wantsBlock = true; - } + /** @type {'transport' | 'application'} */ + get type() { + return this.#type; } +} - /** @type {OnStreamErrorCallback} */ - get onreset() { return this.#onreset; } +// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or +// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for +// the wire code when known: either the OpenSSL-decoded TLS alert +// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes +// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined. +// `reason` is the peer-supplied UTF-8 reason string from the +// CONNECTION_CLOSE / RESET_STREAM frame, often empty. +function quicErrorMessage(prefix, errorCode, reason, errorName) { + let msg = `${prefix} `; + msg += errorName ? `${errorName} (${errorCode})` : `${errorCode}`; + if (reason) msg += `: ${reason}`; + return msg; +} - set onreset(fn) { - if (fn === undefined) { - this.#onreset = undefined; - this.#state.wantsReset = false; - } else { - validateFunction(fn, 'onreset'); - this.#onreset = fn.bind(this); - this.#state.wantsReset = true; - } +function makeQuicError(code, prefix, type, errorCode, reason, errorName) { + const err = new QuicError( + quicErrorMessage(prefix, errorCode, reason, errorName), + { errorCode, code, type }); + ErrorCaptureStackTrace(err, makeQuicError); + if (reason) err.reason = reason; + if (errorName) err.errorName = errorName; + return err; +} + +function convertQuicError(error) { + const type = error[0]; + const code = error[1]; + const reason = error[2]; + const errorName = error[3]; + switch (type) { + case 'transport': + return makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName); + case 'application': + return makeQuicError('ERR_QUIC_APPLICATION_ERROR', + 'QUIC application error', + 'application', code, reason, errorName); + case 'version_negotiation': + return new ERR_QUIC_VERSION_NEGOTIATION_ERROR(); + default: + return makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName); } +} - /** @type {OnHeadersCallback} */ - get [kOnHeaders]() { return this.#onheaders; } +// Convert a JavaScript error into close options suitable for +// `session.close()` / `session.destroy(error, options)`. The returned +// shape is `{ code, type, reason }` matching what `validateCloseOptions` +// expects (and what the native side reads via `MaybeSetCloseError`). +// +// Used so that destroying a session with an error actually emits a +// CONNECTION_CLOSE frame on the wire, instead of dropping the connection +// silently and leaving the peer waiting on its idle timer. +// +// Returns `undefined` when no error was supplied (caller falls back to +// a clean / silent close). +function errorToCloseOptions(error) { + if (error === undefined || error === null) return undefined; + // Generic mapping for now: any error becomes a transport-level + // INTERNAL_ERROR (NGTCP2_INTERNAL_ERROR == 0x1) with the original + // error message used as the human-readable reason. Future work could + // detect specific `ERR_QUIC_*` subclasses and round-trip their + // original code/type back onto the wire. + const reason = typeof error === 'object' && error !== null && error.message ? + `${error.message}` : + `${error}`; + return { code: 0x1n, type: 'transport', reason }; +} - set [kOnHeaders](fn) { - if (fn === undefined) { - this.#onheaders = undefined; - this.#state[kWantsHeaders] = false; - } else { - validateFunction(fn, 'onheaders'); - this.#onheaders = fn.bind(this); - this.#state[kWantsHeaders] = true; +/** + * Safely invoke a user-supplied callback. If the callback throws + * synchronously, the owning object is destroyed with the error. If the + * callback returns a promise that rejects, the rejection is caught and the + * owning object is destroyed. Sync callbacks that do not throw incur no + * promise allocation overhead. + * @param {Function} fn The callback to invoke. + * @param {object} owner The QuicSession or QuicStream that owns the callback. + * @param {...any} args Arguments forwarded to the callback. + */ +function safeCallbackInvoke(fn, owner, ...args) { + try { + const result = fn(...args, owner); + if (isPromise(result)) { + // Block body - do NOT return the result of `owner.destroy(err)`. + // For some owners (e.g. `QuicEndpoint`), `destroy(err)` returns the + // owner's `closed` promise which itself eventually rejects with + // the same error. If we let that propagate through the `.then()` + // chain promise, nobody is awaiting that chain and we surface the + // rejection as unhandled. + PromisePrototypeThen(result, undefined, (err) => { + owner.destroy(err); + }); } + } catch (err) { + owner.destroy(err); } +} - /** @type {OnTrailersCallback} */ - get [kOnTrailers]() { return this.#ontrailers; } - - set [kOnTrailers](fn) { - if (fn === undefined) { - this.#ontrailers = undefined; - this.#state[kWantsTrailers] = false; - } else { - validateFunction(fn, 'ontrailers'); - this.#ontrailers = fn.bind(this); - this.#state[kWantsTrailers] = true; +/** + * Invoke an onerror callback. If the callback itself throws synchronously + * or returns a promise that rejects, a SuppressedError wrapping both the + * onerror failure and the original error is surfaced as an uncaught exception. + * @param {Function} fn The onerror callback. + * @param {any} error The original error that triggered destruction. + */ +function invokeOnerror(fn, error) { + try { + const result = fn(error); + if (isPromise(result)) { + PromisePrototypeThen(result, undefined, (err) => { + process.nextTick(() => { + // eslint-disable-next-line no-restricted-syntax + throw new SuppressedError(err, error, err?.message); + }); + }); } + } catch (err) { + process.nextTick(() => { + // eslint-disable-next-line no-restricted-syntax + throw new SuppressedError(err, error, err?.message); + }); } +} - /** @type {QuicStreamStats} */ - get stats() { return this.#stats; } - - /** @type {QuicStreamState} */ - get state() { return this.#state; } - - /** @type {QuicSession} */ - get session() { return this.#session; } +function validateBody(body) { + if (body === undefined) return body; + // ArrayBuffers, SharedArrayBuffers, and ArrayBufferViews are passed + // through to the C++ layer which copies the bytes into its own + // BackingStore. Callers can therefore safely reuse or mutate their + // input buffers after the call returns. Callers that want to ensure + // their buffer cannot be mutated after handing it off (for example, + // when sharing the source with another async consumer) can call + // ArrayBuffer.prototype.transfer() themselves before passing the + // buffer. + if (isArrayBuffer(body) || + isSharedArrayBuffer(body) || + isArrayBufferView(body)) { + return body; + } + if (isBlob(body)) return body[kBlobHandle]; - /** - * Returns the id for this stream. If the stream is destroyed or still pending, - * `undefined` will be returned. - * @type {bigint} - */ - get id() { - if (this.destroyed || this.pending) return undefined; - return this.#state.id; + // Strings are encoded as UTF-8. + if (typeof body === 'string') { + return Buffer.from(body, 'utf8'); } - /** @type {'bidi'|'uni'} */ - get direction() { - return this.#direction === STREAM_DIRECTION_BIDIRECTIONAL ? 'bidi' : 'uni'; + // FileHandle -- lock it and pass the C++ handle to GetDataQueueFromSource + // which creates an fd-backed DataQueue entry from the file path. + if (FileHandle.isFileHandle(body)) { + if (body[kFileLocked]) { + throw new ERR_INVALID_STATE('FileHandle is locked'); + } + body[kFileLocked] = true; + return body[kFileHandle]; } - /** @returns {boolean} */ - get destroyed() { + throw new ERR_INVALID_ARG_TYPE('options.body', [ + 'string', + 'ArrayBuffer', + 'ArrayBufferView', + 'Blob', + 'FileHandle', + ], body); +} + +/** + * Parses an alternating [name, value, name, value, ...] array from C++ + * into a plain header object. Multi-value headers become arrays. + * @param {string[]} pairs + * @returns {object} + */ +function parseHeaderPairs(pairs) { + assert(ArrayIsArray(pairs)); + assert(pairs.length % 2 === 0); + const block = { __proto__: null }; + for (let n = 0; n + 1 < pairs.length; n += 2) { + if (block[pairs[n]] !== undefined) { + if (ArrayIsArray(block[pairs[n]])) { + ArrayPrototypePush(block[pairs[n]], pairs[n + 1]); + } else { + block[pairs[n]] = [block[pairs[n]], pairs[n + 1]]; + } + } else { + block[pairs[n]] = pairs[n + 1]; + } + } + return block; +} + +/** + * Applies session and stream callbacks from an options object to a session. + * @param {QuicSession} session + * @param {object} cbs + */ +function applyCallbacks(session, cbs) { + if (cbs.onerror) session.onerror = cbs.onerror; + if (cbs.onstream) session.onstream = cbs.onstream; + if (cbs.ondatagram) session.ondatagram = cbs.ondatagram; + if (cbs.ondatagramstatus) session.ondatagramstatus = cbs.ondatagramstatus; + if (cbs.onpathvalidation) session.onpathvalidation = cbs.onpathvalidation; + if (cbs.onsessionticket) session.onsessionticket = cbs.onsessionticket; + if (cbs.onversionnegotiation) session.onversionnegotiation = cbs.onversionnegotiation; + if (cbs.onhandshake) session.onhandshake = cbs.onhandshake; + if (cbs.onnewtoken) session.onnewtoken = cbs.onnewtoken; + if (cbs.onearlyrejected) session.onearlyrejected = cbs.onearlyrejected; + if (cbs.onorigin) session.onorigin = cbs.onorigin; + if (cbs.ongoaway) session.ongoaway = cbs.ongoaway; + if (cbs.onkeylog) session.onkeylog = cbs.onkeylog; + if (cbs.onqlog) session.onqlog = cbs.onqlog; + if (cbs.onapplication) session.onapplication = cbs.onapplication; + if (cbs.onheaders || cbs.ontrailers || cbs.oninfo || cbs.onwanttrailers) { + session[kStreamCallbacks] = { + __proto__: null, + onheaders: cbs.onheaders, + ontrailers: cbs.ontrailers, + oninfo: cbs.oninfo, + onwanttrailers: cbs.onwanttrailers, + }; + } +} + +/** + * Configures the outbound data source for a stream. Detects the source + * type and calls the appropriate C++ method. + * @param {object} handle The C++ stream handle + * @param {QuicStream} stream The JS stream object + * @param {any} body The body source + */ +const kDefaultHighWaterMark = 65536; +const kDefaultMaxPendingDatagrams = 128; + +function configureOutbound(handle, stream, body) { + // body: null - close writable side immediately (FIN) + if (body === null) { + handle.initStreamingSource(); + handle.endWrite(); + return; + } + + // Handle Promise - await and recurse. Native promises auto-flatten, + // so the resolved value will never itself be a promise. + if (isPromise(body)) { + PromisePrototypeThen( + body, + (resolved) => configureOutbound(handle, stream, resolved), + (err) => { + if (!stream.destroyed) { + stream.destroy(err); + } + }, + ); + return; + } + + // Tier: One-shot - string (checked before sync iterable since + // strings are iterable but we want the one-shot path). + // Buffer.from may return a pooled buffer whose ArrayBuffer cannot + // be transferred, so run it through validateBody which copies when + // the buffer is a partial view of a larger ArrayBuffer. + if (typeof body === 'string') { + handle.attachSource(validateBody(Buffer.from(body, 'utf8'))); + return; + } + + // Tier: One-shot - FileHandle. The C++ layer creates an fd-backed + // DataQueue entry from the file path. The FileHandle is locked to + // prevent concurrent use and closed automatically when the stream + // finishes. + if (FileHandle.isFileHandle(body)) { + if (body[kFileLocked]) { + throw new ERR_INVALID_STATE('FileHandle is locked'); + } + body[kFileLocked] = true; + handle.attachSource(body[kFileHandle]); + return; + } + + // Tier: One-shot - ArrayBuffer, SharedArrayBuffer, TypedArray, + // DataView, Blob. validateBody handles transfer-vs-copy logic, + // SharedArrayBuffer copying, and partial view safety. + if (isArrayBuffer(body) || isSharedArrayBuffer(body) || + isArrayBufferView(body) || isBlob(body)) { + handle.attachSource(validateBody(body)); + return; + } + + // Tier: Streaming - AsyncIterable (ReadableStream, stream.Readable, + // async generators, etc.). Checked before sync iterable because some + // objects implement both protocols and we prefer async. + if (isAsyncIterable(body)) { + consumeAsyncSource(handle, stream, body); + return; + } + + // Tier: Sync iterable - consumed synchronously + if (isSyncIterable(body)) { + consumeSyncSource(handle, stream, body); + return; + } + + throw new ERR_INVALID_ARG_TYPE( + 'body', + ['string', 'ArrayBuffer', 'SharedArrayBuffer', 'TypedArray', + 'Blob', 'Iterable', 'AsyncIterable', 'Promise', 'null'], + body, + ); +} + +// Sets the high water mark and initial writeDesiredSize for a streaming +// outbound source. Called after handle.initStreamingSource() for both +// body-source and writer paths. One-shot body sources (string, Uint8Array, +// Blob, FileHandle, etc.) do not use this -- they go through attachSource +// and are not subject to backpressure. +function initStreamingBackpressure(stream) { + const state = getQuicStreamState(stream); + // Only set defaults if the user hasn't already configured them + // (e.g., via createBidirectionalStream({ highWaterMark: N })). + if (state.highWaterMark === 0) { + state.highWaterMark = kDefaultHighWaterMark; + } + if (state.writeDesiredSize === 0) { + state.writeDesiredSize = state.highWaterMark; + } +} + +// Waits for the stream's drain callback to fire, indicating the +// outbound has capacity for more data. +function waitForDrain(stream) { + const { promise, resolve } = PromiseWithResolvers(); + const prevDrain = stream[kDrain]; + stream[kDrain] = () => { + stream[kDrain] = prevDrain; + resolve(); + }; + return promise; +} + +// Writes a batch to the handle, awaiting drain if backpressured. +// Returns true if the stream was destroyed during the wait. +// Only waits when writeDesiredSize is 0 (no capacity at all). +// When there is any capacity, the write proceeds even if the batch +// is larger -- the C++ side buffers the data and writeDesiredSize +// drops toward 0, letting the normal drain mechanism take over. +async function writeBatchWithDrain(handle, stream, batch) { + const state = getQuicStreamState(stream); + + if (state.writeDesiredSize === 0) { + await waitForDrain(stream); + if (stream.destroyed) return true; + } + + // Write the batch. The return value is the total queued byte count + // on success, or undefined on failure (e.g., DataQueue append + // rejected). Guard against silent data loss. + const result = handle.write(batch); + if (result === undefined) { + if (!stream.destroyed) { + stream.destroy(new ERR_INVALID_STATE('Stream write failed')); + } + return true; + } + return false; +} + +async function consumeAsyncSource(handle, stream, source) { + handle.initStreamingSource(); + initStreamingBackpressure(stream); + try { + // Normalize to AsyncIterable + const normalized = streamFrom(source); + for await (const batch of normalized) { + if (stream.destroyed) return; + if (await writeBatchWithDrain(handle, stream, batch)) return; + } + handle.endWrite(); + } catch (err) { + if (!stream.destroyed) { + stream.destroy(err); + } else { + throw err; + } + } +} + +async function consumeSyncSource(handle, stream, source) { + handle.initStreamingSource(); + initStreamingBackpressure(stream); + // Normalize to Iterable. Manually iterate so we can + // pause between next() calls when backpressure hits. + const normalized = streamFromSync(source); + const iter = normalized[SymbolIterator](); + try { + while (true) { + if (stream.destroyed) return; + const { value: batch, done } = iter.next(); + if (done) break; + if (await writeBatchWithDrain(handle, stream, batch)) return; + } + handle.endWrite(); + } catch (err) { + if (!stream.destroyed) { + stream.destroy(err); + } else { + // If the stream is already destroyed, rethrow the error to avoid + // silently swallowing it. Tho in practice this shouldn't happen. + throw err; + } + } +} + +function isAsyncIterable(obj) { + return obj != null && typeof obj[SymbolAsyncIterator] === 'function'; +} + +function isSyncIterable(obj) { + return obj != null && typeof obj[SymbolIterator] === 'function'; +} + +// Functions used specifically for internal or assertion purposes only. +let getQuicStreamState; +let getQuicSessionState; +let getQuicEndpointState; +let assertIsQuicEndpoint; +let assertIsQuicStream; +let assertIsQuicSession; +let assertHeadersSupported; +let assertEndpointNotClosedOrClosing; +let assertEndpointIsNotBusy; +let isQuicStream; +let isQuicSession; +let isQuicEndpoint; + +function maybeGetCloseError(context, status, pendingError) { + switch (context) { + case kCloseContextClose: { + return pendingError; + } + case kCloseContextBindFailure: { + return new ERR_QUIC_ENDPOINT_CLOSED('Bind failure', status); + } + case kCloseContextListenFailure: { + return new ERR_QUIC_ENDPOINT_CLOSED('Listen failure', status); + } + case kCloseContextReceiveFailure: { + return new ERR_QUIC_ENDPOINT_CLOSED('Receive failure', status); + } + case kCloseContextSendFailure: { + return new ERR_QUIC_ENDPOINT_CLOSED('Send failure', status); + } + case kCloseContextStartFailure: { + return new ERR_QUIC_ENDPOINT_CLOSED('Start failure', status); + } + } + // Otherwise return undefined. +} + +class QuicStream { + #handle; + #inner = { + __proto__: null, + session: undefined, + direction: undefined, + isLocal: false, + state: undefined, + stats: undefined, + pendingClose: undefined, + reader: undefined, + destroying: false, + iteratorLocked: false, + outboundSet: false, + writer: undefined, + fileHandle: undefined, + headers: undefined, + pendingTrailers: undefined, + onerror: undefined, + onblocked: undefined, + onreset: undefined, + onheaders: undefined, + ontrailers: undefined, + oninfo: undefined, + onwanttrailers: undefined, + }; + + static { + isQuicStream = function(val) { + return val != null && typeof val === 'object' && #handle in val; + }; + + assertIsQuicStream = function(val) { + if (!isQuicStream(val)) { + throw new ERR_INVALID_THIS('QuicStream'); + } + }; + + assertHeadersSupported = function(session) { + if (getQuicSessionState(session).headersSupported === 2) { + throw new ERR_INVALID_STATE( + 'The negotiated QUIC application protocol does not support headers'); + } + }; + + getQuicStreamState = function(stream) { + assertIsQuicStream(stream); + return stream.#inner.state; + }; + } + + /** + * @param {symbol} privateSymbol + * @param {object} handle + * @param {QuicSession} session + * @param {number} direction + * @param {boolean} [isLocal] + */ + constructor(privateSymbol, handle, session, direction, isLocal) { + assertPrivateSymbol(privateSymbol); + + this.#handle = handle; + handle[kOwner] = this; + const inner = this.#inner; + inner.session = session; + inner.direction = direction; + inner.isLocal = isLocal; + inner.state = new QuicStreamState( + kPrivateConstructor, handle.state, handle.stateByteOffset); + + if (hasObserver('quic')) { + startPerf(this, kPerfEntry, { type: 'quic', name: 'QuicStream' }); + } + + debug('stream created'); + } + + get [kValidatedSource]() { return true; } + + /** + * Returns an AsyncIterator that yields Uint8Array[] batches of + * incoming data. Only one iterator can be obtained per stream. + * Non-readable streams return an immediately-finished iterator. + * @yields {Uint8Array[]} + */ + async *[SymbolAsyncIterator]() { + assertIsQuicStream(this); + const inner = this.#inner; + if (inner.iteratorLocked) { + throw new ERR_INVALID_STATE('Stream is already being read'); + } + inner.iteratorLocked = true; + + inner.reader ??= this.#handle?.getReader(); + // Non-readable stream (outbound-only unidirectional, or closed) + if (!inner.reader) return; + + yield* createBlobReaderIterable(inner.reader, { + getReadError: () => { + // The read side ends for one of three reasons: + // * Clean FIN received from the peer (state.finReceived + // === true). The iterator stops without calling this; + // fall through to the generic state error if it does. + // * Peer sent us a RESET_STREAM. The C++ side records the + // code in state.resetCode regardless of whether the JS + // onreset handler was attached. state.finReceived stays + // false because no FIN was seen. + // * We aborted locally via stream.resetStream() or + // stream.stopSending(). Both paths run EndReadable in + // C++, setting state.readEnded without setting + // state.finReceived. There is no peer code to surface. + if (inner.state.readEnded && !inner.state.finReceived) { + const peerResetCode = inner.state.resetCode; + if (peerResetCode !== undefined && peerResetCode > 0n) { + return new ERR_QUIC_STREAM_RESET(Number(peerResetCode)); + } + return new ERR_QUIC_STREAM_ABORTED( + 'Stream aborted before FIN was received'); + } + return new ERR_INVALID_STATE('The stream is not readable'); + }, + }); + } + + /** + * True if the stream is still pending (i.e. it has not yet been opened + * and assigned an ID). + * @type {boolean} + */ + get pending() { + assertIsQuicStream(this); + return this.#inner.state.pending; + } + + /** + * True if any data on this stream was received as 0-RTT (early data) + * before the TLS handshake completed. Early data is less secure and + * could be replayed by an attacker. + * @type {boolean} + */ + get early() { + assertIsQuicStream(this); + return this.#inner.state.early; + } + + /** + * The high water mark for write backpressure. When the total queued + * outbound bytes exceeds this value, writeSync returns false and + * desiredSize drops to 0. Default is 65536 (64KB). + * @type {number} + */ + get highWaterMark() { + assertIsQuicStream(this); + return this.#inner.state.highWaterMark; + } + + set highWaterMark(val) { + assertIsQuicStream(this); + validateInteger(val, 'highWaterMark', 0, 0xFFFFFFFF); + const inner = this.#inner; + inner.state.highWaterMark = val; + // If writeDesiredSize hasn't been set yet (still 0 from initialization), + // initialize it to the highWaterMark so the first write can proceed. + if (inner.state.writeDesiredSize === 0 && val > 0) { + inner.state.writeDesiredSize = val; + } + } + + /** @type {Function|undefined} */ + get onerror() { + assertIsQuicStream(this); + return this.#inner.onerror; + } + + set onerror(fn) { + assertIsQuicStream(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onerror = undefined; + } else { + validateFunction(fn, 'onerror'); + inner.onerror = FunctionPrototypeBind(fn, this); + // Lazily create the close promise so it can be marked handled. + inner.pendingClose ??= PromiseWithResolvers(); + markPromiseAsHandled(inner.pendingClose.promise); + } + } + + /** @type {OnBlockedCallback} */ + get onblocked() { + assertIsQuicStream(this); + return this.#inner.onblocked; + } + + set onblocked(fn) { + assertIsQuicStream(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onblocked = undefined; + inner.state.wantsBlock = false; + } else { + validateFunction(fn, 'onblocked'); + inner.onblocked = FunctionPrototypeBind(fn, this); + inner.state.wantsBlock = true; + } + } + + /** @type {OnStreamErrorCallback} */ + get onreset() { + assertIsQuicStream(this); + return this.#inner.onreset; + } + + set onreset(fn) { + assertIsQuicStream(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onreset = undefined; + inner.state.wantsReset = false; + } else { + validateFunction(fn, 'onreset'); + inner.onreset = FunctionPrototypeBind(fn, this); + inner.state.wantsReset = true; + } + } + + /** @type {OnHeadersCallback} */ + get onheaders() { + assertIsQuicStream(this); + return this.#inner.onheaders; + } + + set onheaders(fn) { + assertIsQuicStream(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onheaders = undefined; + inner.state.wantsHeaders = false; + } else { + validateFunction(fn, 'onheaders'); + assertHeadersSupported(inner.session); + inner.onheaders = FunctionPrototypeBind(fn, this); + inner.state.wantsHeaders = true; + } + } + + /** @type {Function|undefined} */ + get oninfo() { + assertIsQuicStream(this); + return this.#inner.oninfo; + } + + set oninfo(fn) { + assertIsQuicStream(this); + const inner = this.#inner; + if (fn === undefined) { + inner.oninfo = undefined; + } else { + validateFunction(fn, 'oninfo'); + assertHeadersSupported(inner.session); + inner.oninfo = FunctionPrototypeBind(fn, this); + } + } + + /** @type {Function|undefined} */ + get ontrailers() { + assertIsQuicStream(this); + return this.#inner.ontrailers; + } + + set ontrailers(fn) { + assertIsQuicStream(this); + const inner = this.#inner; + if (fn === undefined) { + inner.ontrailers = undefined; + } else { + validateFunction(fn, 'ontrailers'); + assertHeadersSupported(inner.session); + inner.ontrailers = FunctionPrototypeBind(fn, this); + } + } + + /** @type {Function|undefined} */ + get onwanttrailers() { + assertIsQuicStream(this); + return this.#inner.onwanttrailers; + } + + set onwanttrailers(fn) { + assertIsQuicStream(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onwanttrailers = undefined; + inner.state.wantsTrailers = false; + } else { + validateFunction(fn, 'onwanttrailers'); + assertHeadersSupported(inner.session); + inner.onwanttrailers = FunctionPrototypeBind(fn, this); + inner.state.wantsTrailers = true; + } + } + + /** + * The buffered initial headers received on this stream, or undefined + * if the application does not support headers or no headers have + * been received yet. + * @type {object|undefined} + */ + get headers() { + assertIsQuicStream(this); + return this.#inner.headers; + } + + /** + * Set trailing headers to be sent when nghttp3 asks for them. + * @type {object|undefined} + */ + get pendingTrailers() { + assertIsQuicStream(this); + return this.#inner.pendingTrailers; + } + + set pendingTrailers(headers) { + const inner = this.#inner; + assertIsQuicStream(this); + assertHeadersSupported(inner.session); + if (headers === undefined) { + inner.pendingTrailers = undefined; + return; + } + validateObject(headers, 'headers'); + inner.pendingTrailers = headers; + } + + /** + * The statistics collected for this stream. + * @type {QuicStreamStats} + */ + get stats() { + assertIsQuicStream(this); + const inner = this.#inner; + const handle = this.#handle; + return inner.stats ??= (handle == null) ? + QuicStreamStats[kCreateDisconnected]() : + new QuicStreamStats(kPrivateConstructor, handle.stats, handle.statsByteOffset); + } + + /** + * The session this stream belongs to. If the stream is destroyed, + * `null` will be returned. + * @type {QuicSession | null} + */ + get session() { + assertIsQuicStream(this); + return this.#inner.session; + } + + /** + * Returns the id for this stream. If the stream is still pending, + * `null` will be returned. + * @type {bigint | null} + */ + get id() { + assertIsQuicStream(this); + if (this.pending) return null; + return this.#inner.state.id; + } + + /** + * Returns the directionality of this stream. + * @type {'bidi'|'uni'} + */ + get direction() { + assertIsQuicStream(this); + return this.#inner.direction === kStreamDirectionBidirectional ? 'bidi' : 'uni'; + } + + /** + * True if the stream has been destroyed. + * @type {boolean} + */ + get destroyed() { + assertIsQuicStream(this); return this.#handle === undefined; } - /** @type {Promise} */ - get closed() { - return this.#pendingClose.promise; - } + /** + * A promise that will be resolved when the stream is closed. + * @type {Promise} + */ + get closed() { + assertIsQuicStream(this); + this.#inner.pendingClose ??= PromiseWithResolvers(); + return this.#inner.pendingClose.promise; + } + + /** + * Immediately destroys the stream. Any queued data is discarded. If + * an error is given, the closed promise will be rejected with that + * error. If no error is given, the closed promise will be resolved. + * When destroying with an error, RESET_STREAM and/or STOP_SENDING + * are emitted to the peer for any still-open writable / readable + * side of the stream. The wire code is resolved as: + * `options.code` -> `error.errorCode` (when `error` is a + * `QuicError`) -> the negotiated application's "internal error" + * code from `QuicSessionState.internalErrorCode`. + * @param {any} error + * @param {QuicStreamDestroyOptions} [options] + */ + destroy(error, options = kEmptyObject) { + assertIsQuicStream(this); + const inner = this.#inner; + // Two distinct guards: + // * `#destroying` flips synchronously here so any re-entrant call + // from inside this method's user callbacks hits the guard and + // returns immediately. + // * `destroyed` (i.e. `#handle === undefined`) catches the case + // where the C++ side already finished cleanup via the + // `onStreamClose -> [kFinishClose]` path - which does NOT go + // through `destroy()` and therefore never sets `#destroying`. + // `[kFinishClose]` clears `#handle` at the end of its work. + if (inner.destroying || this.destroyed) return; + // Validate options up front so a malformed `options` argument + // throws before any side effects (mutating `#destroying`, + // emitting wire frames, invoking `onerror`, settling the closed + // promise). The caller may retry with valid options. + validateObject(options, 'options'); + const { code: optionCode, reason } = options; + if (optionCode !== undefined && + typeof optionCode !== 'bigint' && + typeof optionCode !== 'number') { + throw new ERR_INVALID_ARG_TYPE('options.code', + ['bigint', 'number'], optionCode); + } + if (reason !== undefined) { + validateString(reason, 'options.reason'); + } + inner.destroying = true; + // Resolve the wire error code for any RESET_STREAM / STOP_SENDING + // frames emitted below. + let abortCode; + if (optionCode !== undefined) { + abortCode = BigInt(optionCode); + } else if (error !== undefined) { + abortCode = QuicError.isQuicError(error) ? + error.errorCode : + getQuicSessionState(inner.session).internalErrorCode; + } + // When destroying with an error, ensure the peer stops sending + // data we are about to discard by emitting STOP_SENDING. The + // condition gates the emission to error-path destroys with a + // still-open readable side. The C++ state.readEnded flag is + // authoritative -- it is set for locally-initiated uni streams + // (which have no readable side) and when reading completes. + if (abortCode !== undefined && + !inner.state.readEnded) { + this.#handle.stopSending(abortCode); + } + // When destroying with an error, ensure the peer learns about + // it via RESET_STREAM. The writer.fail path inside [kFinishClose] + // emits RESET_STREAM only when a writer has been created; + // streams that destroy without ever accessing stream.writer + // (e.g. used setBody or never wrote at all) need an explicit + // RESET_STREAM here so the write side does not dangle on the + // wire. The C++ state.writeEnded flag is authoritative. + if (abortCode !== undefined && + inner.writer === undefined && + !inner.state.writeEnded) { + this.#handle.resetStream(abortCode); + } + if (error !== undefined && typeof inner.onerror === 'function') { + invokeOnerror(inner.onerror, error); + } + const handle = this.#handle; + this[kFinishClose](error); + handle.destroy(); + } + + /** + * Sets the outbound data source for the stream. This can only be called + * once and must be called before any data will be sent. The body can be + * an ArrayBuffer, a TypedArray or DataView, or a Blob. If the stream + * is destroyed or already has an outbound data source, an error will + * be thrown. + * @param {ArrayBuffer|SharedArrayBuffer|ArrayBufferView|Blob} outbound + */ + setOutbound(outbound) { + assertIsQuicStream(this); + if (this.destroyed) { + throw new ERR_INVALID_STATE('Stream is destroyed'); + } + if (this.#inner.state.hasOutbound) { + throw new ERR_INVALID_STATE('Stream already has an outbound data source'); + } + this.#handle.attachSource(validateBody(outbound)); + } + + /** + * @param {object} headers + * @param {SendHeadersOptions} [options] + * @returns {boolean} + */ + sendHeaders(headers, options = kEmptyObject) { + assertIsQuicStream(this); + if (this.destroyed) return false; + if (getQuicSessionState(this.#inner.session).headersSupported === 2) { + throw new ERR_INVALID_STATE( + 'The negotiated QUIC application protocol does not support headers'); + } + validateObject(headers, 'headers'); + const { terminal = false } = options; + const headerString = buildNgHeaderString( + headers, assertValidPseudoHeader, true /* strictSingleValueFields */); + const flags = terminal ? kHeadersFlagsTerminal : kHeadersFlagsNone; + return this.#handle.sendHeaders(kHeadersKindInitial, headerString, flags); + } + + /** + * Send informational (1xx) headers on this stream. Server only. + * Throws if the application does not support headers. + * @param {object} headers + * @returns {boolean} + */ + sendInformationalHeaders(headers) { + assertIsQuicStream(this); + if (this.destroyed) return false; + if (getQuicSessionState(this.#inner.session).headersSupported === 2) { + throw new ERR_INVALID_STATE( + 'The negotiated QUIC application protocol does not support headers'); + } + validateObject(headers, 'headers'); + const headerString = buildNgHeaderString( + headers, assertValidPseudoHeader, true); + return this.#handle.sendHeaders( + kHeadersKindHints, headerString, kHeadersFlagsNone); + } + + /** + * Send trailing headers on this stream. Must be called synchronously + * during the onwanttrailers callback, or set via pendingTrailers before + * the body completes. Throws if the application does not support headers. + * @param {object} headers + * @returns {boolean} + */ + sendTrailers(headers) { + assertIsQuicStream(this); + if (this.destroyed) return false; + if (getQuicSessionState(this.#inner.session).headersSupported === 2) { + throw new ERR_INVALID_STATE( + 'The negotiated QUIC application protocol does not support headers'); + } + validateObject(headers, 'headers'); + const headerString = buildNgHeaderString(headers); + return this.#handle.sendHeaders( + kHeadersKindTrailing, headerString, kHeadersFlagsNone); + } + + /** + * Returns a Writer for pushing data to this stream incrementally. + * Only available when no body source was provided at creation time + * or via setBody(). Non-writable streams return an already-closed Writer. + * @type {object} + */ + get writer() { + assertIsQuicStream(this); + const inner = this.#inner; + if (inner.writer !== undefined) return inner.writer; + if (inner.outboundSet) { + throw new ERR_INVALID_STATE( + 'Stream outbound already configured with a body source'); + } + + const handle = this.#handle; + const stream = this; + let closed = false; + let errored = false; + let error = null; + let totalBytesWritten = 0; + let drainWakeup = null; + + // Drain callback - C++ fires this when send buffer has space + stream[kDrain] = () => { + if (drainWakeup) { + drainWakeup.resolve(true); + drainWakeup = null; + } + }; + + // A note on backpressure handling: per the stream/iter spec, the default + // backpressure policy for writers is strict, meaning that if the stream + // signals backpressure additional writes are rejected until the buffer has + // capacity again. + + function writeSync(chunk) { + // If the stream is closed, errored, or write-ended, we cannot accept + // more data. Refuse the sync write. + // If a drain is already pending, another operation is waiting + // for capacity. Refuse the sync write. + if (closed || errored || stream.#inner.state.writeEnded || drainWakeup != null) { + return false; + } + chunk = toUint8Array(chunk); + const len = TypedArrayPrototypeGetByteLength(chunk); + if (len === 0) return true; + // Refuse the write only when there is no available capacity at + // all. When writeDesiredSize > 0 we allow the write even if the + // chunk is larger than the remaining capacity -- the C++ side + // will accept the data into the DataQueue and + // UpdateWriteDesiredSize() will drop writeDesiredSize toward 0, + // at which point the standard drain mechanism takes over. + // This follows the Web Streams model where writes beyond the HWM + // succeed and backpressure applies to *subsequent* writes. + if (stream.#inner.state.writeDesiredSize === 0) return false; + const result = handle.write([chunk]); + if (result === undefined) return false; + totalBytesWritten += len; + return true; + } + + async function write(chunk, options = kEmptyObject) { + validateObject(options, 'options'); + const { signal } = options; + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + signal.throwIfAborted(); + } + if (errored) throw error; + if (closed || stream.#inner.state.writeEnded) { + throw new ERR_INVALID_STATE('Writer is closed'); + } + // If a drain is already pending, another operation is waiting + // for capacity. Under strict policy, reject immediately. + // Later, if we add support for other backpressure policies, + // we could instead await the existing drain before proceeding. + if (drainWakeup != null) { + throw new ERR_INVALID_STATE('Stream write buffer is full'); + } + + if (!writeSync(chunk)) { + throw new ERR_INVALID_STATE('Stream write buffer is full'); + } + } + + function writevSync(chunks) { + if (closed || errored || stream.#inner.state.writeEnded || drainWakeup != null) { + return false; + } + chunks = convertChunks(chunks); + let len = 0; + for (const c of chunks) len += TypedArrayPrototypeGetByteLength(c); + if (len === 0) return true; + if (stream.#inner.state.writeDesiredSize === 0) return false; + const result = handle.write(chunks); + if (result === undefined) return false; + totalBytesWritten += len; + return true; + } + + async function writev(chunks, options = kEmptyObject) { + validateObject(options, 'options'); + const { signal } = options; + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + signal.throwIfAborted(); + } + + if (errored) throw error; + if (closed || stream.#inner.state.writeEnded) { + throw new ERR_INVALID_STATE('Writer is closed'); + } + + // If a drain is already pending, another operation is waiting + // for capacity. Under strict policy, reject immediately. + // Later, if we add support for other backpressure policies, + // we could instead await the existing drain before proceeding. + if (drainWakeup != null) { + throw new ERR_INVALID_STATE('Stream write buffer is full'); + } - /** - * Immediately destroys the stream. Any queued data is discarded. If an - * error is given, the closed promise will be rejected with that error. - * If no error is given, the closed promise will be resolved. - * @param {any} error - */ - destroy(error) { - if (this.destroyed) return; - const handle = this.#handle; - this[kFinishClose](error); - handle.destroy(); + if (!writevSync(chunks)) { + throw new ERR_INVALID_STATE('Stream write buffer is full'); + } + } + + function endSync() { + // Per the streams/iter spec, endSync and end follow a try-fallback + // pattern. That is, callers should try endSync first and if it returns + // -1, then they should call and await end(). This is a signal that sync + // end is not currently possible. However, we always support sync end + // here unless the stream is already errored. + if (errored) return -1; + + // If we're already closed, just return the total bytes written. + if (closed) return totalBytesWritten; + + // If we are waiting for drain to complete, we cannot end synchronously. + if (drainWakeup != null) return -1; + + // Fantastic, we can end synchronously! + handle.endWrite(); + closed = true; + return totalBytesWritten; + } + + async function end(options = kEmptyObject) { + validateObject(options, 'options'); + const { signal } = options; + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + signal.throwIfAborted(); + // TODO(@jasnell): The stream/iter spec allows individual sync end + // calls to be canceled via an AbortSignal. We currently do not support + // this, but we can add before the impl is graduated from experimental. + // At most we do here is check for signal abort at the start of the call. + } + + // Per the streams/iter spec, endSync and end follow a try-fallback + // pattern. That is, callers should try endSync first and if it returns + // -1, then they should call and await end(). This is a signal that sync + // end is not currently possible. However, we always support sync end + // here unless the stream is already errored. + // While the user should have already called endSync, we call it again + // here to actually process the end request. At worst it's called twice. + const n = endSync(); + + // A return value of -1 indicates that endSync was not yet able to + // process the end request, either because we are errored or because we + // are awaiting drain. If we're errored, throw the error. If we're waiting + // for drain, await it and then try ending again. + + if (n >= 0) return n; + if (errored) throw error; + + drainWakeup ??= PromiseWithResolvers(); + try { + await drainWakeup.promise; + } finally { + drainWakeup = null; + } + return endSync(); + } + + function fail(reason) { + if (closed || errored) return; + errored = true; + error = reason ?? new ERR_INVALID_STATE('Failed'); + // `writer.fail()` is always an error path, so the wire code on + // RESET_STREAM must never be `0n` (which means "no error" in + // most application protocols). Resolve the code in priority + // order: + // 1. If `reason` is a `QuicError`, use its explicit + // `errorCode`. + // 2. Otherwise fall back to the negotiated application's + // "internal error" code, surfaced via + // `QuicSessionState.internalErrorCode`. For HTTP/3 this is + // `H3_INTERNAL_ERROR` (0x102); for raw QUIC applications + // it falls back to the QUIC transport-layer + // `INTERNAL_ERROR` (0x1). + const code = QuicError.isQuicError(error) ? + error.errorCode : + getQuicSessionState(stream.#inner.session).internalErrorCode; + handle.resetStream(code); + if (drainWakeup != null) { + drainWakeup.reject(error); + drainWakeup = null; + } + } + + const writer = { + __proto__: null, + get desiredSize() { + if (closed || errored || stream.#inner.state.writeEnded) return null; + return stream.#inner.state.writeDesiredSize; + }, + writeSync, + write, + writevSync, + writev, + endSync, + end, + fail, + [drainableProtocol]() { + if (closed || errored) return null; + // If a drain is already pending, return the existing promise. + if (drainWakeup != null) return drainWakeup.promise; + if (stream.#inner.state.writeDesiredSize > 0) return null; + drainWakeup = PromiseWithResolvers(); + return drainWakeup.promise; + }, + [SymbolAsyncDispose]() { + if (!closed && !errored) fail(); + return PromiseResolve(); + }, + [SymbolDispose]() { + if (!closed && !errored) fail(); + }, + }; + + // Non-writable stream - return a pre-closed writer. + // A remote unidirectional stream is read-only and has no writable + // side. isLocal distinguishes locally-initiated (writable) from + // remotely-initiated (read-only) uni streams. + if (!handle || this.destroyed || inner.state.writeEnded || + (inner.direction === kStreamDirectionUnidirectional && + !inner.isLocal)) { + closed = true; + return inner.writer = writer; + } + + // Initialize the outbound DataQueue for streaming writes + handle.initStreamingSource(); + initStreamingBackpressure(this); + + return inner.writer = writer; } /** - * Sets the outbound data source for the stream. This can only be called - * once and must be called before any data will be sent. The body can be - * an ArrayBuffer, a TypedArray or DataView, or a Blob. If the stream - * is destroyed or already has an outbound data source, an error will - * be thrown. - * @param {ArrayBuffer|SharedArrayBuffer|ArrayBufferView|Blob} outbound + * Sets the outbound body source for this stream. Accepts all body + * source types (string, TypedArray, Blob, AsyncIterable, Promise, null). + * Can only be called once. Mutually exclusive with stream.writer. + * @param {any} body */ - setOutbound(outbound) { + setBody(body) { + assertIsQuicStream(this); if (this.destroyed) { throw new ERR_INVALID_STATE('Stream is destroyed'); } - if (this.#state.hasOutbound) { - throw new ERR_INVALID_STATE('Stream already has an outbound data source'); + const inner = this.#inner; + if (inner.outboundSet) { + throw new ERR_INVALID_STATE('Stream outbound already configured'); } - this.#handle.attachSource(validateBody(outbound)); + if (inner.writer !== undefined) { + throw new ERR_INVALID_STATE('Stream writer already accessed'); + } + inner.outboundSet = true; + // If the body is a FileHandle, store it so it is closed + // automatically when the stream finishes. + if (FileHandle.isFileHandle(body)) { + inner.fileHandle = body; + } + configureOutbound(this.#handle, this, body); + } + + /** + * Associates a FileHandle with this stream so it is closed automatically + * when the stream finishes. Called internally when a FileHandle is used + * as a body source. + * @param {FileHandle} fh + */ + [kAttachFileHandle](fh) { + this.#inner.fileHandle = fh; } /** - * @param {bigint} code + * Tells the peer to stop sending data for this stream. The optional error + * code will be sent to the peer as part of the request. If the stream is + * already destroyed, this is a no-op. No acknowledgement of this action + * will be provided. + * @param {number|bigint} code */ stopSending(code = 0n) { - if (this.destroyed) { - throw new ERR_INVALID_STATE('Stream is destroyed'); - } + assertIsQuicStream(this); + if (this.destroyed) return; this.#handle.stopSending(BigInt(code)); } /** - * @param {bigint} code + * Tells the peer that this end will not send any more data on this stream. + * The optional error code will be sent to the peer as part of the + * request. If the stream is already destroyed, this is a no-op. No + * acknowledgement of this action will be provided. + * @param {number|bigint} code */ resetStream(code = 0n) { - if (this.destroyed) { - throw new ERR_INVALID_STATE('Stream is destroyed'); - } + assertIsQuicStream(this); + if (this.destroyed) return; this.#handle.resetStream(BigInt(code)); } - /** @type {'default' | 'low' | 'high'} */ + /** + * The priority of the stream. If the stream is destroyed or if + * the session does not support priority, `null` will be + * returned. + * @type {StreamPriority | null} + */ get priority() { - if (this.destroyed || !this.session.state.isPrioritySupported) return undefined; - switch (this.#handle.getPriority()) { - case 3: return 'default'; - case 7: return 'low'; - case 0: return 'high'; - default: return 'default'; - } + assertIsQuicStream(this); + if (this.destroyed || + !getQuicSessionState(this.#inner.session).isPrioritySupported) return null; + const packed = this.#handle.getPriority(); + const urgency = packed >> 1; + const incremental = !!(packed & 1); + const level = urgency < 3 ? 'high' : urgency > 3 ? 'low' : 'default'; + return { level, incremental }; } - set priority(val) { - if (this.destroyed || !this.session.state.isPrioritySupported) return; - switch (val) { - case 'default': this.#handle.setPriority(3, 1); break; - case 'low': this.#handle.setPriority(7, 1); break; - case 'high': this.#handle.setPriority(0, 1); break; + /** + * Sets the priority of the stream. + * @param {StreamPriority} [options] + */ + setPriority(options = kEmptyObject) { + assertIsQuicStream(this); + if (this.destroyed) return; + if (!getQuicSessionState(this.#inner.session).isPrioritySupported) { + throw new ERR_INVALID_STATE( + 'The session does not support stream priority'); } - // Otherwise ignore the value as invalid. + validateObject(options, 'options'); + const { + level = 'default', + incremental = false, + } = options; + validateOneOf(level, 'options.level', ['default', 'low', 'high']); + validateBoolean(incremental, 'options.incremental'); + const urgency = level === 'high' ? 0 : level === 'low' ? 7 : 3; + this.#handle.setPriority((urgency << 1) | (incremental ? 1 : 0)); } /** @@ -805,205 +2480,754 @@ class QuicStream { * @param {object} headers * @returns {boolean} true if the headers were scheduled to be sent. */ - [kSendHeaders](headers) { + [kSendHeaders](headers, kind = kHeadersKindInitial, + flags = kHeadersFlagsTerminal) { validateObject(headers, 'headers'); + if (getQuicSessionState(this.#inner.session).headersSupported === 2) { + throw new ERR_INVALID_STATE( + 'The negotiated QUIC application protocol does not support headers'); + } if (this.pending) { debug('pending stream enqueuing headers', headers); } else { - debug(`stream ${this.id} sending headers`, headers); + debug(`stream ${this.id} sending headers`, headers); + } + const headerString = buildNgHeaderString( + headers, + assertValidPseudoHeader, + true, // This could become an option in future + ); + return this.#handle.sendHeaders(kind, headerString, flags); + } + + [kFinishClose](error) { + const inner = this.#inner; + inner.pendingClose ??= PromiseWithResolvers(); + if (this.destroyed) { + return inner.pendingClose.promise; + } + if (error !== undefined) { + inner.pendingClose.reject(error); + } else { + inner.pendingClose.resolve(); + } + debug('stream closed'); + if (onStreamClosedChannel.hasSubscribers) { + onStreamClosedChannel.publish({ + __proto__: null, + stream: this, + session: inner.session, + error, + stats: this.stats, + }); + } + if (this[kPerfEntry] && hasObserver('quic')) { + stopPerf(this, kPerfEntry, { + detail: { + stats: this.stats, + direction: this.direction, + }, + }); + } + inner.stats?.[kFinishClose](); + inner.state?.[kFinishClose](); + inner.session[kRemoveStream](this); + inner.writer?.fail(error); + inner.session = undefined; + inner.pendingClose.reject = undefined; + inner.pendingClose.resolve = undefined; + inner.onblocked = undefined; + inner.onreset = undefined; + inner.onheaders = undefined; + inner.onerror = undefined; + inner.ontrailers = undefined; + inner.oninfo = undefined; + inner.onwanttrailers = undefined; + inner.headers = undefined; + inner.pendingTrailers = undefined; + this.#handle = undefined; + if (inner.fileHandle !== undefined) { + // Close the FileHandle that was used as a body source. The close + // may fail if the user already closed it -- that's expected and + // harmless, so mark the promise as handled. + markPromiseAsHandled(this.#inner.fileHandle.close()); + inner.fileHandle = undefined; + } + } + + [kBlocked]() { + const inner = this.#inner; + // The blocked event should only be called if the stream was created with + // an onblocked callback. The callback should always exist here. + assert(inner.onblocked, 'Unexpected stream blocked event'); + if (onStreamBlockedChannel.hasSubscribers) { + onStreamBlockedChannel.publish({ + __proto__: null, + stream: this, + session: inner.session, + }); + } + safeCallbackInvoke(inner.onblocked, this); + } + + [kDrain]() { + // No-op by default. Overridden by the writer closure when + // stream.writer is accessed. + } + + [kReset](error) { + const inner = this.#inner; + // The reset event should only be called if the stream was created with + // an onreset callback. The callback should always exist here. + assert(inner.onreset, 'Unexpected stream reset event'); + if (onStreamResetChannel.hasSubscribers) { + onStreamResetChannel.publish({ + __proto__: null, + stream: this, + session: inner.session, + error, + }); + } + safeCallbackInvoke(inner.onreset, this, error); + } + + [kHeaders](headers, kind) { + const block = parseHeaderPairs(headers); + const kindName = kHeadersKindName[kind] ?? kind; + const inner = this.#inner; + + switch (kindName) { + case 'initial': + assert(inner.onheaders, 'Unexpected stream headers event'); + inner.headers ??= block; + if (onStreamHeadersChannel.hasSubscribers) { + onStreamHeadersChannel.publish({ + __proto__: null, + stream: this, + session: inner.session, + headers: block, + }); + } + safeCallbackInvoke(inner.onheaders, this, block); + break; + case 'trailing': + if (onStreamTrailersChannel.hasSubscribers) { + onStreamTrailersChannel.publish({ + __proto__: null, + stream: this, + session: inner.session, + trailers: block, + }); + } + if (inner.ontrailers) + safeCallbackInvoke(inner.ontrailers, this, block); + break; + case 'hints': + if (onStreamInfoChannel.hasSubscribers) { + onStreamInfoChannel.publish({ + __proto__: null, + stream: this, + session: inner.session, + headers: block, + }); + } + if (typeof inner.oninfo === 'function') + safeCallbackInvoke(inner.oninfo, this, block); + break; + } + } + + [kTrailers]() { + if (this.destroyed) return; + const inner = this.#inner; + + // nghttp3 is asking us to provide trailers to send. + // Check for pre-set pendingTrailers first, then the callback. + if (inner.pendingTrailers) { + this.sendTrailers(inner.pendingTrailers); + inner.pendingTrailers = undefined; + } else if (typeof inner.onwanttrailers === 'function') { + safeCallbackInvoke(inner.onwanttrailers, this); + } + } + + [kInspect](depth, options) { + if (depth < 0) { + return 'QuicStream { }'; + } + + const opts = { + __proto__: null, + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + const { + id, + direction, + pending, + stats, + session, + } = this; + + return `QuicStream ${inspect({ + __proto__: null, + id, + direction, + pending, + stats, + state: this.#inner.state, + session, + }, opts)}`; + } +} + +class QuicSession { + /** @type {object|undefined} */ + #handle; + + #inner = { + __proto__: null, + /** @type {QuicEndpoint} */ + endpoint: undefined, + isPendingClose: false, + selfInitiatedClose: false, + destroying: false, + handshakeCompleted: false, + pendingClose: PromiseWithResolvers(), + pendingOpen: PromiseWithResolvers(), + /** @type {QuicSessionState} */ + state: undefined, + /** @type {QuicSessionStats} */ + stats: undefined, + streams: new SafeSet(), + onerror: undefined, + onstream: undefined, + ondatagram: undefined, + ondatagramstatus: undefined, + onpathvalidation: undefined, + onsessionticket: undefined, + onversionnegotiation: undefined, + onhandshake: undefined, + onnewtoken: undefined, + onearlyrejected: undefined, + onorigin: undefined, + ongoaway: undefined, + onkeylog: undefined, + onqlog: undefined, + pendingQlog: undefined, + // Default to 'manual' (no auto-rejection). Client sessions override + // this via kVerifyPeer in kConnect. Server sessions keep 'manual' + // because server-side cert validation is handled by rejectUnauthorized + // at the C++ level. + verifyPeer: 'manual', + handshakeInfo: undefined, + /** @type {QuicSessionPath|undefined} */ + path: undefined, + certificate: undefined, + peerCertificate: undefined, + ephemeralKeyInfo: undefined, + localTransportParams: undefined, + remoteTransportParams: undefined, + }; + + static { + isQuicSession = function(val) { + return val != null && typeof val === 'object' && #handle in val; + }; + + assertIsQuicSession = function(val) { + if (!isQuicSession(val)) { + throw new ERR_INVALID_THIS('QuicSession'); + } + }; + + getQuicSessionState = function(session) { + assertIsQuicSession(session); + return session.#inner.state; + }; + } + + /** + * @param {symbol} privateSymbol + * @param {object} handle + * @param {QuicEndpoint} endpoint + */ + constructor(privateSymbol, handle, endpoint) { + // Instances of QuicSession can only be created internally. + assertPrivateSymbol(privateSymbol); + + this.#handle = handle; + this.#handle[kOwner] = this; + + const inner = this.#inner; + inner.endpoint = endpoint; + // Move any qlog entries that arrived before the wrapper existed. + if (handle._pendingQlog !== undefined) { + inner.pendingQlog = handle._pendingQlog; + handle._pendingQlog = undefined; + } + inner.stats = new QuicSessionStats( + kPrivateConstructor, handle.stats, handle.statsByteOffset); + inner.state = new QuicSessionState( + kPrivateConstructor, handle.state, handle.stateByteOffset); + + if (hasObserver('quic')) { + startPerf(this, kPerfEntry, { type: 'quic', name: 'QuicSession' }); + } + + debug('session created'); + } + + get applicationOptions() { + // We don't cache application options because they may be updated by the + // C++ layer after session creation depending on the behavior of the + // application. + if (this.destroyed) return null; + return this.#handle.applicationOptions(); + } + + get localTransportParams() { + if (this.#inner.localTransportParams !== undefined) { + return this.#inner.localTransportParams; + } + // If the handle is already gone, we cannot retrieve the transport params. + if (this.destroyed) return null; + const params = this.#handle.localTransportParams(); + if (params.preferredAddressIpv4 !== undefined) { + params.preferredAddressIpv4 = new InternalSocketAddress(params.preferredAddressIpv4); + } + if (params.preferredAddressIpv6 !== undefined) { + params.preferredAddressIpv6 = new InternalSocketAddress(params.preferredAddressIpv6); + } + return this.#inner.localTransportParams = params; + } + + get remoteTransportParams() { + if (this.#inner.remoteTransportParams !== undefined) { + return this.#inner.remoteTransportParams; + } + // If the handle is already gone, we cannot retrieve the transport params. + if (this.destroyed) return null; + const params = this.#handle.remoteTransportParams(); + // If params is undefined, the transport parameters have not yet been received. + // Note the distinction between this and the case where the handle is gone. + // If the handle is gone, we return null because we know the transport + // parameters will be unavailable. If the transport parameters have not yet + // been received, we return undefined to indicate that they may still become + // available in the future. + if (params === undefined) return undefined; + if (params.preferredAddressIpv4 !== undefined) { + params.preferredAddressIpv4 = new InternalSocketAddress(params.preferredAddressIpv4); + } + if (params.preferredAddressIpv6 !== undefined) { + params.preferredAddressIpv6 = new InternalSocketAddress(params.preferredAddressIpv6); + } + return this.#inner.remoteTransportParams = params; + } + + /** @type {boolean} */ + get #isClosedOrClosing() { + return this.#handle === undefined || this.#inner.isPendingClose; + } + + /** @type {Function|undefined} */ + get onerror() { + assertIsQuicSession(this); + return this.#inner.onerror; + } + + set onerror(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onerror = undefined; + } else { + validateFunction(fn, 'onerror'); + inner.onerror = FunctionPrototypeBind(fn, this); + // When an onerror handler is provided, mark the pending promises + // as handled so that rejections from destroy(error) don't surface + // as unhandled rejections. The onerror callback is the + // application's error handler for this session. + markPromiseAsHandled(inner.pendingClose.promise); + markPromiseAsHandled(inner.pendingOpen.promise); + // Also mark existing streams' closed promises. Stream rejections + // during session destruction are expected collateral when the + // session has an error handler. + for (const stream of inner.streams) { + markPromiseAsHandled(stream.closed); + } + } + } + + /** @type {OnStreamCallback} */ + get onstream() { + assertIsQuicSession(this); + return this.#inner.onstream; + } + + set onstream(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onstream = undefined; + } else { + validateFunction(fn, 'onstream'); + inner.onstream = FunctionPrototypeBind(fn, this); + } + } + + /** @type {OnDatagramCallback} */ + get ondatagram() { + assertIsQuicSession(this); + return this.#inner.ondatagram; + } + + set ondatagram(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.ondatagram = undefined; + inner.state.hasDatagramListener = false; + } else { + validateFunction(fn, 'ondatagram'); + inner.ondatagram = FunctionPrototypeBind(fn, this); + inner.state.hasDatagramListener = true; + } + } + + /** + * The ondatagramstatus callback is called when the status of a sent datagram + * is received. This is best-effort only. + * @type {OnDatagramStatusCallback} + */ + get ondatagramstatus() { + assertIsQuicSession(this); + return this.#inner.ondatagramstatus; + } + + set ondatagramstatus(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.ondatagramstatus = undefined; + inner.state.hasDatagramStatusListener = false; + } else { + validateFunction(fn, 'ondatagramstatus'); + inner.ondatagramstatus = FunctionPrototypeBind(fn, this); + inner.state.hasDatagramStatusListener = true; + } + } + + /** @type {Function|undefined} */ + get onpathvalidation() { + assertIsQuicSession(this); + return this.#inner.onpathvalidation; + } + + set onpathvalidation(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onpathvalidation = undefined; + inner.state.hasPathValidationListener = false; + } else { + validateFunction(fn, 'onpathvalidation'); + inner.onpathvalidation = FunctionPrototypeBind(fn, this); + inner.state.hasPathValidationListener = true; + } + } + + get onkeylog() { + assertIsQuicSession(this); + return this.#inner.onkeylog; + } + + set onkeylog(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onkeylog = undefined; + } else { + validateFunction(fn, 'onkeylog'); + inner.onkeylog = FunctionPrototypeBind(fn, this); + } + } + + get onqlog() { + assertIsQuicSession(this); + return this.#inner.onqlog; + } + + set onqlog(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onqlog = undefined; + } else { + validateFunction(fn, 'onqlog'); + inner.onqlog = FunctionPrototypeBind(fn, this); + // Flush any qlog entries that were cached before the callback was set. + if (inner.pendingQlog !== undefined) { + const pending = inner.pendingQlog; + inner.pendingQlog = undefined; + for (let i = 0; i < pending.length; i += 2) { + this[kQlog](pending[i], pending[i + 1]); + } + } + } + } + + /** @type {Function|undefined} */ + get onsessionticket() { + assertIsQuicSession(this); + return this.#inner.onsessionticket; + } + + set onsessionticket(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onsessionticket = undefined; + inner.state.hasSessionTicketListener = false; + } else { + validateFunction(fn, 'onsessionticket'); + inner.onsessionticket = FunctionPrototypeBind(fn, this); + inner.state.hasSessionTicketListener = true; } - const headerString = buildNgHeaderString( - headers, - assertValidPseudoHeader, - true, // This could become an option in future - ); - // TODO(@jasnell): Support differentiating between early headers, primary headers, etc - return this.#handle.sendHeaders(1, headerString, 1); } - [kFinishClose](error) { - if (this.destroyed) return this.#pendingClose.promise; - if (error !== undefined) { - if (this.pending) { - debug(`destroying pending stream with error: ${error}`); - } else { - debug(`destroying stream ${this.id} with error: ${error}`); - } - this.#pendingClose.reject(error); + /** @type {Function|undefined} */ + get onapplication() { + assertIsQuicSession(this); + return this.#inner.onapplication; + } + + set onapplication(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onapplication = undefined; + inner.state.hasApplicationListener = false; } else { - if (this.pending) { - debug('destroying pending stream with no error'); - } else { - debug(`destroying stream ${this.id} with no error`); - } - this.#pendingClose.resolve(); - } - this.#stats[kFinishClose](); - this.#state[kFinishClose](); - this.#session[kRemoveStream](this); - this.#session = undefined; - this.#pendingClose.reject = undefined; - this.#pendingClose.resolve = undefined; - this.#onblocked = undefined; - this.#onreset = undefined; - this.#onheaders = undefined; - this.#ontrailers = undefined; - this.#handle = undefined; + validateFunction(fn, 'onapplication'); + inner.onapplication = FunctionPrototypeBind(fn, this); + inner.state.hasApplicationListener = true; + } } - [kBlocked]() { - // The blocked event should only be called if the stream was created with - // an onblocked callback. The callback should always exist here. - assert(this.#onblocked, 'Unexpected stream blocked event'); - this.#onblocked(); + /** @type {Function|undefined} */ + get onversionnegotiation() { + assertIsQuicSession(this); + return this.#inner.onversionnegotiation; } - [kReset](error) { - // The reset event should only be called if the stream was created with - // an onreset callback. The callback should always exist here. - assert(this.#onreset, 'Unexpected stream reset event'); - this.#onreset(error); + set onversionnegotiation(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onversionnegotiation = undefined; + } else { + validateFunction(fn, 'onversionnegotiation'); + inner.onversionnegotiation = FunctionPrototypeBind(fn, this); + } } - [kHeaders](headers, kind) { - // The headers event should only be called if the stream was created with - // an onheaders callback. The callback should always exist here. - assert(this.#onheaders, 'Unexpected stream headers event'); - assert(ArrayIsArray(headers)); - assert(headers.length % 2 === 0); - const block = { - __proto__: null, - }; - for (let n = 0; n + 1 < headers.length; n += 2) { - if (block[headers[n]] !== undefined) { - block[headers[n]] = [block[headers[n]], headers[n + 1]]; - } else { - block[headers[n]] = headers[n + 1]; - } + /** @type {Function|undefined} */ + get onhandshake() { + assertIsQuicSession(this); + return this.#inner.onhandshake; + } + + set onhandshake(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onhandshake = undefined; + } else { + validateFunction(fn, 'onhandshake'); + inner.onhandshake = FunctionPrototypeBind(fn, this); } + } - this.#onheaders(block, kind); + /** @type {Function|undefined} */ + get onnewtoken() { + assertIsQuicSession(this); + return this.#inner.onnewtoken; } - [kTrailers]() { - // The trailers event should only be called if the stream was created with - // an ontrailers callback. The callback should always exist here. - assert(this.#ontrailers, 'Unexpected stream trailers event'); - this.#ontrailers(); + set onnewtoken(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onnewtoken = undefined; + inner.state.hasNewTokenListener = false; + } else { + validateFunction(fn, 'onnewtoken'); + inner.onnewtoken = FunctionPrototypeBind(fn, this); + inner.state.hasNewTokenListener = true; + } } - [kInspect](depth, options) { - if (depth < 0) - return this; + /** @type {Function|undefined} */ + get onearlyrejected() { + assertIsQuicSession(this); + return this.#inner.onearlyrejected; + } - const opts = { - ...options, - depth: options.depth == null ? null : options.depth - 1, - }; + set onearlyrejected(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onearlyrejected = undefined; + } else { + validateFunction(fn, 'onearlyrejected'); + inner.onearlyrejected = FunctionPrototypeBind(fn, this); + } + } - return `Stream ${inspect({ - id: this.id, - direction: this.direction, - pending: this.pending, - stats: this.stats, - state: this.#state, - session: this.session, - }, opts)}`; + /** @type {Function|undefined} */ + get onorigin() { + assertIsQuicSession(this); + return this.#inner.onorigin; } -} -class QuicSession { - /** @type {QuicEndpoint} */ - #endpoint = undefined; - /** @type {boolean} */ - #isPendingClose = false; - /** @type {object|undefined} */ - #handle; - /** @type {PromiseWithResolvers} */ - #pendingClose = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials - /** @type {PromiseWithResolvers} */ - #pendingOpen = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials - /** @type {QuicSessionState} */ - #state; - /** @type {QuicSessionStats} */ - #stats; - /** @type {Set} */ - #streams = new SafeSet(); - /** @type {OnStreamCallback} */ - #onstream = undefined; - /** @type {OnDatagramCallback|undefined} */ - #ondatagram = undefined; - /** @type {object|undefined} */ - #sessionticket = undefined; + set onorigin(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onorigin = undefined; + inner.state.hasOriginListener = false; + } else { + validateFunction(fn, 'onorigin'); + inner.onorigin = FunctionPrototypeBind(fn, this); + inner.state.hasOriginListener = true; + } + } + + /** @type {Function|undefined} */ + get ongoaway() { + assertIsQuicSession(this); + return this.#inner.ongoaway; + } + + set ongoaway(fn) { + assertIsQuicSession(this); + const inner = this.#inner; + if (fn === undefined) { + inner.ongoaway = undefined; + } else { + validateFunction(fn, 'ongoaway'); + inner.ongoaway = FunctionPrototypeBind(fn, this); + } + } /** - * @param {symbol} privateSymbol - * @param {object} handle - * @param {QuicEndpoint} endpoint + * The maximum datagram size the peer will accept, or 0 if datagrams + * are not supported or the handshake has not yet completed. + * @type {bigint} */ - constructor(privateSymbol, handle, endpoint) { - // Instances of QuicSession can only be created internally. - if (privateSymbol !== kPrivateConstructor) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); - } + get maxDatagramSize() { + assertIsQuicSession(this); + return this.#inner.state.maxDatagramSize; + } - this.#endpoint = endpoint; - this.#handle = handle; - this.#handle[kOwner] = this; - this.#stats = new QuicSessionStats(kPrivateConstructor, handle.stats); - this.#state = new QuicSessionState(kPrivateConstructor, handle.state); - this.#state.hasVersionNegotiationListener = true; - this.#state.hasPathValidationListener = true; - this.#state.hasSessionTicketListener = true; + /** + * Maximum number of datagrams that can be queued while inside a + * ngtcp2 callback scope. When the queue is full, the oldest + * datagram is dropped and reported as lost. Default is 128. + * @type {number} + */ + get maxPendingDatagrams() { + assertIsQuicSession(this); + return this.#inner.state.maxPendingDatagrams; + } - debug('session created'); + set maxPendingDatagrams(val) { + assertIsQuicSession(this); + validateInteger(val, 'maxPendingDatagrams', 0, 0xFFFF); + this.#inner.state.maxPendingDatagrams = val; } - /** @type {boolean} */ - get #isClosedOrClosing() { - return this.#handle === undefined || this.#isPendingClose; + /** + * The statistics collected for this session. + * @type {QuicSessionStats} + */ + get stats() { + assertIsQuicSession(this); + return this.#inner.stats; } /** - * Get the session ticket associated with this session, if any. - * @type {object|undefined} + * The endpoint this session belongs to. If the session has been destroyed, + * `null` will be returned. + * @type {QuicEndpoint|null} */ - get sessionticket() { return this.#sessionticket; } + get endpoint() { + assertIsQuicSession(this); + if (this.destroyed) return null; + return this.#inner.endpoint; + } - /** @type {OnStreamCallback} */ - get onstream() { return this.#onstream; } + /** + * The local and remote socket addresses associated with the session. + * @type {QuicSessionPath | undefined} + */ + get path() { + assertIsQuicSession(this); + if (this.destroyed) return undefined; + return this.#inner.path ??= { + __proto__: null, + local: new InternalSocketAddress(this.#handle.getLocalAddress()), + remote: new InternalSocketAddress(this.#handle.getRemoteAddress()), + }; + } - set onstream(fn) { - if (fn === undefined) { - this.#onstream = undefined; - } else { - validateFunction(fn, 'onstream'); - this.#onstream = fn.bind(this); + /** + * The local certificate as a {@link crypto.X509Certificate}, or undefined + * if no local certificate is available. Server sessions return their + * configured certificate; client sessions return undefined unless a + * client certificate was sent. + * @type {crypto.X509Certificate|undefined} + */ + get certificate() { + assertIsQuicSession(this); + if (this.destroyed) return undefined; + if (this.#inner.certificate === undefined) { + const handle = this.#handle.getCertificate(); + this.#inner.certificate = handle ? new InternalX509Certificate(handle) : null; } + return this.#inner.certificate ?? undefined; } - /** @type {OnDatagramCallback} */ - get ondatagram() { return this.#ondatagram; } - - set ondatagram(fn) { - if (fn === undefined) { - this.#ondatagram = undefined; - this.#state.hasDatagramListener = false; - } else { - validateFunction(fn, 'ondatagram'); - this.#ondatagram = fn.bind(this); - this.#state.hasDatagramListener = true; + /** + * The peer's certificate as a {@link crypto.X509Certificate}, or undefined + * if the peer did not present a certificate or the session is destroyed. + * @type {crypto.X509Certificate|undefined} + */ + get peerCertificate() { + assertIsQuicSession(this); + if (this.destroyed) return undefined; + if (this.#inner.peerCertificate === undefined) { + const handle = this.#handle.getPeerCertificate(); + this.#inner.peerCertificate = handle ? + new InternalX509Certificate(handle) : + null; } + return this.#inner.peerCertificate ?? undefined; } - /** @type {QuicSessionStats} */ - get stats() { return this.#stats; } - - /** @type {QuicSessionState} */ - get [kState]() { return this.#state; } - - /** @type {QuicEndpoint} */ - get endpoint() { return this.#endpoint; } + /** + * The ephemeral key info for the session. Only available on client + * sessions. Returns undefined for server sessions or if the session + * is destroyed. + * @type {object|undefined} + */ + get ephemeralKeyInfo() { + assertIsQuicSession(this); + if (this.destroyed) return undefined; + return this.#inner.ephemeralKeyInfo ??= this.#handle.getEphemeralKey(); + } /** * @param {number} direction @@ -1011,11 +3235,12 @@ class QuicSession { * @returns {QuicStream} */ async #createStream(direction, options = kEmptyObject) { + const inner = this.#inner; if (this.#isClosedOrClosing) { throw new ERR_INVALID_STATE('Session is closed. New streams cannot be opened.'); } - const dir = direction === STREAM_DIRECTION_BIDIRECTIONAL ? 'bidi' : 'uni'; - if (this.#state.isStreamOpenAllowed) { + const dir = direction === kStreamDirectionBidirectional ? 'bidi' : 'uni'; + if (inner.state.isStreamOpenAllowed) { debug(`opening new pending ${dir} stream`); } else { debug(`opening new ${dir} stream`); @@ -1024,15 +3249,18 @@ class QuicSession { validateObject(options, 'options'); const { body, - sendOrder = 50, - [kHeaders]: headers, + priority = 'default', + incremental = false, + highWaterMark = kDefaultHighWaterMark, + headers, + onheaders, + ontrailers, + oninfo, + onwanttrailers, } = options; - if (headers !== undefined) { - validateObject(headers, 'options.headers'); - } - validateNumber(sendOrder, 'options.sendOrder'); - // TODO(@jasnell): Make use of sendOrder to set the priority + validateOneOf(priority, 'options.priority', ['default', 'low', 'high']); + validateBoolean(incremental, 'options.incremental'); const validatedBody = validateBody(body); @@ -1041,18 +3269,40 @@ class QuicSession { throw new ERR_QUIC_OPEN_STREAM_FAILED(); } - if (headers !== undefined) { - // If headers are specified and there's no body, then we assume - // that the headers are terminal. - handle.sendHeaders(1, buildNgHeaderString(headers), - validatedBody === undefined ? 1 : 0); + if (inner.state.isPrioritySupported) { + const urgency = priority === 'high' ? 0 : priority === 'low' ? 7 : 3; + handle.setPriority((urgency << 1) | (incremental ? 1 : 0)); + } + + const stream = new QuicStream( + kPrivateConstructor, handle, this, direction, true /* isLocal */); + inner.streams.add(stream); + if (typeof this.#inner.onerror === 'function') { + markPromiseAsHandled(stream.closed); + } + + // If the body was a FileHandle, store it on the stream so it is + // closed automatically when the stream finishes. + if (FileHandle.isFileHandle(body)) { + stream[kAttachFileHandle](body); } - const stream = new QuicStream(kPrivateConstructor, handle, this, direction); - this.#streams.add(stream); + // Set the high water mark for backpressure. + stream.highWaterMark = highWaterMark; + + // Set stream callbacks before sending headers to avoid missing events. + if (onheaders) stream.onheaders = onheaders; + if (ontrailers) stream.ontrailers = ontrailers; + if (oninfo) stream.oninfo = oninfo; + if (onwanttrailers) stream.onwanttrailers = onwanttrailers; + + if (headers !== undefined) { + stream.sendHeaders(headers, { terminal: validatedBody === undefined }); + } if (onSessionOpenStreamChannel.hasSubscribers) { onSessionOpenStreamChannel.publish({ + __proto__: null, stream, session: this, direction: dir, @@ -1062,19 +3312,25 @@ class QuicSession { } /** + * Creates a new bidirectional stream on this session. If the session + * does not allow new streams to be opened, an error will be thrown. * @param {OpenStreamOptions} [options] * @returns {Promise} */ async createBidirectionalStream(options = kEmptyObject) { - return await this.#createStream(STREAM_DIRECTION_BIDIRECTIONAL, options); + assertIsQuicSession(this); + return await this.#createStream(kStreamDirectionBidirectional, options); } /** + * Creates a new unidirectional stream on this session. If the session + * does not allow new streams to be opened, an error will be thrown. * @param {OpenStreamOptions} [options] * @returns {Promise} */ async createUnidirectionalStream(options = kEmptyObject) { - return await this.#createStream(STREAM_DIRECTION_UNIDIRECTIONAL, options); + assertIsQuicSession(this); + return await this.#createStream(kStreamDirectionUnidirectional, options); } /** @@ -1082,47 +3338,81 @@ class QuicSession { * of the sent datagram will be reported via the datagram-status event if * possible. * - * If a string is given it will be encoded as UTF-8. + * If a string is given it will be encoded using the specified encoding. * - * If an ArrayBufferView is given, the view will be copied. - * @param {ArrayBufferView|string} datagram The datagram payload - * @returns {Promise} + * If an ArrayBufferView is given, the bytes are copied into an internal + * buffer; the caller's source buffer is unchanged and may be reused + * immediately. Callers that want to ensure their source cannot be + * mutated after the call (for example, when handing the buffer off to + * another async consumer) can call ArrayBuffer.prototype.transfer() + * themselves before passing it. + * + * If a Promise is given, it will be awaited before sending. If the + * session closes while awaiting, 0n is returned silently. + * @param {ArrayBufferView|string|Promise} datagram The datagram payload + * @param {string} [encoding] The encoding to use if datagram is a string + * @returns {Promise} The datagram ID */ - async sendDatagram(datagram) { + async sendDatagram(datagram, encoding = 'utf8') { + assertIsQuicSession(this); if (this.#isClosedOrClosing) { throw new ERR_INVALID_STATE('Session is closed'); } + + const maxDatagramSize = this.#inner.state.maxDatagramSize; + + // The peer max datagram size is either unknown or they have explicitly + // indicated that they do not support datagrams by setting it to 0. In + // either case, we do not send the datagram. + if (maxDatagramSize === 0) return kNilDatagramId; + + if (isPromise(datagram)) { + datagram = await datagram; + // Session may have closed while awaiting. Since datagrams are + // inherently unreliable, silently return rather than throwing. + if (this.#isClosedOrClosing) return kNilDatagramId; + } + if (typeof datagram === 'string') { - datagram = Buffer.from(datagram, 'utf8'); - } else { - if (!isArrayBufferView(datagram)) { - throw new ERR_INVALID_ARG_TYPE('datagram', - ['ArrayBufferView', 'string'], - datagram); - } - const length = datagram.byteLength; - const offset = datagram.byteOffset; - datagram = new Uint8Array(ArrayBufferPrototypeTransfer(datagram.buffer), - length, offset); + datagram = new Uint8Array(Buffer.from(datagram, encoding)); + } else if (!isArrayBufferView(datagram)) { + throw new ERR_INVALID_ARG_TYPE('datagram', + ['ArrayBufferView', 'string'], + datagram); } - debug(`sending datagram with ${datagram.byteLength} bytes`); + const length = isDataView(datagram) ? + DataViewPrototypeGetByteLength(datagram) : + TypedArrayPrototypeGetByteLength(datagram); + + // If the view has zero length (e.g. detached buffer), there's + // nothing to send. + if (length === 0) return kNilDatagramId; + + // The peer max datagram size is less than the datagram we want to send, + // so... don't send it. + if (length > maxDatagramSize) return kNilDatagramId; const id = this.#handle.sendDatagram(datagram); - if (onSessionSendDatagramChannel.hasSubscribers) { + if (id !== kNilDatagramId && onSessionSendDatagramChannel.hasSubscribers) { onSessionSendDatagramChannel.publish({ + __proto__: null, id, - length: datagram.byteLength, + length, session: this, }); } + + debug(`datagram ${id} sent with ${length} bytes`); + return id; } /** * Initiate a key update. */ updateKey() { + assertIsQuicSession(this); if (this.#isClosedOrClosing) { throw new ERR_INVALID_STATE('Session is closed'); } @@ -1132,6 +3422,7 @@ class QuicSession { this.#handle.updateKey(); if (onSessionUpdateKeyChannel.hasSubscribers) { onSessionUpdateKeyChannel.publish({ + __proto__: null, session: this, }); } @@ -1145,17 +3436,31 @@ class QuicSession { * New streams will not be allowed to be created. The returned promise will * be resolved when the session closes, or will be rejected if the session * closes abruptly due to an error. + * @param {object} [options] + * @param {bigint|number} [options.code] The error code to send in the + * CONNECTION_CLOSE frame. Defaults to NO_ERROR (0). + * @param {string} [options.type] Either `'transport'` (default) or + * `'application'`. Determines the error code namespace. + * @param {string} [options.reason] An optional human-readable reason + * string included in the CONNECTION_CLOSE frame (diagnostic only). * @returns {Promise} */ - close() { + close(options = kEmptyObject) { + assertIsQuicSession(this); + options = validateCloseOptions(options); + const inner = this.#inner; if (!this.#isClosedOrClosing) { - this.#isPendingClose = true; + inner.isPendingClose = true; + if (options?.code !== undefined) { + inner.selfInitiatedClose = true; + } debug('gracefully closing the session'); - this.#handle?.gracefulClose(); + this.#handle.gracefulClose(options); if (onSessionClosingChannel.hasSubscribers) { onSessionClosingChannel.publish({ + __proto__: null, session: this, }); } @@ -1163,18 +3468,32 @@ class QuicSession { return this.closed; } + /** @type {boolean} */ + get closing() { + return this.#inner.isPendingClose; + } + /** @type {Promise} */ - get opened() { return this.#pendingOpen.promise; } + get opened() { + assertIsQuicSession(this); + return this.#inner.pendingOpen.promise; + } /** * A promise that is resolved when the session is closed, or is rejected if * the session is closed abruptly due to an error. * @type {Promise} */ - get closed() { return this.#pendingClose.promise; } + get closed() { + assertIsQuicSession(this); + return this.#inner.pendingClose.promise; + } /** @type {boolean} */ - get destroyed() { return this.#handle === undefined; } + get destroyed() { + assertIsQuicSession(this); + return this.#handle === undefined; + } /** * Forcefully closes the session abruptly without waiting for streams to be @@ -1183,64 +3502,173 @@ class QuicSession { * the closed promise will be rejected with that error. If no error is given, * the closed promise will be resolved. * @param {any} error + * @param {object} [options] + * @param {bigint|number} [options.code] The error code to send in the + * CONNECTION_CLOSE frame. Defaults to NO_ERROR (0). + * @param {string} [options.type] Either `'transport'` (default) or + * `'application'`. Determines the error code namespace. + * @param {string} [options.reason] An optional human-readable reason + * string included in the CONNECTION_CLOSE frame (diagnostic only). */ - destroy(error) { - if (this.destroyed) return; + destroy(error, options) { + assertIsQuicSession(this); + const inner = this.#inner; + // Two distinct guards (see also `QuicStream.destroy`): + // * `#destroying` flips synchronously here so any re-entrant call + // (e.g. from a user `onerror` callback or from a cascading + // `stream.destroy(error)` whose own `onerror` re-enters + // `session.destroy()`) hits this guard and returns immediately + // without running the teardown twice. + // * `destroyed` (i.e. `#handle === undefined`) signals + // "fully torn down". Defense-in-depth for paths that may have + // finished teardown without setting `#destroying` and for + // repeat invocations after this method has fully run. + if (inner.destroying || this.destroyed) return; + + if (options !== undefined) options = validateCloseOptions(options); + inner.destroying = true; debug('destroying the session'); + if (error !== undefined) { + if (onSessionErrorChannel.hasSubscribers) { + onSessionErrorChannel.publish({ + __proto__: null, + session: this, + error, + }); + } + if (typeof inner.onerror === 'function') { + invokeOnerror(inner.onerror, error); + } + } + // First, forcefully and immediately destroy all open streams, if any. - for (const stream of this.#streams) { + for (const stream of inner.streams) { stream.destroy(error); } // The streams should remove themselves when they are destroyed but let's // be doubly sure. - if (this.#streams.size) { + if (inner.streams.size) { process.emitWarning( - `The session is destroyed with ${this.#streams.size} active streams. ` + + `The session is destroyed with ${inner.streams.size} active streams. ` + 'This should not happen and indicates a bug in Node.js. Please open an ' + 'issue in the Node.js GitHub repository at https://github.com/nodejs/node ' + 'to report the problem.', ); } - this.#streams.clear(); + inner.streams.clear(); // Remove this session immediately from the endpoint - this.#endpoint[kRemoveSession](this); - this.#endpoint = undefined; - this.#isPendingClose = false; + inner.endpoint[kRemoveSession](this); + inner.endpoint = undefined; + inner.isPendingClose = false; + + // If the handshake never completed, reject the opened promise. The + // session is being destroyed, so the handshake will never complete + // and `await session.opened` would otherwise hang forever. The + // documented contract is that opened rejects when the session is + // destroyed before opening; see the `session.opened` docs in + // doc/api/quic.md. `[kHandshake]` clears `#pendingOpen.reject` once + // the handshake completes successfully, so this branch only runs if + // we are racing against a still-pending handshake. + // + // Mark the rejection as handled before rejecting so that callers who + // never explicitly `await session.opened` do not get an unhandled + // rejection warning - common for server-side sessions delivered via + // `onsession`, which often do not await opened. The rejection is + // still observable via `await session.opened`. + if (inner.pendingOpen.reject) { + markPromiseAsHandled(inner.pendingOpen.promise); + inner.pendingOpen.reject(error ?? new ERR_INVALID_STATE( + 'Session was destroyed before it opened')); + } if (error) { // If the session is still waiting to be closed, and error // is specified, reject the closed promise. - this.#pendingClose.reject?.(error); - this.#pendingOpen.reject?.(error); + inner.pendingClose.reject?.(error); } else { - this.#pendingClose.resolve?.(); + inner.pendingClose.resolve?.(); } - this.#pendingClose.reject = undefined; - this.#pendingClose.resolve = undefined; - this.#pendingOpen.reject = undefined; - this.#pendingOpen.resolve = undefined; - - this.#state[kFinishClose](); - this.#stats[kFinishClose](); - - this.#onstream = undefined; - this.#ondatagram = undefined; - this.#sessionticket = undefined; + inner.pendingClose.reject = undefined; + inner.pendingClose.resolve = undefined; + inner.pendingOpen.reject = undefined; + inner.pendingOpen.resolve = undefined; + + inner.state[kFinishClose](); + inner.stats[kFinishClose](); + + if (this[kPerfEntry] && hasObserver('quic')) { + stopPerf(this, kPerfEntry, { + detail: { + stats: inner.stats, + handshake: inner.handshakeInfo, + path: inner.path, + }, + }); + } - // Destroy the underlying C++ handle - this.#handle.destroy(); + inner.onerror = undefined; + inner.onstream = undefined; + inner.ondatagram = undefined; + inner.ondatagramstatus = undefined; + inner.onpathvalidation = undefined; + inner.onsessionticket = undefined; + inner.onapplication = undefined; + inner.onkeylog = undefined; + inner.onversionnegotiation = undefined; + inner.onhandshake = undefined; + inner.onnewtoken = undefined; + inner.onorigin = undefined; + inner.ongoaway = undefined; + inner.path = undefined; + inner.certificate = undefined; + inner.peerCertificate = undefined; + inner.ephemeralKeyInfo = undefined; + + // Destroy the underlying C++ handle. Pass close error options if + // provided so the CONNECTION_CLOSE frame carries the correct code. + // Note: #onqlog is intentionally NOT cleared here because ngtcp2 + // emits the final qlog statement during ngtcp2_conn destruction, + // and the deferred callback must still be reachable. The reference + // is released when the QuicSession object is garbage collected. + this.#handle.destroy(options); this.#handle = undefined; if (onSessionClosedChannel.hasSubscribers) { onSessionClosedChannel.publish({ + __proto__: null, session: this, error, + stats: inner.stats, + }); + } + } + + /** + * Called when the peer sends a GOAWAY frame (HTTP/3 only). The + * lastStreamId indicates the highest stream ID the peer may have + * processed - streams above it were not processed and may be retried. + * @param {bigint} lastStreamId + */ + [kGoaway](lastStreamId) { + const inner = this.#inner; + inner.isPendingClose = true; + if (onSessionClosingChannel.hasSubscribers) { + onSessionClosingChannel.publish({ __proto__: null, session: this }); + } + if (onSessionGoawayChannel.hasSubscribers) { + onSessionGoawayChannel.publish({ + __proto__: null, + session: this, + lastStreamId, }); } + if (typeof inner.ongoaway === 'function') { + safeCallbackInvoke(inner.ongoaway, this, lastStreamId); + } } /** @@ -1248,7 +3676,7 @@ class QuicSession { * @param {number} code * @param {string} [reason] */ - [kFinishClose](errorType, code, reason) { + [kFinishClose](errorType, code, reason, errorName) { // If code is zero, then we closed without an error. Yay! We can destroy // safely without specifying an error. if (code === 0n) { @@ -1257,54 +3685,89 @@ class QuicSession { return; } - debug('finishing closing the session with an error', errorType, code, reason); + debug('finishing closing the session with an error', + errorType, code, reason, errorName); + + // If the local side initiated this close with an error code (via + // close({ code })), this is an intentional shutdown; not an error. + // The closed promise should resolve, not reject. + if (this.#inner.selfInitiatedClose) { + this.destroy(); + return; + } + // Otherwise, errorType indicates the type of error that occurred, code indicates // the specific error, and reason is an optional string describing the error. + // code !== 0n here (the early return above handles code === 0n). + // The errorType values map to ngtcp2_ccerr_type: + // 0 = NGTCP2_CCERR_TYPE_TRANSPORT + // 1 = NGTCP2_CCERR_TYPE_APPLICATION + // 2 = NGTCP2_CCERR_TYPE_VERSION_NEGOTIATION + // 3 = NGTCP2_CCERR_TYPE_IDLE_CLOSE + // 4 = NGTCP2_CCERR_TYPE_DROP_CONN + // 5 = NGTCP2_CCERR_TYPE_RETRY + // The DROP_CONN/RETRY cases are typically intercepted before reaching + // here (DROP_CONN tears the connection down without notifying us, RETRY + // is server-only). The default branch is a safety net so any + // unexpected value still completes the close path - without it the + // session would leak with `closed` hanging forever. switch (errorType) { case 0: /* Transport Error */ - if (code === 0n) { - this.destroy(); - } else { - this.destroy(new ERR_QUIC_TRANSPORT_ERROR(code, reason)); - } + this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName)); break; case 1: /* Application Error */ - if (code === 0n) { - this.destroy(); - } else { - this.destroy(new ERR_QUIC_APPLICATION_ERROR(code, reason)); - } + this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR', + 'QUIC application error', + 'application', code, reason, errorName)); break; case 2: /* Version Negotiation Error */ this.destroy(new ERR_QUIC_VERSION_NEGOTIATION_ERROR()); break; - case 3: /* Idle close */ { - // An idle close is not really an error. We can just destroy. + case 3: /* Idle close */ this.destroy(); break; - } + default: + this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName)); + break; } } + [kKeylog](line) { + const inner = this.#inner; + if (this.destroyed || inner.onkeylog === undefined) return; + safeCallbackInvoke(inner.onkeylog, this, line); + } + + [kQlog](data, fin) { + const inner = this.#inner; + if (inner.onqlog === undefined) return; + safeCallbackInvoke(inner.onqlog, this, data, fin); + } + /** - * @param {Uint8Array} u8 - * @param {boolean} early + * @param {Uint8Array} u8 The datagram payload + * @param {boolean} early A boolean indicating whether this datagram was received before the handshake completed */ [kDatagram](u8, early) { - // The datagram event should only be called if the session was created with + // The datagram event should only be called if the session has // an ondatagram callback. The callback should always exist here. - assert(this.#ondatagram, 'Unexpected datagram event'); + const inner = this.#inner; + assert(typeof inner.ondatagram === 'function', 'Unexpected datagram event'); if (this.destroyed) return; - const length = u8.byteLength; - this.#ondatagram(u8, early); - + const length = TypedArrayPrototypeGetByteLength(u8); if (onSessionReceiveDatagramChannel.hasSubscribers) { onSessionReceiveDatagramChannel.publish({ + __proto__: null, length, early, session: this, }); } + safeCallbackInvoke(inner.ondatagram, this, u8, early); } /** @@ -1312,14 +3775,20 @@ class QuicSession { * @param {'lost'|'acknowledged'} status */ [kDatagramStatus](id, status) { + const inner = this.#inner; + // The datagram status event should only be called if the session has + // an ondatagramstatus callback. The callback should always exist here. + assert(typeof inner.ondatagramstatus === 'function', 'Unexpected datagram status event'); if (this.destroyed) return; if (onSessionReceiveDatagramStatusChannel.hasSubscribers) { onSessionReceiveDatagramStatusChannel.publish({ + __proto__: null, id, status, session: this, }); } + safeCallbackInvoke(inner.ondatagramstatus, this, id, status); } /** @@ -1332,32 +3801,100 @@ class QuicSession { */ [kPathValidation](result, newLocalAddress, newRemoteAddress, oldLocalAddress, oldRemoteAddress, preferredAddress) { + const inner = this.#inner; + assert(typeof inner.onpathvalidation === 'function', + 'Unexpected path validation event'); if (this.destroyed) return; + const newLocal = new InternalSocketAddress(newLocalAddress); + const newRemote = new InternalSocketAddress(newRemoteAddress); + const oldLocal = oldLocalAddress !== undefined ? + new InternalSocketAddress(oldLocalAddress) : null; + const oldRemote = oldRemoteAddress !== undefined ? + new InternalSocketAddress(oldRemoteAddress) : null; if (onSessionPathValidationChannel.hasSubscribers) { onSessionPathValidationChannel.publish({ + __proto__: null, result, - newLocalAddress, - newRemoteAddress, - oldLocalAddress, - oldRemoteAddress, + newLocalAddress: newLocal, + newRemoteAddress: newRemote, + oldLocalAddress: oldLocal, + oldRemoteAddress: oldRemote, preferredAddress, session: this, }); } + safeCallbackInvoke(inner.onpathvalidation, this, result, newLocal, newRemote, + oldLocal, oldRemote, preferredAddress); } /** * @param {object} ticket */ [kSessionTicket](ticket) { + const inner = this.#inner; + assert(typeof inner.onsessionticket === 'function', + 'Unexpected session ticket event'); if (this.destroyed) return; - this.#sessionticket = ticket; if (onSessionTicketChannel.hasSubscribers) { onSessionTicketChannel.publish({ + __proto__: null, ticket, session: this, }); } + safeCallbackInvoke(inner.onsessionticket, this, ticket); + } + + /** + * @param {ApplicationOptions} applicationoptions + */ + [kSessionApplication](applicationoptions) { + if (this.destroyed) return; + if (onSessionApplicationChannel.hasSubscribers) { + onSessionApplicationChannel.publish({ + __proto__: null, + applicationoptions, + session: this, + }); + } + const inner = this.#inner; + if (typeof inner.onapplication === 'function') + safeCallbackInvoke(inner.onapplication, this, applicationoptions); + } + + /** + * @param {Buffer} token + * @param {SocketAddress} address + */ + [kNewToken](token, address) { + const inner = this.#inner; + assert(typeof inner.onnewtoken === 'function', + 'Unexpected new token event'); + if (this.destroyed) return; + const addr = new InternalSocketAddress(address); + if (onSessionNewTokenChannel.hasSubscribers) { + onSessionNewTokenChannel.publish({ + __proto__: null, + token, + address: addr, + session: this, + }); + } + safeCallbackInvoke(inner.onnewtoken, this, token, addr); + } + + [kEarlyDataRejected]() { + if (this.destroyed) return; + if (onSessionEarlyRejectedChannel.hasSubscribers) { + onSessionEarlyRejectedChannel.publish({ + __proto__: null, + session: this, + }); + } + const inner = this.#inner; + if (typeof inner.onearlyrejected === 'function') { + safeCallbackInvoke(inner.onearlyrejected, this); + } } /** @@ -1367,15 +3904,41 @@ class QuicSession { */ [kVersionNegotiation](version, requestedVersions, supportedVersions) { if (this.destroyed) return; - this.destroy(new ERR_QUIC_VERSION_NEGOTIATION_ERROR()); if (onSessionVersionNegotiationChannel.hasSubscribers) { onSessionVersionNegotiationChannel.publish({ + __proto__: null, version, requestedVersions, supportedVersions, session: this, }); } + const inner = this.#inner; + if (typeof inner.onversionnegotiation === 'function') { + safeCallbackInvoke(inner.onversionnegotiation, this, + version, requestedVersions, supportedVersions); + } + // Version negotiation is always a fatal event - the session must be + // destroyed regardless of whether the callback is set. + this.destroy(new ERR_QUIC_VERSION_NEGOTIATION_ERROR()); + } + + /** + * Called when the session receives an ORIGIN frame (RFC 9412). + * @param {string[]} origins + */ + [kOrigin](origins) { + if (this.destroyed) return; + const inner = this.#inner; + assert(typeof inner.onorigin === 'function', 'Unexpected origin event'); + if (onSessionOriginChannel.hasSubscribers) { + onSessionOriginChannel.publish({ + __proto__: null, + origins, + session: this, + }); + } + safeCallbackInvoke(inner.onorigin, this, origins); } /** @@ -1387,13 +3950,16 @@ class QuicSession { * @param {number} validationErrorCode */ [kHandshake](servername, protocol, cipher, cipherVersion, validationErrorReason, - validationErrorCode) { - if (this.destroyed || !this.#pendingOpen.resolve) return; + validationErrorCode, earlyDataAttempted, earlyDataAccepted) { + const inner = this.#inner; + if (this.destroyed || !inner.pendingOpen.resolve) return; + const addr = this.#handle.getRemoteAddress(); const info = { - local: this.#endpoint.address, + __proto__: null, + local: inner.endpoint.address, remote: addr !== undefined ? new InternalSocketAddress(addr) : undefined, @@ -1403,18 +3969,68 @@ class QuicSession { cipherVersion, validationErrorReason, validationErrorCode, + earlyDataAttempted, + earlyDataAccepted, }; - this.#pendingOpen.resolve?.(info); - this.#pendingOpen.resolve = undefined; - this.#pendingOpen.reject = undefined; + // Stash timing-relevant handshake info for the perf entry detail. + inner.handshakeInfo = { + __proto__: null, + servername, + protocol, + earlyDataAttempted, + earlyDataAccepted, + }; if (onSessionHandshakeChannel.hasSubscribers) { onSessionHandshakeChannel.publish({ + __proto__: null, session: this, ...info, }); } + + if (typeof inner.onhandshake === 'function') { + safeCallbackInvoke(inner.onhandshake, this, info); + } + + // In 'auto' mode, reject the connection if peer certificate validation + // failed. In 'manual' mode, resolve regardless and let the application + // decide. In 'strict' mode, the handshake already failed at the C++ + // level (SSL_VERIFY_PEER) so we won't reach here. + if (inner.verifyPeer === 'auto' && validationErrorReason !== undefined) { + const err = makeQuicError( + 'ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', + 0n, + `Peer certificate validation failed: ${validationErrorReason}` + + ` [${validationErrorCode}]`); + inner.pendingOpen.reject?.(err); + inner.pendingOpen.resolve = undefined; + inner.pendingOpen.reject = undefined; + inner.handshakeCompleted = true; + this.destroy(); + return; + } + + inner.pendingOpen.resolve?.(info); + inner.pendingOpen.resolve = undefined; + inner.pendingOpen.reject = undefined; + inner.handshakeCompleted = true; + } + + /** @type {boolean} */ + get [kHandshakeCompleted]() { + return this.#inner.handshakeCompleted; + } + + get [kVerifyPeer]() { + return this.#inner.verifyPeer; + } + + set [kVerifyPeer](value) { + this.#inner.verifyPeer = value; } /** @@ -1422,49 +4038,83 @@ class QuicSession { * @param {number} direction */ [kNewStream](handle, direction) { - const stream = new QuicStream(kPrivateConstructor, handle, this, direction); + const inner = this.#inner; + const stream = new QuicStream(kPrivateConstructor, handle, this, direction, + false /* isLocal */); + + // Set the default high water mark for received streams. + stream.highWaterMark = kDefaultHighWaterMark; // A new stream was received. If we don't have an onstream callback, then // there's nothing we can do about it. Destroy the stream in this case. - if (typeof this.#onstream !== 'function') { + if (typeof inner.onstream !== 'function') { process.emitWarning('A new stream was received but no onstream callback was provided'); stream.destroy(); return; } - this.#streams.add(stream); - this.#onstream(stream); + inner.streams.add(stream); + // If the session has an onerror handler, mark the stream's closed + // promise as handled. See the onerror setter for explanation. + if (typeof inner.onerror === 'function') { + markPromiseAsHandled(stream.closed); + } + + // Apply default stream callbacks set at listen time before + // notifying onstream, so the user sees them already set. + const scbs = this[kStreamCallbacks]; + if (scbs) { + if (scbs.onheaders) stream.onheaders = scbs.onheaders; + if (scbs.ontrailers) stream.ontrailers = scbs.ontrailers; + if (scbs.oninfo) stream.oninfo = scbs.oninfo; + if (scbs.onwanttrailers) stream.onwanttrailers = scbs.onwanttrailers; + } if (onSessionReceivedStreamChannel.hasSubscribers) { onSessionReceivedStreamChannel.publish({ + __proto__: null, stream, session: this, + direction: direction === kStreamDirectionBidirectional ? 'bidi' : 'uni', }); } + + safeCallbackInvoke(inner.onstream, this, stream); } [kRemoveStream](stream) { - this.#streams.delete(stream); + this.#inner.streams.delete(stream); } [kInspect](depth, options) { - if (depth < 0) - return this; + if (depth < 0) { + return 'QuicSession { }'; + } const opts = { + __proto__: null, ...options, depth: options.depth == null ? null : options.depth - 1, }; + const { + isPendingClose: closing, + endpoint, + path, + state, + stats, + streams, + } = this.#inner; + return `QuicSession ${inspect({ closed: this.closed, - closing: this.#isPendingClose, + closing, destroyed: this.destroyed, - endpoint: this.endpoint, - path: this.path, - state: this.#state, - stats: this.stats, - streams: this.#streams, + endpoint, + path, + state, + stats, + streams, }, opts)}`; } @@ -1476,68 +4126,50 @@ class QuicSession { // local UDP port will be lazily bound only when connect() or listen() are // called. class QuicEndpoint { - /** - * The local socket address on which the endpoint is listening (lazily created) - * @type {SocketAddress|undefined} - */ - #address = undefined; - /** - * When true, the endpoint has been marked busy and is temporarily not accepting - * new sessions (only used when the Endpoint is acting as a server) - * @type {boolean} - */ - #busy = false; - /** - * The underlying C++ handle for the endpoint. When undefined the endpoint is - * considered to be closed. - * @type {object} - */ #handle; - /** - * True if endpoint.close() has been called and the [kFinishClose] method has - * not yet been called. - * @type {boolean} - */ - #isPendingClose = false; - /** - * True if the endpoint is acting as a server and actively listening for connections. - * @type {boolean} - */ - #listening = false; - /** - * A promise that is resolved when the endpoint has been closed (or rejected if - * the endpoint closes abruptly due to an error). - * @type {PromiseWithResolvers} - */ - #pendingClose = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials - /** - * If destroy() is called with an error, the error is stored here and used to reject - * the pendingClose promise when [kFinishClose] is called. - * @type {any} - */ - #pendingError = undefined; - /** - * The collection of active sessions. - * @type {Set} - */ - #sessions = new SafeSet(); - /** - * The internal state of the endpoint. Used to efficiently track and update the - * state of the underlying c++ endpoint handle. - * @type {QuicEndpointState} - */ - #state; - /** - * The collected statistics for the endpoint. - * @type {QuicEndpointStats} - */ - #stats; - /** - * The user provided callback that is invoked when a new session is received. - * (used only when the endpoint is acting as a server) - * @type {OnSessionCallback} - */ - #onsession = undefined; + #inner = { + __proto__: null, + address: undefined, + busy: false, + isPendingClose: false, + listening: false, + pendingClose: PromiseWithResolvers(), + pendingError: undefined, + sessions: new SafeSet(), + stat: undefined, + stats: undefined, + onsession: undefined, + sessionCallbacks: undefined, + }; + + static { + isQuicEndpoint = function(val) { + return val != null && typeof val === 'object' && #handle in val; + }; + + assertIsQuicEndpoint = function(val) { + if (!isQuicEndpoint(val)) { + throw new ERR_INVALID_THIS('QuicEndpoint'); + } + }; + + getQuicEndpointState = function(endpoint) { + assertIsQuicEndpoint(endpoint); + return endpoint.#inner.state; + }; + + assertEndpointNotClosedOrClosing = function(endpoint) { + if (endpoint.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Endpoint is closed'); + } + }; + + assertEndpointIsNotBusy = function(endpoint) { + if (endpoint.#inner.state.isBusy) { + throw new ERR_INVALID_STATE('Endpoint is busy'); + } + }; + } /** * @param {EndpointOptions} options @@ -1549,16 +4181,28 @@ class QuicEndpoint { const { retryTokenExpiration, tokenExpiration, - maxConnectionsPerHost, - maxConnectionsTotal, - maxStatelessResetsPerHost, + maxConnectionsPerHost = 100, + maxConnectionsTotal = 10_000, + disableStatelessReset, addressLRUSize, - maxRetries, + retryRate, + retryBurst, + statelessResetRate, + statelessResetBurst, + versionNegotiationRate, + versionNegotiationBurst, + immediateCloseRate, + immediateCloseBurst, + sessionCreationRate, + sessionCreationBurst, + blockList, + blockListPolicy = 'deny', rxDiagnosticLoss, txDiagnosticLoss, udpReceiveBufferSize, udpSendBufferSize, udpTTL, + idleTimeout, validateAddress, ipv6Only, reusePort, @@ -1567,6 +4211,16 @@ class QuicEndpoint { tokenSecret, } = options; + if (blockList !== undefined) { + if (!BlockList.isBlockList(blockList)) { + throw new ERR_INVALID_ARG_TYPE('options.blockList', + 'net.BlockList', blockList); + } + } + + validateOneOf(blockListPolicy, 'options.blockListPolicy', + ['deny', 'allow']); + // All of the other options will be validated internally by the C++ code if (address !== undefined && !SocketAddress.isSocketAddress(address)) { if (typeof address === 'string') { @@ -1583,16 +4237,30 @@ class QuicEndpoint { address: address?.[kSocketAddressHandle], retryTokenExpiration, tokenExpiration, + // Connection limits are set on the state buffer, not passed to C++. maxConnectionsPerHost, maxConnectionsTotal, - maxStatelessResetsPerHost, + disableStatelessReset, addressLRUSize, - maxRetries, + retryRate, + retryBurst, + statelessResetRate, + statelessResetBurst, + versionNegotiationRate, + versionNegotiationBurst, + immediateCloseRate, + immediateCloseBurst, + sessionCreationRate, + sessionCreationBurst, + // Pass the C++ handle, not the JS BlockList wrapper. + blockList: blockList?.[kBlockListHandle], + blockListPolicy, rxDiagnosticLoss, txDiagnosticLoss, udpReceiveBufferSize, udpSendBufferSize, udpTTL, + idleTimeout, validateAddress, ipv6Only, reusePort, @@ -1604,7 +4272,9 @@ class QuicEndpoint { #newSession(handle) { const session = new QuicSession(kPrivateConstructor, handle, this); - this.#sessions.add(session); + this.#inner.sessions.add(session); + // Set default pending datagram queue size. + session.maxPendingDatagrams = kDefaultMaxPendingDatagrams; return session; } @@ -1612,13 +4282,32 @@ class QuicEndpoint { * @param {EndpointOptions} config */ constructor(config = kEmptyObject) { - this.#handle = new Endpoint_(this.#processEndpointOptions(config)); + const options = this.#processEndpointOptions(config); + this.#handle = new Endpoint_(options); this.#handle[kOwner] = this; - this.#stats = new QuicEndpointStats(kPrivateConstructor, this.#handle.stats); - this.#state = new QuicEndpointState(kPrivateConstructor, this.#handle.state); + const inner = this.#inner; + inner.stats = new QuicEndpointStats(kPrivateConstructor, this.#handle.stats); + inner.state = new QuicEndpointState(kPrivateConstructor, this.#handle.state); + + // Connection limits are stored in the shared state buffer so they + // can be read by C++ and mutated from JS after construction. + // Use the public setters which validate the range. + if (options.maxConnectionsPerHost !== undefined) { + this.maxConnectionsPerHost = options.maxConnectionsPerHost; + } + if (options.maxConnectionsTotal !== undefined) { + this.maxConnectionsTotal = options.maxConnectionsTotal; + } + + endpointRegistry.add(this); + + if (hasObserver('quic')) { + startPerf(this, kPerfEntry, { type: 'quic', name: 'QuicEndpoint' }); + } if (onEndpointCreatedChannel.hasSubscribers) { onEndpointCreatedChannel.publish({ + __proto__: null, endpoint: this, config, }); @@ -1631,13 +4320,13 @@ class QuicEndpoint { * Statistics collected while the endpoint is operational. * @type {QuicEndpointStats} */ - get stats() { return this.#stats; } - - /** @type {QuicEndpointState} */ - get [kState]() { return this.#state; } + get stats() { + assertIsQuicEndpoint(this); + return this.#inner.stats; + } get #isClosedOrClosing() { - return this.destroyed || this.#isPendingClose; + return this.destroyed || this.#inner.isPendingClose; } /** @@ -1645,41 +4334,78 @@ class QuicEndpoint { * Existing connections will continue to work. * @type {boolean} */ - get busy() { return this.#busy; } + get busy() { + assertIsQuicEndpoint(this); + return this.#inner.busy; + } /** * @type {boolean} */ set busy(val) { - if (this.#isClosedOrClosing) { - throw new ERR_INVALID_STATE('Endpoint is closed'); - } + assertIsQuicEndpoint(this); + assertEndpointNotClosedOrClosing(this); // The val is allowed to be any truthy value // Non-op if there is no change - if (!!val !== this.#busy) { - debug('toggling endpoint busy status to ', !this.#busy); - this.#busy = !this.#busy; - this.#handle.markBusy(this.#busy); + const inner = this.#inner; + if (!!val !== inner.busy) { + debug('toggling endpoint busy status to ', !inner.busy); + inner.busy = !inner.busy; + this.#handle.markBusy(inner.busy); if (onEndpointBusyChangeChannel.hasSubscribers) { onEndpointBusyChangeChannel.publish({ + __proto__: null, endpoint: this, - busy: this.#busy, + busy: inner.busy, }); } } } + /** + * Maximum concurrent connections per remote IP address. + * 0 means unlimited (default). + * @type {number} + */ + get maxConnectionsPerHost() { + assertIsQuicEndpoint(this); + return this.#inner.state.maxConnectionsPerHost; + } + + set maxConnectionsPerHost(val) { + assertIsQuicEndpoint(this); + validateInteger(val, 'maxConnectionsPerHost', 0, 0xFFFF); + this.#inner.state.maxConnectionsPerHost = val; + } + + /** + * Maximum total concurrent connections. + * 0 means unlimited (default). + * @type {number} + */ + get maxConnectionsTotal() { + assertIsQuicEndpoint(this); + return this.#inner.state.maxConnectionsTotal; + } + + set maxConnectionsTotal(val) { + assertIsQuicEndpoint(this); + validateInteger(val, 'maxConnectionsTotal', 0, 0xFFFF); + this.#inner.state.maxConnectionsTotal = val; + } + /** * The local address the endpoint is bound to (if any) * @type {SocketAddress|undefined} */ get address() { + assertIsQuicEndpoint(this); if (this.#isClosedOrClosing) return undefined; - if (this.#address === undefined) { + if (this.#inner.address === undefined) { const addr = this.#handle.address(); - if (addr !== undefined) this.#address = new InternalSocketAddress(addr); + if (addr !== undefined) this.#inner.address = new InternalSocketAddress(addr); } - return this.#address; + return this.#inner.address; } /** @@ -1688,21 +4414,67 @@ class QuicEndpoint { * @param {SessionOptions} [options] */ [kListen](onsession, options) { - if (this.#isClosedOrClosing) { - throw new ERR_INVALID_STATE('Endpoint is closed'); - } - if (this.#listening) { + assertEndpointNotClosedOrClosing(this); + assertEndpointIsNotBusy(this); + const inner = this.#inner; + if (inner.listening) { throw new ERR_INVALID_STATE('Endpoint is already listening'); } - if (this.#state.isBusy) { - throw new ERR_INVALID_STATE('Endpoint is busy'); - } validateObject(options, 'options'); - this.#onsession = onsession.bind(this); + validateFunction(onsession, 'onsession'); + this.#inner.onsession = FunctionPrototypeBind(onsession, this); + + const { + onerror, + onstream, + ondatagram, + ondatagramstatus, + onpathvalidation, + onsessionticket, + onversionnegotiation, + onhandshake, + onnewtoken, + onearlyrejected, + onorigin, + ongoaway, + onkeylog, + onqlog, + onapplication, + // Stream-level callbacks applied to each incoming stream. + onheaders, + ontrailers, + oninfo, + onwanttrailers, + ...rest + } = options; + + // Store session and stream callbacks to apply to each new incoming session. + inner.sessionCallbacks = { + __proto__: null, + onerror, + onstream, + ondatagram, + ondatagramstatus, + onpathvalidation, + onsessionticket, + onversionnegotiation, + onhandshake, + onnewtoken, + onearlyrejected, + onorigin, + ongoaway, + onkeylog, + onqlog, + onapplication, + onheaders, + ontrailers, + oninfo, + onwanttrailers, + }; + this.#handle.listen(rest); + inner.listening = true; debug('endpoint listening as a server'); - this.#handle.listen(options); - this.#listening = true; } /** @@ -1712,14 +4484,13 @@ class QuicEndpoint { * @returns {QuicSession} */ [kConnect](address, options) { - if (this.#isClosedOrClosing) { - throw new ERR_INVALID_STATE('Endpoint is closed'); - } - if (this.#state.isBusy) { - throw new ERR_INVALID_STATE('Endpoint is busy'); - } + assertEndpointNotClosedOrClosing(this); + assertEndpointIsNotBusy(this); validateObject(options, 'options'); - const { sessionTicket, ...rest } = options; + const { + sessionTicket, + ...rest + } = options; debug('endpoint connecting as a client'); const handle = this.#handle.connect(address, rest, sessionTicket); @@ -1727,7 +4498,13 @@ class QuicEndpoint { throw new ERR_QUIC_CONNECTION_FAILED(); } const session = this.#newSession(handle); - + // Set callbacks before any async work to avoid missing events + // that fire during or immediately after the handshake. + applyCallbacks(session, options); + // Store the verifyPeer policy for use in the handshake handler. + if (options.verifyPeer !== undefined) { + session[kVerifyPeer] = options.verifyPeer; + } return session; } @@ -1740,18 +4517,19 @@ class QuicEndpoint { * @returns {Promise} Returns this.closed */ close() { + assertIsQuicEndpoint(this); if (!this.#isClosedOrClosing) { + debug('gracefully closing the endpoint'); + const inner = this.#inner; + inner.isPendingClose = true; + this.#handle.closeGracefully(); if (onEndpointClosingChannel.hasSubscribers) { onEndpointClosingChannel.publish({ + __proto__: null, endpoint: this, - hasPendingError: this.#pendingError !== undefined, + hasPendingError: inner.pendingError !== undefined, }); } - this.#isPendingClose = true; - - debug('gracefully closing the endpoint'); - - this.#handle?.closeGracefully(); } return this.closed; } @@ -1762,20 +4540,29 @@ class QuicEndpoint { * is set to the same promise that is returned by the close() method. * @type {Promise} */ - get closed() { return this.#pendingClose.promise; } + get closed() { + assertIsQuicEndpoint(this); + return this.#inner.pendingClose.promise; + } /** + * True if the endpoint is pending close. * @type {boolean} */ - get closing() { return this.#isPendingClose; } + get closing() { + assertIsQuicEndpoint(this); + return this.#inner.isPendingClose; + } /** @type {boolean} */ get listening() { - return this.#listening; + assertIsQuicEndpoint(this); + return this.#inner.listening; } /** @type {boolean} */ get destroyed() { + assertIsQuicEndpoint(this); return this.#handle === undefined; } @@ -1788,20 +4575,53 @@ class QuicEndpoint { * @returns {Promise} Returns this.closed */ destroy(error) { + assertIsQuicEndpoint(this); debug('destroying the endpoint'); + const inner = this.#inner; + // Record the error before deciding whether to initiate a close. If + // `close()` was already called (e.g. the user kicked off a graceful + // shutdown and then a fatal error was reported afterwards via + // `destroy(err)`) we still want that error to surface on + // `endpoint.closed` rather than being silently swallowed when the + // last in-flight session finishes draining. Only the *first* error + // is recorded, matching how other Node subsystems handle a + // double-error race. + if (error !== undefined) inner.pendingError ??= error; + // Force all sessions to be abruptly closed *before* signalling the + // endpoint to close gracefully. The order matters: each session's + // `destroy(error, options)` asks the C++ side to emit a + // `CONNECTION_CLOSE` frame via `endpoint.Send(...)`. Once the + // endpoint has entered its closing state (after `close()`) it + // can drop those outgoing packets, in which case the peer would + // never learn of the teardown until its own idle timer fires + // (pimterry's B8). + // + // Important: only pass close options to sessions whose handshake + // has actually completed. Pre-handshake sessions cannot create a + // valid CONNECTION_CLOSE packet on the C++ side; the fallback + // synchronously fires `EmitClose` -> JS `[kFinishClose]` -> + // `destroy()`, which trips the `#destroying` guard and leaves the + // C++ side asserting an inconsistent destroyed state. + const closeOptions = errorToCloseOptions(error); + for (const session of inner.sessions) { + // Mark each cascaded session's `closed` as handled before + // destroying it. This prevents unhandled-rejection warnings when + // the session is collateral damage from an endpoint-level destroy + // (e.g. a synchronous throw out of a user `onsession` callback + // routed through safeCallbackInvoke). The rejection is still + // observable to any caller that explicitly awaits `session.closed`. + markPromiseAsHandled(session.closed); + session.destroy( + error, + session[kHandshakeCompleted] ? closeOptions : undefined); + } if (!this.#isClosedOrClosing) { - this.#pendingError = error; // Trigger a graceful close of the endpoint that'll ensure that the - // endpoint is closed down after all sessions are closed... Because - // we force all sessions to be abruptly destroyed as the next step, - // the endpoint will be closed immediately after all the sessions - // are destroyed. + // endpoint is closed down after all sessions are closed... All + // sessions were just forcefully destroyed above, so this should + // resolve promptly with nothing left to drain. this.close(); } - // Now, force all sessions to be abruptly closed... - for (const session of this.#sessions) { - session.destroy(error); - } return this.closed; } @@ -1811,9 +4631,10 @@ class QuicEndpoint { * replace is true, the entire SNI map is replaced. Otherwise, the * provided entries are merged into the existing map. * @param {object} entries - * @param {{replace?: boolean}} [options] + * @param {SNIContextOptions} [options] */ setSNIContexts(entries, options = kEmptyObject) { + assertIsQuicEndpoint(this); if (this.#handle === undefined) { throw new ERR_INVALID_STATE('Endpoint is destroyed'); } @@ -1840,166 +4661,218 @@ class QuicEndpoint { this.#handle.setSNIContexts(processed, replace); } - #maybeGetCloseError(context, status) { - switch (context) { - case kCloseContextClose: { - return this.#pendingError; - } - case kCloseContextBindFailure: { - return new ERR_QUIC_ENDPOINT_CLOSED('Bind failure', status); - } - case kCloseContextListenFailure: { - return new ERR_QUIC_ENDPOINT_CLOSED('Listen failure', status); - } - case kCloseContextReceiveFailure: { - return new ERR_QUIC_ENDPOINT_CLOSED('Receive failure', status); - } - case kCloseContextSendFailure: { - return new ERR_QUIC_ENDPOINT_CLOSED('Send failure', status); - } - case kCloseContextStartFailure: { - return new ERR_QUIC_ENDPOINT_CLOSED('Start failure', status); - } - } - // Otherwise return undefined. - } - [kFinishClose](context, status) { if (this.#handle === undefined) return; debug('endpoint is finishing close', context, status); + endpointRegistry.delete(this); this.#handle = undefined; - this.#stats[kFinishClose](); - this.#state[kFinishClose](); - this.#address = undefined; - this.#busy = false; - this.#listening = false; - this.#isPendingClose = false; + const inner = this.#inner; + inner.stats[kFinishClose](); + inner.state[kFinishClose](); + if (this[kPerfEntry] && hasObserver('quic')) { + stopPerf(this, kPerfEntry, { + detail: { stats: this.stats }, + }); + } + inner.address = undefined; + inner.busy = false; + inner.listening = false; + inner.isPendingClose = false; // As QuicSessions are closed they are expected to remove themselves // from the sessions collection. Just in case they don't, let's force // it by resetting the set so we don't leak memory. Let's emit a warning, // tho, if the set is not empty at this point as that would indicate a // bug in Node.js that should be fixed. - if (this.#sessions.size > 0) { + if (inner.sessions.size > 0) { process.emitWarning( - `The endpoint is closed with ${this.#sessions.size} active sessions. ` + + `The endpoint is closed with ${inner.sessions.size} active sessions. ` + 'This should not happen and indicates a bug in Node.js. Please open an ' + 'issue in the Node.js GitHub repository at https://github.com/nodejs/node ' + 'to report the problem.', ); } - this.#sessions.clear(); + inner.sessions.clear(); // If destroy was called with an error, then the this.#pendingError will be // set. Or, if context indicates an error condition that caused the endpoint // to be closed, the status will indicate the error code. In either case, // we will reject the pending close promise at this point. - const maybeCloseError = this.#maybeGetCloseError(context, status); + const maybeCloseError = maybeGetCloseError(context, status, inner.pendingError); if (maybeCloseError !== undefined) { if (onEndpointErrorChannel.hasSubscribers) { onEndpointErrorChannel.publish({ + __proto__: null, endpoint: this, error: maybeCloseError, }); } - this.#pendingClose.reject(maybeCloseError); + inner.pendingClose.reject(maybeCloseError); } else { // Otherwise we are good to resolve the pending close promise! - this.#pendingClose.resolve(); + inner.pendingClose.resolve(); } if (onEndpointClosedChannel.hasSubscribers) { onEndpointClosedChannel.publish({ + __proto__: null, endpoint: this, + stats: inner.stats, }); } // Note that we are intentionally not clearing the // this.#pendingClose.promise here. - this.#pendingClose.resolve = undefined; - this.#pendingClose.reject = undefined; - this.#pendingError = undefined; + inner.pendingClose.resolve = undefined; + inner.pendingClose.reject = undefined; + inner.pendingError = undefined; } [kNewSession](handle) { + const inner = this.#inner; + assert(typeof inner.onsession === 'function', + 'onsession callback not specified'); const session = this.#newSession(handle); + // Apply session callbacks stored at listen time before notifying + // the onsession callback, to avoid missing events that fire + // during or immediately after the handshake. + if (inner.sessionCallbacks) { + applyCallbacks(session, inner.sessionCallbacks); + } if (onEndpointServerSessionChannel.hasSubscribers) { onEndpointServerSessionChannel.publish({ + __proto__: null, endpoint: this, session, + address: session.path?.remote, }); } - assert(typeof this.#onsession === 'function', - 'onsession callback not specified'); - this.#onsession(session); + // Route through safeCallbackInvoke so that a synchronous throw or a + // rejected promise from the user's onsession callback destroys this + // endpoint with the error rather than surfacing as an unhandled + // exception or unhandled rejection coming out of the C++ -> JS + // boundary. + safeCallbackInvoke(inner.onsession, this, session); } // Called by the QuicSession when it closes to remove itself from // the active sessions tracked by the QuicEndpoint. [kRemoveSession](session) { - this.#sessions.delete(session); + this.#inner.sessions.delete(session); } - async [SymbolAsyncDispose]() { await this.close(); } - [kInspect](depth, options) { - if (depth < 0) - return this; + if (depth < 0) { + return 'QuicEndpoint { }'; + } const opts = { + __proto__: null, ...options, depth: options.depth == null ? null : options.depth - 1, }; + const { + address, + busy, + isPendingClose: closing, + listening, + sessions, + stats, + state, + } = this.#inner; + return `QuicEndpoint ${inspect({ - address: this.address, - busy: this.busy, + address, + busy, closed: this.closed, - closing: this.#isPendingClose, + closing, destroyed: this.destroyed, - listening: this.#listening, - sessions: this.#sessions, - stats: this.stats, - state: this.#state, + listening, + sessions, + stats, + state, }, opts)}`; } + + async [SymbolAsyncDispose]() { await this.close(); } }; -function readOnlyConstant(value) { - return { - __proto__: null, - value, - writable: false, - configurable: false, - enumerable: true, - }; +/** + * Find an existing endpoint from the registry that is suitable for reuse. + * @param {SocketAddress} [targetAddress] The address the client will connect + * to. If provided, endpoints that are listening on that same address are + * excluded to prevent CID namespace collisions (the client's initial DCID + * association would conflict with the server's session routing on the + * same endpoint). + * @returns {QuicEndpoint|undefined} + */ +function findSuitableEndpoint(targetAddress) { + for (const endpoint of endpointRegistry) { + if (!endpoint.destroyed && + !endpoint.closing && + !endpoint.busy) { + // Don't reuse an endpoint for a connection to itself. + if (targetAddress && endpoint.listening && endpoint.address && + targetAddress.address === endpoint.address.address && + targetAddress.port === endpoint.address.port) { + continue; + } + return endpoint; + } + } + return undefined; } /** - * @param {EndpointOptions} endpoint - * @returns {{ endpoint: Endpoint_, created: boolean }} + * Returns a list of all active endpoints. + * @param {object} [options] + * @param {boolean} [options.active] When true, only return endpoints that are not destroyed, closing, or busy. + * @returns {QuicEndpoint[]} */ -function processEndpointOption(endpoint) { - if (endpoint === undefined) { - return { - endpoint: new QuicEndpoint(), - created: true, - }; - } else if (endpoint instanceof QuicEndpoint) { - return { - endpoint, - created: false, - }; +function listEndpoints(options = kEmptyObject) { + validateObject(options, 'options'); + const { active = true } = options; + validateBoolean(active, 'options.active'); + if (!active) { + return ArrayFrom(endpointRegistry); } - validateObject(endpoint, 'options.endpoint'); - return { - endpoint: new QuicEndpoint(endpoint), - created: true, - }; + return ArrayFrom(endpointRegistry).filter((endpoint) => { + return !endpoint.destroyed && !endpoint.closing && !endpoint.busy; + }); +} + +/** + * @param {EndpointOptions|QuicEndpoint|undefined} endpoint + * @param {boolean} reuseEndpoint + * @param {boolean} forServer + * @param {SocketAddress} [targetAddress] + * @returns {QuicEndpoint} + */ +function processEndpointOption(endpoint, + reuseEndpoint = true, + forServer = false, + targetAddress) { + if (isQuicEndpoint(endpoint)) { + // We were given an existing endpoint. Use it as-is. + return endpoint; + } + if (endpoint !== undefined) { + // We were given endpoint options. If reuse is enabled, we could + // look for a matching endpoint, but endpoint options imply the + // caller wants specific configuration. Create a new one. + return new QuicEndpoint(endpoint); + } + // No endpoint specified. Try to reuse an existing one if allowed. + if (reuseEndpoint && !forServer) { + const existing = findSuitableEndpoint(targetAddress); + if (existing !== undefined) return existing; + } + return new QuicEndpoint(); } /** - * Validate and extract identity options (keys, certs, ca, crl) from - * an SNI entry. + * Validate and extract identity options (keys, certs) from an SNI entry. + * CA and CRL are shared TLS options, not per-identity. * @param {object} identity * @param {string} label * @returns {object} @@ -2008,8 +4881,6 @@ function processIdentityOptions(identity, label) { const { keys, certs, - ca, - crl, verifyPrivateKey = false, } = identity; @@ -2023,26 +4894,6 @@ function processIdentityOptions(identity, label) { } } - if (ca !== undefined) { - const caInputs = ArrayIsArray(ca) ? ca : [ca]; - for (const caCert of caInputs) { - if (!isArrayBufferView(caCert) && !isArrayBuffer(caCert)) { - throw new ERR_INVALID_ARG_TYPE(`${label}.ca`, - ['ArrayBufferView', 'ArrayBuffer'], caCert); - } - } - } - - if (crl !== undefined) { - const crlInputs = ArrayIsArray(crl) ? crl : [crl]; - for (const crlCert of crlInputs) { - if (!isArrayBufferView(crlCert) && !isArrayBuffer(crlCert)) { - throw new ERR_INVALID_ARG_TYPE(`${label}.crl`, - ['ArrayBufferView', 'ArrayBuffer'], crlCert); - } - } - } - const keyHandles = []; if (keys !== undefined) { const keyInputs = ArrayIsArray(keys) ? keys : [keys]; @@ -2065,8 +4916,6 @@ function processIdentityOptions(identity, label) { __proto__: null, keys: keyHandles, certs, - ca, - crl, verifyPrivateKey, }; } @@ -2084,6 +4933,8 @@ function processTlsOptions(tls, forServer) { groups = DEFAULT_GROUPS, keylog = false, verifyClient = false, + rejectUnauthorized = true, + enableEarlyData = true, tlsTrace = false, sni, // Client-only: identity options are specified directly (no sni map) @@ -2105,6 +4956,8 @@ function processTlsOptions(tls, forServer) { } validateBoolean(keylog, 'options.keylog'); validateBoolean(verifyClient, 'options.verifyClient'); + validateBoolean(rejectUnauthorized, 'options.rejectUnauthorized'); + validateBoolean(enableEarlyData, 'options.enableEarlyData'); validateBoolean(tlsTrace, 'options.tlsTrace'); // Encode the ALPN option to wire format (length-prefixed protocol names). @@ -2138,6 +4991,28 @@ function processTlsOptions(tls, forServer) { encodedAlpn = buf.toString('latin1'); } + if (ca !== undefined) { + const caInputs = ArrayIsArray(ca) ? ca : [ca]; + for (const caCert of caInputs) { + if (!isArrayBufferView(caCert) && !isArrayBuffer(caCert)) { + throw new ERR_INVALID_ARG_TYPE('options.ca', + ['ArrayBufferView', 'ArrayBuffer'], + caCert); + } + } + } + + if (crl !== undefined) { + const crlInputs = ArrayIsArray(crl) ? crl : [crl]; + for (const crlCert of crlInputs) { + if (!isArrayBufferView(crlCert) && !isArrayBuffer(crlCert)) { + throw new ERR_INVALID_ARG_TYPE('options.crl', + ['ArrayBufferView', 'ArrayBuffer'], + crlCert); + } + } + } + // Shared TLS options (same for all identities on the endpoint). const shared = { __proto__: null, @@ -2147,32 +5022,45 @@ function processTlsOptions(tls, forServer) { groups, keylog, verifyClient, + rejectUnauthorized, + enableEarlyData, tlsTrace, + ca, + crl, }; // For servers, identity options come from the sni map. - // The '*' entry is the default/fallback identity. + // The '*' entry is the optional default/fallback identity. If omitted, + // only connections with a servername matching a specific entry will + // succeed; all others will be rejected at the TLS level. if (forServer) { if (sni === undefined || typeof sni !== 'object') { throw new ERR_MISSING_ARGS('options.sni'); } - if (sni['*'] === undefined) { - throw new ERR_MISSING_ARGS("options.sni['*']"); - } - // Process the default ('*') identity into the main tls options. - const defaultIdentity = processIdentityOptions(sni['*'], "options.sni['*']"); - if (defaultIdentity.keys.length === 0) { - throw new ERR_MISSING_ARGS("options.sni['*'].keys"); + // Must have at least one identity entry (wildcard or hostname-specific). + // A server with no identity at all cannot serve any connections. + const sniKeys = ObjectKeys(sni); + if (sniKeys.length === 0) { + throw new ERR_MISSING_ARGS('options.sni'); } - if (defaultIdentity.certs === undefined) { - throw new ERR_MISSING_ARGS("options.sni['*'].certs"); + + // Process the default ('*') identity if present. + let defaultIdentity = {}; + if (sni['*'] !== undefined) { + defaultIdentity = processIdentityOptions(sni['*'], "options.sni['*']"); + if (defaultIdentity.keys.length === 0) { + throw new ERR_MISSING_ARGS("options.sni['*'].keys"); + } + if (defaultIdentity.certs === undefined) { + throw new ERR_MISSING_ARGS("options.sni['*'].certs"); + } } // Build the SNI entries (excluding '*') as full TLS options objects. // Each inherits the shared options and overrides the identity fields. const sniEntries = { __proto__: null }; - for (const hostname of ObjectKeys(sni)) { + for (const hostname of sniKeys) { if (hostname === '*') continue; validateString(hostname, 'options.sni key'); const identity = processIdentityOptions(sni[hostname], @@ -2183,11 +5071,18 @@ function processTlsOptions(tls, forServer) { if (identity.certs === undefined) { throw new ERR_MISSING_ARGS(`options.sni['${hostname}'].certs`); } - // Build a full TLS options object: shared + identity. + // Extract ORIGIN frame options from the SNI entry. + const { + port, + authoritative, + } = sni[hostname]; + // Build a full TLS options object: shared + identity + origin options. sniEntries[hostname] = { __proto__: null, ...shared, ...identity, + ...(port !== undefined ? { port } : {}), + ...(authoritative !== undefined ? { authoritative } : {}), }; } @@ -2200,8 +5095,9 @@ function processTlsOptions(tls, forServer) { } // For clients, identity options are specified directly (no sni map). + // CA and CRL are in the shared options, not per-identity. const clientIdentity = processIdentityOptions({ - keys, certs, ca, crl, verifyPrivateKey, + keys, certs, verifyPrivateKey, }, 'options'); return { @@ -2215,67 +5111,221 @@ function processTlsOptions(tls, forServer) { * @param {'use'|'ignore'|'default'} policy * @returns {number} */ +/** + * Validate and normalize close error options for session.close() and + * session.destroy(). Returns the options object to pass to C++. + * @param {object} options + * @returns {object} + */ +function validateCloseOptions(options) { + validateObject(options, 'options'); + const { + code, + type = 'transport', + reason, + } = options; + + if (code !== undefined) { + if (typeof code !== 'bigint' && typeof code !== 'number') { + throw new ERR_INVALID_ARG_TYPE('options.code', + ['bigint', 'number'], code); + } + } + validateOneOf(type, 'options.type', ['transport', 'application']); + if (reason !== undefined) { + validateString(reason, 'options.reason'); + } + + return { __proto__: null, code, type, reason }; +} + function getPreferredAddressPolicy(policy = 'default') { switch (policy) { - case 'use': return PREFERRED_ADDRESS_USE; - case 'ignore': return PREFERRED_ADDRESS_IGNORE; - case 'default': return DEFAULT_PREFERRED_ADDRESS_POLICY; + case 'use': return kPreferredAddressUse; + case 'ignore': return kPreferredAddressIgnore; + case 'default': return kPreferredAddressDefault; } throw new ERR_INVALID_ARG_VALUE('options.preferredAddressPolicy', policy); } /** * @param {SessionOptions} options - * @param {boolean} [forServer] + * @param {ProcessSessionOptions} [config] * @returns {SessionOptions} */ -function processSessionOptions(options, forServer = false) { +function processSessionOptions(options, config = kEmptyObject) { validateObject(options, 'options'); const { endpoint, + reuseEndpoint = true, version, minVersion, - preferredAddressPolicy = 'default', + preferredAddressPolicy = 'ignore', transportParams = kEmptyObject, qlog = false, sessionTicket, + token, maxPayloadSize, unacknowledgedPacketThreshold = 0, handshakeTimeout, + initialRtt, + keepAlive, maxStreamWindow, maxWindow, cc, + datagramDropPolicy = 'drop-oldest', + drainingPeriodMultiplier = 3, + maxDatagramSendAttempts = 5, + streamIdleTimeout, + verifyPeer = 'auto', + // HTTP/3 application-specific options. Nested under `application` + // to separate protocol-specific settings from transport-level ones. + application = kEmptyObject, + // Session callbacks that can be set at construction time to avoid + // race conditions with events that fire during or immediately + // after the handshake. + onerror, + onstream, + ondatagram, + ondatagramstatus, + onpathvalidation, + onsessionticket, + onversionnegotiation, + onhandshake, + onnewtoken, + onearlyrejected, + onorigin, + ongoaway, + onkeylog, + onqlog, + onapplication, + // Application level options changed, e.g. HTTP/3 settings related + // Stream-level callbacks. + onheaders, + ontrailers, + oninfo, + onwanttrailers, } = options; + const { + forServer = false, + targetAddress, + } = config; + + if (token !== undefined) { + if (!isArrayBufferView(token)) { + throw new ERR_INVALID_ARG_TYPE('options.token', + ['ArrayBufferView'], token); + } + } + if (cc !== undefined) { - validateString(cc, 'options.cc'); - if (cc !== 'reno' || cc !== 'bbr' || cc !== 'cubic') { - throw new ERR_INVALID_ARG_VALUE('options.cc', cc); + validateOneOf(cc, 'options.cc', [CC_ALGO_RENO, CC_ALGO_BBR, CC_ALGO_CUBIC]); + } + + validateOneOf(datagramDropPolicy, 'options.datagramDropPolicy', + ['drop-oldest', 'drop-newest']); + + validateOneOf(verifyPeer, 'options.verifyPeer', + ['strict', 'auto', 'manual']); + + validateInteger(drainingPeriodMultiplier, 'options.drainingPeriodMultiplier', + 3, 255); + + validateInteger(maxDatagramSendAttempts, 'options.maxDatagramSendAttempts', + 1, 255); + + // Validate preferred address in transport params if provided. + const { preferredAddressIpv4, preferredAddressIpv6 } = transportParams; + if (preferredAddressIpv4 !== undefined) { + if (!SocketAddress.isSocketAddress(preferredAddressIpv4)) { + throw new ERR_INVALID_ARG_TYPE( + 'options.transportParams.preferredAddressIpv4', + 'SocketAddress', preferredAddressIpv4); + } + if (preferredAddressIpv4.family !== 'ipv4') { + throw new ERR_INVALID_ARG_VALUE( + 'options.transportParams.preferredAddressIpv4', + preferredAddressIpv4, 'must be an IPv4 address'); + } + } + if (preferredAddressIpv6 !== undefined) { + if (!SocketAddress.isSocketAddress(preferredAddressIpv6)) { + throw new ERR_INVALID_ARG_TYPE( + 'options.transportParams.preferredAddressIpv6', + 'SocketAddress', preferredAddressIpv6); + } + if (preferredAddressIpv6.family !== 'ipv6') { + throw new ERR_INVALID_ARG_VALUE( + 'options.transportParams.preferredAddressIpv6', + preferredAddressIpv6, 'must be an IPv6 address'); } } - const { - endpoint: actualEndpoint, - created: endpointCreated, - } = processEndpointOption(endpoint); + const actualEndpoint = processEndpointOption(endpoint, + reuseEndpoint, + forServer, + targetAddress); return { __proto__: null, endpoint: actualEndpoint, - endpointCreated, version, minVersion, preferredAddressPolicy: getPreferredAddressPolicy(preferredAddressPolicy), - transportParams, - tls: processTlsOptions(options, forServer), + transportParams: { + ...transportParams, + preferredAddressIpv4: preferredAddressIpv4?.[kSocketAddressHandle], + preferredAddressIpv6: preferredAddressIpv6?.[kSocketAddressHandle], + }, + tls: { + ...processTlsOptions(options, forServer), + // Forward strict mode to C++ so SSL_VERIFY_PEER is set on the + // client SSL_CTX. For 'auto' and 'manual' modes, the handshake + // completes regardless and the result is handled in JS. + verifyPeerStrict: verifyPeer === 'strict', + // Enable hostname verification for 'strict' and 'auto' modes. + // SSL_set1_host tells OpenSSL to verify the server certificate's + // SAN/CN matches the servername. Without this, a valid cert for + // any domain would be accepted. + verifyHostname: verifyPeer !== 'manual', + }, + verifyPeer, qlog, maxPayloadSize, unacknowledgedPacketThreshold, handshakeTimeout, + initialRtt, + keepAlive, maxStreamWindow, maxWindow, sessionTicket, + token, cc, + datagramDropPolicy, + drainingPeriodMultiplier, + maxDatagramSendAttempts, + streamIdleTimeout, + application, + onerror, + onstream, + ondatagram, + ondatagramstatus, + onpathvalidation, + onsessionticket, + onversionnegotiation, + onhandshake, + onnewtoken, + onearlyrejected, + onorigin, + ongoaway, + onkeylog, + onqlog, + onapplication, + onheaders, + ontrailers, + oninfo, + onwanttrailers, }; } @@ -2291,11 +5341,12 @@ async function listen(callback, options = kEmptyObject) { const { endpoint, ...sessionOptions - } = processSessionOptions(options, true /* for server */); + } = processSessionOptions(options, { forServer: true }); endpoint[kListen](callback, sessionOptions); if (onEndpointListeningChannel.hasSubscribers) { onEndpointListeningChannel.publish({ + __proto__: null, endpoint, options, }); @@ -2324,12 +5375,22 @@ async function connect(address, options = kEmptyObject) { const { endpoint, ...rest - } = processSessionOptions(options); + } = processSessionOptions(options, { targetAddress: address }); + + if (onEndpointConnectChannel.hasSubscribers) { + onEndpointConnectChannel.publish({ + __proto__: null, + endpoint, + address, + options, + }); + } const session = endpoint[kConnect](address[kSocketAddressHandle], rest); if (onEndpointClientSessionChannel.hasSubscribers) { onEndpointClientSessionChannel.publish({ + __proto__: null, endpoint, session, address, @@ -2343,8 +5404,8 @@ async function connect(address, options = kEmptyObject) { ObjectDefineProperties(QuicEndpoint, { Stats: { __proto__: null, - writable: true, - configurable: true, + writable: false, + configurable: false, enumerable: true, value: QuicEndpointStats, }, @@ -2352,8 +5413,8 @@ ObjectDefineProperties(QuicEndpoint, { ObjectDefineProperties(QuicSession, { Stats: { __proto__: null, - writable: true, - configurable: true, + writable: false, + configurable: false, enumerable: true, value: QuicSessionStats, }, @@ -2361,8 +5422,8 @@ ObjectDefineProperties(QuicSession, { ObjectDefineProperties(QuicStream, { Stats: { __proto__: null, - writable: true, - configurable: true, + writable: false, + configurable: false, enumerable: true, value: QuicStreamStats, }, @@ -2374,19 +5435,19 @@ module.exports = { listen, connect, QuicEndpoint, + QuicError, QuicSession, QuicStream, DEFAULT_CIPHERS, DEFAULT_GROUPS, + CC_ALGO_RENO, + CC_ALGO_CUBIC, + CC_ALGO_BBR, + // These are exported only for internal testing purposes. + getQuicStreamState, + getQuicSessionState, + getQuicEndpointState, + listEndpoints, }; -ObjectDefineProperties(module.exports, { - CC_ALGO_RENO: readOnlyConstant(CC_ALGO_RENO_STR), - CC_ALGO_CUBIC: readOnlyConstant(CC_ALGO_CUBIC_STR), - CC_ALGO_BBR: readOnlyConstant(CC_ALGO_BBR_STR), - DEFAULT_CIPHERS: readOnlyConstant(DEFAULT_CIPHERS), - DEFAULT_GROUPS: readOnlyConstant(DEFAULT_GROUPS), -}); - - /* c8 ignore stop */ diff --git a/lib/internal/quic/state.js b/lib/internal/quic/state.js index f8075457825630..8815b0bb4c32cf 100644 --- a/lib/internal/quic/state.js +++ b/lib/internal/quic/state.js @@ -1,15 +1,30 @@ 'use strict'; +// TODO(@jasnell) Temporarily ignoring c8 covrerage for this file while tests +// are still being developed. +/* c8 ignore start */ + const { - ArrayBuffer, DataView, DataViewPrototypeGetBigInt64, DataViewPrototypeGetBigUint64, + DataViewPrototypeGetByteLength, + DataViewPrototypeGetUint16, + DataViewPrototypeGetUint32, DataViewPrototypeGetUint8, + DataViewPrototypeSetUint16, + DataViewPrototypeSetUint32, DataViewPrototypeSetUint8, JSONStringify, + Number, } = primordials; +const { + isBigEndian, +} = internalBinding('os'); + +const kIsLittleEndian = !isBigEndian; + const { getOptionValue, } = require('internal/options'); @@ -36,8 +51,6 @@ const { kFinishClose, kInspect, kPrivateConstructor, - kWantsHeaders, - kWantsTrailers, } = require('internal/quic/symbols'); // This file defines the helper objects for accessing state for @@ -49,10 +62,7 @@ const { // prevent further updates to the buffer. const { - IDX_STATE_SESSION_PATH_VALIDATION, - IDX_STATE_SESSION_VERSION_NEGOTIATION, - IDX_STATE_SESSION_DATAGRAM, - IDX_STATE_SESSION_SESSION_TICKET, + IDX_STATE_SESSION_LISTENER_FLAGS, IDX_STATE_SESSION_CLOSING, IDX_STATE_SESSION_GRACEFUL_CLOSE, IDX_STATE_SESSION_SILENT_CLOSE, @@ -61,15 +71,22 @@ const { IDX_STATE_SESSION_HANDSHAKE_CONFIRMED, IDX_STATE_SESSION_STREAM_OPEN_ALLOWED, IDX_STATE_SESSION_PRIORITY_SUPPORTED, + IDX_STATE_SESSION_HEADERS_SUPPORTED, IDX_STATE_SESSION_WRAPPED, IDX_STATE_SESSION_APPLICATION_TYPE, + IDX_STATE_SESSION_NO_ERROR_CODE, + IDX_STATE_SESSION_INTERNAL_ERROR_CODE, + IDX_STATE_SESSION_MAX_DATAGRAM_SIZE, IDX_STATE_SESSION_LAST_DATAGRAM_ID, + IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS, IDX_STATE_ENDPOINT_BOUND, IDX_STATE_ENDPOINT_RECEIVING, IDX_STATE_ENDPOINT_LISTENING, IDX_STATE_ENDPOINT_CLOSING, IDX_STATE_ENDPOINT_BUSY, + IDX_STATE_ENDPOINT_MAX_CONNECTIONS_PER_HOST, + IDX_STATE_ENDPOINT_MAX_CONNECTIONS_TOTAL, IDX_STATE_ENDPOINT_PENDING_CALLBACKS, IDX_STATE_STREAM_ID, @@ -85,12 +102,13 @@ const { IDX_STATE_STREAM_WANTS_HEADERS, IDX_STATE_STREAM_WANTS_RESET, IDX_STATE_STREAM_WANTS_TRAILERS, + IDX_STATE_STREAM_RECEIVED_EARLY_DATA, + IDX_STATE_STREAM_WRITE_DESIRED_SIZE, + IDX_STATE_STREAM_HIGH_WATER_MARK, + IDX_STATE_STREAM_RESET_CODE, } = internalBinding('quic'); -assert(IDX_STATE_SESSION_PATH_VALIDATION !== undefined); -assert(IDX_STATE_SESSION_VERSION_NEGOTIATION !== undefined); -assert(IDX_STATE_SESSION_DATAGRAM !== undefined); -assert(IDX_STATE_SESSION_SESSION_TICKET !== undefined); +assert(IDX_STATE_SESSION_LISTENER_FLAGS !== undefined); assert(IDX_STATE_SESSION_CLOSING !== undefined); assert(IDX_STATE_SESSION_GRACEFUL_CLOSE !== undefined); assert(IDX_STATE_SESSION_SILENT_CLOSE !== undefined); @@ -99,8 +117,12 @@ assert(IDX_STATE_SESSION_HANDSHAKE_COMPLETED !== undefined); assert(IDX_STATE_SESSION_HANDSHAKE_CONFIRMED !== undefined); assert(IDX_STATE_SESSION_STREAM_OPEN_ALLOWED !== undefined); assert(IDX_STATE_SESSION_PRIORITY_SUPPORTED !== undefined); +assert(IDX_STATE_SESSION_HEADERS_SUPPORTED !== undefined); assert(IDX_STATE_SESSION_WRAPPED !== undefined); assert(IDX_STATE_SESSION_APPLICATION_TYPE !== undefined); +assert(IDX_STATE_SESSION_NO_ERROR_CODE !== undefined); +assert(IDX_STATE_SESSION_INTERNAL_ERROR_CODE !== undefined); +assert(IDX_STATE_SESSION_MAX_DATAGRAM_SIZE !== undefined); assert(IDX_STATE_SESSION_LAST_DATAGRAM_ID !== undefined); assert(IDX_STATE_ENDPOINT_BOUND !== undefined); assert(IDX_STATE_ENDPOINT_RECEIVING !== undefined); @@ -121,6 +143,13 @@ assert(IDX_STATE_STREAM_WANTS_BLOCK !== undefined); assert(IDX_STATE_STREAM_WANTS_HEADERS !== undefined); assert(IDX_STATE_STREAM_WANTS_RESET !== undefined); assert(IDX_STATE_STREAM_WANTS_TRAILERS !== undefined); +assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE !== undefined); +assert(IDX_STATE_STREAM_RESET_CODE !== undefined); + +const kEmptyObject = { __proto__: null }; + +// The internal state objects for endpoints, sessions, and streams. +// These are not exposed directly to users. class QuicEndpointState { /** @type {DataView} */ @@ -142,43 +171,79 @@ class QuicEndpointState { /** @type {boolean} */ get isBound() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_BOUND); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, IDX_STATE_ENDPOINT_BOUND) !== 0; } /** @type {boolean} */ get isReceiving() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_RECEIVING); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, IDX_STATE_ENDPOINT_RECEIVING) !== 0; } /** @type {boolean} */ get isListening() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_LISTENING); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, IDX_STATE_ENDPOINT_LISTENING) !== 0; } /** @type {boolean} */ get isClosing() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_CLOSING); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, IDX_STATE_ENDPOINT_CLOSING) !== 0; } /** @type {boolean} */ get isBusy() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_BUSY); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, IDX_STATE_ENDPOINT_BUSY) !== 0; + } + + /** @type {number} */ + get maxConnectionsPerHost() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint16( + handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_PER_HOST, kIsLittleEndian); + } + + set maxConnectionsPerHost(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint16( + handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_PER_HOST, val, kIsLittleEndian); + } + + /** @type {number} */ + get maxConnectionsTotal() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint16( + handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_TOTAL, kIsLittleEndian); + } + + set maxConnectionsTotal(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint16( + handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_TOTAL, val, kIsLittleEndian); } /** - * The number of underlying callbacks that are pending. If the session - * is closing, these are the number of callbacks that the session is + * The number of underlying callbacks that are pending. If the endpoint + * is closing, these are the number of callbacks that the endpoint is * waiting on before it can be closed. * @type {bigint} */ get pendingCallbacks() { - if (this.#handle.byteLength === 0) return undefined; - return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_ENDPOINT_PENDING_CALLBACKS); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetBigUint64(handle, IDX_STATE_ENDPOINT_PENDING_CALLBACKS, kIsLittleEndian); } toString() { @@ -186,179 +251,321 @@ class QuicEndpointState { } toJSON() { - if (this.#handle.byteLength === 0) return {}; + if (this.#handle === undefined) return kEmptyObject; + const { + isBound, + isReceiving, + isListening, + isClosing, + isBusy, + maxConnectionsPerHost, + maxConnectionsTotal, + pendingCallbacks, + } = this; return { __proto__: null, - isBound: this.isBound, - isReceiving: this.isReceiving, - isListening: this.isListening, - isClosing: this.isClosing, - isBusy: this.isBusy, - pendingCallbacks: `${this.pendingCallbacks}`, + isBound, + isReceiving, + isListening, + isClosing, + isBusy, + maxConnectionsPerHost, + maxConnectionsTotal, + pendingCallbacks: Number(pendingCallbacks), }; } [kInspect](depth, options) { - if (depth < 0) - return this; - - if (this.#handle.byteLength === 0) { + if (this.#handle === undefined) { return 'QuicEndpointState { }'; } + if (depth < 0) { + return 'QuicEndpointState { }'; + } + const opts = { + __proto__: null, ...options, depth: options.depth == null ? null : options.depth - 1, }; + const { + isBound, + isReceiving, + isListening, + isClosing, + isBusy, + maxConnectionsPerHost, + maxConnectionsTotal, + pendingCallbacks, + } = this; + return `QuicEndpointState ${inspect({ - isBound: this.isBound, - isReceiving: this.isReceiving, - isListening: this.isListening, - isClosing: this.isClosing, - isBusy: this.isBusy, - pendingCallbacks: this.pendingCallbacks, + isBound, + isReceiving, + isListening, + isClosing, + isBusy, + maxConnectionsPerHost, + maxConnectionsTotal, + pendingCallbacks, }, opts)}`; } [kFinishClose]() { - // Snapshot the state into a new DataView since the underlying - // buffer will be destroyed. - if (this.#handle.byteLength === 0) return; - this.#handle = new DataView(new ArrayBuffer(0)); + this.#handle = undefined; } } class QuicSessionState { /** @type {DataView} */ #handle; + /** @type {number} */ + #offset = 0; /** * @param {symbol} privateSymbol - * @param {ArrayBuffer} buffer + * @param {DataView|ArrayBuffer} view + * @param {number} [byteOffset] */ - constructor(privateSymbol, buffer) { + constructor(privateSymbol, view, byteOffset = 0) { if (privateSymbol !== kPrivateConstructor) { throw new ERR_ILLEGAL_CONSTRUCTOR(); } - if (!isArrayBuffer(buffer)) { - throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); + if (isArrayBuffer(view)) { + this.#handle = new DataView(view); + } else { + this.#handle = view; } - this.#handle = new DataView(buffer); + this.#offset = byteOffset; } - /** @type {boolean} */ - get hasPathValidationListener() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_PATH_VALIDATION); + // Listener flags are packed into a single uint32_t bitfield. The bit + // positions must match the SessionListenerFlags enum in session.cc. + static #LISTENER_PATH_VALIDATION = 1 << 0; + static #LISTENER_DATAGRAM = 1 << 1; + static #LISTENER_DATAGRAM_STATUS = 1 << 2; + static #LISTENER_SESSION_TICKET = 1 << 3; + static #LISTENER_NEW_TOKEN = 1 << 4; + static #LISTENER_ORIGIN = 1 << 5; + static #LISTENER_APPLICATION = 1 << 6; + + #getListenerFlag(flag) { + const handle = this.#handle; + if (handle === undefined) return undefined; + return (DataViewPrototypeGetUint32( + handle, this.#offset + IDX_STATE_SESSION_LISTENER_FLAGS, kIsLittleEndian) & flag) !== 0; } - /** @type {boolean} */ - set hasPathValidationListener(val) { - if (this.#handle.byteLength === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_PATH_VALIDATION, val ? 1 : 0); + #setListenerFlag(flag, val) { + const handle = this.#handle; + if (handle === undefined) return; + const current = DataViewPrototypeGetUint32( + handle, this.#offset + IDX_STATE_SESSION_LISTENER_FLAGS, kIsLittleEndian); + DataViewPrototypeSetUint32( + handle, this.#offset + IDX_STATE_SESSION_LISTENER_FLAGS, + val ? (current | flag) : (current & ~flag), kIsLittleEndian); } /** @type {boolean} */ - get hasVersionNegotiationListener() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_VERSION_NEGOTIATION); + get hasApplicationListener() { + return this.#getListenerFlag(QuicSessionState.#LISTENER_APPLICATION); + } + set hasApplicationListener(val) { + this.#setListenerFlag(QuicSessionState.#LISTENER_APPLICATION, val); } /** @type {boolean} */ - set hasVersionNegotiationListener(val) { - if (this.#handle.byteLength === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_VERSION_NEGOTIATION, val ? 1 : 0); + get hasPathValidationListener() { + return this.#getListenerFlag(QuicSessionState.#LISTENER_PATH_VALIDATION); + } + set hasPathValidationListener(val) { + this.#setListenerFlag(QuicSessionState.#LISTENER_PATH_VALIDATION, val); } /** @type {boolean} */ get hasDatagramListener() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_DATAGRAM); + return this.#getListenerFlag(QuicSessionState.#LISTENER_DATAGRAM); + } + set hasDatagramListener(val) { + this.#setListenerFlag(QuicSessionState.#LISTENER_DATAGRAM, val); } /** @type {boolean} */ - set hasDatagramListener(val) { - if (this.#handle.byteLength === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_DATAGRAM, val ? 1 : 0); + get hasDatagramStatusListener() { + return this.#getListenerFlag(QuicSessionState.#LISTENER_DATAGRAM_STATUS); + } + set hasDatagramStatusListener(val) { + this.#setListenerFlag(QuicSessionState.#LISTENER_DATAGRAM_STATUS, val); } /** @type {boolean} */ get hasSessionTicketListener() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_SESSION_TICKET); + return this.#getListenerFlag(QuicSessionState.#LISTENER_SESSION_TICKET); + } + set hasSessionTicketListener(val) { + this.#setListenerFlag(QuicSessionState.#LISTENER_SESSION_TICKET, val); } /** @type {boolean} */ - set hasSessionTicketListener(val) { - if (this.#handle.byteLength === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_SESSION_TICKET, val ? 1 : 0); + get hasNewTokenListener() { + return this.#getListenerFlag(QuicSessionState.#LISTENER_NEW_TOKEN); + } + set hasNewTokenListener(val) { + this.#setListenerFlag(QuicSessionState.#LISTENER_NEW_TOKEN, val); + } + + /** @type {boolean} */ + get hasOriginListener() { + return this.#getListenerFlag(QuicSessionState.#LISTENER_ORIGIN); + } + set hasOriginListener(val) { + this.#setListenerFlag(QuicSessionState.#LISTENER_ORIGIN, val); } /** @type {boolean} */ get isClosing() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_CLOSING); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_CLOSING) !== 0; } /** @type {boolean} */ get isGracefulClose() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_GRACEFUL_CLOSE); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_GRACEFUL_CLOSE) !== 0; } /** @type {boolean} */ get isSilentClose() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_SILENT_CLOSE); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_SILENT_CLOSE) !== 0; } /** @type {boolean} */ get isStatelessReset() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_STATELESS_RESET); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_STATELESS_RESET) !== 0; } /** @type {boolean} */ get isHandshakeCompleted() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HANDSHAKE_COMPLETED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_HANDSHAKE_COMPLETED) !== 0; } /** @type {boolean} */ get isHandshakeConfirmed() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HANDSHAKE_CONFIRMED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_HANDSHAKE_CONFIRMED) !== 0; } /** @type {boolean} */ get isStreamOpenAllowed() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_STREAM_OPEN_ALLOWED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_STREAM_OPEN_ALLOWED) !== 0; } /** @type {boolean} */ get isPrioritySupported() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_PRIORITY_SUPPORTED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_PRIORITY_SUPPORTED) !== 0; + } + + /** + * Whether the negotiated application protocol supports headers. + * Returns 0 (unknown), 1 (supported), or 2 (not supported). + * @type {number} + */ + get headersSupported() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_HEADERS_SUPPORTED); } /** @type {boolean} */ get isWrapped() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_WRAPPED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_WRAPPED) !== 0; } /** @type {number} */ get applicationType() { - if (this.#handle.byteLength === 0) return undefined; - return DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_APPLICATION_TYPE); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_APPLICATION_TYPE); + } + + /** + * The negotiated application protocol's "no error" code, populated + * by the C++ layer when the application is selected during ALPN + * negotiation. For raw QUIC this is `0n`; for HTTP/3 this is + * `0x100n` (`H3_NO_ERROR`). + * @type {bigint} + */ + get noErrorCode() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetBigUint64( + handle, this.#offset + IDX_STATE_SESSION_NO_ERROR_CODE, kIsLittleEndian); + } + + /** + * The negotiated application protocol's "internal error" code, + * populated by the C++ layer when the application is selected + * during ALPN negotiation. Used as the wire code for `RESET_STREAM` + * frames when a stream is aborted without a more specific code. + * For raw QUIC this is `0x1n` (NGTCP2_INTERNAL_ERROR); for HTTP/3 + * this is `0x102n` (`H3_INTERNAL_ERROR`). + * @type {bigint} + */ + get internalErrorCode() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetBigUint64( + handle, this.#offset + IDX_STATE_SESSION_INTERNAL_ERROR_CODE, kIsLittleEndian); + } + + /** @type {number} */ + get maxDatagramSize() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint16( + handle, this.#offset + IDX_STATE_SESSION_MAX_DATAGRAM_SIZE, + kIsLittleEndian); } /** @type {bigint} */ get lastDatagramId() { - if (this.#handle.byteLength === 0) return undefined; - return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_SESSION_LAST_DATAGRAM_ID); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetBigUint64( + handle, this.#offset + IDX_STATE_SESSION_LAST_DATAGRAM_ID, + kIsLittleEndian); + } + + /** @type {number} */ + get maxPendingDatagrams() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint16( + handle, this.#offset + IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS, kIsLittleEndian); + } + + set maxPendingDatagrams(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint16( + handle, this.#offset + IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS, val, kIsLittleEndian); } toString() { @@ -366,187 +573,316 @@ class QuicSessionState { } toJSON() { - if (this.#handle.byteLength === 0) return {}; + if (this.#handle === undefined) return kEmptyObject; + const { + hasPathValidationListener, + hasDatagramListener, + hasDatagramStatusListener, + hasSessionTicketListener, + hasNewTokenListener, + hasOriginListener, + isClosing, + isGracefulClose, + isSilentClose, + isStatelessReset, + isHandshakeCompleted, + isHandshakeConfirmed, + isStreamOpenAllowed, + isPrioritySupported, + headersSupported, + isWrapped, + applicationType, + noErrorCode, + internalErrorCode, + maxDatagramSize, + lastDatagramId, + maxPendingDatagrams, + } = this; return { __proto__: null, - hasPathValidationListener: this.hasPathValidationListener, - hasVersionNegotiationListener: this.hasVersionNegotiationListener, - hasDatagramListener: this.hasDatagramListener, - hasSessionTicketListener: this.hasSessionTicketListener, - isClosing: this.isClosing, - isGracefulClose: this.isGracefulClose, - isSilentClose: this.isSilentClose, - isStatelessReset: this.isStatelessReset, - isDestroyed: this.isDestroyed, - isHandshakeCompleted: this.isHandshakeCompleted, - isHandshakeConfirmed: this.isHandshakeConfirmed, - isStreamOpenAllowed: this.isStreamOpenAllowed, - isPrioritySupported: this.isPrioritySupported, - isWrapped: this.isWrapped, - lastDatagramId: `${this.lastDatagramId}`, + hasPathValidationListener, + hasDatagramListener, + hasDatagramStatusListener, + hasSessionTicketListener, + hasNewTokenListener, + hasOriginListener, + isClosing, + isGracefulClose, + isSilentClose, + isStatelessReset, + isHandshakeCompleted, + isHandshakeConfirmed, + isStreamOpenAllowed, + isPrioritySupported, + headersSupported, + isWrapped, + applicationType, + noErrorCode: `${noErrorCode}`, + internalErrorCode: `${internalErrorCode}`, + maxDatagramSize: `${maxDatagramSize}`, + lastDatagramId: `${lastDatagramId}`, + maxPendingDatagrams, }; } [kInspect](depth, options) { - if (depth < 0) - return this; - - if (this.#handle.byteLength === 0) { + if (this.#handle === undefined) { return 'QuicSessionState { }'; } + if (depth < 0) { + return 'QuicSessionState { }'; + } + const opts = { + __proto__: null, ...options, depth: options.depth == null ? null : options.depth - 1, }; + const { + hasPathValidationListener, + hasDatagramListener, + hasDatagramStatusListener, + hasSessionTicketListener, + hasNewTokenListener, + hasOriginListener, + isClosing, + isGracefulClose, + isSilentClose, + isStatelessReset, + isHandshakeCompleted, + isHandshakeConfirmed, + isStreamOpenAllowed, + isPrioritySupported, + headersSupported, + isWrapped, + applicationType, + noErrorCode, + internalErrorCode, + maxDatagramSize, + lastDatagramId, + maxPendingDatagrams, + } = this; + return `QuicSessionState ${inspect({ - hasPathValidationListener: this.hasPathValidationListener, - hasVersionNegotiationListener: this.hasVersionNegotiationListener, - hasDatagramListener: this.hasDatagramListener, - hasSessionTicketListener: this.hasSessionTicketListener, - isClosing: this.isClosing, - isGracefulClose: this.isGracefulClose, - isSilentClose: this.isSilentClose, - isStatelessReset: this.isStatelessReset, - isDestroyed: this.isDestroyed, - isHandshakeCompleted: this.isHandshakeCompleted, - isHandshakeConfirmed: this.isHandshakeConfirmed, - isStreamOpenAllowed: this.isStreamOpenAllowed, - isPrioritySupported: this.isPrioritySupported, - isWrapped: this.isWrapped, - applicationType: this.applicationType, - lastDatagramId: this.lastDatagramId, + hasPathValidationListener, + hasDatagramListener, + hasDatagramStatusListener, + hasSessionTicketListener, + hasNewTokenListener, + hasOriginListener, + isClosing, + isGracefulClose, + isSilentClose, + isStatelessReset, + isHandshakeCompleted, + isHandshakeConfirmed, + isStreamOpenAllowed, + isPrioritySupported, + headersSupported, + isWrapped, + applicationType, + noErrorCode, + internalErrorCode, + maxDatagramSize, + lastDatagramId, + maxPendingDatagrams, }, opts)}`; } [kFinishClose]() { - // Snapshot the state into a new DataView since the underlying - // buffer will be destroyed. - if (this.#handle.byteLength === 0) return; - this.#handle = new DataView(new ArrayBuffer(0)); + this.#handle = undefined; } } class QuicStreamState { /** @type {DataView} */ #handle; + /** @type {number} */ + #offset = 0; + /** @type {bigint|undefined} */ + #id = undefined; /** * @param {symbol} privateSymbol - * @param {ArrayBuffer} buffer + * @param {DataView|ArrayBuffer} view + * @param {number} [byteOffset] */ - constructor(privateSymbol, buffer) { + constructor(privateSymbol, view, byteOffset = 0) { if (privateSymbol !== kPrivateConstructor) { throw new ERR_ILLEGAL_CONSTRUCTOR(); } - if (!isArrayBuffer(buffer)) { - throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); + if (isArrayBuffer(view)) { + this.#handle = new DataView(view); + } else { + this.#handle = view; } - this.#handle = new DataView(buffer); + this.#offset = byteOffset; } /** @type {bigint} */ get id() { - if (this.#handle.byteLength === 0) return undefined; - return DataViewPrototypeGetBigInt64(this.#handle, IDX_STATE_STREAM_ID); + const handle = this.#handle; + if (handle === undefined) return this.#id; + return DataViewPrototypeGetBigInt64(handle, this.#offset + IDX_STATE_STREAM_ID, kIsLittleEndian); } /** @type {boolean} */ get pending() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_PENDING); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_PENDING) !== 0; } /** @type {boolean} */ get finSent() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_FIN_SENT); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_FIN_SENT) !== 0; } /** @type {boolean} */ get finReceived() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_FIN_RECEIVED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_FIN_RECEIVED) !== 0; } /** @type {boolean} */ get readEnded() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_READ_ENDED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_READ_ENDED) !== 0; } /** @type {boolean} */ get writeEnded() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WRITE_ENDED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WRITE_ENDED) !== 0; } - /** @type {boolean} */ get reset() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_RESET); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_RESET) !== 0; } /** @type {boolean} */ get hasOutbound() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_HAS_OUTBOUND); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_HAS_OUTBOUND) !== 0; } /** @type {boolean} */ get hasReader() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_HAS_READER); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_HAS_READER) !== 0; } /** @type {boolean} */ get wantsBlock() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_BLOCK); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_BLOCK) !== 0; } /** @type {boolean} */ set wantsBlock(val) { - if (this.#handle.byteLength === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_BLOCK, val ? 1 : 0); + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_BLOCK, val ? 1 : 0); } /** @type {boolean} */ - get [kWantsHeaders]() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_HEADERS); + get wantsHeaders() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_HEADERS) !== 0; } /** @type {boolean} */ - set [kWantsHeaders](val) { - if (this.#handle.byteLength === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_HEADERS, val ? 1 : 0); + set wantsHeaders(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_HEADERS, val ? 1 : 0); } /** @type {boolean} */ get wantsReset() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_RESET); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_RESET) !== 0; } /** @type {boolean} */ set wantsReset(val) { - if (this.#handle.byteLength === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_RESET, val ? 1 : 0); + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_RESET, val ? 1 : 0); + } + + /** @type {boolean} */ + get wantsTrailers() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_TRAILERS) !== 0; } /** @type {boolean} */ - get [kWantsTrailers]() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_TRAILERS); + set wantsTrailers(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_TRAILERS, val ? 1 : 0); } /** @type {boolean} */ - set [kWantsTrailers](val) { - if (this.#handle.byteLength === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_TRAILERS, val ? 1 : 0); + get early() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_RECEIVED_EARLY_DATA) !== 0; + } + + /** @type {bigint} */ + get resetCode() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetBigUint64( + handle, this.#offset + IDX_STATE_STREAM_RESET_CODE, kIsLittleEndian); + } + + /** @type {bigint} */ + get writeDesiredSize() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint32( + handle, this.#offset + IDX_STATE_STREAM_WRITE_DESIRED_SIZE, kIsLittleEndian); + } + + set writeDesiredSize(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint32( + handle, this.#offset + IDX_STATE_STREAM_WRITE_DESIRED_SIZE, val, kIsLittleEndian); + } + + /** @type {number} */ + get highWaterMark() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint32( + handle, this.#offset + IDX_STATE_STREAM_HIGH_WATER_MARK, kIsLittleEndian); + } + + set highWaterMark(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint32( + handle, this.#offset + IDX_STATE_STREAM_HIGH_WATER_MARK, val, kIsLittleEndian); } toString() { @@ -554,56 +890,109 @@ class QuicStreamState { } toJSON() { - if (this.#handle.byteLength === 0) return {}; + if (this.#handle === undefined) return kEmptyObject; + const { + id, + pending, + finSent, + finReceived, + readEnded, + writeEnded, + reset, + hasOutbound, + hasReader, + wantsBlock, + wantsReset, + wantsHeaders, + wantsTrailers, + early, + resetCode, + writeDesiredSize, + highWaterMark, + } = this; return { __proto__: null, - id: `${this.id}`, - pending: this.pending, - finSent: this.finSent, - finReceived: this.finReceived, - readEnded: this.readEnded, - writeEnded: this.writeEnded, - reset: this.reset, - hasOutbound: this.hasOutbound, - hasReader: this.hasReader, - wantsBlock: this.wantsBlock, - wantsReset: this.wantsReset, + id: `${id}`, + pending, + finSent, + finReceived, + readEnded, + writeEnded, + reset, + hasOutbound, + hasReader, + wantsBlock, + wantsReset, + wantsHeaders, + wantsTrailers, + early, + resetCode, + writeDesiredSize, + highWaterMark, }; } [kInspect](depth, options) { - if (depth < 0) - return this; - - if (this.#handle.byteLength === 0) { + if (this.#handle === undefined || + DataViewPrototypeGetByteLength(this.#handle) === 0) { return 'QuicStreamState { }'; } + if (depth < 0) { + return 'QuicStreamState { }'; + } + const opts = { + __proto__: null, ...options, depth: options.depth == null ? null : options.depth - 1, }; + const { + id, + pending, + finSent, + finReceived, + readEnded, + writeEnded, + reset, + hasOutbound, + hasReader, + wantsBlock, + wantsReset, + wantsHeaders, + wantsTrailers, + early, + resetCode, + writeDesiredSize, + highWaterMark, + } = this; + return `QuicStreamState ${inspect({ - id: this.id, - pending: this.pending, - finSent: this.finSent, - finReceived: this.finReceived, - readEnded: this.readEnded, - writeEnded: this.writeEnded, - reset: this.reset, - hasOutbound: this.hasOutbound, - hasReader: this.hasReader, - wantsBlock: this.wantsBlock, - wantsReset: this.wantsReset, + id, + pending, + finSent, + finReceived, + readEnded, + writeEnded, + reset, + hasOutbound, + hasReader, + wantsBlock, + wantsReset, + wantsHeaders, + wantsTrailers, + early, + resetCode, + writeDesiredSize, + highWaterMark, }, opts)}`; } [kFinishClose]() { - // Snapshot the state into a new DataView since the underlying - // buffer will be destroyed. - if (this.#handle.byteLength === 0) return; - this.#handle = new DataView(new ArrayBuffer(0)); + // Cache the stream ID since the buffer will be zeroed out and the ID will be lost. + this.#id = this.id; + this.#handle = undefined; } } @@ -612,3 +1001,5 @@ module.exports = { QuicSessionState, QuicStreamState, }; + +/* c8 ignore stop */ diff --git a/lib/internal/quic/stats.js b/lib/internal/quic/stats.js index a612356250a06c..cd986827c12c47 100644 --- a/lib/internal/quic/stats.js +++ b/lib/internal/quic/stats.js @@ -1,8 +1,14 @@ 'use strict'; +// TODO(@jasnell) Temporarily ignoring c8 covrerage for this file while tests +// are still being developed. +/* c8 ignore start */ + const { BigUint64Array, JSONStringify, + Symbol, + TypedArrayPrototypeSubarray, } = primordials; const { @@ -21,6 +27,7 @@ const { codes: { ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, + ERR_INVALID_THIS, }, } = require('internal/errors'); @@ -53,16 +60,22 @@ const { IDX_STATS_ENDPOINT_CLIENT_SESSIONS, IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT, IDX_STATS_ENDPOINT_RETRY_COUNT, + IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED, IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT, + IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED, IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT, + IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED, IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT, + IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED, + IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED, + IDX_STATS_ENDPOINT_PACKETS_BLOCKED, IDX_STATS_SESSION_CREATED_AT, + IDX_STATS_SESSION_DESTROYED_AT, IDX_STATS_SESSION_CLOSING_AT, IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT, IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT, IDX_STATS_SESSION_BYTES_RECEIVED, - IDX_STATS_SESSION_BYTES_SENT, IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT, IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT, IDX_STATS_SESSION_UNI_IN_STREAM_COUNT, @@ -76,10 +89,20 @@ const { IDX_STATS_SESSION_RTTVAR, IDX_STATS_SESSION_SMOOTHED_RTT, IDX_STATS_SESSION_SSTHRESH, + IDX_STATS_SESSION_PKT_SENT, + IDX_STATS_SESSION_BYTES_SENT, + IDX_STATS_SESSION_PKT_RECV, + IDX_STATS_SESSION_BYTES_RECV, + IDX_STATS_SESSION_PKT_LOST, + IDX_STATS_SESSION_BYTES_LOST, + IDX_STATS_SESSION_PING_RECV, + IDX_STATS_SESSION_PKT_DISCARDED, IDX_STATS_SESSION_DATAGRAMS_RECEIVED, IDX_STATS_SESSION_DATAGRAMS_SENT, IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED, IDX_STATS_SESSION_DATAGRAMS_LOST, + IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT, + IDX_STATS_SESSION_COUNT, IDX_STATS_STREAM_CREATED_AT, IDX_STATS_STREAM_OPENED_AT, @@ -92,6 +115,9 @@ const { IDX_STATS_STREAM_MAX_OFFSET_ACK, IDX_STATS_STREAM_MAX_OFFSET_RECV, IDX_STATS_STREAM_FINAL_SIZE, + IDX_STATS_STREAM_BYTES_ACCUMULATED, + IDX_STATS_STREAM_MAX_BYTES_ACCUMULATED, + IDX_STATS_STREAM_COUNT, } = internalBinding('quic'); assert(IDX_STATS_ENDPOINT_CREATED_AT !== undefined); @@ -104,15 +130,21 @@ assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined); assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS !== undefined); assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT !== undefined); assert(IDX_STATS_ENDPOINT_RETRY_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED !== undefined); assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED !== undefined); assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED !== undefined); assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED !== undefined); +assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED !== undefined); +assert(IDX_STATS_ENDPOINT_PACKETS_BLOCKED !== undefined); assert(IDX_STATS_SESSION_CREATED_AT !== undefined); +assert(IDX_STATS_SESSION_DESTROYED_AT !== undefined); assert(IDX_STATS_SESSION_CLOSING_AT !== undefined); assert(IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT !== undefined); assert(IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT !== undefined); assert(IDX_STATS_SESSION_BYTES_RECEIVED !== undefined); -assert(IDX_STATS_SESSION_BYTES_SENT !== undefined); assert(IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT !== undefined); assert(IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT !== undefined); assert(IDX_STATS_SESSION_UNI_IN_STREAM_COUNT !== undefined); @@ -126,10 +158,19 @@ assert(IDX_STATS_SESSION_MIN_RTT !== undefined); assert(IDX_STATS_SESSION_RTTVAR !== undefined); assert(IDX_STATS_SESSION_SMOOTHED_RTT !== undefined); assert(IDX_STATS_SESSION_SSTHRESH !== undefined); +assert(IDX_STATS_SESSION_PKT_SENT !== undefined); +assert(IDX_STATS_SESSION_BYTES_SENT !== undefined); +assert(IDX_STATS_SESSION_PKT_RECV !== undefined); +assert(IDX_STATS_SESSION_BYTES_RECV !== undefined); +assert(IDX_STATS_SESSION_PKT_LOST !== undefined); +assert(IDX_STATS_SESSION_BYTES_LOST !== undefined); +assert(IDX_STATS_SESSION_PING_RECV !== undefined); +assert(IDX_STATS_SESSION_PKT_DISCARDED !== undefined); assert(IDX_STATS_SESSION_DATAGRAMS_RECEIVED !== undefined); assert(IDX_STATS_SESSION_DATAGRAMS_SENT !== undefined); assert(IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED !== undefined); assert(IDX_STATS_SESSION_DATAGRAMS_LOST !== undefined); +assert(IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT !== undefined); assert(IDX_STATS_STREAM_CREATED_AT !== undefined); assert(IDX_STATS_STREAM_OPENED_AT !== undefined); assert(IDX_STATS_STREAM_RECEIVED_AT !== undefined); @@ -141,6 +182,25 @@ assert(IDX_STATS_STREAM_MAX_OFFSET !== undefined); assert(IDX_STATS_STREAM_MAX_OFFSET_ACK !== undefined); assert(IDX_STATS_STREAM_MAX_OFFSET_RECV !== undefined); assert(IDX_STATS_STREAM_FINAL_SIZE !== undefined); +assert(IDX_STATS_STREAM_BYTES_ACCUMULATED !== undefined); +assert(IDX_STATS_STREAM_MAX_BYTES_ACCUMULATED !== undefined); +assert(IDX_STATS_STREAM_COUNT !== undefined); +assert(IDX_STATS_SESSION_COUNT !== undefined); + +const kCreateDisconnected = Symbol('kCreateDisconnected'); + +let assertIsQuicEndpointStats; +let assertIsQuicSessionStats; +let assertIsQuicStreamStats; +let isQuicEndpointStats; +let isQuicSessionStats; +let isQuicStreamStats; + +function assertIsPrivateConstructor(privateSymbol) { + if (privateSymbol !== kPrivateConstructor) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } +} class QuicEndpointStats { /** @type {BigUint64Array} */ @@ -148,6 +208,18 @@ class QuicEndpointStats { /** @type {boolean} */ #disconnected = false; + static { + isQuicEndpointStats = function(val) { + return val != null && typeof val === 'object' && #handle in val; + }; + + assertIsQuicEndpointStats = function(val) { + if (!isQuicEndpointStats(val)) { + throw new ERR_INVALID_THIS('QuicEndpointStats'); + } + }; + } + /** * @param {symbol} privateSymbol * @param {ArrayBuffer} buffer @@ -155,9 +227,7 @@ class QuicEndpointStats { constructor(privateSymbol, buffer) { // We use the kPrivateConstructor symbol to restrict the ability to // create new instances of QuicEndpointStats to internal code. - if (privateSymbol !== kPrivateConstructor) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); - } + assertIsPrivateConstructor(privateSymbol); if (!isArrayBuffer(buffer)) { throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); } @@ -166,119 +236,227 @@ class QuicEndpointStats { /** @type {bigint} */ get createdAt() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_CREATED_AT]; } /** @type {bigint} */ get destroyedAt() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_DESTROYED_AT]; } /** @type {bigint} */ get bytesReceived() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_BYTES_RECEIVED]; } /** @type {bigint} */ get bytesSent() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_BYTES_SENT]; } /** @type {bigint} */ get packetsReceived() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_PACKETS_RECEIVED]; } /** @type {bigint} */ get packetsSent() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_PACKETS_SENT]; } /** @type {bigint} */ get serverSessions() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_SERVER_SESSIONS]; } /** @type {bigint} */ get clientSessions() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_CLIENT_SESSIONS]; } /** @type {bigint} */ get serverBusyCount() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT]; } /** @type {bigint} */ get retryCount() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT]; } + /** @type {bigint} */ + get retryRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED]; + } + /** @type {bigint} */ get versionNegotiationCount() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT]; } + /** @type {bigint} */ + get versionNegotiationRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED]; + } + /** @type {bigint} */ get statelessResetCount() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT]; } + /** @type {bigint} */ + get statelessResetRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED]; + } + /** @type {bigint} */ get immediateCloseCount() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT]; } + /** @type {bigint} */ + get immediateCloseRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED]; + } + + /** @type {bigint} */ + get sessionCreationRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED]; + } + + /** @type {bigint} */ + get packetsBlocked() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_PACKETS_BLOCKED]; + } + toString() { return JSONStringify(this.toJSON()); } toJSON() { + assertIsQuicEndpointStats(this); + const { + createdAt, + destroyedAt, + bytesReceived, + bytesSent, + packetsReceived, + packetsSent, + serverSessions, + clientSessions, + serverBusyCount, + retryCount, + retryRateLimited, + versionNegotiationCount, + versionNegotiationRateLimited, + statelessResetCount, + statelessResetRateLimited, + immediateCloseCount, + immediateCloseRateLimited, + sessionCreationRateLimited, + packetsBlocked, + } = this; return { __proto__: null, connected: this.isConnected, // We need to convert the values to strings because JSON does not // support BigInts. - createdAt: `${this.createdAt}`, - destroyedAt: `${this.destroyedAt}`, - bytesReceived: `${this.bytesReceived}`, - bytesSent: `${this.bytesSent}`, - packetsReceived: `${this.packetsReceived}`, - packetsSent: `${this.packetsSent}`, - serverSessions: `${this.serverSessions}`, - clientSessions: `${this.clientSessions}`, - serverBusyCount: `${this.serverBusyCount}`, - retryCount: `${this.retryCount}`, - versionNegotiationCount: `${this.versionNegotiationCount}`, - statelessResetCount: `${this.statelessResetCount}`, - immediateCloseCount: `${this.immediateCloseCount}`, + createdAt: `${createdAt}`, + destroyedAt: `${destroyedAt}`, + bytesReceived: `${bytesReceived}`, + bytesSent: `${bytesSent}`, + packetsReceived: `${packetsReceived}`, + packetsSent: `${packetsSent}`, + serverSessions: `${serverSessions}`, + clientSessions: `${clientSessions}`, + serverBusyCount: `${serverBusyCount}`, + retryCount: `${retryCount}`, + retryRateLimited: `${retryRateLimited}`, + versionNegotiationCount: `${versionNegotiationCount}`, + versionNegotiationRateLimited: `${versionNegotiationRateLimited}`, + statelessResetCount: `${statelessResetCount}`, + statelessResetRateLimited: `${statelessResetRateLimited}`, + immediateCloseCount: `${immediateCloseCount}`, + immediateCloseRateLimited: `${immediateCloseRateLimited}`, + sessionCreationRateLimited: `${sessionCreationRateLimited}`, + packetsBlocked: `${packetsBlocked}`, }; } [kInspect](depth, options) { - if (depth < 0) - return this; + assertIsQuicEndpointStats(this); + if (depth < 0) { + return 'QuicEndpointStats { }'; + } const opts = { + __proto__: null, ...options, depth: options.depth == null ? null : options.depth - 1, }; + const { + createdAt, + destroyedAt, + bytesReceived, + bytesSent, + packetsReceived, + packetsSent, + serverSessions, + clientSessions, + serverBusyCount, + retryCount, + retryRateLimited, + versionNegotiationCount, + versionNegotiationRateLimited, + statelessResetCount, + statelessResetRateLimited, + immediateCloseCount, + immediateCloseRateLimited, + sessionCreationRateLimited, + packetsBlocked, + } = this; + return `QuicEndpointStats ${inspect({ connected: this.isConnected, - createdAt: this.createdAt, - destroyedAt: this.destroyedAt, - bytesReceived: this.bytesReceived, - bytesSent: this.bytesSent, - packetsReceived: this.packetsReceived, - packetsSent: this.packetsSent, - serverSessions: this.serverSessions, - clientSessions: this.clientSessions, - serverBusyCount: this.serverBusyCount, - retryCount: this.retryCount, - versionNegotiationCount: this.versionNegotiationCount, - statelessResetCount: this.statelessResetCount, - immediateCloseCount: this.immediateCloseCount, + createdAt, + destroyedAt, + bytesReceived, + bytesSent, + packetsReceived, + packetsSent, + serverSessions, + clientSessions, + serverBusyCount, + retryCount, + retryRateLimited, + versionNegotiationCount, + versionNegotiationRateLimited, + statelessResetCount, + statelessResetRateLimited, + immediateCloseCount, + immediateCloseRateLimited, + sessionCreationRateLimited, + packetsBlocked, }, opts)}`; } @@ -286,15 +464,14 @@ class QuicEndpointStats { * True if this QuicEndpointStats object is still connected to the underlying * Endpoint stats source. If this returns false, then the stats object is * no longer being updated and should be considered stale. - * @returns {boolean} + * @type {boolean} */ get isConnected() { + assertIsQuicEndpointStats(this); return !this.#disconnected; } [kFinishClose]() { - // Snapshot the stats into a new BigUint64Array since the underlying - // buffer will be destroyed. this.#handle = new BigUint64Array(this.#handle); this.#disconnected = true; } @@ -303,138 +480,222 @@ class QuicEndpointStats { class QuicSessionStats { /** @type {BigUint64Array} */ #handle; - /** @type {boolean} */ #disconnected = false; + #offset = 0; + + static { + isQuicSessionStats = function(val) { + return val != null && typeof val === 'object' && #handle in val; + }; + + assertIsQuicSessionStats = function(val) { + if (!isQuicSessionStats(val)) { + throw new ERR_INVALID_THIS('QuicSessionStats'); + } + }; + } + /** * @param {symbol} privateSymbol - * @param {BigUint64Array} buffer + * @param {ArrayBuffer} view + * @param {number} [byteOffset] */ - constructor(privateSymbol, buffer) { + constructor(privateSymbol, view, byteOffset = 0) { // We use the kPrivateConstructor symbol to restrict the ability to // create new instances of QuicSessionStats to internal code. - if (privateSymbol !== kPrivateConstructor) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + assertIsPrivateConstructor(privateSymbol); + if (isArrayBuffer(view)) { + this.#handle = new BigUint64Array(view); + } else { + this.#handle = view; } - if (!isArrayBuffer(buffer)) { - throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); - } - this.#handle = new BigUint64Array(buffer); + this.#offset = byteOffset / 8; } /** @type {bigint} */ get createdAt() { - return this.#handle[IDX_STATS_SESSION_CREATED_AT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_CREATED_AT]; + } + + /** @type {bigint} */ + get destroyedAt() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_DESTROYED_AT]; } /** @type {bigint} */ get closingAt() { - return this.#handle[IDX_STATS_SESSION_CLOSING_AT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_CLOSING_AT]; } /** @type {bigint} */ get handshakeCompletedAt() { - return this.#handle[IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT]; } /** @type {bigint} */ get handshakeConfirmedAt() { - return this.#handle[IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT]; } /** @type {bigint} */ get bytesReceived() { - return this.#handle[IDX_STATS_SESSION_BYTES_RECEIVED]; - } - - /** @type {bigint} */ - get bytesSent() { - return this.#handle[IDX_STATS_SESSION_BYTES_SENT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BYTES_RECEIVED]; } /** @type {bigint} */ get bidiInStreamCount() { - return this.#handle[IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT]; } /** @type {bigint} */ get bidiOutStreamCount() { - return this.#handle[IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT]; } /** @type {bigint} */ get uniInStreamCount() { - return this.#handle[IDX_STATS_SESSION_UNI_IN_STREAM_COUNT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_UNI_IN_STREAM_COUNT]; } /** @type {bigint} */ get uniOutStreamCount() { - return this.#handle[IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT]; } /** @type {bigint} */ - get maxBytesInFlights() { - return this.#handle[IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT]; + get maxBytesInFlight() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT]; } /** @type {bigint} */ get bytesInFlight() { - return this.#handle[IDX_STATS_SESSION_BYTES_IN_FLIGHT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BYTES_IN_FLIGHT]; } /** @type {bigint} */ get blockCount() { - return this.#handle[IDX_STATS_SESSION_BLOCK_COUNT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BLOCK_COUNT]; } /** @type {bigint} */ get cwnd() { - return this.#handle[IDX_STATS_SESSION_CWND]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_CWND]; } /** @type {bigint} */ get latestRtt() { - return this.#handle[IDX_STATS_SESSION_LATEST_RTT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_LATEST_RTT]; } /** @type {bigint} */ get minRtt() { - return this.#handle[IDX_STATS_SESSION_MIN_RTT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_MIN_RTT]; } /** @type {bigint} */ get rttVar() { - return this.#handle[IDX_STATS_SESSION_RTTVAR]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_RTTVAR]; } /** @type {bigint} */ get smoothedRtt() { - return this.#handle[IDX_STATS_SESSION_SMOOTHED_RTT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_SMOOTHED_RTT]; } /** @type {bigint} */ get ssthresh() { - return this.#handle[IDX_STATS_SESSION_SSTHRESH]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_SSTHRESH]; + } + + get pktSent() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_PKT_SENT]; + } + + get bytesSent() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BYTES_SENT]; + } + + get pktRecv() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_PKT_RECV]; + } + + get bytesRecv() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BYTES_RECV]; + } + + get pktLost() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_PKT_LOST]; + } + + get bytesLost() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BYTES_LOST]; + } + + get pingRecv() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_PING_RECV]; + } + + get pktDiscarded() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_PKT_DISCARDED]; } /** @type {bigint} */ get datagramsReceived() { - return this.#handle[IDX_STATS_SESSION_DATAGRAMS_RECEIVED]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_DATAGRAMS_RECEIVED]; } /** @type {bigint} */ get datagramsSent() { - return this.#handle[IDX_STATS_SESSION_DATAGRAMS_SENT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_DATAGRAMS_SENT]; } /** @type {bigint} */ get datagramsAcknowledged() { - return this.#handle[IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED]; } /** @type {bigint} */ get datagramsLost() { - return this.#handle[IDX_STATS_SESSION_DATAGRAMS_LOST]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_DATAGRAMS_LOST]; + } + + /** @type {bigint} */ + get streamsIdleTimedOut() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + + IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT]; } toString() { @@ -442,75 +703,157 @@ class QuicSessionStats { } toJSON() { + assertIsQuicSessionStats(this); + const { + createdAt, + closingAt, + handshakeCompletedAt, + handshakeConfirmedAt, + bytesReceived, + bidiInStreamCount, + bidiOutStreamCount, + uniInStreamCount, + uniOutStreamCount, + maxBytesInFlight, + bytesInFlight, + blockCount, + cwnd, + latestRtt, + minRtt, + rttVar, + smoothedRtt, + ssthresh, + pktSent, + bytesSent, + pktRecv, + bytesRecv, + pktLost, + bytesLost, + pingRecv, + pktDiscarded, + datagramsReceived, + datagramsSent, + datagramsAcknowledged, + datagramsLost, + streamsIdleTimedOut, + } = this; return { __proto__: null, connected: this.isConnected, // We need to convert the values to strings because JSON does not // support BigInts. - createdAt: `${this.createdAt}`, - closingAt: `${this.closingAt}`, - destroyedAt: `${this.destroyedAt}`, - handshakeCompletedAt: `${this.handshakeCompletedAt}`, - handshakeConfirmedAt: `${this.handshakeConfirmedAt}`, - gracefulClosingAt: `${this.gracefulClosingAt}`, - bytesReceived: `${this.bytesReceived}`, - bytesSent: `${this.bytesSent}`, - bidiInStreamCount: `${this.bidiInStreamCount}`, - bidiOutStreamCount: `${this.bidiOutStreamCount}`, - uniInStreamCount: `${this.uniInStreamCount}`, - uniOutStreamCount: `${this.uniOutStreamCount}`, - maxBytesInFlights: `${this.maxBytesInFlights}`, - bytesInFlight: `${this.bytesInFlight}`, - blockCount: `${this.blockCount}`, - cwnd: `${this.cwnd}`, - latestRtt: `${this.latestRtt}`, - minRtt: `${this.minRtt}`, - rttVar: `${this.rttVar}`, - smoothedRtt: `${this.smoothedRtt}`, - ssthresh: `${this.ssthresh}`, - datagramsReceived: `${this.datagramsReceived}`, - datagramsSent: `${this.datagramsSent}`, - datagramsAcknowledged: `${this.datagramsAcknowledged}`, - datagramsLost: `${this.datagramsLost}`, + createdAt: `${createdAt}`, + closingAt: `${closingAt}`, + handshakeCompletedAt: `${handshakeCompletedAt}`, + handshakeConfirmedAt: `${handshakeConfirmedAt}`, + bytesReceived: `${bytesReceived}`, + bidiInStreamCount: `${bidiInStreamCount}`, + bidiOutStreamCount: `${bidiOutStreamCount}`, + uniInStreamCount: `${uniInStreamCount}`, + uniOutStreamCount: `${uniOutStreamCount}`, + maxBytesInFlight: `${maxBytesInFlight}`, + bytesInFlight: `${bytesInFlight}`, + blockCount: `${blockCount}`, + cwnd: `${cwnd}`, + latestRtt: `${latestRtt}`, + minRtt: `${minRtt}`, + rttVar: `${rttVar}`, + smoothedRtt: `${smoothedRtt}`, + ssthresh: `${ssthresh}`, + pktSent: `${pktSent}`, + bytesSent: `${bytesSent}`, + pktRecv: `${pktRecv}`, + bytesRecv: `${bytesRecv}`, + pktLost: `${pktLost}`, + bytesLost: `${bytesLost}`, + pingRecv: `${pingRecv}`, + pktDiscarded: `${pktDiscarded}`, + datagramsReceived: `${datagramsReceived}`, + datagramsSent: `${datagramsSent}`, + datagramsAcknowledged: `${datagramsAcknowledged}`, + datagramsLost: `${datagramsLost}`, + streamsIdleTimedOut: `${streamsIdleTimedOut}`, }; } [kInspect](depth, options) { - if (depth < 0) - return this; + if (depth < 0) { + return 'QuicSessionStats { }'; + } const opts = { + __proto__: null, ...options, depth: options.depth == null ? null : options.depth - 1, }; + const { + createdAt, + closingAt, + handshakeCompletedAt, + handshakeConfirmedAt, + bytesReceived, + bidiInStreamCount, + bidiOutStreamCount, + uniInStreamCount, + uniOutStreamCount, + maxBytesInFlight, + bytesInFlight, + blockCount, + cwnd, + latestRtt, + minRtt, + rttVar, + smoothedRtt, + ssthresh, + pktSent, + bytesSent, + pktRecv, + bytesRecv, + pktLost, + bytesLost, + pingRecv, + pktDiscarded, + datagramsReceived, + datagramsSent, + datagramsAcknowledged, + datagramsLost, + streamsIdleTimedOut, + } = this; + return `QuicSessionStats ${inspect({ connected: this.isConnected, - createdAt: this.createdAt, - closingAt: this.closingAt, - destroyedAt: this.destroyedAt, - handshakeCompletedAt: this.handshakeCompletedAt, - handshakeConfirmedAt: this.handshakeConfirmedAt, - gracefulClosingAt: this.gracefulClosingAt, - bytesReceived: this.bytesReceived, - bytesSent: this.bytesSent, - bidiInStreamCount: this.bidiInStreamCount, - bidiOutStreamCount: this.bidiOutStreamCount, - uniInStreamCount: this.uniInStreamCount, - uniOutStreamCount: this.uniOutStreamCount, - maxBytesInFlights: this.maxBytesInFlights, - bytesInFlight: this.bytesInFlight, - blockCount: this.blockCount, - cwnd: this.cwnd, - latestRtt: this.latestRtt, - minRtt: this.minRtt, - rttVar: this.rttVar, - smoothedRtt: this.smoothedRtt, - ssthresh: this.ssthresh, - datagramsReceived: this.datagramsReceived, - datagramsSent: this.datagramsSent, - datagramsAcknowledged: this.datagramsAcknowledged, - datagramsLost: this.datagramsLost, + createdAt, + closingAt, + handshakeCompletedAt, + handshakeConfirmedAt, + bytesReceived, + bidiInStreamCount, + bidiOutStreamCount, + uniInStreamCount, + uniOutStreamCount, + maxBytesInFlight, + bytesInFlight, + blockCount, + cwnd, + latestRtt, + minRtt, + rttVar, + smoothedRtt, + ssthresh, + pktSent, + bytesSent, + pktRecv, + bytesRecv, + pktLost, + bytesLost, + pingRecv, + pktDiscarded, + datagramsReceived, + datagramsSent, + datagramsAcknowledged, + datagramsLost, + streamsIdleTimedOut, }, opts)}`; } @@ -518,16 +861,18 @@ class QuicSessionStats { * True if this QuicSessionStats object is still connected to the underlying * Session stats source. If this returns false, then the stats object is * no longer being updated and should be considered stale. - * @returns {boolean} + * @type {boolean} */ get isConnected() { return !this.#disconnected; } [kFinishClose]() { - // Snapshot the stats into a new BigUint64Array since the underlying - // buffer will be destroyed. - this.#handle = new BigUint64Array(this.#handle); + const view = TypedArrayPrototypeSubarray(this.#handle, + this.#offset, + this.#offset + IDX_STATS_STREAM_COUNT); + this.#handle = new BigUint64Array(view); + this.#offset = 0; this.#disconnected = true; } } @@ -535,78 +880,114 @@ class QuicSessionStats { class QuicStreamStats { /** @type {BigUint64Array} */ #handle; - /** type {boolean} */ + #offset = 0; #disconnected = false; + static { + isQuicStreamStats = function(val) { + return val != null && typeof val === 'object' && #handle in val; + }; + + assertIsQuicStreamStats = function(val) { + if (!isQuicStreamStats(val)) { + throw new ERR_INVALID_THIS('QuicStreamStats'); + } + }; + } + /** * @param {symbol} privateSymbol - * @param {ArrayBuffer} buffer + * @param {BigUint64Array|ArrayBuffer} view + * @param {number} [byteOffset] - byte offset into the shared page view */ - constructor(privateSymbol, buffer) { + constructor(privateSymbol, view, byteOffset = 0) { // We use the kPrivateConstructor symbol to restrict the ability to // create new instances of QuicStreamStats to internal code. - if (privateSymbol !== kPrivateConstructor) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + assertIsPrivateConstructor(privateSymbol); + if (isArrayBuffer(view)) { + this.#handle = new BigUint64Array(view); + } else { + this.#handle = view; } - if (!isArrayBuffer(buffer)) { - throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); - } - this.#handle = new BigUint64Array(buffer); + this.#offset = byteOffset / 8; } /** @type {bigint} */ get createdAt() { - return this.#handle[IDX_STATS_STREAM_CREATED_AT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_CREATED_AT]; } /** @type {bigint} */ get openedAt() { - return this.#handle[IDX_STATS_STREAM_OPENED_AT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_OPENED_AT]; } /** @type {bigint} */ get receivedAt() { - return this.#handle[IDX_STATS_STREAM_RECEIVED_AT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_RECEIVED_AT]; } /** @type {bigint} */ get ackedAt() { - return this.#handle[IDX_STATS_STREAM_ACKED_AT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_ACKED_AT]; } /** @type {bigint} */ get destroyedAt() { - return this.#handle[IDX_STATS_STREAM_DESTROYED_AT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_DESTROYED_AT]; } /** @type {bigint} */ get bytesReceived() { - return this.#handle[IDX_STATS_STREAM_BYTES_RECEIVED]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_BYTES_RECEIVED]; } /** @type {bigint} */ get bytesSent() { - return this.#handle[IDX_STATS_STREAM_BYTES_SENT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_BYTES_SENT]; } /** @type {bigint} */ get maxOffset() { - return this.#handle[IDX_STATS_STREAM_MAX_OFFSET]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_MAX_OFFSET]; } /** @type {bigint} */ get maxOffsetAcknowledged() { - return this.#handle[IDX_STATS_STREAM_MAX_OFFSET_ACK]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_MAX_OFFSET_ACK]; } /** @type {bigint} */ get maxOffsetReceived() { - return this.#handle[IDX_STATS_STREAM_MAX_OFFSET_RECV]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_MAX_OFFSET_RECV]; } /** @type {bigint} */ get finalSize() { - return this.#handle[IDX_STATS_STREAM_FINAL_SIZE]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_FINAL_SIZE]; + } + + /** @type {bigint} Current bytes in the receive accumulation buffer. */ + get bytesAccumulated() { + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_BYTES_ACCUMULATED]; + } + + /** @type {bigint} Peak bytes accumulated over the stream's lifetime. */ + get maxBytesAccumulated() { + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_MAX_BYTES_ACCUMULATED]; } toString() { @@ -614,47 +995,85 @@ class QuicStreamStats { } toJSON() { + assertIsQuicStreamStats(this); + const { + createdAt, + openedAt, + receivedAt, + ackedAt, + destroyedAt, + bytesReceived, + bytesSent, + maxOffset, + maxOffsetAcknowledged, + maxOffsetReceived, + finalSize, + bytesAccumulated, + maxBytesAccumulated, + } = this; return { __proto__: null, connected: this.isConnected, // We need to convert the values to strings because JSON does not // support BigInts. - createdAt: `${this.createdAt}`, - openedAt: `${this.openedAt}`, - receivedAt: `${this.receivedAt}`, - ackedAt: `${this.ackedAt}`, - destroyedAt: `${this.destroyedAt}`, - bytesReceived: `${this.bytesReceived}`, - bytesSent: `${this.bytesSent}`, - maxOffset: `${this.maxOffset}`, - maxOffsetAcknowledged: `${this.maxOffsetAcknowledged}`, - maxOffsetReceived: `${this.maxOffsetReceived}`, - finalSize: `${this.finalSize}`, + createdAt: `${createdAt}`, + openedAt: `${openedAt}`, + receivedAt: `${receivedAt}`, + ackedAt: `${ackedAt}`, + destroyedAt: `${destroyedAt}`, + bytesReceived: `${bytesReceived}`, + bytesSent: `${bytesSent}`, + maxOffset: `${maxOffset}`, + maxOffsetAcknowledged: `${maxOffsetAcknowledged}`, + maxOffsetReceived: `${maxOffsetReceived}`, + finalSize: `${finalSize}`, + bytesAccumulated: `${bytesAccumulated}`, + maxBytesAccumulated: `${maxBytesAccumulated}`, }; } [kInspect](depth, options) { - if (depth < 0) - return this; + if (depth < 0) { + return 'QuicStreamStats { }'; + } const opts = { + __proto__: null, ...options, depth: options.depth == null ? null : options.depth - 1, }; - return `StreamStats ${inspect({ + const { + createdAt, + openedAt, + receivedAt, + ackedAt, + destroyedAt, + bytesReceived, + bytesSent, + maxOffset, + maxOffsetAcknowledged, + maxOffsetReceived, + finalSize, + bytesAccumulated, + maxBytesAccumulated, + } = this; + + return `QuicStreamStats ${inspect({ connected: this.isConnected, - createdAt: this.createdAt, - openedAt: this.openedAt, - receivedAt: this.receivedAt, - ackedAt: this.ackedAt, - destroyedAt: this.destroyedAt, - bytesReceived: this.bytesReceived, - bytesSent: this.bytesSent, - maxOffset: this.maxOffset, - maxOffsetAcknowledged: this.maxOffsetAcknowledged, - maxOffsetReceived: this.maxOffsetReceived, - finalSize: this.finalSize, + createdAt, + openedAt, + receivedAt, + ackedAt, + destroyedAt, + bytesReceived, + bytesSent, + maxOffset, + maxOffsetAcknowledged, + maxOffsetReceived, + finalSize, + bytesAccumulated, + maxBytesAccumulated, }, opts)}`; } @@ -662,22 +1081,36 @@ class QuicStreamStats { * True if this QuicStreamStats object is still connected to the underlying * Stream stats source. If this returns false, then the stats object is * no longer being updated and should be considered stale. - * @returns {boolean} + * @type {boolean} */ get isConnected() { return !this.#disconnected; } [kFinishClose]() { - // Snapshot the stats into a new BigUint64Array since the underlying - // buffer will be destroyed. - this.#handle = new BigUint64Array(this.#handle); + const view = TypedArrayPrototypeSubarray(this.#handle, + this.#offset, + this.#offset + IDX_STATS_STREAM_COUNT); + this.#handle = new BigUint64Array(view); + this.#offset = 0; this.#disconnected = true; } + + // Creates an immediately disconnected QuicStreamStats object. Used when + // lazily creating stats for a stream that has already been destroyed. + static [kCreateDisconnected]() { + const count = IDX_STATS_STREAM_COUNT; + const stats = new QuicStreamStats(kPrivateConstructor, new BigUint64Array(count), 0); + stats.#disconnected = true; + return stats; + } } module.exports = { QuicEndpointStats, QuicSessionStats, QuicStreamStats, + kCreateDisconnected, }; + +/* c8 ignore stop */ diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index e2787d842d619b..665ab7ca1911c2 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -1,5 +1,9 @@ 'use strict'; +// TODO(@jasnell) Temporarily ignoring c8 covrerage for this file while tests +// are still being developed. +/* c8 ignore start */ + const { Symbol, } = primordials; @@ -23,18 +27,27 @@ const { // Symbols used to hide various private properties and methods from the // public API. +const kAttachFileHandle = Symbol('kAttachFileHandle'); const kBlocked = Symbol('kBlocked'); const kConnect = Symbol('kConnect'); +const kDrain = Symbol('kDrain'); const kDatagram = Symbol('kDatagram'); const kDatagramStatus = Symbol('kDatagramStatus'); +const kEarlyDataRejected = Symbol('kEarlyDataRejected'); const kFinishClose = Symbol('kFinishClose'); +const kGoaway = Symbol('kGoaway'); const kHandshake = Symbol('kHandshake'); +const kHandshakeCompleted = Symbol('kHandshakeCompleted'); +const kVerifyPeer = Symbol('kVerifyPeer'); const kHeaders = Symbol('kHeaders'); +const kKeylog = Symbol('kKeylog'); const kListen = Symbol('kListen'); +const kQlog = Symbol('kQlog'); const kNewSession = Symbol('kNewSession'); const kNewStream = Symbol('kNewStream'); -const kOnHeaders = Symbol('kOnHeaders'); -const kOnTrailers = Symbol('kOwnTrailers'); +const kNewToken = Symbol('kNewToken'); +const kStreamCallbacks = Symbol('kStreamCallbacks'); +const kOrigin = Symbol('kOrigin'); const kOwner = Symbol('kOwner'); const kPathValidation = Symbol('kPathValidation'); const kPrivateConstructor = Symbol('kPrivateConstructor'); @@ -42,39 +55,46 @@ const kRemoveSession = Symbol('kRemoveSession'); const kRemoveStream = Symbol('kRemoveStream'); const kReset = Symbol('kReset'); const kSendHeaders = Symbol('kSendHeaders'); +const kSessionApplication = Symbol('kSessionApplication'); const kSessionTicket = Symbol('kSessionTicket'); -const kState = Symbol('kState'); const kTrailers = Symbol('kTrailers'); const kVersionNegotiation = Symbol('kVersionNegotiation'); -const kWantsHeaders = Symbol('kWantsHeaders'); -const kWantsTrailers = Symbol('kWantsTrailers'); module.exports = { + kAttachFileHandle, kBlocked, kConnect, kDatagram, kDatagramStatus, + kDrain, + kEarlyDataRejected, kFinishClose, + kGoaway, kHandshake, + kHandshakeCompleted, + kVerifyPeer, kHeaders, kInspect, + kKeylog, kKeyObjectHandle, kListen, kNewSession, kNewStream, - kOnHeaders, - kOnTrailers, + kNewToken, + kStreamCallbacks, + kOrigin, kOwner, + kQlog, kPathValidation, kPrivateConstructor, kRemoveSession, kRemoveStream, kReset, kSendHeaders, + kSessionApplication, kSessionTicket, - kState, kTrailers, kVersionNegotiation, - kWantsHeaders, - kWantsTrailers, }; + +/* c8 ignore stop */ diff --git a/lib/internal/streams/end-of-stream.js b/lib/internal/streams/end-of-stream.js index 867076410cddff..9566954f5db508 100644 --- a/lib/internal/streams/end-of-stream.js +++ b/lib/internal/streams/end-of-stream.js @@ -6,6 +6,8 @@ const { Promise, PromisePrototypeThen, + ReflectApply, + Symbol, SymbolDispose, } = primordials; @@ -62,6 +64,50 @@ function bindAsyncResource(fn, type) { }; } +/** + * Returns the current stream error tracked by eos(), if any. + * @param {import('stream').Stream} stream + * @returns {Error | null} + */ +function getEosErrored(stream) { + const errored = isWritableErrored(stream) || isReadableErrored(stream); + return typeof errored !== 'boolean' && errored || null; +} + +/** + * Returns the error eos() would report from an immediate close, including + * premature close detection for unfinished readable or writable sides. + * @param {import('stream').Stream} stream + * @param {boolean} readable + * @param {boolean | null} readableFinished + * @param {boolean} writable + * @param {boolean | null} writableFinished + * @returns {Error | null} + */ +function getEosOnCloseError(stream, readable, readableFinished, writable, writableFinished) { + const errored = getEosErrored(stream); + if (errored) { + return errored; + } + + if (readable && !readableFinished && isReadableNodeStream(stream, true)) { + if (!isReadableFinished(stream, false)) { + return new ERR_STREAM_PREMATURE_CLOSE(); + } + } + if (writable && !writableFinished) { + if (!isWritableFinished(stream, false)) { + return new ERR_STREAM_PREMATURE_CLOSE(); + } + } + + return null; +} + +// Internal only: if eos() can settle immediately, invoke the callback before +// returning cleanup. Callers must tolerate cleanup yet to be assigned. +const kEosNodeSynchronousCallback = Symbol('kEosNodeSynchronousCallback'); + function eos(stream, options, callback) { if (arguments.length === 2) { callback = options; @@ -74,10 +120,6 @@ function eos(stream, options, callback) { validateFunction(callback, 'callback'); validateAbortSignal(options.signal, 'options.signal'); - // Avoid AsyncResource.bind() because it calls ObjectDefineProperties which - // is a bottleneck here. - callback = once(bindAsyncResource(callback, 'STREAM_END_OF_STREAM')); - if (isReadableStream(stream) || isWritableStream(stream)) { return eosWeb(stream, options, callback); } @@ -89,15 +131,6 @@ function eos(stream, options, callback) { const readable = options.readable ?? isReadableNodeStream(stream); const writable = options.writable ?? isWritableNodeStream(stream); - const wState = stream._writableState; - const rState = stream._readableState; - - const onlegacyfinish = () => { - if (!stream.writable) { - onfinish(); - } - }; - // TODO (ronag): Improve soft detection to include core modules and // common ecosystem modules that do properly emit 'close' but fail // this generic check. @@ -106,8 +139,83 @@ function eos(stream, options, callback) { isReadableNodeStream(stream) === readable && isWritableNodeStream(stream) === writable ); - let writableFinished = isWritableFinished(stream, false); + let readableFinished = isReadableFinished(stream, false); + + const wState = stream._writableState; + const rState = stream._readableState; + + /** + * @type {Error | null | undefined} + * undefined: to be determined + * null: no error + * Error: an error occurred + */ + let immediateResult; + if (isClosed(stream)) { + immediateResult = getEosOnCloseError( + stream, + readable, + readableFinished, + writable, + writableFinished, + ); + } else if (wState?.errorEmitted || rState?.errorEmitted) { + if (!willEmitClose) { + immediateResult = getEosErrored(stream); + } + } else if ( + !readable && + (!willEmitClose || isReadable(stream)) && + (writableFinished || isWritable(stream) === false) && + (wState == null || wState.pendingcb === undefined || wState.pendingcb === 0) + ) { + immediateResult = getEosErrored(stream); + } else if ( + !writable && + (!willEmitClose || isWritable(stream)) && + (readableFinished || isReadable(stream) === false) + ) { + immediateResult = getEosErrored(stream); + } else if ((rState && stream.req && stream.aborted)) { + immediateResult = getEosErrored(stream); + } + let cleanup = () => { + callback = nop; + }; + if (immediateResult !== undefined) { + if (options.error !== false) { + stream.on('error', nop); + cleanup = () => { + callback = nop; + stream.removeListener('error', nop); + }; + } + } else if (options.signal?.aborted) { + immediateResult = new AbortError(undefined, { cause: options.signal.reason }); + } + if (immediateResult !== undefined && options[kEosNodeSynchronousCallback]) { + ReflectApply(callback, stream, immediateResult === null ? [] : [immediateResult]); + return cleanup; + } + + // Avoid AsyncResource.bind() because it calls ObjectDefineProperties which + // is a bottleneck here. + callback = bindAsyncResource(callback, 'STREAM_END_OF_STREAM'); + + if (immediateResult !== undefined) { + process.nextTick(() => ReflectApply(callback, stream, immediateResult === null ? [] : [immediateResult])); + return cleanup; + } + + callback = once(callback); + + const onlegacyfinish = () => { + if (!stream.writable) { + onfinish(); + } + }; + const onfinish = () => { writableFinished = true; // Stream should not be destroyed here. If it is that @@ -126,7 +234,6 @@ function eos(stream, options, callback) { } }; - let readableFinished = isReadableFinished(stream, false); const onend = () => { readableFinished = true; // Stream should not be destroyed here. If it is that @@ -149,41 +256,13 @@ function eos(stream, options, callback) { callback.call(stream, err); }; - let closed = isClosed(stream); - const onclose = () => { - closed = true; - - const errored = isWritableErrored(stream) || isReadableErrored(stream); - - if (errored && typeof errored !== 'boolean') { - return callback.call(stream, errored); - } - - if (readable && !readableFinished && isReadableNodeStream(stream, true)) { - if (!isReadableFinished(stream, false)) - return callback.call(stream, - new ERR_STREAM_PREMATURE_CLOSE()); - } - if (writable && !writableFinished) { - if (!isWritableFinished(stream, false)) - return callback.call(stream, - new ERR_STREAM_PREMATURE_CLOSE()); - } - - callback.call(stream); - }; - - const onclosed = () => { - closed = true; - - const errored = isWritableErrored(stream) || isReadableErrored(stream); - - if (errored && typeof errored !== 'boolean') { - return callback.call(stream, errored); + const error = getEosOnCloseError(stream, readable, readableFinished, writable, writableFinished); + if (error === null) { + callback.call(stream); + } else { + callback.call(stream, error); } - - callback.call(stream); }; const onrequest = () => { @@ -217,30 +296,7 @@ function eos(stream, options, callback) { } stream.on('close', onclose); - if (closed) { - process.nextTick(onclose); - } else if (wState?.errorEmitted || rState?.errorEmitted) { - if (!willEmitClose) { - process.nextTick(onclosed); - } - } else if ( - !readable && - (!willEmitClose || isReadable(stream)) && - (writableFinished || isWritable(stream) === false) && - (wState == null || wState.pendingcb === undefined || wState.pendingcb === 0) - ) { - process.nextTick(onclosed); - } else if ( - !writable && - (!willEmitClose || isWritable(stream)) && - (readableFinished || isReadable(stream) === false) - ) { - process.nextTick(onclosed); - } else if ((rState && stream.req && stream.aborted)) { - process.nextTick(onclosed); - } - - const cleanup = () => { + cleanup = () => { callback = nop; stream.removeListener('aborted', onclose); stream.removeListener('complete', onfinish); @@ -255,7 +311,7 @@ function eos(stream, options, callback) { stream.removeListener('close', onclose); }; - if (options.signal && !closed) { + if (options.signal) { const abort = () => { // Keep it because cleanup removes it. const endCallback = callback; @@ -264,23 +320,23 @@ function eos(stream, options, callback) { stream, new AbortError(undefined, { cause: options.signal.reason })); }; - if (options.signal.aborted) { - process.nextTick(abort); - } else { - addAbortListener ??= require('internal/events/abort_listener').addAbortListener; - const disposable = addAbortListener(options.signal, abort); - const originalCallback = callback; - callback = once((...args) => { - disposable[SymbolDispose](); - originalCallback.apply(stream, args); - }); - } + addAbortListener ??= require('internal/events/abort_listener').addAbortListener; + const disposable = addAbortListener(options.signal, abort); + const originalCallback = callback; + callback = once((...args) => { + disposable[SymbolDispose](); + ReflectApply(originalCallback, stream, args); + }); } return cleanup; } function eosWeb(stream, options, callback) { + // Avoid AsyncResource.bind() because it calls ObjectDefineProperties which + // is a bottleneck here. + callback = once(bindAsyncResource(callback, 'STREAM_END_OF_STREAM')); + let isAborted = false; let abort = nop; if (options.signal) { @@ -339,4 +395,5 @@ function finished(stream, opts) { module.exports = { eos, finished, + kEosNodeSynchronousCallback, }; diff --git a/lib/internal/streams/iter/broadcast.js b/lib/internal/streams/iter/broadcast.js new file mode 100644 index 00000000000000..3592453036d303 --- /dev/null +++ b/lib/internal/streams/iter/broadcast.js @@ -0,0 +1,855 @@ +'use strict'; + +// New Streams API - Broadcast +// +// Push-model multi-consumer streaming. A single writer can push data to +// multiple consumers. Each consumer has an independent cursor into a +// shared buffer. + +const { + ArrayIsArray, + ArrayPrototypePush, + ArrayPrototypeShift, + MathMax, + PromisePrototypeThen, + PromiseReject, + PromiseResolve, + PromiseWithResolvers, + SafeSet, + Symbol, + SymbolAsyncDispose, + SymbolAsyncIterator, + SymbolDispose, + TypedArrayPrototypeGetByteLength, +} = primordials; + +const { lazyDOMException } = require('internal/util'); + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_RETURN_VALUE, + ERR_INVALID_STATE, + }, +} = require('internal/errors'); +const { + validateAbortSignal, + validateInteger, + validateObject, +} = require('internal/validators'); + +const { + broadcastProtocol, + drainableProtocol, +} = require('internal/streams/iter/types'); + +const { + from, + isAsyncIterable, + isSyncIterable, +} = require('internal/streams/iter/from'); + +const { + pull: pullWithTransforms, +} = require('internal/streams/iter/pull'); + +const { + kMultiConsumerDefaultHWM, + kResolvedPromise, + clampHWM, + convertChunks, + getMinCursor, + hasProtocol, + onSignalAbort, + parsePullArgs, + wrapError, + toUint8Array, + validateBackpressure, +} = require('internal/streams/iter/utils'); + +const { + RingBuffer, +} = require('internal/streams/iter/ringbuffer'); + +const kCancelWriter = Symbol('kCancelWriter'); +const kWrite = Symbol('kWrite'); +const kEnd = Symbol('kEnd'); +const kAbort = Symbol('kAbort'); +const kGetDesiredSize = Symbol('kGetDesiredSize'); +const kCanWrite = Symbol('kCanWrite'); +const kOnBufferDrained = Symbol('kOnBufferDrained'); + +// ============================================================================= +// Broadcast Implementation +// ============================================================================= + +class BroadcastImpl { + #buffer = new RingBuffer(); + #bufferStart = 0; + #consumers = new SafeSet(); + #waiters = []; // Consumers with pending resolve (subset of #consumers) + #ended = false; + #error = null; + #cancelled = false; + #options; + #writer = null; + #cachedMinCursor = 0; + #cachedMinCursorConsumers = 0; + + constructor(options) { + this.#options = options; + this[kOnBufferDrained] = null; + } + + setWriter(writer) { + this.#writer = writer; + } + + get backpressurePolicy() { + return this.#options.backpressure; + } + + get highWaterMark() { + return this.#options.highWaterMark; + } + + get consumerCount() { + return this.#consumers.size; + } + + get bufferSize() { + return this.#buffer.length; + } + + push(...args) { + const { transforms, options } = parsePullArgs(args); + const rawConsumer = this.#createRawConsumer(); + + // When transforms are present, delegate to pull() which creates its + // own internal AbortController that follows the external signal. + // When no transforms, return rawConsumer directly (controller elided + // per PULL-02 optimization -- no transforms means no signal recipient). + if (transforms.length > 0 || options?.signal) { + const pullArgs = [...transforms]; + if (options?.signal) { + ArrayPrototypePush(pullArgs, + { __proto__: null, signal: options.signal }); + } + return pullWithTransforms(rawConsumer, ...pullArgs); + } + return rawConsumer; + } + + #createRawConsumer() { + const state = { + __proto__: null, + // Start at the oldest buffered entry so late-joining consumers + // can read data already in the buffer. + cursor: this.#bufferStart, + resolve: null, + reject: null, + pending: [], + detached: false, + }; + + this.#consumers.add(state); + if (this.#consumers.size === 1) { + this.#cachedMinCursor = state.cursor; + this.#cachedMinCursorConsumers = 1; + } else if (state.cursor === this.#cachedMinCursor) { + this.#cachedMinCursorConsumers++; + } else { + this.#recomputeMinCursor(); + } + const self = this; + + const kDone = PromiseResolve( + { __proto__: null, done: true, value: undefined }); + + function detach() { + state.detached = true; + if (state.resolve) { + state.resolve({ __proto__: null, done: true, value: undefined }); + } + self.#resolvePendingDone(state); + if (self.#deleteConsumer(state)) { + self.#tryTrimBuffer(); + } + } + + return { + __proto__: null, + [SymbolAsyncIterator]() { + return { + __proto__: null, + next() { + if (state.detached) { + if (self.#error) return PromiseReject(self.#error); + return kDone; + } + + const bufferIndex = state.cursor - self.#bufferStart; + if (bufferIndex < self.#buffer.length) { + const chunk = self.#buffer.get(bufferIndex); + const cursor = state.cursor; + state.cursor++; + if (cursor === self.#cachedMinCursor && + --self.#cachedMinCursorConsumers === 0) { + self.#tryTrimBuffer(); + } + return PromiseResolve( + { __proto__: null, done: false, value: chunk }); + } + + if (self.#error) { + state.detached = true; + self.#deleteConsumer(state); + return PromiseReject(self.#error); + } + + if (self.#ended || self.#cancelled) { + detach(); + return kDone; + } + + if (state.resolve) { + const { promise, resolve, reject } = PromiseWithResolvers(); + ArrayPrototypePush(state.pending, + { __proto__: null, resolve, reject }); + return promise; + } + + const { promise, resolve, reject } = PromiseWithResolvers(); + state.resolve = resolve; + state.reject = reject; + ArrayPrototypePush(self.#waiters, state); + return promise; + }, + + return() { + detach(); + return kDone; + }, + + throw() { + detach(); + return kDone; + }, + }; + }, + }; + } + + cancel(reason) { + if (this.#cancelled) return; + this.#cancelled = true; + this.#ended = true; // Prevents [kAbort]() from redundantly iterating consumers + + if (reason !== undefined) { + this.#error = reason; + } + + // Reject pending writes on the writer so the pump doesn't hang + this.#writer?.[kCancelWriter](); + + for (const consumer of this.#consumers) { + if (consumer.resolve) { + if (reason !== undefined) { + consumer.reject?.(reason); + } else { + consumer.resolve({ __proto__: null, done: true, value: undefined }); + } + consumer.resolve = null; + consumer.reject = null; + } + if (reason !== undefined) { + this.#rejectPending(consumer, reason); + } else { + this.#resolvePendingDone(consumer); + } + consumer.detached = true; + } + this.#consumers.clear(); + this.#cachedMinCursorConsumers = 0; + } + + [SymbolDispose]() { + this.cancel(); + } + + // Methods accessed by BroadcastWriter via symbol keys + + [kWrite](chunk) { + if (this.#ended || this.#cancelled) return false; + + if (this.#buffer.length >= this.#options.highWaterMark) { + switch (this.#options.backpressure) { + case 'strict': + case 'block': + return false; + case 'drop-oldest': + this.#buffer.shift(); + this.#bufferStart++; + for (const consumer of this.#consumers) { + if (consumer.cursor < this.#bufferStart) { + this.#deleteConsumerFromMin(consumer); + consumer.cursor = this.#bufferStart; + } + } + this.#recomputeMinCursor(); + break; + case 'drop-newest': + return true; + } + } + + this.#buffer.push(chunk); + this.#notifyConsumers(); + return true; + } + + [kEnd]() { + if (this.#ended) return; + this.#ended = true; + + for (const consumer of this.#consumers) { + while (consumer.resolve) { + const bufferIndex = consumer.cursor - this.#bufferStart; + if (bufferIndex < this.#buffer.length) { + const chunk = this.#buffer.get(bufferIndex); + const cursor = consumer.cursor; + consumer.cursor++; + if (cursor === this.#cachedMinCursor && + --this.#cachedMinCursorConsumers === 0) { + this.#tryTrimBuffer(); + } + consumer.resolve({ __proto__: null, done: false, value: chunk }); + } else { + consumer.resolve({ __proto__: null, done: true, value: undefined }); + this.#resolvePendingDone(consumer); + consumer.detached = true; + } + consumer.resolve = null; + consumer.reject = null; + if (consumer.detached && this.#deleteConsumer(consumer)) { + this.#tryTrimBuffer(); + break; + } + } + } + } + + [kAbort](reason) { + if (this.#ended || this.#error) return; + this.#error = reason; + this.#ended = true; + + // Notify all waiting consumers and detach them + for (const consumer of this.#consumers) { + if (consumer.reject) { + consumer.reject(reason); + consumer.resolve = null; + consumer.reject = null; + } + this.#rejectPending(consumer, reason); + consumer.detached = true; + } + this.#consumers.clear(); + this.#cachedMinCursorConsumers = 0; + } + + [kGetDesiredSize]() { + if (this.#ended || this.#cancelled) return null; + return MathMax(0, this.#options.highWaterMark - this.#buffer.length); + } + + [kCanWrite]() { + if (this.#ended || this.#cancelled) return false; + if ((this.#options.backpressure === 'strict' || + this.#options.backpressure === 'block') && + this.#buffer.length >= this.#options.highWaterMark) { + return false; + } + return true; + } + + // Private methods + + #recomputeMinCursor() { + const { minCursor, minCursorConsumers } = getMinCursor( + this.#consumers, this.#bufferStart + this.#buffer.length); + this.#cachedMinCursor = minCursor; + this.#cachedMinCursorConsumers = minCursorConsumers; + } + + #tryTrimBuffer() { + if (this.#cachedMinCursorConsumers === 0) { + this.#recomputeMinCursor(); + } + const trimCount = this.#cachedMinCursor - this.#bufferStart; + if (trimCount > 0) { + this.#buffer.trimFront(trimCount); + this.#bufferStart = this.#cachedMinCursor; + + if (this[kOnBufferDrained] && + this.#buffer.length < this.#options.highWaterMark) { + this[kOnBufferDrained](); + } + } + } + + #notifyConsumers() { + const waiters = this.#waiters; + if (waiters.length === 0) return; + // Swap out the waiters list so consumers that re-wait during + // resolve don't get processed twice in this cycle. + this.#waiters = []; + for (let i = 0; i < waiters.length; i++) { + const consumer = waiters[i]; + if (consumer.resolve) { + const bufferIndex = consumer.cursor - this.#bufferStart; + if (bufferIndex < this.#buffer.length) { + const chunk = this.#buffer.get(bufferIndex); + const cursor = consumer.cursor; + consumer.cursor++; + if (cursor === this.#cachedMinCursor && + --this.#cachedMinCursorConsumers === 0) { + this.#tryTrimBuffer(); + } + const resolve = consumer.resolve; + consumer.resolve = null; + consumer.reject = null; + resolve({ __proto__: null, done: false, value: chunk }); + if (consumer.detached && this.#deleteConsumer(consumer)) { + this.#tryTrimBuffer(); + } else if (this.#promotePending(consumer)) { + ArrayPrototypePush(this.#waiters, consumer); + } + } else { + // Still waiting -- put back + ArrayPrototypePush(this.#waiters, consumer); + } + } + } + } + + #deleteConsumerFromMin(consumer) { + if (consumer.cursor === this.#cachedMinCursor) { + this.#cachedMinCursorConsumers--; + return this.#cachedMinCursorConsumers === 0; + } + return false; + } + + #deleteConsumer(consumer) { + if (this.#consumers.delete(consumer)) { + return this.#deleteConsumerFromMin(consumer); + } + return false; + } + + #promotePending(consumer) { + const next = ArrayPrototypeShift(consumer.pending); + if (next === undefined) return false; + consumer.resolve = next.resolve; + consumer.reject = next.reject; + return true; + } + + #resolvePendingDone(consumer) { + if (consumer.resolve) { + consumer.resolve = null; + consumer.reject = null; + } + while (consumer.pending.length > 0) { + ArrayPrototypeShift(consumer.pending).resolve( + { __proto__: null, done: true, value: undefined }); + } + } + + #rejectPending(consumer, reason) { + while (consumer.pending.length > 0) { + ArrayPrototypeShift(consumer.pending).reject(reason); + } + } +} + +// ============================================================================= +// BroadcastWriter +// ============================================================================= + +let getBroadcastPendingWrites; + +class BroadcastWriter { + #broadcast; + #totalBytes = 0; + #closed; + #aborted = false; + #pendingWrites = new RingBuffer(); + #pendingDrains = []; + + static { + // Used in wireBroadcastWriteSignal ensure the signal listener can be + // constructed without closing over the chunk data, which may be large. + getBroadcastPendingWrites = (obj) => obj.#pendingWrites; + } + + constructor(broadcastImpl) { + this.#broadcast = broadcastImpl; + + this.#broadcast[kOnBufferDrained] = () => { + this.#resolvePendingWrites(); + this.#resolvePendingDrains(true); + }; + } + + // The drainable protocol works with Stream.ondrain to provide a notification + // when the writer can accept more data after being backpressured. + [drainableProtocol]() { + const desired = this.desiredSize; + if (desired === null) return null; + if (desired > 0) return PromiseResolve(true); + const { promise, resolve, reject } = PromiseWithResolvers(); + ArrayPrototypePush(this.#pendingDrains, { __proto__: null, resolve, reject }); + return promise; + } + + #isClosed() { + return this.#closed !== undefined; + } + + #isClosedOrAborted() { + return this.#isClosed() || this.#aborted; + } + + get desiredSize() { + return this.#isClosedOrAborted() ? null : this.#broadcast[kGetDesiredSize](); + } + + #canUseWriteFastPath(options) { + return !options?.signal && !this.#isClosed() && !this.#aborted && + this.#broadcast[kCanWrite](); + } + + write(chunk, options) { + // Fast path: no signal, writer open, buffer has space + if (this.#canUseWriteFastPath(options)) { + const converted = toUint8Array(chunk); + this.#broadcast[kWrite]([converted]); + this.#totalBytes += TypedArrayPrototypeGetByteLength(converted); + return kResolvedPromise; + } + return this.#writevSlow([chunk], options); + } + + writev(chunks, options) { + if (!ArrayIsArray(chunks)) { + throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks); + } + // Fast path: no signal, writer open, buffer has space + if (this.#canUseWriteFastPath(options)) { + const converted = convertChunks(chunks); + this.#broadcast[kWrite](converted); + for (let i = 0; i < converted.length; i++) { + this.#totalBytes += TypedArrayPrototypeGetByteLength(converted[i]); + } + return kResolvedPromise; + } + return this.#writevSlow(chunks, options); + } + + async #writevSlow(chunks, options) { + const signal = options?.signal; + + // Check for pre-aborted + signal?.throwIfAborted(); + + if (this.#isClosedOrAborted()) { + throw new ERR_INVALID_STATE.TypeError('Writer is closed'); + } + + const converted = convertChunks(chunks); + + if (this.#broadcast[kWrite](converted)) { + for (let i = 0; i < converted.length; i++) { + this.#totalBytes += TypedArrayPrototypeGetByteLength(converted[i]); + } + return; + } + + const policy = this.#broadcast.backpressurePolicy; + const hwm = this.#broadcast.highWaterMark; + + if (policy === 'strict') { + if (this.#pendingWrites.length >= hwm) { + throw new ERR_INVALID_STATE.TypeError( + 'Backpressure violation: too many pending writes. ' + + 'Await each write() call to respect backpressure.'); + } + return this.#createPendingWrite(converted, signal); + } + + // 'block' policy + return this.#createPendingWrite(converted, signal); + } + + writeSync(chunk) { + if (this.#isClosedOrAborted()) return false; + if (!this.#broadcast[kCanWrite]()) return false; + const converted = + toUint8Array(chunk); + if (this.#broadcast[kWrite]([converted])) { + this.#totalBytes += TypedArrayPrototypeGetByteLength(converted); + return true; + } + return false; + } + + writevSync(chunks) { + if (!ArrayIsArray(chunks)) { + throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks); + } + if (this.#isClosedOrAborted()) return false; + if (!this.#broadcast[kCanWrite]()) return false; + const converted = convertChunks(chunks); + if (this.#broadcast[kWrite](converted)) { + for (let i = 0; i < converted.length; i++) { + this.#totalBytes += TypedArrayPrototypeGetByteLength(converted[i]); + } + return true; + } + return false; + } + + // end() is synchronous internally - signal accepted for interface compliance. + end(options) { + if (this.#isClosed()) return this.#closed; + this.#closed = PromiseResolve(this.#totalBytes); + this.#broadcast[kEnd](); + this.#resolvePendingDrains(false); + return this.#closed; + } + + endSync() { + if (this.#closed) return this.#totalBytes; + this.#closed = PromiseResolve(this.#totalBytes); + this.#broadcast[kEnd](); + this.#resolvePendingDrains(false); + return this.#totalBytes; + } + + fail(reason) { + if (this.#isClosedOrAborted()) return; + this.#aborted = true; + this.#closed = PromiseResolve(this.#totalBytes); + const error = reason ?? new ERR_INVALID_STATE.TypeError('Failed'); + this.#rejectPendingWrites(error); + this.#rejectPendingDrains(error); + this.#broadcast[kAbort](error); + } + + [SymbolAsyncDispose]() { + this.fail(); + return PromiseResolve(); + } + + [SymbolDispose]() { + this.fail(); + } + + [kCancelWriter]() { + if (this.#isClosed()) return; + this.#closed = PromiseResolve(this.#totalBytes); + this.#rejectPendingWrites( + lazyDOMException('Broadcast cancelled', 'AbortError')); + this.#resolvePendingDrains(false); + } + + /** + * Create a pending write promise, optionally racing against a signal. + * If the signal fires, the entry is removed from pendingWrites and the + * promise rejects. Signal listeners are cleaned up on normal resolution. + * @returns {Promise} + */ + #createPendingWrite(chunk, signal) { + const { promise, resolve, reject } = PromiseWithResolvers(); + const entry = { __proto__: null, chunk, resolve, reject }; + this.#pendingWrites.push(entry); + if (signal) { + wireBroadcastWriteSignal(entry, signal, resolve, reject, this); + } + return promise; + } + + #resolvePendingWrites() { + while (this.#pendingWrites.length > 0 && this.#broadcast[kCanWrite]()) { + const pending = this.#pendingWrites.shift(); + if (this.#broadcast[kWrite](pending.chunk)) { + for (let i = 0; i < pending.chunk.length; i++) { + this.#totalBytes += TypedArrayPrototypeGetByteLength(pending.chunk[i]); + } + pending.resolve(); + } else { + this.#pendingWrites.unshift(pending); + break; + } + } + } + + #rejectPendingWrites(error) { + while (this.#pendingWrites.length > 0) { + this.#pendingWrites.shift().reject(error); + } + } + + #resolvePendingDrains(canWrite) { + const drains = this.#pendingDrains; + this.#pendingDrains = []; + for (let i = 0; i < drains.length; i++) { + drains[i].resolve(canWrite); + } + } + + #rejectPendingDrains(error) { + const drains = this.#pendingDrains; + this.#pendingDrains = []; + for (let i = 0; i < drains.length; i++) { + drains[i].reject(error); + } + } +} + +function wireBroadcastWriteSignal(entry, signal, resolve, reject, self) { + const onAbort = () => { + const pendingWrites = getBroadcastPendingWrites(self); + const idx = pendingWrites.indexOf(entry); + if (idx !== -1) pendingWrites.removeAt(idx); + entry.chunk = null; + reject(signal.reason ?? lazyDOMException('Aborted', 'AbortError')); + }; + entry.resolve = function() { + signal.removeEventListener('abort', onAbort); + entry.chunk = null; + resolve(); + }; + entry.reject = function(reason) { + signal.removeEventListener('abort', onAbort); + entry.chunk = null; + reject(reason); + }; + signal.addEventListener('abort', onAbort, { __proto__: null, once: true }); +} + +function onBroadcastCancel(broadcastImpl, signal) { + onSignalAbort(signal, () => broadcastImpl.cancel(signal.reason)); +} + +// ============================================================================= +// Public API +// ============================================================================= + +/** + * Create a broadcast channel for push-model multi-consumer streaming. + * @param {{ highWaterMark?: number, backpressure?: string, signal?: AbortSignal }} [options] + * @returns {{ writer: Writer, broadcast: Broadcast }} + */ +function broadcast(options = { __proto__: null }) { + validateObject(options, 'options'); + const { + highWaterMark = kMultiConsumerDefaultHWM, + backpressure = 'strict', + signal, + } = options; + validateInteger(highWaterMark, 'options.highWaterMark'); + validateBackpressure(backpressure); + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + } + + const opts = { + __proto__: null, + highWaterMark: clampHWM(highWaterMark), + backpressure, + signal, + }; + + const broadcastImpl = new BroadcastImpl(opts); + const writer = new BroadcastWriter(broadcastImpl); + broadcastImpl.setWriter(writer); + + if (signal) { + onBroadcastCancel(broadcastImpl, signal); + } + + return { __proto__: null, writer, broadcast: broadcastImpl }; +} + +function isBroadcastable(value) { + return hasProtocol(value, broadcastProtocol); +} + +const Broadcast = { + __proto__: null, + from(input, options) { + if (isBroadcastable(input)) { + const bc = input[broadcastProtocol](options); + if (bc === null || typeof bc !== 'object') { + throw new ERR_INVALID_RETURN_VALUE( + 'an object', '[Symbol.for(\'Stream.broadcastProtocol\')]', bc); + } + return { __proto__: null, writer: { __proto__: null }, broadcast: bc }; + } + + const source = from(input); + + if (!isAsyncIterable(source) && !isSyncIterable(source)) { + throw new ERR_INVALID_ARG_TYPE( + 'input', ['Broadcastable', 'AsyncIterable', 'Iterable'], input); + } + + const result = broadcast(options); + const signal = options?.signal; + + const pump = async () => { + const w = result.writer; + try { + if (isAsyncIterable(source)) { + for await (const chunks of source) { + signal?.throwIfAborted(); + if (ArrayIsArray(chunks)) { + if (!w.writevSync(chunks)) { + await w.writev(chunks, signal ? { signal } : undefined); + } + } else if (!w.writeSync(chunks)) { + await w.write(chunks, signal ? { signal } : undefined); + } + } + } else if (isSyncIterable(source)) { + for (const chunks of source) { + signal?.throwIfAborted(); + if (ArrayIsArray(chunks)) { + if (!w.writevSync(chunks)) { + await w.writev(chunks, signal ? { signal } : undefined); + } + } else if (!w.writeSync(chunks)) { + await w.write(chunks, signal ? { signal } : undefined); + } + } + } + if (w.endSync() < 0) { + await w.end(signal ? { signal } : undefined); + } + } catch (error) { + w.fail(wrapError(error)); + } + }; + PromisePrototypeThen(pump(), undefined, () => {}); + + return result; + }, +}; + +module.exports = { + Broadcast, + broadcast, +}; diff --git a/lib/internal/streams/iter/classic.js b/lib/internal/streams/iter/classic.js new file mode 100644 index 00000000000000..b4401632946f0d --- /dev/null +++ b/lib/internal/streams/iter/classic.js @@ -0,0 +1,885 @@ +'use strict'; + +// Interop utilities between classic Node.js streams and the stream/iter API. +// +// These are Node.js-specific (not part of the stream/iter spec) and are +// exported from 'stream/iter' as top-level utility functions: +// +// fromReadable(readable) -- classic Readable (or duck-type) -> stream/iter source +// fromWritable(writable, opts) -- classic Writable (or duck-type) -> stream/iter Writer +// toReadable(source, opts) -- stream/iter source -> classic Readable +// toReadableSync(source, opts) -- stream/iter source (sync) -> classic Readable +// toWritable(writer) -- stream/iter Writer -> classic Writable + +const { + ArrayIsArray, + ArrayPrototypePush, + MathMax, + NumberMAX_SAFE_INTEGER, + Promise, + PromisePrototypeThen, + PromiseReject, + PromiseResolve, + PromiseWithResolvers, + SafeMap, + SafeWeakMap, + SymbolAsyncDispose, + SymbolAsyncIterator, + SymbolDispose, + SymbolIterator, + TypedArrayPrototypeGetByteLength, +} = primordials; + +const { + AbortError, + aggregateTwoErrors, + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_STATE, + ERR_STREAM_WRITE_AFTER_END, + }, +} = require('internal/errors'); + +const { + validateInteger, + validateObject, +} = require('internal/validators'); + +const { eos } = require('internal/streams/end-of-stream'); +const { + addAbortSignal: addAbortSignalNoValidate, +} = require('internal/streams/add-abort-signal'); +const { + queueMicrotask, +} = require('internal/process/task_queues'); + +const { + toAsyncStreamable: kToAsyncStreamable, + kValidatedSource, + drainableProtocol, +} = require('internal/streams/iter/types'); + +const { + validateBackpressure, + toUint8Array, +} = require('internal/streams/iter/utils'); + +const { Buffer } = require('buffer'); +const destroyImpl = require('internal/streams/destroy'); + +// Lazy-loaded to avoid circular dependencies. Readable and Writable +// both require this module's parent, so we defer the require. +let Readable; +let Writable; + +function lazyReadable() { + if (Readable === undefined) { + Readable = require('internal/streams/readable'); + } + return Readable; +} + +function lazyWritable() { + if (Writable === undefined) { + Writable = require('internal/streams/writable'); + } + return Writable; +} + +// ============================================================================ +// fromReadable(readable) -- classic Readable -> stream/iter async iterable +// ============================================================================ + +// Cache: one stream/iter source per Readable instance. +const fromReadableCache = new SafeWeakMap(); + +// Maximum chunks to drain into a single batch. Bounds peak memory when +// _read() synchronously pushes many chunks into the buffer. +const MAX_DRAIN_BATCH = 128; + +const { normalizeAsyncValue } = require('internal/streams/iter/from'); +const { isUint8Array } = require('internal/util/types'); + +// Normalize a batch of raw chunks from an object-mode or encoded +// Readable into Uint8Array values. Returns the normalized batch, +// or null if normalization produced no output. +async function normalizeBatch(raw) { + const batch = []; + for (let i = 0; i < raw.length; i++) { + const value = raw[i]; + if (isUint8Array(value)) { + ArrayPrototypePush(batch, value); + } else { + // normalizeAsyncValue may await for async protocols (e.g. + // toAsyncStreamable on yielded objects). Stream events during + // the suspension are queued, not lost -- errors will surface + // on the next loop iteration after this yield completes. + for await (const normalized of normalizeAsyncValue(value)) { + ArrayPrototypePush(batch, normalized); + } + } + } + return batch.length > 0 ? batch : null; +} + +// Batched async iterator for Readable streams. Same mechanism as +// createAsyncIterator (same event setup, same stream.read() to +// trigger _read(), same teardown) but drains all currently buffered +// chunks into a single Uint8Array[] batch per yield, amortizing the +// Promise/microtask cost across multiple chunks. +// +// When normalize is provided (object-mode / encoded streams), each +// drained batch is passed through it to convert chunks to Uint8Array. +// When normalize is null (byte-mode), chunks are already Buffers +// (Uint8Array subclass) and are yielded directly. +const nop = () => {}; + +async function* createBatchedAsyncIterator(stream, normalize) { + let callback = nop; + + function next(resolve) { + if (this === stream) { + callback(); + callback = nop; + } else { + callback = resolve; + } + } + + stream.on('readable', next); + + let error; + const cleanup = eos(stream, { writable: false }, (err) => { + error = err ? aggregateTwoErrors(error, err) : null; + callback(); + callback = nop; + }); + + try { + while (true) { + const chunk = stream.destroyed ? null : stream.read(); + if (chunk !== null) { + const batch = [chunk]; + while (batch.length < MAX_DRAIN_BATCH && + stream._readableState?.length > 0) { + const c = stream.read(); + if (c === null) break; + ArrayPrototypePush(batch, c); + } + if (normalize !== null) { + const result = await normalize(batch); + if (result !== null) { + yield result; + } + } else { + yield batch; + } + } else if (error) { + throw error; + } else if (error === null) { + return; + } else { + await new Promise(next); + } + } + } catch (err) { + error = aggregateTwoErrors(error, err); + throw error; + } finally { + if (error === undefined || + (stream._readableState?.autoDestroy)) { + destroyImpl.destroyer(stream, null); + } else { + stream.off('readable', next); + cleanup(); + } + } +} + +/** + * Convert a classic Readable (or duck-type) to a stream/iter async iterable. + * + * If the object implements the toAsyncStreamable protocol, delegates to it. + * Otherwise, duck-type checks for read() + EventEmitter (on/off) and + * wraps with a batched async iterator. + * @param {object} readable - A classic Readable or duck-type with + * read() and on()/off() methods. + * @returns {AsyncIterable} A stream/iter async iterable source. + */ +function fromReadable(readable) { + if (readable == null || typeof readable !== 'object') { + throw new ERR_INVALID_ARG_TYPE('readable', 'Readable', readable); + } + + // Check cache first. + const cached = fromReadableCache.get(readable); + if (cached !== undefined) return cached; + + // Protocol path: object implements toAsyncStreamable. + if (typeof readable[kToAsyncStreamable] === 'function') { + const result = readable[kToAsyncStreamable](); + fromReadableCache.set(readable, result); + return result; + } + + // Duck-type path: object has read() and EventEmitter methods. + if (typeof readable.read !== 'function' || + typeof readable.on !== 'function') { + throw new ERR_INVALID_ARG_TYPE('readable', 'Readable', readable); + } + + // Determine normalization. If the stream has _readableState, use it + // to detect object-mode / encoding. Otherwise assume byte-mode. + const state = readable._readableState; + const normalize = (state && (state.objectMode || state.encoding)) ? + normalizeBatch : null; + + const iter = createBatchedAsyncIterator(readable, normalize); + iter[kValidatedSource] = true; + iter.stream = readable; + + fromReadableCache.set(readable, iter); + return iter; +} + + +// ============================================================================ +// toReadable(source, options) -- stream/iter source -> classic Readable +// ============================================================================ + +const kNullPrototype = { __proto__: null }; + +/** + * Create a byte-mode Readable from an AsyncIterable. + * The source must yield Uint8Array[] batches (the stream/iter native + * format). Each Uint8Array in a batch is pushed as a separate chunk. + * @param {AsyncIterable} source + * @param {object} [options] + * @param {number} [options.highWaterMark] + * @param {AbortSignal} [options.signal] + * @returns {stream.Readable} + */ +function toReadable(source, options = kNullPrototype) { + if (typeof source?.[SymbolAsyncIterator] !== 'function') { + throw new ERR_INVALID_ARG_TYPE('source', 'AsyncIterable', source); + } + + validateObject(options, 'options'); + const { + highWaterMark = 64 * 1024, + signal, + } = options; + validateInteger(highWaterMark, 'options.highWaterMark', 0); + + const ReadableCtor = lazyReadable(); + const iterator = source[SymbolAsyncIterator](); + let backpressure; + let pumping = false; + let done = false; + + const readable = new ReadableCtor({ + __proto__: null, + highWaterMark, + read() { + if (backpressure) { + const { resolve } = backpressure; + backpressure = null; + resolve(); + } else if (!pumping && !done) { + pumping = true; + pump(); + } + }, + destroy(err, cb) { + done = true; + // Wake up the pump if it's waiting on backpressure so it + // can see done === true and exit cleanly. + if (backpressure) { + backpressure.resolve(); + backpressure = null; + } + if (typeof iterator.return === 'function') { + PromisePrototypeThen(iterator.return(), + () => cb(err), (e) => cb(e || err)); + } else { + cb(err); + } + }, + }); + + if (signal) { + addAbortSignalNoValidate(signal, readable); + } + + async function pump() { + try { + while (!done) { + const { value: batch, done: iterDone } = await iterator.next(); + if (iterDone) { + done = true; + readable.push(null); + return; + } + for (let i = 0; i < batch.length; i++) { + if (!readable.push(batch[i])) { + backpressure = PromiseWithResolvers(); + await backpressure.promise; + if (done) return; + } + } + } + } catch (err) { + done = true; + readable.destroy(err); + } + } + + return readable; +} + + +// ============================================================================ +// toReadableSync(source, options) -- stream/iter source (sync) -> Readable +// ============================================================================ + +/** + * Create a byte-mode Readable from an Iterable. + * Fully synchronous -- _read() pulls from the iterator directly. + * @param {Iterable} source + * @param {object} [options] + * @param {number} [options.highWaterMark] + * @returns {stream.Readable} + */ +function toReadableSync(source, options = kNullPrototype) { + if (typeof source?.[SymbolIterator] !== 'function') { + throw new ERR_INVALID_ARG_TYPE('source', 'Iterable', source); + } + + validateObject(options, 'options'); + const { + highWaterMark = 64 * 1024, + } = options; + validateInteger(highWaterMark, 'options.highWaterMark', 0); + + const ReadableCtor = lazyReadable(); + const iterator = source[SymbolIterator](); + let hasBatch = false; + let batch; + let batchIndex = 0; + + return new ReadableCtor({ + __proto__: null, + highWaterMark, + read() { + for (;;) { + if (hasBatch) { + while (batchIndex < batch.length) { + if (!this.push(batch[batchIndex++])) return; + } + batch = undefined; + hasBatch = false; + batchIndex = 0; + } + + const result = iterator.next(); + const { done } = result; + if (done) { + this.push(null); + return; + } + batch = result.value; + hasBatch = true; + } + }, + destroy(err, cb) { + batch = undefined; + hasBatch = false; + if (typeof iterator.return === 'function') iterator.return(); + cb(err); + }, + }); +} + + +// ============================================================================ +// fromWritable(writable, options) -- classic Writable -> stream/iter Writer +// ============================================================================ + +// Cache: one Writer adapter per Writable instance. +const fromWritableCache = new SafeWeakMap(); + +/** + * Create a stream/iter Writer adapter from a classic Writable (or duck-type). + * + * Duck-type requirements: write() and on()/off() methods. + * Falls back to sensible defaults for missing properties like + * writableHighWaterMark, writableLength, writableObjectMode. + * @param {object} writable - A classic Writable or duck-type. + * @param {object} [options] + * @param {string} [options.backpressure] - 'strict', 'block', + * 'drop-newest'. 'drop-oldest' is not supported. + * @returns {object} A stream/iter Writer adapter. + */ +function fromWritable(writable, options = kNullPrototype) { + if (writable == null || + typeof writable.write !== 'function' || + typeof writable.on !== 'function') { + throw new ERR_INVALID_ARG_TYPE('writable', 'Writable', writable); + } + + validateObject(options, 'options'); + const { + backpressure = 'strict', + } = options; + validateBackpressure(backpressure); + + // The Writer interface is bytes-only. Object-mode Writables expect + // arbitrary JS values, which is incompatible. + if (writable.writableObjectMode) { + throw new ERR_INVALID_STATE( + 'Cannot create a stream/iter Writer from an object-mode Writable'); + } + + // drop-oldest is not supported for classic stream.Writable. The + // Writable's internal buffer stores individual { chunk, encoding, + // callback } entries with no concept of batch boundaries. A writev() + // call fans out into N separate buffer entries, so a subsequent + // drop-oldest eviction could partially tear apart an earlier atomic + // writev batch. The PushWriter avoids this because writev occupies a + // single slot. Supporting drop-oldest here would require either + // accepting partial writev eviction or adding batch tracking to the + // buffer -- neither is acceptable without a deeper rework of Writable + // internals. + if (backpressure === 'drop-oldest') { + throw new ERR_INVALID_ARG_VALUE('options.backpressure', backpressure, + 'drop-oldest is not supported for classic stream.Writable'); + } + + // Return cached adapter if available. Backpressure policy changes writer + // behavior, so cache one adapter per policy. + let cachedByBackpressure = fromWritableCache.get(writable); + if (cachedByBackpressure !== undefined) { + const cached = cachedByBackpressure.get(backpressure); + if (cached !== undefined) return cached; + } else { + cachedByBackpressure = new SafeMap(); + fromWritableCache.set(writable, cachedByBackpressure); + } + + // Fall back to sensible defaults for duck-typed streams that may not + // expose the full stream.Writable property set. + const hwm = writable.writableHighWaterMark ?? 16384; + let totalBytes = 0; + + // Waiters pending on backpressure resolution (block policy only). + // Multiple un-awaited writes can each add a waiter, so this must be + // a list. A single persistent 'drain' listener and 'error' listener + // (installed once lazily) resolve or reject all waiters to avoid + // accumulating per-write listeners on the stream. + let waiters = []; + let listenersInstalled = false; + let onDrain; + let onError; + + function installListeners() { + if (listenersInstalled) return; + listenersInstalled = true; + onDrain = () => { + const pending = waiters; + waiters = []; + for (let i = 0; i < pending.length; i++) { + pending[i].resolve(); + } + }; + onError = (err) => { + const pending = waiters; + waiters = []; + for (let i = 0; i < pending.length; i++) { + pending[i].reject(err); + } + }; + writable.on('drain', onDrain); + writable.on('error', onError); + } + + // Reject all pending waiters and remove the drain/error listeners. + function cleanup(err) { + const pending = waiters; + waiters = []; + for (let i = 0; i < pending.length; i++) { + pending[i].reject(err ?? new AbortError()); + } + if (!listenersInstalled) return; + listenersInstalled = false; + writable.removeListener('drain', onDrain); + writable.removeListener('error', onError); + } + + function waitForDrain() { + const { promise, resolve, reject } = PromiseWithResolvers(); + ArrayPrototypePush(waiters, { __proto__: null, resolve, reject }); + installListeners(); + return promise; + } + + function isWritable() { + // Duck-typed streams may not have these properties -- treat missing + // as false (i.e., writable is still open). + return !(writable.destroyed ?? false) && + !(writable.writableFinished ?? false) && + !(writable.writableEnded ?? false); + } + + function isFull() { + return (writable.writableLength ?? 0) >= hwm; + } + + function writeChunks(chunks) { + let ok = true; + for (let i = 0; i < chunks.length; i++) { + const bytes = toUint8Array(chunks[i]); + totalBytes += TypedArrayPrototypeGetByteLength(bytes); + ok = writable.write(bytes); + } + return ok; + } + + const writer = { + __proto__: null, + + get desiredSize() { + if (!isWritable()) return null; + return MathMax(0, hwm - (writable.writableLength ?? 0)); + }, + + writeSync(chunk) { + return false; + }, + + writevSync(chunks) { + return false; + }, + + // Backpressure semantics: write() resolves when the data is accepted + // into the Writable's internal buffer, NOT when _write() has flushed + // it to the underlying resource. This matches the Writer spec -- the + // PushWriter resolves on buffer acceptance too. Classic Writable flow + // control works the same way: write rapidly until write() returns + // false, then wait for 'drain'. The _write callback is involved in + // backpressure indirectly -- 'drain' fires after callbacks drain the + // buffer below highWaterMark. Per-write errors from _write surface + // as 'error' events caught by our generic error handler, rejecting + // the next pending operation rather than the already-resolved one. + // + // The options.signal parameter from the Writer interface is ignored. + // Classic stream.Writable has no per-write abort signal support; + // cancellation should be handled at the pipeline level instead. + write(chunk) { + if (!isWritable()) { + return PromiseReject(new ERR_STREAM_WRITE_AFTER_END()); + } + + let bytes; + try { + bytes = toUint8Array(chunk); + } catch (err) { + return PromiseReject(err); + } + + if (backpressure === 'strict' && isFull()) { + return PromiseReject(new ERR_INVALID_STATE.RangeError( + 'Backpressure violation: buffer is full. ' + + 'Await each write() call to respect backpressure.')); + } + + if (backpressure === 'drop-newest' && isFull()) { + // Silently discard. Still count bytes for consistency with + // PushWriter, which counts dropped bytes in totalBytes. + totalBytes += TypedArrayPrototypeGetByteLength(bytes); + return PromiseResolve(); + } + + totalBytes += TypedArrayPrototypeGetByteLength(bytes); + const ok = writable.write(bytes); + if (ok) return PromiseResolve(); + + // backpressure === 'block' (or strict with room that filled on + // this write -- writable.write() accepted the data but returned + // false indicating the buffer is now at/over hwm). + if (backpressure === 'block') { + return waitForDrain(); + } + + // strict: the write was accepted (there was room before writing) + // but the buffer is now full. Resolve -- the *next* write will + // be rejected if the caller ignores backpressure. + return PromiseResolve(); + }, + + writev(chunks) { + if (!ArrayIsArray(chunks)) { + throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks); + } + if (!isWritable()) { + return PromiseReject(new ERR_STREAM_WRITE_AFTER_END()); + } + + if (backpressure === 'strict' && isFull()) { + return PromiseReject(new ERR_INVALID_STATE.RangeError( + 'Backpressure violation: buffer is full. ' + + 'Await each write() call to respect backpressure.')); + } + + if (backpressure === 'drop-newest' && isFull()) { + // Discard entire batch. + for (let i = 0; i < chunks.length; i++) { + totalBytes += + TypedArrayPrototypeGetByteLength(toUint8Array(chunks[i])); + } + return PromiseResolve(); + } + + let ok = true; + if (typeof writable.cork === 'function' && + typeof writable.uncork === 'function') { + writable.cork(); + try { + ok = writeChunks(chunks); + } finally { + writable.uncork(); + } + } else { + ok = writeChunks(chunks); + } + + if (ok) return PromiseResolve(); + + if (backpressure === 'block') { + return waitForDrain(); + } + + return PromiseResolve(); + }, + + endSync() { + return -1; + }, + + // options.signal is ignored for the same reason as write(). + end() { + if ((writable.writableFinished ?? false) || + (writable.destroyed ?? false)) { + cleanup(); + return PromiseResolve(totalBytes); + } + + const { promise, resolve, reject } = PromiseWithResolvers(); + + if (!(writable.writableEnded ?? false)) { + writable.end(); + } + + eos(writable, { writable: true, readable: false }, (err) => { + cleanup(err); + if (err) reject(err); + else resolve(totalBytes); + }); + + return promise; + }, + + fail(reason) { + cleanup(reason); + if (typeof writable.destroy === 'function') { + writable.destroy(reason); + } + }, + + [SymbolAsyncDispose]() { + if (isWritable()) { + cleanup(); + if (typeof writable.destroy === 'function') { + writable.destroy(); + } + } + return PromiseResolve(); + }, + + [SymbolDispose]() { + if (isWritable()) { + cleanup(); + if (typeof writable.destroy === 'function') { + writable.destroy(); + } + } + }, + }; + + // drainableProtocol + writer[drainableProtocol] = function() { + if (!isWritable()) return null; + if ((writable.writableLength ?? 0) < hwm) { + return PromiseResolve(true); + } + const { promise, resolve } = PromiseWithResolvers(); + ArrayPrototypePush(waiters, { + __proto__: null, + resolve() { resolve(true); }, + reject() { resolve(false); }, + }); + installListeners(); + return promise; + }; + + cachedByBackpressure.set(backpressure, writer); + return writer; +} + + +// ============================================================================ +// toWritable(writer) -- stream/iter Writer -> classic Writable +// ============================================================================ + +/** + * Create a classic stream.Writable backed by a stream/iter Writer. + * Each _write/_writev call delegates to the Writer's methods, + * attempting the sync path first (writeSync/writevSync/endSync) and + * falling back to async if the sync path returns false or throws. + * @param {object} writer - A stream/iter Writer (only write() is required). + * @returns {stream.Writable} + */ +function toWritable(writer) { + if (typeof writer?.write !== 'function') { + throw new ERR_INVALID_ARG_TYPE('writer', 'Writer', writer); + } + + const WritableCtor = lazyWritable(); + + const hasWriteSync = typeof writer.writeSync === 'function'; + const hasWritev = typeof writer.writev === 'function'; + const hasWritevSync = hasWritev && + typeof writer.writevSync === 'function'; + const hasEnd = typeof writer.end === 'function'; + const hasEndSync = hasEnd && + typeof writer.endSync === 'function'; + const hasFail = typeof writer.fail === 'function'; + // Try-sync-first pattern: attempt the synchronous method and fall back to the + // async method if it returns false (data not accepted synchronously). + // When the sync path succeeds, the callback is deferred via queueMicrotask + // to preserve the async resolution contract that Writable internals expect + // from _write/_writev/_final callbacks. + + function _write(chunk, encoding, cb) { + const bytes = typeof chunk === 'string' ? + Buffer.from(chunk, encoding) : chunk; + if (hasWriteSync) { + try { + if (writer.writeSync(bytes)) { + queueMicrotask(cb); + return; + } + // WriteSync returned false: not accepted, fall through to async. + } catch (err) { + cb(err); + return; + } + } + try { + PromisePrototypeThen(writer.write(bytes), () => cb(), cb); + } catch (err) { + cb(err); + } + } + + function _writev(entries, cb) { + const chunks = []; + for (let i = 0; i < entries.length; i++) { + const { chunk, encoding } = entries[i]; + chunks[i] = typeof chunk === 'string' ? + Buffer.from(chunk, encoding) : chunk; + } + if (hasWritevSync) { + try { + if (writer.writevSync(chunks)) { + queueMicrotask(cb); + return; + } + // WritevSync returned false: not accepted, fall through to async. + } catch (err) { + cb(err); + return; + } + } + try { + PromisePrototypeThen(writer.writev(chunks), () => cb(), cb); + } catch (err) { + cb(err); + } + } + + function _final(cb) { + if (!hasEnd) { + queueMicrotask(cb); + return; + } + if (hasEndSync) { + try { + const result = writer.endSync(); + if (result >= 0) { + queueMicrotask(cb); + return; + } + // Result < 0: can't end synchronously, fall through to async. + } catch (err) { + cb(err); + return; + } + } + try { + PromisePrototypeThen(writer.end(), () => cb(), cb); + } catch (err) { + cb(err); + } + } + + function _destroy(err, cb) { + if (err && hasFail) { + writer.fail(err); + } + cb(); + } + + const writableOptions = { + __proto__: null, + // Use MAX_SAFE_INTEGER to effectively disable the Writable's + // internal buffering. The underlying stream/iter Writer has its + // own backpressure handling; we want _write to be called + // immediately so the Writer can manage flow control directly. + highWaterMark: NumberMAX_SAFE_INTEGER, + write: _write, + final: _final, + destroy: _destroy, + }; + + if (hasWritev) { + writableOptions.writev = _writev; + } + + return new WritableCtor(writableOptions); +} + + +module.exports = { + // Shared helpers used by Readable.prototype[toAsyncStreamable] in + // readable.js to avoid duplicating the batched iterator logic. + createBatchedAsyncIterator, + normalizeBatch, + + // Public utilities exported from 'stream/iter'. + fromReadable, + fromWritable, + toReadable, + toReadableSync, + toWritable, +}; diff --git a/lib/internal/streams/iter/consumers.js b/lib/internal/streams/iter/consumers.js new file mode 100644 index 00000000000000..7d89607fee0a3b --- /dev/null +++ b/lib/internal/streams/iter/consumers.js @@ -0,0 +1,595 @@ +'use strict'; + +// New Streams API - Consumers & Utilities +// +// bytes(), text(), arrayBuffer() - collect entire stream +// tap(), tapSync() - observe without modifying +// merge() - temporal combining of sources +// ondrain() - backpressure drain utility + +const { + ArrayBufferIsView, + ArrayBufferPrototypeGetByteLength, + ArrayBufferPrototypeSlice, + ArrayPrototypeMap, + ArrayPrototypePush, + ArrayPrototypeShift, + ArrayPrototypeSlice, + Promise, + PromisePrototypeThen, + SafePromiseAllReturnVoid, + SymbolAsyncIterator, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteOffset, +} = primordials; + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_OUT_OF_RANGE, + }, +} = require('internal/errors'); +const { TextDecoder } = require('internal/encoding'); +const { + validateAbortSignal, + validateFunction, + validateInteger, + validateObject, +} = require('internal/validators'); + +const { + markPromiseAsHandled, +} = internalBinding('util'); + +const { + from, + fromSync, + isAsyncIterable, + isSyncIterable, +} = require('internal/streams/iter/from'); + +const { + concatBytes, + yieldAbortable, +} = require('internal/streams/iter/utils'); + +const { + drainableProtocol, + toAsyncStreamable, + toStreamable, +} = require('internal/streams/iter/types'); + +const { + isAnyArrayBuffer, + isSharedArrayBuffer, +} = require('internal/util/types'); + +// ============================================================================= +// Type Guards +// ============================================================================= + +function isMergeOptions(value) { + return ( + value !== null && + typeof value === 'object' && + !ArrayBufferIsView(value) && + typeof value[toStreamable] !== 'function' && + typeof value[toAsyncStreamable] !== 'function' && + !isAsyncIterable(value) && + !isSyncIterable(value) && + !isAnyArrayBuffer(value) + ); +} + +// ============================================================================= +// Shared chunk collection helpers +// ============================================================================= + +/** + * Collect chunks from a sync source into an array. + * @param {Iterable} source + * @param {number} [limit] + * @returns {Uint8Array[]} + */ +function collectSync(source, limit) { + // Normalize source via fromSync() - accepts strings, ArrayBuffers, protocols, etc. + const normalized = fromSync(source); + const chunks = []; + let totalBytes = 0; + + for (const batch of normalized) { + for (let i = 0; i < batch.length; i++) { + const chunk = batch[i]; + if (limit !== undefined) { + totalBytes += TypedArrayPrototypeGetByteLength(chunk); + if (totalBytes > limit) { + throw new ERR_OUT_OF_RANGE('totalBytes', `<= ${limit}`, totalBytes); + } + } + ArrayPrototypePush(chunks, chunk); + } + } + + return chunks; +} + +/** + * Collect chunks from an async or sync source into an array. + * @param {AsyncIterable|Iterable} source + * @param {AbortSignal} [signal] + * @param {number} [limit] + * @returns {Promise} + */ +async function collectAsync(source, signal, limit) { + signal?.throwIfAborted(); + + // Normalize source via from() - accepts strings, ArrayBuffers, protocols, etc. + const abortableSource = signal && isAsyncIterable(source) ? + yieldAbortable(source, signal) : source; + const normalized = from(abortableSource); + const chunks = []; + + // Fast path: no signal and no limit + if (!signal && limit === undefined) { + for await (const batch of normalized) { + for (let i = 0; i < batch.length; i++) { + ArrayPrototypePush(chunks, batch[i]); + } + } + return chunks; + } + + // Slow path: with signal or limit checks + let totalBytes = 0; + const iterable = signal ? yieldAbortable(normalized, signal) : normalized; + + for await (const batch of iterable) { + signal?.throwIfAborted(); + for (let i = 0; i < batch.length; i++) { + const chunk = batch[i]; + if (limit !== undefined) { + totalBytes += TypedArrayPrototypeGetByteLength(chunk); + if (totalBytes > limit) { + throw new ERR_OUT_OF_RANGE('totalBytes', `<= ${limit}`, totalBytes); + } + } + ArrayPrototypePush(chunks, chunk); + } + } + + return chunks; +} + +/** + * Convert a Uint8Array to its backing ArrayBuffer, slicing if necessary. + * Handles both ArrayBuffer and SharedArrayBuffer backing stores. + * @param {Uint8Array} data + * @returns {ArrayBuffer|SharedArrayBuffer} + */ +function toArrayBuffer(data) { + const byteOffset = TypedArrayPrototypeGetByteOffset(data); + const byteLength = TypedArrayPrototypeGetByteLength(data); + const buffer = TypedArrayPrototypeGetBuffer(data); + // SharedArrayBuffer is not available in primordials, so use + // direct property access for its byteLength and slice. + if (isSharedArrayBuffer(buffer)) { + if (byteOffset === 0 && byteLength === buffer.byteLength) { + return buffer; + } + return buffer.slice(byteOffset, byteOffset + byteLength); + } + if (byteOffset === 0 && + byteLength === ArrayBufferPrototypeGetByteLength(buffer)) { + return buffer; + } + return ArrayBufferPrototypeSlice(buffer, byteOffset, + byteOffset + byteLength); +} + +// ============================================================================= +// Shared option validation +// ============================================================================= + +function validateBaseConsumerOptions(options) { + validateObject(options, 'options'); + if (options.limit !== undefined) { + validateInteger(options.limit, 'options.limit', 0); + } + if (options.encoding !== undefined) { + if (typeof options.encoding !== 'string') { + throw new ERR_INVALID_ARG_TYPE('options.encoding', 'string', + options.encoding); + } + try { + new TextDecoder(options.encoding); + } catch { + throw new ERR_INVALID_ARG_VALUE.RangeError( + 'options.encoding', options.encoding); + } + } +} + +function validateConsumerOptions(options) { + validateBaseConsumerOptions(options); + if (options.signal !== undefined) { + validateAbortSignal(options.signal, 'options.signal'); + } +} + +function validateSyncConsumerOptions(options) { + validateBaseConsumerOptions(options); +} + +// ============================================================================= +// Sync Consumers +// ============================================================================= + +const kNullPrototype = { __proto__: null }; + +/** + * Collect all bytes from a sync source. + * @param {Iterable} source + * @param {{ limit?: number }} [options] + * @returns {Uint8Array} + */ +function bytesSync(source, options = kNullPrototype) { + validateSyncConsumerOptions(options); + return concatBytes(collectSync(source, options.limit)); +} + +/** + * Collect and decode text from a sync source. + * @param {Iterable} source + * @param {{ encoding?: string, limit?: number }} [options] + * @returns {string} + */ +function textSync(source, options = kNullPrototype) { + validateSyncConsumerOptions(options); + const data = concatBytes(collectSync(source, options.limit)); + const decoder = new TextDecoder(options.encoding ?? 'utf-8', { + __proto__: null, + fatal: true, + }); + return decoder.decode(data); +} + +/** + * Collect bytes as ArrayBuffer from a sync source. + * @param {Iterable} source + * @param {{ limit?: number }} [options] + * @returns {ArrayBuffer} + */ +function arrayBufferSync(source, options = kNullPrototype) { + validateSyncConsumerOptions(options); + return toArrayBuffer(concatBytes(collectSync(source, options.limit))); +} + +/** + * Collect all chunks as an array from a sync source. + * @param {Iterable} source + * @param {{ limit?: number }} [options] + * @returns {Uint8Array[]} + */ +function arraySync(source, options = kNullPrototype) { + validateSyncConsumerOptions(options); + return collectSync(source, options.limit); +} + +// ============================================================================= +// Async Consumers +// ============================================================================= + +/** + * Collect all bytes from an async or sync source. + * @param {AsyncIterable|Iterable} source + * @param {{ signal?: AbortSignal, limit?: number }} [options] + * @returns {Promise} + */ +async function bytes(source, options = kNullPrototype) { + validateConsumerOptions(options); + const chunks = await collectAsync(source, options.signal, options.limit); + return concatBytes(chunks); +} + +/** + * Collect and decode text from an async or sync source. + * @param {AsyncIterable|Iterable} source + * @param {{ encoding?: string, signal?: AbortSignal, limit?: number }} [options] + * @returns {Promise} + */ +async function text(source, options = kNullPrototype) { + validateConsumerOptions(options); + const chunks = await collectAsync(source, options.signal, options.limit); + const data = concatBytes(chunks); + const decoder = new TextDecoder(options.encoding ?? 'utf-8', { + __proto__: null, + fatal: true, + }); + return decoder.decode(data); +} + +/** + * Collect bytes as ArrayBuffer from an async or sync source. + * @param {AsyncIterable|Iterable} source + * @param {{ signal?: AbortSignal, limit?: number }} [options] + * @returns {Promise} + */ +async function arrayBuffer(source, options = kNullPrototype) { + validateConsumerOptions(options); + const chunks = await collectAsync(source, options.signal, options.limit); + return toArrayBuffer(concatBytes(chunks)); +} + +/** + * Collect all chunks as an array from an async or sync source. + * @param {AsyncIterable|Iterable} source + * @param {{ signal?: AbortSignal, limit?: number }} [options] + * @returns {Promise} + */ +async function array(source, options = kNullPrototype) { + validateConsumerOptions(options); + return collectAsync(source, options.signal, options.limit); +} + +// ============================================================================= +// Tap Utilities +// ============================================================================= + +/** + * Create a pass-through transform that observes chunks without modifying them. + * @param {Function} callback + * @returns {Function} + */ +function tap(callback) { + validateFunction(callback, 'callback'); + return async (chunks, options) => { + await callback(chunks, options); + return chunks; + }; +} + +/** + * Create a sync pass-through transform that observes chunks. + * @param {Function} callback + * @returns {Function} + */ +function tapSync(callback) { + validateFunction(callback, 'callback'); + return (chunks) => { + callback(chunks); + return chunks; + }; +} + +// ============================================================================= +// Drain Utility +// ============================================================================= + +/** + * Wait for a drainable object's backpressure to clear. + * @param {object} drainable + * @returns {Promise|null} + */ +function ondrain(drainable) { + if ( + drainable === null || + drainable === undefined || + typeof drainable !== 'object' + ) { + return null; + } + + if ( + !(drainableProtocol in drainable) || + typeof drainable[drainableProtocol] !== 'function' + ) { + return null; + } + + return drainable[drainableProtocol](); +} + +// ============================================================================= +// Merge Utility +// ============================================================================= + +/** + * Merge multiple async iterables by yielding values in temporal order. + * @param {...(AsyncIterable|object)} args + * @returns {AsyncIterable} + */ +function merge(...args) { + let sources; + let options; + + if (args.length > 0 && isMergeOptions(args[args.length - 1])) { + options = args[args.length - 1]; + sources = ArrayPrototypeSlice(args, 0, -1); + } else { + sources = args; + } + + if (options?.signal !== undefined) { + validateAbortSignal(options.signal, 'options.signal'); + } + + // Normalize each source via from() + const normalized = ArrayPrototypeMap(sources, (source) => from(source)); + + return { + __proto__: null, + async *[SymbolAsyncIterator]() { + const signal = options?.signal; + + signal?.throwIfAborted(); + + if (normalized.length === 0) return; + + if (normalized.length === 1) { + for await (const batch of normalized[0]) { + signal?.throwIfAborted(); + yield batch; + } + return; + } + + // Multiple sources - use a ready queue so that batches that settle + // between consumer pulls are drained synchronously without an extra + // async tick per batch. Each source has at most one pending .next() + // at a time. Every batch from every source is preserved. + const ready = []; + let activeCount = normalized.length; + let waitResolve = null; + let onAbort; + + if (signal) { + onAbort = () => { + if (waitResolve) { + waitResolve(); + waitResolve = null; + } + }; + signal.addEventListener('abort', onAbort, { + __proto__: null, + once: true, + }); + } + + // Called when a source's .next() settles. Pushes the result into + // the ready queue and wakes the consumer if it's waiting. + const onSettled = (iterator, result) => { + if (result.done) { + activeCount--; + } else { + ArrayPrototypePush(ready, result.value); + // Immediately request the next value from this source + // (at most one pending .next() per source) + PromisePrototypeThen( + iterator.next(), + (r) => onSettled(iterator, r), + (err) => { + ArrayPrototypePush(ready, { __proto__: null, error: err }); + if (waitResolve) { + waitResolve(); + waitResolve = null; + } + }, + ); + } + if (waitResolve) { + waitResolve(); + waitResolve = null; + } + }; + + // Start one .next() per source + const iterators = []; + for (let i = 0; i < normalized.length; i++) { + const iterator = normalized[i][SymbolAsyncIterator](); + ArrayPrototypePush(iterators, iterator); + PromisePrototypeThen( + iterator.next(), + (r) => onSettled(iterator, r), + (err) => { + ArrayPrototypePush(ready, { __proto__: null, error: err }); + if (waitResolve) { + waitResolve(); + waitResolve = null; + } + }, + ); + } + + let primaryError; + try { + while (activeCount > 0 || ready.length > 0) { + signal?.throwIfAborted(); + + // Drain ready queue synchronously + while (ready.length > 0) { + const item = ArrayPrototypeShift(ready); + if (item?.error) { + throw item.error; + } + yield item; + } + + // If sources are still active, wait for the next settlement + if (activeCount > 0) { + await new Promise((resolve) => { + waitResolve = resolve; + if (signal?.aborted) { + waitResolve = null; + resolve(); + } + }); + } + } + } catch (err) { + primaryError = err; + } finally { + if (onAbort !== undefined) { + signal.removeEventListener('abort', onAbort); + } + // Clean up: return all iterators. Cleanup errors are not + // swallowed - a broken iterator.return() (e.g., failing to + // release a resource) should be visible to the caller. + await cleanupIterators( + iterators, + primaryError, + signal?.aborted && primaryError === signal.reason, + ); + } + }, + }; +} + +async function cleanupIterators(iterators, primaryError, skipAwaitCleanup) { + let cleanupError; + await SafePromiseAllReturnVoid(iterators, async (iterator) => { + if (iterator.return) { + try { + const result = iterator.return(); + if (skipAwaitCleanup) { + markPromiseAsHandled(result); + } else { + await result; + } + } catch (err) { + // Keep the first cleanup error encountered. + cleanupError ??= err; + } + } + }); + if (cleanupError !== undefined) { + if (primaryError !== undefined) { + // Both a primary error and a cleanup error occurred. + // Wrap in SuppressedError so neither is lost: + // .error = primaryError, .suppressed = cleanupError. + // eslint-disable-next-line no-restricted-syntax + throw new SuppressedError(primaryError, cleanupError); + } + // No primary error - the cleanup error is the only error. + throw cleanupError; + } + if (primaryError !== undefined) { + throw primaryError; + } +} + +module.exports = { + array, + arrayBuffer, + arrayBufferSync, + arraySync, + bytes, + bytesSync, + merge, + ondrain, + tap, + tapSync, + text, + textSync, +}; diff --git a/lib/internal/streams/iter/duplex.js b/lib/internal/streams/iter/duplex.js new file mode 100644 index 00000000000000..bd06f37303cfc6 --- /dev/null +++ b/lib/internal/streams/iter/duplex.js @@ -0,0 +1,137 @@ +'use strict'; + +// New Streams API - Duplex Channel +// +// Creates a pair of connected channels where data written to one +// channel's writer appears in the other channel's readable. + +const { + SymbolAsyncDispose, + SymbolAsyncIterator, +} = primordials; + +const { + push, +} = require('internal/streams/iter/push'); +const { + validateAbortSignal, + validateObject, +} = require('internal/validators'); + +/** + * Create a pair of connected duplex channels for bidirectional communication. + * @param {{ highWaterMark?: number, backpressure?: string, signal?: AbortSignal, + * a?: object, b?: object }} [options] + * @returns {[DuplexChannel, DuplexChannel]} + */ +function duplex(options = { __proto__: null }) { + validateObject(options, 'options'); + const { highWaterMark, backpressure, signal, a, b } = options; + if (a !== undefined) { + validateObject(a, 'options.a'); + } + if (b !== undefined) { + validateObject(b, 'options.b'); + } + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + } + + // Channel A writes to B's readable (A->B direction). + // Signal is NOT passed to push() -- we handle abort via close() below. + const { writer: aWriter, readable: bReadable } = push({ + highWaterMark: a?.highWaterMark ?? highWaterMark, + backpressure: a?.backpressure ?? backpressure, + }); + + // Channel B writes to A's readable (B->A direction) + const { writer: bWriter, readable: aReadable } = push({ + highWaterMark: b?.highWaterMark ?? highWaterMark, + backpressure: b?.backpressure ?? backpressure, + }); + + let aClosed = false; + let bClosed = false; + // Track active iterators so close() can call .return() on them + let aReadableIterator = null; + let bReadableIterator = null; + + const channelA = { + __proto__: null, + get writer() { return aWriter; }, + // Wrap readable to track the iterator for cleanup on close() + get readable() { + return { + __proto__: null, + [SymbolAsyncIterator]() { + const iter = aReadable[SymbolAsyncIterator](); + aReadableIterator = iter; + return iter; + }, + }; + }, + async close() { + if (aClosed) return; + aClosed = true; + // End the writer (signals end-of-stream to B's readable) + aWriter.endSync(); + // Stop iteration of this channel's readable + if (aReadableIterator?.return) { + await aReadableIterator.return(); + aReadableIterator = null; + } + }, + [SymbolAsyncDispose]() { + return this.close(); + }, + }; + + const channelB = { + __proto__: null, + get writer() { return bWriter; }, + get readable() { + return { + __proto__: null, + [SymbolAsyncIterator]() { + const iter = bReadable[SymbolAsyncIterator](); + bReadableIterator = iter; + return iter; + }, + }; + }, + async close() { + if (bClosed) return; + bClosed = true; + bWriter.endSync(); + if (bReadableIterator?.return) { + await bReadableIterator.return(); + bReadableIterator = null; + } + }, + [SymbolAsyncDispose]() { + return this.close(); + }, + }; + + // Signal handler: fail both writers with the abort reason so consumers + // see the error. This is an error-path shutdown, not a clean close. + if (signal) { + const abortBoth = () => { + const reason = signal.reason; + aWriter.fail(reason); + bWriter.fail(reason); + }; + if (signal.aborted) { + abortBoth(); + } else { + signal.addEventListener('abort', abortBoth, + { __proto__: null, once: true }); + } + } + + return [channelA, channelB]; +} + +module.exports = { + duplex, +}; diff --git a/lib/internal/streams/iter/from.js b/lib/internal/streams/iter/from.js new file mode 100644 index 00000000000000..1efe83e9a04162 --- /dev/null +++ b/lib/internal/streams/iter/from.js @@ -0,0 +1,638 @@ +'use strict'; + +// New Streams API - from() and fromSync() +// +// Creates normalized byte stream iterables from various input types. +// Handles recursive flattening of nested iterables and protocol conversions. + +const { + ArrayBufferIsView, + ArrayIsArray, + ArrayPrototypeEvery, + ArrayPrototypePush, + ArrayPrototypeSlice, + DataViewPrototypeGetBuffer, + DataViewPrototypeGetByteLength, + DataViewPrototypeGetByteOffset, + FunctionPrototypeCall, + SymbolAsyncIterator, + SymbolIterator, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteOffset, + Uint8Array, +} = primordials; + +const { + codes: { + ERR_INVALID_ARG_TYPE, + }, +} = require('internal/errors'); + +const { + isAnyArrayBuffer, + isPromise, + isTypedArray, + isUint8Array, +} = require('internal/util/types'); + +const { + kValidatedSource, + toStreamable, + toAsyncStreamable, +} = require('internal/streams/iter/types'); + +const { + hasProtocol, + toUint8Array, +} = require('internal/streams/iter/utils'); + +// Maximum number of chunks to yield per batch from from()/fromSync(). +// Bounds peak memory when arrays flow through transforms, which must +// allocate output for the entire batch at once. +const FROM_BATCH_SIZE = 128; + +// ============================================================================= +// Type Guards and Detection +// ============================================================================= + +/** + * Check if value is a primitive chunk (string, ArrayBuffer, or ArrayBufferView). + * @returns {boolean} + */ +function isPrimitiveChunk(value) { + return typeof value === 'string' || isAnyArrayBuffer(value) || ArrayBufferIsView(value); +} + +/** + * Check if value is a sync iterable (has Symbol.iterator). + * @returns {boolean} + */ +function isSyncIterable(value) { + // We do not consider regular strings to be sync iterables in this context. + // We don't care about boxed strings (String objects) since they are uncommon. + return typeof value !== 'string' && + typeof value?.[SymbolIterator] === 'function'; +} + +/** + * Check if value is an async iterable (has Symbol.asyncIterator). + * @returns {boolean} + */ +function isAsyncIterable(value) { + return typeof value?.[SymbolAsyncIterator] === 'function'; +} + +// ============================================================================= +// Primitive Conversion +// ============================================================================= + +/** + * Convert a primitive chunk to Uint8Array. + * - string: UTF-8 encoded + * - ArrayBuffer: wrapped as Uint8Array view (no copy) + * - ArrayBufferView: converted to Uint8Array view of same memory + * @param {string|ArrayBuffer|ArrayBufferView} chunk + * @returns {Uint8Array} + */ +function primitiveToUint8Array(chunk) { + if (typeof chunk === 'string') { + return toUint8Array(chunk); + } + if (isAnyArrayBuffer(chunk)) { + return new Uint8Array(chunk); + } + if (isUint8Array(chunk)) { + return chunk; + } + // Other ArrayBufferView types (Int8Array, DataView, etc.) + return arrayBufferViewToUint8Array(chunk); +} + +function arrayBufferViewToUint8Array(chunk) { + if (isTypedArray(chunk)) { + return new Uint8Array( + TypedArrayPrototypeGetBuffer(chunk), + TypedArrayPrototypeGetByteOffset(chunk), + TypedArrayPrototypeGetByteLength(chunk), + ); + } + return new Uint8Array( + DataViewPrototypeGetBuffer(chunk), + DataViewPrototypeGetByteOffset(chunk), + DataViewPrototypeGetByteLength(chunk), + ); +} + +// ============================================================================= +// Sync Normalization (for fromSync and sync contexts) +// ============================================================================= + +/** + * Normalize a sync streamable yield value to Uint8Array chunks. + * Recursively flattens arrays, iterables, and protocol conversions. + * @yields {Uint8Array} + */ +function* normalizeSyncValue(value) { + // Handle primitives + if (isPrimitiveChunk(value)) { + yield primitiveToUint8Array(value); + return; + } + + // Handle ToStreamable protocol + if (hasProtocol(value, toStreamable)) { + const result = FunctionPrototypeCall(value[toStreamable], value); + yield* normalizeSyncValue(result); + return; + } + + // Handle arrays (which are also iterable, but check first for efficiency) + if (ArrayIsArray(value)) { + for (let i = 0; i < value.length; i++) { + yield* normalizeSyncValue(value[i]); + } + return; + } + + // Handle other sync iterables + if (isSyncIterable(value)) { + for (const item of value) { + yield* normalizeSyncValue(item); + } + return; + } + + // Reject: no valid conversion + throw new ERR_INVALID_ARG_TYPE( + 'value', + ['string', 'ArrayBuffer', 'ArrayBufferView', 'Iterable', 'toStreamable'], + value, + ); +} + +/** + * Check if value is already a Uint8Array[] batch (fast path). + * @returns {boolean} + */ +function isUint8ArrayBatch(value) { + if (!ArrayIsArray(value)) return false; + const len = value.length; + if (len === 0) return true; + // Fast path: single-element batch (most common from transforms) + if (len === 1) return isUint8Array(value[0]); + // Check first and last before iterating all elements + if (!isUint8Array(value[0]) || !isUint8Array(value[len - 1])) return false; + if (len === 2) return true; + for (let i = 1; i < len - 1; i++) { + if (!isUint8Array(value[i])) return false; + } + return true; +} + +function* yieldBoundedBatch(batch) { + if (batch.length === 0) { + return; + } + if (batch.length <= FROM_BATCH_SIZE) { + yield batch; + return; + } + for (let i = 0; i < batch.length; i += FROM_BATCH_SIZE) { + yield ArrayPrototypeSlice(batch, i, i + FROM_BATCH_SIZE); + } +} + +/** + * Normalize a sync streamable source, yielding batches of Uint8Array. + * @param {Iterable} source + * @yields {Uint8Array[]} + */ +function* normalizeSyncSource(source) { + let batch = []; + + for (const value of source) { + // Fast path 1: value is already a Uint8Array[] batch + if (isUint8ArrayBatch(value)) { + if (batch.length > 0) { + yield batch; + batch = []; + } + yield* yieldBoundedBatch(value); + continue; + } + // Fast path 2: value is a single Uint8Array (very common) + if (isUint8Array(value)) { + ArrayPrototypePush(batch, value); + if (batch.length === FROM_BATCH_SIZE) { + yield batch; + batch = []; + } + continue; + } + // Slow path: normalize the value + if (batch.length > 0) { + yield batch; + batch = []; + } + let valueBatch = []; + for (const chunk of normalizeSyncValue(value)) { + ArrayPrototypePush(valueBatch, chunk); + if (valueBatch.length === FROM_BATCH_SIZE) { + yield valueBatch; + valueBatch = []; + } + } + if (valueBatch.length > 0) { + yield valueBatch; + } + } + + if (batch.length > 0) { + yield batch; + } +} + +// ============================================================================= +// Async Normalization (for from and async contexts) +// ============================================================================= + +/** + * Normalize an async streamable yield value to Uint8Array chunks. + * Recursively flattens arrays, iterables, async iterables, promises, + * and protocol conversions. + * @yields {Uint8Array} + */ +async function* normalizeAsyncValue(value) { + // Handle promises first + if (isPromise(value)) { + const resolved = await value; + yield* normalizeAsyncValue(resolved); + return; + } + + // Handle primitives + if (isPrimitiveChunk(value)) { + yield primitiveToUint8Array(value); + return; + } + + // Handle ToAsyncStreamable protocol (check before ToStreamable) + if (hasProtocol(value, toAsyncStreamable)) { + const result = FunctionPrototypeCall(value[toAsyncStreamable], value); + if (isPromise(result)) { + yield* normalizeAsyncValue(await result); + } else { + yield* normalizeAsyncValue(result); + } + return; + } + + // Handle ToStreamable protocol + if (hasProtocol(value, toStreamable)) { + const result = FunctionPrototypeCall(value[toStreamable], value); + yield* normalizeAsyncValue(result); + return; + } + + // Handle arrays (which are also iterable, but check first for efficiency) + if (ArrayIsArray(value)) { + for (let i = 0; i < value.length; i++) { + yield* normalizeAsyncValue(value[i]); + } + return; + } + + // Handle async iterables (check before sync iterables since some objects + // have both) + if (isAsyncIterable(value)) { + for await (const item of value) { + yield* normalizeAsyncValue(item); + } + return; + } + + // Handle sync iterables + if (isSyncIterable(value)) { + for (const item of value) { + yield* normalizeAsyncValue(item); + } + return; + } + + // Reject: no valid conversion + throw new ERR_INVALID_ARG_TYPE( + 'value', + ['string', 'ArrayBuffer', 'ArrayBufferView', 'Iterable', 'AsyncIterable', + 'toStreamable', 'toAsyncStreamable'], + value, + ); +} + +/** + * Normalize an async streamable source, yielding batches of Uint8Array. + * @param {AsyncIterable|Iterable} source + * @yields {Uint8Array[]} + */ +async function* normalizeAsyncSource(source) { + // Prefer async iteration if available + if (isAsyncIterable(source)) { + for await (const value of source) { + // Fast path 1: value is already a Uint8Array[] batch + if (isUint8ArrayBatch(value)) { + if (value.length > 0) { + yield value; + } + continue; + } + // Fast path 2: value is a single Uint8Array (very common) + if (isUint8Array(value)) { + yield [value]; + continue; + } + // Slow path: normalize the value + const batch = []; + for await (const chunk of normalizeAsyncValue(value)) { + ArrayPrototypePush(batch, chunk); + } + if (batch.length > 0) { + yield batch; + } + } + return; + } + + // Fall back to sync iteration - batch sync values together with a bound. + if (isSyncIterable(source)) { + let batch = []; + + for (const value of source) { + // Fast path 1: value is already a Uint8Array[] batch + if (isUint8ArrayBatch(value)) { + // Flush any accumulated batch first + if (batch.length > 0) { + yield batch; + batch = []; + } + yield* yieldBoundedBatch(value); + continue; + } + // Fast path 2: value is a single Uint8Array (very common) + if (isUint8Array(value)) { + ArrayPrototypePush(batch, value); + if (batch.length === FROM_BATCH_SIZE) { + yield batch; + batch = []; + } + continue; + } + // Slow path: normalize the value - must flush and yield individually + if (batch.length > 0) { + yield batch; + batch = []; + } + let asyncBatch = []; + for await (const chunk of normalizeAsyncValue(value)) { + ArrayPrototypePush(asyncBatch, chunk); + if (asyncBatch.length === FROM_BATCH_SIZE) { + yield asyncBatch; + asyncBatch = []; + } + } + if (asyncBatch.length > 0) { + yield asyncBatch; + } + } + + // Yield any remaining batched values + if (batch.length > 0) { + yield batch; + } + return; + } + + throw new ERR_INVALID_ARG_TYPE( + 'source', + ['Iterable', 'AsyncIterable'], + source, + ); +} + +// ============================================================================= +// Public API: from() and fromSync() +// ============================================================================= + +/** + * Create a SyncByteStreamReadable from a ByteInput or SyncStreamable. + * @param {string|ArrayBuffer|ArrayBufferView|Iterable} input + * @returns {Iterable} + */ +function fromSync(input) { + if (input == null) { + throw new ERR_INVALID_ARG_TYPE('input', 'a non-null value', input); + } + + // Check for primitives first (ByteInput) + if (isPrimitiveChunk(input)) { + const chunk = primitiveToUint8Array(input); + return { + __proto__: null, + *[SymbolIterator]() { + yield [chunk]; + }, + }; + } + + // Fast path: Uint8Array[] - yield in bounded sub-batches. + // Yielding the entire array as one batch forces downstream transforms + // to process all data at once, causing peak memory proportional to total + // data volume. Sub-batching keeps peak memory bounded while preserving + // the throughput benefit of batched processing. + if (ArrayIsArray(input)) { + if (input.length === 0) { + return { + __proto__: null, + *[SymbolIterator]() { + // Empty - yield nothing + }, + }; + } + // Check if it's an array of Uint8Array (common case) + if (isUint8Array(input[0])) { + const allUint8 = ArrayPrototypeEvery(input, isUint8Array); + if (allUint8) { + const batch = input; + return { + __proto__: null, + *[SymbolIterator]() { + if (batch.length <= FROM_BATCH_SIZE) { + yield batch; + } else { + for (let i = 0; i < batch.length; i += FROM_BATCH_SIZE) { + yield ArrayPrototypeSlice(batch, i, i + FROM_BATCH_SIZE); + } + } + }, + }; + } + } + } + + // Check toStreamable protocol (takes precedence over iteration protocols). + // toAsyncStreamable is ignored entirely in fromSync. + if (typeof input[toStreamable] === 'function') { + return fromSync(input[toStreamable]()); + } + + // Reject explicit async inputs + if (isAsyncIterable(input)) { + throw new ERR_INVALID_ARG_TYPE( + 'input', + 'a synchronous input (not AsyncIterable)', + input, + ); + } + if (typeof input === 'object' && input !== null && typeof input.then === 'function') { + throw new ERR_INVALID_ARG_TYPE( + 'input', + 'a synchronous input (not Promise)', + input, + ); + } + + // Must be a SyncStreamable + if (!isSyncIterable(input)) { + throw new ERR_INVALID_ARG_TYPE( + 'input', + ['string', 'ArrayBuffer', 'ArrayBufferView', 'Iterable', 'toStreamable'], + input, + ); + } + + return { + __proto__: null, + *[SymbolIterator]() { + yield* normalizeSyncSource(input); + }, + }; +} + +/** + * Create a ByteStreamReadable from a ByteInput or Streamable. + * @param {string|ArrayBuffer|ArrayBufferView|Iterable|AsyncIterable} input + * @returns {AsyncIterable} + */ +function from(input) { + if (input == null) { + throw new ERR_INVALID_ARG_TYPE('input', 'a non-null value', input); + } + + // Fast path: validated source already yields valid Uint8Array[] batches + if (input[kValidatedSource]) { + return input; + } + + // Check for primitives first (ByteInput) + if (isPrimitiveChunk(input)) { + const chunk = primitiveToUint8Array(input); + return { + __proto__: null, + async *[SymbolAsyncIterator]() { + yield [chunk]; + }, + }; + } + + // Fast path: Uint8Array[] - yield in bounded sub-batches. + // Yielding the entire array as one batch forces downstream transforms + // to process all data at once, causing peak memory proportional to total + // data volume. Sub-batching keeps peak memory bounded while preserving + // the throughput benefit of batched processing. + if (ArrayIsArray(input)) { + if (input.length === 0) { + return { + __proto__: null, + async *[SymbolAsyncIterator]() { + // Empty - yield nothing + }, + }; + } + if (isUint8Array(input[0])) { + const allUint8 = ArrayPrototypeEvery(input, isUint8Array); + if (allUint8) { + const batch = input; + return { + __proto__: null, + async *[SymbolAsyncIterator]() { + if (batch.length <= FROM_BATCH_SIZE) { + yield batch; + } else { + for (let i = 0; i < batch.length; i += FROM_BATCH_SIZE) { + yield ArrayPrototypeSlice(batch, i, i + FROM_BATCH_SIZE); + } + } + }, + }; + } + } + } + + // Check toAsyncStreamable protocol (takes precedence over toStreamable and + // iteration protocols) + if (typeof input[toAsyncStreamable] === 'function') { + const result = input[toAsyncStreamable](); + // Synchronous validated source (e.g. Readable batched iterator) + if (result?.[kValidatedSource]) { + return result; + } + return { + __proto__: null, + async *[SymbolAsyncIterator]() { + // The result may be a Promise. Check validated on both the Promise + // itself (if tagged) and the resolved value. + const resolved = await result; + if (resolved?.[kValidatedSource]) { + yield* resolved[SymbolAsyncIterator](); + return; + } + yield* from(resolved)[SymbolAsyncIterator](); + }, + }; + } + + // Check toStreamable protocol (takes precedence over iteration protocols) + if (typeof input[toStreamable] === 'function') { + return from(input[toStreamable]()); + } + + // Must be a Streamable (sync or async iterable) + if (!isSyncIterable(input) && !isAsyncIterable(input)) { + throw new ERR_INVALID_ARG_TYPE( + 'input', + ['string', 'ArrayBuffer', 'ArrayBufferView', 'Iterable', + 'AsyncIterable', 'toStreamable', 'toAsyncStreamable'], + input, + ); + } + + return normalizeAsyncSource(input); +} + +// ============================================================================= +// Exports +// ============================================================================= + +module.exports = { + arrayBufferViewToUint8Array, + from, + fromSync, + isAsyncIterable, + isPrimitiveChunk, + isSyncIterable, + isUint8ArrayBatch, + normalizeAsyncSource, + normalizeAsyncValue, + normalizeSyncSource, + normalizeSyncValue, + primitiveToUint8Array, +}; diff --git a/lib/internal/streams/iter/pull.js b/lib/internal/streams/iter/pull.js new file mode 100644 index 00000000000000..95ec2a084cdaf3 --- /dev/null +++ b/lib/internal/streams/iter/pull.js @@ -0,0 +1,1060 @@ +'use strict'; + +// New Streams API - Pull Pipeline +// +// pull(), pullSync(), pipeTo(), pipeToSync() +// Pull-through pipelines with transforms. Data flows on-demand from source +// through transforms to consumer. + +const { + ArrayBufferIsView, + ArrayFromAsync, + ArrayIsArray, + ArrayPrototypePush, + ArrayPrototypeSlice, + PromisePrototypeThen, + PromiseResolve, + SymbolAsyncIterator, + SymbolIterator, + TypedArrayPrototypeGetByteLength, + Uint8Array, +} = primordials; + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + }, +} = require('internal/errors'); +const { lazyDOMException } = require('internal/util'); +const { validateAbortSignal } = require('internal/validators'); +const { + isAnyArrayBuffer, + isPromise, + isUint8Array, +} = require('internal/util/types'); +const { AbortController } = require('internal/abort_controller'); + +const { + arrayBufferViewToUint8Array, + from, + fromSync, + isSyncIterable, + isAsyncIterable, + isPrimitiveChunk, + isUint8ArrayBatch, + normalizeAsyncValue, +} = require('internal/streams/iter/from'); + +const { + isPullOptions, + isTransform, + isTransformObject, + parsePullArgs, + toUint8Array, + wrapError, + yieldAbortable, +} = require('internal/streams/iter/utils'); + +const { + kValidatedSource, + kValidatedTransform, + toAsyncStreamable, + toStreamable, +} = require('internal/streams/iter/types'); + +// ============================================================================= +// Type Guards and Helpers +// ============================================================================= + +/** + * Check if a value is a Writer (has write method). + * @returns {boolean} + */ +function hasMethod(value, name) { + return typeof value?.[name] === 'function'; +} + +/** + * Parse pipeTo/pipeToSync arguments: [...transforms, writer, options?] + * @param {Array} args + * @param {string} requiredMethod - 'write' for pipeTo, 'writeSync' for pipeToSync + * @returns {{ transforms: Array, writer: object, options: object }} + */ +function parsePipeToArgs(args, requiredMethod) { + if (args.length === 0) { + throw new ERR_INVALID_ARG_VALUE('args', args, 'pipeTo requires a writer argument'); + } + + let options; + let writerIndex = args.length - 1; + + // Check if last arg is options + const last = args[args.length - 1]; + if (isPullOptions(last) && !hasMethod(last, requiredMethod)) { + options = last; + writerIndex = args.length - 2; + } + + if (writerIndex < 0) { + throw new ERR_INVALID_ARG_VALUE('args', args, 'pipeTo requires a writer argument'); + } + + const writer = args[writerIndex]; + if (!hasMethod(writer, requiredMethod)) { + throw new ERR_INVALID_ARG_TYPE( + 'writer', `object with a ${requiredMethod} method`, writer); + } + + const transforms = ArrayPrototypeSlice(args, 0, writerIndex); + for (let i = 0; i < transforms.length; i++) { + if (!isTransform(transforms[i])) { + throw new ERR_INVALID_ARG_TYPE( + `transforms[${i}]`, ['Function', 'Object with transform()'], + transforms[i]); + } + } + + return { + __proto__: null, + transforms, + writer, + options, + }; +} + +function canUseSyncIterablePipeToFastPath(source, transforms, signal) { + if (signal !== undefined || + transforms.length !== 0 || + isPrimitiveChunk(source) || + ArrayIsArray(source) || + source?.[kValidatedSource] || + !isSyncIterable(source) || + isAsyncIterable(source)) { + return false; + } + + // Preserve from()'s top-level protocol precedence for custom iterables. + return typeof source[toAsyncStreamable] !== 'function' && + typeof source[toStreamable] !== 'function'; +} + +// ============================================================================= +// Transform Output Flattening +// ============================================================================= + +/** + * Flatten transform yield to Uint8Array chunks (sync). + * @yields {Uint8Array} + */ +function* flattenTransformYieldSync(value) { + if (isUint8Array(value)) { + yield value; + return; + } + if (typeof value === 'string') { + yield toUint8Array(value); + return; + } + if (isAnyArrayBuffer(value)) { + yield new Uint8Array(value); + return; + } + if (ArrayBufferIsView(value)) { + yield arrayBufferViewToUint8Array(value); + return; + } + // Must be Iterable + if (isSyncIterable(value)) { + for (const item of value) { + yield* flattenTransformYieldSync(item); + } + return; + } + throw new ERR_INVALID_ARG_TYPE( + 'value', + ['Uint8Array', 'string', 'ArrayBuffer', 'ArrayBufferView', 'Iterable'], + value); +} + +/** + * Flatten transform yield to Uint8Array chunks (async). + * @yields {Uint8Array} + */ +async function* flattenTransformYieldAsync(value) { + if (isUint8Array(value)) { + yield value; + return; + } + if (typeof value === 'string') { + yield toUint8Array(value); + return; + } + if (isAnyArrayBuffer(value)) { + yield new Uint8Array(value); + return; + } + if (ArrayBufferIsView(value)) { + yield arrayBufferViewToUint8Array(value); + return; + } + // Check for async iterable first + if (isAsyncIterable(value)) { + for await (const item of value) { + yield* flattenTransformYieldAsync(item); + } + return; + } + // Must be sync Iterable, no nested async iterables + if (isSyncIterable(value)) { + for (const item of value) { + yield* flattenTransformYieldSync(item); + } + return; + } + throw new ERR_INVALID_ARG_TYPE( + 'value', + ['Uint8Array', 'string', 'ArrayBuffer', 'ArrayBufferView', + 'Iterable', 'AsyncIterable'], + value); +} + +/** + * Process transform result (sync). + * @yields {Uint8Array[]} + */ +function* processTransformResultSync(result) { + if (result === null) { + return; + } + // Single Uint8Array -> wrap as batch + if (isUint8Array(result)) { + yield [result]; + return; + } + // String -> UTF-8 encode and wrap as batch + if (typeof result === 'string') { + yield [toUint8Array(result)]; + return; + } + // ArrayBuffer / ArrayBufferView -> convert and wrap + if (isAnyArrayBuffer(result)) { + yield [new Uint8Array(result)]; + return; + } + if (ArrayBufferIsView(result)) { + yield [arrayBufferViewToUint8Array(result)]; + return; + } + // Uint8Array[] batch + if (isUint8ArrayBatch(result)) { + if (result.length > 0) { + yield result; + } + return; + } + // Iterable or Generator + if (isSyncIterable(result)) { + const batch = []; + for (const item of result) { + for (const chunk of flattenTransformYieldSync(item)) { + ArrayPrototypePush(batch, chunk); + } + } + if (batch.length > 0) { + yield batch; + } + return; + } + throw new ERR_INVALID_ARG_TYPE( + 'result', + ['null', 'Uint8Array', 'string', 'ArrayBuffer', + 'ArrayBufferView', 'Array', 'Iterable'], + result); +} + +/** + * Append normalized transform result batches to an array (sync). + * @param {Array} target + * @param {*} result + */ +function appendTransformResultSync(target, result) { + if (result === null) { + return; + } + if (isUint8ArrayBatch(result)) { + if (result.length > 0) { + ArrayPrototypePush(target, result); + } + return; + } + if (isUint8Array(result)) { + ArrayPrototypePush(target, [result]); + return; + } + if (typeof result === 'string') { + ArrayPrototypePush(target, [toUint8Array(result)]); + return; + } + if (isAnyArrayBuffer(result)) { + ArrayPrototypePush(target, [new Uint8Array(result)]); + return; + } + if (ArrayBufferIsView(result)) { + ArrayPrototypePush(target, [arrayBufferViewToUint8Array(result)]); + return; + } + for (const batch of processTransformResultSync(result)) { + ArrayPrototypePush(target, batch); + } +} + +/** + * Process transform result (async). + * @yields {Uint8Array[]} + */ +async function* processTransformResultAsync(result) { + // Handle Promise + if (isPromise(result)) { + const resolved = await result; + yield* processTransformResultAsync(resolved); + return; + } + if (result === null) { + return; + } + // Single Uint8Array -> wrap as batch + if (isUint8Array(result)) { + yield [result]; + return; + } + // String -> UTF-8 encode and wrap as batch + if (typeof result === 'string') { + yield [toUint8Array(result)]; + return; + } + // ArrayBuffer / ArrayBufferView -> convert and wrap + if (isAnyArrayBuffer(result)) { + yield [new Uint8Array(result)]; + return; + } + if (ArrayBufferIsView(result)) { + yield [arrayBufferViewToUint8Array(result)]; + return; + } + // Uint8Array[] batch + if (isUint8ArrayBatch(result)) { + if (result.length > 0) { + yield result; + } + return; + } + // Check for async iterable/generator first + if (isAsyncIterable(result)) { + const batch = []; + for await (const item of result) { + if (isUint8Array(item)) { + ArrayPrototypePush(batch, item); + continue; + } + for await (const chunk of flattenTransformYieldAsync(item)) { + ArrayPrototypePush(batch, chunk); + } + } + if (batch.length > 0) { + yield batch; + } + return; + } + // Sync Iterable or Generator + if (isSyncIterable(result)) { + const batch = []; + for (const item of result) { + if (isUint8Array(item)) { + ArrayPrototypePush(batch, item); + continue; + } + // Note: This iteration is synchronous, since async iterables + // may not be nested within sync iterables. + for (const chunk of flattenTransformYieldSync(item)) { + ArrayPrototypePush(batch, chunk); + } + } + if (batch.length > 0) { + yield batch; + } + return; + } + throw new ERR_INVALID_ARG_TYPE( + 'result', + ['null', 'Uint8Array', 'string', 'ArrayBuffer', + 'ArrayBufferView', 'Array', 'Iterable', 'AsyncIterable', 'Promise'], + result); +} + +/** + * Append normalized transform result batches to an array (async). + * @param {Array} target + * @param {*} result + * @returns {Promise|undefined} + */ +function appendTransformResultAsync(target, result) { + if (result === null) { + return; + } + if (isUint8ArrayBatch(result)) { + if (result.length > 0) { + ArrayPrototypePush(target, result); + } + return; + } + if (isUint8Array(result)) { + ArrayPrototypePush(target, [result]); + return; + } + if (typeof result === 'string') { + ArrayPrototypePush(target, [toUint8Array(result)]); + return; + } + if (isAnyArrayBuffer(result)) { + ArrayPrototypePush(target, [new Uint8Array(result)]); + return; + } + if (ArrayBufferIsView(result)) { + ArrayPrototypePush(target, [arrayBufferViewToUint8Array(result)]); + return; + } + return appendTransformResultAsyncSlow(target, result); +} + +async function appendTransformResultAsyncSlow(target, result) { + for await (const batch of processTransformResultAsync(result)) { + ArrayPrototypePush(target, batch); + } +} + +// ============================================================================= +// Sync Pipeline Implementation +// ============================================================================= + +/** + * Apply a single stateless sync transform to a source. + * @yields {Uint8Array[]} + */ +/** + * Apply a fused run of stateless sync transforms. + * @param {Iterable} source + * @param {Array} run - Array of stateless transform functions + * @yields {Uint8Array[]} + */ +function* applyFusedStatelessSyncTransforms(source, run) { + for (const chunks of source) { + let current = chunks; + for (let i = 0; i < run.length; i++) { + const result = run[i](current); + if (result === null) { + current = null; + break; + } + current = result; + } + if (current === null) continue; + // Inline normalization with Uint8Array[] batch as the fast path, + // matching the async pipeline's check order. + if (isUint8ArrayBatch(current)) { + if (current.length > 0) yield current; + } else if (isUint8Array(current)) { + yield [current]; + } else if (typeof current === 'string') { + yield [toUint8Array(current)]; + } else if (isAnyArrayBuffer(current)) { + yield [new Uint8Array(current)]; + } else if (ArrayBufferIsView(current)) { + yield [arrayBufferViewToUint8Array(current)]; + } else { + yield* processTransformResultSync(current); + } + } + // Flush each transform after all upstream data, including data emitted by + // earlier flushes, has been processed by that transform. + let pending = []; + for (let i = 0; i < run.length; i++) { + const next = []; + for (let j = 0; j < pending.length; j++) { + appendTransformResultSync(next, run[i](pending[j])); + } + appendTransformResultSync(next, run[i](null)); + pending = next; + } + for (let i = 0; i < pending.length; i++) { + yield pending[i]; + } +} + +/** + * Apply a single stateful sync transform to a source. + * @yields {Uint8Array[]} + */ +function* withFlushSync(source) { + yield* source; + yield null; +} + +function* applyStatefulSyncTransform(source, transform) { + const output = transform(withFlushSync(source)); + for (const item of output) { + const batch = []; + for (const chunk of flattenTransformYieldSync(item)) { + ArrayPrototypePush(batch, chunk); + } + if (batch.length > 0) { + yield batch; + } + } +} + +/** + * Create a sync pipeline from source through transforms. + * @yields {Uint8Array[]} + */ +function* createSyncPipeline(source, transforms) { + let current = source; + + // Apply transforms - fuse consecutive stateless transforms into a single + // generator layer to avoid unnecessary generator ticks. + let statelessRun = []; + + for (let i = 0; i < transforms.length; i++) { + const transform = transforms[i]; + if (isTransformObject(transform)) { + if (statelessRun.length > 0) { + current = applyFusedStatelessSyncTransforms(current, statelessRun); + statelessRun = []; + } + current = applyStatefulSyncTransform(current, transform.transform); + } else { + ArrayPrototypePush(statelessRun, transform); + } + } + if (statelessRun.length > 0) { + current = applyFusedStatelessSyncTransforms(current, statelessRun); + } + + yield* current; +} + +// ============================================================================= +// Async Pipeline Implementation +// ============================================================================= + +/** + * Apply a single stateless async transform to a source. + * @yields {Uint8Array[]} + */ +/** + * Apply a fused run of stateless async transforms to a source. + * All transforms in the run are applied in a tight synchronous loop per batch, + * avoiding the overhead of N async generator ticks for N transforms. + * + * INVARIANT: This function accepts a signal, NOT a pre-built options object. + * A fresh { __proto__: null, signal } options object is created for each + * transform invocation to prevent cross-transform mutation. + * @param {AsyncIterable} source + * @param {Array} run - Array of stateless transform functions + * @param {AbortSignal} signal - The pipeline's abort signal + * @yields {Uint8Array[]} + */ +async function* applyFusedStatelessAsyncTransforms(source, run, signal) { + for await (const chunks of source) { + let current = chunks; + for (let i = 0; i < run.length; i++) { + const result = run[i](current, { __proto__: null, signal }); + if (result === null) { + current = null; + break; + } + if (isPromise(result)) { + const resolved = await result; + if (resolved === null) { + current = null; + break; + } + current = resolved; + } else { + current = result; + } + } + if (current === null) continue; + // Normalize the final output + if (isUint8ArrayBatch(current)) { + if (current.length > 0) yield current; + } else if (isUint8Array(current)) { + yield [current]; + } else if (typeof current === 'string') { + yield [toUint8Array(current)]; + } else if (isAnyArrayBuffer(current)) { + yield [new Uint8Array(current)]; + } else if (ArrayBufferIsView(current)) { + yield [arrayBufferViewToUint8Array(current)]; + } else { + yield* processTransformResultAsync(current); + } + } + // Flush each transform after all upstream data, including data emitted by + // earlier flushes, has been processed by that transform. + let pending = []; + for (let i = 0; i < run.length; i++) { + const next = []; + for (let j = 0; j < pending.length; j++) { + const pendingResult = appendTransformResultAsync( + next, + run[i](pending[j], { __proto__: null, signal })); + if (pendingResult !== undefined) { + await pendingResult; + } + } + const flushResult = appendTransformResultAsync( + next, + run[i](null, { __proto__: null, signal })); + if (flushResult !== undefined) { + await flushResult; + } + pending = next; + } + for (let i = 0; i < pending.length; i++) { + yield pending[i]; + } +} + +/** + * Append a null flush signal after the source is exhausted. + * @yields {Uint8Array[]} + */ +/** + * Append a null flush signal after the source is exhausted. + * @yields {Uint8Array[]} + */ +async function* withFlushAsync(source) { + yield* source; + yield null; +} + +async function* applyStatefulAsyncTransform(source, transform, options) { + const output = transform(withFlushAsync(source), options); + for await (const item of output) { + // Fast path: item is already a Uint8Array[] batch (e.g. compression transforms) + if (isUint8ArrayBatch(item)) { + if (item.length > 0) { + yield item; + } + continue; + } + // Fast path: single Uint8Array + if (isUint8Array(item)) { + yield [item]; + continue; + } + // Slow path: flatten arbitrary transform yield + const batch = []; + for await (const chunk of flattenTransformYieldAsync(item)) { + ArrayPrototypePush(batch, chunk); + } + if (batch.length > 0) { + yield batch; + } + } +} + +/** + * Fast path for validated stateful transforms (e.g. compression). + * Skips withFlushAsync (transform handles done internally) and + * skips isUint8ArrayBatch validation (transform guarantees valid output). + * @yields {Uint8Array[]} + */ +async function* applyValidatedStatefulAsyncTransform(source, transform, options) { + const output = transform(source, options); + for await (const batch of output) { + if (batch.length > 0) { + yield batch; + } + } + // Check abort after the transform completes - without the + // withFlushAsync wrapper there is no extra yield to give + // the outer pipeline a chance to see the abort. + options.signal?.throwIfAborted(); +} + +/** + * Create an async pipeline from source through transforms. + * @yields {Uint8Array[]} + */ +async function* createAsyncPipeline(source, transforms, signal) { + // Check for abort + signal?.throwIfAborted(); + + // Fast path: no transforms, just yield normalized source directly + if (transforms.length === 0) { + yield* yieldAbortable(source, signal); + return; + } + + const normalized = yieldAbortable(source, signal); + + // Create internal controller for transform cancellation. + // Note: if signal was already aborted, we threw above - no need to check here. + const controller = new AbortController(); + let abortHandler; + if (signal) { + abortHandler = () => { + controller.abort(signal.reason ?? + lazyDOMException('Aborted', 'AbortError')); + }; + signal.addEventListener('abort', abortHandler, { __proto__: null, once: true }); + } + + // Apply transforms - fuse consecutive stateless transforms into a single + // generator layer to avoid unnecessary async generator ticks. + // + // INVARIANT: Each transform invocation MUST receive its own fresh options + // object ({ __proto__: null, signal }). Transforms may mutate the options + // object, so sharing a single object across invocations would allow one + // transform to corrupt the options seen by another. The signal is shared + // across calls (mutations to it are acceptable), but the containing options + // object must be unique per call. This is enforced inside + // applyFusedStatelessAsyncTransforms and applyStatefulAsyncTransform, which + // accept the signal directly and create the options object per invocation. + // DO NOT pass a pre-built options object. + let current = normalized; + const transformSignal = controller.signal; + let statelessRun = []; + + for (let i = 0; i < transforms.length; i++) { + const transform = transforms[i]; + if (isTransformObject(transform)) { + // Flush any accumulated stateless run before the stateful transform + if (statelessRun.length > 0) { + current = applyFusedStatelessAsyncTransforms(current, statelessRun, + transformSignal); + statelessRun = []; + } + const opts = { __proto__: null, signal: transformSignal }; + if (transform[kValidatedTransform]) { + current = applyValidatedStatefulAsyncTransform( + current, transform.transform, opts); + } else { + current = applyStatefulAsyncTransform( + current, transform.transform, opts); + } + } else { + ArrayPrototypePush(statelessRun, transform); + } + } + // Flush remaining stateless run + if (statelessRun.length > 0) { + current = applyFusedStatelessAsyncTransforms(current, statelessRun, + transformSignal); + } + + let completed = false; + try { + for await (const batch of current) { + controller.signal.throwIfAborted(); + yield batch; + } + completed = true; + } catch (error) { + if (!controller.signal.aborted) { + controller.abort(wrapError(error)); + } + throw error; + } finally { + if (!completed && !controller.signal.aborted) { + // Consumer stopped early or generator return() was called. + // If a transform listener throws here, let it propagate. + controller.abort(lazyDOMException('Aborted', 'AbortError')); + } + // Clean up user signal listener to prevent holding controller alive + if (signal && abortHandler) { + signal.removeEventListener('abort', abortHandler); + } + } +} + +// ============================================================================= +// Public API: pull() and pullSync() +// ============================================================================= + +/** + * Create a sync pull-through pipeline with transforms. + * @param {Iterable} source - The sync streamable source + * @param {...Function} transforms - Variadic transforms + * @returns {Iterable} + */ +function pullSync(source, ...transforms) { + for (let i = 0; i < transforms.length; i++) { + if (!isTransform(transforms[i])) { + throw new ERR_INVALID_ARG_TYPE( + `transforms[${i}]`, ['Function', 'Object with transform()'], + transforms[i]); + } + } + return { + __proto__: null, + *[SymbolIterator]() { + yield* createSyncPipeline(fromSync(source), transforms); + }, + }; +} + +/** + * Create an async pull-through pipeline with transforms. + * @param {Iterable|AsyncIterable} source - The streamable source + * @param {...(Function|object)} args - Transforms, with optional PullOptions + * as last argument + * @returns {AsyncIterable} + */ +function pull(source, ...args) { + const { transforms, options } = parsePullArgs(args); + const signal = options?.signal; + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + // Eagerly check abort at call time per spec + if (signal.aborted) { + return { + __proto__: null, + // eslint-disable-next-line require-yield + async *[SymbolAsyncIterator]() { + throw signal.reason; + }, + }; + } + } + + return { + __proto__: null, + async *[SymbolAsyncIterator]() { + yield* createAsyncPipeline(from(source), transforms, signal); + }, + }; +} + +// ============================================================================= +// Public API: pipeTo() and pipeToSync() +// ============================================================================= + +/** + * Write a sync source through transforms to a sync writer. + * @param {Iterable} source + * @param {...(Function|object)} args - Transforms, writer, and optional options + * @returns {number} Total bytes written + */ +function pipeToSync(source, ...args) { + const { transforms, writer, options } = parsePipeToArgs(args, 'writeSync'); + + // Normalize source and create pipeline + const normalized = fromSync(source); + const pipeline = transforms.length > 0 ? + createSyncPipeline(normalized, transforms) : + normalized; + + let totalBytes = 0; + const hasWritevSync = typeof writer.writevSync === 'function'; + const hasEndSync = typeof writer.endSync === 'function'; + + try { + let canContinue = true; + for (const batch of pipeline) { + if (!canContinue) { + break; + } + if (hasWritevSync && batch.length > 1) { + if (writer.writevSync(batch) === false) { + break; + } + for (let i = 0; i < batch.length; i++) { + totalBytes += TypedArrayPrototypeGetByteLength(batch[i]); + } + } else { + for (let i = 0; i < batch.length; i++) { + const chunk = batch[i]; + if (writer.writeSync(chunk) === false) { + canContinue = false; + break; + } + totalBytes += TypedArrayPrototypeGetByteLength(chunk); + } + } + } + + if (!options?.preventClose) { + if (!hasEndSync || writer.endSync() < 0) { + writer.end?.(); + } + } + } catch (error) { + if (!options?.preventFail) { + writer.fail?.(wrapError(error)); + } + throw error; + } + + return totalBytes; +} + +/** + * Write an async source through transforms to a writer. + * @param {AsyncIterable|Iterable} source + * @param {...(Function|object)} args - Transforms, writer, and optional options + * @returns {Promise} Total bytes written + */ +async function pipeTo(source, ...args) { + const { transforms, writer, options } = parsePipeToArgs(args, 'write'); + if (options?.signal !== undefined) { + validateAbortSignal(options.signal, 'options.signal'); + } + + const signal = options?.signal; + + // Check for abort + signal?.throwIfAborted(); + + const hasWriteSync = typeof writer.writeSync === 'function'; + const useSyncIterableFastPath = + hasWriteSync && canUseSyncIterablePipeToFastPath(source, transforms, signal); + const normalized = useSyncIterableFastPath ? undefined : from(source); + + let totalBytes = 0; + const hasWritev = typeof writer.writev === 'function'; + const hasWritevSync = typeof writer.writevSync === 'function'; + const hasEndSync = typeof writer.endSync === 'function'; + + // Async fallback for writeBatch when sync write fails partway through. + // Continues writing from batch[startIndex] using async write(). + async function writeBatchAsyncFallback(batch, startIndex) { + for (let i = startIndex; i < batch.length; i++) { + const chunk = batch[i]; + if (hasWriteSync && writer.writeSync(chunk)) { + // Sync retry succeeded + } else { + const result = writer.write( + chunk, signal ? { __proto__: null, signal } : undefined); + if (result !== undefined) { + await result; + } + } + totalBytes += TypedArrayPrototypeGetByteLength(chunk); + } + } + + // Write a batch using try-fallback: sync first, async if needed. + // Returns undefined on sync success, or a Promise when async fallback + // is required. Callers must check: const p = writeBatch(b); if (p) await p; + function writeBatch(batch) { + if (hasWritev && batch.length > 1) { + if (!hasWritevSync || !writer.writevSync(batch)) { + const opts = signal ? { __proto__: null, signal } : undefined; + const writevResult = writer.writev(batch, opts); + if (writevResult === undefined) { + for (let i = 0; i < batch.length; i++) { + totalBytes += TypedArrayPrototypeGetByteLength(batch[i]); + } + return; + } + return PromisePrototypeThen(PromiseResolve(writevResult), () => { + for (let i = 0; i < batch.length; i++) { + totalBytes += TypedArrayPrototypeGetByteLength(batch[i]); + } + }); + } + for (let i = 0; i < batch.length; i++) { + totalBytes += TypedArrayPrototypeGetByteLength(batch[i]); + } + return; + } + for (let i = 0; i < batch.length; i++) { + const chunk = batch[i]; + if (!hasWriteSync || !writer.writeSync(chunk)) { + // Sync path failed at index i - fall back to async for the rest. + return writeBatchAsyncFallback(batch, i); + } + totalBytes += TypedArrayPrototypeGetByteLength(chunk); + } + } + + try { + if (useSyncIterableFastPath) { + // Avoid from()'s async sync-iterable batching path. This keeps writes + // incremental for synchronous sources while preserving async + // normalization for non-primitive yielded values. + for (const value of source) { + if (isUint8ArrayBatch(value)) { + if (value.length > 0) { + const p = writeBatch(value); + if (p) await p; + } + continue; + } + if (isUint8Array(value)) { + const p = writeBatch([value]); + if (p) await p; + continue; + } + + const batch = await ArrayFromAsync(normalizeAsyncValue(value)); + if (batch.length > 0) { + const p = writeBatch(batch); + if (p) await p; + } + } + } else if (transforms.length === 0) { + // Fast path: no transforms - iterate normalized source directly + if (signal) { + for await (const batch of yieldAbortable(normalized, signal)) { + signal.throwIfAborted(); + const p = writeBatch(batch); + if (p) await p; + } + } else { + for await (const batch of normalized) { + const p = writeBatch(batch); + if (p) await p; + } + } + } else { + const pipeline = createAsyncPipeline(normalized, transforms, signal); + + if (signal) { + for await (const batch of pipeline) { + signal.throwIfAborted(); + const p = writeBatch(batch); + if (p) await p; + } + } else { + for await (const batch of pipeline) { + const p = writeBatch(batch); + if (p) await p; + } + } + } + + if (!options?.preventClose) { + if (!hasEndSync || writer.endSync() < 0) { + await writer.end?.(signal ? { __proto__: null, signal } : undefined); + } + } + } catch (error) { + if (!options?.preventFail) { + writer.fail?.(wrapError(error)); + } + throw error; + } + + return totalBytes; +} + +module.exports = { + pipeTo, + pipeToSync, + pull, + pullSync, +}; diff --git a/lib/internal/streams/iter/push.js b/lib/internal/streams/iter/push.js new file mode 100644 index 00000000000000..04ed9646d8334e --- /dev/null +++ b/lib/internal/streams/iter/push.js @@ -0,0 +1,721 @@ +'use strict'; + +// New Streams API - Push Stream Implementation +// +// Creates a bonded pair of writer and async iterable for push-based streaming +// with built-in backpressure. + +const { + ArrayIsArray, + ArrayPrototypePush, + MathMax, + PromiseReject, + PromiseResolve, + PromiseWithResolvers, + Symbol, + SymbolAsyncDispose, + SymbolAsyncIterator, + SymbolDispose, + TypedArrayPrototypeGetByteLength, +} = primordials; + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_STATE, + }, +} = require('internal/errors'); +const { isError, lazyDOMException } = require('internal/util'); +const { + validateAbortSignal, + validateInteger, +} = require('internal/validators'); + +const { + drainableProtocol, +} = require('internal/streams/iter/types'); + +const { + kPushDefaultHWM, + kResolvedPromise, + clampHWM, + onSignalAbort, + toUint8Array, + convertChunks, + parsePullArgs, + validateBackpressure, +} = require('internal/streams/iter/utils'); + +const { + pull: pullWithTransforms, +} = require('internal/streams/iter/pull'); + +const { + RingBuffer, +} = require('internal/streams/iter/ringbuffer'); + +const kNoFailReason = Symbol('kNoFailReason'); + +// ============================================================================= +// PushQueue - Internal Queue with Chunk-Based Backpressure +// ============================================================================= + +class PushQueue { + /** Buffered chunks (each slot is from one write/writev call) */ + #slots = new RingBuffer(); + /** Pending writes waiting for buffer space */ + #pendingWrites = new RingBuffer(); + /** Pending reads waiting for data */ + #pendingReads = new RingBuffer(); + /** Pending drains waiting for backpressure to clear */ + #pendingDrains = []; + /** Writer state: 'open' | 'closing' | 'closed' | 'errored' */ + #writerState = 'open'; + /** Consumer state: 'active' | 'returned' | 'thrown' */ + #consumerState = 'active'; + /** Error that closed the stream */ + #error = null; + /** Total bytes written */ + #bytesWritten = 0; + /** Pending end promise (resolves when consumer drains past end sentinel) */ + #pendingEnd = null; + + /** Configuration */ + #highWaterMark; + #backpressure; + #signal; + #abortHandler; + + constructor(options = { __proto__: null }) { + const { + highWaterMark = kPushDefaultHWM, + backpressure = 'strict', + signal, + } = options; + validateInteger(highWaterMark, 'options.highWaterMark'); + validateBackpressure(backpressure); + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + } + this.#highWaterMark = clampHWM(highWaterMark); + this.#backpressure = backpressure; + this.#signal = signal; + this.#abortHandler = undefined; + + if (this.#signal) { + this.#abortHandler = () => { + this.fail(isError(this.#signal.reason) ? + this.#signal.reason : + lazyDOMException('Aborted', 'AbortError')); + }; + onSignalAbort(this.#signal, this.#abortHandler); + } + } + + // =========================================================================== + // Writer Methods + // =========================================================================== + + /** + * Get slots available before hitting highWaterMark. + * Returns null if writer is closed/errored or consumer has terminated. + * @returns {number | null} + */ + get desiredSize() { + if (this.#writerState !== 'open' || this.#consumerState !== 'active') { + return null; + } + return MathMax(0, this.#highWaterMark - this.#slots.length); + } + + /** + * Check if a sync write would be accepted. + * @returns {boolean} + */ + canWriteSync() { + if (this.#writerState !== 'open') return false; + if (this.#consumerState !== 'active') return false; + if ((this.#backpressure === 'strict' || + this.#backpressure === 'block') && + this.#slots.length >= this.#highWaterMark) { + return false; + } + return true; + } + + /** + * Write chunks synchronously if possible. + * Returns true if write completed, false if buffer is full. + * @returns {boolean} + */ + writeSync(chunks) { + if (this.#writerState !== 'open') return false; + if (this.#consumerState !== 'active') return false; + + if (this.#slots.length >= this.#highWaterMark) { + switch (this.#backpressure) { + case 'strict': + return false; + case 'block': + return false; + case 'drop-oldest': + if (this.#slots.length > 0) { + this.#slots.shift(); + } + break; + case 'drop-newest': + // Discard this write, but return true + for (let i = 0; i < chunks.length; i++) { + this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]); + } + return true; + } + } + + this.#slots.push(chunks); + for (let i = 0; i < chunks.length; i++) { + this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]); + } + + this.#resolvePendingReads(); + return true; + } + + /** + * Write chunks asynchronously. + * If signal is provided, a write blocked on backpressure will reject + * immediately when the signal fires. The cancelled write is removed from + * pendingWrites so it does not occupy a slot. The queue itself is NOT put + * into an error state - this is per-operation cancellation, not terminal + * failure. + * @returns {Promise} + */ + async writeAsync(chunks, signal) { + // Check writer state before signal (spec order: state, then signal) + if (this.#writerState === 'closed') { + throw new ERR_INVALID_STATE.TypeError('Writer is closed'); + } + if (this.#writerState === 'closing') { + throw new ERR_INVALID_STATE.TypeError('Writer is closing'); + } + if (this.#writerState === 'errored') { + throw this.#error; + } + if (this.#consumerState !== 'active') { + throw this.#consumerState === 'thrown' && this.#error ? + this.#error : + new ERR_INVALID_STATE.TypeError('Stream closed by consumer'); + } + + // Check for pre-aborted signal (after state checks per spec) + signal?.throwIfAborted(); + + // Try sync first + if (this.writeSync(chunks)) { + return; + } + + // Buffer is full + switch (this.#backpressure) { + case 'strict': + if (this.#pendingWrites.length >= this.#highWaterMark) { + throw new ERR_INVALID_STATE.RangeError( + 'Backpressure violation: too many pending writes. ' + + 'Await each write() call to respect backpressure.'); + } + return this.#createPendingWrite(chunks, signal); + case 'block': + return this.#createPendingWrite(chunks, signal); + default: + throw new ERR_INVALID_STATE( + 'Unexpected: writeSync should have handled non-strict policy'); + } + } + + /** + * Create a pending write promise, optionally racing against a signal. + * If the signal fires, the entry is removed from pendingWrites and the + * promise rejects. Signal listeners are cleaned up on normal resolution. + * @returns {Promise} + */ + #createPendingWrite(chunks, signal) { + const { promise, resolve, reject } = PromiseWithResolvers(); + const entry = { __proto__: null, chunks, resolve, reject }; + this.#pendingWrites.push(entry); + + if (signal) { + const onAbort = () => { + // Remove from queue so it doesn't occupy a slot + const idx = this.#pendingWrites.indexOf(entry); + if (idx !== -1) this.#pendingWrites.removeAt(idx); + reject(signal.reason ?? lazyDOMException('Aborted', 'AbortError')); + }; + + // Wrap resolve/reject to clean up signal listener + entry.resolve = function() { + signal.removeEventListener('abort', onAbort); + resolve(); + }; + entry.reject = function(reason) { + signal.removeEventListener('abort', onAbort); + reject(reason); + }; + + signal.addEventListener('abort', onAbort, { __proto__: null, once: true }); + } + + return promise; + } + + /** + * Signal end of stream. Returns total bytes written. + * @returns {number} + */ + end() { + if (this.#writerState === 'errored') { + return -2; // Signal to reject with stored error + } + if (this.#writerState === 'closing') { + return -3; // Signal to PushWriter: wait for drain to complete + } + if (this.#writerState === 'closed') { + return this.#bytesWritten; // Idempotent + } + + this.#cleanup(); + this.#rejectPendingWrites( + new ERR_INVALID_STATE.TypeError('Writer closed')); + this.#resolvePendingDrains(false); + + // If buffer is empty, close immediately + if (this.#slots.length === 0) { + this.#writerState = 'closed'; + this.#resolvePendingReads(); + return this.#bytesWritten; + } + + // Buffer has data: transition to closing, defer completion until drained + this.#writerState = 'closing'; + return -3; // Signal to PushWriter: create deferred end promise + } + + /** + * Called by the read path when the consumer has drained all data while + * the writer is in the 'closing' state. Transitions to 'closed' and + * resolves the pending end promise. + */ + endDrained() { + if (this.#writerState !== 'closing') return; + this.#writerState = 'closed'; + if (this.#pendingEnd) { + this.#pendingEnd.resolve(this.#bytesWritten); + this.#pendingEnd = null; + } + } + + /** + * Put queue into terminal error state. + * No-op if errored or closed (fully drained). + * If closing (draining), short-circuits the drain. + */ + fail(reason = kNoFailReason) { + if (this.#writerState === 'errored' || this.#writerState === 'closed') { + return; + } + + const wasClosing = this.#writerState === 'closing'; + this.#writerState = 'errored'; + this.#error = reason === kNoFailReason ? + new ERR_INVALID_STATE('Failed') : + reason; + this.#cleanup(); + this.#rejectPendingReads(this.#error); + this.#rejectPendingDrains(this.#error); + + if (wasClosing) { + // Short-circuit the graceful drain: reject the pending end promise + if (this.#pendingEnd) { + this.#pendingEnd.reject(this.#error); + this.#pendingEnd = null; + } + } else { + this.#rejectPendingWrites(this.#error); + } + } + + get totalBytesWritten() { + return this.#bytesWritten; + } + + get error() { + return this.#error; + } + + get backpressurePolicy() { + return this.#backpressure; + } + + get writerState() { + return this.#writerState; + } + + get pendingEndPromise() { + return this.#pendingEnd?.promise ?? null; + } + + setPendingEnd(pending) { + this.#pendingEnd = pending; + } + + /** + * Wait for backpressure to clear (desiredSize > 0). + * @returns {Promise} + */ + waitForDrain() { + const { promise, resolve, reject } = PromiseWithResolvers(); + ArrayPrototypePush(this.#pendingDrains, { __proto__: null, resolve, reject }); + return promise; + } + + // =========================================================================== + // Consumer Methods + // =========================================================================== + + async read() { + // If there's data in the buffer, return it immediately + if (this.#slots.length > 0) { + const result = this.#drain(); + this.#resolvePendingWrites(); + // After draining, check if writer was closing and buffer is now empty + if (this.#writerState === 'closing' && this.#slots.length === 0) { + this.endDrained(); + } + return { __proto__: null, value: result, done: false }; + } + + // Buffer empty and writer closing = drain complete + if (this.#writerState === 'closing') { + this.endDrained(); + return { __proto__: null, value: undefined, done: true }; + } + + if (this.#writerState === 'closed') { + return { __proto__: null, value: undefined, done: true }; + } + + if (this.#writerState === 'errored') { + throw this.#error; + } + + const { promise, resolve, reject } = PromiseWithResolvers(); + this.#pendingReads.push({ __proto__: null, resolve, reject }); + return promise; + } + + consumerReturn() { + if (this.#consumerState !== 'active') return; + this.#consumerState = 'returned'; + this.#cleanup(); + this.#resolvePendingReads(); + this.#rejectPendingWrites( + new ERR_INVALID_STATE.TypeError('Stream closed by consumer')); + // If closing, reject the pending end promise + if (this.#writerState === 'closing' && this.#pendingEnd) { + this.#pendingEnd.reject( + new ERR_INVALID_STATE.TypeError('Stream closed by consumer')); + this.#pendingEnd = null; + } + // Resolve pending drains with false - no more data will be consumed + this.#resolvePendingDrains(false); + } + + consumerThrow(error) { + if (this.#consumerState !== 'active') return; + this.#consumerState = 'thrown'; + this.#error = error; + this.#cleanup(); + this.#rejectPendingReads(error); + this.#rejectPendingWrites(error); + if (this.#writerState === 'closing' && this.#pendingEnd) { + this.#pendingEnd.reject(error); + this.#pendingEnd = null; + } + // Reject pending drains - the consumer errored + this.#rejectPendingDrains(error); + } + + // =========================================================================== + // Private Methods + // =========================================================================== + + #drain() { + if (this.#slots.length === 1) { + return this.#slots.shift(); + } + + const result = []; + for (let i = 0; i < this.#slots.length; i++) { + const slot = this.#slots.get(i); + for (let j = 0; j < slot.length; j++) { + ArrayPrototypePush(result, slot[j]); + } + } + this.#slots.clear(); + return result; + } + + #resolvePendingReads() { + while (this.#pendingReads.length > 0) { + if (this.#slots.length > 0) { + const pending = this.#pendingReads.shift(); + const result = this.#drain(); + this.#resolvePendingWrites(); + pending.resolve({ __proto__: null, value: result, done: false }); + } else if (this.#writerState === 'closing' && this.#slots.length === 0) { + this.endDrained(); + const pending = this.#pendingReads.shift(); + pending.resolve({ __proto__: null, value: undefined, done: true }); + } else if (this.#writerState === 'closed') { + const pending = this.#pendingReads.shift(); + pending.resolve({ __proto__: null, value: undefined, done: true }); + } else if (this.#writerState === 'errored') { + const pending = this.#pendingReads.shift(); + pending.reject(this.#error); + } else if (this.#consumerState === 'returned') { + const pending = this.#pendingReads.shift(); + pending.resolve({ __proto__: null, done: true, value: undefined }); + } else { + break; + } + } + } + + #resolvePendingWrites() { + while (this.#pendingWrites.length > 0 && + this.#slots.length < this.#highWaterMark) { + const pending = this.#pendingWrites.shift(); + this.#slots.push(pending.chunks); + for (let i = 0; i < pending.chunks.length; i++) { + this.#bytesWritten += TypedArrayPrototypeGetByteLength(pending.chunks[i]); + } + pending.resolve(); + } + + if (this.#slots.length < this.#highWaterMark) { + this.#resolvePendingDrains(true); + } + } + + #resolvePendingDrains(canWrite) { + const drains = this.#pendingDrains; + this.#pendingDrains = []; + for (let i = 0; i < drains.length; i++) { + drains[i].resolve(canWrite); + } + } + + #rejectPendingDrains(error) { + const drains = this.#pendingDrains; + this.#pendingDrains = []; + for (let i = 0; i < drains.length; i++) { + drains[i].reject(error); + } + } + + #rejectPendingReads(error) { + while (this.#pendingReads.length > 0) { + this.#pendingReads.shift().reject(error); + } + } + + #rejectPendingWrites(error) { + while (this.#pendingWrites.length > 0) { + this.#pendingWrites.shift().reject(error); + } + } + + #cleanup() { + if (this.#signal && this.#abortHandler) { + this.#signal.removeEventListener('abort', this.#abortHandler); + this.#abortHandler = undefined; + } + } +} + +// ============================================================================= +// PushWriter Implementation +// ============================================================================= + +class PushWriter { + #queue; + + constructor(queue) { + this.#queue = queue; + } + + [drainableProtocol]() { + const desired = this.desiredSize; + if (desired === null) return null; + if (desired > 0) return PromiseResolve(true); + return this.#queue.waitForDrain(); + } + + get desiredSize() { + return this.#queue.desiredSize; + } + + write(chunk, options) { + if (!options?.signal && this.#queue.canWriteSync()) { + const bytes = toUint8Array(chunk); + this.#queue.writeSync([bytes]); + return kResolvedPromise; + } + const bytes = toUint8Array(chunk); + return this.#queue.writeAsync([bytes], options?.signal); + } + + writev(chunks, options) { + if (!ArrayIsArray(chunks)) { + throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks); + } + if (!options?.signal && this.#queue.canWriteSync()) { + const bytes = convertChunks(chunks); + this.#queue.writeSync(bytes); + return kResolvedPromise; + } + const bytes = convertChunks(chunks); + return this.#queue.writeAsync(bytes, options?.signal); + } + + writeSync(chunk) { + const bytes = toUint8Array(chunk); + return this.#queue.writeSync([bytes]); + } + + writevSync(chunks) { + if (!ArrayIsArray(chunks)) { + throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks); + } + const bytes = convertChunks(chunks); + return this.#queue.writeSync(bytes); + } + + end(options) { + const result = this.#queue.end(); + if (result === -2) { + // Errored: reject with stored error + return PromiseReject(this.#queue.error); + } + if (result === -3) { + // Closing: buffer has data, create deferred promise that resolves + // when consumer drains past the end sentinel + const pendingEndPromise = this.#queue.pendingEndPromise; + if (pendingEndPromise !== null) { + return pendingEndPromise; + } + const { promise, resolve, reject } = PromiseWithResolvers(); + this.#queue.setPendingEnd({ __proto__: null, promise, resolve, reject }); + return promise; + } + // >= 0: byte count (immediate close or idempotent) + return PromiseResolve(result); + } + + endSync() { + const result = this.#queue.end(); + if (result === -2) return -1; // Errored + if (result === -3) return -1; // Buffer not empty, can't wait + return result; + } + + fail(reason) { + this.#queue.fail(arguments.length === 0 ? kNoFailReason : reason); + } + + [SymbolAsyncDispose]() { + const state = this.#queue.writerState; + if (state === 'closing') { + // Wait for graceful drain + return this.#queue.pendingEndPromise ?? PromiseResolve(); + } + if (state === 'open') { + this.fail(); + } + return PromiseResolve(); + } + + [SymbolDispose]() { + this.fail(); + } +} + +// ============================================================================= +// Readable Implementation +// ============================================================================= + +function createReadable(queue) { + return { + __proto__: null, + [SymbolAsyncIterator]() { + return { + __proto__: null, + async next() { + return queue.read(); + }, + async return() { + queue.consumerReturn(); + return { __proto__: null, value: undefined, done: true }; + }, + async throw(error) { + queue.consumerThrow(error); + return { __proto__: null, value: undefined, done: true }; + }, + }; + }, + }; +} + +// ============================================================================= +// Stream.push() Factory +// ============================================================================= + +function parseArgs(args) { + const result = parsePullArgs(args); + // PushQueue constructor requires a non-undefined options object. + if (result.options === undefined) { + result.options = { __proto__: null }; + } + return result; +} + +/** + * Create a push stream with optional transforms. + * @param {...(Function|object)} args - Transforms, then options (optional) + * @returns {{ writer: Writer, readable: AsyncIterable }} + */ +function push(...args) { + const { transforms, options } = parseArgs(args); + + const queue = new PushQueue(options); + const writer = new PushWriter(queue); + const rawReadable = createReadable(queue); + + // Apply transforms lazily if provided + let readable; + if (transforms.length > 0) { + if (options.signal) { + readable = pullWithTransforms( + rawReadable, ...transforms, { __proto__: null, signal: options.signal }); + } else { + readable = pullWithTransforms(rawReadable, ...transforms); + } + } else { + readable = rawReadable; + } + + return { __proto__: null, writer, readable }; +} + +module.exports = { + push, +}; diff --git a/lib/internal/streams/iter/ringbuffer.js b/lib/internal/streams/iter/ringbuffer.js new file mode 100644 index 00000000000000..a05b7825fb86ae --- /dev/null +++ b/lib/internal/streams/iter/ringbuffer.js @@ -0,0 +1,151 @@ +'use strict'; + +// RingBuffer - O(1) FIFO queue with indexed access. +// +// Replaces plain JS arrays that are used as queues with shift()/push(). +// Array.shift() is O(n) because it copies all remaining elements; +// RingBuffer.shift() is O(1) -- it just advances a head pointer. +// +// Also provides O(1) trimFront(count) to replace Array.splice(0, count). +// +// Capacity is always a power of 2, so modulo is replaced with bitwise AND. + +const { + Array, +} = primordials; + +class RingBuffer { + #backing; + #head = 0; + #size = 0; + #mask; + + constructor(initialCapacity = 16) { + this.#mask = initialCapacity - 1; + this.#backing = new Array(initialCapacity); + } + + get length() { + return this.#size; + } + + /** + * Append an item to the tail. O(1) amortized. + */ + push(item) { + if (this.#size > this.#mask) { + this.#grow(); + } + this.#backing[(this.#head + this.#size) & this.#mask] = item; + this.#size++; + } + + /** + * Prepend an item to the head. O(1) amortized. + */ + unshift(item) { + if (this.#size > this.#mask) { + this.#grow(); + } + this.#head = (this.#head - 1 + this.#mask + 1) & this.#mask; + this.#backing[this.#head] = item; + this.#size++; + } + + /** + * Remove and return the item at the head. O(1). + * @returns {any} + */ + shift() { + if (this.#size === 0) return undefined; + const item = this.#backing[this.#head]; + this.#backing[this.#head] = undefined; // Help GC + this.#head = (this.#head + 1) & this.#mask; + this.#size--; + return item; + } + + /** + * Read item at a logical index (0 = head). O(1). + * Returns undefined if index is out of bounds. + * @returns {any} + */ + get(index) { + if (index < 0 || index >= this.#size) return undefined; + return this.#backing[(this.#head + index) & this.#mask]; + } + + /** + * Remove `count` items from the head without returning them. + * O(count) for GC cleanup. + */ + trimFront(count) { + if (count <= 0) return; + if (count >= this.#size) { + this.clear(); + return; + } + for (let i = 0; i < count; i++) { + this.#backing[(this.#head + i) & this.#mask] = undefined; + } + this.#head = (this.#head + count) & this.#mask; + this.#size -= count; + } + + /** + * Find the logical index of `item` (reference equality). O(n). + * Returns -1 if not found. + * @returns {number} + */ + indexOf(item) { + for (let i = 0; i < this.#size; i++) { + if (this.#backing[(this.#head + i) & this.#mask] === item) { + return i; + } + } + return -1; + } + + /** + * Remove the item at logical `index`, shifting later elements. O(n) worst case. + * Used only on rare abort-signal cancellation path. + */ + removeAt(index) { + if (index < 0 || index >= this.#size) return; + for (let i = index; i < this.#size - 1; i++) { + const from = (this.#head + i + 1) & this.#mask; + const to = (this.#head + i) & this.#mask; + this.#backing[to] = this.#backing[from]; + } + const last = (this.#head + this.#size - 1) & this.#mask; + this.#backing[last] = undefined; + this.#size--; + } + + /** + * Remove all items. O(n) for GC cleanup. + */ + clear() { + for (let i = 0; i < this.#size; i++) { + this.#backing[(this.#head + i) & this.#mask] = undefined; + } + this.#head = 0; + this.#size = 0; + } + + /** + * Double the backing capacity, linearizing the circular layout. + */ + #grow() { + const newCapacity = (this.#mask + 1) * 2; + const newBacking = new Array(newCapacity); + for (let i = 0; i < this.#size; i++) { + newBacking[i] = this.#backing[(this.#head + i) & this.#mask]; + } + this.#backing = newBacking; + this.#head = 0; + this.#mask = newCapacity - 1; + } +} + +module.exports = { RingBuffer }; diff --git a/lib/internal/streams/iter/share.js b/lib/internal/streams/iter/share.js new file mode 100644 index 00000000000000..e65a5eb648b620 --- /dev/null +++ b/lib/internal/streams/iter/share.js @@ -0,0 +1,753 @@ +'use strict'; + +// New Streams API - Share +// +// Pull-model multi-consumer streaming. Shares a single source among +// multiple consumers with explicit buffering. + +const { + ArrayPrototypePush, + PromisePrototypeThen, + PromiseResolve, + PromiseWithResolvers, + SafeSet, + SymbolAsyncIterator, + SymbolDispose, + SymbolIterator, +} = primordials; + +const { + shareProtocol, + shareSyncProtocol, +} = require('internal/streams/iter/types'); + +const { + from, + fromSync, + isAsyncIterable, + isSyncIterable, +} = require('internal/streams/iter/from'); + +const { + pull: pullWithTransforms, + pullSync: pullSyncWithTransforms, +} = require('internal/streams/iter/pull'); + +const { + kMultiConsumerDefaultHWM, + clampHWM, + getMinCursor, + hasProtocol, + onSignalAbort, + wrapError, + parsePullArgs, + validateBackpressure, +} = require('internal/streams/iter/utils'); + +const { + RingBuffer, +} = require('internal/streams/iter/ringbuffer'); + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_RETURN_VALUE, + ERR_OUT_OF_RANGE, + }, +} = require('internal/errors'); +const { + validateAbortSignal, + validateInteger, + validateObject, +} = require('internal/validators'); + +// ============================================================================= +// Async Share Implementation +// ============================================================================= + +class ShareImpl { + #source; + #options; + #buffer = new RingBuffer(); + #bufferStart = 0; + #consumers = new SafeSet(); + #sourceIterator = null; + #sourceExhausted = false; + #sourceError = null; + #cancelled = false; + #pulling = false; + #pullWaiters = []; + #cachedMinCursor = 0; + #cachedMinCursorConsumers = 0; + + constructor(source, options) { + this.#source = source; + this.#options = options; + } + + get consumerCount() { + return this.#consumers.size; + } + + get bufferSize() { + return this.#buffer.length; + } + + pull(...args) { + const { transforms, options } = parsePullArgs(args); + const rawConsumer = this.#createRawConsumer(); + + if (transforms.length > 0 || options?.signal) { + if (options) { + return pullWithTransforms(rawConsumer, ...transforms, options); + } + return pullWithTransforms(rawConsumer, ...transforms); + } + return rawConsumer; + } + + #createRawConsumer() { + const state = { + __proto__: null, + cursor: this.#bufferStart, + resolve: null, + reject: null, + detached: false, + pendingNext: PromiseResolve(), + }; + + this.#consumers.add(state); + if (this.#consumers.size === 1) { + this.#cachedMinCursor = state.cursor; + this.#cachedMinCursorConsumers = 1; + } else if (state.cursor === this.#cachedMinCursor) { + this.#cachedMinCursorConsumers++; + } else { + this.#recomputeMinCursor(); + } + const self = this; + + return { + __proto__: null, + [SymbolAsyncIterator]() { + const getNext = async () => { + if (self.#sourceError) { + state.detached = true; + self.#consumers.delete(state); + throw self.#sourceError; + } + + // Loop until we get data, source is exhausted, or + // consumer is detached. Multiple consumers may be woken + // after a single pull - those that find no data at their + // cursor must re-pull rather than terminating prematurely. + for (;;) { + if (state.detached) { + if (self.#sourceError) throw self.#sourceError; + return { __proto__: null, done: true, value: undefined }; + } + + if (self.#cancelled) { + state.detached = true; + self.#deleteConsumer(state); + return { __proto__: null, done: true, value: undefined }; + } + + // Check if data is available in buffer + const bufferIndex = state.cursor - self.#bufferStart; + if (bufferIndex < self.#buffer.length) { + const chunk = self.#buffer.get(bufferIndex); + const cursor = state.cursor; + state.cursor++; + if (cursor === self.#cachedMinCursor && + --self.#cachedMinCursorConsumers === 0) { + self.#tryTrimBuffer(); + } + return { __proto__: null, done: false, value: chunk }; + } + + if (self.#sourceExhausted) { + state.detached = true; + self.#deleteConsumer(state); + if (self.#sourceError) throw self.#sourceError; + return { __proto__: null, done: true, value: undefined }; + } + + // Need to pull from source - check buffer limit + const canPull = await self.#waitForBufferSpace(); + if (!canPull) { + state.detached = true; + self.#deleteConsumer(state); + if (self.#sourceError) throw self.#sourceError; + return { __proto__: null, done: true, value: undefined }; + } + + await self.#pullFromSource(); + } + }; + + return { + __proto__: null, + next() { + const next = PromisePrototypeThen( + state.pendingNext, + getNext, + getNext); + state.pendingNext = + PromisePrototypeThen(next, undefined, () => {}); + return next; + }, + + async return() { + state.detached = true; + state.resolve = null; + state.reject = null; + if (self.#deleteConsumer(state)) { + self.#tryTrimBuffer(); + } + return { __proto__: null, done: true, value: undefined }; + }, + + async throw() { + state.detached = true; + state.resolve = null; + state.reject = null; + if (self.#deleteConsumer(state)) { + self.#tryTrimBuffer(); + } + return { __proto__: null, done: true, value: undefined }; + }, + }; + }, + }; + } + + cancel(reason) { + if (this.#cancelled) return; + this.#cancelled = true; + + if (reason !== undefined) { + this.#sourceError = reason; + } + + if (this.#sourceIterator?.return) { + PromisePrototypeThen(this.#sourceIterator.return(), undefined, () => {}); + } + + for (const consumer of this.#consumers) { + if (consumer.resolve) { + if (reason !== undefined) { + consumer.reject?.(reason); + } else { + consumer.resolve({ __proto__: null, done: true, value: undefined }); + } + consumer.resolve = null; + consumer.reject = null; + } + consumer.detached = true; + } + this.#consumers.clear(); + + for (let i = 0; i < this.#pullWaiters.length; i++) { + this.#pullWaiters[i](); + } + this.#pullWaiters = []; + } + + [SymbolDispose]() { + this.cancel(); + } + + // Internal methods + + async #waitForBufferSpace() { + while (this.#buffer.length >= this.#options.highWaterMark) { + if (this.#cancelled || this.#sourceError || this.#sourceExhausted) { + return !this.#cancelled; + } + + switch (this.#options.backpressure) { + case 'strict': + throw new ERR_OUT_OF_RANGE( + 'buffer size', `<= ${this.#options.highWaterMark}`, + this.#buffer.length); + case 'block': { + const { promise, resolve } = PromiseWithResolvers(); + ArrayPrototypePush(this.#pullWaiters, resolve); + await promise; + break; + } + case 'drop-oldest': + this.#buffer.shift(); + this.#bufferStart++; + for (const consumer of this.#consumers) { + if (consumer.cursor < this.#bufferStart) { + this.#deleteConsumerFromMin(consumer); + consumer.cursor = this.#bufferStart; + } + } + this.#recomputeMinCursor(); + return true; + case 'drop-newest': + return true; + } + } + return true; + } + + #pullFromSource() { + if (this.#sourceExhausted || this.#cancelled) { + return PromiseResolve(); + } + + if (this.#pulling) { + const { promise, resolve } = PromiseWithResolvers(); + ArrayPrototypePush(this.#pullWaiters, resolve); + return promise; + } + + this.#pulling = true; + + return (async () => { + try { + if (!this.#sourceIterator) { + if (isAsyncIterable(this.#source)) { + this.#sourceIterator = + this.#source[SymbolAsyncIterator](); + } else if (isSyncIterable(this.#source)) { + const syncIterator = + this.#source[SymbolIterator](); + this.#sourceIterator = { + __proto__: null, + async next() { + return syncIterator.next(); + }, + async return() { + return syncIterator.return?.() ?? + { __proto__: null, done: true, value: undefined }; + }, + }; + } else { + throw new ERR_INVALID_ARG_TYPE( + 'source', ['AsyncIterable', 'Iterable'], this.#source); + } + } + + const result = await this.#sourceIterator.next(); + + if (result.done) { + this.#sourceExhausted = true; + } else { + this.#buffer.push(result.value); + } + } catch (error) { + this.#sourceError = wrapError(error); + this.#sourceExhausted = true; + } finally { + this.#pulling = false; + for (let i = 0; i < this.#pullWaiters.length; i++) { + this.#pullWaiters[i](); + } + this.#pullWaiters = []; + } + })(); + } + + #tryTrimBuffer() { + if (this.#cachedMinCursorConsumers === 0) { + this.#recomputeMinCursor(); + } + const trimCount = this.#cachedMinCursor - this.#bufferStart; + if (trimCount > 0) { + this.#buffer.trimFront(trimCount); + this.#bufferStart = this.#cachedMinCursor; + for (let i = 0; i < this.#pullWaiters.length; i++) { + this.#pullWaiters[i](); + } + this.#pullWaiters = []; + } + } + + #recomputeMinCursor() { + const { minCursor, minCursorConsumers } = getMinCursor( + this.#consumers, this.#bufferStart + this.#buffer.length); + this.#cachedMinCursor = minCursor; + this.#cachedMinCursorConsumers = minCursorConsumers; + } + + #deleteConsumerFromMin(consumer) { + if (consumer.cursor === this.#cachedMinCursor) { + this.#cachedMinCursorConsumers--; + return this.#cachedMinCursorConsumers === 0; + } + return false; + } + + #deleteConsumer(consumer) { + if (this.#consumers.delete(consumer)) { + return this.#deleteConsumerFromMin(consumer); + } + return false; + } +} + +// ============================================================================= +// Sync Share Implementation +// ============================================================================= + +class SyncShareImpl { + #source; + #options; + #buffer = new RingBuffer(); + #bufferStart = 0; + #consumers = new SafeSet(); + #sourceIterator = null; + #sourceExhausted = false; + #sourceError = null; + #cancelled = false; + #cachedMinCursor = 0; + #cachedMinCursorConsumers = 0; + + constructor(source, options) { + this.#source = source; + this.#options = options; + } + + get consumerCount() { + return this.#consumers.size; + } + + get bufferSize() { + return this.#buffer.length; + } + + pull(...transforms) { + const rawConsumer = this.#createRawConsumer(); + + if (transforms.length > 0) { + return pullSyncWithTransforms(rawConsumer, ...transforms); + } + return rawConsumer; + } + + #createRawConsumer() { + const state = { + __proto__: null, + cursor: this.#bufferStart, + detached: false, + }; + + this.#consumers.add(state); + if (this.#consumers.size === 1) { + this.#cachedMinCursor = state.cursor; + this.#cachedMinCursorConsumers = 1; + } else if (state.cursor === this.#cachedMinCursor) { + this.#cachedMinCursorConsumers++; + } else { + this.#recomputeMinCursor(); + } + const self = this; + + return { + __proto__: null, + [SymbolIterator]() { + return { + __proto__: null, + next() { + if (state.detached) { + return { __proto__: null, done: true, value: undefined }; + } + if (self.#sourceError) { + state.detached = true; + self.#deleteConsumer(state); + throw self.#sourceError; + } + if (self.#cancelled) { + state.detached = true; + self.#deleteConsumer(state); + return { __proto__: null, done: true, value: undefined }; + } + + const bufferIndex = state.cursor - self.#bufferStart; + if (bufferIndex < self.#buffer.length) { + const chunk = self.#buffer.get(bufferIndex); + const cursor = state.cursor; + state.cursor++; + if (cursor === self.#cachedMinCursor && + --self.#cachedMinCursorConsumers === 0) { + self.#tryTrimBuffer(); + } + return { __proto__: null, done: false, value: chunk }; + } + + if (self.#sourceExhausted) { + state.detached = true; + self.#deleteConsumer(state); + return { __proto__: null, done: true, value: undefined }; + } + + // Check buffer limit + if (self.#buffer.length >= self.#options.highWaterMark) { + switch (self.#options.backpressure) { + case 'strict': + throw new ERR_OUT_OF_RANGE( + 'buffer size', `<= ${self.#options.highWaterMark}`, + self.#buffer.length); + case 'block': + throw new ERR_OUT_OF_RANGE( + 'buffer size', `<= ${self.#options.highWaterMark} ` + + '(blocking not available in sync context)', + self.#buffer.length); + case 'drop-oldest': + self.#buffer.shift(); + self.#bufferStart++; + for (const consumer of self.#consumers) { + if (consumer.cursor < self.#bufferStart) { + self.#deleteConsumerFromMin(consumer); + consumer.cursor = self.#bufferStart; + } + } + self.#recomputeMinCursor(); + break; + case 'drop-newest': + state.detached = true; + self.#deleteConsumer(state); + return { __proto__: null, done: true, value: undefined }; + } + } + + self.#pullFromSource(); + + if (self.#sourceError) { + state.detached = true; + self.#deleteConsumer(state); + throw self.#sourceError; + } + + const newBufferIndex = state.cursor - self.#bufferStart; + if (newBufferIndex < self.#buffer.length) { + const chunk = self.#buffer.get(newBufferIndex); + const cursor = state.cursor; + state.cursor++; + if (cursor === self.#cachedMinCursor && + --self.#cachedMinCursorConsumers === 0) { + self.#tryTrimBuffer(); + } + return { __proto__: null, done: false, value: chunk }; + } + + if (self.#sourceExhausted) { + state.detached = true; + self.#deleteConsumer(state); + return { __proto__: null, done: true, value: undefined }; + } + + return { __proto__: null, done: true, value: undefined }; + }, + + return() { + state.detached = true; + if (self.#deleteConsumer(state)) { + self.#tryTrimBuffer(); + } + return { __proto__: null, done: true, value: undefined }; + }, + + throw() { + state.detached = true; + if (self.#deleteConsumer(state)) { + self.#tryTrimBuffer(); + } + return { __proto__: null, done: true, value: undefined }; + }, + }; + }, + }; + } + + cancel(reason) { + if (this.#cancelled) return; + this.#cancelled = true; + + if (reason !== undefined) { + this.#sourceError = reason; + } + + if (this.#sourceIterator?.return) { + this.#sourceIterator.return(); + } + + for (const consumer of this.#consumers) { + consumer.detached = true; + } + this.#consumers.clear(); + } + + [SymbolDispose]() { + this.cancel(); + } + + #pullFromSource() { + if (this.#sourceExhausted || this.#cancelled) return; + + try { + this.#sourceIterator ||= this.#source[SymbolIterator](); + + const result = this.#sourceIterator.next(); + + if (result.done) { + this.#sourceExhausted = true; + } else { + this.#buffer.push(result.value); + } + } catch (error) { + this.#sourceError = wrapError(error); + this.#sourceExhausted = true; + } + } + + #tryTrimBuffer() { + if (this.#cachedMinCursorConsumers === 0) { + this.#recomputeMinCursor(); + } + const trimCount = this.#cachedMinCursor - this.#bufferStart; + if (trimCount > 0) { + this.#buffer.trimFront(trimCount); + this.#bufferStart = this.#cachedMinCursor; + } + } + + #recomputeMinCursor() { + const { minCursor, minCursorConsumers } = getMinCursor( + this.#consumers, this.#bufferStart + this.#buffer.length); + this.#cachedMinCursor = minCursor; + this.#cachedMinCursorConsumers = minCursorConsumers; + } + + #deleteConsumerFromMin(consumer) { + if (consumer.cursor === this.#cachedMinCursor) { + this.#cachedMinCursorConsumers--; + return this.#cachedMinCursorConsumers === 0; + } + return false; + } + + #deleteConsumer(consumer) { + if (this.#consumers.delete(consumer)) { + return this.#deleteConsumerFromMin(consumer); + } + return false; + } +} + +function onShareCancel(shareImpl, signal) { + onSignalAbort(signal, () => shareImpl.cancel(signal.reason)); +} + +// ============================================================================= +// Public API +// ============================================================================= + +function share(source, options = { __proto__: null }) { + // Normalize source via from() - accepts strings, ArrayBuffers, protocols, etc. + const normalized = from(source); + validateObject(options, 'options'); + const { + highWaterMark = kMultiConsumerDefaultHWM, + backpressure = 'strict', + signal, + } = options; + validateInteger(highWaterMark, 'options.highWaterMark'); + validateBackpressure(backpressure); + if (signal !== undefined) { + validateAbortSignal(signal, 'options.signal'); + } + + const opts = { + __proto__: null, + highWaterMark: clampHWM(highWaterMark), + backpressure, + signal, + }; + + const shareImpl = new ShareImpl(normalized, opts); + + if (signal) { + onShareCancel(shareImpl, signal); + } + + return shareImpl; +} + +function shareSync(source, options = { __proto__: null }) { + // Normalize source via fromSync() - accepts strings, ArrayBuffers, protocols, etc. + const normalized = fromSync(source); + validateObject(options, 'options'); + const { + highWaterMark = kMultiConsumerDefaultHWM, + backpressure = 'strict', + } = options; + validateInteger(highWaterMark, 'options.highWaterMark'); + validateBackpressure(backpressure); + + const opts = { + __proto__: null, + highWaterMark: clampHWM(highWaterMark), + backpressure, + }; + + return new SyncShareImpl(normalized, opts); +} + +function isShareable(value) { + return hasProtocol(value, shareProtocol); +} + +function isSyncShareable(value) { + return hasProtocol(value, shareSyncProtocol); +} + +const Share = { + __proto__: null, + from(input, options) { + if (isShareable(input)) { + const result = input[shareProtocol](options); + if (result === null || typeof result !== 'object') { + throw new ERR_INVALID_RETURN_VALUE( + 'an object', '[Symbol.for(\'Stream.shareProtocol\')]', result); + } + return result; + } + if (isAsyncIterable(input) || isSyncIterable(input)) { + return share(input, options); + } + throw new ERR_INVALID_ARG_TYPE( + 'input', ['Shareable', 'AsyncIterable', 'Iterable'], input); + }, +}; + +const SyncShare = { + __proto__: null, + fromSync(input, options) { + if (isSyncShareable(input)) { + const result = input[shareSyncProtocol](options); + if (result === null || typeof result !== 'object') { + throw new ERR_INVALID_RETURN_VALUE( + 'an object', '[Symbol.for(\'Stream.shareSyncProtocol\')]', result); + } + return result; + } + if (isSyncIterable(input)) { + return shareSync(input, options); + } + throw new ERR_INVALID_ARG_TYPE( + 'input', ['SyncShareable', 'Iterable'], input); + }, +}; + +module.exports = { + Share, + SyncShare, + share, + shareSync, +}; diff --git a/lib/internal/streams/iter/transform.js b/lib/internal/streams/iter/transform.js new file mode 100644 index 00000000000000..9782f5f50ebf2c --- /dev/null +++ b/lib/internal/streams/iter/transform.js @@ -0,0 +1,830 @@ +'use strict'; + +// Compression / Decompression Transforms +// +// Creates bare native zlib handles via internalBinding('zlib'), bypassing +// the stream.Transform / ZlibBase / EventEmitter machinery entirely. +// Compression runs on the libuv threadpool via handle.write() (async) so +// I/O and upstream transforms can overlap with compression work. +// Each factory returns a transform descriptor that can be passed to pull(). + +const { + ArrayPrototypeMap, + ArrayPrototypePush, + ArrayPrototypeShift, + MathMax, + NumberIsNaN, + ObjectEntries, + ObjectKeys, + PromiseWithResolvers, + StringPrototypeStartsWith, + SymbolAsyncIterator, + TypedArrayPrototypeFill, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeSlice, + Uint32Array, +} = primordials; + +const { Buffer } = require('buffer'); +const { + codes: { + ERR_BROTLI_INVALID_PARAM, + ERR_INVALID_ARG_TYPE, + ERR_OUT_OF_RANGE, + ERR_ZSTD_INVALID_PARAM, + }, + genericNodeError, +} = require('internal/errors'); +const { lazyDOMException } = require('internal/util'); +const { isArrayBufferView, isAnyArrayBuffer } = require('internal/util/types'); +const { kValidatedTransform } = require('internal/streams/iter/types'); +const { + checkRangesOrGetDefault, + validateFiniteNumber, + validateObject, +} = require('internal/validators'); +const binding = internalBinding('zlib'); +const constants = internalBinding('constants').zlib; + +const { + // Zlib modes + DEFLATE, INFLATE, GZIP, GUNZIP, + BROTLI_ENCODE, BROTLI_DECODE, + ZSTD_COMPRESS, ZSTD_DECOMPRESS, + // Zlib flush + Z_NO_FLUSH, Z_FINISH, + // Zlib defaults + Z_DEFAULT_WINDOWBITS, + Z_DEFAULT_STRATEGY, + // Brotli flush + BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_FINISH, + // Zlib ranges + Z_MIN_CHUNK, Z_MIN_WINDOWBITS, Z_MAX_WINDOWBITS, + Z_MIN_LEVEL, Z_MAX_LEVEL, + Z_MIN_MEMLEVEL, Z_MAX_MEMLEVEL, + Z_FIXED, + // Zstd flush + ZSTD_e_continue, ZSTD_e_end, +} = constants; + +// --------------------------------------------------------------------------- +// Option validation helpers (matching lib/zlib.js validation patterns) +// --------------------------------------------------------------------------- + +// Default output buffer size for compression transforms. Larger than +// Z_DEFAULT_CHUNK (16KB) to reduce the number of threadpool re-entries +// when the engine has more output than fits in one buffer. 64KB matches +// BATCH_HWM and the typical input chunk size from pull(). +const DEFAULT_OUTPUT_SIZE = 64 * 1024; + +// Batch high water mark - yield output in chunks of approximately this size. +const BATCH_HWM = DEFAULT_OUTPUT_SIZE; + +// Pre-allocated empty buffer for flush/finalize calls. +const kEmpty = Buffer.alloc(0); + +function validateChunkSize(options) { + let chunkSize = options.chunkSize; + if (!validateFiniteNumber(chunkSize, 'options.chunkSize')) { + chunkSize = DEFAULT_OUTPUT_SIZE; + } else if (chunkSize < Z_MIN_CHUNK) { + throw new ERR_OUT_OF_RANGE('options.chunkSize', + `>= ${Z_MIN_CHUNK}`, chunkSize); + } + return chunkSize; +} + +function validateDictionary(dictionary) { + if (dictionary === undefined) return undefined; + if (isArrayBufferView(dictionary)) return dictionary; + if (isAnyArrayBuffer(dictionary)) return Buffer.from(dictionary); + throw new ERR_INVALID_ARG_TYPE( + 'options.dictionary', + ['Buffer', 'TypedArray', 'DataView', 'ArrayBuffer'], + dictionary); +} + +function validateParams(params, maxParam, errClass) { + if (params === undefined) return; + if (typeof params !== 'object' || params === null) { + throw new ERR_INVALID_ARG_TYPE('options.params', 'Object', params); + } + const keys = ObjectKeys(params); + for (let i = 0; i < keys.length; i++) { + const origKey = keys[i]; + const key = +origKey; + if (NumberIsNaN(key) || key < 0 || key > maxParam) { + throw new errClass(origKey); + } + const value = params[origKey]; + if (typeof value !== 'number' && typeof value !== 'boolean') { + throw new ERR_INVALID_ARG_TYPE('options.params[key]', 'number', value); + } + } +} + +// --------------------------------------------------------------------------- +// Brotli / Zstd parameter arrays (computed once, reused per init call). +// Mirrors the pattern in lib/zlib.js. +// --------------------------------------------------------------------------- +const kMaxBrotliParam = MathMax( + ...ArrayPrototypeMap( + ObjectEntries(constants), + ({ 0: key, 1: value }) => + (StringPrototypeStartsWith(key, 'BROTLI_PARAM_') ? value : 0), + ), +); +const brotliInitParamsArray = new Uint32Array(kMaxBrotliParam + 1); + +const kMaxZstdCParam = MathMax( + ...ArrayPrototypeMap( + ObjectKeys(constants), + (key) => (StringPrototypeStartsWith(key, 'ZSTD_c_') ? constants[key] : 0), + ), +); +const zstdInitCParamsArray = new Uint32Array(kMaxZstdCParam + 1); + +const kMaxZstdDParam = MathMax( + ...ArrayPrototypeMap( + ObjectKeys(constants), + (key) => (StringPrototypeStartsWith(key, 'ZSTD_d_') ? constants[key] : 0), + ), +); +const zstdInitDParamsArray = new Uint32Array(kMaxZstdDParam + 1); + +// --------------------------------------------------------------------------- +// Handle creation - bare native handles, no Transform/EventEmitter overhead. +// +// Each factory accepts a processCallback (called from the threadpool +// completion path in C++) and an onError handler. +// --------------------------------------------------------------------------- + +/** + * Create a bare Zlib handle (gzip, gunzip, deflate, inflate). + * @returns {{ handle: object, writeState: Uint32Array, chunkSize: number }} + */ +function createZlibHandle(mode, options, processCallback, onError) { + // Validate all options before creating the native handle to avoid + // "close before init" assertion if validation throws. + const chunkSize = validateChunkSize(options); + const windowBits = checkRangesOrGetDefault( + options.windowBits, 'options.windowBits', + Z_MIN_WINDOWBITS, Z_MAX_WINDOWBITS, Z_DEFAULT_WINDOWBITS); + // Default compression level 4 (not Z_DEFAULT_COMPRESSION which maps to + // level 6). Level 4 is ~1.5x faster with only ~5-10% worse compression + // ratio - the sweet spot for streaming and HTTP content-encoding. + const level = checkRangesOrGetDefault( + options.level, 'options.level', + Z_MIN_LEVEL, Z_MAX_LEVEL, 4); + // memLevel 9 uses ~128KB more memory than 8 but provides faster hash + // lookups during compression. Negligible memory cost for the speed gain. + const memLevel = checkRangesOrGetDefault( + options.memLevel, 'options.memLevel', + Z_MIN_MEMLEVEL, Z_MAX_MEMLEVEL, 9); + const strategy = checkRangesOrGetDefault( + options.strategy, 'options.strategy', + Z_DEFAULT_STRATEGY, Z_FIXED, Z_DEFAULT_STRATEGY); + const dictionary = validateDictionary(options.dictionary); + + const handle = new binding.Zlib(mode); + const writeState = new Uint32Array(2); + + handle.onerror = onError; + handle.init( + windowBits, level, memLevel, strategy, + writeState, processCallback, dictionary, + ); + + return { __proto__: null, handle, writeState, chunkSize }; +} + +/** + * Create a bare Brotli handle. + * @returns {{ handle: object, writeState: Uint32Array, chunkSize: number }} + */ +function createBrotliHandle(mode, options, processCallback, onError) { + // Validate before creating native handle. + const chunkSize = validateChunkSize(options); + const dictionary = validateDictionary(options.dictionary); + validateParams(options.params, kMaxBrotliParam, ERR_BROTLI_INVALID_PARAM); + + const handle = mode === BROTLI_ENCODE ? + new binding.BrotliEncoder(mode) : new binding.BrotliDecoder(mode); + const writeState = new Uint32Array(2); + + TypedArrayPrototypeFill(brotliInitParamsArray, -1); + // Streaming-appropriate defaults: quality 6 (not 11) and lgwin 20 (1MB, + // not 4MB). Quality 11 is intended for offline/build-time compression + // and allocates ~400MB of internal state. Quality 6 is ~10x faster with + // only ~10-15% worse compression ratio - the standard for dynamic HTTP + // content-encoding (nginx, Caddy, Cloudflare all use 4-6). + if (mode === BROTLI_ENCODE) { + brotliInitParamsArray[constants.BROTLI_PARAM_QUALITY] = 6; + brotliInitParamsArray[constants.BROTLI_PARAM_LGWIN] = 20; + } + if (options.params) { + // User-supplied params override the defaults above. + const params = options.params; + const keys = ObjectKeys(params); + for (let i = 0; i < keys.length; i++) { + const key = +keys[i]; + brotliInitParamsArray[key] = params[keys[i]]; + } + } + + handle.onerror = onError; + handle.init( + brotliInitParamsArray, + writeState, + processCallback, + dictionary, + ); + + return { __proto__: null, handle, writeState, chunkSize }; +} + +/** + * Create a bare Zstd handle. + * @returns {{ handle: object, writeState: Uint32Array, chunkSize: number }} + */ +function createZstdHandle(mode, options, processCallback, onError) { + const isCompress = mode === ZSTD_COMPRESS; + + // Validate before creating native handle. + const chunkSize = validateChunkSize(options); + const dictionary = validateDictionary(options.dictionary); + const maxParam = isCompress ? kMaxZstdCParam : kMaxZstdDParam; + validateParams(options.params, maxParam, ERR_ZSTD_INVALID_PARAM); + + const pledgedSrcSize = options.pledgedSrcSize; + if (pledgedSrcSize !== undefined) { + if (typeof pledgedSrcSize !== 'number' || NumberIsNaN(pledgedSrcSize)) { + throw new ERR_INVALID_ARG_TYPE('options.pledgedSrcSize', 'number', + pledgedSrcSize); + } + if (pledgedSrcSize < 0) { + throw new ERR_OUT_OF_RANGE('options.pledgedSrcSize', '>= 0', + pledgedSrcSize); + } + } + + const handle = isCompress ? + new binding.ZstdCompress() : new binding.ZstdDecompress(); + const writeState = new Uint32Array(2); + + const initArray = isCompress ? zstdInitCParamsArray : zstdInitDParamsArray; + TypedArrayPrototypeFill(initArray, -1); + if (options.params) { + const params = options.params; + const keys = ObjectKeys(params); + for (let i = 0; i < keys.length; i++) { + const key = +keys[i]; + initArray[key] = params[keys[i]]; + } + } + + handle.onerror = onError; + handle.init( + initArray, + pledgedSrcSize, + writeState, + processCallback, + dictionary, + ); + + return { __proto__: null, handle, writeState, chunkSize }; +} + +// --------------------------------------------------------------------------- +// Core: makeZlibTransform +// +// Uses async handle.write() so compression runs on the libuv threadpool. +// The generator manually iterates the source with pre-reading: the next +// upstream read+transform is started before awaiting the current compression, +// so I/O and upstream work overlap with threadpool compression. +// --------------------------------------------------------------------------- +function makeZlibTransform(createHandleFn, processFlag, finishFlag) { + return { + __proto__: null, + [kValidatedTransform]: true, + transform: async function*(source, options) { + const { signal } = options; + + // Fail fast if already aborted - don't allocate a native handle. + signal?.throwIfAborted(); + + // ---- Per-invocation state shared with the write callback ---- + let outBuf; + let outOffset = 0; + let chunkSize; + let pending = []; + let pendingBytes = 0; + + // Current write operation state (read by the callback for looping). + let resolveWrite, rejectWrite; + let writeInput, writeFlush; + let writeInOff, writeAvailIn, writeAvailOutBefore; + + // processCallback: called by C++ AfterThreadPoolWork when compression + // on the threadpool completes. Collects output, loops if the engine + // has more output to produce (availOut === 0), then resolves the + // promise when all output for this input chunk is collected. + function onWriteComplete() { + const availOut = writeState[0]; + const availInAfter = writeState[1]; + const have = writeAvailOutBefore - availOut; + const bufferExhausted = availOut === 0 || outOffset + have >= chunkSize; + + if (have > 0) { + if (bufferExhausted && outOffset === 0) { + // Entire buffer filled from start - yield directly, no copy. + ArrayPrototypePush(pending, outBuf); + } else if (bufferExhausted) { + // Tail of buffer filled and buffer is being replaced - + // subarray is safe since outBuf reference is overwritten below. + ArrayPrototypePush(pending, + outBuf.subarray(outOffset, outOffset + have)); + } else { + // Partial fill, buffer will be reused - must copy. + ArrayPrototypePush(pending, + TypedArrayPrototypeSlice(outBuf, + outOffset, + outOffset + have)); + } + pendingBytes += have; + outOffset += have; + } + + // Reallocate output buffer if exhausted. + if (bufferExhausted) { + outBuf = Buffer.allocUnsafe(chunkSize); + outOffset = 0; + } + + if (availOut === 0) { + // Engine has more output - but if aborted, don't loop. + if (!resolveWrite) return; + + const consumed = writeAvailIn - availInAfter; + writeInOff += consumed; + writeAvailIn = availInAfter; + writeAvailOutBefore = chunkSize - outOffset; + + handle.write(writeFlush, + writeInput, writeInOff, writeAvailIn, + outBuf, outOffset, writeAvailOutBefore); + return; // Will call onWriteComplete again. + } + + // All input consumed and output collected. + handle.buffer = null; + const resolve = resolveWrite; + resolveWrite = undefined; + rejectWrite = undefined; + if (resolve) resolve(); + } + + // onError: called by C++ when the engine encounters an error. + // Fires instead of onWriteComplete - reject the promise. + function onError(message, errno, code) { + const error = genericNodeError(message, { __proto__: null, errno, code }); + error.errno = errno; + error.code = code; + const reject = rejectWrite; + resolveWrite = undefined; + rejectWrite = undefined; + if (reject) reject(error); + } + + // ---- Create the handle with our callbacks ---- + const result = createHandleFn(onWriteComplete, onError); + const handle = result.handle; + const writeState = result.writeState; + chunkSize = result.chunkSize; + outBuf = Buffer.allocUnsafe(chunkSize); + + // Abort handler: reject any in-flight threadpool operation so the + // generator doesn't block waiting for compression to finish. + const onAbort = () => { + const reject = rejectWrite; + resolveWrite = undefined; + rejectWrite = undefined; + if (reject) { + reject(signal.reason ?? + lazyDOMException('The operation was aborted', 'AbortError')); + } + }; + signal.addEventListener('abort', onAbort, { __proto__: null, once: true }); + + // Dispatch input to the threadpool and return a promise. + function processInputAsync(input, flushFlag) { + const { promise, resolve, reject } = PromiseWithResolvers(); + resolveWrite = resolve; + rejectWrite = reject; + writeInput = input; + writeFlush = flushFlag; + writeInOff = 0; + writeAvailIn = TypedArrayPrototypeGetByteLength(input); + writeAvailOutBefore = chunkSize - outOffset; + + // Keep input alive while the threadpool references it. + handle.buffer = input; + + handle.write(flushFlag, + input, 0, writeAvailIn, + outBuf, outOffset, writeAvailOutBefore); + return promise; + } + + function drainBatch() { + if (pendingBytes <= BATCH_HWM) { + // Swap instead of splice - avoids copying the array. + const batch = pending; + pending = []; + pendingBytes = 0; + return batch; + } + const batch = []; + let batchBytes = 0; + while (pending.length > 0 && batchBytes < BATCH_HWM) { + const buf = ArrayPrototypeShift(pending); + ArrayPrototypePush(batch, buf); + const len = TypedArrayPrototypeGetByteLength(buf); + batchBytes += len; + pendingBytes -= len; + } + return batch; + } + + let finalized = false; + + const iter = source[SymbolAsyncIterator](); + try { + // Manually iterate the source so we can pre-read: calling + // iter.next() starts the upstream read + transform on libuv + // before we await the current compression on the threadpool. + let nextResult = iter.next(); + + while (true) { + const { value: chunks, done } = await nextResult; + if (done) break; + + signal?.throwIfAborted(); + + if (chunks === null) { + // Flush signal - finalize the engine. + if (!finalized) { + finalized = true; + await processInputAsync(kEmpty, finishFlag); + while (pending.length > 0) { + yield drainBatch(); + } + } + nextResult = iter.next(); + continue; + } + + // Pre-read: start upstream I/O + transform for the NEXT batch + // while we compress the current batch on the threadpool. + nextResult = iter.next(); + + for (let i = 0; i < chunks.length; i++) { + await processInputAsync(chunks[i], processFlag); + } + + if (pendingBytes >= BATCH_HWM) { + while (pending.length > 0 && pendingBytes >= BATCH_HWM) { + yield drainBatch(); + } + } + if (pending.length > 0) { + yield drainBatch(); + } + } + + // Source ended - finalize if not already done by a null signal. + if (!finalized && !signal.aborted) { + finalized = true; + await processInputAsync(kEmpty, finishFlag); + while (pending.length > 0) { + yield drainBatch(); + } + } + } finally { + signal.removeEventListener('abort', onAbort); + handle.close(); + // Close the upstream iterator so its finally blocks run promptly + // rather than waiting for GC. + try { await iter.return?.(); } catch { /* Intentional no-op. */ } + } + }, + }; +} + +// --------------------------------------------------------------------------- +// Compression factories +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Core: makeZlibTransformSync +// +// Synchronous counterpart to makeZlibTransform. Uses handle.writeSync() +// which runs compression directly on the main thread (no threadpool). +// Returns a stateful sync transform (generator function). +// --------------------------------------------------------------------------- +function makeZlibTransformSync(createHandleFn, processFlag, finishFlag) { + return { + __proto__: null, + transform: function*(source) { + // The processCallback is never called in sync mode, but handle.init() + // requires it. Pass a no-op. + let error = null; + function onError(message, errno, code) { + error = genericNodeError(message, { __proto__: null, errno, code }); + error.errno = errno; + error.code = code; + } + + const result = createHandleFn(() => {}, onError); + const handle = result.handle; + const writeState = result.writeState; + const chunkSize = result.chunkSize; + let outBuf = Buffer.allocUnsafe(chunkSize); + let outOffset = 0; + let pending = []; + let pendingBytes = 0; + + function processSyncInput(input, flushFlag) { + let inOff = 0; + let availIn = TypedArrayPrototypeGetByteLength(input); + let availOutBefore = chunkSize - outOffset; + + handle.writeSync(flushFlag, + input, inOff, availIn, + outBuf, outOffset, availOutBefore); + if (error) throw error; + + while (true) { + const availOut = writeState[0]; + const availInAfter = writeState[1]; + const have = availOutBefore - availOut; + const bufferExhausted = availOut === 0 || + outOffset + have >= chunkSize; + + if (have > 0) { + if (bufferExhausted && outOffset === 0) { + // Entire buffer filled - yield directly, no copy. + ArrayPrototypePush(pending, outBuf); + } else if (bufferExhausted) { + // Tail filled, buffer being replaced - subarray is safe. + ArrayPrototypePush(pending, + outBuf.subarray(outOffset, outOffset + have)); + } else { + // Partial fill, buffer reused - must copy. + ArrayPrototypePush(pending, + TypedArrayPrototypeSlice(outBuf, + outOffset, + outOffset + have)); + } + pendingBytes += have; + outOffset += have; + } + + if (bufferExhausted) { + outBuf = Buffer.allocUnsafe(chunkSize); + outOffset = 0; + } + + if (availOut === 0) { + // Engine has more output - loop. + const consumed = availIn - availInAfter; + inOff += consumed; + availIn = availInAfter; + availOutBefore = chunkSize - outOffset; + + handle.writeSync(flushFlag, + input, inOff, availIn, + outBuf, outOffset, availOutBefore); + if (error) throw error; + continue; + } + + // All input consumed. + break; + } + } + + function drainBatch() { + if (pendingBytes <= BATCH_HWM) { + const batch = pending; + pending = []; + pendingBytes = 0; + return batch; + } + const batch = []; + let batchBytes = 0; + while (pending.length > 0 && batchBytes < BATCH_HWM) { + const buf = ArrayPrototypeShift(pending); + const len = TypedArrayPrototypeGetByteLength(buf); + ArrayPrototypePush(batch, buf); + batchBytes += len; + pendingBytes -= len; + } + return batch; + } + + try { + for (const batch of source) { + if (batch === null) { + // Flush signal - finalize the engine. + processSyncInput(Buffer.alloc(0), finishFlag); + while (pending.length > 0) { + yield drainBatch(); + } + continue; + } + + for (let i = 0; i < batch.length; i++) { + processSyncInput(batch[i], processFlag); + } + + if (pendingBytes >= BATCH_HWM) { + while (pending.length > 0 && pendingBytes >= BATCH_HWM) { + yield drainBatch(); + } + } + if (pending.length > 0) { + yield drainBatch(); + } + } + } finally { + handle.close(); + } + }, + }; +} + +// --------------------------------------------------------------------------- +// Async compression factories +// --------------------------------------------------------------------------- + +const kNullPrototype = { __proto__: null }; + +function compressGzip(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransform( + (cb, onErr) => createZlibHandle(GZIP, options, cb, onErr), + Z_NO_FLUSH, Z_FINISH, + ); +} + +function compressDeflate(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransform( + (cb, onErr) => createZlibHandle(DEFLATE, options, cb, onErr), + Z_NO_FLUSH, Z_FINISH, + ); +} + +function compressBrotli(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransform( + (cb, onErr) => createBrotliHandle(BROTLI_ENCODE, options, cb, onErr), + BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_FINISH, + ); +} + +function compressZstd(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransform( + (cb, onErr) => createZstdHandle(ZSTD_COMPRESS, options, cb, onErr), + ZSTD_e_continue, ZSTD_e_end, + ); +} + +// --------------------------------------------------------------------------- +// Decompression factories +// --------------------------------------------------------------------------- + +function decompressGzip(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransform( + (cb, onErr) => createZlibHandle(GUNZIP, options, cb, onErr), + Z_NO_FLUSH, Z_FINISH, + ); +} + +function decompressDeflate(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransform( + (cb, onErr) => createZlibHandle(INFLATE, options, cb, onErr), + Z_NO_FLUSH, Z_FINISH, + ); +} + +function decompressBrotli(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransform( + (cb, onErr) => createBrotliHandle(BROTLI_DECODE, options, cb, onErr), + BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_FINISH, + ); +} + +function decompressZstd(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransform( + (cb, onErr) => createZstdHandle(ZSTD_DECOMPRESS, options, cb, onErr), + ZSTD_e_continue, ZSTD_e_end, + ); +} + +// --------------------------------------------------------------------------- +// Sync compression factories +// --------------------------------------------------------------------------- + +function compressGzipSync(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransformSync( + (cb, onErr) => createZlibHandle(GZIP, options, cb, onErr), + Z_NO_FLUSH, Z_FINISH, + ); +} + +function compressDeflateSync(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransformSync( + (cb, onErr) => createZlibHandle(DEFLATE, options, cb, onErr), + Z_NO_FLUSH, Z_FINISH, + ); +} + +function compressBrotliSync(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransformSync( + (cb, onErr) => createBrotliHandle(BROTLI_ENCODE, options, cb, onErr), + BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_FINISH, + ); +} + +function compressZstdSync(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransformSync( + (cb, onErr) => createZstdHandle(ZSTD_COMPRESS, options, cb, onErr), + ZSTD_e_continue, ZSTD_e_end, + ); +} + +// --------------------------------------------------------------------------- +// Sync decompression factories +// --------------------------------------------------------------------------- + +function decompressGzipSync(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransformSync( + (cb, onErr) => createZlibHandle(GUNZIP, options, cb, onErr), + Z_NO_FLUSH, Z_FINISH, + ); +} + +function decompressDeflateSync(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransformSync( + (cb, onErr) => createZlibHandle(INFLATE, options, cb, onErr), + Z_NO_FLUSH, Z_FINISH, + ); +} + +function decompressBrotliSync(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransformSync( + (cb, onErr) => createBrotliHandle(BROTLI_DECODE, options, cb, onErr), + BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_FINISH, + ); +} + +function decompressZstdSync(options = kNullPrototype) { + validateObject(options, 'options'); + return makeZlibTransformSync( + (cb, onErr) => createZstdHandle(ZSTD_DECOMPRESS, options, cb, onErr), + ZSTD_e_continue, ZSTD_e_end, + ); +} + +module.exports = { + compressBrotli, + compressBrotliSync, + compressDeflate, + compressDeflateSync, + compressGzip, + compressGzipSync, + compressZstd, + compressZstdSync, + decompressBrotli, + decompressBrotliSync, + decompressDeflate, + decompressDeflateSync, + decompressGzip, + decompressGzipSync, + decompressZstd, + decompressZstdSync, +}; diff --git a/lib/internal/streams/iter/types.js b/lib/internal/streams/iter/types.js new file mode 100644 index 00000000000000..99ddc8fd582770 --- /dev/null +++ b/lib/internal/streams/iter/types.js @@ -0,0 +1,76 @@ +'use strict'; + +const { + Symbol, + SymbolFor, +} = primordials; + +/** + * Symbol for sync value-to-streamable conversion protocol. + * Objects implementing this can be written to streams or yielded + * from generators. Works in both sync and async contexts. + * + * Third-party: [Symbol.for('Stream.toStreamable')]() { ... } + */ +const toStreamable = SymbolFor('Stream.toStreamable'); + +/** + * Symbol for async value-to-streamable conversion protocol. + * Objects implementing this can be written to async streams. + * Works in async contexts only. + * + * Third-party: [Symbol.for('Stream.toAsyncStreamable')]() { ... } + */ +const toAsyncStreamable = SymbolFor('Stream.toAsyncStreamable'); + +/** + * Symbol for Broadcastable protocol - object can provide a Broadcast. + */ +const broadcastProtocol = SymbolFor('Stream.broadcastProtocol'); + +/** + * Symbol for Shareable protocol - object can provide a Share. + */ +const shareProtocol = SymbolFor('Stream.shareProtocol'); + +/** + * Symbol for SyncShareable protocol - object can provide a SyncShare. + */ +const shareSyncProtocol = SymbolFor('Stream.shareSyncProtocol'); + +/** + * Symbol for Drainable protocol - object can signal when backpressure + * clears. Used to bridge event-driven sources that need drain notification. + */ +const drainableProtocol = SymbolFor('Stream.drainableProtocol'); + +/** + * Internal sentinel for validated stateful transforms. A transform object + * with [kValidatedTransform] = true signals that: + * 1. It handles source exhaustion (done) internally - no withFlushAsync + * wrapper needed. + * 2. It always yields valid Uint8Array[] batches - no isUint8ArrayBatch + * validation needed on each yield. + * This is NOT a public protocol symbol - it uses Symbol() not Symbol.for(). + */ +const kValidatedTransform = Symbol('kValidatedTransform'); + +/** + * Internal sentinel for validated sources. An async iterable with + * [kValidatedSource] = true signals that it already yields valid + * Uint8Array[] batches - no normalizeAsyncSource wrapper needed. + * from() will return such sources directly, skipping all normalization. + * This is NOT a public protocol symbol - it uses Symbol() not Symbol.for(). + */ +const kValidatedSource = Symbol('kValidatedSource'); + +module.exports = { + broadcastProtocol, + drainableProtocol, + kValidatedSource, + kValidatedTransform, + shareProtocol, + shareSyncProtocol, + toAsyncStreamable, + toStreamable, +}; diff --git a/lib/internal/streams/iter/utils.js b/lib/internal/streams/iter/utils.js new file mode 100644 index 00000000000000..ca3d2531d6f9e0 --- /dev/null +++ b/lib/internal/streams/iter/utils.js @@ -0,0 +1,388 @@ +'use strict'; + +const { + Array, + ArrayBufferPrototypeGetByteLength, + ArrayPrototypeSlice, + MathMax, + MathMin, + NumberMAX_SAFE_INTEGER, + PromiseResolve, + PromiseWithResolvers, + SafePromisePrototypeFinally, + SafePromiseRace, + String, + SymbolAsyncIterator, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteOffset, + TypedArrayPrototypeSet, + Uint8Array, +} = primordials; + +const { + markPromiseAsHandled, +} = internalBinding('util'); + +const { TextEncoder } = require('internal/encoding'); +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_OPERATION_FAILED, + }, +} = require('internal/errors'); +const { isError } = require('internal/util'); + +const { isSharedArrayBuffer, isUint8Array } = require('internal/util/types'); + +const { validateOneOf } = require('internal/validators'); + +// Cached resolved promise to avoid allocating a new one on every sync fast-path. +const kResolvedPromise = PromiseResolve(); + +// Shared TextEncoder instance for string conversion. +const encoder = new TextEncoder(); + +// Default high water marks for push and multi-consumer streams. These values +// are somewhat arbitrary but have been tested across various workloads and +// appear to yield the best overall throughput/latency balance. + +/** Default high water mark for push streams (single-consumer). */ +const kPushDefaultHWM = 4; + +/** Default high water mark for broadcast and share streams (multi-consumer). */ +const kMultiConsumerDefaultHWM = 16; + +/** + * Clamp a high water mark to [1, MAX_SAFE_INTEGER]. + * @param {number} value + * @returns {number} + */ +function clampHWM(value) { + return MathMax(1, MathMin(NumberMAX_SAFE_INTEGER, value)); +} + +/** + * Register a handler for an AbortSignal, handling the already-aborted case. + * If the signal is already aborted, calls handler immediately. + * Otherwise, adds a one-time 'abort' listener. + * @param {AbortSignal} signal + * @param {Function} handler + */ +function onSignalAbort(signal, handler) { + if (signal.aborted) { + handler(); + } else { + signal.addEventListener('abort', handler, { __proto__: null, once: true }); + } +} + +function getOnAbort(reject, signal) { + return () => reject(signal.reason); +} + +/** + * Read one item from an async iterator, rejecting early if the signal aborts. + * @param {AsyncIterator} iterator - The iterator to read from. + * @param {AbortSignal|undefined} signal - Optional abort signal. + * @returns {Promise>|IteratorResult} + */ +function abortableNext(iterator, signal) { + if (signal === undefined) { + return iterator.next(); + } + + signal.throwIfAborted(); + + const next = iterator.next(); + const { promise, reject } = PromiseWithResolvers(); + const onAbort = getOnAbort(reject, signal); + signal.addEventListener('abort', onAbort, { __proto__: null, once: true }); + if (signal.aborted) { + onAbort(); + } + + return SafePromisePrototypeFinally(SafePromiseRace([next, promise]), () => { + signal.removeEventListener('abort', onAbort); + }); +} + +/** + * Wrap an async source so each pending read is abort-aware. + * @param {AsyncIterable} source - The source to read from. + * @param {AbortSignal|undefined} signal - Optional abort signal. + * @returns {AsyncIterable} + */ +function yieldAbortable(source, signal) { + if (signal === undefined) { + return source; + } + + return { + __proto__: null, + async *[SymbolAsyncIterator]() { + const iterator = source[SymbolAsyncIterator](); + let completed = false; + let aborted = false; + + try { + while (true) { + const { done, value } = await abortableNext(iterator, signal); + if (done) { + completed = true; + return; + } + signal.throwIfAborted(); + yield value; + } + } catch (error) { + aborted = signal.aborted; + throw error; + } finally { + if (!completed && typeof iterator.return === 'function') { + const result = iterator.return(); + if (aborted) { + markPromiseAsHandled(result); + } else { + await result; + } + } + } + }, + }; +} + +/** + * Compute the minimum cursor across a set of consumers and count how many + * consumers are at that cursor. + * @param {Set} consumers - Set of objects with a `cursor` property + * @param {number} fallback - Cursor to return when set is empty + * @returns {{ minCursor: number, minCursorConsumers: number }} + */ +function getMinCursor(consumers, fallback) { + let minCursor = fallback; + let minCursorConsumers = 0; + for (const consumer of consumers) { + if (consumer.cursor < minCursor) { + minCursor = consumer.cursor; + minCursorConsumers = 1; + } else if (consumer.cursor === minCursor) { + minCursorConsumers++; + } + } + return { __proto__: null, minCursor, minCursorConsumers }; +} + +/** + * Convert a chunk (string or Uint8Array) to Uint8Array. + * Strings are UTF-8 encoded. + * @param {Uint8Array|string} chunk + * @returns {Uint8Array} + */ +function toUint8Array(chunk) { + if (typeof chunk === 'string') { + return encoder.encode(chunk); + } + if (!isUint8Array(chunk)) { + throw new ERR_INVALID_ARG_TYPE('chunk', ['string', 'Uint8Array'], chunk); + } + return chunk; +} + +/** + * Check if all chunks in an array are already Uint8Array (no strings). + * Short-circuits on the first string found. + * @param {Array} chunks + * @returns {boolean} + */ +function allUint8Array(chunks) { + // Ok, well, kind of. This is more a check for "no strings"... + for (let i = 0; i < chunks.length; i++) { + if (typeof chunks[i] === 'string') return false; + } + return true; +} + +/** + * Concatenate multiple Uint8Arrays into a single Uint8Array. + * @param {Uint8Array[]} chunks + * @returns {Uint8Array} + */ +function concatBytes(chunks) { + // Empty stream: return zero-length Uint8Array + if (chunks.length === 0) { + return new Uint8Array(0); + } + // Single chunk: return directly if it covers the entire backing buffer, + // otherwise return a copy + if (chunks.length === 1) { + const chunk = chunks[0]; + // If non-zero offset, skip the remaining buffer checks. + if (TypedArrayPrototypeGetByteOffset(chunk) === 0) { + const buf = TypedArrayPrototypeGetBuffer(chunk); + // SharedArrayBuffer is not available in primordials, so use + // direct property access for its byteLength. + const bufByteLength = isSharedArrayBuffer(buf) ? + buf.byteLength : + ArrayBufferPrototypeGetByteLength(buf); + if (TypedArrayPrototypeGetByteLength(chunk) === bufByteLength) { + return chunk; + } + } + return new Uint8Array(chunk); + } + // Multiple chunks: concatenate + let totalByteLength = 0; + for (let i = 0; i < chunks.length; i++) { + totalByteLength += TypedArrayPrototypeGetByteLength(chunks[i]); + } + const concatenated = new Uint8Array(totalByteLength); + let offset = 0; + for (let i = 0; i < chunks.length; i++) { + TypedArrayPrototypeSet(concatenated, chunks[i], offset); + offset += TypedArrayPrototypeGetByteLength(chunks[i]); + } + return concatenated; +} + +/** + * Convert an array of chunks (strings or Uint8Arrays) to a Uint8Array[]. + * Always returns a fresh copy of the array. + * @param {Array} chunks + * @returns {Uint8Array[]} + */ +function convertChunks(chunks) { + if (allUint8Array(chunks)) { + return ArrayPrototypeSlice(chunks); + } + const len = chunks.length; + const result = new Array(len); + for (let i = 0; i < len; i++) { + result[i] = toUint8Array(chunks[i]); + } + return result; +} + +/** + * Wrap a caught value as an Error, converting non-Error values. + * @param {unknown} error + * @returns {Error} + */ +function wrapError(error) { + return isError(error) ? error : new ERR_OPERATION_FAILED(String(error)); +} + +/** + * Check if a value implements a Symbol-keyed protocol (has a function + * at the given symbol key). + * @param {unknown} value + * @param {symbol} symbol + * @returns {boolean} + */ +function hasProtocol(value, symbol) { + return ( + value !== null && + typeof value === 'object' && + symbol in value && + typeof value[symbol] === 'function' + ); +} + +/** + * Check if a value is PullOptions (object without transform or write property). + * @param {unknown} value + * @returns {boolean} + */ +function isPullOptions(value) { + return ( + value !== null && + typeof value === 'object' && + !('transform' in value) && + !('write' in value) + ); +} + +/** + * Check if a value is a stateful transform object (has a transform method). + * @param {unknown} value + * @returns {boolean} + */ +function isTransformObject(value) { + return typeof value?.transform === 'function'; +} + +/** + * Check if a value is a valid transform (function or transform object). + * @param {unknown} value + * @returns {boolean} + */ +function isTransform(value) { + return typeof value === 'function' || isTransformObject(value); +} + +/** + * Parse variadic arguments for pull/pullSync. + * Returns { transforms, options } + * @param {Array} args + * @returns {{ transforms: Array, options: object|undefined }} + */ +function parsePullArgs(args) { + if (args.length === 0) { + return { __proto__: null, transforms: [], options: undefined }; + } + + let transforms; + let options; + const last = args[args.length - 1]; + if (isPullOptions(last)) { + transforms = ArrayPrototypeSlice(args, 0, -1); + options = last; + } else { + transforms = args; + options = undefined; + } + + for (let i = 0; i < transforms.length; i++) { + if (!isTransform(transforms[i])) { + throw new ERR_INVALID_ARG_TYPE( + `transforms[${i}]`, ['Function', 'Object with transform()'], + transforms[i]); + } + } + + return { __proto__: null, transforms, options }; +} + +/** + * Validate backpressure option value. + * @param {string} value + */ +function validateBackpressure(value) { + validateOneOf(value, 'options.backpressure', [ + 'strict', + 'block', + 'drop-oldest', + 'drop-newest', + ]); +} + +module.exports = { + kMultiConsumerDefaultHWM, + kPushDefaultHWM, + kResolvedPromise, + allUint8Array, + clampHWM, + concatBytes, + convertChunks, + getMinCursor, + hasProtocol, + isPullOptions, + isTransform, + isTransformObject, + onSignalAbort, + parsePullArgs, + toUint8Array, + validateBackpressure, + wrapError, + yieldAbortable, +}; diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index 0a3debed793408..36e731c0d6aaf1 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -34,6 +34,7 @@ const { Symbol, SymbolAsyncDispose, SymbolAsyncIterator, + SymbolFor, SymbolSpecies, TypedArrayPrototypeSet, } = primordials; @@ -51,6 +52,8 @@ const { } = require('internal/streams/add-abort-signal'); const { eos } = require('internal/streams/end-of-stream'); +const { getOptionValue } = require('internal/options'); + let debug = require('internal/util/debuglog').debuglog('stream', (fn) => { debug = fn; }); @@ -81,6 +84,7 @@ const { ERR_INVALID_ARG_TYPE, ERR_METHOD_NOT_IMPLEMENTED, ERR_OUT_OF_RANGE, + ERR_STREAM_ITER_MISSING_FLAG, ERR_STREAM_PUSH_AFTER_EOF, ERR_STREAM_UNSHIFT_AFTER_END_EVENT, ERR_UNKNOWN_ENCODING, @@ -599,6 +603,8 @@ Readable.prototype.setEncoding = function(enc) { for (const data of state.buffer.slice(state.bufferIndex)) { content += decoder.write(data); } + if ((state[kState] & kEnded) !== 0) + content += decoder.end(); state.buffer.length = 0; state.bufferIndex = 0; @@ -1798,3 +1804,42 @@ Readable.wrap = function(src, options) { }, }).wrap(src); }; + +// Interop with the stream/iter API via the toAsyncStreamable protocol. +// +// The batched iterator logic lives in classic.js (shared with the +// fromReadable() utility for duck-typed streams). This prototype method +// calls createBatchedAsyncIterator directly -- it must NOT call +// fromReadable() since fromReadable() checks for toAsyncStreamable, +// which would create infinite recursion. +// +// The flag cannot be checked at module load time (readable.js loads during +// bootstrap before options are available). Instead, toAsyncStreamable is +// always defined but lazily initializes on first call -- throwing if the +// flag is not set. +{ + const toAsyncStreamable = SymbolFor('Stream.toAsyncStreamable'); + let createBatchedAsyncIterator; + let normalizeBatch; + let kValidatedSource; + + Readable.prototype[toAsyncStreamable] = function() { + if (createBatchedAsyncIterator === undefined) { + if (!getOptionValue('--experimental-stream-iter')) { + throw new ERR_STREAM_ITER_MISSING_FLAG(); + } + ({ + createBatchedAsyncIterator, + normalizeBatch, + } = require('internal/streams/iter/classic')); + ({ kValidatedSource } = require('internal/streams/iter/types')); + } + const state = this._readableState; + const normalize = (state.objectMode || state.encoding) ? + normalizeBatch : null; + const iter = createBatchedAsyncIterator(this, normalize); + iter[kValidatedSource] = true; + iter.stream = this; + return iter; + }; +} diff --git a/lib/internal/streams/writable.js b/lib/internal/streams/writable.js index e934c38bbf9acc..94913461cb6044 100644 --- a/lib/internal/streams/writable.js +++ b/lib/internal/streams/writable.js @@ -47,7 +47,6 @@ const Stream = require('internal/streams/legacy').Stream; const { Buffer } = require('buffer'); const destroyImpl = require('internal/streams/destroy'); const { eos } = require('internal/streams/end-of-stream'); - const { addAbortSignal, } = require('internal/streams/add-abort-signal'); @@ -467,6 +466,9 @@ function _write(stream, chunk, encoding, cb) { } if (typeof chunk === 'string') { + if (encoding === 'buffer') { + throw new ERR_UNKNOWN_ENCODING(encoding); + } if ((state[kState] & kDecodeStrings) !== 0) { chunk = Buffer.from(chunk, encoding); encoding = 'buffer'; diff --git a/lib/internal/webstreams/adapters.js b/lib/internal/webstreams/adapters.js index c0f797b185368a..9cd4f923f42778 100644 --- a/lib/internal/webstreams/adapters.js +++ b/lib/internal/webstreams/adapters.js @@ -89,7 +89,10 @@ const { streamBaseState, } = internalBinding('stream_wrap'); -const { eos } = require('internal/streams/end-of-stream'); +const { + eos, + kEosNodeSynchronousCallback, +} = require('internal/streams/end-of-stream'); const { zlib } = internalBinding('constants'); const { UV_EOF } = internalBinding('uv'); @@ -135,6 +138,8 @@ function handleKnownInternalErrors(cause) { } } +const noop = () => {}; + /** * @typedef {import('../../stream').Writable} Writable * @typedef {import('../../stream').Readable} Readable @@ -450,6 +455,8 @@ function newStreamWritableFromWritableStream(writableStream, options = kEmptyObj return writable; } +const kErrorSentinelAttached = Symbol('kErrorSentinelAttached'); + /** * @typedef {import('./queuingstrategies').QueuingStrategy} QueuingStrategy * @param {Readable} streamReadable @@ -476,89 +483,81 @@ function newReadableStreamFromStreamReadable(streamReadable, options = kEmptyObj } const isBYOB = options.type === 'bytes'; - - if (isDestroyed(streamReadable) || !isReadable(streamReadable)) { - const readable = new ReadableStream(); - readable.cancel(); - return readable; - } - - const objectMode = streamReadable.readableObjectMode; - const highWaterMark = streamReadable.readableHighWaterMark; - - const evaluateStrategyOrFallback = (strategy) => { - // If the stream is BYOB, we only use highWaterMark - if (isBYOB) - return { highWaterMark }; - // If there is a strategy available, use it - if (strategy) - return strategy; - - if (objectMode) { - // When running in objectMode explicitly but no strategy, we just fall - // back to CountQueuingStrategy - return new CountQueuingStrategy({ highWaterMark }); - } - - return new ByteLengthQueuingStrategy({ highWaterMark }); - }; - - const strategy = evaluateStrategyOrFallback(options?.strategy); - let controller; let wasCanceled = false; + let strategy; - function onData(chunk) { - // Copy the Buffer to detach it from the pool. - if (Buffer.isBuffer(chunk) && !objectMode) - chunk = new Uint8Array(chunk); - controller.enqueue(chunk); - if (controller.desiredSize <= 0) - streamReadable.pause(); - } + /** @type {UnderlyingSource} */ + const underlyingSource = { + __proto__: null, + type: isBYOB ? 'bytes' : undefined, + start(c) { controller = c; }, + cancel(reason) { + wasCanceled = true; + destroy(streamReadable, reason); + }, + }; - streamReadable.pause(); + const readable = isReadable(streamReadable); + const objectMode = streamReadable.readableObjectMode; + if (readable) { + underlyingSource.pull = function pull() { + streamReadable.resume(); + }; + + const highWaterMark = streamReadable.readableHighWaterMark; + strategy = isBYOB ? { highWaterMark } : + options.strategy ?? new (objectMode ? CountQueuingStrategy : ByteLengthQueuingStrategy)({ highWaterMark }); + } + const readableStream = new ReadableStream(underlyingSource, strategy); - const cleanup = eos(streamReadable, (error) => { + // When adapting a Duplex as a ReadableStream, readable completion should not + // wait for a half-open writable side to finish as well. + let cleanup = noop; + cleanup = eos(streamReadable, { + __proto__: null, + writable: false, + [kEosNodeSynchronousCallback]: true, + }, (error) => { error = handleKnownInternalErrors(error); + // If eos calls the callback synchronously, cleanup is still a no-op here. cleanup(); - // This is a protection against non-standard, legacy streams - // that happen to emit an error event again after finished is called. - streamReadable.on('error', () => {}); - if (error) - return controller.error(error); - // Was already canceled + + if (!(kErrorSentinelAttached in streamReadable)) { + // This is a protection against non-standard, legacy streams + // that happen to emit an error event again after finished is called. + streamReadable.on('error', noop); + streamReadable[kErrorSentinelAttached] = true; + } if (wasCanceled) { return; } + wasCanceled = true; + if (error) + return controller.error(error); controller.close(); + if (isBYOB) + controller.byobRequest?.respond(0); }); - streamReadable.on('data', onData); - - return new ReadableStream({ - type: isBYOB ? 'bytes' : undefined, - start(c) { - controller = c; - if (isBYOB) { - streamReadable.once('end', () => { - // close the controller - controller.close(); - // And unlock the last BYOB read request - controller.byobRequest?.respond(0); - wasCanceled = true; - }); - } - }, - - pull() { streamReadable.resume(); }, + if (wasCanceled) { + // `eos` called the callback synchronously + cleanup(); + } else if (readable) { + streamReadable.pause(); + + streamReadable.on('data', function onData(chunk) { + // Copy the Buffer to detach it from the pool. + if (Buffer.isBuffer(chunk) && !objectMode) + chunk = new Uint8Array(chunk); + controller.enqueue(chunk); + if (controller.desiredSize <= 0) + streamReadable.pause(); + }); + } - cancel(reason) { - wasCanceled = true; - destroy(streamReadable, reason); - }, - }, strategy); + return readableStream; } /** diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index b8c1d4f5312cf0..03549ff26dd613 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -844,15 +844,15 @@ class DefaultReadRequest { } [kChunk](value) { - this[kState].resolve?.({ value, done: false }); + this[kState].resolve({ value, done: false }); } [kClose]() { - this[kState].resolve?.({ value: undefined, done: true }); + this[kState].resolve({ value: undefined, done: true }); } [kError](error) { - this[kState].reject?.(error); + this[kState].reject(error); } get promise() { return this[kState].promise; } @@ -864,15 +864,15 @@ class ReadIntoRequest { } [kChunk](value) { - this[kState].resolve?.({ value, done: false }); + this[kState].resolve({ value, done: false }); } [kClose](value) { - this[kState].resolve?.({ value, done: true }); + this[kState].resolve({ value, done: true }); } [kError](error) { - this[kState].reject?.(error); + this[kState].reject(error); } get promise() { return this[kState].promise; } @@ -2165,7 +2165,7 @@ function readableStreamCancel(stream, reason) { function readableStreamClose(stream) { assert(stream[kState].state === 'readable'); stream[kState].state = 'closed'; - stream[kState].closedPromise?.resolve?.(); + stream[kState].closedPromise?.resolve(); const { reader, } = stream[kState]; @@ -2173,7 +2173,7 @@ function readableStreamClose(stream) { if (reader === undefined) return; - reader[kState].close?.resolve?.(); + reader[kState].close?.resolve(); if (readableStreamHasDefaultReader(stream)) { for (let n = 0; n < reader[kState].readRequests.length; n++) @@ -2189,7 +2189,7 @@ function readableStreamError(stream, error) { const closedPromiseCache = stream[kState].closedPromise; if (closedPromiseCache !== undefined) { setPromiseHandled(closedPromiseCache.promise); - closedPromiseCache.reject?.(error); + closedPromiseCache.reject(error); } const { @@ -2202,7 +2202,7 @@ function readableStreamError(stream, error) { const closeCache = reader[kState].close; if (closeCache !== undefined) { setPromiseHandled(closeCache.promise); - closeCache.reject?.(error); + closeCache.reject(error); } if (readableStreamHasDefaultReader(stream)) { @@ -2396,7 +2396,7 @@ function readableStreamReaderGenericRelease(reader) { const closeCache = reader[kState].close; if (stream[kState].state === 'readable') { if (closeCache !== undefined) { - closeCache.reject?.(lazyReadableReleasedError()); + closeCache.reject(lazyReadableReleasedError()); setPromiseHandled(closeCache.promise); } } else { diff --git a/lib/internal/webstreams/transfer.js b/lib/internal/webstreams/transfer.js index 9ce3f249ffd2bd..e007ada2e0f5e4 100644 --- a/lib/internal/webstreams/transfer.js +++ b/lib/internal/webstreams/transfer.js @@ -192,7 +192,7 @@ class CrossRealmTransformWritableSink { switch (type) { case 'pull': if (this[kState].backpressurePromise !== undefined) - this[kState].backpressurePromise.resolve?.(); + this[kState].backpressurePromise.resolve(); this[kState].backpressurePromise = undefined; break; case 'error': @@ -200,7 +200,7 @@ class CrossRealmTransformWritableSink { this[kState].controller, value); if (this[kState].backpressurePromise !== undefined) - this[kState].backpressurePromise.resolve?.(); + this[kState].backpressurePromise.resolve(); this[kState].backpressurePromise = undefined; break; } diff --git a/lib/internal/webstreams/writablestream.js b/lib/internal/webstreams/writablestream.js index 37ef513a4e4166..9a6af1aa4a1061 100644 --- a/lib/internal/webstreams/writablestream.js +++ b/lib/internal/webstreams/writablestream.js @@ -749,7 +749,7 @@ function writableStreamClose(stream) { stream[kState].closeQueuedOrInFlight = true; const { promise } = stream[kState].closeRequest; if (writer !== undefined && backpressure && state === 'writable') - writer[kState].ready?.resolve?.(); + writer[kState].ready?.resolve(); writableStreamDefaultControllerClose(controller); return promise; } @@ -767,7 +767,7 @@ function writableStreamUpdateBackpressure(stream, backpressure) { // dropping the cache lets the next observation derive it. writer[kState].ready = undefined; } else { - writer[kState].ready?.resolve?.(); + writer[kState].ready?.resolve(); } } streamState.backpressure = backpressure; @@ -796,7 +796,7 @@ function writableStreamRejectCloseAndClosedPromiseIfNeeded(stream) { assert(stream[kState].state === 'errored'); if (stream[kState].closeRequest.promise !== undefined) { assert(stream[kState].inFlightCloseRequest.promise === undefined); - stream[kState].closeRequest.reject?.(stream[kState].storedError); + stream[kState].closeRequest.reject(stream[kState].storedError); stream[kState].closeRequest = kNilRequest; stream[kState].closeQueuedOrInFlight = false; } @@ -804,7 +804,7 @@ function writableStreamRejectCloseAndClosedPromiseIfNeeded(stream) { const closedPromiseCache = stream[kState].closedPromise; if (closedPromiseCache !== undefined) { setPromiseHandled(closedPromiseCache.promise); - closedPromiseCache.reject?.(stream[kState].storedError); + closedPromiseCache.reject(stream[kState].storedError); } const { @@ -814,7 +814,7 @@ function writableStreamRejectCloseAndClosedPromiseIfNeeded(stream) { const closeCache = writer[kState].close; if (closeCache !== undefined) { setPromiseHandled(closeCache.promise); - closeCache.reject?.(stream[kState].storedError); + closeCache.reject(stream[kState].storedError); } } } @@ -847,7 +847,7 @@ function writableStreamHasOperationMarkedInFlight(stream) { function writableStreamFinishInFlightWriteWithError(stream, error) { assert(stream[kState].inFlightWriteRequest.promise !== undefined); - stream[kState].inFlightWriteRequest.reject?.(error); + stream[kState].inFlightWriteRequest.reject(error); stream[kState].inFlightWriteRequest = kNilRequest; assert(stream[kState].state === 'writable' || stream[kState].state === 'erroring'); @@ -856,19 +856,19 @@ function writableStreamFinishInFlightWriteWithError(stream, error) { function writableStreamFinishInFlightWrite(stream) { assert(stream[kState].inFlightWriteRequest.promise !== undefined); - stream[kState].inFlightWriteRequest.resolve?.(); + stream[kState].inFlightWriteRequest.resolve(); stream[kState].inFlightWriteRequest = kNilRequest; } function writableStreamFinishInFlightCloseWithError(stream, error) { assert(stream[kState].inFlightCloseRequest.promise !== undefined); - stream[kState].inFlightCloseRequest.reject?.(error); + stream[kState].inFlightCloseRequest.reject(error); stream[kState].inFlightCloseRequest = kNilRequest; stream[kState].closeQueuedOrInFlight = false; assert(stream[kState].state === 'writable' || stream[kState].state === 'erroring'); if (stream[kState].pendingAbortRequest.abort.promise !== undefined) { - stream[kState].pendingAbortRequest.abort.reject?.(error); + stream[kState].pendingAbortRequest.abort.reject(error); stream[kState].pendingAbortRequest = kNilPendingAbortRequest; } writableStreamDealWithRejection(stream, error); @@ -876,20 +876,20 @@ function writableStreamFinishInFlightCloseWithError(stream, error) { function writableStreamFinishInFlightClose(stream) { assert(stream[kState].inFlightCloseRequest.promise !== undefined); - stream[kState].inFlightCloseRequest.resolve?.(); + stream[kState].inFlightCloseRequest.resolve(); stream[kState].inFlightCloseRequest = kNilRequest; stream[kState].closeQueuedOrInFlight = false; if (stream[kState].state === 'erroring') { stream[kState].storedError = undefined; if (stream[kState].pendingAbortRequest.abort.promise !== undefined) { - stream[kState].pendingAbortRequest.abort.resolve?.(); + stream[kState].pendingAbortRequest.abort.resolve(); stream[kState].pendingAbortRequest = kNilPendingAbortRequest; } } stream[kState].state = 'closed'; if (stream[kState].writer !== undefined) - stream[kState].writer[kState].close?.resolve?.(); - stream[kState].closedPromise?.resolve?.(); + stream[kState].writer[kState].close?.resolve(); + stream[kState].closedPromise?.resolve(); assert(stream[kState].pendingAbortRequest.abort.promise === undefined); assert(stream[kState].storedError === undefined); } @@ -901,7 +901,7 @@ function writableStreamFinishErroring(stream) { stream[kState].controller[kError](); const storedError = stream[kState].storedError; for (let n = 0; n < stream[kState].writeRequests.length; n++) - stream[kState].writeRequests[n].reject?.(storedError); + stream[kState].writeRequests[n].reject(storedError); stream[kState].writeRequests = []; if (stream[kState].pendingAbortRequest.abort.promise === undefined) { @@ -912,18 +912,18 @@ function writableStreamFinishErroring(stream) { const abortRequest = stream[kState].pendingAbortRequest; stream[kState].pendingAbortRequest = kNilPendingAbortRequest; if (abortRequest.wasAlreadyErroring) { - abortRequest.abort.reject?.(storedError); + abortRequest.abort.reject(storedError); writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); return; } PromisePrototypeThen( stream[kState].controller[kAbort](abortRequest.reason), () => { - abortRequest.abort.resolve?.(); + abortRequest.abort.resolve(); writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); }, (error) => { - abortRequest.abort.reject?.(error); + abortRequest.abort.reject(error); writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); }); } @@ -1024,7 +1024,7 @@ function writableStreamDefaultWriterGetDesiredSize(writer) { function writableStreamDefaultWriterEnsureReadyPromiseRejected(writer, error) { const ready = writer[kState].ready; if (ready !== undefined && isPromisePending(ready.promise)) { - ready.reject?.(error); + ready.reject(error); setPromiseHandled(ready.promise); } else { // The spec replaces [[readyPromise]] with a promise rejected with the @@ -1037,7 +1037,7 @@ function writableStreamDefaultWriterEnsureReadyPromiseRejected(writer, error) { function writableStreamDefaultWriterEnsureClosedPromiseRejected(writer, error) { const close = writer[kState].close; if (close !== undefined && isPromisePending(close.promise)) { - close.reject?.(error); + close.reject(error); setPromiseHandled(close.promise); } else { // See writableStreamDefaultWriterEnsureReadyPromiseRejected. diff --git a/lib/quic.js b/lib/quic.js index a6ca37825fbe71..4f50933aeb4577 100644 --- a/lib/quic.js +++ b/lib/quic.js @@ -8,7 +8,9 @@ emitExperimentalWarning('quic'); const { connect, listen, + listEndpoints, QuicEndpoint, + QuicError, QuicSession, QuicStream, CC_ALGO_RENO, @@ -18,12 +20,30 @@ const { DEFAULT_GROUPS, } = require('internal/quic/quic'); +const cc = { + get RENO() { return CC_ALGO_RENO; }, + get CUBIC() { return CC_ALGO_CUBIC; }, + get BBR() { return CC_ALGO_BBR; }, +}; + +const constants = { + get cc() { return cc; }, + get DEFAULT_CIPHERS() { return DEFAULT_CIPHERS; }, + get DEFAULT_GROUPS() { return DEFAULT_GROUPS; }, +}; + module.exports = { connect, listen, + listEndpoints, QuicEndpoint, + QuicError, QuicSession, QuicStream, + get constants() { + return constants; + }, + CC_ALGO_RENO, CC_ALGO_CUBIC, CC_ALGO_BBR, diff --git a/lib/stream/iter.js b/lib/stream/iter.js new file mode 100644 index 00000000000000..4cb499bb161694 --- /dev/null +++ b/lib/stream/iter.js @@ -0,0 +1,196 @@ +'use strict'; + +// Public entry point for the iterable streams API. +// Usage: require('stream/iter') or require('node:stream/iter') +// Requires: --experimental-stream-iter + +const { + ObjectFreeze, +} = primordials; + +const { emitExperimentalWarning } = require('internal/util'); +emitExperimentalWarning('stream/iter'); + +// Protocol symbols +const { + toStreamable, + toAsyncStreamable, + broadcastProtocol, + shareProtocol, + shareSyncProtocol, + drainableProtocol, +} = require('internal/streams/iter/types'); + +// Factories +const { push } = require('internal/streams/iter/push'); +const { duplex } = require('internal/streams/iter/duplex'); +const { from, fromSync } = require('internal/streams/iter/from'); + +// Pipelines +const { + pull, + pullSync, + pipeTo, + pipeToSync, +} = require('internal/streams/iter/pull'); + +// Consumers +const { + bytes, + bytesSync, + text, + textSync, + arrayBuffer, + arrayBufferSync, + array, + arraySync, + tap, + tapSync, + merge, + ondrain, +} = require('internal/streams/iter/consumers'); + +// Classic stream interop (Node.js-specific, not part of the spec) +const { + fromReadable, + fromWritable, + toReadable, + toReadableSync, + toWritable, +} = require('internal/streams/iter/classic'); + +// Multi-consumer +const { broadcast, Broadcast } = require('internal/streams/iter/broadcast'); +const { + share, + shareSync, + Share, + SyncShare, +} = require('internal/streams/iter/share'); + +/** + * Stream namespace - unified access to all stream functions. + * @example + * const { Stream } = require('stream/iter'); + * + * const { writer, readable } = Stream.push(); + * await writer.write("hello"); + * await writer.end(); + * + * const output = Stream.pull(readable, transform1, transform2); + * const data = await Stream.bytes(output); + */ +const Stream = ObjectFreeze({ + // Factories + push, + duplex, + from, + fromSync, + + // Pipelines + pull, + pullSync, + + // Pipe to destination + pipeTo, + pipeToSync, + + // Consumers (async) + bytes, + text, + arrayBuffer, + array, + + // Consumers (sync) + bytesSync, + textSync, + arrayBufferSync, + arraySync, + + // Combining + merge, + + // Multi-consumer (push model) + broadcast, + + // Multi-consumer (pull model) + share, + shareSync, + + // Utilities + tap, + tapSync, + + // Drain utility for event source integration + ondrain, + + // Protocol symbols + toStreamable, + toAsyncStreamable, + broadcastProtocol, + shareProtocol, + shareSyncProtocol, + drainableProtocol, +}); + +module.exports = { + // The Stream namespace + Stream, + + // Also export everything individually for destructured imports + + // Protocol symbols + toStreamable, + toAsyncStreamable, + broadcastProtocol, + shareProtocol, + shareSyncProtocol, + drainableProtocol, + + // Factories + push, + duplex, + from, + fromSync, + + // Pipelines + pull, + pullSync, + pipeTo, + pipeToSync, + + // Consumers (async) + bytes, + text, + arrayBuffer, + array, + + // Consumers (sync) + bytesSync, + textSync, + arrayBufferSync, + arraySync, + + // Combining + merge, + + // Multi-consumer + broadcast, + Broadcast, + share, + shareSync, + Share, + SyncShare, + + // Utilities + tap, + tapSync, + ondrain, + + // Classic stream interop + fromReadable, + fromWritable, + toReadable, + toReadableSync, + toWritable, +}; diff --git a/lib/zlib/iter.js b/lib/zlib/iter.js new file mode 100644 index 00000000000000..7c2d64d75aa63f --- /dev/null +++ b/lib/zlib/iter.js @@ -0,0 +1,53 @@ +'use strict'; + +// Public entry point for the iterable compression/decompression API. +// Usage: require('zlib/iter') or require('node:zlib/iter') +// Requires: --experimental-stream-iter + +const { emitExperimentalWarning } = require('internal/util'); +emitExperimentalWarning('zlib/iter'); + +const { + compressGzip, + compressGzipSync, + compressDeflate, + compressDeflateSync, + compressBrotli, + compressBrotliSync, + compressZstd, + compressZstdSync, + decompressGzip, + decompressGzipSync, + decompressDeflate, + decompressDeflateSync, + decompressBrotli, + decompressBrotliSync, + decompressZstd, + decompressZstdSync, +} = require('internal/streams/iter/transform'); + +module.exports = { + // Compression transforms (async) + compressGzip, + compressDeflate, + compressBrotli, + compressZstd, + + // Compression transforms (sync) + compressGzipSync, + compressDeflateSync, + compressBrotliSync, + compressZstdSync, + + // Decompression transforms (async) + decompressGzip, + decompressDeflate, + decompressBrotli, + decompressZstd, + + // Decompression transforms (sync) + decompressGzipSync, + decompressDeflateSync, + decompressBrotliSync, + decompressZstdSync, +}; diff --git a/node.gyp b/node.gyp index bd66c9b4982e7f..d9c97b5f4df9aa 100644 --- a/node.gyp +++ b/node.gyp @@ -345,7 +345,6 @@ 'src/quic/bindingdata.cc', 'src/quic/cid.cc', 'src/quic/data.cc', - 'src/quic/logstream.cc', 'src/quic/packet.cc', 'src/quic/preferredaddress.cc', 'src/quic/sessionticket.cc', @@ -354,6 +353,7 @@ 'src/quic/endpoint.cc', 'src/quic/http3.cc', 'src/quic/session.cc', + 'src/quic/session_manager.cc', 'src/quic/streams.cc', 'src/quic/tlscontext.cc', 'src/quic/transportparams.cc', @@ -363,7 +363,6 @@ 'src/quic/cid.h', 'src/quic/data.h', 'src/quic/defs.h', - 'src/quic/logstream.h', 'src/quic/packet.h', 'src/quic/preferredaddress.h', 'src/quic/sessionticket.h', @@ -373,6 +372,7 @@ 'src/quic/endpoint.h', 'src/quic/http3.h', 'src/quic/session.h', + 'src/quic/session_manager.h', 'src/quic/streams.h', 'src/quic/tlscontext.h', 'src/quic/guard.h', @@ -449,6 +449,7 @@ 'test/cctest/test_quic_cid.cc', 'test/cctest/test_quic_error.cc', 'test/cctest/test_quic_preferredaddress.cc', + 'test/cctest/test_quic_tokenbucket.cc', 'test/cctest/test_quic_tokens.cc', ], 'node_cctest_inspector_sources': [ diff --git a/node.gypi b/node.gypi index 6587270c9e52f3..a21faa977b7355 100644 --- a/node.gypi +++ b/node.gypi @@ -393,6 +393,8 @@ # For tests './deps/openssl/openssl.gyp:openssl-cli', + './deps/ngtcp2/ngtcp2.gyp:ngtcp2_test_server', + './deps/ngtcp2/ngtcp2.gyp:ngtcp2_test_client', ], 'conditions': [ # -force_load or --whole-archive are not applicable for diff --git a/src/aliased_struct-inl.h b/src/aliased_struct-inl.h index 17d5ff58097e22..ff70f423eb1bd1 100644 --- a/src/aliased_struct-inl.h +++ b/src/aliased_struct-inl.h @@ -47,6 +47,95 @@ AliasedStruct::~AliasedStruct() { if (ptr_ != nullptr) ptr_->~T(); } +// --------------------------------------------------------------------------- +// AliasedStructArena implementation +// --------------------------------------------------------------------------- + +template +typename AliasedStructArena::Page* +AliasedStructArena::FindOrCreatePage(v8::Isolate* isolate) { + for (auto& p : pages_) { + if (p->HasFreeSlots()) return p.get(); + } + auto p = std::make_unique(); + p->Init(isolate); + Page* raw = p.get(); + pages_.push_back(std::move(p)); + return raw; +} + +template +template +typename AliasedStructArena::Slot +AliasedStructArena::Allocate(v8::Isolate* isolate, + Args&&... args) { + Page* page = FindOrCreatePage(isolate); + DCHECK(page->HasFreeSlots()); + + uint32_t idx = page->free_head; + T* raw = &page->base[idx]; + + // Advance freelist before placement new overwrites the linkage. + page->free_head = *reinterpret_cast(raw); + page->used_count++; + + // Placement-construct T in the slot. + T* ptr = new (raw) T(std::forward(args)...); + + Slot slot; + slot.page = static_cast(page); + slot.ptr = static_cast(ptr); + slot.index = idx; + slot.byte_offset = reinterpret_cast(ptr) - + static_cast(page->store->Data()); + return slot; +} + +template +void AliasedStructArena::Release( + typename AliasedStructArena::Slot&& slot) { + if (!slot) return; + auto* page = static_cast(slot.page); + auto* ptr = static_cast(slot.ptr); + uint32_t idx = slot.index; + + // Destruct and zero so JS views see clean data. + ptr->~T(); + memset(ptr, 0, sizeof(T)); + + // Push onto page freelist. + *reinterpret_cast(ptr) = page->free_head; + page->free_head = idx; + page->used_count--; + + slot.page = nullptr; + slot.ptr = nullptr; + + // Drop empty pages. The shared_ptr ensures the + // underlying memory stays alive until V8 GCs any remaining JS + // references to the page's ArrayBuffer/views. + if (page->used_count == 0) { + for (auto it = pages_.begin(); it != pages_.end(); ++it) { + if (it->get() == page) { + pages_.erase(it); + break; + } + } + } +} + +template +void AliasedStructArena::ReleaseSlot(ArenaSlotBase& base) { + Slot slot; + slot.page = base.page; + slot.ptr = base.ptr; + slot.index = base.index; + slot.byte_offset = base.byte_offset; + Release(std::move(slot)); + base.page = nullptr; + base.ptr = nullptr; +} + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/aliased_struct.h b/src/aliased_struct.h index e4df393f4985a3..97753192723feb 100644 --- a/src/aliased_struct.h +++ b/src/aliased_struct.h @@ -3,9 +3,10 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include +#include #include "node_internals.h" #include "v8.h" -#include namespace node { @@ -56,6 +57,190 @@ class AliasedStruct final { v8::Global buffer_; }; +// --------------------------------------------------------------------------- +// ArenaSlot — type-erased handle to a slot in an AliasedStructArena page. +// This can be stored in headers where T is incomplete. The typed accessors +// are provided via a thin typed wrapper (AliasedStructArena::Slot). +struct ArenaSlotBase { + // Opaque page pointer — only the arena knows the concrete type. + void* page = nullptr; + void* ptr = nullptr; + uint32_t index = 0; + size_t byte_offset = 0; + + explicit operator bool() const { return ptr != nullptr; } + + // Returns the page's ArrayBuffer. Implemented below after ArenaPageHeader. + v8::Local GetArrayBuffer(v8::Isolate* isolate) const; + + size_t GetByteOffset() const { return byte_offset; } + + // Returns the page's cached DataView over the full page. + // Callers use byte_offset to index into the correct slot region. + v8::Local GetPageDataView(v8::Isolate* isolate) const; + + // Returns the page's cached BigUint64Array over the full page. + // Callers use byte_offset / sizeof(uint64_t) to index into the + // correct slot region. + v8::Local GetPageBigUint64Array( + v8::Isolate* isolate) const; +}; + +// --------------------------------------------------------------------------- +// AliasedStructArena — pool allocator for AliasedStruct-style shared +// memory. Instead of creating a separate ArrayBuffer + BackingStore per +// instance, the arena pre-allocates pages of N slots backed by a single +// ArrayBuffer each. Callers receive a Slot handle that provides the same +// T*/operator-> interface as AliasedStruct, plus the ability to create a +// JS typed-array view over just that slot's region of the page buffer. +// +// Pages target kPageBytes (default 16 KB) for L1 cache residency during +// sequential access patterns. Slots are recycled via an intrusive +// freelist, and empty pages are dropped when their last slot is released. +// +// Usage: +// AliasedStructArena arena; +// auto slot = arena.Allocate(isolate); +// slot->some_field = 42; +// auto view = slot.GetArrayBuffer(isolate); // JS-visible view +// ... +// arena.Release(std::move(slot)); // return to freelist +// +template +class AliasedStructArena final { + public: + static constexpr size_t kSlotsPerPage = kPageBytes / sizeof(T); + static_assert(kSlotsPerPage >= 4, "Page too small for type T"); + static_assert(sizeof(T) >= sizeof(uint32_t), + "T must be at least 4 bytes for freelist linkage"); + + AliasedStructArena() = default; + ~AliasedStructArena() = default; + + AliasedStructArena(const AliasedStructArena&) = delete; + AliasedStructArena& operator=(const AliasedStructArena&) = delete; + + struct Page { + std::shared_ptr store; + v8::Global buffer; + // Lazily created full-page views shared by all slots in + // this page. Typically only one is used per arena. + v8::Global data_view; + v8::Global big_uint64_array; + size_t page_byte_length = 0; + T* base = nullptr; + uint32_t free_head = 0; + uint32_t used_count = 0; + static constexpr uint32_t kNoFreeSlot = UINT32_MAX; + + void Init(v8::Isolate* isolate) { + const v8::HandleScope handle_scope(isolate); + const size_t total_bytes = kSlotsPerPage * sizeof(T); + store = v8::ArrayBuffer::NewBackingStore(isolate, total_bytes); + memset(store->Data(), 0, total_bytes); + base = static_cast(store->Data()); + page_byte_length = total_bytes; + v8::Local ab = v8::ArrayBuffer::New(isolate, store); + buffer = v8::Global(isolate, ab); + + // Build freelist: each slot points to the next. + for (uint32_t i = 0; i < kSlotsPerPage - 1; i++) { + *reinterpret_cast(&base[i]) = i + 1; + } + *reinterpret_cast(&base[kSlotsPerPage - 1]) = kNoFreeSlot; + free_head = 0; + used_count = 0; + } + + bool HasFreeSlots() const { return free_head != kNoFreeSlot; } + }; + + // Typed slot handle — wraps ArenaSlotBase with T* accessors. + class Slot : public ArenaSlotBase { + public: + Slot() = default; + + const T& operator*() const { return *static_cast(ptr); } + T& operator*() { return *static_cast(ptr); } + const T* operator->() const { return static_cast(ptr); } + T* operator->() { return static_cast(ptr); } + T* Data() { return static_cast(ptr); } + const T* Data() const { return static_cast(ptr); } + }; + + // Allocate a slot, placement-constructing T with the given args. + // Creates a new page if all existing pages are full. + template + Slot Allocate(v8::Isolate* isolate, Args&&... args); + + // Release a slot back to the arena freelist. Calls ~T() and zeros + // the memory so that any JS views see clean data. + void Release(Slot&& slot); + + // Release a slot given a type-erased ArenaSlotBase reference. + // Convenience for callers that store ArenaSlotBase in headers where + // T is incomplete. + void ReleaseSlot(ArenaSlotBase& base); + + private: + Page* FindOrCreatePage(v8::Isolate* isolate); + + std::vector> pages_; +}; + +// ArenaSlotBase accessors need to reach the v8::Globals inside a Page. +// All AliasedStructArena::Page types share the same leading layout. +// The page_byte_length field allows lazy view creation without knowing T. +namespace detail { +struct ArenaPageHeader { + std::shared_ptr store; + v8::Global buffer; + v8::Global data_view; + v8::Global big_uint64_array; + size_t page_byte_length = 0; + + v8::Local GetDataView(v8::Isolate* isolate) { + if (data_view.IsEmpty()) { + const v8::HandleScope handle_scope(isolate); + auto dv = v8::DataView::New(buffer.Get(isolate), 0, page_byte_length); + data_view = v8::Global(isolate, dv); + } + return data_view.Get(isolate); + } + + v8::Local GetBigUint64Array(v8::Isolate* isolate) { + if (big_uint64_array.IsEmpty()) { + const v8::HandleScope handle_scope(isolate); + auto bu = v8::BigUint64Array::New( + buffer.Get(isolate), 0, page_byte_length / sizeof(uint64_t)); + big_uint64_array = v8::Global(isolate, bu); + } + return big_uint64_array.Get(isolate); + } +}; +} // namespace detail + +inline v8::Local ArenaSlotBase::GetArrayBuffer( + v8::Isolate* isolate) const { + DCHECK_NOT_NULL(page); + auto* header = static_cast(page); + return header->buffer.Get(isolate); +} + +inline v8::Local ArenaSlotBase::GetPageDataView( + v8::Isolate* isolate) const { + DCHECK_NOT_NULL(page); + auto* header = static_cast(page); + return header->GetDataView(isolate); +} + +inline v8::Local ArenaSlotBase::GetPageBigUint64Array( + v8::Isolate* isolate) const { + DCHECK_NOT_NULL(page); + auto* header = static_cast(page); + return header->GetBigUint64Array(isolate); +} + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/crypto/crypto_common.cc b/src/crypto/crypto_common.cc index 9d8d6b89b38d98..0db5d0eaac8fc3 100644 --- a/src/crypto/crypto_common.cc +++ b/src/crypto/crypto_common.cc @@ -61,7 +61,7 @@ MaybeLocal GetValidationErrorReason(Environment* env, int err) { (err == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE) || (err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) || ((err == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT) && - !per_process::cli_options->use_system_ca); + !env->options()->use_system_ca); if (suggest_system_ca) { reason.append("; if the root CA is installed locally, " diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index a26577b36a6f6c..02a748ef280c99 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -103,12 +103,39 @@ static thread_local X509_STORE* root_cert_store = nullptr; // copy generated by NewRootCertStore() will then contain the certificates // from this set. static thread_local std::unique_ptr root_certs_from_users; +static thread_local bool has_cleanup_hook = false; -X509_STORE* GetOrCreateRootCertStore() { +static void CleanupRootCertStore(void*) { + if (root_cert_store != nullptr) { + X509_STORE_free(root_cert_store); + root_cert_store = nullptr; + } + + if (root_certs_from_users != nullptr) { + for (X509* cert : *root_certs_from_users) { + X509_free(cert); + } + root_certs_from_users.reset(); + } + + has_cleanup_hook = false; +} + +static void EnsureRootCertStoreCleanupHook(Environment* env) { + if (env == nullptr || has_cleanup_hook) { + return; + } + + env->AddCleanupHook(CleanupRootCertStore, nullptr); + has_cleanup_hook = true; +} + +X509_STORE* GetOrCreateRootCertStore(Environment* env) { + EnsureRootCertStoreCleanupHook(env); if (root_cert_store != nullptr) { return root_cert_store; } - root_cert_store = NewRootCertStore(); + root_cert_store = NewRootCertStore(env); return root_cert_store; } @@ -932,14 +959,9 @@ static void LoadCACertificates(void* data) { "Started loading extra root certificates off-thread\n"); GetExtraCACertificates(); } +} - { - Mutex::ScopedLock cli_lock(node::per_process::cli_options_mutex); - if (!per_process::cli_options->use_system_ca) { - return; - } - } - +static void LoadSystemCACertificates(void* data) { per_process::Debug(DebugCategory::CRYPTO, "Started loading system root certificates off-thread\n"); GetSystemStoreCACertificates(); @@ -947,8 +969,12 @@ static void LoadCACertificates(void* data) { static std::atomic tried_cert_loading_off_thread = false; static std::atomic cert_loading_thread_started = false; +static std::atomic tried_system_cert_loading_off_thread = false; +static std::atomic system_cert_loading_thread_started = false; static Mutex start_cert_loading_thread_mutex; +static Mutex start_system_cert_loading_thread_mutex; static uv_thread_t cert_loading_thread; +static uv_thread_t system_cert_loading_thread; void StartLoadingCertificatesOffThread( const FunctionCallbackInfo& args) { @@ -968,23 +994,46 @@ void StartLoadingCertificatesOffThread( } } + Environment* env = Environment::GetCurrent(args); + const bool use_system_ca = env != nullptr && env->options()->use_system_ca; + per_process::Debug( + DebugCategory::CRYPTO, "StartLoadingCertificatesOffThread env=%p\n", env); // Only try to start the thread once. If it ever fails, we won't try again. - if (tried_cert_loading_off_thread.load()) { - return; - } - { + // Quick check, if it's already tried, no need to lock. + if (!tried_cert_loading_off_thread.load()) { Mutex::ScopedLock lock(start_cert_loading_thread_mutex); - // Re-check under the lock. - if (tried_cert_loading_off_thread.load()) { - return; + // Check again under the lock. + if (!tried_cert_loading_off_thread.load()) { + tried_cert_loading_off_thread.store(true); + int r = + uv_thread_create(&cert_loading_thread, LoadCACertificates, nullptr); + cert_loading_thread_started.store(r == 0); + if (r != 0) { + FPrintF(stderr, + "Warning: Failed to load CA certificates off thread: %s\n", + uv_strerror(r)); + } } - tried_cert_loading_off_thread.store(true); - int r = uv_thread_create(&cert_loading_thread, LoadCACertificates, nullptr); - cert_loading_thread_started.store(r == 0); - if (r != 0) { - FPrintF(stderr, - "Warning: Failed to load CA certificates off thread: %s\n", - uv_strerror(r)); + } + + // If the system CA list hasn't been loaded off-thread yet, allow a worker + // enabling --use-system-ca to trigger its off-thread loading. + // Quick check, if it's already tried, no need to lock. + if (use_system_ca && !has_cached_system_root_certs.load() && + !tried_system_cert_loading_off_thread.load()) { + Mutex::ScopedLock lock(start_system_cert_loading_thread_mutex); + if (!has_cached_system_root_certs.load() && + !tried_system_cert_loading_off_thread.load()) { + tried_system_cert_loading_off_thread.store(true); + int r = uv_thread_create( + &system_cert_loading_thread, LoadSystemCACertificates, nullptr); + system_cert_loading_thread_started.store(r == 0); + if (r != 0) { + FPrintF( + stderr, + "Warning: Failed to load system CA certificates off thread: %s\n", + uv_strerror(r)); + } } } } @@ -1009,13 +1058,13 @@ void StartLoadingCertificatesOffThread( // with all the other flags. // 7. Certificates from --use-bundled-ca, --use-system-ca and // NODE_EXTRA_CA_CERTS are cached after first load. Certificates -// from --use-system-ca are not cached and always reloaded from +// from --use-openssl-ca are not cached and always reloaded from // disk. // 8. If users have reset the root cert store by calling // tls.setDefaultCACertificates(), the store will be populated with // the certificates provided by users. // TODO(joyeecheung): maybe these rules need a bit of consolidation? -X509_STORE* NewRootCertStore() { +X509_STORE* NewRootCertStore(Environment* env) { X509_STORE* store = X509_STORE_new(); CHECK_NOT_NULL(store); @@ -1037,14 +1086,26 @@ X509_STORE* NewRootCertStore() { } #endif - Mutex::ScopedLock cli_lock(node::per_process::cli_options_mutex); - if (per_process::cli_options->ssl_openssl_cert_store) { + bool use_system_ca = false; + bool ssl_openssl_cert_store = false; + { + Mutex::ScopedLock cli_lock(node::per_process::cli_options_mutex); + ssl_openssl_cert_store = per_process::cli_options->ssl_openssl_cert_store; + if (env != nullptr) { + use_system_ca = env->options()->use_system_ca; + } else if (per_process::cli_options->per_isolate != nullptr && + per_process::cli_options->per_isolate->per_env != nullptr) { + use_system_ca = + per_process::cli_options->per_isolate->per_env->use_system_ca; + } + } + if (ssl_openssl_cert_store) { CHECK_EQ(1, X509_STORE_set_default_paths(store)); } else { for (X509* cert : GetBundledRootCertificates()) { CHECK_EQ(1, X509_STORE_add_cert(store, cert)); } - if (per_process::cli_options->use_system_ca) { + if (use_system_ca) { for (X509* cert : GetSystemStoreCACertificates()) { CHECK_EQ(1, X509_STORE_add_cert(store, cert)); } @@ -1061,6 +1122,22 @@ X509_STORE* NewRootCertStore() { } void CleanupCachedRootCertificates() { + // Serialize with starters to avoid the race window. + { + Mutex::ScopedLock lock(start_cert_loading_thread_mutex); + if (tried_cert_loading_off_thread.load() && + cert_loading_thread_started.load()) { + uv_thread_join(&cert_loading_thread); + } + } + { + Mutex::ScopedLock lock(start_system_cert_loading_thread_mutex); + if (tried_system_cert_loading_off_thread.load() && + system_cert_loading_thread_started.load()) { + uv_thread_join(&system_cert_loading_thread); + } + } + if (has_cached_bundled_root_certs.load()) { for (X509* cert : GetBundledRootCertificates()) { X509_free(cert); @@ -1077,13 +1154,6 @@ void CleanupCachedRootCertificates() { X509_free(cert); } } - - // Serialize with starter to avoid the race window. - Mutex::ScopedLock lock(start_cert_loading_thread_mutex); - if (tried_cert_loading_off_thread.load() && - cert_loading_thread_started.load()) { - uv_thread_join(&cert_loading_thread); - } } void GetBundledRootCertificates(const FunctionCallbackInfo& args) { @@ -1195,6 +1265,8 @@ void ResetRootCertStore(const FunctionCallbackInfo& args) { Local context = args.GetIsolate()->GetCurrentContext(); CHECK(args[0]->IsArray()); Local cert_array = args[0].As(); + Environment* env = Environment::GetCurrent(context); + EnsureRootCertStoreCleanupHook(env); if (cert_array->Length() == 0) { // If the array is empty, just clear the user certs and reset the store. @@ -1249,9 +1321,7 @@ void ResetRootCertStore(const FunctionCallbackInfo& args) { X509_STORE_free(root_cert_store); } - // TODO(joyeecheung): we can probably just reset it to nullptr - // and let the next call to NewRootCertStore() create a new one. - root_cert_store = NewRootCertStore(); + root_cert_store = nullptr; } void GetSystemCACertificates(const FunctionCallbackInfo& args) { @@ -1778,11 +1848,12 @@ void SecureContext::SetX509StoreFlag(unsigned long flags) { } X509_STORE* SecureContext::GetCertStoreOwnedByThisSecureContext() { + Environment* env = this->env(); if (own_cert_store_cache_ != nullptr) return own_cert_store_cache_; X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx_.get()); - if (cert_store == GetOrCreateRootCertStore()) { - cert_store = NewRootCertStore(); + if (cert_store == GetOrCreateRootCertStore(env)) { + cert_store = NewRootCertStore(env); SSL_CTX_set_cert_store(ctx_.get(), cert_store); } @@ -1855,7 +1926,8 @@ void SecureContext::AddCRL(const FunctionCallbackInfo& args) { void SecureContext::SetRootCerts() { ClearErrorOnReturn clear_error_on_return; - auto store = GetOrCreateRootCertStore(); + Environment* env = this->env(); + auto store = GetOrCreateRootCertStore(env); // Increment reference count so global store is not deleted along with CTX. X509_STORE_up_ref(store); diff --git a/src/crypto/crypto_context.h b/src/crypto/crypto_context.h index b1c6ba671314b1..ffb0b88816c092 100644 --- a/src/crypto/crypto_context.h +++ b/src/crypto/crypto_context.h @@ -23,9 +23,9 @@ constexpr int kMaxSupportedVersion = TLS1_3_VERSION; void GetRootCertificates( const v8::FunctionCallbackInfo& args); -X509_STORE* NewRootCertStore(); +X509_STORE* NewRootCertStore(Environment* env); -X509_STORE* GetOrCreateRootCertStore(); +X509_STORE* GetOrCreateRootCertStore(Environment* env); ncrypto::BIOPointer LoadBIO(Environment* env, v8::Local v); diff --git a/src/dataqueue/queue.cc b/src/dataqueue/queue.cc index 64643680903a78..8516362b7a8788 100644 --- a/src/dataqueue/queue.cc +++ b/src/dataqueue/queue.cc @@ -179,6 +179,11 @@ class DataQueueImpl final : public DataQueue, for (auto& listener : backpressure_listeners_) listener->EntryRead(amount); } + void NotifyBeforePull() { + if (idempotent_) return; + for (auto& listener : backpressure_listeners_) listener->BeforePull(); + } + bool HasBackpressureListeners() const noexcept { return !backpressure_listeners_.empty(); } @@ -381,6 +386,11 @@ class NonIdempotentDataQueueReader final size_t max_count_hint = bob::kMaxCountHint) override { std::shared_ptr self = shared_from_this(); + // Let listeners flush pending data before we check the entries list. + // This allows, for example, the QUIC Stream to flush its receive + // accumulation buffer into the queue before the pull proceeds. + data_queue_->NotifyBeforePull(); + // If ended is true, this reader has already reached the end and cannot // provide any more data. if (ended_) { @@ -391,10 +401,11 @@ class NonIdempotentDataQueueReader final // If the collection of entries is empty, there's nothing currently left to // read. How we respond depends on whether the data queue has been capped // or not. + if (data_queue_->entries_.empty()) { // If the data_queue_ is empty, and not capped, then we can reasonably // expect more data to be provided later, but we don't know exactly when - // that'll happe, so the proper response here is to return a blocked + // that'll happen, so the proper response here is to return a blocked // status. if (!data_queue_->is_capped()) { std::move(next)(bob::Status::STATUS_BLOCK, nullptr, 0, [](uint64_t) {}); @@ -437,8 +448,11 @@ class NonIdempotentDataQueueReader final CHECK(!pull_pending_); pull_pending_ = true; int status = current_reader->Pull( - [this, next = std::move(next)]( - int status, const DataQueue::Vec* vecs, uint64_t count, Done done) { + [this, next = std::move(next), options, data, count, max_count_hint]( + int status, + const DataQueue::Vec* vecs, + uint64_t vcount, + Done done) mutable { pull_pending_ = false; // In each of these cases, we do not expect that the source will @@ -446,13 +460,27 @@ class NonIdempotentDataQueueReader final CHECK_IMPLIES(status == bob::Status::STATUS_BLOCK || status == bob::Status::STATUS_WAIT || status == bob::Status::STATUS_EOS, - vecs == nullptr && count == 0); + vecs == nullptr && vcount == 0); if (status == bob::Status::STATUS_EOS) { data_queue_->entries_.erase(data_queue_->entries_.begin()); - ended_ = data_queue_->entries_.empty(); current_reader_ = nullptr; - if (!ended_) status = bob::Status::STATUS_CONTINUE; - std::move(next)(status, nullptr, 0, [](uint64_t) {}); + if (!data_queue_->entries_.empty()) { + // More entries remain. Pull from the next entry immediately + // rather than returning empty CONTINUE, which would leave + // callers with no data and no way to know they should retry. + Pull(std::move(next), options, data, count, max_count_hint); + } else if (!data_queue_->is_capped()) { + // The queue is empty but not capped — more data may arrive + // later. Return BLOCK so the consumer waits rather than + // falsely treating this as end-of-stream. + std::move(next)( + bob::Status::STATUS_BLOCK, nullptr, 0, [](uint64_t) {}); + } else { + // Empty and capped — truly done. + ended_ = true; + std::move(next)( + bob::Status::STATUS_EOS, nullptr, 0, [](uint64_t) {}); + } return; } @@ -461,7 +489,7 @@ class NonIdempotentDataQueueReader final if (data_queue_->HasBackpressureListeners()) { // How much did we actually read? size_t read = 0; - for (uint64_t n = 0; n < count; n++) { + for (uint64_t n = 0; n < vcount; n++) { read += vecs[n].len; } data_queue_->NotifyBackpressure(read); @@ -469,7 +497,7 @@ class NonIdempotentDataQueueReader final // Now that we have updated this readers state, we can forward // everything on to the outer next. - std::move(next)(status, vecs, count, std::move(done)); + std::move(next)(status, vecs, vcount, std::move(done)); }, options, data, @@ -916,6 +944,7 @@ class FdEntry final : public EntryImpl { fs::FileHandle::New(realm->GetBindingData(), file, Local(), + {}, entry->start_, entry->end_ - entry->start_)), entry); diff --git a/src/dataqueue/queue.h b/src/dataqueue/queue.h index a37bd27549986e..190c54111b3e01 100644 --- a/src/dataqueue/queue.h +++ b/src/dataqueue/queue.h @@ -147,6 +147,11 @@ class DataQueue : public MemoryRetainer { class BackpressureListener { public: virtual void EntryRead(size_t amount) = 0; + + // Called before the reader pulls from the DataQueue. Gives + // the listener a chance to flush pending data into the queue + // before the pull checks the entries list. + virtual void BeforePull() {} }; // A DataQueue::Entry represents a logical chunk of data in the queue. diff --git a/src/debug_utils.h b/src/debug_utils.h index ffb8fe270018ee..52895a474b4ea4 100644 --- a/src/debug_utils.h +++ b/src/debug_utils.h @@ -54,7 +54,8 @@ void NODE_EXTERN_PRIVATE FWrite(FILE* file, const std::string& str); V(INSPECTOR_CLIENT) \ V(INSPECTOR_PROFILER) \ V(CODE_CACHE) \ - V(NGTCP2_DEBUG) \ + V(NGTCP2) \ + V(NGHTTP3) \ V(SEA) \ V(WASI) \ V(MODULE) \ diff --git a/src/node.cc b/src/node.cc index 078497db7e6dea..f6a0d1142b5e59 100644 --- a/src/node.cc +++ b/src/node.cc @@ -867,15 +867,6 @@ static ExitCode InitializeNodeWithArgsInternal( // default value. V8::SetFlagsFromString("--rehash-snapshot"); -#if HAVE_OPENSSL - // TODO(joyeecheung): make this a per-env option and move the normalization - // into HandleEnvOptions. - std::string use_system_ca; - if (credentials::SafeGetenv("NODE_USE_SYSTEM_CA", &use_system_ca) && - use_system_ca == "1") { - per_process::cli_options->use_system_ca = true; - } -#endif // HAVE_OPENSSL HandleEnvOptions(per_process::cli_options->per_isolate->per_env); std::string node_options; diff --git a/src/node_blob.cc b/src/node_blob.cc index 322c0a603f12c8..d05a693120f264 100644 --- a/src/node_blob.cc +++ b/src/node_blob.cc @@ -156,8 +156,7 @@ Local Blob::GetConstructorTemplate(Environment* env) { Isolate* isolate = env->isolate(); tmpl = NewFunctionTemplate(isolate, nullptr); tmpl->InstanceTemplate()->SetInternalFieldCount(Blob::kInternalFieldCount); - tmpl->SetClassName( - FIXED_ONE_BYTE_STRING(env->isolate(), "Blob")); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Blob")); SetProtoMethod(isolate, tmpl, "getReader", GetReader); SetProtoMethod(isolate, tmpl, "slice", ToSlice); env->set_blob_constructor_template(tmpl); @@ -255,8 +254,7 @@ void Blob::New(const FunctionCallbackInfo& args) { } auto blob = Create(env, DataQueue::CreateIdempotent(std::move(entries))); - if (blob) - args.GetReturnValue().Set(blob->object()); + if (blob) args.GetReturnValue().Set(blob->object()); } void Blob::GetReader(const FunctionCallbackInfo& args) { @@ -278,8 +276,7 @@ void Blob::ToSlice(const FunctionCallbackInfo& args) { size_t start = args[0].As()->Value(); size_t end = args[1].As()->Value(); BaseObjectPtr slice = blob->Slice(env, start, end); - if (slice) - args.GetReturnValue().Set(slice->object()); + if (slice) args.GetReturnValue().Set(slice->object()); } void Blob::MemoryInfo(MemoryTracker* tracker) const { @@ -343,6 +340,7 @@ void Blob::Reader::Pull(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Blob::Reader* reader; ASSIGN_OR_RETURN_UNWRAP(&reader, args.This()); + reader->pull_pending_ = false; CHECK(args[0]->IsFunction()); Local fn = args[0].As(); @@ -414,19 +412,31 @@ void Blob::Reader::Pull(const FunctionCallbackInfo& args) { void Blob::Reader::SetWakeup(const FunctionCallbackInfo& args) { Blob::Reader* reader; ASSIGN_OR_RETURN_UNWRAP(&reader, args.This()); + if (args[0]->IsUndefined()) { + reader->wakeup_.Reset(); + return; + } CHECK(args[0]->IsFunction()); reader->wakeup_.Reset(args.GetIsolate(), args[0].As()); } -void Blob::Reader::NotifyPull() { +void Blob::Reader::NotifyPull(bool fin) { if (wakeup_.IsEmpty() || !env()->can_call_into_js()) return; + // FIN notifications always fire — they must not be suppressed by + // pull_pending_ because there will be no further notifications to + // wake the iterator. Regular data notifications respect pull_pending_ + // to coalesce multiple deliveries within a single packet. + if (!fin && pull_pending_) return; + pull_pending_ = true; HandleScope handle_scope(env()->isolate()); Local fn = wakeup_.Get(env()->isolate()); - MakeCallback(fn, 0, nullptr); + // Pass fin as the first argument so the JS iterator knows EOS is + // imminent and should pull again without waiting for another wakeup. + Local argv[] = {v8::Boolean::New(env()->isolate(), fin)}; + MakeCallback(fn, 1, argv); } -BaseObjectPtr -Blob::BlobTransferData::Deserialize( +BaseObjectPtr Blob::BlobTransferData::Deserialize( Environment* env, Local context, std::unique_ptr self) { @@ -448,10 +458,10 @@ std::unique_ptr Blob::CloneForMessaging() const { void Blob::StoreDataObject(const FunctionCallbackInfo& args) { Realm* realm = Realm::GetCurrent(args); - CHECK(args[0]->IsString()); // ID key + CHECK(args[0]->IsString()); // ID key CHECK(Blob::HasInstance(realm->env(), args[1])); // Blob - CHECK(args[2]->IsUint32()); // Length - CHECK(args[3]->IsString()); // Type + CHECK(args[2]->IsUint32()); // Length + CHECK(args[3]->IsString()); // Type BlobBindingData* binding_data = realm->GetBindingData(); Isolate* isolate = realm->isolate(); @@ -531,12 +541,8 @@ void BlobBindingData::StoredDataObject::MemoryInfo( } BlobBindingData::StoredDataObject::StoredDataObject( - const BaseObjectPtr& blob_, - size_t length_, - const std::string& type_) - : blob(blob_), - length(length_), - type(type_) {} + const BaseObjectPtr& blob_, size_t length_, const std::string& type_) + : blob(blob_), length(length_), type(type_) {} BlobBindingData::BlobBindingData(Realm* realm, Local wrap) : SnapshotableObject(realm, wrap, type_int) { @@ -550,8 +556,7 @@ void BlobBindingData::MemoryInfo(MemoryTracker* tracker) const { } void BlobBindingData::store_data_object( - const std::string& uuid, - const BlobBindingData::StoredDataObject& object) { + const std::string& uuid, const BlobBindingData::StoredDataObject& object) { data_objects_[uuid] = object; } @@ -566,8 +571,7 @@ void BlobBindingData::revoke_data_object(const std::string& uuid) { BlobBindingData::StoredDataObject BlobBindingData::get_data_object( const std::string& uuid) { auto entry = data_objects_.find(uuid); - if (entry == data_objects_.end()) - return BlobBindingData::StoredDataObject {}; + if (entry == data_objects_.end()) return BlobBindingData::StoredDataObject{}; return entry->second; } diff --git a/src/node_blob.h b/src/node_blob.h index 88a56c7ec9a453..e782b96594b564 100644 --- a/src/node_blob.h +++ b/src/node_blob.h @@ -23,8 +23,7 @@ namespace node { class Blob : public BaseObject { public: - static void RegisterExternalReferences( - ExternalReferenceRegistry* registry); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); static void CreatePerIsolateProperties(IsolateData* isolate_data, v8::Local target); @@ -83,7 +82,7 @@ class Blob : public BaseObject { BaseObjectPtr blob); static void Pull(const v8::FunctionCallbackInfo& args); static void SetWakeup(const v8::FunctionCallbackInfo& args); - void NotifyPull(); + void NotifyPull(bool fin = false); explicit Reader(Environment* env, v8::Local obj, @@ -97,6 +96,7 @@ class Blob : public BaseObject { std::shared_ptr inner_; BaseObjectPtr strong_ptr_; bool eos_ = false; + bool pull_pending_ = false; v8::Global wakeup_; }; @@ -134,19 +134,17 @@ class BlobBindingData : public SnapshotableObject { StoredDataObject() = default; - StoredDataObject( - const BaseObjectPtr& blob_, - size_t length_, - const std::string& type_); + StoredDataObject(const BaseObjectPtr& blob_, + size_t length_, + const std::string& type_); void MemoryInfo(MemoryTracker* tracker) const override; SET_SELF_SIZE(StoredDataObject) SET_MEMORY_INFO_NAME(StoredDataObject) }; - void store_data_object( - const std::string& uuid, - const StoredDataObject& object); + void store_data_object(const std::string& uuid, + const StoredDataObject& object); void revoke_data_object(const std::string& uuid); diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 70c628294c98d7..7cfb17901034d3 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -135,15 +135,17 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { "internal/tls/wrap", "internal/tls/secure-context", "internal/http2/core", "internal/http2/compat", "internal/streams/lazy_transform", -#endif // !HAVE_OPENSSL +#endif // !HAVE_OPENSSL #ifndef OPENSSL_NO_QUIC "internal/quic/quic", "internal/quic/symbols", "internal/quic/stats", "internal/quic/state", -#endif // !OPENSSL_NO_QUIC - "quic", // Experimental. - "sqlite", // Experimental. - "sys", // Deprecated. - "wasi", // Experimental. +#endif // !OPENSSL_NO_QUIC + "quic", // Experimental. + "sqlite", // Experimental. + "stream/iter", // Experimental. + "zlib/iter", // Experimental. + "sys", // Deprecated. + "wasi", // Experimental. #if !HAVE_SQLITE "internal/webstorage", // Experimental. "internal/inspector/webstorage", diff --git a/src/node_file.cc b/src/node_file.cc index c46adc83d8542a..8e799fb08ae297 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -49,7 +49,7 @@ #include #if defined(__MINGW32__) || defined(_MSC_VER) -# include +#include #endif #ifdef _WIN32 @@ -88,7 +88,7 @@ using v8::Undefined; using v8::Value; #ifndef S_ISDIR -# define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) +#define S_ISDIR(mode) (((mode)&S_IFMT) == S_IFDIR) #endif #ifdef __POSIX__ @@ -203,8 +203,7 @@ static const char* get_fs_func_name_by_type(uv_fs_type req_type) { // We sometimes need to convert a C++ lambda function to a raw C-style function. // This is helpful, because ReqWrap::Dispatch() does not recognize lambda // functions, and thus does not wrap them properly. -typedef void(*uv_fs_callback_t)(uv_fs_t*); - +typedef void (*uv_fs_callback_t)(uv_fs_t*); void FSContinuationData::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("paths", paths_); @@ -222,9 +221,12 @@ void FSReqBase::MemoryInfo(MemoryTracker* tracker) const { // collection if necessary. If that happens, a process warning will be // emitted (or a fatal exception will occur if the fd cannot be closed.) FileHandle::FileHandle(BindingData* binding_data, - Local obj, int fd) + Local obj, + int fd, + std::string original_name) : AsyncWrap(binding_data->env(), obj, AsyncWrap::PROVIDER_FILEHANDLE), StreamBase(env()), + original_name_(std::move(original_name)), fd_(fd), binding_data_(binding_data) { MakeWeak(); @@ -234,6 +236,7 @@ FileHandle::FileHandle(BindingData* binding_data, FileHandle* FileHandle::New(BindingData* binding_data, int fd, Local obj, + std::string original_name, std::optional maybeOffset, std::optional maybeLength) { Environment* env = binding_data->env(); @@ -242,7 +245,7 @@ FileHandle* FileHandle::New(BindingData* binding_data, .ToLocal(&obj)) { return nullptr; } - auto handle = new FileHandle(binding_data, obj, fd); + auto handle = new FileHandle(binding_data, obj, fd, original_name); if (maybeOffset.has_value()) handle->read_offset_ = maybeOffset.value(); if (maybeLength.has_value()) handle->read_length_ = maybeLength.value(); return handle; @@ -274,6 +277,7 @@ void FileHandle::New(const FunctionCallbackInfo& args) { FileHandle::New(binding_data, args[0].As()->Value(), args.This(), + {}, maybeOffset, maybeLength); } @@ -293,6 +297,7 @@ int FileHandle::DoWrite(WriteWrap* w, void FileHandle::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("current_read", current_read_); + tracker->TrackField("original_name", original_name_); } BaseObject::TransferMode FileHandle::GetTransferMode() const { @@ -330,7 +335,7 @@ BaseObjectPtr FileHandle::TransferData::Deserialize( int fd = fd_; fd_ = -1; - return BaseObjectPtr { FileHandle::New(bd, fd) }; + return BaseObjectPtr{FileHandle::New(bd, fd)}; } // Close the file descriptor if it hasn't already been closed. A process @@ -346,25 +351,28 @@ inline void FileHandle::Close() { FS_SYNC_TRACE_END(close); uv_fs_req_cleanup(&req); - struct err_detail { int ret; int fd; }; + struct err_detail { + int ret; + int fd; + std::string name; + }; - err_detail detail { ret, fd_ }; + err_detail detail{ret, fd_, original_name_}; AfterClose(); if (ret < 0) { // Do not unref this env()->SetImmediate([detail](Environment* env) { - char msg[70]; - snprintf(msg, arraysize(msg), - "Closing file descriptor %d on garbage collection failed", - detail.fd); - // This exception will end up being fatal for the process because - // it is being thrown from within the SetImmediate handler and - // there is no JS stack to bubble it to. In other words, tearing - // down the process is the only reasonable thing we can do here. HandleScope handle_scope(env->isolate()); - env->ThrowUVException(detail.ret, "close", msg); + static constexpr std::string_view unknown_path = ""; + std::string_view filename = + detail.name.empty() ? unknown_path : detail.name; + auto formatted = SPrintF( + "Closing file descriptor %d on garbage collection failed (%s)", + detail.fd, + filename); + env->ThrowUVException(detail.ret, "close", formatted.c_str()); }); return; } @@ -422,7 +430,7 @@ FileHandle::CloseReq::CloseReq(Environment* env, Local obj, Local promise, Local ref) - : ReqWrap(env, obj, AsyncWrap::PROVIDER_FILEHANDLECLOSEREQ) { + : ReqWrap(env, obj, AsyncWrap::PROVIDER_FILEHANDLECLOSEREQ) { promise_.Reset(env->isolate(), promise); ref_.Reset(env->isolate(), ref); } @@ -438,8 +446,6 @@ void FileHandle::CloseReq::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("ref", ref_); } - - // Closes this FileHandle asynchronously and returns a Promise that will be // resolved when the callback is invoked, or rejects with a UVException if // there was a problem closing the fd. This is the preferred mechanism for @@ -467,8 +473,10 @@ MaybeLocal FileHandle::ClosePromise() { Local promise = resolver.As(); Local close_req_obj; - if (!env()->fdclose_constructor_template() - ->NewInstance(env()->context()).ToLocal(&close_req_obj)) { + if (!env() + ->fdclose_constructor_template() + ->NewInstance(env()->context()) + .ToLocal(&close_req_obj)) { return MaybeLocal(); } closing_ = true; @@ -511,6 +519,27 @@ void FileHandle::Close(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(ret); } +void FileHandle::CloseSync(const FunctionCallbackInfo& args) { + FileHandle* fd; + ASSIGN_OR_RETURN_UNWRAP(&fd, args.This()); + + // Already closed or closing - no-op. + if (fd->closed_ || fd->closing_) return; + + uv_fs_t req; + CHECK_NE(fd->fd_, -1); + FS_SYNC_TRACE_BEGIN(close); + int ret = uv_fs_close(fd->env()->event_loop(), &req, fd->fd_, nullptr); + FS_SYNC_TRACE_END(close); + uv_fs_req_cleanup(&req); + + fd->AfterClose(); + + if (ret < 0) { + Environment* env = fd->env(); + env->ThrowUVException(ret, "close"); + } +} void FileHandle::ReleaseFD(const FunctionCallbackInfo& args) { FileHandle* fd; @@ -529,8 +558,7 @@ void FileHandle::AfterClose() { closing_ = false; closed_ = true; fd_ = -1; - if (reading_ && !persistent().IsEmpty()) - EmitRead(UV_EOF); + if (reading_ && !persistent().IsEmpty()) EmitRead(UV_EOF); } void FileHandleReadWrap::MemoryInfo(MemoryTracker* tracker) const { @@ -539,17 +567,15 @@ void FileHandleReadWrap::MemoryInfo(MemoryTracker* tracker) const { } FileHandleReadWrap::FileHandleReadWrap(FileHandle* handle, Local obj) - : ReqWrap(handle->env(), obj, AsyncWrap::PROVIDER_FSREQCALLBACK), - file_handle_(handle) {} + : ReqWrap(handle->env(), obj, AsyncWrap::PROVIDER_FSREQCALLBACK), + file_handle_(handle) {} int FileHandle::ReadStart() { - if (!IsAlive() || IsClosing()) - return UV_EOF; + if (!IsAlive() || IsClosing()) return UV_EOF; reading_ = true; - if (current_read_) - return 0; + if (current_read_) return 0; BaseObjectPtr read_wrap; @@ -595,67 +621,65 @@ int FileHandle::ReadStart() { current_read_ = std::move(read_wrap); FS_ASYNC_TRACE_BEGIN0(UV_FS_READ, current_read_.get()) - current_read_->Dispatch(uv_fs_read, - fd_, - ¤t_read_->buffer_, - 1, - read_offset_, - uv_fs_callback_t{[](uv_fs_t* req) { - FileHandle* handle; - { - FileHandleReadWrap* req_wrap = FileHandleReadWrap::from_req(req); - FS_ASYNC_TRACE_END1( - req->fs_type, req_wrap, "result", static_cast(req->result)) - handle = req_wrap->file_handle_; - CHECK_EQ(handle->current_read_.get(), req_wrap); - } - - // ReadStart() checks whether current_read_ is set to determine whether - // a read is in progress. Moving it into a local variable makes sure that - // the ReadStart() call below doesn't think we're still actively reading. - BaseObjectPtr read_wrap = - std::move(handle->current_read_); - - ssize_t result = req->result; - uv_buf_t buffer = read_wrap->buffer_; - - uv_fs_req_cleanup(req); + current_read_->Dispatch( + uv_fs_read, + fd_, + ¤t_read_->buffer_, + 1, + read_offset_, + uv_fs_callback_t{[](uv_fs_t* req) { + FileHandle* handle; + { + FileHandleReadWrap* req_wrap = FileHandleReadWrap::from_req(req); + FS_ASYNC_TRACE_END1( + req->fs_type, req_wrap, "result", static_cast(req->result)) + handle = req_wrap->file_handle_; + CHECK_EQ(handle->current_read_.get(), req_wrap); + } - // Push the read wrap back to the freelist, or let it be destroyed - // once we’re exiting the current scope. - constexpr size_t kWantedFreelistFill = 100; - auto& freelist = handle->binding_data_->file_handle_read_wrap_freelist; - if (freelist.size() < kWantedFreelistFill) { - read_wrap->Reset(); - freelist.emplace_back(std::move(read_wrap)); - } + // ReadStart() checks whether current_read_ is set to determine whether + // a read is in progress. Moving it into a local variable makes sure + // that the ReadStart() call below doesn't think we're still actively + // reading. + BaseObjectPtr read_wrap = + std::move(handle->current_read_); + + ssize_t result = req->result; + uv_buf_t buffer = read_wrap->buffer_; + + uv_fs_req_cleanup(req); + + // Push the read wrap back to the freelist, or let it be destroyed + // once we’re exiting the current scope. + constexpr size_t kWantedFreelistFill = 100; + auto& freelist = handle->binding_data_->file_handle_read_wrap_freelist; + if (freelist.size() < kWantedFreelistFill) { + read_wrap->Reset(); + freelist.emplace_back(std::move(read_wrap)); + } - if (result >= 0) { - // Read at most as many bytes as we originally planned to. - if (handle->read_length_ >= 0 && handle->read_length_ < result) - result = handle->read_length_; + if (result >= 0) { + // Read at most as many bytes as we originally planned to. + if (handle->read_length_ >= 0 && handle->read_length_ < result) + result = handle->read_length_; - // If we read data and we have an expected length, decrease it by - // how much we have read. - if (handle->read_length_ >= 0) - handle->read_length_ -= result; + // If we read data and we have an expected length, decrease it by + // how much we have read. + if (handle->read_length_ >= 0) handle->read_length_ -= result; - // If we have an offset, increase it by how much we have read. - if (handle->read_offset_ >= 0) - handle->read_offset_ += result; - } + // If we have an offset, increase it by how much we have read. + if (handle->read_offset_ >= 0) handle->read_offset_ += result; + } - // Reading 0 bytes from a file always means EOF, or that we reached - // the end of the requested range. - if (result == 0) - result = UV_EOF; + // Reading 0 bytes from a file always means EOF, or that we reached + // the end of the requested range. + if (result == 0) result = UV_EOF; - handle->EmitRead(result, buffer); + handle->EmitRead(result, buffer); - // Start over, if EmitRead() didn’t tell us to stop. - if (handle->reading_) - handle->ReadStart(); - }}); + // Start over, if EmitRead() didn’t tell us to stop. + if (handle->reading_) handle->ReadStart(); + }}); return 0; } @@ -680,23 +704,23 @@ int FileHandle::DoShutdown(ShutdownWrap* req_wrap) { closing_ = true; CHECK_NE(fd_, -1); FS_ASYNC_TRACE_BEGIN0(UV_FS_CLOSE, wrap) - wrap->Dispatch(uv_fs_close, fd_, uv_fs_callback_t{[](uv_fs_t* req) { - FileHandleCloseWrap* wrap = static_cast( - FileHandleCloseWrap::from_req(req)); - FS_ASYNC_TRACE_END1( - req->fs_type, wrap, "result", static_cast(req->result)) - FileHandle* handle = static_cast(wrap->stream()); - handle->AfterClose(); - - int result = static_cast(req->result); - uv_fs_req_cleanup(req); - wrap->Done(result); - }}); + wrap->Dispatch( + uv_fs_close, fd_, uv_fs_callback_t{[](uv_fs_t* req) { + FileHandleCloseWrap* wrap = static_cast( + FileHandleCloseWrap::from_req(req)); + FS_ASYNC_TRACE_END1( + req->fs_type, wrap, "result", static_cast(req->result)) + FileHandle* handle = static_cast(wrap->stream()); + handle->AfterClose(); + + int result = static_cast(req->result); + uv_fs_req_cleanup(req); + wrap->Done(result); + }}); return 0; } - void FSReqCallback::Reject(Local reject) { MakeCallback(env()->oncomplete_string(), 1, &reject); } @@ -710,10 +734,7 @@ void FSReqCallback::ResolveStatFs(const uv_statfs_t* stat) { } void FSReqCallback::Resolve(Local value) { - Local argv[2] { - Null(env()->isolate()), - value - }; + Local argv[2]{Null(env()->isolate()), value}; MakeCallback(env()->oncomplete_string(), value->IsUndefined() ? 1 : arraysize(argv), argv); @@ -759,7 +780,7 @@ void FSReqAfterScope::Clear() { // which is also why the errors should have been constructed // in JS for more flexibility. void FSReqAfterScope::Reject(uv_fs_t* req) { - BaseObjectPtr wrap { wrap_ }; + BaseObjectPtr wrap{wrap_}; Local exception = UVException(wrap_->env()->isolate(), static_cast(req->result), wrap_->syscall(), @@ -787,8 +808,7 @@ void AfterNoArgs(uv_fs_t* req) { FSReqAfterScope after(req_wrap, req); FS_ASYNC_TRACE_END1( req->fs_type, req_wrap, "result", static_cast(req->result)) - if (after.Proceed()) - req_wrap->Resolve(Undefined(req_wrap->env()->isolate())); + if (after.Proceed()) req_wrap->Resolve(Undefined(req_wrap->env()->isolate())); } void AfterStat(uv_fs_t* req) { @@ -846,8 +866,8 @@ void AfterOpenFileHandle(uv_fs_t* req) { FS_ASYNC_TRACE_END1( req->fs_type, req_wrap, "result", static_cast(req->result)) if (after.Proceed()) { - FileHandle* fd = FileHandle::New(req_wrap->binding_data(), - static_cast(req->result)); + FileHandle* fd = FileHandle::New( + req_wrap->binding_data(), static_cast(req->result), {}, req->path); if (fd == nullptr) return; req_wrap->Resolve(fd->object()); } @@ -940,8 +960,7 @@ void AfterScanDir(uv_fs_t* req) { uv_dirent_t ent; r = uv_fs_scandir_next(req, &ent); - if (r == UV_EOF) - break; + if (r == UV_EOF) break; if (r != 0) { return req_wrap->Reject( UVException(isolate, r, nullptr, req_wrap->syscall(), req->path)); @@ -996,8 +1015,15 @@ void Access(const FunctionCallbackInfo& args) { path.ToStringView()); FS_ASYNC_TRACE_BEGIN1( UV_FS_ACCESS, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "access", UTF8, AfterNoArgs, - uv_fs_access, *path, mode); + AsyncCall(env, + req_wrap_async, + args, + "access", + UTF8, + AfterNoArgs, + uv_fs_access, + *path, + mode); } else { // access(path, mode) THROW_IF_INSUFFICIENT_PERMISSIONS( env, permission::PermissionScope::kFileSystemRead, path.ToStringView()); @@ -1024,8 +1050,8 @@ void Close(const FunctionCallbackInfo& args) { FSReqBase* req_wrap_async = GetReqWrap(args, 1); CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN0(UV_FS_CLOSE, req_wrap_async) - AsyncCall(env, req_wrap_async, args, "close", UTF8, AfterNoArgs, - uv_fs_close, fd); + AsyncCall( + env, req_wrap_async, args, "close", UTF8, AfterNoArgs, uv_fs_close, fd); } else { // close(fd) FSReqWrapSync req_wrap_sync("close"); FS_SYNC_TRACE_BEGIN(close); @@ -1163,7 +1189,9 @@ static void Stat(const FunctionCallbackInfo& args) { if (is_uv_error(result)) { return; } - Local arr = FillGlobalStatsArray(binding_data, use_bigint, + Local arr = FillGlobalStatsArray( + binding_data, + use_bigint, static_cast(req_wrap_sync.req.ptr)); args.GetReturnValue().Set(arr); } @@ -1187,8 +1215,14 @@ static void LStat(const FunctionCallbackInfo& args) { CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN1( UV_FS_LSTAT, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "lstat", UTF8, AfterStat, - uv_fs_lstat, *path); + AsyncCall(env, + req_wrap_async, + args, + "lstat", + UTF8, + AfterStat, + uv_fs_lstat, + *path); } else { // lstat(path, use_bigint, undefined, throw_if_no_entry) bool do_not_throw_if_no_entry = args[3]->IsFalse(); FSReqWrapSync req_wrap_sync("lstat", *path); @@ -1205,7 +1239,9 @@ static void LStat(const FunctionCallbackInfo& args) { return; } - Local arr = FillGlobalStatsArray(binding_data, use_bigint, + Local arr = FillGlobalStatsArray( + binding_data, + use_bigint, static_cast(req_wrap_sync.req.ptr)); args.GetReturnValue().Set(arr); } @@ -1229,8 +1265,8 @@ static void FStat(const FunctionCallbackInfo& args) { FSReqBase* req_wrap_async = GetReqWrap(args, 2, use_bigint); CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN0(UV_FS_FSTAT, req_wrap_async) - AsyncCall(env, req_wrap_async, args, "fstat", UTF8, AfterStat, - uv_fs_fstat, fd); + AsyncCall( + env, req_wrap_async, args, "fstat", UTF8, AfterStat, uv_fs_fstat, fd); } else { // fstat(fd, use_bigint, undefined, do_not_throw_error) bool do_not_throw_error = args[2]->IsTrue(); const auto should_throw = [do_not_throw_error](int result) { @@ -1245,7 +1281,9 @@ static void FStat(const FunctionCallbackInfo& args) { return; } - Local arr = FillGlobalStatsArray(binding_data, use_bigint, + Local arr = FillGlobalStatsArray( + binding_data, + use_bigint, static_cast(req_wrap_sync.req.ptr)); args.GetReturnValue().Set(arr); } @@ -1336,8 +1374,18 @@ static void Symlink(const FunctionCallbackInfo& args) { TRACE_STR_COPY(*target), "path", TRACE_STR_COPY(*path)) - AsyncDestCall(env, req_wrap_async, args, "symlink", *path, path.length(), - UTF8, AfterNoArgs, uv_fs_symlink, *target, *path, flags); + AsyncDestCall(env, + req_wrap_async, + args, + "symlink", + *path, + path.length(), + UTF8, + AfterNoArgs, + uv_fs_symlink, + *target, + *path, + flags); } else { // symlink(target, path, flags, undefined, ctx) FSReqWrapSync req_wrap_sync("symlink", *target, *path); FS_SYNC_TRACE_BEGIN(symlink); @@ -1392,8 +1440,17 @@ static void Link(const FunctionCallbackInfo& args) { TRACE_STR_COPY(*src), "dest", TRACE_STR_COPY(*dest)) - AsyncDestCall(env, req_wrap_async, args, "link", *dest, dest.length(), UTF8, - AfterNoArgs, uv_fs_link, *src, *dest); + AsyncDestCall(env, + req_wrap_async, + args, + "link", + *dest, + dest.length(), + UTF8, + AfterNoArgs, + uv_fs_link, + *src, + *dest); } else { // link(src, dest) // To avoid bypass the link target should be allowed to read and write THROW_IF_INSUFFICIENT_PERMISSIONS( @@ -1430,8 +1487,14 @@ static void ReadLink(const FunctionCallbackInfo& args) { CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN1( UV_FS_READLINK, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "readlink", encoding, AfterStringPtr, - uv_fs_readlink, *path); + AsyncCall(env, + req_wrap_async, + args, + "readlink", + encoding, + AfterStringPtr, + uv_fs_readlink, + *path); } else { // readlink(path, encoding) FSReqWrapSync req_wrap_sync("readlink", *path); FS_SYNC_TRACE_BEGIN(readlink); @@ -1490,9 +1553,17 @@ static void Rename(const FunctionCallbackInfo& args) { TRACE_STR_COPY(*old_path), "new_path", TRACE_STR_COPY(*new_path)) - AsyncDestCall(env, req_wrap_async, args, "rename", *new_path, - new_path.length(), UTF8, AfterNoArgs, uv_fs_rename, - *old_path, *new_path); + AsyncDestCall(env, + req_wrap_async, + args, + "rename", + *new_path, + new_path.length(), + UTF8, + AfterNoArgs, + uv_fs_rename, + *old_path, + *new_path); } else { // rename(old_path, new_path) THROW_IF_INSUFFICIENT_PERMISSIONS( env, permission::PermissionScope::kFileSystemRead, view_old_path); @@ -1528,8 +1599,15 @@ static void FTruncate(const FunctionCallbackInfo& args) { FSReqBase* req_wrap_async = GetReqWrap(args, 2); CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN0(UV_FS_FTRUNCATE, req_wrap_async) - AsyncCall(env, req_wrap_async, args, "ftruncate", UTF8, AfterNoArgs, - uv_fs_ftruncate, fd, len); + AsyncCall(env, + req_wrap_async, + args, + "ftruncate", + UTF8, + AfterNoArgs, + uv_fs_ftruncate, + fd, + len); } else { // ftruncate(fd, len) FSReqWrapSync req_wrap_sync("ftruncate"); FS_SYNC_TRACE_BEGIN(ftruncate); @@ -1553,8 +1631,14 @@ static void Fdatasync(const FunctionCallbackInfo& args) { FSReqBase* req_wrap_async = GetReqWrap(args, 1); CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN0(UV_FS_FDATASYNC, req_wrap_async) - AsyncCall(env, req_wrap_async, args, "fdatasync", UTF8, AfterNoArgs, - uv_fs_fdatasync, fd); + AsyncCall(env, + req_wrap_async, + args, + "fdatasync", + UTF8, + AfterNoArgs, + uv_fs_fdatasync, + fd); } else { // fdatasync(fd) FSReqWrapSync req_wrap_sync("fdatasync"); FS_SYNC_TRACE_BEGIN(fdatasync); @@ -1578,8 +1662,8 @@ static void Fsync(const FunctionCallbackInfo& args) { FSReqBase* req_wrap_async = GetReqWrap(args, 1); CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN0(UV_FS_FSYNC, req_wrap_async) - AsyncCall(env, req_wrap_async, args, "fsync", UTF8, AfterNoArgs, - uv_fs_fsync, fd); + AsyncCall( + env, req_wrap_async, args, "fsync", UTF8, AfterNoArgs, uv_fs_fsync, fd); } else { FSReqWrapSync req_wrap_sync("fsync"); FS_SYNC_TRACE_BEGIN(fsync); @@ -1608,8 +1692,14 @@ static void Unlink(const FunctionCallbackInfo& args) { path.ToStringView()); FS_ASYNC_TRACE_BEGIN1( UV_FS_UNLINK, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "unlink", UTF8, AfterNoArgs, - uv_fs_unlink, *path); + AsyncCall(env, + req_wrap_async, + args, + "unlink", + UTF8, + AfterNoArgs, + uv_fs_unlink, + *path); } else { // unlink(path) THROW_IF_INSUFFICIENT_PERMISSIONS( env, @@ -1639,8 +1729,14 @@ static void RMDir(const FunctionCallbackInfo& args) { CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN1( UV_FS_RMDIR, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "rmdir", UTF8, AfterNoArgs, - uv_fs_rmdir, *path); + AsyncCall(env, + req_wrap_async, + args, + "rmdir", + UTF8, + AfterNoArgs, + uv_fs_rmdir, + *path); } else { // rmdir(path) FSReqWrapSync req_wrap_sync("rmdir", *path); FS_SYNC_TRACE_BEGIN(rmdir); @@ -1808,7 +1904,7 @@ int MKDirpSync(uv_loop_t* loop, if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) { uv_fs_req_cleanup(req); if (orig_err == UV_EEXIST && - req_wrap->continuation_data()->paths().size() > 0) { + req_wrap->continuation_data()->paths().size() > 0) { return UV_ENOTDIR; } return UV_EEXIST; @@ -1824,11 +1920,8 @@ int MKDirpSync(uv_loop_t* loop, return 0; } -int MKDirpAsync(uv_loop_t* loop, - uv_fs_t* req, - const char* path, - int mode, - uv_fs_cb cb) { +int MKDirpAsync( + uv_loop_t* loop, uv_fs_t* req, const char* path, int mode, uv_fs_cb cb) { FSReqBase* req_wrap = FSReqBase::from_req(req); // on the first iteration of algorithm, stash state information. if (req_wrap->continuation_data() == nullptr) { @@ -1839,83 +1932,93 @@ int MKDirpAsync(uv_loop_t* loop, // on each iteration of algorithm, mkdir directory on top of stack. std::string next_path = req_wrap->continuation_data()->PopPath(); - int err = uv_fs_mkdir(loop, req, next_path.c_str(), mode, - uv_fs_callback_t{[](uv_fs_t* req) { - FSReqBase* req_wrap = FSReqBase::from_req(req); - Environment* env = req_wrap->env(); - uv_loop_t* loop = env->event_loop(); - std::string path = req->path; - int err = static_cast(req->result); - - while (true) { - switch (err) { - // Note: uv_fs_req_cleanup in terminal paths will be called by - // FSReqAfterScope::~FSReqAfterScope() - case 0: { - if (req_wrap->continuation_data()->paths().empty()) { - req_wrap->continuation_data()->MaybeSetFirstPath(path); - req_wrap->continuation_data()->Done(0); - } else { - req_wrap->continuation_data()->MaybeSetFirstPath(path); - uv_fs_req_cleanup(req); - MKDirpAsync(loop, req, path.c_str(), - req_wrap->continuation_data()->mode(), nullptr); - } - break; - } - case UV_EACCES: - case UV_ENOSPC: - case UV_ENOTDIR: - case UV_EPERM: { - req_wrap->continuation_data()->Done(err); - break; - } - case UV_ENOENT: { - std::string dirname = - path.substr(0, path.find_last_of(kPathSeparator)); - if (dirname != path) { - req_wrap->continuation_data()->PushPath(path); - req_wrap->continuation_data()->PushPath(std::move(dirname)); - } else if (req_wrap->continuation_data()->paths().empty()) { - err = UV_EEXIST; - continue; - } - uv_fs_req_cleanup(req); - MKDirpAsync(loop, req, path.c_str(), - req_wrap->continuation_data()->mode(), nullptr); - break; - } - default: - uv_fs_req_cleanup(req); - // Stash err for use in the callback. - req->data = reinterpret_cast(static_cast(err)); - int err = uv_fs_stat(loop, req, path.c_str(), - uv_fs_callback_t{[](uv_fs_t* req) { - FSReqBase* req_wrap = FSReqBase::from_req(req); - int err = static_cast(req->result); - if (reinterpret_cast(req->data) == UV_EEXIST && - req_wrap->continuation_data()->paths().size() > 0) { - if (err == 0 && S_ISDIR(req->statbuf.st_mode)) { - Environment* env = req_wrap->env(); - uv_loop_t* loop = env->event_loop(); - std::string path = req->path; + int err = uv_fs_mkdir( + loop, req, next_path.c_str(), mode, uv_fs_callback_t{[](uv_fs_t* req) { + FSReqBase* req_wrap = FSReqBase::from_req(req); + Environment* env = req_wrap->env(); + uv_loop_t* loop = env->event_loop(); + std::string path = req->path; + int err = static_cast(req->result); + + while (true) { + switch (err) { + // Note: uv_fs_req_cleanup in terminal paths will be called by + // FSReqAfterScope::~FSReqAfterScope() + case 0: { + if (req_wrap->continuation_data()->paths().empty()) { + req_wrap->continuation_data()->MaybeSetFirstPath(path); + req_wrap->continuation_data()->Done(0); + } else { + req_wrap->continuation_data()->MaybeSetFirstPath(path); uv_fs_req_cleanup(req); - MKDirpAsync(loop, req, path.c_str(), - req_wrap->continuation_data()->mode(), nullptr); - return; + MKDirpAsync(loop, + req, + path.c_str(), + req_wrap->continuation_data()->mode(), + nullptr); } - err = UV_ENOTDIR; + break; + } + case UV_EACCES: + case UV_ENOSPC: + case UV_ENOTDIR: + case UV_EPERM: { + req_wrap->continuation_data()->Done(err); + break; } - // verify that the path pointed to is actually a directory. - if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) err = UV_EEXIST; - req_wrap->continuation_data()->Done(err); - }}); - if (err < 0) req_wrap->continuation_data()->Done(err); + case UV_ENOENT: { + std::string dirname = + path.substr(0, path.find_last_of(kPathSeparator)); + if (dirname != path) { + req_wrap->continuation_data()->PushPath(path); + req_wrap->continuation_data()->PushPath(std::move(dirname)); + } else if (req_wrap->continuation_data()->paths().empty()) { + err = UV_EEXIST; + continue; + } + uv_fs_req_cleanup(req); + MKDirpAsync(loop, + req, + path.c_str(), + req_wrap->continuation_data()->mode(), + nullptr); + break; + } + default: + uv_fs_req_cleanup(req); + // Stash err for use in the callback. + req->data = reinterpret_cast(static_cast(err)); + int err = uv_fs_stat( + loop, req, path.c_str(), uv_fs_callback_t{[](uv_fs_t* req) { + FSReqBase* req_wrap = FSReqBase::from_req(req); + int err = static_cast(req->result); + if (reinterpret_cast(req->data) == UV_EEXIST && + req_wrap->continuation_data()->paths().size() > 0) { + if (err == 0 && S_ISDIR(req->statbuf.st_mode)) { + Environment* env = req_wrap->env(); + uv_loop_t* loop = env->event_loop(); + std::string path = req->path; + uv_fs_req_cleanup(req); + MKDirpAsync(loop, + req, + path.c_str(), + req_wrap->continuation_data()->mode(), + nullptr); + return; + } + err = UV_ENOTDIR; + } + // verify that the path pointed to is actually a directory. + if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) + err = UV_EEXIST; + req_wrap->continuation_data()->Done(err); + }}); + if (err < 0) req_wrap->continuation_data()->Done(err); + break; + } break; - } - break; - } - }}); + } + }}); return err; } @@ -1944,9 +2047,15 @@ static void MKDir(const FunctionCallbackInfo& args) { CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN1( UV_FS_UNLINK, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "mkdir", UTF8, + AsyncCall(env, + req_wrap_async, + args, + "mkdir", + UTF8, mkdirp ? AfterMkdirp : AfterNoArgs, - mkdirp ? MKDirpAsync : uv_fs_mkdir, *path, mode); + mkdirp ? MKDirpAsync : uv_fs_mkdir, + *path, + mode); } else { // mkdir(path, mode, recursive) FSReqWrapSync req_wrap_sync("mkdir", *path); FS_SYNC_TRACE_BEGIN(mkdir); @@ -1996,8 +2105,14 @@ static void RealPath(const FunctionCallbackInfo& args) { path.ToStringView()); FS_ASYNC_TRACE_BEGIN1( UV_FS_REALPATH, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "realpath", encoding, AfterStringPtr, - uv_fs_realpath, *path); + AsyncCall(env, + req_wrap_async, + args, + "realpath", + encoding, + AfterStringPtr, + uv_fs_realpath, + *path); } else { // realpath(path, encoding, undefined, ctx) THROW_IF_INSUFFICIENT_PERMISSIONS( env, permission::PermissionScope::kFileSystemRead, path.ToStringView()); @@ -2095,8 +2210,7 @@ static void ReadDir(const FunctionCallbackInfo& args) { uv_dirent_t ent; r = uv_fs_scandir_next(&(req_wrap_sync.req), &ent); - if (r == UV_EOF) - break; + if (r == UV_EOF) break; if (is_uv_error(r)) { env->ThrowUVException(r, "scandir", nullptr, *path); return; @@ -2114,13 +2228,10 @@ static void ReadDir(const FunctionCallbackInfo& args) { } } - Local names = Array::New(isolate, name_v.data(), name_v.size()); if (with_types) { Local result[] = { - names, - Array::New(isolate, type_v.data(), type_v.size()) - }; + names, Array::New(isolate, type_v.data(), type_v.size())}; args.GetReturnValue().Set(Array::New(isolate, result, arraysize(result))); } else { args.GetReturnValue().Set(names); @@ -2215,8 +2326,16 @@ static void Open(const FunctionCallbackInfo& args) { req_wrap_async->set_is_plain_open(true); FS_ASYNC_TRACE_BEGIN1( UV_FS_OPEN, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger, - uv_fs_open, *path, flags, mode); + AsyncCall(env, + req_wrap_async, + args, + "open", + UTF8, + AfterInteger, + uv_fs_open, + *path, + flags, + mode); } else { // open(path, flags, mode) if (CheckOpenPermissions(env, path, flags).IsNothing()) return; FSReqWrapSync req_wrap_sync("open", *path); @@ -2254,8 +2373,16 @@ static void OpenFileHandle(const FunctionCallbackInfo& args) { if (req_wrap_async != nullptr) { // openFileHandle(path, flags, mode, req) FS_ASYNC_TRACE_BEGIN1( UV_FS_OPEN, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterOpenFileHandle, - uv_fs_open, *path, flags, mode); + AsyncCall(env, + req_wrap_async, + args, + "open", + UTF8, + AfterOpenFileHandle, + uv_fs_open, + *path, + flags, + mode); } else { // openFileHandle(path, flags, mode, undefined, ctx) CHECK_EQ(argc, 5); FSReqWrapSync req_wrap_sync; @@ -2277,7 +2404,7 @@ static void OpenFileHandle(const FunctionCallbackInfo& args) { if (result < 0) { return; // syscall failed, no need to continue, error info is in ctx } - FileHandle* fd = FileHandle::New(binding_data, result); + FileHandle* fd = FileHandle::New(binding_data, result, {}, path.ToString()); if (fd == nullptr) return; args.GetReturnValue().Set(fd->object()); } @@ -2322,9 +2449,18 @@ static void CopyFile(const FunctionCallbackInfo& args) { TRACE_STR_COPY(*src), "dest", TRACE_STR_COPY(*dest)) - AsyncDestCall(env, req_wrap_async, args, "copyfile", - *dest, dest.length(), UTF8, AfterNoArgs, - uv_fs_copyfile, *src, *dest, flags); + AsyncDestCall(env, + req_wrap_async, + args, + "copyfile", + *dest, + dest.length(), + UTF8, + AfterNoArgs, + uv_fs_copyfile, + *src, + *dest, + flags); } else { // copyFile(src, dest, flags) THROW_IF_INSUFFICIENT_PERMISSIONS( env, permission::PermissionScope::kFileSystemRead, src.ToStringView()); @@ -2385,8 +2521,17 @@ static void WriteBuffer(const FunctionCallbackInfo& args) { FSReqBase* req_wrap_async = GetReqWrap(args, 5); if (req_wrap_async != nullptr) { // write(fd, buffer, off, len, pos, req) FS_ASYNC_TRACE_BEGIN0(UV_FS_WRITE, req_wrap_async) - AsyncCall(env, req_wrap_async, args, "write", UTF8, AfterInteger, - uv_fs_write, fd, &uvbuf, 1, pos); + AsyncCall(env, + req_wrap_async, + args, + "write", + UTF8, + AfterInteger, + uv_fs_write, + fd, + &uvbuf, + 1, + pos); } else { // write(fd, buffer, off, len, pos, undefined, ctx) CHECK_EQ(argc, 7); FSReqWrapSync req_wrap_sync; @@ -2410,7 +2555,6 @@ static void WriteBuffer(const FunctionCallbackInfo& args) { } } - // Wrapper for writev(2). // // bytesWritten = writev(fd, chunks, position, callback) @@ -2471,7 +2615,6 @@ static void WriteBuffers(const FunctionCallbackInfo& args) { } } - // Wrapper for write(2). // // bytesWritten = write(fd, string, position, enc, callback) @@ -2535,12 +2678,8 @@ static void WriteString(const FunctionCallbackInfo& args) { stack_buffer.SetLengthAndZeroTerminate(len); uv_buf_t uvbuf = uv_buf_init(*stack_buffer, len); FS_ASYNC_TRACE_BEGIN0(UV_FS_WRITE, req_wrap_async) - int err = req_wrap_async->Dispatch(uv_fs_write, - fd, - &uvbuf, - 1, - pos, - AfterInteger); + int err = + req_wrap_async->Dispatch(uv_fs_write, fd, &uvbuf, 1, pos, AfterInteger); if (err < 0) { uv_fs_t* uv_req = req_wrap_async->req(); uv_req->result = err; @@ -2552,13 +2691,11 @@ static void WriteString(const FunctionCallbackInfo& args) { CHECK_EQ(argc, 6); FSReqBase::FSReqBuffer stack_buffer; if (buf == nullptr) { - if (!StringBytes::StorageSize(isolate, value, enc).To(&len)) - return; + if (!StringBytes::StorageSize(isolate, value, enc).To(&len)) return; stack_buffer.AllocateSufficientStorage(len + 1); // StorageSize may return too large a char, so correct the actual length // by the write size - len = StringBytes::Write(isolate, *stack_buffer, - len, args[1], enc); + len = StringBytes::Write(isolate, *stack_buffer, len, args[1], enc); stack_buffer.SetLengthAndZeroTerminate(len); buf = *stack_buffer; } @@ -2702,9 +2839,8 @@ static void Read(const FunctionCallbackInfo& args) { CHECK(Buffer::IsWithinBounds(off, len, buffer_length)); CHECK(IsSafeJsInt(args[4]) || args[4]->IsBigInt()); - const int64_t pos = args[4]->IsNumber() ? - args[4].As()->Value() : - args[4].As()->Int64Value(); + const int64_t pos = args[4]->IsNumber() ? args[4].As()->Value() + : args[4].As()->Int64Value(); char* buf = buffer_data + off; uv_buf_t uvbuf = uv_buf_init(buf, len); @@ -2713,8 +2849,17 @@ static void Read(const FunctionCallbackInfo& args) { FSReqBase* req_wrap_async = GetReqWrap(args, 5); CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN0(UV_FS_READ, req_wrap_async) - AsyncCall(env, req_wrap_async, args, "read", UTF8, AfterInteger, - uv_fs_read, fd, &uvbuf, 1, pos); + AsyncCall(env, + req_wrap_async, + args, + "read", + UTF8, + AfterInteger, + uv_fs_read, + fd, + &uvbuf, + 1, + pos); } else { // read(fd, buffer, offset, len, pos) FSReqWrapSync req_wrap_sync("read"); FS_SYNC_TRACE_BEGIN(read); @@ -2838,8 +2983,17 @@ static void ReadBuffers(const FunctionCallbackInfo& args) { FSReqBase* req_wrap_async = GetReqWrap(args, 3); CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN0(UV_FS_READ, req_wrap_async) - AsyncCall(env, req_wrap_async, args, "read", UTF8, AfterInteger, - uv_fs_read, fd, *iovs, iovs.length(), pos); + AsyncCall(env, + req_wrap_async, + args, + "read", + UTF8, + AfterInteger, + uv_fs_read, + fd, + *iovs, + iovs.length(), + pos); } else { // readBuffers(fd, buffers, undefined, ctx) FSReqWrapSync req_wrap_sync("read"); FS_SYNC_TRACE_BEGIN(read); @@ -2853,7 +3007,6 @@ static void ReadBuffers(const FunctionCallbackInfo& args) { } } - /* fs.chmod(path, mode); * Wrapper for chmod(1) / EIO_CHMOD */ @@ -2877,8 +3030,15 @@ static void Chmod(const FunctionCallbackInfo& args) { CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN1( UV_FS_CHMOD, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "chmod", UTF8, AfterNoArgs, - uv_fs_chmod, *path, mode); + AsyncCall(env, + req_wrap_async, + args, + "chmod", + UTF8, + AfterNoArgs, + uv_fs_chmod, + *path, + mode); } else { // chmod(path, mode) FSReqWrapSync req_wrap_sync("chmod", *path); FS_SYNC_TRACE_BEGIN(chmod); @@ -2887,7 +3047,6 @@ static void Chmod(const FunctionCallbackInfo& args) { } } - /* fs.fchmod(fd, mode); * Wrapper for fchmod(1) / EIO_FCHMOD */ @@ -2909,8 +3068,15 @@ static void FChmod(const FunctionCallbackInfo& args) { FSReqBase* req_wrap_async = GetReqWrap(args, 2); CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN0(UV_FS_FCHMOD, req_wrap_async) - AsyncCall(env, req_wrap_async, args, "fchmod", UTF8, AfterNoArgs, - uv_fs_fchmod, fd, mode); + AsyncCall(env, + req_wrap_async, + args, + "fchmod", + UTF8, + AfterNoArgs, + uv_fs_fchmod, + fd, + mode); } else { // fchmod(fd, mode) FSReqWrapSync req_wrap_sync("fchmod"); FS_SYNC_TRACE_BEGIN(fchmod); @@ -2948,8 +3114,16 @@ static void Chown(const FunctionCallbackInfo& args) { path.ToStringView()); FS_ASYNC_TRACE_BEGIN1( UV_FS_CHOWN, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "chown", UTF8, AfterNoArgs, - uv_fs_chown, *path, uid, gid); + AsyncCall(env, + req_wrap_async, + args, + "chown", + UTF8, + AfterNoArgs, + uv_fs_chown, + *path, + uid, + gid); } else { // chown(path, uid, gid) THROW_IF_INSUFFICIENT_PERMISSIONS( env, @@ -2962,7 +3136,6 @@ static void Chown(const FunctionCallbackInfo& args) { } } - /* fs.fchown(fd, uid, gid); * Wrapper for fchown(1) / EIO_FCHOWN */ @@ -2987,8 +3160,16 @@ static void FChown(const FunctionCallbackInfo& args) { FSReqBase* req_wrap_async = GetReqWrap(args, 3); CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN0(UV_FS_FCHOWN, req_wrap_async) - AsyncCall(env, req_wrap_async, args, "fchown", UTF8, AfterNoArgs, - uv_fs_fchown, fd, uid, gid); + AsyncCall(env, + req_wrap_async, + args, + "fchown", + UTF8, + AfterNoArgs, + uv_fs_fchown, + fd, + uid, + gid); } else { // fchown(fd, uid, gid) FSReqWrapSync req_wrap_sync("fchown"); FS_SYNC_TRACE_BEGIN(fchown); @@ -2997,7 +3178,6 @@ static void FChown(const FunctionCallbackInfo& args) { } } - static void LChown(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -3024,8 +3204,16 @@ static void LChown(const FunctionCallbackInfo& args) { path.ToStringView()); FS_ASYNC_TRACE_BEGIN1( UV_FS_LCHOWN, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "lchown", UTF8, AfterNoArgs, - uv_fs_lchown, *path, uid, gid); + AsyncCall(env, + req_wrap_async, + args, + "lchown", + UTF8, + AfterNoArgs, + uv_fs_lchown, + *path, + uid, + gid); } else { // lchown(path, uid, gid) THROW_IF_INSUFFICIENT_PERMISSIONS( env, @@ -3038,7 +3226,6 @@ static void LChown(const FunctionCallbackInfo& args) { } } - static void UTimes(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -3062,8 +3249,16 @@ static void UTimes(const FunctionCallbackInfo& args) { CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN1( UV_FS_UTIME, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "utime", UTF8, AfterNoArgs, - uv_fs_utime, *path, atime, mtime); + AsyncCall(env, + req_wrap_async, + args, + "utime", + UTF8, + AfterNoArgs, + uv_fs_utime, + *path, + atime, + mtime); } else { // utimes(path, atime, mtime) FSReqWrapSync req_wrap_sync("utime", *path); FS_SYNC_TRACE_BEGIN(utimes); @@ -3094,8 +3289,16 @@ static void FUTimes(const FunctionCallbackInfo& args) { FSReqBase* req_wrap_async = GetReqWrap(args, 3); CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN0(UV_FS_FUTIME, req_wrap_async) - AsyncCall(env, req_wrap_async, args, "futime", UTF8, AfterNoArgs, - uv_fs_futime, fd, atime, mtime); + AsyncCall(env, + req_wrap_async, + args, + "futime", + UTF8, + AfterNoArgs, + uv_fs_futime, + fd, + atime, + mtime); } else { // futimes(fd, atime, mtime) FSReqWrapSync req_wrap_sync("futime"); FS_SYNC_TRACE_BEGIN(futimes); @@ -3128,8 +3331,16 @@ static void LUTimes(const FunctionCallbackInfo& args) { CHECK_NOT_NULL(req_wrap_async); FS_ASYNC_TRACE_BEGIN1( UV_FS_LUTIME, req_wrap_async, "path", TRACE_STR_COPY(*path)) - AsyncCall(env, req_wrap_async, args, "lutime", UTF8, AfterNoArgs, - uv_fs_lutime, *path, atime, mtime); + AsyncCall(env, + req_wrap_async, + args, + "lutime", + UTF8, + AfterNoArgs, + uv_fs_lutime, + *path, + atime, + mtime); } else { // lutimes(path, atime, mtime) FSReqWrapSync req_wrap_sync("lutime", *path); FS_SYNC_TRACE_BEGIN(lutimes); @@ -3166,8 +3377,14 @@ static void Mkdtemp(const FunctionCallbackInfo& args) { tmpl.ToStringView()); FS_ASYNC_TRACE_BEGIN1( UV_FS_MKDTEMP, req_wrap_async, "path", TRACE_STR_COPY(*tmpl)) - AsyncCall(env, req_wrap_async, args, "mkdtemp", encoding, AfterStringPath, - uv_fs_mkdtemp, *tmpl); + AsyncCall(env, + req_wrap_async, + args, + "mkdtemp", + encoding, + AfterStringPath, + uv_fs_mkdtemp, + *tmpl); } else { // mkdtemp(tmpl, encoding) THROW_IF_INSUFFICIENT_PERMISSIONS( env, @@ -4014,8 +4231,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, // Create Function Template for FSReqPromise Local fpt = FunctionTemplate::New(isolate); fpt->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data)); - Local promiseString = - FIXED_ONE_BYTE_STRING(isolate, "FSReqPromise"); + Local promiseString = FIXED_ONE_BYTE_STRING(isolate, "FSReqPromise"); fpt->SetClassName(promiseString); Local fpo = fpt->InstanceTemplate(); fpo->SetInternalFieldCount(FSReqBase::kInternalFieldCount); @@ -4025,6 +4241,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, Local fd = NewFunctionTemplate(isolate, FileHandle::New); fd->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data)); SetProtoMethod(isolate, fd, "close", FileHandle::Close); + SetProtoMethod(isolate, fd, "closeSync", FileHandle::CloseSync); SetProtoMethod(isolate, fd, "releaseFD", FileHandle::ReleaseFD); Local fdt = fd->InstanceTemplate(); fdt->SetInternalFieldCount(FileHandle::kInternalFieldCount); @@ -4034,8 +4251,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, // Create FunctionTemplate for FileHandle::CloseReq Local fdclose = FunctionTemplate::New(isolate); - fdclose->SetClassName(FIXED_ONE_BYTE_STRING(isolate, - "FileHandleCloseReq")); + fdclose->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "FileHandleCloseReq")); fdclose->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data)); Local fdcloset = fdclose->InstanceTemplate(); fdcloset->SetInternalFieldCount(FSReqBase::kInternalFieldCount); @@ -4113,6 +4329,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(FileHandle::New); registry->Register(FileHandle::Close); + registry->Register(FileHandle::CloseSync); registry->Register(FileHandle::ReleaseFD); StreamBase::RegisterExternalReferences(registry); } diff --git a/src/node_file.h b/src/node_file.h index c276a873f8516a..17f3b4203c8edd 100644 --- a/src/node_file.h +++ b/src/node_file.h @@ -81,8 +81,7 @@ class BindingData : public SnapshotableObject { AliasedFloat64Array statfs_field_array; AliasedBigInt64Array statfs_field_bigint_array; - std::vector> - file_handle_read_wrap_freelist; + std::vector> file_handle_read_wrap_freelist; SERIALIZABLE_OBJECT_METHODS() SET_BINDING_ID(fs_binding_data) @@ -147,7 +146,8 @@ class FSReqBase : public ReqWrap { const char* data, size_t len, enum encoding encoding); - inline FSReqBuffer& Init(const char* syscall, size_t len, + inline FSReqBuffer& Init(const char* syscall, + size_t len, enum encoding encoding); virtual void Reject(v8::Local reject) = 0; @@ -241,8 +241,7 @@ inline v8::Local FillGlobalStatFsArray(BindingData* binding_data, template class FSReqPromise final : public FSReqBase { public: - static inline FSReqPromise* New(BindingData* binding_data, - bool use_bigint); + static inline FSReqPromise* New(BindingData* binding_data, bool use_bigint); inline ~FSReqPromise() override; inline void Reject(v8::Local reject) override; @@ -331,6 +330,7 @@ class FileHandle final : public AsyncWrap, public StreamBase { static FileHandle* New(BindingData* binding_data, int fd, v8::Local obj = v8::Local(), + std::string original_name = {}, std::optional maybeOffset = std::nullopt, std::optional maybeLength = std::nullopt); ~FileHandle() override; @@ -338,6 +338,7 @@ class FileHandle final : public AsyncWrap, public StreamBase { static void New(const v8::FunctionCallbackInfo& args); int GetFD() override { return fd_; } + const std::string& original_name() const { return original_name_; } int Release(); @@ -345,6 +346,9 @@ class FileHandle final : public AsyncWrap, public StreamBase { // be resolved once closing is complete. static void Close(const v8::FunctionCallbackInfo& args); + // Synchronously closes the FD. Throws on error. + static void CloseSync(const v8::FunctionCallbackInfo& args); + // Releases ownership of the FD. static void ReleaseFD(const v8::FunctionCallbackInfo& args); @@ -397,7 +401,10 @@ class FileHandle final : public AsyncWrap, public StreamBase { int fd_; }; - FileHandle(BindingData* binding_data, v8::Local obj, int fd); + FileHandle(BindingData* binding_data, + v8::Local obj, + int fd, + std::string original_name); // Synchronous close that emits a warning void Close(); @@ -439,6 +446,7 @@ class FileHandle final : public AsyncWrap, public StreamBase { // Asynchronous close v8::MaybeLocal ClosePromise(); + std::string original_name_; int fd_; bool closing_ = false; bool closed_ = false; @@ -495,19 +503,27 @@ inline FSReqBase* GetReqWrap(const v8::FunctionCallbackInfo& args, // Returns nullptr if the operation fails from the start. template -inline FSReqBase* AsyncDestCall(Environment* env, FSReqBase* req_wrap, +inline FSReqBase* AsyncDestCall(Environment* env, + FSReqBase* req_wrap, const v8::FunctionCallbackInfo& args, - const char* syscall, const char* dest, - size_t len, enum encoding enc, uv_fs_cb after, - Func fn, Args... fn_args); + const char* syscall, + const char* dest, + size_t len, + enum encoding enc, + uv_fs_cb after, + Func fn, + Args... fn_args); // Returns nullptr if the operation fails from the start. template inline FSReqBase* AsyncCall(Environment* env, FSReqBase* req_wrap, const v8::FunctionCallbackInfo& args, - const char* syscall, enum encoding enc, - uv_fs_cb after, Func fn, Args... fn_args); + const char* syscall, + enum encoding enc, + uv_fs_cb after, + Func fn, + Args... fn_args); // Template counterpart of SYNC_CALL, except that it only puts // the error number and the syscall in the context instead of diff --git a/src/node_options.cc b/src/node_options.cc index a3660223bb9349..a2fd7a662cc57e 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -199,6 +199,13 @@ void EnvironmentOptions::CheckOptions(std::vector* errors, "used, not both"); } +#if HAVE_OPENSSL + if (use_system_ca && per_process::cli_options->use_openssl_ca) { + errors->push_back("either --use-openssl-ca or --use-system-ca can be " + "used, not both"); + } +#endif // HAVE_OPENSSL + if (heap_snapshot_near_heap_limit < 0) { errors->push_back("--heapsnapshot-near-heap-limit must not be negative"); } @@ -583,6 +590,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::experimental_sqlite, kAllowedInEnvvar, HAVE_SQLITE); + AddOption("--experimental-stream-iter", + "experimental iterable streams API (node:stream/iter)", + &EnvironmentOptions::experimental_stream_iter, + kAllowedInEnvvar); AddOption("--experimental-quic", #ifndef OPENSSL_NO_QUIC "experimental QUIC support", @@ -1052,6 +1063,13 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::trace_env_native_stack, kAllowedInEnvvar); +#if HAVE_OPENSSL + AddOption("--use-system-ca", + "use system's CA store", + &EnvironmentOptions::use_system_ca, + kAllowedInEnvvar); +#endif // HAVE_OPENSSL + AddOption( "--trace-require-module", "Print access to require(esm). Options are 'all' (print all usage) and " @@ -1394,10 +1412,6 @@ PerProcessOptionsParser::PerProcessOptionsParser( , &PerProcessOptions::use_openssl_ca, kAllowedInEnvvar); - AddOption("--use-system-ca", - "use system's CA store", - &PerProcessOptions::use_system_ca, - kAllowedInEnvvar); AddOption("--use-bundled-ca", "use bundled CA store" #if !defined(NODE_OPENSSL_CERT_STORE) @@ -2160,6 +2174,10 @@ void HandleEnvOptions(std::shared_ptr env_options, env_options->use_env_proxy = opt_getter("NODE_USE_ENV_PROXY") == "1"; +#if HAVE_OPENSSL + env_options->use_system_ca = opt_getter("NODE_USE_SYSTEM_CA") == "1"; +#endif // HAVE_OPENSSL + if (env_options->redirect_warnings.empty()) env_options->redirect_warnings = opt_getter("NODE_REDIRECT_WARNINGS"); } diff --git a/src/node_options.h b/src/node_options.h index b1b5a1f6de78d8..7e880fc24abb74 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -130,6 +130,7 @@ class EnvironmentOptions : public Options { bool experimental_eventsource = EXPERIMENTALS_DEFAULT_VALUE; bool experimental_websocket = true; bool experimental_sqlite = HAVE_SQLITE; + bool experimental_stream_iter = false; bool experimental_webstorage = false; #ifndef OPENSSL_NO_QUIC bool experimental_quic = EXPERIMENTALS_DEFAULT_VALUE; @@ -229,6 +230,7 @@ class EnvironmentOptions : public Options { bool trace_env = false; bool trace_env_js_stack = false; bool trace_env_native_stack = false; + bool use_system_ca = false; std::string trace_require_module; bool extra_info_on_fatal_exception = true; std::string unhandled_rejections; @@ -291,7 +293,7 @@ class PerIsolateOptions : public Options { PerIsolateOptions() = default; PerIsolateOptions(PerIsolateOptions&&) = default; - std::shared_ptr per_env { new EnvironmentOptions() }; + std::shared_ptr per_env{new EnvironmentOptions()}; bool track_heap_objects = false; bool report_uncaught_exception = false; bool report_on_signal = false; @@ -324,7 +326,7 @@ class PerProcessOptions : public Options { // using the node::per_process::cli_options_mutex, typically: // // Mutex::ScopedLock lock(node::per_process::cli_options_mutex); - std::shared_ptr per_isolate { new PerIsolateOptions() }; + std::shared_ptr per_isolate{new PerIsolateOptions()}; std::string title; std::string trace_event_categories; @@ -364,7 +366,6 @@ class PerProcessOptions : public Options { bool ssl_openssl_cert_store = false; #endif bool use_openssl_ca = false; - bool use_system_ca = false; bool use_bundled_ca = false; bool enable_fips_crypto = false; bool force_fips_crypto = false; @@ -534,8 +535,7 @@ class OptionsParser { // if the option has a non-option argument (not starting with -) following it. void AddAlias(const char* from, const char* to); void AddAlias(const char* from, const std::vector& to); - void AddAlias(const char* from, - const std::initializer_list& to); + void AddAlias(const char* from, const std::initializer_list& to); // Add implications from some arbitrary option to a boolean one, either // in a way that makes `from` set `to` to true or to false. @@ -547,7 +547,7 @@ class OptionsParser { // type. template void Insert(const OptionsParser& child_options_parser, - ChildOptions* (Options::* get_child)()); + ChildOptions* (Options::*get_child)()); // Parse a sequence of options into an options struct, a list of // arguments that were parsed as options, a list of unknown/JS engine options, @@ -636,17 +636,15 @@ class OptionsParser { // These are helpers that make `Insert()` support properties of other // options structs, if we know how to access them. template - static auto Convert( - std::shared_ptr original, - ChildOptions* (Options::* get_child)()); + static auto Convert(std::shared_ptr original, + ChildOptions* (Options::*get_child)()); template - static auto Convert( - typename OptionsParser::OptionInfo original, - ChildOptions* (Options::* get_child)()); + static auto Convert(typename OptionsParser::OptionInfo original, + ChildOptions* (Options::*get_child)()); template static auto Convert( typename OptionsParser::Implication original, - ChildOptions* (Options::* get_child)()); + ChildOptions* (Options::*get_child)()); std::unordered_map options_; std::unordered_map> aliases_; @@ -673,10 +671,12 @@ class OptionsParser { using StringVector = std::vector; template -void Parse( - StringVector* const args, StringVector* const exec_args, - StringVector* const v8_args, OptionsType* const options, - OptionEnvvarSettings required_env_settings, StringVector* const errors); +void Parse(StringVector* const args, + StringVector* const exec_args, + StringVector* const v8_args, + OptionsType* const options, + OptionEnvvarSettings required_env_settings, + StringVector* const errors); } // namespace options_parser diff --git a/src/node_perf_common.h b/src/node_perf_common.h index ad09658e13ec79..01e7f35241ac12 100644 --- a/src/node_perf_common.h +++ b/src/node_perf_common.h @@ -34,12 +34,13 @@ extern uint64_t performance_v8_start; V(LOOP_EXIT, "loopExit") \ V(BOOTSTRAP_COMPLETE, "bootstrapComplete") -#define NODE_PERFORMANCE_ENTRY_TYPES(V) \ - V(GC, "gc") \ - V(HTTP, "http") \ - V(HTTP2, "http2") \ - V(NET, "net") \ - V(DNS, "dns") +#define NODE_PERFORMANCE_ENTRY_TYPES(V) \ + V(GC, "gc") \ + V(HTTP, "http") \ + V(HTTP2, "http2") \ + V(NET, "net") \ + V(DNS, "dns") \ + V(QUIC, "quic") enum PerformanceMilestone { #define V(name, _) NODE_PERFORMANCE_MILESTONE_##name, diff --git a/src/node_sockaddr-inl.h b/src/node_sockaddr-inl.h index bc055da535c2d4..b02999d047800f 100644 --- a/src/node_sockaddr-inl.h +++ b/src/node_sockaddr-inl.h @@ -186,10 +186,10 @@ typename T::Type* SocketAddressLRU::Peek( } template -void SocketAddressLRU::CheckExpired() { +void SocketAddressLRU::CheckExpired(uint64_t now) { auto it = list_.rbegin(); while (it != list_.rend()) { - if (T::CheckExpired(it->first, it->second)) { + if (T::CheckExpired(it->first, it->second, now)) { map_.erase(it->first); list_.pop_back(); it = list_.rbegin(); @@ -211,21 +211,20 @@ void SocketAddressLRU::MemoryInfo(MemoryTracker* tracker) const { // cache and adjust if necessary. Whether the item exists or not, // purge expired items. template -typename T::Type* SocketAddressLRU::Upsert( - const SocketAddress& address) { - - auto on_exit = OnScopeLeave([&]() { CheckExpired(); }); +typename T::Type* SocketAddressLRU::Upsert(const SocketAddress& address, + uint64_t now) { + auto on_exit = OnScopeLeave([&]() { CheckExpired(now); }); auto it = map_.find(address); if (it != std::end(map_)) { list_.splice(list_.begin(), list_, it->second); - T::Touch(it->first, &it->second->second); + T::Touch(it->first, &it->second->second, now); return &it->second->second; } list_.push_front(Pair(address, { })); map_[address] = list_.begin(); - T::Touch(list_.begin()->first, &list_.begin()->second); + T::Touch(list_.begin()->first, &list_.begin()->second, now); // Drop the last item in the list if we are // over the size limit... diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index c869d423b254cc..9348f0ac8e4dfc 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -40,41 +40,31 @@ SocketAddress FromUVHandle(F fn, const T& handle) { } } // namespace -bool SocketAddress::ToSockAddr( - int32_t family, - const char* host, - uint32_t port, - sockaddr_storage* addr) { +bool SocketAddress::ToSockAddr(int32_t family, + const char* host, + uint32_t port, + sockaddr_storage* addr) { switch (family) { case AF_INET: - return uv_ip4_addr( - host, - port, - reinterpret_cast(addr)) == 0; + return uv_ip4_addr(host, port, reinterpret_cast(addr)) == 0; case AF_INET6: - return uv_ip6_addr( - host, - port, - reinterpret_cast(addr)) == 0; + return uv_ip6_addr(host, port, reinterpret_cast(addr)) == + 0; default: UNREACHABLE(); } } -bool SocketAddress::New( - const char* host, - uint32_t port, - SocketAddress* addr) { +bool SocketAddress::New(const char* host, uint32_t port, SocketAddress* addr) { return New(AF_INET, host, port, addr) || New(AF_INET6, host, port, addr); } -bool SocketAddress::New( - int32_t family, - const char* host, - uint32_t port, - SocketAddress* addr) { - return ToSockAddr(family, host, port, - reinterpret_cast(addr->storage())); +bool SocketAddress::New(int32_t family, + const char* host, + uint32_t port, + SocketAddress* addr) { + return ToSockAddr( + family, host, port, reinterpret_cast(addr->storage())); } size_t SocketAddress::Hash::operator()(const SocketAddress& addr) const { @@ -102,6 +92,43 @@ size_t SocketAddress::Hash::operator()(const SocketAddress& addr) const { } } +size_t SocketAddress::IpHash::operator()(const SocketAddress& addr) const { + // Hash only the IP address bytes, ignoring the port. + switch (addr.family()) { + case AF_INET: { + const sockaddr_in* ipv4 = + reinterpret_cast(addr.raw()); + return HashBytes(reinterpret_cast(&ipv4->sin_addr), 4); + } + case AF_INET6: { + const sockaddr_in6* ipv6 = + reinterpret_cast(addr.raw()); + return HashBytes(reinterpret_cast(&ipv6->sin6_addr), 16); + } + default: + UNREACHABLE(); + } +} + +bool SocketAddress::IpEqual::operator()(const SocketAddress& a, + const SocketAddress& b) const { + if (a.family() != b.family()) return false; + switch (a.family()) { + case AF_INET: { + const sockaddr_in* a4 = reinterpret_cast(a.raw()); + const sockaddr_in* b4 = reinterpret_cast(b.raw()); + return memcmp(&a4->sin_addr, &b4->sin_addr, 4) == 0; + } + case AF_INET6: { + const sockaddr_in6* a6 = reinterpret_cast(a.raw()); + const sockaddr_in6* b6 = reinterpret_cast(b.raw()); + return memcmp(&a6->sin6_addr, &b6->sin6_addr, 16) == 0; + } + default: + UNREACHABLE(); + } +} + SocketAddress SocketAddress::FromSockName(const uv_tcp_t& handle) { return FromUVHandle(uv_tcp_getsockname, handle); } @@ -119,21 +146,15 @@ SocketAddress SocketAddress::FromPeerName(const uv_udp_t& handle) { } namespace { -constexpr uint8_t mask[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff }; - -bool is_match_ipv4( - const SocketAddress& one, - const SocketAddress& two) { - const sockaddr_in* one_in = - reinterpret_cast(one.data()); - const sockaddr_in* two_in = - reinterpret_cast(two.data()); +constexpr uint8_t mask[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; + +bool is_match_ipv4(const SocketAddress& one, const SocketAddress& two) { + const sockaddr_in* one_in = reinterpret_cast(one.data()); + const sockaddr_in* two_in = reinterpret_cast(two.data()); return memcmp(&one_in->sin_addr, &two_in->sin_addr, sizeof(uint32_t)) == 0; } -bool is_match_ipv6( - const SocketAddress& one, - const SocketAddress& two) { +bool is_match_ipv6(const SocketAddress& one, const SocketAddress& two) { const sockaddr_in6* one_in = reinterpret_cast(one.data()); const sockaddr_in6* two_in = @@ -141,29 +162,23 @@ bool is_match_ipv6( return memcmp(&one_in->sin6_addr, &two_in->sin6_addr, 16) == 0; } -bool is_match_ipv4_ipv6( - const SocketAddress& ipv4, - const SocketAddress& ipv6) { +bool is_match_ipv4_ipv6(const SocketAddress& ipv4, const SocketAddress& ipv6) { const sockaddr_in* check_ipv4 = reinterpret_cast(ipv4.data()); const sockaddr_in6* check_ipv6 = reinterpret_cast(ipv6.data()); - const uint8_t* ptr = - reinterpret_cast(&check_ipv6->sin6_addr); + const uint8_t* ptr = reinterpret_cast(&check_ipv6->sin6_addr); return memcmp(ptr, mask, sizeof(mask)) == 0 && - memcmp(ptr + sizeof(mask), - &check_ipv4->sin_addr, - sizeof(uint32_t)) == 0; + memcmp(ptr + sizeof(mask), &check_ipv4->sin_addr, sizeof(uint32_t)) == + 0; } std::partial_ordering compare_ipv4(const SocketAddress& one, const SocketAddress& two) { - const sockaddr_in* one_in = - reinterpret_cast(one.data()); - const sockaddr_in* two_in = - reinterpret_cast(two.data()); + const sockaddr_in* one_in = reinterpret_cast(one.data()); + const sockaddr_in* two_in = reinterpret_cast(two.data()); const uint32_t s_addr_one = ntohl(one_in->sin_addr.s_addr); const uint32_t s_addr_two = ntohl(two_in->sin_addr.s_addr); @@ -193,19 +208,15 @@ std::partial_ordering compare_ipv4_ipv6(const SocketAddress& ipv4, const SocketAddress& ipv6) { const sockaddr_in* ipv4_in = reinterpret_cast(ipv4.data()); - const sockaddr_in6 * ipv6_in = + const sockaddr_in6* ipv6_in = reinterpret_cast(ipv6.data()); - const uint8_t* ptr = - reinterpret_cast(&ipv6_in->sin6_addr); + const uint8_t* ptr = reinterpret_cast(&ipv6_in->sin6_addr); if (memcmp(ptr, mask, sizeof(mask)) != 0) return std::partial_ordering::unordered; - int ret = memcmp( - &ipv4_in->sin_addr, - ptr + sizeof(mask), - sizeof(uint32_t)); + int ret = memcmp(&ipv4_in->sin_addr, ptr + sizeof(mask), sizeof(uint32_t)); if (ret < 0) return std::partial_ordering::less; @@ -214,25 +225,21 @@ std::partial_ordering compare_ipv4_ipv6(const SocketAddress& ipv4, return std::partial_ordering::equivalent; } -bool in_network_ipv4( - const SocketAddress& ip, - const SocketAddress& net, - int prefix) { +bool in_network_ipv4(const SocketAddress& ip, + const SocketAddress& net, + int prefix) { uint32_t mask = ((1ull << prefix) - 1) << (32 - prefix); - const sockaddr_in* ip_in = - reinterpret_cast(ip.data()); - const sockaddr_in* net_in = - reinterpret_cast(net.data()); + const sockaddr_in* ip_in = reinterpret_cast(ip.data()); + const sockaddr_in* net_in = reinterpret_cast(net.data()); return (htonl(ip_in->sin_addr.s_addr) & mask) == (htonl(net_in->sin_addr.s_addr) & mask); } -bool in_network_ipv6( - const SocketAddress& ip, - const SocketAddress& net, - int prefix) { +bool in_network_ipv6(const SocketAddress& ip, + const SocketAddress& net, + int prefix) { // Special case, if prefix == 128, then just do a // straight comparison. if (prefix == 128) @@ -242,27 +249,23 @@ bool in_network_ipv6( int len = (prefix - r) / 8; uint8_t mask = ((1 << r) - 1) << (8 - r); - const sockaddr_in6* ip_in = - reinterpret_cast(ip.data()); + const sockaddr_in6* ip_in = reinterpret_cast(ip.data()); const sockaddr_in6* net_in = reinterpret_cast(net.data()); - if (memcmp(&ip_in->sin6_addr, &net_in->sin6_addr, len) != 0) - return false; + if (memcmp(&ip_in->sin6_addr, &net_in->sin6_addr, len) != 0) return false; - const uint8_t* p1 = reinterpret_cast( - ip_in->sin6_addr.s6_addr); - const uint8_t* p2 = reinterpret_cast( - net_in->sin6_addr.s6_addr); + const uint8_t* p1 = + reinterpret_cast(ip_in->sin6_addr.s6_addr); + const uint8_t* p2 = + reinterpret_cast(net_in->sin6_addr.s6_addr); return (p1[len] & mask) == (p2[len] & mask); } -bool in_network_ipv4_ipv6( - const SocketAddress& ip, - const SocketAddress& net, - int prefix) { - +bool in_network_ipv4_ipv6(const SocketAddress& ip, + const SocketAddress& net, + int prefix) { if (prefix == 128) return compare_ipv4_ipv6(ip, net) == std::partial_ordering::equivalent; @@ -270,8 +273,7 @@ bool in_network_ipv4_ipv6( int len = (prefix - r) / 8; uint8_t mask = ((1 << r) - 1) << (8 - r); - const sockaddr_in* ip_in = - reinterpret_cast(ip.data()); + const sockaddr_in* ip_in = reinterpret_cast(ip.data()); const sockaddr_in6* net_in = reinterpret_cast(net.data()); @@ -279,35 +281,29 @@ bool in_network_ipv4_ipv6( uint8_t* ptr = ip_mask; memcpy(ptr + 12, &ip_in->sin_addr, 4); - if (memcmp(ptr, &net_in->sin6_addr, len) != 0) - return false; + if (memcmp(ptr, &net_in->sin6_addr, len) != 0) return false; ptr += len; - const uint8_t* p2 = reinterpret_cast( - net_in->sin6_addr.s6_addr); + const uint8_t* p2 = + reinterpret_cast(net_in->sin6_addr.s6_addr); return (ptr[0] & mask) == (p2[len] & mask); } -bool in_network_ipv6_ipv4( - const SocketAddress& ip, - const SocketAddress& net, - int prefix) { +bool in_network_ipv6_ipv4(const SocketAddress& ip, + const SocketAddress& net, + int prefix) { if (prefix == 32) return compare_ipv4_ipv6(net, ip) == std::partial_ordering::equivalent; uint32_t m = ((1ull << prefix) - 1) << (32 - prefix); - const sockaddr_in6* ip_in = - reinterpret_cast(ip.data()); - const sockaddr_in* net_in = - reinterpret_cast(net.data()); + const sockaddr_in6* ip_in = reinterpret_cast(ip.data()); + const sockaddr_in* net_in = reinterpret_cast(net.data()); - const uint8_t* ptr = - reinterpret_cast(&ip_in->sin6_addr); + const uint8_t* ptr = reinterpret_cast(&ip_in->sin6_addr); - if (memcmp(ptr, mask, sizeof(mask)) != 0) - return false; + if (memcmp(ptr, mask, sizeof(mask)) != 0) return false; ptr += sizeof(mask); uint32_t check = nbytes::ReadUint32BE(ptr); @@ -324,14 +320,18 @@ bool SocketAddress::is_match(const SocketAddress& other) const { switch (family()) { case AF_INET: switch (other.family()) { - case AF_INET: return is_match_ipv4(*this, other); - case AF_INET6: return is_match_ipv4_ipv6(*this, other); + case AF_INET: + return is_match_ipv4(*this, other); + case AF_INET6: + return is_match_ipv4_ipv6(*this, other); } break; case AF_INET6: switch (other.family()) { - case AF_INET: return is_match_ipv4_ipv6(other, *this); - case AF_INET6: return is_match_ipv6(*this, other); + case AF_INET: + return is_match_ipv4_ipv6(other, *this); + case AF_INET6: + return is_match_ipv6(*this, other); } break; } @@ -342,8 +342,10 @@ std::partial_ordering SocketAddress::compare(const SocketAddress& other) const { switch (family()) { case AF_INET: switch (other.family()) { - case AF_INET: return compare_ipv4(*this, other); - case AF_INET6: return compare_ipv4_ipv6(*this, other); + case AF_INET: + return compare_ipv4(*this, other); + case AF_INET6: + return compare_ipv4_ipv6(*this, other); } break; case AF_INET6: @@ -361,28 +363,31 @@ std::partial_ordering SocketAddress::compare(const SocketAddress& other) const { } break; } - case AF_INET6: return compare_ipv6(*this, other); + case AF_INET6: + return compare_ipv6(*this, other); } break; } return std::partial_ordering::unordered; } -bool SocketAddress::is_in_network( - const SocketAddress& other, - int prefix) const { - +bool SocketAddress::is_in_network(const SocketAddress& other, + int prefix) const { switch (family()) { case AF_INET: switch (other.family()) { - case AF_INET: return in_network_ipv4(*this, other, prefix); - case AF_INET6: return in_network_ipv4_ipv6(*this, other, prefix); + case AF_INET: + return in_network_ipv4(*this, other, prefix); + case AF_INET6: + return in_network_ipv4_ipv6(*this, other, prefix); } break; case AF_INET6: switch (other.family()) { - case AF_INET: return in_network_ipv6_ipv4(*this, other, prefix); - case AF_INET6: return in_network_ipv6(*this, other, prefix); + case AF_INET: + return in_network_ipv6_ipv4(*this, other, prefix); + case AF_INET6: + return in_network_ipv6(*this, other, prefix); } break; } @@ -397,8 +402,7 @@ SocketAddressBlockList::SocketAddressBlockList( void SocketAddressBlockList::AddSocketAddress( const std::shared_ptr& address) { Mutex::ScopedLock lock(mutex_); - std::unique_ptr rule = - std::make_unique(address); + std::unique_ptr rule = std::make_unique(address); rules_.emplace_front(std::move(rule)); address_rules_[*address.get()] = rules_.begin(); } @@ -423,20 +427,17 @@ void SocketAddressBlockList::AddSocketAddressRange( } void SocketAddressBlockList::AddSocketAddressMask( - const std::shared_ptr& network, - int prefix) { + const std::shared_ptr& network, int prefix) { Mutex::ScopedLock lock(mutex_); std::unique_ptr rule = std::make_unique(network, prefix); rules_.emplace_front(std::move(rule)); } -bool SocketAddressBlockList::Apply( - const std::shared_ptr& address) { +bool SocketAddressBlockList::Apply(const SocketAddress& address) { Mutex::ScopedLock lock(mutex_); for (const auto& rule : rules_) { - if (rule->Apply(address)) - return true; + if (rule->Apply(address)) return true; } return parent_ ? parent_->Apply(address) : false; } @@ -448,18 +449,15 @@ SocketAddressBlockList::SocketAddressRule::SocketAddressRule( SocketAddressBlockList::SocketAddressRangeRule::SocketAddressRangeRule( const std::shared_ptr& start_, const std::shared_ptr& end_) - : start(start_), - end(end_) {} + : start(start_), end(end_) {} SocketAddressBlockList::SocketAddressMaskRule::SocketAddressMaskRule( - const std::shared_ptr& network_, - int prefix_) - : network(network_), - prefix(prefix_) {} + const std::shared_ptr& network_, int prefix_) + : network(network_), prefix(prefix_) {} bool SocketAddressBlockList::SocketAddressRule::Apply( - const std::shared_ptr& address) { - return this->address->is_match(*address.get()); + const SocketAddress& address) { + return this->address->is_match(address); } std::string SocketAddressBlockList::SocketAddressRule::ToString() { @@ -471,9 +469,8 @@ std::string SocketAddressBlockList::SocketAddressRule::ToString() { } bool SocketAddressBlockList::SocketAddressRangeRule::Apply( - const std::shared_ptr& address) { - return *address.get() >= *start.get() && - *address.get() <= *end.get(); + const SocketAddress& address) { + return address >= *start.get() && address <= *end.get(); } std::string SocketAddressBlockList::SocketAddressRangeRule::ToString() { @@ -487,8 +484,8 @@ std::string SocketAddressBlockList::SocketAddressRangeRule::ToString() { } bool SocketAddressBlockList::SocketAddressMaskRule::Apply( - const std::shared_ptr& address) { - return address->is_in_network(*network.get(), prefix); + const SocketAddress& address) { + return address.is_in_network(*network.get(), prefix); } std::string SocketAddressBlockList::SocketAddressMaskRule::ToString() { @@ -503,19 +500,16 @@ std::string SocketAddressBlockList::SocketAddressMaskRule::ToString() { MaybeLocal SocketAddressBlockList::ListRules(Environment* env) { Mutex::ScopedLock lock(mutex_); LocalVector rules(env->isolate()); - if (!ListRules(env, &rules)) - return MaybeLocal(); + if (!ListRules(env, &rules)) return MaybeLocal(); return Array::New(env->isolate(), rules.data(), rules.size()); } bool SocketAddressBlockList::ListRules(Environment* env, LocalVector* rules) { - if (parent_ && !parent_->ListRules(env, rules)) - return false; + if (parent_ && !parent_->ListRules(env, rules)) return false; for (const auto& rule : rules_) { Local str; - if (!rule->ToV8String(env).ToLocal(&str)) - return false; + if (!rule->ToV8String(env).ToLocal(&str)) return false; rules->push_back(str); } return true; @@ -545,8 +539,7 @@ SocketAddressBlockListWrap::SocketAddressBlockListWrap( Environment* env, Local wrap, std::shared_ptr blocklist) - : BaseObject(env, wrap), - blocklist_(std::move(blocklist)) { + : BaseObject(env, wrap), blocklist_(std::move(blocklist)) { MakeWeak(); } @@ -554,8 +547,9 @@ BaseObjectPtr SocketAddressBlockListWrap::New( Environment* env) { Local obj; if (!env->blocklist_constructor_template() - ->InstanceTemplate() - ->NewInstance(env->context()).ToLocal(&obj)) { + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj)) { return nullptr; } BaseObjectPtr wrap = @@ -565,25 +559,22 @@ BaseObjectPtr SocketAddressBlockListWrap::New( } BaseObjectPtr SocketAddressBlockListWrap::New( - Environment* env, - std::shared_ptr blocklist) { + Environment* env, std::shared_ptr blocklist) { Local obj; if (!env->blocklist_constructor_template() - ->InstanceTemplate() - ->NewInstance(env->context()).ToLocal(&obj)) { + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj)) { return nullptr; } BaseObjectPtr wrap = MakeBaseObject( - env, - obj, - std::move(blocklist)); + env, obj, std::move(blocklist)); CHECK(wrap); return wrap; } -void SocketAddressBlockListWrap::New( - const FunctionCallbackInfo& args) { +void SocketAddressBlockListWrap::New(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args); new SocketAddressBlockListWrap(env, args.This()); @@ -622,9 +613,8 @@ void SocketAddressBlockListWrap::AddRange( if (*start_addr->address().get() > *end_addr->address().get()) return args.GetReturnValue().Set(false); - wrap->blocklist_->AddSocketAddressRange( - start_addr->address(), - end_addr->address()); + wrap->blocklist_->AddSocketAddressRange(start_addr->address(), + end_addr->address()); args.GetReturnValue().Set(true); } @@ -665,7 +655,7 @@ void SocketAddressBlockListWrap::Check( SocketAddressBase* addr; ASSIGN_OR_RETURN_UNWRAP(&addr, args[0]); - args.GetReturnValue().Set(wrap->blocklist_->Apply(addr->address())); + args.GetReturnValue().Set(wrap->blocklist_->Apply(*addr->address())); } void SocketAddressBlockListWrap::GetRules( @@ -687,9 +677,8 @@ SocketAddressBlockListWrap::CloneForMessaging() const { return std::make_unique(this); } -bool SocketAddressBlockListWrap::HasInstance( - Environment* env, - Local value) { +bool SocketAddressBlockListWrap::HasInstance(Environment* env, + Local value) { return GetConstructorTemplate(env)->HasInstance(value); } @@ -711,11 +700,10 @@ Local SocketAddressBlockListWrap::GetConstructorTemplate( return tmpl; } -void SocketAddressBlockListWrap::Initialize( - Local target, - Local unused, - Local context, - void* priv) { +void SocketAddressBlockListWrap::Initialize(Local target, + Local unused, + Local context, + void* priv) { Environment* env = Environment::GetCurrent(context); SetConstructorFunction(context, @@ -772,12 +760,12 @@ void SocketAddressBase::Initialize(Environment* env, Local target) { } BaseObjectPtr SocketAddressBase::Create( - Environment* env, - std::shared_ptr address) { + Environment* env, std::shared_ptr address) { Local obj; if (!GetConstructorTemplate(env) - ->InstanceTemplate() - ->NewInstance(env->context()).ToLocal(&obj)) { + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj)) { return nullptr; } @@ -788,8 +776,8 @@ void SocketAddressBase::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK(args.IsConstructCall()); CHECK(args[0]->IsString()); // address - CHECK(args[1]->IsInt32()); // port - CHECK(args[2]->IsInt32()); // family + CHECK(args[1]->IsInt32()); // port + CHECK(args[2]->IsInt32()); // family CHECK(args[3]->IsUint32()); // flow label Utf8Value address(env->isolate(), args[0]); @@ -820,19 +808,21 @@ void SocketAddressBase::Detail(const FunctionCallbackInfo& args) { return; if (detail->Set(env->context(), env->address_string(), address).IsJust() && - detail->Set( - env->context(), - env->port_string(), - Int32::New(env->isolate(), base->address_->port())).IsJust() && - detail->Set( - env->context(), - env->family_string(), - Int32::New(env->isolate(), base->address_->family())).IsJust() && - detail->Set( - env->context(), - env->flowlabel_string(), - Uint32::New(env->isolate(), base->address_->flow_label())) - .IsJust()) { + detail + ->Set(env->context(), + env->port_string(), + Int32::New(env->isolate(), base->address_->port())) + .IsJust() && + detail + ->Set(env->context(), + env->family_string(), + Int32::New(env->isolate(), base->address_->family())) + .IsJust() && + detail + ->Set(env->context(), + env->flowlabel_string(), + Uint32::New(env->isolate(), base->address_->flow_label())) + .IsJust()) { args.GetReturnValue().Set(detail); } } @@ -852,12 +842,10 @@ void SocketAddressBase::LegacyDetail(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(address); } -SocketAddressBase::SocketAddressBase( - Environment* env, - Local wrap, - std::shared_ptr address) - : BaseObject(env, wrap), - address_(std::move(address)) { +SocketAddressBase::SocketAddressBase(Environment* env, + Local wrap, + std::shared_ptr address) + : BaseObject(env, wrap), address_(std::move(address)) { MakeWeak(); } @@ -865,8 +853,8 @@ void SocketAddressBase::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("address", address_); } -std::unique_ptr -SocketAddressBase::CloneForMessaging() const { +std::unique_ptr SocketAddressBase::CloneForMessaging() + const { return std::make_unique(this); } diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h index a522505949a263..05bb127b012f83 100644 --- a/src/node_sockaddr.h +++ b/src/node_sockaddr.h @@ -3,9 +3,9 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include "base_object.h" #include "env.h" #include "memory_tracker.h" -#include "base_object.h" #include "node.h" #include "node_worker.h" #include "uv.h" @@ -27,6 +27,16 @@ class SocketAddress : public MemoryRetainer { size_t operator()(const SocketAddress& addr) const; }; + // Hashes and compares only the IP address, ignoring the port. + // Useful for per-host connection counting where clients from + // the same IP but different ports should be treated as one host. + struct IpHash { + size_t operator()(const SocketAddress& addr) const; + }; + struct IpEqual { + bool operator()(const SocketAddress& a, const SocketAddress& b) const; + }; + inline bool operator==(const SocketAddress& other) const; inline bool operator!=(const SocketAddress& other) const; @@ -36,23 +46,18 @@ class SocketAddress : public MemoryRetainer { inline static bool is_numeric_host(const char* hostname, int family); // Returns true if converting {family, host, port} to *addr succeeded. - static bool ToSockAddr( - int32_t family, - const char* host, - uint32_t port, - sockaddr_storage* addr); + static bool ToSockAddr(int32_t family, + const char* host, + uint32_t port, + sockaddr_storage* addr); // Returns true if converting {family, host, port} to *addr succeeded. - static bool New( - int32_t family, - const char* host, - uint32_t port, - SocketAddress* addr); + static bool New(int32_t family, + const char* host, + uint32_t port, + SocketAddress* addr); - static bool New( - const char* host, - uint32_t port, - SocketAddress* addr); + static bool New(const char* host, uint32_t port, SocketAddress* addr); // Returns the port for an IPv4 or IPv6 address. inline static int GetPort(const sockaddr* addr); @@ -135,6 +140,9 @@ class SocketAddress : public MemoryRetainer { template using Map = std::unordered_map; + template + using IpMap = std::unordered_map; + private: sockaddr_storage address_; }; @@ -146,18 +154,16 @@ class SocketAddressBase : public BaseObject { Environment* env); static void Initialize(Environment* env, v8::Local target); static BaseObjectPtr Create( - Environment* env, - std::shared_ptr address); + Environment* env, std::shared_ptr address); static void New(const v8::FunctionCallbackInfo& args); static void Detail(const v8::FunctionCallbackInfo& args); static void LegacyDetail(const v8::FunctionCallbackInfo& args); static void GetFlowLabel(const v8::FunctionCallbackInfo& args); - SocketAddressBase( - Environment* env, - v8::Local wrap, - std::shared_ptr address); + SocketAddressBase(Environment* env, + v8::Local wrap, + std::shared_ptr address); inline const std::shared_ptr& address() const { return address_; @@ -207,8 +213,9 @@ class SocketAddressLRU : public MemoryRetainer { // If the item already exists, returns a reference to // the existing item, adjusting items position in the // LRU. If the item does not exist, emplaces the item - // and returns the new item. - Type* Upsert(const SocketAddress& address); + // and returns the new item. The caller provides a + // timestamp to avoid redundant uv_hrtime() calls. + Type* Upsert(const SocketAddress& address, uint64_t now); // Returns a reference to the item if it exists, or // nullptr. The position in the LRU is not modified. @@ -225,7 +232,7 @@ class SocketAddressLRU : public MemoryRetainer { using Pair = std::pair; using Iterator = typename std::list::iterator; - void CheckExpired(); + void CheckExpired(uint64_t now); std::list list_; SocketAddress::Map map_; @@ -245,22 +252,20 @@ class SocketAddressBlockList : public MemoryRetainer { void RemoveSocketAddress(const std::shared_ptr& address); - void AddSocketAddressRange( - const std::shared_ptr& start, - const std::shared_ptr& end); + void AddSocketAddressRange(const std::shared_ptr& start, + const std::shared_ptr& end); - void AddSocketAddressMask( - const std::shared_ptr& address, - int prefix); + void AddSocketAddressMask(const std::shared_ptr& address, + int prefix); - bool Apply(const std::shared_ptr& address); + bool Apply(const SocketAddress& address); size_t size() const { return rules_.size(); } v8::MaybeLocal ListRules(Environment* env); struct Rule : public MemoryRetainer { - virtual bool Apply(const std::shared_ptr& address) = 0; + virtual bool Apply(const SocketAddress& address) = 0; inline v8::MaybeLocal ToV8String(Environment* env); virtual std::string ToString() = 0; }; @@ -270,7 +275,7 @@ class SocketAddressBlockList : public MemoryRetainer { explicit SocketAddressRule(const std::shared_ptr& address); - bool Apply(const std::shared_ptr& address) override; + bool Apply(const SocketAddress& address) override; std::string ToString() override; void MemoryInfo(node::MemoryTracker* tracker) const override; @@ -282,11 +287,10 @@ class SocketAddressBlockList : public MemoryRetainer { std::shared_ptr start; std::shared_ptr end; - SocketAddressRangeRule( - const std::shared_ptr& start, - const std::shared_ptr& end); + SocketAddressRangeRule(const std::shared_ptr& start, + const std::shared_ptr& end); - bool Apply(const std::shared_ptr& address) override; + bool Apply(const SocketAddress& address) override; std::string ToString() override; void MemoryInfo(node::MemoryTracker* tracker) const override; @@ -298,11 +302,10 @@ class SocketAddressBlockList : public MemoryRetainer { std::shared_ptr network; int prefix; - SocketAddressMaskRule( - const std::shared_ptr& address, - int prefix); + SocketAddressMaskRule(const std::shared_ptr& address, + int prefix); - bool Apply(const std::shared_ptr& address) override; + bool Apply(const SocketAddress& address) override; std::string ToString() override; void MemoryInfo(node::MemoryTracker* tracker) const override; @@ -336,8 +339,7 @@ class SocketAddressBlockListWrap : public BaseObject { static BaseObjectPtr New(Environment* env); static BaseObjectPtr New( - Environment* env, - std::shared_ptr blocklist); + Environment* env, std::shared_ptr blocklist); static void New(const v8::FunctionCallbackInfo& args); static void AddAddress(const v8::FunctionCallbackInfo& args); @@ -346,11 +348,14 @@ class SocketAddressBlockListWrap : public BaseObject { static void Check(const v8::FunctionCallbackInfo& args); static void GetRules(const v8::FunctionCallbackInfo& args); - SocketAddressBlockListWrap( - Environment* env, - v8::Local wrap, - std::shared_ptr blocklist = - std::make_shared()); + SocketAddressBlockListWrap(Environment* env, + v8::Local wrap, + std::shared_ptr blocklist = + std::make_shared()); + + inline const std::shared_ptr& blocklist() const { + return blocklist_; + } void MemoryInfo(node::MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(SocketAddressBlockListWrap) diff --git a/src/node_util.cc b/src/node_util.cc index fbfda9c1551e07..027f668c219443 100644 --- a/src/node_util.cc +++ b/src/node_util.cc @@ -460,6 +460,15 @@ void ConstructSharedArrayBuffer(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(sab); } +// Marks a promise as handled and silent to prevent unhandled rejection +// tracking from triggering. +void MarkPromiseAsHandled(const FunctionCallbackInfo& args) { + CHECK(args[0]->IsPromise()); + Local promise = args[0].As(); + promise->MarkAsHandled(); + promise->MarkAsSilent(); +} + void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(GetPromiseDetails); registry->Register(GetProxyDetails); @@ -478,6 +487,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(DefineLazyProperties); registry->Register(DefineLazyPropertiesGetter); registry->Register(ConstructSharedArrayBuffer); + registry->Register(MarkPromiseAsHandled); } void Initialize(Local target, @@ -583,6 +593,7 @@ void Initialize(Local target, target, "constructSharedArrayBuffer", ConstructSharedArrayBuffer); + SetMethod(context, target, "markPromiseAsHandled", MarkPromiseAsHandled); Local should_abort_on_uncaught_toggle = FIXED_ONE_BYTE_STRING(env->isolate(), "shouldAbortOnUncaughtToggle"); diff --git a/src/quic/README.md b/src/quic/README.md new file mode 100644 index 00000000000000..9583acbcd76417 --- /dev/null +++ b/src/quic/README.md @@ -0,0 +1,418 @@ +# Node.js QUIC Implementation (`src/quic/`) + +This directory contains the C++ implementation of the Node.js experimental QUIC +support (`--experimental-quic`). The implementation builds on three external +libraries: **ngtcp2** (QUIC transport), **nghttp3** (HTTP/3 framing), and +**OpenSSL** (TLS 1.3). + +## Architecture Overview + +The stack is layered as: + +```text +┌─────────────────────────────────────────────┐ +│ JavaScript API (lib/internal/quic/) │ +├─────────────────────────────────────────────┤ +│ Endpoint — UDP socket, packet I/O │ +│ Session — QUIC connection (ngtcp2) │ +│ Application — ALPN protocol logic │ +│ Stream — Bidirectional data flow │ +├─────────────────────────────────────────────┤ +│ ngtcp2 / nghttp3 / OpenSSL │ +├─────────────────────────────────────────────┤ +│ libuv — UDP, timers, thread pool │ +└─────────────────────────────────────────────┘ +``` + +An **Endpoint** binds a UDP socket and dispatches incoming packets to +**Sessions**. Each Session wraps an `ngtcp2_conn` and delegates +protocol-specific behavior to an **Application** (selected by ALPN +negotiation). Sessions contain **Streams** — bidirectional or unidirectional +data channels that carry application data. + +## File Map + +### Foundation + +| File | Purpose | +| ------------- | ------------------------------------------------------------------ | +| `guard.h` | OpenSSL QUIC guard macro | +| `defs.h` | Core enums, typedefs, constants, macros | +| `arena.h` | Block-based arena allocator (header-only template) | +| `data.h/cc` | `Path`, `PathStorage`, `Store`, `QuicError` | +| `cid.h/cc` | `CID` — Connection ID with hash, factory, map alias | +| `tokens.h/cc` | `TokenSecret`, `StatelessResetToken`, `RetryToken`, `RegularToken` | + +### Security + +| File | Purpose | +| -------------------- | ----------------------------------------------------------- | +| `tlscontext.h/cc` | `TLSContext`, `TLSSession` — OpenSSL integration, SNI, ALPN | +| `sessionticket.h/cc` | `SessionTicket` — TLS 1.3 session resumption and 0-RTT | + +### Core + +| File | Purpose | +| ------------------ | ------------------------------------------------------------ | +| `endpoint.h/cc` | `Endpoint` — UDP binding, packet dispatch, retry/validation | +| `session.h/cc` | `Session` — QUIC connection state machine (\~3,500 lines) | +| `streams.h/cc` | `Stream`, `Outbound`, `PendingStream` — data flow | +| `application.h/cc` | `Session::Application` base + `DefaultApplication` | +| `http3.h/cc` | `Http3ApplicationImpl` — nghttp3 integration (\~1,400 lines) | + +### Infrastructure + +| File | Purpose | +| ----------------------- | ------------------------------------------------------------- | +| `bindingdata.h/cc` | `BindingData` — JS binding state, callback scopes, allocators | +| `session_manager.h/cc` | `SessionManager` — per-Realm CID→Session routing | +| `transportparams.h/cc` | `TransportParams` — QUIC transport parameter encoding | +| `packet.h/cc` | `Packet` — arena-allocated outbound packets | +| `preferredaddress.h/cc` | `PreferredAddress` — server preferred address helper | +| `quic.cc` | Module entry point (binding registration) | + +## Key Design Patterns + +### SendPendingDataScope (RAII Send Coalescing) + +Every entry point that may generate outbound data creates a +`SendPendingDataScope`. Scopes nest — an internal depth counter ensures +`Application::SendPendingData()` is called exactly once, when the outermost +scope exits: + +```cpp +{ + SendPendingDataScope outer(session); // depth 1 + { + SendPendingDataScope inner(session); // depth 2 + // ... generate data ... + } // depth 1 — no send yet +} // depth 0 — SendPendingData() fires +``` + +This is used in `Session::Receive`, `Endpoint::Connect`, `Session::Close`, +`Session::ResumeStream`, and all stream write operations. + +### NgTcp2CallbackScope / NgHttp3CallbackScope + +Per-session RAII guards that prevent re-entrant calls into ngtcp2/nghttp3. +While active, `can_send_packets()` returns false, blocking the send loop. +If `Destroy()` is called during a callback (e.g., via JS `MakeCallback`), +destruction is deferred until the scope exits, preventing use-after-free. + +### Bob Protocol (Pull-Based Streaming) + +Data flows through the stack using the **bob** (Bytes-Over-Buffers) pull +protocol defined in `src/node_bob.h`. The consumer calls `Pull()` on a +source, which responds with one of four status codes: + +| Status | Meaning | +| --------------------- | ---------------------------------------------------------------- | +| `STATUS_EOS` (0) | End of stream — no more data | +| `STATUS_CONTINUE` (1) | Data delivered; pull again | +| `STATUS_BLOCK` (2) | No data now; try later | +| `STATUS_WAIT` (3) | Async — source will invoke the `next` callback when data arrives | + +The `Done` callback passed with each pull signals that the consumer is +finished with the buffer memory, enabling zero-copy transfer. + +### Three-Phase Buffer Lifecycle + +Data in `Stream::Outbound` moves through three states: + +```text +Pulled (uncommitted) → Committed (in-flight) → Acknowledged (freed) +``` + +* **Uncommitted**: Read from the DataQueue but not yet accepted by ngtcp2 +* **Committed**: Accepted into a QUIC packet by `ngtcp2_conn_writev_stream` +* **Acknowledged**: Peer ACKed the data; buffer memory is released + +Separate cursors on each buffer entry track progression. This allows ngtcp2 +to retry uncommitted data (e.g., after pacing/congestion clears) without +re-reading from the source. + +### Application Abstraction + +`Session::Application` is a virtual interface that the Session delegates +ALPN-specific behavior to. Two implementations exist: + +* **`DefaultApplication`** (`application.cc`): Used for non-HTTP/3 ALPN + protocols. Maintains its own stream scheduling queue. Streams are scheduled + via an intrusive linked list. + +* **`Http3ApplicationImpl`** (`http3.cc`): Used when ALPN negotiates `h3`. + Wraps `nghttp3_conn` for HTTP/3 framing, header compression (QPACK), + server push, and stream prioritization. Manages unidirectional control + streams internally. + +The Application is selected during ALPN negotiation — immediately for +clients (ALPN known upfront), during the `OnSelectAlpn` TLS callback for +servers. + +### Thread-Local Allocator + +Both ngtcp2 and nghttp3 require custom allocators (`ngtcp2_mem`, +`nghttp3_mem`). These allocator structs must outlive every object they +create. Some nghttp3 objects (notably `rcbuf`s backing V8 external strings) +can survive past `BindingData` destruction during isolate teardown. + +The solution uses `thread_local` storage: + +```cpp +struct QuicAllocState { + BindingData* binding = nullptr; // Nulled in ~BindingData + ngtcp2_mem ngtcp2; + nghttp3_mem nghttp3; +}; +thread_local QuicAllocState quic_alloc_state; +``` + +Each allocation prepends its size before the returned pointer. This allows +`free` and `realloc` to report correct sizes for memory tracking. When +`binding` is null (after `BindingData` destruction), allocations still +succeed but memory tracking is silently skipped. + +## Session Lifecycle + +### Creation + +**Client**: `Endpoint::Connect()` builds a `Session::Config` with +`Side::CLIENT`, creates a `TLSContext`, and calls `Session::Create()` → +`ngtcp2_conn_client_new()`. The Application is selected immediately. + +**Server**: `Endpoint::Receive()` processes an Initial packet through +address validation (retry tokens, LRU cache), then calls `Session::Create()` +→ `ngtcp2_conn_server_new()`. The Application is selected later, during ALPN +negotiation in the TLS handshake. + +### The Receive Path + +```text +uv_udp_recv_cb + → Endpoint::Receive() + → FindSession(dcid) // CID lookup across endpoints + ├── Found → Session::Receive() + └── Not found: + ├── Stateless reset? → process + ├── Short header? → SendStatelessReset() + └── Long header? → acceptInitialPacket() + ├── ngtcp2_accept() + ├── Address validation (retry tokens, LRU) + └── Session::Create() + +Session::Receive() + → SendPendingDataScope // will send after processing + → NgTcp2CallbackScope // re-entrancy guard + → ngtcp2_conn_read_pkt() // decrypt, process frames + triggers callbacks: + ├── recv_stream_data → Application::ReceiveStreamData() + ├── stream_open → Application::ReceiveStreamOpen() + ├── acked_stream_data → Application::AcknowledgeStreamData() + ├── handshake_completed → Session::HandshakeCompleted() + └── ... others + → Application::PostReceive() // deferred operations (e.g., GOAWAY) +``` + +### The Send Loop + +```text +SendPendingDataScope::~SendPendingDataScope() + → Application::SendPendingData() + Loop (up to max_packet_count): + ├── GetStreamData() // pull data from next stream + │ └── stream->Pull() // bob pull from Outbound→DataQueue + ├── WriteVStream() // ngtcp2_conn_writev_stream() + │ encrypts, frames, paces + ├── if ndatalen > 0: StreamCommit() + │ stream->Commit(datalen, fin) + ├── if nwrite > 0: Send() // uv_udp_send() + ├── if WRITE_MORE: continue // room for more in this packet + ├── if STREAM_DATA_BLOCKED: // flow control + │ StreamDataBlocked(), continue + └── if nwrite == 0: // pacing/congestion + ResumeStream() if data pending, return + On exit: UpdateTimer(), UpdateDataStats() +``` + +When `nwrite == 0` and the stream had unsent data (payload or FIN), the +stream is re-scheduled via `Application::ResumeStream()` so the next +timer-triggered `SendPendingData` retries it. + +### Close Methods + +| Method | Behavior | +| ------------ | ----------------------------------------------------------- | +| **DEFAULT** | Destroys all streams, sends CONNECTION\_CLOSE, emits to JS | +| **SILENT** | Same but skips CONNECTION\_CLOSE (errors, stateless resets) | +| **GRACEFUL** | Sends GOAWAY (H3), waits for streams to close naturally | + +### Timer + +`Session::UpdateTimer()` queries `ngtcp2_conn_get_expiry()` and sets a libuv +timer. When it fires, `OnTimeout()` calls `ngtcp2_conn_handle_expiry()` then +`SendPendingData()` to retransmit lost packets, send PINGs, or retry +pacing-blocked sends. + +## Stream Lifecycle + +### Creation + +**Local streams**: `Session::OpenStream()` calls +`ngtcp2_conn_open_bidi_stream()` or `ngtcp2_conn_open_uni_stream()`. If the +handshake is incomplete or the concurrency limit is reached, the stream is +created in **pending** state and queued. When the peer grants capacity +(`ExtendMaxStreams`), pending streams are fulfilled with real stream IDs. + +**Remote streams**: ngtcp2 notifies via callbacks. The Application creates a +`Stream` object and emits it to JavaScript. + +### Outbound Data Flow + +The `Stream::Outbound` class bridges a `DataQueue` (the data source) to +ngtcp2's packet-writing loop. A `DataQueue::Reader` provides the bob +pull interface. + +Supported body source types (via `GetDataQueueFromSource`): + +| Source | Strategy | +| ------------------- | -------------------------------------------- | +| `ArrayBuffer` | Zero-copy detach, or copy if non-detachable | +| `SharedArrayBuffer` | Always copy | +| `ArrayBufferView` | Zero-copy detach of underlying buffer | +| `Blob` | Slice of Blob's existing DataQueue | +| `String` | UTF-8 encode into BackingStore | +| `FileHandle` | `FdEntry` — async file reads via thread pool | + +For `FileHandle` bodies, the `FdEntry::ReaderImpl` dispatches `uv_fs_read` +to the libuv thread pool and returns `STATUS_WAIT`. When the read completes, +the callback appends data to the Outbound buffer and calls +`session().ResumeStream(id)` to re-enter the send loop. + +### Inbound Data Flow + +Received stream data is delivered by ngtcp2 via +`Application::ReceiveStreamData()`, which calls `stream->ReceiveData()`. +Data is appended to the stream's inbound `DataQueue`. The JavaScript side +consumes this via an async iterator (the `stream/iter` `bytes()` helper). +The stream implements `DataQueue::BackpressureListener` to extend the +QUIC flow control window as data is consumed. + +### Streaming Mode (Writer API) + +When no body is provided at stream creation, the JavaScript `stream.writer` +API uses streaming mode. The Outbound creates a non-idempotent DataQueue. +Each `writeSync()` / `write()` call appends an in-memory entry. The +`endSync()` / `end()` call caps the queue, signaling EOS to the send loop. + +## SessionManager + +The `SessionManager` is a per-Realm singleton that owns the authoritative +CID→Session mapping. It enables: + +* **Cross-endpoint routing**: A session's CIDs are registered globally so + packets arriving on any endpoint find the right session. +* **Connection migration**: When a session migrates to a new path, the + SessionManager updates the routing without requiring endpoint-specific + knowledge. +* **Stateless reset token mapping**: Maps reset tokens to sessions for + detecting stateless resets on any endpoint. + +CID lookup uses a three-tier strategy: + +1. Direct SCID match in `SessionManager::sessions_` +2. Cross-endpoint DCID→SCID in `SessionManager::dcid_to_scid_` +3. Per-endpoint DCID→SCID in `Endpoint::dcid_to_scid_` (peer-chosen CIDs) + +## Address Validation + +The endpoint uses an LRU cache to track validated remote addresses. For +unvalidated addresses: + +1. If no token is present, a **Retry** packet is sent with a cryptographic + token (HKDF-derived, time-limited). +2. The client retransmits the Initial with the retry token. +3. The token is validated; the session is created with the original DCID + preserved for transport parameter verification. + +Regular tokens (from `NEW_TOKEN` frames) follow the same validation path +but without the retry handshake. The LRU cache allows subsequent +connections from the same address to skip validation entirely. + +## HTTP/3 Application (`http3.cc`) + +The `Http3ApplicationImpl` wraps `nghttp3_conn` and handles: + +* **Header compression**: QPACK encoding/decoding via nghttp3's internal + encoder/decoder streams (unidirectional). +* **Stream management**: Only bidirectional streams are exposed to + JavaScript. Unidirectional control, encoder, and decoder streams are + managed internally. +* **FIN management**: `stream_fin_managed_by_application()` returns true. + nghttp3 controls when FIN is sent based on HTTP/3 framing (DATA frames, + trailing HEADERS). The `EndWriting()` notification from JavaScript is + forwarded to `nghttp3_conn_shutdown_stream_write()`. +* **Data read callback**: `on_read_data_callback` pulls data from the + stream's Outbound during `nghttp3_conn_writev_stream`. Bytes must be + committed inside the callback (before `StreamCommit`) because QPACK can + cause re-entrant `read_data` calls. +* **GOAWAY**: `BeginShutdown()` sends a GOAWAY frame. The goaway ID is + deferred to `PostReceive()` (outside callback scopes) so it can safely + invoke JavaScript. +* **Settings**: HTTP/3 SETTINGS (max field section size, QPACK capacities, + CONNECT protocol, datagrams) are negotiated and enforced. Datagram + support follows RFC 9297 — when the peer's SETTINGS disable datagrams, + `sendDatagram()` is blocked. +* **0-RTT**: Early data settings are validated during ticket extraction + (`ValidateTicketData` in `ExtractSessionTicketAppData`). If the server's + settings changed incompatibly, the ticket is rejected before TLS accepts + it. + +## Error Handling + +`QuicError` (`data.h`) encapsulates QUIC error codes with a type namespace +(transport, application, version negotiation, idle close). Factory methods +wrap ngtcp2 error codes, TLS alerts, and application errors. + +On the JavaScript side, `convertQuicError()` transforms the C++ error +representation into `ERR_QUIC_TRANSPORT_ERROR` or +`ERR_QUIC_APPLICATION_ERROR` objects. Clean closes (transport NO\_ERROR, +H3 NO\_ERROR, or idle close) resolve `stream.closed`; all other errors +reject it. + +## Packet Allocation + +Outbound packets are allocated from an `ArenaPool` owned by the +Endpoint. The arena provides O(1) allocation from contiguous memory blocks +(128 slots per block), avoiding per-packet heap allocation and V8 object +overhead. Packets are returned to the pool when the UDP send completes +(via the `Packet::Listener::PacketDone` callback). + +## Debug Logging + +Use the `NODE_DEBUG_NATIVE` environment variable to enable detailed debug +logging: + +* `QUIC` - general QUIC events (sessions, streams, packets) +* `NGTCP2` - ngtcp2 callback events and error codes +* `NGHTTP3` - nghttp3 callback events and error codes + +```bash +NODE_DEBUG_NATIVE=QUIC,NGTCP2,NGHTTP3 node --experimental-quic ... +``` + +The debug output will be printed to stderr and can be extremely verbose. + +Used in combination with `qlog` and `keylog` options when creating a +`QuicSession`, this can help significantly with debugging and understanding +QUIC behavior and identifying bugs / performance issues in the implementation. + +## External Dependencies + +| Library | Role | Location | +| ------- | --------------------------------------- | ------------------------- | +| ngtcp2 | QUIC transport protocol | `deps/ngtcp2/ngtcp2/` | +| nghttp3 | HTTP/3 framing, QPACK | `deps/ngtcp2/nghttp3/` | +| OpenSSL | TLS 1.3 handshake, encryption | system or `deps/openssl/` | +| libuv | UDP sockets, timers, thread pool | `deps/uv/` | +| V8 | JavaScript engine, GC, external strings | `deps/v8/` | diff --git a/src/quic/application.cc b/src/quic/application.cc index 81c1c0ebe5f49c..ce5d5e12154d8a 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -1,3 +1,4 @@ +#include "util.h" #if HAVE_OPENSSL && HAVE_QUIC #include "guard.h" #ifndef OPENSSL_NO_QUIC @@ -18,9 +19,13 @@ namespace node { +using v8::BigInt; +using v8::Boolean; +using v8::DictionaryTemplate; using v8::Just; using v8::Local; using v8::Maybe; +using v8::MaybeLocal; using v8::Nothing; using v8::Object; using v8::Value; @@ -34,6 +39,8 @@ const Session::Application_Options Session::Application_Options::kDefault = {}; Session::Application_Options::operator const nghttp3_settings() const { // In theory, Application::Options might contain options for more than just // HTTP/3. Here we extract only the properties that are relevant to HTTP/3. + // Later if we add more application types we can add more properties or + // divide this up into multiple option structs. return nghttp3_settings{ .max_field_section_size = max_field_section_size, .qpack_max_dtable_capacity = @@ -43,11 +50,13 @@ Session::Application_Options::operator const nghttp3_settings() const { .qpack_blocked_streams = static_cast(qpack_blocked_streams), .enable_connect_protocol = enable_connect_protocol, .h3_datagram = enable_datagrams, - // TODO(@jasnell): Support origin frames? + // origin_list is nullptr here because it is set directly on the + // nghttp3_settings in Http3ApplicationImpl::InitializeConnection() + // from the SNI configuration. .origin_list = nullptr, .glitch_ratelim_burst = 1000, .glitch_ratelim_rate = 33, - .qpack_indexing_strat = NGHTTP3_QPACK_INDEXING_STRAT_NONE, + .qpack_indexing_strat = NGHTTP3_QPACK_INDEXING_STRAT_EAGER, }; } @@ -106,9 +115,54 @@ Maybe Session::Application_Options::From( #undef SET + // Ensure the advertised max_field_section_size in SETTINGS is at least + // as large as max_header_length. Otherwise the peer would be told to + // restrict headers to a smaller size than what CanAddHeader accepts. + if (options.max_field_section_size < options.max_header_length) { + options.max_field_section_size = options.max_header_length; + } + return Just(options); } +MaybeLocal Session::Application_Options::ToObject( + Environment* env) const { + auto& binding_data = BindingData::Get(env); + auto tmpl = binding_data.application_options_template(); + static constexpr std::string_view names[] = { + "maxHeaderPairs", + "maxHeaderLength", + "maxFieldSectionSize", + "qpackMaxDtableCapacity", + "qpackEncoderMaxDtableCapacity", + "qpackBlockedStreams", + "enableConnectProtocol", + "enableDatagrams", + }; + if (tmpl.IsEmpty()) { + tmpl = DictionaryTemplate::New(env->isolate(), names); + binding_data.set_application_options_template(tmpl); + } + MaybeLocal values[] = { + BigInt::NewFromUnsigned(env->isolate(), max_header_pairs), + BigInt::NewFromUnsigned(env->isolate(), max_header_length), + BigInt::NewFromUnsigned(env->isolate(), max_field_section_size), + BigInt::NewFromUnsigned(env->isolate(), qpack_max_dtable_capacity), + BigInt::NewFromUnsigned(env->isolate(), + qpack_encoder_max_dtable_capacity), + BigInt::NewFromUnsigned(env->isolate(), qpack_blocked_streams), + Boolean::New(env->isolate(), enable_connect_protocol), + Boolean::New(env->isolate(), enable_datagrams), + }; + static_assert(std::size(values) == std::size(names)); + + auto obj = tmpl->NewInstance(env->context(), values); + if (obj->SetPrototypeV2(env->context(), Null(env->isolate())).IsNothing()) { + return {}; + } + return obj; +} + // ============================================================================ std::string Session::Application::StreamData::ToString() const { @@ -139,53 +193,21 @@ bool Session::Application::Start() { return true; } -bool Session::Application::AcknowledgeStreamData(int64_t stream_id, - size_t datalen) { - if (auto stream = session().FindStream(stream_id)) [[likely]] { +bool Session::Application::AcknowledgeStreamData(stream_id id, size_t datalen) { + if (auto stream = session().FindStream(id)) [[likely]] { stream->Acknowledge(datalen); - return true; } - return false; -} - -void Session::Application::BlockStream(int64_t id) { - // By default do nothing. -} - -bool Session::Application::CanAddHeader(size_t current_count, - size_t current_headers_length, - size_t this_header_length) { - // By default headers are not supported. - return false; -} - -bool Session::Application::SendHeaders(const Stream& stream, - HeadersKind kind, - const Local& headers, - HeadersFlags flags) { - // By default do nothing. - return false; -} - -void Session::Application::ResumeStream(int64_t id) { - // By default do nothing. -} - -void Session::Application::ExtendMaxStreams(EndpointLabel label, - Direction direction, - uint64_t max_streams) { - // By default do nothing. -} - -void Session::Application::ExtendMaxStreamData(Stream* stream, - uint64_t max_data) { - Debug(session_, "Application extending max stream data"); - // By default do nothing. + // Returning true even when the stream is not found is intentional. + // After a stream is destroyed, the peer can still ACK data that was + // previously sent. This is benign and should not be treated as an error. + return true; } void Session::Application::CollectSessionTicketAppData( SessionTicket::AppData* app_data) const { - // By default do nothing. + // By default, write just the application type byte. + uint8_t buf[1] = {static_cast(type())}; + app_data->Set(uv_buf_init(reinterpret_cast(buf), 1)); } SessionTicket::AppData::Status @@ -197,14 +219,39 @@ Session::Application::ExtractSessionTicketAppData( : SessionTicket::AppData::Status::TICKET_USE; } -void Session::Application::SetStreamPriority(const Stream& stream, - StreamPriority priority, - StreamPriorityFlags flags) { - // By default do nothing. +std::optional Session::Application::ParseTicketData( + const uv_buf_t& data) { + if (data.len == 0 || data.base == nullptr) return std::nullopt; + auto app_type = + static_cast(reinterpret_cast(data.base)[0]); + switch (app_type) { + case Type::DEFAULT: + return DefaultTicketData{}; + case Type::HTTP3: + return ParseHttp3TicketData(data); + default: + return std::nullopt; + } } -StreamPriority Session::Application::GetStreamPriority(const Stream& stream) { - return StreamPriority::DEFAULT; +bool Session::Application::ValidateTicketData( + const PendingTicketAppData& data, const Application_Options& options) { + if (std::holds_alternative(data)) { + // TODO(@jasnell): This validation probably belongs in http3.cc but keeping + // it here for now. + const auto& ticket = std::get(data); + return options.max_field_section_size >= ticket.max_field_section_size && + options.qpack_max_dtable_capacity >= + ticket.qpack_max_dtable_capacity && + options.qpack_encoder_max_dtable_capacity >= + ticket.qpack_encoder_max_dtable_capacity && + options.qpack_blocked_streams >= ticket.qpack_blocked_streams && + (!ticket.enable_connect_protocol || + options.enable_connect_protocol) && + (!ticket.enable_datagrams || options.enable_datagrams); + } + // DefaultTicketData always validates. + return true; } Packet::Ptr Session::Application::CreateStreamDataPacket() { @@ -212,42 +259,174 @@ Packet::Ptr Session::Application::CreateStreamDataPacket() { session_->remote_address(), session_->max_packet_size(), "stream data"); } -void Session::Application::StreamClose(Stream* stream, QuicError&& error) { +void Session::Application::ReceiveStreamClose(Stream* stream, + QuicError&& error) { DCHECK_NOT_NULL(stream); stream->Destroy(std::move(error)); } -void Session::Application::StreamStopSending(Stream* stream, - QuicError&& error) { +void Session::Application::ReceiveStreamStopSending(Stream* stream, + QuicError&& error) { DCHECK_NOT_NULL(stream); stream->ReceiveStopSending(std::move(error)); } -void Session::Application::StreamReset(Stream* stream, - uint64_t final_size, - QuicError&& error) { +void Session::Application::ReceiveStreamReset(Stream* stream, + uint64_t final_size, + QuicError&& error) { stream->ReceiveStreamReset(final_size, std::move(error)); } +// Attempts to pack a pending datagram into the current packet. +// Returns the nwrite value from ngtcp2_conn_writev_datagram. +// On fatal error, closes the session and returns the error code. +// The caller should check: +// > 0: packet is complete, send it (pos was NOT advanced — caller +// must add nwrite to pos and send) +// NGTCP2_ERR_WRITE_MORE: datagram packed, room for more +// 0: congestion controlled or doesn't fit, datagram stays in queue +// < 0 (other): fatal error, session already closed +ssize_t Session::Application::TryWritePendingDatagram(PathStorage* path, + uint8_t* dest, + size_t destlen, + uint64_t ts) { + CHECK(session_->HasPendingDatagrams()); + auto max_attempts = session_->config().options.max_datagram_send_attempts; + + // Skip datagrams that have already exceeded the send attempt limit + // from a previous SendPendingData cycle. + while (session_->HasPendingDatagrams()) { + auto& front = session_->PeekPendingDatagram(); + if (front.send_attempts < max_attempts) break; + Debug(session_, + "Datagram %" PRIu64 " abandoned after %u attempts", + front.id, + front.send_attempts); + session_->DatagramStatus(front.id, DatagramStatus::ABANDONED); + session_->PopPendingDatagram(); + } + + if (!session_->HasPendingDatagrams()) return 0; + auto& dg = session_->PeekPendingDatagram(); + ngtcp2_vec dgvec = dg.data; + int accepted = 0; + int dg_flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE; + + // PacketInfo for the datagram path. When libuv gains per-socket ECN + // marking, the value from ngtcp2 should be forwarded to the send path. + PacketInfo dg_pi; + ssize_t dg_nwrite = ngtcp2_conn_writev_datagram(*session_, + &path->path, + dg_pi, + dest, + destlen, + &accepted, + dg_flags, + dg.id, + &dgvec, + 1, + ts); + + if (accepted) { + // Nice, the datagram was accepted! + Debug(session_, "Datagram %" PRIu64 " accepted into packet", dg.id); + session_->DatagramSent(dg.id); + session_->PopPendingDatagram(); + } else { + Debug(session_, "Datagram %" PRIu64 " not accepted into packet", dg.id); + } + + switch (dg_nwrite) { + case 0: { + // If dg_nwrite is 0, we are either congestion controlled or + // there wasn't enough room in the packet for the datagram or + // we aren't in a state where we can send. + // We'll skip this attempt and return 0. + CHECK(!accepted); + dg.send_attempts++; + return 0; + } + case NGTCP2_ERR_WRITE_MORE: { + // There's still room left in the packet! + return NGTCP2_ERR_WRITE_MORE; + } + case NGTCP2_ERR_INVALID_STATE: + case NGTCP2_ERR_INVALID_ARGUMENT: { + // Non-fatal error cases. Peer either does not support datagrams + // or the datagram is too large for the peer's max. + // Abandon the datagram and signal skip by returning std::nullopt. + session_->DatagramStatus(dg.id, DatagramStatus::ABANDONED); + session_->PopPendingDatagram(); + return 0; + } + default: { + // Fatal errors: PKT_NUM_EXHAUSTED, CALLBACK_FAILURE, NOMEM, etc. + Debug(session_, "Fatal datagram error: %zd", dg_nwrite); + session_->SetLastError(QuicError::ForNgtcp2Error(dg_nwrite)); + session_->Close(CloseMethod::SILENT); + return dg_nwrite; + } + } + UNREACHABLE(); +} + +// the SendPendingData method is the primary driver for sending data from the +// application layer. It loops through available stream data and pending +// datagrams and generates packets to send until there is either no more +// data to send or we hit the maximum number of packets to send in one go. +// This method is extremely delicate. A bug in this method can break the +// entire QUIC implementation; so be very careful when making changes here +// and make sure to test thoroughly. When in doubt... don't change it. void Session::Application::SendPendingData() { DCHECK(!session().is_destroyed()); if (!session().can_send_packets()) [[unlikely]] { return; } - static constexpr size_t kMaxPackets = 32; + // Upper bound on packets per SendPendingData call. ngtcp2's send quantum + // is typically 64 KB, which at 1200-byte minimum packet size is ~53 + // packets. 64 covers the worst case with headroom. The actual count per + // call is dynamically capped by ngtcp2_conn_get_send_quantum(). + static constexpr size_t kMaxPackets = 64; Debug(session_, "Application sending pending data"); + // Cache the timestamp once for the entire send loop. ngtcp2 does not + // require nanosecond-accurate monotonicity within a single burst — + // a single timestamp per SendPendingData call is what other QUIC + // implementations use (e.g., quiche, msquic). When kernel-level + // packet pacing becomes available via libuv, this timestamp becomes + // the base for computing per-packet transmit timestamps. + const uint64_t ts = uv_hrtime(); PathStorage path; StreamData stream_data; bool closed = false; + + // Batch accumulation: packets are collected here and flushed via + // Session::SendBatch when the loop exits, the batch is full, or + // on early return. This enables synchronous batched delivery via + // uv_udp_try_send2 (sendmmsg) from the deferred flush path. + Packet::Ptr batch[kMaxPackets]; + PathStorage batch_paths[kMaxPackets]; + size_t batch_count = 0; + + auto flush_batch = [&] { + if (batch_count == 0) return; + session_->SendBatch(batch, batch_paths, batch_count); + batch_count = 0; + }; + auto update_stats = OnScopeLeave([&] { if (closed) return; - auto& s = session(); - if (!s.is_destroyed()) [[likely]] { - s.UpdatePacketTxTime(); - s.UpdateTimer(); - s.UpdateDataStats(); - } + // Flush any remaining accumulated packets before updating stats. + flush_batch(); + if (session().is_destroyed()) [[unlikely]] + return; + + // Get a strong pointer to protect against potential destruction during + // updating the time and data stats. + BaseObjectPtr s(session_); + s->UpdatePacketTxTime(); + s->UpdateTimer(); + s->UpdateDataStats(); }); // The maximum size of packet to create. @@ -258,26 +437,31 @@ void Session::Application::SendPendingData() { kMaxPackets, ngtcp2_conn_get_send_quantum(*session_) / max_packet_size); if (max_packet_count == 0) return; - // The number of packets that have been sent in this call to SendPendingData. + // The number of packets that have been prepared in this call. size_t packet_send_count = 0; Packet::Ptr packet; - uint8_t* pos = nullptr; - uint8_t* begin = nullptr; auto ensure_packet = [&] { if (!packet) { packet = CreateStreamDataPacket(); if (!packet) [[unlikely]] return false; - pos = begin = packet->data(); } DCHECK(packet); - DCHECK_NOT_NULL(pos); - DCHECK_NOT_NULL(begin); return true; }; + // Accumulate a completed packet into the batch. + auto enqueue_packet = + [&](Packet::Ptr& pkt, size_t len, const PacketInfo& pi) { + Debug(session_, "Enqueuing packet with %zu bytes into batch", len); + pkt->Truncate(len); + pkt->set_pkt_info(pi); + path.CopyTo(&batch_paths[batch_count]); + batch[batch_count++] = std::move(pkt); + }; + // We're going to enter a loop here to prepare and send no more than // max_packet_count packets. for (;;) { @@ -302,21 +486,44 @@ void Session::Application::SendPendingData() { } // If we got here, we were at least successful in checking for stream data. - // There might not be any stream data to send. + // There might not be any stream data to send. If there is no stream data, + // that's perfectly fine, we still need to serialize any frames we do have + // (pings, acks, datagrams, etc) so we'll just keep going. if (stream_data.id >= 0) { Debug(session_, "Application using stream data: %s", stream_data); + } else { + Debug(session_, "No stream data to send"); + } + if (session_->HasPendingDatagrams()) { + Debug(session_, "There are pending datagrams to send"); } // Awesome, let's write our packet! - ssize_t nwrite = - WriteVStream(&path, pos, &ndatalen, max_packet_size, stream_data); - + PacketInfo pi; + ssize_t nwrite = WriteVStream(&path, + &pi, + packet->data(), + &ndatalen, + packet->length(), + stream_data, + ts); + + // When ndatalen is > 0, that's our indication that stream data was accepted + // in to the packet. Yay! if (ndatalen > 0) { Debug(session_, "Application accepted %zu bytes from stream %" PRIi64 " into packet", ndatalen, stream_data.id); + if (!StreamCommit(&stream_data, ndatalen)) { + // Data was accepted into the packet, but for some reason adjusting + // the stream's committed data failed. Treat as fatal. + Debug(session_, "Failed to commit accepted bytes in stream"); + session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); + closed = true; + return session_->Close(CloseMethod::SILENT); + } } else if (stream_data.id >= 0) { Debug(session_, "Application did not accept any bytes from stream %" PRIi64 @@ -324,6 +531,23 @@ void Session::Application::SendPendingData() { stream_data.id); } + // When nwrite is zero, it means we are congestion limited or it is + // just not our turn to send something. Re-schedule the stream if it + // had unsent data (payload or FIN) so the next timer-triggered + // SendPendingData retries it. Without this, a FIN-only send that + // hits nwrite=0 is lost forever — the stream already returned EOS + // from Pull and won't be re-scheduled by anyone else. + // We call Application::ResumeStream directly (not Session::ResumeStream) + // to avoid creating a SendPendingDataScope — we're already inside + // SendPendingData and re-entering would just hit nwrite=0 again. + if (nwrite == 0) { + Debug(session_, "Congestion or not our turn to send"); + if (stream_data.id >= 0 && (stream_data.count > 0 || stream_data.fin)) { + ResumeStream(stream_data.id); + } + return; + } + // A negative nwrite value indicates either an error or that there is more // data to write into the packet. if (nwrite < 0) { @@ -344,7 +568,7 @@ void Session::Application::SendPendingData() { case NGTCP2_ERR_STREAM_SHUT_WR: { // Indicates that the writable side of the stream should be closed // locally or the stream is being reset. In either case, we can't send - // any stream data! + // data for this stream! Debug(session_, "Closing stream %" PRIi64 " for writing", stream_data.id); @@ -357,16 +581,35 @@ void Session::Application::SendPendingData() { if (stream_data.stream) [[likely]] { stream_data.stream->EndWritable(); } + // Notify the application that the stream's write side is shut + // so it stops queuing data. Without this, GetStreamData would + // keep returning the same stream and we'd loop forever. + StreamWriteShut(stream_data.id); continue; } case NGTCP2_ERR_WRITE_MORE: { - if (ndatalen >= 0 && !StreamCommit(&stream_data, ndatalen)) { - Debug(session_, - "Failed to commit stream data while writing packets"); - session_->SetLastError( - QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); - closed = true; - return session_->Close(CloseMethod::SILENT); + Debug(session_, "Packet buffer not full, coalesce more data into it"); + // Room for more in this packet. Try to pack a pending datagram + // if there is one. Otherwise just loop around and keep going. + if (session_->HasPendingDatagrams()) { + auto result = TryWritePendingDatagram( + &path, packet->data(), packet->length(), ts); + // When result is 0, either the datagram was congestion controlled, + // didn't fit in the packet, or was abandoned. Skip and continue. + + // When result is > 0, the packet is done and the result is the + // completed size of the packet we're sending. + if (result > 0) { + size_t len = result; + Debug(session_, "Sending packet with %zu bytes", len); + enqueue_packet(packet, len, pi); + if (++packet_send_count == max_packet_count) return; + } else if (result < 0) { + // Any negative result other than NGTCP2_ERR_WRITE_MORE + // at this point is fatal. The session will have been + // closed. + if (result != NGTCP2_ERR_WRITE_MORE) return; + } } continue; } @@ -390,61 +633,59 @@ void Session::Application::SendPendingData() { session_->SetLastError(QuicError::ForNgtcp2Error(nwrite)); closed = true; return session_->Close(CloseMethod::SILENT); - } else if (ndatalen >= 0 && !StreamCommit(&stream_data, ndatalen)) { - session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); - closed = true; - return session_->Close(CloseMethod::SILENT); } - // When nwrite is zero, it means we are congestion limited or it is - // just not our turn now to send something. Stop sending packets. - if (nwrite == 0) { - // If there was stream data selected, we should reschedule it to try - // sending again. - if (stream_data.id >= 0) ResumeStream(stream_data.id); - - // There might be a partial packet already prepared. If so, send it. - size_t datalen = pos - begin; - if (datalen) { - Debug(session_, "Sending packet with %zu bytes", datalen); - packet->Truncate(datalen); - session_->Send(std::move(packet), path); + // At this point we have a packet prepared to send. The nwrite + // is the size of the packet we are sending. + size_t len = nwrite; + Debug(session_, "Sending packet with %zu bytes", len); + enqueue_packet(packet, len, pi); + if (++packet_send_count == max_packet_count) return; + + // If there are pending datagrams, try sending them in a fresh packet. + // This is necessary because ngtcp2_conn_writev_stream only returns + // NGTCP2_ERR_WRITE_MORE when there is actual stream data — when no + // streams are active, the coalescing path above is never reached and + // datagrams would never be sent. + if (session_->HasPendingDatagrams()) { + if (!ensure_packet()) [[unlikely]] { + Debug(session_, "Failed to create packet for datagram"); + session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); + closed = true; + return session_->Close(CloseMethod::SILENT); } - // If no data, Ptr destructor releases the packet. - - return; - } - - // At this point we have a packet prepared to send. - pos += nwrite; - size_t datalen = pos - begin; - Debug(session_, "Sending packet with %zu bytes", datalen); - packet->Truncate(datalen); - session_->Send(std::move(packet), path); - - // If we have sent the maximum number of packets, we're done. - if (++packet_send_count == max_packet_count) { - return; + auto result = + TryWritePendingDatagram(&path, packet->data(), packet->length(), ts); + if (result > 0) { + Debug(session_, "Sending datagram packet with %zd bytes", result); + enqueue_packet(packet, static_cast(result), PacketInfo()); + if (++packet_send_count == max_packet_count) return; + } else if (result < 0 && result != NGTCP2_ERR_WRITE_MORE) { + // Fatal error — session already closed by TryWritePendingDatagram. + return; + } + // If result == 0 (congestion) or NGTCP2_ERR_WRITE_MORE (datagram + // packed but room for more), the loop continues normally. } - - // Prepare to loop back around to prepare a new packet. - // packet is already empty from the std::move above. - pos = begin = nullptr; } } ssize_t Session::Application::WriteVStream(PathStorage* path, + PacketInfo* pi, uint8_t* dest, ssize_t* ndatalen, size_t max_packet_size, - const StreamData& stream_data) { + const StreamData& stream_data, + uint64_t ts) { DCHECK_LE(stream_data.count, kMaxVectorCount); uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; if (stream_data.fin) flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + // The PacketInfo out-param is populated by ngtcp2 with the ECN codepoint + // to apply when sending this packet. When libuv gains per-socket ECN + // marking, the value should be forwarded to the send path. return ngtcp2_conn_writev_stream(*session_, &path->path, - // TODO(@jasnell): ECN blocked on libuv - nullptr, + *pi, dest, max_packet_size, ndatalen, @@ -452,9 +693,10 @@ ssize_t Session::Application::WriteVStream(PathStorage* path, stream_data.id, stream_data, stream_data.count, - uv_hrtime()); + ts); } +// ============================================================================ // The DefaultApplication is the default implementation of Session::Application // that is used for all unrecognized ALPN identifiers. class DefaultApplication final : public Session::Application { @@ -462,7 +704,10 @@ class DefaultApplication final : public Session::Application { // Marked NOLINT because the cpp linter gets confused about this using // statement not being sorted with the using v8 statements at the top // of the namespace. - using Application::Application; // NOLINT + DefaultApplication(Session* session, const Options& options) + : Session::Application(session, options), options_(options) {} + + const Options& options() const override { return options_; } Session::Application::Type type() const override { return Session::Application::Type::DEFAULT; @@ -470,7 +715,38 @@ class DefaultApplication final : public Session::Application { error_code GetNoErrorCode() const override { return 0; } - bool ReceiveStreamData(int64_t stream_id, + // Raw QUIC has no application-defined "general failure" code, so + // fall back to the QUIC transport-level INTERNAL_ERROR (0x1) used + // by ngtcp2 for unspecified failures. + error_code GetInternalErrorCode() const override { + return NGTCP2_INTERNAL_ERROR; + } + + void EarlyDataRejected() override { + // Destroy all open streams — ngtcp2 has already discarded their + // internal state when it rejected the early data. Use the + // application's internal error code since this is an error + // condition (code 0 would be treated as a clean close). + session().DestroyAllStreams( + QuicError::ForApplication(GetInternalErrorCode())); + if (!session().is_destroyed()) { + session().EmitEarlyDataRejected(); + } + } + + bool ApplySessionTicketData(const PendingTicketAppData& data) override { + return std::holds_alternative(data); + } + + bool ReceiveStreamOpen(stream_id id) override { + auto stream = session().CreateStream(id); + if (!stream || session().is_destroyed()) [[unlikely]] { + return !session().is_destroyed(); + } + return true; + } + + bool ReceiveStreamData(stream_id id, const uint8_t* data, size_t datalen, const Stream::ReceiveDataFlags& flags, @@ -478,10 +754,10 @@ class DefaultApplication final : public Session::Application { BaseObjectPtr stream; if (stream_user_data == nullptr) { // This is the first time we're seeing this stream. Implicitly create it. - stream = session().CreateStream(stream_id); - if (!stream) [[unlikely]] { - // We couldn't actually create the stream for whatever reason. - Debug(&session(), "Default application failed to create new stream"); + stream = session().CreateStream(id); + if (!stream || session().is_destroyed()) [[unlikely]] { + // We couldn't create the stream, or the session was destroyed + // during the onstream callback (via MakeCallback re-entrancy). return false; } } else { @@ -546,7 +822,6 @@ class DefaultApplication final : public Session::Application { if (count > 0) { stream->Schedule(&stream_queue_); - } else { } // Not calling done here because we defer committing @@ -569,14 +844,26 @@ class DefaultApplication final : public Session::Application { return 0; } - void ResumeStream(int64_t id) override { ScheduleStream(id); } + void ResumeStream(stream_id id) override { ScheduleStream(id); } - void BlockStream(int64_t id) override { + void BlockStream(stream_id id) override { if (auto stream = session().FindStream(id)) [[likely]] { + // Remove the stream from the send queue. It will be re-scheduled + // via ExtendMaxStreamData when the peer grants more flow control. + // Without this, SendPendingData would repeatedly pop and retry + // the same blocked stream in an infinite loop. + stream->Unschedule(); stream->EmitBlocked(); } } + void ExtendMaxStreamData(Stream* stream, uint64_t max_data) override { + // The peer granted more flow control for this stream. Re-schedule + // it so SendPendingData will resume writing. + DCHECK_NOT_NULL(stream); + stream->Schedule(&stream_queue_); + } + bool StreamCommit(StreamData* stream_data, size_t datalen) override { DCHECK_NOT_NULL(stream_data); CHECK(stream_data->stream); @@ -589,12 +876,14 @@ class DefaultApplication final : public Session::Application { SET_NO_MEMORY_INFO() private: - void ScheduleStream(int64_t id) { + void ScheduleStream(stream_id id) { if (auto stream = session().FindStream(id)) [[likely]] { stream->Schedule(&stream_queue_); } } + Options options_; + Stream::Queue stream_queue_; }; diff --git a/src/quic/application.h b/src/quic/application.h index 11ee977c44967c..0df9b9f0a0e68d 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -2,6 +2,9 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include +#include + #include "base_object.h" #include "bindingdata.h" #include "defs.h" @@ -11,34 +14,86 @@ namespace node::quic { +// Parsed session ticket application data, produced by +// Application::ParseTicketData() before ALPN negotiation and consumed +// by Application::ApplySessionTicketData() after. +struct DefaultTicketData {}; +struct Http3TicketData { + uint64_t max_field_section_size; + uint64_t qpack_max_dtable_capacity; + uint64_t qpack_encoder_max_dtable_capacity; + uint64_t qpack_blocked_streams; + bool enable_connect_protocol; + bool enable_datagrams; +}; +using PendingTicketAppData = + std::variant; + // An Application implements the ALPN-protocol specific semantics on behalf // of a QUIC Session. class Session::Application : public MemoryRetainer { public: using Options = Session::Application_Options; + Application(Session* session, const Options& options); + DISALLOW_COPY_AND_MOVE(Application) + + // Get the active options for this application. These may differ from the + // options passed at construction time since some options can be negotiated. + virtual const Options& options() const = 0; + // The type of Application, exposed via the session state so JS // can observe which Application was selected after ALPN negotiation. + // This is used primarily for testing/debugging. enum class Type : uint8_t { NONE = 0, // Not yet selected (server pre-negotiation) DEFAULT = 1, // DefaultApplication (non-h3 ALPN) HTTP3 = 2, // Http3ApplicationImpl (h3 / h3-XX ALPN) }; - - Application(Session* session, const Options& options); - DISALLOW_COPY_AND_MOVE(Application) - virtual Type type() const = 0; virtual bool Start(); + // Returns true if Start() has been called successfully. + virtual bool is_started() const { return false; } + + // Called when the server rejects 0-RTT early data. The application + // must destroy all streams that were opened during the 0-RTT phase + // since ngtcp2 has already discarded their internal state. + virtual void EarlyDataRejected() = 0; + + // The "no error code" is the application-level error code that signals + // "no error". Per the QUIC spec, this can vary by application protocol + // and is not necessarily 0. virtual error_code GetNoErrorCode() const = 0; + // The "internal error code" is the application-level error code used + // to signal a non-specific failure when no more specific code has + // been provided by the caller. For example, `writer.fail(reason)` on + // the JS side uses this code when `reason` is not a `QuicError` + // carrying an explicit code. For HTTP/3 this is + // `NGHTTP3_H3_INTERNAL_ERROR` (0x102); for raw QUIC applications + // there is no defined application code so we fall back to + // NGTCP2_INTERNAL_ERROR (0x1). + virtual error_code GetInternalErrorCode() const = 0; + + // Called after Session::Receive processes a packet, outside all callback + // scopes. Applications can use this to handle deferred operations that + // require calling into JS (e.g., HTTP/3 GOAWAY processing). + virtual void PostReceive() {} + + // Called when ngtcp2 notifies us that a new remote stream has been + // opened. The Application decides whether to create a Stream object + // (and fire the JS onstream callback) based on the stream type. For + // example, HTTP/3 only creates Stream objects for bidi streams since + // uni streams are managed internally by nghttp3. + virtual bool ReceiveStreamOpen(stream_id id) = 0; + // Session will forward all received stream data immediately on to the // Application. The only additional processing the Session does is to // automatically adjust the session-level flow control window. It is up to // the Application to do the same for the Stream-level flow control. - virtual bool ReceiveStreamData(int64_t stream_id, + virtual bool ReceiveStreamData(stream_id id, const uint8_t* data, size_t datalen, const Stream::ReceiveDataFlags& flags, @@ -46,22 +101,30 @@ class Session::Application : public MemoryRetainer { // Session will forward all data acknowledgements for a stream to the // Application. - virtual bool AcknowledgeStreamData(int64_t stream_id, size_t datalen); + virtual bool AcknowledgeStreamData(stream_id id, size_t datalen); // Called to determine if a Header can be added to this application. // Applications that do not support headers will always return false. virtual bool CanAddHeader(size_t current_count, size_t current_headers_length, - size_t this_header_length); + size_t this_header_length) { + return false; + } + + // Called when ngtcp2 reports NGTCP2_ERR_STREAM_SHUT_WR for a stream. + // Applications that manage their own framing (e.g., HTTP/3) must inform + // their protocol layer that the stream's write side is shut so it stops + // queuing data for that stream. The default is a no-op. + virtual void StreamWriteShut(stream_id id) {} // Called to mark the identified stream as being blocked. Not all // Application types will support blocked streams, and those that do will do // so differently. - virtual void BlockStream(int64_t id); + virtual void BlockStream(stream_id id) {} // Called when the session determines that there is outbound data available // to send for the given stream. - virtual void ResumeStream(int64_t id); + virtual void ResumeStream(stream_id id) {} // Called when the Session determines that the maximum number of // remotely-initiated unidirectional streams has been extended. Not all @@ -69,16 +132,27 @@ class Session::Application : public MemoryRetainer { // nothing. virtual void ExtendMaxStreams(EndpointLabel label, Direction direction, - uint64_t max_streams); + uint64_t max_streams) {} + + // Returns true if the application manages stream FIN internally (e.g., + // HTTP/3 uses nghttp3 which sends FIN via the fin flag in writev_stream). + // When true, the stream infrastructure must NOT call + // ngtcp2_conn_shutdown_stream_write when the JS write side ends — + // the application protocol layer handles it. + virtual bool stream_fin_managed_by_application() const { return false; } // Called when the Session determines that the flow control window for the // given stream has been expanded. Not all Application types will require // this notification so the default is to do nothing. - virtual void ExtendMaxStreamData(Stream* stream, uint64_t max_data); + virtual void ExtendMaxStreamData(Stream* stream, uint64_t max_data) { + Debug(session_, "Application extending max stream data"); + // By default do nothing. + } // Different Applications may wish to set some application data in the // session ticket (e.g. http/3 would set server settings in the application - // data). By default, there's nothing to set. + // data). The first byte written MUST be the Application::Type enum value. + // By default, writes just the type byte. virtual void CollectSessionTicketAppData( SessionTicket::AppData* app_data) const; @@ -89,17 +163,37 @@ class Session::Application : public MemoryRetainer { const SessionTicket::AppData& app_data, SessionTicket::AppData::Source::Flag flag); + // Validates parsed ticket data against current application options. + // Returns false if the stored settings are more permissive than the + // current config (e.g., a feature was enabled when the ticket was + // issued but is now disabled). + static bool ValidateTicketData(const PendingTicketAppData& data, + const Application_Options& options); + + // Parse session ticket app data before ALPN negotiation. Reads the + // type byte and dispatches to the appropriate application-specific + // parser. Returns std::nullopt if parsing fails. + static std::optional ParseTicketData( + const uv_buf_t& data); + + // Called after ALPN negotiation to validate and apply previously + // parsed session ticket app data. Returns false if the data is + // incompatible (e.g., type mismatch or settings downgrade), which + // causes the handshake to fail. + virtual bool ApplySessionTicketData(const PendingTicketAppData& data) = 0; + // Notifies the Application that the identified stream has been closed. - virtual void StreamClose(Stream* stream, QuicError&& error = QuicError()); + virtual void ReceiveStreamClose(Stream* stream, + QuicError&& error = QuicError()); // Notifies the Application that the identified stream has been reset. - virtual void StreamReset(Stream* stream, - uint64_t final_size, - QuicError&& error = QuicError()); + virtual void ReceiveStreamReset(Stream* stream, + uint64_t final_size, + QuicError&& error = QuicError()); // Notifies the Application that the identified stream should stop sending. - virtual void StreamStopSending(Stream* stream, - QuicError&& error = QuicError()); + virtual void ReceiveStreamStopSending(Stream* stream, + QuicError&& error = QuicError()); // Submits an outbound block of headers for the given stream. Not all // Application types will support headers, in which case this function @@ -107,31 +201,57 @@ class Session::Application : public MemoryRetainer { virtual bool SendHeaders(const Stream& stream, HeadersKind kind, const v8::Local& headers, - HeadersFlags flags = HeadersFlags::NONE); + HeadersFlags flags = HeadersFlags::NONE) { + return false; + } // Signals to the Application that it should serialize and transmit any // pending session and stream packets it has accumulated. void SendPendingData(); + // Returns true if the application protocol supports sending and + // receiving headers on streams (e.g. HTTP/3). Applications that + // do not support headers should return false (the default). + virtual bool SupportsHeaders() const { return false; } + + // Initiates application-level graceful shutdown signaling (e.g., + // HTTP/3 GOAWAY). Called when Session::Close(GRACEFUL) is invoked. + virtual void BeginShutdown() {} + + // Completes the application-level graceful shutdown. Called from + // FinishClose() before CONNECTION_CLOSE is sent. For HTTP/3, this + // sends the final GOAWAY with the actual last accepted stream ID. + virtual void CompleteShutdown() {} + // Set the priority level of the stream if supported by the application. Not // all applications support priorities, in which case this function is a // non-op. virtual void SetStreamPriority( const Stream& stream, StreamPriority priority = StreamPriority::DEFAULT, - StreamPriorityFlags flags = StreamPriorityFlags::NONE); + StreamPriorityFlags flags = StreamPriorityFlags::NON_INCREMENTAL) {} + + struct StreamPriorityResult { + StreamPriority priority; + StreamPriorityFlags flags; + }; // Get the priority level of the stream if supported by the application. Not // all applications support priorities, in which case this function returns // the default stream priority. - virtual StreamPriority GetStreamPriority(const Stream& stream); + virtual StreamPriorityResult GetStreamPriority(const Stream& stream) { + return {StreamPriority::DEFAULT, StreamPriorityFlags::NON_INCREMENTAL}; + } + // The StreamData struct is used by the application to pass pending stream + // data to the session for transmission. struct StreamData; virtual int GetStreamData(StreamData* data) = 0; virtual bool StreamCommit(StreamData* data, size_t datalen) = 0; inline Environment* env() const { return session().env(); } + inline Session& session() { CHECK_NOT_NULL(session_); return *session_; @@ -144,12 +264,26 @@ class Session::Application : public MemoryRetainer { private: Packet::Ptr CreateStreamDataPacket(); - // Write the given stream_data into the buffer. + // Tries to pack a pending datagram into the current packet buffer. + // If < 0 is returned, either NGTCP2_ERR_WRITE_MORE or a fatal error is + // returned; the caller must check. If > 0 is returned, the packet is done + // and the value is the size of the finalized packet. If 0 is returned, + // the datagram is either congestion limited or was abandoned + ssize_t TryWritePendingDatagram(PathStorage* path, + uint8_t* dest, + size_t destlen, + uint64_t ts); + + // Write the given stream_data into the buffer. The PacketInfo out-param + // is populated by ngtcp2 with per-packet metadata (e.g., ECN codepoint) + // that should be applied when sending the packet. ssize_t WriteVStream(PathStorage* path, + PacketInfo* pi, uint8_t* buf, ssize_t* ndatalen, size_t max_packet_size, - const StreamData& stream_data); + const StreamData& stream_data, + uint64_t ts); Session* session_ = nullptr; }; @@ -159,7 +293,7 @@ struct Session::Application::StreamData final { size_t count = 0; // The stream identifier. If this is a negative value then no stream is // identified. - int64_t id = -1; + stream_id id = -1; int fin = 0; ngtcp2_vec data[kMaxVectorCount]{}; BaseObjectPtr stream; diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index a8b72900d5a60c..31467a8477a792 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -11,13 +11,20 @@ #include #include #include +#include #include #include "bindingdata.h" +#include "session.h" +#include "session_manager.h" namespace node { +using mem::kReserveSizeAndAlign; +using v8::DictionaryTemplate; using v8::Function; using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Isolate; using v8::Local; using v8::Object; using v8::String; @@ -25,24 +32,231 @@ using v8::Value; namespace quic { +// ============================================================================ +// Thread-local QUIC allocator. +// +// Both ngtcp2 and nghttp3 take an allocator struct (ngtcp2_mem / +// nghttp3_mem) whose pointer is stored inside every object they +// allocate. Some of those objects — notably nghttp3 rcbufs backing +// V8 external strings — can outlive the BindingData that created them +// (freed during V8 isolate teardown, after Environment cleanup). +// +// To handle this safely, both allocators live in a thread-local static +// struct that is never destroyed. Memory tracking goes through the +// BindingData pointer when it is alive and is silently skipped during +// teardown (after ~BindingData nulls the pointer). +// +// The allocation functions use the same prepended-size-header scheme as +// NgLibMemoryManager (node_mem-inl.h) so that frees always know the +// allocation size regardless of whether BindingData is still around. + +namespace { +struct QuicAllocState { + BindingData* binding = nullptr; + ngtcp2_mem ngtcp2 = {}; + nghttp3_mem nghttp3 = {}; +}; +thread_local QuicAllocState quic_alloc_state; + +// Core allocation functions shared by both ngtcp2 and nghttp3. +// user_data always points to the thread-local QuicAllocState. + +void* QuicRealloc(void* ptr, size_t size, void* user_data) { + auto* state = static_cast(user_data); + + size_t previous_size = 0; + char* original_ptr = nullptr; + + if (size > 0) size += kReserveSizeAndAlign; + + if (ptr != nullptr) { + original_ptr = static_cast(ptr) - kReserveSizeAndAlign; + previous_size = *reinterpret_cast(original_ptr); + if (previous_size == 0) { + char* ret = UncheckedRealloc(original_ptr, size); + if (ret != nullptr) ret += kReserveSizeAndAlign; + return ret; + } + } + + if (state->binding) { + state->binding->CheckAllocatedSize(previous_size); + } + + char* mem = UncheckedRealloc(original_ptr, size); + + if (mem != nullptr) { + const int64_t new_size = size - previous_size; + if (state->binding) { + state->binding->IncreaseAllocatedSize(new_size); + state->binding->env()->external_memory_accounter()->Update( + state->binding->env()->isolate(), new_size); + } + *reinterpret_cast(mem) = size; + mem += kReserveSizeAndAlign; + } else if (size == 0) { + if (state->binding) { + state->binding->DecreaseAllocatedSize(previous_size); + state->binding->env()->external_memory_accounter()->Decrease( + state->binding->env()->isolate(), previous_size); + } + } + return mem; +} + +void* QuicMalloc(size_t size, void* user_data) { + return QuicRealloc(nullptr, size, user_data); +} + +void QuicFree(void* ptr, void* user_data) { + if (ptr == nullptr) return; + CHECK_NULL(QuicRealloc(ptr, 0, user_data)); +} + +void* QuicCalloc(size_t nmemb, size_t size, void* user_data) { + size_t real_size = MultiplyWithOverflowCheck(nmemb, size); + void* mem = QuicMalloc(real_size, user_data); + if (mem != nullptr) memset(mem, 0, real_size); + return mem; +} + +// Thin wrappers with the correct function-pointer types for each +// library. The signatures happen to be identical today, but keeping +// them separate avoids ABI coupling between ngtcp2 and nghttp3. + +void* Ngtcp2Malloc(size_t size, void* ud) { + return QuicMalloc(size, ud); +} +void Ngtcp2Free(void* ptr, void* ud) { + QuicFree(ptr, ud); +} +void* Ngtcp2Calloc(size_t n, size_t s, void* ud) { + return QuicCalloc(n, s, ud); +} +void* Ngtcp2Realloc(void* ptr, size_t size, void* ud) { + return QuicRealloc(ptr, size, ud); +} + +void* Nghttp3Malloc(size_t size, void* ud) { + return QuicMalloc(size, ud); +} +void Nghttp3Free(void* ptr, void* ud) { + QuicFree(ptr, ud); +} +void* Nghttp3Calloc(size_t n, size_t s, void* ud) { + return QuicCalloc(n, s, ud); +} +void* Nghttp3Realloc(void* ptr, size_t size, void* ud) { + return QuicRealloc(ptr, size, ud); +} +} // namespace + +// ============================================================================ +// CheckWrap / CheckWrapHandle + +void CheckWrap::Start() { + if (check_.data == nullptr) return; + uv_check_start(&check_, OnCheck); +} + +void CheckWrap::Stop() { + if (check_.data == nullptr) return; + uv_check_stop(&check_); +} + +void CheckWrap::Close() { + check_.data = nullptr; + env_->CloseHandle(reinterpret_cast(&check_), CheckClosedCb); +} + +void CheckWrap::Ref() { + if (check_.data == nullptr) return; + uv_ref(reinterpret_cast(&check_)); +} + +void CheckWrap::Unref() { + if (check_.data == nullptr) return; + uv_unref(reinterpret_cast(&check_)); +} + +void CheckWrap::OnCheck(uv_check_t* check) { + CheckWrap* wrap = ContainerOf(&CheckWrap::check_, check); + wrap->fn_(); +} + +void CheckWrap::CheckClosedCb(uv_handle_t* handle) { + std::unique_ptr ptr( + ContainerOf(&CheckWrap::check_, reinterpret_cast(handle))); +} + +void CheckWrapHandle::Start() { + if (check_ != nullptr) check_->Start(); +} + +void CheckWrapHandle::Stop() { + if (check_ != nullptr) check_->Stop(); +} + +void CheckWrapHandle::Close() { + if (check_ != nullptr) { + check_->env()->RemoveCleanupHook(CleanupHook, this); + check_->Close(); + } + check_ = nullptr; +} + +void CheckWrapHandle::Ref() { + if (check_ != nullptr) check_->Ref(); +} + +void CheckWrapHandle::Unref() { + if (check_ != nullptr) check_->Unref(); +} + +void CheckWrapHandle::MemoryInfo(MemoryTracker* tracker) const { + if (check_ != nullptr) tracker->TrackField("check", *check_); +} + +void CheckWrapHandle::CleanupHook(void* data) { + static_cast(data)->Close(); +} + +// ============================================================================ + BindingData& BindingData::Get(Environment* env) { return *(env->principal_realm()->GetBindingData()); } -BindingData::operator ngtcp2_mem() { - return MakeAllocator(); +BindingData::~BindingData() { + quic_alloc_state.binding = nullptr; + // flush_check_ is cleaned up by ~CheckWrapHandle() after the destructor + // body completes. The inner CheckWrap (and its uv_check_t) will be freed + // later by the uv_close callback, after CleanupHandles() runs uv_run(). + pending_flush_sessions_.clear(); +} + +ngtcp2_mem* BindingData::ngtcp2_allocator() { + quic_alloc_state.binding = this; + quic_alloc_state.ngtcp2 = { + &quic_alloc_state, + Ngtcp2Malloc, + Ngtcp2Free, + Ngtcp2Calloc, + Ngtcp2Realloc, + }; + return &quic_alloc_state.ngtcp2; } -BindingData::operator nghttp3_mem() { - ngtcp2_mem allocator = *this; - nghttp3_mem http3_allocator = { - allocator.user_data, - allocator.malloc, - allocator.free, - allocator.calloc, - allocator.realloc, +nghttp3_mem* BindingData::nghttp3_allocator() { + quic_alloc_state.binding = this; + quic_alloc_state.nghttp3 = { + &quic_alloc_state, + Nghttp3Malloc, + Nghttp3Free, + Nghttp3Calloc, + Nghttp3Realloc, }; - return http3_allocator; + return &quic_alloc_state.nghttp3; } void BindingData::CheckAllocatedSize(size_t previous_size) const { @@ -59,7 +273,20 @@ void BindingData::DecreaseAllocatedSize(size_t size) { current_ngtcp2_memory_ -= size; } +// Forwards detailed(verbose) debugging information from nghttp3. Enabled using +// the NODE_DEBUG_NATIVE=NGHTTP3 category. +void nghttp3_debug_log(const char* fmt, va_list args) { + auto isolate = Isolate::GetCurrent(); + if (isolate == nullptr) return; + auto env = Environment::GetCurrent(isolate); + if (env->enabled_debug_list()->enabled(DebugCategory::NGHTTP3)) { + fprintf(stderr, "nghttp3 "); + vfprintf(stderr, fmt, args); + } +} + void BindingData::InitPerContext(Realm* realm, Local target) { + nghttp3_set_debug_vprintf_callback(nghttp3_debug_log); SetMethod(realm->context(), target, "setCallbacks", SetCallbacks); Realm::GetCurrent(realm->context())->AddBindingData(target); } @@ -71,8 +298,56 @@ void BindingData::RegisterExternalReferences( } BindingData::BindingData(Realm* realm, Local object) - : BaseObject(realm, object) { + : BaseObject(realm, object), + flush_check_(env(), [this]() { OnFlushCheck(); }) { MakeWeak(); + // Unref so the check handle doesn't keep the event loop alive on its own. + flush_check_.Unref(); +} + +SessionManager& BindingData::session_manager() { + if (!session_manager_) { + session_manager_ = std::make_unique(); + } + return *session_manager_; +} + +void BindingData::ScheduleSessionFlush(const BaseObjectPtr& session) { + pending_flush_sessions_.push_back(session); + if (!flush_check_started_) { + flush_check_.Start(); + flush_check_started_ = true; + } +} + +void BindingData::OnFlushCheck() { + if (pending_flush_sessions_.empty()) { + flush_check_.Stop(); + flush_check_started_ = false; + return; + } + + HandleScope scope(env()->isolate()); + + // Swap to a local vector before iterating. SendPendingData may trigger + // MakeCallback which runs JS that could cause more packet receives via + // re-entry (e.g., a stream data callback that synchronously writes to + // another session). Any sessions added during the flush remain in + // pending_flush_sessions_ and are picked up on the next check tick. + auto sessions = std::move(pending_flush_sessions_); + for (auto& session : sessions) { + session->flags_.pending_flush = false; + if (!session->is_destroyed()) { + session->FlushPendingData(); + } + } + + // If no new sessions were added during the flush, stop the check + // to avoid per-tick callback overhead when idle. + if (pending_flush_sessions_.empty()) { + flush_check_.Stop(); + flush_check_started_ = false; + } } void BindingData::MemoryInfo(MemoryTracker* tracker) const { @@ -103,6 +378,26 @@ QUIC_CONSTRUCTORS(V) #undef V +void BindingData::set_transport_params_template( + Local tmpl) { + transport_params_template_.Reset(env()->isolate(), tmpl); +} + +Local BindingData::transport_params_template() const { + return PersistentToLocal::Default(env()->isolate(), + transport_params_template_); +} + +void BindingData::set_application_options_template( + Local tmpl) { + application_options_template_.Reset(env()->isolate(), tmpl); +} + +Local BindingData::application_options_template() const { + return PersistentToLocal::Default(env()->isolate(), + application_options_template_); +} + #define V(name, _) \ void BindingData::set_##name##_callback(Local fn) { \ name##_callback_.Reset(env()->isolate(), fn); \ @@ -140,6 +435,14 @@ QUIC_JS_CALLBACKS(V) #undef V +Local BindingData::error_name_string(const char* name) { + auto& slot = error_name_strings_[name]; + if (slot.IsEmpty()) { + slot.Set(env()->isolate(), OneByteString(env()->isolate(), name)); + } + return slot.Get(env()->isolate()); +} + JS_METHOD_IMPL(BindingData::SetCallbacks) { auto env = Environment::GetCurrent(args); auto isolate = env->isolate(); @@ -162,36 +465,31 @@ JS_METHOD_IMPL(BindingData::SetCallbacks) { #undef V } -NgTcp2CallbackScope::NgTcp2CallbackScope(Environment* env) : env(env) { - auto& binding = BindingData::Get(env); - CHECK(!binding.in_ngtcp2_callback_scope); - binding.in_ngtcp2_callback_scope = true; +NgTcp2CallbackScope::NgTcp2CallbackScope(Session* session) : session(session) { + CHECK(!session->flags_.in_ngtcp2_callback_scope); + session->flags_.in_ngtcp2_callback_scope = true; } NgTcp2CallbackScope::~NgTcp2CallbackScope() { - auto& binding = BindingData::Get(env); - binding.in_ngtcp2_callback_scope = false; -} - -bool NgTcp2CallbackScope::in_ngtcp2_callback(Environment* env) { - auto& binding = BindingData::Get(env); - return binding.in_ngtcp2_callback_scope; + session->flags_.in_ngtcp2_callback_scope = false; + if (session->flags_.destroy_deferred) { + session->flags_.destroy_deferred = false; + session->Destroy(); + } } -NgHttp3CallbackScope::NgHttp3CallbackScope(Environment* env) : env(env) { - auto& binding = BindingData::Get(env); - CHECK(!binding.in_nghttp3_callback_scope); - binding.in_nghttp3_callback_scope = true; +NgHttp3CallbackScope::NgHttp3CallbackScope(Session* session) + : session(session) { + CHECK(!session->flags_.in_nghttp3_callback_scope); + session->flags_.in_nghttp3_callback_scope = true; } NgHttp3CallbackScope::~NgHttp3CallbackScope() { - auto& binding = BindingData::Get(env); - binding.in_nghttp3_callback_scope = false; -} - -bool NgHttp3CallbackScope::in_nghttp3_callback(Environment* env) { - auto& binding = BindingData::Get(env); - return binding.in_nghttp3_callback_scope; + session->flags_.in_nghttp3_callback_scope = false; + if (session->flags_.destroy_deferred) { + session->flags_.destroy_deferred = false; + session->Destroy(); + } } CallbackScopeBase::CallbackScopeBase(Environment* env) diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 7dee5713558619..7879220e02b482 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -10,21 +10,26 @@ #include #include #include +#include #include +#include +#include #include +#include #include "defs.h" namespace node::quic { class Endpoint; class Packet; +class Session; +class SessionManager; // ============================================================================ // The FunctionTemplates the BindingData will store for us. #define QUIC_CONSTRUCTORS(V) \ V(endpoint) \ - V(logstream) \ V(session) \ V(stream) \ V(udp) @@ -36,37 +41,51 @@ class Packet; #define QUIC_JS_CALLBACKS(V) \ V(endpoint_close, EndpointClose) \ V(session_close, SessionClose) \ + V(session_application, SessionApplication) \ + V(session_early_data_rejected, SessionEarlyDataRejected) \ + V(session_goaway, SessionGoaway) \ V(session_datagram, SessionDatagram) \ V(session_datagram_status, SessionDatagramStatus) \ V(session_handshake, SessionHandshake) \ + V(session_keylog, SessionKeyLog) \ + V(session_qlog, SessionQlog) \ V(session_new, SessionNew) \ V(session_new_token, SessionNewToken) \ + V(session_origin, SessionOrigin) \ V(session_path_validation, SessionPathValidation) \ V(session_ticket, SessionTicket) \ V(session_version_negotiation, SessionVersionNegotiation) \ V(stream_blocked, StreamBlocked) \ V(stream_close, StreamClose) \ V(stream_created, StreamCreated) \ + V(stream_drain, StreamDrain) \ V(stream_headers, StreamHeaders) \ V(stream_reset, StreamReset) \ V(stream_trailers, StreamTrailers) // The various JS strings the implementation uses. #define QUIC_STRINGS(V) \ + V(abandoned, "abandoned") \ V(aborted, "aborted") \ V(acknowledged, "acknowledged") \ V(ack_delay_exponent, "ackDelayExponent") \ V(active_connection_id_limit, "activeConnectionIDLimit") \ V(address_lru_size, "addressLRUSize") \ + V(allow, "allow") \ V(application, "application") \ + V(authoritative, "authoritative") \ V(bbr, "bbr") \ V(ca, "ca") \ V(cc_algorithm, "cc") \ V(certs, "certs") \ + V(code, "code") \ V(ciphers, "ciphers") \ V(crl, "crl") \ V(cubic, "cubic") \ + V(datagram_drop_policy, "datagramDropPolicy") \ + V(deny, "deny") \ V(disable_stateless_reset, "disableStatelessReset") \ + V(draining_period_multiplier, "drainingPeriodMultiplier") \ V(enable_connect_protocol, "enableConnectProtocol") \ V(enable_early_data, "enableEarlyData") \ V(enable_datagrams, "enableDatagrams") \ @@ -77,6 +96,8 @@ class Packet; V(groups, "groups") \ V(handshake_timeout, "handshakeTimeout") \ V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ + V(initial_rtt, "initialRtt") \ + V(keep_alive_timeout, "keepAlive") \ V(initial_max_data, "initialMaxData") \ V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \ V(initial_max_stream_data_bidi_remote, "initialMaxStreamDataBidiRemote") \ @@ -87,28 +108,44 @@ class Packet; V(reuse_port, "reusePort") \ V(keylog, "keylog") \ V(keys, "keys") \ - V(logstream, "LogStream") \ V(lost, "lost") \ V(max_ack_delay, "maxAckDelay") \ V(max_connections_per_host, "maxConnectionsPerHost") \ V(max_connections_total, "maxConnectionsTotal") \ V(max_datagram_frame_size, "maxDatagramFrameSize") \ + V(max_datagram_send_attempts, "maxDatagramSendAttempts") \ + V(stream_idle_timeout, "streamIdleTimeout") \ V(max_field_section_size, "maxFieldSectionSize") \ V(max_header_length, "maxHeaderLength") \ V(max_header_pairs, "maxHeaderPairs") \ + V(idle_timeout, "idleTimeout") \ V(max_idle_timeout, "maxIdleTimeout") \ V(max_payload_size, "maxPayloadSize") \ - V(max_retries, "maxRetries") \ - V(max_stateless_resets, "maxStatelessResetsPerHost") \ + V(retry_rate, "retryRate") \ + V(retry_burst, "retryBurst") \ + V(stateless_reset_rate, "statelessResetRate") \ + V(stateless_reset_burst, "statelessResetBurst") \ + V(version_negotiation_rate, "versionNegotiationRate") \ + V(version_negotiation_burst, "versionNegotiationBurst") \ + V(immediate_close_rate, "immediateCloseRate") \ + V(immediate_close_burst, "immediateCloseBurst") \ + V(session_creation_rate, "sessionCreationRate") \ + V(session_creation_burst, "sessionCreationBurst") \ + V(block_list, "blockList") \ + V(block_list_policy, "blockListPolicy") \ V(max_stream_window, "maxStreamWindow") \ V(max_window, "maxWindow") \ V(min_version, "minVersion") \ + V(port, "port") \ + V(preferred_address_ipv4, "preferredAddressIpv4") \ + V(preferred_address_ipv6, "preferredAddressIpv6") \ V(preferred_address_strategy, "preferredAddressPolicy") \ V(alpn, "alpn") \ V(qlog, "qlog") \ V(qpack_blocked_streams, "qpackBlockedStreams") \ V(qpack_encoder_max_dtable_capacity, "qpackEncoderMaxDTableCapacity") \ V(qpack_max_dtable_capacity, "qpackMaxDTableCapacity") \ + V(reason, "reason") \ V(reject_unauthorized, "rejectUnauthorized") \ V(reno, "reno") \ V(reset_token_secret, "resetTokenSecret") \ @@ -123,7 +160,9 @@ class Packet; V(token, "token") \ V(token_expiration, "tokenExpiration") \ V(token_secret, "tokenSecret") \ + V(transport, "transport") \ V(transport_params, "transportParams") \ + V(type, "type") \ V(tx_loss, "txDiagnosticLoss") \ V(udp_receive_buffer_size, "udpReceiveBufferSize") \ V(udp_send_buffer_size, "udpSendBufferSize") \ @@ -131,9 +170,86 @@ class Packet; V(unacknowledged_packet_threshold, "unacknowledgedPacketThreshold") \ V(validate_address, "validateAddress") \ V(verify_client, "verifyClient") \ + V(verify_hostname, "verifyHostname") \ + V(verify_peer_strict, "verifyPeerStrict") \ V(verify_private_key, "verifyPrivateKey") \ V(version, "version") +// ============================================================================= +// Lightweight wrappers around uv_check_t that ensure safe handle closure. +// The check handle is embedded in a heap-allocated CheckWrap whose destruction +// is deferred until the uv_close callback fires, preventing use-after-free +// when the owning object is destroyed before libuv finishes closing the handle. +// Follows the same two-layer pattern as TimerWrap / TimerWrapHandle +// (see timer_wrap.h). +// TODO(@jasnell): Consider moving it out to a separate file like timer_wrap.h. +class CheckWrap final : public MemoryRetainer { + public: + using CheckCb = std::function; + + template + explicit CheckWrap(Environment* env, Args&&... args) + : env_(env), fn_(std::forward(args)...) { + uv_check_init(env->event_loop(), &check_); + check_.data = this; + } + + DISALLOW_COPY_AND_MOVE(CheckWrap) + + inline Environment* env() const { return env_; } + + void Start(); + void Stop(); + void Close(); + void Ref(); + void Unref(); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(CheckWrap) + SET_SELF_SIZE(CheckWrap) + + private: + static void OnCheck(uv_check_t* check); + static void CheckClosedCb(uv_handle_t* handle); + ~CheckWrap() = default; + + Environment* env_; + CheckCb fn_; + uv_check_t check_; + + friend std::unique_ptr::deleter_type; +}; + +class CheckWrapHandle : public MemoryRetainer { + public: + template + explicit CheckWrapHandle(Environment* env, Args&&... args) + : check_(new CheckWrap(env, std::forward(args)...)) { + env->AddCleanupHook(CleanupHook, this); + } + + DISALLOW_COPY_AND_MOVE(CheckWrapHandle) + + ~CheckWrapHandle() { Close(); } + + inline operator bool() const { return check_ != nullptr; } + + void Start(); + void Stop(); + void Close(); + void Ref(); + void Unref(); + + void MemoryInfo(node::MemoryTracker* tracker) const override; + + SET_MEMORY_INFO_NAME(CheckWrapHandle) + SET_SELF_SIZE(CheckWrapHandle) + + private: + static void CleanupHook(void* data); + CheckWrap* check_; +}; + // ============================================================================= // The BindingState object holds state for the internalBinding('quic') binding // instance. It is mostly used to hold the persistent constructors, strings, and @@ -152,27 +268,46 @@ class BindingData final static inline BindingData& Get(Realm* realm) { return Get(realm->env()); } BindingData(Realm* realm, v8::Local object); + ~BindingData() override; DISALLOW_COPY_AND_MOVE(BindingData) void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(BindingData) SET_SELF_SIZE(BindingData) - // NgLibMemoryManager - operator ngtcp2_mem(); - operator nghttp3_mem(); + // NgLibMemoryManager — the base class provides CheckAllocatedSize, + // IncreaseAllocatedSize, DecreaseAllocatedSize, and StopTrackingMemory. + // Actual allocations go through the thread-local allocators below. void CheckAllocatedSize(size_t previous_size) const; void IncreaseAllocatedSize(size_t size); void DecreaseAllocatedSize(size_t size); + // Thread-local allocators that outlive BindingData destruction. + // Both ngtcp2 and nghttp3 store the allocator pointer inside every + // object they allocate; some of those objects (e.g., nghttp3 rcbufs + // backing V8 external strings) can be freed after BindingData is gone. + ngtcp2_mem* ngtcp2_allocator(); + nghttp3_mem* nghttp3_allocator(); + // Installs the set of JavaScript callback functions that are used to // bridge out to the JS API. JS_METHOD(SetCallbacks); + // Lazily-created per-Realm SessionManager. Centralizes CID -> Session + // routing so that any endpoint can route packets to any session. + SessionManager& session_manager(); + + // Schedule a session for deferred SendPendingData. Sessions are accumulated + // during the I/O poll phase (via Endpoint::Receive -> Session::ReadPacket) + // and flushed in a uv_check callback immediately after poll completes. + // This batches multiple received packets before generating responses, + // allowing ngtcp2 to make better ACK coalescing decisions. + void ScheduleSessionFlush(const BaseObjectPtr& session); + std::unordered_map> listening_endpoints; - bool in_ngtcp2_callback_scope = false; - bool in_nghttp3_callback_scope = false; + v8::Local error_name_string(const char* name); + size_t current_ngtcp2_memory_ = 0; // The following set up various storage and accessors for common strings, @@ -186,6 +321,12 @@ class BindingData final QUIC_CONSTRUCTORS(V) #undef V + void set_transport_params_template(v8::Local tmpl); + v8::Local transport_params_template() const; + + void set_application_options_template(v8::Local tmpl); + v8::Local application_options_template() const; + #define V(name, _) \ void set_##name##_callback(v8::Local fn); \ v8::Local name##_callback() const; @@ -204,6 +345,9 @@ class BindingData final QUIC_CONSTRUCTORS(V) #undef V + v8::Global transport_params_template_; + v8::Global application_options_template_; + #define V(name, _) v8::Global name##_callback_; QUIC_JS_CALLBACKS(V) #undef V @@ -215,6 +359,34 @@ class BindingData final #define V(name, _) mutable v8::Eternal on_##name##_string_; QUIC_JS_CALLBACKS(V) #undef V + + // Lazy cache backing error_name_string() + std::unordered_map> error_name_strings_; + + std::unique_ptr session_manager_; + + // Type-erased arena storage. The concrete AliasedStructArena types + // are only complete in the .cc files where Stream::State etc. are defined. + // Each .cc file provides typed accessor methods. The deleters are set + // when the arenas are created so that ~BindingData destroys them correctly. + using ArenaDeleter = void (*)(void*); + using ArenaPtr = std::unique_ptr; + ArenaPtr stream_state_arena_{nullptr, +[](void*) {}}; + ArenaPtr stream_stats_arena_{nullptr, +[](void*) {}}; + ArenaPtr session_state_arena_{nullptr, +[](void*) {}}; + ArenaPtr session_stats_arena_{nullptr, +[](void*) {}}; + ArenaPtr endpoint_state_arena_{nullptr, +[](void*) {}}; + ArenaPtr endpoint_stats_arena_{nullptr, +[](void*) {}}; + + // Deferred send flush state. The CheckWrapHandle fires immediately after + // the I/O poll phase in the same event loop tick, allowing batched + // receive processing: all packets are read during poll, then + // SendPendingData is called once per dirty session in the check callback. + CheckWrapHandle flush_check_; + std::vector> pending_flush_sessions_; + bool flush_check_started_ = false; + + void OnFlushCheck(); }; JS_METHOD_IMPL(IllegalConstructor); @@ -222,20 +394,22 @@ JS_METHOD_IMPL(IllegalConstructor); // The ngtcp2 and nghttp3 callbacks have certain restrictions // that forbid re-entry. We provide the following scopes for // use in those to help protect against it. +// These callback scopes are per-session, not per-environment. This ensures +// that one session's ngtcp2/nghttp3 callback does not block an unrelated +// session from sending packets. A BaseObjectPtr prevents the Session from +// being prematurely freed while the scope is alive on the stack. struct NgTcp2CallbackScope final { - Environment* env; - explicit NgTcp2CallbackScope(Environment* env); + BaseObjectPtr session; + explicit NgTcp2CallbackScope(Session* session); DISALLOW_COPY_AND_MOVE(NgTcp2CallbackScope) ~NgTcp2CallbackScope(); - static bool in_ngtcp2_callback(Environment* env); }; struct NgHttp3CallbackScope final { - Environment* env; - explicit NgHttp3CallbackScope(Environment* env); + BaseObjectPtr session; + explicit NgHttp3CallbackScope(Session* session); DISALLOW_COPY_AND_MOVE(NgHttp3CallbackScope) ~NgHttp3CallbackScope(); - static bool in_nghttp3_callback(Environment* env); }; struct CallbackScopeBase { diff --git a/src/quic/data.cc b/src/quic/data.cc index f43ae4ce6edbc4..9599adec62f805 100644 --- a/src/quic/data.cc +++ b/src/quic/data.cc @@ -1,13 +1,15 @@ #if HAVE_OPENSSL && HAVE_QUIC #include "guard.h" #ifndef OPENSSL_NO_QUIC -#include "data.h" #include #include #include #include +#include #include #include +#include "bindingdata.h" +#include "data.h" #include "defs.h" #include "util.h" @@ -17,7 +19,10 @@ using v8::Array; using v8::ArrayBuffer; using v8::ArrayBufferView; using v8::BackingStore; +using v8::BackingStoreInitializationMode; +using v8::BackingStoreOnFailureMode; using v8::BigInt; +using v8::Isolate; using v8::Just; using v8::Local; using v8::Maybe; @@ -88,52 +93,80 @@ Store::Store(std::unique_ptr store, size_t length, size_t offset) CHECK_LE(length_, store_->ByteLength() - offset_); } -Maybe Store::From(Local buffer, Local detach_key) { - if (!buffer->IsDetachable()) { - return Nothing(); - } - bool res; - auto backing = buffer->GetBackingStore(); +Maybe Store::From(Local buffer) { + Isolate* isolate = Isolate::GetCurrent(); + Environment* env = Environment::GetCurrent(isolate->GetCurrentContext()); auto length = buffer->ByteLength(); - if (!buffer->Detach(detach_key).To(&res) || !res) { + auto dest = ArrayBuffer::NewBackingStore( + isolate, + length, + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); + if (!dest) { + THROW_ERR_MEMORY_ALLOCATION_FAILED(env); return Nothing(); } - return Just(Store(std::move(backing), length, 0)); + if (length > 0) { + memcpy(dest->Data(), buffer->Data(), length); + } + return Just(Store(std::move(dest), length, 0)); } -Maybe Store::From(Local view, Local detach_key) { - if (!view->Buffer()->IsDetachable()) { - return Nothing(); - } - bool res; - auto backing = view->Buffer()->GetBackingStore(); +Maybe Store::From(Local view) { + Isolate* isolate = Isolate::GetCurrent(); + Environment* env = Environment::GetCurrent(isolate->GetCurrentContext()); auto length = view->ByteLength(); auto offset = view->ByteOffset(); - if (!view->Buffer()->Detach(detach_key).To(&res) || !res) { + auto dest = ArrayBuffer::NewBackingStore( + isolate, + length, + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); + if (!dest) { + THROW_ERR_MEMORY_ALLOCATION_FAILED(env); return Nothing(); } - return Just(Store(std::move(backing), length, offset)); + if (length > 0) { + memcpy(dest->Data(), + static_cast(view->Buffer()->Data()) + offset, + length); + } + return Just(Store(std::move(dest), length, 0)); } Store Store::CopyFrom(Local buffer) { - v8::Isolate* isolate = v8::Isolate::GetCurrent(); + Isolate* isolate = Isolate::GetCurrent(); auto backing = buffer->GetBackingStore(); auto length = buffer->ByteLength(); auto dest = ArrayBuffer::NewBackingStore( - isolate, length, v8::BackingStoreInitializationMode::kUninitialized); + isolate, + length, + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); + if (!dest) { + THROW_ERR_MEMORY_ALLOCATION_FAILED(Environment::GetCurrent(isolate)); + return Store(); + } // copy content memcpy(dest->Data(), backing->Data(), length); return Store(std::move(dest), length, 0); } Store Store::CopyFrom(Local view) { - v8::Isolate* isolate = v8::Isolate::GetCurrent(); + Isolate* isolate = Isolate::GetCurrent(); auto backing = view->Buffer()->GetBackingStore(); auto length = view->ByteLength(); auto offset = view->ByteOffset(); auto dest = ArrayBuffer::NewBackingStore( - isolate, length, v8::BackingStoreInitializationMode::kUninitialized); + isolate, + length, + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); // copy content + if (!dest) { + THROW_ERR_MEMORY_ALLOCATION_FAILED(Environment::GetCurrent(isolate)); + return Store(); + } memcpy(dest->Data(), static_cast(backing->Data()) + offset, length); return Store(std::move(dest), length, 0); } @@ -161,8 +194,7 @@ T Store::convert() const { // We can only safely convert to T if we have a valid store. CHECK(store_); T buf; - buf.base = - store_ != nullptr ? static_cast(store_->Data()) + offset_ : nullptr; + buf.base = static_cast(store_->Data()) + offset_; buf.len = length_; return buf; } @@ -223,6 +255,45 @@ QuicError::QuicError(const ngtcp2_ccerr& error) error_(error), ptr_(&error_) {} +QuicError::QuicError(QuicError&& other) noexcept + : reason_(std::move(other.reason_)), + error_(other.error_), + ptr_(other.ptr_ == &other.error_ ? &error_ : other.ptr_) { + // Fix up the internal reason pointer after moving. + error_.reason = reason_c_str(); + error_.reasonlen = reason_.length(); +} + +QuicError& QuicError::operator=(QuicError&& other) noexcept { + if (this != &other) { + reason_ = std::move(other.reason_); + error_ = other.error_; + ptr_ = (other.ptr_ == &other.error_) ? &error_ : other.ptr_; + error_.reason = reason_c_str(); + error_.reasonlen = reason_.length(); + } + return *this; +} + +QuicError::QuicError(const QuicError& other) + : reason_(other.reason_), + error_(other.error_), + ptr_(other.ptr_ == &other.error_ ? &error_ : other.ptr_) { + error_.reason = reason_c_str(); + error_.reasonlen = reason_.length(); +} + +QuicError& QuicError::operator=(const QuicError& other) { + if (this != &other) { + reason_ = other.reason_; + error_ = other.error_; + ptr_ = (other.ptr_ == &other.error_) ? &error_ : other.ptr_; + error_.reason = reason_c_str(); + error_.reasonlen = reason_.length(); + } + return *this; +} + const uint8_t* QuicError::reason_c_str() const { return reinterpret_cast(reason_.c_str()); } @@ -294,13 +365,71 @@ std::optional QuicError::get_crypto_error() const { return code() & ~NGTCP2_CRYPTO_ERROR; } +const char* QuicError::name() const { + // CRYPTO_ERROR carries a TLS alert in its low byte (RFC 9001 sec. 4.8). + // OpenSSL's SSL_alert_desc_string_long owns a stable string for every + // alert it knows about; we filter out the "unknown" placeholder so the + // JS side can present `errorName` as undefined for unrecognised alerts. + if (auto alert = get_crypto_error()) { + const char* n = SSL_alert_desc_string_long(*alert); + if (n != nullptr && std::string_view(n) != "unknown") return n; + return nullptr; + } + // Named transport-layer error codes from RFC 9000 sec. 20.1 (and the + // RFC 9368 version-negotiation extension). Application error codes are + // opaque to QUIC, so we only decode for transport. + if (type() != Type::TRANSPORT) return nullptr; + switch (code()) { + case NGTCP2_NO_ERROR: + return "NO_ERROR"; + case NGTCP2_INTERNAL_ERROR: + return "INTERNAL_ERROR"; + case NGTCP2_CONNECTION_REFUSED: + return "CONNECTION_REFUSED"; + case NGTCP2_FLOW_CONTROL_ERROR: + return "FLOW_CONTROL_ERROR"; + case NGTCP2_STREAM_LIMIT_ERROR: + return "STREAM_LIMIT_ERROR"; + case NGTCP2_STREAM_STATE_ERROR: + return "STREAM_STATE_ERROR"; + case NGTCP2_FINAL_SIZE_ERROR: + return "FINAL_SIZE_ERROR"; + case NGTCP2_FRAME_ENCODING_ERROR: + return "FRAME_ENCODING_ERROR"; + case NGTCP2_TRANSPORT_PARAMETER_ERROR: + return "TRANSPORT_PARAMETER_ERROR"; + case NGTCP2_CONNECTION_ID_LIMIT_ERROR: + return "CONNECTION_ID_LIMIT_ERROR"; + case NGTCP2_PROTOCOL_VIOLATION: + return "PROTOCOL_VIOLATION"; + case NGTCP2_INVALID_TOKEN: + return "INVALID_TOKEN"; + case NGTCP2_APPLICATION_ERROR: + return "APPLICATION_ERROR"; + case NGTCP2_CRYPTO_BUFFER_EXCEEDED: + return "CRYPTO_BUFFER_EXCEEDED"; + case NGTCP2_KEY_UPDATE_ERROR: + return "KEY_UPDATE_ERROR"; + case NGTCP2_AEAD_LIMIT_REACHED: + return "AEAD_LIMIT_REACHED"; + case NGTCP2_NO_VIABLE_PATH: + return "NO_VIABLE_PATH"; + case NGTCP2_VERSION_NEGOTIATION_ERROR: + return "VERSION_NEGOTIATION_ERROR"; + default: + return nullptr; + } +} + MaybeLocal QuicError::ToV8Value(Environment* env) const { if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) || - (type() == Type::APPLICATION && code() == NGHTTP3_H3_NO_ERROR)) { - // Note that we only return undefined for *known* no-error application - // codes. It is possible that other application types use other specific - // no-error codes, but since we don't know which application is being used, - // we'll just return the error code value for those below. + (type() == Type::APPLICATION && + (code() == 0 || code() == NGHTTP3_H3_NO_ERROR)) || + type() == Type::IDLE_CLOSE) { + // Application code 0 is the default no-error code for raw QUIC + // applications (DefaultApplication::GetNoErrorCode() returns 0). + // NGHTTP3_H3_NO_ERROR (0x100) is the HTTP/3 no-error code. + // Idle close is always clean — the session timed out normally. return Undefined(env->isolate()); } @@ -313,6 +442,7 @@ MaybeLocal QuicError::ToV8Value(Environment* env) const { type_str, BigInt::NewFromUnsigned(env->isolate(), code()), Undefined(env->isolate()), + Undefined(env->isolate()), }; // Note that per the QUIC specification, the reason, if present, is @@ -326,6 +456,13 @@ MaybeLocal QuicError::ToV8Value(Environment* env) const { return {}; } + // Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1 + // names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown + // codes leave the slot as undefined. + if (const char* n = name()) { + argv[3] = BindingData::Get(env).error_name_string(n); + } + return Array::New(env->isolate(), argv, arraysize(argv)).As(); } diff --git a/src/quic/data.h b/src/quic/data.h index bd974ac0c8ba0a..9c92a30c1ddf4c 100644 --- a/src/quic/data.h +++ b/src/quic/data.h @@ -19,6 +19,40 @@ namespace node::quic { template concept OneByteType = sizeof(T) == 1; +// Lightweight wrapper around ngtcp2_pkt_info. Insulates the Node.js QUIC +// code from the ngtcp2 struct layout and provides a clean API boundary +// for per-packet metadata (currently ECN codepoint; may grow as ngtcp2 +// and libuv evolve). +// +// Default-constructed PacketInfo is zero-initialized, which ngtcp2 treats +// as ECN Not-ECT — identical to passing nullptr for the pkt_info parameter. +class PacketInfo final { + public: + // ECN codepoints as defined by RFC 3168. + enum class Ecn : uint32_t { + NOT_ECT = 0, // Not ECN-Capable Transport + ECT_1 = 1, // ECN-Capable Transport(1) + ECT_0 = 2, // ECN-Capable Transport(0) + CE = 3, // Congestion Experienced + }; + + PacketInfo() : info_{} {} + explicit PacketInfo(const ngtcp2_pkt_info& info) : info_(info) {} + + // ECN codepoint for this packet. When libuv gains per-packet ECN + // reporting, populate via set_ecn() from the receive metadata + // before passing to ReadPacket(). + Ecn ecn() const { return static_cast(info_.ecn); } + void set_ecn(Ecn ecn) { info_.ecn = static_cast(ecn); } + + // Conversion operators for ngtcp2 API calls. + operator const ngtcp2_pkt_info*() const { return &info_; } + operator ngtcp2_pkt_info*() { return &info_; } + + private: + ngtcp2_pkt_info info_; +}; + struct Path final : public ngtcp2_path { explicit Path(const SocketAddress& local, const SocketAddress& remote); Path(Path&& other) noexcept = default; @@ -54,28 +88,28 @@ class Store final : public MemoryRetainer { size_t length, size_t offset = 0); - // Creates a Store from the contents of an ArrayBuffer, always detaching - // it in the process. An empty Maybe will be returned if the ArrayBuffer - // is not detachable or detaching failed (likely due to a detach key - // mismatch). - static v8::Maybe From( - v8::Local buffer, - v8::Local detach_key = v8::Local()); - - // Creates a Store from the contents of an ArrayBufferView, always detaching - // it in the process. An empty Maybe will be returned if the ArrayBuffer - // is not detachable or detaching failed (likely due to a detach key - // mismatch). - static v8::Maybe From( - v8::Local view, - v8::Local detach_key = v8::Local()); + // Creates a Store by copying the contents of an ArrayBuffer into a fresh + // BackingStore. The caller's buffer is not modified, so callers can safely + // reuse or mutate it after the call returns. Returns an empty Maybe on + // allocation failure, in which case an `ERR_MEMORY_ALLOCATION_FAILED` + // exception will have been scheduled on the isolate. + static v8::Maybe From(v8::Local buffer); + + // Creates a Store by copying the contents of an ArrayBufferView into a + // fresh BackingStore. The caller's view (and its underlying ArrayBuffer) + // is not modified. Returns an empty Maybe on allocation failure, in which + // case an `ERR_MEMORY_ALLOCATION_FAILED` exception will have been + // scheduled on the isolate. + static v8::Maybe From(v8::Local view); // Creates a Store from the contents of an ArrayBuffer, always copying the - // content. + // content. Equivalent to `From()` but returns a Store directly without + // surfacing allocation failure. static Store CopyFrom(v8::Local buffer); - // Creates a Store from the contents of an ArrayBufferView, always copying the - // content. + // Creates a Store from the contents of an ArrayBufferView, always copying + // the content. Equivalent to `From()` but returns a Store directly without + // surfacing allocation failure. static Store CopyFrom(v8::Local view); v8::Local ToUint8Array(Environment* env) const; @@ -208,6 +242,16 @@ class QuicError final : public MemoryRetainer { explicit QuicError(const ngtcp2_ccerr* ptr); explicit QuicError(const ngtcp2_ccerr& error); + // Move constructor and assignment must fix up ptr_ when it points + // to the internal error_ member (as set by the default constructor + // and the ForTransport/ForApplication factory methods). + QuicError(QuicError&& other) noexcept; + QuicError& operator=(QuicError&& other) noexcept; + + // Copy constructor and assignment must also fix up ptr_. + QuicError(const QuicError& other); + QuicError& operator=(const QuicError& other); + Type type() const; error_code code() const; const std::string_view reason() const; @@ -221,6 +265,9 @@ class QuicError final : public MemoryRetainer { bool is_crypto_error() const; std::optional get_crypto_error() const; + // Returns a human-readable name for this error if known, or nullptr + const char* name() const; + // Note that since application errors are application-specific and we // don't know which application is being used here, it is possible that // the comparing two different QuicError instances from different applications diff --git a/src/quic/defs.h b/src/quic/defs.h index b26ca5f9a4f12e..75ae915335be93 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -12,8 +12,8 @@ namespace node::quic { #define NGTCP2_SUCCESS 0 -#define NGTCP2_ERR(V) (V != NGTCP2_SUCCESS) -#define NGTCP2_OK(V) (V == NGTCP2_SUCCESS) +#define NGTCP2_ERR(V) ((V) != NGTCP2_SUCCESS) +#define NGTCP2_OK(V) ((V) == NGTCP2_SUCCESS) #define IF_QUIC_DEBUG(env) \ if (env->enabled_debug_list()->enabled(DebugCategory::QUIC)) [[unlikely]] @@ -83,6 +83,39 @@ bool SetOption(Environment* env, return true; } +template +bool SetOption(Environment* env, + Opt* options, + const v8::Local& object, + const v8::Local& name) { + v8::Local value; + if (!object->Get(env->context(), name).ToLocal(&value)) return false; + if (!value->IsUndefined()) { + if (!value->IsUint32()) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be an uint16", nameStr); + return false; + } + v8::Local num; + if (!value->ToUint32(env->context()).ToLocal(&num)) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be an uint16", nameStr); + return false; + } + uint32_t val = num->Value(); + if (val > 0xFFFF) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must fit in a uint16", nameStr); + return false; + } + options->*member = static_cast(val); + } + return true; +} + template bool SetOption(Environment* env, Opt* options, @@ -205,7 +238,7 @@ uint64_t GetStat(Stats* stats) { if (!GetConstructorTemplate(env) \ ->InstanceTemplate() \ ->NewInstance(env->context()) \ - .ToLocal(&obj)) { \ + .ToLocal(&name)) { \ return ret; \ } @@ -214,7 +247,7 @@ uint64_t GetStat(Stats* stats) { if (!GetConstructorTemplate(env) \ ->InstanceTemplate() \ ->NewInstance(env->context()) \ - .ToLocal(&obj)) { \ + .ToLocal(&name)) { \ return; \ } @@ -285,8 +318,14 @@ enum class StreamPriority : uint8_t { }; enum class StreamPriorityFlags : uint8_t { - NONE, NON_INCREMENTAL, + INCREMENTAL, +}; + +enum class HeadersSupportState : uint8_t { + UNKNOWN, + SUPPORTED, + UNSUPPORTED, }; enum class PathValidationResult : uint8_t { @@ -298,6 +337,7 @@ enum class PathValidationResult : uint8_t { enum class DatagramStatus : uint8_t { ACKNOWLEDGED, LOST, + ABANDONED, }; #define CC_ALGOS(V) \ @@ -320,6 +360,33 @@ constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS; constexpr size_t kMaxVectorCount = 16; constexpr stream_id kMaxStreamId = std::numeric_limits::max(); +// A token bucket rate limiter using lazy refill. No timer needed — tokens +// are computed on demand from the elapsed time since the last check. +// Used to cap the total rate of stateless responses (retry, reset, +// version negotiation, immediate close) regardless of source address, +// preventing spoofed-source floods from bypassing per-host limits. +struct TokenBucket final { + double rate; // tokens per second (refill rate) + double burst; // maximum tokens (bucket capacity) + double tokens; // current token count + uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime) + + TokenBucket() : rate(0), burst(0), tokens(0), last_ts(0) {} + TokenBucket(double rate, double burst); + + // Reinitialize the bucket with new rate/burst parameters if it + // hasn't been initialized yet (last_ts == 0). Used for per-host + // buckets in the address LRU where the rate/burst aren't known + // at construction time. + void InitOnce(double r, double b, uint64_t now); + + // Try to consume one token. Refills based on elapsed time, then + // attempts to consume. Returns true if the request is allowed. + // The caller provides the current timestamp to avoid redundant + // uv_hrtime() calls in hot paths. + bool consume(uint64_t now); +}; + class DebugIndentScope final { public: inline DebugIndentScope() { ++indent_; } diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 176f5d0262c927..aa53a31f91277b 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include "endpoint.h" #include "http3.h" #include "ncrypto.h" +#include "session_manager.h" namespace node { @@ -52,6 +54,10 @@ namespace quic { V(CLOSING, closing, uint8_t) \ /* Temporarily paused serving new initial requests */ \ V(BUSY, busy, uint8_t) \ + /* Max concurrent connections per IP (0 = unlimited) */ \ + V(MAX_CONNECTIONS_PER_HOST, max_connections_per_host, uint16_t) \ + /* Max total concurrent connections (0 = unlimited) */ \ + V(MAX_CONNECTIONS_TOTAL, max_connections_total, uint16_t) \ /* The number of pending send callbacks */ \ V(PENDING_CALLBACKS, pending_callbacks, uint64_t) @@ -66,9 +72,15 @@ namespace quic { V(CLIENT_SESSIONS, client_sessions) \ V(SERVER_BUSY_COUNT, server_busy_count) \ V(RETRY_COUNT, retry_count) \ + V(RETRY_RATE_LIMITED, retry_rate_limited) \ V(VERSION_NEGOTIATION_COUNT, version_negotiation_count) \ + V(VERSION_NEGOTIATION_RATE_LIMITED, version_negotiation_rate_limited) \ V(STATELESS_RESET_COUNT, stateless_reset_count) \ - V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) + V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \ + V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \ + V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) \ + V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited) \ + V(PACKETS_BLOCKED, packets_blocked) struct Endpoint::State { #define V(_, name, type) type name; @@ -78,6 +90,31 @@ struct Endpoint::State { STAT_STRUCT(Endpoint, ENDPOINT) +TokenBucket::TokenBucket(double rate, double burst) + : rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {} + +void TokenBucket::InitOnce(double r, double b, uint64_t now) { + if (last_ts == 0) { + rate = r; + burst = b; + tokens = b; + last_ts = now; + } +} + +// Try to consume one token. Refills based on elapsed time, then +// attempts to consume. Returns true if the request is allowed. +bool TokenBucket::consume(uint64_t now) { + double elapsed = static_cast(now - last_ts) / 1e9; // seconds + last_ts = now; + tokens = std::min(burst, tokens + elapsed * rate); + if (tokens >= 1.0) { + tokens -= 1.0; + return true; + } + return false; +} + // ============================================================================ // Endpoint::Options namespace { @@ -90,6 +127,7 @@ bool is_diagnostic_packet_loss(double probability) { CHECK(ncrypto::CSPRNG(&c, 1)); return (static_cast(c) / 255) < probability; } +#endif // DEBUG template bool SetOption(Environment* env, @@ -99,18 +137,24 @@ bool SetOption(Environment* env, Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { - Local num; - if (!value->ToNumber(env->context()).ToLocal(&num)) { + if (!value->IsNumber()) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be a number", nameStr); return false; } - options->*member = num->Value(); + Local num = value.As(); + double dbl = num->Value(); + if (dbl < 0) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be a non-negative number", nameStr); + return false; + } + options->*member = dbl; } return true; } -#endif // DEBUG template bool SetOption(Environment* env, @@ -126,15 +170,15 @@ bool SetOption(Environment* env, env, "The %s option must be an uint8", nameStr); return false; } - Local num; - if (!value->ToUint32(env->context()).ToLocal(&num) || - num->Value() > std::numeric_limits::max()) { + Local num = value.As(); + uint32_t val = num->Value(); + if (val > std::numeric_limits::max()) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be an uint8", nameStr); return false; } - options->*member = num->Value(); + options->*member = val; } return true; } @@ -188,15 +232,19 @@ Maybe Endpoint::Options::From(Environment* env, env, &options, params, state.name##_string()) if (!SET(retry_token_expiration) || !SET(token_expiration) || - !SET(max_connections_per_host) || !SET(max_connections_total) || - !SET(max_stateless_resets) || !SET(address_lru_size) || - !SET(max_retries) || !SET(validate_address) || + !SET(address_lru_size) || !SET(validate_address) || !SET(disable_stateless_reset) || !SET(ipv6_only) || !SET(reuse_port) || + !SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) || + !SET(stateless_reset_burst) || !SET(version_negotiation_rate) || + !SET(version_negotiation_burst) || !SET(immediate_close_rate) || + !SET(immediate_close_burst) || !SET(session_creation_rate) || + !SET(session_creation_burst) || #ifdef DEBUG !SET(rx_loss) || !SET(tx_loss) || #endif !SET(udp_receive_buffer_size) || !SET(udp_send_buffer_size) || - !SET(udp_ttl) || !SET(reset_token_secret) || !SET(token_secret)) { + !SET(udp_ttl) || !SET(idle_timeout) || !SET(reset_token_secret) || + !SET(token_secret)) { return Nothing(); } @@ -220,6 +268,42 @@ Maybe Endpoint::Options::From(Environment* env, } } + // Parse block list option. Expects the C++ SocketAddressBlockListWrap handle + // (the JS side extracts [kHandle] before passing it through). + Local block_list_val; + if (!params->Get(env->context(), state.block_list_string()) + .ToLocal(&block_list_val)) { + return Nothing(); + } + if (!block_list_val->IsUndefined()) { + if (!SocketAddressBlockListWrap::HasInstance(env, block_list_val)) { + THROW_ERR_INVALID_ARG_TYPE( + env, "The blockList option must be a BlockList handle"); + return Nothing(); + } + auto* wrap = + FromJSObject(block_list_val.As()); + options.block_list = wrap->blocklist(); + } + + // Parse block list policy. + Local policy_val; + if (!params->Get(env->context(), state.block_list_policy_string()) + .ToLocal(&policy_val)) { + return Nothing(); + } + if (!policy_val->IsUndefined()) { + if (policy_val->StrictEquals(state.allow_string())) { + options.block_list_policy = Options::BlockListPolicy::ALLOW; + } else if (policy_val->StrictEquals(state.deny_string())) { + options.block_list_policy = Options::BlockListPolicy::DENY; + } else { + THROW_ERR_INVALID_ARG_VALUE( + env, "The blockListPolicy option must be 'deny' or 'allow'"); + return Nothing(); + } + } + return Just(options); #undef SET @@ -244,14 +328,26 @@ std::string Endpoint::Options::ToString() const { " seconds"; res += prefix + "token expiration: " + std::to_string(token_expiration) + " seconds"; - res += prefix + "max connections per host: " + - std::to_string(max_connections_per_host); - res += prefix + - "max connections total: " + std::to_string(max_connections_total); - res += - prefix + "max stateless resets: " + std::to_string(max_stateless_resets); res += prefix + "address lru size: " + std::to_string(address_lru_size); - res += prefix + "max retries: " + std::to_string(max_retries); + res += prefix + "retry rate: " + std::to_string(retry_rate) + "/s"; + res += prefix + "retry burst: " + std::to_string(retry_burst); + res += prefix + + "stateless reset rate: " + std::to_string(stateless_reset_rate) + "/s"; + res += prefix + + "stateless reset burst: " + std::to_string(stateless_reset_burst); + res += prefix + "version negotiation rate: " + + std::to_string(version_negotiation_rate) + "/s"; + res += prefix + "version negotiation burst: " + + std::to_string(version_negotiation_burst); + res += prefix + + "immediate close rate: " + std::to_string(immediate_close_rate) + "/s"; + res += prefix + + "immediate close burst: " + std::to_string(immediate_close_burst); + res += prefix + + "session creation rate: " + std::to_string(session_creation_rate) + + "/s"; + res += prefix + + "session creation burst: " + std::to_string(session_creation_burst); res += prefix + "validate address: " + boolToString(validate_address); res += prefix + "disable stateless reset: " + boolToString(disable_stateless_reset); @@ -268,6 +364,7 @@ std::string Endpoint::Options::ToString() const { res += prefix + "udp send buffer size: " + std::to_string(udp_send_buffer_size); res += prefix + "udp ttl: " + std::to_string(udp_ttl); + res += prefix + "idle timeout: " + std::to_string(idle_timeout) + " seconds"; res += indent.Close(); return res; @@ -299,7 +396,10 @@ class Endpoint::UDP::Impl final : public HandleWrap { reinterpret_cast(&handle_), PROVIDER_QUIC_UDP), endpoint_(endpoint) { - CHECK_EQ(uv_udp_init(endpoint->env()->event_loop(), &handle_), 0); + CHECK_EQ(uv_udp_init_ex(endpoint->env()->event_loop(), + &handle_, + AF_UNSPEC | UV_UDP_RECVMMSG), + 0); handle_.data = this; } @@ -308,10 +408,26 @@ class Endpoint::UDP::Impl final : public HandleWrap { SET_SELF_SIZE(Impl) private: + // Pre-allocated receive buffer sized for recvmmsg batching. libuv's + // recvmmsg path partitions the alloc buffer into 64KB chunks (one per + // datagram). With kRecvBatchSize chunks we can receive up to that many + // packets in a single recvmmsg syscall. ngtcp2_conn_read_pkt is + // synchronous — it copies what it needs — so the buffer is safely + // reused across batches. + // libuv's recvmmsg partitions the buffer into UV__UDP_DGRAM_MAXSIZE (64KB) + // chunks regardless of actual packet size. QUIC packets are ~1200 bytes, + // so most of each 64KB chunk is wasted. We use a modest batch size to + // balance syscall reduction against memory usage. + static constexpr size_t kDgramMaxSize = 65536; // UV__UDP_DGRAM_MAXSIZE + static constexpr size_t kRecvBatchSize = 5; + static constexpr size_t kRecvBufferSize = kDgramMaxSize * kRecvBatchSize; + std::array recv_buf_ = {}; + static void OnAlloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { - *buf = From(handle)->env()->allocate_managed_buffer(suggested_size); + auto* impl = From(handle); + *buf = uv_buf_init(impl->recv_buf_.data(), kRecvBufferSize); } static void OnReceive(uv_udp_t* handle, @@ -323,26 +439,32 @@ class Endpoint::UDP::Impl final : public HandleWrap { DCHECK_NOT_NULL(impl); DCHECK_NOT_NULL(impl->endpoint_); - auto release_buf = [&]() { - if (buf->base != nullptr) impl->env()->release_managed_buffer(*buf); - }; + // UV_UDP_MMSG_FREE signals the end of a recvmmsg batch — the + // buffer can be reused. Since our buffer is pre-allocated and + // persistent, there is nothing to free. + if (flags & UV_UDP_MMSG_FREE) { + return; + } // Nothing to do in these cases. Specifically, if the nread // is zero or we have received a partial packet, we are just - // going to ignore it. + // going to ignore it. No buffer release needed — recv_buf_ + // is pre-allocated and reused. if (nread == 0 || flags & UV_UDP_PARTIAL) { - release_buf(); return; } if (nread < 0) { - release_buf(); impl->endpoint_->Destroy(CloseContext::RECEIVE_FAILURE, static_cast(nread)); return; } - impl->endpoint_->Receive(uv_buf_init(buf->base, static_cast(nread)), + // UV_UDP_MMSG_CHUNK is set for each packet in a recvmmsg batch. + // Processing is the same as for a single-message receive — ngtcp2 + // copies what it needs synchronously from the buf slice. + impl->endpoint_->Receive(reinterpret_cast(buf->base), + static_cast(nread), SocketAddress(addr)); } @@ -371,7 +493,7 @@ Endpoint::UDP::~UDP() { } int Endpoint::UDP::Bind(const Options& options) { - if (is_bound_) return UV_EALREADY; + if (flags_.is_bound) return UV_EALREADY; if (is_closed_or_closing()) return UV_EBADF; int flags = 0; @@ -382,7 +504,7 @@ int Endpoint::UDP::Bind(const Options& options) { int size; if (!err) { - is_bound_ = true; + flags_.is_bound = true; size = static_cast(options.udp_receive_buffer_size); if (size > 0) { err = uv_recv_buffer_size(reinterpret_cast(&impl_->handle_), @@ -421,34 +543,34 @@ void Endpoint::UDP::Unref() { int Endpoint::UDP::Start() { if (is_closed_or_closing()) return UV_EBADF; - if (is_started_) return 0; + if (flags_.is_started) return 0; int err = uv_udp_recv_start(&impl_->handle_, Impl::OnAlloc, Impl::OnReceive); - is_started_ = (err == 0); + flags_.is_started = (err == 0); return err; } void Endpoint::UDP::Stop() { - if (is_closed_or_closing() || !is_started_) return; + if (is_closed_or_closing() || !flags_.is_started) return; USE(uv_udp_recv_stop(&impl_->handle_)); - is_started_ = false; + flags_.is_started = false; } void Endpoint::UDP::Close() { if (is_closed_or_closing()) return; DCHECK(impl_); Stop(); - is_bound_ = false; - is_closed_ = true; + flags_.is_bound = false; + flags_.is_closed = true; impl_->Close(); impl_.reset(); } bool Endpoint::UDP::is_bound() const { - return is_bound_; + return flags_.is_bound; } bool Endpoint::UDP::is_closed() const { - return is_closed_; + return flags_.is_closed; } bool Endpoint::UDP::is_closed_or_closing() const { @@ -472,7 +594,6 @@ int Endpoint::UDP::Send(Packet::Ptr packet) { // Detach from the Ptr — libuv takes ownership until the callback fires. Packet* raw = packet.release(); uv_buf_t buf = *raw; - int err = uv_udp_send(raw->req(), &impl_->handle_, &buf, @@ -490,6 +611,24 @@ int Endpoint::UDP::Send(Packet::Ptr packet) { return err; } +int Endpoint::UDP::TrySend(const Packet::Ptr& packet) { + DCHECK(packet); + if (is_closed_or_closing()) return UV_EBADF; + uv_buf_t buf = *packet; + return uv_udp_try_send( + &impl_->handle_, &buf, 1, packet->destination().data()); +} + +int Endpoint::UDP::TrySendBatch(uv_buf_t* bufs[], + unsigned int nbufs[], + struct sockaddr* addrs[], + size_t count) { + DCHECK_GT(count, 0); + if (is_closed_or_closing()) return UV_EBADF; + return uv_udp_try_send2( + &impl_->handle_, static_cast(count), bufs, nbufs, addrs, 0); +} + void Endpoint::UDP::MemoryInfo(MemoryTracker* tracker) const { if (impl_) tracker->TrackField("impl", impl_); } @@ -533,11 +672,7 @@ void Endpoint::InitPerContext(Realm* realm, Local target) { ENDPOINT_STATE(V) #undef V - NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_CONNECTIONS); - NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_CONNECTIONS_PER_HOST); NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE); - NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_STATELESS_RESETS); - NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_RETRY_LIMIT); static constexpr auto DEFAULT_RETRYTOKEN_EXPIRATION = RetryToken::QUIC_DEFAULT_RETRYTOKEN_EXPIRATION / NGTCP2_SECONDS; @@ -594,9 +729,22 @@ Endpoint::Endpoint(Environment* env, packet_pool_(kDefaultMaxPacketLength, ArenaPool::kDefaultSlotsPerBlock), udp_(this), - addrLRU_(options_.address_lru_size) { + idle_timer_(env, + [this] { + HandleScope scope(this->env()->isolate()); + Destroy(); + }), + addr_validation_lru_(options_.address_lru_size), + retry_bucket_(options_.retry_rate, options_.retry_burst), + stateless_reset_bucket_(options_.stateless_reset_rate, + options_.stateless_reset_burst), + version_negotiation_bucket_(options_.version_negotiation_rate, + options_.version_negotiation_burst), + immediate_close_bucket_(options_.immediate_close_rate, + options_.immediate_close_burst) { MakeWeak(); udp_.Unref(); + idle_timer_.Unref(); STAT_RECORD_TIMESTAMP(Stats, created_at); IF_QUIC_DEBUG(env) { Debug(this, "Endpoint created. Options %s", options.ToString()); @@ -641,11 +789,12 @@ RegularToken Endpoint::GenerateNewToken(uint32_t version, return RegularToken(version, remote_address, options_.token_secret); } +SessionManager& Endpoint::session_manager() const { + return BindingData::Get(env()).session_manager(); +} + StatelessResetToken Endpoint::GenerateNewStatelessResetToken( uint8_t* token, const CID& cid) const { - Debug(const_cast(this), - "Generating new stateless reset token for CID %s", - cid); DCHECK(!is_closed() && !is_closing()); return StatelessResetToken(token, options_.reset_token_secret, cid); } @@ -653,9 +802,38 @@ StatelessResetToken Endpoint::GenerateNewStatelessResetToken( void Endpoint::AddSession(const CID& cid, BaseObjectPtr session) { DCHECK(!is_closed() && !is_closing()); Debug(this, "Adding session for CID %s", cid); - IncrementSocketAddressCounter(session->remote_address()); + if (state_->max_connections_per_host > 0) { + conn_counts_per_host_[session->remote_address()]++; + } + auto& mgr = session_manager(); + // Associate peer-chosen CIDs in the local dcid_to_scid_ map. AssociateCID(session->config().dcid, session->config().scid); - sessions_[cid] = session; + mgr.AddSession(cid, session); + mgr.SetPrimaryEndpoint(session.get(), this); + // For server sessions, associate the client's original DCID (ocid) so + // that 0-RTT packets arriving in a separate UDP datagram can be routed + // to this session. This must happen after the session is added (so + // FindSession can resolve the mapping) but before EmitNewSession (which + // runs JS and may yield to libuv, allowing the 0-RTT packet to arrive). + if (session->is_server() && session->config().ocid) { + AssociateCID(session->config().ocid, session->config().scid); + } + // After Retry, the client continues to use the Retry SCID as its DCID + // until the handshake completes. Register it so retransmitted Initials + // and subsequent handshake packets can be routed to this session. + if (session->is_server() && session->config().retry_scid) { + AssociateCID(session->config().retry_scid, session->config().scid); + } + // Increment the primary session count and ref the handle BEFORE + // EmitNewSession. EmitNewSession calls into JS, which may close/destroy + // the session synchronously. The session's ~Impl calls RemoveSession + // which decrements the count. If we increment after EmitNewSession, + // RemoveSession would see count=0 and the count would be permanently + // off by one. + if (primary_session_count_++ == 0) { + idle_timer_.Stop(); + udp_.Ref(); + } if (session->is_server()) { STAT_INCREMENT(Stats, server_sessions); // We only emit the new session event for server sessions. @@ -665,46 +843,60 @@ void Endpoint::AddSession(const CID& cid, BaseObjectPtr session) { } else { STAT_INCREMENT(Stats, client_sessions); } - udp_.Ref(); } void Endpoint::RemoveSession(const CID& cid, const SocketAddress& remote_address) { if (is_closed()) return; Debug(this, "Removing session for CID %s", cid); - if (sessions_.erase(cid)) { - DecrementSocketAddressCounter(remote_address); + auto it = conn_counts_per_host_.find(remote_address); + if (it != conn_counts_per_host_.end()) { + if (--it->second == 0) { + conn_counts_per_host_.erase(it); + } } - if (sessions_.empty()) { - udp_.Unref(); + if (primary_session_count_ > 0 && --primary_session_count_ == 0) { + if (!is_listening()) { + udp_.Unref(); + } + session_manager().RemoveSession(cid); + // The endpoint may be idle (no sessions, not listening). MaybeDestroy + // handles both closing (immediate destroy) and idle timeout (start + // timer or destroy based on idle_timeout setting). + MaybeDestroy(); + return; } + session_manager().RemoveSession(cid); if (state_->closing == 1) MaybeDestroy(); } BaseObjectPtr Endpoint::FindSession(const CID& cid) { - auto session_it = sessions_.find(cid); - if (session_it == std::end(sessions_)) { - // If our given cid is not a match that doesn't mean we - // give up. A session might be identified by multiple - // CIDs. Let's see if our secondary map has a match! - auto scid_it = dcid_to_scid_.find(cid); - if (scid_it != std::end(dcid_to_scid_)) { - session_it = sessions_.find(scid_it->second); - CHECK_NE(session_it, std::end(sessions_)); - return session_it->second; - } - // No match found. - return {}; + // First, try the SessionManager's primary sessions_ map directly. + // This handles the common case where the CID is a locally-generated SCID. + auto session = session_manager().FindSession(cid); + if (session) return session; + + // If not found, check this endpoint's local dcid_to_scid_ map for a + // secondary CID mapping. This map contains peer-chosen CID values that + // are only meaningful in the context of this endpoint's sessions. + auto scid_it = dcid_to_scid_.find(cid); + if (scid_it != dcid_to_scid_.end()) { + session = session_manager().FindSession(scid_it->second); + if (session) return session; + // Stale mapping — clean up. + dcid_to_scid_.erase(scid_it); } - // Match found! - return session_it->second; + + return {}; } void Endpoint::AssociateCID(const CID& cid, const CID& scid) { - if (!is_closed() && !is_closing() && cid && scid && cid != scid && - dcid_to_scid_[cid] != scid) { - Debug(this, "Associating CID %s with SCID %s", cid, scid); - dcid_to_scid_.emplace(cid, scid); + if (!is_closed() && !is_closing() && cid && scid && cid != scid) { + auto it = dcid_to_scid_.find(cid); + if (it == dcid_to_scid_.end() || it->second != scid) { + Debug(this, "Associating CID %s with SCID %s", cid, scid); + dcid_to_scid_[cid] = scid; + } } } @@ -719,14 +911,14 @@ void Endpoint::AssociateStatelessResetToken(const StatelessResetToken& token, Session* session) { if (is_closed() || is_closing()) return; Debug(this, "Associating stateless reset token %s with session", token); - token_map_[token] = session; + session_manager().AssociateStatelessResetToken(token, session); } void Endpoint::DisassociateStatelessResetToken( const StatelessResetToken& token) { if (!is_closed()) { Debug(this, "Disassociating stateless reset token %s", token); - token_map_.erase(token); + session_manager().DisassociateStatelessResetToken(token); } } @@ -764,62 +956,146 @@ void Endpoint::Send(Packet::Ptr packet) { STAT_INCREMENT(Stats, packets_sent); } -void Endpoint::SendRetry(const PathDescriptor& options) { - // Generating and sending retry packets does consume some system resources, - // and it is possible for a malicious peer to trigger sending a large number - // of retry packets, resulting in a potential DOS vector. To help ward that - // off, we track how many retry packets we send to a particular host and - // enforce limits. Note that since we are using an LRU cache these limits - // aren't strict. If a retry is sent, we increment the retry_count statistic - // to give application code a means of detecting and responding to abuse on - // its own. What this count does not give is the rate of retry, so it is still - // somewhat limited. - Debug(this, "Sending retry on path %s", options); - auto info = addrLRU_.Upsert(options.remote_address); - if (++(info->retry_count) <= options_.max_retries) { - auto packet = - Packet::CreateRetryPacket(*this, options, options_.token_secret); - if (packet) { - STAT_INCREMENT(Stats, retry_count); - Send(std::move(packet)); +void Endpoint::SendOrTrySend(Packet::Ptr packet) { +#ifdef DEBUG + if (is_diagnostic_packet_loss(options_.tx_loss)) [[unlikely]] { + return; + } +#endif + + if (is_closed() || is_closing() || !packet || packet->length() == 0) { + return; + } + + Debug(this, "TrySend %s", packet->ToString()); + + // Attempt synchronous send. On success (returns number of bytes sent), + // the packet is delivered immediately — no callback overhead, no + // waiting for the next poll cycle. + int err = udp_.TrySend(packet); + if (err >= 0) { + // Synchronous send succeeded. + STAT_INCREMENT_N(Stats, bytes_sent, packet->length()); + STAT_INCREMENT(Stats, packets_sent); + // Ptr destructor releases back to arena pool. + return; + } + + if (err == UV_EAGAIN) { + // Socket not writable or async sends are queued. Fall back to the + // async path — the packet will be queued and flushed on the next + // POLLOUT cycle. + Debug(this, "TrySend got EAGAIN, falling back to async Send"); + return Send(std::move(packet)); + } + + // Other errors are fatal. + Debug(this, "TrySend failed with error %d", err); + Destroy(CloseContext::SEND_FAILURE, err); +} + +void Endpoint::SendBatch(Packet::Ptr* packets, size_t count) { + if (count == 0) return; + +#ifdef DEBUG + if (is_diagnostic_packet_loss(options_.tx_loss)) [[unlikely]] { + for (size_t i = 0; i < count; i++) packets[i].reset(); + return; + } +#endif + + if (is_closed() || is_closing()) { + for (size_t i = 0; i < count; i++) packets[i].reset(); + return; + } + + static constexpr size_t kMaxBatch = 64; + DCHECK_LE(count, kMaxBatch); + + // Build libuv argument arrays directly from the Ptr array. + // Packets with zero length are released and skipped. + uv_buf_t bufs[kMaxBatch]; + uv_buf_t* buf_ptrs[kMaxBatch]; + unsigned int nbufs[kMaxBatch]; + struct sockaddr* addrs[kMaxBatch]; + // Map from valid-index back to the original packets[] index. + size_t index_map[kMaxBatch]; + size_t valid_count = 0; + + for (size_t i = 0; i < count; i++) { + if (!packets[i] || packets[i]->length() == 0) { + packets[i].reset(); + continue; } + bufs[valid_count] = *packets[i]; + buf_ptrs[valid_count] = &bufs[valid_count]; + nbufs[valid_count] = 1; + addrs[valid_count] = + const_cast(packets[i]->destination().data()); + index_map[valid_count] = i; + valid_count++; + } + + if (valid_count == 0) return; + + // Attempt synchronous batched send via sendmmsg. + int sent = udp_.TrySendBatch(buf_ptrs, nbufs, addrs, valid_count); + + if (sent > 0) { + // Packets [0, sent) were delivered synchronously. + // Release them immediately — no async callback needed. + for (size_t i = 0; i < static_cast(sent); i++) { + size_t idx = index_map[i]; + STAT_INCREMENT_N(Stats, bytes_sent, packets[idx]->length()); + STAT_INCREMENT(Stats, packets_sent); + packets[idx].reset(); + } + } - // If creating the retry is unsuccessful, we just drop things on the floor. - // It's not worth committing any further resources to this one packet. We - // might want to log the failure at some point tho. + // Any unsent packets (EAGAIN, partial send, or total failure) fall + // back to async uv_udp_send. + size_t start = (sent > 0) ? static_cast(sent) : 0; + for (size_t i = start; i < valid_count; i++) { + size_t idx = index_map[i]; + Send(std::move(packets[idx])); } } -void Endpoint::SendVersionNegotiation(const PathDescriptor& options) { - Debug(this, "Sending version negotiation on path %s", options); - // A malicious peer can trivially force version negotiation packets by - // sending packets with unsupported QUIC versions, potentially from - // spoofed source addresses. Rate-limit per remote host to prevent - // amplification attacks. - const auto exceeds_limits = [&] { - SocketAddressInfoTraits::Type* counts = - addr_validation_lru_.Peek(options.remote_address); - auto count = counts != nullptr ? counts->version_negotiation_count : 0; - return count >= kMaxVersionNegotiations; - }; +void Endpoint::SendRetry(const PathDescriptor& options, uint64_t now) { + Debug(this, "Sending retry on path %s", options); + if (!retry_bucket_.consume(now)) { + Debug(this, "Retry rate limit exceeded (global)"); + STAT_INCREMENT(Stats, retry_rate_limited); + return; + } + + auto packet = + Packet::CreateRetryPacket(*this, options, options_.token_secret); + if (packet) { + STAT_INCREMENT(Stats, retry_count); + Send(std::move(packet)); + } +} - if (exceeds_limits()) { - Debug(this, "Version negotiation rate limit exceeded for %s", - options.remote_address); +void Endpoint::SendVersionNegotiation(const PathDescriptor& options, + uint64_t now) { + Debug(this, "Sending version negotiation on path %s", options); + if (!version_negotiation_bucket_.consume(now)) { + Debug(this, "Version negotiation rate limit exceeded (global)"); + STAT_INCREMENT(Stats, version_negotiation_rate_limited); return; } auto packet = Packet::CreateVersionNegotiationPacket(*this, options); if (packet) { - addr_validation_lru_.Upsert(options.remote_address) - ->version_negotiation_count++; STAT_INCREMENT(Stats, version_negotiation_count); Send(std::move(packet)); } } bool Endpoint::SendStatelessReset(const PathDescriptor& options, - size_t source_len) { + size_t source_len, + uint64_t now) { if (options_.disable_stateless_reset) [[unlikely]] { return false; } @@ -828,56 +1104,41 @@ bool Endpoint::SendStatelessReset(const PathDescriptor& options, options, source_len); - const auto exceeds_limits = [&] { - SocketAddressInfoTraits::Type* counts = - addrLRU_.Peek(options.remote_address); - auto count = counts != nullptr ? counts->reset_count : 0; - return count >= options_.max_stateless_resets; - }; - - // Per the QUIC spec, we need to protect against sending too many stateless - // reset tokens to an endpoint to prevent endless looping. - if (exceeds_limits()) return false; + if (!stateless_reset_bucket_.consume(now)) { + Debug(this, "Stateless reset rate limit exceeded (global)"); + STAT_INCREMENT(Stats, stateless_reset_rate_limited); + return false; + } auto packet = Packet::CreateStatelessResetPacket( *this, options, options_.reset_token_secret, source_len); if (packet) { - addrLRU_.Upsert(options.remote_address)->reset_count++; + Debug(this, "Sending stateless reset packet (%zu bytes)", packet->length()); STAT_INCREMENT(Stats, stateless_reset_count); Send(std::move(packet)); return true; } + Debug(this, "Failed to create stateless reset packet"); return false; } void Endpoint::SendImmediateConnectionClose(const PathDescriptor& options, - QuicError reason) { + QuicError reason, + uint64_t now) { Debug(this, "Sending immediate connection close on path %s with reason %s", options, reason); - // A malicious peer can trigger immediate connection close packets by - // sending Initial packets with invalid tokens or when the server is - // busy. Rate-limit per remote host to prevent amplification attacks. - const auto exceeds_limits = [&] { - SocketAddressInfoTraits::Type* counts = - addr_validation_lru_.Peek(options.remote_address); - auto count = counts != nullptr ? counts->immediate_close_count : 0; - return count >= kMaxImmediateCloses; - }; - - if (exceeds_limits()) { - Debug(this, "Immediate connection close rate limit exceeded for %s", - options.remote_address); + if (!immediate_close_bucket_.consume(now)) { + Debug(this, "Immediate connection close rate limit exceeded (global)"); + STAT_INCREMENT(Stats, immediate_close_rate_limited); return; } auto packet = Packet::CreateImmediateConnectionClosePacket(*this, options, reason); if (packet) { - addr_validation_lru_.Upsert(options.remote_address) - ->immediate_close_count++; STAT_INCREMENT(Stats, immediate_close_count); Send(std::move(packet)); } @@ -911,8 +1172,9 @@ bool Endpoint::Start() { return false; } - BindingData::Get(env()).listening_endpoints[this] = - BaseObjectPtr(this); + auto& binding = BindingData::Get(env()); + binding.listening_endpoints[this] = BaseObjectPtr(this); + binding.session_manager().RegisterEndpoint(this, udp_.local_address()); state_->receiving = 1; return true; } @@ -932,7 +1194,7 @@ void Endpoint::Listen(const Session::Options& options) { "not what you want."); } - auto context = TLSContext::CreateServer(options.tls_options); + auto context = TLSContext::CreateServer(env(), options.tls_options); if (!*context) { THROW_ERR_INVALID_STATE( env(), "Failed to create TLS context: %s", context->validation_error()); @@ -954,6 +1216,7 @@ void Endpoint::Listen(const Session::Options& options) { }; if (Start()) { Debug(this, "Listening with options %s", server_state_->options); + idle_timer_.Stop(); state_->listening = 1; } } @@ -974,7 +1237,7 @@ BaseObjectPtr Endpoint::Connect( config, session_ticket.has_value() ? "yes" : "no"); - auto tls_context = TLSContext::CreateClient(options.tls_options); + auto tls_context = TLSContext::CreateClient(env(), options.tls_options); if (!*tls_context) { THROW_ERR_INVALID_STATE(env(), "Failed to create TLS context: %s", @@ -1003,13 +1266,25 @@ BaseObjectPtr Endpoint::Connect( } void Endpoint::MaybeDestroy() { - if (!is_closed() && sessions_.empty() && state_->pending_callbacks == 0 && - state_->listening == 0) { - // Destroy potentially creates v8 handles so let's make sure - // we have a HandleScope on the stack. - HandleScope scope(env()->isolate()); - Destroy(); + if (is_closed() || primary_session_count_ > 0 || + state_->pending_callbacks > 0 || state_->listening == 1) { + return; + } + if (options_.idle_timeout > 0) { + // Start the idle timer. If it fires before a new session or listen + // call reactivates this endpoint, the endpoint will be destroyed. + idle_timer_.Update(options_.idle_timeout * 1000); + return; } + // With idle_timeout == 0, only destroy if the endpoint is actively + // closing (via close() or CloseGracefully). An idle endpoint that + // is not closing stays alive with an unref'd handle so the process + // can still exit. + if (state_->closing != 1) return; + // Destroy potentially creates v8 handles so let's make sure + // we have a HandleScope on the stack. + HandleScope scope(env()->isolate()); + Destroy(); } void Endpoint::Destroy(CloseContext context, int status) { @@ -1045,14 +1320,9 @@ void Endpoint::Destroy(CloseContext context, int status) { // If there are open sessions still, shut them down. As those clean themselves // up, they will remove themselves. The cleanup here will be synchronous and // no attempt will be made to communicate further with the peer. - // Intentionally copy the sessions map so that we can safely iterate over it - // while those clean themselves up. - auto sessions = sessions_; - for (auto& session : sessions) - session.second->Close(Session::CloseMethod::SILENT); - sessions.clear(); - DCHECK(sessions_.empty()); - token_map_.clear(); + idle_timer_.Close(); + session_manager().CloseAllSessionsFor(this); + DCHECK_EQ(primary_session_count_, 0); dcid_to_scid_.clear(); server_state_.reset(); @@ -1060,7 +1330,9 @@ void Endpoint::Destroy(CloseContext context, int status) { state_->closing = 0; state_->bound = 0; state_->receiving = 0; - BindingData::Get(env()).listening_endpoints.erase(this); + auto& binding = BindingData::Get(env()); + binding.listening_endpoints.erase(this); + binding.session_manager().UnregisterEndpoint(this); STAT_RECORD_TIMESTAMP(Stats, destroyed_at); EmitClose(close_context_, close_status_); @@ -1070,7 +1342,6 @@ void Endpoint::CloseGracefully() { if (is_closed() || is_closing()) return; Debug(this, "Closing gracefully"); - state_->listening = 0; state_->closing = 1; @@ -1078,28 +1349,77 @@ void Endpoint::CloseGracefully() { MaybeDestroy(); } -void Endpoint::Receive(const uv_buf_t& buf, +void Endpoint::Receive(const uint8_t* data, + size_t len, const SocketAddress& remote_address) { + const uint64_t now = uv_hrtime(); + + // Block list filtering — applied before any packet processing to + // minimize resource expenditure on blocked sources. + if (options_.block_list) { + bool matched = options_.block_list->Apply(remote_address); + bool drop = (options_.block_list_policy == Options::BlockListPolicy::DENY) + ? matched // deny list: drop if address matches + : !matched; // allow list: drop if address doesn't match + if (drop) { + Debug(this, "Packet from %s blocked by block list", remote_address); + STAT_INCREMENT(Stats, packets_blocked); + return; + } + } + const auto receive = [&](Session* session, - Store&& store, + const uint8_t* pkt_data, + size_t pkt_len, const SocketAddress& local_address, const SocketAddress& remote_address, const CID& dcid, const CID& scid) { DCHECK_NOT_NULL(session); - DCHECK(!session->is_destroyed()); - size_t len = store.length(); - if (session->Receive(std::move(store), local_address, remote_address)) { - STAT_INCREMENT_N(Stats, bytes_received, len); + if (session->is_destroyed()) return; + // Use ReadPacket (no SendPendingDataScope) so that multiple packets + // received in the same I/O burst are processed before any responses + // are generated. The deferred flush via BindingData's uv_check + // callback calls SendPendingData once per dirty session after all + // packets in the burst have been read. + if (session->ReadPacket(pkt_data, + pkt_len, + local_address, + remote_address, + PacketInfo(), + now)) { + STAT_INCREMENT_N(Stats, bytes_received, pkt_len); STAT_INCREMENT(Stats, packets_received); } + // Schedule the session for deferred SendPendingData if it hasn't + // been scheduled already in this burst. + if (!session->is_destroyed() && !session->flags_.pending_flush) { + session->flags_.pending_flush = true; + BindingData::Get(env()).ScheduleSessionFlush( + BaseObjectPtr(session)); + } }; - const auto accept = [&](const Session::Config& config, Store&& store) { + const auto accept = [&](const Session::Config& config, + const uint8_t* pkt_data, + size_t pkt_len) { // One final check. If the endpoint is closed, closing, or is not listening // as a server, then we cannot accept the initial packet. if (is_closed() || is_closing() || !is_listening()) return; + // Per-host session creation rate limit. The bucket is initialized + // on first access with the configured rate/burst from options. + auto info = addr_validation_lru_.Upsert(config.remote_address, now); + info->session_creation_bucket.InitOnce( + options_.session_creation_rate, options_.session_creation_burst, now); + if (!info->session_creation_bucket.consume(now)) { + Debug(this, + "Session creation rate limit exceeded for %s", + config.remote_address); + STAT_INCREMENT(Stats, session_creation_rate_limited); + return; + } + Debug(this, "Creating new session for %s", config.dcid); std::optional no_ticket = std::nullopt; @@ -1125,7 +1445,8 @@ void Endpoint::Receive(const uv_buf_t& buf, return; receive(session.get(), - std::move(store), + pkt_data, + pkt_len, config.local_address, config.remote_address, config.dcid, @@ -1135,7 +1456,8 @@ void Endpoint::Receive(const uv_buf_t& buf, const auto acceptInitialPacket = [&](const uint32_t version, const CID& dcid, const CID& scid, - Store&& store, + const uint8_t* pkt_data, + size_t pkt_len, const SocketAddress& local_address, const SocketAddress& remote_address) { // If we're not listening as a server, do not accept an initial packet. @@ -1145,8 +1467,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // This is our first condition check... A minimal check to see if ngtcp2 can // even recognize this packet as a quic packet. - ngtcp2_vec vec = store; - if (ngtcp2_accept(&hd, vec.base, vec.len) != NGTCP2_SUCCESS) { + if (ngtcp2_accept(&hd, pkt_data, pkt_len) != NGTCP2_SUCCESS) { // Per the ngtcp2 docs, ngtcp2_accept returns 0 if the check was // successful, or an error code if it was not. Currently there's only one // documented error code (NGTCP2_ERR_INVALID_ARGUMENT) but we'll handle @@ -1154,32 +1475,28 @@ void Endpoint::Receive(const uv_buf_t& buf, return; } - // If ngtcp2_is_supported_version returns a non-zero value, the version is - // recognized and supported. If it returns 0, we'll go ahead and send a - // version negotiation packet in response. - if (ngtcp2_is_supported_version(hd.version) == 0) { - Debug(this, - "Packet not acceptable because the version (%d) is not supported. " - "Will attempt to send version negotiation", - hd.version); - SendVersionNegotiation( - PathDescriptor{version, dcid, scid, local_address, remote_address}); - // The packet was successfully processed, even if we did refuse the - // connection. - STAT_INCREMENT(Stats, packets_received); - return; - } + // Unsupported versions are handled earlier in Receive() via the + // NGTCP2_ERR_VERSION_NEGOTIATION return from ngtcp2_pkt_decode_version_cid. + // If we reach here, the version must be supported. + CHECK_NE(ngtcp2_is_supported_version(hd.version), 0); // This is the next important condition check... If the server has been // marked busy or the remote peer has exceeded their maximum number of // concurrent connections, any new connections will be shut down // immediately. const auto limits_exceeded = ([&] { - if (sessions_.size() >= options_.max_connections_total) return true; - - SocketAddressInfoTraits::Type* counts = addrLRU_.Peek(remote_address); - auto count = counts != nullptr ? counts->active_connections : 0; - return count >= options_.max_connections_per_host; + if (state_->max_connections_total > 0 && + primary_session_count_ >= state_->max_connections_total) { + return true; + } + if (state_->max_connections_per_host > 0) { + auto it = conn_counts_per_host_.find(remote_address); + if (it != conn_counts_per_host_.end() && + it->second >= state_->max_connections_per_host) { + return true; + } + } + return false; })(); if (state_->busy || limits_exceeded) { @@ -1193,8 +1510,9 @@ void Endpoint::Receive(const uv_buf_t& buf, // the same. if (state_->busy) STAT_INCREMENT(Stats, server_busy_count); SendImmediateConnectionClose( - PathDescriptor{version, scid, dcid, local_address, remote_address}, - QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED)); + PathDescriptor{version, dcid, scid, local_address, remote_address}, + QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED), + now); // The packet was successfully processed, even if we did refuse the // connection. STAT_INCREMENT(Stats, packets_received); @@ -1204,6 +1522,12 @@ void Endpoint::Receive(const uv_buf_t& buf, Debug( this, "Accepting initial packet for %s from %s", dcid, remote_address); + // Generate a fresh server SCID rather than reusing the client's original + // DCID. The client's original DCID is typically short (8 bytes) and we + // need a 20-byte SCID to properly match short_dcidlen passed to + // ngtcp2_pkt_decode_version_cid. + auto server_scid = server_state_->options.cid_factory->Generate(); + // At this point, we start to set up the configuration for our local // session. We pass the received scid here as the dcid argument value // because that is the value *this* session will use as the outbound dcid. @@ -1214,109 +1538,102 @@ void Endpoint::Receive(const uv_buf_t& buf, local_address, remote_address, scid, - dcid, + server_scid, dcid); Debug(this, "Using session config %s", config); // The this point, the config.scid and config.dcid represent *our* views of // the CIDs. Specifically, config.dcid identifies the peer and config.scid - // identifies us. config.dcid should equal scid, and config.scid should - // equal dcid. + // identifies us. config.dcid should equal scid (peer's SCID is our DCID), + // and config.ocid should equal dcid (peer's original DCID). DCHECK(config.dcid == scid); - DCHECK(config.scid == dcid); + DCHECK(config.ocid == dcid); const auto is_remote_address_validated = ([&] { - auto info = addrLRU_.Peek(remote_address); + auto info = addr_validation_lru_.Peek(remote_address); return info != nullptr ? info->validated : false; })(); - // QUIC has address validation built in to the handshake but allows for - // an additional explicit validation request using RETRY frames. If we - // are using explicit validation, we check for the existence of a valid - // token in the packet. If one does not exist, we send a retry with - // a new token. If it does exist, and if it is valid, we grab the original - // cid and continue. - if (!is_remote_address_validated) { + // Retry token processing and address validation are two separate + // concerns. A retry token MUST always be parsed when present because + // it carries the original_destination_connection_id (ODCID) that the + // server must echo in its transport parameters. Without it, the peer + // will reject the connection with PROTOCOL_VIOLATION. + // + // The address validation LRU cache determines whether we need to + // *send* a Retry, but must NOT skip *processing* an incoming retry + // token — a concurrent connection may have already validated the + // address (populating the LRU) while this connection's Retry was + // still in flight. + + // Step 1: Always process a retry token if present, to extract the + // ODCID regardless of address validation state. + if (hd.type == NGTCP2_PKT_INITIAL && hd.tokenlen > 0 && + hd.token[0] == RetryToken::kTokenMagic) { + RetryToken token(hd.token, hd.tokenlen); + Debug(this, + "Initial packet from %s has retry token %s", + remote_address, + token); + auto ocid = + token.Validate(version, + remote_address, + dcid, + options_.token_secret, + options_.retry_token_expiration * NGTCP2_SECONDS); + if (!ocid.has_value()) { + Debug(this, "Retry token from %s is invalid.", remote_address); + SendImmediateConnectionClose( + PathDescriptor{version, scid, dcid, local_address, remote_address}, + QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED), + now); + STAT_INCREMENT(Stats, packets_received); + return; + } + + Debug(this, + "Retry token from %s is valid. Original dcid %s", + remote_address, + ocid.value()); + config.ocid = ocid.value(); + config.retry_scid = dcid; + config.set_token(token); + + // Mark the address as validated since the retry round-trip proves + // reachability. + Debug(this, "Remote address %s is validated", remote_address); + addr_validation_lru_.Upsert(remote_address, now)->validated = true; + } + + // Step 2: Address validation — decide whether to send a Retry or + // accept the packet. This only applies when the address has not + // been validated yet (no LRU hit and no retry token above). + if (!is_remote_address_validated && !config.retry_scid) { Debug(this, "Remote address %s is not validated", remote_address); switch (hd.type) { case NGTCP2_PKT_INITIAL: - // First, let's see if we need to do anything here. - if (options_.validate_address) { - // If there is no token, generate and send one. if (hd.tokenlen == 0) { Debug(this, "Initial packet has no token. Sending retry to %s to start " "validation", remote_address); - // In this case we sent a retry to the remote peer and return - // without creating a session. What we expect to happen next is - // that the remote peer will try again with a new initial packet - // that includes the retry token we are sending them. It's - // possible, however, that they just give up and go away or send - // us another initial packet that does not have the token. In that - // case we'll end up right back here asking them to validate - // again. - // - // It is possible that the SendRetry(...) won't actually send a - // retry if the remote address has exceeded the maximum number of - // retry attempts it is allowed as tracked by the addressLRU - // cache. In that case, we'll just drop the packet on the floor. - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); - // We still consider this a successfully handled packet even - // if we send a retry. + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } - // We have two kinds of tokens, each prefixed with a different - // magic byte. + // Non-retry tokens (regular tokens). switch (hd.token[0]) { - case RetryToken::kTokenMagic: { - RetryToken token(hd.token, hd.tokenlen); - Debug(this, - "Initial packet from %s has retry token %s", - remote_address, - token); - auto ocid = token.Validate( - version, - remote_address, - dcid, - options_.token_secret, - options_.retry_token_expiration * NGTCP2_SECONDS); - if (!ocid.has_value()) { - Debug( - this, "Retry token from %s is invalid.", remote_address); - // Invalid retry token was detected. Close the connection. - SendImmediateConnectionClose( - PathDescriptor{ - version, scid, dcid, local_address, remote_address}, - QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED)); - // We still consider this a successfully handled packet even - // if we send a connection close. - STAT_INCREMENT(Stats, packets_received); - return; - } - - // The ocid is the original dcid that was encoded into the - // original retry packet sent to the client. We use it for - // validation. - Debug(this, - "Retry token from %s is valid. Original dcid %s", - remote_address, - ocid.value()); - config.ocid = ocid.value(); - config.retry_scid = dcid; - config.set_token(token); - break; - } case RegularToken::kTokenMagic: { RegularToken token(hd.token, hd.tokenlen); Debug(this, @@ -1331,19 +1648,15 @@ void Endpoint::Receive(const uv_buf_t& buf, Debug(this, "Regular token from %s is invalid.", remote_address); - // If the regular token is invalid, let's send a retry to be - // lenient. There's a small risk that a malicious peer is - // trying to make us do some work but the risk is fairly low - // here. - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); - // We still consider this to be a successfully handled packet - // if a retry is sent. + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1355,62 +1668,42 @@ void Endpoint::Receive(const uv_buf_t& buf, Debug(this, "Initial packet from %s has unknown token type", remote_address); - // If our prefix bit does not match anything we know about, - // let's send a retry to be lenient. There's a small risk that a - // malicious peer is trying to make us do some work but the risk - // is fairly low here. The SendRetry will avoid sending a retry - // if the remote address has exceeded the maximum number of - // retry attempts it is allowed as tracked by the addressLRU - // cache. - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } } - // Ok! If we've got this far, our token is valid! Which means our - // path to the remote address is valid (for now). Let's record that - // so we don't have to do this dance again for this endpoint - // instance. Debug(this, "Remote address %s is validated", remote_address); - addrLRU_.Upsert(remote_address)->validated = true; + addr_validation_lru_.Upsert(remote_address, now)->validated = true; } else if (hd.tokenlen > 0) { Debug(this, "Ignoring initial packet from %s with unexpected token", remote_address); - // If validation is turned off and there is a token, that's weird. - // The peer should only have a token if we sent it to them and we - // wouldn't have sent it unless validation was turned on. Let's - // assume the peer is buggy or malicious and drop the packet on the - // floor. return; } break; case NGTCP2_PKT_0RTT: - // 0-RTT packets are inherently replayable and could be sent - // from a spoofed source address to trigger amplification. - // When address validation is enabled, we send a Retry to - // force the client to prove it can receive at its claimed - // address. This adds a round trip but prevents amplification - // attacks. When address validation is disabled (e.g., on - // trusted networks), we skip the Retry and allow 0-RTT to - // proceed without additional validation. if (options_.validate_address) { Debug( this, "Sending retry to %s due to 0RTT packet", remote_address); - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1422,7 +1715,7 @@ void Endpoint::Receive(const uv_buf_t& buf, } } - accept(config, std::move(store)); + accept(config, pkt_data, pkt_len); }; // When a received packet contains a QUIC short header but cannot be matched @@ -1438,14 +1731,15 @@ void Endpoint::Receive(const uv_buf_t& buf, // possible to avoid a DOS vector. const auto maybeStatelessReset = [&](const CID& dcid, const CID& scid, - Store& store, + const uint8_t* pkt_data, + size_t pkt_len, const SocketAddress& local_address, const SocketAddress& remote_address) { // Support for stateless resets can be disabled by the application. If that // case, or if the packet is too short to contain a reset token, then we // skip the remaining checks. if (options_.disable_stateless_reset || - store.length() < NGTCP2_STATELESS_RESET_TOKENLEN) { + pkt_len < NGTCP2_STATELESS_RESET_TOKENLEN) { return false; } @@ -1453,19 +1747,21 @@ void Endpoint::Receive(const uv_buf_t& buf, // NGTCP2_STATELESS_RESET_TOKENLEN bytes in the received packet. If it is a // stateless reset then then rest of the bytes in the packet are garbage // that we'll ignore. - ngtcp2_vec vec = store; - vec.base += (vec.len - NGTCP2_STATELESS_RESET_TOKENLEN); + const uint8_t* token_pos = + pkt_data + (pkt_len - NGTCP2_STATELESS_RESET_TOKENLEN); // If a Session has been associated with the token, then it is a valid // stateless reset token. We need to dispatch it to the session to be // processed. - auto it = token_map_.find(StatelessResetToken(vec.base)); - if (it != token_map_.end()) { + auto* session = session_manager().FindSessionByStatelessResetToken( + StatelessResetToken(token_pos)); + if (session != nullptr) { // If the session happens to have been destroyed already, we'll // just ignore the packet. - if (!it->second->is_destroyed()) [[likely]] { - receive(it->second, - std::move(store), + if (!session->is_destroyed()) [[likely]] { + receive(session, + pkt_data, + pkt_len, local_address, remote_address, dcid, @@ -1487,28 +1783,8 @@ void Endpoint::Receive(const uv_buf_t& buf, } #endif // DEBUG - // TODO(@jasnell): Implement blocklist support - // if (block_list_->Apply(remote_address)) [[unlikely]] { - // Debug(this, "Ignoring blocked remote address: %s", remote_address); - // return; - // } - - Debug(this, "Received %zu-byte packet from %s", buf.len, remote_address); + Debug(this, "Received %zu-byte packet from %s", len, remote_address); - // The managed buffer here contains the received packet. We do not yet know - // at this point if it is a valid QUIC packet. We need to do some basic - // checks. It is critical at this point that we do as little work as possible - // to avoid a DOS vector. - std::shared_ptr backing = env()->release_managed_buffer(buf); - if (!backing) [[unlikely]] { - // At this point something bad happened and we need to treat this as a fatal - // case. There's likely no way to test this specific condition reliably. - return Destroy(CloseContext::RECEIVE_FAILURE, UV_ENOMEM); - } - - Store store(std::move(backing), buf.len, 0); - - ngtcp2_vec vec = store; ngtcp2_version_cid pversion_cid; // This is our first check to see if the received data can be processed as a @@ -1516,10 +1792,51 @@ void Endpoint::Receive(const uv_buf_t& buf, // cannot be processed; all we can do is ignore it. If it succeeds, we have a // valid QUIC header but there is still no guarantee that the packet can be // successfully processed. - if (ngtcp2_pkt_decode_version_cid( - &pversion_cid, vec.base, vec.len, NGTCP2_MAX_CIDLEN) < 0) { - Debug(this, "Failed to decode packet header, ignoring"); - return; // Ignore the packet! + switch (ngtcp2_pkt_decode_version_cid( + &pversion_cid, data, len, NGTCP2_MAX_CIDLEN)) { + case 0: + break; // Supported version, continue processing. + case NGTCP2_ERR_VERSION_NEGOTIATION: { + // The packet has an unsupported version but the CIDs were + // successfully decoded. Send a Version Negotiation response + // per RFC 9000 Section 6. The VN packet's DCID is the client's + // SCID and vice versa (mirrored back to the client). + // + // ngtcp2_pkt_decode_version_cid() only enforces the + // NGTCP2_MAX_CIDLEN limit for *supported* versions; for an + // unsupported version it returns the raw connection ID lengths + // taken from the single-byte length fields on the wire, which can + // be up to 255. Constructing a CID -- backed by a fixed + // NGTCP2_MAX_CIDLEN-byte buffer -- from such a length writes past + // the buffer (an assertion abort in release builds). A single + // unauthenticated UDP datagram could therefore crash the endpoint + // before any handshake. Drop these packets, mirroring the + // CID-length policy applied below for supported versions. + if (pversion_cid.dcidlen > NGTCP2_MAX_CIDLEN || + pversion_cid.scidlen > NGTCP2_MAX_CIDLEN) { + Debug(this, + "Version negotiation packet had incorrectly sized CIDs, " + "ignoring"); + return; + } + Debug(this, + "Packet version %d is not supported, sending version negotiation", + pversion_cid.version); + CID dcid(pversion_cid.dcid, pversion_cid.dcidlen); + CID scid(pversion_cid.scid, pversion_cid.scidlen); + SendVersionNegotiation(PathDescriptor{pversion_cid.version, + dcid, + scid, + local_address(), + remote_address}, + now); + STAT_INCREMENT(Stats, packets_received); + return; + } + default: + // Truly invalid packet — cannot be decoded at all. + Debug(this, "Failed to decode packet header, ignoring"); + return; } // QUIC currently requires CID lengths of max NGTCP2_MAX_CIDLEN. Ignore any @@ -1575,18 +1892,30 @@ void Endpoint::Receive(const uv_buf_t& buf, // stateless reset, the packet will be handled with no additional action // necessary here. We want to return immediately without committing any // further resources. - if (!scid && maybeStatelessReset(dcid, scid, store, addr, remote_address)) { + if (pversion_cid.version == 0 && + maybeStatelessReset(dcid, scid, data, len, addr, remote_address)) { Debug(this, "Packet was a stateless reset"); return; // Stateless reset! Don't do any further processing. } + // If this is a short header packet for an unknown DCID, send a + // stateless reset so the peer knows the session is gone. Short header + // packets are identified by version == 0 (set by ngtcp2_pkt_decode_ + // version_cid). We must NOT use !scid here because long header Initial + // packets can have a 0-length SCID (valid per RFC 9000 Section 7.2). + if (pversion_cid.version == 0) { + Debug(this, "Sending stateless reset for unknown short header packet"); + SendStatelessReset( + PathDescriptor{ + pversion_cid.version, dcid, scid, addr, remote_address}, + len, + now); + return; + } + // Process the packet as an initial packet... - return acceptInitialPacket(pversion_cid.version, - dcid, - scid, - std::move(store), - addr, - remote_address); + return acceptInitialPacket( + pversion_cid.version, dcid, scid, data, len, addr, remote_address); } if (session->is_destroyed()) [[unlikely]] { @@ -1598,7 +1927,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // If we got here, the dcid matched the scid of a known local session. Yay! // The session will take over any further processing of the packet. Debug(this, "Dispatching packet to known session"); - receive(session.get(), std::move(store), addr, remote_address, dcid, scid); + receive(session.get(), data, len, addr, remote_address, dcid, scid); // It is important to note that the session may have been destroyed during // the call to receive(...). If that's the case, the session object still @@ -1612,18 +1941,9 @@ void Endpoint::PacketDone(int status) { DCHECK_GE(state_->pending_callbacks, 1); state_->pending_callbacks--; env()->DecreaseWaitingRequestCounter(); - // Can we go ahead and close now? - if (state_->closing == 1) MaybeDestroy(); -} - -void Endpoint::IncrementSocketAddressCounter(const SocketAddress& addr) { - addrLRU_.Upsert(addr)->active_connections++; -} - -void Endpoint::DecrementSocketAddressCounter(const SocketAddress& addr) { - auto* counts = addrLRU_.Peek(addr); - if (counts != nullptr && counts->active_connections > 0) - counts->active_connections--; + // Check if we can close or start the idle timer now that this + // pending callback has completed. + if (state_->closing == 1 || primary_session_count_ == 0) MaybeDestroy(); } bool Endpoint::is_closed() const { @@ -1644,23 +1964,21 @@ void Endpoint::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("server_options", server_state_->options); tracker->TrackField("server_tls_context", server_state_->tls_context); } - tracker->TrackField("token_map", token_map_); - tracker->TrackField("sessions", sessions_); - tracker->TrackField("cid_map", dcid_to_scid_); - tracker->TrackField("address LRU", addrLRU_); + tracker->TrackField("address LRU", addr_validation_lru_); } // ====================================================================================== // Endpoint::SocketAddressInfoTraits bool Endpoint::SocketAddressInfoTraits::CheckExpired( - const SocketAddress& address, const Type& type) { - return (uv_hrtime() - type.timestamp) > kSocketAddressInfoTimeout; + const SocketAddress& address, const Type& type, uint64_t now) { + return (now - type.timestamp) > kSocketAddressInfoTimeout; } void Endpoint::SocketAddressInfoTraits::Touch(const SocketAddress& address, - Type* type) { - type->timestamp = uv_hrtime(); + Type* type, + uint64_t now) { + type->timestamp = now; } // ====================================================================================== diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index 0156ea23ff844e..f54e028ead4554 100644 --- a/src/quic/endpoint.h +++ b/src/quic/endpoint.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include "bindingdata.h" #include "packet.h" #include "session.h" +#include "session_manager.h" #include "sessionticket.h" #include "tokens.h" @@ -24,27 +26,31 @@ namespace node::quic { // client and server simultaneously. class Endpoint final : public AsyncWrap, public Packet::Listener { public: - static constexpr uint64_t DEFAULT_MAX_CONNECTIONS = - std::min(kMaxSizeT, kMaxSafeJsInteger); - static constexpr uint64_t DEFAULT_MAX_CONNECTIONS_PER_HOST = 100; - static constexpr uint64_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE = - (DEFAULT_MAX_CONNECTIONS_PER_HOST * 10); - static constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS = 10; - static constexpr uint64_t DEFAULT_MAX_RETRY_LIMIT = 10; - - // Maximum number of version negotiation packets that will be sent to a - // given remote host within the LRU tracking window. Version negotiation - // packets are cheap to generate but can be used as an amplification - // vector with spoofed source addresses. - // TODO(@jasnell): Consider making this configurable via Endpoint::Options. - static constexpr uint64_t kMaxVersionNegotiations = 10; - - // Maximum number of immediate connection close packets that will be sent - // to a given remote host within the LRU tracking window. These are sent - // when the server is busy or a token is invalid — a malicious peer could - // trigger a large number of them. - // TODO(@jasnell): Consider making this configurable via Endpoint::Options. - static constexpr uint64_t kMaxImmediateCloses = 10; + // The socket address LRU is used for tracking validated remote addresses. + static constexpr uint64_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE = 1024; + + // Default rate limits for stateless responses. These are global token + // bucket limits that cap the total rate of each response type regardless + // of source address. This prevents spoofed-source floods from bypassing + // per-host limits (which are keyed by source IP and trivially defeated + // by rotating spoofed addresses). The rate is in responses per second + // and the burst is the maximum tokens the bucket can hold. + static constexpr double DEFAULT_RETRY_RATE = 100; + static constexpr double DEFAULT_RETRY_BURST = 200; + static constexpr double DEFAULT_STATELESS_RESET_RATE = 100; + static constexpr double DEFAULT_STATELESS_RESET_BURST = 200; + static constexpr double DEFAULT_VERSION_NEGOTIATION_RATE = 100; + static constexpr double DEFAULT_VERSION_NEGOTIATION_BURST = 200; + static constexpr double DEFAULT_IMMEDIATE_CLOSE_RATE = 100; + static constexpr double DEFAULT_IMMEDIATE_CLOSE_BURST = 200; + + // Per-host session creation rate limit. This is tracked per validated + // remote address in the address LRU, preventing a single source from + // churning through sessions faster than the server can handle. Unlike + // the global stateless response buckets, this only applies after address + // validation (spoofed sources can't reach this path). + static constexpr double DEFAULT_SESSION_CREATION_RATE = 50; + static constexpr double DEFAULT_SESSION_CREATION_BURST = 100; // Endpoint configuration options struct Options final : public MemoryRetainer { @@ -64,41 +70,32 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { RetryToken::QUIC_DEFAULT_RETRYTOKEN_EXPIRATION / NGTCP2_SECONDS; // Tokens issued using NEW_TOKEN are time-limited. By default, tokens expire - // after DEFAULT_TOKEN_EXPIRATION *seconds*. + // after QUIC_DEFAULT_REGULARTOKEN_EXPIRATION *seconds*. uint64_t token_expiration = RegularToken::QUIC_DEFAULT_REGULARTOKEN_EXPIRATION / NGTCP2_SECONDS; - // Each Endpoint places limits on the number of concurrent connections from - // a single host, and the total number of concurrent connections allowed as - // a whole. These are set to fairly modest, and arbitrary defaults. We can - // set these to whatever we'd like. - uint64_t max_connections_per_host = DEFAULT_MAX_CONNECTIONS_PER_HOST; - uint64_t max_connections_total = DEFAULT_MAX_CONNECTIONS; - - // A stateless reset in QUIC is a discrete mechanism that one endpoint can - // use to communicate to a peer that it has lost whatever state it - // previously held about a session. Because generating a stateless reset - // consumes resources (even very modestly), they can be a DOS vector in - // which a malicious peer intentionally sends a large number of stateless - // reset eliciting packets. To protect against that risk, we limit the - // number of stateless resets that may be generated for a given remote host - // within a window of time. This is not mandated by QUIC, and the limit is - // arbitrary. We can set it to whatever we'd like. - uint64_t max_stateless_resets = DEFAULT_MAX_STATELESS_RESETS; - - // For tracking the number of connections per host, the number of stateless - // resets that have been sent, and tracking the path verification status of - // a remote host, we maintain an LRU cache of the most recently seen hosts. - // The address_lru_size parameter determines the size of that cache. The - // default is set modestly at 10 times the default max connections per host. + // For tracking the path verification status of remote hosts, we maintain + // an LRU cache of the most recently seen hosts. uint64_t address_lru_size = DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE; - // Similar to stateless resets, we enforce a limit on the number of retry - // packets that can be generated and sent for a remote host. Generating - // retry packets consumes a modest amount of resources and it's fairly - // trivial for a malicious peer to trigger generation of a large number of - // retries, so limiting them helps prevent a DOS vector. - uint64_t max_retries = DEFAULT_MAX_RETRY_LIMIT; + // Global token bucket rate limits for stateless responses. These cap + // the total rate of each response type regardless of source address, + // preventing spoofed-source floods. Rate is in responses per second, + // burst is the maximum number of responses that can be sent in a burst. + double retry_rate = DEFAULT_RETRY_RATE; + double retry_burst = DEFAULT_RETRY_BURST; + double stateless_reset_rate = DEFAULT_STATELESS_RESET_RATE; + double stateless_reset_burst = DEFAULT_STATELESS_RESET_BURST; + double version_negotiation_rate = DEFAULT_VERSION_NEGOTIATION_RATE; + double version_negotiation_burst = DEFAULT_VERSION_NEGOTIATION_BURST; + double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE; + double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST; + + // Per-host session creation rate limit. Tracked per validated remote + // address in the address LRU. Set to high values for benchmarking + // where traffic comes from a single source. + double session_creation_rate = DEFAULT_SESSION_CREATION_RATE; + double session_creation_burst = DEFAULT_SESSION_CREATION_BURST; // The validate_address parameter instructs the Endpoint to perform explicit // address validation using retry tokens. This is strongly recommended and @@ -154,6 +151,25 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // Setting to 0 uses the default. uint8_t udp_ttl = 0; + // When an endpoint becomes idle (not listening and no primary sessions), + // it will be destroyed after this many seconds. A value of 0 means + // destroy immediately when idle (default, preserves pre-SessionManager + // behavior). A positive value keeps the endpoint alive for potential + // reuse by future connect() or listen() calls. + static constexpr uint64_t DEFAULT_IDLE_TIMEOUT = 0; + uint64_t idle_timeout = DEFAULT_IDLE_TIMEOUT; + + // Optional block list for filtering incoming packets by source address. + // When block_list_policy is DENY, packets from addresses matching the + // block list are dropped. When ALLOW, only packets from addresses + // matching the block list are accepted (all others dropped). + enum class BlockListPolicy : uint8_t { + DENY, // Drop packets from matching addresses (blocklist) + ALLOW, // Drop packets from non-matching addresses (allowlist) + }; + std::shared_ptr block_list; + BlockListPolicy block_list_policy = BlockListPolicy::DENY; + void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Endpoint::Config) SET_SELF_SIZE(Options) @@ -212,6 +228,20 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { void Send(Packet::Ptr packet); + // Attempt synchronous send via uv_udp_try_send. If the socket is + // writable, the packet is sent immediately and the Ptr is released. + // If the socket is not writable (UV_EAGAIN), falls back to the + // async Send path. Used by the deferred flush callback to avoid + // the one-tick latency of async uv_udp_send. + void SendOrTrySend(Packet::Ptr packet); + + // Send a batch of packets using uv_udp_try_send2 (sendmmsg) for + // synchronous batched delivery. Packets successfully sent are released + // immediately. On EAGAIN or partial send, remaining packets fall back + // to async uv_udp_send. The Packet::Ptr array is consumed: all entries + // will be empty (released or moved) on return. + void SendBatch(Packet::Ptr* packets, size_t count); + // Acquire a Packet from the pool. length sets the initial working // size (must be <= pool capacity). The slot is always allocated at // full capacity to avoid fragmentation. @@ -233,24 +263,27 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // ellicit retry packets (It can do so by intentionally sending initial // packets that ignore the retry token). To help mitigate that risk, we limit // the number of retries we send to a given remote endpoint. - void SendRetry(const PathDescriptor& options); + void SendRetry(const PathDescriptor& options, uint64_t now); // Sends a version negotiation packet. This is terminal for the connection and // is sent only when a QUIC packet is received for an unsupported QUIC // version. It is possible that a malicious packet triggered this so we need // to be careful not to commit too many resources. - void SendVersionNegotiation(const PathDescriptor& options); + void SendVersionNegotiation(const PathDescriptor& options, uint64_t now); // Possibly generates and sends a stateless reset packet. This is terminal for // the connection. It is possible that a malicious packet triggered this so we // need to be careful not to commit too many resources. - bool SendStatelessReset(const PathDescriptor& options, size_t source_len); + bool SendStatelessReset(const PathDescriptor& options, + size_t source_len, + uint64_t now); // Shutdown a connection prematurely, before a Session is created. This should // only be called at the start of a session before the crypto keys have been // established. void SendImmediateConnectionClose(const PathDescriptor& options, - QuicError error); + QuicError error, + uint64_t now); // Listen for connections (act as a server). void Listen(const Session::Options& options); @@ -285,6 +318,20 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { void Close(); int Send(Packet::Ptr packet); + // Synchronous send using uv_udp_try_send. Returns the number of + // bytes sent on success, UV_EAGAIN if the socket is not writable + // or the send queue is non-empty, or another negative error code. + // The Ptr is not consumed — the caller manages the lifecycle. + int TrySend(const Packet::Ptr& packet); + + // Synchronous batched send using uv_udp_try_send2 (sendmmsg). + // Takes pre-built libuv argument arrays. Returns the number of + // messages successfully sent (>= 0), or a negative error code. + int TrySendBatch(uv_buf_t* bufs[], + unsigned int nbufs[], + struct sockaddr* addrs[], + size_t count); + // Returns the local UDP socket address to which we are bound, // or fail with an assert if we are not bound. SocketAddress local_address() const; @@ -305,9 +352,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { class Impl; BaseObjectWeakPtr impl_; - bool is_bound_ = false; - bool is_started_ = false; - bool is_closed_ = false; + struct Flags { + uint8_t is_bound : 1 = 0; + uint8_t is_started : 1 = 0; + uint8_t is_closed : 1 = 0; + }; + Flags flags_; }; bool is_closed() const; @@ -344,9 +394,6 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { void EmitNewSession(const BaseObjectPtr& session); void EmitClose(CloseContext context, int status); - void IncrementSocketAddressCounter(const SocketAddress& address); - void DecrementSocketAddressCounter(const SocketAddress& address); - // JavaScript API // Create a new Endpoint. @@ -388,7 +435,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // Ref() causes a listening Endpoint to keep the event loop active. JS_METHOD(Ref); - void Receive(const uv_buf_t& buf, const SocketAddress& from); + void Receive(const uint8_t* data, size_t len, const SocketAddress& from); AliasedStruct stats_; AliasedStruct state_; @@ -396,6 +443,11 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { ArenaPool packet_pool_; UDP udp_; + // Idle timer: started when the endpoint becomes idle (not listening, + // no primary sessions). When it fires, the endpoint is destroyed. + // Stopped when a new session is added or listening begins. + TimerWrapHandle idle_timer_; + struct ServerState { Session::Options options; std::shared_ptr tls_context; @@ -403,31 +455,56 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // Set if/when the endpoint is configured to listen. std::optional server_state_ = std::nullopt; - // A Session is generally identified by one or more CIDs. We use two - // maps for this rather than one to avoid creating a whole bunch of - // BaseObjectPtr references. The primary map (sessions_) just maps - // the original CID to the Session, the second map (dcid_to_scid_) - // maps the additional CIDs to the primary. - CID::Map> sessions_; + // Count of sessions for which this endpoint is the primary endpoint. + // Drives ref/unref and idle timer logic. The actual session-to-endpoint + // mapping is maintained by the SessionManager. + size_t primary_session_count_ = 0; + + // Per-endpoint CID -> SCID mapping for peer-chosen CIDs from connection + // establishment (config.dcid, config.ocid). These are kept per-endpoint + // because peer-chosen values can collide across endpoints (e.g., a + // client's random outgoing DCID matching an incoming DCID on the server + // endpoint). Locally-generated CIDs that need cross-endpoint routing + // (preferred address, multipath) go in SessionManager::dcid_to_scid_. + // + // Endpoint::FindSession does a three-tier lookup: + // 1. SessionManager::sessions_[cid] (direct SCID match) + // 2. SessionManager::dcid_to_scid_[cid] (cross-endpoint CID) + // 3. Endpoint::dcid_to_scid_[cid] (peer-chosen CID) + // Each tier resolves to an SCID and looks up SessionManager::sessions_. CID::Map dcid_to_scid_; - StatelessResetToken::Map token_map_; + + SessionManager& session_manager() const; struct SocketAddressInfoTraits final { struct Type final { - size_t active_connections; - size_t reset_count; - size_t retry_count; - size_t version_negotiation_count; - size_t immediate_close_count; uint64_t timestamp; bool validated; + TokenBucket session_creation_bucket; }; - static bool CheckExpired(const SocketAddress& address, const Type& type); - static void Touch(const SocketAddress& address, Type* type); + static bool CheckExpired(const SocketAddress& address, + const Type& type, + uint64_t now); + static void Touch(const SocketAddress& address, Type* type, uint64_t now); }; - SocketAddressLRU addrLRU_; + SocketAddressLRU addr_validation_lru_; + + // Global token buckets for stateless response rate limiting. + // These cap the total server-wide rate of each response type, + // regardless of source address. + TokenBucket retry_bucket_; + TokenBucket stateless_reset_bucket_; + TokenBucket version_negotiation_bucket_; + TokenBucket immediate_close_bucket_; + + // Per-IP connection counts for maxConnectionsPerHost enforcement. + // Only populated when max_connections_per_host > 0. Entries are + // added in AddSession and removed when the count reaches 0 in + // RemoveSession. The map size is bounded by the number of active + // sessions (each entry has count >= 1). + SocketAddress::IpMap conn_counts_per_host_; CloseContext close_context_ = CloseContext::CLOSE; int close_status_ = 0; diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 2a21c0cf321970..bc479f96990577 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include "application.h" #include "bindingdata.h" #include "defs.h" @@ -25,6 +26,63 @@ using v8::Local; namespace quic { +namespace { +constexpr uint8_t kSessionTicketAppDataVersion = 1; +// Layout: [type(1)][version(1)][crc(4)][payload(34)] = 40 bytes +constexpr size_t kSessionTicketAppDataSize = 40; +constexpr size_t kSessionTicketAppDataHeaderSize = 6; // type + version + crc +constexpr size_t kSessionTicketAppDataPayloadSize = + kSessionTicketAppDataSize - kSessionTicketAppDataHeaderSize; + +inline void WriteBE32(uint8_t* buf, uint32_t val) { + buf[0] = static_cast((val >> 24) & 0xff); + buf[1] = static_cast((val >> 16) & 0xff); + buf[2] = static_cast((val >> 8) & 0xff); + buf[3] = static_cast(val & 0xff); +} + +inline uint32_t ReadBE32(const uint8_t* buf) { + return (static_cast(buf[0]) << 24) | + (static_cast(buf[1]) << 16) | + (static_cast(buf[2]) << 8) | static_cast(buf[3]); +} + +inline void WriteBE64(uint8_t* buf, uint64_t val) { + buf[0] = static_cast((val >> 56) & 0xff); + buf[1] = static_cast((val >> 48) & 0xff); + buf[2] = static_cast((val >> 40) & 0xff); + buf[3] = static_cast((val >> 32) & 0xff); + buf[4] = static_cast((val >> 24) & 0xff); + buf[5] = static_cast((val >> 16) & 0xff); + buf[6] = static_cast((val >> 8) & 0xff); + buf[7] = static_cast(val & 0xff); +} + +inline uint64_t ReadBE64(const uint8_t* buf) { + return (static_cast(buf[0]) << 56) | + (static_cast(buf[1]) << 48) | + (static_cast(buf[2]) << 40) | + (static_cast(buf[3]) << 32) | + (static_cast(buf[4]) << 24) | + (static_cast(buf[5]) << 16) | + (static_cast(buf[6]) << 8) | static_cast(buf[7]); +} + +// Serialize an nghttp3_pri into an RFC 9218 priority field value +// (e.g., "u=3" or "u=0, i"). Returns the number of bytes written. +// This is used only for setting the priority field of HTTP/3 streams on +// the client side. +inline size_t FormatPriority(char* buf, size_t buflen, const nghttp3_pri& pri) { + int len; + if (pri.inc) { + len = snprintf(buf, buflen, "u=%d, i", pri.urgency); + } else { + len = snprintf(buf, buflen, "u=%d", pri.urgency); + } + return static_cast(len); +} +} // namespace + struct Http3HeadersTraits { using nv_t = nghttp3_nv; }; @@ -85,20 +143,69 @@ class Http3ApplicationImpl final : public Session::Application { public: Http3ApplicationImpl(Session* session, const Options& options) : Application(session, options), - allocator_(BindingData::Get(env())), + allocator_(BindingData::Get(env()).nghttp3_allocator()), options_(options), - conn_(InitializeConnection()) { + conn_(nullptr) { + // Build the ORIGIN frame payload from the SNI configuration before + // creating the nghttp3 connection, since InitializeConnection needs + // the origin_vec_ to be ready for settings.origin_list. + if (session->is_server()) { + BuildOriginPayload(); + } + conn_ = InitializeConnection(); session->set_priority_supported(); } + const Options& options() const override { return options_; } + Session::Application::Type type() const override { return Session::Application::Type::HTTP3; } error_code GetNoErrorCode() const override { return NGHTTP3_H3_NO_ERROR; } + // HTTP/3 defines H3_INTERNAL_ERROR (0x102) for non-specific failures + // initiated by the implementation; this is the right code to send + // on RESET_STREAM when a stream is being aborted without an + // application-supplied code. + error_code GetInternalErrorCode() const override { + return NGHTTP3_H3_INTERNAL_ERROR; + } + + void EarlyDataRejected() override { + // When 0-RTT is rejected, destroy the nghttp3 connection and all + // open streams — ngtcp2 has discarded their internal state. + // Reset started_ so Start() is called again via on_receive_rx_key + // at 1RTT to recreate the nghttp3 connection. Use the + // application's internal error code since this is an error + // condition (code 0 would be treated as a clean close). + conn_.reset(); + started_ = false; + session().DestroyAllStreams( + QuicError::ForApplication(GetInternalErrorCode())); + if (!session().is_destroyed()) { + session().EmitEarlyDataRejected(); + } + } + + bool ReceiveStreamOpen(stream_id id) override { + // In HTTP/3, only create Stream objects for bidirectional streams. + // Unidirectional streams (control, QPACK encoder/decoder) are + // managed internally by nghttp3 and should not be exposed to JS. + if (!ngtcp2_is_bidi_stream(id)) return true; + auto stream = session().CreateStream(id); + if (!stream || session().is_destroyed()) [[unlikely]] { + return !session().is_destroyed(); + } + return true; + } + + bool SupportsHeaders() const override { return true; } + + bool is_started() const override { return started_; } + bool Start() override { - CHECK(!started_); + if (started_) return true; started_ = true; Debug(&session(), "Starting HTTP/3 application."); @@ -158,20 +265,35 @@ class Http3ApplicationImpl final : public Session::Application { return ret; } - bool ReceiveStreamData(int64_t stream_id, + void BeginShutdown() override { + // Only submit a shutdown notice if the H3 connection was fully + // started (control streams bound). If the TLS handshake failed + // before Start() was called, conn_ exists but its control streams + // are unbound, and nghttp3_conn_submit_shutdown_notice would crash. + if (conn_ && started_) nghttp3_conn_submit_shutdown_notice(*this); + } + + void CompleteShutdown() override { + // Same guard as BeginShutdown — nghttp3_conn_shutdown asserts + // that the control stream is bound (conn->tx.ctrl != NULL). + if (conn_ && started_) nghttp3_conn_shutdown(*this); + } + + bool ReceiveStreamData(stream_id id, const uint8_t* data, size_t datalen, const Stream::ReceiveDataFlags& flags, void* unused) override { Debug(&session(), "HTTP/3 application received %zu bytes of data " - "on stream %" PRIi64 ". Is final? %d", + "on stream %" PRIi64 ". Is final? %d. Is early? %d", datalen, - stream_id, - flags.fin); + id, + flags.fin, + flags.early); - ssize_t nread = nghttp3_conn_read_stream( - *this, stream_id, data, datalen, flags.fin ? 1 : 0); + auto nread = nghttp3_conn_read_stream2( + *this, id, data, datalen, flags.fin ? 1 : 0, uv_hrtime()); if (nread < 0) { Debug(&session(), @@ -184,20 +306,29 @@ class Http3ApplicationImpl final : public Session::Application { Debug(&session(), "Extending stream and connection offset by %zd bytes", nread); - session().ExtendStreamOffset(stream_id, nread); + session().ExtendStreamOffset(id, nread); session().ExtendOffset(nread); } + // If this data arrived as 0-RTT, mark the stream. We set it after + // nghttp3_conn_read_stream2 because the stream may not exist until + // nghttp3 processes the headers (via on_begin_headers). + if (flags.early) { + if (auto stream = session().FindStream(id)) { + stream->set_early(); + } + } + return true; } - bool AcknowledgeStreamData(int64_t stream_id, size_t datalen) override { + bool AcknowledgeStreamData(stream_id id, size_t datalen) override { Debug(&session(), "HTTP/3 application received acknowledgement for %zu bytes of data " "on stream %" PRIi64, datalen, - stream_id); - return nghttp3_conn_add_ack_offset(*this, stream_id, datalen) == 0; + id); + return nghttp3_conn_add_ack_offset(*this, id, datalen) == 0; } bool CanAddHeader(size_t current_count, @@ -205,18 +336,24 @@ class Http3ApplicationImpl final : public Session::Application { size_t this_header_length) override { // We cannot add the header if we've either reached // * the max number of header pairs or - // * the max number of header bytes + // * the max number of header bytes (name + value combined) return (current_count < options_.max_header_pairs) && (current_headers_length + this_header_length) <= options_.max_header_length; } - void BlockStream(int64_t id) override { + bool stream_fin_managed_by_application() const override { return true; } + + void StreamWriteShut(stream_id id) override { + nghttp3_conn_shutdown_stream_write(*this, id); + } + + void BlockStream(stream_id id) override { nghttp3_conn_block_stream(*this, id); Application::BlockStream(id); } - void ResumeStream(int64_t id) override { + void ResumeStream(stream_id id) override { nghttp3_conn_resume_stream(*this, id); Application::ResumeStream(id); } @@ -259,31 +396,126 @@ class Http3ApplicationImpl final : public Session::Application { void CollectSessionTicketAppData( SessionTicket::AppData* app_data) const override { - // TODO(@jasnell): When HTTP/3 settings become dynamic or - // configurable per-connection, store them here so they can be - // validated on 0-RTT resumption. Candidates include: - // max_field_section_size, qpack_max_dtable_capacity, - // qpack_encoder_max_dtable_capacity, qpack_blocked_streams, - // enable_connect_protocol, and enable_datagrams. On extraction, - // compare stored values against current settings and return - // TICKET_IGNORE_RENEW if incompatible. + uint8_t buf[kSessionTicketAppDataSize]; + buf[0] = static_cast(Type::HTTP3); + buf[1] = kSessionTicketAppDataVersion; + + uint8_t* payload = buf + kSessionTicketAppDataHeaderSize; + WriteBE64(payload, options_.max_field_section_size); + WriteBE64(payload + 8, options_.qpack_max_dtable_capacity); + WriteBE64(payload + 16, options_.qpack_encoder_max_dtable_capacity); + WriteBE64(payload + 24, options_.qpack_blocked_streams); + payload[32] = options_.enable_connect_protocol ? 1 : 0; + payload[33] = options_.enable_datagrams ? 1 : 0; + + uLong crc = crc32(0L, Z_NULL, 0); + crc = crc32(crc, payload, kSessionTicketAppDataPayloadSize); + WriteBE32(buf + 2, static_cast(crc)); + + app_data->Set( + uv_buf_init(reinterpret_cast(buf), kSessionTicketAppDataSize)); } SessionTicket::AppData::Status ExtractSessionTicketAppData( const SessionTicket::AppData& app_data, SessionTicket::AppData::Source::Flag flag) override { - // See CollectSessionTicketAppData above. + auto data = app_data.Get(); + if (!data || data->len != kSessionTicketAppDataSize) { + return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW; + } + + const uint8_t* buf = reinterpret_cast(data->base); + + // buf[0] is the application type byte, buf[1] is the version. + if (buf[0] != static_cast(Type::HTTP3) || + buf[1] != kSessionTicketAppDataVersion) { + Debug(&session(), + "Ticket app data rejected: type=%d version=%d " + "(expected type=%d version=%d)", + buf[0], + buf[1], + static_cast(Type::HTTP3), + kSessionTicketAppDataVersion); + return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW; + } + + const uint8_t* payload = buf + kSessionTicketAppDataHeaderSize; + uint32_t stored_crc = ReadBE32(buf + 2); + uLong computed_crc = crc32(0L, Z_NULL, 0); + computed_crc = + crc32(computed_crc, payload, kSessionTicketAppDataPayloadSize); + if (stored_crc != static_cast(computed_crc)) { + Debug(&session(), + "Ticket app data rejected: CRC mismatch " + "(stored=%u computed=%u)", + stored_crc, + static_cast(computed_crc)); + return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW; + } + + uint64_t stored_max_field_section_size = ReadBE64(payload); + uint64_t stored_qpack_max_dtable_capacity = ReadBE64(payload + 8); + uint64_t stored_qpack_encoder_max_dtable_capacity = ReadBE64(payload + 16); + uint64_t stored_qpack_blocked_streams = ReadBE64(payload + 24); + bool stored_enable_connect_protocol = payload[32] != 0; + bool stored_enable_datagrams = payload[33] != 0; + + Debug(&session(), + "Ticket app data: stored mfss=%" PRIu64 " qmdc=%" PRIu64 + " qemdc=%" PRIu64 " qbs=%" PRIu64 " ecp=%d ed=%d", + stored_max_field_section_size, + stored_qpack_max_dtable_capacity, + stored_qpack_encoder_max_dtable_capacity, + stored_qpack_blocked_streams, + stored_enable_connect_protocol, + stored_enable_datagrams); + Debug(&session(), + "Current opts: mfss=%" PRIu64 " qmdc=%" PRIu64 " qemdc=%" PRIu64 + " qbs=%" PRIu64 " ecp=%d ed=%d", + options_.max_field_section_size, + options_.qpack_max_dtable_capacity, + options_.qpack_encoder_max_dtable_capacity, + options_.qpack_blocked_streams, + options_.enable_connect_protocol, + options_.enable_datagrams); + if (options_.max_field_section_size < stored_max_field_section_size || + options_.qpack_max_dtable_capacity < stored_qpack_max_dtable_capacity || + options_.qpack_encoder_max_dtable_capacity < + stored_qpack_encoder_max_dtable_capacity || + options_.qpack_blocked_streams < stored_qpack_blocked_streams || + (stored_enable_connect_protocol && !options_.enable_connect_protocol) || + (stored_enable_datagrams && !options_.enable_datagrams)) { + Debug(&session(), "Ticket app data REJECTED"); + return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW; + } + Debug(&session(), "Ticket app data ACCEPTED"); + return flag == SessionTicket::AppData::Source::Flag::STATUS_RENEW ? SessionTicket::AppData::Status::TICKET_USE_RENEW : SessionTicket::AppData::Status::TICKET_USE; } - void StreamClose(Stream* stream, QuicError&& error = QuicError()) override { + bool ApplySessionTicketData(const PendingTicketAppData& data) override { + if (!std::holds_alternative(data)) return false; + const auto& ticket = std::get(data); + // Validate that current settings are >= stored settings. + return options_.max_field_section_size >= ticket.max_field_section_size && + options_.qpack_max_dtable_capacity >= + ticket.qpack_max_dtable_capacity && + options_.qpack_encoder_max_dtable_capacity >= + ticket.qpack_encoder_max_dtable_capacity && + options_.qpack_blocked_streams >= ticket.qpack_blocked_streams && + (!ticket.enable_connect_protocol || + options_.enable_connect_protocol) && + (!ticket.enable_datagrams || options_.enable_datagrams); + } + + void ReceiveStreamClose(Stream* stream, + QuicError&& error = QuicError()) override { Debug( &session(), "HTTP/3 application closing stream %" PRIi64, stream->id()); - uint64_t code = NGHTTP3_H3_NO_ERROR; - if (error) { - CHECK_EQ(error.type(), QuicError::Type::APPLICATION); + error_code code = NGHTTP3_H3_NO_ERROR; + if (error.type() == QuicError::Type::APPLICATION) { code = error.code(); } @@ -303,9 +535,9 @@ class Http3ApplicationImpl final : public Session::Application { session().Close(); } - void StreamReset(Stream* stream, - uint64_t final_size, - QuicError&& error = QuicError()) override { + void ReceiveStreamReset(Stream* stream, + uint64_t final_size, + QuicError&& error = QuicError()) override { // We are shutting down the readable side of the local stream here. Debug(&session(), "HTTP/3 application resetting stream %" PRIi64, @@ -321,9 +553,9 @@ class Http3ApplicationImpl final : public Session::Application { session().Close(); } - void StreamStopSending(Stream* stream, - QuicError&& error = QuicError()) override { - Application::StreamStopSending(stream, std::move(error)); + void ReceiveStreamStopSending(Stream* stream, + QuicError&& error = QuicError()) override { + Application::ReceiveStreamStopSending(stream, std::move(error)); } bool SendHeaders(const Stream& stream, @@ -363,8 +595,11 @@ class Http3ApplicationImpl final : public Session::Application { "Submitting %" PRIu64 " response headers for stream %" PRIu64, nva.length(), stream.id()); - return nghttp3_conn_submit_response( - *this, stream.id(), nva.data(), nva.length(), reader_ptr); + return nghttp3_conn_submit_response(*this, + stream.id(), + nva.data(), + nva.length(), + reader_ptr) == 0; } else { // Otherwise we're submitting a request... Debug(&session(), @@ -398,7 +633,7 @@ class Http3ApplicationImpl final : public Session::Application { StreamPriority priority, StreamPriorityFlags flags) override { nghttp3_pri pri; - pri.inc = (flags == StreamPriorityFlags::NON_INCREMENTAL) ? 0 : 1; + pri.inc = (flags == StreamPriorityFlags::INCREMENTAL) ? 1 : 0; switch (priority) { case StreamPriority::HIGH: pri.urgency = NGHTTP3_URGENCY_HIGH; @@ -412,33 +647,44 @@ class Http3ApplicationImpl final : public Session::Application { } if (session().is_server()) { nghttp3_conn_set_server_stream_priority(*this, stream.id(), &pri); + } else { + // The client API takes a serialized RFC 9218 priority field value + // (e.g., "u=0, i") rather than an nghttp3_pri struct. + char buf[8]; + size_t len = FormatPriority(buf, sizeof(buf), pri); + nghttp3_conn_set_client_stream_priority( + *this, stream.id(), reinterpret_cast(buf), len); } - // Client-side priority is set at request submission time via - // nghttp3_conn_submit_request and is not typically changed - // after the fact. The client API takes a serialized RFC 9218 - // field value rather than an nghttp3_pri struct. } - StreamPriority GetStreamPriority(const Stream& stream) override { + StreamPriorityResult GetStreamPriority(const Stream& stream) override { + // nghttp3_conn_get_stream_priority is only available on the server + // side, where it reflects the peer's requested priority (e.g., from + // PRIORITY_UPDATE frames). Client-side priority is tracked by the + // Stream itself and returned directly from GetPriority in streams.cc. + if (!session().is_server()) { + auto& stored = stream.stored_priority(); + return {stored.priority, stored.flags}; + } nghttp3_pri pri; if (nghttp3_conn_get_stream_priority(*this, &pri, stream.id()) == 0) { - // TODO(@jasnell): The nghttp3_pri.inc (incremental) flag is - // not yet exposed. When priority-based stream scheduling is - // implemented, GetStreamPriority should return both urgency - // and the incremental flag (making get/set symmetrical). - // The inc flag determines whether the server should interleave - // data from this stream with others of the same urgency - // (inc=1) or complete it first (inc=0). + StreamPriority level; switch (pri.urgency) { case NGHTTP3_URGENCY_HIGH: - return StreamPriority::HIGH; + level = StreamPriority::HIGH; + break; case NGHTTP3_URGENCY_LOW: - return StreamPriority::LOW; + level = StreamPriority::LOW; + break; default: - return StreamPriority::DEFAULT; + level = StreamPriority::DEFAULT; + break; } + return {level, + pri.inc ? StreamPriorityFlags::INCREMENTAL + : StreamPriorityFlags::NON_INCREMENTAL}; } - return StreamPriority::DEFAULT; + return {StreamPriority::DEFAULT, StreamPriorityFlags::NON_INCREMENTAL}; } int GetStreamData(StreamData* data) override { @@ -454,7 +700,7 @@ class Http3ApplicationImpl final : public Session::Application { } data->count = static_cast(ret); - if (data->id > 0 && data->id != control_stream_id_ && + if (data->id >= 0 && data->id != control_stream_id_ && data->id != qpack_dec_stream_id_ && data->id != qpack_enc_stream_id_) { data->stream = session().FindStream(data->id); @@ -469,13 +715,32 @@ class Http3ApplicationImpl final : public Session::Application { "HTTP/3 application committing stream %" PRIi64 " data %zu", data->id, datalen); + // datalen is the total framed bytes consumed by ngtcp2, which includes + // H3 frame overhead (HEADERS frame bytes, DATA frame type/length). + // nghttp3 tracks its own offset via add_write_offset. int err = nghttp3_conn_add_write_offset(*this, data->id, datalen); if (err != 0) { session().SetLastError(QuicError::ForApplication( nghttp3_err_infer_quic_app_error_code(err))); return false; } - if (data->stream) data->stream->Commit(datalen, data->fin); + // Raw application bytes are committed to the stream's outbound + // immediately in on_read_data_callback (so that re-entrant + // fill_outq calls see the advanced position). We only need to + // propagate the fin flag here. + if (data->stream && data->fin) { + data->stream->Commit(0, true); + } + // After body data is committed, if on_read_data_callback signaled + // EOF+NO_END_STREAM (trailers pending), emit the want-trailers + // event to JS. This runs outside the NgHttp3CallbackScope so it's + // safe to call into JS. The JS handler calls sendTrailers() which + // calls nghttp3_conn_submit_trailers, queuing the TRAILERS frame + // for the next writev_stream in the send loop. + if (pending_trailers_stream_ == data->id) { + pending_trailers_stream_ = -1; + if (data->stream) data->stream->EmitWantTrailers(); + } return true; } @@ -489,27 +754,56 @@ class Http3ApplicationImpl final : public Session::Application { return conn_.get(); } - inline bool is_control_stream(int64_t id) const { + inline bool is_control_stream(stream_id id) const { return id == control_stream_id_ || id == qpack_dec_stream_id_ || id == qpack_enc_stream_id_; } + void BuildOriginPayload() { + // Build the serialized ORIGIN frame payload from the SNI configuration. + // Each origin entry is: 2-byte BE length + origin string. + // Wildcard ('*') entries and entries with authoritative=false are skipped. + auto& sni = session().config().options.sni; + for (auto& [hostname, opts] : sni) { + if (hostname == "*" || !opts.authoritative) continue; + std::string origin = "https://"; + origin += hostname; + if (opts.port != 443) { + origin += ":"; + origin += std::to_string(opts.port); + } + // 2-byte BE length prefix + uint16_t len = static_cast(origin.size()); + origin_payload_.push_back(static_cast((len >> 8) & 0xff)); + origin_payload_.push_back(static_cast(len & 0xff)); + // Origin string bytes + origin_payload_.insert( + origin_payload_.end(), origin.begin(), origin.end()); + } + if (!origin_payload_.empty()) { + origin_vec_ = {origin_payload_.data(), origin_payload_.size()}; + } + } + Http3ConnectionPointer InitializeConnection() { nghttp3_conn* conn = nullptr; nghttp3_settings settings = options_; + if (!origin_payload_.empty()) { + settings.origin_list = &origin_vec_; + } if (session().is_server()) { CHECK_EQ(nghttp3_conn_server_new( - &conn, &kCallbacks, &settings, &allocator_, this), + &conn, &kCallbacks, &settings, allocator_, this), 0); } else { CHECK_EQ(nghttp3_conn_client_new( - &conn, &kCallbacks, &settings, &allocator_, this), + &conn, &kCallbacks, &settings, allocator_, this), 0); } return Http3ConnectionPointer(conn); } - void OnStreamClose(Stream* stream, uint64_t app_error_code) { + void OnStreamClose(Stream* stream, error_code app_error_code) { if (app_error_code != NGHTTP3_H3_NO_ERROR) { Debug(&session(), "HTTP/3 application received stream close for stream %" PRIi64 @@ -522,24 +816,23 @@ class Http3ApplicationImpl final : public Session::Application { ExtendMaxStreams(EndpointLabel::REMOTE, direction, 1); } - void OnBeginHeaders(int64_t stream_id) { - auto stream = session().FindStream(stream_id); - // If the stream does not exist or is destroyed, ignore! + void OnBeginHeaders(stream_id id) { + auto stream = FindOrCreateStream(conn_.get(), &session(), id); if (!stream) [[unlikely]] return; Debug(&session(), "HTTP/3 application beginning initial block of headers for stream " "%" PRIi64, - stream_id); + id); stream->BeginHeaders(HeadersKind::INITIAL); } - void OnReceiveHeader(int64_t stream_id, Http3Header&& header) { - auto stream = session().FindStream(stream_id); + void OnReceiveHeader(stream_id id, std::unique_ptr header) { + auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; - if (header.name() == ":status" && header.value()[0] == '1') { + if (header->name() == ":status" && header->value()[0] == '1') { Debug(&session(), "HTTP/3 application switching to hints headers for stream %" PRIi64, stream->id()); @@ -548,23 +841,23 @@ class Http3ApplicationImpl final : public Session::Application { IF_QUIC_DEBUG(env()) { Debug(&session(), "Received header \"%s: %s\"", - header.name(), - header.value()); + header->name(), + header->value()); } stream->AddHeader(std::move(header)); } - void OnEndHeaders(int64_t stream_id, int fin) { - auto stream = session().FindStream(stream_id); + void OnEndHeaders(stream_id id, int fin) { + auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; Debug(&session(), "HTTP/3 application received end of headers for stream %" PRIi64, - stream_id); + id); stream->EmitHeaders(); if (fin) { // The stream is done. There's no more data to receive! - Debug(&session(), "Headers are final for stream %" PRIi64, stream_id); + Debug(&session(), "Headers are final for stream %" PRIi64, id); Stream::ReceiveDataFlags flags{ .fin = true, .early = false, @@ -573,39 +866,39 @@ class Http3ApplicationImpl final : public Session::Application { } } - void OnBeginTrailers(int64_t stream_id) { - auto stream = session().FindStream(stream_id); + void OnBeginTrailers(stream_id id) { + auto stream = FindOrCreateStream(conn_.get(), &session(), id); if (!stream) [[unlikely]] return; Debug(&session(), "HTTP/3 application beginning block of trailers for stream %" PRIi64, - stream_id); + id); stream->BeginHeaders(HeadersKind::TRAILING); } - void OnReceiveTrailer(int64_t stream_id, Http3Header&& header) { - auto stream = session().FindStream(stream_id); + void OnReceiveTrailer(stream_id id, std::unique_ptr header) { + auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; IF_QUIC_DEBUG(env()) { Debug(&session(), "Received header \"%s: %s\"", - header.name(), - header.value()); + header->name(), + header->value()); } - stream->AddHeader(header); + stream->AddHeader(std::move(header)); } - void OnEndTrailers(int64_t stream_id, int fin) { - auto stream = session().FindStream(stream_id); + void OnEndTrailers(stream_id id, int fin) { + auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; Debug(&session(), "HTTP/3 application received end of trailers for stream %" PRIi64, - stream_id); + id); stream->EmitHeaders(); if (fin) { - Debug(&session(), "Trailers are final for stream %" PRIi64, stream_id); + Debug(&session(), "Trailers are final for stream %" PRIi64, id); Stream::ReceiveDataFlags flags{ .fin = true, .early = false, @@ -614,13 +907,13 @@ class Http3ApplicationImpl final : public Session::Application { } } - void OnEndStream(int64_t stream_id) { - auto stream = session().FindStream(stream_id); + void OnEndStream(stream_id id) { + auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; Debug(&session(), "HTTP/3 application received end of stream for stream %" PRIi64, - stream_id); + id); Stream::ReceiveDataFlags flags{ .fin = true, .early = false, @@ -628,63 +921,128 @@ class Http3ApplicationImpl final : public Session::Application { stream->ReceiveData(nullptr, 0, flags); } - void OnStopSending(int64_t stream_id, uint64_t app_error_code) { - auto stream = session().FindStream(stream_id); + void OnStopSending(stream_id id, error_code app_error_code) { + auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; Debug(&session(), "HTTP/3 application received stop sending for stream %" PRIi64, - stream_id); + id); stream->ReceiveStopSending(QuicError::ForApplication(app_error_code)); } - void OnResetStream(int64_t stream_id, uint64_t app_error_code) { - auto stream = session().FindStream(stream_id); + void OnResetStream(stream_id id, error_code app_error_code) { + auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; Debug(&session(), "HTTP/3 application received reset stream for stream %" PRIi64, - stream_id); + id); stream->ReceiveStreamReset(0, QuicError::ForApplication(app_error_code)); } - void OnShutdown(int64_t id) { - // The peer has sent a GOAWAY frame initiating a graceful shutdown. - // For a client, id is the stream ID beyond which the server will - // not process requests. For a server, id is a push ID (server - // push is not implemented). Streams/pushes with IDs >= id will - // not be processed by the peer. - // - // When id equals NGHTTP3_SHUTDOWN_NOTICE_STREAM_ID (client) or - // NGHTTP3_SHUTDOWN_NOTICE_PUSH_ID (server), this is a notice of - // intent to shut down rather than an immediate refusal. + void OnShutdown(stream_id id) { + // The peer has sent a GOAWAY frame. This callback fires inside + // NgHttp3CallbackScope, so we cannot call into JS, destroy streams, + // or enter Close(GRACEFUL) here (which could trigger FinishClose and + // deferred destroy, preventing PostReceive from running). // - // This can be called multiple times with a decreasing id as the - // peer progressively reduces the set of streams it will process. + // Store the GOAWAY stream ID — PostReceive() handles everything + // outside all callback scopes. For the shutdown notice (first phase, + // sentinel ID), we still store it so PostReceive knows to enter + // graceful close mode. For the final GOAWAY (real stream ID), we + // overwrite with the lower value. Debug(&session(), "HTTP/3 received GOAWAY (id=%" PRIi64 ")", id); - session().Close(Session::CloseMethod::GRACEFUL); + pending_goaway_id_ = id; } - void OnReceiveSettings(const nghttp3_settings* settings) { + void PostReceive() override { + if (pending_goaway_id_ < 0) return; + stream_id goaway_id = pending_goaway_id_; + pending_goaway_id_ = -1; + + bool is_notice = + static_cast(goaway_id) >= NGHTTP3_SHUTDOWN_NOTICE_STREAM_ID; + + // For the shutdown notice, replace the sentinel stream ID with -1 + // so JS sees a clean marker instead of a huge implementation detail. + stream_id emit_id = is_notice ? -1 : goaway_id; + + if (!is_notice) { + // Final GOAWAY: destroy client-initiated bidi streams with + // IDs > goaway_id. These were not processed by the peer and + // can be retried. Copy the map because Destroy modifies it. + auto streams = session().streams(); + for (auto& [id, stream] : streams) { + if (session().is_destroyed()) return; + if (ngtcp2_is_bidi_stream(id) && id > goaway_id) { + stream->Destroy( + QuicError::ForApplication(NGHTTP3_H3_REQUEST_REJECTED)); + } + } + if (session().is_destroyed()) return; + } + + // Notify JS for both notice and final GOAWAY. The notice uses + // -1 to signal "server is shutting down, stop new requests" without + // implying any specific stream boundary. The final GOAWAY (if it + // arrives separately) provides the exact stream ID for retry decisions. + // + // We do NOT call Close(GRACEFUL) here. The JS ongoaway handler sets + // isPendingClose (preventing new streams). The session closes naturally + // when the peer sends CONNECTION_CLOSE after all streams finish. + // Calling Close(GRACEFUL) would send a GOAWAY back and trigger + // BeginShutdown, which can interfere with in-progress streams. + session().EmitGoaway(emit_id); + } + + void OnReceiveSettings(const nghttp3_proto_settings* settings) { options_.enable_connect_protocol = settings->enable_connect_protocol; options_.enable_datagrams = settings->h3_datagram; options_.max_field_section_size = settings->max_field_section_size; options_.qpack_blocked_streams = settings->qpack_blocked_streams; - options_.qpack_encoder_max_dtable_capacity = - settings->qpack_encoder_max_dtable_capacity; options_.qpack_max_dtable_capacity = settings->qpack_max_dtable_capacity; + + // Per RFC 9297 §3, an H3 endpoint MUST NOT send HTTP Datagrams + // unless the peer indicated support via SETTINGS_H3_DATAGRAM=1. + // If the peer disabled it, set the session's max datagram size to 0 + // which blocks sends at the existing JS/C++ check. + if (!settings->h3_datagram) { + session().set_max_datagram_size(0); + } + Debug(&session(), "HTTP/3 application received updated settings: %s", options_); + // The settings are part of the application + session().EmitApplication(); } bool started_ = false; - nghttp3_mem allocator_; + nghttp3_mem* allocator_; Options options_; Http3ConnectionPointer conn_; - int64_t control_stream_id_ = -1; - int64_t qpack_dec_stream_id_ = -1; - int64_t qpack_enc_stream_id_ = -1; + stream_id control_stream_id_ = -1; + stream_id qpack_dec_stream_id_ = -1; + stream_id qpack_enc_stream_id_ = -1; + + // Set by on_read_data_callback when EOF+NO_END_STREAM (trailers pending). + // Consumed by StreamCommit to trigger EmitWantTrailers outside the + // nghttp3 callback scope. + stream_id pending_trailers_stream_ = -1; + + // Set by OnShutdown when the peer sends a final GOAWAY. Consumed by + // PostReceive() outside all callback scopes to destroy rejected + // streams and notify JS. + stream_id pending_goaway_id_ = -1; + + // ORIGIN frame support (RFC 9412). + // origin_payload_ holds the serialized ORIGIN frame payload for sending. + // origin_vec_ points into origin_payload_ for nghttp3_settings.origin_list. + // received_origins_ accumulates origins from received ORIGIN frames. + std::vector origin_payload_; + nghttp3_vec origin_vec_{nullptr, 0}; + std::vector received_origins_; // ========================================================================== // Static callbacks @@ -698,11 +1056,11 @@ class Http3ApplicationImpl final : public Session::Application { static BaseObjectWeakPtr FindOrCreateStream(nghttp3_conn* conn, Session* session, - int64_t stream_id) { - if (auto stream = session->FindStream(stream_id)) { + stream_id id) { + if (auto stream = session->FindStream(id)) { return stream; } - if (auto stream = session->CreateStream(stream_id)) { + if (auto stream = session->CreateStream(id)) { return stream; } return {}; @@ -712,10 +1070,10 @@ class Http3ApplicationImpl final : public Session::Application { auto ptr = From(conn, conn_user_data); \ CHECK_NOT_NULL(ptr); \ auto& name = *ptr; \ - NgHttp3CallbackScope scope(name.env()); + NgHttp3CallbackScope scope(&name.session()); static nghttp3_ssize on_read_data_callback(nghttp3_conn* conn, - int64_t stream_id, + stream_id id, nghttp3_vec* vec, size_t veccnt, uint32_t* pflags, @@ -724,13 +1082,17 @@ class Http3ApplicationImpl final : public Session::Application { auto ptr = From(conn, conn_user_data); CHECK_NOT_NULL(ptr); auto& app = *ptr; - NgHttp3CallbackScope scope(app.env()); + NgHttp3CallbackScope scope(&app.session()); - auto stream = app.session().FindStream(stream_id); + auto stream = app.session().FindStream(id); if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE; if (stream->is_eos()) { *pflags |= NGHTTP3_DATA_FLAG_EOF; + if (stream->wants_trailers()) { + *pflags |= NGHTTP3_DATA_FLAG_NO_END_STREAM; + app.pending_trailers_stream_ = id; + } return 0; } @@ -746,12 +1108,35 @@ class Http3ApplicationImpl final : public Session::Application { return; case bob::Status::STATUS_EOS: *pflags |= NGHTTP3_DATA_FLAG_EOF; + if (stream->wants_trailers()) { + *pflags |= NGHTTP3_DATA_FLAG_NO_END_STREAM; + app.pending_trailers_stream_ = id; + } break; } count = std::min(count, max_count); + // nghttp3 requires read_data to return either data (count > 0), + // EOF, or WOULDBLOCK. A STATUS_CONTINUE with 0 vecs means the + // outbound has no uncommitted data right now (e.g., all data was + // already committed on a previous call, or the DataQueue is empty + // but not yet capped). Map this to WOULDBLOCK so nghttp3 sets + // READ_DATA_BLOCKED and waits for ResumeStream. + if (count == 0 && !((*pflags) & NGHTTP3_DATA_FLAG_EOF)) { + result = NGHTTP3_ERR_WOULDBLOCK; + return; + } + size_t raw_bytes = 0; for (size_t n = 0; n < count; n++) { vec[n].base = data[n].base; vec[n].len = data[n].len; + raw_bytes += data[n].len; + } + // Commit the raw application bytes immediately so that the + // next Pull (if fill_outq re-enters read_data) sees the + // advanced position. Commit only moves the offset — the + // underlying buffers stay valid until Acknowledge. + if (raw_bytes > 0) { + stream->Commit(raw_bytes); } result = static_cast(count); }; @@ -767,30 +1152,40 @@ class Http3ApplicationImpl final : public Session::Application { } static int on_acked_stream_data(nghttp3_conn* conn, - int64_t stream_id, + stream_id id, uint64_t datalen, void* conn_user_data, void* stream_user_data) { - NGHTTP3_CALLBACK_SCOPE(app); - return app.AcknowledgeStreamData(stream_id, static_cast(datalen)) - ? NGTCP2_SUCCESS - : NGHTTP3_ERR_CALLBACK_FAILURE; + // This callback is invoked by nghttp3_conn_add_ack_offset() (called + // from Http3ApplicationImpl::AcknowledgeStreamData). We must NOT call + // AcknowledgeStreamData here — that would re-enter nghttp3 via + // nghttp3_conn_add_ack_offset, triggering the NgHttp3CallbackScope + // re-entrancy assertion. Instead, directly notify the stream that data + // was acknowledged, which is what the base Application implementation + // does. + auto ptr = From(conn, conn_user_data); + CHECK_NOT_NULL(ptr); + auto& app = *ptr; + if (auto stream = app.session().FindStream(id)) { + stream->Acknowledge(static_cast(datalen)); + } + return NGTCP2_SUCCESS; } static int on_stream_close(nghttp3_conn* conn, - int64_t stream_id, - uint64_t app_error_code, + stream_id id, + error_code app_error_code, void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); - if (auto stream = app.session().FindStream(stream_id)) { + if (auto stream = app.session().FindStream(id)) { app.OnStreamClose(stream.get(), app_error_code); } return NGTCP2_SUCCESS; } static int on_receive_data(nghttp3_conn* conn, - int64_t stream_id, + stream_id id, const uint8_t* data, size_t datalen, void* conn_user_data, @@ -799,12 +1194,11 @@ class Http3ApplicationImpl final : public Session::Application { // The on_receive_data callback will never be called for control streams, // so we know that if we get here, the data received is for a stream that // we know is for an HTTP payload. - if (app.is_control_stream(stream_id)) [[unlikely]] { + if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } auto& session = app.session(); - if (auto stream = FindOrCreateStream(conn, &session, stream_id)) - [[likely]] { + if (auto stream = FindOrCreateStream(conn, &session, id)) [[likely]] { stream->ReceiveData(data, datalen, Stream::ReceiveDataFlags{}); return NGTCP2_SUCCESS; } @@ -812,32 +1206,32 @@ class Http3ApplicationImpl final : public Session::Application { } static int on_deferred_consume(nghttp3_conn* conn, - int64_t stream_id, + stream_id id, size_t consumed, void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); auto& session = app.session(); Debug(&session, "HTTP/3 application deferred consume %zu bytes", consumed); - session.ExtendStreamOffset(stream_id, consumed); + session.ExtendStreamOffset(id, consumed); session.ExtendOffset(consumed); return NGTCP2_SUCCESS; } static int on_begin_headers(nghttp3_conn* conn, - int64_t stream_id, + stream_id id, void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); - if (app.is_control_stream(stream_id)) [[unlikely]] { + if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } - app.OnBeginHeaders(stream_id); + app.OnBeginHeaders(id); return NGTCP2_SUCCESS; } static int on_receive_header(nghttp3_conn* conn, - int64_t stream_id, + stream_id id, int32_t token, nghttp3_rcbuf* name, nghttp3_rcbuf* value, @@ -845,42 +1239,43 @@ class Http3ApplicationImpl final : public Session::Application { void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); - if (app.is_control_stream(stream_id)) [[unlikely]] { + if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } if (Http3Header::IsZeroLength(token, name, value)) return NGTCP2_SUCCESS; - app.OnReceiveHeader(stream_id, - Http3Header(app.env(), token, name, value, flags)); + app.OnReceiveHeader( + id, + std::make_unique(app.env(), token, name, value, flags)); return NGTCP2_SUCCESS; } static int on_end_headers(nghttp3_conn* conn, - int64_t stream_id, + stream_id id, int fin, void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); - if (app.is_control_stream(stream_id)) [[unlikely]] { + if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } - app.OnEndHeaders(stream_id, fin); + app.OnEndHeaders(id, fin); return NGTCP2_SUCCESS; } static int on_begin_trailers(nghttp3_conn* conn, - int64_t stream_id, + stream_id id, void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); - if (app.is_control_stream(stream_id)) [[unlikely]] { + if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } - app.OnBeginTrailers(stream_id); + app.OnBeginTrailers(id); return NGTCP2_SUCCESS; } static int on_receive_trailer(nghttp3_conn* conn, - int64_t stream_id, + stream_id id, int32_t token, nghttp3_rcbuf* name, nghttp3_rcbuf* value, @@ -888,74 +1283,77 @@ class Http3ApplicationImpl final : public Session::Application { void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); - if (app.is_control_stream(stream_id)) [[unlikely]] { + if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } if (Http3Header::IsZeroLength(token, name, value)) return NGTCP2_SUCCESS; - app.OnReceiveTrailer(stream_id, - Http3Header(app.env(), token, name, value, flags)); + app.OnReceiveTrailer( + id, + std::make_unique(app.env(), token, name, value, flags)); return NGTCP2_SUCCESS; } static int on_end_trailers(nghttp3_conn* conn, - int64_t stream_id, + stream_id id, int fin, void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); - if (app.is_control_stream(stream_id)) [[unlikely]] { + if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } - app.OnEndTrailers(stream_id, fin); + app.OnEndTrailers(id, fin); return NGTCP2_SUCCESS; } static int on_end_stream(nghttp3_conn* conn, - int64_t stream_id, + stream_id id, void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); - if (app.is_control_stream(stream_id)) [[unlikely]] { + if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } - app.OnEndStream(stream_id); + app.OnEndStream(id); return NGTCP2_SUCCESS; } static int on_stop_sending(nghttp3_conn* conn, - int64_t stream_id, - uint64_t app_error_code, + stream_id id, + error_code app_error_code, void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); - if (app.is_control_stream(stream_id)) [[unlikely]] { + if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } - app.OnStopSending(stream_id, app_error_code); + app.OnStopSending(id, app_error_code); return NGTCP2_SUCCESS; } static int on_reset_stream(nghttp3_conn* conn, - int64_t stream_id, - uint64_t app_error_code, + stream_id id, + error_code app_error_code, void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); - if (app.is_control_stream(stream_id)) [[unlikely]] { + if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } - app.OnResetStream(stream_id, app_error_code); + app.OnResetStream(id, app_error_code); return NGTCP2_SUCCESS; } - static int on_shutdown(nghttp3_conn* conn, int64_t id, void* conn_user_data) { + static int on_shutdown(nghttp3_conn* conn, + stream_id id, + void* conn_user_data) { NGHTTP3_CALLBACK_SCOPE(app); app.OnShutdown(id); return NGTCP2_SUCCESS; } static int on_receive_settings(nghttp3_conn* conn, - const nghttp3_settings* settings, + const nghttp3_proto_settings* settings, void* conn_user_data) { NGHTTP3_CALLBACK_SCOPE(app); app.OnReceiveSettings(settings); @@ -966,14 +1364,18 @@ class Http3ApplicationImpl final : public Session::Application { const uint8_t* origin, size_t originlen, void* conn_user_data) { - // ORIGIN frames (RFC 8336) are used for connection coalescing - // across multiple origins. Not yet implemented u2014 requires - // connection pooling and multi-origin reuse support. + NGHTTP3_CALLBACK_SCOPE(app); + app.received_origins_.emplace_back(reinterpret_cast(origin), + originlen); return NGTCP2_SUCCESS; } static int on_end_origin(nghttp3_conn* conn, void* conn_user_data) { - // See on_receive_origin above. + NGHTTP3_CALLBACK_SCOPE(app); + if (!app.received_origins_.empty()) { + app.session().EmitOrigins(std::move(app.received_origins_)); + app.received_origins_.clear(); + } return NGTCP2_SUCCESS; } @@ -981,27 +1383,52 @@ class Http3ApplicationImpl final : public Session::Application { CHECK(ncrypto::CSPRNG(dest, destlen)); } - static constexpr nghttp3_callbacks kCallbacks = {on_acked_stream_data, - on_stream_close, - on_receive_data, - on_deferred_consume, - on_begin_headers, - on_receive_header, - on_end_headers, - on_begin_trailers, - on_receive_trailer, - on_end_trailers, - on_stop_sending, - on_end_stream, - on_reset_stream, - on_shutdown, - on_receive_settings, - on_receive_origin, - on_end_origin, - on_rand, - nullptr}; + static constexpr nghttp3_callbacks kCallbacks = { + on_acked_stream_data, + on_stream_close, + on_receive_data, + on_deferred_consume, + on_begin_headers, + on_receive_header, + on_end_headers, + on_begin_trailers, + on_receive_trailer, + on_end_trailers, + on_stop_sending, + on_end_stream, + on_reset_stream, + on_shutdown, + nullptr, // recv_settings (deprecated) + on_receive_origin, + on_end_origin, + on_rand, + on_receive_settings}; }; +std::optional ParseHttp3TicketData(const uv_buf_t& data) { + if (data.len != kSessionTicketAppDataSize) return std::nullopt; + + const uint8_t* buf = reinterpret_cast(data.base); + + // buf[0] is the type byte (already checked by caller), buf[1] is version. + if (buf[1] != kSessionTicketAppDataVersion) return std::nullopt; + + const uint8_t* payload = buf + kSessionTicketAppDataHeaderSize; + uint32_t stored_crc = ReadBE32(buf + 2); + uLong computed_crc = crc32(0L, Z_NULL, 0); + computed_crc = crc32(computed_crc, payload, kSessionTicketAppDataPayloadSize); + if (stored_crc != static_cast(computed_crc)) return std::nullopt; + + return Http3TicketData{ + ReadBE64(payload), + ReadBE64(payload + 8), + ReadBE64(payload + 16), + ReadBE64(payload + 24), + payload[32] != 0, + payload[33] != 0, + }; +} + std::unique_ptr CreateHttp3Application( Session* session, const Session::Application_Options& options) { Debug(session, "Selecting HTTP/3 application"); diff --git a/src/quic/http3.h b/src/quic/http3.h index b49f3daf8b1621..f1a1b674d96903 100644 --- a/src/quic/http3.h +++ b/src/quic/http3.h @@ -3,6 +3,8 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include +#include +#include "application.h" #include "session.h" namespace node::quic { @@ -13,6 +15,11 @@ namespace node::quic { std::unique_ptr CreateHttp3Application( Session* session, const Session::Application_Options& options); +// Parse HTTP/3 specific session ticket app data. Called from +// Application::ParseTicketData() when the type byte is HTTP3. +// The data includes the type byte prefix. +std::optional ParseHttp3TicketData(const uv_buf_t& data); + } // namespace node::quic #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/logstream.cc b/src/quic/logstream.cc deleted file mode 100644 index 511b2a1ef46ebe..00000000000000 --- a/src/quic/logstream.cc +++ /dev/null @@ -1,140 +0,0 @@ -#if HAVE_OPENSSL && HAVE_QUIC -#include "guard.h" -#ifndef OPENSSL_NO_QUIC -#include "logstream.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include "bindingdata.h" - -namespace node { - -using v8::FunctionTemplate; -using v8::Local; -using v8::Object; - -namespace quic { - -JS_CONSTRUCTOR_IMPL(LogStream, logstream_constructor_template, { - tmpl = FunctionTemplate::New(env->isolate()); - JS_INHERIT(AsyncWrap); - JS_CLASS_FIELDS(logstream, StreamBase::kInternalFieldCount); - StreamBase::AddMethods(env, tmpl); -}) - -BaseObjectPtr LogStream::Create(Environment* env) { - JS_NEW_INSTANCE_OR_RETURN(env, obj, nullptr); - return MakeDetachedBaseObject(env, obj); -} - -LogStream::LogStream(Environment* env, Local obj) - : AsyncWrap(env, obj, PROVIDER_QUIC_LOGSTREAM), StreamBase(env) { - MakeWeak(); - AttachToObject(GetObject()); -} - -void LogStream::Emit(const uint8_t* data, size_t len, EmitOption option) { - if (fin_seen_) return; - fin_seen_ = option == EmitOption::FIN; - - size_t remaining = len; - // If the len is greater than the size of the buffer returned by - // EmitAlloc then EmitRead will be called multiple times. - while (remaining != 0) { - uv_buf_t buf = EmitAlloc(remaining); - size_t chunk_len = std::min(remaining, buf.len); - memcpy(buf.base, data, chunk_len); - remaining -= chunk_len; - data += chunk_len; - // If we are actively reading from the stream, we'll call emit - // read immediately. Otherwise we buffer the chunk and will push - // the chunks out the next time ReadStart() is called. - if (reading_) { - EmitRead(chunk_len, buf); - } else { - // The total measures the total memory used so we always - // increment but buf.len and not chunk len. - ensure_space(buf.len); - total_ += buf.len; - buffer_.push_back(Chunk{chunk_len, buf}); - } - } - - if (ended_ && reading_) { - EmitRead(UV_EOF); - } -} - -void LogStream::Emit(const std::string_view line, EmitOption option) { - Emit(reinterpret_cast(line.data()), line.length(), option); -} - -void LogStream::End() { - ended_ = true; -} - -int LogStream::ReadStart() { - if (reading_) return 0; - // Flush any chunks that have already been buffered. - for (const auto& chunk : buffer_) EmitRead(chunk.len, chunk.buf); - total_ = 0; - buffer_.clear(); - if (fin_seen_) { - // If we've already received the fin, there's nothing else to wait for. - EmitRead(UV_EOF); - return ReadStop(); - } - // Otherwise, we're going to wait for more chunks to be written. - reading_ = true; - return 0; -} - -int LogStream::ReadStop() { - reading_ = false; - return 0; -} - -// We do not use either of these. -int LogStream::DoShutdown(ShutdownWrap* req_wrap) { - UNREACHABLE(); -} -int LogStream::DoWrite(WriteWrap* w, - uv_buf_t* bufs, - size_t count, - uv_stream_t* send_handle) { - UNREACHABLE(); -} - -bool LogStream::IsAlive() { - return !ended_; -} - -bool LogStream::IsClosing() { - return ended_; -} - -AsyncWrap* LogStream::GetAsyncWrap() { - return this; -} - -void LogStream::MemoryInfo(MemoryTracker* tracker) const { - tracker->TrackFieldWithSize("buffer", total_); -} - -// The LogStream buffer enforces a maximum size of kMaxLogStreamBuffer. -void LogStream::ensure_space(size_t amt) { - while (total_ + amt > kMaxLogStreamBuffer) { - total_ -= buffer_.front().buf.len; - buffer_.pop_front(); - } -} -} // namespace quic -} // namespace node - -#endif // OPENSSL_NO_QUIC -#endif // HAVE_OPENSSL && HAVE_QUIC diff --git a/src/quic/logstream.h b/src/quic/logstream.h deleted file mode 100644 index e5cf078fa2bae8..00000000000000 --- a/src/quic/logstream.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS - -#include -#include -#include -#include -#include -#include "defs.h" - -namespace node::quic { - -// The LogStream is a utility that the QUIC impl uses to publish both QLog -// and Keylog diagnostic data (one instance for each). -class LogStream final : public AsyncWrap, public StreamBase { - public: - JS_CONSTRUCTOR(LogStream); - enum InternalFields { - kInternalFieldCount = std::max(AsyncWrap::kInternalFieldCount, - StreamBase::kInternalFieldCount), - }; - - static BaseObjectPtr Create(Environment* env); - - LogStream(Environment* env, v8::Local obj); - - enum class EmitOption : uint8_t { - NONE, - FIN, - }; - - void Emit(const uint8_t* data, - size_t len, - EmitOption option = EmitOption::NONE); - - void Emit(const std::string_view line, EmitOption option = EmitOption::NONE); - - void End(); - - int ReadStart() override; - - int ReadStop() override; - - // We do not use either of these. - int DoShutdown(ShutdownWrap* req_wrap) override; - int DoWrite(WriteWrap* w, - uv_buf_t* bufs, - size_t count, - uv_stream_t* send_handle) override; - - bool IsAlive() override; - bool IsClosing() override; - AsyncWrap* GetAsyncWrap() override; - - void MemoryInfo(MemoryTracker* tracker) const override; - SET_MEMORY_INFO_NAME(LogStream) - SET_SELF_SIZE(LogStream) - - private: - struct Chunk { - // len will be <= buf.len - size_t len; - uv_buf_t buf; - }; - size_t total_ = 0; - std::list buffer_; - bool fin_seen_ = false; - bool ended_ = false; - bool reading_ = false; - - // The value here is fairly arbitrary. Once we get everything - // fully implemented and start working with this, we might - // tune this number further. - static constexpr size_t kMaxLogStreamBuffer = 1024 * 10; - - // The LogStream buffer enforces a maximum size of kMaxLogStreamBuffer. - void ensure_space(size_t amt); -}; - -} // namespace node::quic - -#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/packet.cc b/src/quic/packet.cc index f7a3f3d35d47b7..71a817e49ed5c1 100644 --- a/src/quic/packet.cc +++ b/src/quic/packet.cc @@ -127,11 +127,15 @@ Packet::Ptr Packet::CreateImmediateConnectionClosePacket( "immediate connection close (endpoint)"); if (!packet) return packet; ngtcp2_vec vec = *packet; + // ngtcp2_crypto_write_connection_close expects dcid to be the + // client's SCID and scid to be the client's DCID (mirrored). + // PathDescriptor carries the incoming packet's CIDs as-is, so + // we swap here. ssize_t nwrite = ngtcp2_crypto_write_connection_close(vec.base, vec.len, path_descriptor.version, - path_descriptor.dcid, path_descriptor.scid, + path_descriptor.dcid, reason.code(), nullptr, 0); @@ -160,9 +164,11 @@ Packet::Ptr Packet::CreateStatelessResetPacket( if (!packet) return packet; ngtcp2_vec vec = *packet; - ssize_t nwrite = ngtcp2_pkt_write_stateless_reset( + auto nwrite = ngtcp2_pkt_write_stateless_reset2( vec.base, pktlen, token, random, kRandlen); - if (nwrite <= static_cast(kMinStatelessResetLen)) return Ptr(); + if (nwrite < static_cast(kMinStatelessResetLen)) { + return Ptr(); + } packet->Truncate(static_cast(nwrite)); return packet; @@ -203,14 +209,18 @@ Packet::Ptr Packet::CreateVersionNegotiationPacket( if (!packet) return packet; ngtcp2_vec vec = *packet; + // ngtcp2_pkt_write_version_negotiation expects dcid to be the + // client's SCID and scid to be the client's DCID (mirrored). + // PathDescriptor carries the incoming packet's CIDs as-is, so + // we swap here. ssize_t nwrite = ngtcp2_pkt_write_version_negotiation(vec.base, pktlen, 0, - path_descriptor.dcid, - path_descriptor.dcid.length(), path_descriptor.scid, path_descriptor.scid.length(), + path_descriptor.dcid, + path_descriptor.dcid.length(), sv, arraysize(sv)); if (nwrite <= 0) return Ptr(); diff --git a/src/quic/packet.h b/src/quic/packet.h index 78eb51a9d6b3fa..a94ee1264c2a6a 100644 --- a/src/quic/packet.h +++ b/src/quic/packet.h @@ -68,7 +68,18 @@ class Packet final { size_t length() const { return length_; } size_t capacity() const { return capacity_; } const SocketAddress& destination() const { return destination_; } + const PacketInfo& pkt_info() const { return pkt_info_; } + void set_pkt_info(const PacketInfo& pi) { pkt_info_ = pi; } Listener* listener() const { return listener_; } + + // Redirect the packet to a different endpoint for cross-endpoint sends + // (e.g., PATH_RESPONSE on a preferred address path). Updates the + // listener (for pending_callbacks accounting) and the destination + // (for uv_udp_send targeting). The packet data is unchanged. + void Redirect(Listener* listener, const SocketAddress& destination) { + listener_ = listener; + destination_ = destination; + } uv_udp_send_t* req() { return &req_; } operator uv_buf_t() const { @@ -139,6 +150,7 @@ class Packet final { Listener* listener_; // Touched at send time. + PacketInfo pkt_info_; SocketAddress destination_; // Only touched by libuv during uv_udp_send and in the send callback. diff --git a/src/quic/preferredaddress.cc b/src/quic/preferredaddress.cc index 3d584be2d74811..7ce27798a63423 100644 --- a/src/quic/preferredaddress.cc +++ b/src/quic/preferredaddress.cc @@ -17,6 +17,7 @@ namespace node { using v8::Just; using v8::Local; using v8::Maybe; +using v8::Object; using v8::Value; namespace quic { @@ -131,7 +132,7 @@ Maybe PreferredAddress::tryGetPolicy( : Just(FromV8Value(value)); } -void PreferredAddress::Initialize(Environment* env, Local target) { +void PreferredAddress::Initialize(Environment* env, Local target) { // The QUIC_* constants are expected to be exported out to be used on // the JavaScript side of the API. static constexpr auto PREFERRED_ADDRESS_USE = @@ -139,7 +140,7 @@ void PreferredAddress::Initialize(Environment* env, Local target) { static constexpr auto PREFERRED_ADDRESS_IGNORE = static_cast(Policy::IGNORE_PREFERRED); static constexpr auto DEFAULT_PREFERRED_ADDRESS_POLICY = - static_cast(Policy::USE_PREFERRED); + static_cast(Policy::IGNORE_PREFERRED); NODE_DEFINE_CONSTANT(target, PREFERRED_ADDRESS_IGNORE); NODE_DEFINE_CONSTANT(target, PREFERRED_ADDRESS_USE); diff --git a/src/quic/quic.cc b/src/quic/quic.cc index d39215a06827d3..36e781f1d700b7 100644 --- a/src/quic/quic.cc +++ b/src/quic/quic.cc @@ -13,7 +13,7 @@ #include "node_external_reference.h" #include -#include + namespace node { using v8::Context; @@ -25,7 +25,11 @@ using v8::Value; namespace quic { namespace { -std::once_flag crypto_init_flag; +uv_once_t crypto_init_flag = UV_ONCE_INIT; + +void InitNgtcp2CryptoOnce() { + ngtcp2_crypto_ossl_init(); +} } // namespace void CreatePerIsolateProperties(IsolateData* isolate_data, @@ -39,8 +43,8 @@ void CreatePerContextProperties(Local target, Local unused, Local context, void* priv) { + uv_once(&crypto_init_flag, InitNgtcp2CryptoOnce); Realm* realm = Realm::GetCurrent(context); - std::call_once(crypto_init_flag, ngtcp2_crypto_ossl_init); BindingData::InitPerContext(realm, target); Endpoint::InitPerContext(realm, target); Session::InitPerContext(realm, target); diff --git a/src/quic/session.cc b/src/quic/session.cc index 09a509a1044ad2..8380e477c01e80 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -24,11 +24,11 @@ #include "defs.h" #include "endpoint.h" #include "http3.h" -#include "logstream.h" #include "ncrypto.h" #include "packet.h" #include "preferredaddress.h" #include "session.h" +#include "session_manager.h" #include "sessionticket.h" #include "streams.h" #include "tlscontext.h" @@ -40,6 +40,9 @@ using v8::Array; using v8::ArrayBufferView; using v8::BigInt; using v8::Boolean; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::Global; using v8::HandleScope; using v8::Int32; using v8::Integer; @@ -49,19 +52,81 @@ using v8::LocalVector; using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; +using v8::Number; using v8::Object; using v8::ObjectTemplate; using v8::String; +using v8::Uint32; using v8::Undefined; using v8::Value; namespace quic { +// Listener flags are packed into a single uint32_t bitfield to reduce +// the size of the shared state buffer. Each bit indicates whether a +// corresponding JS callback is registered. +enum class SessionListenerFlags : uint32_t { + PATH_VALIDATION = 1 << 0, + DATAGRAM = 1 << 1, + DATAGRAM_STATUS = 1 << 2, + SESSION_TICKET = 1 << 3, + NEW_TOKEN = 1 << 4, + ORIGIN = 1 << 5, + APPLICATION = 1 << 6 +}; + +inline SessionListenerFlags operator|(SessionListenerFlags a, + SessionListenerFlags b) { + return static_cast(static_cast(a) | + static_cast(b)); +} + +inline SessionListenerFlags operator&(SessionListenerFlags a, + SessionListenerFlags b) { + return static_cast(static_cast(a) & + static_cast(b)); +} + +inline SessionListenerFlags operator&(uint32_t a, SessionListenerFlags b) { + return static_cast(a & static_cast(b)); +} + +inline bool operator!(SessionListenerFlags a) { + return static_cast(a) == 0; +} + +inline bool HasListenerFlag(uint32_t flags, SessionListenerFlags flag) { + return !!(flags & flag); +} + +// Compute the maximum datagram payload that fits within the peer's +// max_datagram_frame_size transport parameter. The DATAGRAM frame has +// overhead of 1 byte (frame type) + variable-length integer encoding +// of the payload length. This mirrors the check in ngtcp2's +// ngtcp2_pkt_datagram_framelen (1 + varint_len(payload) + payload). +uint64_t MaxDatagramPayload(uint64_t max_frame_size) { + // A DATAGRAM frame needs at least 1 (type) + 1 (varint) + 0 (data). + if (max_frame_size < 2) return 0; + // QUIC variable-length integer encoding sizes (RFC 9000 Section 16). + auto varint_len = [](uint64_t n) -> uint64_t { + if (n < 64) return 1; + if (n < 16384) return 2; + if (n < 1073741824) return 4; + return 8; + }; + // Start with the optimistic payload assuming minimum varint (1 byte). + uint64_t payload = max_frame_size - 2; + // If the payload requires a larger varint, the overhead increases. + // Recompute with the actual varint length of the candidate payload. + uint64_t overhead = 1 + varint_len(payload); + if (overhead + payload > max_frame_size) { + payload = max_frame_size - 1 - varint_len(max_frame_size - 3); + } + return payload; +} + #define SESSION_STATE(V) \ - V(PATH_VALIDATION, path_validation, uint8_t) \ - V(VERSION_NEGOTIATION, version_negotiation, uint8_t) \ - V(DATAGRAM, datagram, uint8_t) \ - V(SESSION_TICKET, session_ticket, uint8_t) \ + V(LISTENER_FLAGS, listener_flags, uint32_t) \ V(CLOSING, closing, uint8_t) \ V(GRACEFUL_CLOSE, graceful_close, uint8_t) \ V(SILENT_CLOSE, silent_close, uint8_t) \ @@ -70,17 +135,22 @@ namespace quic { V(HANDSHAKE_CONFIRMED, handshake_confirmed, uint8_t) \ V(STREAM_OPEN_ALLOWED, stream_open_allowed, uint8_t) \ V(PRIORITY_SUPPORTED, priority_supported, uint8_t) \ + V(HEADERS_SUPPORTED, headers_supported, uint8_t) \ V(WRAPPED, wrapped, uint8_t) \ V(APPLICATION_TYPE, application_type, uint8_t) \ - V(LAST_DATAGRAM_ID, last_datagram_id, datagram_id) + V(NO_ERROR_CODE, no_error_code, error_code) \ + V(INTERNAL_ERROR_CODE, internal_error_code, error_code) \ + V(MAX_DATAGRAM_SIZE, max_datagram_size, uint16_t) \ + V(LAST_DATAGRAM_ID, last_datagram_id, datagram_id) \ + V(MAX_PENDING_DATAGRAMS, max_pending_datagrams, uint16_t) #define SESSION_STATS(V) \ V(CREATED_AT, created_at) \ + V(DESTROYED_AT, destroyed_at) \ V(CLOSING_AT, closing_at) \ V(HANDSHAKE_COMPLETED_AT, handshake_completed_at) \ V(HANDSHAKE_CONFIRMED_AT, handshake_confirmed_at) \ V(BYTES_RECEIVED, bytes_received) \ - V(BYTES_SENT, bytes_sent) \ V(BIDI_IN_STREAM_COUNT, bidi_in_stream_count) \ V(BIDI_OUT_STREAM_COUNT, bidi_out_stream_count) \ V(UNI_IN_STREAM_COUNT, uni_in_stream_count) \ @@ -94,22 +164,38 @@ namespace quic { V(RTTVAR, rttvar) \ V(SMOOTHED_RTT, smoothed_rtt) \ V(SSTHRESH, ssthresh) \ + V(PKT_SENT, pkt_sent) \ + V(BYTES_SENT, bytes_sent) \ + V(PKT_RECV, pkt_recv) \ + V(BYTES_RECV, bytes_recv) \ + V(PKT_LOST, pkt_lost) \ + V(BYTES_LOST, bytes_lost) \ + V(PING_RECV, ping_recv) \ + V(PKT_DISCARDED, pkt_discarded) \ V(DATAGRAMS_RECEIVED, datagrams_received) \ V(DATAGRAMS_SENT, datagrams_sent) \ V(DATAGRAMS_ACKNOWLEDGED, datagrams_acknowledged) \ - V(DATAGRAMS_LOST, datagrams_lost) + V(DATAGRAMS_LOST, datagrams_lost) \ + V(STREAMS_IDLE_TIMED_OUT, streams_idle_timed_out) + +#define NO_SIDE_EFFECT true +#define SIDE_EFFECT false #define SESSION_JS_METHODS(V) \ - V(Destroy, destroy, false) \ - V(GetRemoteAddress, getRemoteAddress, true) \ - V(GetCertificate, getCertificate, true) \ - V(GetEphemeralKeyInfo, getEphemeralKey, true) \ - V(GetPeerCertificate, getPeerCertificate, true) \ - V(GracefulClose, gracefulClose, false) \ - V(SilentClose, silentClose, false) \ - V(UpdateKey, updateKey, false) \ - V(OpenStream, openStream, false) \ - V(SendDatagram, sendDatagram, false) + V(Destroy, destroy, SIDE_EFFECT) \ + V(GetRemoteAddress, getRemoteAddress, NO_SIDE_EFFECT) \ + V(GetLocalAddress, getLocalAddress, NO_SIDE_EFFECT) \ + V(GetCertificate, getCertificate, NO_SIDE_EFFECT) \ + V(GetEphemeralKeyInfo, getEphemeralKey, NO_SIDE_EFFECT) \ + V(GetPeerCertificate, getPeerCertificate, NO_SIDE_EFFECT) \ + V(GracefulClose, gracefulClose, SIDE_EFFECT) \ + V(SilentClose, silentClose, SIDE_EFFECT) \ + V(UpdateKey, updateKey, SIDE_EFFECT) \ + V(OpenStream, openStream, SIDE_EFFECT) \ + V(SendDatagram, sendDatagram, SIDE_EFFECT) \ + V(LocalTransportParams, localTransportParams, NO_SIDE_EFFECT) \ + V(RemoteTransportParams, remoteTransportParams, NO_SIDE_EFFECT) \ + V(ApplicationOptions, applicationOptions, NO_SIDE_EFFECT) struct Session::State final { #define V(_, name, type) type name; @@ -119,6 +205,46 @@ struct Session::State final { STAT_STRUCT(Session, SESSION) +using SessionStateArena = AliasedStructArena; +using SessionStatsArena = AliasedStructArena; + +// Session uses arena-allocated stats, not AliasedStruct, so override the +// STAT_* macros to use impl_->stats() instead of stats_.Data(). +#undef STAT_INCREMENT +#undef STAT_INCREMENT_N +#undef STAT_RECORD_TIMESTAMP +#undef STAT_SET +#undef STAT_GET +#define STAT_INCREMENT(Type, name) \ + IncrementStat(impl_->stats()); +#define STAT_INCREMENT_N(Type, name, amt) \ + IncrementStat(impl_->stats(), amt); +#define STAT_RECORD_TIMESTAMP(Type, name) \ + RecordTimestampStat(impl_->stats()); +#define STAT_SET(Type, name, val) \ + SetStat(impl_->stats(), val) +#define STAT_GET(Type, name) GetStat(impl_->stats()) + +namespace { +SessionStateArena& GetSessionStateArena(BindingData& binding) { + if (!binding.session_state_arena_) { + auto* arena = new SessionStateArena(); + binding.session_state_arena_ = BindingData::ArenaPtr( + arena, +[](void* p) { delete static_cast(p); }); + } + return *static_cast(binding.session_state_arena_.get()); +} + +SessionStatsArena& GetSessionStatsArena(BindingData& binding) { + if (!binding.session_stats_arena_) { + auto* arena = new SessionStatsArena(); + binding.session_stats_arena_ = BindingData::ArenaPtr( + arena, +[](void* p) { delete static_cast(p); }); + } + return *static_cast(binding.session_stats_arena_.get()); +} +} // namespace + // ============================================================================ class Http3Application; @@ -191,11 +317,12 @@ void on_qlog_write(void* user_data, uint32_t flags, const void* data, size_t len) { - static_cast(user_data)->HandleQlog(flags, data, len); + static_cast(user_data)->EmitQlog( + flags, std::string_view(static_cast(data), len)); } // Forwards detailed(verbose) debugging information from ngtcp2. Enabled using -// the NODE_DEBUG_NATIVE=NGTCP2_DEBUG category. +// the NODE_DEBUG_NATIVE=NGTCP2 category. void ngtcp2_debug_log(void* user_data, const char* fmt, ...) { va_list ap; va_start(ap, fmt); @@ -203,7 +330,9 @@ void ngtcp2_debug_log(void* user_data, const char* fmt, ...) { format[strlen(fmt)] = '\n'; // Debug() does not work with the va_list here. So we use vfprintf // directly instead. Ngtcp2DebugLog is only enabled when the debug - // category is enabled. + // category is enabled. The thread ID prefix helps distinguish output + // from concurrent sessions across worker threads. + fprintf(stderr, "ngtcp2 "); vfprintf(stderr, format.c_str(), ap); va_end(ap); } @@ -296,6 +425,31 @@ bool SetOption(Environment* env, return true; } +template +bool SetOption(Environment* env, + Opt* options, + const Local& object, + const Local& name) { + Local value; + if (!object->Get(env->context(), name).ToLocal(&value)) return false; + if (!value->IsUndefined()) { + if (!value->IsUint32()) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be an uint8", *nameStr); + return false; + } + uint32_t val = value.As()->Value(); + if (val > 255) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be <= 255", *nameStr); + return false; + } + options->*member = static_cast(val); + } + return true; +} } // namespace // ============================================================================ @@ -319,6 +473,20 @@ Session::Config::Config(Environment* env, ngtcp2_settings_default(&settings); settings.initial_ts = uv_hrtime(); + // Advertise all versions ngtcp2 supports for compatible version + // negotiation (RFC 9368). The preferred list orders the newest + // version first so that negotiation upgrades when possible. The + // initial packet version (options.version) defaults to V1 for + // maximum compatibility with peers that don't support version + // negotiation. + static const uint32_t kSupportedVersions[] = {NGTCP2_PROTO_VER_V2, + NGTCP2_PROTO_VER_V1}; + + settings.preferred_versions = kSupportedVersions; + settings.preferred_versionslen = std::size(kSupportedVersions); + settings.available_versions = kSupportedVersions; + settings.available_versionslen = std::size(kSupportedVersions); + // TODO(@jasnell): Path MTU Discovery is disabled because libuv does not // currently expose the IP_DONTFRAG / IP_MTU_DISCOVER socket options // needed for PMTUD probes to work correctly. Revisit when libuv adds @@ -340,11 +508,22 @@ Session::Config::Config(Environment* env, settings.qlog_write = on_qlog_write; } - if (env->enabled_debug_list()->enabled(DebugCategory::NGTCP2_DEBUG)) { + if (env->enabled_debug_list()->enabled(DebugCategory::NGTCP2)) { settings.log_printf = ngtcp2_debug_log; } - settings.handshake_timeout = options.handshake_timeout; + // The handshake_timeout option is in milliseconds; ngtcp2 expects + // nanoseconds (ngtcp2_duration). UINT64_MAX means no timeout. + settings.handshake_timeout = + options.handshake_timeout == UINT64_MAX + ? UINT64_MAX + : options.handshake_timeout * NGTCP2_MILLISECONDS; + + // The initial_rtt option is in milliseconds; ngtcp2 expects nanoseconds. + // A value of 0 leaves the ngtcp2 default (333ms) unchanged. + if (options.initial_rtt > 0) + settings.initial_rtt = options.initial_rtt * NGTCP2_MILLISECONDS; + settings.max_stream_window = options.max_stream_window; settings.max_window = options.max_window; settings.ack_thresh = options.unacknowledged_packet_threshold; @@ -436,14 +615,39 @@ Maybe Session::Options::From(Environment* env, if (!SET(version) || !SET(min_version) || !SET(preferred_address_strategy) || !SET(transport_params) || !SET(tls_options) || !SET(qlog) || - !SET(handshake_timeout) || !SET(max_stream_window) || !SET(max_window) || + !SET(handshake_timeout) || !SET(initial_rtt) || + !SET(keep_alive_timeout) || !SET(max_stream_window) || !SET(max_window) || !SET(max_payload_size) || !SET(unacknowledged_packet_threshold) || - !SET(cc_algorithm)) { + !SET(cc_algorithm) || !SET(draining_period_multiplier) || + !SET(max_datagram_send_attempts) || !SET(stream_idle_timeout)) { return Nothing(); } #undef SET + // RFC 9000 Section 10.2 requires the draining period to be at least 3x PTO. + static const uint8_t kMinDrainingPeriodMultiplier = 3; + options.draining_period_multiplier = std::max( + options.draining_period_multiplier, kMinDrainingPeriodMultiplier); + + // At least 1 send attempt is required. + options.max_datagram_send_attempts = + std::max(options.max_datagram_send_attempts, static_cast(1)); + + // Parse the datagram drop policy from a string option. + { + Local policy_val; + if (params->Get(env->context(), state.datagram_drop_policy_string()) + .ToLocal(&policy_val) && + !policy_val->IsUndefined()) { + Utf8Value policy_str(env->isolate(), policy_val); + if (strcmp(*policy_str, "drop-newest") == 0) { + options.datagram_drop_policy = DatagramDropPolicy::DROP_NEWEST; + } + // Default is DROP_OLDEST, no need to check for "drop-oldest". + } + } + // Parse the application-specific options (HTTP/3 qpack settings, etc.). // These are used if the negotiated ALPN selects Http3ApplicationImpl. { @@ -534,6 +738,12 @@ std::string Session::Options::ToString() const { res += prefix + "handshake timeout: " + std::to_string(handshake_timeout) + " nanoseconds"; } + if (initial_rtt > 0) { + res += prefix + "initial rtt: " + std::to_string(initial_rtt) + + " milliseconds"; + } else { + res += prefix + "initial rtt: "; + } res += prefix + "max stream window: " + std::to_string(max_stream_window); res += prefix + "max window: " + std::to_string(max_window); res += prefix + "max payload size: " + std::to_string(max_payload_size); @@ -552,16 +762,19 @@ std::string Session::Options::ToString() const { // ngtcp2 static callback functions // Utility used only within Session::Impl to reduce boilerplate +// Resolves the Session* from ngtcp2 callback arguments. The +// NgTcp2CallbackScope is NOT created here — it is placed at the +// ngtcp2 entry points (Receive, OnTimeout) so that the deferred +// destroy only fires after all callbacks for that call have completed. #define NGTCP2_CALLBACK_SCOPE(name) \ auto name = Impl::From(conn, user_data); \ - if (name == nullptr) return NGTCP2_ERR_CALLBACK_FAILURE; \ - NgTcp2CallbackScope scope(name->env()); + if (name == nullptr) return NGTCP2_ERR_CALLBACK_FAILURE; // Session::Impl maintains most of the internal state of an active Session. struct Session::Impl final : public MemoryRetainer { Session* session_; - AliasedStruct stats_; - AliasedStruct state_; + ArenaSlotBase stats_slot_; + ArenaSlotBase state_slot_; BaseObjectWeakPtr endpoint_; Config config_; SocketAddress local_address_; @@ -571,59 +784,48 @@ struct Session::Impl final : public MemoryRetainer { TimerWrapHandle timer_; size_t send_scope_depth_ = 0; QuicError last_error_; + + // Datagrams queued for sending. Serialized into packets by + // SendPendingData alongside stream data. + std::deque pending_datagrams_; PendingStream::PendingStreamQueue pending_bidi_stream_queue_; PendingStream::PendingStreamQueue pending_uni_stream_queue_; + // Session ticket app data parsed before ALPN negotiation. + // Validated and applied in SetApplication() after ALPN selects + // the application type. + std::optional pending_ticket_data_; + + // When true, the handshake is deferred until the first stream or + // datagram is sent. This is set for client sessions with a session + // ticket, enabling 0-RTT: the first send triggers the handshake + // and the stream/datagram data is included in the 0-RTT flight. + bool handshake_deferred_ = false; + + Stats* stats() { return static_cast(stats_slot_.ptr); } + const Stats* stats() const { + return static_cast(stats_slot_.ptr); + } + State* state() { return static_cast(state_slot_.ptr); } + const State* state() const { + return static_cast(state_slot_.ptr); + } + Impl(Session* session, Endpoint* endpoint, const Config& config) : session_(session), - stats_(env()->isolate()), - state_(env()->isolate()), endpoint_(endpoint), config_(config), local_address_(config.local_address), remote_address_(config.remote_address), timer_(session_->env(), [this] { session_->OnTimeout(); }) { + auto& binding = BindingData::Get(env()); + stats_slot_ = GetSessionStatsArena(binding).Allocate(env()->isolate()); + state_slot_ = GetSessionStateArena(binding).Allocate(env()->isolate()); timer_.Unref(); } DISALLOW_COPY_AND_MOVE(Impl) - inline bool is_closing() const { return state_->closing; } - - /** - * @returns {boolean} Returns true if the Session can be destroyed - * immediately. - */ - bool Close() { - if (state_->closing) return true; - state_->closing = 1; - STAT_RECORD_TIMESTAMP(Stats, closing_at); - - // Iterate through all of the known streams and close them. The streams - // will remove themselves from the Session as soon as they are closed. - // Note: we create a copy because the streams will remove themselves - // while they are cleaning up which will invalidate the iterator. - StreamsMap streams = streams_; - for (auto& stream : streams) stream.second->Destroy(last_error_); - DCHECK(streams.empty()); - - // Clear the pending streams. - while (!pending_bidi_stream_queue_.IsEmpty()) { - pending_bidi_stream_queue_.PopFront()->reject(last_error_); - } - while (!pending_uni_stream_queue_.IsEmpty()) { - pending_uni_stream_queue_.PopFront()->reject(last_error_); - } - - // If we are able to send packets, we should try sending a connection - // close packet to the remote peer. - if (!state_->silent_close) { - session_->SendConnectionClose(); - } - - timer_.Close(); - - return !state_->wrapped; - } + inline bool is_closing() const { return state()->closing; } ~Impl() { // Ensure that Close() was called before dropping @@ -640,9 +842,9 @@ struct Session::Impl final : public MemoryRetainer { ngtcp2_conn_get_scid(*session_, nullptr)); ngtcp2_conn_get_scid(*session_, cids.out()); - MaybeStackBuffer tokens( - ngtcp2_conn_get_active_dcid(*session_, nullptr)); - ngtcp2_conn_get_active_dcid(*session_, tokens.out()); + MaybeStackBuffer tokens( + ngtcp2_conn_get_active_dcid2(*session_, nullptr)); + ngtcp2_conn_get_active_dcid2(*session_, tokens.out()); endpoint->DisassociateCID(config_.dcid); endpoint->DisassociateCID(config_.preferred_address_cid); @@ -654,11 +856,15 @@ struct Session::Impl final : public MemoryRetainer { for (size_t n = 0; n < tokens.length(); n++) { if (tokens[n].token_present) { endpoint->DisassociateStatelessResetToken( - StatelessResetToken(tokens[n].token)); + StatelessResetToken(&tokens[n].token)); } } endpoint->RemoveSession(config_.scid, remote_address_); + + auto& binding = BindingData::Get(env()); + if (stats_slot_) GetSessionStatsArena(binding).ReleaseSlot(stats_slot_); + if (state_slot_) GetSessionStateArena(binding).ReleaseSlot(state_slot_); } void MemoryInfo(MemoryTracker* tracker) const override { @@ -692,6 +898,86 @@ struct Session::Impl final : public MemoryRetainer { // TODO(@jasnell): Fast API alternatives for each of these + // Parse optional close error code options: { code, type, reason } + // Returns true on success (including when no options were provided). + // Returns false on validation error (exception thrown). + // Sets *did_set to true if an error code was actually applied. + static bool MaybeSetCloseError(const FunctionCallbackInfo& args, + int options_index, + Session* session, + bool* did_set = nullptr) { + if (did_set) *did_set = false; + auto env = Environment::GetCurrent(args); + if (args.Length() <= options_index || args[options_index]->IsUndefined()) { + return true; + } + if (!args[options_index]->IsObject()) { + THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); + return false; + } + auto options = args[options_index].As(); + auto& state = BindingData::Get(env); + auto context = env->context(); + + // code: bigint (optional) + Local code_val; + if (!options->Get(context, state.code_string()).ToLocal(&code_val)) { + return false; + } + if (code_val->IsUndefined()) return true; + + uint64_t code; + if (code_val->IsBigInt()) { + bool lossless; + code = code_val.As()->Uint64Value(&lossless); + if (!lossless) { + THROW_ERR_INVALID_ARG_VALUE(env, "options.code is too large"); + return false; + } + } else if (code_val->IsNumber()) { + code = static_cast(code_val.As()->Value()); + } else { + THROW_ERR_INVALID_ARG_TYPE(env, + "options.code must be a bigint or number"); + return false; + } + + // type: string (optional, default 'transport') + Local type_val; + if (!options->Get(context, state.type_string()).ToLocal(&type_val)) { + return false; + } + bool is_application = false; + if (!type_val->IsUndefined()) { + if (type_val->StrictEquals(state.application_string())) { + is_application = true; + } else if (!type_val->StrictEquals(state.transport_string())) { + THROW_ERR_INVALID_ARG_VALUE( + env, "options.type must be 'transport' or 'application'"); + return false; + } + } + + // reason: string (optional) + std::string reason; + Local reason_val; + if (!options->Get(context, state.reason_string()).ToLocal(&reason_val)) { + return false; + } + if (!reason_val->IsUndefined()) { + Utf8Value reason_str(env->isolate(), reason_val); + reason = std::string(*reason_str, reason_str.length()); + } + + if (is_application) { + session->SetLastError(QuicError::ForApplication(code, std::move(reason))); + } else { + session->SetLastError(QuicError::ForTransport(code, std::move(reason))); + } + if (did_set) *did_set = true; + return true; + } + JS_METHOD(Destroy) { auto env = Environment::GetCurrent(args); Session* session; @@ -703,6 +989,15 @@ struct Session::Impl final : public MemoryRetainer { // as we strictly enforce it here. return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } + // args[0] is the optional close error options object. + bool has_close_options = false; + if (!MaybeSetCloseError(args, 0, session, &has_close_options)) return; + // If an error code was provided by the caller, send CONNECTION_CLOSE + // with that code before destroying. SendConnectionClose writes the + // packet and hands it to the endpoint — it doesn't wait for ack. + if (has_close_options) { + session->SendConnectionClose(); + } session->Destroy(); } @@ -721,6 +1016,21 @@ struct Session::Impl final : public MemoryRetainer { ->object()); } + JS_METHOD(GetLocalAddress) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); + + if (session->is_destroyed()) { + return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + } + + auto address = session->local_address(); + args.GetReturnValue().Set( + SocketAddressBase::Create(env, std::make_shared(address)) + ->object()); + } + JS_METHOD(GetCertificate) { auto env = Environment::GetCurrent(args); Session* session; @@ -773,6 +1083,8 @@ struct Session::Impl final : public MemoryRetainer { return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } + // args[0] is the optional close error options object. + if (!MaybeSetCloseError(args, 0, session)) return; session->Close(CloseMethod::GRACEFUL); } @@ -819,11 +1131,12 @@ struct Session::Impl final : public MemoryRetainer { // GetDataQueueFromSource handles type validation. std::shared_ptr data_source; - if (!Stream::GetDataQueueFromSource(env, args[1]).To(&data_source) || - data_source == nullptr) [[unlikely]] { - THROW_ERR_INVALID_ARG_VALUE(env, "Invalid data source"); + if (!Stream::GetDataQueueFromSource(env, args[1]).To(&data_source)) + [[unlikely]] { + return THROW_ERR_INVALID_ARG_VALUE(env, "Invalid data source"); } + session->impl_->handshake_deferred_ = false; SendPendingDataScope send_scope(session); auto direction = FromV8Value(args[0]); Local stream; @@ -843,6 +1156,7 @@ struct Session::Impl final : public MemoryRetainer { } DCHECK(args[0]->IsArrayBufferView()); + session->impl_->handshake_deferred_ = false; SendPendingDataScope send_scope(session); Store store; @@ -854,6 +1168,54 @@ struct Session::Impl final : public MemoryRetainer { BigInt::New(env->isolate(), session->SendDatagram(std::move(store)))); } + JS_METHOD(LocalTransportParams) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); + + ngtcp2_conn* conn = *session; + TransportParams params(ngtcp2_conn_get_local_transport_params(conn)); + Local obj; + if (params.ToObject(env).ToLocal(&obj)) { + args.GetReturnValue().Set(obj); + } + } + + JS_METHOD(RemoteTransportParams) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); + + ngtcp2_conn* conn = *session; + auto params = ngtcp2_conn_get_remote_transport_params(conn); + if (params == nullptr) { + // Remote transport parameters are not yet available. + return args.GetReturnValue().SetUndefined(); + } + TransportParams tp(params); + Local obj; + if (tp.ToObject(env).ToLocal(&obj)) { + args.GetReturnValue().Set(obj); + } + } + + JS_METHOD(ApplicationOptions) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); + + Local obj; + if (!session->has_application()) { + // The application has not yet been selected (ALPN negotiation is not + // yet complete on the server) or the session has been destroyed. In + // either case, the application options are not available. + return args.GetReturnValue().SetUndefined(); + } + auto& options = session->application().options(); + if (options.ToObject(env).ToLocal(&obj)) { + args.GetReturnValue().Set(obj); + } + } // Internal ngtcp2 callbacks static int on_acknowledge_stream_data_offset(ngtcp2_conn* conn, @@ -888,7 +1250,7 @@ struct Session::Impl final : public MemoryRetainer { ngtcp2_connection_id_status_type type, uint64_t seq, const ngtcp2_cid* cid, - const uint8_t* token, + const ngtcp2_stateless_reset_token* token, void* user_data) { NGTCP2_CALLBACK_SCOPE(session) std::optional maybe_reset_token; @@ -896,7 +1258,6 @@ struct Session::Impl final : public MemoryRetainer { auto& endpoint = session->endpoint(); switch (type) { case NGTCP2_CONNECTION_ID_STATUS_TYPE_ACTIVATE: { - endpoint.AssociateCID(session->config().scid, CID(cid)); if (token != nullptr) { endpoint.AssociateStatelessResetToken(StatelessResetToken(token), session); @@ -904,7 +1265,6 @@ struct Session::Impl final : public MemoryRetainer { break; } case NGTCP2_CONNECTION_ID_STATUS_TYPE_DEACTIVATE: { - endpoint.DisassociateCID(CID(cid)); if (token != nullptr) { endpoint.DisassociateStatelessResetToken(StatelessResetToken(token)); } @@ -953,14 +1313,15 @@ struct Session::Impl final : public MemoryRetainer { void* user_data, void* stream_user_data) { NGTCP2_CALLBACK_SCOPE(session) - session->application().ExtendMaxStreamData(Stream::From(stream_user_data), - max_data); + if (auto* stream = Stream::From(stream_user_data)) { + session->application().ExtendMaxStreamData(stream, max_data); + } return NGTCP2_SUCCESS; } static int on_get_new_cid(ngtcp2_conn* conn, ngtcp2_cid* cid, - uint8_t* token, + ngtcp2_stateless_reset_token* token, size_t cidlen, void* user_data) { NGTCP2_CALLBACK_SCOPE(session) @@ -1047,6 +1408,10 @@ struct Session::Impl final : public MemoryRetainer { if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return NGTCP2_SUCCESS; + // If the application was already started via on_receive_tx_key + // (0-RTT path), this is a no-op. + if (session->application().is_started()) return NGTCP2_SUCCESS; + Debug(session, "Receiving RX key for level %s for dcid %s", to_string(level), @@ -1057,10 +1422,17 @@ struct Session::Impl final : public MemoryRetainer { } static int on_receive_stateless_reset(ngtcp2_conn* conn, - const ngtcp2_pkt_stateless_reset* sr, + const ngtcp2_pkt_stateless_reset2* sr, void* user_data) { NGTCP2_CALLBACK_SCOPE(session) - session->impl_->state_->stateless_reset = 1; + Debug(session, "Received stateless reset from peer"); + // This callback is informational. ngtcp2 has already set the + // connection state to NGTCP2_CS_DRAINING before invoking this + // callback, and ngtcp2_conn_read_pkt will return + // NGTCP2_ERR_DRAINING. The actual close handling happens in + // Session::Receive when it processes that return value and + // checks this flag. + session->impl_->state()->stateless_reset = 1; return NGTCP2_SUCCESS; } @@ -1107,9 +1479,22 @@ struct Session::Impl final : public MemoryRetainer { ngtcp2_encryption_level level, void* user_data) { NGTCP2_CALLBACK_SCOPE(session); - CHECK(session->is_server()); - if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return NGTCP2_SUCCESS; + // For SERVER: fires at 1RTT — start the application after handshake. + // For CLIENT: fires at 0RTT — start the application early so that + // HTTP/3 control/QPACK streams are bound before 0-RTT requests. + // Without this, nghttp3_conn_submit_request asserts because the + // QPACK encoder stream isn't bound yet. + if (session->is_server()) { + if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return NGTCP2_SUCCESS; + } else { + if (level != NGTCP2_ENCRYPTION_LEVEL_0RTT) return NGTCP2_SUCCESS; + } + + // application_ may be null if ALPN selection hasn't happened yet + // (e.g., ALPN mismatch causes the handshake to fail during key + // installation). Without an application, we can't start. + if (!session->impl_->application_) return NGTCP2_ERR_CALLBACK_FAILURE; Debug(session, "Receiving TX key for level %s for dcid %s", @@ -1154,12 +1539,21 @@ struct Session::Impl final : public MemoryRetainer { void* user_data, void* stream_user_data) { NGTCP2_CALLBACK_SCOPE(session) + auto* stream = Stream::From(stream_user_data); + if (stream == nullptr) return NGTCP2_SUCCESS; if (flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET) { - session->application().StreamClose( - Stream::From(stream_user_data), - QuicError::ForApplication(app_error_code)); + session->application().ReceiveStreamClose( + stream, QuicError::ForApplication(app_error_code)); } else { - session->application().StreamClose(Stream::From(stream_user_data)); + session->application().ReceiveStreamClose(stream); + } + return NGTCP2_SUCCESS; + } + + static int on_stream_open(ngtcp2_conn* conn, stream_id id, void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + if (!session->application().ReceiveStreamOpen(id)) { + return NGTCP2_ERR_CALLBACK_FAILURE; } return NGTCP2_SUCCESS; } @@ -1171,10 +1565,10 @@ struct Session::Impl final : public MemoryRetainer { void* user_data, void* stream_user_data) { NGTCP2_CALLBACK_SCOPE(session) - session->application().StreamReset( - Stream::From(stream_user_data), - final_size, - QuicError::ForApplication(app_error_code)); + auto* stream = Stream::From(stream_user_data); + if (stream == nullptr) return NGTCP2_SUCCESS; + session->application().ReceiveStreamReset( + stream, final_size, QuicError::ForApplication(app_error_code)); return NGTCP2_SUCCESS; } @@ -1184,9 +1578,10 @@ struct Session::Impl final : public MemoryRetainer { void* user_data, void* stream_user_data) { NGTCP2_CALLBACK_SCOPE(session) - session->application().StreamStopSending( - Stream::From(stream_user_data), - QuicError::ForApplication(app_error_code)); + auto* stream = Stream::From(stream_user_data); + if (stream == nullptr) return NGTCP2_SUCCESS; + session->application().ReceiveStreamStopSending( + stream, QuicError::ForApplication(app_error_code)); return NGTCP2_SUCCESS; } @@ -1200,6 +1595,9 @@ struct Session::Impl final : public MemoryRetainer { auto session = Impl::From(conn, user_data); if (session == nullptr) return NGTCP2_ERR_CALLBACK_FAILURE; Debug(session, "Early data was rejected"); + if (session->impl_->application_) { + session->application().EarlyDataRejected(); + } return NGTCP2_SUCCESS; } @@ -1224,14 +1622,14 @@ struct Session::Impl final : public MemoryRetainer { ngtcp2_crypto_hp_mask_cb, on_receive_stream_data, on_acknowledge_stream_data_offset, - nullptr, + on_stream_open, on_stream_close, - on_receive_stateless_reset, + nullptr, // recv_stateless_reset (deprecated, use v2 below) ngtcp2_crypto_recv_retry_cb, on_extend_max_streams_bidi, on_extend_max_streams_uni, on_rand, - on_get_new_cid, + nullptr, // get_new_connection_id (deprecated, use v2 below) on_remove_connection_id, ngtcp2_crypto_update_key_cb, on_path_validation, @@ -1240,7 +1638,7 @@ struct Session::Impl final : public MemoryRetainer { on_extend_max_remote_streams_bidi, on_extend_max_remote_streams_uni, on_extend_max_stream_data, - on_cid_status, + nullptr, // dcid_status (deprecated, use v2 below) on_handshake_confirmed, on_receive_new_token, ngtcp2_crypto_delete_crypto_aead_ctx_cb, @@ -1248,13 +1646,17 @@ struct Session::Impl final : public MemoryRetainer { on_receive_datagram, on_acknowledge_datagram, on_lost_datagram, - ngtcp2_crypto_get_path_challenge_data_cb, + nullptr, // get_path_challenge_data (deprecated, use v2 below) on_stream_stop_sending, ngtcp2_crypto_version_negotiation_cb, on_receive_rx_key, - nullptr, + on_receive_tx_key, on_early_data_rejected, - on_begin_path_validation}; + on_begin_path_validation, + on_receive_stateless_reset, + on_get_new_cid, + on_cid_status, + ngtcp2_crypto_get_path_challenge_data2_cb}; static constexpr ngtcp2_callbacks SERVER = { nullptr, @@ -1267,14 +1669,14 @@ struct Session::Impl final : public MemoryRetainer { ngtcp2_crypto_hp_mask_cb, on_receive_stream_data, on_acknowledge_stream_data_offset, - nullptr, + on_stream_open, on_stream_close, - on_receive_stateless_reset, + nullptr, // recv_stateless_reset (deprecated, use v2 below) nullptr, on_extend_max_streams_bidi, on_extend_max_streams_uni, on_rand, - on_get_new_cid, + nullptr, // get_new_connection_id (deprecated, use v2 below) on_remove_connection_id, ngtcp2_crypto_update_key_cb, on_path_validation, @@ -1283,7 +1685,7 @@ struct Session::Impl final : public MemoryRetainer { on_extend_max_remote_streams_bidi, on_extend_max_remote_streams_uni, on_extend_max_stream_data, - on_cid_status, + nullptr, // dcid_status (deprecated, use v2 below) nullptr, nullptr, ngtcp2_crypto_delete_crypto_aead_ctx_cb, @@ -1291,13 +1693,17 @@ struct Session::Impl final : public MemoryRetainer { on_receive_datagram, on_acknowledge_datagram, on_lost_datagram, - ngtcp2_crypto_get_path_challenge_data_cb, + nullptr, // get_path_challenge_data (deprecated, use v2 below) on_stream_stop_sending, ngtcp2_crypto_version_negotiation_cb, nullptr, on_receive_tx_key, on_early_data_rejected, - on_begin_path_validation}; + on_begin_path_validation, + on_receive_stateless_reset, + on_get_new_cid, + on_cid_status, + ngtcp2_crypto_get_path_challenge_data2_cb}; }; #undef NGTCP2_CALLBACK_SCOPE @@ -1312,13 +1718,18 @@ Session::SendPendingDataScope::SendPendingDataScope(Session* session) Session::SendPendingDataScope::SendPendingDataScope( const BaseObjectPtr& session) - : SendPendingDataScope(session.get()) {} + : session(session.get()) { + CHECK_NOT_NULL(session); + CHECK(!session->is_destroyed()); + ++session->impl_->send_scope_depth_; +} Session::SendPendingDataScope::~SendPendingDataScope() { if (session->is_destroyed()) return; DCHECK_GE(session->impl_->send_scope_depth_, 1); + Debug(session, "Send Scope Depth %zu", session->impl_->send_scope_depth_); if (--session->impl_->send_scope_depth_ == 0 && - session->impl_->application_) { + session->impl_->application_ && !session->impl_->handshake_deferred_) { session->application().SendPendingData(); } } @@ -1341,11 +1752,12 @@ Session::Session(Endpoint* endpoint, const std::optional& session_ticket) : AsyncWrap(endpoint->env(), object, PROVIDER_QUIC_SESSION), side_(config.side), - allocator_(BindingData::Get(env())), + allocator_(BindingData::Get(env()).ngtcp2_allocator()), impl_(std::make_unique(this, endpoint, config)), connection_(InitConnection()), tls_session_(tls_context->NewSession(this, session_ticket)) { DCHECK(impl_); + STAT_RECORD_TIMESTAMP(Stats, created_at); // For clients, select the Application immediately — the ALPN is // known upfront from the options. For servers, application_ stays @@ -1356,24 +1768,50 @@ Session::Session(Endpoint* endpoint, if (app) SetApplication(std::move(app)); } + // For client sessions with a session ticket and early data enabled, + // defer the handshake until the first stream or datagram is sent. + // This enables 0-RTT: the stream/datagram data is included in the + // first flight alongside the ClientHello. When early data is + // disabled, the handshake starts immediately (no 0-RTT attempt). + if (config.side == Side::CLIENT && session_ticket.has_value() && + config.options.tls_options.enable_early_data) { + impl_->handshake_deferred_ = true; + } + + if (config.options.keep_alive_timeout > 0) { + ngtcp2_conn_set_keep_alive_timeout( + *this, config.options.keep_alive_timeout * NGTCP2_MILLISECONDS); + } + MakeWeak(); Debug(this, "Session created."); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->stats_string(), impl_->stats_.GetArrayBuffer()); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->state_string(), impl_->state_.GetArrayBuffer()); - - if (config.options.qlog) [[unlikely]] { - qlog_stream_ = LogStream::Create(env()); + { + const HandleScope handle_scope(env()->isolate()); JS_DEFINE_READONLY_PROPERTY( - env(), object, binding.qlog_string(), qlog_stream_->object()); - } - - if (config.options.tls_options.keylog) [[unlikely]] { - keylog_stream_ = LogStream::Create(env()); + env(), + object, + env()->state_string(), + impl_->state_slot_.GetPageDataView(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "stateByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(impl_->state_slot_.GetByteOffset()))); JS_DEFINE_READONLY_PROPERTY( - env(), object, binding.keylog_string(), keylog_stream_->object()); + env(), + object, + env()->stats_string(), + impl_->stats_slot_.GetPageBigUint64Array(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "statsByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(impl_->stats_slot_.GetByteOffset()))); } UpdateDataStats(); @@ -1406,7 +1844,7 @@ Session::QuicConnectionPointer Session::InitConnection() { &Impl::SERVER, &config().settings, transport_params, - &allocator_, + allocator_, this), 0); break; @@ -1420,7 +1858,7 @@ Session::QuicConnectionPointer Session::InitConnection() { &Impl::CLIENT, &config().settings, transport_params, - &allocator_, + allocator_, this), 0); break; @@ -1438,23 +1876,30 @@ bool Session::is_server() const { } bool Session::is_destroyed() const { - return !impl_; + return !impl_ || flags_.destroy_deferred; } bool Session::is_destroyed_or_closing() const { - return !impl_ || impl_->state_->closing; + return !impl_ || impl_->state()->closing; } void Session::Close(CloseMethod method) { if (is_destroyed()) return; - auto& stats_ = impl_->stats_; + + // If the handshake was deferred (0-RTT client that never sent), + // no packets were ever transmitted. Close silently since there is + // nothing to communicate to the peer. + if (impl_->handshake_deferred_) { + impl_->handshake_deferred_ = false; + method = CloseMethod::SILENT; + } if (impl_->last_error_) { Debug(this, "Closing with error: %s", impl_->last_error_); } STAT_RECORD_TIMESTAMP(Stats, closing_at); - impl_->state_->closing = 1; + impl_->state()->closing = 1; // With both the DEFAULT and SILENT options, we will proceed to closing // the session immediately. All open streams will be immediately destroyed @@ -1470,28 +1915,71 @@ void Session::Close(CloseMethod method) { switch (method) { case CloseMethod::DEFAULT: { Debug(this, "Immediately closing session"); - impl_->state_->silent_close = 0; + impl_->state()->silent_close = 0; return FinishClose(); } case CloseMethod::SILENT: { Debug(this, "Immediately closing session silently"); - impl_->state_->silent_close = 1; + impl_->state()->silent_close = 1; return FinishClose(); } case CloseMethod::GRACEFUL: { - // If there are no open streams, then we can close just immediately and + // If we are already closing gracefully, do nothing. + if (impl_->state()->graceful_close) [[unlikely]] { + return; + } + impl_->state()->graceful_close = 1; + + // application_ may be null for server sessions if close() is called + // before the TLS handshake selects the ALPN. Without an application + // we cannot do a graceful shutdown (GOAWAY, CONNECTION_CLOSE etc.), + // so fall through to a silent close. + if (!impl_->application_) { + impl_->state()->silent_close = 1; + return FinishClose(); + } + + // The SendPendingDataScope ensures that the GOAWAY packet queued + // by BeginShutdown is actually sent. Without it, the GOAWAY sits + // in nghttp3's outq until the next Receive() triggers a send. + SendPendingDataScope send_scope(this); + + // Signal application-level graceful shutdown (e.g., HTTP/3 GOAWAY). + // BeginShutdown can trigger callbacks that re-enter JS and destroy + // this session, so check is_destroyed() after it returns. + application().BeginShutdown(); + if (is_destroyed()) return; + + // If there are no open streams, then we can close immediately and // not worry about waiting around. if (impl_->streams_.empty()) { - impl_->state_->silent_close = 0; - impl_->state_->graceful_close = 0; + impl_->state()->silent_close = 0; return FinishClose(); } - // If we are already closing gracefully, do nothing. - if (impl_->state_->graceful_close) [[unlikely]] { - return; + // Shut down the writable side of streams whose readable side is + // already ended (e.g., peer called resetStream or sent FIN). Without + // this, such half-closed streams will never fire on_stream_close and + // the graceful close hangs. Streams still actively receiving data + // are left alone to complete naturally. + // + // When the application manages stream FIN (HTTP/3), skip this — a + // writable stream with a closed read side is the normal request/ + // response pattern (server received full request, still sending + // response). The application protocol handles stream completion. + if (!application().stream_fin_managed_by_application()) { + Session::SendPendingDataScope send_scope(this); + for (auto& [id, stream] : impl_->streams_) { + if (stream->is_writable() && !stream->is_readable()) { + stream->EndWritable(); + ngtcp2_conn_shutdown_stream_write(*this, 0, id, 0); + } + } } - impl_->state_->graceful_close = 1; + // The SendPendingDataScope destructor can trigger callbacks that + // re-enter JS and destroy this session. + if (is_destroyed()) return; + Debug(this, "Gracefully closing session (waiting on %zu streams)", impl_->streams_.size()); @@ -1503,37 +1991,78 @@ void Session::Close(CloseMethod method) { void Session::FinishClose() { // FinishClose() should be called only after, and as a result of, Close() - // being called first. - DCHECK(!is_destroyed()); - DCHECK(impl_->state_->closing); + // being called first. However, re-entrancy through MakeCallback or timer + // callbacks can cause impl_ to be destroyed at any point during this + // method. We must check is_destroyed() after every operation that could + // trigger MakeCallback (stream destruction, pending queue rejection, + // SendConnectionClose, EmitClose). + if (is_destroyed()) return; + DCHECK(impl_->state()->closing); + + // Clear the graceful_close flag to prevent RemoveStream() from + // re-entering FinishClose() when we destroy streams below. + impl_->state()->graceful_close = 0; - // If impl_->Close() returns true, then the session can be destroyed - // immediately without round-tripping through JavaScript. - if (impl_->Close()) { - return Destroy(); + // Destroy all open streams immediately. We copy the map because + // streams remove themselves during destruction. Each Destroy() call + // triggers MakeCallback which can destroy impl_ via JS re-entrancy. + StreamsMap streams = impl_->streams_; + for (auto& stream : streams) { + if (is_destroyed()) return; + stream.second->Destroy(impl_->last_error_); } + if (is_destroyed()) return; - // Otherwise, we emit a close callback so that the JavaScript side can - // clean up anything it needs to clean up before destroying. - EmitClose(); + // Clear pending stream queues. + while (!impl_->pending_bidi_stream_queue_.IsEmpty()) { + impl_->pending_bidi_stream_queue_.PopFront()->reject(impl_->last_error_); + } + while (!impl_->pending_uni_stream_queue_.IsEmpty()) { + impl_->pending_uni_stream_queue_.PopFront()->reject(impl_->last_error_); + } + + // Send final application-level shutdown and CONNECTION_CLOSE + // unless this is a silent close. + if (!impl_->state()->silent_close) { + if (impl_->application_) { + application().CompleteShutdown(); + } + SendConnectionClose(); + } + if (is_destroyed()) return; + + impl_->timer_.Close(); + + // If the session was passed to JavaScript, we need to round-trip + // through JS so it can clean up before we destroy. The JS side + // will synchronously call destroy(), which calls Session::Destroy(). + if (impl_->state()->wrapped) { + EmitClose(impl_->last_error_); + } else { + Destroy(); + } } void Session::Destroy() { - // Destroy() should be called only after, and as a result of, Close() - // being called first. DCHECK(impl_); - DCHECK(impl_->state_->closing); + // Ensure the closing flag is set for the ~Impl() DCHECK. Normally + // this is set by Session::Close(), but JS destroy() can be called + // directly without going through Close() first. + impl_->state()->closing = 1; + + // If we're inside a ngtcp2 or nghttp3 callback scope, we cannot + // destroy impl_ now because the callback is executing methods on + // objects owned by impl_ (e.g., the Application). Defer the + // destruction until the scope exits. + if (flags_.in_ngtcp2_callback_scope || flags_.in_nghttp3_callback_scope) { + Debug(this, "Session destroy deferred (in callback scope)"); + flags_.destroy_deferred = true; + return; + } + Debug(this, "Session destroyed"); + STAT_RECORD_TIMESTAMP(Stats, destroyed_at); impl_.reset(); - if (qlog_stream_ || keylog_stream_) { - env()->SetImmediate( - [qlog = qlog_stream_, keylog = keylog_stream_](Environment*) { - if (qlog) qlog->End(); - if (keylog) keylog->End(); - }); - } - qlog_stream_.reset(); - keylog_stream_.reset(); } PendingStream::PendingStreamQueue& Session::pending_bidi_stream_queue() const { @@ -1566,6 +2095,10 @@ TLSSession& Session::tls_session() const { return *tls_session_; } +bool Session::has_application() const { + return !is_destroyed() && impl_->application_ != nullptr; +} + Session::Application& Session::application() const { DCHECK(!is_destroyed()); DCHECK(impl_->application_); @@ -1595,7 +2128,29 @@ std::unique_ptr Session::SelectApplicationFromAlpn( void Session::SetApplication(std::unique_ptr app) { DCHECK(!impl_->application_); - impl_->state_->application_type = static_cast(app->type()); + // If we have pending ticket data from a session ticket that was + // parsed before ALPN negotiation, validate it against the selected + // application now. If the type doesn't match or the application + // rejects the data, the handshake will fail (application_ stays null + // and the caller returns an error). + if (impl_->pending_ticket_data_.has_value()) { + auto data = std::move(*impl_->pending_ticket_data_); + impl_->pending_ticket_data_.reset(); + if (!app->ApplySessionTicketData(data)) { + Debug(this, "Session ticket app data rejected by application"); + return; + } + } + impl_->state()->application_type = static_cast(app->type()); + impl_->state()->headers_supported = static_cast( + app->SupportsHeaders() ? HeadersSupportState::SUPPORTED + : HeadersSupportState::UNSUPPORTED); + // Surface the application's "no error" and "internal error" codes via + // session state so that JS-side code (e.g. the stream writer's fail() + // path) can resolve the right wire code for the negotiated ALPN + // without duplicating the per-application table. + impl_->state()->no_error_code = app->GetNoErrorCode(); + impl_->state()->internal_error_code = app->GetInternalErrorCode(); impl_->application_ = std::move(app); } @@ -1632,20 +2187,50 @@ const Session::Options& Session::options() const { return impl_->config_.options; } -void Session::HandleQlog(uint32_t flags, const void* data, size_t len) { - DCHECK(qlog_stream_); +void Session::EmitQlog(uint32_t flags, std::string_view data) { + if (!env()->can_call_into_js()) return; + + bool fin = (flags & NGTCP2_QLOG_WRITE_FLAG_FIN) != 0; + // Fun fact... ngtcp2 does not emit the final qlog statement until the - // ngtcp2_conn object is destroyed. - std::vector buffer(len); - memcpy(buffer.data(), data, len); - Debug(this, "Emitting qlog data to the qlog stream"); - env()->SetImmediate([ptr = qlog_stream_, buffer = std::move(buffer), flags]( - Environment*) { - ptr->Emit(buffer.data(), - buffer.size(), - flags & NGTCP2_QLOG_WRITE_FLAG_FIN ? LogStream::EmitOption::FIN - : LogStream::EmitOption::NONE); - }); + // ngtcp2_conn object is destroyed. That means this method is called + // synchronously during impl_.reset() in Session::Destroy(), at which + // point is_destroyed() is true. We cannot use MakeCallback here because + // it can trigger microtask processing and re-entrancy while the + // ngtcp2_conn is mid-destruction. Defer the final chunk via SetImmediate. + if (is_destroyed()) { + auto isolate = env()->isolate(); + Global recv(isolate, object()); + Global cb(isolate, + BindingData::Get(env()).session_qlog_callback()); + std::string buf(data); + env()->SetImmediate([recv = std::move(recv), + cb = std::move(cb), + buf = std::move(buf), + fin](Environment* env) { + HandleScope handle_scope(env->isolate()); + auto context = env->context(); + Local argv[] = { + Undefined(env->isolate()), + Boolean::New(env->isolate(), fin), + }; + if (!ToV8Value(context, buf).ToLocal(&argv[0])) return; + USE(cb.Get(env->isolate()) + ->Call(context, recv.Get(env->isolate()), arraysize(argv), argv)); + }); + return; + } + + auto isolate = env()->isolate(); + Local argv[] = {Undefined(isolate), Boolean::New(isolate, fin)}; + if (!ToV8Value(env()->context(), data).ToLocal(&argv[0])) { + Debug(this, "Failed to convert qlog data to V8 string"); + return; + } + + Debug(this, "Emitting qlog data"); + MakeCallback( + BindingData::Get(env()).session_qlog_callback(), arraysize(argv), argv); } const TransportParams Session::local_transport_params() const { @@ -1664,45 +2249,69 @@ void Session::SetLastError(QuicError&& error) { impl_->last_error_ = std::move(error); } -bool Session::Receive(Store&& store, +bool Session::Receive(const uint8_t* data, + size_t len, const SocketAddress& local_address, - const SocketAddress& remote_address) { + const SocketAddress& remote_address, + const PacketInfo& pkt_info, + uint64_t ts) { + // Convenience wrapper: reads the packet and immediately triggers + // SendPendingData. Used by paths that need an immediate response + // (e.g., Endpoint::Connect for client Initial packets). + // The hot receive path uses ReadPacket() directly with deferred + // flush via BindingData's uv_check callback. + SendPendingDataScope send_scope(this); + return ReadPacket(data, len, local_address, remote_address, pkt_info, ts); +} + +bool Session::ReadPacket(const uint8_t* data, + size_t len, + const SocketAddress& local_address, + const SocketAddress& remote_address, + const PacketInfo& pkt_info, + uint64_t ts) { DCHECK(!is_destroyed()); impl_->remote_address_ = remote_address; - // When we are done processing this packet, we arrange to send any - // pending data for this session. - SendPendingDataScope send_scope(this); - - ngtcp2_vec vec = store; Path path(local_address, remote_address); Debug(this, "Session is receiving %zu-byte packet received along path %s", - vec.len, + len, path); // It is important to understand that reading the packet will cause // callback functions to be invoked, any one of which could lead to - // the Session being closed/destroyed synchronously. After calling - // ngtcp2_conn_read_pkt here, we will need to double check that the - // session is not destroyed before we try doing anything with it - // (like updating stats, sending pending data, etc). - int err = - ngtcp2_conn_read_pkt(*this, - &path, - // TODO(@jasnell): ECN pkt_info blocked on libuv - nullptr, - vec.base, - vec.len, - uv_hrtime()); + // the Session being closed/destroyed synchronously. The callback scope + // ensures that any deferred destroy waits until all callbacks for this + // packet have completed. After calling ngtcp2_conn_read_pkt here, we + // will need to double check that the session is not destroyed before + // we try doing anything with it (like updating stats, etc). + int err; + { + NgTcp2CallbackScope callback_scope(this); + // The PacketInfo carries per-packet metadata (currently ECN codepoint). + // When libuv gains per-packet ECN reporting, the caller should + // populate pkt_info from the receive metadata before calling + // ReadPacket(). + // When ts is 0 (the default), call uv_hrtime() here. The batched + // receive path caches a timestamp and passes it to all ReadPacket() + // calls in the same I/O burst. + if (ts == 0) ts = uv_hrtime(); + err = ngtcp2_conn_read_pkt(*this, &path, pkt_info, data, len, ts); + } + if (is_destroyed()) return false; + + Debug(this, "Session receiving %zu-byte packet with result %d", len, err); switch (err) { case 0: { - Debug(this, "Session successfully received %zu-byte packet", vec.len); + Debug(this, "Session successfully received %zu-byte packet", len); if (!is_destroyed()) [[likely]] { - auto& stats_ = impl_->stats_; - STAT_INCREMENT_N(Stats, bytes_received, vec.len); + STAT_INCREMENT_N(Stats, bytes_received, len); + // Process deferred operations that couldn't run inside callback + // scopes (e.g., HTTP/3 GOAWAY handling that calls into JS). + application().PostReceive(); } return true; } @@ -1717,10 +2326,28 @@ bool Session::Receive(Store&& store, return false; } case NGTCP2_ERR_DRAINING: { - // Connection has entered the draining state, no further data should be - // sent. This happens when the remote peer has already sent a - // CONNECTION_CLOSE. - Debug(this, "Receiving packet failed: Session is draining"); + // Connection has entered the draining state, no further data + // should be sent. This can happen for two reasons: + // + // 1. The remote peer sent a CONNECTION_CLOSE. In this case we + // start the draining timer and let OnTimeout handle the + // close, extracting the peer's error via FromConnectionClose. + // + // 2. The remote peer sent a stateless reset. ngtcp2 set the + // draining state internally and invoked our informational + // on_receive_stateless_reset callback (which set the flag). + // There is no point in waiting for a draining period — the + // peer has no state. Close immediately with an error. + if (!is_destroyed()) [[likely]] { + if (impl_->state()->stateless_reset) { + Debug(this, "Session received stateless reset, closing"); + SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_DRAINING)); + Close(CloseMethod::SILENT); + } else { + Debug(this, "Session is draining, starting draining timer"); + UpdateTimer(); + } + } return false; } case NGTCP2_ERR_CLOSING: { @@ -1746,13 +2373,15 @@ bool Session::Receive(Store&& store, DCHECK(is_server()); Debug(this, "Receiving packet failed: Server must send a retry packet"); if (!is_destroyed()) { - endpoint().SendRetry(PathDescriptor{ - version(), - config().dcid, - config().scid, - impl_->local_address_, - impl_->remote_address_, - }); + endpoint().SendRetry( + PathDescriptor{ + version(), + config().dcid, + config().scid, + impl_->local_address_, + impl_->remote_address_, + }, + uv_hrtime()); Close(CloseMethod::SILENT); } return false; @@ -1779,6 +2408,72 @@ bool Session::Receive(Store&& store, return false; } +void Session::SendBatch(Packet::Ptr* packets, + PathStorage* paths, + size_t count) { + DCHECK(!is_destroyed()); + if (count == 0) return; + + // Separate packets into those going to the primary endpoint and those + // redirected to other endpoints (rare: path validation, preferred address). + // Redirected packets are sent individually via the target endpoint. + static constexpr size_t kMaxBatch = 64; + DCHECK_LE(count, kMaxBatch); + Packet::Ptr primary_packets[kMaxBatch]; + size_t primary_count = 0; + + for (size_t i = 0; i < count; i++) { + if (!packets[i] || !can_send_packets()) { + packets[i].reset(); + continue; + } + + UpdatePath(paths[i]); + + // Check for cross-endpoint redirect. + bool redirected = false; + if (paths[i].path.local.addrlen > 0) { + SocketAddress local_addr(paths[i].path.local.addr); + auto& mgr = BindingData::Get(env()).session_manager(); + Endpoint* target = mgr.FindEndpointForAddress(local_addr); + if (target != nullptr && target != &endpoint()) { + SocketAddress remote_addr(paths[i].path.remote.addr); + packets[i]->Redirect(static_cast(target), + remote_addr); + target->Send(std::move(packets[i])); + redirected = true; + } + } + + if (!redirected) { + primary_packets[primary_count++] = std::move(packets[i]); + } + } + + if (primary_count == 0) return; + + // Use batched send for the primary endpoint. + if (flags_.prefer_try_send) { + endpoint().SendBatch(primary_packets, primary_count); + } else { + // Non-flush path: send individually via async uv_udp_send. + for (size_t i = 0; i < primary_count; i++) { + Send(std::move(primary_packets[i])); + } + } +} + +void Session::FlushPendingData() { + DCHECK(!is_destroyed()); + if (impl_->application_) { + // Prefer synchronous sends during the deferred flush to avoid the + // one-tick latency of async uv_udp_send from the uv_check callback. + flags_.prefer_try_send = true; + application().SendPendingData(); + flags_.prefer_try_send = false; + } +} + void Session::Send(Packet::Ptr packet) { // Sending a Packet is generally best effort. If we're not in a state // where we can send a packet, it's ok to drop it on the floor. The @@ -1795,9 +2490,17 @@ void Session::Send(Packet::Ptr packet) { return; } + // When called from the deferred flush path (uv_check callback), + // prefer synchronous send to avoid the one-tick latency of async + // uv_udp_send. SendOrTrySend uses uv_udp_try_send first, falling + // back to uv_udp_send on EAGAIN. + if (flags_.prefer_try_send) { + Debug(this, "Session is sending (try_send) %s", packet->ToString()); + endpoint().SendOrTrySend(std::move(packet)); + return; + } + Debug(this, "Session is sending %s", packet->ToString()); - auto& stats_ = impl_->stats_; - STAT_INCREMENT_N(Stats, bytes_sent, packet->length()); endpoint().Send(std::move(packet)); } @@ -1805,6 +2508,31 @@ void Session::Send(Packet::Ptr packet, const PathStorage& path) { DCHECK(!is_destroyed()); DCHECK(!is_in_draining_period()); UpdatePath(path); + + // Check if ngtcp2 wants this packet sent on a different path than the + // primary endpoint. This happens during path validation for preferred + // address or connection migration — e.g., a PATH_RESPONSE needs to be + // sent from the preferred address endpoint, not the primary. + if (path.path.local.addrlen > 0) { + SocketAddress local_addr(path.path.local.addr); + auto& mgr = BindingData::Get(env()).session_manager(); + Endpoint* target = mgr.FindEndpointForAddress(local_addr); + if (target != nullptr && target != &endpoint()) { + // Redirect the packet to the target endpoint. This updates the + // listener (for pending_callbacks accounting in the ArenaPool + // completion callback) and the destination address. + SocketAddress remote_addr(path.path.remote.addr); + packet->Redirect(static_cast(target), remote_addr); + if (can_send_packets()) [[likely]] { + Debug(this, + "Sending via non-primary endpoint for path %s", + local_addr.ToString()); + target->Send(std::move(packet)); + } + return; + } + } + Send(std::move(packet)); } @@ -1815,181 +2543,63 @@ datagram_id Session::SendDatagram(Store&& data) { // we just return 0 to indicate that the datagram was not sent an the // data is dropped on the floor. - if (!can_send_packets()) { - Debug(this, "Unable to send datagram"); + // If the session is destroyed, draining, or closing, we cannot send. + if (is_destroyed() || is_in_draining_period() || is_in_closing_period()) { return 0; } const ngtcp2_transport_params* tp = remote_transport_params(); - uint64_t max_datagram_size = tp->max_datagram_frame_size; + uint64_t max_datagram_size = MaxDatagramPayload(tp->max_datagram_frame_size); + + // These size and length checks should have been caught by the JavaScript + // side, but handle it gracefully here just in case. We might have some future + // case where datagram frames are sent from C++ code directly, so it's good to + // have these checks as a backstop regardless. if (max_datagram_size == 0) { Debug(this, "Datagrams are disabled"); return 0; } - if (data.length() > max_datagram_size) { + if (data.length() > max_datagram_size) [[unlikely]] { Debug(this, "Ignoring oversized datagram"); return 0; } - if (data.length() == 0) { + if (data.length() == 0) [[unlikely]] { Debug(this, "Ignoring empty datagram"); return 0; } - Packet::Ptr packet; - uint8_t* pos = nullptr; - int accepted = 0; - ngtcp2_vec vec = data; - PathStorage path; - int flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE; - datagram_id did = impl_->state_->last_datagram_id + 1; - - Debug(this, "Sending %zu-byte datagram %" PRIu64, data.length(), did); - - // Let's give it a max number of attempts to send the datagram. - static const int kMaxAttempts = 16; - int attempts = 0; - - auto on_exit = OnScopeLeave([&] { - UpdatePacketTxTime(); - UpdateTimer(); - UpdateDataStats(); - }); - - for (;;) { - // We may have to make several attempts at encoding and sending the - // datagram packet. On each iteration here we'll try to encode the - // datagram. It's entirely up to ngtcp2 whether to include the datagram - // in the packet on each call to ngtcp2_conn_writev_datagram. - if (!packet) { - packet = endpoint().CreatePacket( - impl_->remote_address_, - ngtcp2_conn_get_max_tx_udp_payload_size(*this), - "datagram"); - // Typically sending datagrams is best effort, but if we cannot create - // the packet, then we handle it as a fatal error as that indicates - // something else is likely very wrong. - if (!packet) { - SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); - Close(CloseMethod::SILENT); - return 0; - } - pos = packet->data(); - } - - ssize_t nwrite = ngtcp2_conn_writev_datagram(*this, - &path.path, - nullptr, - pos, - packet->length(), - &accepted, - flags, - did, - &vec, - 1, - uv_hrtime()); - - if (nwrite <= 0) { - // Nothing was written to the packet. - switch (nwrite) { - case 0: { - // We cannot send data because of congestion control or the data will - // not fit. Since datagrams are best effort, we are going to abandon - // the attempt and just return. - DCHECK_EQ(accepted, 0); - return 0; - } - case NGTCP2_ERR_WRITE_MORE: { - // The library wants us to keep writing more data to the packet. - // This is typically an indication that the packet is not yet - // full enough. - continue; - } - case NGTCP2_ERR_INVALID_STATE: { - // The remote endpoint does not want to accept datagrams. That's ok, - // just return 0. - DCHECK_EQ(accepted, 0); - return 0; - } - case NGTCP2_ERR_INVALID_ARGUMENT: { - // The datagram is too large. That should have been caught above but - // that's ok. We'll just abandon the attempt and return. - DCHECK_EQ(accepted, 0); - return 0; - } - case NGTCP2_ERR_PKT_NUM_EXHAUSTED: { - // We've exhausted the packet number space. Sadly we have to treat it - // as a fatal condition (which we will do after the switch) - DCHECK_EQ(accepted, 0); - Debug(this, - "ngtcp2_conn_writev_datagram failed: Packet number " - "exhausted"); - break; - } - case NGTCP2_ERR_CALLBACK_FAILURE: { - // There was an internal failure. Sadly we have to treat it as a fatal - // condition. (which we will do after the switch) - Debug(this, - "ngtcp2_conn_writev_datagram failed: Callback " - "failure"); - break; - } - case NGTCP2_ERR_NOMEM: { - // Out of memory. Sadly we have to treat it as a fatal condition. - // (which we will do after the switch) - Debug(this, "ngtcp2_conn_writev_datagram failed: Out of memory"); - break; - } - default: { - // Some other unknown, and unexpected failure. - // We have to treat it as a fatal condition. - Debug(this, - "ngtcp2_conn_writev_datagram failed with an unexpected " - "error: %zd", - nwrite); - break; - } - } - SetLastError(QuicError::ForTransport(nwrite)); - Close(CloseMethod::SILENT); - return 0; - } - - // In this case, a complete packet was written and we need to send it along. - // Note that this doesn't mean that the packet actually contains the - // datagram! We'll check that next by checking the accepted value. - packet->Truncate(nwrite); - Send(std::move(packet)); - // packet is now empty; next loop iteration creates a new one. - - if (accepted) { - // Yay! The datagram was accepted into the packet we just sent and we can - // return the datagram ID. Note that per the spec, datagrams cannot be - // fragmented, so if it was accepted, the entire datagram was sent. - Debug(this, "Datagram %" PRIu64 " sent", did); - auto& stats_ = impl_->stats_; - STAT_INCREMENT(Stats, datagrams_sent); - STAT_INCREMENT_N(Stats, bytes_sent, vec.len); - impl_->state_->last_datagram_id = did; - return did; - } + // Assign the datagram ID. + datagram_id did = ++impl_->state()->last_datagram_id; - // We sent a packet, but it wasn't the datagram packet. That can happen. - // Let's loop around and try again. We will limit the number of retries - // we do here to avoid looping indefinitely. - if (++attempts == kMaxAttempts) [[unlikely]] { - Debug(this, "Too many attempts to send datagram. Canceling."); - // Too many attempts to send the datagram. - break; + // Check queue capacity. Apply the drop policy when full. + auto max_pending = impl_->state()->max_pending_datagrams; + if (max_pending > 0 && impl_->pending_datagrams_.size() >= max_pending) { + auto drop_policy = impl_->config_.options.datagram_drop_policy; + if (drop_policy == DatagramDropPolicy::DROP_OLDEST) { + auto& oldest = impl_->pending_datagrams_.front(); + Debug(this, + "Datagram queue full, dropping oldest datagram %" PRIu64, + oldest.id); + DatagramStatus(oldest.id, DatagramStatus::ABANDONED); + impl_->pending_datagrams_.pop_front(); + } else { + // DROP_NEWEST: reject the incoming datagram. + Debug( + this, "Datagram queue full, dropping newest datagram %" PRIu64, did); + DatagramStatus(did, DatagramStatus::ABANDONED); + return did; } - - // If we get here that means the datagram has not yet been sent. - // We're going to loop around to try again. } - return 0; + // Queue the datagram. It will be serialized into packets by + // SendPendingData alongside stream data. + Debug(this, "Queuing %zu-byte datagram %" PRIu64, data.length(), did); + impl_->pending_datagrams_.push_back({did, std::move(data)}); + + return did; } void Session::UpdatePacketTxTime() { @@ -2016,6 +2626,11 @@ BaseObjectPtr Session::FindStream(stream_id id) const { return it->second; } +Session::StreamsMap Session::streams() const { + if (is_destroyed()) return {}; + return impl_->streams_; +} + BaseObjectPtr Session::CreateStream( stream_id id, CreateStreamOption option, @@ -2114,13 +2729,23 @@ void Session::AddStream(BaseObjectPtr stream, ngtcp2_conn_set_stream_user_data(*this, id, stream.get()); + // If the stream already has outbound data (body was provided at creation + // time), resume it now that it is registered in the streams map and can + // be found by FindStream. + if (stream->has_outbound()) { + ResumeStream(id); + } + if (option == CreateStreamOption::NOTIFY) { EmitStream(stream); + // EmitStream triggers the JS onstream callback via MakeCallback. + // If the callback throws, safeCallbackInvoke calls session.destroy() + // which resets impl_. We must bail out if that happened. + if (is_destroyed()) return; } // Update tracking statistics for the number of streams associated with this // session. - auto& stats_ = impl_->stats_; if (ngtcp2_conn_is_local_stream(*this, id)) { switch (direction) { case Direction::BIDIRECTIONAL: { @@ -2172,7 +2797,7 @@ void Session::RemoveStream(stream_id id) { // then we can proceed to finishing the close now. Note that the // expectation is that the session will be destroyed once FinishClose // returns. - if (impl_->state_->closing && impl_->state_->graceful_close) { + if (impl_->state()->closing && impl_->state()->graceful_close) { FinishClose(); CHECK(is_destroyed()); } @@ -2188,29 +2813,40 @@ void Session::ShutdownStream(stream_id id, QuicError error) { DCHECK(!is_destroyed()); Debug(this, "Shutting down stream %" PRIi64 " with error %s", id, error); SendPendingDataScope send_scope(this); - ngtcp2_conn_shutdown_stream(*this, - 0, - id, - error.type() == QuicError::Type::APPLICATION - ? error.code() - : application().GetNoErrorCode()); + // STOP_SENDING and RESET_STREAM frames carry application-level error + // codes (RFC 9000 §19.4, §19.5). Map the QuicError to an appropriate + // application code: APPLICATION errors pass through directly; transport + // no-error maps to the application's no-error code; any other error + // maps to the application's internal error code. + error_code code; + if (error.type() == QuicError::Type::APPLICATION) { + code = error.code(); + } else if (error.code() == NGTCP2_NO_ERROR) { + code = application().GetNoErrorCode(); + } else { + code = application().GetInternalErrorCode(); + } + ngtcp2_conn_shutdown_stream(*this, 0, id, code); } -void Session::ShutdownStreamWrite(stream_id id, QuicError code) { +void Session::ShutdownStreamWrite(stream_id id, QuicError error) { DCHECK(!is_destroyed()); - Debug(this, "Shutting down stream %" PRIi64 " write with error %s", id, code); + Debug( + this, "Shutting down stream %" PRIi64 " write with error %s", id, error); SendPendingDataScope send_scope(this); - ngtcp2_conn_shutdown_stream_write(*this, - 0, - id, - code.type() == QuicError::Type::APPLICATION - ? code.code() - : application().GetNoErrorCode()); + error_code code; + if (error.type() == QuicError::Type::APPLICATION) { + code = error.code(); + } else if (error.code() == NGTCP2_NO_ERROR) { + code = application().GetNoErrorCode(); + } else { + code = application().GetInternalErrorCode(); + } + ngtcp2_conn_shutdown_stream_write(*this, 0, id, code); } void Session::StreamDataBlocked(stream_id id) { DCHECK(!is_destroyed()); - auto& stats_ = impl_->stats_; STAT_INCREMENT(Stats, block_count); application().BlockStream(id); } @@ -2224,7 +2860,42 @@ void Session::CollectSessionTicketAppData( SessionTicket::AppData::Status Session::ExtractSessionTicketAppData( const SessionTicket::AppData& app_data, Flag flag) { DCHECK(!is_destroyed()); - return application().ExtractSessionTicketAppData(app_data, flag); + // If the application is already selected (client side, or server after + // ALPN), delegate directly. + if (impl_->application_) { + return application().ExtractSessionTicketAppData(app_data, flag); + } + // The application is not yet selected (server during ClientHello + // processing, before ALPN). Parse the ticket data now while the + // SSL_SESSION is still valid, and stash the result for validation + // after ALPN negotiation in SetApplication(). + auto data = app_data.Get(); + if (!data.has_value() || data->len == 0) { + // No app data in the ticket. Accept optimistically. + return flag == Flag::STATUS_RENEW + ? SessionTicket::AppData::Status::TICKET_USE_RENEW + : SessionTicket::AppData::Status::TICKET_USE; + } + auto parsed = Application::ParseTicketData(*data); + if (!parsed.has_value()) { + return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW; + } + // Pre-validate the ticket data against the current application options. + // If the stored settings are more permissive than the current config + // (e.g., a feature was enabled when the ticket was issued but is now + // disabled), reject the ticket so 0-RTT is not used. This must happen + // here (during TLS ticket processing) rather than in SetApplication, + // because by SetApplication time the TLS layer has already accepted + // the ticket and told the client 0-RTT is ok. + if (!Application::ValidateTicketData(*parsed, + config().options.application_options)) { + Debug(this, "Session ticket app data incompatible with current settings"); + return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW; + } + impl_->pending_ticket_data_ = std::move(parsed); + return flag == Flag::STATUS_RENEW + ? SessionTicket::AppData::Status::TICKET_USE_RENEW + : SessionTicket::AppData::Status::TICKET_USE; } void Session::MemoryInfo(MemoryTracker* tracker) const { @@ -2232,12 +2903,6 @@ void Session::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("impl", impl_); } tracker->TrackField("tls_session", tls_session_); - if (qlog_stream_) { - tracker->TrackField("qlog_stream", qlog_stream_); - } - if (keylog_stream_) { - tracker->TrackField("keylog_stream", keylog_stream_); - } } bool Session::is_in_closing_period() const { @@ -2251,19 +2916,32 @@ bool Session::is_in_draining_period() const { } bool Session::wants_session_ticket() const { - return !is_destroyed() && impl_->state_->session_ticket == 1; + return !is_destroyed() && + HasListenerFlag(impl_->state()->listener_flags, + SessionListenerFlags::SESSION_TICKET); } void Session::SetStreamOpenAllowed() { DCHECK(!is_destroyed()); - impl_->state_->stream_open_allowed = 1; + impl_->state()->stream_open_allowed = 1; +} + +void Session::PopulateEarlyTransportParamsState() { + DCHECK(!is_destroyed()); + const ngtcp2_transport_params* tp = remote_transport_params(); + if (tp != nullptr) { + impl_->state()->max_datagram_size = + MaxDatagramPayload(tp->max_datagram_frame_size); + } } bool Session::can_send_packets() const { - // We can send packets if we're not in the middle of a ngtcp2 callback, - // we're not destroyed, we're not in a draining or closing period, and - // endpoint is set. - return !is_destroyed() && !NgTcp2CallbackScope::in_ngtcp2_callback(env()) && + // We can send packets if we're not in the middle of a ngtcp2 callback + // on THIS session, we're not destroyed, and we're not in a draining + // or closing period. The callback scope check is per-session so that + // one session's ngtcp2 callback does not block unrelated sessions + // from sending. + return !is_destroyed() && !flags_.in_ngtcp2_callback_scope && !is_in_draining_period() && !is_in_closing_period(); } @@ -2273,7 +2951,7 @@ bool Session::can_create_streams() const { } bool Session::can_open_streams() const { - return !is_destroyed() && impl_->state_->stream_open_allowed; + return !is_destroyed() && impl_->state()->stream_open_allowed; } uint64_t Session::max_data_left() const { @@ -2294,12 +2972,12 @@ uint64_t Session::max_local_streams_bidi() const { void Session::set_wrapped() { DCHECK(!is_destroyed()); - impl_->state_->wrapped = 1; + impl_->state()->wrapped = 1; } void Session::set_priority_supported(bool on) { DCHECK(!is_destroyed()); - impl_->state_->priority_supported = on ? 1 : 0; + impl_->state()->priority_supported = on ? 1 : 0; } void Session::ExtendStreamOffset(stream_id id, size_t amount) { @@ -2314,9 +2992,31 @@ void Session::ExtendOffset(size_t amount) { ngtcp2_conn_extend_max_offset(*this, amount); } +bool Session::HasPendingDatagrams() const { + return impl_ && !impl_->pending_datagrams_.empty(); +} + +Session::PendingDatagram& Session::PeekPendingDatagram() { + return impl_->pending_datagrams_.front(); +} + +void Session::PopPendingDatagram() { + impl_->pending_datagrams_.pop_front(); +} + +size_t Session::PendingDatagramCount() const { + return impl_ ? impl_->pending_datagrams_.size() : 0; +} + +void Session::DatagramSent(datagram_id id) { + Debug(this, "Datagram %" PRIu64 " sent", id); + STAT_INCREMENT(Stats, datagrams_sent); +} + void Session::UpdateDataStats() { + if (is_destroyed()) return; Debug(this, "Updating data stats"); - auto& stats_ = impl_->stats_; + ngtcp2_conn_info info; ngtcp2_conn_get_conn_info(*this, &info); STAT_SET(Stats, bytes_in_flight, info.bytes_in_flight); @@ -2326,16 +3026,61 @@ void Session::UpdateDataStats() { STAT_SET(Stats, rttvar, info.rttvar); STAT_SET(Stats, smoothed_rtt, info.smoothed_rtt); STAT_SET(Stats, ssthresh, info.ssthresh); + STAT_SET(Stats, pkt_sent, info.pkt_sent); + STAT_SET(Stats, bytes_sent, info.bytes_sent); + STAT_SET(Stats, pkt_recv, info.pkt_recv); + STAT_SET(Stats, bytes_recv, info.bytes_recv); + STAT_SET(Stats, pkt_lost, info.pkt_lost); + STAT_SET(Stats, bytes_lost, info.bytes_lost); + STAT_SET(Stats, ping_recv, info.ping_recv); + STAT_SET(Stats, pkt_discarded, info.pkt_discarded); + STAT_SET( Stats, max_bytes_in_flight, std::max(STAT_GET(Stats, max_bytes_in_flight), info.bytes_in_flight)); } +void Session::CheckStreamIdleTimeout(uint64_t now) { + if (is_destroyed()) return; + uint64_t timeout = options().stream_idle_timeout; + if (timeout == 0) return; + + uint64_t timeout_ns = timeout * NGTCP2_MILLISECONDS; + auto all_streams = streams(); + + for (const auto& [id, stream] : all_streams) { + if (!stream) continue; + + // Only check peer-initiated streams. Locally-initiated streams + // that haven't been written to are the application's concern. + if (ngtcp2_conn_is_local_stream(*this, id)) continue; + + uint64_t last_activity = stream->last_activity_timestamp(); + if (last_activity > 0 && (now - last_activity) > timeout_ns) { + Debug(this, "Stream %" PRId64 " idle timeout exceeded, destroying", id); + // Notify the peer before destroying. ShutdownStream sends both + // STOP_SENDING and RESET_STREAM as appropriate, using the + // application's no-error code for non-APPLICATION errors (since + // these frames carry application-level error codes per RFC 9000). + // Without this, the peer's stream sits orphaned until the + // session closes. + auto error = + QuicError::ForNgtcp2Error(NGTCP2_ERR_PROTO, "stream idle timeout"); + ShutdownStream(id, error); + stream->Destroy(error); + STAT_INCREMENT(Stats, streams_idle_timed_out); + } + } +} + void Session::SendConnectionClose() { - // Method is a non-op if the session is in a state where packets cannot - // be transmitted to the remote peer. - if (!can_send_packets()) return; + // Method is a non-op if the session is already destroyed or the + // endpoint cannot send. Note: we intentionally do NOT check + // can_send_packets() here because ngtcp2_conn_write_connection_close + // puts the connection into the closing period, and the resulting packet + // must still be sent to the endpoint. + if (is_destroyed()) return; Debug(this, "Sending connection close packet to peer"); @@ -2349,7 +3094,9 @@ void Session::SendConnectionClose() { if (auto packet = Packet::CreateConnectionClosePacket( endpoint(), impl_->remote_address_, *this, impl_->last_error_)) [[likely]] { - return Send(std::move(packet)); + // Send directly to endpoint, bypassing Session::Send which + // would drop the packet because we're now in the closing period. + return endpoint().Send(std::move(packet)); } // If we are unable to create a connection close packet then @@ -2383,27 +3130,71 @@ void Session::SendConnectionClose() { } packet->Truncate(nwrite); - return Send(std::move(packet)); + // Send directly to endpoint — ngtcp2 has entered the closing period + // at this point, so Session::Send() would drop the packet. + return endpoint().Send(std::move(packet)); } void Session::OnTimeout() { - DCHECK(!is_destroyed()); + if (is_destroyed()) return; + if (!impl_->application_) return; + // Hold a strong reference to prevent the Session from being freed during + // re-entrant calls. SendPendingData's scope guard calls UpdateTimer(), + // which can synchronously re-enter OnTimeout() when the timer has already + // expired. That re-entrant path can reach FinishClose → EmitClose → + // Destroy → impl_.reset() → ~Impl → RemoveSession(), dropping the last + // BaseObjectPtr from the endpoint map and freeing the Session. Without + // this guard, the outer OnTimeout / SendPendingData frames would operate + // on a freed object. + BaseObjectPtr ref(this); HandleScope scope(env()->isolate()); - int ret = ngtcp2_conn_handle_expiry(*this, uv_hrtime()); + int ret; + { + NgTcp2CallbackScope callback_scope(this); + ret = ngtcp2_conn_handle_expiry(*this, uv_hrtime()); + } + // handle_expiry can trigger ngtcp2 callbacks that invoke MakeCallback, + // which can synchronously destroy the session. Guard before proceeding. + if (is_destroyed()) return; if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period()) { - return application().SendPendingData(); + application().SendPendingData(); + if (is_destroyed()) return; + CheckStreamIdleTimeout(uv_hrtime()); + return; } + if (is_destroyed()) return; Debug(this, "Session timed out"); - SetLastError(QuicError::ForNgtcp2Error(ret)); + + // When the draining period expires, the peer has already sent + // CONNECTION_CLOSE. Use their close error so a clean close (code 0) + // propagates as no-error, allowing stream.closed promises to resolve. + if (is_in_draining_period()) { + SetLastError(QuicError::FromConnectionClose(*this)); + } else { + SetLastError(QuicError::ForNgtcp2Error(ret)); + } Close(CloseMethod::SILENT); } void Session::UpdateTimer() { DCHECK(!is_destroyed()); // Both uv_hrtime and ngtcp2_conn_get_expiry return nanosecond units. - uint64_t expiry = ngtcp2_conn_get_expiry(*this); uint64_t now = uv_hrtime(); + uint64_t expiry; + + if (is_in_draining_period()) { + // RFC 9000 Section 10.2: The draining state SHOULD persist for at + // least three times the current Probe Timeout (PTO). ngtcp2 does + // not set a draining timer internally — the application must + // compute it. + ngtcp2_duration pto = ngtcp2_conn_get_pto(*this); + uint8_t multiplier = impl_->config_.options.draining_period_multiplier; + expiry = now + multiplier * pto; + } else { + expiry = ngtcp2_conn_get_expiry(*this); + } + Debug( this, "Updating timer. Expiry: %" PRIu64 ", now: %" PRIu64, expiry, now); @@ -2415,6 +3206,15 @@ void Session::UpdateTimer() { auto timeout = (expiry - now) / NGTCP2_MILLISECONDS; Debug(this, "Updating timeout to %zu milliseconds", timeout); + // If a stream idle timeout is configured, ensure the timer fires at + // least that often so CheckStreamIdleTimeout runs. Without this, an + // idle session with idle streams might not fire the timer until the + // connection idle timeout, which could be much longer. + uint64_t stream_idle = options().stream_idle_timeout; + if (stream_idle > 0 && timeout > stream_idle) { + timeout = stream_idle; + } + // If timeout is zero here, it means our timer is less than a millisecond // off from expiry. Let's bump the timer to 1. impl_->timer_.Update(timeout == 0 ? 1 : timeout); @@ -2423,7 +3223,7 @@ void Session::UpdateTimer() { void Session::DatagramStatus(datagram_id datagramId, quic::DatagramStatus status) { DCHECK(!is_destroyed()); - auto& stats_ = impl_->stats_; + switch (status) { case DatagramStatus::ACKNOWLEDGED: { Debug(this, "Datagram %" PRIu64 " was acknowledged", datagramId); @@ -2435,8 +3235,16 @@ void Session::DatagramStatus(datagram_id datagramId, STAT_INCREMENT(Stats, datagrams_lost); break; } + case DatagramStatus::ABANDONED: { + Debug(this, "Datagram %" PRIu64 " was abandoned", datagramId); + STAT_INCREMENT(Stats, datagrams_lost); + break; + } + } + if (HasListenerFlag(impl_->state()->listener_flags, + SessionListenerFlags::DATAGRAM_STATUS)) { + EmitDatagramStatus(datagramId, status); } - EmitDatagramStatus(datagramId, status); } void Session::DatagramReceived(const uint8_t* data, @@ -2445,10 +3253,13 @@ void Session::DatagramReceived(const uint8_t* data, DCHECK(!is_destroyed()); // If there is nothing watching for the datagram on the JavaScript side, // or if the datagram is zero-length, we just drop it on the floor. - if (impl_->state_->datagram == 0 || datalen == 0) return; + if (!HasListenerFlag(impl_->state()->listener_flags, + SessionListenerFlags::DATAGRAM) || + datalen == 0) + return; Debug(this, "Session is receiving datagram of size %zu", datalen); - auto& stats_ = impl_->stats_; + STAT_INCREMENT(Stats, datagrams_received); JS_TRY_ALLOCATE_BACKING(env(), backing, datalen) memcpy(backing->Data(), data, datalen); @@ -2457,7 +3268,7 @@ void Session::DatagramReceived(const uint8_t* data, void Session::GenerateNewConnectionId(ngtcp2_cid* cid, size_t len, - uint8_t* token) { + ngtcp2_stateless_reset_token* token) { DCHECK(!is_destroyed()); CID cid_ = impl_->config_.options.cid_factory->GenerateInto(cid, len); Debug(this, "Generated new connection id %s", cid_); @@ -2469,14 +3280,20 @@ void Session::GenerateNewConnectionId(ngtcp2_cid* cid, bool Session::HandshakeCompleted() { DCHECK(!is_destroyed()); - DCHECK(!impl_->state_->handshake_completed); + DCHECK(!impl_->state()->handshake_completed); Debug(this, "Session handshake completed"); - impl_->state_->handshake_completed = 1; - auto& stats_ = impl_->stats_; + impl_->state()->handshake_completed = 1; + STAT_RECORD_TIMESTAMP(Stats, handshake_completed_at); SetStreamOpenAllowed(); + // Capture the peer's max datagram frame size from the remote transport + // parameters so JavaScript can check it without a C++ round-trip. + const ngtcp2_transport_params* tp = remote_transport_params(); + impl_->state()->max_datagram_size = + MaxDatagramPayload(tp->max_datagram_frame_size); + // If early data was attempted but rejected by the server, // tell ngtcp2 so it can retransmit the data as 1-RTT. // The status of early data will only be rejected if an @@ -2509,10 +3326,10 @@ bool Session::HandshakeCompleted() { void Session::HandshakeConfirmed() { DCHECK(!is_destroyed()); - DCHECK(!impl_->state_->handshake_confirmed); + DCHECK(!impl_->state()->handshake_confirmed); Debug(this, "Session handshake confirmed"); - impl_->state_->handshake_confirmed = 1; - auto& stats_ = impl_->stats_; + impl_->state()->handshake_confirmed = 1; + STAT_RECORD_TIMESTAMP(Stats, handshake_confirmed_at); } @@ -2635,12 +3452,21 @@ void Session::EmitClose(const QuicError& error) { Integer::New(env()->isolate(), static_cast(error.type())), BigInt::NewFromUnsigned(env()->isolate(), error.code()), Undefined(env()->isolate()), + Undefined(env()->isolate()), }; if (error.reason().length() > 0 && !ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) { return; } + // Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1 + // names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown + // codes leave the slot as undefined. See QuicError::name() for the + // matching path on stream-level errors. + if (const char* n = error.name()) { + argv[3] = BindingData::Get(env()).error_name_string(n); + } + MakeCallback( BindingData::Get(env()).session_close_callback(), arraysize(argv), argv); @@ -2648,6 +3474,26 @@ void Session::EmitClose(const QuicError& error) { CHECK(is_destroyed()); } +void Session::set_max_datagram_size(uint16_t size) { + if (!is_destroyed()) { + impl_->state()->max_datagram_size = size; + } +} + +void Session::EmitGoaway(stream_id last_stream_id) { + if (is_destroyed()) return; + if (!env()->can_call_into_js()) return; + + CallbackScope cb_scope(this); + + Local argv[] = { + BigInt::New(env()->isolate(), last_stream_id), + }; + + MakeCallback( + BindingData::Get(env()).session_goaway_callback(), arraysize(argv), argv); +} + void Session::EmitDatagram(Store&& datagram, DatagramReceivedFlags flag) { DCHECK(!is_destroyed()); if (!env()->can_call_into_js()) return; @@ -2677,6 +3523,8 @@ void Session::EmitDatagramStatus(datagram_id id, quic::DatagramStatus status) { return state.acknowledged_string(); case DatagramStatus::LOST: return state.lost_string(); + case DatagramStatus::ABANDONED: + return state.abandoned_string(); } UNREACHABLE(); })(); @@ -2743,7 +3591,8 @@ void Session::EmitPathValidation(PathValidationResult result, if (!env()->can_call_into_js()) return; - if (impl_->state_->path_validation == 0) [[likely]] { + if (!HasListenerFlag(impl_->state()->listener_flags, + SessionListenerFlags::PATH_VALIDATION)) [[likely]] { return; } @@ -2769,7 +3618,8 @@ void Session::EmitPathValidation(PathValidationResult result, SocketAddressBase::Create(env(), newPath.remote)->object(), Undefined(isolate), Undefined(isolate), - Boolean::New(isolate, flags.preferredAddress)}; + is_server() ? Undefined(isolate) + : Boolean::New(isolate, flags.preferredAddress)}; if (oldPath.has_value()) { argv[3] = SocketAddressBase::Create(env(), oldPath->local)->object(); @@ -2787,17 +3637,28 @@ void Session::EmitSessionTicket(Store&& ticket) { // If there is nothing listening for the session ticket, don't bother // emitting. - if (impl_->state_->session_ticket == 0) [[likely]] { + if (!HasListenerFlag(impl_->state()->listener_flags, + SessionListenerFlags::SESSION_TICKET)) [[likely]] { Debug(this, "Session ticket was discarded"); return; } CallbackScope cb_scope(this); - auto& remote_params = remote_transport_params(); - Store transport_params; - if (remote_params) { - if (auto transport_params = remote_params.Encode(env())) { + // Encode the 0-RTT transport params using ngtcp2's matched pair format. + // This must use ngtcp2_conn_encode_0rtt_transport_params (not the + // generic ngtcp2_transport_params_encode_versioned) so that the + // receiver can decode with ngtcp2_conn_decode_and_set_0rtt_transport_params. + ssize_t tp_size = ngtcp2_conn_encode_0rtt_transport_params(*this, nullptr, 0); + if (tp_size > 0) { + JS_TRY_ALLOCATE_BACKING(env(), tp_backing, static_cast(tp_size)) + ssize_t tp_written = ngtcp2_conn_encode_0rtt_transport_params( + *this, + static_cast(tp_backing->Data()), + static_cast(tp_size)); + if (tp_written > 0) { + Store transport_params(std::move(tp_backing), + static_cast(tp_written)); SessionTicket session_ticket(std::move(ticket), std::move(transport_params)); Local argv; @@ -2809,8 +3670,60 @@ void Session::EmitSessionTicket(Store&& ticket) { } } +void Session::EmitApplication() { + if (is_destroyed()) return; + if (!env()->can_call_into_js()) return; + + if (!has_application()) { + // The application has not yet been selected (ALPN negotiation is not + // yet complete on the server) or the session has been destroyed. In + // either case, the application options are not available. + // Should not happen, but we bail out + return; + } + + if (!HasListenerFlag(impl_->state()->listener_flags, + SessionListenerFlags::APPLICATION)) [[likely]] { + return; + } + + CallbackScope cb_scope(this); + + Local argv; + auto& options = application().options(); + if (options.ToObject(env()).ToLocal(&argv)) { + MakeCallback( + BindingData::Get(env()).session_application_callback(), 1, &argv); + } +} + +void Session::DestroyAllStreams(const QuicError& error) { + DCHECK(!is_destroyed()); + // Copy the streams map since streams remove themselves during + // destruction. Each Destroy() call triggers MakeCallback which + // can destroy impl_ via JS re-entrancy. + StreamsMap streams = impl_->streams_; + for (auto& stream : streams) { + if (is_destroyed()) return; + stream.second->Destroy(error); + } +} + +void Session::EmitEarlyDataRejected() { + DCHECK(!is_destroyed()); + if (!env()->can_call_into_js()) return; + + CallbackScope cb_scope(this); + MakeCallback(BindingData::Get(env()).session_early_data_rejected_callback(), + 0, + nullptr); +} + void Session::EmitNewToken(const uint8_t* token, size_t len) { DCHECK(!is_destroyed()); + if (!HasListenerFlag(impl_->state()->listener_flags, + SessionListenerFlags::NEW_TOKEN)) + return; if (!env()->can_call_into_js()) return; CallbackScope cb_scope(this); @@ -2882,13 +3795,44 @@ void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, argv); } +void Session::EmitOrigins(std::vector&& origins) { + DCHECK(!is_destroyed()); + if (!HasListenerFlag(impl_->state()->listener_flags, + SessionListenerFlags::ORIGIN)) + return; + if (!env()->can_call_into_js()) return; + + CallbackScope cb_scope(this); + + auto isolate = env()->isolate(); + + LocalVector elements(env()->isolate(), origins.size()); + for (size_t i = 0; i < origins.size(); i++) { + Local str; + if (!ToV8Value(env()->context(), origins[i]).ToLocal(&str)) [[unlikely]] { + return; + } + elements[i] = str; + } + + Local argv[] = {Array::New(isolate, elements.data(), elements.size())}; + MakeCallback( + BindingData::Get(env()).session_origin_callback(), arraysize(argv), argv); +} + void Session::EmitKeylog(const char* line) { + DCHECK(!is_destroyed()); if (!env()->can_call_into_js()) return; - if (keylog_stream_) { - Debug(this, "Emitting keylog line"); - env()->SetImmediate([ptr = keylog_stream_, data = std::string(line) + "\n"]( - Environment* env) { ptr->Emit(data); }); + + auto str = std::string(line); + Local argv[] = {Undefined(env()->isolate())}; + if (!ToV8Value(env()->context(), str).ToLocal(&argv[0])) { + Debug(this, "Failed to convert keylog line to V8 string"); + return; } + + MakeCallback( + BindingData::Get(env()).session_keylog_callback(), arraysize(argv), argv); } // ============================================================================ @@ -2943,6 +3887,10 @@ void Session::InitPerContext(Realm* realm, Local target) { NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MAX); NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MIN); + static constexpr auto DEFAULT_HANDSHAKE_TIMEOUT = + Session::Options::DEFAULT_HANDSHAKE_TIMEOUT; + NODE_DEFINE_CONSTANT(target, DEFAULT_HANDSHAKE_TIMEOUT); + NODE_DEFINE_STRING_CONSTANT( target, "DEFAULT_CIPHERS", TLSContext::DEFAULT_CIPHERS); NODE_DEFINE_STRING_CONSTANT( @@ -2957,6 +3905,8 @@ void Session::InitPerContext(Realm* realm, Local target) { SESSION_STATE(V) #undef V + NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_COUNT); + #define V(name, _) NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_##name); SESSION_STATS(V) NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_COUNT); diff --git a/src/quic/session.h b/src/quic/session.h index 92055e856fac60..0caeb764ba56c8 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -16,7 +16,6 @@ #include "cid.h" #include "data.h" #include "defs.h" -#include "logstream.h" #include "packet.h" #include "preferredaddress.h" #include "sessionticket.h" @@ -73,10 +72,14 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { uint64_t max_header_length = DEFAULT_MAX_HEADER_LENGTH; // HTTP/3 specific options. - uint64_t max_field_section_size = 0; - uint64_t qpack_max_dtable_capacity = 0; - uint64_t qpack_encoder_max_dtable_capacity = 0; - uint64_t qpack_blocked_streams = 0; + // The maximum header section size advertised to the peer in SETTINGS. + // Defaults to match max_header_length so the SETTINGS frame accurately + // reflects the enforcement limit. A value of 0 would incorrectly tell + // the peer not to send any headers at all. + uint64_t max_field_section_size = DEFAULT_MAX_HEADER_LENGTH; + uint64_t qpack_max_dtable_capacity = 4096; + uint64_t qpack_encoder_max_dtable_capacity = 4096; + uint64_t qpack_blocked_streams = 100; bool enable_connect_protocol = true; bool enable_datagrams = true; @@ -92,6 +95,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { std::string ToString() const; + v8::MaybeLocal ToObject(Environment* env) const; + static const Application_Options kDefault; }; @@ -112,6 +117,12 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // (ALPN negotiated during handshake). Must be called before any // application data is received. void SetApplication(std::unique_ptr app); + // Controls which datagram to drop when the pending datagram queue is full. + enum class DatagramDropPolicy : uint8_t { + DROP_OLDEST = 0, // Drop the oldest queued datagram (default). + DROP_NEWEST = 1, // Drop the incoming datagram. + }; + // The options used to configure a session. Most of these deal directly with // the transport parameters that are exchanged with the remote peer during // handshake. @@ -148,8 +159,29 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool qlog = false; // The amount of time (in milliseconds) that the endpoint will wait for the - // completion of the tls handshake. - uint64_t handshake_timeout = UINT64_MAX; + // completion of the TLS handshake. If the handshake does not complete + // within this time, the session is closed. This prevents a peer from + // holding a session open indefinitely in the handshake state, consuming + // server resources (ngtcp2 connection, TLS state, JS objects) without + // ever completing the connection. The default of 10 seconds is generous + // enough to accommodate slow networks with retransmissions while still + // bounding resource exposure. Set to UINT64_MAX to disable. + static constexpr uint64_t DEFAULT_HANDSHAKE_TIMEOUT = 10'000; + uint64_t handshake_timeout = DEFAULT_HANDSHAKE_TIMEOUT; + + // The initial round-trip time estimate in milliseconds. ngtcp2 uses this + // for PTO computation, initial pacing, and early loss detection before + // the first RTT sample is collected. The default of 0 uses ngtcp2's + // built-in default of 333ms, which is appropriate for the general + // internet. For low-latency environments (e.g., loopback or same-rack + // deployments), setting a value closer to the actual RTT avoids + // unnecessarily conservative initial behavior. + uint64_t initial_rtt = 0; + + // The keep-alive timeout in milliseconds. When set to a non-zero value, + // ngtcp2 will automatically send PING frames to keep the connection alive + // before the idle timeout fires. Set to 0 to disable (default). + uint64_t keep_alive_timeout = 0; // Maximum initial flow control window size for a stream. uint64_t max_stream_window = 0; @@ -180,6 +212,29 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // is the better of the two for our needs. ngtcp2_cc_algo cc_algorithm = CC_ALGO_CUBIC; + // Controls which datagram to drop when the pending queue is full. + DatagramDropPolicy datagram_drop_policy = DatagramDropPolicy::DROP_OLDEST; + + // Maximum number of SendPendingData attempts before a datagram is + // abandoned. When a datagram cannot be sent due to congestion control + // or packet size constraints, it remains in the queue and the counter + // is incremented. Once the limit is reached, the datagram is dropped + // and reported as abandoned. Range: 1-255. Default: 5. + uint8_t max_datagram_send_attempts = 5; + + // Multiplier for the Probe Timeout (PTO) used to compute the draining + // period duration after receiving CONNECTION_CLOSE. RFC 9000 Section + // 10.2 requires at least 3x PTO. Range: 3-255. Default: 3. + uint8_t draining_period_multiplier = 3; + + // The amount of time (in milliseconds) that a stream can be idle + // (no data received) before it is automatically destroyed. This + // protects against slowloris-style attacks where a peer opens streams + // but never sends data, holding server resources indefinitely. + // Only applies to peer-initiated streams. Set to 0 to disable. + static constexpr uint64_t DEFAULT_STREAM_IDLE_TIMEOUT = 30'000; + uint64_t stream_idle_timeout = DEFAULT_STREAM_IDLE_TIMEOUT; + // An optional NEW_TOKEN from a previous connection to the same // server. When set, the token is included in the Initial packet // to skip address validation. Client-side only. @@ -283,6 +338,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { uint32_t version() const; Endpoint& endpoint() const; TLSSession& tls_session() const; + bool has_application() const; Application& application() const; const Config& config() const; const Options& options() const; @@ -312,6 +368,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { struct Stats; void HandleQlog(uint32_t flags, const void* data, size_t len); + void EmitQlog(uint32_t flags, std::string_view data); private: struct Impl; @@ -327,14 +384,62 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool early = false; }; - bool Receive(Store&& store, + bool Receive(const uint8_t* data, + size_t len, const SocketAddress& local_address, - const SocketAddress& remote_address); + const SocketAddress& remote_address, + const PacketInfo& pkt_info = PacketInfo(), + uint64_t ts = 0); + + // ReadPacket processes a single inbound packet through ngtcp2 without + // triggering SendPendingData. This is the building block for batched + // receive processing: the caller (Endpoint::Receive) accumulates + // dirty sessions and a uv_check callback flushes them after all + // packets in the I/O burst have been read. + // Receive() is kept as a convenience wrapper that calls ReadPacket() + // then triggers SendPendingData (for paths like Connect that need + // immediate response). + // The data pointer is used synchronously — ngtcp2_conn_read_pkt does + // not retain a reference after returning, so the caller's buffer can + // be reused immediately. + // When ts is 0 (the default), uv_hrtime() is called internally. + // The batched receive path caches a timestamp and passes it to all + // ReadPacket() calls in the same I/O burst. + bool ReadPacket(const uint8_t* data, + size_t len, + const SocketAddress& local_address, + const SocketAddress& remote_address, + const PacketInfo& pkt_info = PacketInfo(), + uint64_t ts = 0); + + // Called by BindingData's flush callback to trigger SendPendingData + // on this session. Encapsulates the application() access so that + // bindingdata.cc doesn't need the full Application type definition. + void FlushPendingData(); + + // Send a batch of packets accumulated by SendPendingData. Uses + // Endpoint::SendBatch (uv_udp_try_send2 / sendmmsg) for synchronous + // batched delivery when called from the deferred flush path. + // Handles per-packet path updates and cross-endpoint redirects. + // All Ptr entries are consumed (released or moved) on return. + void SendBatch(Packet::Ptr* packets, PathStorage* paths, size_t count); void Send(Packet::Ptr packet); void Send(Packet::Ptr packet, const PathStorage& path); datagram_id SendDatagram(Store&& data); + // Pending datagram accessors for use by SendPendingData. + struct PendingDatagram { + datagram_id id; + Store data; + uint8_t send_attempts = 0; + }; + bool HasPendingDatagrams() const; + PendingDatagram& PeekPendingDatagram(); + void PopPendingDatagram(); + size_t PendingDatagramCount() const; + void DatagramSent(datagram_id id); + // A non-const variation to allow certain modifications. Config& config(); @@ -343,6 +448,9 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { DO_NOT_NOTIFY, }; BaseObjectPtr FindStream(stream_id id) const; + // Returns a copy of the streams map (safe for iteration while streams + // are being destroyed). + StreamsMap streams() const; BaseObjectPtr CreateStream( stream_id id, CreateStreamOption option = CreateStreamOption::NOTIFY, @@ -422,6 +530,12 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool wants_session_ticket() const; void SetStreamOpenAllowed(); + // Populate state buffer fields from the 0-RTT transport params. + // Called after ngtcp2_conn_decode_and_set_0rtt_transport_params + // succeeds, so that values like maxDatagramSize are available + // before the handshake completes. + void PopulateEarlyTransportParamsState(); + // It's a terrible name but "wrapped" here means that the Session has been // passed out to JavaScript and should be "wrapped" by whatever handler is // defined there to manage it. @@ -463,6 +577,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // Has to be called after certain operations that generate packets. void UpdatePacketTxTime(); void UpdateDataStats(); + void CheckStreamIdleTimeout(uint64_t now); void UpdatePath(const PathStorage& path); void ProcessPendingBidiStreams(); @@ -471,10 +586,17 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // JavaScript callouts void EmitClose(const QuicError& error = QuicError()); + void EmitGoaway(stream_id last_stream_id); + + // Sets the max datagram payload size in the shared state. Used by + // Http3ApplicationImpl to block datagram sends when the peer's + // SETTINGS_H3_DATAGRAM=0 (RFC 9297 §3). + void set_max_datagram_size(uint16_t size); void EmitDatagram(Store&& datagram, DatagramReceivedFlags flag); void EmitDatagramStatus(datagram_id id, DatagramStatus status); void EmitHandshakeComplete(); void EmitKeylog(const char* line); + void EmitOrigins(std::vector&& origins); struct ValidatedPath { std::shared_ptr local; @@ -487,15 +609,20 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { const std::optional& oldPath); void EmitSessionTicket(Store&& ticket); void EmitNewToken(const uint8_t* token, size_t len); + void EmitEarlyDataRejected(); + void DestroyAllStreams(const QuicError& error); void EmitStream(const BaseObjectWeakPtr& stream); void EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, const uint32_t* sv, size_t nsv); + void EmitApplication(); void DatagramStatus(datagram_id datagramId, DatagramStatus status); void DatagramReceived(const uint8_t* data, size_t datalen, DatagramReceivedFlags flag); - void GenerateNewConnectionId(ngtcp2_cid* cid, size_t len, uint8_t* token); + void GenerateNewConnectionId(ngtcp2_cid* cid, + size_t len, + ngtcp2_stateless_reset_token* token); bool HandshakeCompleted(); void HandshakeConfirmed(); void SelectPreferredAddress(PreferredAddress* preferredAddress); @@ -503,17 +630,42 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { QuicConnectionPointer InitConnection(); Side side_; - ngtcp2_mem allocator_; + const ngtcp2_mem* allocator_; std::unique_ptr impl_; + + struct Flags { + // These flags live on Session (not Impl) so that the NgTcp2CallbackScope + // and NgHttp3CallbackScope destructors can safely clear them even after + // Impl has been destroyed via MakeCallback re-entrancy during a callback. + // The scope is placed at the ngtcp2/nghttp3 entry point (e.g. Receive, + // OnTimeout) rather than on individual callbacks, so the deferred destroy + // only fires after all callbacks for that entry point have completed. + uint8_t in_ngtcp2_callback_scope : 1 = 0; + uint8_t in_nghttp3_callback_scope : 1 = 0; + uint8_t destroy_deferred : 1 = 0; + // Set when this session is in BindingData's pending_flush_sessions_ vector. + // Cleared by the flush callback before calling SendPendingData. + // Provides O(1) dedup so a session receiving multiple packets in one I/O + // burst is only scheduled for flush once. + uint8_t pending_flush : 1 = 0; + // When true, Session::Send prefers synchronous delivery via + // Endpoint::SendOrTrySend (uv_udp_try_send with async fallback). + // Set during FlushPendingData to avoid the one-tick latency of + // async-only sends from the uv_check callback. + uint8_t prefer_try_send : 1 = 0; + }; + Flags flags_; + QuicConnectionPointer connection_; std::unique_ptr tls_session_; - BaseObjectPtr qlog_stream_; - BaseObjectPtr keylog_stream_; - + friend struct NgTcp2CallbackScope; + friend struct NgHttp3CallbackScope; friend class Application; + friend class BindingData; friend class DefaultApplication; friend class Http3ApplicationImpl; friend class Endpoint; + friend class SessionManager; friend class Stream; friend class PendingStream; friend class TLSContext; diff --git a/src/quic/session_manager.cc b/src/quic/session_manager.cc new file mode 100644 index 00000000000000..20f92bb74bb944 --- /dev/null +++ b/src/quic/session_manager.cc @@ -0,0 +1,166 @@ +#if HAVE_OPENSSL && HAVE_QUIC +#include "guard.h" +#ifndef OPENSSL_NO_QUIC +#include +#include +#include +#include "endpoint.h" +#include "session.h" +#include "session_manager.h" + +namespace node::quic { + +BaseObjectPtr SessionManager::FindSession(const CID& cid) { + // Direct SCID match. + auto it = sessions_.find(cid); + if (it != sessions_.end()) return it->second; + + // Cross-endpoint CID mapping (locally-generated CIDs for preferred + // address, multipath, etc.). + auto scid_it = dcid_to_scid_.find(cid); + if (scid_it != dcid_to_scid_.end()) { + it = sessions_.find(scid_it->second); + if (it != sessions_.end()) return it->second; + // Stale mapping — clean up. + dcid_to_scid_.erase(scid_it); + } + + return {}; +} + +void SessionManager::AddSession(const CID& scid, + BaseObjectPtr session) { + sessions_[scid] = std::move(session); +} + +void SessionManager::AssociateCID(const CID& cid, const CID& scid) { + if (cid && scid && cid != scid) { + dcid_to_scid_[cid] = scid; + } +} + +void SessionManager::DisassociateCID(const CID& cid) { + if (cid) { + dcid_to_scid_.erase(cid); + } +} + +void SessionManager::RemoveSession(const CID& scid) { + auto it = sessions_.find(scid); + if (it != sessions_.end()) { + primary_map_.erase(it->second.get()); + sessions_.erase(it); + } +} + +void SessionManager::AssociateStatelessResetToken( + const StatelessResetToken& token, Session* session) { + token_map_[token] = session; +} + +void SessionManager::DisassociateStatelessResetToken( + const StatelessResetToken& token) { + token_map_.erase(token); +} + +Session* SessionManager::FindSessionByStatelessResetToken( + const StatelessResetToken& token) const { + auto it = token_map_.find(token); + if (it != token_map_.end()) return it->second; + return nullptr; +} + +void SessionManager::RegisterEndpoint(Endpoint* endpoint, + const SocketAddress& local_address) { + endpoints_.insert(endpoint); + endpoint_addrs_[endpoint] = local_address; +} + +void SessionManager::UnregisterEndpoint(Endpoint* endpoint) { + endpoints_.erase(endpoint); + endpoint_addrs_.erase(endpoint); + // If no endpoints remain, destroy all sessions. + if (endpoints_.empty()) { + DestroyAllSessions(); + } +} + +bool SessionManager::HasEndpoints() const { + return !endpoints_.empty(); +} + +size_t SessionManager::endpoint_count() const { + return endpoints_.size(); +} + +Endpoint* SessionManager::FindEndpointForAddress( + const SocketAddress& local_addr) const { + // First pass: exact match. + for (const auto& [endpoint, addr] : endpoint_addrs_) { + if (addr == local_addr) return endpoint; + } + // Second pass: wildcard fallback. An endpoint bound to 0.0.0.0:port + // or [::]:port can serve any address on that port. + int port = SocketAddress::GetPort(local_addr.data()); + for (const auto& [endpoint, addr] : endpoint_addrs_) { + if (SocketAddress::GetPort(addr.data()) == port) { + auto host = addr.address(); + if (host == "0.0.0.0" || host == "::") { + return endpoint; + } + } + } + return nullptr; +} + +void SessionManager::SetPrimaryEndpoint(Session* session, Endpoint* endpoint) { + primary_map_[session] = endpoint; +} + +Endpoint* SessionManager::GetPrimaryEndpoint(Session* session) const { + auto it = primary_map_.find(session); + if (it != primary_map_.end()) return it->second; + return nullptr; +} + +void SessionManager::CloseAllSessionsFor(Endpoint* endpoint) { + // Collect sessions whose primary is this endpoint, then close them. + // We collect first because closing a session modifies primary_map_. + std::vector> to_close; + for (const auto& [session, ep] : primary_map_) { + if (ep == endpoint) { + // Look up the owning reference from sessions_ so the session + // stays alive during close. + for (const auto& [cid, sess_ptr] : sessions_) { + if (sess_ptr.get() == session) { + to_close.push_back(sess_ptr); + break; + } + } + } + } + for (auto& session : to_close) { + session->Close(Session::CloseMethod::SILENT); + } +} + +void SessionManager::DestroyAllSessions() { + // Copy the map since closing sessions will modify it. + auto sessions = sessions_; + for (auto& [cid, session] : sessions) { + session->Close(Session::CloseMethod::SILENT); + } + sessions.clear(); + token_map_.clear(); + dcid_to_scid_.clear(); + primary_map_.clear(); +} + +bool SessionManager::is_empty() const { + return sessions_.empty(); +} + +} // namespace node::quic + +#endif // OPENSSL_NO_QUIC +#endif // HAVE_OPENSSL && HAVE_QUIC diff --git a/src/quic/session_manager.h b/src/quic/session_manager.h new file mode 100644 index 00000000000000..0dfa47b41dd378 --- /dev/null +++ b/src/quic/session_manager.h @@ -0,0 +1,107 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include +#include +#include +#include +#include "cid.h" +#include "tokens.h" + +namespace node::quic { + +class Endpoint; +class Session; + +// SessionManager is a per-Realm singleton that centralizes QUIC session +// routing. It holds the authoritative CID -> Session mapping, enabling +// any Endpoint to route packets to any session. This decouples session +// lifetime from individual endpoints, which is required for preferred +// address, connection migration, and multi-path QUIC. +// +// SessionManager is held by BindingData and lazily created on first access. +// It is not exposed to JavaScript. +class SessionManager final { + public: + explicit SessionManager() = default; + ~SessionManager() = default; + + // Session routing. The sessions_ map holds BaseObjectPtr (owning + // references). SessionManager is the single authority for session ownership. + BaseObjectPtr FindSession(const CID& dcid); + void AddSession(const CID& scid, BaseObjectPtr session); + void RemoveSession(const CID& scid); + + // Cross-endpoint CID association. This map holds locally-generated CIDs + // that need to be routable from any endpoint (e.g., preferred address CID, + // multipath NEW_CONNECTION_ID CIDs). Peer-chosen CIDs from connection + // establishment (config.dcid, config.ocid) go in Endpoint::dcid_to_scid_ + // instead, because those values can collide across endpoints. + void AssociateCID(const CID& cid, const CID& scid); + void DisassociateCID(const CID& cid); + + // Stateless reset token association. The token_map_ holds raw (non-owning) + // pointers. Entries are valid only while the corresponding session exists + // in sessions_. Sessions clean up their tokens during teardown. + void AssociateStatelessResetToken(const StatelessResetToken& token, + Session* session); + void DisassociateStatelessResetToken(const StatelessResetToken& token); + Session* FindSessionByStatelessResetToken( + const StatelessResetToken& token) const; + + // Endpoint registry. Endpoints register themselves when they start + // receiving and unregister when they close. + void RegisterEndpoint(Endpoint* endpoint, const SocketAddress& local_address); + void UnregisterEndpoint(Endpoint* endpoint); + bool HasEndpoints() const; + size_t endpoint_count() const; + + // Find the endpoint bound to a given local address. Used by the session + // send path to route packets through the correct endpoint based on the + // ngtcp2 packet path. Tries exact match first, then wildcard fallback + // (0.0.0.0 or [::] on the same port). + Endpoint* FindEndpointForAddress(const SocketAddress& local_addr) const; + + // Primary endpoint tracking. Each session has one primary endpoint + // responsible for its lifecycle. + void SetPrimaryEndpoint(Session* session, Endpoint* endpoint); + Endpoint* GetPrimaryEndpoint(Session* session) const; + + // Close all sessions whose primary endpoint is the given endpoint. + // Used by Endpoint::Destroy(). + void CloseAllSessionsFor(Endpoint* endpoint); + + // Destroy all sessions. Used when the last endpoint is removed. + void DestroyAllSessions(); + + bool is_empty() const; + + private: + // The sessions_ map holds strong owning references keyed by locally- + // generated SCIDs. This is the single source of truth for session + // ownership. + CID::Map> sessions_; + + // Cross-endpoint CID -> primary SCID mapping. Contains locally-generated + // CIDs that need to be routable from any endpoint. Peer-chosen CIDs + // from connection establishment are in Endpoint::dcid_to_scid_ instead. + CID::Map dcid_to_scid_; + + // Stateless reset token -> Session (non-owning). + StatelessResetToken::Map token_map_; + + // All registered endpoints. + std::unordered_set endpoints_; + + // Endpoint -> bound local address, for FindEndpointForAddress lookups. + std::unordered_map endpoint_addrs_; + + // Session -> primary Endpoint mapping (non-owning both directions; + // sessions are owned by sessions_, endpoints are externally owned). + std::unordered_map primary_map_; +}; + +} // namespace node::quic + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/sessionticket.cc b/src/quic/sessionticket.cc index ac394fd572765b..5d9c4104cffcdd 100644 --- a/src/quic/sessionticket.cc +++ b/src/quic/sessionticket.cc @@ -136,25 +136,33 @@ SSL_TICKET_RETURN SessionTicket::DecryptedCallback(SSL* ssl, case SSL_TICKET_NO_DECRYPT: return SSL_TICKET_RETURN_IGNORE_RENEW; case SSL_TICKET_SUCCESS_RENEW: - [[fallthrough]]; + return static_cast( + AppData::Extract(ssl, session, AppData::Source::Flag::STATUS_RENEW)); case SSL_TICKET_SUCCESS: - return static_cast(AppData::Extract(ssl)); + return static_cast(AppData::Extract(ssl, session)); } } -SessionTicket::AppData::AppData(SSL* ssl) : ssl_(ssl) {} +SessionTicket::AppData::AppData(SSL* ssl, SSL_SESSION* session) + : ssl_(ssl), session_(session) {} + +SSL_SESSION* SessionTicket::AppData::GetSession() const { + return session_ != nullptr ? session_ : SSL_get0_session(ssl_); +} bool SessionTicket::AppData::Set(const uv_buf_t& data) { if (set_ || data.base == nullptr || data.len == 0) return false; set_ = true; - SSL_SESSION_set1_ticket_appdata(SSL_get0_session(ssl_), data.base, data.len); + SSL_SESSION_set1_ticket_appdata(GetSession(), data.base, data.len); return set_; } std::optional SessionTicket::AppData::Get() const { + auto* sess = GetSession(); + if (sess == nullptr) return std::nullopt; uv_buf_t buf; int ret = - SSL_SESSION_get0_ticket_appdata(SSL_get0_session(ssl_), + SSL_SESSION_get0_ticket_appdata(sess, reinterpret_cast(&buf.base), reinterpret_cast(&buf.len)); if (ret != 1) return std::nullopt; @@ -168,11 +176,12 @@ void SessionTicket::AppData::Collect(SSL* ssl) { } } -SessionTicket::AppData::Status SessionTicket::AppData::Extract(SSL* ssl) { +SessionTicket::AppData::Status SessionTicket::AppData::Extract( + SSL* ssl, SSL_SESSION* session, Source::Flag flag) { auto source = GetAppDataSource(ssl); if (source != nullptr) { - AppData app_data(ssl); - return source->ExtractSessionTicketAppData(app_data); + AppData app_data(ssl, session); + return source->ExtractSessionTicketAppData(app_data, flag); } return Status::TICKET_IGNORE; } diff --git a/src/quic/sessionticket.h b/src/quic/sessionticket.h index 2e795cbbcd4869..8c46470a153ca4 100644 --- a/src/quic/sessionticket.h +++ b/src/quic/sessionticket.h @@ -72,7 +72,7 @@ class SessionTicket::AppData final { TICKET_USE_RENEW = SSL_TICKET_RETURN_USE_RENEW, }; - explicit AppData(SSL* session); + explicit AppData(SSL* ssl, SSL_SESSION* session = nullptr); DISALLOW_COPY_AND_MOVE(AppData) bool Set(const uv_buf_t& data); @@ -94,11 +94,15 @@ class SessionTicket::AppData final { }; static void Collect(SSL* ssl); - static Status Extract(SSL* ssl); + static Status Extract(SSL* ssl, + SSL_SESSION* session, + Source::Flag flag = Source::Flag::STATUS_NONE); private: + SSL_SESSION* GetSession() const; bool set_ = false; SSL* ssl_; + SSL_SESSION* session_; }; } // namespace node::quic diff --git a/src/quic/streams.cc b/src/quic/streams.cc index 6edbb97d829f9c..e838392361f946 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -1,7 +1,7 @@ +#include "ngtcp2/ngtcp2.h" #if HAVE_OPENSSL && HAVE_QUIC #include "guard.h" #ifndef OPENSSL_NO_QUIC -#include "streams.h" #include #include #include @@ -9,27 +9,36 @@ #include #include #include +#include #include #include "application.h" #include "bindingdata.h" #include "defs.h" #include "session.h" +#include "streams.h" namespace node { using v8::Array; using v8::ArrayBuffer; using v8::ArrayBufferView; +using v8::BackingStore; +using v8::BackingStoreInitializationMode; using v8::BigInt; +using v8::FunctionCallbackInfo; using v8::Global; +using v8::HandleScope; using v8::Integer; using v8::Just; using v8::Local; +using v8::LocalVector; using v8::Maybe; using v8::Nothing; using v8::Object; using v8::ObjectTemplate; using v8::SharedArrayBuffer; +using v8::String; +using v8::Uint32; using v8::Uint8Array; using v8::Value; @@ -43,6 +52,7 @@ namespace quic { V(READ_ENDED, read_ended, uint8_t) \ V(WRITE_ENDED, write_ended, uint8_t) \ V(RESET, reset, uint8_t) \ + V(RESET_CODE, reset_code, uint64_t) \ V(HAS_OUTBOUND, has_outbound, uint8_t) \ V(HAS_READER, has_reader, uint8_t) \ /* Set when the stream has a block event handler */ \ @@ -52,7 +62,11 @@ namespace quic { /* Set when the stream has a reset event handler */ \ V(WANTS_RESET, wants_reset, uint8_t) \ /* Set when the stream has a trailers event handler */ \ - V(WANTS_TRAILERS, wants_trailers, uint8_t) + V(WANTS_TRAILERS, wants_trailers, uint8_t) \ + /* True when 0-RTT early data was received */ \ + V(RECEIVED_EARLY_DATA, received_early_data, uint8_t) \ + V(WRITE_DESIRED_SIZE, write_desired_size, uint32_t) \ + V(HIGH_WATER_MARK, high_water_mark, uint32_t) #define STREAM_STATS(V) \ /* Marks the timestamp when the stream object was created. */ \ @@ -73,7 +87,11 @@ namespace quic { V(MAX_OFFSET, max_offset) \ V(MAX_OFFSET_ACK, max_offset_ack) \ V(MAX_OFFSET_RECV, max_offset_received) \ - V(FINAL_SIZE, final_size) + V(FINAL_SIZE, final_size) \ + /* Bytes in the receive accumulation buffer */ \ + V(BYTES_ACCUMULATED, bytes_accumulated) \ + /* Peak bytes accumulated over stream lifetime */ \ + V(MAX_BYTES_ACCUMULATED, max_bytes_accumulated) #define STREAM_JS_METHODS(V) \ V(AttachSource, attachSource, false) \ @@ -88,6 +106,91 @@ namespace quic { V(Write, write, false) \ V(EndWrite, endWrite, false) +// ============================================================================ +// RecvAccumulator implementation + +RecvAccumulator::RecvAccumulator(size_t max_capacity) + : buf_(kMinCapacity), max_capacity_(std::max(max_capacity, kMinCapacity)) {} + +size_t RecvAccumulator::Write(const uint8_t* data, size_t len) { + if (len == 0) return 0; + + size_t capacity = buf_.size(); + + // If the buffer is full, caller must flush or grow first. + size_t space = remaining(); + if (space == 0) return 0; + + size_t to_write = std::min(len, space); + + // Write into the buffer, handling wrap-around. + size_t physical_write = write_pos_ % capacity; + size_t first_chunk = std::min(to_write, capacity - physical_write); + memcpy(buf_.data() + physical_write, data, first_chunk); + if (first_chunk < to_write) { + memcpy(buf_.data(), data + first_chunk, to_write - first_chunk); + } + + write_pos_ += to_write; + len_ += to_write; + return to_write; +} + +std::unique_ptr RecvAccumulator::Flush(Environment* env) { + if (len_ == 0) return nullptr; + + size_t capacity = buf_.size(); + + auto store = ArrayBuffer::NewBackingStore( + env->isolate(), len_, BackingStoreInitializationMode::kUninitialized); + auto* dest = static_cast(store->Data()); + + // Copy from the ring buffer. Handle the wrap-around case. + size_t physical_read = read_pos_ % capacity; + size_t first_chunk = std::min(len_, capacity - physical_read); + memcpy(dest, buf_.data() + physical_read, first_chunk); + if (first_chunk < len_) { + memcpy(dest + first_chunk, buf_.data(), len_ - first_chunk); + } + + size_t flushed = len_; + + // Reset cursors. + read_pos_ = 0; + write_pos_ = 0; + len_ = 0; + + // Shrink back toward kMinCapacity if we had expanded. + if (capacity > kMinCapacity) { + buf_.resize(kMinCapacity); + buf_.shrink_to_fit(); + } + + return DataQueue::CreateInMemoryEntryFromBackingStore( + std::move(store), 0, flushed); +} + +void RecvAccumulator::Grow() { + size_t capacity = buf_.size(); + size_t new_capacity = std::min(capacity * 2, max_capacity_); + if (new_capacity <= capacity) return; // already at max + + // Linearize the data and grow in one step. + std::vector new_buf(new_capacity); + if (len_ > 0) { + size_t physical_read = read_pos_ % capacity; + size_t first_chunk = std::min(len_, capacity - physical_read); + memcpy(new_buf.data(), buf_.data() + physical_read, first_chunk); + if (first_chunk < len_) { + memcpy(new_buf.data() + first_chunk, buf_.data(), len_ - first_chunk); + } + } + + buf_ = std::move(new_buf); + read_pos_ = 0; + write_pos_ = len_; +} + // ============================================================================ PendingStream::PendingStream(Direction direction, @@ -141,34 +244,64 @@ struct Stream::State { STAT_STRUCT(Stream, STREAM) +// Stream uses arena-allocated stats, not AliasedStruct, so override the +// STAT_* macros to use the stats() accessor instead of stats_.Data(). +#undef STAT_INCREMENT +#undef STAT_INCREMENT_N +#undef STAT_RECORD_TIMESTAMP +#undef STAT_SET +#undef STAT_GET +#define STAT_INCREMENT(Type, name) IncrementStat(stats()); +#define STAT_INCREMENT_N(Type, name, amt) \ + IncrementStat(stats(), amt); +#define STAT_RECORD_TIMESTAMP(Type, name) \ + RecordTimestampStat(stats()); +#define STAT_SET(Type, name, val) SetStat(stats(), val) +#define STAT_GET(Type, name) GetStat(stats()) + +using StreamStateArena = AliasedStructArena; +using StreamStatsArena = AliasedStructArena; + +namespace { +StreamStateArena& GetStreamStateArena(BindingData& binding) { + if (!binding.stream_state_arena_) { + auto* arena = new StreamStateArena(); + binding.stream_state_arena_ = BindingData::ArenaPtr( + arena, +[](void* p) { delete static_cast(p); }); + } + return *static_cast(binding.stream_state_arena_.get()); +} + +StreamStatsArena& GetStreamStatsArena(BindingData& binding) { + if (!binding.stream_stats_arena_) { + auto* arena = new StreamStatsArena(); + binding.stream_stats_arena_ = BindingData::ArenaPtr( + arena, +[](void* p) { delete static_cast(p); }); + } + return *static_cast(binding.stream_stats_arena_.get()); +} +} // namespace + // ============================================================================ namespace { -// Creates an in-memory DataQueue entry from an ArrayBuffer by either -// detaching it (zero-copy) or copying its contents if detach is not -// possible (e.g., SharedArrayBuffer-backed or non-detachable). -// Returns nullptr on failure (error already thrown if allocation failed). +// Creates an in-memory DataQueue entry by copying the requested range of +// the given ArrayBuffer into a fresh BackingStore. The caller's buffer is +// not detached or otherwise modified, so callers can safely reuse or +// mutate it after the call returns. Callers that want to ensure their +// buffer cannot be mutated after handing it off can call +// `ArrayBuffer.prototype.transfer()` themselves before calling into the +// QUIC API. +// Returns nullptr on zero length or allocation failure. std::unique_ptr CreateEntryFromBuffer( Environment* env, Local buffer, size_t offset, size_t length) { if (length == 0) return nullptr; - std::shared_ptr backing; - if (buffer->IsDetachable()) { - backing = buffer->GetBackingStore(); - if (buffer->Detach(Local()).IsNothing()) { - backing.reset(); - } - } - if (!backing) { - // Buffer is not detachable or detach failed. Copy the data. - JS_TRY_ALLOCATE_BACKING_OR_RETURN(env, copy, length, nullptr); - memcpy(copy->Data(), - static_cast(buffer->Data()) + offset, - length); - offset = 0; - backing = std::move(copy); - } + JS_TRY_ALLOCATE_BACKING_OR_RETURN(env, copy, length, nullptr); + memcpy(copy->Data(), + static_cast(buffer->Data()) + offset, + length); return DataQueue::CreateInMemoryEntryFromBackingStore( - std::move(backing), offset, length); + std::move(copy), 0, length); } } // namespace @@ -226,10 +359,42 @@ Maybe> Stream::GetDataQueueFromSource( JS_TRY_ALLOCATE_BACKING_OR_RETURN( env, backing, str.length(), Nothing>()); memcpy(backing->Data(), *str, str.length()); + auto len = backing->ByteLength(); entries.push_back(DataQueue::CreateInMemoryEntryFromBackingStore( - std::move(backing), 0, backing->ByteLength())); + std::move(backing), 0, len)); return Just(DataQueue::CreateIdempotent(std::move(entries))); } + // FileHandle — create an fd-backed DataQueue from the file path. + // The JS side validates and locks the FileHandle before passing + // the C++ handle here. We detect FileHandle by checking if the + // object's constructor name is "FileHandle". + if (value->IsObject()) { + auto obj = value.As(); + Local ctor_name; + auto maybe_name = obj->GetConstructorName(); + if (!maybe_name.IsEmpty()) { + ctor_name = maybe_name; + Utf8Value name(env->isolate(), ctor_name); + if (strcmp(*name, "FileHandle") == 0) { + fs::FileHandle* file_handle; + ASSIGN_OR_RETURN_UNWRAP( + &file_handle, value, Nothing>()); + Local path; + if (!ToV8Value(env->context(), file_handle->original_name()) + .ToLocal(&path)) { + return Nothing>(); + } + auto entry = DataQueue::CreateFdEntry(env, path); + if (!entry) return Nothing>(); + size_t size = entry->size().value_or(0); + auto queue = DataQueue::Create(); + if (!queue) return Nothing>(); + queue->append(std::move(entry)); + queue->cap(size); + return Just(std::move(queue)); + } + } + } // TODO(jasnell): Add streaming sources... THROW_ERR_INVALID_ARG_TYPE(env, "Invalid data source type"); return Nothing>(); @@ -247,6 +412,14 @@ struct Stream::Impl { std::shared_ptr dataqueue; if (GetDataQueueFromSource(env, args[0]).To(&dataqueue)) { stream->set_outbound(std::move(dataqueue)); + // set_outbound does not call ResumeStream because during + // construction the stream is not yet registered with the session. + // When attaching a source after creation (via setBody), the + // stream is already registered and must be resumed to enter the + // send queue. + if (!stream->is_pending()) { + stream->session().ResumeStream(stream->id()); + } } } @@ -254,7 +427,7 @@ struct Stream::Impl { JS_METHOD(Destroy) { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); - if (args.Length() > 1) { + if (args.Length() >= 1) { CHECK(args[0]->IsBigInt()); bool lossless = false; uint64_t code = args[0].As()->Uint64Value(&lossless); @@ -272,8 +445,7 @@ struct Stream::Impl { // Sends a block of headers to the peer. If the stream is not yet open, // the headers will be queued and sent immediately when the stream is - // opened. If the application does not support sending headers on streams, - // they will be ignored and dropped on the floor. + // opened. Returns false if the application does not support headers. JS_METHOD(SendHeaders) { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); @@ -287,8 +459,13 @@ struct Stream::Impl { // If the stream is pending, the headers will be queued until the // stream is opened, at which time the queued header block will be - // immediately sent when the stream is opened. + // immediately sent when the stream is opened. If we already know + // that the application does not support headers, return false + // immediately so the JS side can throw an appropriate error. if (stream->is_pending()) { + if (!stream->session().application().SupportsHeaders()) { + return args.GetReturnValue().Set(false); + } stream->EnqueuePendingHeaders(kind, headers, flags); return args.GetReturnValue().Set(true); } @@ -335,14 +512,14 @@ struct Stream::Impl { code = args[0].As()->Uint64Value(&lossless); } - if (stream->state_->reset == 1) return; + if (stream->state()->reset == 1) return; stream->EndWritable(); // We can release our outbound here now. Since the stream is being reset // on the ngtcp2 side, we do not need to keep any of the data around // waiting for acknowledgement that will never come. stream->outbound_.reset(); - stream->state_->reset = 1; + stream->state()->reset = 1; if (!stream->is_pending()) { if (stream->is_remote_unidirectional()) return; @@ -355,18 +532,22 @@ struct Stream::Impl { JS_METHOD(SetPriority) { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); - CHECK(args[0]->IsUint32()); // Priority - CHECK(args[1]->IsUint32()); // Priority flag + CHECK(args[0]->IsUint32()); // Packed: (urgency << 1) | incremental - StreamPriority priority = FromV8Value(args[0]); - StreamPriorityFlags flags = FromV8Value(args[1]); + uint32_t packed = args[0].As()->Value(); + StreamPriority priority = static_cast(packed >> 1); + StreamPriorityFlags flags = (packed & 1) + ? StreamPriorityFlags::INCREMENTAL + : StreamPriorityFlags::NON_INCREMENTAL; - if (stream->is_pending()) { - stream->pending_priority_ = PendingPriority{ - .priority = priority, - .flags = flags, - }; - } else { + // Always update the stored priority on the stream. + stream->priority_ = StoredPriority{ + .priority = priority, + .flags = flags, + .pending = stream->is_pending(), + }; + + if (!stream->is_pending()) { stream->session().application().SetStreamPriority( *stream, priority, flags); } @@ -376,13 +557,23 @@ struct Stream::Impl { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); - if (stream->is_pending()) { - return args.GetReturnValue().Set( - static_cast(StreamPriority::DEFAULT)); + // On the client side, priority is always read from the stream's + // stored value since the client is the one setting it. On the + // server side, we delegate to the application which can read + // the peer's requested priority (e.g., from PRIORITY_UPDATE + // frames in HTTP/3). + if (!stream->session().is_server()) { + auto& pri = stream->priority_; + uint32_t packed = (static_cast(pri.priority) << 1) | + (pri.flags == StreamPriorityFlags::INCREMENTAL ? 1 : 0); + return args.GetReturnValue().Set(packed); } - auto priority = stream->session().application().GetStreamPriority(*stream); - args.GetReturnValue().Set(static_cast(priority)); + auto result = stream->session().application().GetStreamPriority(*stream); + uint32_t packed = + (static_cast(result.priority) << 1) | + (result.flags == StreamPriorityFlags::INCREMENTAL ? 1 : 0); + args.GetReturnValue().Set(packed); } // Returns a Blob::Reader that can be used to read data that has been @@ -391,9 +582,9 @@ struct Stream::Impl { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); BaseObjectPtr reader = stream->get_reader(); - if (reader) return args.GetReturnValue().Set(reader->object()); - THROW_ERR_INVALID_STATE(Environment::GetCurrent(args), - "Unable to get a reader for the stream"); + if (reader) args.GetReturnValue().Set(reader->object()); + // Returns undefined when the stream is not readable (e.g. a local + // unidirectional stream). The JS side checks for this. } JS_METHOD(InitStreamingSource) { @@ -429,8 +620,9 @@ class Stream::Outbound final : public MemoryRetainer { explicit Outbound(Stream* stream) : stream_(stream), queue_(DataQueue::Create()), - reader_(queue_->get_reader()), - streaming_(true) {} + reader_(queue_->get_reader()) { + flags_.streaming = true; + } void Acknowledge(size_t amount) { size_t remaining = std::min(amount, total_ - uncommitted_); @@ -503,15 +695,26 @@ class Stream::Outbound final : public MemoryRetainer { if (queue_) queue_->cap(); } - bool is_streaming() const { return streaming_; } + bool is_streaming() const { return flags_.streaming; } size_t total() const { return total_; } + size_t uncommitted() const { return uncommitted_; } + + // Total bytes in the pipeline: data appended to the DataQueue that + // hasn't been pulled yet, plus data pulled but not yet acknowledged. + // This is the number to compare against highWaterMark for backpressure. + size_t queued_bytes() const { return queued_ + total_; } // Appends an entry to the underlying DataQueue. Only valid when // the Outbound was created in streaming mode. bool AppendEntry(std::unique_ptr entry) { - if (!streaming_ || !queue_) return false; + if (!flags_.streaming || !queue_) return false; + auto size = entry->size(); auto result = queue_->append(std::move(entry)); - return result.has_value() && result.value(); + if (result.has_value() && result.value()) { + if (size.has_value()) queued_ += size.value(); + return true; + } + return false; } int Pull(bob::Next next, @@ -519,19 +722,27 @@ class Stream::Outbound final : public MemoryRetainer { ngtcp2_vec* data, size_t count, size_t max_count_hint) { - if (next_pending_) { + if (flags_.next_pending) { + // An async read is in flight, but there may be uncommitted bytes + // from a previous read that ngtcp2 didn't accept (nwrite=0 due + // to pacing/congestion). Return those bytes so the send loop can + // retry rather than blocking until the async read completes. + if (uncommitted_ > 0) { + PullUncommitted(std::move(next)); + return bob::Status::STATUS_CONTINUE; + } std::move(next)(bob::Status::STATUS_BLOCK, nullptr, 0, [](int) {}); return bob::Status::STATUS_BLOCK; } - if (errored_) { + if (flags_.errored) { std::move(next)(UV_EBADF, nullptr, 0, [](int) {}); return UV_EBADF; } // If eos_ is true and there are no uncommitted bytes we'll return eos, // otherwise, return whatever is in the uncommitted queue. - if (eos_) { + if (flags_.eos) { if (uncommitted_ > 0) { PullUncommitted(std::move(next)); return bob::Status::STATUS_CONTINUE; @@ -557,9 +768,6 @@ class Stream::Outbound final : public MemoryRetainer { // that the pull is sync but allow for it to be async. int ret = reader_->Pull( [this](auto status, auto vecs, auto count, auto done) { - // Always make sure next_pending_ is false when we're done. - auto on_exit = OnScopeLeave([this] { next_pending_ = false; }); - // The status should never be wait here. DCHECK_NE(status, bob::Status::STATUS_WAIT); @@ -567,7 +775,8 @@ class Stream::Outbound final : public MemoryRetainer { // If next_pending_ is true then a pull from the reader ended up // being asynchronous, our stream is blocking waiting for the data, // but we have an error! oh no! We need to error the stream. - if (next_pending_) { + if (flags_.next_pending) { + flags_.next_pending = false; stream_->Destroy( QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR)); // We do not need to worry about calling MarkErrored in this case @@ -586,7 +795,10 @@ class Stream::Outbound final : public MemoryRetainer { // Here, there is no more data to read, but we will might have data // in the uncommitted queue. We'll resume the stream so that the // session will try to read from it again. - if (next_pending_) { + // We must clear next_pending_ before calling ResumeStream because + // ResumeStream can synchronously re-enter Outbound::Pull. + if (flags_.next_pending) { + flags_.next_pending = false; stream_->session().ResumeStream(stream_->id()); } return; @@ -610,7 +822,10 @@ class Stream::Outbound final : public MemoryRetainer { // being asynchronous, our stream is blocking waiting for the data. // Now that we have data, let's resume the stream so the session will // pull from it again. - if (next_pending_) { + // We must clear next_pending_ before calling ResumeStream because + // ResumeStream can synchronously re-enter Outbound::Pull. + if (flags_.next_pending) { + flags_.next_pending = false; stream_->session().ResumeStream(stream_->id()); } }, @@ -667,9 +882,17 @@ class Stream::Outbound final : public MemoryRetainer { // Reads here are generally expected to be synchronous. If we have a reader // that insists on providing data asynchronously, then we'll have to block - // until the data is actually available. + // until the data is actually available. However, if there are uncommitted + // bytes already buffered (from a previous async read), return those now + // rather than blocking — the async callback will resume the stream when + // more data arrives. if (ret == bob::Status::STATUS_WAIT) { - next_pending_ = true; + flags_.next_pending = true; + if (uncommitted_ > 0) { + PullUncommitted(std::move(next)); + return bob::Status::STATUS_CONTINUE; + } + std::move(next)(bob::Status::STATUS_BLOCK, nullptr, 0, [](int) {}); return bob::Status::STATUS_BLOCK; } @@ -712,7 +935,7 @@ class Stream::Outbound final : public MemoryRetainer { } void MarkErrored() { - errored_ = true; + flags_.errored = true; head_.reset(); tail_ = nullptr; commit_head_ = nullptr; @@ -723,7 +946,7 @@ class Stream::Outbound final : public MemoryRetainer { } void MarkEnded() { - eos_ = true; + flags_.eos = true; queue_.reset(); reader_.reset(); } @@ -751,6 +974,11 @@ class Stream::Outbound final : public MemoryRetainer { count_++; total_ += vectors[n].len; uncommitted_ += vectors[n].len; + if (queued_ >= vectors[n].len) { + queued_ -= vectors[n].len; + } else { + queued_ = 0; + } } } @@ -758,17 +986,17 @@ class Stream::Outbound final : public MemoryRetainer { std::shared_ptr queue_; std::shared_ptr reader_; - bool errored_ = false; - - // True when in streaming mode (non-idempotent queue, appendable). - bool streaming_ = false; - - // Will be set to true if the reader_ ends up providing a pull result - // asynchronously. - bool next_pending_ = false; - - // Will be set to true once reader_ has returned eos. - bool eos_ = false; + struct Flags { + uint8_t errored : 1 = 0; + // True when in streaming mode (non-idempotent queue, appendable). + uint8_t streaming : 1 = 0; + // Will be set to true if the reader_ ends up providing a pull result + // asynchronously. + uint8_t next_pending : 1 = 0; + // Will be set to true once reader_ has returned eos. + uint8_t eos : 1 = 0; + }; + Flags flags_; // The collection of buffers that we have pulled from reader_ and that we // are holding onto until they are acknowledged. @@ -797,6 +1025,10 @@ class Stream::Outbound final : public MemoryRetainer { // waiting to be acknowledged. When we receive acknowledgement, we will // automatically free held bytes from the buffer. size_t uncommitted_ = 0; + + // Bytes appended to the DataQueue that haven't been pulled yet. + // Decremented in Pull() when data moves from the queue to the buffer. + size_t queued_ = 0; }; // ============================================================================ @@ -844,6 +1076,8 @@ void Stream::InitPerContext(Realm* realm, Local target) { STREAM_STATE(V) #undef V + NODE_DEFINE_CONSTANT(target, IDX_STATS_STREAM_COUNT); + constexpr int QUIC_STREAM_HEADERS_KIND_HINTS = static_cast(HeadersKind::HINTS); constexpr int QUIC_STREAM_HEADERS_KIND_INITIAL = @@ -865,7 +1099,6 @@ void Stream::InitPerContext(Realm* realm, Local target) { } Stream* Stream::From(void* stream_user_data) { - DCHECK_NOT_NULL(stream_user_data); return static_cast(stream_user_data); } @@ -893,29 +1126,55 @@ Stream::Stream(BaseObjectWeakPtr session, stream_id id, std::shared_ptr source) : AsyncWrap(session->env(), object, PROVIDER_QUIC_STREAM), - stats_(env()->isolate()), - state_(env()->isolate()), session_(std::move(session)), - inbound_(DataQueue::Create()), - headers_(env()->isolate()) { + inbound_(DataQueue::Create()) { + auto& binding = BindingData::Get(env()); + stats_slot_ = GetStreamStatsArena(binding).Allocate(env()->isolate()); + state_slot_ = GetStreamStateArena(binding).Allocate(env()->isolate()); MakeWeak(); DCHECK(id < kMaxStreamId); - state_->id = id; - state_->pending = 0; + state()->id = id; + state()->pending = 0; // Allows us to be notified when data is actually read from the // inbound queue so that we can update the stream flow control. inbound_->addBackpressureListener(this); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->state_string(), state_.GetArrayBuffer()); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->stats_string(), stats_.GetArrayBuffer()); + { + const HandleScope handle_scope(env()->isolate()); + // Pass the page's shared views and this slot's byte offset. JS uses + // the offset to index into the shared view — no per-stream V8 object + // creation. + JS_DEFINE_READONLY_PROPERTY(env(), + object, + env()->state_string(), + state_slot_.GetPageDataView(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "stateByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(state_slot_.GetByteOffset()))); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + env()->stats_string(), + stats_slot_.GetPageBigUint64Array(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "statsByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(stats_slot_.GetByteOffset()))); + } set_outbound(std::move(source)); + STAT_RECORD_TIMESTAMP(Stats, created_at); auto params = ngtcp2_conn_get_local_transport_params(this->session()); STAT_SET(Stats, max_offset, params->initial_max_data); - STAT_SET(Stats, opened_at, stats_->created_at); + STAT_SET(Stats, opened_at, stats()->created_at); } Stream::Stream(BaseObjectWeakPtr session, @@ -923,43 +1182,75 @@ Stream::Stream(BaseObjectWeakPtr session, Direction direction, std::shared_ptr source) : AsyncWrap(session->env(), object, PROVIDER_QUIC_STREAM), - stats_(env()->isolate()), - state_(env()->isolate()), session_(std::move(session)), inbound_(DataQueue::Create()), maybe_pending_stream_( - std::make_unique(direction, this, session_)), - headers_(env()->isolate()) { + std::make_unique(direction, this, session_)) { + auto& binding = BindingData::Get(env()); + stats_slot_ = GetStreamStatsArena(binding).Allocate(env()->isolate()); + state_slot_ = GetStreamStateArena(binding).Allocate(env()->isolate()); MakeWeak(); - state_->id = kMaxStreamId; - state_->pending = 1; + state()->id = kMaxStreamId; + state()->pending = 1; // Allows us to be notified when data is actually read from the // inbound queue so that we can update the stream flow control. inbound_->addBackpressureListener(this); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->state_string(), state_.GetArrayBuffer()); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->stats_string(), stats_.GetArrayBuffer()); + { + const HandleScope handle_scope(env()->isolate()); + JS_DEFINE_READONLY_PROPERTY(env(), + object, + env()->state_string(), + state_slot_.GetPageDataView(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "stateByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(state_slot_.GetByteOffset()))); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + env()->stats_string(), + stats_slot_.GetPageBigUint64Array(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "statsByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(stats_slot_.GetByteOffset()))); + } set_outbound(std::move(source)); + STAT_RECORD_TIMESTAMP(Stats, created_at); auto params = ngtcp2_conn_get_local_transport_params(this->session()); STAT_SET(Stats, max_offset, params->initial_max_data); } Stream::~Stream() { // Make sure that Destroy() was called before Stream is actually destructed. - DCHECK_NE(stats_->destroyed_at, 0); + DCHECK_NE(stats()->destroyed_at, 0); + + // Release arena slots back to the freelist. + auto& binding = BindingData::Get(env()); + if (stats_slot_) { + GetStreamStatsArena(binding).ReleaseSlot(stats_slot_); + } + if (state_slot_) { + GetStreamStateArena(binding).ReleaseSlot(state_slot_); + } } void Stream::NotifyStreamOpened(stream_id id) { CHECK(is_pending()); DCHECK(id < kMaxStreamId); Debug(this, "Pending stream opened with id %" PRIi64, id); - state_->pending = 0; - state_->id = id; + state()->pending = 0; + state()->id = id; STAT_RECORD_TIMESTAMP(Stats, opened_at); // Now that the stream is actually opened, add it to the sessions // list of known open streams. @@ -969,27 +1260,37 @@ void Stream::NotifyStreamOpened(stream_id id) { CHECK_EQ(ngtcp2_conn_set_stream_user_data(this->session(), id, this), 0); maybe_pending_stream_.reset(); - if (pending_priority_) { - auto& priority = pending_priority_.value(); + if (priority_.pending) { session().application().SetStreamPriority( - *this, priority.priority, priority.flags); - pending_priority_ = std::nullopt; - } - decltype(pending_headers_queue_) queue; - pending_headers_queue_.swap(queue); - for (auto& headers : queue) { - // TODO(@jasnell): What if the application does not support headers? - session().application().SendHeaders(*this, - headers->kind, - headers->headers.Get(env()->isolate()), - headers->flags); + *this, priority_.priority, priority_.flags); + priority_.pending = false; + } + if (!pending_headers_queue_.empty()) { + if (!session().application().SupportsHeaders()) { + // Headers were enqueued while the application was not yet known + // (headers_supported == 0), and the negotiated application does + // not support headers. This is a fatal mismatch. + Destroy(QuicError::ForApplication( + session().application().GetInternalErrorCode())); + return; + } + decltype(pending_headers_queue_) queue; + pending_headers_queue_.swap(queue); + for (auto& headers : queue) { + session().application().SendHeaders( + *this, + headers->kind, + headers->headers.Get(env()->isolate()), + headers->flags); + } } // If the stream is not a local undirectional stream and is_readable is // false, then we should shutdown the streams readable side now. if (!is_local_unidirectional() && !is_readable()) { NotifyReadableEnded(pending_close_read_code_); } - if (!is_remote_unidirectional() && !is_writable()) { + if (!is_remote_unidirectional() && !is_writable() && + !session_->application().stream_fin_managed_by_application()) { NotifyWritableEnded(pending_close_write_code_); } @@ -1003,7 +1304,7 @@ void Stream::NotifyStreamOpened(stream_id id) { void Stream::NotifyReadableEnded(error_code code) { CHECK(!is_pending()); Session::SendPendingDataScope send_scope(&session()); - ngtcp2_conn_shutdown_stream_read(session(), 0, id(), code); + CHECK_EQ(ngtcp2_conn_shutdown_stream_read(session(), 0, id(), code), 0); } void Stream::NotifyWritableEnded(error_code code) { @@ -1021,32 +1322,37 @@ void Stream::EnqueuePendingHeaders(HeadersKind kind, } bool Stream::is_pending() const { - return state_->pending; + return state()->pending; } stream_id Stream::id() const { - return state_->id; + return state()->id; } Side Stream::origin() const { CHECK(!is_pending()); - return (state_->id & 0b01) ? Side::SERVER : Side::CLIENT; + return (state()->id & 0b01) ? Side::SERVER : Side::CLIENT; } Direction Stream::direction() const { - if (state_->pending) { + if (state()->pending) { CHECK(maybe_pending_stream_.has_value()); auto& val = maybe_pending_stream_.value(); return val->direction(); } - return (state_->id & 0b10) ? Direction::UNIDIRECTIONAL - : Direction::BIDIRECTIONAL; + return (state()->id & 0b10) ? Direction::UNIDIRECTIONAL + : Direction::BIDIRECTIONAL; } Session& Stream::session() const { return *session_; } +uint64_t Stream::last_activity_timestamp() const { + uint64_t ts = stats()->received_at; + return ts != 0 ? ts : stats()->created_at; +} + bool Stream::is_local_unidirectional() const { return direction() == Direction::UNIDIRECTIONAL && ngtcp2_conn_is_local_stream(*session_, id()); @@ -1058,7 +1364,15 @@ bool Stream::is_remote_unidirectional() const { } bool Stream::is_eos() const { - return state_->fin_sent; + return state()->fin_sent; +} + +bool Stream::wants_trailers() const { + return state()->wants_trailers; +} + +void Stream::set_early() { + state()->received_early_data = 1; } bool Stream::is_writable() const { @@ -1068,7 +1382,19 @@ bool Stream::is_writable() const { !ngtcp2_conn_is_local_stream(session(), id())) { return false; } - return state_->write_ended == 0; + return state()->write_ended == 0; +} + +bool Stream::has_outbound() const { + return outbound_ != nullptr; +} + +bool Stream::has_reader() const { + return reader_ != nullptr; +} + +Blob::Reader* Stream::reader() const { + return reader_.get(); } bool Stream::is_readable() const { @@ -1078,21 +1404,27 @@ bool Stream::is_readable() const { ngtcp2_conn_is_local_stream(session(), id())) { return false; } - return state_->read_ended == 0; + return state()->read_ended == 0; } BaseObjectPtr Stream::get_reader() { - if (!is_readable() || state_->has_reader) return {}; - state_->has_reader = 1; + if (state()->has_reader || !inbound_) return {}; + // Local unidirectional streams are never readable. + if (!is_pending() && direction() == Direction::UNIDIRECTIONAL && + ngtcp2_conn_is_local_stream(session(), id())) { + return {}; + } + + state()->has_reader = 1; auto reader = Blob::Reader::Create(env(), Blob::Create(env(), inbound_)); reader_ = reader; return reader; } void Stream::set_final_size(uint64_t final_size) { - DCHECK_IMPLIES(state_->fin_received == 1, + DCHECK_IMPLIES(state()->fin_received == 1, final_size <= STAT_GET(Stats, final_size)); - state_->fin_received = 1; + state()->fin_received = 1; STAT_SET(Stats, final_size, final_size); } @@ -1101,8 +1433,12 @@ void Stream::set_outbound(std::shared_ptr source) { Debug(this, "Setting the outbound data source"); DCHECK_NULL(outbound_); outbound_ = std::make_unique(this, std::move(source)); - state_->has_outbound = 1; - if (!is_pending()) session_->ResumeStream(id()); + state()->has_outbound = 1; + // Note: We intentionally do NOT call ResumeStream here. During + // construction, the stream has not yet been added to the session's + // streams map, so FindStream would fail. The caller (CreateStream / + // AddStream) is responsible for calling ResumeStream after the + // stream is registered. } void Stream::InitStreaming() { @@ -1116,11 +1452,11 @@ void Stream::InitStreaming() { } Debug(this, "Initializing streaming outbound source"); outbound_ = std::make_unique(this); - state_->has_outbound = 1; + state()->has_outbound = 1; if (!is_pending()) session_->ResumeStream(id()); } -void Stream::WriteStreamData(const v8::FunctionCallbackInfo& args) { +void Stream::WriteStreamData(const FunctionCallbackInfo& args) { auto env = this->env(); if (outbound_ == nullptr || !outbound_->is_streaming()) { return THROW_ERR_INVALID_STATE(env, "Streaming source is not initialized"); @@ -1161,6 +1497,7 @@ void Stream::WriteStreamData(const v8::FunctionCallbackInfo& args) { if (!is_pending()) session_->ResumeStream(id()); + UpdateWriteDesiredSize(); args.GetReturnValue().Set(static_cast(outbound_->total())); } @@ -1179,13 +1516,36 @@ void Stream::EndWriting() { } void Stream::EntryRead(size_t amount) { - // Tells us that amount bytes we're reading from inbound_ - // We use this as a signal to extend the flow control - // window to receive more bytes. + // Called when the JS consumer reads data from the inbound DataQueue. + // Extend the flow control window so the sender can transmit more. + if (session().is_destroyed()) return; + Session::SendPendingDataScope send_scope(&session()); session().ExtendStreamOffset(id(), amount); session().ExtendOffset(amount); } +void Stream::BeforePull() { + // Called before the DataQueue reader pulls. Flush any accumulated + // receive data into the DataQueue so the pull finds it immediately. + if (recv_accumulator_ && recv_accumulator_->available() > 0) { + FlushAccumulation(); + } +} + +void Stream::FlushAccumulation() { + if (!recv_accumulator_ || recv_accumulator_->available() == 0) return; + auto entry = recv_accumulator_->Flush(env()); + if (entry) { + inbound_->append(std::move(entry)); + // Notify the reader that data is now available in the DataQueue. + // This is the only place we notify — not on every ReceiveData call — + // so the reader only wakes up when there is a well-sized entry to + // consume. + if (reader_) reader_->NotifyPull(); + } + STAT_SET(Stats, bytes_accumulated, 0); +} + int Stream::DoPull(bob::Next next, int options, ngtcp2_vec* data, @@ -1220,27 +1580,16 @@ void Stream::set_headers_kind(HeadersKind kind) { headers_kind_ = kind; } -bool Stream::AddHeader(const Header& header) { - size_t len = header.length(); +bool Stream::AddHeader(std::unique_ptr
header) { + size_t len = header->length(); if (!session_->application().CanAddHeader( headers_.size(), headers_length_, len)) { return false; } headers_length_ += len; - - auto& state = BindingData::Get(env()); - - const auto push = [&](auto raw) { - Local value; - if (!raw.ToLocal(&value)) [[unlikely]] { - return false; - } - headers_.push_back(value); - return true; - }; - - return push(header.GetName(&state)) && push(header.GetValue(&state)); + headers_.push_back(std::move(header)); + return true; } void Stream::Acknowledge(size_t datalen) { @@ -1250,19 +1599,19 @@ void Stream::Acknowledge(size_t datalen) { // ngtcp2 guarantees that offset must always be greater than the previously // received offset. - DCHECK_GE(datalen, STAT_GET(Stats, max_offset_ack)); - STAT_SET(Stats, max_offset_ack, datalen); + STAT_INCREMENT_N(Stats, max_offset_ack, datalen); // Consumes the given number of bytes in the buffer. outbound_->Acknowledge(datalen); STAT_RECORD_TIMESTAMP(Stats, acked_at); + UpdateWriteDesiredSize(); } void Stream::Commit(size_t datalen, bool fin) { Debug(this, "Committing %zu bytes", datalen); STAT_INCREMENT_N(Stats, bytes_sent, datalen); if (outbound_) outbound_->Commit(datalen); - if (fin) state_->fin_sent = 1; + if (fin) state()->fin_sent = 1; } void Stream::EndWritable() { @@ -1272,33 +1621,38 @@ void Stream::EndWritable() { // will be a non-op since we're not going to be writing any more data // into it anyway. if (outbound_) outbound_->Cap(); - state_->write_ended = 1; + state()->write_ended = 1; } void Stream::EndReadable(std::optional maybe_final_size) { if (!is_readable()) return; - state_->read_ended = 1; + state()->read_ended = 1; + // Flush any accumulated data before capping so the reader can see it. + FlushAccumulation(); set_final_size(maybe_final_size.value_or(STAT_GET(Stats, bytes_received))); inbound_->cap(STAT_GET(Stats, final_size)); - // Notify the JS reader so it can see EOS. - if (reader_) reader_->NotifyPull(); + // Notify the JS reader so it can see EOS. Pass fin=true so the + // wakeup promise resolves with a value the iterator can check to + // avoid waiting for another wakeup that will never come. + if (reader_) reader_->NotifyPull(true); } void Stream::Destroy(QuicError error) { - if (stats_->destroyed_at != 0) return; + if (stats()->destroyed_at != 0) return; + // Record the destroyed at timestamp before notifying the JavaScript side // that the stream is being destroyed. STAT_RECORD_TIMESTAMP(Stats, destroyed_at); DCHECK_NOT_NULL(session_.get()); - if (!state_->pending) { + if (!state()->pending) { Debug( this, "Stream %" PRIi64 " being destroyed with error %s", id(), error); } else { Debug(this, "Pending stream being destroyed with error %s", error); } - state_->pending = 0; + state()->pending = 0; maybe_pending_stream_.reset(); @@ -1311,6 +1665,10 @@ void Stream::Destroy(QuicError error) { // We are going to release our reference to the outbound_ queue here. outbound_.reset(); + // EndReadable() above already flushed accumulated data. Just release + // the ring buffer memory. + recv_accumulator_.reset(); + // We reset the inbound here also. However, it's important to note that // the JavaScript side could still have a reader on the inbound DataQueue, // which may keep that data alive a bit longer. @@ -1327,7 +1685,12 @@ void Stream::Destroy(QuicError error) { auto session = session_; session_.reset(); - session->RemoveStream(id()); + // EmitClose above triggers MakeCallback which can destroy the session + // via JS re-entrancy. The weak pointer may still be non-null (the + // Session BaseObject can be kept alive by a BaseObjectPtr elsewhere, + // e.g. OnTimeout's ref) even though impl_ has been reset. We must + // check is_destroyed() to avoid dereferencing the null impl_. + if (session && !session->is_destroyed()) session->RemoveStream(id()); // Critically, make sure that the RemoveStream call is the last thing // trying to use this stream object. Once that call is made, the stream @@ -1342,35 +1705,107 @@ void Stream::ReceiveData(const uint8_t* data, ReceiveDataFlags flags) { // If reading has ended, or there is no data, there's nothing to do but maybe // end the readable side if this is the last bit of data we've received. - Debug(this, "Receiving %zu bytes of data", len); - - if (state_->read_ended == 1 || len == 0) { + if (state()->read_ended == 1 || len == 0) { if (flags.fin) EndReadable(); return; } + if (flags.early) state()->received_early_data = 1; STAT_INCREMENT_N(Stats, bytes_received, len); STAT_SET(Stats, max_offset_received, STAT_GET(Stats, bytes_received)); STAT_RECORD_TIMESTAMP(Stats, received_at); - JS_TRY_ALLOCATE_BACKING(env(), backing, len) - memcpy(backing->Data(), data, len); - inbound_->append(DataQueue::CreateInMemoryEntryFromBackingStore( - std::move(backing), 0, len)); - // Notify the JS reader that data is available. - if (reader_) reader_->NotifyPull(); + // Lazy-allocate the receive accumulation buffer on first data-carrying + // call. Streams that never receive data (write-only, immediately reset) + // pay zero cost. + if (!recv_accumulator_) { + // Use the per-stream flow control window as the max capacity. The + // peer cannot send more than this without the JS side consuming and + // extending the window, so the ring buffer can never overflow. + auto params = ngtcp2_conn_get_local_transport_params(session()); + size_t max_cap; + if (direction() == Direction::BIDIRECTIONAL) { + max_cap = + ngtcp2_conn_is_local_stream(session(), id()) + ? static_cast(params->initial_max_stream_data_bidi_local) + : static_cast( + params->initial_max_stream_data_bidi_remote); + } else { + max_cap = static_cast(params->initial_max_stream_data_uni); + } + recv_accumulator_ = std::make_unique(max_cap); + } + + // Track whether the accumulator was empty before this call. Used to + // notify the reader exactly once per accumulation cycle (empty → non-empty + // transition) rather than on every callback. + bool was_empty = recv_accumulator_->available() == 0; + + // Accumulate into the ring buffer. When the buffer reaches the flush + // threshold (64 KB of accumulated data), flush it into the DataQueue as + // a single entry and continue writing the remainder. If the reader is + // not consuming (no reader, or reader hasn't pulled), allow the buffer + // to grow beyond 64 KB up to the flow control window instead of + // flushing — keeping data in cheap C++ memory rather than creating + // V8 BackingStore entries nobody is reading yet. + while (len > 0) { + size_t written = recv_accumulator_->Write(data, len); + data += written; + len -= written; + + if (len > 0) { + // Buffer is full. Decide whether to flush or grow. + if (recv_accumulator_->should_flush() && reader_) { + // Reader exists and we've hit the flush threshold — produce a + // well-sized DataQueue entry for the reader to consume. + FlushAccumulation(); + } else if (recv_accumulator_->remaining() == 0) { + // No reader yet, or below flush threshold but physically full. + // Try to grow the buffer to absorb more data. + recv_accumulator_->Grow(); + // If Grow() couldn't expand (already at max), flush as fallback. + if (recv_accumulator_->remaining() == 0) { + FlushAccumulation(); + } + } + } + } + + // Update accumulation stats. + STAT_SET(Stats, bytes_accumulated, recv_accumulator_->available()); + if (recv_accumulator_->available() > STAT_GET(Stats, max_bytes_accumulated)) { + STAT_SET(Stats, max_bytes_accumulated, recv_accumulator_->available()); + } - if (flags.fin) EndReadable(); + if (flags.fin) { + FlushAccumulation(); + EndReadable(); + } else if (reader_ && was_empty) { + // Notify the reader once when the accumulator transitions from empty + // to non-empty. This wakes the reader exactly once per accumulation + // cycle. When the reader wakes and calls pull(), BeforePull() flushes + // the accumulated data, and the subsequent EntryRead() extends the + // flow control window. We do NOT notify on every callback — that + // would defeat coalescing by flushing tiny amounts each time. + reader_->NotifyPull(); + } } void Stream::ReceiveStopSending(QuicError error) { - // Note that this comes from *this* endpoint, not the other side. We handle it - // if we haven't already shutdown our *receiving* side of the stream. - if (state_->read_ended) return; + // STOP_SENDING from the peer asks us to stop sending. Per RFC 9000 + // §3.5 the receiver SHOULD respond with RESET_STREAM, which is what + // ngtcp2_conn_shutdown_stream_write below schedules. If our + // writable side has already been shut down (e.g. we already sent + // RESET_STREAM ourselves or finished sending with FIN) there is + // nothing more to do here. The previous guard checked + // `state()->read_ended` which is unrelated to the writable side and + // suppressed STOP_SENDING handling whenever a sibling RESET_STREAM + // frame had been processed first within the same packet. + if (state()->write_ended) return; Debug(this, "Received stop sending with error %s", error); - ngtcp2_conn_shutdown_stream_read(session(), 0, id(), error.code()); - EndReadable(); + ngtcp2_conn_shutdown_stream_write(session(), 0, id(), error.code()); + EndWritable(); } void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) { @@ -1383,6 +1818,7 @@ void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) { "Received stream reset with final size %" PRIu64 " and error %s", final_size, error); + state()->reset_code = error.code(); EndReadable(final_size); EmitReset(error); } @@ -1390,16 +1826,70 @@ void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) { // ============================================================================ void Stream::EmitBlocked() { - // state_->wants_block will be set from the javascript side if the + // state()->wants_block will be set from the javascript side if the // stream object has a handler for the blocked event. Debug(this, "Blocked"); - if (!env()->can_call_into_js() || !state_->wants_block) { + if (!env()->can_call_into_js() || !state()->wants_block) { return; } CallbackScope cb_scope(this); MakeCallback(BindingData::Get(env()).stream_blocked_callback(), 0, nullptr); } +void Stream::EmitDrain() { + if (!env()->can_call_into_js()) return; + CallbackScope cb_scope(this); + MakeCallback(BindingData::Get(env()).stream_drain_callback(), 0, nullptr); +} + +void Stream::UpdateWriteDesiredSize() { + if (!outbound_ || !outbound_->is_streaming()) return; + + uint64_t available; + uint64_t hwm = state()->high_water_mark; + + if (is_pending()) { + // Pending streams don't have a stream ID yet, so ngtcp2 can't + // report their flow control window. Use the high water mark as + // the available capacity so writes can proceed while pending. + available = hwm > 0 ? hwm : std::numeric_limits::max(); + } else { + // Calculate available capacity based on QUIC flow control. + // The effective limit is the minimum of stream-level and + // connection-level flow control remaining. + ngtcp2_conn* conn = session(); + uint64_t stream_left = ngtcp2_conn_get_max_stream_data_left(conn, id()); + uint64_t conn_left = ngtcp2_conn_get_max_data_left(conn); + available = std::min(stream_left, conn_left); + + // Apply the high water mark as an additional ceiling. + if (hwm > 0) { + available = std::min(available, hwm); + } + } + + // Total bytes in the pipeline: data in the DataQueue (not yet pulled by + // ngtcp2) plus data pulled but not yet acknowledged. Using queued_bytes() + // ensures that data appended via writeSync is accounted for in + // backpressure even before ngtcp2 pulls it. + uint64_t buffered = outbound_->queued_bytes(); + uint64_t desired = (available > buffered) ? (available - buffered) : 0; + + // Clamp to uint32 range since write_desired_size is uint32_t. + uint32_t clamped = static_cast( + std::min(desired, std::numeric_limits::max())); + + uint32_t old_size = state()->write_desired_size; + state()->write_desired_size = clamped; + + // Fire drain when transitioning from 0 to non-zero. + // writeDesiredSize == 0 means the buffer is full or flow control is + // exhausted, so the JS side may be waiting for capacity. + if (old_size == 0 && clamped > 0) { + EmitDrain(); + } +} + void Stream::EmitClose(const QuicError& error) { if (!env()->can_call_into_js()) return; CallbackScope cb_scope(this); @@ -1409,28 +1899,45 @@ void Stream::EmitClose(const QuicError& error) { } void Stream::EmitHeaders() { - // state_->wants_headers will be set from the javascript side if the + STAT_RECORD_TIMESTAMP(Stats, received_at); + // state()->wants_headers will be set from the javascript side if the // stream object has a handler for the headers event. - if (!env()->can_call_into_js() || !state_->wants_headers) { + if (!env()->can_call_into_js() || !state()->wants_headers) { + headers_.clear(); return; } CallbackScope cb_scope(this); + auto& binding = BindingData::Get(env()); + size_t count = headers_.size() * 2; + LocalVector values(env()->isolate(), count); + + for (size_t i = 0; i < headers_.size(); i++) { + Local name; + Local value; + if (!headers_[i]->GetName(&binding).ToLocal(&name) || + !headers_[i]->GetValue(&binding).ToLocal(&value)) [[unlikely]] { + headers_.clear(); + return; + } + values[i * 2] = name; + values[i * 2 + 1] = value; + } + + headers_.clear(); + Local argv[] = { - Array::New(env()->isolate(), headers_.data(), headers_.size()), + Array::New(env()->isolate(), values.data(), count), Integer::NewFromUnsigned(env()->isolate(), static_cast(headers_kind_))}; - headers_.clear(); - - MakeCallback( - BindingData::Get(env()).stream_headers_callback(), arraysize(argv), argv); + MakeCallback(binding.stream_headers_callback(), arraysize(argv), argv); } void Stream::EmitReset(const QuicError& error) { - // state_->wants_reset will be set from the javascript side if the + // state()->wants_reset will be set from the javascript side if the // stream object has a handler for the reset event. - if (!env()->can_call_into_js() || !state_->wants_reset) { + if (!env()->can_call_into_js() || !state()->wants_reset) { return; } CallbackScope cb_scope(this); @@ -1441,9 +1948,9 @@ void Stream::EmitReset(const QuicError& error) { } void Stream::EmitWantTrailers() { - // state_->wants_trailers will be set from the javascript side if the + // state()->wants_trailers will be set from the javascript side if the // stream object has a handler for the trailers event. - if (!env()->can_call_into_js() || !state_->wants_trailers) { + if (!env()->can_call_into_js() || !state()->wants_trailers) { return; } CallbackScope cb_scope(this); @@ -1458,6 +1965,13 @@ void Stream::Schedule(Queue* queue) { if (outbound_ && stream_queue_.IsEmpty()) queue->PushBack(this); } +void Stream::Unschedule() { + // Remove this stream from the send queue. Used when the stream becomes + // flow-control blocked so that SendPendingData does not spin retrying it. + Debug(this, "Unscheduled"); + stream_queue_.Remove(); +} + } // namespace quic } // namespace node diff --git a/src/quic/streams.h b/src/quic/streams.h index 610aac2de334f4..86cb36b2668985 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -15,11 +15,61 @@ #include "bindingdata.h" #include "data.h" +#include + namespace node::quic { class Session; class Stream; +// An elastic ring buffer used by Stream to coalesce received data before +// flushing it into the DataQueue. This avoids creating many small V8 +// BackingStore allocations from per-QUIC-frame ngtcp2 callbacks. Data is +// memcpy'd into the ring buffer and flushed as a single right-sized +// BackingStore when the reader pulls or the buffer reaches its flush +// threshold. The buffer starts at kMinCapacity and can grow up to a +// configured maximum (typically the stream flow control window). +class RecvAccumulator final { + public: + static constexpr size_t kMinCapacity = 64 * 1024; // 64 KB + static constexpr size_t kFlushThreshold = kMinCapacity; // flush at 64 KB + + explicit RecvAccumulator(size_t max_capacity); + ~RecvAccumulator() = default; + + DISALLOW_COPY_AND_MOVE(RecvAccumulator) + + // Append data. Returns the number of bytes written (may be less than + // len if the buffer is full). The caller should flush and retry with + // the remaining bytes. + size_t Write(const uint8_t* data, size_t len); + + // Flush accumulated data as a single DataQueue entry backed by a + // right-sized V8 BackingStore. Returns nullptr if nothing is + // accumulated. Resets the read/write cursors and shrinks the buffer + // back toward kMinCapacity if it was expanded. + std::unique_ptr Flush(Environment* env); + + // Current number of bytes awaiting flush. + size_t available() const { return len_; } + + // Number of bytes that can still be written before the buffer is full. + size_t remaining() const { return buf_.size() - len_; } + + // True if accumulated data has reached the flush threshold. + bool should_flush() const { return len_ >= kFlushThreshold; } + + // Grow the buffer capacity (doubles, up to max_capacity_). + void Grow(); + + private: + std::vector buf_; + size_t max_capacity_; + size_t read_pos_ = 0; + size_t write_pos_ = 0; + size_t len_ = 0; // write_pos_ - read_pos_, accounting for wrap +}; + using Ngtcp2Source = bob::SourceImpl; // When a request to open a stream is made before a Session is able to actually @@ -208,6 +258,11 @@ class Stream final : public AsyncWrap, Session& session() const; + // Returns the most recent activity timestamp for this stream in + // nanoseconds (uv_hrtime). Uses received_at if data has been received, + // otherwise falls back to created_at. Returns 0 if neither is set. + uint64_t last_activity_timestamp() const; + // True if this stream was created in a pending state and is still waiting // to be created. bool is_pending() const; @@ -218,12 +273,27 @@ class Stream final : public AsyncWrap, // data to be acknowledged by the remote peer. bool is_eos() const; + // True if the stream wants to send trailing headers after the body. + bool wants_trailers() const; + + // Marks this stream as having received 0-RTT early data. + void set_early(); + // True if this stream is still in a readable state. bool is_readable() const; // True if this stream is still in a writable state. bool is_writable() const; + // True if an outbound data source has been configured. + bool has_outbound() const; + + // True if a Blob::Reader has been created for the inbound data. + bool has_reader() const; + + // Returns the Blob::Reader for the inbound data, or nullptr. + Blob::Reader* reader() const; + // Called by the session/application to indicate that the specified number // of bytes have been acknowledged by the peer. void Acknowledge(size_t datalen); @@ -238,6 +308,7 @@ class Stream final : public AsyncWrap, void EndWritable(); void EndReadable(std::optional maybe_final_size = std::nullopt); void EntryRead(size_t amount) override; + void BeforePull() override; // Pulls data from the internal outbound DataQueue configured for this stream. // This is called by the session/application when it is preparing to send @@ -278,7 +349,7 @@ class Stream final : public AsyncWrap, // Returns false if the header cannot be added. This will typically happen // if the application does not support headers, a maximum number of headers // have already been added, or the maximum total header length is reached. - bool AddHeader(const Header& header); + bool AddHeader(std::unique_ptr
header); // TODO(@jasnell): Implement MemoryInfo to track outbound_, inbound_, // reader_, headers_, and pending_headers_queue_. @@ -289,12 +360,27 @@ class Stream final : public AsyncWrap, struct State; struct Stats; + // Typed accessors for arena-allocated state/stats. These are defined + // in streams.cc where State and Stats are complete types. + inline State* state() { return static_cast(state_slot_.ptr); } + inline const State* state() const { + return static_cast(state_slot_.ptr); + } + inline Stats* stats() { return static_cast(stats_slot_.ptr); } + inline const Stats* stats() const { + return static_cast(stats_slot_.ptr); + } + private: struct Impl; struct PendingHeaders; class Outbound; + // Flushes any data accumulated in the receive ring buffer into the + // inbound DataQueue as a single right-sized entry. + void FlushAccumulation(); + // Gets a reader for the data received for this stream from the peer, BaseObjectPtr get_reader(); @@ -326,6 +412,14 @@ class Stream final : public AsyncWrap, // blocked because of flow control restriction. void EmitBlocked(); + // Notifies the JavaScript side that the outbound buffer has capacity + // for more data. Fires when write_desired_size transitions from 0 to > 0. + void EmitDrain(); + + // Updates the write_desired_size state field based on current flow control + // and outbound buffer state. Emits drain if transitioning from 0 to > 0. + void UpdateWriteDesiredSize(); + // Delivers the set of inbound headers that have been collected. void EmitHeaders(); @@ -339,12 +433,13 @@ class Stream final : public AsyncWrap, v8::Local headers, HeadersFlags flags); - AliasedStruct stats_; - AliasedStruct state_; + ArenaSlotBase stats_slot_; + ArenaSlotBase state_slot_; BaseObjectWeakPtr session_; std::unique_ptr outbound_; std::shared_ptr inbound_; BaseObjectWeakPtr reader_; + std::unique_ptr recv_accumulator_; // If the stream cannot be opened yet, it will be created in a pending state. // Once the owning session is able to, it will complete opening of the stream @@ -355,16 +450,21 @@ class Stream final : public AsyncWrap, error_code pending_close_read_code_ = 0; error_code pending_close_write_code_ = 0; - struct PendingPriority { - StreamPriority priority; - StreamPriorityFlags flags; + struct StoredPriority { + StreamPriority priority = StreamPriority::DEFAULT; + StreamPriorityFlags flags = StreamPriorityFlags::NON_INCREMENTAL; + bool pending = false; }; - std::optional pending_priority_ = std::nullopt; + StoredPriority priority_; + + const StoredPriority& stored_priority() const { return priority_; } // The headers_ field holds a block of headers that have been received and - // are being buffered for delivery to the JavaScript side. - // TODO(@jasnell): Use v8::Global instead of v8::Local here. - v8::LocalVector headers_; + // are being buffered for delivery to the JavaScript side. Headers are + // stored as C++ objects during collection (AddHeader) and converted to + // V8 strings only when emitted (EmitHeaders), avoiding StrongRootAllocator + // mutex contention on the per-header hot path. + std::vector> headers_; // The headers_kind_ field indicates the kind of headers that are being // buffered. @@ -393,6 +493,7 @@ class Stream final : public AsyncWrap, using Queue = ListHead; void Schedule(Queue* queue); + void Unschedule(); }; } // namespace node::quic diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 13e45bb11d2be2..c0a1610540ed3a 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -30,13 +30,16 @@ using ncrypto::SSLCtxPointer; using ncrypto::SSLPointer; using ncrypto::SSLSessionPointer; using ncrypto::X509Pointer; +using v8::Array; using v8::ArrayBuffer; +using v8::ArrayBufferView; using v8::Just; using v8::Local; using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; using v8::Object; +using v8::String; using v8::Undefined; using v8::Value; @@ -47,7 +50,7 @@ namespace quic { namespace { // Temporarily wraps an SSL pointer but does not take ownership. -// Use by a few of the TLSSession methods that need access to the SSL* +// Used by a few of the TLSSession methods that need access to the SSL* // pointer held by the OSSLContext but cannot take ownership of it. class SSLPointerRef final { public: @@ -95,7 +98,7 @@ template Opt::*member> bool SetOption(Environment* env, Opt* options, const Local& object, - const Local& name) { + const Local& name) { Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; @@ -105,7 +108,7 @@ bool SetOption(Environment* env, if (value->IsArray()) { auto context = env->context(); - auto values = value.As(); + auto values = value.As(); uint32_t count = values->Length(); for (uint32_t n = 0; n < count; n++) { Local item; @@ -125,7 +128,7 @@ bool SetOption(Environment* env, } } else if constexpr (std::is_same::value) { if (item->IsArrayBufferView()) { - Store store = Store::CopyFrom(item.As()); + Store store = Store::CopyFrom(item.As()); (options->*member).push_back(std::move(store)); } else if (item->IsArrayBuffer()) { Store store = Store::CopyFrom(item.As()); @@ -154,7 +157,7 @@ bool SetOption(Environment* env, } } else if constexpr (std::is_same::value) { if (value->IsArrayBufferView()) { - Store store = Store::CopyFrom(value.As()); + Store store = Store::CopyFrom(value.As()); (options->*member).push_back(std::move(store)); } else if (value->IsArrayBuffer()) { Store store = Store::CopyFrom(value.As()); @@ -185,7 +188,7 @@ void OSSLContext::reset() { if (ctx_) { // The SSL object inside the ngtcp2 ctx may not have been set if // SSL creation failed. Guard against null before clearing app data. - if (SSL* ssl = ngtcp2_crypto_ossl_ctx_get_ssl(ctx_); ssl != nullptr) { + if (SSL* ssl = *this; ssl != nullptr) { SSL_set_app_data(ssl, nullptr); } // connection_ is set during Initialize(). If Initialize() was @@ -222,7 +225,7 @@ void OSSLContext::Initialize(SSL* ssl, connection_ = connection; } -std::string OSSLContext::get_cipher_name() const { +std::string_view OSSLContext::get_cipher_name() const { return SSL_get_cipher_name(*this); } @@ -258,6 +261,18 @@ bool OSSLContext::set_hostname(std::string_view hostname) const { const_cast(name.c_str())) == 1; } +bool OSSLContext::set_verify_hostname(std::string_view hostname) const { + // SSL_set1_host tells OpenSSL to verify the peer certificate's + // subject name (SAN/CN) matches this hostname. This is separate + // from SSL_set_tlsext_host_name which only sets the SNI extension. + static const char* kDefaultHostname = "localhost"; + if (hostname.empty()) { + return SSL_set1_host(*this, kDefaultHostname) == 1; + } else { + return SSL_set1_host(*this, hostname.data()) == 1; + } +} + bool OSSLContext::set_early_data_enabled() const { return SSL_set_quic_tls_early_data_enabled(*this, 1) == 1; } @@ -298,16 +313,18 @@ bool OSSLContext::ConfigureClient() const { // ============================================================================ -std::shared_ptr TLSContext::CreateClient(const Options& options) { - return std::make_shared(Side::CLIENT, options); +std::shared_ptr TLSContext::CreateClient(Environment* env, + const Options& options) { + return std::make_shared(env, Side::CLIENT, options); } -std::shared_ptr TLSContext::CreateServer(const Options& options) { - return std::make_shared(Side::SERVER, options); +std::shared_ptr TLSContext::CreateServer(Environment* env, + const Options& options) { + return std::make_shared(env, Side::SERVER, options); } -TLSContext::TLSContext(Side side, const Options& options) - : side_(side), options_(options), ctx_(Initialize()) {} +TLSContext::TLSContext(Environment* env, Side side, const Options& options) + : side_(side), options_(options), ctx_(Initialize(env)) {} TLSContext::operator SSL_CTX*() const { DCHECK(ctx_); @@ -336,7 +353,7 @@ int TLSContext::OnSelectAlpn(SSL* ssl, in, inlen) == OPENSSL_NPN_NO_OVERLAP) { Debug(&tls_session.session(), "ALPN negotiation failed"); - return SSL_TLSEXT_ERR_NOACK; + return SSL_TLSEXT_ERR_ALERT_FATAL; } // ALPN negotiated successfully. *out/*outlen point to the selected @@ -440,7 +457,7 @@ std::unique_ptr TLSContext::NewSession( return tls_session; } -SSLCtxPointer TLSContext::Initialize() { +SSLCtxPointer TLSContext::Initialize(Environment* env) { SSLCtxPointer ctx; switch (side_) { case Side::SERVER: { @@ -495,6 +512,14 @@ SSLCtxPointer TLSContext::Initialize() { SSL_CTX_set_session_cache_mode( ctx.get(), SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL); SSL_CTX_sess_set_new_cb(ctx.get(), OnNewSession); + + // In strict mode, set SSL_VERIFY_PEER so OpenSSL aborts the + // handshake if the server's certificate fails validation. In + // non-strict modes, verification still occurs but the handshake + // completes regardless — the result is surfaced to JS. + if (options_.verify_peer_strict) { + SSL_CTX_set_verify(ctx.get(), SSL_VERIFY_PEER, nullptr); + } break; } } @@ -522,14 +547,14 @@ SSLCtxPointer TLSContext::Initialize() { { ClearErrorOnReturn clear_error_on_return; if (options_.ca.empty()) { - auto store = crypto::GetOrCreateRootCertStore(); + auto store = crypto::GetOrCreateRootCertStore(env); X509_STORE_up_ref(store); SSL_CTX_set_cert_store(ctx.get(), store); } else { for (const auto& ca : options_.ca) { uv_buf_t buf = ca; if (buf.len == 0) { - auto store = crypto::GetOrCreateRootCertStore(); + auto store = crypto::GetOrCreateRootCertStore(env); X509_STORE_up_ref(store); SSL_CTX_set_cert_store(ctx.get(), store); } else { @@ -539,8 +564,8 @@ SSLCtxPointer TLSContext::Initialize() { while ( auto x509 = X509Pointer(PEM_read_bio_X509_AUX( bio.get(), nullptr, crypto::NoPasswordCallback, nullptr))) { - if (cert_store == crypto::GetOrCreateRootCertStore()) { - cert_store = crypto::NewRootCertStore(); + if (cert_store == crypto::GetOrCreateRootCertStore(env)) { + cert_store = crypto::NewRootCertStore(env); SSL_CTX_set_cert_store(ctx.get(), cert_store); } CHECK_EQ(1, X509_STORE_add_cert(cert_store, x509.get())); @@ -597,8 +622,8 @@ SSLCtxPointer TLSContext::Initialize() { } X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx.get()); - if (cert_store == crypto::GetOrCreateRootCertStore()) { - cert_store = crypto::NewRootCertStore(); + if (cert_store == crypto::GetOrCreateRootCertStore(env)) { + cert_store = crypto::NewRootCertStore(env); SSL_CTX_set_cert_store(ctx.get(), cert_store); } @@ -629,8 +654,16 @@ int TLSContext::OnSNI(SSL* ssl, int* ad, void* arg) { auto it = default_ctx->sni_contexts_.find(servername); if (it != default_ctx->sni_contexts_.end()) { SSL_set_SSL_CTX(ssl, it->second->ctx_.get()); + return SSL_TLSEXT_ERR_OK; } } + // No matching hostname found. If the default context has a certificate + // (from the sni['*'] wildcard identity), fall through to use it. + // Otherwise, reject the connection with an unrecognized_name alert. + if (SSL_CTX_get0_certificate(default_ctx->ctx_.get()) == nullptr) { + *ad = SSL_AD_UNRECOGNIZED_NAME; + return SSL_TLSEXT_ERR_ALERT_FATAL; + } return SSL_TLSEXT_ERR_OK; } @@ -693,11 +726,13 @@ Maybe TLSContext::Options::From(Environment* env, env, &options, params, state.name##_string()) if (!SET(verify_client) || !SET(reject_unauthorized) || + !SET(verify_hostname) || !SET(verify_peer_strict) || !SET(enable_early_data) || !SET(enable_tls_trace) || !SET(alpn) || !SET(servername) || !SET(ciphers) || !SET(groups) || - !SET(verify_private_key) || !SET(keylog) || - !SET_VECTOR(crypto::KeyObjectData, keys) || !SET_VECTOR(Store, certs) || - !SET_VECTOR(Store, ca) || !SET_VECTOR(Store, crl)) { + !SET(verify_private_key) || !SET(keylog) || !SET(port) || + !SET(authoritative) || !SET_VECTOR(crypto::KeyObjectData, keys) || + !SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) || + !SET_VECTOR(Store, crl)) { return Nothing(); } @@ -716,6 +751,8 @@ std::string TLSContext::Options::ToString() const { (verify_client ? std::string("yes") : std::string("no")); res += prefix + "reject unauthorized: " + (reject_unauthorized ? std::string("yes") : std::string("no")); + res += prefix + "verify peer strict: " + + (verify_peer_strict ? std::string("yes") : std::string("no")); res += prefix + "enable early data: " + (enable_early_data ? std::string("yes") : std::string("no")); res += prefix + "enable_tls_trace: " + @@ -830,6 +867,14 @@ void TLSSession::Initialize( return; } + if (options.verify_hostname) { + if (!ossl_context_.set_verify_hostname(options.servername)) { + validation_error_ = "Failed to set verify hostname"; + ossl_context_.reset(); + return; + } + } + if (maybeSessionTicket.has_value()) { const auto& sessionTicket = *maybeSessionTicket; uv_buf_t buf = sessionTicket.ticket(); @@ -838,15 +883,23 @@ void TLSSession::Initialize( // The early data will just be ignored if it's invalid. if (ossl_context_.set_session_ticket(ticket)) { - ngtcp2_vec rtp = sessionTicket.transport_params(); - if (ngtcp2_conn_decode_and_set_0rtt_transport_params( - *session_, rtp.base, rtp.len) == 0) { - if (!ossl_context_.set_early_data_enabled()) { - validation_error_ = "Failed to enable early data"; - ossl_context_.reset(); - return; + // Only enable 0-RTT if the option allows it. The session + // ticket is still used for TLS resumption (1-RTT) either way. + if (options.enable_early_data) { + ngtcp2_vec rtp = sessionTicket.transport_params(); + if (ngtcp2_conn_decode_and_set_0rtt_transport_params( + *session_, rtp.base, rtp.len) == 0) { + if (!ossl_context_.set_early_data_enabled()) { + validation_error_ = "Failed to enable early data"; + ossl_context_.reset(); + return; + } + session_->SetStreamOpenAllowed(); + // Populate the state buffer from the 0-RTT transport + // params so that maxDatagramSize and other values are + // available before the handshake completes. + session_->PopulateEarlyTransportParamsState(); } - session_->SetStreamOpenAllowed(); } } } diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h index 3e57ab2a7cac1c..ffec7d083f4c0f 100644 --- a/src/quic/tlscontext.h +++ b/src/quic/tlscontext.h @@ -45,12 +45,13 @@ class OSSLContext final { ngtcp2_conn* connection, SSL_CTX* ssl_ctx); - std::string get_cipher_name() const; + std::string_view get_cipher_name() const; std::string get_selected_alpn() const; std::string_view get_negotiated_group() const; bool set_alpn_protocols(std::string_view protocols) const; bool set_hostname(std::string_view hostname) const; + bool set_verify_hostname(std::string_view hostname) const; bool set_early_data_enabled() const; bool set_transport_params(const ngtcp2_vec& tp) const; @@ -207,6 +208,21 @@ class TLSContext final : public MemoryRetainer, // This option is only used by the server side. bool reject_unauthorized = true; + // When true, the client will set SSL_VERIFY_PEER so that OpenSSL + // aborts the handshake if the server's certificate fails validation. + // This is the "strict" verify_peer mode. When false (the default), + // the handshake completes regardless and VerifyPeerIdentity is + // called after to surface errors to JS. This option is only used + // by the client side. + bool verify_peer_strict = false; + + // When true, OpenSSL verifies that the server's certificate matches + // the servername (hostname verification via SSL_set1_host). Should + // be true for 'strict' and 'auto' verifyPeer modes, false for + // 'manual'. Without this, a valid certificate for any domain would + // be accepted. This option is only used by the client side. + bool verify_hostname = false; + // When true (the default), the server accepts 0-RTT early data // from clients with valid session tickets. When false, early data // is disabled and clients must complete a full handshake before @@ -241,6 +257,15 @@ class TLSContext final : public MemoryRetainer, // JavaScript option name "crl" std::vector crl; + // The port to advertise in ORIGIN frames for this hostname. + // Defaults to 443 (the standard HTTPS port). Only relevant for + // server-side SNI entries used with HTTP/3. + uint16_t port = 443; + + // Whether this hostname should be included in ORIGIN frames. + // Only relevant for server-side SNI entries. + bool authoritative = true; + void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(TLSContext::Options) SET_SELF_SIZE(Options) @@ -254,10 +279,12 @@ class TLSContext final : public MemoryRetainer, std::string ToString() const; }; - static std::shared_ptr CreateClient(const Options& options); - static std::shared_ptr CreateServer(const Options& options); + static std::shared_ptr CreateClient(Environment* env, + const Options& options); + static std::shared_ptr CreateServer(Environment* env, + const Options& options); - TLSContext(Side side, const Options& options); + TLSContext(Environment* env, Side side, const Options& options); DISALLOW_COPY_AND_MOVE(TLSContext) // Each QUIC Session has exactly one TLSSession. Each TLSSession maintains @@ -286,7 +313,7 @@ class TLSContext final : public MemoryRetainer, SET_SELF_SIZE(TLSContext) private: - ncrypto::SSLCtxPointer Initialize(); + ncrypto::SSLCtxPointer Initialize(Environment* env); operator SSL_CTX*() const; static void OnKeylog(const SSL* ssl, const char* line); @@ -304,9 +331,9 @@ class TLSContext final : public MemoryRetainer, Options options_; ncrypto::X509Pointer cert_; ncrypto::X509Pointer issuer_; - ncrypto::SSLCtxPointer ctx_; std::string validation_error_ = ""; std::unordered_map> sni_contexts_; + ncrypto::SSLCtxPointer ctx_; friend class TLSSession; }; diff --git a/src/quic/tokens.cc b/src/quic/tokens.cc index 7b2ee5c70e03b5..f9a429cef66279 100644 --- a/src/quic/tokens.cc +++ b/src/quic/tokens.cc @@ -61,42 +61,59 @@ std::string TokenSecret::ToString() const { // ============================================================================ // StatelessResetToken -StatelessResetToken::StatelessResetToken() : ptr_(nullptr), buf_() {} +StatelessResetToken::StatelessResetToken() + : ngtcp2_stateless_reset_token(), ptr_(nullptr) {} -StatelessResetToken::StatelessResetToken(const uint8_t* token) : ptr_(token) {} +StatelessResetToken::StatelessResetToken(const uint8_t* token) + : ptr_(reinterpret_cast(token)) {} + +StatelessResetToken::StatelessResetToken( + const ngtcp2_stateless_reset_token* token) + : ptr_(token) {} StatelessResetToken::StatelessResetToken(const TokenSecret& secret, const CID& cid) - : ptr_(buf_) { + : ptr_(this) { CHECK_EQ(ngtcp2_crypto_generate_stateless_reset_token( - buf_, secret, kStatelessTokenLen, cid), + data, secret, kStatelessTokenLen, cid), 0); } StatelessResetToken::StatelessResetToken(uint8_t* token, const TokenSecret& secret, const CID& cid) - : ptr_(token) { + : ptr_(reinterpret_cast(token)) { CHECK_EQ(ngtcp2_crypto_generate_stateless_reset_token( token, secret, kStatelessTokenLen, cid), 0); } +StatelessResetToken::StatelessResetToken(ngtcp2_stateless_reset_token* token, + const TokenSecret& secret, + const CID& cid) + : ptr_(token) { + CHECK_EQ(ngtcp2_crypto_generate_stateless_reset_token( + token->data, secret, kStatelessTokenLen, cid), + 0); +} + StatelessResetToken::StatelessResetToken(const StatelessResetToken& other) - : ptr_(buf_) { + : ngtcp2_stateless_reset_token(), ptr_(other ? this : nullptr) { if (other) { - memcpy(buf_, other.ptr_, kStatelessTokenLen); - } else { - ptr_ = nullptr; + memcpy(data, other.ptr_->data, kStatelessTokenLen); } } StatelessResetToken::operator const uint8_t*() const { - return ptr_ != nullptr ? ptr_ : buf_; + return ptr_ != nullptr ? ptr_->data : data; +} + +StatelessResetToken::operator const ngtcp2_stateless_reset_token*() const { + return ptr_; } StatelessResetToken::operator const char*() const { - return reinterpret_cast(ptr_ != nullptr ? ptr_ : buf_); + return reinterpret_cast(ptr_ != nullptr ? ptr_->data : data); } StatelessResetToken::operator bool() const { @@ -109,7 +126,7 @@ bool StatelessResetToken::operator==(const StatelessResetToken& other) const { (ptr_ != nullptr && other.ptr_ == nullptr)) { return false; } - return CRYPTO_memcmp(ptr_, other.ptr_, kStatelessTokenLen) == 0; + return CRYPTO_memcmp(ptr_->data, other.ptr_->data, kStatelessTokenLen) == 0; } bool StatelessResetToken::operator!=(const StatelessResetToken& other) const { @@ -128,7 +145,7 @@ std::string StatelessResetToken::ToString() const { size_t StatelessResetToken::Hash::operator()( const StatelessResetToken& token) const { if (token.ptr_ == nullptr) return 0; - return HashBytes(token.ptr_, kStatelessTokenLen); + return HashBytes(token.ptr_->data, kStatelessTokenLen); } StatelessResetToken StatelessResetToken::kInvalid; diff --git a/src/quic/tokens.h b/src/quic/tokens.h index cfbaa94e344f8d..5438a4d5d8c414 100644 --- a/src/quic/tokens.h +++ b/src/quic/tokens.h @@ -70,7 +70,8 @@ class TokenSecret final : public MemoryRetainer { // // StatlessResetTokens are always kStatelessTokenLen bytes, // as are the secrets used to generate the token. -class StatelessResetToken final : public MemoryRetainer { +class StatelessResetToken final : public ngtcp2_stateless_reset_token, + public MemoryRetainer { public: static constexpr int kStatelessTokenLen = NGTCP2_STATELESS_RESET_TOKENLEN; @@ -78,30 +79,35 @@ class StatelessResetToken final : public MemoryRetainer { // Generates a stateless reset token using HKDF with the cid and token secret // as input. The token secret is either provided by user code when an Endpoint - // is created or is generated randomly. + // is created or is generated randomly. The token is stored in the inherited + // ngtcp2_stateless_reset_token::data and ptr_ is set to this. StatelessResetToken(const TokenSecret& secret, const CID& cid); - // Generates a stateless reset token using the given token storage. + // Generates a stateless reset token into the given external storage. // The StatelessResetToken wraps the token and does not take ownership. - // The token storage must be at least kStatelessTokenLen bytes in length. - // The length is not verified so care must be taken when using this - // constructor. StatelessResetToken(uint8_t* token, const TokenSecret& secret, const CID& cid); + // Generates a stateless reset token into the given external storage. + // The StatelessResetToken wraps the token and does not take ownership. + StatelessResetToken(ngtcp2_stateless_reset_token* token, + const TokenSecret& secret, + const CID& cid); + // Wraps the given token. Does not take over ownership of the token storage. - // The token must be at least kStatelessTokenLen bytes in length. - // The length is not verified so care must be taken when using this - // constructor. explicit StatelessResetToken(const uint8_t* token); + // Wraps the given token. Does not take over ownership of the token storage. + explicit StatelessResetToken(const ngtcp2_stateless_reset_token* token); + StatelessResetToken(const StatelessResetToken& other); DISALLOW_MOVE(StatelessResetToken) std::string ToString() const; operator const uint8_t*() const; + operator const ngtcp2_stateless_reset_token*() const; operator bool() const; bool operator==(const StatelessResetToken& other) const; @@ -124,8 +130,7 @@ class StatelessResetToken final : public MemoryRetainer { private: operator const char*() const; - const uint8_t* ptr_; - uint8_t buf_[NGTCP2_STATELESS_RESET_TOKENLEN]; + const ngtcp2_stateless_reset_token* ptr_; }; // A RETRY packet communicates a retry token to the client. Retry tokens are diff --git a/src/quic/transportparams.cc b/src/quic/transportparams.cc index da665ea01bf35a..183ba973ac1823 100644 --- a/src/quic/transportparams.cc +++ b/src/quic/transportparams.cc @@ -1,6 +1,8 @@ #if HAVE_OPENSSL && HAVE_QUIC #include "guard.h" #ifndef OPENSSL_NO_QUIC +#include +#include #include #include #include @@ -10,14 +12,19 @@ #include "defs.h" #include "endpoint.h" #include "session.h" +#include "session_manager.h" #include "tokens.h" #include "transportparams.h" namespace node { +using v8::BigInt; +using v8::Boolean; +using v8::DictionaryTemplate; using v8::Just; using v8::Local; using v8::Maybe; +using v8::MaybeLocal; using v8::Nothing; using v8::Object; using v8::Value; @@ -69,10 +76,49 @@ Maybe TransportParams::Options::From( #undef SET - // TODO(@jasnell): We are not yet exposing the ability to set the preferred - // adddress via the options, tho the underlying support is here in the class. - options.preferred_address_ipv4 = std::nullopt; - options.preferred_address_ipv6 = std::nullopt; + // Parse the preferred address options. These are SocketAddress objects + // (or undefined to skip). Only meaningful for server sessions. + Local preferred_ipv4; + if (!params->Get(env->context(), state.preferred_address_ipv4_string()) + .ToLocal(&preferred_ipv4)) { + return Nothing(); + } + if (!preferred_ipv4->IsUndefined()) { + if (!SocketAddressBase::HasInstance(env, preferred_ipv4)) { + THROW_ERR_INVALID_ARG_TYPE( + env, "transportParams.preferredAddressIpv4 must be a SocketAddress"); + return Nothing(); + } + auto* addr = BaseObject::FromJSObject( + preferred_ipv4.As()); + if (addr->address()->family() != AF_INET) { + THROW_ERR_INVALID_ARG_VALUE( + env, "transportParams.preferredAddressIpv4 must be an IPv4 address"); + return Nothing(); + } + options.preferred_address_ipv4 = *addr->address(); + } + + Local preferred_ipv6; + if (!params->Get(env->context(), state.preferred_address_ipv6_string()) + .ToLocal(&preferred_ipv6)) { + return Nothing(); + } + if (!preferred_ipv6->IsUndefined()) { + if (!SocketAddressBase::HasInstance(env, preferred_ipv6)) { + THROW_ERR_INVALID_ARG_TYPE( + env, "transportParams.preferredAddressIpv6 must be a SocketAddress"); + return Nothing(); + } + auto* addr = BaseObject::FromJSObject( + preferred_ipv6.As()); + if (addr->address()->family() != AF_INET6) { + THROW_ERR_INVALID_ARG_VALUE( + env, "transportParams.preferredAddressIpv6 must be an IPv6 address"); + return Nothing(); + } + options.preferred_address_ipv6 = *addr->address(); + } return Just(options); } @@ -113,8 +159,6 @@ std::string TransportParams::Options::ToString() const { res += prefix + "max ack delay: " + std::to_string(max_ack_delay); res += prefix + "max datagram frame size: " + std::to_string(max_datagram_frame_size); - res += prefix + "disable active migration: " + - (disable_active_migration ? std::string("yes") : std::string("no")); res += indent.Close(); return res; } @@ -151,8 +195,8 @@ TransportParams::TransportParams(const Config& config, const Options& options) SET_PARAM(ack_delay_exponent); SET_PARAM(max_datagram_frame_size); SET_PARAM_V(max_idle_timeout, options.max_idle_timeout * NGTCP2_SECONDS); - SET_PARAM_V(disable_active_migration, - options.disable_active_migration ? 1 : 0); + SET_PARAM_V(disable_active_migration, 0); + SET_PARAM_V(grease_quic_bit, 1); SET_PARAM_V(preferred_addr_present, 0); SET_PARAM_V(stateless_reset_token_present, 0); SET_PARAM_V(retry_scid_present, 0); @@ -172,11 +216,13 @@ TransportParams::TransportParams(const Config& config, const Options& options) #undef SET_PARAM #undef SET_PARAM_V - if (options.preferred_address_ipv4.has_value()) + if (options.preferred_address_ipv4.has_value()) { SetPreferredAddress(options.preferred_address_ipv4.value()); + } - if (options.preferred_address_ipv6.has_value()) + if (options.preferred_address_ipv6.has_value()) { SetPreferredAddress(options.preferred_address_ipv6.value()); + } } TransportParams::TransportParams(const ngtcp2_vec& vec, Version version) @@ -288,7 +334,162 @@ void TransportParams::GeneratePreferredAddressToken(Session* session) { params_.preferred_addr.stateless_reset_token, config.preferred_address_cid), session); + // Register the preferred address CID with SessionManager for + // cross-endpoint routing. This is a locally-generated CID that needs + // to be routable from the preferred address endpoint (which may be + // different from the primary endpoint). + auto& mgr = BindingData::Get(session->env()).session_manager(); + mgr.AssociateCID(config.preferred_address_cid, config.scid); + } +} + +v8::MaybeLocal TransportParams::ToObject(Environment* env) const { + auto& binding_data = BindingData::Get(env); + auto tmpl = binding_data.transport_params_template(); + static constexpr std::string_view names[] = { + "preferredAddressIpv4", + "preferredAddressIpv6", + "originalDCID", + "initialSCID", + "retrySCID", + "initialMaxStreamDataBidiLocal", + "initialMaxStreamDataBidiRemote", + "initialMaxStreamDataUni", + "initialMaxData", + "initialMaxStreamsBidi", + "initialMaxStreamsUni", + "maxIdleTimeout", + "activeConnectionIDLimit", + "ackDelayExponent", + "maxAckDelay", + "maxDatagramFrameSize", + "disableActiveMigration", + }; + if (tmpl.IsEmpty()) { + tmpl = DictionaryTemplate::New(env->isolate(), names); + binding_data.set_transport_params_template(tmpl); + } + + MaybeLocal values[] = { + Undefined(env->isolate()), // preferredAddressIpv4 + Undefined(env->isolate()), // preferredAddressIpv6 + Undefined(env->isolate()), // originalDCID + Undefined(env->isolate()), // initialSCID + Undefined(env->isolate()), // retrySCID + Undefined(env->isolate()), // initialMaxStreamDataBidiLocal + Undefined(env->isolate()), // initialMaxStreamDataBidiRemote + Undefined(env->isolate()), // initialMaxStreamDataUni + Undefined(env->isolate()), // initialMaxData + Undefined(env->isolate()), // initialMaxStreamsBidi + Undefined(env->isolate()), // initialMaxStreamsUni + Undefined(env->isolate()), // maxIdleTimeout + Undefined(env->isolate()), // activeConnectionIDLimit + Undefined(env->isolate()), // ackDelayExponent + Undefined(env->isolate()), // maxAckDelay + Undefined(env->isolate()), // maxDatagramFrameSize + Undefined(env->isolate()), // disableActiveMigration + }; + + static_assert(std::size(values) == std::size(names)); + + static constexpr size_t kPreferredAddressIpv4Index = 0; + static constexpr size_t kPreferredAddressIpv6Index = 1; + static constexpr size_t kOriginalDCIDIndex = 2; + static constexpr size_t kInitialSCIDIndex = 3; + static constexpr size_t kRetrySCIDIndex = 4; + static constexpr size_t kInitialMaxStreamDataBidiLocalIndex = 5; + static constexpr size_t kInitialMaxStreamDataBidiRemoteIndex = 6; + static constexpr size_t kInitialMaxStreamDataUniIndex = 7; + static constexpr size_t kInitialMaxDataIndex = 8; + static constexpr size_t kInitialMaxStreamsBidiIndex = 9; + static constexpr size_t kInitialMaxStreamsUniIndex = 10; + static constexpr size_t kMaxIdleTimeoutIndex = 11; + static constexpr size_t kActiveConnectionIDLimitIndex = 12; + static constexpr size_t kAckDelayExponentIndex = 13; + static constexpr size_t kMaxAckDelayIndex = 14; + static constexpr size_t kMaxDatagramFrameSizeIndex = 15; + static constexpr size_t kDisableActiveMigrationIndex = 16; + + if (ptr_ != nullptr) { + if (ptr_->preferred_addr_present) { + if (ptr_->preferred_addr.ipv4_present) { + auto address = std::make_shared( + reinterpret_cast(&ptr_->preferred_addr.ipv4)); + auto addr = SocketAddressBase::Create(env, std::move(address)); + if (!addr) return {}; + values[kPreferredAddressIpv4Index] = addr->object(); + } + + if (ptr_->preferred_addr.ipv6_present) { + auto address = std::make_shared( + reinterpret_cast(&ptr_->preferred_addr.ipv6)); + auto addr = SocketAddressBase::Create(env, std::move(address)); + if (!addr) return {}; + values[kPreferredAddressIpv6Index] = addr->object(); + } + // ngtcp2_preferred_addr preferred_addr; + } + + if (ptr_->original_dcid_present) { + CID cid(ptr_->original_dcid); + Local value; + if (!ToV8Value(env->context(), cid.ToString()).ToLocal(&value)) { + return {}; + } + values[kOriginalDCIDIndex] = value; + } + + if (ptr_->initial_scid_present) { + CID cid(ptr_->initial_scid); + Local value; + if (!ToV8Value(env->context(), cid.ToString()).ToLocal(&value)) { + return {}; + } + values[kInitialSCIDIndex] = value; + } + + if (ptr_->retry_scid_present) { + CID cid(ptr_->retry_scid); + Local value; + if (!ToV8Value(env->context(), cid.ToString()).ToLocal(&value)) { + return {}; + } + values[kRetrySCIDIndex] = value; + } + + values[kInitialMaxStreamDataBidiLocalIndex] = BigInt::NewFromUnsigned( + env->isolate(), ptr_->initial_max_stream_data_bidi_local); + values[kInitialMaxStreamDataBidiRemoteIndex] = BigInt::NewFromUnsigned( + env->isolate(), ptr_->initial_max_stream_data_bidi_remote); + values[kInitialMaxStreamDataUniIndex] = BigInt::NewFromUnsigned( + env->isolate(), ptr_->initial_max_stream_data_uni); + values[kInitialMaxDataIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->initial_max_data); + values[kInitialMaxStreamsBidiIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->initial_max_streams_bidi); + values[kInitialMaxStreamsUniIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->initial_max_streams_uni); + values[kMaxIdleTimeoutIndex] = BigInt::NewFromUnsigned( + env->isolate(), ptr_->max_idle_timeout / NGTCP2_SECONDS); + values[kActiveConnectionIDLimitIndex] = BigInt::NewFromUnsigned( + env->isolate(), ptr_->active_connection_id_limit); + values[kAckDelayExponentIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->ack_delay_exponent); + values[kMaxAckDelayIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->ack_delay_exponent); + values[kMaxAckDelayIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->max_ack_delay); + values[kMaxDatagramFrameSizeIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->max_datagram_frame_size); + values[kDisableActiveMigrationIndex] = + Boolean::New(env->isolate(), ptr_->disable_active_migration); + } + + auto obj = tmpl->NewInstance(env->context(), values); + if (obj->SetPrototypeV2(env->context(), Null(env->isolate())).IsNothing()) { + return {}; } + return obj; } TransportParams::operator const ngtcp2_transport_params&() const { diff --git a/src/quic/transportparams.h b/src/quic/transportparams.h index 45ee0d49e79a15..46724574611aff 100644 --- a/src/quic/transportparams.h +++ b/src/quic/transportparams.h @@ -114,16 +114,9 @@ class TransportParams final { // The maximum size of DATAGRAM frames that the endpoint will accept. // Setting the value to 0 will disable DATAGRAM support. // https://datatracker.ietf.org/doc/html/rfc9221#section-3 - uint64_t max_datagram_frame_size = kDefaultMaxPacketLength; + uint16_t max_datagram_frame_size = kDefaultMaxPacketLength; // When true, communicates that the Session does not support active - // connection migration. See the QUIC specification for more details on - // connection migration. - // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.30.1 - // TODO(@jasnell): Active connection migration is not yet implemented. - // This will be revisited in a future update. - bool disable_active_migration = true; - static const Options kDefault; void MemoryInfo(MemoryTracker* tracker) const override; @@ -171,6 +164,9 @@ class TransportParams final { size_t len, Version version = Version::V1) const; + // Returns a JavaScript object representing the transport parameters. + v8::MaybeLocal ToObject(Environment* env) const; + private: void SetPreferredAddress(const SocketAddress& address); void GeneratePreferredAddressToken(Session* session); diff --git a/src/stream_base.cc b/src/stream_base.cc index 566204e667c8e8..360986c8935dde 100644 --- a/src/stream_base.cc +++ b/src/stream_base.cc @@ -296,14 +296,10 @@ int StreamBase::Writev(const FunctionCallbackInfo& args) { int StreamBase::WriteBuffer(const FunctionCallbackInfo& args) { CHECK(args[0]->IsObject()); + CHECK(args[1]->IsUint8Array()); Environment* env = Environment::GetCurrent(args); - if (!args[1]->IsUint8Array()) { - node::THROW_ERR_INVALID_ARG_TYPE(env, "Second argument must be a buffer"); - return 0; - } - Local req_wrap_obj = args[0].As(); uv_buf_t buf; buf.base = Buffer::Data(args[1]); diff --git a/test/cctest/test_dataqueue.cc b/test/cctest/test_dataqueue.cc index 73488fcab0a4d1..7c75bf9bfc42d9 100644 --- a/test/cctest/test_dataqueue.cc +++ b/test/cctest/test_dataqueue.cc @@ -495,25 +495,10 @@ TEST(DataQueue, NonIdempotentDataQueue) { CHECK(!waitingForPull); CHECK_EQ(status, node::bob::STATUS_CONTINUE); - // We can read the expected data from reader1. Because the entries are - // InMemoryEntry instances, reads will be fully synchronous here. + // The next read produces buffer2. When the first entry's reader returns + // EOS, the NonIdempotentDataQueueReader immediately pulls from the next + // entry (recursive Pull), so the transition is seamless. waitingForPull = true; - - status = reader->Pull( - [&](int status, const DataQueue::Vec* vecs, size_t count, auto done) { - waitingForPull = false; - CHECK_EQ(status, node::bob::STATUS_CONTINUE); - CHECK_EQ(count, 0); - }, - node::bob::OPTIONS_SYNC, - nullptr, - 0, - node::bob::kMaxCountHint); - - CHECK(!waitingForPull); - CHECK_EQ(status, node::bob::STATUS_CONTINUE); - - // The next read produces buffer2, and should be the end. status = reader->Pull( [&](int status, const DataQueue::Vec* vecs, size_t count, auto done) { waitingForPull = false; @@ -628,6 +613,9 @@ TEST(DataQueue, DataQueueEntry) { CHECK(!pullIsPending); CHECK_EQ(status, node::bob::STATUS_CONTINUE); + // Cap the queue so the reader can reach EOS after draining all entries. + data_queue2->cap(); + // Read to completion... while (status != node::bob::STATUS_EOS) { status = reader->Pull( diff --git a/test/cctest/test_node_crypto.cc b/test/cctest/test_node_crypto.cc index fa17b069b4a7e8..94940e3c55f939 100644 --- a/test/cctest/test_node_crypto.cc +++ b/test/cctest/test_node_crypto.cc @@ -18,7 +18,7 @@ */ TEST(NodeCrypto, NewRootCertStore) { node::per_process::cli_options->ssl_openssl_cert_store = true; - X509_STORE* store = node::crypto::NewRootCertStore(); + X509_STORE* store = node::crypto::NewRootCertStore(nullptr); ASSERT_TRUE(store); ASSERT_EQ(ERR_peek_error(), 0UL) << "NewRootCertStore should not have left " "any errors on the OpenSSL error stack\n"; diff --git a/test/cctest/test_quic_tokenbucket.cc b/test/cctest/test_quic_tokenbucket.cc new file mode 100644 index 00000000000000..5c5ef5b3cbc975 --- /dev/null +++ b/test/cctest/test_quic_tokenbucket.cc @@ -0,0 +1,125 @@ +#if HAVE_OPENSSL && HAVE_QUIC +#include "quic/guard.h" +#ifndef OPENSSL_NO_QUIC +#include +#include +#include +#include + +namespace node::quic { +namespace { + +// Helper: nanoseconds from seconds +static constexpr uint64_t secs(double s) { + return static_cast(s * 1e9); +} + +TEST(QuicTokenBucket, AllowsBurst) { + TokenBucket bucket(10, 5); // 10/sec rate, burst of 5 + uint64_t now = secs(1000); + + // Should allow up to burst count immediately + for (int i = 0; i < 5; i++) { + EXPECT_TRUE(bucket.consume(now)) << "consume " << i << " should succeed"; + } + + // Bucket should be empty now + EXPECT_FALSE(bucket.consume(now)); +} + +TEST(QuicTokenBucket, RefillsOverTime) { + TokenBucket bucket(1000, 1); // 1000/sec rate, burst of 1 + uint64_t now = secs(1000); + + // Drain the bucket + EXPECT_TRUE(bucket.consume(now)); + EXPECT_FALSE(bucket.consume(now)); + + // Advance 2ms — at 1000/sec that's ~2 tokens, but burst is 1 + now += secs(0.002); + + // Should have refilled to 1 (capped by burst) + EXPECT_TRUE(bucket.consume(now)); + EXPECT_FALSE(bucket.consume(now)); +} + +TEST(QuicTokenBucket, BurstCapacity) { + TokenBucket bucket(10000, 3); // high rate, burst of 3 + uint64_t now = secs(1000); + + // Drain + EXPECT_TRUE(bucket.consume(now)); + EXPECT_TRUE(bucket.consume(now)); + EXPECT_TRUE(bucket.consume(now)); + EXPECT_FALSE(bucket.consume(now)); + + // Advance enough to fully refill + now += secs(1); + + // Should be capped at burst (3), not more + int count = 0; + while (bucket.consume(now)) count++; + EXPECT_EQ(count, 3); +} + +TEST(QuicTokenBucket, ZeroRateAlwaysDenies) { + TokenBucket bucket(0, 0); + uint64_t now = secs(1000); + EXPECT_FALSE(bucket.consume(now)); + now += secs(10); + EXPECT_FALSE(bucket.consume(now)); +} + +TEST(QuicTokenBucket, HighRateAllowsRapidConsume) { + TokenBucket bucket(1000000, 1000); // 1M/sec, burst 1000 + uint64_t now = secs(1000); + + // Should be able to consume the full burst + int count = 0; + for (int i = 0; i < 1000; i++) { + if (bucket.consume(now)) count++; + } + EXPECT_EQ(count, 1000); +} + +TEST(QuicTokenBucket, InitOnce) { + TokenBucket bucket; // default: rate=0, burst=0, last_ts=0 + uint64_t now = secs(1000); + + // Init with rate=100, burst=5 + bucket.InitOnce(100, 5, now); + EXPECT_TRUE(bucket.consume(now)); + + // InitOnce is idempotent — second call is a no-op + bucket.InitOnce(0, 0, now); // would make it deny-all if applied + EXPECT_TRUE(bucket.consume(now)); // still has tokens from first init +} + +TEST(QuicTokenBucket, DefaultConstructorDenies) { + TokenBucket bucket; // default: rate=0, burst=0, last_ts=0 + uint64_t now = secs(1000); + EXPECT_FALSE(bucket.consume(now)); +} + +TEST(QuicTokenBucket, GradualRefill) { + TokenBucket bucket(10, 10); // 10/sec rate, burst of 10 + uint64_t now = secs(1000); + + // Drain fully + for (int i = 0; i < 10; i++) { + EXPECT_TRUE(bucket.consume(now)); + } + EXPECT_FALSE(bucket.consume(now)); + + // Advance 500ms — should get 5 tokens + now += secs(0.5); + int count = 0; + while (bucket.consume(now)) count++; + EXPECT_EQ(count, 5); +} + +} // namespace +} // namespace node::quic + +#endif // OPENSSL_NO_QUIC +#endif // HAVE_OPENSSL && HAVE_QUIC diff --git a/test/cctest/test_quic_tokens.cc b/test/cctest/test_quic_tokens.cc index 1003b1a0e8005f..f24e0fc50dfc7a 100644 --- a/test/cctest/test_quic_tokens.cc +++ b/test/cctest/test_quic_tokens.cc @@ -56,7 +56,7 @@ TEST(StatelessResetToken, Basic) { CHECK_EQ(token, token2); - // Let's pretend out secret is also a token just for the sake + // Let's pretend our secret is also a token just for the sake // of the test. That's ok because they're the same length. StatelessResetToken token3(secret); @@ -85,6 +85,83 @@ TEST(StatelessResetToken, Basic) { CHECK_EQ(found->second, token); } +TEST(StatelessResetToken, Ngtcp2StructIntegration) { + uint8_t secret[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6}; + uint8_t cid_data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; + ngtcp2_cid cid_; + ngtcp2_cid_init(&cid_, cid_data, 10); + TokenSecret fixed_secret(secret); + CID cid(cid_); + + // Owning token — generated into the inherited ngtcp2_stateless_reset_token + StatelessResetToken owning(fixed_secret, cid); + CHECK(owning); + + // The ngtcp2_stateless_reset_token* conversion operator should return + // a valid pointer to the token data. + const ngtcp2_stateless_reset_token* as_struct = owning; + CHECK_NE(as_struct, nullptr); + // The struct's data should match the uint8_t* conversion. + const uint8_t* as_bytes = owning; + CHECK_EQ( + memcmp( + as_struct->data, as_bytes, StatelessResetToken::kStatelessTokenLen), + 0); + + // Non-owning from const ngtcp2_stateless_reset_token* — wraps an + // existing struct without copying. + StatelessResetToken from_struct(as_struct); + CHECK(from_struct); + CHECK_EQ(from_struct, owning); + // The pointer should be the same (non-owning wraps, doesn't copy). + const ngtcp2_stateless_reset_token* from_struct_ptr = from_struct; + CHECK_EQ(from_struct_ptr, as_struct); + + // Owning into external ngtcp2_stateless_reset_token — generates the + // token into a caller-provided struct. + ngtcp2_stateless_reset_token external_struct{}; + StatelessResetToken into_struct(&external_struct, fixed_secret, cid); + CHECK(into_struct); + CHECK_EQ(into_struct, owning); + // The external struct should now contain the generated token. + CHECK_EQ(memcmp(external_struct.data, + as_bytes, + StatelessResetToken::kStatelessTokenLen), + 0); + // The conversion operator should return a pointer to the external struct. + const ngtcp2_stateless_reset_token* into_struct_ptr = into_struct; + CHECK_EQ(into_struct_ptr, &external_struct); + + // Copy of an owning token should itself be owning (independent copy). + StatelessResetToken copy_of_owning = owning; + CHECK_EQ(copy_of_owning, owning); + const ngtcp2_stateless_reset_token* copy_ptr = copy_of_owning; + // Should NOT point to the same memory as the original. + CHECK_NE(copy_ptr, as_struct); + // But data should match. + CHECK_EQ( + memcmp(copy_ptr->data, as_bytes, StatelessResetToken::kStatelessTokenLen), + 0); + + // Copy of a non-owning token should become owning (copies data). + StatelessResetToken copy_of_non_owning = from_struct; + CHECK_EQ(copy_of_non_owning, from_struct); + const ngtcp2_stateless_reset_token* copy_no_ptr = copy_of_non_owning; + // Should NOT point to the original non-owning source. + CHECK_NE(copy_no_ptr, from_struct_ptr); + + // kInvalid conversions. + const ngtcp2_stateless_reset_token* invalid_ptr = + StatelessResetToken::kInvalid; + CHECK_EQ(invalid_ptr, nullptr); + const uint8_t* invalid_bytes = StatelessResetToken::kInvalid; + // When ptr_ is null, falls back to inherited data (zeroed). + uint8_t zeroed[StatelessResetToken::kStatelessTokenLen]{}; + CHECK_EQ( + memcmp(invalid_bytes, zeroed, StatelessResetToken::kStatelessTokenLen), + 0); +} + TEST(RetryToken, Basic) { auto& random = CID::Factory::random(); TokenSecret secret; diff --git a/test/cctest/test_sockaddr.cc b/test/cctest/test_sockaddr.cc index 68b8739f97e1fc..727a5ec021dd3a 100644 --- a/test/cctest/test_sockaddr.cc +++ b/test/cctest/test_sockaddr.cc @@ -1,5 +1,5 @@ -#include "node_sockaddr-inl.h" #include "gtest/gtest.h" +#include "node_sockaddr-inl.h" using node::SocketAddress; using node::SocketAddressBlockList; @@ -43,6 +43,85 @@ TEST(SocketAddress, SocketAddress) { CHECK_EQ(map[addr], 2); } +TEST(SocketAddress, IpHashAndIpEqual) { + sockaddr_storage s1, s2, s3, s4; + // Same IP, different ports. + SocketAddress::ToSockAddr(AF_INET, "10.0.0.1", 443, &s1); + SocketAddress::ToSockAddr(AF_INET, "10.0.0.1", 8080, &s2); + // Different IP. + SocketAddress::ToSockAddr(AF_INET, "10.0.0.2", 443, &s3); + + SocketAddress addr1(reinterpret_cast(&s1)); + SocketAddress addr2(reinterpret_cast(&s2)); + SocketAddress addr3(reinterpret_cast(&s3)); + + SocketAddress::IpHash ip_hash; + SocketAddress::IpEqual ip_equal; + + // Same IP, different port: should hash equal and compare equal. + CHECK_EQ(ip_hash(addr1), ip_hash(addr2)); + CHECK(ip_equal(addr1, addr2)); + + // Different IP: should not compare equal. + CHECK(!ip_equal(addr1, addr3)); + + // Full Hash (includes port) should differ for same IP, different port. + CHECK_NE(SocketAddress::Hash()(addr1), SocketAddress::Hash()(addr2)); + + // IpMap should treat same-IP-different-port as the same key. + SocketAddress::IpMap map; + map[addr1] = 1; + map[addr2]++; // Same IP as addr1, should increment the same entry. + CHECK_EQ(map[addr1], 2); + CHECK_EQ(map.size(), 1); + + map[addr3] = 10; + CHECK_EQ(map.size(), 2); + CHECK_EQ(map[addr3], 10); +} + +TEST(SocketAddress, IpHashIPv6) { + sockaddr_storage s1, s2, s3; + SocketAddress::ToSockAddr(AF_INET6, "::1", 443, &s1); + SocketAddress::ToSockAddr(AF_INET6, "::1", 8080, &s2); + SocketAddress::ToSockAddr(AF_INET6, "::2", 443, &s3); + + SocketAddress addr1(reinterpret_cast(&s1)); + SocketAddress addr2(reinterpret_cast(&s2)); + SocketAddress addr3(reinterpret_cast(&s3)); + + SocketAddress::IpHash ip_hash; + SocketAddress::IpEqual ip_equal; + + // Same IPv6, different port: equal. + CHECK_EQ(ip_hash(addr1), ip_hash(addr2)); + CHECK(ip_equal(addr1, addr2)); + + // Different IPv6: not equal. + CHECK(!ip_equal(addr1, addr3)); + + // IpMap with IPv6 keys. + SocketAddress::IpMap map; + map[addr1] = 5; + map[addr2]++; + CHECK_EQ(map[addr1], 6); + CHECK_EQ(map.size(), 1); +} + +TEST(SocketAddress, IpEqualCrossFamily) { + sockaddr_storage s1, s2; + SocketAddress::ToSockAddr(AF_INET, "127.0.0.1", 443, &s1); + SocketAddress::ToSockAddr(AF_INET6, "::1", 443, &s2); + + SocketAddress addr1(reinterpret_cast(&s1)); + SocketAddress addr2(reinterpret_cast(&s2)); + + SocketAddress::IpEqual ip_equal; + + // Different address families should never be equal. + CHECK(!ip_equal(addr1, addr2)); +} + TEST(SocketAddress, SocketAddressIPv6) { sockaddr_storage storage; SocketAddress::ToSockAddr(AF_INET6, "::1", 443, &storage); @@ -67,11 +146,13 @@ TEST(SocketAddressLRU, SocketAddressLRU) { struct FooLRUTraits { using Type = Foo; - static bool CheckExpired(const SocketAddress& address, const Type& type) { + static bool CheckExpired(const SocketAddress& address, + const Type& type, + uint64_t now) { return type.expired; } - static void Touch(const SocketAddress& address, Type* type) { + static void Touch(const SocketAddress& address, Type* type, uint64_t now) { type->expired = false; } }; @@ -85,13 +166,13 @@ TEST(SocketAddressLRU, SocketAddressLRU) { SocketAddress::ToSockAddr(AF_INET, "123.123.123.125", 443, &storage[2]); SocketAddress::ToSockAddr(AF_INET, "123.123.123.123", 443, &storage[3]); - SocketAddress addr1(reinterpret_cast(&storage[0])); SocketAddress addr2(reinterpret_cast(&storage[1])); SocketAddress addr3(reinterpret_cast(&storage[2])); SocketAddress addr4(reinterpret_cast(&storage[3])); - Foo* foo = lru.Upsert(addr1); + uint64_t now = uv_hrtime(); + Foo* foo = lru.Upsert(addr1, now); CHECK_NOT_NULL(foo); CHECK_EQ(foo->c, 0); CHECK_EQ(foo->expired, false); @@ -99,14 +180,14 @@ TEST(SocketAddressLRU, SocketAddressLRU) { foo->c = 1; foo->expired = true; - foo = lru.Upsert(addr1); + foo = lru.Upsert(addr1, now); CHECK_NOT_NULL(lru.Peek(addr1)); CHECK_EQ(lru.Peek(addr1), lru.Peek(addr4)); CHECK_EQ(lru.Peek(addr1)->c, 1); CHECK_EQ(lru.Peek(addr1)->expired, false); CHECK_EQ(lru.size(), 1); - foo = lru.Upsert(addr2); + foo = lru.Upsert(addr2, now); foo->c = 2; foo->expired = true; CHECK_NOT_NULL(lru.Peek(addr2)); @@ -115,7 +196,7 @@ TEST(SocketAddressLRU, SocketAddressLRU) { foo->expired = true; - foo = lru.Upsert(addr3); + foo = lru.Upsert(addr3, now); foo->c = 3; foo->expired = false; CHECK_NOT_NULL(lru.Peek(addr3)); @@ -197,21 +278,19 @@ TEST(SocketAddressBlockList, Simple) { sockaddr_storage storage[2]; SocketAddress::ToSockAddr(AF_INET, "10.0.0.1", 0, &storage[0]); SocketAddress::ToSockAddr(AF_INET, "10.0.0.2", 0, &storage[1]); - std::shared_ptr addr1 = - std::make_shared( - reinterpret_cast(&storage[0])); - std::shared_ptr addr2 = - std::make_shared( - reinterpret_cast(&storage[1])); + std::shared_ptr addr1 = std::make_shared( + reinterpret_cast(&storage[0])); + std::shared_ptr addr2 = std::make_shared( + reinterpret_cast(&storage[1])); bl.AddSocketAddress(addr1); bl.AddSocketAddress(addr2); - CHECK(bl.Apply(addr1)); - CHECK(bl.Apply(addr2)); + CHECK(bl.Apply(*addr1)); + CHECK(bl.Apply(*addr2)); bl.RemoveSocketAddress(addr1); - CHECK(!bl.Apply(addr1)); - CHECK(bl.Apply(addr2)); + CHECK(!bl.Apply(*addr1)); + CHECK(bl.Apply(*addr2)); } diff --git a/test/common/quic.mjs b/test/common/quic.mjs new file mode 100644 index 00000000000000..d05ee634f5e5ea --- /dev/null +++ b/test/common/quic.mjs @@ -0,0 +1,61 @@ +// Shared helpers for QUIC tests. +// +// Usage: +// import { key, cert, listen, connect } from '../common/quic.mjs'; +// +// Provides pre-loaded TLS credentials and thin wrappers around node:quic +// listen/connect that apply default options suitable for most tests. + +import * as fixtures from '../common/fixtures.mjs'; + +const { createPrivateKey } = await import('node:crypto'); +const quic = await import('node:quic'); + +// Pre-loaded TLS credentials from the standard agent1 fixture pair. +const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); +const cert = fixtures.readKey('agent1-cert.pem'); + +/** + * Start a QUIC server with sensible test defaults. + * @param {Function} callback The session callback (receives QuicSession). + * @param {object} [options] Options forwarded to quic.listen(). The + * following defaults are applied when not specified: + * - sni: { '*': { keys: [key], certs: [cert] } } + * - alpn: ['quic-test'] + * @returns {Promise} + */ +async function listen(callback, options = {}) { + const { + sni = { '*': { keys: [key], certs: [cert] } }, + alpn = ['quic-test'], + ...rest + } = options; + return quic.listen(callback, { sni, alpn, ...rest }); +} + +/** + * Connect a QUIC client with sensible test defaults. + * @param {SocketAddress|string} address The server address. + * @param {object} [options] Options forwarded to quic.connect(). The + * following defaults are applied when not specified: + * - alpn: 'quic-test' + * @returns {Promise} + */ +async function connect(address, options = {}) { + const { + alpn = 'quic-test', + // Test helper defaults to 'manual' because tests use self-signed + // certs without a CA. Tests that want to verify cert validation + // behavior should set verifyPeer explicitly. + verifyPeer = 'manual', + ...rest + } = options; + return quic.connect(address, { alpn, verifyPeer, ...rest }); +} + +export { + key, + cert, + listen, + connect, +}; diff --git a/test/common/quic/test-client.mjs b/test/common/quic/test-client.mjs new file mode 100644 index 00000000000000..1e450e99ed673f --- /dev/null +++ b/test/common/quic/test-client.mjs @@ -0,0 +1,64 @@ +import { resolve } from 'node:path'; +import { spawn } from 'node:child_process'; + +export default class QuicTestClient { + #pathToClient; + #runningProcess; + + constructor() { + this.#pathToClient = resolve(process.execPath, '../ngtcp2_test_client'); + console.log(this.#pathToClient); + } + + help(options = { stdio: 'inherit' }) { + const { promise, resolve, reject } = Promise.withResolvers(); + const proc = spawn(this.#pathToClient, ['--help'], options); + proc.on('error', reject); + proc.on('exit', (code, signal) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Process exited with code ${code} and signal ${signal}`)); + } + }); + return promise; + } + + run(address, port, uri, options = { stdio: 'inherit' }) { + const { promise, resolve, reject } = Promise.withResolvers(); + if (this.#runningProcess) { + reject(new Error('Server is already running')); + return promise; + } + const args = [ + address, + port, + uri ?? '', + ]; + this.#runningProcess = spawn(this.#pathToClient, args, options); + this.#runningProcess.on('error', (err) => { + this.#runningProcess = undefined; + reject(err); + }); + this.#runningProcess.on('exit', (code, signal) => { + if (code === 0) { + resolve(); + } else { + if (code === null && signal === 'SIGTERM') { + // Normal termination due to stop() being called. + resolve(); + return; + } + reject(new Error(`Process exited with code ${code} and signal ${signal}`)); + } + }); + return promise; + } + + stop() { + if (this.#runningProcess) { + this.#runningProcess.kill(); + this.#runningProcess = undefined; + } + } +}; diff --git a/test/common/quic/test-server.mjs b/test/common/quic/test-server.mjs new file mode 100644 index 00000000000000..5d0168ed233938 --- /dev/null +++ b/test/common/quic/test-server.mjs @@ -0,0 +1,66 @@ +import { resolve } from 'node:path'; +import { spawn } from 'node:child_process'; + +export default class QuicTestServer { + #pathToServer; + #runningProcess; + + constructor() { + this.#pathToServer = resolve(process.execPath, '../ngtcp2_test_server'); + console.log(this.#pathToServer); + } + + help(options = { stdio: 'inherit' }) { + const { promise, resolve, reject } = Promise.withResolvers(); + const proc = spawn(this.#pathToServer, ['--help'], options); + proc.on('error', reject); + proc.on('exit', (code, signal) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Process exited with code ${code} and signal ${signal}`)); + } + }); + return promise; + } + + run(address, port, keyFile, certFile, options = { stdio: 'inherit' }) { + const { promise, resolve, reject } = Promise.withResolvers(); + if (this.#runningProcess) { + reject(new Error('Server is already running')); + return promise; + } + const args = [ + address, + port, + keyFile, + certFile, + ]; + this.#runningProcess = spawn(this.#pathToServer, args, options); + this.#runningProcess.on('error', (err) => { + this.#runningProcess = undefined; + reject(err); + }); + this.#runningProcess.on('exit', (code, signal) => { + this.#runningProcess = undefined; + if (code === 0) { + resolve(); + } else { + if (code === null && signal === 'SIGTERM') { + // Normal termination due to stop() being called. + resolve(); + return; + } + reject(new Error(`Process exited with code ${code} and signal ${signal}`)); + } + }); + return promise; + } + + stop() { + if (this.#runningProcess) { + this.#runningProcess.kill(); + this.#runningProcess = undefined; + } + } +}; diff --git a/test/fixtures/keys/Makefile b/test/fixtures/keys/Makefile index d5bdd8f46ff573..c1e2fde9c3874b 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -2,6 +2,7 @@ all: \ ca1-cert.pem \ ca2-cert.pem \ ca2-crl.pem \ + ca2-crl-agent3.pem \ ca3-cert.pem \ ca4-cert.pem \ ca5-cert.pem \ @@ -533,6 +534,28 @@ ca2-crl.pem: ca2-key.pem ca2-cert.pem ca2.cnf agent4-cert.pem -out ca2-crl.pem \ -passin 'pass:password' +# +# Make CRL with agent3 being rejected +# Uses a separate temporary database so the ca2-crl.pem revocation of agent4 +# does not contaminate this CRL. +# +ca2-crl-agent3.pem: ca2-key.pem ca2-cert.pem ca2.cnf agent3-cert.pem + @> ca2-crl-agent3-database.txt + @sed 's/ca2-database/ca2-crl-agent3-database/' ca2.cnf > ca2-crl-agent3.cnf + openssl ca -revoke agent3-cert.pem \ + -keyfile ca2-key.pem \ + -cert ca2-cert.pem \ + -config ca2-crl-agent3.cnf \ + -passin 'pass:password' + openssl ca \ + -keyfile ca2-key.pem \ + -cert ca2-cert.pem \ + -config ca2-crl-agent3.cnf \ + -gencrl \ + -out ca2-crl-agent3.pem \ + -passin 'pass:password' + @rm -f ca2-crl-agent3.cnf ca2-crl-agent3-database.txt* + # # agent5 is signed by ca2 (client cert) # @@ -1177,7 +1200,7 @@ irrelevant_san_correct_subject-key.pem: openssl ecparam -name prime256v1 -genkey -noout -out irrelevant_san_correct_subject-key.pem clean: - rm -f *.pfx *.pem *.srl ca2-database.txt ca2-serial fake-startcom-root-serial *.print *.old fake-startcom-root-issued-certs/*.pem + rm -f *.pfx *.pem *.srl ca2-database.txt ca2-crl-agent3-database.txt* ca2-crl-agent3.cnf ca2-serial fake-startcom-root-serial *.print *.old fake-startcom-root-issued-certs/*.pem @> fake-startcom-root-database.txt test: agent1-verify agent2-verify agent3-verify agent4-verify agent5-verify agent6-verify agent7-verify agent8-verify agent10-verify ec10-verify diff --git a/test/fixtures/keys/ca2-crl-agent3.pem b/test/fixtures/keys/ca2-crl-agent3.pem new file mode 100644 index 00000000000000..9dcb4568d8a84a --- /dev/null +++ b/test/fixtures/keys/ca2-crl-agent3.pem @@ -0,0 +1,13 @@ +-----BEGIN X509 CRL----- +MIIB/jCB5wIBATANBgkqhkiG9w0BAQ0FADB6MQswCQYDVQQGEwJVUzELMAkGA1UE +CAwCQ0ExCzAJBgNVBAcMAlNGMQ8wDQYDVQQKDAZKb3llbnQxEDAOBgNVBAsMB05v +ZGUuanMxDDAKBgNVBAMMA2NhMjEgMB4GCSqGSIb3DQEJARYRcnlAdGlueWNsb3Vk +cy5vcmcXDTI2MDQxNjA0MDM1MloYDzIwNTMwOTAxMDQwMzUyWjAnMCUCFHtnB1Iw +05rTKjL+Xc+x+pXi6jGdFw0yNjA0MTYwNDAzNTJaoA4wDDAKBgNVHRQEAwIBATAN +BgkqhkiG9w0BAQ0FAAOCAQEAS7PnQxPHv+VXvmCOcTQOYWns16+G5cmaY8/fYjwM +6zOQPTItJTH+S2EJ3JvqES3Xm3KH+2Qh/8gAiiGNL9zdBpuNcJyUlJpIPuvWPd0P +Bup7u2YEvc9NjuP8thslf267A8tieFf4mF+AO1lvFp+CGoyRSwtNGOWCMkFDGgGn +ZOVXw5Q782PhUwThozGjR40zDkNjW/uFPJjMkz/RZFEmWshGf9t3VzahRs8PUApr +XTdatufBUPrWiTWyQAuME50ajzq/tfuj2kokqfOvy1mkoNwtySVxKSlwGjejd5Xj +yV/v4a5FDjXw4AwqEe+Cul9J2eyBb1jHkc+R9rutHTKEZA== +-----END X509 CRL----- diff --git a/test/fixtures/system-ca/fetch-worker.mjs b/test/fixtures/system-ca/fetch-worker.mjs new file mode 100644 index 00000000000000..1da8bac4fbcb56 --- /dev/null +++ b/test/fixtures/system-ca/fetch-worker.mjs @@ -0,0 +1,14 @@ +import { parentPort, workerData } from 'node:worker_threads'; + +const { url } = workerData; +try { + const res = await fetch(url); + const text = await res.text(); + parentPort.postMessage({ ok: true, status: res.status, text }); +} catch (err) { + parentPort.postMessage({ + ok: false, + code: err?.cause?.code, + message: err.message, + }); +} diff --git a/test/fixtures/tls-get-ca-certificates-worker.js b/test/fixtures/tls-get-ca-certificates-worker.js new file mode 100644 index 00000000000000..1d05fcacac54da --- /dev/null +++ b/test/fixtures/tls-get-ca-certificates-worker.js @@ -0,0 +1,10 @@ +'use strict'; + +const tls = require('tls'); +const { parentPort } = require('worker_threads'); + +parentPort.postMessage({ + bundledLen: tls.getCACertificates('bundled').length, + systemLen: tls.getCACertificates('system').length, + defaultLen: tls.getCACertificates('default').length, +}); diff --git a/test/parallel/test-cli-node-options.js b/test/parallel/test-cli-node-options.js index 0d17fc73dd4d02..90c399790f65a4 100644 --- a/test/parallel/test-cli-node-options.js +++ b/test/parallel/test-cli-node-options.js @@ -68,7 +68,7 @@ if (common.hasCrypto) { if (!hasOpenSSL3) expectNoWorker('--openssl-config=_ossl_cfg', 'B\n'); if (common.isMacOS) { - expectNoWorker('--use-system-ca', 'B\n'); + expect('--use-system-ca', 'B\n'); } } diff --git a/test/parallel/test-fs-promises-file-handle-pull.js b/test/parallel/test-fs-promises-file-handle-pull.js new file mode 100644 index 00000000000000..3fc531baf7138b --- /dev/null +++ b/test/parallel/test-fs-promises-file-handle-pull.js @@ -0,0 +1,440 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const { open } = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const { text, bytes } = require('stream/iter'); + +tmpdir.refresh(); + +const tmpDir = tmpdir.path; + +// ============================================================================= +// Basic pull() +// ============================================================================= + +async function testBasicPull() { + const filePath = path.join(tmpDir, 'pull-basic.txt'); + fs.writeFileSync(filePath, 'hello from file'); + + const fh = await open(filePath, 'r'); + try { + const readable = fh.pull(); + const data = await text(readable); + assert.strictEqual(data, 'hello from file'); + } finally { + await fh.close(); + } +} + +async function testPullBinary() { + const filePath = path.join(tmpDir, 'pull-binary.bin'); + const buf = Buffer.alloc(256); + for (let i = 0; i < 256; i++) buf[i] = i; + fs.writeFileSync(filePath, buf); + + const fh = await open(filePath, 'r'); + try { + const readable = fh.pull(); + const data = await bytes(readable); + assert.strictEqual(data.byteLength, 256); + for (let i = 0; i < 256; i++) { + assert.strictEqual(data[i], i); + } + } finally { + await fh.close(); + } +} + +async function testPullEmptyFile() { + const filePath = path.join(tmpDir, 'pull-empty.txt'); + fs.writeFileSync(filePath, ''); + + const fh = await open(filePath, 'r'); + try { + const readable = fh.pull(); + const data = await bytes(readable); + assert.strictEqual(data.byteLength, 0); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// Large file (multi-chunk) +// ============================================================================= + +async function testPullLargeFile() { + const filePath = path.join(tmpDir, 'pull-large.bin'); + // Write 64KB - enough for multiple 16KB read chunks + const size = 64 * 1024; + const buf = Buffer.alloc(size, 0x42); + fs.writeFileSync(filePath, buf); + + const fh = await open(filePath, 'r'); + try { + const readable = fh.pull(); + const data = await bytes(readable); + assert.strictEqual(data.byteLength, size); + // Verify content + for (let i = 0; i < data.byteLength; i++) { + assert.strictEqual(data[i], 0x42); + } + } finally { + await fh.close(); + } +} + +// ============================================================================= +// With transforms +// ============================================================================= + +async function testPullWithTransform() { + const filePath = path.join(tmpDir, 'pull-transform.txt'); + fs.writeFileSync(filePath, 'hello'); + + const fh = await open(filePath, 'r'); + try { + const upper = (chunks) => { + if (chunks === null) return null; + return chunks.map((c) => { + const str = new TextDecoder().decode(c); + return new TextEncoder().encode(str.toUpperCase()); + }); + }; + + const readable = fh.pull(upper); + const data = await text(readable); + assert.strictEqual(data, 'HELLO'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// autoClose option +// ============================================================================= + +async function testPullAutoClose() { + const filePath = path.join(tmpDir, 'pull-autoclose.txt'); + fs.writeFileSync(filePath, 'auto close data'); + + const fh = await open(filePath, 'r'); + const readable = fh.pull({ autoClose: true }); + const data = await text(readable); + assert.strictEqual(data, 'auto close data'); + + // After consuming with autoClose, the file handle should be closed + // Trying to read again should throw + await assert.rejects( + async () => { + await fh.stat(); + }, + (err) => err.code === 'ERR_INVALID_STATE' || err.code === 'EBADF', + ); +} + +// ============================================================================= +// Locking +// ============================================================================= + +async function testPullLocking() { + const filePath = path.join(tmpDir, 'pull-lock.txt'); + fs.writeFileSync(filePath, 'lock data'); + + const fh = await open(filePath, 'r'); + try { + // First pull locks the handle + const readable = fh.pull(); + + // Second pull while locked should throw + assert.throws( + () => fh.pull(), + { code: 'ERR_INVALID_STATE' }, + ); + + // Consume the first stream to unlock + await text(readable); + + // Now it should be usable again + const readable2 = fh.pull(); + const data = await text(readable2); + assert.strictEqual(data, ''); // Already read to end + } finally { + await fh.close(); + } +} + +// ============================================================================= +// Closed handle +// ============================================================================= + +async function testPullClosedHandle() { + const filePath = path.join(tmpDir, 'pull-closed.txt'); + fs.writeFileSync(filePath, 'data'); + + const fh = await open(filePath, 'r'); + await fh.close(); + + assert.throws( + () => fh.pull(), + { code: 'ERR_INVALID_STATE' }, + ); +} + +// ============================================================================= +// AbortSignal +// ============================================================================= + +async function testPullAbortSignal() { + const filePath = path.join(tmpDir, 'pull-abort.txt'); + // Write enough data that we can abort mid-stream + fs.writeFileSync(filePath, 'a'.repeat(1024)); + + const ac = new AbortController(); + const fh = await open(filePath, 'r'); + try { + ac.abort(); + const readable = fh.pull({ signal: ac.signal }); + + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { + assert.fail('Should not reach here'); + } + }, + (err) => err.name === 'AbortError', + ); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// Iterate batches directly +// ============================================================================= + +async function testPullIterateBatches() { + const filePath = path.join(tmpDir, 'pull-batches.txt'); + fs.writeFileSync(filePath, 'batch data'); + + const fh = await open(filePath, 'r'); + try { + const readable = fh.pull(); + const batches = []; + for await (const batch of readable) { + batches.push(batch); + // Each batch should be an array of Uint8Array + assert.ok(Array.isArray(batch)); + for (const chunk of batch) { + assert.ok(chunk instanceof Uint8Array); + } + } + assert.ok(batches.length > 0); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pull() with start option - read from specific position +// ============================================================================= + +async function testPullStart() { + const filePath = path.join(tmpDir, 'pull-start.txt'); + fs.writeFileSync(filePath, 'AAABBBCCC'); + + const fh = await open(filePath, 'r'); + try { + // Read from offset 3 + const data = await text(fh.pull({ start: 3 })); + assert.strictEqual(data, 'BBBCCC'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pull() with limit option - read at most N bytes +// ============================================================================= + +async function testPullLimit() { + const filePath = path.join(tmpDir, 'pull-limit.txt'); + fs.writeFileSync(filePath, 'Hello, World! Extra data here.'); + + const fh = await open(filePath, 'r'); + try { + const data = await text(fh.pull({ limit: 13 })); + assert.strictEqual(data, 'Hello, World!'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pull() with start + limit - read a slice +// ============================================================================= + +async function testPullStartAndLimit() { + const filePath = path.join(tmpDir, 'pull-start-limit.txt'); + fs.writeFileSync(filePath, 'AAABBBCCCDDD'); + + const fh = await open(filePath, 'r'); + try { + // Read 3 bytes starting at offset 3 + const data = await text(fh.pull({ start: 3, limit: 3 })); + assert.strictEqual(data, 'BBB'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pull() with limit larger than file - reads whole file +// ============================================================================= + +async function testPullLimitLargerThanFile() { + const filePath = path.join(tmpDir, 'pull-limit-large.txt'); + fs.writeFileSync(filePath, 'short'); + + const fh = await open(filePath, 'r'); + try { + const data = await text(fh.pull({ limit: 1000000 })); + assert.strictEqual(data, 'short'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pull() with limit spanning multiple chunks +// ============================================================================= + +async function testPullLimitMultiChunk() { + const filePath = path.join(tmpDir, 'pull-limit-multi.bin'); + // 300KB file - spans multiple 128KB reads + const input = Buffer.alloc(300 * 1024, 'x'); + fs.writeFileSync(filePath, input); + + const fh = await open(filePath, 'r'); + try { + // Read exactly 200KB from offset 50KB + const data = await bytes(fh.pull({ start: 50 * 1024, limit: 200 * 1024 })); + assert.strictEqual(data.byteLength, 200 * 1024); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pull() with start + limit + transforms +// ============================================================================= + +async function testPullStartLimitWithTransforms() { + const filePath = path.join(tmpDir, 'pull-start-limit-transform.txt'); + fs.writeFileSync(filePath, 'aaabbbcccddd'); + + const fh = await open(filePath, 'r'); + try { + const { compressGzip, decompressGzip } = require('zlib/iter'); + const compressed = fh.pull(compressGzip(), { start: 3, limit: 6 }); + const decompressed = await text( + require('stream/iter').pull(compressed, decompressGzip())); + assert.strictEqual(decompressed, 'bbbccc'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pull() with chunkSize option +// ============================================================================= + +async function testPullChunkSize() { + const filePath = path.join(tmpDir, 'pull-chunksize.bin'); + // Write 64KB of data + const input = Buffer.alloc(64 * 1024, 'z'); + fs.writeFileSync(filePath, input); + + const fh = await open(filePath, 'r'); + try { + // Use 16KB chunks - should produce 4 batches + let batchCount = 0; + for await (const batch of fh.pull({ chunkSize: 16 * 1024 })) { + batchCount++; + for (const chunk of batch) { + assert.ok(chunk.byteLength <= 16 * 1024, + `Chunk ${chunk.byteLength} should be <= 16384`); + } + } + assert.strictEqual(batchCount, 4); + } finally { + await fh.close(); + } +} + +async function testPullChunkSizeSmall() { + const filePath = path.join(tmpDir, 'pull-chunksize-small.txt'); + fs.writeFileSync(filePath, 'hello'); + + const fh = await open(filePath, 'r'); + try { + // 1-byte chunks + let totalBytes = 0; + let batchCount = 0; + for await (const batch of fh.pull({ chunkSize: 1 })) { + batchCount++; + for (const chunk of batch) totalBytes += chunk.byteLength; + } + assert.strictEqual(totalBytes, 5); + assert.strictEqual(batchCount, 5); + } finally { + await fh.close(); + } +} + +async function testPullSyncArgumentValidation() { + const filePath = path.join(tmpDir, 'pull-arg-validation.txt'); + fs.writeFileSync(filePath, 'data'); + + const fh = await open(filePath, 'r'); + try { + assert.throws(() => fh.pull({ autoClose: 'no' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.pull({ start: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.pull({ limit: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.pull({ chunkSize: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.pull({ signal: {} }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.pull({ start: 1.1 }), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => fh.pull({ limit: 1.1 }), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => fh.pull({ chunkSize: 1.1 }), { code: 'ERR_OUT_OF_RANGE' }); + } finally { + await fh.close(); + } +} + +Promise.all([ + testBasicPull(), + testPullBinary(), + testPullEmptyFile(), + testPullLargeFile(), + testPullWithTransform(), + testPullAutoClose(), + testPullLocking(), + testPullClosedHandle(), + testPullAbortSignal(), + testPullIterateBatches(), + testPullStart(), + testPullLimit(), + testPullStartAndLimit(), + testPullLimitLargerThanFile(), + testPullLimitMultiChunk(), + testPullStartLimitWithTransforms(), + testPullChunkSize(), + testPullChunkSizeSmall(), + testPullSyncArgumentValidation(), +]).then(common.mustCall()); diff --git a/test/parallel/test-fs-promises-file-handle-pullsync.js b/test/parallel/test-fs-promises-file-handle-pullsync.js new file mode 100644 index 00000000000000..20c429972573d6 --- /dev/null +++ b/test/parallel/test-fs-promises-file-handle-pullsync.js @@ -0,0 +1,498 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const { open } = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const { + textSync, + bytesSync, + pipeToSync, + pullSync, +} = require('stream/iter'); +const { + compressGzipSync, + decompressGzipSync, +} = require('zlib/iter'); + +tmpdir.refresh(); + +const tmpDir = tmpdir.path; + +// ============================================================================= +// Basic pullSync() +// ============================================================================= + +async function testBasicPullSync() { + const filePath = path.join(tmpDir, 'pullsync-basic.txt'); + fs.writeFileSync(filePath, 'hello from sync file read'); + + const fh = await open(filePath, 'r'); + try { + const data = textSync(fh.pullSync()); + assert.strictEqual(data, 'hello from sync file read'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// Large file (multi-chunk) +// ============================================================================= + +async function testLargeFile() { + const filePath = path.join(tmpDir, 'pullsync-large.txt'); + const input = 'sync large data test. '.repeat(10000); + fs.writeFileSync(filePath, input); + + const fh = await open(filePath, 'r'); + try { + const data = textSync(fh.pullSync()); + assert.strictEqual(data, input); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// Binary data round-trip +// ============================================================================= + +async function testBinaryData() { + const filePath = path.join(tmpDir, 'pullsync-binary.bin'); + const input = Buffer.alloc(200000); + for (let i = 0; i < input.length; i++) input[i] = i & 0xff; + fs.writeFileSync(filePath, input); + + const fh = await open(filePath, 'r'); + try { + const data = bytesSync(fh.pullSync()); + assert.deepStrictEqual(Buffer.from(data), input); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pullSync with sync compression transform round-trip +// ============================================================================= + +async function testPullSyncWithCompression() { + const filePath = path.join(tmpDir, 'pullsync-compress-src.txt'); + const dstPath = path.join(tmpDir, 'pullsync-compress-dst.gz'); + const input = 'compress via sync pullSync. '.repeat(1000); + fs.writeFileSync(filePath, input); + + // Compress: pullSync -> compressGzipSync -> write to file + const srcFh = await open(filePath, 'r'); + const dstFh = await open(dstPath, 'w'); + try { + const w = dstFh.writer(); + pipeToSync(srcFh.pullSync(compressGzipSync()), w); + } finally { + await srcFh.close(); + await dstFh.close(); + } + + // Verify compressed file is smaller + const compressedSize = fs.statSync(dstPath).size; + assert.ok(compressedSize < Buffer.byteLength(input), + `Compressed ${compressedSize} should be < original ` + + `${Buffer.byteLength(input)}`); + + // Decompress and verify + const readFh = await open(dstPath, 'r'); + try { + const result = textSync(readFh.pullSync(decompressGzipSync())); + assert.strictEqual(result, input); + } finally { + await readFh.close(); + } +} + +// ============================================================================= +// pullSync with stateless transform +// ============================================================================= + +async function testPullSyncWithStatelessTransform() { + const filePath = path.join(tmpDir, 'pullsync-upper.txt'); + fs.writeFileSync(filePath, 'hello world'); + + const upper = (chunks) => { + if (chunks === null) return null; + const out = new Array(chunks.length); + for (let j = 0; j < chunks.length; j++) { + const src = chunks[j]; + const buf = Buffer.allocUnsafe(src.length); + for (let i = 0; i < src.length; i++) { + const b = src[i]; + buf[i] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b; + } + out[j] = buf; + } + return out; + }; + + const fh = await open(filePath, 'r'); + try { + const data = textSync(fh.pullSync(upper)); + assert.strictEqual(data, 'HELLO WORLD'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pullSync with mixed stateless + stateful transforms +// ============================================================================= + +async function testPullSyncMixedTransforms() { + const filePath = path.join(tmpDir, 'pullsync-mixed.txt'); + const input = 'mixed transform test '.repeat(500); + fs.writeFileSync(filePath, input); + + const upper = (chunks) => { + if (chunks === null) return null; + const out = new Array(chunks.length); + for (let j = 0; j < chunks.length; j++) { + const src = chunks[j]; + const buf = Buffer.allocUnsafe(src.length); + for (let i = 0; i < src.length; i++) { + const b = src[i]; + buf[i] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b; + } + out[j] = buf; + } + return out; + }; + + const fh = await open(filePath, 'r'); + try { + // Upper + compress + decompress + const data = textSync( + pullSync(fh.pullSync(upper, compressGzipSync()), decompressGzipSync()), + ); + assert.strictEqual(data, input.toUpperCase()); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// autoClose: true - handle closed after iteration completes +// ============================================================================= + +async function testAutoClose() { + const filePath = path.join(tmpDir, 'pullsync-autoclose.txt'); + fs.writeFileSync(filePath, 'auto close test'); + + const fh = await open(filePath, 'r'); + const data = textSync(fh.pullSync({ autoClose: true })); + assert.strictEqual(data, 'auto close test'); + + // Handle should be closed + await assert.rejects(fh.stat(), { code: 'EBADF' }); +} + +// ============================================================================= +// autoClose: true with early break +// ============================================================================= + +async function testAutoCloseEarlyBreak() { + const filePath = path.join(tmpDir, 'pullsync-autoclose-break.txt'); + fs.writeFileSync(filePath, 'x'.repeat(1000000)); + + const fh = await open(filePath, 'r'); + // eslint-disable-next-line no-unused-vars + for (const batch of fh.pullSync({ autoClose: true })) { + break; // Early exit + } + + // Handle should be closed by autoClose + await assert.rejects(fh.stat(), { code: 'EBADF' }); +} + +// ============================================================================= +// autoClose: false (default) - handle stays open +// ============================================================================= + +async function testNoAutoClose() { + const filePath = path.join(tmpDir, 'pullsync-no-autoclose.txt'); + fs.writeFileSync(filePath, 'still open'); + + const fh = await open(filePath, 'r'); + const data = textSync(fh.pullSync()); + assert.strictEqual(data, 'still open'); + + // Handle should still be open and reusable + const stat = await fh.stat(); + assert.ok(stat.size > 0); + await fh.close(); +} + +// ============================================================================= +// Lock semantics - pullSync locks the handle +// ============================================================================= + +async function testLocked() { + const filePath = path.join(tmpDir, 'pullsync-locked.txt'); + fs.writeFileSync(filePath, 'lock test'); + + const fh = await open(filePath, 'r'); + const iter = fh.pullSync()[Symbol.iterator](); + iter.next(); // Start iteration, handle is locked + + assert.throws(() => fh.pullSync(), { + code: 'ERR_INVALID_STATE', + }); + + assert.throws(() => fh.pull(), { + code: 'ERR_INVALID_STATE', + }); + + // Finish iteration to unlock + while (!iter.next().done) { /* drain */ } + await fh.close(); +} + +// ============================================================================= +// Empty file +// ============================================================================= + +async function testEmptyFile() { + const filePath = path.join(tmpDir, 'pullsync-empty.txt'); + fs.writeFileSync(filePath, ''); + + const fh = await open(filePath, 'r'); + try { + const data = textSync(fh.pullSync()); + assert.strictEqual(data, ''); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pipeToSync: file-to-file sync pipeline +// ============================================================================= + +async function testPipeToSync() { + const srcPath = path.join(tmpDir, 'pullsync-pipeto-src.txt'); + const dstPath = path.join(tmpDir, 'pullsync-pipeto-dst.txt'); + const input = 'pipeToSync test data '.repeat(200); + fs.writeFileSync(srcPath, input); + + const srcFh = await open(srcPath, 'r'); + const dstFh = await open(dstPath, 'w'); + try { + const w = dstFh.writer(); + pipeToSync(srcFh.pullSync(), w); + } finally { + await srcFh.close(); + await dstFh.close(); + } + + assert.strictEqual(fs.readFileSync(dstPath, 'utf8'), input); +} + +// ============================================================================= +// pullSync() with start option +// ============================================================================= + +async function testPullSyncStart() { + const filePath = path.join(tmpDir, 'pullsync-start.txt'); + fs.writeFileSync(filePath, 'AAABBBCCC'); + + const fh = await open(filePath, 'r'); + try { + const data = textSync(fh.pullSync({ start: 3 })); + assert.strictEqual(data, 'BBBCCC'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pullSync() with limit option +// ============================================================================= + +async function testPullSyncLimit() { + const filePath = path.join(tmpDir, 'pullsync-limit.txt'); + fs.writeFileSync(filePath, 'Hello, World! Extra data here.'); + + const fh = await open(filePath, 'r'); + try { + const data = textSync(fh.pullSync({ limit: 13 })); + assert.strictEqual(data, 'Hello, World!'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pullSync() with start + limit +// ============================================================================= + +async function testPullSyncStartAndLimit() { + const filePath = path.join(tmpDir, 'pullsync-start-limit.txt'); + fs.writeFileSync(filePath, 'AAABBBCCCDDD'); + + const fh = await open(filePath, 'r'); + try { + const data = textSync(fh.pullSync({ start: 3, limit: 3 })); + assert.strictEqual(data, 'BBB'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pullSync() with limit spanning multiple chunks +// ============================================================================= + +async function testPullSyncLimitMultiChunk() { + const filePath = path.join(tmpDir, 'pullsync-limit-multi.bin'); + const input = Buffer.alloc(300 * 1024, 'x'); + fs.writeFileSync(filePath, input); + + const fh = await open(filePath, 'r'); + try { + const data = bytesSync(fh.pullSync({ start: 50 * 1024, limit: 200 * 1024 })); + assert.strictEqual(data.byteLength, 200 * 1024); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pullSync() with start + limit + compression transform +// ============================================================================= + +async function testPullSyncStartLimitWithTransforms() { + const filePath = path.join(tmpDir, 'pullsync-start-limit-transform.txt'); + fs.writeFileSync(filePath, 'aaabbbcccddd'); + + const fh = await open(filePath, 'r'); + try { + const compressed = fh.pullSync(compressGzipSync(), + { start: 3, limit: 6 }); + const decompressed = textSync(pullSync(compressed, decompressGzipSync())); + assert.strictEqual(decompressed, 'bbbccc'); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// pullSync() with start + autoClose +// ============================================================================= + +async function testPullSyncStartAutoClose() { + const filePath = path.join(tmpDir, 'pullsync-start-autoclose.txt'); + fs.writeFileSync(filePath, 'AAABBBCCC'); + + const fh = await open(filePath, 'r'); + const data = textSync(fh.pullSync({ start: 3, autoClose: true })); + assert.strictEqual(data, 'BBBCCC'); + + // Handle should be closed + await assert.rejects(fh.stat(), { code: 'EBADF' }); +} + +// ============================================================================= +// pullSync() with chunkSize option +// ============================================================================= + +async function testPullSyncChunkSize() { + const filePath = path.join(tmpDir, 'pullsync-chunksize.bin'); + const input = Buffer.alloc(64 * 1024, 'z'); + fs.writeFileSync(filePath, input); + + const fh = await open(filePath, 'r'); + try { + let batchCount = 0; + for (const batch of fh.pullSync({ chunkSize: 16 * 1024 })) { + batchCount++; + for (const chunk of batch) { + assert.ok(chunk.byteLength <= 16 * 1024, + `Chunk ${chunk.byteLength} should be <= 16384`); + } + } + assert.strictEqual(batchCount, 4); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// writer() with chunkSize option (sync write threshold) +// ============================================================================= + +async function testWriterChunkSize() { + const filePath = path.join(tmpDir, 'pullsync-writer-chunksize.txt'); + const fh = await open(filePath, 'w'); + // Set chunkSize to 1024 - writes larger than this should fall back to async + const w = fh.writer({ chunkSize: 1024 }); + + // Small write should succeed sync + assert.strictEqual(w.writeSync(Buffer.alloc(512, 'a')), true); + + // Write larger than chunkSize should return false + assert.strictEqual(w.writeSync(Buffer.alloc(2048, 'b')), false); + + await w.end(); + await fh.close(); +} + +// ============================================================================= +// Argument validation +// ============================================================================= + +async function testPullArgumentValidation() { + const filePath = path.join(tmpDir, 'pull-arg-validation.txt'); + fs.writeFileSync(filePath, 'data'); + + const fh = await open(filePath, 'r'); + try { + assert.throws(() => fh.pullSync({ autoClose: 'no' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.pullSync({ start: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.pullSync({ limit: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.pullSync({ chunkSize: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.pullSync({ start: 1.1 }), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => fh.pullSync({ limit: 1.1 }), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => fh.pullSync({ chunkSize: 1.1 }), { code: 'ERR_OUT_OF_RANGE' }); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// Run all tests +// ============================================================================= + +Promise.all([ + testBasicPullSync(), + testLargeFile(), + testBinaryData(), + testPullSyncWithCompression(), + testPullSyncWithStatelessTransform(), + testPullSyncMixedTransforms(), + testAutoClose(), + testAutoCloseEarlyBreak(), + testNoAutoClose(), + testLocked(), + testEmptyFile(), + testPipeToSync(), + testPullSyncStart(), + testPullSyncLimit(), + testPullSyncStartAndLimit(), + testPullSyncLimitMultiChunk(), + testPullSyncStartLimitWithTransforms(), + testPullSyncStartAutoClose(), + testPullSyncChunkSize(), + testWriterChunkSize(), + testPullArgumentValidation(), +]).then(common.mustCall()); diff --git a/test/parallel/test-fs-promises-file-handle-writer.js b/test/parallel/test-fs-promises-file-handle-writer.js new file mode 100644 index 00000000000000..95ba8756fbe3cd --- /dev/null +++ b/test/parallel/test-fs-promises-file-handle-writer.js @@ -0,0 +1,1126 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const { open } = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const { + pipeTo, text, +} = require('stream/iter'); +const { + compressGzip, decompressGzip, +} = require('zlib/iter'); + +tmpdir.refresh(); + +const tmpDir = tmpdir.path; + +// ============================================================================= +// Basic write() +// ============================================================================= + +async function testBasicWrite() { + const filePath = path.join(tmpDir, 'writer-basic.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + await w.write(Buffer.from('Hello ')); + await w.write(Buffer.from('World!')); + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, 12); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'Hello World!'); +} + +// ============================================================================= +// Basic writev() +// ============================================================================= + +async function testBasicWritev() { + const filePath = path.join(tmpDir, 'writer-writev.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + await w.writev([ + Buffer.from('aaa'), + Buffer.from('bbb'), + Buffer.from('ccc'), + ]); + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, 9); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'aaabbbccc'); +} + +// ============================================================================= +// Mixed write() and writev() +// ============================================================================= + +async function testMixedWriteAndWritev() { + const filePath = path.join(tmpDir, 'writer-mixed.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + await w.write(Buffer.from('head-')); + await w.writev([Buffer.from('mid1-'), Buffer.from('mid2-')]); + await w.write(Buffer.from('tail')); + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, 19); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'head-mid1-mid2-tail'); +} + +// ============================================================================= +// end() returns totalBytesWritten +// ============================================================================= + +async function testEndReturnsTotalBytes() { + const filePath = path.join(tmpDir, 'writer-totalbytes.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + // Write some data in various sizes + const sizes = [100, 200, 300, 400, 500]; + let expected = 0; + for (const size of sizes) { + await w.write(Buffer.alloc(size, 0x41)); + expected += size; + } + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, expected); + assert.strictEqual(totalBytes, 1500); + assert.strictEqual(fs.statSync(filePath).size, 1500); +} + +// ============================================================================= +// autoClose: true - handle closed after end() +// ============================================================================= + +async function testAutoCloseOnEnd() { + const filePath = path.join(tmpDir, 'writer-autoclose-end.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer({ autoClose: true }); + await w.write(Buffer.from('auto close test')); + await w.end(); + + // Handle should be closed + await assert.rejects(fh.stat(), { code: 'EBADF' }); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'auto close test'); +} + +// ============================================================================= +// autoClose: true - handle closed after fail() +// ============================================================================= + +async function testAutoCloseOnFail() { + const filePath = path.join(tmpDir, 'writer-autoclose-fail.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer({ autoClose: true }); + await w.write(Buffer.from('partial')); + w.fail(new Error('test fail')); + + // Handle should be closed + await assert.rejects(fh.stat(), { code: 'EBADF' }); + // Partial data should still be on disk (fail doesn't truncate) + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'partial'); +} + +// ============================================================================= +// start option - write at specified offset +// ============================================================================= + +async function testStartOption() { + const filePath = path.join(tmpDir, 'writer-start.txt'); + // Pre-fill with 10 A's + fs.writeFileSync(filePath, 'AAAAAAAAAA'); + + const fh = await open(filePath, 'r+'); + const w = fh.writer({ start: 3 }); + await w.write(Buffer.from('BBB')); + await w.end(); + await fh.close(); + + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'AAABBBAAAA'); +} + +// ============================================================================= +// start option - sequential writes advance position +// ============================================================================= + +async function testStartSequentialPosition() { + const filePath = path.join(tmpDir, 'writer-start-seq.txt'); + fs.writeFileSync(filePath, 'XXXXXXXXXX'); + + const fh = await open(filePath, 'r+'); + const w = fh.writer({ start: 2 }); + await w.write(Buffer.from('AA')); + await w.write(Buffer.from('BB')); + await w.writev([Buffer.from('C'), Buffer.from('D')]); + await w.end(); + await fh.close(); + + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'XXAABBCDXX'); +} + +// ============================================================================= +// Locked state - can't create second writer while active +// ============================================================================= + +async function testLockedState() { + const filePath = path.join(tmpDir, 'writer-locked.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + assert.throws(() => fh.writer(), { + name: 'Error', + message: /locked/, + }); + + // Also can't pull while writer is active + assert.throws(() => fh.pull(), { + name: 'Error', + message: /locked/, + }); + + await w.end(); + await fh.close(); +} + +// ============================================================================= +// Unlock after end - handle reusable +// ============================================================================= + +async function testUnlockAfterEnd() { + const filePath = path.join(tmpDir, 'writer-unlock.txt'); + const fh = await open(filePath, 'w'); + + const w1 = fh.writer(); + await w1.write(Buffer.from('first')); + await w1.end(); + + // Should work - handle is unlocked + const w2 = fh.writer(); + await w2.write(Buffer.from(' second')); + await w2.end(); + await fh.close(); + + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'first second'); +} + +// ============================================================================= +// Unlock after fail - handle reusable +// ============================================================================= + +async function testUnlockAfterFail() { + const filePath = path.join(tmpDir, 'writer-unlock-fail.txt'); + const fh = await open(filePath, 'w'); + + const w1 = fh.writer(); + await w1.write(Buffer.from('failed')); + await w1.fail(new Error('test')); + + // Should work - handle is unlocked + const w2 = fh.writer(); + await w2.write(Buffer.from('recovered')); + await w2.end(); + await fh.close(); + + // 'recovered' is appended after 'failed' at current file offset + const content = fs.readFileSync(filePath, 'utf8'); + assert.ok(content.startsWith('failed')); + assert.ok(content.includes('recovered')); +} + +// ============================================================================= +// Write after end/fail rejects +// ============================================================================= + +async function testWriteAfterEndRejects() { + const filePath = path.join(tmpDir, 'writer-closed.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + await w.write(Buffer.from('data')); + await w.end(); + + await assert.rejects(w.write(Buffer.from('more')), { + name: 'TypeError', + message: /closed/, + }); + await assert.rejects(w.writev([Buffer.from('more')]), { + name: 'TypeError', + message: /closed/, + }); + + await fh.close(); +} + +// ============================================================================= +// Closed handle - writer() throws +// ============================================================================= + +async function testClosedHandle() { + const filePath = path.join(tmpDir, 'writer-closed-handle.txt'); + const fh = await open(filePath, 'w'); + await fh.close(); + + assert.throws(() => fh.writer(), { + name: 'Error', + message: /closed/, + }); +} + +// ============================================================================= +// pipeTo() integration - pipe source through writer +// ============================================================================= + +async function testPipeToIntegration() { + const srcPath = path.join(tmpDir, 'writer-pipeto-src.txt'); + const dstPath = path.join(tmpDir, 'writer-pipeto-dst.txt'); + const data = 'The quick brown fox jumps over the lazy dog.\n'.repeat(500); + fs.writeFileSync(srcPath, data); + + const rfh = await open(srcPath, 'r'); + const wfh = await open(dstPath, 'w'); + const w = wfh.writer(); + + const totalBytes = await pipeTo(rfh.pull(), w); + + await rfh.close(); + await wfh.close(); + + assert.strictEqual(totalBytes, Buffer.byteLength(data)); + assert.strictEqual(fs.readFileSync(dstPath, 'utf8'), data); +} + +// ============================================================================= +// pipeTo() with transforms - uppercase through writer +// ============================================================================= + +async function testPipeToWithTransform() { + const srcPath = path.join(tmpDir, 'writer-transform-src.txt'); + const dstPath = path.join(tmpDir, 'writer-transform-dst.txt'); + const data = 'hello world from transforms test\n'.repeat(200); + fs.writeFileSync(srcPath, data); + + function uppercase(chunks) { + if (chunks === null) return null; + const out = new Array(chunks.length); + for (let i = 0; i < chunks.length; i++) { + const src = chunks[i]; + const buf = Buffer.allocUnsafe(src.length); + for (let j = 0; j < src.length; j++) { + const b = src[j]; + buf[j] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b; + } + out[i] = buf; + } + return out; + } + + const rfh = await open(srcPath, 'r'); + const wfh = await open(dstPath, 'w'); + const w = wfh.writer(); + + await pipeTo(rfh.pull(), uppercase, w); + + await rfh.close(); + await wfh.close(); + + assert.strictEqual(fs.readFileSync(dstPath, 'utf8'), data.toUpperCase()); +} + +// ============================================================================= +// Round-trip: pull → compress → writer, pull → decompress → verify +// ============================================================================= + +async function testCompressRoundTrip() { + const srcPath = path.join(tmpDir, 'writer-rt-src.txt'); + const gzPath = path.join(tmpDir, 'writer-rt.gz'); + const original = 'Round trip compression test data. '.repeat(2000); + fs.writeFileSync(srcPath, original); + + // Compress: pull → gzip → writer + { + const rfh = await open(srcPath, 'r'); + const wfh = await open(gzPath, 'w'); + const w = wfh.writer({ autoClose: true }); + await pipeTo(rfh.pull(), compressGzip(), w); + await rfh.close(); + } + + // Verify compressed file is smaller + const compressedSize = fs.statSync(gzPath).size; + assert.ok(compressedSize < Buffer.byteLength(original), + `Compressed ${compressedSize} should be < original ${Buffer.byteLength(original)}`); + + // Decompress: pull → gunzip → text → verify + { + const rfh = await open(gzPath, 'r'); + const result = await text(rfh.pull(decompressGzip())); + await rfh.close(); + assert.strictEqual(result, original); + } +} + +// ============================================================================= +// Large file write - write 1MB in 64KB chunks +// ============================================================================= + +async function testLargeFileWrite() { + const filePath = path.join(tmpDir, 'writer-large.bin'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + const chunkSize = 65536; + const totalSize = 1024 * 1024; // 1MB + const chunk = Buffer.alloc(chunkSize, 0x42); + let written = 0; + + while (written < totalSize) { + await w.write(chunk); + written += chunkSize; + } + + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, totalSize); + assert.strictEqual(fs.statSync(filePath).size, totalSize); + + // Verify content + const data = fs.readFileSync(filePath); + for (let i = 0; i < data.length; i++) { + if (data[i] !== 0x42) { + assert.fail(`Byte at offset ${i} is ${data[i]}, expected 0x42`); + } + } +} + +// ============================================================================= +// Symbol.asyncDispose - await using +// ============================================================================= + +async function testAsyncDispose() { + const filePath = path.join(tmpDir, 'writer-async-dispose.txt'); + { + await using fh = await open(filePath, 'w'); + await using w = fh.writer({ autoClose: true }); + await w.write(Buffer.from('async dispose')); + } + // Both writer and file handle should be cleaned up + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'async dispose'); + + // Verify the handle is actually closed by trying to open a new one + // (if the old one were still open with a write lock on some OSes, + // this could fail - but it should succeed). + const fh2 = await open(filePath, 'r'); + await fh2.close(); +} + +// ============================================================================= +// Symbol.asyncDispose - cleanup on error (await using unwinds) +// ============================================================================= + +async function testAsyncDisposeOnError() { + const filePath = path.join(tmpDir, 'writer-dispose-error.txt'); + const fh = await open(filePath, 'w'); + + try { + await using w = fh.writer(); + await w.write(Buffer.from('before error')); + throw new Error('intentional'); + } catch (e) { + assert.strictEqual(e.message, 'intentional'); + } + + // If asyncDispose ran, the handle should be unlocked and reusable + const w2 = fh.writer(); + await w2.write(Buffer.from('after error')); + await w2.end(); + await fh.close(); + + const content = fs.readFileSync(filePath, 'utf8'); + assert.ok(content.includes('after error'), + `Expected 'after error' in ${JSON.stringify(content)}`); +} + +// ============================================================================= +// Pre-aborted signal rejects write/writev/end +// ============================================================================= + +async function testWriteWithAbortedSignalRejects() { + const filePath = path.join(tmpDir, 'writer-signal-write.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + const ac = new AbortController(); + ac.abort(); + + await assert.rejects( + w.write(Buffer.from('data'), { signal: ac.signal }), + { name: 'AbortError' }, + ); + + // Writer should still be usable after a signal rejection + await w.write(Buffer.from('ok')); + await w.end(); + await fh.close(); + + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'ok'); +} + +async function testWritevWithAbortedSignalRejects() { + const filePath = path.join(tmpDir, 'writer-signal-writev.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + const ac = new AbortController(); + ac.abort(); + + await assert.rejects( + w.writev([Buffer.from('a'), Buffer.from('b')], { signal: ac.signal }), + { name: 'AbortError' }, + ); + + await w.writev([Buffer.from('ok')]); + await w.end(); + await fh.close(); + + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'ok'); +} + +async function testEndWithAbortedSignalRejects() { + const filePath = path.join(tmpDir, 'writer-signal-end.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + await w.write(Buffer.from('data')); + + const ac = new AbortController(); + ac.abort(); + + await assert.rejects( + w.end({ signal: ac.signal }), + { name: 'AbortError' }, + ); + + // end() was rejected so writer is still open - end it cleanly + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, 4); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'data'); +} + +// ============================================================================= +// write() with string input (UTF-8 encoding) +// ============================================================================= + +async function testWriteString() { + const filePath = path.join(tmpDir, 'writer-string.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + await w.write('Hello '); + await w.write('World!'); + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, 12); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'Hello World!'); +} + +// ============================================================================= +// write() with string containing multi-byte UTF-8 characters +// ============================================================================= + +async function testWriteStringMultibyte() { + const filePath = path.join(tmpDir, 'writer-string-multibyte.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + const input = 'café ☕ 日本語'; + await w.write(input); + const totalBytes = await w.end(); + await fh.close(); + + const expected = Buffer.from(input, 'utf8'); + assert.strictEqual(totalBytes, expected.byteLength); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), input); +} + +// ============================================================================= +// writev() with string chunks (UTF-8 encoding) +// ============================================================================= + +async function testWritevStrings() { + const filePath = path.join(tmpDir, 'writer-writev-strings.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + await w.writev(['aaa', 'bbb', 'ccc']); + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, 9); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'aaabbbccc'); +} + +// ============================================================================= +// writev() with mixed string and Uint8Array chunks +// ============================================================================= + +async function testWritevMixed() { + const filePath = path.join(tmpDir, 'writer-writev-mixed.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + await w.writev(['hello', Buffer.from(' '), 'world']); + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, 11); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'hello world'); +} + +// ============================================================================= +// Symbol.dispose calls fail() +// ============================================================================= + +async function testSyncDispose() { + const filePath = path.join(tmpDir, 'writer-sync-dispose.txt'); + const fh = await open(filePath, 'w'); + + { + using w = fh.writer(); + await w.write(Buffer.from('before dispose')); + } + // Symbol.dispose calls fail(), which unlocks the handle. + // The handle should be reusable. + const w2 = fh.writer(); + await w2.write(Buffer.from('after dispose')); + await w2.end(); + await fh.close(); + + const content = fs.readFileSync(filePath, 'utf8'); + assert.ok(content.includes('after dispose'), + `Expected 'after dispose' in ${JSON.stringify(content)}`); +} + +// ============================================================================= +// Symbol.dispose on error unwind +// ============================================================================= + +async function testSyncDisposeOnError() { + const filePath = path.join(tmpDir, 'writer-sync-dispose-error.txt'); + const fh = await open(filePath, 'w'); + + try { + using w = fh.writer(); + await w.write(Buffer.from('data')); + throw new Error('intentional'); + } catch (e) { + assert.strictEqual(e.message, 'intentional'); + } + + // Handle should be unlocked and reusable after sync dispose + const w2 = fh.writer(); + await w2.write(Buffer.from('recovered')); + await w2.end(); + await fh.close(); + + const content = fs.readFileSync(filePath, 'utf8'); + assert.ok(content.includes('recovered'), + `Expected 'recovered' in ${JSON.stringify(content)}`); +} + +// ============================================================================= +// writeSync() basic +// ============================================================================= + +async function testWriteSyncBasic() { + const filePath = path.join(tmpDir, 'writer-writesync-basic.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + assert.strictEqual(w.writeSync('Hello '), true); + assert.strictEqual(w.writeSync(Buffer.from('World!')), true); + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, 12); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'Hello World!'); +} + +// ============================================================================= +// writevSync() basic +// ============================================================================= + +async function testWritevSyncBasic() { + const filePath = path.join(tmpDir, 'writer-writevsync-basic.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + assert.strictEqual(w.writevSync(['aaa', Buffer.from('bbb'), 'ccc']), true); + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, 9); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'aaabbbccc'); +} + +// ============================================================================= +// writeSync() returns false for large chunks +// ============================================================================= + +async function testWriteSyncLargeChunk() { + const filePath = path.join(tmpDir, 'writer-writesync-large.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + // Chunk larger than 131072 should return false + const bigChunk = Buffer.alloc(131073, 'x'); + assert.strictEqual(w.writeSync(bigChunk), false); + + // Chunk at exactly 131072 should succeed + const exactChunk = Buffer.alloc(131072, 'y'); + assert.strictEqual(w.writeSync(exactChunk), true); + + await w.end(); + await fh.close(); + + // Only the exact chunk should have been written + const content = fs.readFileSync(filePath); + assert.strictEqual(content.length, 131072); +} + +// ============================================================================= +// writeSync() returns false when async op is in flight +// ============================================================================= + +async function testWriteSyncReturnsFalseDuringAsync() { + const filePath = path.join(tmpDir, 'writer-writesync-async.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + // Start an async write but don't await yet + const p = w.write(Buffer.from('async')); + + // Sync write should return false because async is in flight + assert.strictEqual(w.writeSync(Buffer.from('sync')), false); + + await p; + + // After async completes, sync should work again + assert.strictEqual(w.writeSync(Buffer.from(' then sync')), true); + + await w.end(); + await fh.close(); + + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'async then sync'); +} + +// ============================================================================= +// writeSync() returns false on closed/errored writer +// ============================================================================= + +async function testWriteSyncClosedErrored() { + const filePath = path.join(tmpDir, 'writer-writesync-closed.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + await w.end(); + + // Should return false after end() + assert.strictEqual(w.writeSync(Buffer.from('data')), false); + await fh.close(); + + // Test errored state + const fh2 = await open(filePath, 'w'); + const w2 = fh2.writer(); + w2.fail(new Error('test')); + assert.strictEqual(w2.writeSync(Buffer.from('data')), false); + await fh2.close(); +} + +// ============================================================================= +// endSync() basic +// ============================================================================= + +async function testEndSyncBasic() { + const filePath = path.join(tmpDir, 'writer-endsync-basic.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + w.writeSync(Buffer.from('hello')); + const totalBytes = w.endSync(); + await fh.close(); + + assert.strictEqual(totalBytes, 5); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'hello'); +} + +// ============================================================================= +// endSync() returns -1 when async op is in flight +// ============================================================================= + +async function testEndSyncReturnsFalseDuringAsync() { + const filePath = path.join(tmpDir, 'writer-endsync-async.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + const p = w.write(Buffer.from('data')); + assert.strictEqual(w.endSync(), -1); + + await p; + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, 4); +} + +// ============================================================================= +// endSync() idempotent on closed writer +// ============================================================================= + +async function testEndSyncIdempotent() { + const filePath = path.join(tmpDir, 'writer-endsync-idempotent.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + w.writeSync(Buffer.from('data')); + const first = w.endSync(); + const second = w.endSync(); + + assert.strictEqual(first, 4); + assert.strictEqual(second, 4); // Idempotent + await fh.close(); +} + +// ============================================================================= +// endSync() with autoClose fires handle.close() +// ============================================================================= + +async function testEndSyncAutoClose() { + const filePath = path.join(tmpDir, 'writer-endsync-autoclose.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer({ autoClose: true }); + + w.writeSync(Buffer.from('auto')); + const totalBytes = w.endSync(); + + assert.strictEqual(totalBytes, 4); + + // Handle should be closed synchronously + await assert.rejects(fh.stat(), { code: 'EBADF' }); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'auto'); +} + +// ============================================================================= +// Full sync pipeline: writeSync + endSync (no async at all) +// ============================================================================= + +async function testFullSyncPipeline() { + const filePath = path.join(tmpDir, 'writer-full-sync.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + // Entirely synchronous write pipeline + w.writeSync('line 1\n'); + w.writeSync('line 2\n'); + w.writevSync(['line 3\n', 'line 4\n']); + const totalBytes = w.endSync(); + await fh.close(); + + assert.strictEqual(totalBytes, 28); + assert.strictEqual( + fs.readFileSync(filePath, 'utf8'), + 'line 1\nline 2\nline 3\nline 4\n', + ); +} + +// ============================================================================= +// end() rejects on errored writer +// ============================================================================= + +async function testEndRejectsOnErrored() { + const filePath = path.join(tmpDir, 'writer-end-errored.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + await w.write(Buffer.from('data')); + w.fail(new Error('test error')); + + await assert.rejects( + w.end(), + { message: 'test error' }, + ); + await fh.close(); +} + +// ============================================================================= +// end() is idempotent when closing/closed +// ============================================================================= + +async function testEndIdempotent() { + const filePath = path.join(tmpDir, 'writer-end-idempotent.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + await w.write(Buffer.from('data')); + + // Call end() twice concurrently - second should return same promise + const p1 = w.end(); + const p2 = w.end(); + const [bytes1, bytes2] = await Promise.all([p1, p2]); + + assert.strictEqual(bytes1, 4); + assert.strictEqual(bytes2, 4); + + // After closed, calling end() again returns totalBytesWritten + const bytes3 = await w.end(); + assert.strictEqual(bytes3, 4); + + await fh.close(); +} + +// ============================================================================= +// asyncDispose waits for pending end() when closing +// ============================================================================= + +async function testAsyncDisposeWhileClosing() { + const filePath = path.join(tmpDir, 'writer-dispose-closing.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer({ autoClose: true }); + + await w.write(Buffer.from('closing test')); + + // Start end() but don't await - writer is now "closing" + const endPromise = w.end(); + + // asyncDispose should wait for the pending end, not call fail() + await w[Symbol.asyncDispose](); + await endPromise; + + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), 'closing test'); +} + +// ============================================================================= +// asyncDispose calls fail() on open writer (not graceful cleanup) +// ============================================================================= + +async function testAsyncDisposeCallsFail() { + const filePath = path.join(tmpDir, 'writer-dispose-fails.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer(); + + await w.write(Buffer.from('some data')); + + // Dispose without end() - should call fail(), not graceful cleanup + await w[Symbol.asyncDispose](); + + // Writer should be in errored state - write should reject + await assert.rejects( + w.write(Buffer.from('more')), + (err) => err instanceof Error, + ); + + // Handle should be unlocked and reusable + const w2 = fh.writer(); + await w2.end(); + await fh.close(); +} + +// ============================================================================= +// writer() with limit - async write within limit succeeds +// ============================================================================= + +async function testWriterLimit() { + const filePath = path.join(tmpDir, 'writer-limit.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer({ limit: 10 }); + + await w.write(Buffer.from('12345')); // 5 bytes, 5 remaining + await w.write(Buffer.from('67890')); // 5 bytes, 0 remaining + const totalBytes = await w.end(); + await fh.close(); + + assert.strictEqual(totalBytes, 10); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), '1234567890'); +} + +// ============================================================================= +// writer() with limit - async write exceeding limit rejects +// ============================================================================= + +async function testWriterLimitExceeded() { + const filePath = path.join(tmpDir, 'writer-limit-exceeded.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer({ limit: 5 }); + + await w.write(Buffer.from('123')); // 3 bytes, 2 remaining + + await assert.rejects( + w.write(Buffer.from('45678')), // 5 bytes > 2 remaining + { code: 'ERR_OUT_OF_RANGE' }, + ); + + await w.end(); + await fh.close(); +} + +// ============================================================================= +// writer() with limit - writev exceeding limit rejects +// ============================================================================= + +async function testWriterLimitWritev() { + const filePath = path.join(tmpDir, 'writer-limit-writev.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer({ limit: 6 }); + + await w.writev([Buffer.from('ab'), Buffer.from('cd')]); // 4 bytes + + await assert.rejects( + w.writev([Buffer.from('ef'), Buffer.from('gh')]), // 4 bytes > 2 remaining + { code: 'ERR_OUT_OF_RANGE' }, + ); + + await w.end(); + await fh.close(); +} + +// ============================================================================= +// writer() with limit - writeSync returns false when exceeding limit +// ============================================================================= + +async function testWriterLimitWriteSync() { + const filePath = path.join(tmpDir, 'writer-limit-writesync.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer({ limit: 10 }); + + assert.strictEqual(w.writeSync(Buffer.from('12345')), true); // 5 ok + assert.strictEqual(w.writeSync(Buffer.from('678')), true); // 3 ok + assert.strictEqual(w.writeSync(Buffer.from('901')), false); // 3 > 2 remaining + + const totalBytes = w.endSync(); + await fh.close(); + + assert.strictEqual(totalBytes, 8); + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), '12345678'); +} + +// ============================================================================= +// writer() with limit - writevSync returns false when exceeding limit +// ============================================================================= + +async function testWriterLimitWritevSync() { + const filePath = path.join(tmpDir, 'writer-limit-writevsync.txt'); + const fh = await open(filePath, 'w'); + const w = fh.writer({ limit: 5 }); + + assert.strictEqual(w.writevSync([Buffer.from('ab')]), true); + // 4 bytes > 3 remaining + assert.strictEqual( + w.writevSync([Buffer.from('cd'), Buffer.from('ef')]), false); + + w.endSync(); + await fh.close(); +} + +// ============================================================================= +// writer() with limit + start +// ============================================================================= + +async function testWriterLimitAndStart() { + const filePath = path.join(tmpDir, 'writer-limit-start.txt'); + // Pre-fill file with dots + fs.writeFileSync(filePath, '...........'); // 11 dots + + const fh = await open(filePath, 'r+'); + const w = fh.writer({ start: 3, limit: 5 }); + + await w.write(Buffer.from('HELLO')); // Write at offset 3 + await w.end(); + await fh.close(); + + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), '...HELLO...'); +} + +// ============================================================================= +// Argument validation +// ============================================================================= + +async function testWriterArgumentValidation() { + const filePath = path.join(tmpDir, 'pull-arg-validation.txt'); + fs.writeFileSync(filePath, 'data'); + + const fh = await open(filePath, 'r'); + try { + assert.throws(() => fh.writer({ autoClose: 'no' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.writer({ start: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.writer({ limit: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.writer({ chunkSize: 'a' }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => fh.writer({ start: 1.1 }), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => fh.writer({ limit: 1.1 }), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => fh.writer({ chunkSize: 1.1 }), { code: 'ERR_OUT_OF_RANGE' }); + } finally { + await fh.close(); + } +} + +// ============================================================================= +// Run all tests +// ============================================================================= + +Promise.all([ + testBasicWrite(), + testBasicWritev(), + testMixedWriteAndWritev(), + testEndReturnsTotalBytes(), + testAutoCloseOnEnd(), + testAutoCloseOnFail(), + testStartOption(), + testStartSequentialPosition(), + testLockedState(), + testUnlockAfterEnd(), + testUnlockAfterFail(), + testWriteAfterEndRejects(), + testClosedHandle(), + testPipeToIntegration(), + testPipeToWithTransform(), + testCompressRoundTrip(), + testLargeFileWrite(), + testAsyncDispose(), + testAsyncDisposeOnError(), + testWriteWithAbortedSignalRejects(), + testWritevWithAbortedSignalRejects(), + testEndWithAbortedSignalRejects(), + testWriteString(), + testWriteStringMultibyte(), + testWritevStrings(), + testWritevMixed(), + testSyncDispose(), + testSyncDisposeOnError(), + testWriteSyncBasic(), + testWritevSyncBasic(), + testWriteSyncLargeChunk(), + testWriteSyncReturnsFalseDuringAsync(), + testWriteSyncClosedErrored(), + testEndSyncBasic(), + testEndSyncReturnsFalseDuringAsync(), + testEndSyncIdempotent(), + testEndSyncAutoClose(), + testFullSyncPipeline(), + testEndRejectsOnErrored(), + testEndIdempotent(), + testAsyncDisposeWhileClosing(), + testAsyncDisposeCallsFail(), + testWriterLimit(), + testWriterLimitExceeded(), + testWriterLimitWritev(), + testWriterLimitWriteSync(), + testWriterLimitWritevSync(), + testWriterLimitAndStart(), + testWriterArgumentValidation(), +]).then(common.mustCall()); diff --git a/test/parallel/test-openssl-ca-options.js b/test/parallel/test-openssl-ca-options.js index b51b0ecf698035..d05884cf5ba3c5 100644 --- a/test/parallel/test-openssl-ca-options.js +++ b/test/parallel/test-openssl-ca-options.js @@ -19,6 +19,17 @@ assert.strictEqual(result.stderr, `${process.execPath ); assert.strictEqual(result.status, 9); +const useSystemCA = childProcess.spawnSync( + process.execPath, + [ '--use-openssl-ca', '--use-system-ca', '-p', 'process.version' ], + { encoding: 'utf8' } +); + +assert.strictEqual(useSystemCA.stderr, `${process.execPath +}: either --use-openssl-ca or --use-system-ca can be used, not both${os.EOL}` +); +assert.strictEqual(useSystemCA.status, 9); + const useBundledCA = childProcess.spawnSync(process.execPath, [ '--use-bundled-ca', '-p', 'process.version']); diff --git a/test/parallel/test-quic-address-validation.mjs b/test/parallel/test-quic-address-validation.mjs new file mode 100644 index 00000000000000..6be0681ff49902 --- /dev/null +++ b/test/parallel/test-quic-address-validation.mjs @@ -0,0 +1,49 @@ +// Flags: --experimental-quic --no-warnings + +// Test: validateAddress triggers Retry flow. +// When the server endpoint has validateAddress: true, it should send +// a Retry packet before accepting the connection. The handshake still +// completes successfully. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect, QuicEndpoint } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const endpoint = new QuicEndpoint({ validateAddress: true }); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + const info = await serverSession.opened; + // The handshake should complete despite the Retry flow. + strictEqual(info.protocol, 'quic-test'); + serverSession.close(); +}), { + endpoint, + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], +}); + +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + servername: 'localhost', +}); + +const info = await clientSession.opened; +strictEqual(info.protocol, 'quic-test'); + +// The serverEndpoint must be closed after we wait for the clientSession to close. +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-alpn-h3.mjs b/test/parallel/test-quic-alpn-h3.mjs index 9a473352d7ed87..e9adca59d1aeca 100644 --- a/test/parallel/test-quic-alpn-h3.mjs +++ b/test/parallel/test-quic-alpn-h3.mjs @@ -4,6 +4,9 @@ import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; import * as fixtures from '../common/fixtures.mjs'; +const { strictEqual, notStrictEqual } = assert; +const { readKey } = fixtures; + if (!hasQuic) { skip('QUIC is not enabled'); } @@ -11,34 +14,35 @@ if (!hasQuic) { const { listen, connect } = await import('node:quic'); const { createPrivateKey } = await import('node:crypto'); -const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); -const cert = fixtures.readKey('agent1-cert.pem'); +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); // Test h3 ALPN negotiation with Http3ApplicationImpl. // Both server and client use the default ALPN (h3). const serverOpened = Promise.withResolvers(); -const clientOpened = Promise.withResolvers(); - -const serverEndpoint = await listen(mustCall((serverSession) => { - serverSession.opened.then(mustCall((info) => { - assert.strictEqual(info.protocol, 'h3'); - serverOpened.resolve(); - serverSession.close(); - })); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + const info = await serverSession.opened; + strictEqual(info.protocol, 'h3'); + serverOpened.resolve(); + serverSession.close(); }), { sni: { '*': { keys: [key], certs: [cert] } }, }); -assert.ok(serverEndpoint.address !== undefined); +notStrictEqual(serverEndpoint.address, undefined); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); -clientSession.opened.then(mustCall((info) => { - assert.strictEqual(info.protocol, 'h3'); - clientOpened.resolve(); -})); -await Promise.all([serverOpened.promise, clientOpened.promise]); -clientSession.close(); +async function checkClient() { + const info = await clientSession.opened; + strictEqual(info.protocol, 'h3'); +} + +await Promise.all([serverOpened.promise, checkClient()]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-alpn-mismatch.mjs b/test/parallel/test-quic-alpn-mismatch.mjs new file mode 100644 index 00000000000000..64d8e1f0006380 --- /dev/null +++ b/test/parallel/test-quic-alpn-mismatch.mjs @@ -0,0 +1,55 @@ +// Flags: --experimental-quic --no-warnings + +// Test: ALPN mismatch causes connection failure. +// The server offers 'quic-test' but the client requests 'nonexistent'. +// The handshake should fail with a `no_application_protocol` alert. +// +// The QUIC transport error code for a CRYPTO_ERROR carrying a TLS alert is +// 0x100 | . For `no_application_protocol` (alert 120 / 0x78) this +// is 0x178 == 376. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual, match } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const expected = { + code: 'ERR_QUIC_TRANSPORT_ERROR', + errorCode: 376n, + message: /no application protocol/ +}; + +const onerror = mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + strictEqual(err.errorCode, 376n); + match(err.message, /no application protocol/); +}, 2); +const transportParams = { maxIdleTimeout: 1 }; + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await rejects(serverSession.opened, expected); + await rejects(serverSession.closed, expected); +}), { + transportParams, + onerror, +}); + +// Client requests an ALPN the server doesn't offer. +const clientSession = await connect(serverEndpoint.address, { + alpn: 'nonexistent-protocol', + transportParams, + onerror, +}); + +await rejects(clientSession.opened, expected); + +// The handshake should fail — opened may reject or never resolve. +// The session should close with an error. +await rejects(clientSession.closed, expected); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-alpn.mjs b/test/parallel/test-quic-alpn.mjs index b5eedf65373e1c..020fea3d308a86 100644 --- a/test/parallel/test-quic-alpn.mjs +++ b/test/parallel/test-quic-alpn.mjs @@ -4,6 +4,9 @@ import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; import * as fixtures from '../common/fixtures.mjs'; +const { notStrictEqual, strictEqual } = assert; +const { readKey } = fixtures; + if (!hasQuic) { skip('QUIC is not enabled'); } @@ -11,37 +14,36 @@ if (!hasQuic) { const { listen, connect } = await import('node:quic'); const { createPrivateKey } = await import('node:crypto'); -const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); -const cert = fixtures.readKey('agent1-cert.pem'); +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); // Server offers multiple ALPNs. Client requests one that the server supports. // Verify the negotiated protocol matches on both sides. const serverOpened = Promise.withResolvers(); -const clientOpened = Promise.withResolvers(); - -const serverEndpoint = await listen(mustCall((serverSession) => { - serverSession.opened.then(mustCall((info) => { - // The server should negotiate proto-b (client's choice from server's list) - assert.strictEqual(info.protocol, 'proto-b'); - serverOpened.resolve(); - serverSession.close(); - })); + +async function checkSession(session) { + const info = await session.opened; + // The client should negotiate proto-b (the only protocol it requested) + strictEqual(info.protocol, 'proto-b'); +} + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await checkSession(serverSession); + serverOpened.resolve(); }), { sni: { '*': { keys: [key], certs: [cert] } }, alpn: ['proto-a', 'proto-b', 'proto-c'], }); -assert.ok(serverEndpoint.address !== undefined); +notStrictEqual(serverEndpoint.address, undefined); const clientSession = await connect(serverEndpoint.address, { alpn: 'proto-b', servername: 'localhost', + verifyPeer: 'manual', }); -clientSession.opened.then(mustCall((info) => { - assert.strictEqual(info.protocol, 'proto-b'); - clientOpened.resolve(); -})); -await Promise.all([serverOpened.promise, clientOpened.promise]); -clientSession.close(); +await Promise.all([serverOpened.promise, checkSession(clientSession)]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-blocklist.mjs b/test/parallel/test-quic-blocklist.mjs new file mode 100644 index 00000000000000..5a3d4e98310f4b --- /dev/null +++ b/test/parallel/test-quic-blocklist.mjs @@ -0,0 +1,79 @@ +// Flags: --experimental-quic --no-warnings + +// Test: endpoint block list filtering. +// Deny mode: packets from blocked addresses are dropped. +// Allow mode: only packets from listed addresses are accepted. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import net from 'node:net'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// --- Deny mode: block 127.0.0.1 --- +{ + const blockList = new net.BlockList(); + blockList.addAddress('127.0.0.1'); + + const serverEndpoint = await listen(mustNotCall(), { + endpoint: { + blockList, + blockListPolicy: 'deny', + }, + }); + + // Connecting from 127.0.0.1 should be silently dropped. + // The client will time out waiting for a response. + const clientSession = await connect(serverEndpoint.address, { + handshakeTimeout: 500, + verifyPeer: 'manual', + onerror: mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + }), + }); + + // The session should fail — the server never sees the packet. + await rejects(clientSession.opened, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); + + // Verify the stat counter. + ok(serverEndpoint.stats.packetsBlocked > 0n, + 'packetsBlocked should be non-zero'); + + await serverEndpoint.close(); +} + +// --- Allow mode: only allow 127.0.0.1 --- +{ + const allowList = new net.BlockList(); + allowList.addAddress('127.0.0.1'); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + serverSession.close(); + }), { + endpoint: { + blockList: allowList, + blockListPolicy: 'allow', + }, + }); + + // Connecting from 127.0.0.1 should succeed — it's in the allow list. + const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', + }); + await clientSession.opened; + await clientSession.close(); + + strictEqual(serverEndpoint.stats.packetsBlocked, 0n, + 'No packets should be blocked for allowed address'); + + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-callback-error-onblocked.mjs b/test/parallel/test-quic-callback-error-onblocked.mjs new file mode 100644 index 00000000000000..db61c378f46ad0 --- /dev/null +++ b/test/parallel/test-quic-callback-error-onblocked.mjs @@ -0,0 +1,46 @@ +// Flags: --experimental-quic --no-warnings + +// Test: onblocked callback error handling. +// A sync throw in stream.onblocked destroys the stream via +// safeCallbackInvoke. The stream.closed promise rejects with the error. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const testError = new Error('onblocked throw'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { + // Small stream window to trigger flow control blocking. + transportParams: { + maxIdleTimeout: 1, + initialMaxStreamDataBidiRemote: 256, + }, +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, +}); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); + +stream.onblocked = mustCall(() => { + throw testError; +}); + +// Body larger than the 256-byte flow control window triggers onblocked. +stream.setBody(new Uint8Array(4096)); + +// The stream's closed promise should reject with the error from the throw. +await rejects(stream.closed, testError); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-ondatagram-async.mjs b/test/parallel/test-quic-callback-error-ondatagram-async.mjs new file mode 100644 index 00000000000000..23eade07161c2c --- /dev/null +++ b/test/parallel/test-quic-callback-error-ondatagram-async.mjs @@ -0,0 +1,42 @@ +// Flags: --experimental-quic --no-warnings + +// Test: async rejection in ondatagram destroys session. +// safeCallbackInvoke detects the returned promise and attaches a +// rejection handler that calls session.destroy(err). The error is +// delivered to the onerror callback. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const testError = new Error('async ondatagram rejection'); +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await assert.rejects(serverSession.closed, testError); + serverDone.resolve(); +}), { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall(async () => { + throw testError; + }), + onerror: mustCall((err) => { + assert.strictEqual(err, testError); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 }, +}); +await clientSession.opened; + +await clientSession.sendDatagram(new Uint8Array([1, 2, 3])); + +await serverDone.promise; +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-ondatagram.mjs b/test/parallel/test-quic-callback-error-ondatagram.mjs new file mode 100644 index 00000000000000..69d1440ed49da6 --- /dev/null +++ b/test/parallel/test-quic-callback-error-ondatagram.mjs @@ -0,0 +1,45 @@ +// Flags: --experimental-quic --no-warnings + +// Test: ondatagram callback error handling. +// A sync throw in ondatagram destroys the session via safeCallbackInvoke. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const testError = new Error('ondatagram throw'); +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + // The session is destroyed by the ondatagram throw. The closed promise + // rejects with testError. Verify that and signal completion. + await rejects(serverSession.closed, testError); + serverDone.resolve(); +}), { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram() { + throw testError; + }, + onerror: mustCall((err) => { + strictEqual(err, testError); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 }, +}); +await clientSession.opened; + +// Send a datagram to trigger the server's ondatagram callback. +await clientSession.sendDatagram(new Uint8Array([1, 2, 3])); + +await serverDone.promise; +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-ondatagramstatus.mjs b/test/parallel/test-quic-callback-error-ondatagramstatus.mjs new file mode 100644 index 00000000000000..c03cacdefb03b4 --- /dev/null +++ b/test/parallel/test-quic-callback-error-ondatagramstatus.mjs @@ -0,0 +1,41 @@ +// Flags: --experimental-quic --no-warnings + +// Test: ondatagramstatus callback error handling. +// A sync throw in ondatagramstatus destroys the session via safeCallbackInvoke. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const testError = new Error('ondatagramstatus throw'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { + transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 }, +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 }, + ondatagramstatus() { + throw testError; + }, + onerror: mustCall((err) => { + strictEqual(err, testError); + }), +}); +await clientSession.opened; + +// Send a datagram. The status callback fires when the peer ACKs it. +await clientSession.sendDatagram(new Uint8Array([1, 2, 3])); + +// The session's closed should reject with the error from the throw. +await rejects(clientSession.closed, testError); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-onerror-option.mjs b/test/parallel/test-quic-callback-error-onerror-option.mjs new file mode 100644 index 00000000000000..ecee9c3f81ed07 --- /dev/null +++ b/test/parallel/test-quic-callback-error-onerror-option.mjs @@ -0,0 +1,37 @@ +// Flags: --experimental-quic --no-warnings + +// Test: onerror set via connect() options. +// The onerror callback can be provided in the options object at +// session creation time to avoid race conditions with errors that +// occur during or immediately after the handshake. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const transportParams = { maxIdleTimeout: 1 }; +const testError = new Error('destroy with error'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { transportParams }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams, + onerror: mustCall((err) => { + strictEqual(err, testError); + }), +}); +await clientSession.opened; + +clientSession.destroy(testError); + +await rejects(clientSession.closed, testError); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-onerror-validation.mjs b/test/parallel/test-quic-callback-error-onerror-validation.mjs new file mode 100644 index 00000000000000..f44d8c4830ad55 --- /dev/null +++ b/test/parallel/test-quic-callback-error-onerror-validation.mjs @@ -0,0 +1,63 @@ +// Flags: --experimental-quic --no-warnings + +// Test: onerror setter validation. +// Setting onerror to a non-function (including null) throws. +// Setting to undefined clears it. Setting to a function works. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, throws } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const errorCheck = { + code: 'ERR_INVALID_ARG_TYPE', +}; + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + + // Session onerror validation: non-functions throw. + throws(() => { serverSession.onerror = 'not a function'; }, errorCheck); + throws(() => { serverSession.onerror = 42; }, errorCheck); + throws(() => { serverSession.onerror = null; }, errorCheck); + + // Setting to a function works. + const fn = () => {}; + serverSession.onerror = fn; + // The getter returns the bound version, not the original. + strictEqual(typeof serverSession.onerror, 'function'); + + // Setting to undefined clears it. + serverSession.onerror = undefined; + strictEqual(serverSession.onerror, undefined); + + serverSession.close(); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Client-side stream onerror validation. +const stream = await clientSession.createBidirectionalStream({ + body: new TextEncoder().encode('x'), +}); + +throws(() => { stream.onerror = 'not a function'; }, errorCheck); +throws(() => { stream.onerror = 42; }, errorCheck); +throws(() => { stream.onerror = null; }, errorCheck); + +// Setting to a function works. +stream.onerror = () => {}; +strictEqual(typeof stream.onerror, 'function'); + +// Setting to undefined clears it. +stream.onerror = undefined; +strictEqual(stream.onerror, undefined); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-onerror.mjs b/test/parallel/test-quic-callback-error-onerror.mjs new file mode 100644 index 00000000000000..6306d36acf5f12 --- /dev/null +++ b/test/parallel/test-quic-callback-error-onerror.mjs @@ -0,0 +1,75 @@ +// Flags: --experimental-quic --no-warnings + +// Test: onerror callback behavior +// session.onerror fires when session is destroyed with error. +// session.onerror receives the original error as argument. +// session.closed rejects with the original error after onerror. +// session.onerror not called when destroy() has no error. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// quic.session.error fires when a session is destroyed with an error. +// It should fire once for the first client session (destroyed with error) +// and not for the second (destroyed without error). +dc.subscribe('quic.session.error', mustCall((msg) => { + ok(msg.session, 'session.error should include session'); + ok(msg.error, 'session.error should include error'); +})); + +const transportParams = { maxIdleTimeout: 1 }; + +// All tested using a single endpoint with two client sessions. +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}, 2), { transportParams }); + +// First client: destroy WITH error — onerror fires. +{ + const testError = new Error('destroy with error'); + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + await clientSession.opened; + + let onerrorCalled = false; + clientSession.onerror = mustCall((err) => { + // Receives the original error. + strictEqual(err, testError); + onerrorCalled = true; + }); + + clientSession.destroy(testError); + + // Onerror was called synchronously during destroy. + strictEqual(onerrorCalled, true); + + // Closed rejects with the original error. + await rejects(clientSession.closed, testError); +} + +// Second client: destroy WITHOUT error — onerror should NOT fire. +{ + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + await clientSession.opened; + + clientSession.onerror = mustNotCall('onerror should not be called'); + + clientSession.destroy(); + + // Closed resolves (no error). + await clientSession.closed; +} + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-onhandshake.mjs b/test/parallel/test-quic-callback-error-onhandshake.mjs new file mode 100644 index 00000000000000..7c69be5f69ee98 --- /dev/null +++ b/test/parallel/test-quic-callback-error-onhandshake.mjs @@ -0,0 +1,36 @@ +// Flags: --experimental-quic --no-warnings + +// Test: onhandshake callback error handling. +// A sync throw in onhandshake destroys the session via safeCallbackInvoke. +// The error is delivered to the onerror callback and the session's +// closed promise rejects with the error. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const testError = new Error('onhandshake throw'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { transportParams: { maxIdleTimeout: 1 } }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, + onhandshake() { + throw testError; + }, + onerror: mustCall((err) => { + assert.strictEqual(err, testError); + }), +}); + +// The session's closed should reject with the error from the throw. +await assert.rejects(clientSession.closed, testError); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-onnewtoken.mjs b/test/parallel/test-quic-callback-error-onnewtoken.mjs new file mode 100644 index 00000000000000..882752adc01dab --- /dev/null +++ b/test/parallel/test-quic-callback-error-onnewtoken.mjs @@ -0,0 +1,42 @@ +// Flags: --experimental-quic --no-warnings + +// Test: onnewtoken callback error handling. +// A sync throw in onnewtoken destroys the session via safeCallbackInvoke. +// The server submits a NEW_TOKEN after handshake completes; the client +// receives it via the onnewtoken callback. Since the session ticket and +// NEW_TOKEN both arrive after the handshake, session.opened is already +// resolved and there is no unhandled rejection / uncaught exception. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const testError = new Error('onnewtoken throw'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { transportParams: { maxIdleTimeout: 1 } }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, + onnewtoken() { + throw testError; + }, + onerror: mustCall((err) => { + strictEqual(err, testError); + }), +}); + +await clientSession.opened; + +// The session's closed should reject with the error from the throw. +await rejects(clientSession.closed, testError); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-onpathvalidation.mjs b/test/parallel/test-quic-callback-error-onpathvalidation.mjs new file mode 100644 index 00000000000000..d60033e3efe304 --- /dev/null +++ b/test/parallel/test-quic-callback-error-onpathvalidation.mjs @@ -0,0 +1,54 @@ +// Flags: --experimental-quic --no-warnings + +// Test: onpathvalidation callback error handling. +// A sync throw in onpathvalidation destroys the session via +// safeCallbackInvoke. The error is delivered to the onerror +// callback and the session's closed promise rejects. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const testError = new Error('onpathvalidation throw'); + +// The preferred endpoint never receives a new session — it only +// routes PATH_CHALLENGE packets via SessionManager. +const preferredEndpoint = await listen(mustNotCall(), {}); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + // The server session closes with a transport error when the + // client is destroyed by the throw. That's expected. + await rejects(serverSession.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); +}), { + transportParams: { + preferredAddressIpv4: preferredEndpoint.address, + }, +}); + +const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, + preferredAddressPolicy: 'use', + onpathvalidation() { + throw testError; + }, + onerror: mustCall((err) => { + // The error from the throw should be delivered here. + strictEqual(err, testError); + }), +}); +await clientSession.opened; + +// The session's closed should reject with the thrown error. +await rejects(clientSession.closed, testError); + +await serverEndpoint.close(); +await preferredEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-onreset.mjs b/test/parallel/test-quic-callback-error-onreset.mjs new file mode 100644 index 00000000000000..798d916be6f2e6 --- /dev/null +++ b/test/parallel/test-quic-callback-error-onreset.mjs @@ -0,0 +1,66 @@ +// Flags: --experimental-quic --no-warnings + +// Test: onreset callback error handling. +// A sync throw in stream.onreset destroys the STREAM (not the session) +// via safeCallbackInvoke. The stream.onerror fires with the original +// error, and stream.closed rejects. The session remains alive. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); +const testError = new Error('onreset throw'); + +const serverReady = Promise.withResolvers(); +const serverStreamDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // The stream's onerror should fire with the throw from onreset. + stream.onerror = mustCall((err) => { + strictEqual(err, testError); + }); + + stream.onreset = () => { + throw testError; + }; + + serverReady.resolve(); + + // Stream closed rejects because the onreset throw destroyed it. + await rejects(stream.closed, testError); + serverStreamDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('trigger onstream'), +}); + +// Wait for the server to have the stream before resetting. +await serverReady.promise; +stream.resetStream(1n); + +// Wait for the server stream to be destroyed by the onreset throw. +await serverStreamDone.promise; + +// The client stream was reset. Destroy it explicitly to clean up +// (resetStream only shuts the write side; the read side is still open +// waiting for the server which won't send anything now). +stream.destroy(); +await stream.closed; + +// Close both sides. +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-onsessionticket.mjs b/test/parallel/test-quic-callback-error-onsessionticket.mjs new file mode 100644 index 00000000000000..cc514286fd5fe0 --- /dev/null +++ b/test/parallel/test-quic-callback-error-onsessionticket.mjs @@ -0,0 +1,41 @@ +// Flags: --experimental-quic --no-warnings + +// Test: onsessionticket callback error handling. +// A sync throw in onsessionticket destroys the session via safeCallbackInvoke. +// Unlike onhandshake throws, no uncaughtException is produced because the +// session ticket arrives after the handshake completes (session.opened is +// already resolved so there is no unhandled rejection). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const testError = new Error('onsessionticket throw'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { transportParams: { maxIdleTimeout: 1 } }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, + onsessionticket() { + throw testError; + }, + onerror: mustCall((err) => { + strictEqual(err, testError); + }), +}); + +await clientSession.opened; + +// The session's closed should reject with the error from the throw. +await rejects(clientSession.closed, testError); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-onstream-async.mjs b/test/parallel/test-quic-callback-error-onstream-async.mjs new file mode 100644 index 00000000000000..1505643e69a733 --- /dev/null +++ b/test/parallel/test-quic-callback-error-onstream-async.mjs @@ -0,0 +1,46 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: async rejection in onstream destroys session. +// safeCallbackInvoke detects the returned promise and attaches a +// rejection handler that calls session.destroy(err). The error is +// delivered to the onerror callback. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const testError = new Error('async onstream rejection'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onerror = mustCall((err) => { + strictEqual(err, testError); + }); + + serverSession.onstream = async () => { + throw testError; + }; + + // Session closed rejects with the error from the async rejection. + await rejects(serverSession.closed, testError); +}), { transportParams: { maxIdleTimeout: 1 } }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, +}); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: new TextEncoder().encode('trigger onstream'), +}); + +// The client session closes via CONNECTION_CLOSE or idle timeout +// after the server session is destroyed by the async rejection. +await Promise.all([stream.closed, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-onstream.mjs b/test/parallel/test-quic-callback-error-onstream.mjs new file mode 100644 index 00000000000000..116b3136fc6e4e --- /dev/null +++ b/test/parallel/test-quic-callback-error-onstream.mjs @@ -0,0 +1,49 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: callback error handling for onstream. +// Sync throw in onstream destroys the session. +// safeCallbackInvoke catches the throw and calls session.destroy(error). +// The error is delivered to the onerror callback. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const testError = new Error('sync onstream throw'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onerror = mustCall((err) => { + strictEqual(err, testError); + }); + + serverSession.onstream = () => { + throw testError; + }; + + // The session's closed rejects with the error from destroy(). + await rejects(serverSession.closed, testError); +}), { transportParams: { maxIdleTimeout: 1 } }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, +}); +await clientSession.opened; + +// Send data to trigger onstream on the server. +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('trigger onstream'), +}); + +// The client session will close via CONNECTION_CLOSE or idle timeout +// after the server session is destroyed. +await Promise.all([stream.closed, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-stream-onerror.mjs b/test/parallel/test-quic-callback-error-stream-onerror.mjs new file mode 100644 index 00000000000000..51cb89b6b1f692 --- /dev/null +++ b/test/parallel/test-quic-callback-error-stream-onerror.mjs @@ -0,0 +1,83 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream.onerror callback behavior. +// * stream.onerror fires when stream is destroyed with error. +// * stream.onerror receives the original error as argument. +// * stream.closed rejects with the original error after onerror. +// * stream.onerror not called when destroy() has no error. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +{ + const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('will error'), + }); + + const testError = new Error('stream destroy error'); + + let onerrorCalled = false; + stream.onerror = mustCall((err) => { + // Receives the original error. + strictEqual(err, testError); + onerrorCalled = true; + }); + + stream.destroy(testError); + + // The onerror was called synchronously during destroy. + strictEqual(onerrorCalled, true); + + // The stream.closed rejects with the original error. + await rejects(stream.closed, testError); +} + +// The stream.onerror not called when destroy() has no error. +// Create a stream with no body — use the writer API so the server sees +// it and can close cleanly. +{ + const stream = await clientSession.createBidirectionalStream(); + const w = stream.writer; + + stream.onerror = mustNotCall('stream.onerror should not be called'); + + // Send data so the server's onstream fires, then end. + w.writeSync('no error'); + w.endSync(); + + // Wait for the server to process and close its side. + await serverDone.promise; + + // Now destroy without error. + stream.destroy(); + + // Closed should resolve (not reject). + await stream.closed; +} + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-suppressed-async.mjs b/test/parallel/test-quic-callback-error-suppressed-async.mjs new file mode 100644 index 00000000000000..f1578908e7d6b8 --- /dev/null +++ b/test/parallel/test-quic-callback-error-suppressed-async.mjs @@ -0,0 +1,53 @@ +// Flags: --experimental-quic --no-warnings + +// Test: SuppressedError when async onerror rejects. +// When session.onerror returns a Promise that rejects, a SuppressedError +// wrapping both the rejection reason and the original error is thrown +// via process.nextTick as an uncaught exception. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const originalError = new Error('original destroy error'); +const onerrorRejection = new Error('async onerror rejected'); + +const transportParams = { maxIdleTimeout: 1 }; + +// The SuppressedError is thrown via process.nextTick after the +// onerror promise rejects, so it appears as an uncaught exception. +process.on('uncaughtException', mustCall((err) => { + ok(err instanceof SuppressedError); + // .error is the onerror rejection reason + strictEqual(err.error, onerrorRejection); + // .suppressed is the original error that triggered destroy + strictEqual(err.suppressed, originalError); +})); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { transportParams }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams, +}); +await clientSession.opened; + +// Async onerror: returns a promise that rejects. +clientSession.onerror = mustCall(async () => { + throw onerrorRejection; +}); + +clientSession.destroy(originalError); + +// Closed rejects with the original error (not the SuppressedError). +await rejects(clientSession.closed, originalError); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-callback-error-suppressed.mjs b/test/parallel/test-quic-callback-error-suppressed.mjs new file mode 100644 index 00000000000000..38e932f281fcdc --- /dev/null +++ b/test/parallel/test-quic-callback-error-suppressed.mjs @@ -0,0 +1,53 @@ +// Flags: --experimental-quic --no-warnings + +// Test: SuppressedError when onerror throws. +// If session.onerror throws synchronously, a SuppressedError +// wrapping both the onerror error and the original error is +// thrown via process.nextTick as an uncaught exception. +// The SuppressedError's .error is the onerror failure and +// .suppressed is the original error. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const originalError = new Error('original destroy error'); +const onerrorError = new Error('onerror itself threw'); + +const transportParams = { maxIdleTimeout: 1 }; + +// The SuppressedError is thrown via process.nextTick, so it appears +// as an uncaught exception. +process.on('uncaughtException', mustCall((err) => { + ok(err instanceof SuppressedError); + // .error is the onerror failure + strictEqual(err.error, onerrorError); + // .suppressed is the original error that triggered destroy + strictEqual(err.suppressed, originalError); +})); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { transportParams }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams, +}); +await clientSession.opened; + +clientSession.onerror = mustCall(() => { + throw onerrorError; +}); + +clientSession.destroy(originalError); + +// Closed rejects with the original error (not the SuppressedError). +await rejects(clientSession.closed, originalError); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-cc-algorithm.mjs b/test/parallel/test-quic-cc-algorithm.mjs new file mode 100644 index 00000000000000..36e96c2fc15bcb --- /dev/null +++ b/test/parallel/test-quic-cc-algorithm.mjs @@ -0,0 +1,52 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: congestion control algorithm selection. +// Verify that each CC algorithm (reno, cubic, bbr) can be selected +// and that a session completes a data transfer successfully with each. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const payload = encoder.encode('congestion control test'); +const payloadLength = payload.byteLength; + +for (const cc of ['reno', 'cubic', 'bbr']) { + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await bytes(stream); + strictEqual(data.byteLength, payloadLength); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + }), { cc }); + + const clientSession = await connect(serverEndpoint.address, { cc }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('congestion control test'), + }); + + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await Promise.all([stream.closed, serverDone.promise]); + + // Verify the session stats show congestion control was active. + ok(clientSession.stats.cwnd > 0n, `${cc}: cwnd should be > 0`); + + await clientSession.closed; + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-connection-concurrent.mjs b/test/parallel/test-quic-connection-concurrent.mjs new file mode 100644 index 00000000000000..6cc75fd185309e --- /dev/null +++ b/test/parallel/test-quic-connection-concurrent.mjs @@ -0,0 +1,56 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: concurrent connections from multiple clients. +// Multiple clients connect to the same server simultaneously and each +// exchanges data successfully. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const numClients = 5; +let serverStreamCount = 0; +const allDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Echo back the data. + const w = stream.writer; + w.writeSync(await bytes(stream)); + w.endSync(); + await stream.closed; + if (++serverStreamCount === numClients) { + allDone.resolve(); + } + }); +}, numClients)); + +// Connect all clients concurrently. +const clientPromises = []; +for (let i = 0; i < numClients; i++) { + clientPromises.push((async () => { + const cs = await connect(serverEndpoint.address, { reuseEndpoint: false }); + await cs.opened; + const message = `client ${i}`; + const stream = await cs.createBidirectionalStream({ + body: encoder.encode(message), + }); + const received = await bytes(stream); + strictEqual(new TextDecoder().decode(received), message); + await stream.closed; + cs.close(); + await cs.closed; + })()); +} + +await Promise.all([...clientPromises, allDone.promise]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-connection-limits.mjs b/test/parallel/test-quic-connection-limits.mjs new file mode 100644 index 00000000000000..7d2f4b07119f7b --- /dev/null +++ b/test/parallel/test-quic-connection-limits.mjs @@ -0,0 +1,80 @@ +// Flags: --experimental-quic --no-warnings + +// Test: connection total limit enforcement. +// maxConnectionsTotal limits total concurrent connections. +// When the limit is exceeded, the server sends CONNECTION_REFUSED +// and the client's session is destroyed with ERR_QUIC_TRANSPORT_ERROR. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { rejects, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect, QuicEndpoint } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +// Create endpoint with maxConnectionsTotal = 1. +const endpoint = new QuicEndpoint({ maxConnectionsTotal: 1 }); + +// Verify the limits are readable and mutable. +strictEqual(endpoint.maxConnectionsTotal, 1); +// The default maxConnectionsPerHost is 100 — a non-zero default that +// prevents a single host from exhausting server resources. +strictEqual(endpoint.maxConnectionsPerHost, 100); +endpoint.maxConnectionsPerHost = 50; +strictEqual(endpoint.maxConnectionsPerHost, 50); +endpoint.maxConnectionsPerHost = 0; + +let sessionCount = 0; + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + sessionCount++; + await Promise.all([serverSession.opened, serverSession.closed]); +}), { + endpoint, + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + transportParams: { maxIdleTimeout: 2 }, +}); + +// First connection should succeed. +const cs1 = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 2 }, +}); +await cs1.opened; + +// Second connection — server rejects with CONNECTION_REFUSED. +const cs2 = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + onerror: mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + }), +}); + +await Promise.all([ + rejects(cs2.opened, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }), + rejects(cs2.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }), +]); + +// Only 1 session should have been accepted by the server. +strictEqual(sessionCount, 1); + +await cs1.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-datagram-abandoned.mjs b/test/parallel/test-quic-datagram-abandoned.mjs new file mode 100644 index 00000000000000..e99c3bd9754702 --- /dev/null +++ b/test/parallel/test-quic-datagram-abandoned.mjs @@ -0,0 +1,64 @@ +// Flags: --experimental-quic --no-warnings + +// Test: datagram abandoned status for queue overflow. +// When the datagram pending queue is full and a new datagram is sent, +// the drop policy causes a datagram to be dropped. The dropped datagram +// should be reported with status 'abandoned' (not 'lost'), indicating +// it was never actually sent on the wire. + +import { hasQuic, skip, mustCall, mustCallAtLeast } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { notStrictEqual, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +let serverSession; + +const serverEndpoint = await listen(mustCall((session) => { + serverSession = session; +}), { + transportParams: { maxDatagramFrameSize: 1200 }, +}); + +const ids = [0n, 0n, 0n]; +let abandoned = false; + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagramstatus: mustCallAtLeast((id, status) => { + if (status === 'abandoned') { + strictEqual(id, ids[0]); + abandoned = true; + } + // We'll likely only get status for one other datagram. + }), +}); +await clientSession.opened; + +// Set a very small queue so overflow happens immediately. +clientSession.maxPendingDatagrams = 2; + +// Send 3 datagrams with a queue size of 2. The first datagram should +// be abandoned when the third is sent (drop-oldest policy is default). +ids[0] = await clientSession.sendDatagram(new Uint8Array([1])); +ids[1] = await clientSession.sendDatagram(new Uint8Array([2])); +ids[2] = await clientSession.sendDatagram(new Uint8Array([3])); + +notStrictEqual(ids[0], 0n); +notStrictEqual(ids[1], 0n); +notStrictEqual(ids[2], 0n); + +// The abandoned status fires synchronously during sendDatagram when the +// queue overflows. It should already be set +strictEqual(abandoned, true); + +await Promise.all([ + serverSession.close(), + clientSession.close(), +]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-datagram-drop-newest.mjs b/test/parallel/test-quic-datagram-drop-newest.mjs new file mode 100644 index 00000000000000..7225ea9cd08e0a --- /dev/null +++ b/test/parallel/test-quic-datagram-drop-newest.mjs @@ -0,0 +1,83 @@ +// Flags: --experimental-quic --no-warnings + +// Test: datagram drop-newest policy. +// With maxPendingDatagrams=2 and drop-newest, sending 5 datagrams +// rapidly should reject the newest when the queue is full. The +// server should receive the oldest datagrams. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const allReceived = Promise.withResolvers(); +const allStatusReceived = Promise.withResolvers(); + +let serverCounter = 0; +let clientAbandonCounter = 0; +let clientAckCounter = 0; + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await Promise.all([serverSession.opened, allStatusReceived.promise]); + serverSession.close(); + await serverSession.closed; +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall(function(data, early) { + // We whould only receive datagrams 1 and 2 + strictEqual(data.length, 1); + ok(data[0] === 0 || data[0] === 1); + ok(!early); + if (++serverCounter === 2) allReceived.resolve(); + }, 2), +}); + +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxDatagramFrameSize: 1200 }, + datagramDropPolicy: 'drop-newest', + ondatagramstatus: mustCall((_, status) => { + if (status === 'abandoned') { + clientAbandonCounter++; + } else if (status === 'acknowledged') { + clientAckCounter++; + } + if (clientAbandonCounter + clientAckCounter === 5) { + allStatusReceived.resolve(); + } + }, 5), +}); + +await clientSession.opened; + +clientSession.maxPendingDatagrams = 2; + +// Send 5 datagrams. With drop-newest, the 3rd/4th/5th are rejected +// (the queue holds the 1st and 2nd). +for (let i = 0; i < 5; i++) { + await clientSession.sendDatagram(new Uint8Array([i])); +} + +await Promise.all([allReceived.promise, allStatusReceived.promise]); + +// 3 abandoned (datagrams 1, 2, 3) and 2 acknowledged (datagrams 4, 5). +strictEqual(clientAbandonCounter, 3); +strictEqual(clientAckCounter, 2); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-datagram-drop-oldest.mjs b/test/parallel/test-quic-datagram-drop-oldest.mjs new file mode 100644 index 00000000000000..923e80bf73460a --- /dev/null +++ b/test/parallel/test-quic-datagram-drop-oldest.mjs @@ -0,0 +1,84 @@ +// Flags: --experimental-quic + +// Test: datagram drop-oldest policy. +// With maxPendingDatagrams=2 and drop-oldest, sending 5 datagrams +// rapidly should drop the oldest when the queue overflows. The +// server should receive the most recent datagrams (4th and 5th). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const allReceived = Promise.withResolvers(); +const allStatusReceived = Promise.withResolvers(); + +let serverCounter = 0; +let clientAbandonCounter = 0; +let clientAckCounter = 0; + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await Promise.all([serverSession.opened, allStatusReceived.promise]); + await serverSession.close(); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall(function(data, early) { + // With drop-oldest, the queue keeps the newest. After 5 sends with + // queue size 2, only datagrams 4 and 5 (values 3 and 4) remain. + strictEqual(data.length, 1); + ok(data[0] === 3 || data[0] === 4); + ok(!early); + if (++serverCounter === 2) allReceived.resolve(); + }, 2), +}); + +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxDatagramFrameSize: 1200 }, + datagramDropPolicy: 'drop-oldest', + ondatagramstatus: mustCall((_, status) => { + if (status === 'abandoned') { + clientAbandonCounter++; + } else if (status === 'acknowledged') { + clientAckCounter++; + } + if (clientAbandonCounter + clientAckCounter === 5) { + allStatusReceived.resolve(); + } + }, 5), +}); + +await clientSession.opened; + +clientSession.maxPendingDatagrams = 2; + +// Send 5 datagrams. With drop-oldest and queue size 2: +// 1 queued, 2 queued, 3 arrives → 1 dropped, 4 arrives → 2 dropped, +// 5 arrives → 3 dropped. Queue ends with [4, 5]. +for (let i = 0; i < 5; i++) { + await clientSession.sendDatagram(new Uint8Array([i])); +} + +await Promise.all([allReceived.promise, allStatusReceived.promise]); + +// 3 abandoned (datagrams 1, 2, 3) and 2 acknowledged (datagrams 4, 5). +strictEqual(clientAbandonCounter, 3); +strictEqual(clientAckCounter, 2); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-datagram-echo.mjs b/test/parallel/test-quic-datagram-echo.mjs new file mode 100644 index 00000000000000..a0870ba49ce77a --- /dev/null +++ b/test/parallel/test-quic-datagram-echo.mjs @@ -0,0 +1,71 @@ +// Flags: --experimental-quic --no-warnings + +// Test: datagram server-to-client and echo round-trip +// Server sends datagram, client receives via ondatagram. +// Datagram echo — client sends, server echoes back in +// its ondatagram callback (queued, flushed via SendPendingData). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +const { setTimeout } = await import('node:timers/promises'); + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const serverGot = Promise.withResolvers(); +const clientGot = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await Promise.all([serverSession.opened, serverGot.promise]); + // Give time for the echo to be sent before closing. + await setTimeout(100); + await serverSession.close(); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + transportParams: { maxDatagramFrameSize: 10 }, + // Server echoes received datagram data back to client. + // The sendDatagram call happens inside ondatagram (ngtcp2 callback + // scope). The datagram is queued and flushed by SendPendingData. + ondatagram: mustCall((data, early, session) => { + ok(data instanceof Uint8Array); + ok(!early); + session.sendDatagram(data); + serverGot.resolve(); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxDatagramFrameSize: 10 }, + // Client receives datagram from server. + ondatagram: mustCall(function(data) { + ok(data instanceof Uint8Array); + strictEqual(data.byteLength, 3); + strictEqual(data[0], 10); + strictEqual(data[1], 20); + strictEqual(data[2], 30); + clientGot.resolve(); + }), +}); + +await clientSession.opened; + +// Client sends datagram to trigger the echo. +await clientSession.sendDatagram(new Uint8Array([10, 20, 30])); + +await Promise.all([serverGot.promise, clientGot.promise, clientSession.closed]); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-datagram-edge-cases.mjs b/test/parallel/test-quic-datagram-edge-cases.mjs new file mode 100644 index 00000000000000..1abf9f80c7f311 --- /dev/null +++ b/test/parallel/test-quic-datagram-edge-cases.mjs @@ -0,0 +1,93 @@ +// Flags: --experimental-quic --no-warnings + +// Test: datagram edge cases. +// DGRAM-08 / DGIMP-08: Zero-length datagram returns 0n (not sent). +// DGC-01 / DGIMP-09: maxDatagramFrameSize: 0 disables datagrams entirely. +// Datagram arrives with no ondatagram callback — no crash. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; + +const { strictEqual, notStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// --- DGRAM-08 / DGIMP-08: Zero-length datagram returns 0n --- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + }), { + transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 }, + }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 }, + }); + await clientSession.opened; + + // Zero-length ArrayBufferView + const zeroId = await clientSession.sendDatagram(new Uint8Array(0)); + strictEqual(zeroId, 0n); + + // Zero-length string + const emptyStringId = await clientSession.sendDatagram(''); + strictEqual(emptyStringId, 0n); + + await clientSession.close(); + await serverEndpoint.close(); +} + +// --- DGC-01 / DGIMP-09: maxDatagramFrameSize: 0 disables datagrams --- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + }), { + // Server advertises 0 — no datagrams accepted. + transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 0 }, + }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 10 }, + }); + await clientSession.opened; + + // maxDatagramSize reflects the peer's (server's) transport param. + strictEqual(clientSession.maxDatagramSize, 0); + + // Sending returns 0n immediately — datagram not sent. + const id = await clientSession.sendDatagram(new Uint8Array([1, 2, 3])); + strictEqual(id, 0n); + + await clientSession.close(); + await serverEndpoint.close(); +} + +// --- DGRAM-11: No ondatagram callback — no crash --- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + // No ondatagram set — datagrams arrive but are silently discarded. + await serverSession.opened; + // Give time for the datagram to arrive and be processed without crash. + await setTimeout(200); + await serverSession.close(); + }), { + transportParams: { maxDatagramFrameSize: 1200 }, + }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, + }); + await clientSession.opened; + + // Send a datagram even though the server has no ondatagram handler. + const id = await clientSession.sendDatagram(new Uint8Array([1, 2, 3])); + notStrictEqual(id, 0n); + + await clientSession.closed; + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-datagram-frame-size-validation.mjs b/test/parallel/test-quic-datagram-frame-size-validation.mjs new file mode 100644 index 00000000000000..b6cdea66e37f9c --- /dev/null +++ b/test/parallel/test-quic-datagram-frame-size-validation.mjs @@ -0,0 +1,58 @@ +// Flags: --experimental-quic --no-warnings + +// Test: maxDatagramFrameSize transport param validation. +// The maxDatagramFrameSize transport parameter must be a uint16 +// (0-65535). Values outside this range or of the wrong type should +// be rejected. + +import { hasQuic, skip, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { rejects } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const sni = { '*': { keys: [key], certs: [cert] } }; +const alpn = ['quic-test']; + +// Invalid values for maxDatagramFrameSize — must be rejected. +const invalid = [ + -1, + 65536, + 1.5, + 'a', + null, + false, + true, + {}, + [], + () => {}, +]; + +for (const maxDatagramFrameSize of invalid) { + const transportParams = { maxDatagramFrameSize }; + await rejects( + listen(mustNotCall(), { sni, alpn, transportParams }), + { code: 'ERR_INVALID_ARG_VALUE' }, + `listen should reject maxDatagramFrameSize: ${maxDatagramFrameSize}`, + ); +} + +// Valid values — should not throw. +const valid = [0, 1, 100, 1200, 65535]; + +for (const maxDatagramFrameSize of valid) { + const transportParams = { maxDatagramFrameSize }; + const ep = await listen(mustNotCall(), { sni, alpn, transportParams }); + ep.close(); + await ep.closed; +} diff --git a/test/parallel/test-quic-datagram-multiple.mjs b/test/parallel/test-quic-datagram-multiple.mjs new file mode 100644 index 00000000000000..96e86bec8a99e7 --- /dev/null +++ b/test/parallel/test-quic-datagram-multiple.mjs @@ -0,0 +1,85 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: multiple datagrams and datagrams alongside streams +// Client sends multiple datagrams. +// Datagrams sent alongside an active bidi stream. +// Datagrams are unreliable — we verify at least some arrive. +// The stream is opened first to establish bidirectional traffic, +// keeping the congestion window healthy for datagram sends. + +import { hasQuic, skip, mustCall, mustCallAtLeast } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const numDatagrams = 5; +let serverDatagramCount = 0; +const gotSomeDg = Promise.withResolvers(); +const streamDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + + // Server receives stream data alongside datagrams. + serverSession.onstream = mustCall(async (stream) => { + stream.writer.endSync(); + await bytes(stream); + await stream.closed; + streamDone.resolve(); + }); + + await Promise.all([gotSomeDg.promise, streamDone.promise]); + await serverSession.close(); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCallAtLeast((data) => { + ok(data instanceof Uint8Array); + serverDatagramCount++; + gotSomeDg.resolve(); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxDatagramFrameSize: 1200 }, +}); + +await clientSession.opened; + +// Open a stream FIRST to establish bidirectional traffic. +// This ensures ACKs flow back from the server, keeping the +// congestion window open for subsequent datagram sends. +const stream = await clientSession.createBidirectionalStream({ + body: new TextEncoder().encode('hello'), +}); + +// Send multiple datagrams alongside the active stream. +for (let i = 0; i < numDatagrams; i++) { + await clientSession.sendDatagram(new Uint8Array([i])); +} + +// Complete the stream. +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await stream.closed; + +// At least some datagrams should have arrived. +ok(serverDatagramCount > 0, 'Server should have received at least one datagram'); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-datagram-no-detach.mjs b/test/parallel/test-quic-datagram-no-detach.mjs new file mode 100644 index 00000000000000..cdcbe53691d678 --- /dev/null +++ b/test/parallel/test-quic-datagram-no-detach.mjs @@ -0,0 +1,72 @@ +// Flags: --experimental-quic --no-warnings + +// session.sendDatagram() must not detach the caller's underlying +// ArrayBuffer. The bytes are copied into an internal buffer on every +// send, so the caller's source ArrayBuffer remains live and may be +// reused for further sends, mutated, or sliced into additional views. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, deepStrictEqual, notStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const received = []; +const allReceived = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await allReceived.promise; + await serverSession.close(); +}), { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall((data) => { + received.push(Buffer.from(data)); + if (received.length === 3) allReceived.resolve(); + }, 3), +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, +}); +await clientSession.opened; + +// A full-buffer Uint8Array. The same source must be reusable across +// multiple sendDatagram calls. +const source = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + +const firstId = await clientSession.sendDatagram(source); +notStrictEqual(firstId, 0n); +strictEqual(source.buffer.detached, false, + 'source ArrayBuffer must not be detached after sendDatagram'); + +const secondId = await clientSession.sendDatagram(source); +notStrictEqual(secondId, 0n); +strictEqual(source.buffer.detached, false, + 'source ArrayBuffer must remain live after second sendDatagram'); + +// Mutating the source after the previous sendDatagram returned must +// not affect what the peer ultimately receives — the bytes have +// already been copied into the QUIC layer's internal buffer. +source[0] = 99; +const thirdId = await clientSession.sendDatagram(source); +notStrictEqual(thirdId, 0n); + +await allReceived.promise; + +// Datagrams are unreliable and may be reordered, but the test fixture +// does not exercise loss or reordering — assert by sorting. +const sorted = received.map((b) => b.toString('hex')).sort(); +const expected = [ + Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]).toString('hex'), + Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]).toString('hex'), + Buffer.from([99, 2, 3, 4, 5, 6, 7, 8]).toString('hex'), +].sort(); +deepStrictEqual(sorted, expected); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-datagram-size-limits.mjs b/test/parallel/test-quic-datagram-size-limits.mjs new file mode 100644 index 00000000000000..026df4cb1cbeaa --- /dev/null +++ b/test/parallel/test-quic-datagram-size-limits.mjs @@ -0,0 +1,64 @@ +// Flags: --experimental-quic --no-warnings + +// Test: datagram size limit enforcement. +// Datagram larger than maxDatagramSize returns 0n (not sent). +// Datagram at exactly maxDatagramSize is accepted and delivered. +// Same as DGRAM-03 via sendDatagram return value. +// maxDatagramSize reflects the maximum datagram payload the peer can +// receive, accounting for DATAGRAM frame overhead (type byte + varint +// length encoding). It is derived from the peer's maxDatagramFrameSize +// transport parameter minus the frame overhead. +// We use maxDatagramFrameSize: 200 so that the exact-max datagram fits +// comfortably within a QUIC packet (which has its own header + AEAD +// overhead on top of the DATAGRAM frame). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual, notStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverGot = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverGot.promise; + serverSession.close(); + await serverSession.closed; +}), { + transportParams: { maxDatagramFrameSize: 200 }, + ondatagram: mustCall((data) => { + ok(data instanceof Uint8Array); + serverGot.resolve(); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 200 }, +}); +await clientSession.opened; + +const maxSize = clientSession.maxDatagramSize; + +// maxDatagramSize should be less than maxDatagramFrameSize due to +// the DATAGRAM frame overhead (1 byte type + varint length encoding). +ok(maxSize > 0); +ok(maxSize < 200); + +// DGRAM-03 / DGIMP-10: Datagram too large — returns 0n. +const oversized = new Uint8Array(maxSize + 1); +const tooLargeId = await clientSession.sendDatagram(oversized); +strictEqual(tooLargeId, 0n); + +// Datagram at exactly maxDatagramSize — accepted and delivered. +const exactMax = new Uint8Array(maxSize); +exactMax[0] = 42; +const exactId = await clientSession.sendDatagram(exactMax); +notStrictEqual(exactId, 0n); + +await Promise.all([serverGot.promise, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-datagram-sources.mjs b/test/parallel/test-quic-datagram-sources.mjs new file mode 100644 index 00000000000000..55122f53354c1f --- /dev/null +++ b/test/parallel/test-quic-datagram-sources.mjs @@ -0,0 +1,220 @@ +// Flags: --experimental-quic --no-warnings + +// Test: sendDatagram with various input source types. +// String with custom encoding (e.g., 'hex'). +// Promise input — resolves then sends. +// Promise input — session closes during await, returns 0n. +// SharedArrayBuffer copies instead of transfers. +// Pooled Buffer (partial view) copies correctly. +// DataView input. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { deepStrictEqual, notStrictEqual, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// --- DGIMP-01: String with custom encoding --- +{ + const received = []; + const allReceived = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await allReceived.promise; + await serverSession.close(); + }), { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall((data) => { + received.push(Buffer.from(data)); + if (received.length === 2) allReceived.resolve(); + }, 2), + }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, + }); + await clientSession.opened; + + // Send hex-encoded string — '48656c6c6f' is 'Hello' in hex. + const hexId = await clientSession.sendDatagram('48656c6c6f', 'hex'); + notStrictEqual(hexId, 0n); + + // Send base64-encoded string — 'V29ybGQ=' is 'World' in base64. + const b64Id = await clientSession.sendDatagram('V29ybGQ=', 'base64'); + notStrictEqual(b64Id, 0n); + + await allReceived.promise; + + deepStrictEqual(received[0], Buffer.from('Hello')); + deepStrictEqual(received[1], Buffer.from('World')); + + await clientSession.closed; + await serverEndpoint.close(); +} + +// --- DGIMP-02: Promise input --- +{ + const serverGot = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverGot.promise; + await serverSession.close(); + }), { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall((data) => { + deepStrictEqual(Buffer.from(data), Buffer.from([42])); + serverGot.resolve(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, + }); + await clientSession.opened; + + // Send a Promise that resolves to a Uint8Array. + const promiseId = await clientSession.sendDatagram( + Promise.resolve(new Uint8Array([42])), + ); + notStrictEqual(promiseId, 0n); + + await Promise.all([serverGot.promise, clientSession.closed]); + await serverEndpoint.close(); +} + +// --- DGIMP-03: Promise input, session closes during await --- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + }), { + transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 }, + onerror() {}, + }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 }, + }); + await clientSession.opened; + + // Create a promise that resolves after the session starts closing. + // sendDatagram passes the initial checks, then awaits the promise. + // While awaiting, the session closes. When the promise resolves, + // sendDatagram finds the session closed and returns 0n. + const slowPromise = new Promise((resolve) => { + setImmediate(mustCall(async () => { + await clientSession.close(); + resolve(new Uint8Array([1])); + })); + }); + + const id = await clientSession.sendDatagram(slowPromise); + strictEqual(id, 0n); + + await serverEndpoint.close(); +} + +// --- DGIMP-04: SharedArrayBuffer --- +{ + const serverGot = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverGot.promise; + await serverSession.close(); + }), { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall((data) => { + deepStrictEqual(Buffer.from(data), Buffer.from([10, 20, 30])); + serverGot.resolve(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, + }); + await clientSession.opened; + + // Create a SharedArrayBuffer-backed view. + const sab = new SharedArrayBuffer(3); + const view = new Uint8Array(sab); + view[0] = 10; + view[1] = 20; + view[2] = 30; + + const id = await clientSession.sendDatagram(view); + notStrictEqual(id, 0n); + + // The SharedArrayBuffer should still be usable (copied, not transferred). + strictEqual(view[0], 10); + + await Promise.all([serverGot.promise, clientSession.closed]); + await serverEndpoint.close(); +} + +// --- DGIMP-05: Pooled Buffer (partial view) --- +{ + const serverGot = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverGot.promise; + await serverSession.close(); + }), { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall((data) => { + // The received data should match the slice content. + deepStrictEqual(Buffer.from(data), Buffer.from('hello')); + serverGot.resolve(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, + }); + await clientSession.opened; + + // Buffer.from('hello') creates a pooled buffer — its backing + // ArrayBuffer is larger and the view has a non-zero offset. + const pooledBuf = Buffer.from('hello'); + const id = await clientSession.sendDatagram(pooledBuf); + notStrictEqual(id, 0n); + + await Promise.all([serverGot.promise, clientSession.closed]); + await serverEndpoint.close(); +} + +// --- DGIMP-06: DataView --- +{ + const serverGot = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverGot.promise; + await serverSession.close(); + }), { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall((data) => { + deepStrictEqual(Buffer.from(data), Buffer.from([0xCA, 0xFE])); + serverGot.resolve(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, + }); + await clientSession.opened; + + const ab = new ArrayBuffer(4); + const fullView = new Uint8Array(ab); + fullView.set([0xDE, 0xAD, 0xCA, 0xFE]); + + // DataView over bytes [2, 3] of the buffer. + const dv = new DataView(ab, 2, 2); + const id = await clientSession.sendDatagram(dv); + notStrictEqual(id, 0n); + + await Promise.all([serverGot.promise, clientSession.closed]); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-datagram-status.mjs b/test/parallel/test-quic-datagram-status.mjs new file mode 100644 index 00000000000000..98e6bf56cee986 --- /dev/null +++ b/test/parallel/test-quic-datagram-status.mjs @@ -0,0 +1,77 @@ +// Flags: --experimental-quic --no-warnings + +// Test: ondatagramstatus callback. +// After sending a datagram, the ondatagramstatus callback fires +// with the datagram ID and either 'acknowledged' or 'lost'. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +const { setTimeout } = await import('node:timers/promises'); + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const serverGot = Promise.withResolvers(); +const statusReceived = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await Promise.all([serverSession.opened, serverGot.promise]); + // Give a moment for the ACK to propagate to the client so the + // ondatagramstatus callback fires before the session closes. + await setTimeout(100); + await serverSession.close(); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall(function() { + serverGot.resolve(); + }), +}); + +let statusId; +let statusValue; + +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagramstatus: mustCall((id, status) => { + strictEqual(typeof id, 'bigint'); + strictEqual(typeof status, 'string'); + ok( + status === 'acknowledged' || status === 'lost' || status === 'abandoned', + `status should be 'acknowledged', 'lost', or 'abandoned', got '${status}'`, + ); + + statusId = id; + statusValue = status; + statusReceived.resolve(); + }), +}); + +await clientSession.opened; +const id = await clientSession.sendDatagram(new Uint8Array([1, 2, 3])); + +// Wait for the server to receive and the status callback to fire. +await Promise.all([serverGot.promise, statusReceived.promise]); + +// The status callback should have been called with the same ID. +strictEqual(statusId, id); +// On localhost the datagram should be acknowledged, not lost. +strictEqual(statusValue, 'acknowledged'); + +await clientSession.closed; + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-datagram-utf8.mjs b/test/parallel/test-quic-datagram-utf8.mjs new file mode 100644 index 00000000000000..c0051ff6f6b95e --- /dev/null +++ b/test/parallel/test-quic-datagram-utf8.mjs @@ -0,0 +1,46 @@ +// Flags: --experimental-quic --no-warnings + +// Test: string datagram with multi-byte UTF-8 characters. +// Verifies that sendDatagram with a string containing multi-byte UTF-8 +// characters (CJK, emoji, etc.) encodes correctly and the receiver +// gets the exact bytes. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, deepStrictEqual, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const message = '\u4f60\u597d\u4e16\u754c'; // "Hello World" in Chinese +const expected = Buffer.from(message, 'utf8'); + +const serverGot = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverGot.promise; + await serverSession.close(); +}), { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall((data) => { + ok(data instanceof Uint8Array); + // Verify the received bytes match the UTF-8 encoding. + deepStrictEqual(Buffer.from(data), expected); + serverGot.resolve(); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, +}); +await clientSession.opened; + +const id = await clientSession.sendDatagram(message); +strictEqual(id, 1n); + +await Promise.all([serverGot.promise, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-datagram.mjs b/test/parallel/test-quic-datagram.mjs new file mode 100644 index 00000000000000..acd2d88356239a --- /dev/null +++ b/test/parallel/test-quic-datagram.mjs @@ -0,0 +1,63 @@ +// Flags: --experimental-quic --no-warnings + +// Test: basic datagram send and receive. +// Client sends datagram, server receives via ondatagram. +// maxDatagramSize reflects peer's transport param. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const serverGot = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + // maxDatagramSize reflects peer's max payload (frame size + // minus DATAGRAM frame overhead of type byte + varint length). + ok(serverSession.maxDatagramSize > 0); + ok(serverSession.maxDatagramSize < 1200); + // Wait for the datagram before closing. + await serverGot.promise; + await serverSession.close(); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall((data) => { + ok(data instanceof Uint8Array); + strictEqual(data.byteLength, 3); + serverGot.resolve(); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxDatagramFrameSize: 1200 }, +}); + +await clientSession.opened; + +// Client maxDatagramSize reflects actual payload max. +ok(clientSession.maxDatagramSize > 0); +ok(clientSession.maxDatagramSize < 1200); + +// Client sends datagram. +const id = await clientSession.sendDatagram(new Uint8Array([1, 2, 3])); +assert.strictEqual(id, 1n); + +await Promise.all([serverGot.promise, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-default-stream-limits.mjs b/test/parallel/test-quic-default-stream-limits.mjs new file mode 100644 index 00000000000000..d6198d50403a7a --- /dev/null +++ b/test/parallel/test-quic-default-stream-limits.mjs @@ -0,0 +1,55 @@ +// Flags: --experimental-quic --no-warnings + +// Test: default transport parameter limits are reasonable. +// Verify that the default transport parameters have sane values: +// not zero (which would prevent streams), and not excessively large +// (which could waste resources). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + const info = await serverSession.opened; + + // The handshake info should be available. + ok(info, 'handshake info should be available'); + + await serverSession.closed; +})); + +const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, +}); +const info = await clientSession.opened; + +// Verify the handshake completed and we can inspect the session. +ok(info, 'handshake info should be available'); + +// Check that the session has reasonable default stream limits by +// verifying we can create at least one bidirectional and one +// unidirectional stream. +const bidiStream = await clientSession.createBidirectionalStream(); +ok(bidiStream, 'should be able to create a bidi stream'); +bidiStream.destroy(); + +const uniStream = await clientSession.createUnidirectionalStream(); +ok(uniStream, 'should be able to create a uni stream'); +uniStream.destroy(); + +// Check the endpoint's maxConnectionsPerHost and maxConnectionsTotal +// defaults are either 0 (unlimited) or a reasonable positive number. +const maxPerHost = serverEndpoint.maxConnectionsPerHost; +const maxTotal = serverEndpoint.maxConnectionsTotal; +ok(maxPerHost >= 0, 'maxConnectionsPerHost should be non-negative'); +ok(maxTotal >= 0, 'maxConnectionsTotal should be non-negative'); + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-diagnostics-channel-busy.mjs b/test/parallel/test-quic-diagnostics-channel-busy.mjs new file mode 100644 index 00000000000000..df680aa51d5b1a --- /dev/null +++ b/test/parallel/test-quic-diagnostics-channel-busy.mjs @@ -0,0 +1,44 @@ +// Flags: --experimental-quic --no-warnings + +// Test: diagnostics_channel endpoint busy change. +// quic.endpoint.busy.change fires on endpoint.busy toggle. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, QuicEndpoint } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +let busyChangeCount = 0; +dc.subscribe('quic.endpoint.busy.change', mustCall((msg) => { + busyChangeCount++; + ok(msg.endpoint); + strictEqual(typeof msg.busy, 'boolean'); +}, 2)); + +const endpoint = new QuicEndpoint(); +const serverEndpoint = await listen(mustNotCall(), { + endpoint, + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], +}); + +// Toggle busy on and off. +endpoint.busy = true; +endpoint.busy = false; + +strictEqual(busyChangeCount, 2); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-diagnostics-channel-datagram-status.mjs b/test/parallel/test-quic-diagnostics-channel-datagram-status.mjs new file mode 100644 index 00000000000000..73610746a47966 --- /dev/null +++ b/test/parallel/test-quic-diagnostics-channel-datagram-status.mjs @@ -0,0 +1,48 @@ +// Flags: --experimental-quic --no-warnings + +// Test: diagnostics_channel datagram status event. +// quic.session.receive.datagram.status fires with ack/lost status. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const statusDone = Promise.withResolvers(); + +// quic.session.receive.datagram.status fires with status. +dc.subscribe('quic.session.receive.datagram.status', mustCall((msg) => { + ok(msg.session); + ok(msg.id); + strictEqual(msg?.status, 'acknowledged'); + statusDone.resolve(); +})); + +const serverEndpoint = await listen(async (serverSession) => { + // Server stays alive until the client closes so the ACK + // has time to propagate back to the client. + await serverSession.closed; +}, { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram() {}, +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagramstatus() {}, +}); +await clientSession.opened; + +await clientSession.sendDatagram(new Uint8Array([1, 2, 3])); + +// Wait for the status event before closing. +await statusDone.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-diagnostics-channel-datagram.mjs b/test/parallel/test-quic-diagnostics-channel-datagram.mjs new file mode 100644 index 00000000000000..572cbe3e099f6a --- /dev/null +++ b/test/parallel/test-quic-diagnostics-channel-datagram.mjs @@ -0,0 +1,52 @@ +// Flags: --experimental-quic --no-warnings + +// Test: diagnostics_channel datagram events. +// quic.session.receive.datagram fires on datagram receipt. +// quic.session.send.datagram fires on datagram send. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverGot = Promise.withResolvers(); + +// quic.session.send.datagram fires on send. +dc.subscribe('quic.session.send.datagram', mustCall((msg) => { + ok(msg.session); + ok(msg.id); + ok(msg.length > 0); +})); + +// quic.session.receive.datagram fires on receipt. +dc.subscribe('quic.session.receive.datagram', mustCall((msg) => { + ok(msg.session); + ok(msg.length > 0); +})); + +const serverEndpoint = await listen(async (serverSession) => { + await serverSession.closed; +}, { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram() { + serverGot.resolve(); + }, +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, +}); +await clientSession.opened; + +await clientSession.sendDatagram(new Uint8Array([1, 2, 3])); + +await serverGot.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-diagnostics-channel-error.mjs b/test/parallel/test-quic-diagnostics-channel-error.mjs new file mode 100644 index 00000000000000..49644ef437d05f --- /dev/null +++ b/test/parallel/test-quic-diagnostics-channel-error.mjs @@ -0,0 +1,50 @@ +// Flags: --experimental-quic --no-warnings + +// Test: diagnostics_channel endpoint error event. +// quic.endpoint.error fires on endpoint error. +// Trigger a bind failure (port conflict) and verify the channel fires. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; +import * as fixtures from '../common/fixtures.mjs'; + +const { readKey } = fixtures; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const sni = { '*': { keys: [key], certs: [cert] } }; +const alpn = ['quic-test']; + +dc.subscribe('quic.endpoint.error', mustCall((msg) => { + ok(msg.endpoint); + ok(msg.error); +})); + +// Create first endpoint to occupy a port. +const ep1 = await listen(mustNotCall(), { sni, alpn }); +const { port } = ep1.address; + +// Create second endpoint on the same port — triggers bind error. +const ep2 = await listen(mustNotCall(), { + sni, + alpn, + endpoint: { address: `127.0.0.1:${port}` }, +}); + +// ep2 is destroyed due to bind failure. +strictEqual(ep2.destroyed, true); +await rejects(ep2.closed, { + code: 'ERR_QUIC_ENDPOINT_CLOSED', + message: /Bind failure/, +}); +await ep1.close(); diff --git a/test/parallel/test-quic-diagnostics-channel-path.mjs b/test/parallel/test-quic-diagnostics-channel-path.mjs new file mode 100644 index 00000000000000..63305025e6efbf --- /dev/null +++ b/test/parallel/test-quic-diagnostics-channel-path.mjs @@ -0,0 +1,60 @@ +// Flags: --experimental-quic --no-warnings + +// Test: diagnostics_channel path validation event. +// quic.session.path.validation fires when path validation completes +// during preferred address migration. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const clientChannelFired = Promise.withResolvers(); + +// Subscribe to the path validation diagnostics channel. +// Verify the client-side event fires with the correct properties. +dc.subscribe('quic.session.path.validation', (msg) => { + ok(msg.session, 'message should have session'); + ok(msg.result, 'message should have result'); + ok(msg.newLocalAddress, 'message should have newLocalAddress'); + ok(msg.newRemoteAddress, 'message should have newRemoteAddress'); + if (msg.preferredAddress === true) { + clientChannelFired.resolve(); + } +}); + +const preferredEndpoint = await listen(mustNotCall(), { + onpathvalidation() {}, + onerror() {}, +}); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { + transportParams: { + preferredAddressIpv4: preferredEndpoint.address, + }, + onpathvalidation() {}, + onerror() {}, +}); + +const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, + preferredAddressPolicy: 'use', + // The onpathvalidation must be set for the JS handler to fire, + // which in turn publishes to the diagnostics channel. + onpathvalidation: mustCall(), +}); + +await Promise.all([clientSession.opened, clientChannelFired.promise]); + +await clientSession.close(); +await serverEndpoint.close(); +await preferredEndpoint.close(); diff --git a/test/parallel/test-quic-diagnostics-channel-session.mjs b/test/parallel/test-quic-diagnostics-channel-session.mjs new file mode 100644 index 00000000000000..3d11687f1e7006 --- /dev/null +++ b/test/parallel/test-quic-diagnostics-channel-session.mjs @@ -0,0 +1,49 @@ +// Flags: --experimental-quic --no-warnings + +// Test: diagnostics_channel session events. +// quic.session.handshake fires when handshake completes. +// quic.session.update.key fires on key update. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// quic.session.handshake fires on both sides. +let handshakeCount = 0; +dc.subscribe('quic.session.handshake', mustCall((msg) => { + handshakeCount++; + ok(msg.session); + // The handshake info should include standard TLS fields. + strictEqual(typeof msg.protocol, 'string'); + strictEqual(typeof msg.servername, 'string'); +}, 2)); + +// quic.session.update.key fires on key update. +dc.subscribe('quic.session.update.key', mustCall((msg) => { + ok(msg.session); +})); + +const serverEndpoint = await listen(async (serverSession) => { + await serverSession.opened; + await serverSession.close(); +}); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Trigger a key update to fire a key update event. +clientSession.updateKey(); + +await clientSession.closed; +await serverEndpoint.close(); + +// Both client and server handshakes should have fired. +strictEqual(handshakeCount, 2); diff --git a/test/parallel/test-quic-diagnostics-channel-stream.mjs b/test/parallel/test-quic-diagnostics-channel-stream.mjs new file mode 100644 index 00000000000000..9d6d4e08b2e98b --- /dev/null +++ b/test/parallel/test-quic-diagnostics-channel-stream.mjs @@ -0,0 +1,67 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: diagnostics_channel stream and handshake events +// quic.session.handshake fires on handshake complete (not in +// the channel list but we test the opened event path). +// quic.session.open.stream fires when a stream is created locally. +// quic.session.received.stream fires when a remote stream arrives. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); + +// Fires when the client creates a stream. +dc.subscribe('quic.session.open.stream', mustCall((msg) => { + ok(msg.stream); + ok(msg.session); + strictEqual(msg.direction, 'bidi', 'open.stream direction should be bidi'); +})); + +// Fires when the server receives a stream. +dc.subscribe('quic.session.received.stream', mustCall((msg) => { + ok(msg.stream); + ok(msg.session); + strictEqual(msg.direction, 'bidi', 'received.stream direction should be bidi'); +})); + +// Fires when a stream is destroyed. +dc.subscribe('quic.stream.closed', mustCall((msg) => { + ok(msg.stream); + ok(msg.session); + ok(msg.stats, 'stream.closed should include stats'); +}, 2)); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await bytes(stream); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('diagnostics test'), +}); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + +await Promise.all([stream.closed, serverDone.promise, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-diagnostics-channel-token.mjs b/test/parallel/test-quic-diagnostics-channel-token.mjs new file mode 100644 index 00000000000000..a2b4cdc5486a58 --- /dev/null +++ b/test/parallel/test-quic-diagnostics-channel-token.mjs @@ -0,0 +1,54 @@ +// Flags: --experimental-quic --no-warnings + +// Test: diagnostics_channel token/ticket events. +// quic.session.ticket fires when session ticket received. +// quic.session.new.token fires when NEW_TOKEN received. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const allDone = Promise.withResolvers(); +let ticketFired = false; +let tokenFired = false; + +function checkDone() { + if (ticketFired && tokenFired) allDone.resolve(); +} + +// quic.session.ticket fires when session ticket received. +dc.subscribe('quic.session.ticket', mustCall((msg) => { + ok(msg.session); + ok(msg.ticket); + ticketFired = true; + checkDone(); +})); + +// quic.session.new.token fires when NEW_TOKEN received. +dc.subscribe('quic.session.new.token', mustCall((msg) => { + ok(msg.session); + ok(msg.token); + ok(msg.address); + tokenFired = true; + checkDone(); +})); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +})); + +const clientSession = await connect(serverEndpoint.address, { + onsessionticket: mustCall((ticket) => { ok(ticket); }), + onnewtoken: mustCall((token) => { ok(token); }), +}); +await Promise.all([clientSession.opened, allDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-diagnostics-channel.mjs b/test/parallel/test-quic-diagnostics-channel.mjs new file mode 100644 index 00000000000000..7768c02e79a0e0 --- /dev/null +++ b/test/parallel/test-quic-diagnostics-channel.mjs @@ -0,0 +1,106 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: diagnostics_channel events. +// quic.endpoint.created fires when endpoint is created. +// quic.endpoint.listen fires when endpoint starts listening. +// quic.endpoint.closing fires when endpoint begins closing. +// quic.endpoint.closed fires when endpoint finishes closing. +// quic.session.created.client fires for client sessions. +// quic.session.created.server fires for server sessions. +// quic.session.closing fires when session begins closing. +// quic.session.closed fires when session finishes closing. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const events = []; + +// endpoint.created fires for both server and client endpoints. +dc.subscribe('quic.endpoint.created', mustCall((msg) => { + events.push('endpoint.created'); + ok(msg.endpoint); +}, 2)); + +// endpoint.listen fires once (server only). +dc.subscribe('quic.endpoint.listen', mustCall((msg) => { + events.push('endpoint.listen'); + ok(msg.endpoint); +})); + +// endpoint.closing fires once (server endpoint closes). +dc.subscribe('quic.endpoint.closing', mustCall((msg) => { + events.push('endpoint.closing'); + ok(msg.endpoint); +})); + +// endpoint.closed fires once (server endpoint closed). +dc.subscribe('quic.endpoint.closed', mustCall((msg) => { + events.push('endpoint.closed'); + ok(msg.endpoint); + ok(msg.stats, 'endpoint.closed should include stats'); +})); + +// endpoint.connect fires before a client session is created. +dc.subscribe('quic.endpoint.connect', mustCall((msg) => { + events.push('endpoint.connect'); + ok(msg.endpoint); + ok(msg.address); + ok(msg.options); +})); + +// session.created.client fires for the client session. +dc.subscribe('quic.session.created.client', mustCall((msg) => { + events.push('session.created.client'); + ok(msg.session); +})); + +// session.created.server fires for the server session. +dc.subscribe('quic.session.created.server', mustCall((msg) => { + events.push('session.created.server'); + ok(msg.session); + ok(msg.address, 'server session should include remote address'); +})); + +// session.closing fires when session.close() is called. +// Only fires for sessions where close() is explicitly called (the server). +// The client session closes via CONNECTION_CLOSE without going through close(). +dc.subscribe('quic.session.closing', mustCall((msg) => { + events.push('session.closing'); + ok(msg.session); +})); + +// session.closed fires when session is fully closed. +dc.subscribe('quic.session.closed', mustCall((msg) => { + events.push('session.closed'); + ok(msg.session); + ok(msg.stats, 'session.closed should include stats'); +}, 2)); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + serverSession.close(); + await serverSession.closed; +})); + +const clientSession = await connect(serverEndpoint.address); +await Promise.all([clientSession.opened, clientSession.closed]); + +await serverEndpoint.close(); + +// Verify key events occurred. +ok(events.includes('endpoint.created'), 'missing endpoint.created'); +ok(events.includes('endpoint.listen'), 'missing endpoint.listen'); +ok(events.includes('endpoint.connect'), 'missing endpoint.connect'); +ok(events.includes('session.created.client'), 'missing session.created.client'); +ok(events.includes('session.created.server'), 'missing session.created.server'); +ok(events.includes('endpoint.closing'), 'missing endpoint.closing'); +ok(events.includes('endpoint.closed'), 'missing endpoint.closed'); diff --git a/test/parallel/test-quic-draining-period.mjs b/test/parallel/test-quic-draining-period.mjs new file mode 100644 index 00000000000000..46a04f676902c9 --- /dev/null +++ b/test/parallel/test-quic-draining-period.mjs @@ -0,0 +1,103 @@ +// Flags: --experimental-quic --no-warnings + +// Test: drainingPeriodMultiplier option validation and behavior. +// 1. Default value (3) results in prompt session close after peer closes. +// 2. Custom value is accepted and affects draining duration. +// 3. Values below 3 are clamped to 3. +// 4. Invalid values are rejected. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// Test 1: Default drainingPeriodMultiplier (3) — session closes promptly +// after server closes, not after the full idle timeout. +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + await serverSession.close(); + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + // Measure how long clientSession.closed takes to resolve. + const start = Date.now(); + await clientSession.closed; + const elapsed = Date.now() - start; + + // With 3x PTO on localhost (~1-4ms RTT), the draining period should + // be well under 1 second. The idle timeout is 10 seconds. If the + // draining period fix is working, elapsed should be much less than 10s. + ok(elapsed < 2000, `Expected draining to complete in < 2s, took ${elapsed}ms`); + + await serverEndpoint.close(); +} + +// Test 2: Custom drainingPeriodMultiplier is accepted. +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + await serverSession.close(); + })); + + const clientSession = await connect(serverEndpoint.address, { + drainingPeriodMultiplier: 10, + }); + await clientSession.opened; + + const start = Date.now(); + await clientSession.closed; + const elapsed = Date.now() - start; + + // 10x PTO is still very short on localhost. Should complete promptly. + ok(elapsed < 2000, `Expected draining to complete in < 2s, took ${elapsed}ms`); + + await serverEndpoint.close(); +} + +// Test 3: Values below 3 are rejected by JS validation. +{ + const serverEndpoint = await listen(mustNotCall()); + + await assert.rejects( + connect(serverEndpoint.address, { + drainingPeriodMultiplier: 1, + }), + { code: 'ERR_OUT_OF_RANGE' }, + ); + + await serverEndpoint.close(); +} + +// Test 4: Invalid types are rejected. +{ + const serverEndpoint = await listen(mustNotCall(), { + transportParams: { maxIdleTimeout: 1 }, + }); + + await assert.rejects( + connect(serverEndpoint.address, { + drainingPeriodMultiplier: 'fast', + transportParams: { maxIdleTimeout: 1 }, + }), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + + await assert.rejects( + connect(serverEndpoint.address, { + drainingPeriodMultiplier: 300, + transportParams: { maxIdleTimeout: 1 }, + }), + { code: 'ERR_OUT_OF_RANGE' }, + ); + + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-edge-closing-ops.mjs b/test/parallel/test-quic-edge-closing-ops.mjs new file mode 100644 index 00000000000000..812c4be1d515c3 --- /dev/null +++ b/test/parallel/test-quic-edge-closing-ops.mjs @@ -0,0 +1,50 @@ +// Flags: --experimental-quic --no-warnings + +// Test: operations on a closing session. +// createBidirectionalStream on closing session throws. +// sendDatagram on closing session throws. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { + transportParams: { maxDatagramFrameSize: 1200 }, +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, +}); +await clientSession.opened; + +// Initiate graceful close. +clientSession.close(); + +// Creating a stream on a closing session rejects. +await rejects( + clientSession.createBidirectionalStream(), + { code: 'ERR_INVALID_STATE' }, +); + +await rejects( + clientSession.createUnidirectionalStream(), + { code: 'ERR_INVALID_STATE' }, +); + +// sendDatagram on a closing session throws. +await rejects( + clientSession.sendDatagram(new Uint8Array([1, 2, 3])), + { code: 'ERR_INVALID_STATE' }, +); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-edge-concurrent-close.mjs b/test/parallel/test-quic-edge-concurrent-close.mjs new file mode 100644 index 00000000000000..120ab0ccafbd54 --- /dev/null +++ b/test/parallel/test-quic-edge-concurrent-close.mjs @@ -0,0 +1,41 @@ +// Flags: --experimental-quic --no-warnings + +// Test: concurrent close() from both client and server. +// Both sides initiate close simultaneously. Neither should crash +// and both closed promises should settle. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Once the stream arrives, both sides close simultaneously. + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + }); + await serverSession.closed; + serverDone.resolve(); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Open a stream so the server session has work, then close from both sides. +const stream = await clientSession.createBidirectionalStream(); +stream.writer.endSync(); +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await stream.closed; + +// Client close happens around the same time as server close above. +await clientSession.close(); + +await serverDone.promise; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-edge-destroyed-ops.mjs b/test/parallel/test-quic-edge-destroyed-ops.mjs new file mode 100644 index 00000000000000..9fb8a8cefcfcb6 --- /dev/null +++ b/test/parallel/test-quic-edge-destroyed-ops.mjs @@ -0,0 +1,55 @@ +// Flags: --experimental-quic --no-warnings + +// Test: operations on destroyed session/stream. +// Operations on a destroyed session return gracefully. +// Operations on a destroyed stream return gracefully. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); + +// Destroy the stream, then try operations on it. +stream.destroy(); +strictEqual(stream.destroyed, true); + +// Operations on destroyed stream should not throw. +stream.destroy(); // Idempotent. +stream.writer.endSync(); // No-op on destroyed. + +// Destroy the session, then try operations on it. +clientSession.destroy(); +strictEqual(clientSession.destroyed, true); + +// Properties should return null/undefined gracefully. +strictEqual(clientSession.endpoint, null); +strictEqual(clientSession.path, undefined); +strictEqual(clientSession.certificate, undefined); +strictEqual(clientSession.peerCertificate, undefined); +strictEqual(clientSession.ephemeralKeyInfo, undefined); + +// destroy() again is idempotent. +clientSession.destroy(); + +// sendDatagram on destroyed session throws ERR_INVALID_STATE. +await rejects( + clientSession.sendDatagram(new Uint8Array([1])), + { code: 'ERR_INVALID_STATE' }, +); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-edge-endpoint-destroy-active.mjs b/test/parallel/test-quic-edge-endpoint-destroy-active.mjs new file mode 100644 index 00000000000000..c5790def6f187f --- /dev/null +++ b/test/parallel/test-quic-edge-endpoint-destroy-active.mjs @@ -0,0 +1,55 @@ +// Flags: --experimental-quic --no-warnings + +// Test: endpoint closed while sessions are active. +// When endpoint.close() is called while sessions are active, the +// endpoint waits for sessions to finish. When the client closes +// its session, the server session closes (via CONNECTION_CLOSE), +// and the endpoint finishes closing. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + stream.writer.endSync(); + await stream.closed; + }); + await serverSession.closed; + serverDone.resolve(); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Create a stream so there's active work. +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('hello'), +}); +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await stream.closed; + +// Close the endpoint while the server session is still active +// (the session is open but the stream is done). +serverEndpoint.close(); +strictEqual(serverEndpoint.closing, true); +strictEqual(serverEndpoint.destroyed, false); + +// The endpoint is waiting for the server session. Close the +// client session to trigger the server session to close. +await clientSession.close(); + +// The server session should close from the CONNECTION_CLOSE. +await Promise.all([serverDone.promise, serverEndpoint.closed]); +strictEqual(serverEndpoint.destroyed, true); diff --git a/test/parallel/test-quic-edge-idempotent.mjs b/test/parallel/test-quic-edge-idempotent.mjs new file mode 100644 index 00000000000000..8cb448facc501f --- /dev/null +++ b/test/parallel/test-quic-edge-idempotent.mjs @@ -0,0 +1,53 @@ +// Flags: --experimental-quic --no-warnings + +// Test: double close/destroy are idempotent. +// Double close() on session is idempotent. +// Double close() on endpoint is idempotent. +// Double destroy() on session is idempotent. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + }); + await serverSession.closed; +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Signal server to close via stream first (before we close). +const stream = await clientSession.createBidirectionalStream(); +stream.writer.endSync(); +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await stream.closed; + +// Double close() on session — both return the same promise. +const p1 = clientSession.close(); +const p2 = clientSession.close(); +strictEqual(p1, p2); +await clientSession.closed; + +// Double destroy() — second call is no-op. +clientSession.destroy(); +strictEqual(clientSession.destroyed, true); +clientSession.destroy(); // Should not throw. +strictEqual(clientSession.destroyed, true); + +// Double close() on endpoint. +const ep1 = serverEndpoint.close(); +const ep2 = serverEndpoint.close(); +strictEqual(ep1, ep2); +await serverEndpoint.closed; diff --git a/test/parallel/test-quic-edge-session-close-immediate.mjs b/test/parallel/test-quic-edge-session-close-immediate.mjs new file mode 100644 index 00000000000000..35fdbe5d2629dc --- /dev/null +++ b/test/parallel/test-quic-edge-session-close-immediate.mjs @@ -0,0 +1,27 @@ +// Flags: --experimental-quic --no-warnings + +// Test: session created and immediately closed. +// Calling close() on a session right after creation (before handshake +// completes) should gracefully close the session without crashing. + +import { hasQuic, skip } from '../common/index.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverEndpoint = await listen(async (serverSession) => { + await serverSession.closed; +}, { + transportParams: { maxIdleTimeout: 1 }, +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, +}); + +// Close immediately without waiting for opened. +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-edge-session-destroy-immediate.mjs b/test/parallel/test-quic-edge-session-destroy-immediate.mjs new file mode 100644 index 00000000000000..db4cbf3eedafc7 --- /dev/null +++ b/test/parallel/test-quic-edge-session-destroy-immediate.mjs @@ -0,0 +1,37 @@ +// Flags: --experimental-quic --no-warnings + +// Test: session created and immediately destroyed. +// Calling destroy() on a session that hasn't completed handshake should +// not crash. The opened and closed promises should settle appropriately. + +import { hasQuic, skip, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// The client destroys before handshake completes, so the server +// should never see a session. +const serverEndpoint = await listen(mustNotCall(), { + transportParams: { maxIdleTimeout: 1 }, +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, +}); + +// Destroy immediately without waiting for opened. +clientSession.destroy(); + +strictEqual(clientSession.destroyed, true); + +// Opened may reject (session destroyed before handshake completed) +// or resolve if handshake completed fast enough. +// Closed should resolve (destroy without error). +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-enable-early-data.mjs b/test/parallel/test-quic-enable-early-data.mjs new file mode 100644 index 00000000000000..0c868025240fbf --- /dev/null +++ b/test/parallel/test-quic-enable-early-data.mjs @@ -0,0 +1,60 @@ +// Flags: --experimental-quic --no-warnings + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { rejects, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +// enableEarlyData must be a boolean +await rejects(connect({ port: 1234 }, { + alpn: 'quic-test', + enableEarlyData: 'yes', +}), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +// With enableEarlyData: false, early data should not be attempted. +// (Without a session ticket, early data is never attempted regardless, +// but this verifies the option is functional and passes through to C++.) + +const serverOpened = Promise.withResolvers(); +const clientOpened = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.opened.then(mustCall((info) => { + serverOpened.resolve(); + serverSession.close(); + })); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + enableEarlyData: false, +}); + +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + servername: 'localhost', + enableEarlyData: false, +}); +clientSession.opened.then(mustCall((info) => { + strictEqual(info.earlyDataAttempted, false); + strictEqual(info.earlyDataAccepted, false); + clientOpened.resolve(); +})); + +await Promise.all([serverOpened.promise, clientOpened.promise]); +clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-endpoint-async-dispose.mjs b/test/parallel/test-quic-endpoint-async-dispose.mjs new file mode 100644 index 00000000000000..e97915aca5e258 --- /dev/null +++ b/test/parallel/test-quic-endpoint-async-dispose.mjs @@ -0,0 +1,39 @@ +// Flags: --experimental-quic --no-warnings + +// Test: Symbol.asyncDispose for endpoint and session. +// endpoint[Symbol.asyncDispose] closes the endpoint. +// session[Symbol.asyncDispose] closes the session. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + // Wait for the session to close (triggered by the client's close). + await serverSession.closed; + serverDone.resolve(); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// session[Symbol.asyncDispose] closes the session. +strictEqual(typeof clientSession[Symbol.asyncDispose], 'function'); +await clientSession[Symbol.asyncDispose](); +strictEqual(clientSession.destroyed, true); + +await serverDone.promise; + +// endpoint[Symbol.asyncDispose] closes the endpoint. +strictEqual(typeof serverEndpoint[Symbol.asyncDispose], 'function'); +await serverEndpoint[Symbol.asyncDispose](); +strictEqual(serverEndpoint.destroyed, true); diff --git a/test/parallel/test-quic-endpoint-bind-failure.mjs b/test/parallel/test-quic-endpoint-bind-failure.mjs new file mode 100644 index 00000000000000..60ddb6dd9295b9 --- /dev/null +++ b/test/parallel/test-quic-endpoint-bind-failure.mjs @@ -0,0 +1,49 @@ +// Flags: --experimental-quic --no-warnings + +// Test: ERR_QUIC_ENDPOINT_CLOSED on bind failure. +// Attempting to listen on a port that's already in use by another +// QUIC endpoint produces ERR_QUIC_ENDPOINT_CLOSED with a +// 'Bind failure' context. The listen() call may return an endpoint +// that is immediately destroyed — the error surfaces via the +// endpoint's closed promise. + +import { hasQuic, skip, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual, ok, rejects } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const sni = { '*': { keys: [key], certs: [cert] } }; +const alpn = ['quic-test']; + +// Bind first endpoint to get an assigned port. +const ep1 = await listen(mustNotCall(), { sni, alpn }); +const { port } = ep1.address; +ok(port > 0); + +// Attempt to listen on the same port — should fail with bind error. +// listen() returns an endpoint that is immediately destroyed. +const ep2 = await listen(mustNotCall(), { + sni, + alpn, + endpoint: { address: `127.0.0.1:${port}` }, +}); +strictEqual(ep2.destroyed, true); + +// The bind failure surfaces as a rejected closed promise. +await rejects(ep2.closed, { + code: 'ERR_QUIC_ENDPOINT_CLOSED', + message: /Bind failure/, +}); + +await ep1.close(); diff --git a/test/parallel/test-quic-endpoint-bind.mjs b/test/parallel/test-quic-endpoint-bind.mjs new file mode 100644 index 00000000000000..dc91ccda01b73f --- /dev/null +++ b/test/parallel/test-quic-endpoint-bind.mjs @@ -0,0 +1,56 @@ +// Flags: --experimental-quic --no-warnings + +// Test: endpoint binding options. +// Binding to specific address. +// Binding to specific port. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual, ok } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect, QuicEndpoint } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +// Binding to a specific port. +{ + const endpoint = new QuicEndpoint({ + address: { address: '127.0.0.1', port: 0 }, + }); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + }), { + endpoint, + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + transportParams: { maxIdleTimeout: 1 }, + }); + + // The address should reflect what we bound to. + const addr = serverEndpoint.address; + strictEqual(addr.address, '127.0.0.1'); + strictEqual(addr.family, 'ipv4'); + strictEqual(typeof addr.port, 'number'); + ok(addr.port > 0, 'port should be assigned'); + + // Verify a client can connect to the bound address. + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + }); + await clientSession.opened; + await clientSession.close(); + + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-endpoint-busy.mjs b/test/parallel/test-quic-endpoint-busy.mjs new file mode 100644 index 00000000000000..5df58ce5886542 --- /dev/null +++ b/test/parallel/test-quic-endpoint-busy.mjs @@ -0,0 +1,73 @@ +// Flags: --experimental-quic --no-warnings + +// Test: endpoint.busy rejects new sessions. +// When the busy flag is set, the server sends CONNECTION_REFUSED +// for new connection attempts. Existing sessions are not affected. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { rejects, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect, QuicEndpoint } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const endpoint = new QuicEndpoint(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + await serverSession.closed; +}), { + endpoint, + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + transportParams: { maxIdleTimeout: 2 }, +}); + +// First connection before busy — should succeed. +const cs1 = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 2 }, +}); +await cs1.opened; + +// Set the endpoint busy. +strictEqual(endpoint.busy, false); +endpoint.busy = true; +strictEqual(endpoint.busy, true); + +// Second connection while busy — server rejects. +const cs2 = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + onerror: mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + }), +}); + +await rejects(cs2.opened, { + code: 'ERR_QUIC_TRANSPORT_ERROR', +}); + +await rejects(cs2.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', +}); + +// Unset busy. +endpoint.busy = false; +strictEqual(endpoint.busy, false); + +// Clean up. +await cs1.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-endpoint-close-destroy.mjs b/test/parallel/test-quic-endpoint-close-destroy.mjs new file mode 100644 index 00000000000000..28eb069552086d --- /dev/null +++ b/test/parallel/test-quic-endpoint-close-destroy.mjs @@ -0,0 +1,79 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: endpoint close and destroy lifecycle. +// endpoint.close() waits for active sessions to finish. +// endpoint.destroy() forcefully closes all sessions. +// endpoint.closing property reflects close state. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +const { setTimeout } = await import('node:timers/promises'); +const { bytes } = await import('stream/iter'); + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + // Set onstream before awaiting anything so the callback isn't + // missed if data arrives quickly. + const streamDone = Promise.withResolvers(); + serverSession.onstream = mustCall(async (stream) => { + await bytes(stream); + stream.writer.endSync(); + await stream.closed; + streamDone.resolve(); + }); + + await serverSession.opened; + + // Before close, closing is false. + strictEqual(serverEndpoint.closing, false); + + // Initiate endpoint close — it should wait for this session. + serverEndpoint.close(); + + // After close() is called, closing is true. + strictEqual(serverEndpoint.closing, true); + + // The endpoint's closed promise should NOT resolve yet — session is open. + let endpointClosed = false; + serverEndpoint.closed.then(mustCall(() => { endpointClosed = true; })); + + // Give a tick to confirm endpoint hasn't closed yet. + await setTimeout(100); + strictEqual(endpointClosed, false); + + // Wait for the stream to complete, then close the session. + await streamDone.promise; + serverSession.close(); + serverDone.resolve(); + }), { + transportParams: { maxIdleTimeout: 1 }, + }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, + }); + await clientSession.opened; + + // Send some data so the server session has work to do. + const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('test'), + }); + + await Promise.all([serverDone.promise, + stream.closed, + serverEndpoint.closed]); + + strictEqual(serverEndpoint.destroyed, true); +} diff --git a/test/parallel/test-quic-endpoint-destroy-after-close.mjs b/test/parallel/test-quic-endpoint-destroy-after-close.mjs new file mode 100644 index 00000000000000..66e1f22accbdab --- /dev/null +++ b/test/parallel/test-quic-endpoint-destroy-after-close.mjs @@ -0,0 +1,66 @@ +// Flags: --experimental-quic --no-warnings + +// Test: When `endpoint.destroy(err)` is called *after* `endpoint.close()` +// has already initiated graceful shutdown, the error must still surface +// on `endpoint.closed` instead of being swallowed. First-error-wins +// semantics: a fatal error reported via `destroy(err)` after a +// graceful close was already in flight is still propagated through +// `endpoint.closed`. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// Hold the connection open for a while so that close() has actual work +// to wait on. +const transportParams = { maxIdleTimeout: 5000 }; + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + // Don't observe `serverSession.closed` directly here - the cascade + // from `endpoint.destroy(err)` will reject it, and the outer + // `markPromiseAsHandled` in the cascade keeps that from surfacing as + // an unhandled rejection. We just need this callback to keep running + // until the test ends. + try { + await serverSession.closed; + } catch { + // Expected: the cascade destroys the session with an error. + } + serverDone.resolve(); +}), { transportParams }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams, +}); +await clientSession.opened; + +// Step 1: kick off a graceful close. After this, the endpoint is in +// the "closing" state - so `#isClosedOrClosing` is true and the +// pre-fix `destroy()` would skip recording `#pendingError`. +const closingPromise = serverEndpoint.close(); + +// Step 2: while the graceful close is in flight, call destroy(err). +// With the fix, the error is still recorded and surfaces on +// `endpoint.closed`. Without the fix, it'd be silently dropped. +const destroyError = new Error('destroy after close'); +const closedAssertion = rejects(serverEndpoint.closed, destroyError); + +serverEndpoint.destroy(destroyError); + +await closedAssertion; + +// `endpoint.close()` returns the same promise as `endpoint.closed`, so +// it should reject with the same error as well. +await rejects(closingPromise, destroyError); + +await serverDone.promise; +clientSession.destroy(); diff --git a/test/parallel/test-quic-endpoint-destroy-cascade-close-frame.mjs b/test/parallel/test-quic-endpoint-destroy-cascade-close-frame.mjs new file mode 100644 index 00000000000000..0cf59bb65e2007 --- /dev/null +++ b/test/parallel/test-quic-endpoint-destroy-cascade-close-frame.mjs @@ -0,0 +1,86 @@ +// Flags: --experimental-quic --no-warnings + +// Test: when `endpoint.destroy(err)` is called on a side that has open, +// fully-handshaked sessions, the cascade through `session.destroy(err)` +// passes close options derived from the error so each session emits a +// CONNECTION_CLOSE on the wire. The peer learns about the teardown via +// that frame, not via its own idle timer. +// +// How the test distinguishes the two cases: +// +// * If CONNECTION_CLOSE is sent, the client's `session.closed` +// rejects after the network round-trip with an +// `ERR_QUIC_TRANSPORT_ERROR` carrying the cascaded code. +// * If CONNECTION_CLOSE is NOT sent, the client only learns of the +// teardown via its own idle timer, which hits `[kFinishClose]` +// case 3 (`/* Idle close */`) and resolves `session.closed` +// *cleanly*. The `mustCall` rejection-handler would then never +// fire and the test fails. +// +// A short `maxIdleTimeout` keeps the failure mode fast. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// `maxIdleTimeout` is measured in seconds. One second is far longer +// than CONNECTION_CLOSE on loopback needs to win, while still short +// enough that a regression in which `CONNECTION_CLOSE` is *not* sent +// fails the test promptly: the idle-close path takes the +// `[kFinishClose]` case-3 branch and resolves `session.closed` cleanly +// instead of rejecting, so the rejection-handler `mustCall` below +// would fail with "expected exactly 1, actual 0". +const transportParams = { maxIdleTimeout: 1 }; + +const serverError = new Error('cascade close frame test'); + +// Capture the server-side session and wait for *its* `onhandshake` to +// fire before triggering the cascade. The client's `session.opened` +// resolves as soon as the client receives the server's TLS Finished, +// which can land slightly *before* the server has processed the +// client's reciprocal Finished. Without this synchronization the +// server-side `kHandshakeCompleted` flag may still be `false` at +// destroy time and the cascade would skip emitting `CONNECTION_CLOSE` +// (which is the regression this test is designed to catch). +const serverHandshake = Promise.withResolvers(); +const onsession = mustCall((serverSession) => { + serverSession.onhandshake = mustCall(() => { + serverHandshake.resolve(); + }); +}); +const serverEndpoint = await listen(onsession, { transportParams }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams, +}); +await clientSession.opened; +await serverHandshake.promise; + +// Attach the rejection handlers BEFORE triggering destroy so neither +// `serverEndpoint.closed` (rejects with `serverError` via the +// `#pendingError` semantics from B7) nor `clientSession.closed` +// (rejects with the transport error decoded from the CONNECTION_CLOSE +// frame) ends up as an unhandled rejection in the brief window before +// this test gets back to awaiting them. +const serverClosedAssertion = rejects(serverEndpoint.closed, serverError); +const clientClosedAssertion = rejects(clientSession.closed, mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + return true; +})); + +serverEndpoint.destroy(serverError); + +await clientClosedAssertion; +await serverClosedAssertion; + +// Explicit cleanup: the client-side session has been rejected via +// CONNECTION_CLOSE but the underlying client endpoint is still alive. +// Tear it down so the event loop drains promptly. +clientSession.destroy(); diff --git a/test/parallel/test-quic-endpoint-idle-timeout.mjs b/test/parallel/test-quic-endpoint-idle-timeout.mjs new file mode 100644 index 00000000000000..90d19ca455411c --- /dev/null +++ b/test/parallel/test-quic-endpoint-idle-timeout.mjs @@ -0,0 +1,79 @@ +// Flags: --experimental-quic --no-warnings + +// Test: endpoint idle timeout behavior. +// When an endpoint has idleTimeout > 0 and becomes idle (no sessions, +// not listening), it stays alive for the timeout duration before +// being destroyed. With idleTimeout = 0 (default), the endpoint is +// destroyed immediately when idle. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { QuicEndpoint } = await import('node:quic'); +const { listen, connect } = await import('../common/quic.mjs'); + +// --- Test 1: Default idleTimeout (0) --- endpoint becomes idle +// immediately when it has no sessions and is not listening. The +// UDP handle is unref'd so it won't block process exit. +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + })); + + // Create a client with an explicit endpoint so we can track it. + const clientEndpoint = new QuicEndpoint(); + const client = await connect(serverEndpoint.address, { + endpoint: clientEndpoint, + verifyPeer: 'manual', + }); + await client.opened; + + // Endpoint is alive while the session is active. + ok(!clientEndpoint.destroyed, 'endpoint should be alive'); + + await client.close(); + + // The endpoint's UDP handle is unref'd when all sessions close, + // so it won't block process exit. Explicitly close it for cleanup. + await clientEndpoint.close(); + ok(clientEndpoint.destroyed, 'endpoint should be destroyed after close'); + + await serverEndpoint.close(); +} + +// --- Test 2: idleTimeout > 0 --- endpoint stays alive briefly +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + })); + + // Create endpoint with a 1-second idle timeout. + const clientEndpoint = new QuicEndpoint({ idleTimeout: 1 }); + const client = await connect(serverEndpoint.address, { + endpoint: clientEndpoint, + verifyPeer: 'manual', + }); + await client.opened; + await client.close(); + + // The endpoint should NOT be immediately destroyed — idle timer + // is running. + ok(!clientEndpoint.destroyed, + 'endpoint should still be alive during idle timeout'); + + // Wait for the idle timeout to fire (1 second + margin). + // Use a ref'd timer to keep the event loop alive while the + // unref'd idle timer runs. + await setTimeout(2000); + ok(clientEndpoint.destroyed, + 'endpoint should be destroyed after idle timeout'); + + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-endpoint-onsession-throws.mjs b/test/parallel/test-quic-endpoint-onsession-throws.mjs new file mode 100644 index 00000000000000..7f9a5483820f09 --- /dev/null +++ b/test/parallel/test-quic-endpoint-onsession-throws.mjs @@ -0,0 +1,76 @@ +// Flags: --experimental-quic --no-warnings + +// Test: When the user's `onsession` callback throws synchronously or +// returns a rejected promise, the endpoint is destroyed with that +// error rather than surfacing as an unhandled exception/rejection out +// of the C++ -> JS callback boundary. The error is observable through +// `endpoint.closed` rejecting with the thrown value. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const transportParams = { maxIdleTimeout: 1 }; + +// ------------------------------------------------------------------- +// 1. Synchronous throw in onsession -> endpoint.closed rejects with +// that error. +// ------------------------------------------------------------------- +{ + const sessionError = new Error('sync onsession failure'); + + const serverEndpoint = await listen(mustCall(() => { + throw sessionError; + }), { transportParams }); + + // Attach the rejection handler BEFORE initiating the connection so + // there is no window where serverEndpoint.closed is rejected without + // a handler attached. The throw inside `onsession` is delivered + // synchronously from the C++ -> JS callback, so the rejection can + // arrive on the very next microtask after `connect()` returns. + const closedAssertion = rejects(serverEndpoint.closed, sessionError); + + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + + await closedAssertion; + + // Explicitly tear down the client side. Even though the endpoint + // cascade now sends CONNECTION_CLOSE so the client learns about the + // teardown promptly, dropping our reference here keeps the test + // robust to network-dropped close packets and stops the event loop + // from waiting on the client's idle timer to expire. + clientSession.destroy(); + await rejects(serverEndpoint.close(), sessionError); +} + +// ------------------------------------------------------------------- +// 2. onsession returns a rejected promise -> endpoint.closed rejects +// with that error. +// ------------------------------------------------------------------- +{ + const sessionError = new Error('async onsession failure'); + + const serverEndpoint = await listen(mustCall(async () => { + throw sessionError; + }), { transportParams, onerror() {} }); + + const closedAssertion = rejects(serverEndpoint.closed, sessionError); + + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + + await closedAssertion; + + clientSession.destroy(); + await rejects(serverEndpoint.close(), sessionError); +} diff --git a/test/parallel/test-quic-endpoint-reuse.mjs b/test/parallel/test-quic-endpoint-reuse.mjs new file mode 100644 index 00000000000000..fc4af988595c4b --- /dev/null +++ b/test/parallel/test-quic-endpoint-reuse.mjs @@ -0,0 +1,89 @@ +// Flags: --experimental-quic --no-warnings + +// Test: endpoint reuse behavior for connect(). +// 1. Multiple connect() calls without explicit endpoint share +// the same endpoint (connection pooling). +// 2. connect() with reuseEndpoint: false creates a separate endpoint. +// 3. connect() to the same address as a listening endpoint does NOT +// reuse the listening endpoint (self-connect exclusion). +// 4. connect() to a different address with a listening endpoint in +// the registry reuses the listening endpoint (dual-role). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, notStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// --- Test 1: connect() reuses endpoints by default --- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + }, 2)); + + const client1 = await connect(serverEndpoint.address); + await client1.opened; + + const client2 = await connect(serverEndpoint.address); + await client2.opened; + + // Both client sessions should share the same endpoint because + // findSuitableEndpoint returns the first available non-listening + // non-closing endpoint. After client1 is created, its endpoint + // is available for client2. + strictEqual(client1.endpoint, client2.endpoint, + 'client sessions should share an endpoint'); + + await client1.close(); + await client2.close(); + await serverEndpoint.close(); +} + +// --- Test 2: reuseEndpoint: false creates separate endpoints --- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + }, 2)); + + const client1 = await connect(serverEndpoint.address, { + reuseEndpoint: false, + }); + await client1.opened; + + const client2 = await connect(serverEndpoint.address, { + reuseEndpoint: false, + }); + await client2.opened; + + notStrictEqual(client1.endpoint, client2.endpoint, + 'client sessions should have separate endpoints'); + + await client1.close(); + await client2.close(); + await serverEndpoint.close(); +} + +// --- Test 3: connect() to a listening endpoint's address is not reused --- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + })); + + const client = await connect(serverEndpoint.address); + await client.opened; + + // The client endpoint should NOT be the server endpoint, even though + // the server endpoint is in the registry. Self-connect is excluded + // because the client's DCID associations would collide with the + // server's session routing on the same endpoint. + notStrictEqual(client.endpoint, serverEndpoint, + 'client should not reuse the server endpoint'); + + await client.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-endpoint-state-transitions.mjs b/test/parallel/test-quic-endpoint-state-transitions.mjs new file mode 100644 index 00000000000000..5997f812ae8d72 --- /dev/null +++ b/test/parallel/test-quic-endpoint-state-transitions.mjs @@ -0,0 +1,88 @@ +// Flags: --expose-internals --experimental-quic --no-warnings + +// Test: endpoint state transitions. +// State transitions: created → bound → receiving → listening → closing. +// Binding to 0.0.0.0 (all interfaces). +// Binding to ::1 (IPv6 loopback). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect, QuicEndpoint } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +{ + const endpoint = new QuicEndpoint(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + serverSession.close(); + await serverSession.closed; + }), { + endpoint, + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + }); + + // After listen, the endpoint should be listening. + strictEqual(serverEndpoint.listening, true); + strictEqual(serverEndpoint.closing, false); + strictEqual(serverEndpoint.destroyed, false); + + const cs = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + }); + await cs.opened; + await cs.close(); + + // After close(), closing transitions to true. The endpoint is still + // "listening" in the sense that it holds the socket, but closing is true. + serverEndpoint.close(); + strictEqual(serverEndpoint.closing, true); + + await serverEndpoint.closed; + strictEqual(serverEndpoint.destroyed, true); +} + +// Binding to 0.0.0.0. +{ + const endpoint = new QuicEndpoint({ + address: { address: '0.0.0.0', port: 0 }, + }); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + }), { + endpoint, + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + transportParams: { maxIdleTimeout: 1 }, + }); + + const addr = serverEndpoint.address; + strictEqual(addr.address, '0.0.0.0'); + ok(addr.port > 0); + + // Connect via 127.0.0.1 since 0.0.0.0 listens on all interfaces. + const cs = await connect(`127.0.0.1:${addr.port}`, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + }); + await cs.opened; + await cs.close(); + + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-error-class.mjs b/test/parallel/test-quic-error-class.mjs new file mode 100644 index 00000000000000..d8f01c6cbd6724 --- /dev/null +++ b/test/parallel/test-quic-error-class.mjs @@ -0,0 +1,160 @@ +// Flags: --experimental-quic --no-warnings + +// Test: QuicError class. Validates the public surface of the new +// `QuicError` class exported from `node:quic`: +// * Required `message` and `options.errorCode`. +// * `errorCode` accepts bigint or number; coerces to bigint. +// * `errorCode` is range-checked against the QUIC 62-bit varint +// maximum. +// * `type` defaults to 'application' and is restricted to +// 'application' | 'transport'. +// * `code` defaults to 'ERR_QUIC_STREAM_ABORTED' and may be +// overridden with any Node.js-style error code string. + +import { hasQuic, skip } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, throws, ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { QuicError } = await import('node:quic'); + +// Sanity: QuicError is a function (class) and extends Error. +strictEqual(typeof QuicError, 'function'); +ok(new QuicError('msg', { errorCode: 0n }) instanceof Error); + +// Required arguments. +// `message` must be a string -> validateString throws ERR_INVALID_ARG_TYPE. +throws(() => new QuicError(), { code: 'ERR_INVALID_ARG_TYPE' }); +// `options` defaults to an empty object, so `errorCode` is missing. +throws(() => new QuicError('msg'), { code: 'ERR_MISSING_ARGS' }); +// Explicit non-object options -> validateObject throws ERR_INVALID_ARG_TYPE. +throws(() => new QuicError('msg', null), { code: 'ERR_INVALID_ARG_TYPE' }); +// Empty options -> errorCode missing -> ERR_MISSING_ARGS. +throws(() => new QuicError('msg', {}), { code: 'ERR_MISSING_ARGS' }); + +// `message` must be a string. +throws(() => new QuicError(42, { errorCode: 0n }), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +// `errorCode` must be bigint or number. +throws(() => new QuicError('msg', { errorCode: 'oops' }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +throws(() => new QuicError('msg', { errorCode: true }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +// `null` is preserved by destructuring (only `undefined` triggers the +// default), so it flows through to the type check rather than the +// missing-args check. +throws(() => new QuicError('msg', { errorCode: null }), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +// `errorCode` range checks. +throws(() => new QuicError('msg', { errorCode: -1 }), { + code: 'ERR_OUT_OF_RANGE', +}); +throws(() => new QuicError('msg', { errorCode: -1n }), { + code: 'ERR_OUT_OF_RANGE', +}); +// 2**62 is the first invalid value (max varint is 2**62 - 1). +throws(() => new QuicError('msg', { errorCode: 1n << 62n }), { + code: 'ERR_OUT_OF_RANGE', +}); + +// `type` validation. +throws(() => new QuicError('msg', { errorCode: 0n, type: 'bogus' }), { + code: 'ERR_INVALID_ARG_VALUE', +}); +throws(() => new QuicError('msg', { errorCode: 0n, type: 42 }), { + code: 'ERR_INVALID_ARG_VALUE', +}); + +// `code` (Node.js error code) must be a string when supplied. +throws(() => new QuicError('msg', { errorCode: 0n, code: 42 }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +throws(() => new QuicError('msg', { errorCode: 0n, code: null }), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +// Happy paths. + +// 1. Bigint errorCode, default type, default Node code. +{ + const err = new QuicError('something broke', { errorCode: 0x42n }); + strictEqual(err.message, 'something broke'); + strictEqual(err.code, 'ERR_QUIC_STREAM_ABORTED'); + strictEqual(err.errorCode, 0x42n); + strictEqual(err.type, 'application'); + ok(err instanceof Error); + ok(err instanceof QuicError); +} + +// 2. Number errorCode, coerced to bigint. +{ + const err = new QuicError('numeric code', { errorCode: 5 }); + strictEqual(err.errorCode, 5n); + strictEqual(typeof err.errorCode, 'bigint'); +} + +// 3. Boundary: 0n is allowed. +{ + const err = new QuicError('zero', { errorCode: 0n }); + strictEqual(err.errorCode, 0n); +} + +// 4. Boundary: 2**62 - 1 is allowed (largest valid 62-bit varint). +{ + const max = (1n << 62n) - 1n; + const err = new QuicError('max', { errorCode: max }); + strictEqual(err.errorCode, max); +} + +// 5. Explicit type='transport'. +{ + const err = new QuicError('transport-level', { + errorCode: 0x1n, + type: 'transport', + }); + strictEqual(err.type, 'transport'); +} + +// 6. Explicit type='application' (default). +{ + const err = new QuicError('app-level', { + errorCode: 0x102n, + type: 'application', + }); + strictEqual(err.type, 'application'); +} + +// 7. Custom Node.js error code string via options.code. +{ + const err = new QuicError('custom', { + errorCode: 0x10cn, + code: 'ERR_CUSTOM_QUIC_FAILURE', + }); + strictEqual(err.code, 'ERR_CUSTOM_QUIC_FAILURE'); + strictEqual(err.errorCode, 0x10cn); +} + +// 8. Properties are read-only via getters (errorCode, type backed by +// private fields). The base Error's message is writable but the +// QuicError-specific accessors must not be assignable through the +// prototype. +{ + const err = new QuicError('msg', { errorCode: 0x1n }); + // The getters live on the prototype; assigning through the instance + // is silently ignored in non-strict mode (modules are strict, so + // this throws TypeError). We just assert the value is unchanged. + throws(() => { err.errorCode = 0xffn; }, { name: 'TypeError' }); + throws(() => { err.type = 'transport'; }, { name: 'TypeError' }); + strictEqual(err.errorCode, 0x1n); + strictEqual(err.type, 'application'); +} diff --git a/test/parallel/test-quic-error-destroy-rejects-promises.mjs b/test/parallel/test-quic-error-destroy-rejects-promises.mjs new file mode 100644 index 00000000000000..e8ec070f3baa06 --- /dev/null +++ b/test/parallel/test-quic-error-destroy-rejects-promises.mjs @@ -0,0 +1,59 @@ +// Flags: --experimental-quic --no-warnings + +// Test: session.destroy(error) rejects both opened and closed promises +// . +// When destroyed before the handshake completes, both opened and closed +// reject. When destroyed after, opened stays resolved and closed rejects. + +import { hasQuic, skip } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const transportParams = { maxIdleTimeout: 1 }; + +// The server may see 1 or 2 sessions — the first client destroys before +// the handshake completes, so the server session may or may not be created. +const serverEndpoint = await listen(async (serverSession) => { + await serverSession.closed; +}, { transportParams }); + +// First client: destroy BEFORE the handshake completes. +{ + const testError = new Error('early destroy'); + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + + // Destroy immediately — the handshake hasn't completed yet. + clientSession.destroy(testError); + + // Both opened and closed should reject with the same error. + await rejects(clientSession.opened, testError); + await rejects(clientSession.closed, testError); +} + +// Second client: destroy AFTER the handshake completes. +{ + const testError = new Error('late destroy'); + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + await clientSession.opened; + + clientSession.destroy(testError); + + // Opened already resolved — stays resolved. + await clientSession.opened; + + // Closed rejects with the error. + await rejects(clientSession.closed, testError); +} + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-exports-constants.mjs b/test/parallel/test-quic-exports-constants.mjs new file mode 100644 index 00000000000000..da1269723f8008 --- /dev/null +++ b/test/parallel/test-quic-exports-constants.mjs @@ -0,0 +1,49 @@ +// Flags: --experimental-quic --no-warnings + +// Test: node:quic exports and constants. + +import { hasQuic, skip } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, throws, ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const quic = await import('node:quic'); + +// Top-level exports. +strictEqual(typeof quic.listen, 'function'); +strictEqual(typeof quic.connect, 'function'); +strictEqual(typeof quic.QuicEndpoint, 'function'); +strictEqual(typeof quic.QuicSession, 'function'); +strictEqual(typeof quic.QuicStream, 'function'); +strictEqual(typeof quic.constants, 'object'); + +// Congestion control constants. +strictEqual(quic.constants.cc.RENO, 'reno'); +strictEqual(quic.constants.cc.CUBIC, 'cubic'); +strictEqual(quic.constants.cc.BBR, 'bbr'); + +// DEFAULT_CIPHERS. +strictEqual(typeof quic.constants.DEFAULT_CIPHERS, 'string'); +ok(quic.constants.DEFAULT_CIPHERS.length > 0); +ok(quic.constants.DEFAULT_CIPHERS.includes('TLS_AES_128_GCM_SHA256')); + +// DEFAULT_GROUPS. +strictEqual(typeof quic.constants.DEFAULT_GROUPS, 'string'); +ok(quic.constants.DEFAULT_GROUPS.length > 0); + +// QuicEndpoint can be constructed directly. +// QuicSession and QuicStream cannot — they throw ERR_ILLEGAL_CONSTRUCTOR. +{ + const ep = new quic.QuicEndpoint(); + ok(ep instanceof quic.QuicEndpoint); +} +throws(() => new quic.QuicSession(), { + code: 'ERR_ILLEGAL_CONSTRUCTOR', +}); +throws(() => new quic.QuicStream(), { + code: 'ERR_ILLEGAL_CONSTRUCTOR', +}); diff --git a/test/parallel/test-quic-exports.mjs b/test/parallel/test-quic-exports.mjs new file mode 100644 index 00000000000000..1fba0d96cbcad8 --- /dev/null +++ b/test/parallel/test-quic-exports.mjs @@ -0,0 +1,39 @@ +// Flags: --experimental-quic --no-warnings +import { hasQuic, skip } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, throws } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const quic = await import('node:quic'); + +// Test that the main exports exist and are of the correct type. +strictEqual(typeof quic.connect, 'function'); +strictEqual(typeof quic.listen, 'function'); +strictEqual(typeof quic.QuicEndpoint, 'function'); +strictEqual(typeof quic.QuicSession, 'function'); +strictEqual(typeof quic.QuicStream, 'function'); +strictEqual(typeof quic.QuicEndpoint.Stats, 'function'); +strictEqual(typeof quic.QuicSession.Stats, 'function'); +strictEqual(typeof quic.QuicStream.Stats, 'function'); +strictEqual(typeof quic.constants, 'object'); +strictEqual(typeof quic.constants.cc, 'object'); + +// Test that the constants exist and are of the correct type. +strictEqual(quic.constants.cc.RENO, 'reno'); +strictEqual(quic.constants.cc.CUBIC, 'cubic'); +strictEqual(quic.constants.cc.BBR, 'bbr'); +strictEqual(quic.constants.DEFAULT_CIPHERS, + 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:' + + 'TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256'); +strictEqual(quic.constants.DEFAULT_GROUPS, 'X25519:P-256:P-384:P-521'); + +// Ensure the constants are.. well, constant. +throws(() => { quic.constants.cc.RENO = 'foo'; }, TypeError); +strictEqual(quic.constants.cc.RENO, 'reno'); + +throws(() => { quic.constants.DEFAULT_CIPHERS = 123; }, TypeError); +strictEqual(typeof quic.constants.DEFAULT_CIPHERS, 'string'); diff --git a/test/parallel/test-quic-flow-control-blob.mjs b/test/parallel/test-quic-flow-control-blob.mjs new file mode 100644 index 00000000000000..e0ce18f32a381e --- /dev/null +++ b/test/parallel/test-quic-flow-control-blob.mjs @@ -0,0 +1,50 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: blob body larger than stream data window. +// A Blob body that exceeds the initial stream data window should +// still complete successfully — ngtcp2 handles the flow control +// extensions transparently for one-shot body sources. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { deepStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +// 8KB blob, 1KB stream window — requires flow control extension. +const data = new Uint8Array(8192); +for (let i = 0; i < data.length; i++) data[i] = i & 0xFF; +const blob = new Blob([data]); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + deepStrictEqual(received, data); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + transportParams: { initialMaxStreamDataBidiRemote: 1024 }, +}); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: blob, +}); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-flow-control-block-resume.mjs b/test/parallel/test-quic-flow-control-block-resume.mjs new file mode 100644 index 00000000000000..df8630877589a7 --- /dev/null +++ b/test/parallel/test-quic-flow-control-block-resume.mjs @@ -0,0 +1,52 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: small flow control window blocks sender, resumes after FC +// update. +// With a very small initialMaxStreamDataBidiRemote, the sender +// blocks when the window is exhausted. The transfer completes +// successfully after the receiver extends the window. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const dataLength = 8192; +const data = new Uint8Array(dataLength); +for (let i = 0; i < dataLength; i++) data[i] = i & 0xff; + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Read all data — this extends the flow control window. + const received = await bytes(stream); + strictEqual(received.byteLength, dataLength); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + // Very small window — sender will block multiple times. + transportParams: { initialMaxStreamDataBidiRemote: 128 }, +}); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +stream.setBody(data); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + +await Promise.all([stream.closed, serverDone.promise, clientSession.closed]); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-flow-control-params.mjs b/test/parallel/test-quic-flow-control-params.mjs new file mode 100644 index 00000000000000..72fe43263a8cd3 --- /dev/null +++ b/test/parallel/test-quic-flow-control-params.mjs @@ -0,0 +1,72 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: flow control transport parameters. +// initialMaxData limits total connection-level data. +// initialMaxStreamDataBidiLocal limits stream data for locally +// initiated bidi streams (server perspective for server-opened). +// initialMaxStreamDataBidiRemote limits stream data for remotely +// initiated bidi streams (server perspective for client-opened). +// These tests verify that data transfers complete successfully even when +// flow control windows are very small, proving that flow control extension +// (MAX_DATA / MAX_STREAM_DATA) works correctly. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes, drainableProtocol: dp } = await import('stream/iter'); + +const encoder = new TextEncoder(); + +// Small initialMaxStreamDataBidiRemote — limits how much the +// client can send initially before the server extends flow control. +{ + const message = 'a]'.repeat(2048); // 4KB, larger than the 1KB window + const expected = encoder.encode(message); + + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, expected.byteLength); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + }), { + // Very small stream window — forces multiple flow control extensions. + transportParams: { initialMaxStreamDataBidiRemote: 1024 }, + }); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + highWaterMark: 512, + }); + const w = stream.writer; + + // Write in small chunks, respecting backpressure. + const chunkSize = 256; + for (let offset = 0; offset < expected.byteLength; offset += chunkSize) { + const chunk = expected.slice(offset, offset + chunkSize); + while (!w.writeSync(chunk)) { + const drain = w[dp](); + if (drain) await drain; + } + } + w.endSync(); + + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await Promise.all([stream.closed, serverDone.promise]); + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-flow-control-uni.mjs b/test/parallel/test-quic-flow-control-uni.mjs new file mode 100644 index 00000000000000..11685700d6da1e --- /dev/null +++ b/test/parallel/test-quic-flow-control-uni.mjs @@ -0,0 +1,58 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: uni stream flow control. +// initialMaxStreamDataUni limits the flow control window for +// unidirectional streams. Data transfer still completes because +// the receiver extends the window. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes, drainableProtocol: dp } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const message = 'x'.repeat(4096); // 4KB, larger than the 1KB window +const expected = encoder.encode(message); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, expected.byteLength); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + transportParams: { initialMaxStreamDataUni: 1024 }, +}); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createUnidirectionalStream({ + highWaterMark: 512, +}); +const w = stream.writer; + +const chunkSize = 256; +for (let offset = 0; offset < expected.byteLength; offset += chunkSize) { + const chunk = expected.slice(offset, offset + chunkSize); + while (!w.writeSync(chunk)) { + const drain = w[dp](); + if (drain) await drain; + } +} +w.endSync(); + +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-callback-errors.mjs b/test/parallel/test-quic-h3-callback-errors.mjs new file mode 100644 index 00000000000000..226a3f6b96fbd5 --- /dev/null +++ b/test/parallel/test-quic-h3-callback-errors.mjs @@ -0,0 +1,283 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 callback error handling. +// Sync throw in onorigin callback destroys the session +// Sync throw in onheaders callback destroys the stream +// Async rejection in onheaders callback destroys the stream +// Sync throw in ontrailers callback destroys the stream +// Sync throw in onwanttrailers callback destroys the stream + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual, rejects } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const encoder = new TextEncoder(); + +async function makeServer(onheadersHandler, extraOpts = {}) { + const done = Promise.withResolvers(); + const ep = await listen(mustCall(async (ss) => { + ss.onstream = mustCall((stream) => { + // The server completes its response before the client's + // callback throws, so the server stream always resolves. + stream.closed.then(mustCall()); + }); + await ss.closed; + done.resolve(); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + transportParams: { maxIdleTimeout: 1 }, + onheaders: onheadersHandler, + ...extraOpts, + }); + return { ep, done }; +} + +// Sync throw in onheaders callback destroys the stream. +{ + const { ep, done } = await makeServer( + mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('ok')); + this.writer.endSync(); + }), + ); + + const c = await connect(ep.address, { + servername: 'localhost', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + }); + await c.opened; + + const s = await c.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function() { + throw new Error('onheaders sync error'); + }), + }); + + await rejects(s.closed, mustCall((err) => { + strictEqual(err.message, 'onheaders sync error'); + return true; + })); + strictEqual(s.destroyed, true); + + c.close(); + await done.promise; + ep.close(); +} + +// Async rejection in onheaders callback destroys the stream. +{ + const { ep, done } = await makeServer( + mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('ok')); + this.writer.endSync(); + }), + ); + + const c = await connect(ep.address, { + servername: 'localhost', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + }); + await c.opened; + + const s = await c.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(async function() { + throw new Error('onheaders async error'); + }), + }); + + await rejects(s.closed, mustCall((err) => { + strictEqual(err.message, 'onheaders async error'); + return true; + })); + strictEqual(s.destroyed, true); + + c.close(); + await done.promise; + ep.close(); +} + +// Sync throw in ontrailers callback destroys the stream. +{ + const { ep, done } = await makeServer( + mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('body')); + this.writer.endSync(); + }), + { + onwanttrailers: mustCall(function() { + this.sendTrailers({ 'x-trailer': 'value' }); + }), + }, + ); + + const c = await connect(ep.address, { + servername: 'localhost', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + }); + await c.opened; + + const s = await c.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + ontrailers: mustCall(function() { + throw new Error('ontrailers sync error'); + }), + }); + + await rejects(s.closed, mustCall((err) => { + strictEqual(err.message, 'ontrailers sync error'); + return true; + })); + strictEqual(s.destroyed, true); + + c.close(); + await done.promise; + ep.close(); +} + +// Sync throw in onorigin callback destroys the session. +{ + const serverEndpoint = await listen(mustCall(async (ss) => { + await ss.closed; + }), { + sni: { + '*': { keys: [key], certs: [cert] }, + 'example.com': { keys: [key], certs: [cert] }, + }, + transportParams: { maxIdleTimeout: 1 }, + onheaders(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.endSync(); + }, + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'example.com', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + onorigin: mustCall(function() { + throw new Error('onorigin error'); + }), + onerror: mustCall(function(error) { + strictEqual(error.message, 'onorigin error'); + }), + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/', + ':scheme': 'https', + ':authority': 'example.com', + }, + }); + + // The session is destroyed by the callback error, which + // destroys the stream with the same error. + await rejects(stream.closed, mustCall((err) => { + strictEqual(err.message, 'onorigin error'); + return true; + })); + + await rejects(clientSession.closed, mustCall(() => true)); + + serverEndpoint.close(); +} + +// Sync throw in onwanttrailers callback destroys the +// server stream. The server stream's closed promise rejects with +// the thrown error. +{ + const serverStreamRejected = Promise.withResolvers(); + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + // The server stream rejects because onwanttrailers threw. + await rejects(stream.closed, mustCall((err) => { + strictEqual(err.message, 'onwanttrailers error'); + serverStreamRejected.resolve(); + return true; + })); + }); + await ss.closed; + serverDone.resolve(); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + transportParams: { maxIdleTimeout: 1 }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('body')); + this.writer.endSync(); + }), + onwanttrailers: mustCall(function() { + throw new Error('onwanttrailers error'); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + + // Verify the server stream was destroyed by the throw. + await serverStreamRejected.promise; + + // The client stream is still open (server error doesn't propagate + // to client automatically). Closing the client session destroys it. + clientSession.close(); + await Promise.all([stream.closed, serverDone.promise]); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-close-behavior.mjs b/test/parallel/test-quic-h3-close-behavior.mjs new file mode 100644 index 00000000000000..02b34945087267 --- /dev/null +++ b/test/parallel/test-quic-h3-close-behavior.mjs @@ -0,0 +1,95 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 close behavior. +// session.close() with open streams - streams complete cleanly +// Graceful H3 shutdown uses H3_NO_ERROR (0x100) + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const decoder = new TextDecoder(); + +// Two streams. The graceful close waits for both streams to complete, +// then sends CONNECTION_CLOSE with H3_NO_ERROR. +{ + let serverSession; + let requestCount = 0; + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + serverSession = ss; + ss.onstream = mustCall(2); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall((headers, stream) => { + stream.sendHeaders({ ':status': '200' }); + stream.writer.writeSync(headers[':path']); + stream.writer.endSync(); + + // Close after both responses are written. The + // close is deferred to exit the nghttp3 callback scope. + if (++requestCount === 2) { + setImmediate(mustCall(() => { + serverSession.close(); + serverDone.resolve(); + })); + } + }, 2), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + const stream1 = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/one', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall((headers) => { + strictEqual(headers[':status'], '200'); + }), + }); + + const stream2 = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/two', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall((headers) => { + strictEqual(headers[':status'], '200'); + }), + }); + + // Both streams should complete normally despite the close. + const bodies = await Promise.all([bytes(stream1), bytes(stream2)]); + strictEqual(decoder.decode(bodies[0]), '/one'); + strictEqual(decoder.decode(bodies[1]), '/two'); + + await Promise.all([stream1.closed, + stream2.closed, + serverDone.promise, + clientSession.closed]); + + serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-concurrent-requests.mjs b/test/parallel/test-quic-h3-concurrent-requests.mjs new file mode 100644 index 00000000000000..6f0aa50b7f02ae --- /dev/null +++ b/test/parallel/test-quic-h3-concurrent-requests.mjs @@ -0,0 +1,92 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: Multiple concurrent HTTP/3 requests on a single session. +// Client opens several bidi streams in parallel, each with different +// request paths. Server responds to each with a path-specific body. +// Verifies: +// - Multiple streams can be opened concurrently on one session +// - Each stream receives the correct response (no cross-talk) +// - All streams complete independently + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const decoder = new TextDecoder(); + +const REQUEST_COUNT = 5; +let serverStreamsCompleted = 0; +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall((stream) => { + stream.closed.then(mustCall(() => { + if (++serverStreamsCompleted === REQUEST_COUNT) { + serverSession.close(); + serverDone.resolve(); + } + })); + }, REQUEST_COUNT); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + const path = headers[':path']; + this.sendHeaders({ + ':status': '200', + 'content-type': 'text/plain', + }); + const w = this.writer; + w.writeSync(`response for ${path}`); + w.endSync(); + }, REQUEST_COUNT), +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', +}); +await clientSession.opened; + +// Open all requests concurrently. +const paths = Array.from({ length: REQUEST_COUNT }, (_, i) => `/path/${i}`); + +const requests = paths.map(mustCall(async (path) => { + const headersReceived = Promise.withResolvers(); + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': path, + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall((headers) => { + strictEqual(headers[':status'], '200'); + headersReceived.resolve(); + }), + }); + + await headersReceived.promise; + const body = await bytes(stream); + const text = decoder.decode(body); + strictEqual(text, `response for ${path}`); + await stream.closed; +}, REQUEST_COUNT)); + +await Promise.all([...requests, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-datagram.mjs b/test/parallel/test-quic-h3-datagram.mjs new file mode 100644 index 00000000000000..ea00cec42bc8f4 --- /dev/null +++ b/test/parallel/test-quic-h3-datagram.mjs @@ -0,0 +1,175 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 datagrams with SETTINGS_H3_DATAGRAM negotiation. +// Verifies that QUIC datagrams work correctly with H3 sessions, including +// the SETTINGS_H3_DATAGRAM negotiation required by RFC 9297. +// 1. Both sides enableDatagrams: true — datagrams work alongside H3 streams +// 2. Server enableDatagrams: false — client should not be able to send +// datagrams (peer's SETTINGS_H3_DATAGRAM=0) + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); +const { setTimeout: sleep } = await import('timers/promises'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const decoder = new TextDecoder(); + +// Test 1: H3 datagrams with enableDatagrams: true on both sides. +// Datagrams work alongside H3 request/response. +{ + const serverGotDatagram = Promise.withResolvers(); + const clientGotDatagram = Promise.withResolvers(); + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + }); + await serverGotDatagram.promise; + await sleep(50); + ss.close(); + serverDone.resolve(); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + application: { enableDatagrams: true }, + transportParams: { maxDatagramFrameSize: 100 }, + // Server echoes received datagram back to client. + ondatagram: mustCall(function(data) { + ok(data instanceof Uint8Array); + strictEqual(data.byteLength, 3); + strictEqual(data[0], 10); + strictEqual(data[1], 20); + strictEqual(data[2], 30); + // Echo it back. + this.sendDatagram(new Uint8Array([42, 43, 44])); + serverGotDatagram.resolve(); + }), + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync('ok'); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + application: { enableDatagrams: true }, + transportParams: { maxDatagramFrameSize: 100 }, + // Client receives datagram from server. + ondatagram: mustCall(function(data) { + ok(data instanceof Uint8Array); + strictEqual(data.byteLength, 3); + strictEqual(data[0], 42); + strictEqual(data[1], 43); + strictEqual(data[2], 44); + clientGotDatagram.resolve(); + }), + }); + await clientSession.opened; + + // Datagrams work alongside H3 request/response. + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/with-datagram', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + + // Send datagram from client. + await clientSession.sendDatagram(new Uint8Array([10, 20, 30])); + + // H3 response body is received. + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'ok'); + await stream.closed; + + // Both sides received their datagram. + await Promise.all([serverGotDatagram.promise, clientGotDatagram.promise]); + + await serverDone.promise; + await clientSession.close(); + await serverEndpoint.close(); +} + +// Test 2: Server has enableDatagrams: false. The peer's H3 SETTINGS +// should indicate SETTINGS_H3_DATAGRAM=0. The client's datagram send +// should return 0 (no datagram sent) because the peer doesn't support +// H3 datagrams. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + ss.close(); + serverDone.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + // Server explicitly disables H3 datagrams. + application: { enableDatagrams: false }, + // But transport-level datagrams ARE supported. + transportParams: { maxDatagramFrameSize: 100 }, + // Server should NOT receive any datagrams. + ondatagram: mustNotCall(), + onheaders: mustCall((headers, stream) => { + stream.sendHeaders({ ':status': '200' }); + stream.writer.writeSync('no-dgram'); + stream.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + application: { enableDatagrams: true }, + transportParams: { maxDatagramFrameSize: 100 }, + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/no-datagram', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall((headers) => { + strictEqual(headers[':status'], '200'); + }), + }); + + // The H3 request triggers SETTINGS exchange. After the server's + // SETTINGS (with h3_datagram=0) arrive, the client should know + // the peer doesn't support H3 datagrams. + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'no-dgram'); + + // Attempt to send a datagram. Since the peer's H3 SETTINGS + // indicate h3_datagram=0, this should return 0 (not sent). + const dgId = await clientSession.sendDatagram(new Uint8Array([1, 2, 3])); + strictEqual(dgId, 0n); + + await Promise.all([stream.closed, serverDone.promise]); + clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-error-codes.mjs b/test/parallel/test-quic-h3-error-codes.mjs new file mode 100644 index 00000000000000..f9aebadc85cfd2 --- /dev/null +++ b/test/parallel/test-quic-h3-error-codes.mjs @@ -0,0 +1,125 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 error code handling. +// H3 application error codes are propagated correctly +// Graceful close uses H3_NO_ERROR - streams complete cleanly + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual, rejects } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const decoder = new TextDecoder(); + +// Server closes with explicit application error code. +// Client's session closed rejects with the error. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + // Close with an explicit H3 application error code. + ss.close({ code: 0x101, type: 'application' }); + serverDone.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync('ok'); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'ok'); + await Promise.all([stream.closed, serverDone.promise]); + + // Client sees the application error code. + await rejects(clientSession.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + + await serverEndpoint.close(); +} + +// Graceful close with no explicit error code. +// Both streams complete normally. The close uses H3_NO_ERROR +// which is treated as a clean shutdown (not an error). +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + ss.close(); + serverDone.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync('ok'); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'ok'); + await stream.closed; + + // Graceful close - session close promise resolves + // because H3_NO_ERROR is a clean close. + await serverDone.promise; + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-goaway-non-h3.mjs b/test/parallel/test-quic-h3-goaway-non-h3.mjs new file mode 100644 index 00000000000000..e0dd89d20279a0 --- /dev/null +++ b/test/parallel/test-quic-h3-goaway-non-h3.mjs @@ -0,0 +1,66 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: Non-H3 session close does not fire ongoaway. +// GOAWAY is an HTTP/3 concept. When a non-H3 session closes, the +// ongoaway callback must not fire. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { setImmediate } from 'node:timers/promises'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + // Read client data, send response, close stream. + const data = await bytes(stream); + strictEqual(decoder.decode(data), 'ping'); + stream.writer.writeSync('pong'); + stream.writer.endSync(); + await stream.closed; + ss.close(); + serverDone.resolve(); + }); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: 'quic-test', +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + alpn: 'quic-test', + // Ongoaway must NOT fire for non-H3 sessions. + ongoaway: mustNotCall(), +}); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('ping'), +}); + +const response = await bytes(stream); +strictEqual(decoder.decode(response), 'pong'); +await Promise.all([stream.closed, serverDone.promise]); + +// Wait a tick for any deferred callbacks to fire. +await setImmediate(); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-goaway.mjs b/test/parallel/test-quic-h3-goaway.mjs new file mode 100644 index 00000000000000..7542849f35eeed --- /dev/null +++ b/test/parallel/test-quic-h3-goaway.mjs @@ -0,0 +1,150 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 GOAWAY handling. +// Graceful close sends GOAWAY - client receives ongoaway callback +// After GOAWAY, new stream creation fails +// Existing streams continue and complete after GOAWAY +// Opens two concurrent streams. Server responds to the first immediately +// and holds the second response. The server session.close() is called from +// the main test body (not a callback) after the client confirms both +// streams' headers were received. The second stream is still active, +// ensuring the GOAWAY is sent separately from CONNECTION_CLOSE. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; +import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { ok, strictEqual, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +// quic.session.goaway fires when the peer sends GOAWAY. +dc.subscribe('quic.session.goaway', mustCall((msg) => { + ok(msg.session, 'goaway should include session'); + strictEqual(typeof msg.lastStreamId, 'bigint', 'goaway should include lastStreamId'); +})); + +{ + let serverSession; + let pendingSecondStream; + const goawayReceived = Promise.withResolvers(); + const completeSecondResponse = Promise.withResolvers(); + const bothHeadersReceived = Promise.withResolvers(); + let clientHeaderCount = 0; + + const serverEndpoint = await listen(mustCall(async (ss) => { + serverSession = ss; + ss.onstream = mustCall(2); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + const path = headers[':path']; + this.sendHeaders({ ':status': '200' }); + + if (path === '/first') { + // Respond immediately to the first request. + this.writer.writeSync(encoder.encode('first')); + this.writer.endSync(); + } else if (path === '/second') { + // Hold the second response until signaled. + pendingSecondStream = this; + completeSecondResponse.promise.then(mustCall(() => { + pendingSecondStream.writer.writeSync(encoder.encode('second')); + pendingSecondStream.writer.endSync(); + })); + } + }, 2), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + // Ongoaway fires when the peer sends GOAWAY. + ongoaway: mustCall(function(lastStreamId) { + strictEqual(lastStreamId, -1n); + goawayReceived.resolve(); + }), + }); + await clientSession.opened; + + const onClientHeaders = mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + if (++clientHeaderCount === 2) { + bothHeadersReceived.resolve(); + } + }, 2); + + const stream1 = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/first', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: onClientHeaders, + }); + + const stream2 = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/second', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: onClientHeaders, + }); + + // First stream completes immediately. + const body1 = await bytes(stream1); + strictEqual(decoder.decode(body1), 'first'); + + // Wait for both streams' headers to arrive on the client, confirming + // the server has processed both requests. + await bothHeadersReceived.promise; + + // Close the server session from the main test body. The second + // stream's body is still pending, so the graceful close sends + // GOAWAY (shutdown notice) separately from CONNECTION_CLOSE. + serverSession.close(); + + // Wait for GOAWAY notification on the client. + await goawayReceived.promise; + + // After GOAWAY, new stream creation should fail. + await rejects( + clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/new', + ':scheme': 'https', + ':authority': 'localhost', + }, + }), + { code: 'ERR_INVALID_STATE' }, + ); + + // Signal the server to complete the second response. + completeSecondResponse.resolve(); + + // Second stream also completes despite GOAWAY. + const body2 = await bytes(stream2); + strictEqual(decoder.decode(body2), 'second'); + + // Both streams close cleanly. + await Promise.all([stream1.closed, stream2.closed]); + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-handshake-failure.mjs b/test/parallel/test-quic-h3-handshake-failure.mjs new file mode 100644 index 00000000000000..640f7e54c40209 --- /dev/null +++ b/test/parallel/test-quic-h3-handshake-failure.mjs @@ -0,0 +1,57 @@ +// Flags: --experimental-quic --no-warnings + +// Regression test: HTTP/3 server must not crash when a session is closed +// before the H3 application is fully started (control streams bound). +// Previously, closing such a session would call nghttp3_conn_shutdown on +// an H3 connection whose control streams were never bound, causing an +// assertion failure in nghttp3 (conn->tx.ctrl != NULL). +// +// The test creates an H3 server and a client that immediately closes the +// session before the handshake completes. The server creates the H3 +// application during ALPN negotiation, but Start() (which binds control +// streams) hasn't been called yet when the session is torn down. +// The server must handle this gracefully without crashing. + +import { hasQuic, skip, mustNotCall } from '../common/index.mjs'; +import { setTimeout } from 'node:timers/promises'; +import * as fixtures from '../common/fixtures.mjs'; + +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const serverEndpoint = await listen(async (serverSession) => { + await serverSession.closed; +}, { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustNotCall(), +}); + +// Connect then immediately close the session before the handshake completes. +// This exercises the H3 shutdown path on the server while the H3 application +// exists but hasn't started (control streams not yet bound). +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + // h3 ALPN — must match the server so the H3 application is selected + // on the server side before we tear it down. +}); + +// Close immediately — don't wait for handshake. +await clientSession.close(); + +// Give the server time to process the close and tear down the session. +await setTimeout(500); + +// The critical assertion: reaching this point without a crash means the +// server correctly handled the H3 shutdown before control streams were +// bound. Verify the endpoint is still alive by closing it gracefully. +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-header-validation.mjs b/test/parallel/test-quic-h3-header-validation.mjs new file mode 100644 index 00000000000000..43673e0cc00f1e --- /dev/null +++ b/test/parallel/test-quic-h3-header-validation.mjs @@ -0,0 +1,161 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 header validation (RFC 9114 §4.2-4.3). +// H3V-01: Header names are lowercased on send. +// H3V-02 through H3V-14 (receive-side validations) are handled +// automatically by nghttp3. The library rejects Transfer-Encoding, +// Connection headers, misplaced pseudo-headers, missing required +// pseudo-headers, uppercase header names from peer, etc. These +// validations are always enabled and cannot be disabled. They are +// verified by nghttp3's own test suite. +// This test verifies: +// - H3V-01: Mixed-case header names are lowercased when received +// - Headers with various valid pseudo-header combinations work +// - Custom headers are delivered correctly + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual, ok } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const decoder = new TextDecoder(); + +// H3V-01: Header names are lowercased on send. +// Send headers with mixed case — the server should receive them +// lowercased (buildNgHeaderString lowercases before passing to nghttp3). +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + ss.close(); + serverDone.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + // H3V-01: All header names should be lowercase regardless + // of how the client sent them. + for (const name of Object.keys(headers)) { + strictEqual(name, name.toLowerCase(), + `Header name "${name}" should be lowercase`); + } + + // Verify specific headers arrived lowercased. + strictEqual(headers[':method'], 'GET'); + strictEqual(headers[':path'], '/test'); + strictEqual(headers['x-custom-header'], 'Value1'); + strictEqual(headers['content-type'], 'text/plain'); + strictEqual(headers['x-mixed-case'], 'MixedValue'); + + // Verify values are NOT lowercased — only names are. + strictEqual(headers['x-custom-header'], 'Value1'); + + this.sendHeaders({ + // Response with mixed-case names — should be lowercased. + ':status': '200', + 'Content-Type': 'text/html', + 'X-Response-Header': 'ResponseValue', + }); + this.writer.writeSync('ok'); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + // Mixed-case names — should be lowercased by buildNgHeaderString. + ':method': 'GET', + ':path': '/test', + ':scheme': 'https', + ':authority': 'localhost', + 'X-Custom-Header': 'Value1', + 'Content-Type': 'text/plain', + 'X-Mixed-Case': 'MixedValue', + }, + onheaders: mustCall(function(headers) { + // Client should also receive lowercased response header names. + strictEqual(headers[':status'], '200'); + strictEqual(headers['content-type'], 'text/html'); + strictEqual(headers['x-response-header'], 'ResponseValue'); + + // Verify all names are lowercase. + for (const name of Object.keys(headers)) { + strictEqual(name, name.toLowerCase(), + `Response header name "${name}" should be lowercase`); + } + }), + }); + + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'ok'); + await Promise.all([stream.closed, serverDone.promise]); + await clientSession.close(); + await serverEndpoint.close(); +} + +// Verify multiple pseudo-header combinations work correctly. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + ss.close(); + serverDone.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + // All four required pseudo-headers present. + ok(headers[':method']); + ok(headers[':path']); + ok(headers[':scheme']); + ok(headers[':authority']); + + this.sendHeaders({ ':status': '204' }); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'POST', + ':path': '/api/data', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall((headers) => { + strictEqual(headers[':status'], '204'); + }), + }); + + await Promise.all([bytes(stream), stream.closed, serverDone.promise]); + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-headers-support.mjs b/test/parallel/test-quic-h3-headers-support.mjs new file mode 100644 index 00000000000000..8807adde6ef276 --- /dev/null +++ b/test/parallel/test-quic-h3-headers-support.mjs @@ -0,0 +1,78 @@ +// Flags: --experimental-quic --no-warnings + +// Test: Headers support detection for non-H3 sessions. +// headersSupported is UNSUPPORTED for non-H3 sessions +// Sending headers on non-H3 session throws ERR_INVALID_STATE +// Setting header callbacks on non-H3 stream throws ERR_INVALID_STATE + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { throws } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Sending headers on non-H3 stream throws. + throws(() => stream.sendHeaders({ ':status': '200' }), { code: 'ERR_INVALID_STATE' }); + + // Setting onheaders on non-H3 stream throws. + throws(() => stream.onheaders = () => {}, { code: 'ERR_INVALID_STATE' }); + + // Setting ontrailers on non-H3 stream throws. + throws(() => stream.ontrailers = () => {}, { code: 'ERR_INVALID_STATE' }); + + // Setting oninfo on non-H3 stream throws. + throws(() => stream.oninfo = () => {}, { code: 'ERR_INVALID_STATE' }); + + // Setting onwanttrailers on non-H3 stream throws. + throws(() => stream.onwanttrailers = () => {}, { code: 'ERR_INVALID_STATE' }); + + // sendInformationalHeaders throws on non-H3. + throws(() => stream.sendInformationalHeaders({ ':status': '103' }), { + code: 'ERR_INVALID_STATE', + }); + + // sendTrailers throws on non-H3. + throws(() => stream.sendTrailers({ 'x-trailer': 'value' }), { code: 'ERR_INVALID_STATE' }); + + stream.writer.endSync(); + + serverSession.close(); + serverDone.resolve(); + }); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: 'quic-test', +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + alpn: 'quic-test', +}); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('ping'), +}); + +// Client side — sending headers on non-H3 stream throws. +throws(() => stream.sendHeaders({ ':method': 'GET' }), { code: 'ERR_INVALID_STATE' }); + +await serverDone.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-informational-headers.mjs b/test/parallel/test-quic-h3-informational-headers.mjs new file mode 100644 index 00000000000000..6fa950b7bccbd6 --- /dev/null +++ b/test/parallel/test-quic-h3-informational-headers.mjs @@ -0,0 +1,117 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 informational (1xx) headers. +// Server sends a 103 Early Hints response before the final 200 response. +// Client receives the informational headers via oninfo, then the final +// response via onheaders. +// Verifies: +// - sendInformationalHeaders delivers 1xx headers to the client +// - oninfo callback fires with the informational headers +// - onheaders callback fires separately with the final response +// - Body data is delivered after the final response headers + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const decoder = new TextDecoder(); +const responseBody = 'final response'; + +// quic.stream.info fires when informational (1xx) headers are received. +dc.subscribe('quic.stream.info', mustCall((msg) => { + ok(msg.stream, 'stream.info should include stream'); + ok(msg.session, 'stream.info should include session'); + ok(msg.headers, 'stream.info should include headers'); + strictEqual(msg.headers[':status'], '103'); +})); + +// quic.stream.headers also fires for the final response headers. +dc.subscribe('quic.stream.headers', mustCall((msg) => { + ok(msg.stream, 'stream.headers should include stream'); + ok(msg.headers, 'stream.headers should include headers'); +}, 2)); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + // Send 103 Early Hints before the final response. + this.sendInformationalHeaders({ + ':status': '103', + 'link': '; rel=preload; as=style', + }); + + // Send final response headers + body. + this.sendHeaders({ + ':status': '200', + 'content-type': 'text/plain', + }); + + const w = this.writer; + w.writeSync(responseBody); + w.endSync(); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', +}); +await clientSession.opened; + +const clientInfoReceived = Promise.withResolvers(); +const clientHeadersReceived = Promise.withResolvers(); + +const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/page', + ':scheme': 'https', + ':authority': 'localhost', + }, + oninfo: mustCall(function(headers) { + strictEqual(headers[':status'], '103'); + strictEqual(headers.link, '; rel=preload; as=style'); + clientInfoReceived.resolve(); + }), + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + strictEqual(headers['content-type'], 'text/plain'); + clientHeadersReceived.resolve(); + }), +}); + +await Promise.all([clientInfoReceived.promise, clientHeadersReceived.promise]); + +// Read the response body. +const body = await bytes(stream); +strictEqual(decoder.decode(body), responseBody); + +// stream.headers should return the final (initial) headers, not 1xx. +strictEqual(stream.headers[':status'], '200'); + +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-origin.mjs b/test/parallel/test-quic-h3-origin.mjs new file mode 100644 index 00000000000000..4f1fdf50e58d6d --- /dev/null +++ b/test/parallel/test-quic-h3-origin.mjs @@ -0,0 +1,189 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 ORIGIN frames (RFC 9412). +// Server with SNI entries sends ORIGIN frame +// Wildcard (*) SNI entries excluded from ORIGIN +// Client receives ORIGIN frame via onorigin callback + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual, ok } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +// Server sends ORIGIN frame based on SNI entries. +// Wildcard entries are excluded. +{ + const originReceived = Promise.withResolvers(); + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + ss.close(); + serverDone.resolve(); + }); + }), { + sni: { + // Wildcard entry should NOT appear in ORIGIN frame. + '*': { keys: [key], certs: [cert] }, + // These specific hostnames should appear in ORIGIN. + 'example.com': { keys: [key], certs: [cert] }, + 'api.example.com': { keys: [key], certs: [cert] }, + }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('ok')); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'example.com', + verifyPeer: 'manual', + // Client receives ORIGIN frame via onorigin callback. + onorigin: mustCall(function(origins) { + ok(Array.isArray(origins)); + // The origins should include the specific SNI hostnames. + ok(origins.length >= 2); + // The wildcard (*) should NOT be in the list. + const originStrings = origins.join(','); + ok(originStrings.includes('example.com'), 'should include example.com'); + ok(originStrings.includes('api.example.com'), + 'should include api.example.com'); + ok(!originStrings.includes('*'), 'should not include wildcard'); + originReceived.resolve(); + }), + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/', + ':scheme': 'https', + ':authority': 'example.com', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'ok'); + + await Promise.all([originReceived.promise, stream.closed, serverDone.promise]); + await clientSession.close(); + await serverEndpoint.close(); +} + +// port: 8443 produces origin "https://hostname:8443" +// default port (443) omits port from origin string +// authoritative: false excluded from ORIGIN frame +// authoritative: true (default) included in ORIGIN frame +{ + const originReceived = Promise.withResolvers(); + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + ss.close(); + serverDone.resolve(); + }); + }), { + sni: { + '*': { keys: [key], certs: [cert] }, + // Non-default port → origin includes port. + 'custom-port.example.com': { keys: [key], certs: [cert], port: 8443 }, + // Default port (443) → origin omits port. + 'default-port.example.com': { keys: [key], certs: [cert], port: 443 }, + // authoritative: false → excluded from ORIGIN frame. + 'not-authoritative.example.com': { + keys: [key], certs: [cert], authoritative: false, + }, + // authoritative: true (explicit) → included. + 'authoritative.example.com': { + keys: [key], certs: [cert], authoritative: true, + }, + // Authoritative defaults to true when omitted. + 'default-auth.example.com': { keys: [key], certs: [cert] }, + }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('ok')); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'custom-port.example.com', + verifyPeer: 'manual', + onorigin: mustCall(function(origins) { + ok(Array.isArray(origins)); + + // Custom port included in origin string. + ok(origins.includes('https://custom-port.example.com:8443'), + 'should include origin with custom port'); + + // Default port 443 omitted from origin string. + ok(origins.includes('https://default-port.example.com'), + 'should include origin without port for 443'); + // Verify port 443 is NOT appended. + const defaultPortOrigin = origins.find((o) => + o.includes('default-port.example.com')); + ok(!defaultPortOrigin.includes(':443'), + 'default port 443 should be omitted'); + + // Non-authoritative entry excluded. + const allOrigins = origins.join(','); + ok(!allOrigins.includes('not-authoritative'), + 'non-authoritative entry should be excluded'); + + // Explicitly authoritative entry included. + ok(allOrigins.includes('authoritative.example.com'), + 'explicitly authoritative entry should be included'); + + // Default authoritative (true when omitted) included. + ok(allOrigins.includes('default-auth.example.com'), + 'default authoritative entry should be included'); + + originReceived.resolve(); + }), + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/', + ':scheme': 'https', + ':authority': 'custom-port.example.com', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'ok'); + + await Promise.all([originReceived.promise, stream.closed, serverDone.promise]); + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-pending-stream.mjs b/test/parallel/test-quic-h3-pending-stream.mjs new file mode 100644 index 00000000000000..fd269e64e0543a --- /dev/null +++ b/test/parallel/test-quic-h3-pending-stream.mjs @@ -0,0 +1,89 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: Pending H3 stream behavior. +// Priority set at creation time is applied to pending stream +// Headers enqueued at creation time are sent when stream opens + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual, deepStrictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +// The stream is initially pending (waiting for the QUIC handshake +// to open it). Priority and headers should be applied when it opens. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + ss.close(); + serverDone.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + // Headers were enqueued before the stream opened + // and should arrive correctly. + strictEqual(headers[':method'], 'GET'); + strictEqual(headers[':path'], '/pending'); + + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('ok')); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + + // Create the stream BEFORE awaiting opened. The stream is pending + // until the handshake completes and the QUIC stream can be opened. + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/pending', + ':scheme': 'https', + ':authority': 'localhost', + }, + // Priority set at creation time. + priority: 'high', + incremental: true, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + + // Priority should reflect what was set even while pending. + deepStrictEqual(stream.priority, { level: 'high', incremental: true }); + + // Now wait for the handshake. + await clientSession.opened; + + // Priority persists after stream opens. + deepStrictEqual(stream.priority, { level: 'high', incremental: true }); + + // Headers were sent and server responded. + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'ok'); + await Promise.all([stream.closed, serverDone.promise]); + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-post-filehandle.mjs b/test/parallel/test-quic-h3-post-filehandle.mjs new file mode 100644 index 00000000000000..46e30d8376d6cd --- /dev/null +++ b/test/parallel/test-quic-h3-post-filehandle.mjs @@ -0,0 +1,97 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 POST request with FileHandle body. +// Client sends a POST with an fd-backed body source. Server reads the body +// and echoes it back in the response. Verifies that the FdEntry async I/O +// path works correctly through the H3 application layer. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +import { writeFileSync } from 'node:fs'; +import { open } from 'node:fs/promises'; + +const tmpdir = await import('../common/tmpdir.js'); + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const decoder = new TextDecoder(); +const testContent = 'Hello from a file!\nLine two.\n'; + +tmpdir.refresh(); +const testFile = tmpdir.resolve('quic-h3-fh-test.txt'); +writeFileSync(testFile, testContent); + +// FileHandle as POST body in createBidirectionalStream. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const body = await bytes(stream); + strictEqual(decoder.decode(body), testContent); + + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':method'], 'POST'); + strictEqual(headers[':path'], '/upload'); + + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync('ok'); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + + const info = await clientSession.opened; + strictEqual(info.protocol, 'h3'); + + const clientHeadersReceived = Promise.withResolvers(); + + const fh = await open(testFile, 'r'); + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'POST', + ':path': '/upload', + ':scheme': 'https', + ':authority': 'localhost', + }, + body: fh, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + clientHeadersReceived.resolve(); + }), + }); + + await clientHeadersReceived.promise; + + const responseBody = await bytes(stream); + strictEqual(decoder.decode(responseBody), 'ok'); + + await Promise.all([stream.closed, serverDone.promise]); + clientSession.close(); + await clientSession.closed; + await serverEndpoint.close(); + // FileHandle is closed automatically when the stream finishes. +} diff --git a/test/parallel/test-quic-h3-post-request.mjs b/test/parallel/test-quic-h3-post-request.mjs new file mode 100644 index 00000000000000..a12458ef10df30 --- /dev/null +++ b/test/parallel/test-quic-h3-post-request.mjs @@ -0,0 +1,103 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 request with body data (POST-like). +// Client sends request pseudo-headers plus a body, server reads the body +// and echoes it back in the response. +// Verifies: +// - Client can send request headers + body via createBidirectionalStream +// - Server receives the request body via async iteration +// - Server response with echoed body is delivered to the client +// - The terminal flag is correctly NOT set when body is provided + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const requestBody = 'Hello from the client'; + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Read the full request body from the client. + const body = await bytes(stream); + const text = decoder.decode(body); + strictEqual(text, requestBody); + + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':method'], 'POST'); + strictEqual(headers[':path'], '/submit'); + + // Echo the request body back in the response. + // At this point, request body hasn't arrived yet — we use onstream + // to read it. But we can send response headers immediately. + this.sendHeaders({ + ':status': '200', + 'content-type': 'text/plain', + }); + // Write echoed body after reading it in onstream. For simplicity, + // we write a fixed response here and verify the request body + // separately in onstream. + const w = this.writer; + w.writeSync(encoder.encode('echo:' + requestBody)); + w.endSync(); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', +}); + +const info = await clientSession.opened; +strictEqual(info.protocol, 'h3'); + +const clientHeadersReceived = Promise.withResolvers(); + +// Send a POST request with body. When body is provided, terminal is NOT +// set on the HEADERS frame (body follows). +const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'POST', + ':path': '/submit', + ':scheme': 'https', + ':authority': 'localhost', + }, + body: encoder.encode(requestBody), + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + clientHeadersReceived.resolve(); + }), +}); + +await clientHeadersReceived.promise; + +// Read the response body. +const responseBody = await bytes(stream); +strictEqual(decoder.decode(responseBody), 'echo:' + requestBody); + +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-priority.mjs b/test/parallel/test-quic-h3-priority.mjs new file mode 100644 index 00000000000000..fc7ca231f0d63a --- /dev/null +++ b/test/parallel/test-quic-h3-priority.mjs @@ -0,0 +1,243 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 stream priority. +// Set priority at stream creation (priority/incremental options) +// setPriority({ level: 'high' }) on H3 stream +// setPriority({ incremental: true }) on H3 stream +// priority getter returns { level, incremental } on H3 +// priority getter on client H3 stream returns what was set +// Priority set at creation time reflects in stream.priority +// Server priority getter reflects peer's PRIORITY_UPDATE + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { deepStrictEqual, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +{ + let requestCount = 0; + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall((stream) => { + // Server sees priority on the stream. + const pri = stream.priority; + strictEqual(typeof pri, 'object'); + strictEqual(typeof pri.level, 'string'); + strictEqual(typeof pri.incremental, 'boolean'); + }, 4); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode(headers[':path'])); + this.writer.endSync(); + if (++requestCount === 4) { + serverDone.resolve(); + } + }, 4), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + // Priority set at creation time via options. + const stream1 = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/high', + ':scheme': 'https', + ':authority': 'localhost', + }, + priority: 'high', + incremental: false, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + + // Priority reflects what was set at creation. + deepStrictEqual(stream1.priority, { level: 'high', incremental: false }); + + // Priority 'low' + incremental at creation. + const stream2 = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/low-inc', + ':scheme': 'https', + ':authority': 'localhost', + }, + priority: 'low', + incremental: true, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + deepStrictEqual(stream2.priority, { level: 'low', incremental: true }); + + // Default priority at creation. + const stream3 = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/default', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + deepStrictEqual(stream3.priority, { level: 'default', incremental: false }); + + // setPriority after creation. + const stream4 = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/changed', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + // Default priority initially. + deepStrictEqual(stream4.priority, { level: 'default', incremental: false }); + + // Change to high. + stream4.setPriority({ level: 'high' }); + deepStrictEqual(stream4.priority, { level: 'high', incremental: false }); + + // Change to incremental. + stream4.setPriority({ level: 'low', incremental: true }); + deepStrictEqual(stream4.priority, { level: 'low', incremental: true }); + + // Back to default. + stream4.setPriority({ level: 'default', incremental: false }); + deepStrictEqual(stream4.priority, { level: 'default', incremental: false }); + + // Read all bodies. + const allBodies = await Promise.all([ + bytes(stream1), + bytes(stream2), + bytes(stream3), + bytes(stream4), + ]); + + strictEqual(decoder.decode(allBodies[0]), '/high'); + strictEqual(decoder.decode(allBodies[1]), '/low-inc'); + strictEqual(decoder.decode(allBodies[2]), '/default'); + strictEqual(decoder.decode(allBodies[3]), '/changed'); + + await Promise.all([stream1.closed, + stream2.closed, + stream3.closed, + stream4.closed, + serverDone.promise]); + await clientSession.close(); + await serverEndpoint.close(); +} + +// Server priority getter reflects peer's PRIORITY_UPDATE. +// The client creates a stream with default priority, changes it to +// 'high', then sends body data as a signal. The server reads priority +// after receiving the body — by then the PRIORITY_UPDATE frame (sent +// on the control stream) has been processed by nghttp3 internally. +{ + const serverSawHighPriority = Promise.withResolvers(); + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + // Read the request body — this acts as a signal that the + // client's PRIORITY_UPDATE has been sent. The control stream + // (carrying PRIORITY_UPDATE) is processed before bidi stream + // data in nghttp3, so by the time body arrives the priority + // has been updated. + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'signal'); + + // The server's priority getter should reflect the + // client's PRIORITY_UPDATE (high, incremental). + deepStrictEqual(stream.priority, { level: 'high', incremental: true }); + serverSawHighPriority.resolve(); + + await stream.closed; + ss.close(); + serverDone.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('ok')); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + // Create stream with default priority and a body. The body serves + // as a signal — by the time it arrives at the server, the + // PRIORITY_UPDATE (sent on the control stream) will have been + // processed. setPriority is called BEFORE createBidirectionalStream + // so the PRIORITY_UPDATE is queued before the stream data. + // + // Note: setPriority must be called after createBidirectionalStream + // because the stream handle is needed. But the PRIORITY_UPDATE + // travels on the control stream which nghttp3 processes before + // bidi stream data, so the ordering is guaranteed. + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'POST', + ':path': '/pri-update', + ':scheme': 'https', + ':authority': 'localhost', + }, + body: encoder.encode('signal'), + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + deepStrictEqual(stream.priority, { level: 'default', incremental: false }); + + // Change priority — this sends a PRIORITY_UPDATE frame on the + // control stream. The body data was already provided at creation + // but the PRIORITY_UPDATE travels on the control stream which + // nghttp3 prioritizes over bidi streams. + stream.setPriority({ level: 'high', incremental: true }); + deepStrictEqual(stream.priority, { level: 'high', incremental: true }); + + // Read the response. + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'ok'); + + // Wait for server to confirm it saw the updated priority. + await Promise.all([serverSawHighPriority.promise, + stream.closed, + serverDone.promise]); + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-qpack-settings.mjs b/test/parallel/test-quic-h3-qpack-settings.mjs new file mode 100644 index 00000000000000..6ca5671ef5b91f --- /dev/null +++ b/test/parallel/test-quic-h3-qpack-settings.mjs @@ -0,0 +1,123 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 QPACK settings. +// Default dynamic table capacity is 4096 (implicit — H3 works) +// Default blocked streams is 100 (implicit — H3 works) +// Custom qpackMaxDTableCapacity overrides default +// Verifies that H3 sessions work with both default and custom QPACK +// settings. The defaults (4096 capacity, 100 blocked streams) are +// tested implicitly by all H3 tests. This test explicitly verifies +// custom values are accepted and functional. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +async function makeRequest(clientSession, path) { + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': path, + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + const body = await bytes(stream); + strictEqual(decoder.decode(body), path); + await stream.closed; +} + +// Custom qpackMaxDTableCapacity = 0 (disables dynamic table). +// QPACK compression still works via the static table, but the dynamic +// table is not used. Verifies the option is passed through to nghttp3. +{ + const serverDone = Promise.withResolvers(); + let requestCount = 0; + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(2); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + // Server disables QPACK dynamic table. + application: { qpackMaxDTableCapacity: 0, qpackBlockedStreams: 0 }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode(headers[':path'])); + this.writer.endSync(); + if (++requestCount === 2) { + serverDone.resolve(); + } + }, 2), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + // Client also disables QPACK dynamic table. + application: { qpackMaxDTableCapacity: 0, qpackBlockedStreams: 0 }, + }); + await clientSession.opened; + + // Multiple requests to exercise header compression paths. + await makeRequest(clientSession, '/first'); + await makeRequest(clientSession, '/second'); + + await serverDone.promise; + await clientSession.close(); + await serverEndpoint.close(); +} + +// Custom qpackMaxDTableCapacity = 8192 (larger than default). +// Verifies large dynamic table capacity is accepted. +{ + const serverDone = Promise.withResolvers(); + let requestCount = 0; + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(2); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + application: { qpackMaxDTableCapacity: 8192, qpackBlockedStreams: 200 }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode(headers[':path'])); + this.writer.endSync(); + if (++requestCount === 2) { + serverDone.resolve(); + } + }, 2), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + application: { qpackMaxDTableCapacity: 8192, qpackBlockedStreams: 200 }, + }); + await clientSession.opened; + + await makeRequest(clientSession, '/alpha'); + await makeRequest(clientSession, '/beta'); + + await serverDone.promise; + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-request-response.mjs b/test/parallel/test-quic-h3-request-response.mjs new file mode 100644 index 00000000000000..1610f8deec1d41 --- /dev/null +++ b/test/parallel/test-quic-h3-request-response.mjs @@ -0,0 +1,116 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: basic HTTP/3 request/response. +// Client sends a GET request with H3 pseudo-headers, server receives +// the request and sends back a 200 response with a text body. +// Verifies: +// - Request pseudo-headers are delivered to the server via onheaders +// - stream.headers property returns the initial headers +// - Response headers and status are delivered to the client +// - Response body data is readable + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const responseBody = 'Hello from H3 server'; + +const serverDone = Promise.withResolvers(); + +// The onheaders callback signature is (headers, kind) with `this` bound +// to the stream. A regular function is used so `this` is accessible. +// safeCallbackInvoke(fn, owner, ...args) consumes the owner for error +// handling and forwards only ...args to fn. +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + // Default ALPN is h3 — omitted intentionally to exercise the default. + // + // onheaders is provided via listen options so it is applied to + // incoming streams (via kStreamCallbacks) BEFORE onstream fires. + // For H3, onheaders must be set because the H3 application delivers + // headers and stream[kHeaders] asserts the callback exists. + onheaders: mustCall(function(headers) { + // Verify request pseudo-headers. + strictEqual(headers[':method'], 'GET'); + strictEqual(headers[':path'], '/index.html'); + strictEqual(headers[':scheme'], 'https'); + strictEqual(headers[':authority'], 'localhost'); + + // After onheaders, stream.headers returns the initial headers. + // `this` is the stream (bound by the onheaders setter). + strictEqual(this.headers[':method'], 'GET'); + + // Send response headers (terminal: false is the default — body follows). + this.sendHeaders({ + ':status': '200', + 'content-type': 'text/plain', + }); + + // Write response body and close the write side. + const w = this.writer; + w.writeSync(encoder.encode(responseBody)); + w.endSync(); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + // Default ALPN is h3. +}); + +const info = await clientSession.opened; +strictEqual(info.protocol, 'h3'); + +const clientHeadersReceived = Promise.withResolvers(); + +// Send a GET request. With body omitted, the terminal flag is set +// automatically (END_STREAM on the HEADERS frame). +const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/index.html', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + strictEqual(headers['content-type'], 'text/plain'); + clientHeadersReceived.resolve(); + }), +}); + +await clientHeadersReceived.promise; + +// Read the full response body. +const body = await bytes(stream); +strictEqual(decoder.decode(body), responseBody); + +// stream.headers should return the buffered response headers. +strictEqual(stream.headers[':status'], '200'); + +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-settings.mjs b/test/parallel/test-quic-h3-settings.mjs new file mode 100644 index 00000000000000..d954813c9c2564 --- /dev/null +++ b/test/parallel/test-quic-h3-settings.mjs @@ -0,0 +1,200 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 settings enforcement. +// maxHeaderPairs enforcement - reject headers exceeding pair count +// maxHeaderLength enforcement - reject headers exceeding byte length +// enableConnectProtocol setting (accepted without error) +// enableDatagrams setting (accepted without error) + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +// maxHeaderPairs enforcement. +// Server limits to 5 header pairs. Client sends 4 pseudo-headers + +// 2 custom headers = 6 pairs. The 6th pair is silently dropped. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + ss.close(); + serverDone.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + // Allow 5 header pairs: 4 pseudo-headers + 1 custom. + application: { maxHeaderPairs: 5 }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':method'], 'GET'); + strictEqual(headers[':path'], '/limited'); + strictEqual(headers[':scheme'], 'https'); + strictEqual(headers[':authority'], 'localhost'); + // x-first is the 5th pair — accepted. + strictEqual(headers['x-first'], 'one'); + // x-second would be the 6th pair — dropped. + strictEqual(headers['x-second'], undefined); + + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('ok')); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/limited', + ':scheme': 'https', + ':authority': 'localhost', + 'x-first': 'one', + 'x-second': 'two', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'ok'); + await stream.closed; + await serverDone.promise; + await clientSession.close(); + await serverEndpoint.close(); +} + +// maxHeaderLength enforcement. +// Server limits total header byte length (name chars + value chars). +// The 4 pseudo-headers take ~45 bytes. A long custom header value +// pushes the total over the limit. +{ + const serverDone = Promise.withResolvers(); + const longValue = 'x'.repeat(200); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + ss.close(); + serverDone.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + // Limit total header bytes. The 4 pseudo-headers fit within 100 + // bytes, but adding x-long (6 + 200 = 206 bytes) exceeds it. + application: { maxHeaderLength: 100 }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':method'], 'GET'); + strictEqual(headers[':path'], '/length-limited'); + // x-long should be dropped — would push total over 100 bytes. + strictEqual(headers['x-long'], undefined); + + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('ok')); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/length-limited', + ':scheme': 'https', + ':authority': 'localhost', + 'x-long': longValue, + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'ok'); + await Promise.all([stream.closed, serverDone.promise]); + await clientSession.close(); + await serverEndpoint.close(); +} + +// enableConnectProtocol and enableDatagrams settings. +// Verify these options are accepted and H3 sessions work with them. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + ss.close(); + serverDone.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + application: { enableConnectProtocol: true, enableDatagrams: true }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('settings-ok')); + this.writer.endSync(); + }), + onapplication: mustCall((appopt) => { + strictEqual(appopt.enableDatagrams, true); + strictEqual(appopt.enableConnectProtocol, false); + // Must be false, as this is only sent from server side + }) + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + application: { enableConnectProtocol: true, enableDatagrams: true }, + }); + clientSession.onapplication = mustCall((appopt) => { + strictEqual(appopt.enableConnectProtocol, true); + strictEqual(appopt.enableDatagrams, true); + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/settings', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'settings-ok'); + await Promise.all([stream.closed, serverDone.promise]); + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-stream-destroy-with-headers.mjs b/test/parallel/test-quic-h3-stream-destroy-with-headers.mjs new file mode 100644 index 00000000000000..67286d9a5ad6a9 --- /dev/null +++ b/test/parallel/test-quic-h3-stream-destroy-with-headers.mjs @@ -0,0 +1,60 @@ +// Flags: --experimental-quic --no-warnings + +// Test: Stream with pending headers destroyed before send. +// Creating an H3 stream with headers and immediately destroying it +// should clean up without crashing or leaking. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (ss) => { + // The server may or may not see the stream depending on timing. + // Either way, it should not crash. + await ss.closed; + serverDone.resolve(); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', +}); +await clientSession.opened; + +// Create a stream with headers, then immediately destroy it. +const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/destroyed', + ':scheme': 'https', + ':authority': 'localhost', + }, +}); + +// Destroy the stream before headers can be sent/processed. +stream.destroy(); + +// Verify the stream is destroyed without crash. +strictEqual(stream.destroyed, true); + +// Close everything cleanly. +await clientSession.close(); +await serverDone.promise; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-stream-idle-timeout.mjs b/test/parallel/test-quic-h3-stream-idle-timeout.mjs new file mode 100644 index 00000000000000..5c851caca42465 --- /dev/null +++ b/test/parallel/test-quic-h3-stream-idle-timeout.mjs @@ -0,0 +1,185 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 stream idle timeout. +// Peer-initiated streams that receive no data within the configured +// timeout are automatically destroyed. This test verifies the behavior +// with HTTP/3 bidirectional streams, where ShutdownStream maps +// transport errors to the application's internal error code +// (NGHTTP3_H3_INTERNAL_ERROR = 0x102) on the wire. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; +import * as fixtures from '../common/fixtures.mjs'; +import { text } from 'node:stream/iter'; + +const { rejects, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const encoder = new TextEncoder(); + +// --- H3 stream destroyed after idle timeout --- +{ + const streamDestroyed = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Don't read — let the stream sit idle after the initial headers. + // The stream idle timeout should destroy it, rejecting stream.closed. + await rejects(stream.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); + streamDestroyed.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + streamIdleTimeout: 100, + onheaders() { + // Receive headers but do nothing — let the stream go idle. + }, + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + }); + + await clientSession.opened; + + // Send a POST request with a body byte so the server creates the + // stream, then stop sending (don't end the write side). + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'POST', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders() {}, + }); + const writer = stream.writer; + writer.writeSync(encoder.encode('x')); + + // Wait for the server to destroy the idle stream. + await streamDestroyed.promise; + + // The server sent STOP_SENDING / RESET_STREAM when it destroyed the + // idle stream. ShutdownStream maps the transport error to the H3 + // internal error code (0x102) on the wire. + await rejects(stream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + + await clientSession.close(); + await serverEndpoint.close(); +} + +// --- H3 stream with ongoing data is NOT destroyed --- +{ + const serverGotData = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await text(stream); + strictEqual(data, 'xy'); + serverGotData.resolve(); + await serverSession.close(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + streamIdleTimeout: 500, + onheaders: mustCall(function(headers) { + strictEqual(headers[':method'], 'POST'); + // Send response headers so the stream is fully established. + this.sendHeaders({ ':status': '200' }, { terminal: true }); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + onheaders() {}, + }); + stream.sendHeaders({ + ':method': 'POST', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + 'content-length': '2', + }, { terminal: false }); + const writer = stream.writer; + writer.writeSync(encoder.encode('x')); + + // Wait less than the 500ms idle timeout, then send more data. + await setTimeout(300); + + writer.writeSync(encoder.encode('y')); + writer.endSync(); + + await Promise.all([serverGotData.promise, clientSession.closed]); + await serverEndpoint.close(); +} + +// --- Disabled when set to 0 --- +{ + const streamSurvived = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await text(stream); + strictEqual(data, 'xy'); + streamSurvived.resolve(); + }); + + await setTimeout(700); + await serverSession.close(); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + streamIdleTimeout: 0, // Disabled + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }, { terminal: true }); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + onheaders() {}, + }); + stream.sendHeaders({ + ':method': 'POST', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + }, { terminal: false }); + const writer = stream.writer; + writer.writeSync(encoder.encode('x')); + + // Pause beyond any reasonable idle timeout to verify it's disabled. + await setTimeout(600); + + writer.writeSync(encoder.encode('y')); + writer.endSync(); + + await Promise.all([streamSurvived.promise, clientSession.closed]); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-trailing-headers.mjs b/test/parallel/test-quic-h3-trailing-headers.mjs new file mode 100644 index 00000000000000..436cb243b3dd99 --- /dev/null +++ b/test/parallel/test-quic-h3-trailing-headers.mjs @@ -0,0 +1,124 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 trailing headers. +// Server sends response headers, body data, then trailing headers. +// Client receives all three in order. +// Verifies: +// - onwanttrailers callback fires after body is sent +// - sendTrailers delivers trailing headers to the peer +// - ontrailers callback fires on the receiving side with kind 'trailing' +// - stream.headers still returns the initial headers (not trailers) + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; +import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const responseBody = 'body with trailers'; + +// quic.stream.headers fires when initial headers are received. +// Fires for both the server (request headers) and client (response headers). +dc.subscribe('quic.stream.headers', mustCall((msg) => { + ok(msg.stream, 'stream.headers should include stream'); + ok(msg.session, 'stream.headers should include session'); + ok(msg.headers, 'stream.headers should include headers'); +}, 2)); + +// quic.stream.trailers fires when trailing headers are received. +dc.subscribe('quic.stream.trailers', mustCall((msg) => { + ok(msg.stream, 'stream.trailers should include stream'); + ok(msg.session, 'stream.trailers should include session'); + ok(msg.trailers, 'stream.trailers should include trailers'); + strictEqual(msg.trailers['x-checksum'], 'abc123'); +})); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + // Send response headers. + this.sendHeaders({ + ':status': '200', + 'content-type': 'text/plain', + }); + + // Write body and close. + const w = this.writer; + w.writeSync(encoder.encode(responseBody)); + w.endSync(); + }), + // Fires after the body is fully sent (EOF + NO_END_STREAM). + // The server provides trailing headers here. + onwanttrailers: mustCall(function() { + this.sendTrailers({ + 'x-checksum': 'abc123', + 'x-request-id': '42', + }); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', +}); +await clientSession.opened; + +const clientHeadersReceived = Promise.withResolvers(); +const clientTrailersReceived = Promise.withResolvers(); + +const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/with-trailers', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + clientHeadersReceived.resolve(); + }), + ontrailers: mustCall(function(trailers) { + strictEqual(trailers['x-checksum'], 'abc123'); + strictEqual(trailers['x-request-id'], '42'); + clientTrailersReceived.resolve(); + }), +}); + +await clientHeadersReceived.promise; + +// Read the response body. +const body = await bytes(stream); +strictEqual(decoder.decode(body), responseBody); + +// Trailers arrive after the body. +await clientTrailersReceived.promise; + +// stream.headers should still be the initial headers, not trailers. +strictEqual(stream.headers[':status'], '200'); + +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-zero-rtt-bogus-ticket.mjs b/test/parallel/test-quic-h3-zero-rtt-bogus-ticket.mjs new file mode 100644 index 00000000000000..e724b6b04a485b --- /dev/null +++ b/test/parallel/test-quic-h3-zero-rtt-bogus-ticket.mjs @@ -0,0 +1,39 @@ +// Flags: --experimental-quic --no-warnings + +// Test: Bogus session ticket data is rejected gracefully. +// Providing random bytes as a session ticket throws ERR_INVALID_ARG_VALUE +// because the ticket format is validated before use. The connection +// cannot proceed with garbage ticket data. + +import { hasQuic, skip, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { rejects } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey, randomBytes } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const serverEndpoint = await listen(mustNotCall(), { + sni: { '*': { keys: [key], certs: [cert] } }, +}); + +// Bogus ticket data (random bytes) is rejected at the format level. +await rejects( + connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + sessionTicket: randomBytes(256), + }), + { code: 'ERR_INVALID_ARG_VALUE' }, +); + +serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-zero-rtt-rejected-settings.mjs b/test/parallel/test-quic-h3-zero-rtt-rejected-settings.mjs new file mode 100644 index 00000000000000..41f77a63a1f980 --- /dev/null +++ b/test/parallel/test-quic-h3-zero-rtt-rejected-settings.mjs @@ -0,0 +1,179 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: H3 0-RTT rejected when server reduces application settings. +// 0-RTT rejected when max_field_section_size decreased +// 0-RTT rejected when enable_connect_protocol disabled +// 0-RTT rejected when enable_datagrams disabled +// Each test creates two endpoints with the same key/cert/tokenSecret. +// The first endpoint issues a ticket with generous H3 settings. The +// second endpoint has reduced settings, causing the H3 session ticket +// app data validation to reject 0-RTT. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, strictEqual, rejects } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey, randomBytes } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const sni = { '*': { keys: [key], certs: [cert] } }; +const decoder = new TextDecoder(); + +// Helper: establish an H3 session, get a ticket, close. +async function getTicket(endpointOptions) { + let savedTicket; + let savedToken; + const gotTicket = Promise.withResolvers(); + const gotToken = Promise.withResolvers(); + + const ep = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + await stream.closed; + ss.close(); + }); + }), { + sni, + ...endpointOptions, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync('ok'); + this.writer.endSync(); + }), + }); + + const cs = await connect(ep.address, { + servername: 'localhost', + verifyPeer: 'manual', + ...endpointOptions, + onsessionticket(ticket) { + ok(Buffer.isBuffer(ticket)); + savedTicket = ticket; + gotTicket.resolve(); + }, + onnewtoken(token) { + ok(Buffer.isBuffer(token)); + savedToken = token; + gotToken.resolve(); + }, + }); + await cs.opened; + await Promise.all([gotTicket.promise, gotToken.promise]); + + const s = await cs.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/ticket', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), + }); + const body = await bytes(s); + strictEqual(decoder.decode(body), 'ok'); + await Promise.all([s.closed, cs.closed]); + await ep.close(); + + return { ticket: savedTicket, token: savedToken }; +} + +// Helper: attempt 0-RTT with reduced settings, expect rejection. +// When 0-RTT is rejected, the H3 application is torn down and +// recreated (EarlyDataRejected destroys the nghttp3 connection). +// The initial 0-RTT stream may not survive this transition, so we +// only verify earlyDataAccepted is false and close cleanly. +async function attemptRejected0RTT(endpointOptions, ticket, token) { + const ep = await listen(mustCall(async (ss) => { + await ss.closed; + }), { + sni, + ...endpointOptions, + }); + + const cs = await connect(ep.address, { + servername: 'localhost', + verifyPeer: 'manual', + ...endpointOptions, + sessionTicket: ticket, + token, + }); + + // Trigger the deferred handshake by opening a stream. + // With 0-RTT, the handshake is deferred until the first stream + // or datagram is sent. When 0-RTT is rejected, the stream is + // destroyed by EarlyDataRejected — its closed promise rejects + // with an application error. + const s = await cs.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/rejected', + ':scheme': 'https', + ':authority': 'localhost', + }, + }); + await rejects(s.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + + const info = await cs.opened; + strictEqual(info.earlyDataAttempted, true); + strictEqual(info.earlyDataAccepted, false); + + cs.close(); + ep.close(); +} + +const tokenSecret = randomBytes(16); + +// enable_connect_protocol disabled. +{ + const { ticket, token } = await getTicket({ + endpoint: { tokenSecret }, + application: { enableConnectProtocol: true }, + }); + + await attemptRejected0RTT({ + endpoint: { tokenSecret }, + // EnableConnectProtocol reduced from true to false. + application: { enableConnectProtocol: false }, + }, ticket, token); +} + +// enable_datagrams disabled. +{ + const { ticket, token } = await getTicket({ + endpoint: { tokenSecret }, + application: { enableDatagrams: true }, + }); + + await attemptRejected0RTT({ + endpoint: { tokenSecret }, + // EnableDatagrams reduced from true to false. + application: { enableDatagrams: false }, + }, ticket, token); +} + +// max_field_section_size decreased. +{ + const { ticket, token } = await getTicket({ + endpoint: { tokenSecret }, + application: { maxFieldSectionSize: 10000 }, + }); + + await attemptRejected0RTT({ + endpoint: { tokenSecret }, + // MaxFieldSectionSize reduced from 10000 to 100. + application: { maxFieldSectionSize: 100 }, + }, ticket, token); +} diff --git a/test/parallel/test-quic-h3-zero-rtt.mjs b/test/parallel/test-quic-h3-zero-rtt.mjs new file mode 100644 index 00000000000000..4e51958d7c864a --- /dev/null +++ b/test/parallel/test-quic-h3-zero-rtt.mjs @@ -0,0 +1,133 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 0-RTT session resumption with session ticket app data. +// Session ticket includes HTTP/3 settings +// H3 + 0-RTT: Client sends H3 request in 0-RTT flight +// Uses a single server endpoint for both connections so the TLS +// session ticket encryption key is shared. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { bytes } = await import('stream/iter'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +let savedTicket; +let savedToken; +const gotTicket = Promise.withResolvers(); +const gotToken = Promise.withResolvers(); + +let serverSessionCount = 0; +const secondDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((ss) => { + const num = ++serverSessionCount; + ss.onstream = mustCall(async (stream) => { + if (num === 2) { + // Resolve with the stream so we can check stream.early after + // data has been received (the early flag is set after + // nghttp3 processes the 0-RTT headers, not at stream creation). + secondDone.resolve(stream); + } + await stream.closed; + ss.close(); + }); +}, 2), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode(headers[':path'])); + this.writer.endSync(); + }, 2), +}); + +// --- First connection: establish H3 session, receive ticket --- +const cs1 = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + onsessionticket: mustCall(function(ticket) { + ok(Buffer.isBuffer(ticket)); + ok(ticket.length > 0); + savedTicket = ticket; + gotTicket.resolve(); + }, 2), + onnewtoken: mustCall(function(token) { + ok(Buffer.isBuffer(token)); + savedToken = token; + gotToken.resolve(); + }), +}); + +const info1 = await cs1.opened; +strictEqual(info1.earlyDataAttempted, false); +strictEqual(info1.earlyDataAccepted, false); + +await Promise.all([gotTicket.promise, gotToken.promise]); + +const s1 = await cs1.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/first', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), +}); +const body1 = await bytes(s1); +strictEqual(decoder.decode(body1), '/first'); +await Promise.all([s1.closed, cs1.closed]); + +// Session ticket should have been received. +ok(savedTicket); +ok(savedToken); + +// --- Second connection: 0-RTT with H3 --- +const cs2 = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + sessionTicket: savedTicket, + token: savedToken, +}); + +// Send H3 request BEFORE handshake completes — true 0-RTT. +const s2 = await cs2.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/early', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders: mustCall(function(headers) { + strictEqual(headers[':status'], '200'); + }), +}); + +const info2 = await cs2.opened; +strictEqual(info2.earlyDataAttempted, true); +strictEqual(info2.earlyDataAccepted, true); + +const body2 = await bytes(s2); +strictEqual(decoder.decode(body2), '/early'); +await s2.closed; + +const earlyStream = await secondDone.promise; +strictEqual(earlyStream.early, true); + +await cs2.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-handshake-ipv6-only.mjs b/test/parallel/test-quic-handshake-ipv6-only.mjs new file mode 100644 index 00000000000000..30543ac9310173 --- /dev/null +++ b/test/parallel/test-quic-handshake-ipv6-only.mjs @@ -0,0 +1,74 @@ +// Flags: --experimental-quic --no-warnings + +import { hasQuic, hasIPv6, mustCall, skip } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +if (!hasIPv6) { + skip('IPv6 is not supported'); +} + +// Import after the hasQuic check +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); +const cert = fixtures.readKey('agent1-cert.pem'); + +const check = { + // The SNI value + servername: 'localhost', + // The selected ALPN protocol + protocol: 'h3', + // The negotiated cipher suite + cipher: 'TLS_AES_128_GCM_SHA256', + cipherVersion: 'TLSv1.3', +}; + +// The opened promise should resolve when the handshake is complete. + +const serverOpened = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + const info = await serverSession.opened; + assert.partialDeepStrictEqual(info, check); + serverOpened.resolve(); + await serverSession.close(); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + endpoint: { + address: { + address: '::1', + family: 'ipv6', + }, + ipv6Only: true, + }, +}); +// Buffer is not detached. +assert.strictEqual(cert.buffer.detached, false); + +// The server must have an address to connect to after listen resolves. +assert.ok(serverEndpoint.address !== undefined); + +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + endpoint: { + address: { + address: '::', + family: 'ipv6', + }, + } +}); + +const info = await clientSession.opened; +assert.partialDeepStrictEqual(info, check); + +await serverOpened.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-handshake-timeout.mjs b/test/parallel/test-quic-handshake-timeout.mjs new file mode 100644 index 00000000000000..51798950cdd50e --- /dev/null +++ b/test/parallel/test-quic-handshake-timeout.mjs @@ -0,0 +1,33 @@ +// Flags: --experimental-quic --no-warnings + +// Test: handshake timeout. +// The server accepts sessions but the client uses a very short idle +// timeout, causing the session to close before the handshake can +// complete on the server side. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { transportParams: { maxIdleTimeout: 1 } }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, +}); + +// Don't send any data. Just wait for idle timeout. +await Promise.all([clientSession.opened, clientSession.closed]); + +// The session closed via idle timeout. Verify it was destroyed. +strictEqual(clientSession.destroyed, true); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-handshake.mjs b/test/parallel/test-quic-handshake.mjs index 7374d4c929398e..1a8a90d1f32516 100644 --- a/test/parallel/test-quic-handshake.mjs +++ b/test/parallel/test-quic-handshake.mjs @@ -3,6 +3,9 @@ import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { partialDeepStrictEqual, strictEqual, ok } = assert; if (!hasQuic) { skip('QUIC is not enabled'); @@ -12,8 +15,8 @@ if (!hasQuic) { const { listen, connect } = await import('node:quic'); const { createPrivateKey } = await import('node:crypto'); -const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); -const cert = fixtures.readKey('agent1-cert.pem'); +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); const check = { // The SNI value @@ -23,16 +26,18 @@ const check = { // The negotiated cipher suite cipher: 'TLS_AES_128_GCM_SHA256', cipherVersion: 'TLSv1.3', + // No session ticket provided, so early data was not attempted + earlyDataAttempted: false, + earlyDataAccepted: false, }; // The opened promise should resolve when the handshake is complete. const serverOpened = Promise.withResolvers(); -const clientOpened = Promise.withResolvers(); const serverEndpoint = await listen(mustCall((serverSession) => { serverSession.opened.then((info) => { - assert.partialDeepStrictEqual(info, check); + partialDeepStrictEqual(info, check); serverOpened.resolve(); serverSession.close(); }).then(mustCall()); @@ -42,18 +47,19 @@ const serverEndpoint = await listen(mustCall((serverSession) => { }); // Buffer is not detached. -assert.strictEqual(cert.buffer.detached, false); +strictEqual(cert.buffer.detached, false); // The server must have an address to connect to after listen resolves. -assert.ok(serverEndpoint.address !== undefined); +ok(serverEndpoint.address !== undefined); const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', }); -clientSession.opened.then((info) => { - assert.partialDeepStrictEqual(info, check); - clientOpened.resolve(); -}).then(mustCall()); -await Promise.all([serverOpened.promise, clientOpened.promise]); -clientSession.close(); +const info = await clientSession.opened; +partialDeepStrictEqual(info, check); + +await serverOpened.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-internal-endpoint-listen-defaults.mjs b/test/parallel/test-quic-internal-endpoint-listen-defaults.mjs index f6215106ca3d9e..7dda0a6f28d865 100644 --- a/test/parallel/test-quic-internal-endpoint-listen-defaults.mjs +++ b/test/parallel/test-quic-internal-endpoint-listen-defaults.mjs @@ -1,10 +1,13 @@ // Flags: --expose-internals --experimental-quic --no-warnings -import { hasQuic, skip } from '../common/index.mjs'; +import { hasQuic, skip, mustNotCall } from '../common/index.mjs'; import assert from 'node:assert'; import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; import { SocketAddress } from 'node:net'; +const { strictEqual, rejects, ok, throws } = assert; + if (!hasQuic) { skip('QUIC is not enabled'); } @@ -12,68 +15,67 @@ if (!hasQuic) { // Import after the hasQuic check const { listen, QuicEndpoint } = await import('node:quic'); const { createPrivateKey } = await import('node:crypto'); -const { kState } = (await import('internal/quic/symbols')).default; +const { getQuicEndpointState } = (await import('internal/quic/quic')).default; -const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); -const cert = fixtures.readKey('agent1-cert.pem'); +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); const sni = { '*': { keys: [key], certs: [cert] } }; const endpoint = new QuicEndpoint(); +const state = getQuicEndpointState(endpoint); +ok(!state.isBound); +ok(!state.isReceiving); +ok(!state.isListening); -assert.ok(!endpoint[kState].isBound); -assert.ok(!endpoint[kState].isReceiving); -assert.ok(!endpoint[kState].isListening); - -assert.strictEqual(endpoint.address, undefined); +strictEqual(endpoint.address, undefined); -await assert.rejects(listen(123, { sni, endpoint }), { +await rejects(listen(123, { sni, endpoint }), { code: 'ERR_INVALID_ARG_TYPE', }); // Buffer is not detached. -assert.strictEqual(cert.buffer.detached, false); +strictEqual(cert.buffer.detached, false); -await assert.rejects(listen(() => {}, 123), { +await rejects(listen(mustNotCall(), 123), { code: 'ERR_INVALID_ARG_TYPE', }); -await listen(() => {}, { sni, endpoint }); +await listen(mustNotCall(), { sni, endpoint }); // Buffer is not detached. -assert.strictEqual(cert.buffer.detached, false); +strictEqual(cert.buffer.detached, false); -await assert.rejects(listen(() => {}, { sni, endpoint }), { +await rejects(listen(mustNotCall(), { sni, endpoint }), { code: 'ERR_INVALID_STATE', }); - // Buffer is not detached. -assert.strictEqual(cert.buffer.detached, false); +strictEqual(cert.buffer.detached, false); -assert.ok(endpoint[kState].isBound); -assert.ok(endpoint[kState].isReceiving); -assert.ok(endpoint[kState].isListening); +ok(state.isBound); +ok(state.isReceiving); +ok(state.isListening); const address = endpoint.address; -assert.ok(address instanceof SocketAddress); +ok(address instanceof SocketAddress); -assert.strictEqual(address.address, '127.0.0.1'); -assert.strictEqual(address.family, 'ipv4'); -assert.strictEqual(address.flowlabel, 0); -assert.ok(address.port !== 0); +strictEqual(address.address, '127.0.0.1'); +strictEqual(address.family, 'ipv4'); +strictEqual(address.flowlabel, 0); +ok(address.port !== 0); -assert.ok(!endpoint.destroyed); +ok(!endpoint.destroyed); endpoint.destroy(); -assert.strictEqual(endpoint.closed, endpoint.close()); +strictEqual(endpoint.closed, endpoint.close()); await endpoint.closed; -assert.ok(endpoint.destroyed); +ok(endpoint.destroyed); -await assert.rejects(listen(() => {}, { sni, endpoint }), { +await rejects(listen(mustNotCall(), { sni, endpoint }), { code: 'ERR_INVALID_STATE', }); // Buffer is not detached. -assert.strictEqual(cert.buffer.detached, false); +strictEqual(cert.buffer.detached, false); -assert.throws(() => { endpoint.busy = true; }, { +throws(() => { endpoint.busy = true; }, { code: 'ERR_INVALID_STATE', }); await endpoint[Symbol.asyncDispose](); -assert.strictEqual(endpoint.address, undefined); +strictEqual(endpoint.address, undefined); diff --git a/test/parallel/test-quic-internal-endpoint-options.mjs b/test/parallel/test-quic-internal-endpoint-options.mjs index aa4d4c96e87e5d..1d922b1ce77136 100644 --- a/test/parallel/test-quic-internal-endpoint-options.mjs +++ b/test/parallel/test-quic-internal-endpoint-options.mjs @@ -3,16 +3,19 @@ import { hasQuic, skip } from '../common/index.mjs'; import assert from 'node:assert'; import { inspect } from 'node:util'; +const { strictEqual, throws } = assert; + if (!hasQuic) { skip('QUIC is not enabled'); } // Import after the hasQuic check const { QuicEndpoint } = await import('node:quic'); +const { BlockList } = await import('node:net'); // Reject invalid options ['a', null, false, NaN].forEach((i) => { - assert.throws(() => new QuicEndpoint(i), { + throws(() => new QuicEndpoint(i), { code: 'ERR_INVALID_ARG_TYPE', }); }); @@ -39,37 +42,83 @@ const cases = [ { key: 'maxConnectionsPerHost', valid: [ - 1, 10, 100, 1000, 10000, 10000n, + 0, 1, 10, 100, 1000, 10000, 65535, ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + invalid: [-1, 65536, 1.5, 'a', null, false, true, {}, [], () => {}] }, { key: 'maxConnectionsTotal', valid: [ - 1, 10, 100, 1000, 10000, 10000n, + 0, 1, 10, 100, 1000, 10000, 65535, ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + invalid: [-1, 65536, 1.5, 'a', null, false, true, {}, [], () => {}] }, { - key: 'maxStatelessResetsPerHost', + key: 'addressLRUSize', valid: [ 1, 10, 100, 1000, 10000, 10000n, ], invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] }, { - key: 'addressLRUSize', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + key: 'retryRate', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] }, { - key: 'maxRetries', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + key: 'retryBurst', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'statelessResetRate', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'statelessResetBurst', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'versionNegotiationRate', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'versionNegotiationBurst', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'immediateCloseRate', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'immediateCloseBurst', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'sessionCreationRate', + valid: [0, 1, 10, 100.5, 1000, Infinity], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'sessionCreationBurst', + valid: [0, 1, 10, 100.5, 1000, Infinity], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'blockList', + valid: [new BlockList()], + invalid: ['a', 0, null, false, true, {}, [], () => {}] + }, + { + key: 'blockListPolicy', + valid: ['deny', 'allow'], + invalid: ['invalid', 0, null, false, true, {}, [], () => {}] }, { key: 'validateAddress', @@ -147,15 +196,15 @@ for (const { key, valid, invalid } of cases) { for (const value of invalid) { const options = {}; options[key] = value; - assert.throws(() => new QuicEndpoint(options), { - message: new RegExp(`${key}`), + throws(() => new QuicEndpoint(options), { + message: new RegExp(`${RegExp.escape(key)}`), }, value); } } // It can be inspected const endpoint = new QuicEndpoint({}); -assert.strictEqual(typeof inspect(endpoint), 'string'); +strictEqual(typeof inspect(endpoint), 'string'); endpoint.close(); await endpoint.closed; @@ -166,6 +215,6 @@ new QuicEndpoint({ new QuicEndpoint({ address: '127.0.0.1:0', }); -assert.throws(() => new QuicEndpoint({ address: 123 }), { +throws(() => new QuicEndpoint({ address: 123 }), { code: 'ERR_INVALID_ARG_TYPE', }); diff --git a/test/parallel/test-quic-internal-endpoint-stats-state.mjs b/test/parallel/test-quic-internal-endpoint-stats-state.mjs index faa1b58d06c04d..2af27724eb4e18 100644 --- a/test/parallel/test-quic-internal-endpoint-stats-state.mjs +++ b/test/parallel/test-quic-internal-endpoint-stats-state.mjs @@ -3,6 +3,8 @@ import { hasQuic, skip } from '../common/index.mjs'; import { inspect } from 'node:util'; import assert from 'node:assert'; +const { strictEqual, deepStrictEqual, throws } = assert; + if (!hasQuic) { skip('QUIC is not enabled'); } @@ -19,47 +21,54 @@ const { const { kFinishClose, kPrivateConstructor, - kState, } = (await import('internal/quic/symbols')).default; +const { + getQuicEndpointState, +} = (await import('internal/quic/quic')).default; { const endpoint = new QuicEndpoint(); + const state = getQuicEndpointState(endpoint); - assert.strictEqual(endpoint[kState].isBound, false); - assert.strictEqual(endpoint[kState].isReceiving, false); - assert.strictEqual(endpoint[kState].isListening, false); - assert.strictEqual(endpoint[kState].isClosing, false); - assert.strictEqual(endpoint[kState].isBusy, false); - assert.strictEqual(endpoint[kState].pendingCallbacks, 0n); + strictEqual(state.isBound, false); + strictEqual(state.isReceiving, false); + strictEqual(state.isListening, false); + strictEqual(state.isClosing, false); + strictEqual(state.isBusy, false); + strictEqual(state.pendingCallbacks, 0n); - assert.deepStrictEqual(JSON.parse(JSON.stringify(endpoint[kState])), { + deepStrictEqual(JSON.parse(JSON.stringify(state)), { isBound: false, isReceiving: false, isListening: false, isClosing: false, isBusy: false, - pendingCallbacks: '0', + maxConnectionsPerHost: 100, + maxConnectionsTotal: 10_000, + pendingCallbacks: 0, }); endpoint.busy = true; - assert.strictEqual(endpoint[kState].isBusy, true); + strictEqual(state.isBusy, true); endpoint.busy = false; - assert.strictEqual(endpoint[kState].isBusy, false); - assert.strictEqual(typeof inspect(endpoint[kState]), 'string'); + strictEqual(state.isBusy, false); + strictEqual(typeof inspect(state), 'string'); } { // It is not bound after close. const endpoint = new QuicEndpoint(); - endpoint[kState][kFinishClose](); - assert.strictEqual(endpoint[kState].isBound, undefined); + const state = getQuicEndpointState(endpoint); + state[kFinishClose](); + strictEqual(state.isBound, undefined); } { // State constructor argument is ArrayBuffer const endpoint = new QuicEndpoint(); - const StateCons = endpoint[kState].constructor; - assert.throws(() => new StateCons(kPrivateConstructor, 1), { + const state = getQuicEndpointState(endpoint); + const StateCons = state.constructor; + throws(() => new StateCons(kPrivateConstructor, 1), { code: 'ERR_INVALID_ARG_TYPE' }); } @@ -68,22 +77,28 @@ const { // Endpoint stats are readable and have expected properties const endpoint = new QuicEndpoint(); - assert.strictEqual(typeof endpoint.stats.isConnected, 'boolean'); - assert.strictEqual(typeof endpoint.stats.createdAt, 'bigint'); - assert.strictEqual(typeof endpoint.stats.destroyedAt, 'bigint'); - assert.strictEqual(typeof endpoint.stats.bytesReceived, 'bigint'); - assert.strictEqual(typeof endpoint.stats.bytesSent, 'bigint'); - assert.strictEqual(typeof endpoint.stats.packetsReceived, 'bigint'); - assert.strictEqual(typeof endpoint.stats.packetsSent, 'bigint'); - assert.strictEqual(typeof endpoint.stats.serverSessions, 'bigint'); - assert.strictEqual(typeof endpoint.stats.clientSessions, 'bigint'); - assert.strictEqual(typeof endpoint.stats.serverBusyCount, 'bigint'); - assert.strictEqual(typeof endpoint.stats.retryCount, 'bigint'); - assert.strictEqual(typeof endpoint.stats.versionNegotiationCount, 'bigint'); - assert.strictEqual(typeof endpoint.stats.statelessResetCount, 'bigint'); - assert.strictEqual(typeof endpoint.stats.immediateCloseCount, 'bigint'); - - assert.deepStrictEqual(Object.keys(endpoint.stats.toJSON()), [ + strictEqual(typeof endpoint.stats.isConnected, 'boolean'); + strictEqual(typeof endpoint.stats.createdAt, 'bigint'); + strictEqual(typeof endpoint.stats.destroyedAt, 'bigint'); + strictEqual(typeof endpoint.stats.bytesReceived, 'bigint'); + strictEqual(typeof endpoint.stats.bytesSent, 'bigint'); + strictEqual(typeof endpoint.stats.packetsReceived, 'bigint'); + strictEqual(typeof endpoint.stats.packetsSent, 'bigint'); + strictEqual(typeof endpoint.stats.serverSessions, 'bigint'); + strictEqual(typeof endpoint.stats.clientSessions, 'bigint'); + strictEqual(typeof endpoint.stats.serverBusyCount, 'bigint'); + strictEqual(typeof endpoint.stats.retryCount, 'bigint'); + strictEqual(typeof endpoint.stats.retryRateLimited, 'bigint'); + strictEqual(typeof endpoint.stats.versionNegotiationCount, 'bigint'); + strictEqual(typeof endpoint.stats.versionNegotiationRateLimited, 'bigint'); + strictEqual(typeof endpoint.stats.statelessResetCount, 'bigint'); + strictEqual(typeof endpoint.stats.statelessResetRateLimited, 'bigint'); + strictEqual(typeof endpoint.stats.immediateCloseCount, 'bigint'); + strictEqual(typeof endpoint.stats.immediateCloseRateLimited, 'bigint'); + strictEqual(typeof endpoint.stats.sessionCreationRateLimited, 'bigint'); + strictEqual(typeof endpoint.stats.packetsBlocked, 'bigint'); + + deepStrictEqual(Object.keys(endpoint.stats.toJSON()), [ 'connected', 'createdAt', 'destroyedAt', @@ -95,28 +110,34 @@ const { 'clientSessions', 'serverBusyCount', 'retryCount', + 'retryRateLimited', 'versionNegotiationCount', + 'versionNegotiationRateLimited', 'statelessResetCount', + 'statelessResetRateLimited', 'immediateCloseCount', + 'immediateCloseRateLimited', + 'sessionCreationRateLimited', + 'packetsBlocked', ]); - assert.strictEqual(typeof inspect(endpoint.stats), 'string'); + strictEqual(typeof inspect(endpoint.stats), 'string'); } { // Stats are still readable after close const endpoint = new QuicEndpoint(); - assert.strictEqual(typeof endpoint.stats.toJSON(), 'object'); + strictEqual(typeof endpoint.stats.toJSON(), 'object'); endpoint.stats[kFinishClose](); - assert.strictEqual(endpoint.stats.isConnected, false); - assert.strictEqual(typeof endpoint.stats.destroyedAt, 'bigint'); - assert.strictEqual(typeof endpoint.stats.toJSON(), 'object'); + strictEqual(endpoint.stats.isConnected, false); + strictEqual(typeof endpoint.stats.destroyedAt, 'bigint'); + strictEqual(typeof endpoint.stats.toJSON(), 'object'); } { // Stats constructor argument is ArrayBuffer const endpoint = new QuicEndpoint(); const StatsCons = endpoint.stats.constructor; - assert.throws(() => new StatsCons(kPrivateConstructor, 1), { + throws(() => new StatsCons(kPrivateConstructor, 1), { code: 'ERR_INVALID_ARG_TYPE', }); } @@ -128,76 +149,89 @@ const { const streamState = new QuicStreamState(kPrivateConstructor, new ArrayBuffer(1024)); const sessionState = new QuicSessionState(kPrivateConstructor, new ArrayBuffer(1024)); -assert.strictEqual(streamState.pending, false); -assert.strictEqual(streamState.finSent, false); -assert.strictEqual(streamState.finReceived, false); -assert.strictEqual(streamState.readEnded, false); -assert.strictEqual(streamState.writeEnded, false); -assert.strictEqual(streamState.reset, false); -assert.strictEqual(streamState.hasReader, false); -assert.strictEqual(streamState.wantsBlock, false); -assert.strictEqual(streamState.wantsReset, false); - -assert.strictEqual(sessionState.hasPathValidationListener, false); -assert.strictEqual(sessionState.hasVersionNegotiationListener, false); -assert.strictEqual(sessionState.hasDatagramListener, false); -assert.strictEqual(sessionState.hasSessionTicketListener, false); -assert.strictEqual(sessionState.isClosing, false); -assert.strictEqual(sessionState.isGracefulClose, false); -assert.strictEqual(sessionState.isSilentClose, false); -assert.strictEqual(sessionState.isStatelessReset, false); -assert.strictEqual(sessionState.isHandshakeCompleted, false); -assert.strictEqual(sessionState.isHandshakeConfirmed, false); -assert.strictEqual(sessionState.isStreamOpenAllowed, false); -assert.strictEqual(sessionState.isPrioritySupported, false); -assert.strictEqual(sessionState.isWrapped, false); -assert.strictEqual(sessionState.lastDatagramId, 0n); - -assert.strictEqual(typeof streamState.toJSON(), 'object'); -assert.strictEqual(typeof sessionState.toJSON(), 'object'); -assert.strictEqual(typeof inspect(streamState), 'string'); -assert.strictEqual(typeof inspect(sessionState), 'string'); +strictEqual(streamState.pending, false); +strictEqual(streamState.finSent, false); +strictEqual(streamState.finReceived, false); +strictEqual(streamState.readEnded, false); +strictEqual(streamState.writeEnded, false); +strictEqual(streamState.reset, false); +strictEqual(streamState.hasReader, false); +strictEqual(streamState.wantsBlock, false); +strictEqual(streamState.wantsReset, false); + +strictEqual(sessionState.hasPathValidationListener, false); +strictEqual(sessionState.hasDatagramListener, false); +strictEqual(sessionState.hasDatagramStatusListener, false); +strictEqual(sessionState.hasSessionTicketListener, false); +strictEqual(sessionState.hasNewTokenListener, false); +strictEqual(sessionState.hasOriginListener, false); +strictEqual(sessionState.isClosing, false); +strictEqual(sessionState.isGracefulClose, false); +strictEqual(sessionState.isSilentClose, false); +strictEqual(sessionState.isStatelessReset, false); +strictEqual(sessionState.isHandshakeCompleted, false); +strictEqual(sessionState.isHandshakeConfirmed, false); +strictEqual(sessionState.isStreamOpenAllowed, false); +strictEqual(sessionState.isPrioritySupported, false); +strictEqual(sessionState.headersSupported, 0); +strictEqual(sessionState.isWrapped, false); +strictEqual(sessionState.maxDatagramSize, 0); +strictEqual(sessionState.lastDatagramId, 0n); + +strictEqual(typeof streamState.toJSON(), 'object'); +strictEqual(typeof sessionState.toJSON(), 'object'); +strictEqual(typeof inspect(streamState), 'string'); +strictEqual(typeof inspect(sessionState), 'string'); const streamStats = new QuicStreamStats(kPrivateConstructor, new ArrayBuffer(1024)); const sessionStats = new QuicSessionStats(kPrivateConstructor, new ArrayBuffer(1024)); -assert.strictEqual(streamStats.createdAt, 0n); -assert.strictEqual(streamStats.openedAt, 0n); -assert.strictEqual(streamStats.receivedAt, 0n); -assert.strictEqual(streamStats.ackedAt, 0n); -assert.strictEqual(streamStats.destroyedAt, 0n); -assert.strictEqual(streamStats.bytesReceived, 0n); -assert.strictEqual(streamStats.bytesSent, 0n); -assert.strictEqual(streamStats.maxOffset, 0n); -assert.strictEqual(streamStats.maxOffsetAcknowledged, 0n); -assert.strictEqual(streamStats.maxOffsetReceived, 0n); -assert.strictEqual(streamStats.finalSize, 0n); -assert.strictEqual(typeof streamStats.toJSON(), 'object'); -assert.strictEqual(typeof inspect(streamStats), 'string'); +strictEqual(streamStats.createdAt, 0n); +strictEqual(streamStats.openedAt, 0n); +strictEqual(streamStats.receivedAt, 0n); +strictEqual(streamStats.ackedAt, 0n); +strictEqual(streamStats.destroyedAt, 0n); +strictEqual(streamStats.bytesReceived, 0n); +strictEqual(streamStats.bytesSent, 0n); +strictEqual(streamStats.maxOffset, 0n); +strictEqual(streamStats.maxOffsetAcknowledged, 0n); +strictEqual(streamStats.maxOffsetReceived, 0n); +strictEqual(streamStats.finalSize, 0n); +strictEqual(typeof streamStats.toJSON(), 'object'); +strictEqual(typeof inspect(streamStats), 'string'); streamStats[kFinishClose](); -assert.strictEqual(typeof sessionStats.createdAt, 'bigint'); -assert.strictEqual(typeof sessionStats.closingAt, 'bigint'); -assert.strictEqual(typeof sessionStats.handshakeCompletedAt, 'bigint'); -assert.strictEqual(typeof sessionStats.handshakeConfirmedAt, 'bigint'); -assert.strictEqual(typeof sessionStats.bytesReceived, 'bigint'); -assert.strictEqual(typeof sessionStats.bytesSent, 'bigint'); -assert.strictEqual(typeof sessionStats.bidiInStreamCount, 'bigint'); -assert.strictEqual(typeof sessionStats.bidiOutStreamCount, 'bigint'); -assert.strictEqual(typeof sessionStats.uniInStreamCount, 'bigint'); -assert.strictEqual(typeof sessionStats.uniOutStreamCount, 'bigint'); -assert.strictEqual(typeof sessionStats.maxBytesInFlights, 'bigint'); -assert.strictEqual(typeof sessionStats.bytesInFlight, 'bigint'); -assert.strictEqual(typeof sessionStats.blockCount, 'bigint'); -assert.strictEqual(typeof sessionStats.cwnd, 'bigint'); -assert.strictEqual(typeof sessionStats.latestRtt, 'bigint'); -assert.strictEqual(typeof sessionStats.minRtt, 'bigint'); -assert.strictEqual(typeof sessionStats.rttVar, 'bigint'); -assert.strictEqual(typeof sessionStats.smoothedRtt, 'bigint'); -assert.strictEqual(typeof sessionStats.ssthresh, 'bigint'); -assert.strictEqual(typeof sessionStats.datagramsReceived, 'bigint'); -assert.strictEqual(typeof sessionStats.datagramsSent, 'bigint'); -assert.strictEqual(typeof sessionStats.datagramsAcknowledged, 'bigint'); -assert.strictEqual(typeof sessionStats.datagramsLost, 'bigint'); -assert.strictEqual(typeof sessionStats.toJSON(), 'object'); -assert.strictEqual(typeof inspect(sessionStats), 'string'); +strictEqual(typeof sessionStats.createdAt, 'bigint'); +strictEqual(typeof sessionStats.closingAt, 'bigint'); +strictEqual(typeof sessionStats.handshakeCompletedAt, 'bigint'); +strictEqual(typeof sessionStats.handshakeConfirmedAt, 'bigint'); +strictEqual(typeof sessionStats.bytesReceived, 'bigint'); +strictEqual(typeof sessionStats.bytesSent, 'bigint'); +strictEqual(typeof sessionStats.bidiInStreamCount, 'bigint'); +strictEqual(typeof sessionStats.bidiOutStreamCount, 'bigint'); +strictEqual(typeof sessionStats.uniInStreamCount, 'bigint'); +strictEqual(typeof sessionStats.uniOutStreamCount, 'bigint'); +strictEqual(typeof sessionStats.maxBytesInFlight, 'bigint'); +strictEqual(typeof sessionStats.bytesInFlight, 'bigint'); +strictEqual(typeof sessionStats.blockCount, 'bigint'); +strictEqual(typeof sessionStats.cwnd, 'bigint'); +strictEqual(typeof sessionStats.latestRtt, 'bigint'); +strictEqual(typeof sessionStats.minRtt, 'bigint'); +strictEqual(typeof sessionStats.rttVar, 'bigint'); +strictEqual(typeof sessionStats.smoothedRtt, 'bigint'); +strictEqual(typeof sessionStats.ssthresh, 'bigint'); +strictEqual(typeof sessionStats.pktSent, 'bigint'); +strictEqual(typeof sessionStats.bytesSent, 'bigint'); +strictEqual(typeof sessionStats.pktRecv, 'bigint'); +strictEqual(typeof sessionStats.bytesRecv, 'bigint'); +strictEqual(typeof sessionStats.pktLost, 'bigint'); +strictEqual(typeof sessionStats.bytesLost, 'bigint'); +strictEqual(typeof sessionStats.pingRecv, 'bigint'); +strictEqual(typeof sessionStats.pktDiscarded, 'bigint'); +strictEqual(typeof sessionStats.datagramsReceived, 'bigint'); +strictEqual(typeof sessionStats.datagramsSent, 'bigint'); +strictEqual(typeof sessionStats.datagramsAcknowledged, 'bigint'); +strictEqual(typeof sessionStats.datagramsLost, 'bigint'); +strictEqual(typeof sessionStats.streamsIdleTimedOut, 'bigint'); +strictEqual(typeof sessionStats.toJSON(), 'object'); +strictEqual(typeof inspect(sessionStats), 'string'); streamStats[kFinishClose](); diff --git a/test/parallel/test-quic-internal-setcallbacks.mjs b/test/parallel/test-quic-internal-setcallbacks.mjs index cebbee43376d6e..b485b5e9b43457 100644 --- a/test/parallel/test-quic-internal-setcallbacks.mjs +++ b/test/parallel/test-quic-internal-setcallbacks.mjs @@ -2,6 +2,8 @@ import { hasQuic, skip } from '../common/index.mjs'; import assert from 'node:assert'; +const { throws } = assert; + if (!hasQuic) { skip('QUIC is not enabled'); } @@ -13,16 +15,23 @@ const callbacks = { onEndpointClose() {}, onSessionNew() {}, onSessionClose() {}, + onSessionApplication() {}, onSessionDatagram() {}, onSessionDatagramStatus() {}, onSessionHandshake() {}, onSessionPathValidation() {}, onSessionTicket() {}, onSessionNewToken() {}, + onSessionKeyLog() {}, + onSessionQlog() {}, + onSessionEarlyDataRejected() {}, + onSessionOrigin() {}, + onSessionGoaway() {}, onSessionVersionNegotiation() {}, onStreamCreated() {}, onStreamBlocked() {}, onStreamClose() {}, + onStreamDrain() {}, onStreamReset() {}, onStreamHeaders() {}, onStreamTrailers() {}, @@ -31,7 +40,7 @@ const callbacks = { for (const fn of Object.keys(callbacks)) { // eslint-disable-next-line no-unused-vars const { [fn]: _, ...rest } = callbacks; - assert.throws(() => quic.setCallbacks(rest), { + throws(() => quic.setCallbacks(rest), { code: 'ERR_MISSING_ARGS', }); } diff --git a/test/parallel/test-quic-keepalive.mjs b/test/parallel/test-quic-keepalive.mjs new file mode 100644 index 00000000000000..e5e44cd6350939 --- /dev/null +++ b/test/parallel/test-quic-keepalive.mjs @@ -0,0 +1,68 @@ +// Flags: --experimental-quic --no-warnings + +// Test: keepAlive option. +// keepAlive keeps idle connection alive past default timeout. +// keepAlive: 0 (default) does not send PING frames. +// Keep-alive PING frames visible in session stats (pingRecv). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// KA-01/03: With keepAlive set, the connection stays alive and +// PING frames are sent. After a brief idle period, the peer's +// pingRecv stat should be > 0. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + // Wait for keep-alive PINGs to arrive. + await setTimeout(300); + // Server should have received PING frames. + ok(serverSession.stats.pingRecv > 0n, + 'Server should receive keep-alive PINGs'); + serverSession.close(); + serverDone.resolve(); + }), { + transportParams: { maxIdleTimeout: 10 }, + }); + + const clientSession = await connect(serverEndpoint.address, { + keepAlive: 100, // Send PING every 100ms. + transportParams: { maxIdleTimeout: 10 }, + }); + + await Promise.all([clientSession.opened, serverDone.promise, clientSession.closed]); + await serverEndpoint.close(); +} + +// Without keepAlive (default), no additional PINGs after handshake. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + // Record PINGs from handshake. + const handshakePings = serverSession.stats.pingRecv; + await setTimeout(200); + // No additional PINGs should arrive without keepAlive. + strictEqual(serverSession.stats.pingRecv, handshakePings); + serverSession.close(); + serverDone.resolve(); + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + await Promise.all([serverDone.promise, clientSession.closed]); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-key-update-peer.mjs b/test/parallel/test-quic-key-update-peer.mjs new file mode 100644 index 00000000000000..1474d15487865f --- /dev/null +++ b/test/parallel/test-quic-key-update-peer.mjs @@ -0,0 +1,50 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: peer-initiated key update handled transparently. +// The server initiates a key update. Data continues flowing on +// the client side without interruption. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + + // Server initiates key update. + serverSession.updateKey(); + + serverSession.onstream = mustCall(async (stream) => { + const data = await bytes(stream); + // Data should arrive correctly despite key update. + strictEqual(Buffer.from(data).toString(), 'after key update'); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Send data after the server's key update — should work transparently. +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('after key update'), +}); +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-key-update.mjs b/test/parallel/test-quic-key-update.mjs new file mode 100644 index 00000000000000..1cdc667c5e0531 --- /dev/null +++ b/test/parallel/test-quic-key-update.mjs @@ -0,0 +1,50 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: session.updateKey() initiates key update, data continues +// flowing. +// After calling updateKey(), the session transitions to new encryption +// keys. Existing and new streams should continue to work normally. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const dataLength = 1024; +const data = new Uint8Array(dataLength); +for (let i = 0; i < dataLength; i++) data[i] = i & 0xff; + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, dataLength); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Initiate key update before sending data. +clientSession.updateKey(); + +// Open a stream and send data — should work with new keys. +const stream = await clientSession.createBidirectionalStream(); +stream.setBody(data); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-list-endpoints.mjs b/test/parallel/test-quic-list-endpoints.mjs new file mode 100644 index 00000000000000..121353ec308ece --- /dev/null +++ b/test/parallel/test-quic-list-endpoints.mjs @@ -0,0 +1,105 @@ +// Flags: --experimental-quic --no-warnings + +// Test: listEndpoints returns live endpoints filtered by active state. +// - Returns an empty array when no endpoints exist. +// - Includes all created endpoints. +// - active=true (default) filters out destroyed, closing, and busy endpoints. +// - active=false returns all registered endpoints regardless of state. +// - Validates options argument types. + +import { hasQuic, skip } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { deepStrictEqual, strictEqual, throws, ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listEndpoints, QuicEndpoint } = await import('node:quic'); + +// listEndpoints is exported as a function. +strictEqual(typeof listEndpoints, 'function'); + +// Empty when no endpoints have been created. +deepStrictEqual(listEndpoints(), []); +deepStrictEqual(listEndpoints({ active: false }), []); + +// Created endpoints appear in the list. +{ + const ep1 = new QuicEndpoint(); + const ep2 = new QuicEndpoint(); + + const list = listEndpoints(); + strictEqual(list.length, 2); + ok(list.includes(ep1)); + ok(list.includes(ep2)); + + // active=false also returns them. + const all = listEndpoints({ active: false }); + strictEqual(all.length, 2); + ok(all.includes(ep1)); + ok(all.includes(ep2)); + + // Returns plain QuicEndpoint instances, not wrapped in arrays. + ok(list[0] instanceof QuicEndpoint); + ok(list[1] instanceof QuicEndpoint); + + await ep1.close(); + await ep2.close(); +} + +// Destroyed endpoints are excluded by active filter. +{ + const ep = new QuicEndpoint(); + strictEqual(listEndpoints().length, 1); + + await ep.close(); + await ep.closed; + strictEqual(ep.destroyed, true); + + // Default (active=true) excludes destroyed endpoint. + strictEqual(listEndpoints().length, 0); + + // active=false still includes it... but destroyed endpoints are removed + // from the registry entirely in kFinishClose, so it won't appear at all. + strictEqual(listEndpoints({ active: false }).length, 0); +} + +// Busy endpoints are excluded by the active filter. +{ + const ep = new QuicEndpoint(); + strictEqual(listEndpoints().length, 1); + + ep.busy = true; + strictEqual(ep.busy, true); + + // active=true excludes busy endpoint. + strictEqual(listEndpoints().length, 0); + + // active=false includes it. + const all = listEndpoints({ active: false }); + strictEqual(all.length, 1); + ok(all.includes(ep)); + + ep.busy = false; + + // After un-busying, it reappears in the active list. + strictEqual(listEndpoints().length, 1); + + await ep.close(); +} + +// Options validation. +throws(() => listEndpoints('bad'), { + code: 'ERR_INVALID_ARG_TYPE', +}); +throws(() => listEndpoints(null), { + code: 'ERR_INVALID_ARG_TYPE', +}); +throws(() => listEndpoints({ active: 'yes' }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +throws(() => listEndpoints({ active: 1 }), { + code: 'ERR_INVALID_ARG_TYPE', +}); diff --git a/test/parallel/test-quic-max-payload-size.mjs b/test/parallel/test-quic-max-payload-size.mjs new file mode 100644 index 00000000000000..c71a93206d03cd --- /dev/null +++ b/test/parallel/test-quic-max-payload-size.mjs @@ -0,0 +1,58 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: maxPayloadSize causes smaller packets. +// With a smaller maxPayloadSize, packets should be smaller. +// We verify by checking that more packets are needed to transfer +// the same amount of data compared to the default. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const dataLength = 4096; + +// Transfer with default maxPayloadSize (1200). +async function transferAndGetPacketCount(maxPayloadSize) { + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, dataLength); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + }), maxPayloadSize ? { maxPayloadSize } : {}); + + const clientSession = await connect(serverEndpoint.address, + maxPayloadSize ? { maxPayloadSize } : {}); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream(); + stream.setBody(new Uint8Array(dataLength)); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await Promise.all([stream.closed, serverDone.promise]); + + const pktSent = clientSession.stats.pktSent; + await clientSession.closed; + await serverEndpoint.close(); + return pktSent; +} + +const defaultPkts = await transferAndGetPacketCount(); +const smallPkts = await transferAndGetPacketCount(1200); + +// With the same or default payload size, packet counts should be similar. +// The key assertion: the option is accepted and data transfers correctly. +ok(defaultPkts > 0n); +ok(smallPkts > 0n); diff --git a/test/parallel/test-quic-max-window.mjs b/test/parallel/test-quic-max-window.mjs new file mode 100644 index 00000000000000..51544d40142d22 --- /dev/null +++ b/test/parallel/test-quic-max-window.mjs @@ -0,0 +1,77 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: maxStreamWindow and maxWindow limits. +// maxStreamWindow limits per-stream receive window. +// maxWindow limits session-level receive window. +// With smaller windows, the transfer should still complete but may +// require more flow control updates. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const dataLength = 8192; + +// maxStreamWindow limits per-stream window. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, dataLength); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + }), { + // Small per-stream receive window. + maxStreamWindow: 1024, + }); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream(); + stream.setBody(new Uint8Array(dataLength)); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await Promise.all([stream.closed, serverDone.promise, clientSession.closed]); + await serverEndpoint.close(); +} + +// maxWindow limits session-level window. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, dataLength); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + }), { + // Small session-level receive window. + maxWindow: 2048, + }); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream(); + stream.setBody(new Uint8Array(dataLength)); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await Promise.all([stream.closed, serverDone.promise, clientSession.closed]); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-module-exports.mjs b/test/parallel/test-quic-module-exports.mjs new file mode 100644 index 00000000000000..1b4b368c60ce61 --- /dev/null +++ b/test/parallel/test-quic-module-exports.mjs @@ -0,0 +1,62 @@ +// Flags: --experimental-quic --no-warnings + +// Test: module exports completeness (CONST-06, CONST-07, CONST-08, +// CONST-09). +// Module exports are sealed. +// Stats classes exist on constructors. +// session ticket getter works after handshake. +// session token getter works after NEW_TOKEN. + +import { hasQuic, skip } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, throws } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const quic = await import('node:quic'); +const { listen, connect } = await import('../common/quic.mjs'); + +// Module exports are frozen/sealed. +throws(() => { quic.newProperty = true; }, TypeError); + +// Stats classes exist. +ok(quic.QuicEndpoint); + +// CONST-08/09: Session ticket and token getters. +{ + let savedTicket; + let savedToken; + const gotTicket = Promise.withResolvers(); + const gotToken = Promise.withResolvers(); + + const serverEndpoint = await listen(async (serverSession) => { + await serverSession.closed; + }); + + const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', + onsessionticket(ticket) { + savedTicket = ticket; + gotTicket.resolve(); + }, + onnewtoken(token) { + savedToken = token; + gotToken.resolve(); + }, + }); + await Promise.all([clientSession.opened, gotTicket.promise, gotToken.promise]); + + // Session ticket is a Buffer. + ok(Buffer.isBuffer(savedTicket)); + ok(savedTicket.length > 0); + + // Token is a Buffer. + ok(Buffer.isBuffer(savedToken)); + ok(savedToken.length > 0); + + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-new-token.mjs b/test/parallel/test-quic-new-token.mjs new file mode 100644 index 00000000000000..5304fa66ccdfc5 --- /dev/null +++ b/test/parallel/test-quic-new-token.mjs @@ -0,0 +1,57 @@ +// Flags: --experimental-quic --no-warnings + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, rejects } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +// The token option must be an ArrayBufferView if provided +await rejects(connect({ port: 1234 }, { + alpn: 'quic-test', + token: 'not-a-buffer', +}), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +// After a successful handshake, the server automatically sends a +// NEW_TOKEN frame. The client should receive it via the onnewtoken +// callback set at connection time. + +const clientToken = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.opened.then(mustCall()); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], +}); + +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + servername: 'localhost', + // Set onnewtoken at connection time to avoid missing the event. + onnewtoken: mustCall(function(token, address) { + ok(Buffer.isBuffer(token), 'token should be a Buffer'); + ok(token.length > 0, 'token should not be empty'); + ok(address !== undefined, 'address should be defined'); + clientToken.resolve(); + }), +}); + +await Promise.all([clientSession.opened, clientToken.promise]); + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-perf-hooks.mjs b/test/parallel/test-quic-perf-hooks.mjs new file mode 100644 index 00000000000000..48352d8eedb9ad --- /dev/null +++ b/test/parallel/test-quic-perf-hooks.mjs @@ -0,0 +1,98 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: PerformanceObserver integration for QUIC. +// QuicEndpoint, QuicSession, and QuicStream emit PerformanceEntry +// objects with entryType 'quic' when a PerformanceObserver is active. + +import { hasQuic, skip, mustCall, mustCallAtLeast } from '../common/index.mjs'; +import assert from 'node:assert'; +import { PerformanceObserver } from 'node:perf_hooks'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const entries = []; + +const observerDone = Promise.withResolvers(); + +// Collect all quic perf entries. +const obs = new PerformanceObserver(mustCallAtLeast((list) => { + for (const entry of list.getEntries()) { + entries.push(entry); + } + // We expect at least: 1 endpoint + 2 sessions + 2 streams = 5 entries. + // The observer may be called multiple times as entries arrive in batches. + // Resolve once we have enough entries. + if (entries.length >= 5) { + observerDone.resolve(); + } +})); +obs.observe({ entryTypes: ['quic'] }); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await bytes(stream); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('perf test'), +}); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise, clientSession.closed]); +await serverEndpoint.close(); + +// Wait for the observer to collect all entries. +await observerDone.promise; +obs.disconnect(); + +// Verify we got all expected entry types. +const endpointEntries = entries.filter((e) => e.name === 'QuicEndpoint'); +const sessionEntries = entries.filter((e) => e.name === 'QuicSession'); +const streamEntries = entries.filter((e) => e.name === 'QuicStream'); + +ok(endpointEntries.length >= 1, `Expected QuicEndpoint entries, got ${endpointEntries.length}`); +ok(sessionEntries.length >= 2, `Expected >= 2 QuicSession entries, got ${sessionEntries.length}`); +ok(streamEntries.length >= 2, `Expected >= 2 QuicStream entries, got ${streamEntries.length}`); + +// Verify common fields on all entries. +for (const entry of entries) { + strictEqual(entry.entryType, 'quic'); + strictEqual(typeof entry.startTime, 'number'); + ok(entry.duration >= 0, `duration should be >= 0, got ${entry.duration}`); + ok(entry.detail, 'entry should have detail'); + ok(entry.detail.stats, 'entry.detail should have stats'); +} + +// Verify session-specific detail fields. +for (const entry of sessionEntries) { + // The handshake may be undefined if destroyed before handshake completes, + // but in this test both sessions complete handshakes. + ok(entry.detail.handshake, 'session entry should have handshake info'); + strictEqual(typeof entry.detail.handshake.protocol, 'string'); + strictEqual(typeof entry.detail.handshake.earlyDataAttempted, 'boolean'); + strictEqual(typeof entry.detail.handshake.earlyDataAccepted, 'boolean'); +} + +// Verify stream-specific detail fields. +for (const entry of streamEntries) { + ok(entry.detail.direction === 'bidi' || entry.detail.direction === 'uni', + `stream direction should be bidi or uni, got ${entry.detail.direction}`); +} diff --git a/test/parallel/test-quic-preferred-address-ignore.mjs b/test/parallel/test-quic-preferred-address-ignore.mjs new file mode 100644 index 00000000000000..f8067e660b5a78 --- /dev/null +++ b/test/parallel/test-quic-preferred-address-ignore.mjs @@ -0,0 +1,60 @@ +// Flags: --experimental-quic --no-warnings + +// Test: preferred address ignored by client. +// Server advertises a preferred address, but the client is configured +// with preferredAddressPolicy: 'ignore'. No path validation should +// occur and all data stays on the original path. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const allStatusDone = Promise.withResolvers(); +const serverGot = Promise.withResolvers(); +let statusCount = 0; + +const preferredEndpoint = await listen(mustNotCall()); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await allStatusDone.promise; + await serverGot.promise; + await serverSession.close(); +}), { + transportParams: { + preferredAddressIpv4: preferredEndpoint.address, + }, + ondatagram: mustCall(() => { + serverGot.resolve(); + }, 2), +}); + +const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, + preferredAddressPolicy: 'ignore', + transportParams: { maxDatagramFrameSize: 1200 }, + // Path validation should NOT fire when ignoring preferred address. + onpathvalidation: mustNotCall(), + ondatagramstatus: mustCall((id, status) => { + if (++statusCount >= 2) allStatusDone.resolve(); + }, 2), +}); +await clientSession.opened; + +await clientSession.sendDatagram(new Uint8Array([1])); +await clientSession.sendDatagram(new Uint8Array([2])); + +await Promise.all([serverGot.promise, allStatusDone.promise]); + +strictEqual(clientSession.stats.datagramsSent, 2n); +ok(clientSession.stats.datagramsAcknowledged >= 1n); + +await clientSession.closed; +await serverEndpoint.close(); +await preferredEndpoint.close(); diff --git a/test/parallel/test-quic-qlog.mjs b/test/parallel/test-quic-qlog.mjs new file mode 100644 index 00000000000000..6bb7666d53e360 --- /dev/null +++ b/test/parallel/test-quic-qlog.mjs @@ -0,0 +1,94 @@ +// Flags: --experimental-quic --no-warnings + +// Test: qlog callback. +// When qlog: true, qlog data is delivered to the session.onqlog +// callback during the connection lifecycle. The final chunk is +// emitted synchronously during ngtcp2_conn destruction with +// fin=true. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { setImmediate } from 'node:timers/promises'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const clientChunks = []; +const serverChunks = []; +let clientFinReceived = false; +let serverFinReceived = false; + +function assertQlogOutput(chunks, finReceived, side) { + ok(chunks.length > 0, `Expected ${side} qlog chunks, got ${chunks.length}`); + ok(finReceived, `Expected ${side} to receive fin`); + + for (const { data, fin } of chunks) { + strictEqual(typeof data, 'string', + `Each ${side} qlog chunk should be a string`); + strictEqual(typeof fin, 'boolean', + `Each ${side} fin flag should be a boolean`); + } + + // Only the last chunk should have fin=true. + for (let i = 0; i < chunks.length - 1; i++) { + strictEqual(chunks[i].fin, false, + `${side} chunk ${i} should not be fin`); + } + strictEqual(chunks[chunks.length - 1].fin, true, + `${side} last chunk should be fin`); + + // ngtcp2 emits qlog in JSON-SEQ format (RFC 7464): each record is + // prefixed with 0x1e (Record Separator) and terminated by a newline. + // Parse the individual records and verify the header has expected fields. + const joined = chunks.map((c) => c.data).join(''); + const records = joined.split('\x1e').filter((s) => s.trim().length > 0); + ok(records.length > 0, `${side} qlog should have at least one record`); + + // The first record is the qlog header with format metadata. + const header = JSON.parse(records[0]); + + ok(header.qlog_version !== undefined || header.qlog_format !== undefined, + `${side} qlog header should have qlog_version or qlog_format field`); + + for (let i = 1; i < records.length; i++) { + const record = JSON.parse(records[i]); + ok('name' in record); + ok('data' in record); + ok('time' in record); + } +} + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + serverSession.close(); +}), { + qlog: true, + onqlog(data, fin) { + serverChunks.push({ data, fin }); + if (fin) serverFinReceived = true; + }, +}); + +const clientSession = await connect(serverEndpoint.address, { + qlog: true, + onqlog(data, fin) { + clientChunks.push({ data, fin }); + if (fin) clientFinReceived = true; + }, +}); + +await Promise.all([clientSession.opened, clientSession.closed]); +await serverEndpoint.close(); + +// The final qlog chunk (fin=true) is delivered via SetImmediate because +// it is emitted during ngtcp2_conn destruction when MakeCallback is +// unsafe. Yield to let the deferred callback run before asserting. +await setImmediate(); + +assertQlogOutput(clientChunks, clientFinReceived, 'client'); +assertQlogOutput(serverChunks, serverFinReceived, 'server'); diff --git a/test/parallel/test-quic-reject-unauthorized.mjs b/test/parallel/test-quic-reject-unauthorized.mjs new file mode 100644 index 00000000000000..33fa3661a857f2 --- /dev/null +++ b/test/parallel/test-quic-reject-unauthorized.mjs @@ -0,0 +1,101 @@ +// Flags: --experimental-quic --no-warnings + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { strictEqual, ok, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +// rejectUnauthorized must be a boolean +await rejects(connect({ port: 1234 }, { + alpn: 'quic-test', + rejectUnauthorized: 'yes', +}), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +// verifyPeer must be one of 'strict', 'auto', 'manual' +await rejects(connect({ port: 1234 }, { + alpn: 'quic-test', + verifyPeer: 'invalid', +}), { + code: 'ERR_INVALID_ARG_VALUE', +}); + +const serverEndpoint = await listen(async (serverSession) => { + serverSession.onerror = () => {}; + await rejects(serverSession.opened, (err) => { + ok(err.code === 'ERR_QUIC_TRANSPORT_ERROR' || + err.code === 'ERR_INVALID_STATE'); + return true; + }); + serverSession.close(); +}, { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], +}); + +// --- verifyPeer: 'manual' (current behavior) --- +// The session.opened promise resolves even with an invalid cert. +// The validation error is available in the info object. +{ + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + servername: 'localhost', + verifyPeer: 'manual', + }); + + const info = await clientSession.opened; + // Self-signed cert without CA should produce a validation error. + strictEqual(typeof info.validationErrorReason, 'string'); + ok(info.validationErrorReason.length > 0); + strictEqual(typeof info.validationErrorCode, 'string'); + ok(info.validationErrorCode.length > 0); + + await clientSession.close(); +} + +// --- verifyPeer: 'auto' (default) --- +// The session.opened promise rejects when the cert is invalid. +{ + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + servername: 'localhost', + // verifyPeer defaults to 'auto' + }); + + await rejects(clientSession.opened, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); +} + +// --- verifyPeer: 'strict' --- +// The TLS handshake itself fails. The session.opened promise rejects. +{ + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + servername: 'localhost', + verifyPeer: 'strict', + // The TLS failure triggers onerror before opened rejects. + onerror: mustCall((err) => { + ok(err, 'strict mode should fire onerror on invalid cert'); + }), + }); + + await rejects(clientSession.opened, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); +} + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-application-options.mjs b/test/parallel/test-quic-session-application-options.mjs new file mode 100644 index 00000000000000..1f5c9c0926808c --- /dev/null +++ b/test/parallel/test-quic-session-application-options.mjs @@ -0,0 +1,105 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: session.applicationOptions +// Verifies that applicationOptions is available after ALPN negotiation +// completes (i.e., once the application has been selected), returns a +// null-prototype object, and reflects the configured values. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const customAppOptions = { + maxHeaderPairs: 50n, + maxHeaderLength: 8192n, + maxFieldSectionSize: 16384n, + qpackMaxDTableCapacity: 2048n, + qpackEncoderMaxDTableCapacity: 2048n, + qpackBlockedStreams: 50n, + enableConnectProtocol: false, + enableDatagrams: false, +}; + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // After the stream arrives, the handshake and ALPN negotiation are + // complete, so applicationOptions should be available. + const opts = serverSession.applicationOptions; + + ok(opts != null, 'server applicationOptions should be available after handshake'); + strictEqual(typeof opts, 'object'); + strictEqual(Object.getPrototypeOf(opts), null); + + // Verify configured values are reflected. + strictEqual(opts.maxHeaderPairs, BigInt(customAppOptions.maxHeaderPairs)); + strictEqual(opts.maxHeaderLength, BigInt(customAppOptions.maxHeaderLength)); + strictEqual(opts.maxFieldSectionSize, + BigInt(customAppOptions.maxFieldSectionSize)); + strictEqual(opts.qpackMaxDtableCapacity, + BigInt(customAppOptions.qpackMaxDTableCapacity)); + strictEqual(opts.qpackEncoderMaxDtableCapacity, + BigInt(customAppOptions.qpackEncoderMaxDTableCapacity)); + strictEqual(opts.qpackBlockedStreams, + BigInt(customAppOptions.qpackBlockedStreams)); + strictEqual(opts.enableConnectProtocol, + customAppOptions.enableConnectProtocol); + strictEqual(opts.enableDatagrams, customAppOptions.enableDatagrams); + + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + application: customAppOptions, +}); + +const clientSession = await connect(serverEndpoint.address, { + application: customAppOptions, +}); +await clientSession.opened; + +// After opened, ALPN negotiation is complete and applicationOptions +// should be available on the client session. +const clientOpts = clientSession.applicationOptions; +ok(clientOpts != null, 'client applicationOptions should be available after handshake'); +strictEqual(typeof clientOpts, 'object'); +strictEqual(Object.getPrototypeOf(clientOpts), null); + +// Verify configured values on the client side. +strictEqual(clientOpts.maxHeaderPairs, BigInt(customAppOptions.maxHeaderPairs)); +strictEqual(clientOpts.maxHeaderLength, BigInt(customAppOptions.maxHeaderLength)); +strictEqual(clientOpts.maxFieldSectionSize, + customAppOptions.maxFieldSectionSize); +strictEqual(clientOpts.qpackMaxDtableCapacity, + customAppOptions.qpackMaxDTableCapacity); +strictEqual(clientOpts.qpackEncoderMaxDtableCapacity, + customAppOptions.qpackEncoderMaxDTableCapacity); +strictEqual(clientOpts.qpackBlockedStreams, + customAppOptions.qpackBlockedStreams); +strictEqual(clientOpts.enableConnectProtocol, + customAppOptions.enableConnectProtocol); +strictEqual(clientOpts.enableDatagrams, customAppOptions.enableDatagrams); + +// Exchange data to let the server side run its assertions. +const stream = await clientSession.createBidirectionalStream(); +stream.writer.endSync(); + +// eslint-disable-next-line no-unused-vars +for await (const _ of stream) { /* drain */ } +await Promise.all([stream.closed, serverDone.promise]); + +// After close, applicationOptions should return null. +await clientSession.close(); +strictEqual(clientSession.applicationOptions, null); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-close-error-code.mjs b/test/parallel/test-quic-session-close-error-code.mjs new file mode 100644 index 00000000000000..5d0392bf102585 --- /dev/null +++ b/test/parallel/test-quic-session-close-error-code.mjs @@ -0,0 +1,169 @@ +// Flags: --experimental-quic --no-warnings + +// Test: session close/destroy with application and transport error codes +// . +// Application error propagated as ERR_QUIC_APPLICATION_ERROR. +// Session close with specific app error code — peer receives it. +// Verifies that close() and destroy() with { code, type, reason } options +// send the correct CONNECTION_CLOSE frame, and the peer receives the +// correct error type, code, and reason. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; + +const { strictEqual, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// --- Test 1: close() with application error code --- +// The client closes with an application error. The server receives +// ERR_QUIC_APPLICATION_ERROR with the exact code and reason. +{ + const serverGot = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onerror = mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); + strictEqual(err.message.includes('42'), true, + 'error message should contain the code'); + strictEqual(err.message.includes('client shutdown'), true, + 'error message should contain the reason'); + strictEqual(err.errorCode, 42n); + strictEqual(err.type, 'application'); + strictEqual(err.reason, 'client shutdown'); + }); + await rejects(serverSession.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + errorCode: 42n, + reason: 'client shutdown', + }); + serverGot.resolve(); + })); + + const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, + }); + await clientSession.opened; + + // Small delay to ensure handshake is fully confirmed so ngtcp2 + // generates the 1-RTT APPLICATION CONNECTION_CLOSE frame. + await setTimeout(100); + + // close() with application error — the client's closed promise + // resolves because the close was locally initiated (intentional). + // The peer receives the error code, but the local side is not in error. + await clientSession.close({ + code: 42n, + type: 'application', + reason: 'client shutdown', + }); + + await serverGot.promise; + await serverEndpoint.close(); +} + +// --- Test 2: close() with transport error code --- +// The client closes with a transport error. The server receives +// ERR_QUIC_TRANSPORT_ERROR with the exact code. +{ + const serverGot = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onerror = mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + strictEqual(err.message.includes('1'), true, + 'error message should contain the code'); + strictEqual(err.errorCode, 1n); + strictEqual(err.type, 'transport'); + }); + await rejects(serverSession.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); + serverGot.resolve(); + })); + + const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, + }); + await clientSession.opened; + await setTimeout(100); + + // close() with transport error — resolves locally (intentional). + await clientSession.close({ code: 1n }); + + await serverGot.promise; + await serverEndpoint.close(); +} + +// --- Test 3: destroy() with application error code --- +// The client destroys with both a JS error and a QUIC error code. +// The peer receives the QUIC application error. +{ + const serverGot = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onerror = mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); + strictEqual(err.message.includes('99'), true); + strictEqual(err.errorCode, 99n); + strictEqual(err.type, 'application'); + strictEqual(err.reason, 'destroy with code'); + }); + await rejects(serverSession.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + serverGot.resolve(); + })); + + const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, + onerror: mustCall((err) => { + // The JS error passed to destroy is delivered via onerror. + strictEqual(err.message, 'fatal error'); + }), + }); + await clientSession.opened; + await setTimeout(100); + + const jsError = new Error('fatal error'); + clientSession.destroy(jsError, { + code: 99n, + type: 'application', + reason: 'destroy with code', + }); + + // The closed promise rejects with the JS error, not the QUIC error. + await rejects(clientSession.closed, jsError); + + await serverGot.promise; + await serverEndpoint.close(); +} + +// --- Test 4: close() with no options (default behavior) --- +// Verify the default close sends NO_ERROR and the peer closes cleanly. +{ + const serverGot = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + // No onerror — clean close should not trigger errors. + await serverSession.closed; + serverGot.resolve(); + })); + + const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, + }); + await clientSession.opened; + await setTimeout(100); + + // Default close — no error code, clean shutdown. + await clientSession.close(); + + await serverGot.promise; + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-session-close-graceful.mjs b/test/parallel/test-quic-session-close-graceful.mjs new file mode 100644 index 00000000000000..fef5384696960e --- /dev/null +++ b/test/parallel/test-quic-session-close-graceful.mjs @@ -0,0 +1,90 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: graceful session close with open streams. +// session.close() with open streams waits for streams to close +// before the session's closed promise resolves. +// After close() is called, no new streams can be created. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); + +// ------------------------------------------------------------------- +// close() waits for open streams to finish. +// ------------------------------------------------------------------- +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, 5); + stream.writer.endSync(); + await stream.closed; + serverDone.resolve(); + }); + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + // Open a stream and send data. + const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('hello'), + }); + + // Call close() while the stream is still open. The closed promise + // should NOT resolve until the stream finishes. + let closedResolved = false; + const closePromise = clientSession.close(); + closePromise.then(mustCall(() => { closedResolved = true; })); + + // Wait for the stream to complete normally. + await serverDone.promise; + for await (const batch of stream) { /* drain server FIN */ } // eslint-disable-line no-unused-vars + await stream.closed; + + // Now the closed promise should resolve. + await closePromise; + strictEqual(closedResolved, true); + strictEqual(clientSession.destroyed, true); + + await serverEndpoint.close(); +} + +// ------------------------------------------------------------------- +// No new streams after close() is called. +// ------------------------------------------------------------------- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + clientSession.close(); + + // Attempting to create a stream after close() should reject. + await rejects( + clientSession.createBidirectionalStream({ + body: encoder.encode('too late'), + }), + { + code: 'ERR_INVALID_STATE', + }, + ); + + await clientSession.closed; + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-session-close-sends-frame.mjs b/test/parallel/test-quic-session-close-sends-frame.mjs new file mode 100644 index 00000000000000..3d398726585b0b --- /dev/null +++ b/test/parallel/test-quic-session-close-sends-frame.mjs @@ -0,0 +1,44 @@ +// Flags: --experimental-quic --no-warnings + +// Test: active session sends CONNECTION_CLOSE when closing. +// When the server calls session.close() on an active session, the peer +// receives a CONNECTION_CLOSE frame with NO_ERROR. The client's closed +// promise resolves (clean close), rather than rejecting with an idle +// timeout or transport error. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverReady = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + // Signal to the client that the server is ready, then close. + serverReady.resolve(); + await serverSession.close(); +})); + +const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, +}); + +await Promise.all([clientSession.opened, serverReady.promise]); + +// The client receives CONNECTION_CLOSE with NO_ERROR. +// The closed promise should resolve (not reject). If the server +// failed to send CONNECTION_CLOSE (e.g., used silent close or +// stateless reset), the client would time out and closed would +// reject with ERR_QUIC_TRANSPORT_ERROR. +await clientSession.closed; + +// If we reach here, the session closed cleanly — CONNECTION_CLOSE +// was received, not an idle timeout. +assert.ok(clientSession.destroyed, 'session should be destroyed'); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-close.mjs b/test/parallel/test-quic-session-close.mjs new file mode 100644 index 00000000000000..5d63326f04c0e6 --- /dev/null +++ b/test/parallel/test-quic-session-close.mjs @@ -0,0 +1,77 @@ +// Flags: --experimental-quic --no-warnings + +// Test: session.close() lifecycle. +// session.close() with no open streams resolves the closed promise. +// Server-initiated close delivers CONNECTION_CLOSE to the client. +// Client-initiated close — server sees the session end. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// ------------------------------------------------------------------- +// session.close() with no open streams resolves closed. +// ------------------------------------------------------------------- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + // Server just waits for the client to close. + await serverSession.closed; + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + // No streams opened. close() should resolve. + await clientSession.close(); + strictEqual(clientSession.destroyed, true); + await serverEndpoint.close(); +} + +// ------------------------------------------------------------------- +// Server-initiated close — client sees session end. +// ------------------------------------------------------------------- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + // Server initiates close. + serverSession.close(); + await serverSession.closed; + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + // Client's closed promise should resolve when the server closes. + await clientSession.closed; + strictEqual(clientSession.destroyed, true); + await serverEndpoint.close(); +} + +// ------------------------------------------------------------------- +// Client-initiated close — server sees session end. +// ------------------------------------------------------------------- +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + // The server's closed promise should resolve when the client closes. + await serverSession.closed; + strictEqual(serverSession.destroyed, true); + serverDone.resolve(); + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + // Client initiates close. + await clientSession.close(); + await serverDone.promise; + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-session-destroy-reentrant.mjs b/test/parallel/test-quic-session-destroy-reentrant.mjs new file mode 100644 index 00000000000000..15d3ca28cf4ac8 --- /dev/null +++ b/test/parallel/test-quic-session-destroy-reentrant.mjs @@ -0,0 +1,188 @@ +// Flags: --experimental-quic --no-warnings + +// Test: QuicSession.destroy and QuicStream.destroy are safely re-entrant. +// +// Calling destroy() from within an onerror callback (or from a stream +// onerror that fires during the session destroy cascade) used to run +// cleanup twice because the `if (this.destroyed)` guard checked `#handle`, +// which was only cleared at the END of destroy() — well after user-visible +// callbacks ran. +// +// The fix introduces a `#destroying` boolean flag that flips +// synchronously at the top of `destroy()`, so a re-entrant call hits +// the guard and returns immediately. The `#handle === undefined` +// invariant remains the "fully torn down" signal used by `kFinishClose` +// and only flips once teardown completes - so `session.destroyed` / +// `stream.destroyed` is still false from inside `onerror` (which runs +// during teardown), and only becomes true once `destroy()` returns. +// +// Cases covered: +// 1. session.onerror calls session.destroy() recursively. Verify the +// session is not torn down twice (no double publish on the +// diagnostics_channel, closed promise settles exactly once). +// 2. session.destroy(err) cascades to stream.destroy; the stream's +// onerror calls session.destroy() again. Verify safe. +// 3. stream.onerror calls stream.destroy() recursively. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import diagnostics_channel from 'node:diagnostics_channel'; +import { setImmediate } from 'node:timers/promises'; + +const { strictEqual, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// Short idle timeout so the server cleans up quickly after we destroy +// the client without sending CONNECTION_CLOSE. +const transportParams = { maxIdleTimeout: 1 }; + +// ------------------------------------------------------------------- +// 1. session.onerror calls session.destroy() recursively. +// ------------------------------------------------------------------- +{ + const serverDone = Promise.withResolvers(); + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + serverDone.resolve(); + }), { transportParams }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + await clientSession.opened; + + // Count diagnostics_channel publishes. Even though `destroy()` is + // called twice (once explicitly and once recursively from inside the + // `onerror` handler), the `#destroying` guard makes the second call + // a true no-op so each channel publishes exactly once. + const errSub = mustCall((msg) => { + strictEqual(msg.session, clientSession); + }); + const closedSub = mustCall((msg) => { + strictEqual(msg.session, clientSession); + }); + diagnostics_channel.subscribe('quic.session.error', errSub); + diagnostics_channel.subscribe('quic.session.closed', closedSub); + + const testError = new Error('reentrant destroy test'); + + clientSession.onerror = mustCall((err) => { + strictEqual(err, testError); + // Re-enter destroy synchronously from the error handler. This must + // be a no-op because `#destroying` is already set; `destroyed` is + // still false here because `#handle` is cleared at the end of + // `destroy()`. The fact that the closed promise still rejects with + // `testError` (asserted below) - and not `'should be ignored'` - + // proves the recursive call did not run a second teardown. + clientSession.destroy(new Error('should be ignored')); + }); + + clientSession.destroy(testError); + // Once `destroy()` has returned, `destroyed` flips to true. + strictEqual(clientSession.destroyed, true); + + await rejects(clientSession.closed, testError); + + // Give the diagnostics_channel a tick to deliver any deferred messages. + await setImmediate(); + + diagnostics_channel.unsubscribe('quic.session.error', errSub); + diagnostics_channel.unsubscribe('quic.session.closed', closedSub); + + await serverDone.promise; + await serverEndpoint.close(); +} + +// ------------------------------------------------------------------- +// 2. session.destroy(err) -> stream onerror -> session.destroy() again. +// The stream's onerror is invoked during the session's destroy +// cascade; if that handler calls session.destroy() again, the second +// destroy must be a no-op. +// ------------------------------------------------------------------- +{ + const serverDone = Promise.withResolvers(); + const serverEndpoint = await listen(mustCall(async (serverSession) => { + // Server-side onstream callbacks are not always invoked here because + // the client may destroy before the server processes the stream open. + // We don't make assertions about server stream activity in this case; + // the test is purely about client-side re-entrancy. + await serverSession.closed; + serverDone.resolve(); + }), { transportParams }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream(); + + const testError = new Error('cascade reentrant destroy test'); + + stream.onerror = mustCall((err) => { + strictEqual(err, testError); + // Re-enter session.destroy from inside the stream's onerror. This + // is happening DURING the session's stream cascade, so the session + // is mid-destroy. The second destroy() call must be a no-op. + clientSession.destroy(new Error('should be ignored')); + }); + + clientSession.onerror = mustCall((err) => { + strictEqual(err, testError); + }); + + clientSession.destroy(testError); + strictEqual(clientSession.destroyed, true); + strictEqual(stream.destroyed, true); + + await rejects(clientSession.closed, testError); + await rejects(stream.closed, testError); + + await serverDone.promise; + await serverEndpoint.close(); +} + +// ------------------------------------------------------------------- +// 3. stream.onerror calls stream.destroy() recursively. +// ------------------------------------------------------------------- +{ + const serverDone = Promise.withResolvers(); + const serverEndpoint = await listen(mustCall(async (serverSession) => { + try { await serverSession.closed; } catch { /* server cascade-close */ } + serverDone.resolve(); + }), { transportParams }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream(); + + const testError = new Error('stream reentrant destroy test'); + + stream.onerror = mustCall((err) => { + strictEqual(err, testError); + // Re-enter stream.destroy from inside its own onerror. The + // `#destroying` flag traps the recursive call; `destroyed` (i.e. + // `#handle === undefined`) is still false here because the handle + // is cleared inside `[kFinishClose]` later in this method. + stream.destroy(new Error('should be ignored')); + }); + + stream.destroy(testError); + // Once `destroy()` has returned, `destroyed` flips to true. + strictEqual(stream.destroyed, true); + + await rejects(stream.closed, testError); + + clientSession.close(); + await clientSession.closed; + await serverDone.promise; + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-session-destroy-validate-options.mjs b/test/parallel/test-quic-session-destroy-validate-options.mjs new file mode 100644 index 00000000000000..d3812a7b341f16 --- /dev/null +++ b/test/parallel/test-quic-session-destroy-validate-options.mjs @@ -0,0 +1,133 @@ +// Flags: --experimental-quic --no-warnings + +// Test: QuicSession.destroy(error, options) validates `options` up front, +// before any side effects. A malformed `options` argument must throw +// without: +// * publishing on the `quic.session.error` diagnostics channel, +// * invoking the user's `onerror` callback, +// * cascading destroy to open streams, +// * clearing the session's private state, +// * destroying the underlying C++ handle. +// +// After such a throw the session is still alive and a subsequent +// destroy() with valid options must succeed: the client emits a +// CONNECTION_CLOSE frame with the supplied transport error code, and +// the local `closed` promise rejects with the original error. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import diagnostics_channel from 'node:diagnostics_channel'; + +const { strictEqual, rejects, throws } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// `maxIdleTimeout` is measured in seconds. Set short so any regression +// that prevents CONNECTION_CLOSE from being sent fails promptly via +// idle close instead of hanging the test. +const transportParams = { maxIdleTimeout: 1 }; + +// Capture the server-side session in the listen callback. Do not +// `await serverSession.closed` from inside the callback: the final +// destroy in this test sends a transport-error CONNECTION_CLOSE which +// makes that promise reject, and an unhandled rejection inside the +// listen callback would be routed by `safeCallbackInvoke` to +// `endpoint.destroy(err)` and surface as a process-level +// unhandled-rejection crash before the test body has had a chance +// to attach its own rejection handler. +const serverSessionReady = Promise.withResolvers(); +let serverSession; +const serverEndpoint = await listen(mustCall((session) => { + serverSession = session; + serverSessionReady.resolve(); +}), { transportParams }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams, +}); +await clientSession.opened; +await serverSessionReady.promise; + +// Open a stream so the destroy cascade has work to do; if the +// validation hoist regresses and side effects run before the throw, +// this stream will be destroyed by the failed destroy() and the +// `mustNotCall` handler will fire. +const stream = await clientSession.createBidirectionalStream(); +stream.onerror = mustNotCall( + 'stream.onerror must not fire when destroy() throws on bad options'); + +// Subscribe to diagnostics channels so we can assert nothing is +// published before the validation throw. +const errSub = mustNotCall( + 'quic.session.error must not publish when destroy() throws on bad options'); +diagnostics_channel.subscribe('quic.session.error', errSub); + +clientSession.onerror = mustNotCall( + 'session.onerror must not fire when destroy() throws on bad options'); + +const goodError = new Error('intended teardown'); + +// 1. options is not an object -> throws ERR_INVALID_ARG_TYPE. +throws(() => clientSession.destroy(goodError, 'not an object'), { + code: 'ERR_INVALID_ARG_TYPE', +}); +strictEqual(clientSession.destroyed, false); +strictEqual(stream.destroyed, false); + +// 2. options.code is the wrong type -> throws ERR_INVALID_ARG_TYPE. +throws(() => clientSession.destroy(goodError, { code: 'oops' }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +strictEqual(clientSession.destroyed, false); +strictEqual(stream.destroyed, false); + +// 3. options.type is not in the allowed set -> throws ERR_INVALID_ARG_VALUE. +throws(() => clientSession.destroy(goodError, { type: 'bogus' }), { + code: 'ERR_INVALID_ARG_VALUE', +}); +strictEqual(clientSession.destroyed, false); +strictEqual(stream.destroyed, false); + +// 4. options.reason is the wrong type -> throws ERR_INVALID_ARG_TYPE. +throws(() => clientSession.destroy(goodError, { reason: 42 }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +strictEqual(clientSession.destroyed, false); +strictEqual(stream.destroyed, false); + +// Now switch the handlers to expect the real teardown so the final +// destroy with valid options can run cleanly. +diagnostics_channel.unsubscribe('quic.session.error', errSub); +clientSession.onerror = mustCall((err) => { strictEqual(err, goodError); }); +stream.onerror = mustCall((err) => { strictEqual(err, goodError); }); + +// Pre-attach rejection handlers on both sides BEFORE triggering the +// final destroy, so the rejections do not race ahead of any awaits in +// the test body. The client rejects with the original `goodError`; +// the server decodes the CONNECTION_CLOSE frame transport code into +// an `ERR_QUIC_TRANSPORT_ERROR`. +const clientClosedAssertion = rejects(clientSession.closed, goodError); +const serverClosedAssertion = rejects(serverSession.closed, mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + return true; +})); + +// 5. Valid options after the failed attempts -> session destroys +// normally, the underlying handle sends CONNECTION_CLOSE with the +// supplied transport code, and the local closed promise rejects +// with the original error. +clientSession.destroy(goodError, { + code: 1n, + type: 'transport', + reason: 'after validation throw', +}); +strictEqual(clientSession.destroyed, true); +strictEqual(stream.destroyed, true); + +await clientClosedAssertion; +await serverClosedAssertion; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-destroy.mjs b/test/parallel/test-quic-session-destroy.mjs new file mode 100644 index 00000000000000..03468e429178c7 --- /dev/null +++ b/test/parallel/test-quic-session-destroy.mjs @@ -0,0 +1,103 @@ +// Flags: --experimental-quic --no-warnings + +// Test: session.destroy() forceful close. +// destroy() without error resolves the closed promise. +// destroy(error) rejects the closed promise with that error. +// destroy() works without a prior close() call. +// Note: destroy() is forceful and does not send CONNECTION_CLOSE. +// The server session remains alive until idle timeout unless we also +// destroy the server session explicitly. We use a short idle timeout +// to keep the tests fast, and destroy both sides in each section. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// Use a short idle timeout so the server session cleans up quickly +// after the client destroys without CONNECTION_CLOSE. +const transportParams = { maxIdleTimeout: 1 }; + +// ------------------------------------------------------------------- +// destroy() without error resolves the closed promise. +// ------------------------------------------------------------------- +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + serverDone.resolve(); + }), { transportParams }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + await clientSession.opened; + + clientSession.destroy(); + strictEqual(clientSession.destroyed, true); + + // Closed should resolve (no error). + await clientSession.closed; + + await serverDone.promise; + await serverEndpoint.close(); +} + +// ------------------------------------------------------------------- +// destroy(error) rejects closed with that error. +// ------------------------------------------------------------------- +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + serverDone.resolve(); + }), { transportParams }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + await clientSession.opened; + + const testError = new Error('intentional destroy error'); + clientSession.destroy(testError); + strictEqual(clientSession.destroyed, true); + + // Closed should reject with the same error. + await rejects(clientSession.closed, testError); + + await serverDone.promise; + await serverEndpoint.close(); +} + +// ------------------------------------------------------------------- +// destroy() works without prior close(). +// ------------------------------------------------------------------- +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + serverDone.resolve(); + }), { transportParams }); + + const clientSession = await connect(serverEndpoint.address, { + transportParams, + }); + await clientSession.opened; + + // Destroy directly without calling close() first. + clientSession.destroy(); + strictEqual(clientSession.destroyed, true); + await clientSession.closed; + + await serverDone.promise; + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-session-idle-timeout.mjs b/test/parallel/test-quic-session-idle-timeout.mjs new file mode 100644 index 00000000000000..e55f9a0a16fda2 --- /dev/null +++ b/test/parallel/test-quic-session-idle-timeout.mjs @@ -0,0 +1,37 @@ +// Flags: --experimental-quic --no-warnings + +// Test: idle timeout closes the session. +// Both client and server are configured with a short maxIdleTimeout. +// After the handshake completes, neither side sends any data. The idle +// timeout fires and both sessions close without error. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const transportParams = { maxIdleTimeout: 1 }; // 1 second + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + // The server's closed promise should resolve when the idle timeout fires. + await serverSession.closed; + serverDone.resolve(); +}), { + transportParams, +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams, +}); + +await clientSession.opened; + +// Don't send anything. Just wait for the idle timeout to close the session. +await Promise.all([clientSession.closed, serverDone.promise]); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-initial-rtt.mjs b/test/parallel/test-quic-session-initial-rtt.mjs new file mode 100644 index 00000000000000..5044cc5f7bcdb5 --- /dev/null +++ b/test/parallel/test-quic-session-initial-rtt.mjs @@ -0,0 +1,60 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: initialRtt session option is accepted and the session functions +// correctly with a custom initial RTT estimate. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const payload = encoder.encode('hello rtt'); +const serverDone = Promise.withResolvers(); + +// Use a low initialRtt (1ms) to simulate a low-latency environment. +// The session should complete successfully and the smoothed RTT in +// stats should converge to a value well below the default 333ms. +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await bytes(stream); + ok(data.byteLength > 0); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + initialRtt: 1, // 1ms +}); + +const clientSession = await connect(serverEndpoint.address, { + initialRtt: 1, // 1ms +}); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: payload, +}); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await stream.closed; +await serverDone.promise; + +// After data exchange, the smoothed RTT should have converged to a +// realistic value. On loopback it should be well under 10ms (10,000,000ns). +// The stat is in nanoseconds. +const smoothedRtt = clientSession.stats.smoothedRtt; +ok(smoothedRtt > 0n, 'smoothedRtt should be non-zero after data exchange'); +ok(smoothedRtt < 10_000_000n, + `smoothedRtt should be under 10ms on loopback, got ${smoothedRtt}ns`); + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-opened-early-destroy.mjs b/test/parallel/test-quic-session-opened-early-destroy.mjs new file mode 100644 index 00000000000000..9c1cbd7f71f3e0 --- /dev/null +++ b/test/parallel/test-quic-session-opened-early-destroy.mjs @@ -0,0 +1,150 @@ +// Flags: --experimental-quic --no-warnings + +// Test: session.opened rejects when the session is destroyed before the +// TLS handshake completes. +// +// Per doc/api/quic.md: "If the handshake fails or the session is destroyed +// before the handshake completes, the promise will be rejected." +// +// Cases covered: +// 1. session.destroy() (no error) before handshake -> opened rejects +// with ERR_INVALID_STATE. +// 2. session.destroy(error) before handshake -> opened rejects with +// that error. +// 3. endpoint.destroy() while a client handshake is in flight -> +// session.opened rejects (the endpoint cascades destroy to its +// sessions). +// 4. session.destroy() AFTER opened has already resolved -> opened +// stays resolved (no late rejection). + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { QuicEndpoint } = await import('node:quic'); + +// Use a short idle timeout so any leftover server-side state cleans up +// quickly when the client tears down before the handshake completes. +const transportParams = { maxIdleTimeout: 1 }; + +// ------------------------------------------------------------------- +// 1. session.destroy() (no error) before handshake -> opened rejects +// with ERR_INVALID_STATE. +// ------------------------------------------------------------------- +{ + const serverEndpoint = await listen(() => { + // The server may or may not see this session before the client + // destroys; we don't await it here. + }, { transportParams }); + + const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', + transportParams, + }); + + // Synchronously destroy the session before the handshake can finish. + // `connect()` returns the session as soon as the C++ handle is created; + // the handshake itself happens asynchronously over the network. + clientSession.destroy(); + + await rejects(clientSession.opened, { + code: 'ERR_INVALID_STATE', + message: /destroyed before it opened/, + }); + + await serverEndpoint.close(); +} + +// ------------------------------------------------------------------- +// 2. session.destroy(error) before handshake -> opened rejects with +// that error. Setting `onerror` registers the session-level error +// handler and marks both `opened` and `closed` as handled, so we +// only need to assert the rejection on `opened`. +// ------------------------------------------------------------------- +{ + const serverEndpoint = await listen(mustNotCall, { transportParams }); + + const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', + transportParams, + }); + + const testError = new Error('intentional early destroy'); + clientSession.onerror = mustCall((err) => { + assert.strictEqual(err, testError); + }); + clientSession.destroy(testError); + + await assert.rejects(clientSession.opened, testError); + + await serverEndpoint.close(); +} + +// ------------------------------------------------------------------- +// 3. endpoint.destroy() while a client handshake is in flight -> +// session.opened rejects. +// ------------------------------------------------------------------- +{ + const serverEndpoint = await listen(mustNotCall(), { transportParams }); + + // Use a dedicated client endpoint so destroying it does not affect + // the shared default endpoint used by other tests in this file. + const clientEndpoint = new QuicEndpoint(); + + const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', + transportParams, + endpoint: clientEndpoint, + }); + + // Synchronously destroy the endpoint while the handshake is still + // in flight. The endpoint should cascade destroy() to its in-flight + // session, which should reject session.opened. + clientEndpoint.destroy(); + + await rejects(clientSession.opened, { + code: 'ERR_INVALID_STATE', + message: /destroyed before it opened/, + }); + + await serverEndpoint.close(); +} + +// ------------------------------------------------------------------- +// 4. session.destroy() AFTER opened resolved -> opened stays resolved. +// Sanity check that the new "always reject pendingOpen" logic does +// not stomp on an already-resolved promise. +// ------------------------------------------------------------------- +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; + serverDone.resolve(); + }), { transportParams }); + + const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', + transportParams, + }); + + const info = await clientSession.opened; + strictEqual(typeof info.protocol, 'string'); + + clientSession.destroy(); + + // Awaiting opened a second time should still resolve to the same + // info object (PromiseWithResolvers caches it; we just assert the + // promise is not rejected). + const infoAgain = await clientSession.opened; + strictEqual(infoAgain, info); + + await serverDone.promise; + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-session-opened-info.mjs b/test/parallel/test-quic-session-opened-info.mjs new file mode 100644 index 00000000000000..e954f079452ab6 --- /dev/null +++ b/test/parallel/test-quic-session-opened-info.mjs @@ -0,0 +1,72 @@ +// Flags: --experimental-quic --no-warnings + +// Test: session.opened resolves with handshake info (INFO-05, INFO-06, +// INFO-07, INFO-08). +// local and remote SocketAddress objects are correct. +// servername matches the SNI sent by the client. +// protocol matches the negotiated ALPN. +// cipher and cipherVersion reflect the negotiated cipher suite. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { notStrictEqual, strictEqual, ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + const info = await serverSession.opened; + + // Server sees its own local address and the client's remote. + strictEqual(info.local.address, '127.0.0.1'); + strictEqual(info.local.family, 'ipv4'); + strictEqual(typeof info.local.port, 'number'); + strictEqual(info.remote.address, '127.0.0.1'); + strictEqual(info.remote.family, 'ipv4'); + strictEqual(typeof info.remote.port, 'number'); + + // Local and remote ports should differ. + notStrictEqual(info.local.port, info.remote.port); + + // Servername matches the SNI. + strictEqual(info.servername, 'localhost'); + + // Protocol matches ALPN. + strictEqual(info.protocol, 'quic-test'); + + // cipher info. + strictEqual(typeof info.cipher, 'string'); + ok(info.cipher.length > 0); + strictEqual(info.cipherVersion, 'TLSv1.3'); + + serverSession.close(); + serverDone.resolve(); +})); + +const clientSession = await connect(serverEndpoint.address); +const clientInfo = await clientSession.opened; + +// Client sees its own local address and the server's remote. +strictEqual(clientInfo.local.address, '127.0.0.1'); +strictEqual(clientInfo.remote.address, '127.0.0.1'); +notStrictEqual(clientInfo.local.port, clientInfo.remote.port); + +// servername matches. +strictEqual(clientInfo.servername, 'localhost'); + +// Protocol matches ALPN. +strictEqual(clientInfo.protocol, 'quic-test'); + +// cipher info. +strictEqual(typeof clientInfo.cipher, 'string'); +ok(clientInfo.cipher.length > 0); +strictEqual(clientInfo.cipherVersion, 'TLSv1.3'); + +await Promise.all([serverDone.promise, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-opened-validation.mjs b/test/parallel/test-quic-session-opened-validation.mjs new file mode 100644 index 00000000000000..a73fd18734dbff --- /dev/null +++ b/test/parallel/test-quic-session-opened-validation.mjs @@ -0,0 +1,45 @@ +// Flags: --experimental-quic --no-warnings + +// Test: opened info includes cert validation error details. +// validationErrorReason populated on cert validation failure. +// validationErrorCode populated on cert validation failure. +// The test helper uses self-signed certs so validation always fails +// (unless rejectUnauthorized is explicitly set). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + const serverInfo = await serverSession.opened; + + // Server also sees validation info about the peer. + strictEqual(typeof serverInfo.validationErrorReason, 'string'); + strictEqual(typeof serverInfo.validationErrorCode, 'string'); + + serverSession.close(); +})); + +const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', +}); +const clientInfo = await clientSession.opened; + +// validationErrorReason is a non-empty string describing +// why the cert failed validation (self-signed cert). +strictEqual(typeof clientInfo.validationErrorReason, 'string'); +ok(clientInfo.validationErrorReason.length > 0); + +// validationErrorCode is the OpenSSL error code string. +strictEqual(typeof clientInfo.validationErrorCode, 'string'); +ok(clientInfo.validationErrorCode.length > 0); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-preferred-address-ignore.mjs b/test/parallel/test-quic-session-preferred-address-ignore.mjs new file mode 100644 index 00000000000000..f59f4a0dc814e4 --- /dev/null +++ b/test/parallel/test-quic-session-preferred-address-ignore.mjs @@ -0,0 +1,69 @@ +// Flags: --experimental-quic --no-warnings + +// Test: Create two listening endpoints, one secondary and one +// preferred. Initiate a connection with the secondary, with +// preferred advertised. Client should ignore the preferred +// address and continue on with the original + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const allStatusDone = Promise.withResolvers(); +const serverGot = Promise.withResolvers(); +let statusCount = 0; + +const handleSession = mustCall(async (serverSession) => { + await allStatusDone.promise; + await serverGot.promise; + await serverSession.close(); +}); + +const sessionOptions = { + ondatagram: mustCall((data) => { + serverGot.resolve(); + }, 4), + onpathvalidation: mustNotCall(), +}; + +const preferredEndpoint = await listen(handleSession, sessionOptions); +const serverEndpoint = await listen(handleSession, { + ...sessionOptions, + transportParams: { + preferredAddressIpv4: preferredEndpoint.address, + } +}); + +const clientSession = await connect(serverEndpoint.address, { + // We don't want this endpoint to reuse either of the two listening endpoints. + reuseEndpoint: false, + preferredAddressPolicy: 'ignore', + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagramstatus: mustCall((id, status) => { + if (++statusCount >= 4) allStatusDone.resolve(); + }, 4), + onpathvalidation: mustNotCall(), +}); +await clientSession.opened; + +// Send datagrams. +await clientSession.sendDatagram(new Uint8Array([1])); +await clientSession.sendDatagram(new Uint8Array([2])); +await clientSession.sendDatagram(new Uint8Array([3])); +await clientSession.sendDatagram(new Uint8Array([4])); + +await Promise.all([serverGot.promise, allStatusDone.promise]); + +strictEqual(clientSession.stats.datagramsSent, 4n); +ok(clientSession.stats.datagramsAcknowledged >= 1n); + +await clientSession.closed; +await serverEndpoint.close(); +await preferredEndpoint.close(); diff --git a/test/parallel/test-quic-session-preferred-address-ipv6.mjs b/test/parallel/test-quic-session-preferred-address-ipv6.mjs new file mode 100644 index 00000000000000..e9f23c3bf554d5 --- /dev/null +++ b/test/parallel/test-quic-session-preferred-address-ipv6.mjs @@ -0,0 +1,125 @@ +// Flags: --experimental-quic --no-warnings + +// Test: Create two listening ipv6 endpoints, one secondary and one +// preferred. Initiate a connection with the secondary, with +// preferred advertised. Client should automatically migrate +// to the preferred address without interupting data flow. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, strictEqual, notStrictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { createPrivateKey } = await import('node:crypto'); + +const allStatusDone = Promise.withResolvers(); +const serverGot = Promise.withResolvers(); +const serverPathValidated = Promise.withResolvers(); +let statusCount = 0; + +const handleSession = mustCall(async (serverSession) => { + await allStatusDone.promise; + await serverGot.promise; + await serverSession.close(); +}); + +function assertEqualAddress(addr1, addr2) { + strictEqual(addr1.address, addr2.address); + strictEqual(addr1.port, addr2.port); + strictEqual(addr1.family, addr2.family); +} + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const sessionOptions = { + ondatagram: mustCall((data) => { + serverGot.resolve(); + }, 4), + onpathvalidation: mustCall((result, newLocal, newRemote, oldLocal, oldRemote, preferred) => { + // The status here can be 'success' or 'aborted' depending on timing. + // The 'aborted' status only means that path validation is no longer + // necessary for a number of reasons (usually ngtcp2 received a non-probing + // packet on the new path). + notStrictEqual(result, 'failure'); + assertEqualAddress(newLocal, preferredEndpoint.address); + assertEqualAddress(oldLocal, serverEndpoint.address); + assertEqualAddress(newRemote, oldRemote); + // The preferred arg is only passed on client side + strictEqual(preferred, undefined); + serverPathValidated.resolve(); + }), + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + endpoint: { + address: { + address: '::1', + family: 'ipv6', + }, + ipv6Only: true, + }, +}; + +const preferredEndpoint = await listen(handleSession, sessionOptions); +const serverEndpoint = await listen(handleSession, { + ...sessionOptions, + transportParams: { + preferredAddressIpv6: preferredEndpoint.address, + } +}); + +console.log(preferredEndpoint.address); +console.log(serverEndpoint.address); + +const clientSession = await connect(serverEndpoint.address, { + // We don't want this endpoint to reuse either of the two listening endpoints. + reuseEndpoint: false, + preferredAddressPolicy: 'use', + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagramstatus: mustCall((id, status) => { + if (++statusCount >= 4) allStatusDone.resolve(); + }, 4), + onpathvalidation: mustCall((result, newLocal, newRemote, oldLocal, oldRemote, preferred) => { + strictEqual(result, 'success'); + assertEqualAddress(newLocal, clientSession.endpoint.address); + assertEqualAddress(newRemote, preferredEndpoint.address); + strictEqual(oldLocal, null); + strictEqual(oldRemote, null); + strictEqual(preferred, true); + }), + endpoint: { + address: { + address: '::', + family: 'ipv6', + }, + }, +}); +await clientSession.opened; + +// Send two datagrams. +await clientSession.sendDatagram(new Uint8Array([1])); +await clientSession.sendDatagram(new Uint8Array([2])); + +await serverPathValidated.promise; + +// Send more datagrams after the preferred address migration completes +// To show that data is still flowing after we close the original +// endpoint. +await clientSession.sendDatagram(new Uint8Array([3])); +await clientSession.sendDatagram(new Uint8Array([4])); + +await Promise.all([serverGot.promise, allStatusDone.promise]); + +strictEqual(clientSession.stats.datagramsSent, 4n); +ok(clientSession.stats.datagramsAcknowledged >= 1n); + +await clientSession.closed; +await serverEndpoint.close(); +await preferredEndpoint.close(); diff --git a/test/parallel/test-quic-session-preferred-address.mjs b/test/parallel/test-quic-session-preferred-address.mjs new file mode 100644 index 00000000000000..c4a55ee4d42b74 --- /dev/null +++ b/test/parallel/test-quic-session-preferred-address.mjs @@ -0,0 +1,103 @@ +// Flags: --experimental-quic --no-warnings + +// Test: preferred address migration. +// Two server endpoints: one initial, one preferred. The server +// advertises the preferred endpoint's address in transport params. +// After the handshake, the client migrates to the preferred address +// via path validation. Datagrams sent before migration take the +// original path; datagrams sent after take the preferred path. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual, notStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const allStatusDone = Promise.withResolvers(); +const serverGot = Promise.withResolvers(); +const serverPathValidated = Promise.withResolvers(); +let statusCount = 0; + +const handleSession = mustCall(async (serverSession) => { + await allStatusDone.promise; + await serverGot.promise; + await serverSession.close(); +}); + +function assertEqualAddress(addr1, addr2) { + strictEqual(addr1.address, addr2.address); + strictEqual(addr1.port, addr2.port); + strictEqual(addr1.family, addr2.family); +} + +const sessionOptions = { + ondatagram: mustCall((data) => { + serverGot.resolve(); + }, 4), + onpathvalidation: mustCall((result, newLocal, newRemote, oldLocal, oldRemote, preferred) => { + // The status here can be 'success' or 'aborted' depending on timing. + // The 'aborted' status only means that path validation is no longer + // necessary for a number of reasons (usually ngtcp2 received a non-probing + // packet on the new path). + notStrictEqual(result, 'failure'); + assertEqualAddress(newLocal, preferredEndpoint.address); + assertEqualAddress(oldLocal, serverEndpoint.address); + assertEqualAddress(newRemote, oldRemote); + // The preferred arg is only passed on client side + strictEqual(preferred, undefined); + serverPathValidated.resolve(); + }), +}; + +const preferredEndpoint = await listen(handleSession, sessionOptions); +const serverEndpoint = await listen(handleSession, { + ...sessionOptions, + transportParams: { + preferredAddressIpv4: preferredEndpoint.address, + } +}); + +const clientSession = await connect(serverEndpoint.address, { + // We don't want this endpoint to reuse either of the two listening endpoints. + reuseEndpoint: false, + preferredAddressPolicy: 'use', + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagramstatus: mustCall((id, status) => { + if (++statusCount >= 4) allStatusDone.resolve(); + }, 4), + onpathvalidation: mustCall((result, newLocal, newRemote, oldLocal, oldRemote, preferred) => { + strictEqual(result, 'success'); + assertEqualAddress(newLocal, clientSession.endpoint.address); + assertEqualAddress(newRemote, preferredEndpoint.address); + strictEqual(oldLocal, null); + strictEqual(oldRemote, null); + strictEqual(preferred, true); + }), +}); +await clientSession.opened; + +// Send two datagrams. +await clientSession.sendDatagram(new Uint8Array([1])); +await clientSession.sendDatagram(new Uint8Array([2])); + +await serverPathValidated.promise; + +// Send more datagrams after the preferred address migration completes +// To show that data is still flowing after we close the original +// endpoint. +await clientSession.sendDatagram(new Uint8Array([3])); +await clientSession.sendDatagram(new Uint8Array([4])); + +await Promise.all([serverGot.promise, allStatusDone.promise]); + +strictEqual(clientSession.stats.datagramsSent, 4n); +ok(clientSession.stats.datagramsAcknowledged >= 1n); + +await clientSession.closed; +await serverEndpoint.close(); +await preferredEndpoint.close(); diff --git a/test/parallel/test-quic-session-properties.mjs b/test/parallel/test-quic-session-properties.mjs new file mode 100644 index 00000000000000..9936301e53ea1f --- /dev/null +++ b/test/parallel/test-quic-session-properties.mjs @@ -0,0 +1,103 @@ +// Flags: --experimental-quic --no-warnings + +// Test: session properties (PATH-03, PATH-06, PATH-07, PATH-08, +// CERT-01, CERT-02, CERT-03, CERT-04, CERT-05). +// PATH-03/06: session.path returns { local, remote } with addresses. +// session.path is cached (same object on second access). +// session.path returns undefined after destroy. +// session.certificate returns own cert object. +// session.peerCertificate returns peer cert. +// session.ephemeralKeyInfo returns key info on client. +// All three cached. +// All three return undefined after destroy. + +import { hasQuic, skip, mustCall, hasCrypto } from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasCrypto) + skip('missing crypto'); + +const { X509Certificate } = await import('node:crypto'); + +// The QUIC test helpers configure both sides with the agent1 fixture cert, +const expectedCert = new X509Certificate(fixtures.readKey('agent1-cert.pem')); + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + + // PATH-03/06: Server path has local and remote. + const path = serverSession.path; + ok(path); + ok(path.local); + ok(path.remote); + + // Cached. + strictEqual(serverSession.path, path); + + // Own certificate. + const cert = serverSession.certificate; + ok(cert instanceof X509Certificate); + strictEqual(cert.subject, expectedCert.subject); + strictEqual(cert.issuer, expectedCert.issuer); + strictEqual(cert.fingerprint256, expectedCert.fingerprint256); + + // Peer certificate (client's cert — not set in this + // test since we don't use verifyClient, so it's undefined). + strictEqual(serverSession.peerCertificate, undefined); + + // Cached. + strictEqual(serverSession.certificate, cert); + + await serverSession.close(); + serverDone.resolve(); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// PATH-03/06: Client path. +const path = clientSession.path; +ok(path); +ok(path.local); +ok(path.remote); + +// Cached. +strictEqual(clientSession.path, path); + +// Peer certificate (server's cert). +const peerCert = clientSession.peerCertificate; +ok(peerCert instanceof X509Certificate); +strictEqual(peerCert.subject, expectedCert.subject); +strictEqual(peerCert.issuer, expectedCert.issuer); +strictEqual(peerCert.fingerprint256, expectedCert.fingerprint256); + +// Ephemeral key info (client only). +const keyInfo = clientSession.ephemeralKeyInfo; +ok(keyInfo); + +// Cached. +strictEqual(clientSession.peerCertificate, peerCert); +strictEqual(clientSession.ephemeralKeyInfo, keyInfo); + +await Promise.all([clientSession.closed, serverDone.promise]); + +// Returns undefined after destroy. +strictEqual(clientSession.path, undefined); + +// Returns undefined after destroy. +strictEqual(clientSession.certificate, undefined); +strictEqual(clientSession.peerCertificate, undefined); +strictEqual(clientSession.ephemeralKeyInfo, undefined); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-stats-datagram.mjs b/test/parallel/test-quic-session-stats-datagram.mjs new file mode 100644 index 00000000000000..7749e98e6a72d7 --- /dev/null +++ b/test/parallel/test-quic-session-stats-datagram.mjs @@ -0,0 +1,58 @@ +// Flags: --experimental-quic --no-warnings + +// Test: session datagram stats counters. +// After sending datagrams, the session stats should reflect +// datagramsSent, datagramsReceived, and datagramsAcknowledged. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const allStatusDone = Promise.withResolvers(); +const serverGot = Promise.withResolvers(); +let statusCount = 0; + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + // Wait for the client to receive all status updates before closing. + // The server must stay alive long enough to ACK the datagrams. + await allStatusDone.promise; + + // Server received datagrams. + ok(serverSession.stats.datagramsReceived > 0n); + + serverSession.close(); + await serverSession.closed; +}), { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagram: mustCall((data) => { + serverGot.resolve(); + }, 2), +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, + ondatagramstatus(id, status) { + if (++statusCount >= 2) allStatusDone.resolve(); + }, +}); +await clientSession.opened; + +// Send two datagrams. +await clientSession.sendDatagram(new Uint8Array([1])); +await clientSession.sendDatagram(new Uint8Array([2])); + +await Promise.all([serverGot.promise, allStatusDone.promise]); + +// Client sent datagrams. +strictEqual(clientSession.stats.datagramsSent, 2n); +ok(clientSession.stats.datagramsAcknowledged >= 1n); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-stats-detailed.mjs b/test/parallel/test-quic-session-stats-detailed.mjs new file mode 100644 index 00000000000000..8908543520e494 --- /dev/null +++ b/test/parallel/test-quic-session-stats-detailed.mjs @@ -0,0 +1,65 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: detailed session stats. +// RTT fields populated after data transfer. +// cwnd, bytesInFlight populated under load. +// V2 fields (pktSent, pktReceived, etc.) populated. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await bytes(stream); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Send enough data to generate meaningful stats. +const data = new Uint8Array(8192); +const stream = await clientSession.createBidirectionalStream(); +stream.setBody(data); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); + +const stats = clientSession.stats; + +// RTT fields populated. +ok(stats.smoothedRtt >= 0n, 'smoothedRtt should be >= 0'); +ok(stats.latestRtt >= 0n, 'latestRtt should be >= 0'); +ok(stats.minRtt >= 0n, 'minRtt should be >= 0'); +strictEqual(typeof stats.rttVar, 'bigint'); + +// Congestion fields. +ok(stats.cwnd > 0n, 'cwnd should be > 0'); +strictEqual(typeof stats.bytesInFlight, 'bigint'); +strictEqual(typeof stats.ssthresh, 'bigint'); + +// V2 packet/byte fields. +ok(stats.pktSent > 0n, 'pktSent should be > 0'); +ok(stats.pktRecv > 0n, 'pktRecv should be > 0'); +strictEqual(typeof stats.pktLost, 'bigint'); +ok(stats.bytesSent > 0n, 'bytesSent should be > 0'); +ok(stats.bytesRecv > 0n, 'bytesRecv should be > 0'); +strictEqual(typeof stats.bytesLost, 'bigint'); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-stats.mjs b/test/parallel/test-quic-session-stats.mjs new file mode 100644 index 00000000000000..cf65da46641fd0 --- /dev/null +++ b/test/parallel/test-quic-session-stats.mjs @@ -0,0 +1,72 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: session stats increment with data transfer and track streams +// bytesReceived/bytesSent increment after data transfer. +// bidiInStreamCount/bidiOutStreamCount track streams. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const payload = encoder.encode('hello stats world'); +const payloadLength = payload.byteLength; +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await bytes(stream); + strictEqual(data.byteLength, payloadLength); + stream.writer.endSync(); + await stream.closed; + + // Server sees one inbound bidi stream. + strictEqual(serverSession.stats.bidiInStreamCount, 1n); + strictEqual(serverSession.stats.bidiOutStreamCount, 0n); + + // Server received data bytes. + ok(serverSession.stats.bytesReceived > 0n); + ok(serverSession.stats.bytesSent > 0n); + + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Before sending, bytes should be from handshake only. +const bytesSentBefore = clientSession.stats.bytesSent; +ok(bytesSentBefore > 0n, 'handshake bytes should be counted'); + +const stream = await clientSession.createBidirectionalStream({ + body: payload, +}); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await stream.closed; +await serverDone.promise; + +// After sending, bytesSent should have increased. +ok(clientSession.stats.bytesSent > bytesSentBefore, + 'bytesSent should increase after data transfer'); +ok(clientSession.stats.bytesReceived > 0n); + +// Client opened one outbound bidi stream. +strictEqual(clientSession.stats.bidiOutStreamCount, 1n); +strictEqual(clientSession.stats.bidiInStreamCount, 0n); + +// Verify RTT fields are populated (connection was active). +ok(clientSession.stats.smoothedRtt > 0n); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-stream-lifecycle.mjs b/test/parallel/test-quic-session-stream-lifecycle.mjs index dcd9fa9987eae6..a4bd287cdc6dcd 100644 --- a/test/parallel/test-quic-session-stream-lifecycle.mjs +++ b/test/parallel/test-quic-session-stream-lifecycle.mjs @@ -3,6 +3,9 @@ import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { ok, strictEqual } = assert; if (!hasQuic) { skip('QUIC is not enabled'); @@ -12,87 +15,86 @@ if (!hasQuic) { const quic = await import('node:quic'); const { createPrivateKey } = await import('node:crypto'); -const keys = createPrivateKey(fixtures.readKey('agent1-key.pem')); -const certs = fixtures.readKey('agent1-cert.pem'); +const keys = createPrivateKey(readKey('agent1-key.pem')); +const certs = readKey('agent1-cert.pem'); const serverDone = Promise.withResolvers(); -const clientDone = Promise.withResolvers(); // Create a server endpoint -const serverEndpoint = await quic.listen(mustCall((serverSession) => { - serverSession.opened.then((info) => { - assert.ok(serverSession.endpoint !== null); - assert.strictEqual(serverSession.destroyed, false); - - const stats = serverSession.stats; - assert.strictEqual(stats.isConnected, true); - assert.ok(stats.handshakeCompletedAt > 0n); - assert.ok(stats.handshakeConfirmedAt > 0n); - assert.strictEqual(stats.closingAt, 0n); - - serverDone.resolve(); - serverSession.close(); - }).then(mustCall()); +const serverEndpoint = await quic.listen(mustCall(async (serverSession) => { + await serverSession.opened; + ok(serverSession.endpoint !== null); + strictEqual(serverSession.destroyed, false); + + const stats = serverSession.stats; + strictEqual(stats.isConnected, true); + ok(stats.handshakeCompletedAt > 0n); + ok(stats.handshakeConfirmedAt > 0n); + strictEqual(stats.closingAt, 0n); + + serverDone.resolve(); + serverSession.close(); }), { sni: { '*': { keys, certs } } }); -assert.strictEqual(serverEndpoint.busy, false); -assert.strictEqual(serverEndpoint.closing, false); -assert.strictEqual(serverEndpoint.destroyed, false); -assert.strictEqual(serverEndpoint.listening, true); +strictEqual(serverEndpoint.busy, false); +strictEqual(serverEndpoint.closing, false); +strictEqual(serverEndpoint.destroyed, false); +strictEqual(serverEndpoint.listening, true); -assert.ok(serverEndpoint.address !== undefined); -assert.strictEqual(serverEndpoint.address.family, 'ipv4'); -assert.strictEqual(serverEndpoint.address.address, '127.0.0.1'); -assert.ok(typeof serverEndpoint.address.port === 'number'); -assert.ok(serverEndpoint.address.port > 0); +ok(serverEndpoint.address !== undefined); +strictEqual(serverEndpoint.address.family, 'ipv4'); +strictEqual(serverEndpoint.address.address, '127.0.0.1'); +ok(typeof serverEndpoint.address.port === 'number'); +ok(serverEndpoint.address.port > 0); const epStats = serverEndpoint.stats; -assert.strictEqual(epStats.isConnected, true); -assert.ok(epStats.createdAt > 0n); +strictEqual(epStats.isConnected, true); +ok(epStats.createdAt > 0n); // Connect with a client -const clientSession = await quic.connect(serverEndpoint.address); - -assert.strictEqual(clientSession.destroyed, false); -assert.ok(clientSession.endpoint !== null); -assert.strictEqual(clientSession.stats.isConnected, true); +const clientSession = await quic.connect(serverEndpoint.address, { + verifyPeer: 'manual', +}); -clientSession.opened.then((clientInfo) => { - assert.strictEqual(clientInfo.servername, 'localhost'); - assert.strictEqual(clientInfo.protocol, 'h3'); - assert.strictEqual(clientInfo.cipherVersion, 'TLSv1.3'); - assert.ok(clientInfo.local !== undefined); - assert.ok(clientInfo.remote !== undefined); +strictEqual(clientSession.destroyed, false); +ok(clientSession.endpoint !== null); +strictEqual(clientSession.stats.isConnected, true); - const cStats = clientSession.stats; - assert.strictEqual(cStats.isConnected, true); - assert.ok(cStats.handshakeCompletedAt > 0n); - assert.ok(cStats.bytesSent > 0n, 'Expected bytesSent > 0 after handshake'); +const clientInfo = await clientSession.opened; +strictEqual(clientInfo.servername, 'localhost'); +strictEqual(clientInfo.protocol, 'h3'); +strictEqual(clientInfo.cipherVersion, 'TLSv1.3'); +ok(clientInfo.local !== undefined); +ok(clientInfo.remote !== undefined); - clientDone.resolve(); -}).then(mustCall()); +const cStats = clientSession.stats; +strictEqual(cStats.isConnected, true); +ok(cStats.handshakeCompletedAt > 0n); +ok(cStats.bytesSent > 0n, 'Expected bytesSent > 0 after handshake'); -await Promise.all([serverDone.promise, clientDone.promise]); +await serverDone.promise; // Open a bidirectional stream. const stream = await clientSession.createBidirectionalStream(); -assert.strictEqual(stream.destroyed, false); -assert.strictEqual(stream.direction, 'bidi'); -assert.strictEqual(stream.session, clientSession); -assert.ok(stream.id !== null, 'Non-pending stream should have an id'); -assert.strictEqual(typeof stream.id, 'bigint'); -assert.strictEqual(stream.pending, false); -assert.strictEqual(stream.stats.isConnected, true); -assert.ok(stream.readable instanceof ReadableStream); +strictEqual(stream.destroyed, false); +strictEqual(stream.direction, 'bidi'); +strictEqual(stream.session, clientSession); +ok(stream.id !== null, 'Non-pending stream should have an id'); +strictEqual(typeof stream.id, 'bigint'); +strictEqual(stream.pending, false); +strictEqual(stream.stats.isConnected, true); // Destroying the session should destroy it and the stream, and clear its properties. clientSession.destroy(); -assert.strictEqual(clientSession.destroyed, true); -assert.strictEqual(clientSession.endpoint, null); -assert.strictEqual(clientSession.stats.isConnected, false); - -assert.strictEqual(stream.destroyed, true); -assert.strictEqual(stream.session, null); -assert.strictEqual(stream.id, null); -assert.strictEqual(stream.direction, null); +strictEqual(clientSession.destroyed, true); +strictEqual(clientSession.endpoint, null); +strictEqual(clientSession.stats.isConnected, false); + +strictEqual(stream.destroyed, true); + +// The stream id and direction should still be available after destruction +strictEqual(stream.id, 0n); +strictEqual(stream.direction, 'bidi'); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-transport-params.mjs b/test/parallel/test-quic-session-transport-params.mjs new file mode 100644 index 00000000000000..cd0d51838d24d0 --- /dev/null +++ b/test/parallel/test-quic-session-transport-params.mjs @@ -0,0 +1,139 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: session.localTransportParams and session.remoteTransportParams +// Verifies that local transport params are available immediately after +// session creation, remote transport params are available after the +// handshake completes, and values match what was configured. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverTransportParams = { + initialMaxStreamsBidi: 200, + initialMaxData: 2 * 1024 * 1024, +}; + +const clientTransportParams = { + initialMaxStreamsBidi: 500, + initialMaxData: 8 * 1024 * 1024, +}; + +const serverDone = Promise.withResolvers(); +let serverLocalParams; +let serverRemoteParams; + +const serverEndpoint = await listen(mustCall((serverSession) => { + // localTransportParams should be available immediately. + serverLocalParams = serverSession.localTransportParams; + ok(serverLocalParams != null, 'server localTransportParams should be available immediately'); + strictEqual(typeof serverLocalParams, 'object'); + strictEqual(Object.getPrototypeOf(serverLocalParams), null); + + // Verify server's configured values are reflected. + strictEqual(serverLocalParams.initialMaxStreamsBidi, + BigInt(serverTransportParams.initialMaxStreamsBidi)); + strictEqual(serverLocalParams.initialMaxData, + BigInt(serverTransportParams.initialMaxData)); + + // Verify defaults are present and reasonable. + ok(serverLocalParams.activeConnectionIDLimit > 0n, + 'activeConnectionIDLimit should be positive'); + ok(serverLocalParams.ackDelayExponent > 0n, + 'ackDelayExponent should be positive'); + ok(serverLocalParams.maxAckDelay > 0n, + 'maxAckDelay should be positive'); + strictEqual(serverLocalParams.disableActiveMigration, false); + + serverSession.onstream = mustCall(async (stream) => { + // After the stream arrives, the handshake is complete and + // remoteTransportParams should be available. + serverRemoteParams = serverSession.remoteTransportParams; + ok(serverRemoteParams != null, + 'server remoteTransportParams should be available after handshake'); + strictEqual(typeof serverRemoteParams, 'object'); + strictEqual(Object.getPrototypeOf(serverRemoteParams), null); + + // Remote params should reflect the client's configured values. + strictEqual(serverRemoteParams.initialMaxStreamsBidi, + BigInt(clientTransportParams.initialMaxStreamsBidi)); + strictEqual(serverRemoteParams.initialMaxData, + BigInt(clientTransportParams.initialMaxData)); + + // CID fields should be present. + strictEqual(typeof serverRemoteParams.initialSCID, 'string'); + ok(serverRemoteParams.initialSCID.length > 0, + 'remote initialSCID should be non-empty'); + + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + transportParams: serverTransportParams, +}); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: clientTransportParams, +}); +await clientSession.opened; + +// After opened, the handshake is complete. Both local and remote +// transport params should be available on the client session. +const clientLocalParams = clientSession.localTransportParams; +ok(clientLocalParams != null, 'client localTransportParams should be available'); +strictEqual(typeof clientLocalParams, 'object'); +strictEqual(Object.getPrototypeOf(clientLocalParams), null); + +// Verify client's configured values. +strictEqual(clientLocalParams.initialMaxStreamsBidi, + BigInt(clientTransportParams.initialMaxStreamsBidi)); +strictEqual(clientLocalParams.initialMaxData, + BigInt(clientTransportParams.initialMaxData)); + +const clientRemoteParams = clientSession.remoteTransportParams; +ok(clientRemoteParams != null, + 'client remoteTransportParams should be available after handshake'); +strictEqual(typeof clientRemoteParams, 'object'); +strictEqual(Object.getPrototypeOf(clientRemoteParams), null); + +// Remote params should reflect the server's configured values. +strictEqual(clientRemoteParams.initialMaxStreamsBidi, + BigInt(serverTransportParams.initialMaxStreamsBidi)); +strictEqual(clientRemoteParams.initialMaxData, + BigInt(serverTransportParams.initialMaxData)); + +// CID fields should be present on the client's view of server params. +strictEqual(typeof clientRemoteParams.initialSCID, 'string'); +ok(clientRemoteParams.initialSCID.length > 0, + 'remote initialSCID should be non-empty'); + +// Cross-validation: server's remote matches client's local and vice versa. +const stream = await clientSession.createBidirectionalStream(); +stream.writer.endSync(); + +// eslint-disable-next-line no-unused-vars +for await (const _ of stream) { /* drain */ } +await stream.closed; +await serverDone.promise; + +// Cross-validate after both sides have captured params. +strictEqual(serverRemoteParams.initialMaxStreamsBidi, + clientLocalParams.initialMaxStreamsBidi); +strictEqual(serverRemoteParams.initialMaxData, + clientLocalParams.initialMaxData); +strictEqual(clientRemoteParams.initialMaxStreamsBidi, + serverLocalParams.initialMaxStreamsBidi); +strictEqual(clientRemoteParams.initialMaxData, + serverLocalParams.initialMaxData); + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-shared-endpoint-stream-close.mjs b/test/parallel/test-quic-shared-endpoint-stream-close.mjs new file mode 100644 index 00000000000000..19e530e50b9ec0 --- /dev/null +++ b/test/parallel/test-quic-shared-endpoint-stream-close.mjs @@ -0,0 +1,94 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Regression test: when a client QuicEndpoint has a session terminated +// by a stateless reset, subsequent sessions on the same endpoint must +// be able to complete their stream close handshake. +// Without the fix, the server-side stream.closed for session 2 never +// resolves and the test hangs. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); +const { QuicEndpoint } = await import('node:quic'); + +const encoder = new TextEncoder(); + +let sessionCount = 0; +const serverDone1 = Promise.withResolvers(); +const serverDone2 = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + sessionCount++; + const which = sessionCount; + + serverSession.onstream = mustCall(async (stream) => { + const data = await bytes(stream); + assert.ok(data.byteLength > 0); + stream.writer.endSync(); + // For session 2 this hangs when the bug is present — the server + // never receives the client's ACK for its FIN. + await stream.closed; + + if (which === 1) serverSession.destroy(); + (which === 1 ? serverDone1 : serverDone2).resolve(); + }); +}, 2), { + onerror(err) { /* marks promises as handled */ }, +}); + +// Both sessions share one endpoint — same source UDP address. +const clientEndpoint = new QuicEndpoint(); + +// Session 1: complete a full round-trip, then the server destroys +// without sending CONNECTION_CLOSE. The client sends a packet to the +// now-unknown DCID, which causes the server to send a stateless reset. +// The client receives the stateless reset and closes session 1. +const client1 = await connect(serverEndpoint.address, { + endpoint: clientEndpoint, + verifyPeer: 'manual', + onerror: mustCall((err) => { assert.ok(err); }), +}); +await client1.opened; + +const s1 = await client1.createBidirectionalStream({ + body: encoder.encode('session1'), +}); +for await (const _ of s1) { /* drain */ } // eslint-disable-line no-unused-vars +await s1.closed; + +await serverDone1.promise; + +// Trigger the stateless reset. +// eslint-disable-next-line no-unused-vars +const s1b = await client1.createBidirectionalStream({ + body: encoder.encode('trigger'), +}); +await assert.rejects(client1.closed, { code: 'ERR_QUIC_TRANSPORT_ERROR' }); + +// Session 2: uses the same endpoint as session 1. The bug manifests +// as serverDone2 never resolving because the server's stream.closed +// for session 2 hangs. +const client2 = await connect(serverEndpoint.address, { + endpoint: clientEndpoint, + verifyPeer: 'manual', + onerror(err) { /* marks promises as handled */ }, +}); +await client2.opened; + +const s2 = await client2.createBidirectionalStream({ + body: encoder.encode('session2'), +}); +for await (const _ of s2) { /* drain */ } // eslint-disable-line no-unused-vars +await s2.closed; + +// If the bug is present, this never resolves and the test hangs. +await serverDone2.promise; + +await serverEndpoint.close(); +await clientEndpoint.close(); diff --git a/test/parallel/test-quic-sni-mismatch.mjs b/test/parallel/test-quic-sni-mismatch.mjs new file mode 100644 index 00000000000000..dbb2de4c011a30 --- /dev/null +++ b/test/parallel/test-quic-sni-mismatch.mjs @@ -0,0 +1,62 @@ +// Flags: --experimental-quic --no-warnings + +// Test: SNI mismatch. +// Client connects with a servername that doesn't match any SNI entry +// and no wildcard is configured. The handshake should fail with a +// TLS alert (unrecognized_name). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { rejects, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +// Server only has an entry for 'specific.example.com', no wildcard. +// Connections to any other hostname will be rejected at the TLS level. +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await rejects(serverSession.opened, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); + await rejects(serverSession.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); +}), { + sni: { 'specific.example.com': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + transportParams: { maxIdleTimeout: 1 }, + onerror: mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + }), +}); + +// Client connects with a different servername — no matching identity. +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + servername: 'wrong.example.com', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + onerror: mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + }), +}); + +await rejects(clientSession.opened, { + code: 'ERR_QUIC_TRANSPORT_ERROR', +}); + +await rejects(clientSession.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', +}); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-sni-multi-entry.mjs b/test/parallel/test-quic-sni-multi-entry.mjs new file mode 100644 index 00000000000000..2348c844fc96d6 --- /dev/null +++ b/test/parallel/test-quic-sni-multi-entry.mjs @@ -0,0 +1,84 @@ +// Flags: --experimental-quic --no-warnings + +// Test: SNI with multiple entries. +// Server has 3+ SNI entries. Different servername values should +// negotiate successfully using the correct identity. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key1 = createPrivateKey(readKey('agent1-key.pem')); +const cert1 = readKey('agent1-cert.pem'); +const key2 = createPrivateKey(readKey('agent2-key.pem')); +const cert2 = readKey('agent2-cert.pem'); +const key3 = createPrivateKey(readKey('agent3-key.pem')); +const cert3 = readKey('agent3-cert.pem'); + +let sessionCount = 0; +const allDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + const info = await serverSession.opened; + // Each client should negotiate with the correct servername. + strictEqual(typeof info.servername, 'string'); + serverSession.close(); + await serverSession.closed; + if (++sessionCount === 3) allDone.resolve(); +}, 3), { + sni: { + 'host1.example.com': { keys: [key1], certs: [cert1] }, + 'host2.example.com': { keys: [key2], certs: [cert2] }, + '*': { keys: [key3], certs: [cert3] }, + }, + alpn: ['quic-test'], +}); + +// Client 1: connects with servername 'host1.example.com'. +{ + const cs = await connect(serverEndpoint.address, { + servername: 'host1.example.com', + verifyPeer: 'manual', + alpn: 'quic-test', + }); + const info = await cs.opened; + strictEqual(info.servername, 'host1.example.com'); + await cs.closed; +} + +// Client 2: connects with servername 'host2.example.com'. +{ + const cs = await connect(serverEndpoint.address, { + servername: 'host2.example.com', + verifyPeer: 'manual', + alpn: 'quic-test', + }); + const info = await cs.opened; + strictEqual(info.servername, 'host2.example.com'); + await cs.closed; +} + +// Client 3: connects with servername 'unknown.example.com' → wildcard. +{ + const cs = await connect(serverEndpoint.address, { + servername: 'unknown.example.com', + verifyPeer: 'manual', + alpn: 'quic-test', + }); + const info = await cs.opened; + assert.strictEqual(info.servername, 'unknown.example.com'); + await cs.closed; +} + +await allDone.promise; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-sni-setcontexts.mjs b/test/parallel/test-quic-sni-setcontexts.mjs new file mode 100644 index 00000000000000..5ad665cdeac960 --- /dev/null +++ b/test/parallel/test-quic-sni-setcontexts.mjs @@ -0,0 +1,74 @@ +// Flags: --experimental-quic --no-warnings + +// Test: setSNIContexts hot-swap and options. +// setSNIContexts() updates TLS identities at runtime. +// setSNIContexts() with replace: true replaces all entries. +// setSNIContexts() with replace: false merges new entries. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect, QuicEndpoint } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key1 = createPrivateKey(readKey('agent1-key.pem')); +const cert1 = readKey('agent1-cert.pem'); +const key2 = createPrivateKey(readKey('agent2-key.pem')); +const cert2 = readKey('agent2-cert.pem'); + +const endpoint = new QuicEndpoint(); + +// Start with agent1 cert for all hosts. +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + serverSession.close(); + await serverSession.closed; +}, 2), { + endpoint, + sni: { '*': { keys: [key1], certs: [cert1] } }, + alpn: ['quic-test'], + transportParams: { maxIdleTimeout: 2 }, +}); + +// First connection uses agent1 cert. +{ + const cs = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 2 }, + }); + const info = await cs.opened; + strictEqual(info.servername, 'localhost'); + await cs.closed; +} + +endpoint.setSNIContexts( + { '*': { keys: [key2], certs: [cert2] } }, + { replace: true }, +); + +// Second connection should use agent2 cert. +{ + const cs = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 2 }, + }); + const info = await cs.opened; + strictEqual(info.servername, 'localhost'); + // The cert changed — we can verify by checking the connection succeeded + // (if the old cert was still used and the new one was expected, the + // handshake would still succeed since both are self-signed and + // rejectUnauthorized defaults to false in the test helper). + await cs.closed; +} + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-sni.mjs b/test/parallel/test-quic-sni.mjs index b2fe9968eee746..fe93260aceb1f5 100644 --- a/test/parallel/test-quic-sni.mjs +++ b/test/parallel/test-quic-sni.mjs @@ -4,6 +4,9 @@ import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; import * as fixtures from '../common/fixtures.mjs'; +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + if (!hasQuic) { skip('QUIC is not enabled'); } @@ -12,22 +15,17 @@ const { listen, connect } = await import('node:quic'); const { createPrivateKey } = await import('node:crypto'); // Use two different keys/certs for the default and SNI host. -const defaultKey = createPrivateKey(fixtures.readKey('agent1-key.pem')); -const defaultCert = fixtures.readKey('agent1-cert.pem'); -const sniKey = createPrivateKey(fixtures.readKey('agent2-key.pem')); -const sniCert = fixtures.readKey('agent2-cert.pem'); +const defaultKey = createPrivateKey(readKey('agent1-key.pem')); +const defaultCert = readKey('agent1-cert.pem'); +const sniKey = createPrivateKey(readKey('agent2-key.pem')); +const sniCert = readKey('agent2-cert.pem'); // Server with SNI: default ('*') uses agent1, 'localhost' uses agent2. -const serverOpened = Promise.withResolvers(); -const clientOpened = Promise.withResolvers(); - -const serverEndpoint = await listen(mustCall((serverSession) => { - serverSession.opened.then((info) => { - // The server should see the client's requested servername. - assert.strictEqual(info.servername, 'localhost'); - serverOpened.resolve(); - serverSession.close(); - }).then(mustCall()); +const serverEndpoint = await listen(mustCall(async (serverSession) => { + const info = await serverSession.opened; + // The server should see the client's requested servername. + strictEqual(info.servername, 'localhost'); + await serverSession.close(); }), { sni: { '*': { keys: [defaultKey], certs: [defaultCert] }, @@ -36,17 +34,16 @@ const serverEndpoint = await listen(mustCall((serverSession) => { alpn: ['quic-test'], }); -assert.ok(serverEndpoint.address !== undefined); +ok(serverEndpoint.address !== undefined); // Client connects with servername 'localhost' — should match the SNI entry. const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', alpn: 'quic-test', }); -clientSession.opened.then((info) => { - assert.strictEqual(info.servername, 'localhost'); - clientOpened.resolve(); -}).then(mustCall()); +const clientInfo = await clientSession.opened; +strictEqual(clientInfo.servername, 'localhost'); -await Promise.all([serverOpened.promise, clientOpened.promise]); -clientSession.close(); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stateless-reset.mjs b/test/parallel/test-quic-stateless-reset.mjs new file mode 100644 index 00000000000000..3af4fcc75dfddf --- /dev/null +++ b/test/parallel/test-quic-stateless-reset.mjs @@ -0,0 +1,236 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stateless reset. +// When the server loses session state and the client sends +// data, the server sends a stateless reset and the client +// session closes. +// When disableStatelessReset is true, the server does NOT +// send a stateless reset. +// Global token bucket rate limits the total number of resets. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); + +// Stateless reset received closes session. +{ + const serverDestroyed = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Do a complete data exchange first so both sides are + // fully at 1-RTT with all ACKs exchanged. + const data = await bytes(stream); + ok(data.byteLength > 0); + stream.writer.endSync(); + await stream.closed; + + // Now forcefully destroy the server session WITHOUT sending + // CONNECTION_CLOSE. The client doesn't know the session + // is gone. + serverSession.destroy(); + serverDestroyed.resolve(); + }); + }), { + onerror(err) { ok(err); }, + }); + + const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, + verifyPeer: 'manual', + onerror: mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + }), + }); + await clientSession.opened; + + // First exchange: complete round-trip to confirm 1-RTT. + const stream1 = await clientSession.createBidirectionalStream({ + body: encoder.encode('hello'), + }); + for await (const _ of stream1) { /* drain */ } // eslint-disable-line no-unused-vars + await stream1.closed; + + // Wait for the server to destroy. + await serverDestroyed.promise; + + // Open a second stream — this sends a short header (1-RTT) packet + // to the server. The server endpoint doesn't recognize the DCID + // and should send a stateless reset. + // eslint-disable-next-line no-unused-vars + const stream2 = await clientSession.createBidirectionalStream({ + body: encoder.encode('after destroy'), + }); + + // The client session should be closed by the stateless reset. + await rejects(clientSession.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); + + ok(serverEndpoint.stats.statelessResetCount > 0n, + 'Server should have sent a stateless reset'); + + await serverEndpoint.close(); +} + +// disableStatelessReset prevents the server from sending resets. +{ + const serverDestroyed = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await bytes(stream); + ok(data.byteLength > 0); + stream.writer.endSync(); + await stream.closed; + + serverSession.destroy(); + serverDestroyed.resolve(); + }); + }), { + endpoint: { disableStatelessReset: true }, + onerror(err) { ok(err); }, + }); + + const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, + verifyPeer: 'manual', + // Short idle timeout so the client doesn't hang waiting for + // a stateless reset that will never arrive. + transportParams: { maxIdleTimeout: 1 }, + // Onerror marks stream closed promises as handled so that the + // idle-timeout stream destruction doesn't cause unhandled rejections. + onerror(err) { ok(err); }, + }); + await clientSession.opened; + + const stream1 = await clientSession.createBidirectionalStream({ + body: encoder.encode('hello'), + }); + for await (const _ of stream1) { /* drain */ } // eslint-disable-line no-unused-vars + await stream1.closed; + + await serverDestroyed.promise; + + // Send a packet after the server session is destroyed. The server + // endpoint silently drops the packet (stateless reset disabled). + // eslint-disable-next-line no-unused-vars + const stream2 = await clientSession.createBidirectionalStream({ + body: encoder.encode('after destroy'), + }); + + // The client should NOT receive a stateless reset. It will close + // via idle timeout instead. + await clientSession.closed; + + strictEqual(serverEndpoint.stats.statelessResetCount, 0n, + 'No stateless reset should have been sent'); + + await serverEndpoint.close(); +} + +// Global token bucket rate limits stateless resets. +// With burst=1 and rate=0, only one reset can be sent. +{ + let sessionCount = 0; + const serverDestroyed1 = Promise.withResolvers(); + const serverDestroyed2 = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + sessionCount++; + const which = sessionCount; + const deferred = which === 1 ? serverDestroyed1 : serverDestroyed2; + + serverSession.onstream = mustCall(async (stream) => { + const data = await bytes(stream); + ok(data.byteLength > 0); + stream.writer.endSync(); + await stream.closed; + + serverSession.destroy(); + deferred.resolve(); + }); + }, 2), { + endpoint: { statelessResetBurst: 1, statelessResetRate: 0 }, + onerror(err) { ok(err); }, + }); + + // The global token bucket rate limiter applies regardless of + // client source address. + const { QuicEndpoint } = await import('node:quic'); + const clientEndpoint = new QuicEndpoint(); + + // --- First session: triggers a stateless reset --- + + const client1 = await connect(serverEndpoint.address, { + endpoint: clientEndpoint, + verifyPeer: 'manual', + onerror: mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + }), + }); + await client1.opened; + + // Send data so the server onstream fires and destroys the session. + await client1.createBidirectionalStream({ + body: encoder.encode('session1'), + }); + await serverDestroyed1.promise; + + // Send a packet to trigger stateless reset. + // eslint-disable-next-line no-unused-vars + const s1b = await client1.createBidirectionalStream({ + body: encoder.encode('after destroy 1'), + }); + await rejects(client1.closed, { code: 'ERR_QUIC_TRANSPORT_ERROR' }); + + strictEqual(serverEndpoint.stats.statelessResetCount, 1n, + 'First reset should have been sent'); + + // --- Second session: rate-limited, no reset sent --- + + const client2 = await connect(serverEndpoint.address, { + endpoint: clientEndpoint, + verifyPeer: 'manual', + // Short idle timeout so the client closes after the server + // destroys (no stateless reset will arrive, rate-limited). + transportParams: { maxIdleTimeout: 1 }, + // Onerror marks stream closed promises as handled. + onerror(err) { ok(err); }, + }); + await client2.opened; + + // Send data so the server onstream fires and destroys the session. + await client2.createBidirectionalStream({ + body: encoder.encode('session2'), + }); + await serverDestroyed2.promise; + + // Send a packet — the server would normally send a stateless reset, + // but the global rate limit (burst of 1) is already exhausted. + // eslint-disable-next-line no-unused-vars + const s2b = await client2.createBidirectionalStream({ + body: encoder.encode('after destroy 2'), + }); + + // The client closes via idle timeout (no stateless reset). + await client2.closed; + + strictEqual(serverEndpoint.stats.statelessResetCount, 1n, + 'Second reset should have been rate-limited'); + ok(serverEndpoint.stats.statelessResetRateLimited > 0n, + 'Rate-limited counter should be non-zero'); + + await clientEndpoint.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-stats-tojson-inspect.mjs b/test/parallel/test-quic-stats-tojson-inspect.mjs new file mode 100644 index 00000000000000..860b02c2ed09a0 --- /dev/null +++ b/test/parallel/test-quic-stats-tojson-inspect.mjs @@ -0,0 +1,67 @@ +// Flags: --experimental-quic --no-warnings + +// Test: toJSON() and inspect() on stats objects. +// Verifies that stats objects from endpoints and sessions +// support toJSON() and util.inspect(). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { inspect } from 'node:util'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + // Session stats toJSON and inspect. + const sessionStatsJson = serverSession.stats.toJSON(); + ok(sessionStatsJson); + strictEqual(typeof sessionStatsJson.createdAt, 'string'); + strictEqual(typeof sessionStatsJson.bytesSent, 'string'); + + const sessionStatsInspect = inspect(serverSession.stats); + ok(sessionStatsInspect.includes('QuicSessionStats')); + + serverSession.onstream = mustCall(async (stream) => { + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +// Endpoint stats toJSON and inspect. +const endpointStatsJson = serverEndpoint.stats.toJSON(); +ok(endpointStatsJson); +strictEqual(typeof endpointStatsJson.createdAt, 'string'); + +const endpointStatsInspect = inspect(serverEndpoint.stats); +ok(endpointStatsInspect.includes('QuicEndpointStats')); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Client session stats. +const clientStatsJson = clientSession.stats.toJSON(); +ok(clientStatsJson); +strictEqual(typeof clientStatsJson.createdAt, 'string'); + +const clientStatsInspect = inspect(clientSession.stats); +ok(clientStatsInspect.includes('QuicSessionStats')); + +const stream = await clientSession.createBidirectionalStream({ + body: new TextEncoder().encode('test'), +}); +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + +await Promise.all([stream.closed, serverDone.promise]); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-bidi-basic.mjs b/test/parallel/test-quic-stream-bidi-basic.mjs new file mode 100644 index 00000000000000..de71890e888ac9 --- /dev/null +++ b/test/parallel/test-quic-stream-bidi-basic.mjs @@ -0,0 +1,60 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: basic bidirectional stream data transfer. +// The client creates a bidi stream with a fixed body. The server reads the +// data via async iteration (using stream/iter bytes()), verifies integrity, +// then closes its write side of the stream. Both sides await stream.closed +// to ensure the stream is fully acknowledged before the session is torn down. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { deepStrictEqual, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const message = 'Hello from the client'; +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +// Keep a separate copy for comparison — the body passed to +// createBidirectionalStream will have its ArrayBuffer transferred. +const body = encoder.encode(message); +const expected = encoder.encode(message); + +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + + deepStrictEqual(received, expected); + strictEqual(decoder.decode(received), message); + + // Close the server's write side of the bidi stream (FIN with no data) + // so the stream is fully closed on both directions. + stream.writer.endSync(); + + // Wait for the stream to be fully closed before closing the session. + await stream.closed; + serverSession.close(); + done.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Create a bidi stream with the message as the body. +// For DefaultApplication, the server's onstream fires when data arrives. +const stream = await clientSession.createBidirectionalStream({ + body: body, +}); + +await Promise.all([stream.closed, done.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-bidi-concurrent.mjs b/test/parallel/test-quic-stream-bidi-concurrent.mjs new file mode 100644 index 00000000000000..240f377d7d5513 --- /dev/null +++ b/test/parallel/test-quic-stream-bidi-concurrent.mjs @@ -0,0 +1,65 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: multiple concurrent bidirectional streams on a single session. +// The client opens several bidi streams in parallel, each sending a +// distinct message. The server reads each stream independently and +// verifies data integrity. All streams and the session are closed +// cleanly after all transfers complete. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const numStreams = 5; + +const messages = Array.from({ length: numStreams }, + (_, i) => `message from stream ${i}`); + +let serverStreamsReceived = 0; +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + const text = decoder.decode(received); + + // Verify it's one of the expected messages. + ok(messages.includes(text), + `Unexpected message: ${text}`); + + stream.writer.endSync(); + await stream.closed; + + serverStreamsReceived++; + if (serverStreamsReceived === numStreams) { + serverSession.close(); + done.resolve(); + } + }, numStreams); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Open all streams concurrently. +const clientStreams = await Promise.all( + messages.map((msg) => + clientSession.createBidirectionalStream({ + body: encoder.encode(msg), + }), + ), +); + +await Promise.all([done.promise, ...clientStreams.map((s) => s.closed)]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-bidi-echo.mjs b/test/parallel/test-quic-stream-bidi-echo.mjs new file mode 100644 index 00000000000000..dbc82d16646f26 --- /dev/null +++ b/test/parallel/test-quic-stream-bidi-echo.mjs @@ -0,0 +1,54 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: bidirectional stream echo. +// The client sends a message, the server reads it and echoes it back. +// Both directions of the bidi stream carry data and are properly FIN'd. +// Verifies that both client and server can read and write on the same stream. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const message = 'ping from client'; +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Read client's data. + const received = await bytes(stream); + + // Echo it back and close the write side. + const w = stream.writer; + w.writeSync(received); + w.endSync(); + + await stream.closed; + serverSession.close(); + done.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const body = encoder.encode(message); +const stream = await clientSession.createBidirectionalStream({ body }); + +// Read the echoed response from the server. +const echoed = await bytes(stream); +strictEqual(decoder.decode(echoed), message); + +await Promise.all([stream.closed, done.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-bidi-halfclose.mjs b/test/parallel/test-quic-stream-bidi-halfclose.mjs new file mode 100644 index 00000000000000..5f94d281355d2b --- /dev/null +++ b/test/parallel/test-quic-stream-bidi-halfclose.mjs @@ -0,0 +1,60 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: half-close on a bidirectional stream. +// The client sends a body and closes its write side (FIN). While the +// client's writable side is closed, the server's writable side remains +// open. The server reads all client data, then sends a response back. +// The client reads the server's response and verifies both payloads. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const clientMessage = 'request from client'; +const serverMessage = 'response from server'; + +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Read the client's data (client has already sent FIN). + const received = await bytes(stream); + strictEqual(decoder.decode(received), clientMessage); + + // The server's writable side is still open. Send a response. + const w = stream.writer; + w.writeSync(encoder.encode(serverMessage)); + w.endSync(); + + await stream.closed; + serverSession.close(); + done.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Create a stream with a body -- this sends FIN after the body. +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode(clientMessage), +}); + +// The client's writable side is closed (FIN sent with body), but +// the readable side is still open. Read the server's response. +const response = await bytes(stream); +strictEqual(decoder.decode(response), serverMessage); + +await Promise.all([stream.closed, done.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-bidi-large.mjs b/test/parallel/test-quic-stream-bidi-large.mjs new file mode 100644 index 00000000000000..329dc519bd702a --- /dev/null +++ b/test/parallel/test-quic-stream-bidi-large.mjs @@ -0,0 +1,88 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: large bidirectional data transfer with backpressure. +// The client sends >1MB of data using the writer API, exercising the +// QUIC flow control path. The server reads all data and verifies the +// total byte count and a checksum. This tests that backpressure is +// correctly applied and released across the full transfer. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes, drainableProtocol: dp } = await import('stream/iter'); + +// 1.5 MB payload — large enough to trigger flow control. +const totalSize = 1.5 * 1024 * 1024; +const chunkSize = 16 * 1024; +const numChunks = Math.ceil(totalSize / chunkSize); + +// Build a deterministic payload so we can verify integrity. +function buildChunk(index) { + const chunk = new Uint8Array(chunkSize); + // Fill with a pattern derived from the chunk index. + const val = index & 0xff; + for (let i = 0; i < chunkSize; i++) { + chunk[i] = (val + i) & 0xff; + } + return chunk; +} + +function checksum(data) { + let sum = 0; + for (let i = 0; i < data.byteLength; i++) { + sum = (sum + data[i]) | 0; + } + return sum; +} + +// Compute expected checksum. +let expectedChecksum = 0; +for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + expectedChecksum = (expectedChecksum + checksum(chunk)) | 0; +} + +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, numChunks * chunkSize); + strictEqual(checksum(received), expectedChecksum); + + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + done.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +const w = stream.writer; + +// Write chunks, respecting backpressure via drainableProtocol. +for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + while (!w.writeSync(chunk)) { + // Flow controlled — wait for drain before retrying. + const drainable = w[dp](); + if (drainable) await drainable; + } +} + +const totalWritten = w.endSync(); +strictEqual(totalWritten, numChunks * chunkSize); + +await Promise.all([stream.closed, done.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-bidi-server-initiated.mjs b/test/parallel/test-quic-stream-bidi-server-initiated.mjs new file mode 100644 index 00000000000000..30328ffde508ec --- /dev/null +++ b/test/parallel/test-quic-stream-bidi-server-initiated.mjs @@ -0,0 +1,57 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: server-initiated bidirectional stream. +// The server creates a bidi stream and sends a body to the client. +// The client receives the data via its onstream handler and verifies +// integrity. This is the reverse of test-quic-stream-bidi-basic.mjs. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { deepStrictEqual, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const message = 'Hello from the server'; +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const expected = encoder.encode(message); + +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + + const stream = await serverSession.createBidirectionalStream({ + body: encoder.encode(message), + }); + + // Drain the client's write side (client sends FIN with no data). + for await (const batch of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +clientSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + + deepStrictEqual(received, expected); + strictEqual(decoder.decode(received), message); + + // Close the client's write side so the stream fully closes. + stream.writer.endSync(); + await stream.closed; + + clientSession.close(); + done.resolve(); +}); + +await done.promise; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-bidi-setbody.mjs b/test/parallel/test-quic-stream-bidi-setbody.mjs new file mode 100644 index 00000000000000..856211fc36cda3 --- /dev/null +++ b/test/parallel/test-quic-stream-bidi-setbody.mjs @@ -0,0 +1,59 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream.setBody() after creation. +// Creates a bidirectional stream without an initial body, then attaches +// a body via setBody(). The server reads the data and verifies integrity. +// Also verifies that calling setBody() a second time throws. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { deepStrictEqual, strictEqual, throws } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const message = 'body set after creation'; +const expected = encoder.encode(message); + +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + + deepStrictEqual(received, expected); + strictEqual(decoder.decode(received), message); + + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + done.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Create a stream with no body. +const stream = await clientSession.createBidirectionalStream(); + +// Attach a body after creation. +stream.setBody(encoder.encode(message)); + +// Calling setBody() again should throw. +throws(() => { + stream.setBody(encoder.encode('second body')); +}, { + code: 'ERR_INVALID_STATE', +}); + +await Promise.all([stream.closed, done.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-bidi-varchunklen.mjs b/test/parallel/test-quic-stream-bidi-varchunklen.mjs new file mode 100644 index 00000000000000..a8928c7ced34dc --- /dev/null +++ b/test/parallel/test-quic-stream-bidi-varchunklen.mjs @@ -0,0 +1,91 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: bidirectional data transfer with varying chunk sizes. +// This is a regression test for a stall caused by a mismatch between +// writeSync (which rejects when chunk > writeDesiredSize) and +// drainableProtocol (which returned null when writeDesiredSize > 0). +// When chunks don't evenly fill the high water mark, writeDesiredSize +// can be positive but smaller than the next chunk, causing the +// while(!writeSync) { dp(); await } loop to spin without yielding. +// See: https://github.com/nodejs/node/issues/63216 + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes, drainableProtocol: dp } = await import('stream/iter'); + +// Varying chunk sizes — the pattern of alternating large and small +// chunks is effective at triggering the writeDesiredSize gap. +const chunkSizes = [60000, 12, 50000, 1600, 20000, 30000, 0, 100]; +const numChunks = chunkSizes.length; +const byteLength = chunkSizes.reduce((a, b) => a + b, 0); + +// Build a deterministic payload so we can verify integrity. +function buildChunk(index) { + const chunk = new Uint8Array(chunkSizes[index]); + const val = index & 0xff; + for (let i = 0; i < chunkSizes[index]; i++) { + chunk[i] = (val + i) & 0xff; + } + return chunk; +} + +function checksum(data) { + let sum = 0; + for (let i = 0; i < data.byteLength; i++) { + sum = (sum + data[i]) | 0; + } + return sum; +} + +// Compute expected checksum. +let expectedChecksum = 0; +for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + expectedChecksum = (expectedChecksum + checksum(chunk)) | 0; +} + +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, byteLength); + strictEqual(checksum(received), expectedChecksum); + + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + done.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +const w = stream.writer; + +// Write chunks, respecting backpressure via drainableProtocol. +for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + while (!w.writeSync(chunk)) { + // Flow controlled — wait for drain before retrying. + const drainable = w[dp](); + if (drainable) await drainable; + } +} + +const totalWritten = w.endSync(); +strictEqual(totalWritten, byteLength); + +await Promise.all([stream.closed, done.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-bidi-writer.mjs b/test/parallel/test-quic-stream-bidi-writer.mjs new file mode 100644 index 00000000000000..972c376257ec96 --- /dev/null +++ b/test/parallel/test-quic-stream-bidi-writer.mjs @@ -0,0 +1,63 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: writer API for bidirectional streams. +// Exercises writeSync, write (async), endSync, and verifies that data +// written in multiple chunks arrives intact and in order on the server. +// Also tests that the writer reports correct state after operations. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(decoder.decode(received), 'chunk1chunk2chunk3'); + + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + done.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +const w = stream.writer; + +// Writer should be open. +strictEqual(typeof w.desiredSize, 'number'); + +// Write multiple chunks synchronously. +strictEqual(w.writeSync(encoder.encode('chunk1')), true); +strictEqual(w.writeSync(encoder.encode('chunk2')), true); +strictEqual(w.writeSync(encoder.encode('chunk3')), true); + +// End the write side — returns total bytes written. +const totalWritten = w.endSync(); +strictEqual(totalWritten, 18); // 6 * 3 + +// After end, write should return false. +strictEqual(w.writeSync(encoder.encode('nope')), false); + +// desiredSize should be null after close. +strictEqual(w.desiredSize, null); + +await Promise.all([stream.closed, done.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-body-async-error.mjs b/test/parallel/test-quic-stream-body-async-error.mjs new file mode 100644 index 00000000000000..b84df950a34997 --- /dev/null +++ b/test/parallel/test-quic-stream-body-async-error.mjs @@ -0,0 +1,46 @@ +// Flags: --experimental-quic --no-warnings + +// Test: async iterable source error destroys the stream. +// When the async iterable body source throws, the stream should be +// destroyed with the error and stream.closed should reject. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { transportParams: { maxIdleTimeout: 1 } }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, +}); +await clientSession.opened; + +const testError = new Error('async source error'); + +async function* failingSource() { + yield encoder.encode('partial '); + throw testError; +} + +const stream = await clientSession.createBidirectionalStream(); + +// Attach the closed handler BEFORE setBody so the rejection from +// stream.destroy(err) is caught before it becomes unhandled. +const closedPromise = rejects(stream.closed, testError); + +stream.setBody(failingSource()); + +// The stream should be destroyed with the source error. +await Promise.all([closedPromise, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-body-async-iterable.mjs b/test/parallel/test-quic-stream-body-async-iterable.mjs new file mode 100644 index 00000000000000..b73cfd07b67441 --- /dev/null +++ b/test/parallel/test-quic-stream-body-async-iterable.mjs @@ -0,0 +1,51 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: body from async iterable source. +// An async generator is used as the body source. The data is consumed +// via the streaming path in configureOutbound. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { deepStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const chunks = ['hello ', 'from ', 'async ', 'iterable']; +const expected = encoder.encode(chunks.join('')); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + deepStrictEqual(received, expected); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +async function* generateChunks() { + for (const chunk of chunks) { + yield encoder.encode(chunk); + } +} + +const stream = await clientSession.createBidirectionalStream(); +stream.setBody(generateChunks()); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-body-error.mjs b/test/parallel/test-quic-stream-body-error.mjs new file mode 100644 index 00000000000000..6045d8a82f4d95 --- /dev/null +++ b/test/parallel/test-quic-stream-body-error.mjs @@ -0,0 +1,51 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream destroyed during async source consumption. +// When the stream is destroyed while an async iterable body source is +// active, the source consumption should stop. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; +const { setTimeout } = await import('node:timers/promises'); + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { transportParams: { maxIdleTimeout: 1 } }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, +}); +await clientSession.opened; + +let yieldCount = 0; +async function* slowSource() { + while (true) { + yield encoder.encode(`chunk ${yieldCount++} `); + await setTimeout(50); + } +} + +const stream = await clientSession.createBidirectionalStream(); +stream.setBody(slowSource()); + +// Destroy the stream after a short delay. +await setTimeout(200); +stream.destroy(); +await stream.closed; + +// The source should have stopped. It may yield a few chunks +// but not an unbounded number. +ok(yieldCount < 50, `yieldCount too high: ${yieldCount}`); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-body-filehandle.mjs b/test/parallel/test-quic-stream-body-filehandle.mjs new file mode 100644 index 00000000000000..a990f3a23ae14f --- /dev/null +++ b/test/parallel/test-quic-stream-body-filehandle.mjs @@ -0,0 +1,122 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: FileHandle as body source for QUIC streams. +// The file contents are sent via an fd-backed DataQueue. The FileHandle +// is automatically closed when the stream finishes. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { writeFileSync } from 'node:fs'; +import { open } from 'node:fs/promises'; + +const tmpdir = await import('../common/tmpdir.js'); + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const decoder = new TextDecoder(); +const testContent = 'Hello from a file!\nLine two.\n'; + +tmpdir.refresh(); +const testFile = tmpdir.resolve('quic-fh-test.txt'); +writeFileSync(testFile, testContent); + +// FileHandle as body in createBidirectionalStream. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const body = await bytes(stream); + strictEqual(decoder.decode(body), testContent); + stream.writer.writeSync('ok'); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + const fh = await open(testFile, 'r'); + const stream = await clientSession.createBidirectionalStream({ + body: fh, + }); + + const response = await bytes(stream); + strictEqual(decoder.decode(response), 'ok'); + await Promise.all([stream.closed, serverDone.promise, clientSession.closed]); + await serverEndpoint.close(); + // FileHandle is closed automatically when the stream finishes. +} + +// FileHandle as body in setBody. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const body = await bytes(stream); + strictEqual(decoder.decode(body), testContent); + stream.writer.writeSync('ok'); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + const fh = await open(testFile, 'r'); + const stream = await clientSession.createBidirectionalStream(); + stream.setBody(fh); + + const response = await bytes(stream); + strictEqual(decoder.decode(response), 'ok'); + await Promise.all([stream.closed, serverDone.promise, clientSession.closed]); + await serverEndpoint.close(); + // FileHandle is closed automatically when the stream finishes. +} + +// Locked FileHandle rejects on second use. +{ + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Drain the incoming data so the stream can close cleanly. + await bytes(stream); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + }); + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + const fh = await open(testFile, 'r'); + + // First use locks the FileHandle. + const stream1 = await clientSession.createBidirectionalStream({ + body: fh, + }); + + // Second use should reject because it's locked. + await assert.rejects( + clientSession.createBidirectionalStream({ body: fh }), + { code: 'ERR_INVALID_STATE' }, + ); + + await Promise.all([stream1.closed, clientSession.closed]); + await serverEndpoint.close(); + // FileHandle is closed automatically when the stream finishes. +} diff --git a/test/parallel/test-quic-stream-body-pooled-buffer.mjs b/test/parallel/test-quic-stream-body-pooled-buffer.mjs new file mode 100644 index 00000000000000..f716c95ac2eac1 --- /dev/null +++ b/test/parallel/test-quic-stream-body-pooled-buffer.mjs @@ -0,0 +1,51 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: pooled Buffer body copies correctly. +// Buffer.from() creates pooled buffers that share a larger ArrayBuffer. +// The QUIC body handling must copy (not transfer) partial views. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const message = 'pooled buffer test data'; +const expected = Buffer.from(message); + +// Verify this IS a pooled buffer (byteLength < buffer.byteLength). +ok( + expected.buffer.byteLength > expected.byteLength, + 'Buffer should be pooled for this test to be meaningful', +); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(Buffer.from(received).toString(), message); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Send the pooled buffer as body via setBody. +const stream = await clientSession.createBidirectionalStream(); +stream.setBody(expected); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-body-promise-error.mjs b/test/parallel/test-quic-stream-body-promise-error.mjs new file mode 100644 index 00000000000000..e3cec3fe94e76d --- /dev/null +++ b/test/parallel/test-quic-stream-body-promise-error.mjs @@ -0,0 +1,38 @@ +// Flags: --experimental-quic --no-warnings + +// Test: body: Promise rejection destroys the stream. +// When the body is a Promise that rejects, the stream should be +// destroyed with the rejection error. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}), { transportParams: { maxIdleTimeout: 5 } }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 5 }, +}); +await clientSession.opened; + +const testError = new Error('promise body rejected'); +const stream = await clientSession.createBidirectionalStream(); + +// Attach the closed handler BEFORE setBody so the rejection from +// stream.destroy(err) is caught before it becomes unhandled. +const closedPromise = rejects(stream.closed, testError); + +stream.setBody(Promise.reject(testError)); + +// The stream should be destroyed with the rejection error. +await Promise.all([closedPromise, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-body-promise-reject.mjs b/test/parallel/test-quic-stream-body-promise-reject.mjs new file mode 100644 index 00000000000000..6cd4e1004fbeac --- /dev/null +++ b/test/parallel/test-quic-stream-body-promise-reject.mjs @@ -0,0 +1,51 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: body: nested promise resolution. +// Native promises auto-flatten, so Promise> resolves +// to the inner string value. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +// Nested promises — native promises auto-flatten, so the +// resolved value is never itself a promise. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual( + new TextDecoder().decode(received), + 'nested promise', + ); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + // Double-nested promise: Promise> + const stream = await clientSession.createBidirectionalStream(); + stream.setBody( + Promise.resolve(Promise.resolve('nested promise')), + ); + + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await Promise.all([stream.closed, serverDone.promise]); + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-stream-body-promise.mjs b/test/parallel/test-quic-stream-body-promise.mjs new file mode 100644 index 00000000000000..040372f2c38832 --- /dev/null +++ b/test/parallel/test-quic-stream-body-promise.mjs @@ -0,0 +1,71 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: body from Promise. +// Promise is awaited then configured as a string body. +// Promise is awaited then closes the writable side. +// BODY-13 (Promise rejection) is not tested here because the rejected +// promise calls resetStream synchronously which may or may not cause +// the server's onstream to fire depending on timing. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { text, bytes } = await import('stream/iter'); + +let streamIdx = 0; +const totalStreams = 2; +const allDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const idx = streamIdx++; + + if (idx === 0) { + // Promise resolved to string data. + const received = await text(stream); + strictEqual(received, 'resolved string'); + } else if (idx === 1) { + // Promise closes the writable side. + const received = await bytes(stream); + strictEqual(received.byteLength, 0); + } + + stream.writer.endSync(); + await stream.closed; + + if (streamIdx === totalStreams) { + serverSession.close(); + allDone.resolve(); + } + }, totalStreams); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Promise +{ + const stream = await clientSession.createBidirectionalStream(); + stream.setBody(Promise.resolve('resolved string')); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +// Promise +{ + const stream = await clientSession.createBidirectionalStream(); + stream.setBody(Promise.resolve(null)); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +await allDone.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-body-readable-stream.mjs b/test/parallel/test-quic-stream-body-readable-stream.mjs new file mode 100644 index 00000000000000..5d23b74947d1bc --- /dev/null +++ b/test/parallel/test-quic-stream-body-readable-stream.mjs @@ -0,0 +1,66 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: body from ReadableStream and stream.Readable. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { deepStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); +const { Readable } = await import('node:stream'); + +const encoder = new TextEncoder(); +const message = 'readable stream body'; +const expected = encoder.encode(message); + +let serverStreamCount = 0; +const allDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + deepStrictEqual(received, expected); + stream.writer.endSync(); + await stream.closed; + if (++serverStreamCount === 2) { + serverSession.close(); + allDone.resolve(); + } + }, 2); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Web ReadableStream as body source. +{ + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(message)); + controller.close(); + }, + }); + const stream = await clientSession.createBidirectionalStream(); + stream.setBody(rs); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +// Node.js stream.Readable as body source. +{ + const readable = Readable.from([encoder.encode(message)]); + const stream = await clientSession.createBidirectionalStream(); + stream.setBody(readable); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +await allDone.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-body-sources.mjs b/test/parallel/test-quic-stream-body-sources.mjs new file mode 100644 index 00000000000000..06b8cad7ec56ef --- /dev/null +++ b/test/parallel/test-quic-stream-body-sources.mjs @@ -0,0 +1,88 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: various body source types for createBidirectionalStream. +// Verifies that ArrayBuffer, ArrayBufferView (with non-zero byteOffset), +// SharedArrayBuffer, and Blob bodies all deliver data correctly. +// Covers BIDI-07, BIDI-08, BIDI-09, BIDI-10, BIDI-11, BODY-03..06. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const message = 'hello body sources'; +const expectedBytes = encoder.encode(message); + +let testIndex = 0; +const totalTests = 4; +const allDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await bytes(stream); + stream.writer.endSync(); + await stream.closed; + + testIndex++; + if (testIndex === totalTests) { + serverSession.close(); + allDone.resolve(); + } + }, totalTests); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Test 1: ArrayBuffer body +{ + const buf = encoder.encode(message); + const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + const stream = await clientSession.createBidirectionalStream({ body: ab }); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +// Test 2: ArrayBufferView with non-zero byteOffset +{ + const backing = new ArrayBuffer(64); + const fullView = new Uint8Array(backing); + const offset = 10; + fullView.set(expectedBytes, offset); + const view = new Uint8Array(backing, offset, expectedBytes.byteLength); + const stream = await clientSession.createBidirectionalStream({ body: view }); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +// Test 3: SharedArrayBuffer body +{ + const sab = new SharedArrayBuffer(expectedBytes.byteLength); + const sabView = new Uint8Array(sab); + sabView.set(expectedBytes); + const stream = await clientSession.createBidirectionalStream({ body: sabView }); + // The SharedArrayBuffer should still be usable (copied, not transferred). + strictEqual(sab.byteLength, expectedBytes.byteLength); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +// Test 4: Blob body +{ + const blob = new Blob([expectedBytes]); + const stream = await clientSession.createBidirectionalStream({ body: blob }); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +await allDone.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-body-state.mjs b/test/parallel/test-quic-stream-body-state.mjs new file mode 100644 index 00000000000000..8dcf6d6c6771ef --- /dev/null +++ b/test/parallel/test-quic-stream-body-state.mjs @@ -0,0 +1,85 @@ +// Flags: --experimental-quic --no-warnings + +// Test: setBody / writer mutual exclusion. +// setBody() after writer accessed throws. +// writer after setBody() throws. +// setBody() on destroyed stream throws. +// BODY-17 (setBody twice throws) is already covered by +// test-quic-stream-bidi-setbody.mjs. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, throws } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +let streamCount = 0; +// BODY-20 destroys the stream before data is sent, so the server only sees 2. +const totalStreams = 2; +const allDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Close the server's write side so the stream can fully close. + stream.writer.endSync(); + await stream.closed; + if (++streamCount === totalStreams) { + serverSession.close(); + allDone.resolve(); + } + }, totalStreams); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// setBody() after writer accessed throws. +{ + const stream = await clientSession.createBidirectionalStream(); + // Access the writer — this initializes the streaming source. + const w = stream.writer; + ok(w); + throws(() => { + stream.setBody(encoder.encode('too late')); + }, { + code: 'ERR_INVALID_STATE', + }); + w.endSync(); + await stream.closed; +} + +// Writer after setBody() throws. +{ + const stream = await clientSession.createBidirectionalStream(); + stream.setBody(encoder.encode('body set')); + throws(() => { + stream.writer; // eslint-disable-line no-unused-expressions + }, { + code: 'ERR_INVALID_STATE', + }); + await stream.closed; +} + +// setBody() on destroyed stream throws. +{ + const stream = await clientSession.createBidirectionalStream(); + stream.destroy(); + throws(() => { + stream.setBody(encoder.encode('destroyed')); + }, { + code: 'ERR_INVALID_STATE', + }); + // stream.closed resolves (destroy without error). + await stream.closed; +} + +await allDone.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-body-string-shorthand.mjs b/test/parallel/test-quic-stream-body-string-shorthand.mjs new file mode 100644 index 00000000000000..04535def7568fb --- /dev/null +++ b/test/parallel/test-quic-stream-body-string-shorthand.mjs @@ -0,0 +1,106 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: string body shorthand for stream creation and setBody. +// Strings are automatically encoded as UTF-8. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const decoder = new TextDecoder(); + +// String body in createBidirectionalStream options. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'hello from string body'); + stream.writer.writeSync(new TextEncoder().encode('ok')); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + // Body provided as a string — should be UTF-8 encoded automatically. + const stream = await clientSession.createBidirectionalStream({ + body: 'hello from string body', + }); + + const response = await bytes(stream); + strictEqual(decoder.decode(response), 'ok'); + await Promise.all([stream.closed, serverDone.promise, clientSession.closed]); + await serverEndpoint.close(); +} + +// String body with setBody. +{ + const serverDone = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const body = await bytes(stream); + strictEqual(decoder.decode(body), 'setBody string'); + stream.writer.writeSync(new TextEncoder().encode('ok')); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream(); + stream.setBody('setBody string'); + + const response = await bytes(stream); + strictEqual(decoder.decode(response), 'ok'); + await Promise.all([stream.closed, serverDone.promise, clientSession.closed]); + await serverEndpoint.close(); +} + +// UTF-8 multi-byte characters preserved correctly. +{ + const serverDone = Promise.withResolvers(); + const testString = 'Hello \u{1F600} world \u00E9\u00FC\u00F1'; + + const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const body = await bytes(stream); + strictEqual(decoder.decode(body), testString); + stream.writer.writeSync(new TextEncoder().encode('ok')); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); + })); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + body: testString, + }); + + const response = await bytes(stream); + strictEqual(decoder.decode(response), 'ok'); + await Promise.all([stream.closed, serverDone.promise, clientSession.closed]); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-stream-body-string.mjs b/test/parallel/test-quic-stream-body-string.mjs new file mode 100644 index 00000000000000..6b3f96ee7b0d23 --- /dev/null +++ b/test/parallel/test-quic-stream-body-string.mjs @@ -0,0 +1,43 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: body: string sends UTF-8 encoded data. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { text } = await import('stream/iter'); + +const message = 'Hello from a string body! 🎉 Unicode works.'; + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await text(stream); + strictEqual(received, message); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// setBody with a string — configureOutbound handles this via +// Buffer.from(body, 'utf8'). +const stream = await clientSession.createBidirectionalStream(); +stream.setBody(message); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-body-sync-iterable.mjs b/test/parallel/test-quic-stream-body-sync-iterable.mjs new file mode 100644 index 00000000000000..f022df9b5ad7dc --- /dev/null +++ b/test/parallel/test-quic-stream-body-sync-iterable.mjs @@ -0,0 +1,45 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: body from sync iterable source. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { deepStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const chunks = ['sync ', 'iterable ', 'source']; +const expected = encoder.encode(chunks.join('')); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + deepStrictEqual(received, expected); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Create an array of Uint8Arrays as a sync iterable body. +const bodyChunks = chunks.map((c) => encoder.encode(c)); +const stream = await clientSession.createBidirectionalStream(); +stream.setBody(bodyChunks); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-closed-promise.mjs b/test/parallel/test-quic-stream-closed-promise.mjs new file mode 100644 index 00000000000000..2c30a44fb70bb3 --- /dev/null +++ b/test/parallel/test-quic-stream-closed-promise.mjs @@ -0,0 +1,40 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream.closed promise resolves after normal completion. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await bytes(stream); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('normal close'), +}); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + +// Closed should resolve (not reject). +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-closed-rejects.mjs b/test/parallel/test-quic-stream-closed-rejects.mjs new file mode 100644 index 00000000000000..aa0f3c083b715e --- /dev/null +++ b/test/parallel/test-quic-stream-closed-rejects.mjs @@ -0,0 +1,55 @@ +// Flags: --experimental-quic --no-warnings + +// Test: stream.closed promise rejects on error. +// The server resets the stream, causing both sides' closed to reject +// with the application error code. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + stream.resetStream(1n); + + // The server's own stream.closed should also reject with the + // reset error code. + await rejects(stream.closed, (error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes('1')); + return true; + }); + + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('will error'), +}); + +// Client's closed should reject with the reset error code. +await rejects(stream.closed, (error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes('1')); + return true; +}); + +await serverDone.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-destroy-emits-reset.mjs b/test/parallel/test-quic-stream-destroy-emits-reset.mjs new file mode 100644 index 00000000000000..e565f5e9ff7b8f --- /dev/null +++ b/test/parallel/test-quic-stream-destroy-emits-reset.mjs @@ -0,0 +1,69 @@ +// Flags: --experimental-quic --no-warnings + +// stream.destroy(err) emits RESET_STREAM on the wire even when the +// user never accessed `stream.writer`. Previously the wire frame was +// only emitted via the writer.fail path inside [kFinishClose], so +// streams that destroyed without ever creating a writer (e.g. used +// `setBody()` or never wrote at all) left the write side dangling on +// the wire and the peer kept the stream alive until the session-level +// idle timer fired. +// +// Verified by observing the server-side `onreset` callback. The wire +// code is the negotiated application's "internal error" code: for +// the test fixture's non-h3 ALPN (`quic-test`) the C++ +// DefaultApplication reports `1n`, which propagates to the server +// as `ERR_QUIC_APPLICATION_ERROR` exposing `errorCode === 1n`. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverResetSeen = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + stream.onreset = mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); + // The DefaultApplication's internal error code is 0x1n. + strictEqual(err.errorCode, 1n); + serverResetSeen.resolve(); + }); + + // The peer's reset causes stream.closed to reject with the reset + // error code. + await rejects(stream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Bidirectional stream with a body source set up front. No JS-side +// writer is accessed -- the body is consumed and pushed by the C++ +// streaming source. Pre-B13, calling destroy() on this stream would +// not emit RESET_STREAM because the writer.fail path was the only +// emission site. +const stream = await clientSession.createBidirectionalStream({ + body: 'hello world', +}); + +const err = new Error('destroy without writer'); +// Pre-attach the rejection assertion before destroying so the +// resulting `stream.closed` rejection isn't reported as unhandled. +const clientClosedAssertion = rejects(stream.closed, err); + +stream.destroy(err); + +await Promise.all([clientClosedAssertion, serverResetSeen.promise]); + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-destroy-emits-stop-sending.mjs b/test/parallel/test-quic-stream-destroy-emits-stop-sending.mjs new file mode 100644 index 00000000000000..47a6d8f1857e4f --- /dev/null +++ b/test/parallel/test-quic-stream-destroy-emits-stop-sending.mjs @@ -0,0 +1,88 @@ +// Flags: --experimental-quic --no-warnings + +// stream.destroy(err) emits STOP_SENDING on the wire so the peer +// stops sending data the local side is about to discard. Previously, +// destroy never sent STOP_SENDING - the readable side stayed open +// from the peer's perspective and they would keep transmitting until +// the session-level idle timer fired. +// +// Verified via the server-side writer's `desiredSize` getter, which +// returns `null` once `state.writeEnded` is set. STOP_SENDING from +// the client triggers `Stream::ReceiveStopSending -> EndWritable` on +// the server, which sets `state.writeEnded = 1`. +// +// ngtcp2 packs RESET_STREAM before STOP_SENDING in the same packet +// regardless of the order our JS code makes the calls. The server +// processes RESET_STREAM first (firing `onreset`) and then +// STOP_SENDING. The observation must therefore be deferred until +// after the `onreset` callback returns so ngtcp2 can finish +// processing the packet. Capturing `writer.desiredSize` synchronously +// inside `onreset` would always see the pre-STOP_SENDING value. +// Throwing inside `onreset` would also crash the process before +// STOP_SENDING could be processed. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverObservation = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const writer = stream.writer; + // Sanity: the writer is active before the peer tears the stream + // down, so desiredSize is a number reflecting the initial + // flow-control window. + if (typeof writer.desiredSize !== 'number') { + serverObservation.reject(new Error( + `expected initial writer.desiredSize to be a number, ` + + `got ${writer.desiredSize}`)); + return; + } + stream.onreset = mustCall(() => { + // Defer the writeEnded observation: ngtcp2 fires this callback + // synchronously while processing the RESET_STREAM frame, before + // it gets to the STOP_SENDING frame in the same packet. By the + // next event loop tick the rest of the packet has been + // processed and `Stream::ReceiveStopSending -> EndWritable` has + // flipped `state.writeEnded` to 1, which makes the writer's + // desiredSize getter return null. + setImmediate(() => { + serverObservation.resolve(writer.desiredSize); + }); + }); + + // The peer's reset causes stream.closed to reject. + await assert.rejects(stream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: 'client-data', +}); + +const err = new Error('destroy with error'); +const clientClosedAssertion = assert.rejects(stream.closed, err); + +stream.destroy(err); + +const observedDesiredSize = await serverObservation.promise; +strictEqual(observedDesiredSize, null); + +await clientClosedAssertion; + +clientSession.close(); +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-destroy-options-code.mjs b/test/parallel/test-quic-stream-destroy-options-code.mjs new file mode 100644 index 00000000000000..b1fd70018d456b --- /dev/null +++ b/test/parallel/test-quic-stream-destroy-options-code.mjs @@ -0,0 +1,60 @@ +// Flags: --experimental-quic --no-warnings + +// stream.destroy(error, options) accepts an explicit `options.code` +// that overrides the default wire code derived from `error`. The +// caller-supplied code is sent on RESET_STREAM (and STOP_SENDING for +// the readable side) so the peer observes exactly that code. +// +// For the test fixture's non-h3 ALPN (`quic-test`), the +// DefaultApplication's `internalErrorCode` is `0x1n`. Without +// `options.code`, a plain `Error` would result in the peer seeing +// `0x1n`. With `options.code = 0x42n`, the peer must see `0x42n`. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverResetSeen = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + stream.onreset = mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); + strictEqual(err.errorCode, 66n); + serverResetSeen.resolve(); + }); + + // The peer's reset causes stream.closed to reject with the reset + // error code. + await rejects(stream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: 'data', +}); + +const err = new Error('explicit code via options'); +const clientClosedAssertion = rejects(stream.closed, err); + +// `options.code` (0x42n) takes precedence over the default that +// would have been derived from `error` (which would be the session's +// internalErrorCode, 0x1n for non-h3). +stream.destroy(err, { code: 66n }); + +await Promise.all([clientClosedAssertion, serverResetSeen.promise]); + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-destroy-options-validate.mjs b/test/parallel/test-quic-stream-destroy-options-validate.mjs new file mode 100644 index 00000000000000..ba8064121a9821 --- /dev/null +++ b/test/parallel/test-quic-stream-destroy-options-validate.mjs @@ -0,0 +1,73 @@ +// Flags: --experimental-quic --no-warnings + +// stream.destroy(error, options) validates `options` up front, before +// any side effects. A malformed `options` argument must throw without +// mutating `#destroying`, emitting wire frames, invoking `onerror`, +// or settling the `closed` promise. After the throw, the stream is +// still alive and a subsequent destroy with valid options must +// succeed. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, throws, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverEndpoint = await listen(mustCall(() => true)); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); + +stream.onerror = mustNotCall( + 'stream.onerror must not fire when destroy() throws on bad options'); + +// 1. options is not an object -> throws ERR_INVALID_ARG_TYPE. +throws(() => stream.destroy(new Error('x'), 'not an object'), { + code: 'ERR_INVALID_ARG_TYPE', +}); +strictEqual(stream.destroyed, false); + +// 2. options.code is the wrong type -> throws ERR_INVALID_ARG_TYPE. +throws(() => stream.destroy(new Error('x'), { code: 'oops' }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +strictEqual(stream.destroyed, false); + +throws(() => stream.destroy(new Error('x'), { code: true }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +strictEqual(stream.destroyed, false); + +// 3. options.reason is the wrong type -> throws ERR_INVALID_ARG_TYPE. +throws(() => stream.destroy(new Error('x'), { reason: 42 }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +strictEqual(stream.destroyed, false); + +// Switch to the real error handler before the final destroy so the +// `mustNotCall` above does not fire on the legitimate teardown. +const finalError = new Error('final destroy'); +stream.onerror = mustCall((err) => { strictEqual(err, finalError); }); + +const clientClosedAssertion = rejects(stream.closed, finalError); + +// 4. Valid options accepted: bigint code. +stream.destroy(finalError, { code: 0x10n, reason: 'cleanup' }); +strictEqual(stream.destroyed, true); + +// 5. Re-entry with arbitrarily bad options is a no-op (the +// re-entrancy guard returns before validation runs). +stream.destroy(new Error('after-destroy'), { code: 'still-bad' }); +strictEqual(stream.destroyed, true); + +await clientClosedAssertion; + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-error-graceful-close.mjs b/test/parallel/test-quic-stream-error-graceful-close.mjs new file mode 100644 index 00000000000000..ffd88c5026462b --- /dev/null +++ b/test/parallel/test-quic-stream-error-graceful-close.mjs @@ -0,0 +1,52 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream errors during graceful close are handled. +// When a session is gracefully closing and an open stream encounters +// an error, the session should still close cleanly without crashing. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Read some data then reset the stream while the client + // is still sending — this creates a stream error. + const data = await bytes(stream); + ok(data.byteLength > 0); + stream.resetStream(99n); + serverSession.close(); + serverDone.resolve(); + }); +}), { + transportParams: { initialMaxStreamDataBidiRemote: 256 }, + onerror(err) { ok(err); }, +}); + +const clientSession = await connect(serverEndpoint.address, { + onerror(err) { ok(err); }, +}); +await clientSession.opened; + +// Send data on a stream. The server will reset it. +const stream = await clientSession.createBidirectionalStream(); +stream.setBody(new Uint8Array(4096)); + +// The stream will error due to the server's reset. +// The session should still close gracefully. +await rejects(stream.closed, (error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + return true; +}); +await Promise.all([serverDone.promise, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-id-ordering.mjs b/test/parallel/test-quic-stream-id-ordering.mjs new file mode 100644 index 00000000000000..ee80480fc2cd71 --- /dev/null +++ b/test/parallel/test-quic-stream-id-ordering.mjs @@ -0,0 +1,56 @@ +// Flags: --experimental-quic --no-warnings + +// Test: stream IDs are strictly increasing and unique. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); +const serverDone = Promise.withResolvers(); +let serverStreamCount = 0; + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + stream.writer.endSync(); + await stream.closed; + if (++serverStreamCount === 10) { + serverSession.close(); + serverDone.resolve(); + } + }, 10); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const ids = []; +for (let i = 0; i < 10; i++) { + const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode(`stream ${i}`), + }); + ids.push(stream.id); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +// Verify IDs are strictly increasing. +for (let i = 1; i < ids.length; i++) { + ok(ids[i] > ids[i - 1], + `Stream ID ${ids[i]} should be > ${ids[i - 1]}`); +} + +// Verify all IDs are unique. +const uniqueIds = new Set(ids); +strictEqual(uniqueIds.size, ids.length); + +await Promise.all([serverDone.promise, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-idle-timeout.mjs b/test/parallel/test-quic-stream-idle-timeout.mjs new file mode 100644 index 00000000000000..9b2b36b41a8e47 --- /dev/null +++ b/test/parallel/test-quic-stream-idle-timeout.mjs @@ -0,0 +1,141 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream idle timeout. +// Peer-initiated streams that receive no data within the configured +// timeout are automatically destroyed. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; +import { text } from 'node:stream/iter'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// --- Stream destroyed after idle timeout --- +{ + const streamDestroyed = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Don't read — let the stream sit idle after the initial data. + // The stream idle timeout should destroy it, rejecting stream.closed. + await rejects(stream.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); + streamDestroyed.resolve(); + }); + }), { + // Short idle timeout for the test — 500ms. + streamIdleTimeout: 100, + }); + + const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + }); + + await clientSession.opened; + + // Open a stream and send one byte so the server creates the + // peer-initiated stream, then stop sending. + const clientStream = await clientSession.createUnidirectionalStream(); + const writer = clientStream.writer; + writer.writeSync('x'); + + // Wait for the server to destroy the idle stream. + await streamDestroyed.promise; + + // The server sent STOP_SENDING when it destroyed the idle stream. + // ShutdownStream maps the transport error to the application's + // internal error code on the wire, so the client sees an application + // error indicating the server rejected the stream. + await rejects(clientStream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + + await clientSession.close(); + await serverEndpoint.close(); +} + +// --- Stream with data is NOT destroyed --- +{ + const encoder = new TextEncoder(); + const serverGotData = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await text(stream); + strictEqual(data, 'xy'); + serverGotData.resolve(); + await serverSession.close(); + }); + }), { + streamIdleTimeout: 500, + }); + + const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', + }); + await clientSession.opened; + + // Open a stream and send data immediately. + const clientStrema = await clientSession.createUnidirectionalStream(); + const writer = clientStrema.writer; + writer.writeSync(encoder.encode('x')); + + // Less than the 500ms idle timeout. + await setTimeout(300); + + writer.writeSync(encoder.encode('y')); + writer.endSync(); + + await Promise.all([serverGotData.promise, clientSession.closed]); + await serverEndpoint.close(); +} + +// --- Disabled when set to 0 --- +{ + const streamSurvived = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + + // Do not await this yet. + const data = await text(stream); + + // We should receive all the data, even though it is sent with a pause + // longer than the default idle timeout. + strictEqual(data, 'xy'); + streamSurvived.resolve(); + }); + + await setTimeout(700); + await serverSession.close(); + }), { + streamIdleTimeout: 0, // Disabled + }); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + // Send one byte so the server creates the stream, then stop. + const clientStream = await clientSession.createUnidirectionalStream(); + const writer = clientStream.writer; + writer.writeSync('x'); + + // Now pause beyond the 500ms default timeout to verify the stream is not + // killed by idle timeout. + await setTimeout(600); + + writer.writeSync('y'); + writer.endSync(); + + await Promise.all([streamSurvived.promise, clientSession.closed]); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-stream-iteration-batching.mjs b/test/parallel/test-quic-stream-iteration-batching.mjs new file mode 100644 index 00000000000000..092f270304ff12 --- /dev/null +++ b/test/parallel/test-quic-stream-iteration-batching.mjs @@ -0,0 +1,66 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: batching — multiple chunks collected per iteration step. +// When multiple chunks are available synchronously (e.g., the server +// sends them rapidly), the async iterator should batch them into +// arrays of Uint8Array, not yield one chunk at a time. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Send multiple small chunks rapidly — they should be batched + // on the receiving side. + const w = stream.writer; + for (let i = 0; i < 20; i++) { + w.writeSync(encoder.encode(`chunk${i} `)); + } + w.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('request'), +}); + +// Iterate and count batches vs total chunks. +let batchCount = 0; +let totalChunks = 0; +for await (const batch of stream) { + batchCount++; + ok(Array.isArray(batch), 'Each iteration step yields an array'); + for (const chunk of batch) { + ok(chunk instanceof Uint8Array, 'Each item is a Uint8Array'); + totalChunks++; + } +} + +// There should be fewer batches than total chunks — proving batching. +// (On very slow machines, each chunk might arrive separately, so we +// can't assert batchCount < totalChunks strictly. But totalChunks +// should be > 0.) +ok(totalChunks > 0, 'Should have received chunks'); +ok(batchCount > 0, 'Should have received batches'); + +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-iteration-break.mjs b/test/parallel/test-quic-stream-iteration-break.mjs new file mode 100644 index 00000000000000..23febf831b26d6 --- /dev/null +++ b/test/parallel/test-quic-stream-iteration-break.mjs @@ -0,0 +1,58 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: iterator cleanup on break. +// When the consumer breaks out of a for-await loop, the iterator's +// finally block should clean up (clear wakeup, release reader). +// The stream should still be usable for closing. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Send multiple chunks so the client can break mid-stream. + const w = stream.writer; + for (let i = 0; i < 10; i++) { + w.writeSync(encoder.encode(`chunk ${i} `)); + } + w.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('request'), +}); + +// Break out of the iterator after the first batch. +let batchCount = 0; +for await (const batch of stream) { + batchCount++; + ok(Array.isArray(batch)); + break; // Exit early — should trigger iterator cleanup. +} +strictEqual(batchCount, 1); + +// After break, the stream should still be closable. +// End the writable side (it was already ended by the body). +// The stream closed promise should resolve. +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-iteration-destroyed.mjs b/test/parallel/test-quic-stream-iteration-destroyed.mjs new file mode 100644 index 00000000000000..0825b00c94e2d7 --- /dev/null +++ b/test/parallel/test-quic-stream-iteration-destroyed.mjs @@ -0,0 +1,39 @@ +// Flags: --experimental-quic --no-warnings + +// Test: destroyed stream returns finished iterator. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('destroy test'), +}); + +// Destroy the stream immediately. +stream.destroy(); + +// Iterating a destroyed stream should immediately finish. +const iter = stream[Symbol.asyncIterator](); +const { done } = await iter.next(); +strictEqual(done, true); + +await stream.closed; +await clientSession.close(); +await serverEndpoint.destroy(); diff --git a/test/parallel/test-quic-stream-iteration-double.mjs b/test/parallel/test-quic-stream-iteration-double.mjs new file mode 100644 index 00000000000000..e667d5910bf701 --- /dev/null +++ b/test/parallel/test-quic-stream-iteration-double.mjs @@ -0,0 +1,60 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: second iterator rejects when first is active. +// Because [Symbol.asyncIterator]() is an async generator, creating the +// generator always succeeds. The lock check runs inside the body, so +// the ERR_INVALID_STATE manifests as a rejected .next() promise, not +// a synchronous throw on generator creation. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const iter1 = stream[Symbol.asyncIterator](); + // Advance the first iterator so the lock is set. + const first = await iter1.next(); + strictEqual(first.done, false); + + // A second iterator can be created (generator object), but + // advancing it should reject because the lock is held. + const iter2 = stream[Symbol.asyncIterator](); + await rejects(iter2.next(), { + code: 'ERR_INVALID_STATE', + }); + + // Drain the first iterator. + for (;;) { + const { done } = await iter1.next(); + if (done) break; + } + + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('double iter test'), +}); +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-iteration-nonreadable.mjs b/test/parallel/test-quic-stream-iteration-nonreadable.mjs new file mode 100644 index 00000000000000..e7f0ff4dc2c12a --- /dev/null +++ b/test/parallel/test-quic-stream-iteration-nonreadable.mjs @@ -0,0 +1,46 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: non-readable stream returns finished iterator. +// The sender side of a unidirectional stream is not readable, so +// iterating it should immediately return done: true. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await bytes(stream); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createUnidirectionalStream({ + body: encoder.encode('uni data'), +}); + +// The sender side of a uni stream is not readable. +const iter = stream[Symbol.asyncIterator](); +const { done } = await iter.next(); +strictEqual(done, true); + +await Promise.all([serverDone.promise, stream.closed]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-iteration-pipeto.mjs b/test/parallel/test-quic-stream-iteration-pipeto.mjs new file mode 100644 index 00000000000000..a169f2d3af4b4f --- /dev/null +++ b/test/parallel/test-quic-stream-iteration-pipeto.mjs @@ -0,0 +1,48 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: pipeTo pipes a QUIC stream to another writable. +// The server reads from the incoming stream and pipes it to the +// outgoing writer side of the same bidi stream (echo). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes, pipeTo } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const message = 'pipeTo test data'; +const expected = encoder.encode(message); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Pipe the readable side of the stream to the writable side (echo). + // pipeTo(source, destination) where destination is the stream's writer. + await pipeTo(stream, stream.writer); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode(message), +}); + +// Read the echoed data. +const echoed = await bytes(stream); +assert.deepStrictEqual(echoed, expected); + +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-iteration-pull.mjs b/test/parallel/test-quic-stream-iteration-pull.mjs new file mode 100644 index 00000000000000..69dc70f6b7d439 --- /dev/null +++ b/test/parallel/test-quic-stream-iteration-pull.mjs @@ -0,0 +1,52 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: pull applies a transform to a QUIC stream. +// Verifies that pull() can process chunks from a QUIC stream through +// a synchronous transform function. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { deepStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes, pull } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const message = 'pull test'; +const expected = encoder.encode(message); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Use pull with an identity transform — pass chunks through. + const transformed = pull(stream, (chunk) => { + if (chunk === null) return null; + return chunk; + }); + const result = await bytes(transformed); + deepStrictEqual(result, expected); + + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode(message), +}); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-iteration-reset.mjs b/test/parallel/test-quic-stream-iteration-reset.mjs new file mode 100644 index 00000000000000..7b0d077064c563 --- /dev/null +++ b/test/parallel/test-quic-stream-iteration-reset.mjs @@ -0,0 +1,66 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: peer RESET_STREAM causes iterator to error. +// When the server resets the stream, the client's async iterator +// should throw or return early. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { ok, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverReady = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Reset the stream from the server side. + stream.resetStream(42n); + await rejects(stream.closed, mustCall((err) => { + assert.ok(err); + return true; + })); + serverReady.resolve(); + await serverSession.closed; + }); +}), { transportParams: { maxIdleTimeout: 1 } }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams: { maxIdleTimeout: 1 }, +}); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('will be reset by server'), +}); + +// Set up the closed handler before the reset to avoid unhandled rejection. +const closedPromise = rejects(stream.closed, mustCall((err) => { + assert.ok(err); + return true; +})); + +await serverReady.promise; + +// The async iterator should either throw or return early when the +// peer resets the readable side. +try { + for await (const batch of stream) { + // May receive some data before the reset arrives. + ok(Array.isArray(batch)); + } +} catch { + // The iterator may throw when the reset arrives mid-iteration. +} + +// Either way, the stream should close. +await closedPromise; +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-iteration.mjs b/test/parallel/test-quic-stream-iteration.mjs new file mode 100644 index 00000000000000..c33a486276c099 --- /dev/null +++ b/test/parallel/test-quic-stream-iteration.mjs @@ -0,0 +1,81 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream iteration basics. +// All use a single endpoint with one stream each to avoid the +// sequential endpoint bug. +// for-await yields Uint8Array[] batches. +// text() consumes stream as string. +// bytes() consumes stream as Uint8Array. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual, deepStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes, text } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const message = 'iteration test data'; +const expected = encoder.encode(message); + +let streamCount = 0; +const allDone = Promise.withResolvers(); +const totalStreams = 3; + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const idx = streamCount++; + + if (idx === 0) { + // for-await yields batches. + const chunks = []; + for await (const batch of stream) { + ok(Array.isArray(batch), 'batch should be an array'); + for (const chunk of batch) { + ok(chunk instanceof Uint8Array, 'chunk should be Uint8Array'); + chunks.push(chunk); + } + } + ok(chunks.length > 0); + } else if (idx === 1) { + // text() consumes as string. + const result = await text(stream); + strictEqual(typeof result, 'string'); + strictEqual(result, message); + } else if (idx === 2) { + // bytes() consumes as Uint8Array. + const result = await bytes(stream); + ok(result instanceof Uint8Array); + deepStrictEqual(result, expected); + } + + stream.writer.endSync(); + await stream.closed; + + if (streamCount === totalStreams) { + serverSession.close(); + allDone.resolve(); + } + }, totalStreams); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Send three streams sequentially. +for (let i = 0; i < totalStreams; i++) { + const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode(message), + }); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +await allDone.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-limits-pending.mjs b/test/parallel/test-quic-stream-limits-pending.mjs new file mode 100644 index 00000000000000..fed900696d91c5 --- /dev/null +++ b/test/parallel/test-quic-stream-limits-pending.mjs @@ -0,0 +1,71 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream limits and pending behavior. +// initialMaxStreamsBidi limits concurrent bidi streams. +// When the limit is reached, new streams are queued as pending +// and open when existing streams close. +// initialMaxStreamsUni limits concurrent uni streams (same behavior). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const allDone = Promise.withResolvers(); +let serverStreamCount = 0; + +// Server allows only 1 bidi stream at a time. +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await bytes(stream); + stream.writer.endSync(); + await stream.closed; + if (++serverStreamCount === 2) { + serverSession.close(); + allDone.resolve(); + } + }, 2); +}), { + transportParams: { initialMaxStreamsBidi: 1 }, +}); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// First stream opens immediately (within the limit). +const s1 = await clientSession.createBidirectionalStream({ + body: encoder.encode('stream 1'), +}); + +// Second stream is created but queued as pending because the +// server only allows 1 concurrent bidi stream. +const s2 = await clientSession.createBidirectionalStream({ + body: encoder.encode('stream 2'), +}); + +// s2 should be pending until s1 closes and the server grants +// more stream credits. +strictEqual(s2.pending, true); + +// Drain and close the first stream. +for await (const _ of s1) { /* drain */ } // eslint-disable-line no-unused-vars +await s1.closed; + +// After s1 closes, the server sends MAX_STREAMS which opens s2. +// Wait for the server to receive both streams. +await allDone.promise; + +// s2 should no longer be pending. +for await (const _ of s2) { /* drain */ } // eslint-disable-line no-unused-vars +await s2.closed; + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-limits-uni.mjs b/test/parallel/test-quic-stream-limits-uni.mjs new file mode 100644 index 00000000000000..4851b06a3cdffe --- /dev/null +++ b/test/parallel/test-quic-stream-limits-uni.mjs @@ -0,0 +1,56 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: uni stream limits and pending behavior. +// initialMaxStreamsUni = 1 limits concurrent uni streams. The second +// stream is queued as pending and opens after the first closes. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const allDone = Promise.withResolvers(); +let serverStreamCount = 0; + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await bytes(stream); + await stream.closed; + if (++serverStreamCount === 2) { + serverSession.close(); + allDone.resolve(); + } + }, 2); +}), { + transportParams: { initialMaxStreamsUni: 1 }, +}); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// First uni stream opens immediately. +const s1 = await clientSession.createUnidirectionalStream({ + body: encoder.encode('uni 1'), +}); + +// Second uni stream is pending (limit = 1). +const s2 = await clientSession.createUnidirectionalStream({ + body: encoder.encode('uni 2'), +}); +strictEqual(s2.pending, true); + +// Wait for both to complete. +await s1.closed; +await allDone.promise; +await s2.closed; + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-many-rapid.mjs b/test/parallel/test-quic-stream-many-rapid.mjs new file mode 100644 index 00000000000000..77aaf0432430dc --- /dev/null +++ b/test/parallel/test-quic-stream-many-rapid.mjs @@ -0,0 +1,58 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: many streams opened and closed rapidly. +// Open 50 bidirectional streams in rapid succession, each with a +// small body. All streams should close successfully and the server +// should receive all data. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const streamCount = 50; +const encoder = new TextEncoder(); +let serverReceived = 0; +const allReceived = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await bytes(stream); + strictEqual(data.byteLength, 5); + stream.writer.endSync(); + await stream.closed; + if (++serverReceived === streamCount) { + serverSession.close(); + allReceived.resolve(); + } + }, streamCount); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Open 50 streams rapidly, each with a small body. +const streams = []; +for (let i = 0; i < streamCount; i++) { + const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('hello'), + }); + streams.push(stream); +} + +// Wait for all client streams to close. +await Promise.all(streams.map(async (stream) => { + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +})); + +await allReceived.promise; +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-onblocked.mjs b/test/parallel/test-quic-stream-onblocked.mjs new file mode 100644 index 00000000000000..3532d70fc5c32a --- /dev/null +++ b/test/parallel/test-quic-stream-onblocked.mjs @@ -0,0 +1,73 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream.onblocked fires when flow control blocks a stream. +// When the peer's stream-level receive window is exhausted, ngtcp2 returns +// NGTCP2_ERR_STREAM_DATA_BLOCKED. The stream is unscheduled and the +// onblocked callback fires. The stream resumes automatically when the peer +// sends MAX_STREAM_DATA to extend the window. +// Strategy: set the body to a buffer larger than the flow control window. +// ngtcp2 sends the initial window then blocks. onblocked fires. Flow +// control extension eventually unblocks and the full transfer completes. + +import { hasQuic, skip, mustCall, mustCallAtLeast } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +// quic.stream.blocked fires when a stream is flow-control blocked. +dc.subscribe('quic.stream.blocked', mustCallAtLeast((msg) => { + ok(msg.stream, 'stream.blocked should include stream'); + ok(msg.session, 'stream.blocked should include session'); +}, 1)); + +const totalSize = 4096; +const body = new Uint8Array(totalSize); +for (let i = 0; i < totalSize; i++) body[i] = i & 0xff; + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, totalSize); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + // Small stream window — forces stream-level flow control blocking. + transportParams: { initialMaxStreamDataBidiRemote: 256 }, +}); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); + +let blockedCount = 0; +stream.onblocked = mustCallAtLeast(() => { + blockedCount++; +}, 1); + +// Set the body via setBody() — larger than the flow control window. +// ngtcp2 sends the first 256 bytes then returns +// NGTCP2_ERR_STREAM_DATA_BLOCKED, triggering onblocked. +stream.setBody(body); + +for await (const _ of stream) { /* drain readable side */ } // eslint-disable-line no-unused-vars +await stream.closed; +await serverDone.promise; + +ok(blockedCount > 0, `Expected onblocked to fire, got ${blockedCount} calls`); + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-pending.mjs b/test/parallel/test-quic-stream-pending.mjs new file mode 100644 index 00000000000000..9b847a9ae6ff00 --- /dev/null +++ b/test/parallel/test-quic-stream-pending.mjs @@ -0,0 +1,57 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: pending streams. +// Stream created before handshake completes, opens after. +// stream.pending is true before open, false after. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(new TextDecoder().decode(received), 'pending stream'); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); + +// Create a stream BEFORE awaiting opened — the handshake may not have +// completed yet. The stream should be created in a pending state. +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('pending stream'), +}); + +// The stream should initially be pending (no ID assigned yet). +// On fast machines the handshake might already be done. +strictEqual(typeof stream.pending, 'boolean'); + +// The server's onstream fires only after the handshake completes AND +// the pending stream opens. By the time we get data on the server, +// the stream is definitely no longer pending. +await serverDone.promise; + +// After the server received data, the stream opened successfully. +// The data arrival proves (pending stream opens after handshake). + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await stream.closed; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-priority.mjs b/test/parallel/test-quic-stream-priority.mjs new file mode 100644 index 00000000000000..bed1fd065fdfc9 --- /dev/null +++ b/test/parallel/test-quic-stream-priority.mjs @@ -0,0 +1,96 @@ +// Flags: --experimental-quic --no-warnings + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +const { readKey } = fixtures; + +const { rejects, strictEqual, throws } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + await serverSession.close(); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], +}); + +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', +}); +await clientSession.opened; + +// Collect stream.closed promises so we can await them all at the end. +// We must not await them inline because the server's CONNECTION_CLOSE +// arrives asynchronously and would put the session into a closing state, +// preventing subsequent createBidirectionalStream calls. +const streamClosedPromises = []; + +// Test 1: Priority getter returns null for non-HTTP/3 sessions. +// setPriority throws because the session doesn't support priority. +{ + const stream = await clientSession.createBidirectionalStream(); + streamClosedPromises.push(stream.closed); + strictEqual(stream.priority, null); + + throws( + () => stream.setPriority({ level: 'high', incremental: true }), + { code: 'ERR_INVALID_STATE' }, + ); +} + +// Test 2: Validation of createStream priority/incremental options +{ + await rejects( + clientSession.createBidirectionalStream({ priority: 'urgent' }), + { code: 'ERR_INVALID_ARG_VALUE' }, + ); + await rejects( + clientSession.createBidirectionalStream({ priority: 42 }), + { code: 'ERR_INVALID_ARG_VALUE' }, + ); + await rejects( + clientSession.createBidirectionalStream({ incremental: 'yes' }), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + await rejects( + clientSession.createBidirectionalStream({ incremental: 1 }), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +// Test 3: setPriority throws on non-H3 sessions regardless of arguments +{ + const stream = await clientSession.createBidirectionalStream(); + streamClosedPromises.push(stream.closed); + + throws( + () => stream.setPriority({ level: 'high' }), + { code: 'ERR_INVALID_STATE' }, + ); + throws( + () => stream.setPriority({ level: 'low', incremental: true }), + { code: 'ERR_INVALID_STATE' }, + ); + throws( + () => stream.setPriority(), + { code: 'ERR_INVALID_STATE' }, + ); +} + +// Wait for all streams to close (they close when the session closes +// in response to the server's CONNECTION_CLOSE). +await Promise.all(streamClosedPromises); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-read-after-blocked.mjs b/test/parallel/test-quic-stream-read-after-blocked.mjs new file mode 100644 index 00000000000000..58551abe07212c --- /dev/null +++ b/test/parallel/test-quic-stream-read-after-blocked.mjs @@ -0,0 +1,62 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: a sender blocked on flow control is resumed once the consumer +// starts reading. This confirms that we do flush MAX_STREAM_DATA +// frames as expected when the consumer reads. + +import { hasQuic, skip, mustCall, mustCallAtLeast } from '../common/index.mjs'; +import { setTimeout as delay } from 'node:timers/promises'; +import assert from 'node:assert'; + +const { strictEqual, deepStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +// Larger than the default 256 KB stream flow-control window: +const size = 1024 * 1024; +const expected = new Uint8Array(size); +for (let i = 0; i < size; i++) expected[i] = i & 0xff; + +const senderBlocked = Promise.withResolvers(); +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + stream.onblocked = mustCallAtLeast(() => senderBlocked.resolve(stream), 1); + stream.setBody(expected); + await stream.closed; + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Write a byte to open the stream: +const stream = await clientSession.createBidirectionalStream(); +await stream.writer.write(new Uint8Array([1])); + +// Wait until the sender has filled the window and blocked. +const serverStream = await senderBlocked.promise; + +// Poll until everything has been acked, so the stream is fully idle: +while (serverStream.stats.maxOffsetAcknowledged !== serverStream.stats.bytesSent) { + await delay(1); +} + +// Try to read: +const received = await bytes(stream); +strictEqual(received.byteLength, expected.byteLength); +deepStrictEqual(received, expected); + +stream.writer.endSync(); +await stream.closed; +clientSession.close(); +done.resolve(); + +await Promise.all([done.promise, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-read-after-fin.mjs b/test/parallel/test-quic-stream-read-after-fin.mjs new file mode 100644 index 00000000000000..b680a9d7c4b7a4 --- /dev/null +++ b/test/parallel/test-quic-stream-read-after-fin.mjs @@ -0,0 +1,57 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: reading a stream after its read side has received FIN, although +// the stream is still alive. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import { setTimeout as delay } from 'node:timers/promises'; +import assert from 'node:assert'; + +const { deepStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const expected = new TextEncoder().encode('data sent before any reader'); +const serverSent = Promise.withResolvers(); +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Reply with the full body plus FIN: + stream.setBody(expected); + serverSent.resolve(); + await stream.closed; + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); + +// Write a byte to open the stream: +const writer = stream.writer; +await writer.write(new Uint8Array([1])); + +// Wait until the server has sent its response, with a buffer for the local +// delivery time itself. We can't actually read to check without undermining +// the test itself unfortunately. +await serverSent.promise; +await delay(10); + +const received = await bytes(stream); +deepStrictEqual(received, expected); + +writer.endSync(); +await stream.closed; +clientSession.close(); +done.resolve(); + +await done.promise; +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-recv-coalescing.mjs b/test/parallel/test-quic-stream-recv-coalescing.mjs new file mode 100644 index 00000000000000..0eb2814c1d0d26 --- /dev/null +++ b/test/parallel/test-quic-stream-recv-coalescing.mjs @@ -0,0 +1,132 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: receive-side coalescing produces reasonably-sized chunks. +// Sends multiple payloads of varying sizes through a bidi stream and +// verifies that the received chunks are coalesced rather than +// delivered as ~1154-byte QUIC frame fragments. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { drainableProtocol: dp } = await import('stream/iter'); + +// Payloads of varying sizes with diverse prime factors to exercise +// different flow control boundary alignments. Avoids round multiples +// of 1000 so that frame boundaries, accumulation buffer thresholds, +// and flow control windows are hit at irregular offsets. +const payloadSizes = [ + 97, // Small prime: fits in a single frame + 4_999, // Medium prime: a few frames + 65_521, // Largest prime below 64 KB buffer flush threshold + 1_153, // Small prime after large + 196_613, // Large prime: well beyond 64 KB buffer + 509, // Small prime tail +]; +const totalBytes = payloadSizes.reduce((a, b) => a + b, 0); + +// Build deterministic payloads for integrity checking. +function buildPayload(size, seed) { + const buf = new Uint8Array(size); + for (let i = 0; i < size; i++) { + buf[i] = (seed + i) & 0xff; + } + return buf; +} + +function checksum(data) { + let sum = 0; + for (let i = 0; i < data.byteLength; i++) { + sum = (sum + data[i]) | 0; + } + return sum; +} + +// Compute expected checksum across all payloads. +let expectedChecksum = 0; +for (let i = 0; i < payloadSizes.length; i++) { + expectedChecksum = (expectedChecksum + + checksum(buildPayload(payloadSizes[i], i))) | 0; +} + +// --- Server side --- + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const receivedChunkSizes = []; + let receivedBytes = 0; + let receivedChecksum = 0; + + for await (const chunks of stream) { + for (const chunk of chunks) { + receivedChunkSizes.push(chunk.byteLength); + receivedBytes += chunk.byteLength; + receivedChecksum = (receivedChecksum + checksum(chunk)) | 0; + } + } + + // Data integrity. + strictEqual(receivedBytes, totalBytes); + strictEqual(receivedChecksum, expectedChecksum); + + // Coalescing effectiveness: verify that at least some chunks are larger + // than the ~1154-byte QUIC frame payload, showing that the accumulator + // is combining callbacks. On localhost, round-trip latency is near + // zero so coalescing opportunities are limited to I/O bursts where + // multiple packets arrive in the same event loop iteration. Under + // real network conditions the effect is much more pronounced. + const maxChunkSize = Math.max(...receivedChunkSizes); + ok( + maxChunkSize > 1154, + `Expected at least one chunk larger than a single QUIC frame ` + + `(1154 bytes), but max was ${maxChunkSize}` + ); + + // Stats: after full drain, bytes_accumulated should be 0. + strictEqual(stream.stats.bytesAccumulated, 0n); + + // max_bytes_accumulated should be nonzero — data passed through + // the accumulator. + ok( + stream.stats.maxBytesAccumulated > 0n, + 'maxBytesAccumulated should be nonzero' + ); + + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +// --- Client side --- + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +const w = stream.writer; + +for (let i = 0; i < payloadSizes.length; i++) { + const payload = buildPayload(payloadSizes[i], i); + while (!w.writeSync(payload)) { + const drainable = w[dp](); + if (drainable) await drainable; + } +} +w.endSync(); + +// Drain the server's empty response to let the stream close cleanly. +// eslint-disable-next-line no-unused-vars +for await (const _ of stream) { /* drain response */ } +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-reset-after-data.mjs b/test/parallel/test-quic-stream-reset-after-data.mjs new file mode 100644 index 00000000000000..75f3650b2c3a2a --- /dev/null +++ b/test/parallel/test-quic-stream-reset-after-data.mjs @@ -0,0 +1,67 @@ +// Flags: --experimental-quic --no-warnings + +// Test: resetStream() after all data written but before ACK. +// The stream is in the Data Sent state — all data has been sent +// including FIN, but the peer hasn't acknowledged everything yet. +// Calling resetStream() aborts the stream. The server's onreset +// callback fires with the error code. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); +const serverDone = Promise.withResolvers(); +const serverReady = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall((stream) => { + rejects(stream.closed, (error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + return true; + }).then(mustCall()); + + stream.onreset = mustCall((error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes('44')); + serverSession.close(); + serverDone.resolve(); + }); + serverReady.resolve(); + }); +}), { + onerror: mustNotCall(), +}); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Send a small body — it will be sent quickly (including FIN), +// putting the stream in "Data Sent" state. +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('small payload'), +}); + +// Wait for the server to receive the stream before resetting. +await serverReady.promise; + +// Reset after data was written. The data and FIN have been sent +// but may not be fully acknowledged yet. +stream.resetStream(44n); + +await rejects(stream.closed, (error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes('44')); + return true; +}); + +await serverDone.promise; +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-reset-before-data.mjs b/test/parallel/test-quic-stream-reset-before-data.mjs new file mode 100644 index 00000000000000..cb298aef3a7fda --- /dev/null +++ b/test/parallel/test-quic-stream-reset-before-data.mjs @@ -0,0 +1,83 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: resetStream() before any data is written. +// The stream is in the Ready state — no data has been sent by the +// client. The client calls resetStream() which sends RESET_STREAM +// to the server. The server receives the stream via onstream (the +// RESET_STREAM implicitly creates the bidi stream), and onreset +// fires. The server then sends data back on its side of the bidi +// stream, which the client reads — verifying that even when the +// client's send side is reset, the server can still use its send +// side and the client can still receive. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; + +const { ok, deepStrictEqual, strictEqual, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +// quic.stream.reset fires when a stream receives RESET_STREAM from the peer. +dc.subscribe('quic.stream.reset', mustCall((msg) => { + ok(msg.stream, 'stream.reset should include stream'); + ok(msg.session, 'stream.reset should include session'); + ok(msg.error, 'stream.reset should include error'); +})); + +const encoder = new TextEncoder(); +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + stream.onreset = mustCall((error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes('42')); + + // The client reset its send side, but the server can still + // send data on its side of the bidi stream. + stream.setBody(encoder.encode('response')); + }); + + // The stream's closed promise may reject because the client's + // send side was reset. Either way, clean up. + await rejects(stream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address, { + onerror: mustNotCall(), +}); +await clientSession.opened; + +// Create a bidi stream but do NOT write any data. +const stream = await clientSession.createBidirectionalStream(); + +// Reset immediately — no data was ever written. This sends +// RESET_STREAM to the server which implicitly creates the bidi +// stream on the server side. +stream.resetStream(42n); + +// The client should still be able to receive data from the server +// on the readable side of this bidi stream. +const received = await bytes(stream); +deepStrictEqual(Buffer.from(received), Buffer.from('response')); + +// stream.closed rejects with the reset error (the client's send +// side was reset). Verify the error and consume the rejection. +await stream.closed.catch((error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes('42')); +}); +await serverDone.promise; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-reset-mid-transfer.mjs b/test/parallel/test-quic-stream-reset-mid-transfer.mjs new file mode 100644 index 00000000000000..1c32a5ee28ea74 --- /dev/null +++ b/test/parallel/test-quic-stream-reset-mid-transfer.mjs @@ -0,0 +1,66 @@ +// Flags: --experimental-quic --no-warnings + +// Test: resetStream() mid-transfer. +// The stream is in the Send state — data is being sent. Calling +// resetStream() aborts the transfer. The server's onreset callback +// fires with the error code. The server may receive partial data +// before the reset. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverDone = Promise.withResolvers(); +const serverReady = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall((stream) => { + rejects(stream.closed, (error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + return true; + }).then(mustCall()); + + stream.onreset = mustCall((error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes('43')); + serverSession.close(); + serverDone.resolve(); + }); + serverReady.resolve(); + }); +}), { + // Small flow control window to keep data in flight longer. + transportParams: { initialMaxStreamDataBidiRemote: 256 }, + onerror: mustNotCall(), +}); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Send a large body — with the 256-byte flow control window, the +// transfer will be in progress when we reset. +const stream = await clientSession.createBidirectionalStream(); +stream.setBody(new Uint8Array(8192)); + +// Wait for the server to receive the stream (first STREAM frames). +await serverReady.promise; + +// Reset mid-transfer. +stream.resetStream(43n); + +await rejects(stream.closed, (error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes('43')); + return true; +}); + +await serverDone.promise; +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-reset-stop.mjs b/test/parallel/test-quic-stream-reset-stop.mjs new file mode 100644 index 00000000000000..6741dabfef5b7b --- /dev/null +++ b/test/parallel/test-quic-stream-reset-stop.mjs @@ -0,0 +1,65 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: RESET_STREAM and STOP_SENDING. +// server's onreset fires with that code. +// NOTE: CTRL-01/CTRL-08 (stopSending with specific code) is tested +// separately because it requires a second endpoint. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); +const serverReady = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall((stream) => { + // The server's stream.closed will reject when the session is + // gracefully closed after the peer's reset. + rejects(stream.closed, (error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + return true; + }).then(mustCall()); + + stream.onreset = mustCall((error) => { + // The error is the raw close tuple: [type, code, reason]. + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes('42')); + serverSession.close(); + serverDone.resolve(); + }); + serverReady.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('will be reset'), +}); + +// Wait for the server to receive the stream before resetting. +await serverReady.promise; +stream.resetStream(42n); + +await serverDone.promise; +// After the server closes (sending CONNECTION_CLOSE), the client +// session enters draining and all streams are destroyed. The client's +// stream.closed rejects with the reset code. +await rejects(stream.closed, (error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes('42')); + return true; +}); +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-setbody-errors.mjs b/test/parallel/test-quic-stream-setbody-errors.mjs new file mode 100644 index 00000000000000..4b41ac4cb66ea3 --- /dev/null +++ b/test/parallel/test-quic-stream-setbody-errors.mjs @@ -0,0 +1,63 @@ +// Flags: --experimental-quic --no-warnings + +// Test: setBody throws when body already configured or writer +// already accessed. +// Writer throws ERR_INVALID_STATE if body was already set. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { throws } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Test 1: setBody after setBody throws. +{ + const stream = await clientSession.createBidirectionalStream(); + stream.setBody(encoder.encode('first')); + + throws(() => stream.setBody(encoder.encode('second')), { + code: 'ERR_INVALID_STATE', + message: /outbound already configured/, + }); + + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +// Test 2: setBody after writer accessed throws. +{ + const stream = await clientSession.createBidirectionalStream(); + // Access the writer — this prevents setBody from being used. + const w = stream.writer; + w.endSync(); + + throws(() => stream.setBody(encoder.encode('data')), { + code: 'ERR_INVALID_STATE', + message: /writer already accessed/, + }); + + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-slow-consumer.mjs b/test/parallel/test-quic-stream-slow-consumer.mjs new file mode 100644 index 00000000000000..98d45f7474146a --- /dev/null +++ b/test/parallel/test-quic-stream-slow-consumer.mjs @@ -0,0 +1,58 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: slow consumer applies backpressure. +// With a small flow control window and a large body, the sender +// blocks waiting for the receiver to extend the window. The transfer +// completes when the receiver reads the data. + +import { hasQuic, skip, mustCall, mustCallAtLeast } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const dataLength = 4096; +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, dataLength); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +}), { + // Small stream window forces the sender to block repeatedly. + transportParams: { initialMaxStreamDataBidiRemote: 256 }, +}); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +let blockedCount = 0; +const stream = await clientSession.createBidirectionalStream(); + +// The actual number of blocks can vary on a range of factors. We're +// only validating that blocking occurs at least once. +stream.onblocked = mustCallAtLeast(() => { + blockedCount++; +}, 1); + +stream.setBody(new Uint8Array(dataLength)); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); + +// The sender should have been blocked multiple times. +ok(blockedCount > 0, `Expected blocking, got ${blockedCount}`); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-stats.mjs b/test/parallel/test-quic-stream-stats.mjs new file mode 100644 index 00000000000000..8d002bc7248fd9 --- /dev/null +++ b/test/parallel/test-quic-stream-stats.mjs @@ -0,0 +1,73 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream stats fields. +// Verify that stream stats are populated with correct types and +// that bytesReceived/bytesSent reflect actual data transfer. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const payload = encoder.encode('stream stats test data'); +const payloadLength = payload.byteLength; +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await bytes(stream); + strictEqual(data.byteLength, payloadLength); + + // Stream stats should reflect received bytes. + strictEqual(stream.stats.bytesReceived, BigInt(payloadLength)); + strictEqual(typeof stream.stats.createdAt, 'bigint'); + strictEqual(typeof stream.stats.receivedAt, 'bigint'); + + // Send response. + stream.setBody(encoder.encode('response')); + await stream.closed; + + // After close, bytesSent should reflect response. + strictEqual(stream.stats.bytesSent, BigInt('response'.length)); + + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: payload, +}); + +// Stats should have correct types before transfer completes. +strictEqual(typeof stream.stats.createdAt, 'bigint'); +strictEqual(typeof stream.stats.bytesReceived, 'bigint'); +strictEqual(typeof stream.stats.bytesSent, 'bigint'); +strictEqual(typeof stream.stats.maxOffset, 'bigint'); + +// Verify toJSON works. +const json = stream.stats.toJSON(); +ok(json); +strictEqual(typeof json.createdAt, 'string'); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await stream.closed; +await serverDone.promise; + +// After transfer, bytesSent should reflect the payload. +strictEqual(stream.stats.bytesSent, BigInt(payloadLength)); +ok(stream.stats.bytesReceived > 0n); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-stop-sending-interaction.mjs b/test/parallel/test-quic-stream-stop-sending-interaction.mjs new file mode 100644 index 00000000000000..c7b80402260dc6 --- /dev/null +++ b/test/parallel/test-quic-stream-stop-sending-interaction.mjs @@ -0,0 +1,78 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: STOP_SENDING / RESET_STREAM interaction. +// When the peer sends STOP_SENDING, the local sending side is +// notified — the stream closes on the sender side. +// Receiving STOP_SENDING automatically triggers RESET_STREAM +// to the peer (ngtcp2 handles this internally). Verified by +// the server's stream.closed rejecting with the error code. +// The error code from STOP_SENDING is copied to the automatic +// RESET_STREAM — the server's stream.closed rejects with the +// same code that was passed to stopSending(). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual, rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); +const stopCode = 77n; + +const serverDone = Promise.withResolvers(); +const clientStreamReady = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Wait for the client stream to be fully set up. + await clientStreamReady.promise; + + // Send STOP_SENDING with a specific error code. + stream.stopSending(stopCode); + + // Send data from server to client (the other direction is unaffected). + const w = stream.writer; + w.writeSync(encoder.encode('server data')); + w.endSync(); + + // The server's stream.closed rejects because + // the client automatically sends RESET_STREAM in response to + // STOP_SENDING. The error code matches the STOP_SENDING code. + await rejects(stream.closed, (error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes(String(stopCode))); + return true; + }); + + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('initial data'), +}); +clientStreamReady.resolve(); + +// Read the server's data. The server→client direction is unaffected +// by STOP_SENDING on the client→server direction. +const received = await bytes(stream); +strictEqual(new TextDecoder().decode(received), 'server data'); + +// The client's stream.closed resolves. The STOP_SENDING caused +// the client's write side to end (ngtcp2 sends RESET_STREAM +// automatically), but from the client's JS perspective the stream +// completed: the read side got FIN from the server, and the write +// side was handled internally by ngtcp2. stream.closed only rejects +// on the side that receives the RESET_STREAM (the server). +await Promise.all([stream.closed, serverDone.promise, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-stop-sending.mjs b/test/parallel/test-quic-stream-stop-sending.mjs new file mode 100644 index 00000000000000..0b2b9edd75db7a --- /dev/null +++ b/test/parallel/test-quic-stream-stop-sending.mjs @@ -0,0 +1,54 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stopSending. +// Server calls stopSending(99n) on an incoming stream from the client. +// The server's stream.closed rejects with error code 99 (the stop +// sending code). The client's stream.closed resolves normally because +// the server's write side completed (endSync sent FIN). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Tell the client to stop sending with code 99. + stream.stopSending(99n); + stream.writer.endSync(); + + // The server's stream.closed rejects with the stop-sending code + // because the inbound side was reset by the peer in response. + await rejects(stream.closed, (error) => { + strictEqual(error.code, 'ERR_QUIC_APPLICATION_ERROR'); + ok(error.message.includes('99')); + return true; + }); + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + body: encoder.encode('stop me'), +}); + +// The client's stream.closed resolves because the server sent FIN +// on its write side (endSync) and the read side completed normally. + +await Promise.all([serverDone.promise, stream.closed]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-uni-basic.mjs b/test/parallel/test-quic-stream-uni-basic.mjs new file mode 100644 index 00000000000000..4ab4c31e094252 --- /dev/null +++ b/test/parallel/test-quic-stream-uni-basic.mjs @@ -0,0 +1,65 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: basic unidirectional stream data transfer. +// The client creates a unidirectional stream with a body. The server reads +// the data and verifies integrity. The unidirectional stream is write-only +// on the client side and read-only on the server side. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { deepStrictEqual, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const message = 'unidirectional payload'; +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const body = encoder.encode(message); +const expected = encoder.encode(message); + +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + strictEqual(stream.direction, 'uni'); + + const received = await bytes(stream); + deepStrictEqual(received, expected); + strictEqual(decoder.decode(received), message); + + // The server side of a remote unidirectional stream is not writable. + // The writer should be pre-closed (desiredSize returns null). + const w = stream.writer; + strictEqual(w.desiredSize, null); + strictEqual(w.endSync(), 0); + + await stream.closed; + serverSession.close(); + done.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createUnidirectionalStream({ body }); +strictEqual(stream.direction, 'uni'); + +// The client-side uni stream is write-only — async iteration yields nothing. +const iter = stream[Symbol.asyncIterator](); +const { done: iterDone } = await iter.next(); +strictEqual(iterDone, true); + +await done.promise; +// The server closed its session, delivering CONNECTION_CLOSE to the client. +// The client session enters the draining period, after which all streams +// and the session itself close cleanly. +await stream.closed; +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-uni-server-initiated.mjs b/test/parallel/test-quic-stream-uni-server-initiated.mjs new file mode 100644 index 00000000000000..7853938626617d --- /dev/null +++ b/test/parallel/test-quic-stream-uni-server-initiated.mjs @@ -0,0 +1,58 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: server-initiated unidirectional stream. +// The server creates a uni stream and sends data to the client. +// The client receives the data via its onstream handler and verifies +// integrity. The receiving side should not have a usable writer. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { deepStrictEqual, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const message = 'server uni stream data'; +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const expected = encoder.encode(message); + +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + + const stream = await serverSession.createUnidirectionalStream({ + body: encoder.encode(message), + }); + + // Uni stream has no readable side for the sender. + await stream.closed; +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +clientSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + + deepStrictEqual(received, expected); + strictEqual(decoder.decode(received), message); + + // The receiving side of a uni stream should not be writable. + // The writer should be pre-closed. + const w = stream.writer; + strictEqual(w.desiredSize, null); + + await stream.closed; + clientSession.close(); + done.resolve(); +}); + +await done.promise; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-write-partial-view.mjs b/test/parallel/test-quic-stream-write-partial-view.mjs new file mode 100644 index 00000000000000..fcb3db58cc84bc --- /dev/null +++ b/test/parallel/test-quic-stream-write-partial-view.mjs @@ -0,0 +1,75 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// writer.writeSync() must not detach the caller's underlying +// ArrayBuffer. The bytes from each write are copied into an internal +// buffer, so the caller's source ArrayBuffer remains live and may be +// reused, mutated, or sliced into additional views — including +// successive subarrays of the same Uint8Array. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +// Eight bytes split into two 4-byte halves. +const source = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); +const sourceCopy = source.slice(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + // Server receives the original 8 bytes in order, regardless of + // any caller-side mutation that happens after writeSync returns. + strictEqual(received.length, sourceCopy.length); + for (let i = 0; i < sourceCopy.length; i++) { + strictEqual(received[i], sourceCopy[i], + `byte ${i} mismatch: got ${received[i]}, ` + + `expected ${sourceCopy[i]}`); + } + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +const writer = stream.writer; + +// First half. The underlying ArrayBuffer must stay live after the +// write so that the caller can build the next view from it. +writer.writeSync(source.subarray(0, 4)); +strictEqual(source.buffer.detached, false, + 'source ArrayBuffer must not be detached after writeSync'); +strictEqual(source.byteLength, 8, + 'source view must remain usable after writeSync'); + +// Second half — slicing into the same backing buffer must succeed. +writer.writeSync(source.subarray(4, 8)); +strictEqual(source.buffer.detached, false, + 'source ArrayBuffer must remain live after second writeSync'); + +// The C++ layer has already copied the bytes by the time writeSync +// returned. Mutating the source here must not affect the data the +// peer ultimately observes. +for (let i = 0; i < source.length; i++) source[i] = 0; +ok(source.every((b) => b === 0), 'source mutation should succeed'); + +writer.endSync(); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-writer-api.mjs b/test/parallel/test-quic-stream-writer-api.mjs new file mode 100644 index 00000000000000..6003473584c51d --- /dev/null +++ b/test/parallel/test-quic-stream-writer-api.mjs @@ -0,0 +1,144 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: writer API methods (WRIT-02, WRIT-03, WRIT-04, WRIT-06, +// WRIT-07, WRIT-08, WRIT-12, WRIT-15). +// Uses a single endpoint with multiple streams, one per test. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); + +const totalStreams = 5; +const serverResults = []; +const allDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + serverResults.push(received); + stream.writer.endSync(); + await stream.closed; + + if (serverResults.length === totalStreams) { + serverSession.close(); + allDone.resolve(); + } + }, totalStreams); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// write() async +{ + const stream = await clientSession.createBidirectionalStream(); + const w = stream.writer; + await w.write(encoder.encode('async write')); + const n = w.endSync(); + strictEqual(n, 11); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +// writevSync() vectored write +{ + const stream = await clientSession.createBidirectionalStream(); + const w = stream.writer; + const result = w.writevSync([ + encoder.encode('hello '), + encoder.encode('writev'), + ]); + strictEqual(result, true); + const n = w.endSync(); + strictEqual(n, 12); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +// writev() async vectored write +{ + const stream = await clientSession.createBidirectionalStream(); + const w = stream.writer; + await w.writev([ + encoder.encode('async '), + encoder.encode('writev'), + ]); + const n = w.endSync(); + strictEqual(n, 12); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +// end() async close +{ + const stream = await clientSession.createBidirectionalStream(); + const w = stream.writer; + w.writeSync(encoder.encode('end async')); + const n = await w.end(); + strictEqual(n, 9); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +{ + const stream = await clientSession.createBidirectionalStream(); + const w = stream.writer; + // desiredSize should be a number (may be 0 initially before flow + // control window opens, or > 0 if the window is already open). + strictEqual(typeof w.desiredSize, 'number'); + ok(w.desiredSize >= 0, `desiredSize should be >= 0, got ${w.desiredSize}`); + // drainableProtocol should return null when desiredSize > 0 (has capacity), + // or a promise when desiredSize <= 0 (backpressured). Either way, it + // should not throw. + const { drainableProtocol: dp } = await import('stream/iter'); + const drain = w[dp](); + ok(drain === null || drain instanceof Promise); + w.writeSync(encoder.encode('capacity')); + w.endSync(); + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + await stream.closed; +} + +// Return null when errored. +{ + const stream = await clientSession.createBidirectionalStream(); + const w = stream.writer; + const testError = new Error('writer fail test'); + w.fail(testError); + // After fail, desiredSize is null. + strictEqual(w.desiredSize, null); + // drainableProtocol returns null when errored. + const { drainableProtocol: dp } = await import('stream/iter'); + strictEqual(w[dp](), null); + // endSync after fail returns -1 (errored). + strictEqual(w.endSync(), -1); + // WriteSync after fail returns false. + strictEqual(w.writeSync(encoder.encode('x')), false); + // Write after fail throws with the original error. + await rejects(w.write(encoder.encode('x')), testError); + // Don't await stream.closed here — the reset stream may not trigger + // server onstream (no data was sent before fail), so the server + // won't count it. The stream is cleaned up when the session closes. +} + +await allDone.promise; +await clientSession.close(); +await serverEndpoint.close(); + +// Verify server received the right data. +const decoder = new TextDecoder(); +strictEqual(decoder.decode(serverResults[0]), 'async write'); +strictEqual(decoder.decode(serverResults[1]), 'hello writev'); +strictEqual(decoder.decode(serverResults[2]), 'async writev'); +strictEqual(decoder.decode(serverResults[3]), 'end async'); +strictEqual(decoder.decode(serverResults[4]), 'capacity'); diff --git a/test/parallel/test-quic-stream-writer-dispose.mjs b/test/parallel/test-quic-stream-writer-dispose.mjs new file mode 100644 index 00000000000000..3899f2f2973636 --- /dev/null +++ b/test/parallel/test-quic-stream-writer-dispose.mjs @@ -0,0 +1,50 @@ +// Flags: --experimental-quic --no-warnings + +// Test: writer Symbol.dispose. +// Symbol.dispose calls fail() if the writer is not already closed/errored. +// After disposal, the writer is in an errored state. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const transportParams = { maxIdleTimeout: 1 }; + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + // The server session will close via idle timeout because the client + // resets the stream before any data is sent. + await serverSession.closed; +}), { transportParams }); + +const clientSession = await connect(serverEndpoint.address, { + transportParams, +}); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +const w = stream.writer; + +// Writer is active — desiredSize should be a number (not null). +strictEqual(typeof w.desiredSize, 'number'); + +// Symbol.dispose calls fail() if not already closed/errored. +w[Symbol.dispose](); + +// After dispose, writer should be errored. +strictEqual(w.desiredSize, null); +strictEqual(w.writeSync(encoder.encode('x')), false); + +// stream.closed resolves because fail() with default code 0 +// is treated as a clean close (no error). +// The session will close via idle timeout or CONNECTION_CLOSE. +await Promise.all([stream.closed, clientSession.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-writer-fail-error-code.mjs b/test/parallel/test-quic-stream-writer-fail-error-code.mjs new file mode 100644 index 00000000000000..ff3c18f69c5d03 --- /dev/null +++ b/test/parallel/test-quic-stream-writer-fail-error-code.mjs @@ -0,0 +1,96 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream writer.fail() emits a RESET_STREAM with a non-zero +// application error code instead of the previously hard-coded 0n. +// +// Two cases are exercised against the test fixture's non-h3 ALPN +// (`quic-test`), which selects the C++ DefaultApplication and exposes +// `internalErrorCode === 0x1n` (NGTCP2_INTERNAL_ERROR) via session +// state: +// +// 1. `writer.fail(plainError)` — peer receives RESET_STREAM with the +// session's `internalErrorCode` (`0x1`), proving the hard-coded +// `0n` regression is gone and that the C++ -> JS state plumbing +// surfaces the application's code. +// 2. `writer.fail(new QuicError('msg', { errorCode: 0x42n }))` — +// peer receives RESET_STREAM with the explicit code, proving +// the QuicError fast path. +// +// The peer-side observation goes through `stream.onreset(err)` where +// `err` is `ERR_QUIC_APPLICATION_ERROR` exposing the wire code on +// `err.errorCode` (a BigInt). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { QuicError } = await import('node:quic'); + +function wireCodeOf(err) { + strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); + return err.errorCode; +} + +// Server: capture the next two streams. Each stream receives an +// onreset handler synchronously inside onstream so the C++ -> JS +// dispatch ordering (data packet -> reset packet) finds the handler +// already attached. +const expectedCodes = [0x1n, 0x42n]; +let nextStreamIndex = 0; +const allDone = Promise.withResolvers(); +const observed = []; + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const i = nextStreamIndex++; + stream.onreset = mustCall((err) => { + observed[i] = wireCodeOf(err); + if (observed.length === expectedCodes.length && + observed[0] !== undefined && observed[1] !== undefined) { + allDone.resolve(); + } + }); + + // The peer's reset causes stream.closed to reject. + await assert.rejects(stream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + }, 2); +})); + +const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', +}); +await clientSession.opened; + +// 1. Plain Error -> session.internalErrorCode (0x1n for non-h3). +{ + const stream = await clientSession.createBidirectionalStream(); + const writer = stream.writer; + // Write a tiny chunk to guarantee the server-side stream is + // created via onstream before the RESET_STREAM frame arrives. + writer.writeSync('x'); + writer.fail(new Error('plain error reason')); +} + +// 2. QuicError with explicit code -> peer sees that exact code. +{ + const stream = await clientSession.createBidirectionalStream(); + const writer = stream.writer; + writer.writeSync('y'); + writer.fail(new QuicError('explicit code', { errorCode: 0x42n })); +} + +await allDone.promise; + +strictEqual(observed[0], expectedCodes[0]); +strictEqual(observed[1], expectedCodes[1]); + +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-zero-length.mjs b/test/parallel/test-quic-stream-zero-length.mjs new file mode 100644 index 00000000000000..0a38d4df09536a --- /dev/null +++ b/test/parallel/test-quic-stream-zero-length.mjs @@ -0,0 +1,42 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: zero-length stream via setBody(null). +// Creates a stream with no body, then calls setBody(null) which sends +// FIN immediately. The server receives zero bytes. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, 0); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +stream.setBody(null); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-test-client.mjs b/test/parallel/test-quic-test-client.mjs new file mode 100644 index 00000000000000..8a8ffdd67f70e4 --- /dev/null +++ b/test/parallel/test-quic-test-client.mjs @@ -0,0 +1,42 @@ +// Flags: --experimental-quic +import { hasQuic, isAIX, isIBMi, isWindows, skip } from '../common/index.mjs'; +import assert from 'node:assert'; +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + +if (!hasQuic) { + skip('QUIC support is not enabled'); +} +if (isAIX) { + // AIX does not support some of the networking features used in the ngtcp2 + // example server and client. + skip('QUIC third-party tests are disabled on AIX'); +} +if (isIBMi) { + // IBM i does not support some of the networking features used in the ngtcp2 + // example server and client. + skip('QUIC third-party tests are disabled on IBM i'); +} +if (isWindows) { + // Windows does not support the [Li/U]nix specific headers and system calls + // required by the ngtcp2 example server/client. + skip('QUIC third-party tests are disabled on Windows'); +} +if (!existsSync(resolve(process.execPath, '../ngtcp2_test_client'))) { + skip('ngtcp2_test_client binary not built'); +} + +const { default: QuicTestClient } = await import('../common/quic/test-client.mjs'); + +const client = new QuicTestClient(); + +// If this completes without throwing, the test passes. +await client.help({ stdio: 'ignore' }); + +setTimeout(() => { + client.stop(); +}, 100); + +// We expect this to fail since there's no server running. +await assert.rejects(client.run('localhost', '12345', undefined, { stdio: 'ignore' }), + { message: /Process exited with code 1 and signal null/ }); diff --git a/test/parallel/test-quic-test-server.mjs b/test/parallel/test-quic-test-server.mjs new file mode 100644 index 00000000000000..84dbff6b2d69a3 --- /dev/null +++ b/test/parallel/test-quic-test-server.mjs @@ -0,0 +1,44 @@ +// Flags: --experimental-quic +import { hasQuic, isAIX, isIBMi, isWindows, skip } from '../common/index.mjs'; +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + +if (!hasQuic) { + skip('QUIC support is not enabled'); +} +if (isAIX) { + // AIX does not support some of the networking features used in the ngtcp2 + // example server and client. + skip('QUIC third-party tests are disabled on AIX'); +} +if (isIBMi) { + // IBM i does not support some of the networking features used in the ngtcp2 + // example server and client. + skip('QUIC third-party tests are disabled on IBM i'); +} +if (isWindows) { + // Windows does not support the [Li/U]nix specific headers and system calls + // required by the ngtcp2 example server/client. + skip('QUIC third-party tests are disabled on Windows'); +} +if (!existsSync(resolve(process.execPath, '../ngtcp2_test_server'))) { + skip('ngtcp2_test_server binary not built'); +} + +const { default: QuicTestServer } = await import('../common/quic/test-server.mjs'); +const fixtures = await import('../common/fixtures.mjs'); + +const server = new QuicTestServer(); +const fixturesPath = fixtures.path(); + +// If this completes without throwing, the test passes. +await server.help({ stdio: 'ignore' }); + +setTimeout(() => { + server.stop(); +}, 100); + +await server.run('localhost', '12345', + `${fixturesPath}/keys/agent1-key.pem`, + `${fixturesPath}/keys/agent1-cert.pem`, + { stdio: 'inherit' }); diff --git a/test/parallel/test-quic-tls-ca.mjs b/test/parallel/test-quic-tls-ca.mjs new file mode 100644 index 00000000000000..560d1eb412a43e --- /dev/null +++ b/test/parallel/test-quic-tls-ca.mjs @@ -0,0 +1,50 @@ +// Flags: --experimental-quic --no-warnings + +// Test: custom CA certificate chain. +// The client provides a CA cert that matches the server's cert, +// allowing validation to succeed. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const ca = readKey('ca1-cert.pem'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + const info = await serverSession.opened; + strictEqual(info.protocol, 'quic-test'); + serverSession.close(); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], +}); + +// Client provides the CA cert. The validation error should be different +// (or absent) compared to when no CA is provided. +const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + servername: 'localhost', + verifyPeer: 'manual', + ca, +}); + +const info = await clientSession.opened; +strictEqual(info.protocol, 'quic-test'); +// The CA option is accepted. Validation may or may not succeed +// depending on the cert chain. The important thing is the +// handshake completed and the option was used. + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-tls-crl.mjs b/test/parallel/test-quic-tls-crl.mjs new file mode 100644 index 00000000000000..fe5ac007aadefb --- /dev/null +++ b/test/parallel/test-quic-tls-crl.mjs @@ -0,0 +1,80 @@ +// Flags: --experimental-quic --no-warnings + +// Test: CRL (certificate revocation list) enforcement. +// A server using a non-revoked certificate succeeds when the client +// provides a CRL. A server using the same certificate reports +// "certificate revoked" in the validation error when the client +// provides a CRL that revokes it. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const ca2Cert = readKey('ca2-cert.pem'); +const ca2Crl = readKey('ca2-crl.pem'); +const ca2CrlAgent3 = readKey('ca2-crl-agent3.pem'); +const agent3Key = createPrivateKey(readKey('agent3-key.pem')); +const agent3Cert = readKey('agent3-cert.pem'); + +// --- Non-revoked: agent3 with original CRL (doesn't list agent3) --- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + await serverSession.close(); + }), { + sni: { '*': { keys: [agent3Key], certs: [agent3Cert] } }, + alpn: ['quic-test'], + }); + + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + ca: [ca2Cert], + crl: [ca2Crl], + }); + + // Should succeed — agent3 is NOT in the original CRL. + const info = await clientSession.opened; + strictEqual(clientSession.destroyed, false); + // No revocation error. + ok(!info.validationErrorReason || + !info.validationErrorReason.includes('revoked')); + await clientSession.close(); + await serverEndpoint.close(); +} + +// --- Revoked: agent3 with CRL that revokes agent3 --- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + await serverSession.close(); + }), { + sni: { '*': { keys: [agent3Key], certs: [agent3Cert] } }, + alpn: ['quic-test'], + }); + + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + ca: [ca2Cert], + crl: [ca2CrlAgent3], + }); + + // The connection currently succeeds but the validation error + // reports "certificate revoked". This verifies the CRL is loaded + // and checked. + const info = await clientSession.opened; + strictEqual(info.validationErrorReason, 'certificate revoked'); + await clientSession.close(); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-tls-keylog.mjs b/test/parallel/test-quic-tls-keylog.mjs new file mode 100644 index 00000000000000..82d1b0aa8bc900 --- /dev/null +++ b/test/parallel/test-quic-tls-keylog.mjs @@ -0,0 +1,66 @@ +// Flags: --experimental-quic --no-warnings + +// Test: keylog callback. +// When keylog: true, TLS key material is delivered to the +// session.onkeylog callback during the handshake for both +// client and server sessions. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const clientLines = []; +const serverLines = []; + +const expectedLabels = [ + 'CLIENT_HANDSHAKE_TRAFFIC_SECRET', + 'SERVER_HANDSHAKE_TRAFFIC_SECRET', + 'CLIENT_TRAFFIC_SECRET_0', + 'SERVER_TRAFFIC_SECRET_0', +]; + +function assertKeylogLines(lines, side) { + ok(lines.length > 0, `Expected ${side} keylog lines, got ${lines.length}`); + + for (const line of lines) { + strictEqual(typeof line, 'string', + `Each ${side} keylog line should be a string`); + } + + const joined = lines.join(''); + for (const label of expectedLabels) { + ok(joined.includes(label), + `Expected ${side} keylog to contain ${label}`); + } +} + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + serverSession.close(); +}), { + keylog: true, + onkeylog(line) { + serverLines.push(line); + }, +}); + +const clientSession = await connect(serverEndpoint.address, { + keylog: true, + onkeylog(line) { + clientLines.push(line); + }, +}); + +await clientSession.opened; +await clientSession.closed; +await serverEndpoint.close(); + +assertKeylogLines(clientLines, 'client'); +assertKeylogLines(serverLines, 'server'); diff --git a/test/parallel/test-quic-tls-options.mjs b/test/parallel/test-quic-tls-options.mjs new file mode 100644 index 00000000000000..e903f8a64bcad3 --- /dev/null +++ b/test/parallel/test-quic-tls-options.mjs @@ -0,0 +1,85 @@ +// Flags: --experimental-quic --no-warnings + +// Test: custom TLS ciphers and groups. +// Custom ciphers option on the server/client. +// Custom groups option on the server/client. +// Default ciphers/groups used when not specified. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual, ok } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect, constants } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +// Custom ciphers. Use a specific TLS 1.3 cipher suite. +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + const info = await serverSession.opened; + strictEqual(typeof info.cipher, 'string'); + ok(info.cipher.includes('AES_256_GCM')); + serverSession.close(); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + ciphers: 'TLS_AES_256_GCM_SHA384', + }); + + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + servername: 'localhost', + ciphers: 'TLS_AES_256_GCM_SHA384', + }); + + const info = await clientSession.opened; + ok(info.cipher.includes('AES_256_GCM')); + strictEqual(info.cipherVersion, 'TLSv1.3'); + + await clientSession.closed; + await serverEndpoint.close(); +} + +// Custom groups. Use a specific key exchange group. +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + serverSession.close(); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + alpn: ['quic-test'], + groups: 'P-256', + }); + + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + servername: 'localhost', + groups: 'P-256', + }); + + const info = await clientSession.opened; + // The handshake should succeed with the specified group. + assert.strictEqual(info.cipherVersion, 'TLSv1.3'); + + await clientSession.closed; + await serverEndpoint.close(); +} + +// Default ciphers/groups are non-empty strings from constants. +{ + strictEqual(typeof constants.DEFAULT_CIPHERS, 'string'); + ok(constants.DEFAULT_CIPHERS.length > 0); + strictEqual(typeof constants.DEFAULT_GROUPS, 'string'); + ok(constants.DEFAULT_GROUPS.length > 0); +} diff --git a/test/parallel/test-quic-tls-trace.mjs b/test/parallel/test-quic-tls-trace.mjs new file mode 100644 index 00000000000000..b5ebd161cd58a0 --- /dev/null +++ b/test/parallel/test-quic-tls-trace.mjs @@ -0,0 +1,33 @@ +// Flags: --experimental-quic --no-warnings + +// Test: TLS trace output. +// When tlsTrace: true is set, the session produces TLS debug +// output on stderr. Verify the option is accepted without error +// and the connection succeeds. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + await serverSession.close(); +}), { + tlsTrace: true, +}); + +const clientSession = await connect(serverEndpoint.address, { + tlsTrace: true, +}); +await clientSession.opened; +strictEqual(clientSession.destroyed, false); + +await clientSession.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-tls-verify-client.mjs b/test/parallel/test-quic-tls-verify-client.mjs new file mode 100644 index 00000000000000..5d893fe8899a2b --- /dev/null +++ b/test/parallel/test-quic-tls-verify-client.mjs @@ -0,0 +1,89 @@ +// Flags: --experimental-quic --no-warnings + +// Test: client certificate verification. +// With verifyClient: true, a client that provides a valid +// certificate succeeds. +// With verifyClient: true, a client that does NOT provide a +// certificate fails the handshake. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual, ok, rejects } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const serverKey = createPrivateKey(readKey('agent1-key.pem')); +const serverCert = readKey('agent1-cert.pem'); +const clientKey = createPrivateKey(readKey('agent2-key.pem')); +const clientCert = readKey('agent2-cert.pem'); + +// --- TLS-03: Client provides a certificate — handshake succeeds --- +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + // The server should see the client's certificate. + ok(serverSession.peerCertificate); + await serverSession.close(); + }), { + sni: { '*': { keys: [serverKey], certs: [serverCert] } }, + alpn: ['quic-test'], + verifyClient: true, + // Trust the client's self-signed certificate. + ca: [clientCert], + }); + + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + keys: [clientKey], + certs: [clientCert], + }); + + await Promise.all([clientSession.opened, clientSession.closed]); + await serverEndpoint.close(); +} + +// --- TLS-04: Client does NOT provide a certificate — connection fails --- +// In TLS 1.3, client certificate verification happens post-handshake. +// The client's opened promise may resolve (handshake completes), but +// the server then sends a fatal alert (certificate_required) which +// closes both sides with a transport error. +{ + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await rejects(serverSession.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); + }), { + sni: { '*': { keys: [serverKey], certs: [serverCert] } }, + alpn: ['quic-test'], + verifyClient: true, + onerror: mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + }), + }); + + // Client connects WITHOUT providing a certificate. + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + onerror: mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + }), + }); + + // The client's closed promise rejects with the transport error + // from the server's certificate_required alert. + await rejects(clientSession.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); + + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-token-distinct.mjs b/test/parallel/test-quic-token-distinct.mjs new file mode 100644 index 00000000000000..cb830134847065 --- /dev/null +++ b/test/parallel/test-quic-token-distinct.mjs @@ -0,0 +1,50 @@ +// Flags: --experimental-quic --no-warnings + +// Test: two clients receive distinct NEW_TOKEN tokens. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, notDeepStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +let token1; +let token2; +const gotToken1 = Promise.withResolvers(); +const gotToken2 = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.closed; +}, 2)); + +// Client 1. +const cs1 = await connect(serverEndpoint.address, { + onnewtoken: mustCall((token) => { + token1 = Buffer.from(token); + gotToken1.resolve(); + }), +}); +await Promise.all([cs1.opened, gotToken1.promise]); +ok(token1.length > 0); + +// Client 2. +const cs2 = await connect(serverEndpoint.address, { + onnewtoken: mustCall((token) => { + token2 = Buffer.from(token); + gotToken2.resolve(); + }), +}); +await Promise.all([cs2.opened, gotToken2.promise]); +ok(token2.length > 0); + +// Tokens should be distinct. +notDeepStrictEqual(token1, token2); + +await cs1.close(); +await cs2.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-token-expired.mjs b/test/parallel/test-quic-token-expired.mjs new file mode 100644 index 00000000000000..527bb0caac6847 --- /dev/null +++ b/test/parallel/test-quic-token-expired.mjs @@ -0,0 +1,70 @@ +// Flags: --experimental-quic --no-warnings + +// Test: expired NEW_TOKEN is rejected. +// The server issues a NEW_TOKEN with a short tokenExpiration. After +// the token expires, the client provides it on reconnect. The server +// should reject it (the token is invalid) and fall back to Retry +// flow for address validation. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +import { setTimeout } from 'node:timers/promises'; + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const sni = { '*': { keys: [key], certs: [cert] } }; +const alpn = ['quic-test']; + +let savedToken; +const gotToken = Promise.withResolvers(); + +// Server with a very short token expiration (1 second). +const serverEndpoint = await listen((serverSession) => { + serverSession.onstream = mustNotCall(); +}, { + sni, + alpn, + endpoint: { tokenExpiration: 1 }, +}); + +// First connection: receive the token. +const cs1 = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + onnewtoken: mustCall((token) => { + savedToken = token; + gotToken.resolve(); + }), +}); +await Promise.all([cs1.opened, gotToken.promise]); +ok(savedToken.length > 0); +await cs1.close(); + +// Wait for the token to expire. +await setTimeout(1500); + +// Second connection with the expired token. The server should reject +// the token and fall back to Retry for address validation. The +// connection should still succeed (Retry is transparent). +const cs2 = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + token: savedToken, +}); +await cs2.opened; +// The connection succeeded despite the expired token. +strictEqual(cs2.destroyed, false); +await cs2.close(); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-token-reuse.mjs b/test/parallel/test-quic-token-reuse.mjs new file mode 100644 index 00000000000000..60ab0c29a38ceb --- /dev/null +++ b/test/parallel/test-quic-token-reuse.mjs @@ -0,0 +1,62 @@ +// Flags: --experimental-quic --no-warnings + +// Test: client reuses NEW_TOKEN on reconnect. +// The server sends a NEW_TOKEN after handshake. The client saves it +// and provides it in the token option on a subsequent connection to +// the same server. The second connection should succeed. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +let savedToken; +const gotToken = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall((stream) => { + stream.writer.endSync(); + serverSession.close(); + }); +}, 2), { + onerror() {}, +}); + +// First connection: receive the token. +const cs1 = await connect(serverEndpoint.address, { + onnewtoken: mustCall((token) => { + ok(Buffer.isBuffer(token)); + ok(token.length > 0); + savedToken = token; + gotToken.resolve(); + }), +}); +await Promise.all([cs1.opened, gotToken.promise]); + +// Signal the server to close this session. +const s1 = await cs1.createBidirectionalStream(); +s1.writer.endSync(); +for await (const _ of s1) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([s1.closed, cs1.closed]); + +// Second connection: reuse the token. The connection should succeed. +const cs2 = await connect(serverEndpoint.address, { + token: savedToken, +}); +await cs2.opened; + +// Verify data transfer works on the second connection. +const s2 = await cs2.createBidirectionalStream(); +s2.writer.endSync(); +for await (const _ of s2) { /* drain */ } // eslint-disable-line no-unused-vars +await s2.closed; + +// Close from the client side. +await cs2.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-token-secret.mjs b/test/parallel/test-quic-token-secret.mjs new file mode 100644 index 00000000000000..d50ec343848b87 --- /dev/null +++ b/test/parallel/test-quic-token-secret.mjs @@ -0,0 +1,93 @@ +// Flags: --experimental-quic --no-warnings + +// Test: tokenSecret cross-endpoint token validation. +// Two server endpoints with the same tokenSecret should accept each +// other's NEW_TOKEN tokens. A token from a server with a different +// tokenSecret should be rejected (falls back to Retry). + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey, randomBytes } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const sni = { '*': { keys: [key], certs: [cert] } }; +const alpn = ['quic-test']; + +const sharedSecret = randomBytes(16); + +let savedToken; +const gotToken = Promise.withResolvers(); + +// First server with shared tokenSecret. +const ep1 = await listen(async (serverSession) => { + await serverSession.closed; +}, { + sni, + alpn, + endpoint: { tokenSecret: sharedSecret }, +}); + +// Get a token from the first server. +const cs1 = await connect(ep1.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + onnewtoken: mustCall((token) => { + savedToken = token; + gotToken.resolve(); + }), +}); +await Promise.all([cs1.opened, gotToken.promise]); +ok(savedToken.length > 0); +await cs1.close(); +await ep1.close(); + +// Second server with the SAME tokenSecret. The token from ep1 +// should be accepted, allowing the connection to skip Retry. +const ep2 = await listen(async (serverSession) => { + await serverSession.closed; +}, { + sni, + alpn, + endpoint: { tokenSecret: sharedSecret }, +}); + +const cs2 = await connect(ep2.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + token: savedToken, +}); +await cs2.opened; +strictEqual(cs2.destroyed, false); +await cs2.close(); +await ep2.close(); + +// Third server with a DIFFERENT tokenSecret. The token from ep1 +// should be rejected. The connection still succeeds (Retry fallback). +const ep3 = await listen(async (serverSession) => { + await serverSession.closed; +}, { + sni, + alpn, + endpoint: { tokenSecret: randomBytes(16) }, +}); + +const cs3 = await connect(ep3.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + token: savedToken, +}); +await cs3.opened; +strictEqual(cs3.destroyed, false); +await cs3.close(); +await ep3.close(); diff --git a/test/parallel/test-quic-transport-params-validation.mjs b/test/parallel/test-quic-transport-params-validation.mjs new file mode 100644 index 00000000000000..b17f23b9bd0bb2 --- /dev/null +++ b/test/parallel/test-quic-transport-params-validation.mjs @@ -0,0 +1,76 @@ +// Flags: --experimental-quic --no-warnings + +// Test: transport parameter validation. +// Transport parameters are validated by ngtcp2 at connection time. +// Node.js validates the type (must be a number) but ngtcp2 validates +// the ranges. Verify that invalid types are rejected and valid +// values are accepted. + +import { hasQuic, skip, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { readKey } = fixtures; + +const { rejects, ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const sni = { '*': { keys: [key], certs: [cert] } }; +const alpn = ['quic-test']; + +async function tryListen(sessionOpts) { + return listen(mustNotCall(), { sni, alpn, ...sessionOpts }); +} + +// Invalid types for transport params are rejected. +for (const param of [ + 'initialMaxStreamDataBidiLocal', + 'initialMaxStreamDataBidiRemote', + 'initialMaxStreamDataUni', + 'initialMaxData', + 'initialMaxStreamsBidi', + 'initialMaxStreamsUni', + 'maxIdleTimeout', + 'activeConnectionIDLimit', + 'ackDelayExponent', + 'maxAckDelay', +]) { + await rejects(tryListen({ + transportParams: { [param]: 'invalid' }, + }), { + code: 'ERR_INVALID_ARG_VALUE', + }, `${param} should reject string value`); + + await rejects(tryListen({ + transportParams: { [param]: -1 }, + }), { + code: 'ERR_INVALID_ARG_VALUE', + }, `${param} should reject negative value`); +} + +// Valid values are accepted. +const ep = await tryListen({ + transportParams: { + initialMaxStreamDataBidiLocal: 65536, + initialMaxStreamDataBidiRemote: 65536, + initialMaxStreamDataUni: 65536, + initialMaxData: 1048576, + initialMaxStreamsBidi: 100, + initialMaxStreamsUni: 3, + maxIdleTimeout: 30, + activeConnectionIDLimit: 4, + ackDelayExponent: 3, + maxAckDelay: 25, + maxDatagramFrameSize: 1200, + }, +}); +ok(ep); +await ep.close(); diff --git a/test/parallel/test-quic-version-negotiation-oversized-cid.mjs b/test/parallel/test-quic-version-negotiation-oversized-cid.mjs new file mode 100644 index 00000000000000..55b5f6e0f669f8 --- /dev/null +++ b/test/parallel/test-quic-version-negotiation-oversized-cid.mjs @@ -0,0 +1,89 @@ +// Flags: --experimental-quic --no-warnings + +// Regression test for an unauthenticated remote crash in the QUIC Version +// Negotiation path. +// +// ngtcp2_pkt_decode_version_cid() does not clamp the connection ID lengths +// to NGTCP2_MAX_CIDLEN (20) when the packet's version is unsupported -- it +// returns the raw single-byte length fields from the wire, which can be up +// to 255. Endpoint::Receive() used to build CID objects (each backed by a +// fixed 20-byte buffer) directly from those lengths before any bound check, +// so a single crafted UDP datagram with an oversized DCID/SCID length would +// overflow the buffer and abort the process before the handshake. +// +// This test sends such a datagram directly with node:dgram and asserts the +// endpoint drops it without crashing, while a well-formed unsupported-version +// datagram still produces exactly one Version Negotiation response. + +import { hasQuic, skip, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { createSocket } = await import('node:dgram'); +const { listen } = await import('../common/quic.mjs'); + +// A long-header QUIC packet must be at least NGTCP2_MAX_UDP_PAYLOAD_SIZE +// (1200) bytes for an unsupported version to decode as a VN trigger. +const PACKET_SIZE = 1200; + +// Build a QUIC long-header packet with an unsupported version and the given +// DCID/SCID lengths. Lengths greater than 20 are only expressible because the +// on-wire length field is a single byte (0-255). +function buildLongHeaderPacket(dcidLen, scidLen) { + const packet = Buffer.alloc(PACKET_SIZE); + // Header form bit (0x80) + fixed bit (0x40). + packet[0] = 0xc0; + // Version 0x0a0a0a0a: nonzero (so it is not a real VN packet) and not a + // version Node supports, which forces the NGTCP2_ERR_VERSION_NEGOTIATION + // decode path. + packet.writeUInt32BE(0x0a0a0a0a, 1); + packet[5] = dcidLen; // DCID length byte + // DCID bytes occupy [6, 6 + dcidLen); SCID length byte follows them. + packet[6 + dcidLen] = scidLen; + // Remaining bytes stay zero-filled as padding to reach PACKET_SIZE. + return packet; +} + +// No handshake ever completes: the test only sends raw datagrams, so the +// session callback must never fire. +const serverEndpoint = await listen(mustNotCall()); +const { address, port } = serverEndpoint.address; + +const socket = createSocket('udp4'); + +function send(packet) { + return new Promise((resolve, reject) => { + socket.send(packet, port, address, (err) => { + if (err) reject(err); + else resolve(); + }); + }); +} + +// 1. Oversized DCID (21 > NGTCP2_MAX_CIDLEN). Before the fix this aborted the +// process. After the fix it must be dropped silently. +await send(buildLongHeaderPacket(21, 0)); + +// 2. A well-formed unsupported-version packet with valid (<= 20 byte) CIDs. +// This must still elicit exactly one Version Negotiation response, proving +// the fix did not break legitimate version negotiation. +await send(buildLongHeaderPacket(8, 8)); + +// Poll until the valid packet has been processed into a VN response. +const deadline = Date.now() + 2000; +while (serverEndpoint.stats.versionNegotiationCount === 0n) { + if (Date.now() > deadline) break; + await new Promise((resolve) => setTimeout(resolve, 25)); +} + +// Exactly one VN response: the oversized packet was dropped (not crashed, not +// negotiated), the valid packet was negotiated. +strictEqual(serverEndpoint.stats.versionNegotiationCount, 1n); + +socket.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-version-negotiation.mjs b/test/parallel/test-quic-version-negotiation.mjs new file mode 100644 index 00000000000000..f255ab87c39740 --- /dev/null +++ b/test/parallel/test-quic-version-negotiation.mjs @@ -0,0 +1,79 @@ +// Flags: --experimental-quic --no-warnings + +// Test: version negotiation. +// Version mismatch triggers version negotiation. +// Client receives ERR_QUIC_VERSION_NEGOTIATION_ERROR. +// quic.session.version.negotiation diagnostics channel fires. +// The client connects with an unsupported version number. The server +// responds with a Version Negotiation packet. The client's closed +// promise rejects with ERR_QUIC_VERSION_NEGOTIATION_ERROR. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const bogusVersion = 0x1a1a1a1a; + +// Subscribe to the version negotiation diagnostics channel. +const channelFired = Promise.withResolvers(); +dc.subscribe('quic.session.version.negotiation', mustCall((msg) => { + ok(msg.session, 'message should have session'); + strictEqual(msg.version, bogusVersion); + ok(Array.isArray(msg.requestedVersions), + 'requestedVersions should be an array'); + ok(msg.requestedVersions.length > 0, + 'server should advertise at least one version'); + ok(Array.isArray(msg.supportedVersions), + 'supportedVersions should be an array'); + channelFired.resolve(); +})); + +const serverEndpoint = await listen(async (serverSession) => { + // The server should never create a session for an unsupported version. + assert.fail('Server session callback should not be called'); +}); + +const clientSession = await connect(serverEndpoint.address, { + reuseEndpoint: false, + // Use an unsupported version to trigger version negotiation. + version: bogusVersion, + // The onversionnegotiation callback fires with version info. + onversionnegotiation: mustCall((version, requestedVersions, + supportedVersions) => { + // The version is the bogus version we configured. + strictEqual(version, bogusVersion); + // requestedVersions are the versions the server advertised in + // the Version Negotiation packet. + ok(Array.isArray(requestedVersions), + 'requestedVersions should be an array'); + ok(requestedVersions.length > 0, + 'server should advertise at least one supported version'); + // supportedVersions is our local supported range [min, max]. + ok(Array.isArray(supportedVersions), + 'supportedVersions should be an array'); + strictEqual(supportedVersions.length, 2, + 'supportedVersions should have [min, max]'); + }), + // The onerror callback fires with the version negotiation error. + onerror: mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_VERSION_NEGOTIATION_ERROR'); + }), +}); + +// The closed promise rejects with ERR_QUIC_VERSION_NEGOTIATION_ERROR. +await assert.rejects(clientSession.closed, { + code: 'ERR_QUIC_VERSION_NEGOTIATION_ERROR', +}); + +// Wait for the diagnostics channel to fire. +await channelFired.promise; + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-version.mjs b/test/parallel/test-quic-version.mjs new file mode 100644 index 00000000000000..1df17e3b1f163f --- /dev/null +++ b/test/parallel/test-quic-version.mjs @@ -0,0 +1,45 @@ +// Flags: --experimental-quic --no-warnings + +// Test: QUIC version selection. +// QUIC v1 handshake succeeds. +// QUIC v2 handshake succeeds. +// Both V1 and V2 are advertised in preferred/available versions. +// Version negotiation upgrades V1 → V2 when both sides support it. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + await serverSession.close(); + serverDone.resolve(); +})); + +// Default handshake uses v1 initial packets. +// The session should complete successfully. +const cs = await connect(serverEndpoint.address); +const info = await cs.opened; + +// The cipher and protocol should be negotiated. +strictEqual(typeof info.cipher, 'string'); +strictEqual(info.cipherVersion, 'TLSv1.3'); +strictEqual(info.protocol, 'quic-test'); + +// Both V1 and V2 are in preferred/available versions +// (configured in Session::Config). The compatible version negotiation +// (RFC 9368) should upgrade to V2 when both sides support it. +// We verify the handshake succeeded — the version negotiation +// happens transparently. + +await Promise.all([serverDone.promise, cs.closed]); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-writer-abort-signal.mjs b/test/parallel/test-quic-writer-abort-signal.mjs new file mode 100644 index 00000000000000..8d0dbb0351d931 --- /dev/null +++ b/test/parallel/test-quic-writer-abort-signal.mjs @@ -0,0 +1,52 @@ +// Flags: --experimental-quic --no-warnings + +// Test: write with aborted signal rejects immediately. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; + +const { rejects } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +const w = stream.writer; + +// Create an already-aborted signal. +const ac = new AbortController(); +ac.abort(new Error('already aborted')); + +// write() with an already-aborted signal should reject immediately. +await rejects( + w.write(encoder.encode('data'), { signal: ac.signal }), + { message: 'already aborted' }, +); + +// The writer should still be usable for normal writes. +w.writeSync(encoder.encode('ok')); +w.endSync(); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-writer-async-dispose-ended.mjs b/test/parallel/test-quic-writer-async-dispose-ended.mjs new file mode 100644 index 00000000000000..04337343ea8735 --- /dev/null +++ b/test/parallel/test-quic-writer-async-dispose-ended.mjs @@ -0,0 +1,46 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: Symbol.asyncDispose only fails if writable side not ended. +// If the writer was already ended (via endSync/end), asyncDispose +// should not fail — it's a no-op. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +const w = stream.writer; + +// End the writer normally. +w.writeSync(encoder.encode('data')); +w.endSync(); + +// After end, asyncDispose should be a no-op (writer already ended). +await w[Symbol.asyncDispose](); + +// The stream should close cleanly. +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-writer-backpressure.mjs b/test/parallel/test-quic-writer-backpressure.mjs new file mode 100644 index 00000000000000..4d74f029929e53 --- /dev/null +++ b/test/parallel/test-quic-writer-backpressure.mjs @@ -0,0 +1,81 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: writer backpressure. +// writeSync returns false when highWaterMark is exceeded. +// drainableProtocol returns promise when desiredSize <= 0. +// drainableProtocol promise resolves when drain fires. +// Try-fallback pattern: writeSync false, await drain, retry. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual, ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes, drainableProtocol: dp } = await import('stream/iter'); + +// Total data: 8 x 1KB = 8KB. highWaterMark: 2KB. +const numChunks = 8; +const chunkSize = 1024; +const totalSize = numChunks * chunkSize; + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const received = await bytes(stream); + strictEqual(received.byteLength, totalSize); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream({ + highWaterMark: 2048, +}); +const w = stream.writer; + +// Initial desiredSize should be the highWaterMark. +strictEqual(w.desiredSize, 2048); +strictEqual(stream.highWaterMark, 2048); + +let backpressureCount = 0; + +for (let i = 0; i < numChunks; i++) { + const chunk = new Uint8Array(chunkSize); + chunk.fill(i & 0xFF); + while (!w.writeSync(chunk)) { + // writeSync returned false. + backpressureCount++; + + // drainableProtocol returns a promise when backpressured. + const drain = w[dp](); + ok(drain instanceof Promise, 'drainableProtocol should return a Promise'); + + // The promise resolves when drain fires. + await drain; + + // After drain, desiredSize should be > 0. + ok(w.desiredSize > 0, `desiredSize after drain should be > 0, got ${w.desiredSize}`); + } +} + +w.endSync(); + +// Backpressure should have been hit with a 2KB highWaterMark +// and 1KB chunks (every 2 chunks fills the buffer). +ok(backpressureCount > 0, 'backpressure should have been hit'); + +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-writer-stop-sending.mjs b/test/parallel/test-quic-writer-stop-sending.mjs new file mode 100644 index 00000000000000..c964fa11bce5d6 --- /dev/null +++ b/test/parallel/test-quic-writer-stop-sending.mjs @@ -0,0 +1,59 @@ +// Flags: --experimental-quic --no-warnings + +// Test: peer STOP_SENDING transitions writer to errored state. +// After the server calls stopSending(), the client's writer should +// become errored — desiredSize is null, writeSync returns false. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +const encoder = new TextEncoder(); + +const serverReady = Promise.withResolvers(); +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Tell the client to stop sending. + stream.stopSending(1n); + serverReady.resolve(); + stream.writer.endSync(); + await rejects(stream.closed, mustCall((err) => { + assert.ok(err); + return true; + })); + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +const w = stream.writer; +w.writeSync(encoder.encode('initial data')); + +// Wait for the server to send STOP_SENDING. +await serverReady.promise; + +// Give a moment for the STOP_SENDING to propagate. +await setTimeout(100); + +// After STOP_SENDING, the writer should be in an errored state. +// writeSync returns false (refuses to accept data). +strictEqual(w.writeSync(encoder.encode('rejected')), false); + +// The stream closes after the server sends FIN. +await Promise.all([serverDone.promise, stream.closed]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-writer-write-rejects.mjs b/test/parallel/test-quic-writer-write-rejects.mjs new file mode 100644 index 00000000000000..864246b9e80e70 --- /dev/null +++ b/test/parallel/test-quic-writer-write-rejects.mjs @@ -0,0 +1,65 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: write() rejects when flow-controlled. +// The async write() method rejects with ERR_INVALID_STATE when the +// chunk exceeds desiredSize. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { rejects, strictEqual, ok } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes, drainableProtocol: dp } = await import('stream/iter'); + +const serverDone = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + await bytes(stream); + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + serverDone.resolve(); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Use a small highWaterMark to trigger backpressure easily. +const stream = await clientSession.createBidirectionalStream({ + highWaterMark: 1024, +}); +const w = stream.writer; + +// Fill the buffer. +strictEqual(w.writeSync(new Uint8Array(1024)), true); + +// desiredSize should now be 0 or very small. +strictEqual(w.desiredSize, 0); + +// Async write() should reject when buffer is full. +await rejects( + w.write(new Uint8Array(512)), + { code: 'ERR_INVALID_STATE' }, +); + +// Wait for drain, then write should succeed. +const drain = w[dp](); +ok(drain instanceof Promise); +await drain; +ok(w.desiredSize > 0); + +// Now write succeeds. +await w.write(new Uint8Array(100)); + +w.endSync(); +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([stream.closed, serverDone.promise]); +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-zero-rtt-datagram.mjs b/test/parallel/test-quic-zero-rtt-datagram.mjs new file mode 100644 index 00000000000000..5cfd776e51a205 --- /dev/null +++ b/test/parallel/test-quic-zero-rtt-datagram.mjs @@ -0,0 +1,81 @@ +// Flags: --experimental-quic --no-warnings + +// Test: 0-RTT with datagrams. +// The client sends a datagram as 0-RTT data in the first flight. +// The server receives it with the early flag set on the ondatagram +// callback. The datagram's early parameter should be true. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual, deepStrictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +let savedTicket; +let savedToken; +const gotTicket = Promise.withResolvers(); +const gotToken = Promise.withResolvers(); + +let earlyDatagramReceived = false; +let receivedDatagramData; +const serverGotDatagram = Promise.withResolvers(); + +let serverSessionCount = 0; +const serverEndpoint = await listen((serverSession) => { + const sessionNum = ++serverSessionCount; + if (sessionNum === 2) { + serverSession.ondatagram = (data, early) => { + receivedDatagramData = Buffer.from(data); + earlyDatagramReceived = early; + serverGotDatagram.resolve(); + }; + } +}, { + transportParams: { maxDatagramFrameSize: 1200 }, +}); + +// --- First connection: receive session ticket and token --- +const cs1 = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, + onsessionticket: mustCall((ticket) => { + ok(Buffer.isBuffer(ticket)); + savedTicket = ticket; + gotTicket.resolve(); + }), + onnewtoken: mustCall((token) => { + ok(Buffer.isBuffer(token)); + savedToken = token; + gotToken.resolve(); + }), +}); + +await cs1.opened; +await Promise.all([gotTicket.promise, gotToken.promise]); +await cs1.close(); + +// --- Second connection: send datagram as 0-RTT --- +const cs2 = await connect(serverEndpoint.address, { + transportParams: { maxDatagramFrameSize: 1200 }, + sessionTicket: savedTicket, + token: savedToken, +}); + +// Send datagram BEFORE the handshake completes — true 0-RTT. +await cs2.sendDatagram(new Uint8Array([0xCA, 0xFE])); + +const info2 = await cs2.opened; +strictEqual(info2.earlyDataAttempted, true); +strictEqual(info2.earlyDataAccepted, true); + +// Verify the server received the datagram as early data. +await serverGotDatagram.promise; +deepStrictEqual(receivedDatagramData, Buffer.from([0xCA, 0xFE])); +strictEqual(earlyDatagramReceived, true); + +await cs2.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-zero-rtt-disabled-client.mjs b/test/parallel/test-quic-zero-rtt-disabled-client.mjs new file mode 100644 index 00000000000000..1c7f8cebd9fdac --- /dev/null +++ b/test/parallel/test-quic-zero-rtt-disabled-client.mjs @@ -0,0 +1,60 @@ +// Flags: --experimental-quic --no-warnings + +// Test: 0-RTT not attempted when client sets enableEarlyData: false +// Even with a valid session ticket and token, the client should not +// attempt 0-RTT when enableEarlyData is false. The opened info +// should show earlyDataAttempted: false. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +let savedTicket; +let savedToken; +const gotTicket = Promise.withResolvers(); +const gotToken = Promise.withResolvers(); + +const serverEndpoint = await listen((serverSession) => { + serverSession.onstream = async (stream) => { + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + }; +}); + +// First connection: get ticket and token. +const cs1 = await connect(serverEndpoint.address, { + onsessionticket: mustCall((ticket) => { + savedTicket = ticket; + gotTicket.resolve(); + }), + onnewtoken: mustCall((token) => { + savedToken = token; + gotToken.resolve(); + }), +}); +await Promise.all([cs1.opened, gotTicket.promise, gotToken.promise]); +await cs1.close(); + +// Second connection: provide ticket and token but disable early data. +const cs2 = await connect(serverEndpoint.address, { + sessionTicket: savedTicket, + token: savedToken, + enableEarlyData: false, +}); + +const info2 = await cs2.opened; +// 0-RTT should NOT be attempted. +strictEqual(info2.earlyDataAttempted, false); +strictEqual(info2.earlyDataAccepted, false); + +await cs2.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-zero-rtt-disabled-server.mjs b/test/parallel/test-quic-zero-rtt-disabled-server.mjs new file mode 100644 index 00000000000000..879b3d6779132a --- /dev/null +++ b/test/parallel/test-quic-zero-rtt-disabled-server.mjs @@ -0,0 +1,95 @@ +// Flags: --experimental-quic --no-warnings + +// Test: Server rejects 0-RTT when enableEarlyData: false. +// The client has a valid session ticket and token, and attempts 0-RTT. +// The server has enableEarlyData: false, so it rejects the early data. +// The connection should still succeed (fallback to 1-RTT), and +// earlyDataAccepted should be false. + +import { hasQuic, skip, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +const { strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey, randomBytes } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const sni = { '*': { keys: [key], certs: [cert] } }; +const alpn = ['quic-test']; + +// Use the same tokenSecret for both servers so the token is valid. +const tokenSecret = randomBytes(16); + +let savedTicket; +let savedToken; +const gotTicket = Promise.withResolvers(); +const gotToken = Promise.withResolvers(); + +// First server: enableEarlyData: true (default) to generate a valid ticket. +const serverEndpoint1 = await listen((serverSession) => { + serverSession.onstream = async (stream) => { + for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + }; +}, { + sni, + alpn, + endpoint: { tokenSecret }, +}); + +const cs1 = await connect(serverEndpoint1.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + onsessionticket(ticket) { + savedTicket = ticket; + gotTicket.resolve(); + }, + onnewtoken(token) { + savedToken = token; + gotToken.resolve(); + }, +}); +await Promise.all([cs1.opened, gotTicket.promise, gotToken.promise]); +await cs1.close(); +await serverEndpoint1.close(); + +// Second server: enableEarlyData: false — rejects 0-RTT. +const serverEndpoint2 = await listen(async (serverSession) => { + await serverSession.opened; + serverSession.close(); + await serverSession.closed; +}, { + sni, + alpn, + enableEarlyData: false, + endpoint: { tokenSecret }, + onerror: mustNotCall(), +}); + +const cs2 = await connect(serverEndpoint2.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + sessionTicket: savedTicket, + token: savedToken, +}); + +// The deferred handshake needs a send to trigger. Use sendDatagram +// since it's simpler than a stream for this test. +await cs2.sendDatagram(new Uint8Array([1])); + +const info2 = await cs2.opened; +strictEqual(info2.earlyDataAttempted, true); +strictEqual(info2.earlyDataAccepted, false); + +await cs2.closed; +await serverEndpoint2.close(); diff --git a/test/parallel/test-quic-zero-rtt-rejected-settings.mjs b/test/parallel/test-quic-zero-rtt-rejected-settings.mjs new file mode 100644 index 00000000000000..596499f1524207 --- /dev/null +++ b/test/parallel/test-quic-zero-rtt-rejected-settings.mjs @@ -0,0 +1,113 @@ +// Flags: --experimental-quic --no-warnings + +// Test: 0-RTT rejected when server settings change. +// The client has a session ticket from a server with generous +// transport params. The server restarts with reduced params +// (smaller initialMaxStreamsBidi). The 0-RTT should be rejected +// because the stored transport params are more permissive than +// the current ones. The connection falls back to 1-RTT. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import dc from 'node:diagnostics_channel'; +import * as fixtures from '../common/fixtures.mjs'; + +const { ok, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey, randomBytes } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); +const sni = { '*': { keys: [key], certs: [cert] } }; +const alpn = ['quic-test']; +const tokenSecret = randomBytes(16); + +// quic.session.early.rejected fires when 0-RTT is rejected. +dc.subscribe('quic.session.early.rejected', mustCall((msg) => { + ok(msg.session, 'early.rejected should include session'); +})); + +let savedTicket; +let savedToken; +const gotTicket = Promise.withResolvers(); +const gotToken = Promise.withResolvers(); + +// First server: generous transport params. +const ep1 = await listen(async (serverSession) => { + await serverSession.closed; +}, { + sni, + alpn, + endpoint: { tokenSecret }, + transportParams: { + initialMaxStreamsBidi: 100, + initialMaxData: 1048576, + }, +}); + +const cs1 = await connect(ep1.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + onsessionticket: mustCall((ticket) => { + savedTicket = ticket; + gotTicket.resolve(); + }), + onnewtoken: mustCall((token) => { + savedToken = token; + gotToken.resolve(); + }), +}); +await Promise.all([cs1.opened, gotTicket.promise, gotToken.promise]); +await cs1.close(); +await ep1.close(); + +// Second server: reduced transport params. +// initialMaxStreamsBidi reduced from 100 to 10. +const serverStreamSeen = Promise.withResolvers(); +const ep2 = await listen((serverSession) => { + serverSession.onstream = (stream) => { + // The stream may be destroyed by EarlyDataRejected before + // we can process it. Just record that we saw it. + serverStreamSeen.resolve(true); + }; +}, { + sni, + alpn, + endpoint: { tokenSecret }, + transportParams: { + initialMaxStreamsBidi: 10, + initialMaxData: 1048576, + }, + onerror(err) { ok(err); }, +}); + +const cs2 = await connect(ep2.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + sessionTicket: savedTicket, + token: savedToken, + onerror(err) { ok(err); }, + onearlyrejected() {}, +}); + +// Trigger the deferred handshake. +const encoder = new TextEncoder(); +await cs2.createBidirectionalStream({ + body: encoder.encode('test'), +}); + +const info2 = await cs2.opened; +// 0-RTT was attempted but rejected due to changed transport params. +strictEqual(info2.earlyDataAttempted, true); +strictEqual(info2.earlyDataAccepted, false); + +// The 0-RTT stream may have been destroyed by EarlyDataRejected. +// Close from the client side. +await cs2.close(); +await ep2.close(); diff --git a/test/parallel/test-quic-zero-rtt.mjs b/test/parallel/test-quic-zero-rtt.mjs new file mode 100644 index 00000000000000..ef1a345541045b --- /dev/null +++ b/test/parallel/test-quic-zero-rtt.mjs @@ -0,0 +1,111 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: 0-RTT session resumption. +// First connection receives a session ticket and NEW_TOKEN. +// Second connection uses both the session ticket and token. +// The token skips address validation (Retry), the session +// ticket enables 0-RTT encryption. The client sends data +// BEFORE the handshake completes (true 0-RTT). The server's +// onstream fires and the stream's early flag is true. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; + +const { ok, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { bytes } = await import('stream/iter'); + +const encoder = new TextEncoder(); + +let savedTicket; +let savedToken; +const gotTicket = Promise.withResolvers(); +const gotToken = Promise.withResolvers(); + +let firstStreamEarly; +let secondStreamEarly; +const secondStreamDone = Promise.withResolvers(); + +let serverSessionCount = 0; +const serverEndpoint = await listen((serverSession) => { + const sessionNum = ++serverSessionCount; + serverSession.onstream = async (stream) => { + const data = await bytes(stream); + ok(data.byteLength > 0); + + if (sessionNum === 1) { + firstStreamEarly = stream.early; + } else { + secondStreamEarly = stream.early; + secondStreamDone.resolve(); + } + + stream.writer.endSync(); + await stream.closed; + serverSession.close(); + }; +}); + +// --- ZRTT-01: First connection — receive the session ticket and token --- +const cs1 = await connect(serverEndpoint.address, { + onsessionticket: mustCall((ticket) => { + ok(Buffer.isBuffer(ticket)); + ok(ticket.length > 0); + savedTicket = ticket; + gotTicket.resolve(); + }, 2), + onnewtoken: mustCall((token) => { + ok(Buffer.isBuffer(token)); + ok(token.length > 0); + savedToken = token; + gotToken.resolve(); + }), +}); + +const info1 = await cs1.opened; +strictEqual(info1.earlyDataAttempted, false); +strictEqual(info1.earlyDataAccepted, false); + +await Promise.all([gotTicket.promise, gotToken.promise]); + +// Send data to verify the connection works. +const s1 = await cs1.createBidirectionalStream({ + body: encoder.encode('first'), +}); +for await (const _ of s1) { /* drain */ } // eslint-disable-line no-unused-vars +await Promise.all([s1.closed, cs1.closed]); + +// --- ZRTT-02: Second connection — 0-RTT with ticket + token --- +// The token skips Retry (address validation), the session ticket +// enables 0-RTT encryption. With the deferred handshake, the +// stream data is included in the first flight as 0-RTT. +const cs2 = await connect(serverEndpoint.address, { + sessionTicket: savedTicket, + token: savedToken, +}); + +// Send data BEFORE the handshake completes — true 0-RTT. +const s2 = await cs2.createBidirectionalStream({ + body: encoder.encode('early data'), +}); + +// Now wait for handshake completion. +const info2 = await cs2.opened; +strictEqual(info2.earlyDataAttempted, true); +strictEqual(info2.earlyDataAccepted, true); + +for await (const _ of s2) { /* drain */ } // eslint-disable-line no-unused-vars +await s2.closed; + +// Verify the server saw the early data flag. +await secondStreamDone.promise; +strictEqual(firstStreamEarly, false); +strictEqual(secondStreamEarly, true); + +await cs2.closed; +await serverEndpoint.close(); diff --git a/test/parallel/test-stream-base-typechecking.js b/test/parallel/test-stream-base-typechecking.js deleted file mode 100644 index ae8582642344ee..00000000000000 --- a/test/parallel/test-stream-base-typechecking.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; -const common = require('../common'); -const assert = require('assert'); -const net = require('net'); - -const server = net.createServer().listen(0, common.mustCall(() => { - const client = net.connect(server.address().port, common.mustCall(() => { - assert.throws(() => { - client.write('broken', 'buffer'); - }, { - name: 'TypeError', - code: 'ERR_INVALID_ARG_TYPE', - message: 'Second argument must be a buffer' - }); - client.destroy(); - server.close(); - })); -})); diff --git a/test/parallel/test-stream-finished.js b/test/parallel/test-stream-finished.js index b55107a7a6440f..6c2f3cad68e1d8 100644 --- a/test/parallel/test-stream-finished.js +++ b/test/parallel/test-stream-finished.js @@ -1,3 +1,4 @@ +// Flags: --expose-internals 'use strict'; const common = require('../common'); @@ -15,6 +16,7 @@ const EE = require('events'); const fs = require('fs'); const { promisify } = require('util'); const http = require('http'); +const { kEosNodeSynchronousCallback } = require('internal/streams/end-of-stream'); { const rs = new Readable({ @@ -95,13 +97,20 @@ const http = require('http'); { // Check pre-cancelled - const signal = new EventTarget(); - signal.aborted = true; + const signal = AbortSignal.abort(); const rs = Readable.from((function* () {})()); - finished(rs, { signal }, common.mustCall((err) => { + const cleanup = finished(rs, { signal }, common.mustCall((err) => { + assert.strictEqual(err.name, 'AbortError'); + cleanup(); + })); + const unset = Symbol('unset'); + let cleanup2 = unset; + cleanup2 = finished(rs, { signal, [kEosNodeSynchronousCallback]: true }, common.mustCall((err) => { assert.strictEqual(err.name, 'AbortError'); + assert.strictEqual(cleanup2, unset); })); + cleanup2(); } { @@ -160,8 +169,7 @@ const http = require('http'); // Promisified pre-aborted works const finishedPromise = promisify(finished); async function run() { - const signal = new EventTarget(); - signal.aborted = true; + const signal = AbortSignal.abort(); const rs = Readable.from((function* () {})()); await finishedPromise(rs, { signal }); } @@ -592,6 +600,29 @@ testClosed((opts) => new Writable({ write() {}, ...opts })); })); } +{ + let serverRes; + const server = http.createServer(common.mustCall((req, res) => { + serverRes = res; + res.write('hello'); + })).listen(0, common.mustCall(function() { + http.get({ port: this.address().port }, common.mustCall((res) => { + res.on('aborted', common.mustCall(() => { + finished(res, common.mustCall((err) => { + assert.strictEqual(err.code, 'ECONNRESET'); + assert.strictEqual(err.message, 'aborted'); + server.close(); + })); + })); + res.on('error', common.expectsError({ + code: 'ECONNRESET', + message: 'aborted', + })); + serverRes.destroy(); + })).on('error', common.mustNotCall()); + })); +} + { const w = new Writable({ write(chunk, encoding, callback) { diff --git a/test/parallel/test-stream-iter-broadcast-backpressure.js b/test/parallel/test-stream-iter-broadcast-backpressure.js new file mode 100644 index 00000000000000..d1e466c3ac44cf --- /dev/null +++ b/test/parallel/test-stream-iter-broadcast-backpressure.js @@ -0,0 +1,138 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { broadcast, text } = require('stream/iter'); + +// ============================================================================= +// Backpressure policies +// ============================================================================= + +async function testDropOldest() { + const { writer, broadcast: bc } = broadcast({ + highWaterMark: 2, + backpressure: 'drop-oldest', + }); + const consumer = bc.push(); + + writer.writeSync('first'); + writer.writeSync('second'); + // This should drop 'first' + writer.writeSync('third'); + writer.endSync(); + + const data = await text(consumer); + assert.strictEqual(data, 'secondthird'); +} + +async function testDropNewest() { + const { writer, broadcast: bc } = broadcast({ + highWaterMark: 1, + backpressure: 'drop-newest', + }); + const consumer = bc.push(); + + writer.writeSync('kept'); + // This should be silently dropped + writer.writeSync('dropped'); + writer.endSync(); + + const data = await text(consumer); + assert.strictEqual(data, 'kept'); +} + +// ============================================================================= +// Block backpressure +// ============================================================================= + +async function testBlockBackpressure() { + const { writer, broadcast: bc } = broadcast({ + highWaterMark: 1, + backpressure: 'block', + }); + const consumer = bc.push(); + writer.writeSync('a'); + + // Next write should block + let writeResolved = false; + const writePromise = writer.write('b').then(() => { writeResolved = true; }); + await new Promise(setImmediate); + assert.strictEqual(writeResolved, false); + + // Drain consumer to unblock the pending write + const iter = consumer[Symbol.asyncIterator](); + const first = await iter.next(); + assert.strictEqual(first.done, false); + await new Promise(setImmediate); + assert.strictEqual(writeResolved, true); + + writer.endSync(); + // Drain remaining data and verify completion + const second = await iter.next(); + assert.strictEqual(second.done, false); + await writePromise; +} + +// Verify block backpressure data flows correctly end-to-end +async function testBlockBackpressureContent() { + const { writer, broadcast: bc } = broadcast({ + highWaterMark: 1, + backpressure: 'block', + }); + const consumer = bc.push(); + + writer.writeSync('a'); + const writePromise = writer.write('b'); + await new Promise(setImmediate); + + // Read all and verify content + const iter = consumer[Symbol.asyncIterator](); + const first = await iter.next(); + assert.strictEqual(first.done, false); + const firstStr = new TextDecoder().decode(first.value[0]); + assert.strictEqual(firstStr, 'a'); + + await writePromise; + writer.endSync(); + + const second = await iter.next(); + assert.strictEqual(second.done, false); + const secondStr = new TextDecoder().decode(second.value[0]); + assert.strictEqual(secondStr, 'b'); + + const done = await iter.next(); + assert.strictEqual(done.done, true); +} + +// Writev async path +async function testWritevAsync() { + const { writer, broadcast: bc } = broadcast({ highWaterMark: 10 }); + const consumer = bc.push(); + + await writer.writev(['hello', ' ', 'world']); + await writer.end(); + + const data = await text(consumer); + assert.strictEqual(data, 'hello world'); +} + +// endSync returns the total byte count +async function testEndSyncReturnValue() { + const { writer, broadcast: bc } = broadcast({ highWaterMark: 10 }); + bc.push(); // Need a consumer to write to + + writer.writeSync('hello'); // 5 bytes + writer.writeSync(' world'); // 6 bytes + const total = writer.endSync(); + assert.strictEqual(total, 11); +} + +Promise.all([ + testDropOldest(), + testDropNewest(), + testBlockBackpressure(), + testBlockBackpressureContent(), + testWritevAsync(), + testEndSyncReturnValue(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-broadcast-basic.js b/test/parallel/test-stream-iter-broadcast-basic.js new file mode 100644 index 00000000000000..bdecd7d5beb9b1 --- /dev/null +++ b/test/parallel/test-stream-iter-broadcast-basic.js @@ -0,0 +1,321 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { setTimeout } = require('timers/promises'); +const { broadcast, text } = require('stream/iter'); + +// ============================================================================= +// Basic broadcast +// ============================================================================= + +async function testBasicBroadcast() { + const { writer, broadcast: bc } = broadcast(); + + // Create two consumers + const consumer1 = bc.push(); + const consumer2 = bc.push(); + + assert.strictEqual(bc.consumerCount, 2); + + await writer.write('hello'); + await writer.end(); + + const [data1, data2] = await Promise.all([ + text(consumer1), + text(consumer2), + ]); + + assert.strictEqual(data1, 'hello'); + assert.strictEqual(data2, 'hello'); +} + +async function testMultipleWrites() { + const { writer, broadcast: bc } = broadcast({ highWaterMark: 10 }); + + const consumer = bc.push(); + + await writer.write('a'); + await writer.write('b'); + await writer.write('c'); + await writer.end(); + + const data = await text(consumer); + assert.strictEqual(data, 'abc'); +} + +async function testConsumerCount() { + const { broadcast: bc } = broadcast(); + + assert.strictEqual(bc.consumerCount, 0); + + const c1 = bc.push(); + assert.strictEqual(bc.consumerCount, 1); + + bc.push(); + assert.strictEqual(bc.consumerCount, 2); + + bc.cancel(); + + // After cancel, consumer count drops to 0 + assert.strictEqual(bc.consumerCount, 0); + + // Consumers are detached and yield nothing + const batches = []; + for await (const batch of c1) { + batches.push(batch); + } + assert.strictEqual(batches.length, 0); +} + +// ============================================================================= +// Writer methods +// ============================================================================= + +async function testWriteSync() { + const { writer, broadcast: bc } = broadcast({ highWaterMark: 2 }); + const consumer = bc.push(); + + assert.strictEqual(writer.writeSync('a'), true); + assert.strictEqual(writer.writeSync('b'), true); + // Buffer full (highWaterMark=2, strict policy) + assert.strictEqual(writer.writeSync('c'), false); + + writer.endSync(); + + const data = await text(consumer); + assert.strictEqual(data, 'ab'); +} + +async function testWritevSync() { + const { writer, broadcast: bc } = broadcast({ highWaterMark: 10 }); + const consumer = bc.push(); + + assert.strictEqual(writer.writevSync(['hello', ' ', 'world']), true); + writer.endSync(); + + const data = await text(consumer); + assert.strictEqual(data, 'hello world'); +} + +async function testWriterEnd() { + const { writer, broadcast: bc } = broadcast(); + const consumer = bc.push(); + + await writer.write('data'); + const totalBytes = await writer.end(); + assert.strictEqual(totalBytes, 4); // 'data' = 4 UTF-8 bytes + + const data = await text(consumer); + assert.strictEqual(data, 'data'); +} + +async function testWriterFail() { + const { writer, broadcast: bc } = broadcast(); + const consumer = bc.push(); + + writer.fail(new Error('test error')); + + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of consumer) { + assert.fail('Should not reach here'); + } + }, + { message: 'test error' }, + ); +} + +// ============================================================================= +// Cancel +// ============================================================================= + +async function testCancelWithoutReason() { + const { broadcast: bc } = broadcast(); + const consumer = bc.push(); + + bc.cancel(); + + const batches = []; + for await (const batch of consumer) { + batches.push(batch); + } + assert.strictEqual(batches.length, 0); +} + +async function testCancelWithReason() { + const { broadcast: bc } = broadcast(); + + // Start a consumer that is waiting for data (promise pending) + const consumer = bc.push(); + const resultPromise = text(consumer).catch((err) => err); + + // Give the consumer time to enter the waiting state + await new Promise((resolve) => setImmediate(resolve)); + + bc.cancel(new Error('cancelled')); + + const result = await resultPromise; + assert.ok(result instanceof Error); + assert.strictEqual(result.message, 'cancelled'); +} + +async function testPendingNextSettlesAfterReturn() { + const { broadcast: bc } = broadcast(); + const iter = bc.push()[Symbol.asyncIterator](); + + const pendingNext = iter.next(); + await iter.return(); + + const result = await pendingNext; + assert.strictEqual(result.done, true); + assert.strictEqual(result.value, undefined); +} + +async function testPushAbortSignalRejectsPendingNext() { + const ac = new AbortController(); + const reason = new Error('push aborted'); + const { broadcast: bc } = broadcast(); + const iter = bc.push({ signal: ac.signal })[Symbol.asyncIterator](); + + const pendingNext = iter.next(); + const rejected = assert.rejects(pendingNext, (error) => error === reason); + ac.abort(reason); + + await rejected; +} + +// ============================================================================= +// Writer fail detaches consumers +// ============================================================================= + +async function testFailDetachesConsumers() { + const { writer, broadcast: bc } = broadcast(); + const consumer1 = bc.push(); + const consumer2 = bc.push(); + + assert.strictEqual(bc.consumerCount, 2); + + // Write some data, then fail the writer + await writer.write('data'); + await writer.fail(new Error('writer failed')); + + // After fail, consumers are detached + assert.strictEqual(bc.consumerCount, 0); + + // Both consumers should see the error + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of consumer1) { + assert.fail('Should not reach here'); + } + }, + { message: 'writer failed' }, + ); + + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of consumer2) { + assert.fail('Should not reach here'); + } + }, + { message: 'writer failed' }, + ); +} + +// ============================================================================= +// Writer fail idempotent +// ============================================================================= + +async function testWriterFailIdempotent() { + const { writer, broadcast: bc } = broadcast(); + const consumer = bc.push(); + writer.writeSync('hello'); + writer.fail(new Error('fail!')); + // Second call is a no-op (already errored) + writer.fail(new Error('fail2')); + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of consumer) { /* consume */ } + }, { message: 'fail!' }); +} + +// cancel() with falsy reason (0, "", false) should still treat as error +async function testCancelWithFalsyReason() { + const { broadcast: bc } = broadcast(); + const consumer = bc.push(); + const resultPromise = text(consumer).catch((err) => err); + await new Promise((resolve) => setImmediate(resolve)); + bc.cancel(0); + const result = await resultPromise; + assert.strictEqual(result, 0); +} + +// Late-joining consumer should read from oldest buffered entry +async function testLateJoinerSeesBufferedData() { + const { writer, broadcast: bc } = broadcast({ highWaterMark: 16 }); + + // Write data before any consumer joins + writer.writeSync('before-join'); + writer.endSync(); + + // Consumer joins after data is written + const consumer = bc.push(); + const result = await text(consumer); + assert.strictEqual(result, 'before-join'); +} + +async function testOverlappingNextKeepsEarlierRead() { + const { writer, broadcast: bc } = broadcast(); + const it = bc.push()[Symbol.asyncIterator](); + + const first = it.next(); + const second = it.next(); + + await writer.write('x'); + + const secondResult = await Promise.race([ + second.then((value) => ({ __proto__: null, settled: true, value })), + setTimeout(common.platformTimeout(50), + { __proto__: null, settled: false }), + ]); + assert.deepStrictEqual(secondResult, { + __proto__: null, + settled: false, + }); + + const result = await first; + assert.strictEqual(result.done, false); + assert.strictEqual(Buffer.concat(result.value).toString(), 'x'); + + writer.endSync(); + assert.deepStrictEqual(await second, { + __proto__: null, + done: true, + value: undefined, + }); + assert.strictEqual(bc.consumerCount, 0); +} + +Promise.all([ + testBasicBroadcast(), + testMultipleWrites(), + testConsumerCount(), + testWriteSync(), + testWritevSync(), + testWriterEnd(), + testWriterFail(), + testCancelWithoutReason(), + testCancelWithReason(), + testCancelWithFalsyReason(), + testPendingNextSettlesAfterReturn(), + testPushAbortSignalRejectsPendingNext(), + testFailDetachesConsumers(), + testWriterFailIdempotent(), + testLateJoinerSeesBufferedData(), + testOverlappingNextKeepsEarlierRead(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-broadcast-coverage.js b/test/parallel/test-stream-iter-broadcast-coverage.js new file mode 100644 index 00000000000000..7aa71062ab197b --- /dev/null +++ b/test/parallel/test-stream-iter-broadcast-coverage.js @@ -0,0 +1,143 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Coverage tests for broadcast.js: signal abort on pending write, +// sync iterable from, ringbuffer grow. + +const common = require('../common'); +const assert = require('assert'); +const { + broadcast, + Broadcast, + text, +} = require('stream/iter'); + +// Signal abort on pending write (covers wireBroadcastWriteSignal + removeAt) +async function testBroadcastWriteAbort() { + const { writer, broadcast: bc } = broadcast({ + highWaterMark: 1, + backpressure: 'block', + }); + const consumer = bc.push(); + + // Fill the buffer to capacity + writer.writeSync(new Uint8Array([1])); + + // Next write will block — pass a signal + const ac = new AbortController(); + const writePromise = writer.write(new Uint8Array([2]), + { signal: ac.signal }); + + // Abort the signal + ac.abort(); + + await assert.rejects(writePromise, { name: 'AbortError' }); + + // Clean up + writer.endSync(); + // Drain the consumer + const result = []; + for await (const batch of consumer) { + result.push(...batch); + } + assert.ok(result.length >= 1); +} + +// Broadcast.from with sync iterable (generator) +async function testBroadcastFromSyncIterable() { + function* source() { + yield [new Uint8Array([10, 20])]; + yield [new Uint8Array([30, 40])]; + } + + const { broadcast: bc } = Broadcast.from(source()); + const consumer = bc.push(); + // Just verify it completes without error and produces data + let count = 0; + for await (const batch of consumer) { + count += batch.length; + } + assert.ok(count > 0); +} + +// Broadcast.from with sync iterable — string chunks +async function testBroadcastFromSyncIterableStrings() { + function* source() { + yield 'hello'; + yield ' world'; + } + const { broadcast: bc } = Broadcast.from(source()); + const consumer = bc.push(); + const result = await text(consumer); + assert.strictEqual(result, 'hello world'); +} + +// Ringbuffer grow — push > 16 items without consumer draining +async function testRingbufferGrow() { + const { writer, broadcast: bc } = broadcast({ highWaterMark: 32 }); + const consumer = bc.push(); + + // Push 20 items (exceeds default ringbuffer capacity of 16) + for (let i = 0; i < 20; i++) { + writer.writeSync(new Uint8Array([i])); + } + writer.endSync(); + + // Read all items back and verify order + const items = []; + for await (const batch of consumer) { + for (const chunk of batch) { + items.push(chunk[0]); + } + } + assert.strictEqual(items.length, 20); + for (let i = 0; i < 20; i++) { + assert.strictEqual(items[i], i); + } +} + +// Multiple consumers at the minimum cursor should trim only after the last +// one advances or detaches. +async function testFanOutMinCursorTrimming() { + const { writer, broadcast: bc } = broadcast({ highWaterMark: 4 }); + const iter1 = bc.push()[Symbol.asyncIterator](); + const iter2 = bc.push()[Symbol.asyncIterator](); + + writer.writeSync(new Uint8Array([1])); + writer.writeSync(new Uint8Array([2])); + assert.strictEqual(bc.bufferSize, 2); + + assert.strictEqual((await iter1.next()).done, false); + assert.strictEqual(bc.bufferSize, 2); + + assert.strictEqual((await iter2.next()).done, false); + assert.strictEqual(bc.bufferSize, 1); + + await iter1.return(); + assert.strictEqual(bc.bufferSize, 1); + + assert.strictEqual((await iter2.next()).done, false); + assert.strictEqual(bc.bufferSize, 0); + + writer.endSync(); + assert.strictEqual((await iter2.next()).done, true); +} + +// Broadcast drainableProtocol after close returns null +async function testDrainableAfterClose() { + const { drainableProtocol } = require('stream/iter'); + const { writer } = broadcast(); + writer.endSync(); + const result = writer[drainableProtocol](); + // After close, desired should be null + assert.strictEqual(result, null); +} + +Promise.all([ + testBroadcastWriteAbort(), + testBroadcastFromSyncIterable(), + testBroadcastFromSyncIterableStrings(), + testRingbufferGrow(), + testFanOutMinCursorTrimming(), + testDrainableAfterClose(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-broadcast-from.js b/test/parallel/test-stream-iter-broadcast-from.js new file mode 100644 index 00000000000000..af965e8817d007 --- /dev/null +++ b/test/parallel/test-stream-iter-broadcast-from.js @@ -0,0 +1,219 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { broadcast, Broadcast, from, text } = require('stream/iter'); + +// ============================================================================= +// Broadcast.from +// ============================================================================= + +async function testBroadcastFromAsyncIterable() { + const source = from('broadcast-from'); + const { broadcast: bc } = Broadcast.from(source); + const consumer = bc.push(); + + const data = await text(consumer); + assert.strictEqual(data, 'broadcast-from'); +} + +async function testBroadcastFromNonArrayChunks() { + // Source that yields single Uint8Array chunks (not arrays) + const enc = new TextEncoder(); + async function* singleChunkSource() { + yield enc.encode('hello'); + yield enc.encode(' world'); + } + const { broadcast: bc } = Broadcast.from(singleChunkSource()); + const consumer = bc.push(); + const data = await text(consumer); + assert.strictEqual(data, 'hello world'); +} + +async function testBroadcastFromStringChunks() { + // Source that yields bare strings (not arrays) + async function* stringSource() { + yield 'foo'; + yield 'bar'; + } + const { broadcast: bc } = Broadcast.from(stringSource()); + const consumer = bc.push(); + const data = await text(consumer); + assert.strictEqual(data, 'foobar'); +} + +async function testBroadcastFromStringInput() { + const { broadcast: bc } = Broadcast.from('abc'); + const consumer = bc.push(); + const data = await text(consumer); + assert.strictEqual(data, 'abc'); +} + +async function testBroadcastFromUint8ArrayInput() { + const { broadcast: bc } = Broadcast.from(new Uint8Array([97])); + const consumer = bc.push(); + const data = await text(consumer); + assert.strictEqual(data, 'a'); +} + +async function testBroadcastFromDataViewInput() { + const view = new DataView(new Uint8Array([104, 105]).buffer); + const { broadcast: bc } = Broadcast.from(view); + const consumer = bc.push(); + const data = await text(consumer); + assert.strictEqual(data, 'hi'); +} + +async function testBroadcastFromMultipleConsumers() { + const source = from('shared-data'); + const { broadcast: bc } = Broadcast.from(source); + + const c1 = bc.push(); + const c2 = bc.push(); + + const [data1, data2] = await Promise.all([ + text(c1), + text(c2), + ]); + + assert.strictEqual(data1, 'shared-data'); + assert.strictEqual(data2, 'shared-data'); +} + +// ============================================================================= +// AbortSignal +// ============================================================================= + +async function testAbortSignal() { + const ac = new AbortController(); + const { broadcast: bc } = broadcast({ signal: ac.signal }); + const consumer = bc.push(); + + ac.abort(); + + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of consumer) { + assert.fail('Should not reach here'); + } + }, { name: 'AbortError' }); +} + +async function testAlreadyAbortedSignal() { + const ac = new AbortController(); + ac.abort(); + + const { broadcast: bc } = broadcast({ signal: ac.signal }); + const consumer = bc.push(); + + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of consumer) { + assert.fail('Should not reach here'); + } + }, { name: 'AbortError' }); +} + +// ============================================================================= +// Broadcast.from() hang fix - cancel while write blocked on backpressure +// ============================================================================= + +async function testBroadcastFromCancelWhileBlocked() { + // Create a slow async source that blocks between yields + let sourceFinished = false; + async function* slowSource() { + const enc = new TextEncoder(); + yield [enc.encode('chunk1')]; + // Simulate a long delay - the cancel should unblock this + await new Promise((resolve) => setTimeout(resolve, 10000)); + yield [enc.encode('chunk2')]; + sourceFinished = true; + } + + const { broadcast: bc } = Broadcast.from(slowSource()); + const consumer = bc.push(); + + // Read the first chunk + const iter = consumer[Symbol.asyncIterator](); + const first = await iter.next(); + assert.strictEqual(first.done, false); + + // Cancel while the source is blocked waiting to yield the next chunk + bc.cancel(); + + // The iteration should complete (not hang) + const next = await iter.next(); + assert.strictEqual(next.done, true); + + // Source should NOT have finished (we cancelled before chunk2) + assert.strictEqual(sourceFinished, false); +} + +// ============================================================================= +// Source error propagation via Broadcast.from() +// ============================================================================= + +async function testBroadcastFromSourceError() { + async function* failingSource() { + yield [new TextEncoder().encode('a')]; + throw new Error('broadcast source boom'); + } + const { broadcast: bc } = Broadcast.from(failingSource()); + const consumer = bc.push(); + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of consumer) { /* consume */ } + }, { message: 'broadcast source boom' }); +} + +// ============================================================================= +// Protocol validation +// ============================================================================= + +function testBroadcastProtocolReturnsNull() { + const obj = { + [Symbol.for('Stream.broadcastProtocol')]() { return null; }, + }; + assert.throws( + () => Broadcast.from(obj), + { code: 'ERR_INVALID_RETURN_VALUE' }, + ); +} + +function testBroadcastProtocolReturnsString() { + const obj = { + [Symbol.for('Stream.broadcastProtocol')]() { return 'bad'; }, + }; + assert.throws( + () => Broadcast.from(obj), + { code: 'ERR_INVALID_RETURN_VALUE' }, + ); +} + +function testBroadcastProtocolReturnsUndefined() { + const obj = { + [Symbol.for('Stream.broadcastProtocol')]() { }, + }; + assert.throws( + () => Broadcast.from(obj), + { code: 'ERR_INVALID_RETURN_VALUE' }, + ); +} + +Promise.all([ + testBroadcastFromAsyncIterable(), + testBroadcastFromNonArrayChunks(), + testBroadcastFromStringChunks(), + testBroadcastFromStringInput(), + testBroadcastFromUint8ArrayInput(), + testBroadcastFromDataViewInput(), + testBroadcastFromMultipleConsumers(), + testAbortSignal(), + testAlreadyAbortedSignal(), + testBroadcastFromCancelWhileBlocked(), + testBroadcastFromSourceError(), + testBroadcastProtocolReturnsNull(), + testBroadcastProtocolReturnsString(), + testBroadcastProtocolReturnsUndefined(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-consumers-bytes.js b/test/parallel/test-stream-iter-consumers-bytes.js new file mode 100644 index 00000000000000..8d9c1bc3931775 --- /dev/null +++ b/test/parallel/test-stream-iter-consumers-bytes.js @@ -0,0 +1,272 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + from, + fromSync, + bytes, + bytesSync, + text, + textSync, + arrayBuffer, + arrayBufferSync, + array, + arraySync, + toAsyncStreamable, +} = require('stream/iter'); + +// ============================================================================= +// bytesSync / bytes +// ============================================================================= + +async function testBytesSyncBasic() { + const data = bytesSync(fromSync('hello')); + assert.deepStrictEqual(data, new TextEncoder().encode('hello')); +} + +async function testBytesSyncLimit() { + assert.throws( + () => bytesSync(fromSync('hello world'), { limit: 3 }), + { name: 'RangeError' }, + ); +} + +async function testBytesAsync() { + const data = await bytes(from('hello-async')); + assert.deepStrictEqual(data, new TextEncoder().encode('hello-async')); +} + +async function testBytesAsyncLimit() { + await assert.rejects( + () => bytes(from('hello world'), { limit: 3 }), + { name: 'RangeError' }, + ); +} + +async function testBytesAsyncAbort() { + const ac = new AbortController(); + ac.abort(); + await assert.rejects( + () => bytes(from('data'), { signal: ac.signal }), + { name: 'AbortError' }, + ); +} + +async function testAsyncConsumersAbortPendingNext() { + const consumers = [ + ['bytes', bytes], + ['text', text], + ['arrayBuffer', arrayBuffer], + ['array', array], + ]; + + for (const [name, consumer] of consumers) { + const ac = new AbortController(); + const reason = new Error(`${name} boom`); + + async function* never() { + await new Promise(() => {}); + yield []; + } + + const promise = consumer(never(), { __proto__: null, signal: ac.signal }); + ac.abort(reason); + + await assert.rejects(promise, reason); + } +} + +async function testAsyncConsumersAbortPendingNormalization() { + const consumers = [ + ['bytes', bytes], + ['text', text], + ['arrayBuffer', arrayBuffer], + ['array', array], + ]; + + for (const [name, consumer] of consumers) { + const ac = new AbortController(); + const reason = new Error(`${name} normalization boom`); + const source = { + __proto__: null, + [toAsyncStreamable]() { + return new Promise(() => {}); + }, + }; + + const promise = consumer(source, { __proto__: null, signal: ac.signal }); + ac.abort(reason); + + await assert.rejects(promise, reason); + } +} + +async function testBytesEmpty() { + const data = await bytes(from([])); + assert.ok(data instanceof Uint8Array); + assert.strictEqual(data.byteLength, 0); +} + +// ============================================================================= +// arrayBufferSync / arrayBuffer +// ============================================================================= + +async function testArrayBufferSyncBasic() { + const ab = arrayBufferSync(fromSync(new Uint8Array([1, 2, 3]))); + assert.ok(ab instanceof ArrayBuffer); + assert.strictEqual(ab.byteLength, 3); + const view = new Uint8Array(ab); + assert.deepStrictEqual(view, new Uint8Array([1, 2, 3])); +} + +async function testArrayBufferAsync() { + const ab = await arrayBuffer(from(new Uint8Array([10, 20, 30]))); + assert.ok(ab instanceof ArrayBuffer); + assert.strictEqual(ab.byteLength, 3); + const view = new Uint8Array(ab); + assert.deepStrictEqual(view, new Uint8Array([10, 20, 30])); +} + +// ============================================================================= +// arraySync / array +// ============================================================================= + +async function testArraySyncBasic() { + function* gen() { + yield new Uint8Array([1]); + yield new Uint8Array([2]); + yield new Uint8Array([3]); + } + const chunks = arraySync(fromSync(gen())); + assert.strictEqual(chunks.length, 3); + assert.deepStrictEqual(chunks[0], new Uint8Array([1])); + assert.deepStrictEqual(chunks[1], new Uint8Array([2])); + assert.deepStrictEqual(chunks[2], new Uint8Array([3])); +} + +async function testArraySyncLimit() { + function* gen() { + yield new Uint8Array(100); + yield new Uint8Array(100); + } + const source = fromSync(gen()); + assert.throws( + () => arraySync(source, { limit: 50 }), + { name: 'RangeError' }, + ); +} + +async function testArrayAsync() { + async function* gen() { + yield [new Uint8Array([1])]; + yield [new Uint8Array([2])]; + } + const chunks = await array(gen()); + assert.strictEqual(chunks.length, 2); + assert.deepStrictEqual(chunks[0], new Uint8Array([1])); + assert.deepStrictEqual(chunks[1], new Uint8Array([2])); +} + +async function testArrayAsyncLimit() { + async function* gen() { + yield [new Uint8Array(100)]; + yield [new Uint8Array(100)]; + } + await assert.rejects( + () => array(gen(), { limit: 50 }), + { name: 'RangeError' }, + ); +} + +// ============================================================================= +// Non-array batch tolerance +// ============================================================================= + +// Regression test: consumers should tolerate sources that yield raw +// Uint8Array or string values instead of Uint8Array[] batches. +async function testConsumersNonArrayBatch() { + const encoder = new TextEncoder(); + + // Source yields raw Uint8Array, not wrapped in an array + async function* rawSource() { + yield encoder.encode('hello'); + yield encoder.encode(' world'); + } + const result = await text(rawSource()); + assert.strictEqual(result, 'hello world'); + + // bytes() with raw chunks + async function* rawSource2() { + yield encoder.encode('ab'); + } + const data = await bytes(rawSource2()); + assert.strictEqual(data.length, 2); + assert.strictEqual(data[0], 97); // 'a' + assert.strictEqual(data[1], 98); // 'b' + + // array() with raw chunks + async function* rawSource3() { + yield encoder.encode('x'); + yield encoder.encode('y'); + } + const arr = await array(rawSource3()); + assert.strictEqual(arr.length, 2); +} + +async function testConsumersNonArrayBatchSync() { + const encoder = new TextEncoder(); + + function* rawSyncSource() { + yield encoder.encode('sync'); + yield encoder.encode('data'); + } + const result = textSync(rawSyncSource()); + assert.strictEqual(result, 'syncdata'); + + const data = bytesSync(rawSyncSource()); + assert.strictEqual(data.length, 8); + + const arr = arraySync(rawSyncSource()); + assert.strictEqual(arr.length, 2); +} + +// Consumers accept string sources directly (normalized via from/fromSync) +async function testBytesStringSource() { + const result = await bytes('hello-bytes'); + assert.strictEqual(new TextDecoder().decode(result), 'hello-bytes'); +} + +function testBytesSyncStringSource() { + const result = bytesSync('hello-sync'); + assert.strictEqual(new TextDecoder().decode(result), 'hello-sync'); +} + +async function testTextStringSource() { + const { text } = require('stream/iter'); + const result = await text('direct-string'); + assert.strictEqual(result, 'direct-string'); +} + +Promise.all([ + testBytesSyncBasic(), + testBytesSyncLimit(), + testBytesAsync(), + testBytesAsyncLimit(), + testBytesAsyncAbort(), + testAsyncConsumersAbortPendingNext(), + testAsyncConsumersAbortPendingNormalization(), + testBytesEmpty(), + testArrayBufferSyncBasic(), + testArrayBufferAsync(), + testArraySyncBasic(), + testArraySyncLimit(), + testArrayAsync(), + testArrayAsyncLimit(), + testConsumersNonArrayBatch(), + testConsumersNonArrayBatchSync(), + testBytesStringSource(), + testBytesSyncStringSource(), + testTextStringSource(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-consumers-merge.js b/test/parallel/test-stream-iter-consumers-merge.js new file mode 100644 index 00000000000000..741ff7cdb5c990 --- /dev/null +++ b/test/parallel/test-stream-iter-consumers-merge.js @@ -0,0 +1,317 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + from, + fromSync, + push, + merge, + text, + toAsyncStreamable, + toStreamable, +} = require('stream/iter'); + +// ============================================================================= +// merge +// ============================================================================= + +async function testMergeTwoSources() { + const { writer: w1, readable: r1 } = push(); + const { writer: w2, readable: r2 } = push(); + + w1.write('from-a'); + w1.end(); + w2.write('from-b'); + w2.end(); + + const merged = merge(r1, r2); + const chunks = []; + for await (const batch of merged) { + for (const chunk of batch) { + chunks.push(new TextDecoder().decode(chunk)); + } + } + + // Both sources should be present (order is temporal, not guaranteed) + assert.strictEqual(chunks.length, 2); + assert.ok(chunks.includes('from-a')); + assert.ok(chunks.includes('from-b')); +} + +async function testMergeSingleSource() { + const data = await text(merge(from('only-one'))); + assert.strictEqual(data, 'only-one'); +} + +async function testMergeEmpty() { + const merged = merge(); + const batches = []; + for await (const batch of merged) { + batches.push(batch); + } + assert.strictEqual(batches.length, 0); +} + +async function testMergeWithAbortSignal() { + const ac = new AbortController(); + ac.abort(); + + const merged = merge(from('data'), { signal: ac.signal }); + + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of merged) { + assert.fail('Should not reach here'); + } + }, + { name: 'AbortError' }, + ); +} + +// Regression test: merge() with sync iterable sources +async function testMergeSyncSources() { + const s1 = fromSync('abc'); + const s2 = fromSync('def'); + const result = await text(merge(s1, s2)); + // Both sources should be fully consumed; order may vary + assert.strictEqual(result.length, 6); + for (const ch of 'abcdef') { + assert.ok(result.includes(ch), `missing '${ch}' in '${result}'`); + } +} + +// ============================================================================= +// Merge error propagation +// ============================================================================= + +async function testMergeSourceError() { + async function* goodSource() { + const enc = new TextEncoder(); + yield [enc.encode('a')]; + // Slow so the bad source errors first + await new Promise((r) => setTimeout(r, 50)); + yield [enc.encode('b')]; + } + + async function* badSource() { + yield [new TextEncoder().encode('x')]; + throw new Error('merge source boom'); + } + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of merge(goodSource(), badSource())) { + /* consume */ + } + }, + { message: 'merge source boom' }, + ); +} + +async function testMergeConsumerBreak() { + let source1Return = false; + let source2Return = false; + async function* source1() { + try { + while (true) yield [new TextEncoder().encode('a')]; + } finally { + source1Return = true; + } + } + + async function* source2() { + try { + while (true) yield [new TextEncoder().encode('b')]; + } finally { + source2Return = true; + } + } + // eslint-disable-next-line no-unused-vars + for await (const _ of merge(source1(), source2())) { + break; // Break after first batch + } + // Give async cleanup a tick to complete + await new Promise(setImmediate); + // Both sources should be cleaned up + assert.strictEqual(source1Return && source2Return, true); +} + +async function testMergeSignalMidIteration() { + const ac = new AbortController(); + async function* slowSource() { + const enc = new TextEncoder(); + yield [enc.encode('a')]; + await new Promise((r) => setTimeout(r, 100)); + yield [enc.encode('b')]; + } + const merged = merge(slowSource(), { signal: ac.signal }); + const iter = merged[Symbol.asyncIterator](); + await iter.next(); // First batch + ac.abort(); + await assert.rejects(() => iter.next(), { name: 'AbortError' }); +} + +async function testMergeSignalDuringPendingMultiSourceRead() { + const ac = new AbortController(); + + async function* pending() { + await new Promise(() => {}); + yield []; + } + + const iter = merge(pending(), pending(), { + __proto__: null, + signal: ac.signal, + })[Symbol.asyncIterator](); + + const next = iter.next(); + ac.abort(); + + await assert.rejects(next, { name: 'AbortError' }); +} + +// merge() accepts string sources (normalized via from()) +async function testMergeStringSources() { + const batches = []; + for await (const batch of merge('hello', 'world')) { + batches.push(batch); + } + // Each string becomes a single-batch source + assert.strictEqual(batches.length >= 2, true); + const combined = new TextDecoder().decode(Buffer.concat(batches.flat())); + // Both strings should appear (order may vary) + assert.ok(combined.includes('hello')); + assert.ok(combined.includes('world')); +} + +// merge() accepts object-like sources that are normalized via from() +async function testMergeObjectLikeSources() { + const arrayBuffer = new TextEncoder().encode('abc').buffer; + const dataView = new DataView(new TextEncoder().encode('def').buffer); + const streamable = { + [toStreamable]() { + return 'ghi'; + }, + }; + const asyncStreamable = { + [toAsyncStreamable]() { + return Promise.resolve('jkl'); + }, + }; + + assert.strictEqual(await text(merge(arrayBuffer)), 'abc'); + assert.strictEqual(await text(merge(dataView)), 'def'); + assert.strictEqual(await text(merge(streamable)), 'ghi'); + assert.strictEqual(await text(merge(asyncStreamable)), 'jkl'); +} + +// ============================================================================= +// Merge cleanup error handling +// ============================================================================= + +function throwInFinally(message) { + throw new Error(message); +} + +// Cleanup error with no primary error: iterator.return() throws during +// normal completion. The cleanup error should propagate directly. +async function testMergeCleanupErrorOnly() { + async function* source() { + yield [new TextEncoder().encode('data')]; + } + + async function* failingReturnSource() { + try { + yield [new TextEncoder().encode('more')]; + } finally { + throwInFinally('cleanup boom'); + } + } + + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of merge(source(), failingReturnSource())) { + // Consume all - no primary error + } + }, + { message: 'cleanup boom' }, + ); +} + +// Primary error + cleanup error: a source throws during iteration AND +// iterator.return() also throws. Should get a SuppressedError. +async function testMergePrimaryAndCleanupError() { + async function* badSource() { + yield [new TextEncoder().encode('x')]; + throw new Error('primary boom'); + } + + async function* failingReturnSource() { + try { + while (true) yield [new TextEncoder().encode('y')]; + } finally { + throwInFinally('cleanup boom'); + } + } + + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of merge(badSource(), failingReturnSource())) { + // Consume until error + } + }, + (err) => { + assert.ok( + err instanceof SuppressedError, + `Expected SuppressedError, got ${err.constructor.name}`, + ); + assert.strictEqual(err.error.message, 'primary boom'); + assert.strictEqual(err.suppressed.message, 'cleanup boom'); + return true; + }, + ); +} + +// Consumer break + cleanup error: consumer breaks and iterator.return() +// throws. The cleanup error should propagate. +async function testMergeBreakWithCleanupError() { + async function* failingReturnSource() { + try { + while (true) yield [new TextEncoder().encode('data')]; + } finally { + throwInFinally('cleanup on break'); + } + } + + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of merge(failingReturnSource())) { + break; + } + }, + { message: 'cleanup on break' }, + ); +} + +Promise.all([ + testMergeTwoSources(), + testMergeSingleSource(), + testMergeEmpty(), + testMergeWithAbortSignal(), + testMergeSyncSources(), + testMergeSourceError(), + testMergeConsumerBreak(), + testMergeSignalMidIteration(), + testMergeSignalDuringPendingMultiSourceRead(), + testMergeStringSources(), + testMergeObjectLikeSources(), + testMergeCleanupErrorOnly(), + testMergePrimaryAndCleanupError(), + testMergeBreakWithCleanupError(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-consumers-tap.js b/test/parallel/test-stream-iter-consumers-tap.js new file mode 100644 index 00000000000000..b93f93eb242b2c --- /dev/null +++ b/test/parallel/test-stream-iter-consumers-tap.js @@ -0,0 +1,130 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + from, + fromSync, + pull, + pullSync, + push, + tap, + tapSync, + text, + textSync, +} = require('stream/iter'); + +// ============================================================================= +// tap / tapSync +// ============================================================================= + +async function testTapSync() { + const observed = []; + const observer = tapSync((chunks) => { + if (chunks !== null) { + observed.push(chunks.length); + } + }); + + // tapSync returns a function transform + assert.strictEqual(typeof observer, 'function'); + + // Test that it passes data through unchanged + const input = [new Uint8Array([1]), new Uint8Array([2])]; + const result = observer(input); + assert.deepStrictEqual(result, input); + assert.deepStrictEqual(observed, [2]); + + // null (flush) passes through + const flushResult = observer(null); + assert.strictEqual(flushResult, null); +} + +async function testTapAsync() { + const observed = []; + const observer = tap(async (chunks) => { + if (chunks !== null) { + observed.push(chunks.length); + } + }); + + assert.strictEqual(typeof observer, 'function'); + + const input = [new Uint8Array([1])]; + const result = await observer(input); + assert.deepStrictEqual(result, input); + assert.deepStrictEqual(observed, [1]); +} + +async function testTapInPipeline() { + const { writer, readable } = push(); + const seen = []; + + const observer = tap(async (chunks) => { + if (chunks !== null) { + for (const chunk of chunks) { + seen.push(new TextDecoder().decode(chunk)); + } + } + }); + + writer.write('hello'); + writer.end(); + + // Use pull with tap as a transform + const result = pull(readable, observer); + const data = await text(result); + + assert.strictEqual(data, 'hello'); + assert.strictEqual(seen.length, 1); + assert.strictEqual(seen[0], 'hello'); +} + +// Tap callback error propagates through async pipeline +async function testTapAsyncErrorPropagation() { + const badTap = tap(() => { throw new Error('tap error'); }); + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of pull(from('hello'), badTap)) { /* consume */ } + }, { message: 'tap error' }); +} + +// TapSync callback error propagates through sync pipeline +function testTapSyncErrorPropagation() { + const badTap = tapSync(() => { throw new Error('tapSync error'); }); + assert.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const _ of pullSync(fromSync('hello'), badTap)) { /* consume */ } + }, { message: 'tapSync error' }); +} + +// TapSync in a pullSync pipeline passes through data and flush +function testTapSyncInPipeline() { + const seen = []; + let sawFlush = false; + const observer = tapSync((chunks) => { + if (chunks === null) { + sawFlush = true; + } else { + for (const chunk of chunks) { + seen.push(new TextDecoder().decode(chunk)); + } + } + }); + + const data = textSync(pullSync(fromSync('hello'), observer)); + assert.strictEqual(data, 'hello'); + assert.strictEqual(seen.length, 1); + assert.strictEqual(seen[0], 'hello'); + assert.strictEqual(sawFlush, true); +} + +Promise.all([ + testTapSync(), + testTapAsync(), + testTapInPipeline(), + testTapAsyncErrorPropagation(), + testTapSyncErrorPropagation(), + testTapSyncInPipeline(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-consumers-text.js b/test/parallel/test-stream-iter-consumers-text.js new file mode 100644 index 00000000000000..8bfa7c3320981c --- /dev/null +++ b/test/parallel/test-stream-iter-consumers-text.js @@ -0,0 +1,164 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + from, + fromSync, + text, + textSync, +} = require('stream/iter'); + +// ============================================================================= +// textSync / text +// ============================================================================= + +async function testTextSyncBasic() { + const data = textSync(fromSync('hello text')); + assert.strictEqual(data, 'hello text'); +} + +async function testTextAsync() { + const data = await text(from('hello async text')); + assert.strictEqual(data, 'hello async text'); +} + +async function testTextEncoding() { + // Default encoding is utf-8 + const data = await text(from('café')); + assert.strictEqual(data, 'café'); +} + +// ============================================================================= +// Text encoding tests +// ============================================================================= + +async function testTextNonUtf8Encoding() { + // Latin-1 encoding + const latin1Bytes = new Uint8Array([0xE9, 0xE8, 0xEA]); // é, è, ê in latin1 + const result = await text(from(latin1Bytes), { encoding: 'iso-8859-1' }); + assert.strictEqual(result, 'éèê'); +} + +async function testTextSyncNonUtf8Encoding() { + const latin1Bytes = new Uint8Array([0xE9, 0xE8, 0xEA]); + const result = textSync(fromSync(latin1Bytes), { encoding: 'iso-8859-1' }); + assert.strictEqual(result, 'éèê'); +} + +async function testTextInvalidUtf8() { + // Invalid UTF-8 sequence with fatal: true should throw + const invalid = new Uint8Array([0xFF, 0xFE]); + await assert.rejects( + () => text(from(invalid)), + { name: 'TypeError' }, // TextDecoder fatal throws TypeError + ); +} + +async function testTextWithLimit() { + // Limit caps total bytes; exceeding throws ERR_OUT_OF_RANGE + await assert.rejects( + () => text(from('hello world'), { limit: 5 }), + { code: 'ERR_OUT_OF_RANGE' }, + ); + // Within limit should succeed + const result = await text(from('hello'), { limit: 10 }); + assert.strictEqual(result, 'hello'); + + // Exact boundary: 'hello' is 5 UTF-8 bytes, limit: 5 should succeed + // (source uses > not >=) + const exact = await text(from('hello'), { limit: 5 }); + assert.strictEqual(exact, 'hello'); +} + +async function testTextSyncWithLimit() { + // Sync version of limit testing + assert.throws( + () => textSync(fromSync('hello world'), { limit: 5 }), + { code: 'ERR_OUT_OF_RANGE' }, + ); + const result = textSync(fromSync('hello'), { limit: 10 }); + assert.strictEqual(result, 'hello'); + + // Exact boundary + const exact = textSync(fromSync('hello'), { limit: 5 }); + assert.strictEqual(exact, 'hello'); +} + +async function testTextEmpty() { + const result = await text(from('')); + assert.strictEqual(result, ''); + + const syncResult = textSync(fromSync('')); + assert.strictEqual(syncResult, ''); +} + +// text() with abort signal +async function testTextWithSignal() { + const ac = new AbortController(); + ac.abort(); + await assert.rejects( + () => text(from('data'), { signal: ac.signal }), + { name: 'AbortError' }, + ); +} + +// Multi-chunk source with a multi-byte UTF-8 character split across chunks +async function testTextMultiChunkSplitCodepoint() { + // '€' is U+20AC, encoded as 3 UTF-8 bytes: 0xE2, 0x82, 0xAC + // Split these bytes across two chunks to test proper re-assembly + async function* splitSource() { + yield [new Uint8Array([0xE2, 0x82])]; // First 2 bytes of '€' + yield [new Uint8Array([0xAC])]; // Last byte of '€' + } + const result = await text(splitSource()); + assert.strictEqual(result, '€'); +} + +// BOM should be stripped (ignoreBOM defaults to false per spec) +async function testTextBOMStripped() { + // UTF-8 BOM: 0xEF, 0xBB, 0xBF followed by 'hi' + const withBOM = new Uint8Array([0xEF, 0xBB, 0xBF, 0x68, 0x69]); + const result = await text(from(withBOM)); + assert.strictEqual(result, 'hi'); +} + +async function testTextSyncBOMStripped() { + const withBOM = new Uint8Array([0xEF, 0xBB, 0xBF, 0x68, 0x69]); + const result = textSync(fromSync(withBOM)); + assert.strictEqual(result, 'hi'); +} + +// Unsupported encoding throws RangeError +async function testTextUnsupportedEncodingThrowsRangeError() { + await assert.rejects( + () => text(from('hello'), { encoding: 'not-a-real-encoding' }), + { name: 'RangeError' }, + ); +} + +function testTextSyncUnsupportedEncodingThrowsRangeError() { + assert.throws( + () => textSync(fromSync('hello'), { encoding: 'not-a-real-encoding' }), + { name: 'RangeError' }, + ); +} + +Promise.all([ + testTextSyncBasic(), + testTextAsync(), + testTextEncoding(), + testTextNonUtf8Encoding(), + testTextSyncNonUtf8Encoding(), + testTextInvalidUtf8(), + testTextWithLimit(), + testTextSyncWithLimit(), + testTextEmpty(), + testTextWithSignal(), + testTextMultiChunkSplitCodepoint(), + testTextBOMStripped(), + testTextSyncBOMStripped(), + testTextUnsupportedEncodingThrowsRangeError(), + testTextSyncUnsupportedEncodingThrowsRangeError(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-cross-realm.js b/test/parallel/test-stream-iter-cross-realm.js new file mode 100644 index 00000000000000..6a6e92179253ec --- /dev/null +++ b/test/parallel/test-stream-iter-cross-realm.js @@ -0,0 +1,132 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const { from, fromSync, pull, text, bytesSync } = require('stream/iter'); + +// Cross-realm objects are created in a different VM context. +// They have different prototypes, so `instanceof` checks fail. +// These tests verify that stream/iter correctly handles cross-realm types. + +// Helper: compare Uint8Array content regardless of realm. +function assertBytes(actual, expected) { + assert.strictEqual(actual.length, expected.length, + `length mismatch: ${actual.length} !== ${expected.length}`); + for (let i = 0; i < expected.length; i++) { + assert.strictEqual(actual[i], expected[i], `byte mismatch at index ${i}`); + } +} + +// ============================================================================= +// from() / fromSync() with cross-realm Uint8Array +// ============================================================================= + +async function testFromSyncCrossRealmUint8Array() { + const crossRealm = vm.runInNewContext('new Uint8Array([1, 2, 3])'); + const data = bytesSync(fromSync(crossRealm)); + assertBytes(data, new Uint8Array([1, 2, 3])); +} + +async function testFromCrossRealmUint8Array() { + const crossRealm = vm.runInNewContext('new Uint8Array([4, 5, 6])'); + const result = await text(from(crossRealm)); + assert.strictEqual(result, '\x04\x05\x06'); +} + +// ============================================================================= +// from() / fromSync() with cross-realm ArrayBuffer +// ============================================================================= + +async function testFromSyncCrossRealmArrayBuffer() { + const crossRealm = vm.runInNewContext( + 'new Uint8Array([7, 8, 9]).buffer', + ); + const data = bytesSync(fromSync(crossRealm)); + assertBytes(data, new Uint8Array([7, 8, 9])); +} + +async function testFromCrossRealmArrayBuffer() { + const crossRealm = vm.runInNewContext( + 'new Uint8Array([10, 11, 12]).buffer', + ); + const result = await text(from(crossRealm)); + assert.strictEqual(result, '\x0a\x0b\x0c'); +} + +// ============================================================================= +// from() / fromSync() with cross-realm Uint8Array[] +// ============================================================================= + +async function testFromSyncCrossRealmUint8ArrayArray() { + const crossRealm = vm.runInNewContext( + '[new Uint8Array([1, 2]), new Uint8Array([3, 4])]', + ); + const data = bytesSync(fromSync(crossRealm)); + assertBytes(data, new Uint8Array([1, 2, 3, 4])); +} + +async function testFromCrossRealmUint8ArrayArray() { + const crossRealm = vm.runInNewContext( + '[new Uint8Array([5, 6]), new Uint8Array([7, 8])]', + ); + const result = await text(from(crossRealm)); + assert.strictEqual(result, '\x05\x06\x07\x08'); +} + +// ============================================================================= +// pull() with cross-realm Uint8Array from transforms +// ============================================================================= + +async function testPullCrossRealmTransformOutput() { + // Transform that returns cross-realm Uint8Array[] batches + const crossRealmTransform = (chunks) => { + if (chunks === null) return null; + // Re-encode each chunk as cross-realm Uint8Array + return vm.runInNewContext( + `[new Uint8Array([${[...chunks[0]]}])]`, + ); + }; + const output = await text(pull(from('hello'), crossRealmTransform)); + assert.strictEqual(output, 'hello'); +} + +// ============================================================================= +// from() with cross-realm Promise +// ============================================================================= + +async function testFromCrossRealmPromise() { + const crossRealmPromise = vm.runInNewContext( + 'Promise.resolve("promised-data")', + ); + async function* gen() { + yield crossRealmPromise; + } + const result = await text(from(gen())); + assert.strictEqual(result, 'promised-data'); +} + +// ============================================================================= +// from() with cross-realm typed arrays (non-Uint8Array views) +// ============================================================================= + +async function testFromSyncCrossRealmInt32Array() { + const crossRealm = vm.runInNewContext('new Int32Array([1])'); + const data = bytesSync(fromSync(crossRealm)); + // Int32Array([1]) = 4 bytes, endianness varies by platform + assert.strictEqual(data.length, 4); + assert.strictEqual(new Int32Array(data.buffer, data.byteOffset, 1)[0], 1); +} + +Promise.all([ + testFromSyncCrossRealmUint8Array(), + testFromCrossRealmUint8Array(), + testFromSyncCrossRealmArrayBuffer(), + testFromCrossRealmArrayBuffer(), + testFromSyncCrossRealmUint8ArrayArray(), + testFromCrossRealmUint8ArrayArray(), + testPullCrossRealmTransformOutput(), + testFromCrossRealmPromise(), + testFromSyncCrossRealmInt32Array(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-disabled.js b/test/parallel/test-stream-iter-disabled.js new file mode 100644 index 00000000000000..8c8538ffae9268 --- /dev/null +++ b/test/parallel/test-stream-iter-disabled.js @@ -0,0 +1,34 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { spawnPromisified } = common; + +async function testRequireNodeStreamIterWithoutFlag() { + const { stderr, code } = await spawnPromisified(process.execPath, [ + '-e', 'require("node:stream/iter")', + ]); + assert.match(stderr, /No such built-in module: node:stream\/iter/); + assert.notStrictEqual(code, 0); +} + +async function testRequireStreamIterWithoutFlag() { + const { stderr, code } = await spawnPromisified(process.execPath, [ + '-e', 'require("stream/iter")', + ]); + assert.match(stderr, /Cannot find module/); + assert.notStrictEqual(code, 0); +} + +async function testRequireWithFlag() { + const { code } = await spawnPromisified(process.execPath, [ + '--experimental-stream-iter', + '-e', 'require("node:stream/iter")', + ]); + assert.strictEqual(code, 0); +} + +Promise.all([ + testRequireNodeStreamIterWithoutFlag(), + testRequireStreamIterWithoutFlag(), + testRequireWithFlag(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-duplex.js b/test/parallel/test-stream-iter-duplex.js new file mode 100644 index 00000000000000..83c85d7be00816 --- /dev/null +++ b/test/parallel/test-stream-iter-duplex.js @@ -0,0 +1,188 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { duplex, text, bytes } = require('stream/iter'); + +// ============================================================================= +// Basic duplex +// ============================================================================= + +async function testBasicDuplex() { + const [channelA, channelB] = duplex(); + + // A writes, B reads + await channelA.writer.write('hello from A'); + await channelA.close(); + + const dataAtB = await text(channelB.readable); + assert.strictEqual(dataAtB, 'hello from A'); +} + +async function testBidirectional() { + const [channelA, channelB] = duplex(); + + // A writes to B, B writes to A concurrently + const writeA = (async () => { + await channelA.writer.write('A to B'); + await channelA.close(); + })(); + + const writeB = (async () => { + await channelB.writer.write('B to A'); + await channelB.close(); + })(); + + const readAtB = text(channelB.readable); + const readAtA = text(channelA.readable); + + await Promise.all([writeA, writeB]); + + const [dataAtA, dataAtB] = await Promise.all([readAtA, readAtB]); + + assert.strictEqual(dataAtB, 'A to B'); + assert.strictEqual(dataAtA, 'B to A'); +} + +async function testMultipleWrites() { + const [channelA, channelB] = duplex({ highWaterMark: 10 }); + + await channelA.writer.write('one'); + await channelA.writer.write('two'); + await channelA.writer.write('three'); + await channelA.close(); + + const data = await text(channelB.readable); + assert.strictEqual(data, 'onetwothree'); +} + +async function testChannelClose() { + const [channelA, channelB] = duplex(); + + await channelA.close(); + + // Should be able to close twice without error + await channelA.close(); + + // B's readable should end (A -> B direction is closed) + const batches = []; + for await (const batch of channelB.readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 0); +} + +async function testWithOptions() { + const [channelA, channelB] = duplex({ + highWaterMark: 2, + backpressure: 'strict', + }); + + await channelA.writer.write('msg'); + await channelA.close(); + + const data = await text(channelB.readable); + assert.strictEqual(data, 'msg'); +} + +async function testPerChannelOptions() { + const [channelA, channelB] = duplex({ + a: { highWaterMark: 1 }, + b: { highWaterMark: 4 }, + }); + + // Channel A -> B direction uses A's options + // Channel B -> A direction uses B's options + await channelA.writer.write('from-a'); + await channelA.close(); + + await channelB.writer.write('from-b'); + await channelB.close(); + + const [dataAtA, dataAtB] = await Promise.all([ + text(channelA.readable), + text(channelB.readable), + ]); + + assert.strictEqual(dataAtB, 'from-a'); + assert.strictEqual(dataAtA, 'from-b'); +} + +async function testAbortSignal() { + const ac = new AbortController(); + const [channelA] = duplex({ signal: ac.signal }); + + ac.abort(); + + // Both directions should error + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of channelA.readable) { + assert.fail('Should not reach here'); + } + }, + (err) => err.name === 'AbortError', + ); +} + +async function testEmptyDuplex() { + const [channelA, channelB] = duplex(); + + // Close without writing + await channelA.close(); + await channelB.close(); + + const dataAtA = await bytes(channelA.readable); + const dataAtB = await bytes(channelB.readable); + + assert.strictEqual(dataAtA.byteLength, 0); + assert.strictEqual(dataAtB.byteLength, 0); +} + +// Channel fail propagation +async function testChannelFail() { + const [a, b] = duplex(); + a.writer.fail(new Error('channel failed')); + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of b.readable) { /* consume */ } + }, { message: 'channel failed' }); + await b.close(); +} + +// Abort signal affects both channels +async function testAbortSignalBothChannels() { + const ac = new AbortController(); + const [channelA, channelB] = duplex({ signal: ac.signal }); + + ac.abort(); + + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of channelA.readable) { + assert.fail('Should not reach here'); + } + }, (err) => err.name === 'AbortError'); + + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of channelB.readable) { + assert.fail('Should not reach here'); + } + }, (err) => err.name === 'AbortError'); +} + +Promise.all([ + testBasicDuplex(), + testBidirectional(), + testMultipleWrites(), + testChannelClose(), + testWithOptions(), + testPerChannelOptions(), + testAbortSignal(), + testEmptyDuplex(), + testChannelFail(), + testAbortSignalBothChannels(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-from-async.js b/test/parallel/test-stream-iter-from-async.js new file mode 100644 index 00000000000000..8080b7a5cd86ca --- /dev/null +++ b/test/parallel/test-stream-iter-from-async.js @@ -0,0 +1,251 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { from, text, Stream } = require('stream/iter'); + +async function testFromString() { + const readable = from('hello-async'); + const batches = []; + for await (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], + new TextEncoder().encode('hello-async')); +} + +async function testFromAsyncGenerator() { + async function* gen() { + yield new Uint8Array([10, 20]); + yield new Uint8Array([30, 40]); + } + const readable = from(gen()); + const batches = []; + for await (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 2); + assert.deepStrictEqual(batches[0][0], new Uint8Array([10, 20])); + assert.deepStrictEqual(batches[1][0], new Uint8Array([30, 40])); +} + +async function testFromSyncIterableAsAsync() { + // Sync iterable passed to from() should work + function* gen() { + yield new Uint8Array([1]); + yield new Uint8Array([2]); + } + const readable = from(gen()); + const batches = []; + for await (const batch of readable) { + batches.push(batch); + } + // Sync iterables get batched together into a single batch + assert.strictEqual(batches.length, 1); + assert.strictEqual(batches[0].length, 2); + assert.deepStrictEqual(batches[0][0], new Uint8Array([1])); + assert.deepStrictEqual(batches[0][1], new Uint8Array([2])); +} + +async function testFromToAsyncStreamableProtocol() { + const sym = Symbol.for('Stream.toAsyncStreamable'); + const obj = { + [sym]() { + return 'async-protocol-data'; + }, + }; + async function* gen() { + yield obj; + } + const readable = from(gen()); + const batches = []; + for await (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], + new TextEncoder().encode('async-protocol-data')); +} + +function testFromRejectsNonStreamable() { + assert.throws( + () => from(12345), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + assert.throws( + () => from(null), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +async function testFromEmptyArray() { + const readable = from([]); + const batches = []; + for await (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 0); +} + +// Also accessible via Stream namespace +async function testStreamNamespace() { + const readable = Stream.from('via-namespace'); + const batches = []; + for await (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], new TextEncoder().encode('via-namespace')); +} + +async function testCustomToStringInStreamRejects() { + // Objects with custom toString but no toStreamable protocol are rejected. + // Use toStreamable protocol instead. + const obj = { toString() { return 'from toString'; } }; + async function* source() { + yield obj; + } + await assert.rejects( + () => text(from(source())), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +async function testCustomToPrimitiveInStreamRejects() { + // Objects with Symbol.toPrimitive but no toStreamable protocol are rejected. + const obj = { + [Symbol.toPrimitive](hint) { + if (hint === 'string') return 'from toPrimitive'; + return 42; + }, + }; + async function* source() { + yield obj; + } + await assert.rejects( + () => text(from(source())), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +async function testToStreamableProtocolInStream() { + // Objects should use toStreamable protocol instead of toString + const obj = { + [Symbol.for('Stream.toStreamable')]() { return 'from protocol'; }, + }; + async function* source() { + yield obj; + } + const result = await text(from(source())); + assert.strictEqual(result, 'from protocol'); +} + +// Both toAsyncStreamable and toStreamable: async takes precedence +async function testFromAsyncStreamablePrecedence() { + const obj = { + [Symbol.for('Stream.toStreamable')]() { return 'sync version'; }, + [Symbol.for('Stream.toAsyncStreamable')]() { return 'async version'; }, + }; + async function* gen() { yield obj; } + const result = await text(from(gen())); + assert.strictEqual(result, 'async version'); +} + +// Top-level toAsyncStreamable protocol on input to from() +async function testFromTopLevelToAsyncStreamable() { + const obj = { + [Symbol.for('Stream.toAsyncStreamable')]() { + return 'top-level-async'; + }, + }; + const result = await text(from(obj)); + assert.strictEqual(result, 'top-level-async'); +} + +// Top-level toAsyncStreamable returning a Promise +async function testFromTopLevelToAsyncStreamablePromise() { + const obj = { + [Symbol.for('Stream.toAsyncStreamable')]() { + return Promise.resolve('async-promise'); + }, + }; + const result = await text(from(obj)); + assert.strictEqual(result, 'async-promise'); +} + +// Top-level toStreamable protocol on input to from() +async function testFromTopLevelToStreamable() { + const obj = { + [Symbol.for('Stream.toStreamable')]() { + return 'top-level-sync'; + }, + }; + const result = await text(from(obj)); + assert.strictEqual(result, 'top-level-sync'); +} + +// Top-level: toAsyncStreamable takes precedence over toStreamable +async function testFromTopLevelAsyncPrecedence() { + const obj = { + [Symbol.for('Stream.toStreamable')]() { return 'sync'; }, + [Symbol.for('Stream.toAsyncStreamable')]() { return 'async'; }, + }; + const result = await text(from(obj)); + assert.strictEqual(result, 'async'); +} + +// Top-level: toAsyncStreamable takes precedence over Symbol.asyncIterator +async function testFromTopLevelProtocolOverIterator() { + const obj = { + [Symbol.for('Stream.toAsyncStreamable')]() { return 'from-protocol'; }, + async *[Symbol.asyncIterator]() { yield [new TextEncoder().encode('from-iterator')]; }, + }; + const result = await text(from(obj)); + assert.strictEqual(result, 'from-protocol'); +} + +// DataView input should be converted to Uint8Array (zero-copy) +async function testFromDataView() { + const buf = new ArrayBuffer(5); + const view = new DataView(buf); + // Write "hello" into the DataView + view.setUint8(0, 0x68); // h + view.setUint8(1, 0x65); // e + view.setUint8(2, 0x6c); // l + view.setUint8(3, 0x6c); // l + view.setUint8(4, 0x6f); // o + const result = await text(from(view)); + assert.strictEqual(result, 'hello'); +} + +function testFromNullThrows() { + assert.throws(() => from(null), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +function testFromUndefinedThrows() { + assert.throws(() => from(undefined), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +Promise.all([ + testFromString(), + testFromAsyncGenerator(), + testFromSyncIterableAsAsync(), + testFromToAsyncStreamableProtocol(), + testFromRejectsNonStreamable(), + testFromEmptyArray(), + testStreamNamespace(), + testCustomToStringInStreamRejects(), + testCustomToPrimitiveInStreamRejects(), + testToStreamableProtocolInStream(), + testFromAsyncStreamablePrecedence(), + testFromNullThrows(), + testFromUndefinedThrows(), + testFromTopLevelToAsyncStreamable(), + testFromTopLevelToAsyncStreamablePromise(), + testFromTopLevelToStreamable(), + testFromTopLevelAsyncPrecedence(), + testFromTopLevelProtocolOverIterator(), + testFromDataView(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-from-coverage.js b/test/parallel/test-stream-iter-from-coverage.js new file mode 100644 index 00000000000000..eb75bec4826206 --- /dev/null +++ b/test/parallel/test-stream-iter-from-coverage.js @@ -0,0 +1,178 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Coverage tests for from.js: sub-batching >128, DataView in generator, +// non-Uint8Array TypedArray normalization. + +const common = require('../common'); +const assert = require('assert'); +const { + from, + fromSync, + bytes, + bytesSync, +} = require('stream/iter'); + +// fromSync: Uint8Array[] with > 128 elements triggers sub-batching +async function testFromSyncSubBatching() { + const bigBatch = Array.from({ length: 200 }, + (_, i) => new Uint8Array([i & 0xFF])); + const batches = []; + for (const batch of fromSync(bigBatch)) { + batches.push(batch); + } + // Should be split into sub-batches: 128 + 72 + assert.strictEqual(batches.length, 2); + assert.strictEqual(batches[0].length, 128); + assert.strictEqual(batches[1].length, 72); + // Verify no data loss + let totalChunks = 0; + for (const batch of batches) totalChunks += batch.length; + assert.strictEqual(totalChunks, 200); +} + +// fromSync: generic sync iterables of Uint8Array use bounded batches +async function testFromSyncIterableSubBatching() { + function* gen() { + for (let i = 0; i < 200; i++) { + yield new Uint8Array([i & 0xFF]); + } + } + const batches = []; + for (const batch of fromSync(gen())) { + batches.push(batch); + } + assert.strictEqual(batches.length, 2); + assert.strictEqual(batches[0].length, 128); + assert.strictEqual(batches[1].length, 72); +} + +// from: Uint8Array[] with > 128 elements triggers sub-batching (async) +async function testFromAsyncSubBatching() { + const bigBatch = Array.from({ length: 200 }, + (_, i) => new Uint8Array([i & 0xFF])); + const batches = []; + for await (const batch of from(bigBatch)) { + batches.push(batch); + } + assert.strictEqual(batches.length, 2); + assert.strictEqual(batches[0].length, 128); + assert.strictEqual(batches[1].length, 72); +} + +// from: sync iterables use bounded batches instead of one unbounded batch +async function testFromAsyncSyncIterableSubBatching() { + function* gen() { + for (let i = 0; i < 200; i++) { + yield new Uint8Array([i & 0xFF]); + } + } + const batches = []; + for await (const batch of from(gen())) { + batches.push(batch); + } + assert.strictEqual(batches.length, 2); + assert.strictEqual(batches[0].length, 128); + assert.strictEqual(batches[1].length, 72); +} + +// Exact boundary: 128 elements → single batch (no split) +async function testFromSubBatchingBoundary() { + const exactBatch = Array.from({ length: 128 }, + (_, i) => new Uint8Array([i])); + const batches = []; + for (const batch of fromSync(exactBatch)) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.strictEqual(batches[0].length, 128); +} + +// 129 elements → 2 batches (128 + 1) +async function testFromSubBatchingBoundaryPlus1() { + const batch129 = Array.from({ length: 129 }, + (_, i) => new Uint8Array([i & 0xFF])); + const batches = []; + for await (const batch of from(batch129)) { + batches.push(batch); + } + assert.strictEqual(batches.length, 2); + assert.strictEqual(batches[0].length, 128); + assert.strictEqual(batches[1].length, 1); +} + +// DataView yielded from a sync generator → normalizeSyncValue path +async function testFromSyncDataViewInGenerator() { + function* gen() { + const buf = new ArrayBuffer(3); + const dv = new DataView(buf); + dv.setUint8(0, 65); + dv.setUint8(1, 66); + dv.setUint8(2, 67); + yield dv; + } + const data = bytesSync(fromSync(gen())); + assert.deepStrictEqual(data, new Uint8Array([65, 66, 67])); +} + +// DataView yielded from an async generator → normalizeAsyncValue path +async function testFromAsyncDataViewInGenerator() { + async function* gen() { + const buf = new ArrayBuffer(3); + const dv = new DataView(buf); + dv.setUint8(0, 68); + dv.setUint8(1, 69); + dv.setUint8(2, 70); + yield dv; + } + const data = await bytes(from(gen())); + assert.deepStrictEqual(data, new Uint8Array([68, 69, 70])); +} + +// Int16Array yielded from generator → primitiveToUint8Array fallback +async function testFromSyncInt16ArrayInGenerator() { + function* gen() { + yield new Int16Array([0x0102, 0x0304]); + } + const data = bytesSync(fromSync(gen())); + assert.strictEqual(data.byteLength, 4); // 2 int16 = 4 bytes +} + +// Float64Array as top-level input to from() +async function testFromFloat64Array() { + const f64 = new Float64Array([1.0]); + const batches = []; + for await (const batch of from(f64)) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.strictEqual(batches[0][0].byteLength, 8); // 1 float64 = 8 bytes +} + +// Sync generator yielding invalid type → ERR_INVALID_ARG_TYPE +async function testFromSyncInvalidYield() { + function* gen() { + yield 42; // Not a valid stream value + } + assert.throws( + () => { + // eslint-disable-next-line no-unused-vars + for (const batch of fromSync(gen())) { /* consume */ } + }, + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +Promise.all([ + testFromSyncSubBatching(), + testFromSyncIterableSubBatching(), + testFromAsyncSubBatching(), + testFromAsyncSyncIterableSubBatching(), + testFromSubBatchingBoundary(), + testFromSubBatchingBoundaryPlus1(), + testFromSyncDataViewInGenerator(), + testFromAsyncDataViewInGenerator(), + testFromSyncInt16ArrayInGenerator(), + testFromFloat64Array(), + testFromSyncInvalidYield(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-from-sync.js b/test/parallel/test-stream-iter-from-sync.js new file mode 100644 index 00000000000000..d3a5cf671e10d8 --- /dev/null +++ b/test/parallel/test-stream-iter-from-sync.js @@ -0,0 +1,237 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { fromSync } = require('stream/iter'); + +function testFromSyncString() { + // String input should be UTF-8 encoded + const readable = fromSync('hello'); + const batches = []; + for (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.strictEqual(batches[0].length, 1); + assert.deepStrictEqual(batches[0][0], + new TextEncoder().encode('hello')); +} + +function testFromSyncUint8Array() { + const input = new Uint8Array([1, 2, 3]); + const readable = fromSync(input); + const batches = []; + for (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.strictEqual(batches[0].length, 1); + assert.deepStrictEqual(batches[0][0], input); +} + +function testFromSyncArrayBuffer() { + const ab = new ArrayBuffer(4); + new Uint8Array(ab).set([10, 20, 30, 40]); + const readable = fromSync(ab); + const batches = []; + for (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], new Uint8Array([10, 20, 30, 40])); +} + +function testFromSyncUint8ArrayArray() { + // Array of Uint8Array should yield as a single batch + const chunks = [new Uint8Array([1]), new Uint8Array([2])]; + const readable = fromSync(chunks); + const batches = []; + for (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.strictEqual(batches[0].length, 2); + assert.deepStrictEqual(batches[0][0], new Uint8Array([1])); + assert.deepStrictEqual(batches[0][1], new Uint8Array([2])); +} + +function testFromSyncGenerator() { + function* gen() { + yield new Uint8Array([1, 2]); + yield new Uint8Array([3, 4]); + } + const readable = fromSync(gen()); + const batches = []; + for (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.strictEqual(batches[0].length, 2); + assert.deepStrictEqual(batches[0][0], new Uint8Array([1, 2])); + assert.deepStrictEqual(batches[0][1], new Uint8Array([3, 4])); +} + +function testFromSyncNestedIterables() { + // Nested arrays and strings should be flattened + function* gen() { + yield ['hello', ' ', 'world']; + } + const readable = fromSync(gen()); + const batches = []; + for (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.strictEqual(batches[0].length, 3); + assert.deepStrictEqual(batches[0][0], new TextEncoder().encode('hello')); + assert.deepStrictEqual(batches[0][1], new TextEncoder().encode(' ')); + assert.deepStrictEqual(batches[0][2], new TextEncoder().encode('world')); +} + +function testFromSyncToStreamableProtocol() { + const sym = Symbol.for('Stream.toStreamable'); + const obj = { + [sym]() { + return 'protocol-data'; + }, + }; + function* gen() { + yield obj; + } + const readable = fromSync(gen()); + const batches = []; + for (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], + new TextEncoder().encode('protocol-data')); +} + +function testFromSyncGeneratorError() { + function* gen() { + yield new Uint8Array([1]); + throw new Error('generator boom'); + } + const readable = fromSync(gen()); + assert.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const _ of readable) { /* consume */ } + }, { message: 'generator boom' }); +} + +function testFromSyncRejectsNonStreamable() { + assert.throws( + () => fromSync(12345), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + assert.throws( + () => fromSync(null), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +function testFromSyncEmptyGenerator() { + function* empty() {} + let count = 0; + // eslint-disable-next-line no-unused-vars + for (const _ of fromSync(empty())) { count++; } + assert.strictEqual(count, 0); +} + +// Top-level toStreamable protocol on input to fromSync() +function testFromSyncTopLevelToStreamable() { + const obj = { + [Symbol.for('Stream.toStreamable')]() { + return 'top-level-sync'; + }, + }; + const batches = []; + for (const batch of fromSync(obj)) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], + new TextEncoder().encode('top-level-sync')); +} + +// Top-level: toStreamable takes precedence over Symbol.iterator +function testFromSyncTopLevelProtocolOverIterator() { + const obj = { + [Symbol.for('Stream.toStreamable')]() { return 'from-protocol'; }, + *[Symbol.iterator]() { yield [new TextEncoder().encode('from-iterator')]; }, + }; + const batches = []; + for (const batch of fromSync(obj)) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], + new TextEncoder().encode('from-protocol')); +} + +// Top-level: toAsyncStreamable is ignored by fromSync +function testFromSyncIgnoresAsyncStreamable() { + const obj = { + [Symbol.for('Stream.toAsyncStreamable')]() { return 'async'; }, + }; + // Has no toStreamable and no Symbol.iterator, should throw + assert.throws(() => fromSync(obj), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// Explicit async iterable rejected +function testFromSyncRejectsAsyncIterable() { + async function* gen() { yield [new TextEncoder().encode('a')]; } + assert.throws(() => fromSync(gen()), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// Promise rejected +function testFromSyncRejectsPromise() { + assert.throws(() => fromSync(Promise.resolve('hello')), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// DataView input should be converted to Uint8Array (zero-copy) +function testFromSyncDataView() { + const buf = new ArrayBuffer(3); + const view = new DataView(buf); + view.setUint8(0, 0x48); // H + view.setUint8(1, 0x49); // I + view.setUint8(2, 0x21); // ! + const batches = []; + for (const batch of fromSync(view)) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], new Uint8Array([0x48, 0x49, 0x21])); +} + +function testFromSyncNullThrows() { + assert.throws(() => fromSync(null), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +function testFromSyncUndefinedThrows() { + assert.throws(() => fromSync(undefined), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +Promise.all([ + testFromSyncString(), + testFromSyncUint8Array(), + testFromSyncArrayBuffer(), + testFromSyncUint8ArrayArray(), + testFromSyncGenerator(), + testFromSyncNestedIterables(), + testFromSyncToStreamableProtocol(), + testFromSyncGeneratorError(), + testFromSyncRejectsNonStreamable(), + testFromSyncEmptyGenerator(), + testFromSyncNullThrows(), + testFromSyncUndefinedThrows(), + testFromSyncTopLevelToStreamable(), + testFromSyncTopLevelProtocolOverIterator(), + testFromSyncIgnoresAsyncStreamable(), + testFromSyncRejectsAsyncIterable(), + testFromSyncRejectsPromise(), + testFromSyncDataView(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-from-writable-cache-options.js b/test/parallel/test-stream-iter-from-writable-cache-options.js new file mode 100644 index 00000000000000..67e673c36f6f2f --- /dev/null +++ b/test/parallel/test-stream-iter-from-writable-cache-options.js @@ -0,0 +1,45 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Writable } = require('stream'); +const { fromWritable } = require('stream/iter'); + +{ + const writable = new Writable({ write() {} }); + + fromWritable(writable); + + assert.throws( + () => fromWritable(writable, { backpressure: 'invalid' }), + { code: 'ERR_INVALID_ARG_VALUE' }, + ); + + writable.destroy(); +} + +async function testCachedWritableUsesLaterBackpressureOptions() { + const chunks = []; + const writable = new Writable({ + highWaterMark: 1, + write(chunk, encoding, callback) { + chunks.push(Buffer.from(chunk)); + }, + }); + + fromWritable(writable); + const writer = fromWritable(writable, { backpressure: 'drop-newest' }); + + await writer.write('a'); + await writer.write('b'); + + assert.deepStrictEqual( + chunks.map((chunk) => chunk.toString()), + ['a'], + ); + + writable.destroy(); +} + +testCachedWritableUsesLaterBackpressureOptions().then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-namespace.js b/test/parallel/test-stream-iter-namespace.js new file mode 100644 index 00000000000000..ce197e85846dfe --- /dev/null +++ b/test/parallel/test-stream-iter-namespace.js @@ -0,0 +1,210 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const streamNew = require('stream/iter'); + +// ============================================================================= +// Stream namespace object +// ============================================================================= + +async function testStreamNamespaceExists() { + assert.ok(streamNew.Stream); + assert.strictEqual(typeof streamNew.Stream, 'object'); +} + +async function testStreamNamespaceFrozen() { + assert.ok(Object.isFrozen(streamNew.Stream)); +} + +async function testStreamNamespaceFactories() { + const { Stream } = streamNew; + + assert.strictEqual(typeof Stream.push, 'function'); + assert.strictEqual(typeof Stream.duplex, 'function'); + assert.strictEqual(typeof Stream.from, 'function'); + assert.strictEqual(typeof Stream.fromSync, 'function'); +} + +async function testStreamNamespacePipelines() { + const { Stream } = streamNew; + + assert.strictEqual(typeof Stream.pull, 'function'); + assert.strictEqual(typeof Stream.pullSync, 'function'); + assert.strictEqual(typeof Stream.pipeTo, 'function'); + assert.strictEqual(typeof Stream.pipeToSync, 'function'); +} + +async function testStreamNamespaceAsyncConsumers() { + const { Stream } = streamNew; + + assert.strictEqual(typeof Stream.bytes, 'function'); + assert.strictEqual(typeof Stream.text, 'function'); + assert.strictEqual(typeof Stream.arrayBuffer, 'function'); + assert.strictEqual(typeof Stream.array, 'function'); +} + +async function testStreamNamespaceSyncConsumers() { + const { Stream } = streamNew; + + assert.strictEqual(typeof Stream.bytesSync, 'function'); + assert.strictEqual(typeof Stream.textSync, 'function'); + assert.strictEqual(typeof Stream.arrayBufferSync, 'function'); + assert.strictEqual(typeof Stream.arraySync, 'function'); +} + +async function testStreamNamespaceCombining() { + const { Stream } = streamNew; + + assert.strictEqual(typeof Stream.merge, 'function'); + assert.strictEqual(typeof Stream.broadcast, 'function'); + assert.strictEqual(typeof Stream.share, 'function'); + assert.strictEqual(typeof Stream.shareSync, 'function'); +} + +async function testStreamNamespaceUtilities() { + const { Stream } = streamNew; + + assert.strictEqual(typeof Stream.tap, 'function'); + assert.strictEqual(typeof Stream.tapSync, 'function'); + assert.strictEqual(typeof Stream.ondrain, 'function'); +} + +async function testStreamNamespaceProtocols() { + const { Stream } = streamNew; + + assert.strictEqual(typeof Stream.toStreamable, 'symbol'); + assert.strictEqual(typeof Stream.toAsyncStreamable, 'symbol'); + assert.strictEqual(typeof Stream.broadcastProtocol, 'symbol'); + assert.strictEqual(typeof Stream.shareProtocol, 'symbol'); + assert.strictEqual(typeof Stream.shareSyncProtocol, 'symbol'); + assert.strictEqual(typeof Stream.drainableProtocol, 'symbol'); +} + +// ============================================================================= +// Individual exports (destructured imports) +// ============================================================================= + +async function testIndividualExports() { + // Factories + assert.strictEqual(typeof streamNew.push, 'function'); + assert.strictEqual(typeof streamNew.duplex, 'function'); + assert.strictEqual(typeof streamNew.from, 'function'); + assert.strictEqual(typeof streamNew.fromSync, 'function'); + + // Pipelines + assert.strictEqual(typeof streamNew.pull, 'function'); + assert.strictEqual(typeof streamNew.pullSync, 'function'); + assert.strictEqual(typeof streamNew.pipeTo, 'function'); + assert.strictEqual(typeof streamNew.pipeToSync, 'function'); + + // Consumers + assert.strictEqual(typeof streamNew.bytes, 'function'); + assert.strictEqual(typeof streamNew.bytesSync, 'function'); + assert.strictEqual(typeof streamNew.text, 'function'); + assert.strictEqual(typeof streamNew.textSync, 'function'); + assert.strictEqual(typeof streamNew.arrayBuffer, 'function'); + assert.strictEqual(typeof streamNew.arrayBufferSync, 'function'); + assert.strictEqual(typeof streamNew.array, 'function'); + assert.strictEqual(typeof streamNew.arraySync, 'function'); + + // Combining + assert.strictEqual(typeof streamNew.merge, 'function'); + assert.strictEqual(typeof streamNew.broadcast, 'function'); + assert.strictEqual(typeof streamNew.share, 'function'); + assert.strictEqual(typeof streamNew.shareSync, 'function'); + + // Utilities + assert.strictEqual(typeof streamNew.tap, 'function'); + assert.strictEqual(typeof streamNew.tapSync, 'function'); + assert.strictEqual(typeof streamNew.ondrain, 'function'); + + // Protocol symbols + assert.strictEqual(typeof streamNew.toStreamable, 'symbol'); + assert.strictEqual(typeof streamNew.toAsyncStreamable, 'symbol'); + assert.strictEqual(typeof streamNew.broadcastProtocol, 'symbol'); + assert.strictEqual(typeof streamNew.shareProtocol, 'symbol'); + assert.strictEqual(typeof streamNew.shareSyncProtocol, 'symbol'); + assert.strictEqual(typeof streamNew.drainableProtocol, 'symbol'); +} + +async function testMultiConsumerExports() { + // Broadcast and Share constructors/factories + assert.ok(streamNew.Broadcast); + assert.strictEqual(typeof streamNew.Broadcast.from, 'function'); + assert.ok(streamNew.Share); + assert.strictEqual(typeof streamNew.Share.from, 'function'); + assert.ok(streamNew.SyncShare); + assert.strictEqual(typeof streamNew.SyncShare.fromSync, 'function'); +} + +// ============================================================================= +// Cross-check: namespace matches individual exports +// ============================================================================= + +async function testNamespaceMatchesExports() { + const { Stream } = streamNew; + + // Every function on Stream should also be available as a direct export + assert.strictEqual(Stream.push, streamNew.push); + assert.strictEqual(Stream.duplex, streamNew.duplex); + assert.strictEqual(Stream.from, streamNew.from); + assert.strictEqual(Stream.fromSync, streamNew.fromSync); + assert.strictEqual(Stream.pull, streamNew.pull); + assert.strictEqual(Stream.pullSync, streamNew.pullSync); + assert.strictEqual(Stream.pipeTo, streamNew.pipeTo); + assert.strictEqual(Stream.pipeToSync, streamNew.pipeToSync); + assert.strictEqual(Stream.bytes, streamNew.bytes); + assert.strictEqual(Stream.text, streamNew.text); + assert.strictEqual(Stream.arrayBuffer, streamNew.arrayBuffer); + assert.strictEqual(Stream.array, streamNew.array); + assert.strictEqual(Stream.bytesSync, streamNew.bytesSync); + assert.strictEqual(Stream.textSync, streamNew.textSync); + assert.strictEqual(Stream.arrayBufferSync, streamNew.arrayBufferSync); + assert.strictEqual(Stream.arraySync, streamNew.arraySync); + assert.strictEqual(Stream.merge, streamNew.merge); + assert.strictEqual(Stream.broadcast, streamNew.broadcast); + assert.strictEqual(Stream.share, streamNew.share); + assert.strictEqual(Stream.shareSync, streamNew.shareSync); + assert.strictEqual(Stream.tap, streamNew.tap); + assert.strictEqual(Stream.tapSync, streamNew.tapSync); + assert.strictEqual(Stream.ondrain, streamNew.ondrain); + + // Protocol symbols + assert.strictEqual(Stream.toStreamable, streamNew.toStreamable); + assert.strictEqual(Stream.toAsyncStreamable, streamNew.toAsyncStreamable); + assert.strictEqual(Stream.broadcastProtocol, streamNew.broadcastProtocol); + assert.strictEqual(Stream.shareProtocol, streamNew.shareProtocol); + assert.strictEqual(Stream.shareSyncProtocol, streamNew.shareSyncProtocol); + assert.strictEqual(Stream.drainableProtocol, streamNew.drainableProtocol); +} + +// ============================================================================= +// Require paths +// ============================================================================= + +async function testRequirePaths() { + // Both require('stream/iter') and require('node:stream/iter') should work + const fromPlain = require('stream/iter'); + const fromNode = require('node:stream/iter'); + + assert.strictEqual(fromPlain.Stream, fromNode.Stream); + assert.strictEqual(fromPlain.push, fromNode.push); +} + +Promise.all([ + testStreamNamespaceExists(), + testStreamNamespaceFrozen(), + testStreamNamespaceFactories(), + testStreamNamespacePipelines(), + testStreamNamespaceAsyncConsumers(), + testStreamNamespaceSyncConsumers(), + testStreamNamespaceCombining(), + testStreamNamespaceUtilities(), + testStreamNamespaceProtocols(), + testIndividualExports(), + testMultiConsumerExports(), + testNamespaceMatchesExports(), + testRequirePaths(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-pipeto-edge.js b/test/parallel/test-stream-iter-pipeto-edge.js new file mode 100644 index 00000000000000..3f09c4dfd42d00 --- /dev/null +++ b/test/parallel/test-stream-iter-pipeto-edge.js @@ -0,0 +1,68 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Edge case tests for pipeToSync: endSync fallback, preventFail. + +const common = require('../common'); +const assert = require('assert'); +const { pipeToSync, fromSync } = require('stream/iter'); + +// pipeToSync endSync returns negative → falls back to end() +async function testPipeToSyncEndSyncFallback() { + let endCalled = false; + const writer = { + writeSync() { return true; }, + endSync() { return -1; }, // Negative → triggers end() fallback + end() { endCalled = true; }, + }; + pipeToSync(fromSync('data'), writer); + assert.strictEqual(endCalled, true); +} + +// pipeToSync endSync missing → falls back to end() +async function testPipeToSyncNoEndSync() { + let endCalled = false; + const writer = { + writeSync() { return true; }, + end() { endCalled = true; }, + }; + pipeToSync(fromSync('data'), writer); + assert.strictEqual(endCalled, true); +} + +// pipeToSync with preventFail: true — source error does NOT call fail() +async function testPipeToSyncPreventFail() { + let failCalled = false; + const writer = { + writeSync() { return true; }, + endSync() { return 0; }, + fail() { failCalled = true; }, + }; + function* badSource() { + yield [new Uint8Array([1])]; + throw new Error('source error'); + } + assert.throws( + () => pipeToSync(badSource(), writer, { preventFail: true }), + { message: 'source error' }, + ); + assert.strictEqual(failCalled, false); +} + +// pipeToSync with preventClose: true — end/endSync not called +async function testPipeToSyncPreventClose() { + let endCalled = false; + const writer = { + writeSync() { return true; }, + endSync() { endCalled = true; return 0; }, + }; + pipeToSync(fromSync('data'), writer, { preventClose: true }); + assert.strictEqual(endCalled, false); +} + +Promise.all([ + testPipeToSyncEndSyncFallback(), + testPipeToSyncNoEndSync(), + testPipeToSyncPreventFail(), + testPipeToSyncPreventClose(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-pipeto-signal.js b/test/parallel/test-stream-iter-pipeto-signal.js new file mode 100644 index 00000000000000..ec1324a4e04bdc --- /dev/null +++ b/test/parallel/test-stream-iter-pipeto-signal.js @@ -0,0 +1,124 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Tests for pipeTo with live (non-pre-aborted) AbortSignal, +// both with and without transforms. + +const common = require('../common'); +const assert = require('assert'); +const { setTimeout } = require('timers/promises'); +const { pipeTo, from } = require('stream/iter'); + +// pipeTo with live signal, no transforms — abort mid-stream +async function testPipeToLiveSignalNoTransforms() { + const ac = new AbortController(); + const written = []; + const writer = { + async write(chunk) { written.push(chunk); }, + async end() {}, + }; + async function* source() { + yield [new Uint8Array([1])]; + yield [new Uint8Array([2])]; + ac.abort(); + yield [new Uint8Array([3])]; + } + await assert.rejects( + () => pipeTo(source(), writer, { signal: ac.signal }), + { name: 'AbortError' }, + ); + // Should have written at least the first two chunks before abort + assert.ok(written.length >= 1); +} + +// pipeTo with live signal, no transforms — abort while waiting for next chunk +async function testPipeToLiveSignalNoTransformsPendingNext() { + const ac = new AbortController(); + const reason = new Error('abort reason'); + const writer = { + write: common.mustNotCall(), + }; + const source = { + [Symbol.asyncIterator]() { + return { + next() { + return new Promise(() => {}); + }, + }; + }, + }; + + setTimeout(10) + .then(() => ac.abort(reason)) + .then(common.mustCall()); + + const result = await Promise.race([ + assert.rejects( + () => pipeTo(source, writer, { signal: ac.signal }), + reason, + ).then(() => 'aborted'), + setTimeout(1000, 'timed out'), + ]); + + assert.strictEqual(result, 'aborted'); +} + +// pipeTo with live signal + transforms — abort mid-stream +async function testPipeToLiveSignalWithTransforms() { + const ac = new AbortController(); + const written = []; + const writer = { + async write(chunk) { written.push(chunk); }, + async end() {}, + }; + const identity = (chunks) => chunks; + async function* source() { + yield [new Uint8Array([10])]; + yield [new Uint8Array([20])]; + ac.abort(); + yield [new Uint8Array([30])]; + } + await assert.rejects( + () => pipeTo(source(), identity, writer, { signal: ac.signal }), + { name: 'AbortError' }, + ); + assert.ok(written.length >= 1); +} + +// pipeTo with live signal, no abort — runs to completion +async function testPipeToLiveSignalCompletes() { + const ac = new AbortController(); + const written = []; + const writer = { + write(chunk) { written.push(chunk); }, + writeSync(chunk) { written.push(chunk); return true; }, + async end() {}, + endSync() { return written.length; }, + }; + await pipeTo(from('signal-ok'), writer, { signal: ac.signal }); + assert.ok(written.length > 0); +} + +// pipeTo with live signal + transforms, no abort — runs to completion +async function testPipeToLiveSignalWithTransformsCompletes() { + const ac = new AbortController(); + const written = []; + const writer = { + write(chunk) { written.push(chunk); }, + writeSync(chunk) { written.push(chunk); return true; }, + async end() {}, + endSync() { return written.length; }, + }; + const identity = (chunks) => chunks; + await pipeTo(from('signal-tx-ok'), identity, writer, + { signal: ac.signal }); + assert.ok(written.length > 0); +} + +Promise.all([ + testPipeToLiveSignalNoTransforms(), + testPipeToLiveSignalNoTransformsPendingNext(), + testPipeToLiveSignalWithTransforms(), + testPipeToLiveSignalCompletes(), + testPipeToLiveSignalWithTransformsCompletes(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-pipeto-writev.js b/test/parallel/test-stream-iter-pipeto-writev.js new file mode 100644 index 00000000000000..c466b007dc4a75 --- /dev/null +++ b/test/parallel/test-stream-iter-pipeto-writev.js @@ -0,0 +1,260 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Tests for pipeTo writev/writevSync paths and writeBatchAsyncFallback. + +const common = require('../common'); +const assert = require('assert'); +const { setImmediate: setImmediatePromise } = require('timers/promises'); +const { pipeTo, pipeToSync, push, text } = require('stream/iter'); + +// Multi-chunk batch with writevSync (sync success path) +async function testWritevSyncSuccess() { + const batches = []; + const writer = { + write(chunk) {}, + writevSync(chunks) { batches.push(chunks); return true; }, + writev(chunks) { batches.push(chunks); }, + writeSync(chunk) { return true; }, + endSync() { return 0; }, + }; + // Source that yields multi-chunk batches + async function* source() { + yield [new Uint8Array([1]), new Uint8Array([2]), new Uint8Array([3])]; + yield [new Uint8Array([4]), new Uint8Array([5])]; + } + const total = await pipeTo(source(), writer); + assert.ok(batches.length > 0); + // writevSync was used for multi-chunk batches + assert.ok(batches.some((b) => b.length > 1)); + assert.strictEqual(total, 5); +} + +// Multi-chunk batch with writev async (no writevSync) +async function testWritevAsyncFallback() { + const batches = []; + const writer = { + async writev(chunks) { batches.push(chunks); }, + async write(chunk) { batches.push([chunk]); }, + async end() {}, + }; + async function* source() { + yield [new Uint8Array([1]), new Uint8Array([2]), new Uint8Array([3])]; + } + await pipeTo(source(), writer); + assert.ok(batches.length > 0); + assert.ok(batches.some((b) => b.length > 1)); +} + +// Multi-chunk batch with synchronous writev success (returns undefined) +async function testWritevSyncUndefinedSuccess() { + const chunks = []; + const writer = { + write(chunk) { + chunks.push(chunk); + }, + writev(batch) { + chunks.push(...batch); + }, + end() {}, + }; + async function* source() { + yield [new Uint8Array([65]), new Uint8Array([66])]; + } + const total = await pipeTo(source(), writer); + assert.strictEqual(total, 2); + assert.strictEqual(Buffer.concat(chunks).toString(), 'AB'); +} + +// writevSync returns false — falls through to async writev +async function testWritevSyncFails() { + const asyncCalls = []; + const writer = { + write() {}, + writevSync() { return false; }, + async writev(chunks) { asyncCalls.push(chunks); }, + writeSync() { return true; }, + endSync() { return 0; }, + }; + async function* source() { + yield [new Uint8Array([1]), new Uint8Array([2])]; + } + await pipeTo(source(), writer); + assert.strictEqual(asyncCalls.length, 1); + assert.strictEqual(asyncCalls[0].length, 2); +} + +// writeSync fails mid-batch — triggers writeBatchAsyncFallback +async function testWriteSyncFailsMidBatch() { + const asyncWrites = []; + const writer = { + writeSync(chunk) { + // Fail for chunk value 2 — always, including retries + if (chunk[0] === 2) return false; + return true; + }, + async write(chunk) { asyncWrites.push(chunk); }, + async end() {}, + }; + // Single batch with 3 chunks + async function* source() { + yield [new Uint8Array([1]), new Uint8Array([2]), new Uint8Array([3])]; + } + const total = await pipeTo(source(), writer); + // Chunk 1: writeSync succeeds + // Chunk 2: writeSync fails → writeBatchAsyncFallback → write() called + // Chunk 3: writeBatchAsyncFallback retries writeSync → succeeds + assert.ok(asyncWrites.length >= 1); + assert.deepStrictEqual(asyncWrites[0], new Uint8Array([2])); + assert.strictEqual(total, 3); +} + +// writeSync always fails — all chunks go through async +async function testWriteSyncAlwaysFails() { + const asyncWrites = []; + const writer = { + writeSync() { return false; }, + async write(chunk) { asyncWrites.push(chunk); }, + async end() {}, + }; + async function* source() { + yield [new Uint8Array([10]), new Uint8Array([20])]; + } + const total = await pipeTo(source(), writer); + assert.strictEqual(asyncWrites.length, 2); + assert.strictEqual(total, 2); +} + +// PushWriter block mode accepts sync writes even when returning false for +// backpressure. pipeTo must wait for drain, not retry the same write. +async function assertPushWriterBlockPipeTo(source, expected, expectedTotal) { + const { writer, readable } = push({ + highWaterMark: 1, + backpressure: 'block', + }); + + const pipe = pipeTo(source, writer); + await setImmediatePromise(); + const data = await text(readable); + const total = await pipe; + + assert.strictEqual(data, expected); + assert.strictEqual(total, expectedTotal); +} + +async function testPushWriterBlockSyncFalseAccepted() { + await assertPushWriterBlockPipeTo((async function*() { + yield [new Uint8Array([97])]; + yield [new Uint8Array([98])]; + })(), 'ab', 2); + + await assertPushWriterBlockPipeTo((async function*() { + yield [new Uint8Array([97, 98])]; + yield [new Uint8Array([99]), new Uint8Array([100])]; + })(), 'abcd', 4); +} + +async function testPipeToSyncPushWriterStrictFalseRejected() { + const decoder = new TextDecoder(); + const { writer, readable } = push({ highWaterMark: 1 }); + + const total = pipeToSync(['a', 'b'], writer, { preventClose: true }); + assert.strictEqual(total, 1); + + const iter = readable[Symbol.asyncIterator](); + const first = await iter.next(); + assert.strictEqual(first.done, false); + assert.strictEqual(decoder.decode(first.value[0]), 'a'); + + const second = await Promise.race([ + iter.next().then((result) => { + return result.done ? '' : decoder.decode(result.value[0]); + }), + setImmediatePromise().then(() => ''), + ]); + assert.strictEqual(second, ''); + + await iter.return?.(); +} + +async function testPipeToSyncWritevFalseNotCounted() { + const writer = { + writevSync() { return false; }, + writeSync: common.mustNotCall(), + endSync() { return 0; }, + }; + function* source() { + yield [new Uint8Array([1]), new Uint8Array([2])]; + } + + const total = pipeToSync(source(), writer); + assert.strictEqual(total, 0); +} + +// pipeToSync with writevSync +async function testPipeToSyncWritev() { + const batches = []; + const writer = { + writevSync(chunks) { batches.push(chunks); }, + writeSync(chunk) { return true; }, + endSync() { return 0; }, + }; + function* source() { + yield [new Uint8Array([1]), new Uint8Array([2]), new Uint8Array([3])]; + yield [new Uint8Array([4])]; + } + pipeToSync(source(), writer); + // Multi-chunk batch should have used writevSync + assert.ok(batches.some((b) => b.length > 1)); +} + +// pipeToSync batches plain Uint8Array chunks for writevSync +async function testPipeToSyncPlainChunksWritev() { + const batches = []; + const writes = []; + const writer = { + writevSync(chunks) { batches.push(chunks); }, + writeSync(chunk) { writes.push(chunk); return true; }, + endSync() { return 0; }, + }; + function* source() { + yield new Uint8Array([1]); + yield new Uint8Array([2]); + yield new Uint8Array([3]); + } + const total = pipeToSync(source(), writer); + assert.strictEqual(total, 3); + assert.strictEqual(batches.length, 1); + assert.strictEqual(batches[0].length, 3); + assert.strictEqual(writes.length, 0); +} + +// pipeToSync with writer that has write() and writeSync() — writeSync preferred +async function testPipeToSyncWriteFallback() { + const syncWrites = []; + const writer = { + writeSync(chunk) { syncWrites.push(chunk); return true; }, + write(chunk) { /* should not be called */ }, + endSync() { return 0; }, + }; + function* source() { + yield [new Uint8Array([1]), new Uint8Array([2])]; + } + pipeToSync(source(), writer); + assert.strictEqual(syncWrites.length, 2); +} + +Promise.all([ + testWritevSyncSuccess(), + testWritevAsyncFallback(), + testWritevSyncUndefinedSuccess(), + testWritevSyncFails(), + testWriteSyncFailsMidBatch(), + testWriteSyncAlwaysFails(), + testPushWriterBlockSyncFalseAccepted(), + testPipeToSyncPushWriterStrictFalseRejected(), + testPipeToSyncWritevFalseNotCounted(), + testPipeToSyncWritev(), + testPipeToSyncPlainChunksWritev(), + testPipeToSyncWriteFallback(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-pipeto.js b/test/parallel/test-stream-iter-pipeto.js new file mode 100644 index 00000000000000..5d8b5088f54089 --- /dev/null +++ b/test/parallel/test-stream-iter-pipeto.js @@ -0,0 +1,340 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { pipeTo, pipeToSync, from, fromSync } = require('stream/iter'); + +async function testPipeToSync() { + const written = []; + const writer = { + writeSync(chunk) { written.push(chunk); return true; }, + endSync() { return written.length; }, + fail() {}, + }; + + const totalBytes = pipeToSync(fromSync('pipe-data'), writer); + assert.strictEqual(totalBytes, 9); // 'pipe-data' = 9 UTF-8 bytes + assert.ok(written.length > 0); + const result = new TextDecoder().decode( + new Uint8Array(written.reduce((acc, c) => [...acc, ...c], []))); + assert.strictEqual(result, 'pipe-data'); +} + +async function testPipeTo() { + const written = []; + const writer = { + async write(chunk) { written.push(chunk); }, + async end() { return written.length; }, + async fail() {}, + }; + + const totalBytes = await pipeTo(from('async-pipe-data'), writer); + assert.strictEqual(totalBytes, 15); // 'async-pipe-data' = 15 UTF-8 bytes + assert.ok(written.length > 0); +} + +async function testPipeToPreventClose() { + let endCalled = false; + const writer = { + async write() {}, + async end() { endCalled = true; }, + async fail() {}, + }; + + await pipeTo(from('data'), writer, { preventClose: true }); + assert.strictEqual(endCalled, false); +} + +// PipeTo source error calls writer.fail() +async function testPipeToSourceError() { + let failCalled = false; + let failReason; + const writer = { + write() {}, + fail(reason) { failCalled = true; failReason = reason; }, + }; + async function* failingSource() { + yield [new TextEncoder().encode('a')]; + throw new Error('pipe source boom'); + } + await assert.rejects( + () => pipeTo(failingSource(), writer), + { message: 'pipe source boom' }, + ); + assert.strictEqual(failCalled, true); + assert.strictEqual(failReason.message, 'pipe source boom'); +} + +// PipeToSync source error calls writer.fail() +async function testPipeToSyncSourceError() { + let failCalled = false; + const writer = { + writeSync() { return true; }, + fail(reason) { failCalled = true; }, + }; + function* failingSource() { + yield [new TextEncoder().encode('a')]; + throw new Error('sync pipe boom'); + } + assert.throws( + () => pipeToSync(failingSource(), writer), + { message: 'sync pipe boom' }, + ); + assert.strictEqual(failCalled, true); +} + +// PipeTo with AbortSignal +async function testPipeToWithSignal() { + const ac = new AbortController(); + const chunks = []; + const writer = { + write(chunk) { chunks.push(chunk); }, + }; + async function* slowSource() { + yield [new TextEncoder().encode('a')]; + await new Promise((r) => setTimeout(r, 50)); + yield [new TextEncoder().encode('b')]; + } + ac.abort(); + await assert.rejects( + () => pipeTo(slowSource(), writer, { signal: ac.signal }), + { name: 'AbortError' }, + ); +} + +// PipeTo with transforms +async function testPipeToWithTransforms() { + const chunks = []; + const writer = { + write(chunk) { chunks.push(new TextDecoder().decode(chunk)); }, + }; + const upper = (batch) => { + if (batch === null) return null; + return batch.map((c) => { + const out = new Uint8Array(c); + for (let i = 0; i < out.length; i++) + out[i] -= (out[i] >= 97 && out[i] <= 122) * 32; + return out; + }); + }; + await pipeTo(from('hello'), upper, writer); + assert.strictEqual(chunks.join(''), 'HELLO'); +} + +// PipeToSync with transforms +async function testPipeToSyncWithTransforms() { + const chunks = []; + const writer = { + writeSync(chunk) { chunks.push(new TextDecoder().decode(chunk)); return true; }, + }; + const upper = (batch) => { + if (batch === null) return null; + return batch.map((c) => { + const out = new Uint8Array(c); + for (let i = 0; i < out.length; i++) + out[i] -= (out[i] >= 97 && out[i] <= 122) * 32; + return out; + }); + }; + pipeToSync(fromSync('hello'), upper, writer); + assert.strictEqual(chunks.join(''), 'HELLO'); +} + +async function testPipeToWriterTransformMethodIgnored() { + const chunks = []; + const writer = { + transform: common.mustNotCall(), + write(chunk) { chunks.push(new TextDecoder().decode(chunk)); }, + }; + + await pipeTo(from('hello'), writer); + assert.strictEqual(chunks.join(''), 'hello'); +} + +async function testPipeToSyncWriterTransformMethodIgnored() { + const chunks = []; + const writer = { + transform: common.mustNotCall(), + writeSync(chunk) { + chunks.push(new TextDecoder().decode(chunk)); + return true; + }, + }; + + pipeToSync(fromSync('hello'), writer); + assert.strictEqual(chunks.join(''), 'hello'); +} + +// PipeTo with writev writer +async function testPipeToWithWritevWriter() { + const allChunks = []; + const writer = { + write(chunk) { allChunks.push(chunk); }, + writev(chunks) { allChunks.push(...chunks); }, + }; + await pipeTo(from('hello world'), writer); + assert.strictEqual(allChunks.length > 0, true); +} + +// PipeTo with writeSync/writevSync fallback +async function testPipeToSyncFallback() { + const chunks = []; + const writer = { + writeSync(chunk) { chunks.push(chunk); return true; }, + write(chunk) { chunks.push(chunk); }, + }; + await pipeTo(from('hello'), writer); + assert.strictEqual(chunks.length > 0, true); +} + +// PipeTo preventFail option +async function testPipeToPreventFail() { + let failCalled = false; + const writer = { + write() {}, + fail() { failCalled = true; }, + }; + // eslint-disable-next-line require-yield + async function* failingSource() { + throw new Error('boom'); + } + await assert.rejects( + () => pipeTo(failingSource(), writer, { preventFail: true }), + { message: 'boom' }, + ); + assert.strictEqual(failCalled, false); +} + +// PipeToSync preventClose option +async function testPipeToSyncPreventClose() { + let endCalled = false; + const writer = { + writeSync() { return true; }, + endSync() { endCalled = true; return 0; }, + }; + pipeToSync(fromSync('hello'), writer, { preventClose: true }); + assert.strictEqual(endCalled, false); +} + +// Regression test: pipeTo should work with a minimal writer that only +// implements write(). end(), fail(), and all *Sync methods are optional. +async function testPipeToMinimalWriter() { + const chunks = []; + const minimalWriter = { + write(chunk) { + chunks.push(chunk); + }, + }; + + await pipeTo(from('minimal'), minimalWriter); + assert.strictEqual(chunks.length > 0, true); +} + +async function testPipeToSyncMinimalWriter() { + const chunks = []; + const minimalWriter = { + writeSync(chunk) { + chunks.push(chunk); + return true; + }, + }; + + pipeToSync(fromSync('minimal-sync'), minimalWriter); + assert.strictEqual(chunks.length > 0, true); +} + +async function testPipeToSyncIterableFastPathWritesIncrementally() { + let pulled = 0; + let firstWritePulled = 0; + const chunks = []; + function* source() { + for (let i = 0; i < 3; i++) { + pulled++; + yield new Uint8Array([0x61 + i]); + } + } + const writer = { + write: common.mustNotCall(), + writeSync(chunk) { + if (firstWritePulled === 0) { + firstWritePulled = pulled; + } + chunks.push(chunk); + return true; + }, + }; + + const totalBytes = await pipeTo(source(), writer); + assert.strictEqual(totalBytes, 3); + assert.strictEqual(firstWritePulled, 1); + assert.deepStrictEqual(chunks, [ + new Uint8Array([0x61]), + new Uint8Array([0x62]), + new Uint8Array([0x63]), + ]); +} + +async function testPipeToSyncIterableFastPathWriteFallback() { + const asyncWrites = []; + const writer = { + writeSync(chunk) { + return chunk[0] !== 0x62; + }, + async write(chunk) { + asyncWrites.push(chunk); + }, + }; + function* source() { + yield new Uint8Array([0x61]); + yield new Uint8Array([0x62]); + yield new Uint8Array([0x63]); + } + + const totalBytes = await pipeTo(source(), writer); + assert.strictEqual(totalBytes, 3); + assert.deepStrictEqual(asyncWrites, [new Uint8Array([0x62])]); +} + +async function testPipeToSyncIterableFastPathAsyncValue() { + const chunks = []; + const writer = { + write: common.mustNotCall(), + writeSync(chunk) { + chunks.push(chunk); + return true; + }, + }; + function* source() { + yield Promise.resolve('a'); + yield new Uint8Array([0x62]); + } + + const totalBytes = await pipeTo(source(), writer); + assert.strictEqual(totalBytes, 2); + const result = new TextDecoder().decode( + new Uint8Array(chunks.reduce((acc, c) => [...acc, ...c], []))); + assert.strictEqual(result, 'ab'); +} + +Promise.all([ + testPipeToSync(), + testPipeTo(), + testPipeToPreventClose(), + testPipeToSourceError(), + testPipeToSyncSourceError(), + testPipeToWithSignal(), + testPipeToWithTransforms(), + testPipeToSyncWithTransforms(), + testPipeToWriterTransformMethodIgnored(), + testPipeToSyncWriterTransformMethodIgnored(), + testPipeToWithWritevWriter(), + testPipeToSyncFallback(), + testPipeToPreventFail(), + testPipeToSyncPreventClose(), + testPipeToMinimalWriter(), + testPipeToSyncMinimalWriter(), + testPipeToSyncIterableFastPathWritesIncrementally(), + testPipeToSyncIterableFastPathWriteFallback(), + testPipeToSyncIterableFastPathAsyncValue(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-pull-async.js b/test/parallel/test-stream-iter-pull-async.js new file mode 100644 index 00000000000000..2d4a287e336216 --- /dev/null +++ b/test/parallel/test-stream-iter-pull-async.js @@ -0,0 +1,425 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { pull, from, text, tap } = require('stream/iter'); + +async function testPullIdentity() { + const data = await text(pull(from('hello-async'))); + assert.strictEqual(data, 'hello-async'); +} + +async function testPullStatelessTransform() { + const upper = (chunks) => { + if (chunks === null) return null; + return chunks.map((c) => { + const str = new TextDecoder().decode(c); + return new TextEncoder().encode(str.toUpperCase()); + }); + }; + const data = await text(pull(from('abc'), upper)); + assert.strictEqual(data, 'ABC'); +} + +async function testPullStatefulTransform() { + const stateful = { + transform: async function*(source) { + for await (const chunks of source) { + if (chunks === null) { + yield new TextEncoder().encode('-ASYNC-END'); + continue; + } + for (const chunk of chunks) { + yield chunk; + } + } + }, + }; + const data = await text(pull(from('data'), stateful)); + assert.strictEqual(data, 'data-ASYNC-END'); +} + +async function testPullWithAbortSignal() { + const ac = new AbortController(); + ac.abort(); + + async function* gen() { + yield [new Uint8Array([1])]; + } + + const result = pull(gen(), { signal: ac.signal }); + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of result) { + assert.fail('Should not reach here'); + } + }, + { name: 'AbortError' }, + ); +} + +async function testPullChainedTransforms() { + const enc = new TextEncoder(); + const transforms = [ + (chunks) => { + if (chunks === null) return null; + return [...chunks, enc.encode('!')]; + }, + (chunks) => { + if (chunks === null) return null; + return [...chunks, enc.encode('?')]; + }, + ]; + const data = await text(pull(from('hello'), ...transforms)); + assert.strictEqual(data, 'hello!?'); +} + +// Source error → controller.abort() → transform listener throws → +// source error propagates to consumer; listener error becomes uncaught +// exception (per EventTarget spec behavior). +async function testTransformSignalListenerErrorOnSourceError() { + // Listener errors from dispatchEvent are rethrown via process.nextTick, + // so we must catch them as uncaught exceptions. + const uncaughtErrors = []; + const handler = (err) => uncaughtErrors.push(err); + process.on('uncaughtException', handler); + + const throwingTransform = { + transform(source, options) { + options.signal.addEventListener('abort', () => { + throw new Error('listener boom'); + }); + return source; + }, + }; + + async function* failingSource() { + yield [new TextEncoder().encode('a')]; + throw new Error('source error'); + } + + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of pull(failingSource(), throwingTransform)) { + // Consume + } + }, + { message: 'source error' }, + ); + + // Give the nextTick rethrow a chance to fire + await new Promise(setImmediate); + process.removeListener('uncaughtException', handler); + + assert.strictEqual(uncaughtErrors.length, 1); + assert.strictEqual(uncaughtErrors[0].message, 'listener boom'); +} + +// Pull source error propagates to consumer +async function testPullSourceError() { + async function* failingSource() { + yield [new TextEncoder().encode('a')]; + throw new Error('source boom'); + } + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of pull(failingSource())) { /* consume */ } + }, { message: 'source boom' }); +} + +// Tap callback error propagates through pipeline +async function testTapCallbackError() { + const badTap = tap(() => { throw new Error('tap boom'); }); + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of pull(from('hello'), badTap)) { /* consume */ } + }, { message: 'tap boom' }); +} + +// Pull signal aborted mid-iteration (not pre-aborted) +async function testPullSignalAbortMidIteration() { + const ac = new AbortController(); + const enc = new TextEncoder(); + async function* slowSource() { + yield [enc.encode('a')]; + yield [enc.encode('b')]; + yield [enc.encode('c')]; + } + const result = pull(slowSource(), { signal: ac.signal }); + const iter = result[Symbol.asyncIterator](); + const first = await iter.next(); // Read first batch + assert.strictEqual(first.done, false); + ac.abort(); + await assert.rejects(() => iter.next(), { name: 'AbortError' }); +} + +async function testPullSignalAbortWhileSourceNextPending() { + const source = { + [Symbol.asyncIterator]() { + return { + async next() { + await new Promise(() => {}); + }, + }; + }, + }; + const ac = new AbortController(); + const iter = pull(source, { signal: ac.signal })[Symbol.asyncIterator](); + const next = iter.next(); + ac.abort(); + await assert.rejects(next, { name: 'AbortError' }); +} + +async function testPullSignalAbortWithTransformWhileSourceNextPending() { + const source = { + [Symbol.asyncIterator]() { + return { + async next() { + await new Promise(() => {}); + }, + }; + }, + }; + const ac = new AbortController(); + const iter = pull( + source, + (chunks) => chunks, + { signal: ac.signal }, + )[Symbol.asyncIterator](); + const next = iter.next(); + ac.abort(); + await assert.rejects(next, { name: 'AbortError' }); +} + +// Pull consumer break (return()) cleans up transform signal +async function testPullConsumerBreakCleanup() { + let signalAborted = false; + const trackingTransform = { + transform(source, options) { + options.signal.addEventListener('abort', () => { + signalAborted = true; + }); + return source; + }, + }; + async function* infiniteSource() { + let i = 0; + while (true) { + yield [new TextEncoder().encode(`chunk${i++}`)]; + } + } + // Consumer breaks after first chunk + // eslint-disable-next-line no-unused-vars + for await (const _ of pull(infiniteSource(), trackingTransform)) { + break; + } + // Give the abort handler a tick to fire + await new Promise(setImmediate); + assert.strictEqual(signalAborted, true); +} + +// Pull transform returning a Promise +async function testPullTransformReturnsPromise() { + const asyncTransform = async (chunks) => { + if (chunks === null) return null; + return chunks; + }; + const result = await text(pull(from('hello'), asyncTransform)); + assert.strictEqual(result, 'hello'); +} + +// Stateless transform error propagates +async function testPullStatelessTransformError() { + const badTransform = (chunks) => { + if (chunks === null) return null; + throw new Error('async stateless boom'); + }; + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of pull(from('hello'), badTransform)) { /* consume */ } + }, { message: 'async stateless boom' }); +} + +// Stateful transform error propagates +async function testPullStatefulTransformError() { + const badStateful = { + transform: async function*(source) { // eslint-disable-line require-yield + for await (const chunks of source) { + if (chunks === null) continue; + throw new Error('async stateful boom'); + } + }, + }; + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of pull(from('hello'), badStateful)) { /* consume */ } + }, { message: 'async stateful boom' }); +} + +// Stateless transform flush emitting data +async function testPullStatelessTransformFlush() { + const withTrailer = (chunks) => { + if (chunks === null) { + return [new TextEncoder().encode('-TRAILER')]; + } + return chunks; + }; + const data = await text(pull(from('data'), withTrailer)); + assert.strictEqual(data, 'data-TRAILER'); +} + +// Consecutive stateless transforms each receive a final flush signal after +// upstream flush output has been processed. +async function testPullConsecutiveStatelessTransformFlush() { + const enc = new TextEncoder(); + const addAOnFlush = (chunks) => (chunks === null ? + [enc.encode('-A')] : chunks); + const addBOnFlush = (chunks) => (chunks === null ? + [enc.encode('-B')] : chunks); + + const data = await text(pull(from('x'), addAOnFlush, addBOnFlush)); + assert.strictEqual(data, 'x-A-B'); +} + +// Stateless transform flush error propagates +async function testPullStatelessTransformFlushError() { + const badFlush = (chunks) => { + if (chunks === null) { + throw new Error('async flush boom'); + } + return chunks; + }; + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of pull(from('hello'), badFlush)) { /* consume */ } + }, { message: 'async flush boom' }); +} + +// Pull with a sync iterable source (not async) +async function testPullWithSyncSource() { + function* gen() { + yield new TextEncoder().encode('sync-source'); + } + const data = await text(pull(gen())); + assert.strictEqual(data, 'sync-source'); +} + +// Pull transform yielding strings +async function testPullTransformYieldsStrings() { + const stringTransform = (chunks) => { + if (chunks === null) return null; + return chunks.map((c) => new TextDecoder().decode(c)); + }; + const result = await text(pull(from('hello'), stringTransform)); + assert.strictEqual(result, 'hello'); +} + +// pull() accepts a string source directly (normalized via from()) +async function testPullStringSource() { + const data = await text(pull('hello-direct')); + assert.strictEqual(data, 'hello-direct'); +} + +// Transform returning a single Uint8Array should be wrapped as a batch, +// not iterated byte-by-byte +async function testTransformReturnsSingleUint8Array() { + const transform = (chunks) => { + if (chunks === null) return null; + // Return a single Uint8Array, not an array + const enc = new TextEncoder(); + return enc.encode('transformed'); + }; + const data = await text(pull(from('input'), transform)); + assert.strictEqual(data, 'transformed'); +} + +// Transform returning a single string should be UTF-8 encoded, +// not iterated character-by-character +async function testTransformReturnsSingleString() { + const transform = (chunks) => { + if (chunks === null) return null; + return 'hello-string'; + }; + const data = await text(pull(from('input'), transform)); + assert.strictEqual(data, 'hello-string'); +} + +// Transform returning an ArrayBuffer should be converted to Uint8Array +async function testTransformReturnsArrayBuffer() { + const transform = (chunks) => { + if (chunks === null) return null; + const enc = new TextEncoder(); + return enc.encode('arraybuf').buffer; + }; + const data = await text(pull(from('input'), transform)); + assert.strictEqual(data, 'arraybuf'); +} + +// pipeTo() accepts a string source directly (normalized via from()) +async function testPipeToStringSource() { + const { pipeTo, push: pushFn, text: textFn } = require('stream/iter'); + const { writer, readable } = pushFn({ highWaterMark: 10 }); + const consume = (async () => textFn(readable))(); + await pipeTo('hello-pipe', writer); + const data = await consume; + assert.strictEqual(data, 'hello-pipe'); +} + +// INVARIANT: Each transform invocation receives its own options object. +// A transform that mutates options must not affect subsequent transforms. +async function testTransformOptionsNotShared() { + const seen = []; + const transform1 = (chunks, options) => { + // Mutate the options object + options.mutated = true; + seen.push({ id: 1, mutated: options.mutated }); + return chunks; + }; + const transform2 = (chunks, options) => { + // Should NOT see mutation from transform1 + seen.push({ id: 2, mutated: options.mutated }); + return chunks; + }; + await text(pull(from('test'), transform1, transform2)); + // transform1 sees its own mutation + assert.strictEqual(seen[0].mutated, true); + // transform2 gets a fresh options object - no mutation visible + assert.strictEqual(seen[1].mutated, undefined); +} + +// Run the uncaughtException test sequentially (it installs a global handler +// that would interfere with concurrent tests). +(async () => { + await Promise.all([ + testPullIdentity(), + testPullStatelessTransform(), + testPullStatefulTransform(), + testPullWithAbortSignal(), + testPullChainedTransforms(), + testPullSourceError(), + testTapCallbackError(), + testPullSignalAbortMidIteration(), + testPullSignalAbortWhileSourceNextPending(), + testPullSignalAbortWithTransformWhileSourceNextPending(), + testPullConsumerBreakCleanup(), + testPullTransformReturnsPromise(), + testPullTransformYieldsStrings(), + testPullStatelessTransformError(), + testPullStatefulTransformError(), + testPullStatelessTransformFlush(), + testPullConsecutiveStatelessTransformFlush(), + testPullStatelessTransformFlushError(), + testPullWithSyncSource(), + testPullStringSource(), + testTransformReturnsSingleUint8Array(), + testTransformReturnsSingleString(), + testTransformReturnsArrayBuffer(), + testPipeToStringSource(), + testTransformOptionsNotShared(), + ]); + // Run after all concurrent tests complete to avoid global handler races + await testTransformSignalListenerErrorOnSourceError(); +})().then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-pull-sync.js b/test/parallel/test-stream-iter-pull-sync.js new file mode 100644 index 00000000000000..c47a6b3f92330d --- /dev/null +++ b/test/parallel/test-stream-iter-pull-sync.js @@ -0,0 +1,193 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { pullSync, fromSync, bytesSync, tapSync } = require('stream/iter'); + +function testPullSyncIdentity() { + // No transforms - just pass through + const data = bytesSync(pullSync(fromSync('hello'))); + assert.deepStrictEqual(data, new TextEncoder().encode('hello')); +} + +function testPullSyncStatelessTransform() { + const upper = (chunks) => { + if (chunks === null) return null; + return chunks.map((c) => { + const str = new TextDecoder().decode(c); + return new TextEncoder().encode(str.toUpperCase()); + }); + }; + const data = bytesSync(pullSync(fromSync('abc'), upper)); + assert.deepStrictEqual(data, new TextEncoder().encode('ABC')); +} + +function testPullSyncStatefulTransform() { + const source = fromSync('data'); + const stateful = { + transform: function*(source) { + for (const chunks of source) { + if (chunks === null) { + // Flush: emit trailer + yield new TextEncoder().encode('-END'); + continue; + } + for (const chunk of chunks) { + yield chunk; + } + } + }, + }; + const result = pullSync(source, stateful); + const data = new TextDecoder().decode(bytesSync(result)); + assert.strictEqual(data, 'data-END'); +} + +function testPullSyncChainedTransforms() { + const addExcl = (chunks) => { + if (chunks === null) return null; + return [...chunks, new TextEncoder().encode('!')]; + }; + const addQ = (chunks) => { + if (chunks === null) return null; + return [...chunks, new TextEncoder().encode('?')]; + }; + const result = pullSync(fromSync('hello'), addExcl, addQ); + const data = new TextDecoder().decode(bytesSync(result)); + assert.strictEqual(data, 'hello!?'); +} + +// PullSync source error propagates +function testPullSyncSourceError() { + function* failingSource() { + yield [new TextEncoder().encode('a')]; + throw new Error('sync source boom'); + } + assert.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const _ of pullSync(failingSource())) { /* consume */ } + }, { message: 'sync source boom' }); +} + +// PullSync with empty source +function testPullSyncEmptySource() { + function* empty() {} + const result = bytesSync(pullSync(empty())); + assert.strictEqual(result.length, 0); +} + +// TapSync callback error propagates +function testTapSyncCallbackError() { + const badTap = tapSync(() => { throw new Error('tapSync boom'); }); + assert.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const _ of pullSync(fromSync('hello'), badTap)) { /* consume */ } + }, { message: 'tapSync boom' }); +} + +// Stateless transform error propagates +function testPullSyncStatelessTransformError() { + const badTransform = (chunks) => { + if (chunks === null) return null; + throw new Error('stateless transform boom'); + }; + assert.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const _ of pullSync(fromSync('hello'), badTransform)) { /* consume */ } + }, { message: 'stateless transform boom' }); +} + +// Stateful transform error propagates +function testPullSyncStatefulTransformError() { + const badStateful = { + transform: function*(source) { // eslint-disable-line require-yield + for (const chunks of source) { + if (chunks === null) continue; + throw new Error('stateful transform boom'); + } + }, + }; + assert.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const _ of pullSync(fromSync('hello'), badStateful)) { /* consume */ } + }, { message: 'stateful transform boom' }); +} + +// Stateless transform flush emitting data +function testPullSyncStatelessTransformFlush() { + const withTrailer = (chunks) => { + if (chunks === null) { + // Flush: emit trailing data + return [new TextEncoder().encode('-TRAILER')]; + } + return chunks; + }; + const data = new TextDecoder().decode(bytesSync(pullSync(fromSync('data'), withTrailer))); + assert.strictEqual(data, 'data-TRAILER'); +} + +// Consecutive stateless transforms each receive a final flush signal after +// upstream flush output has been processed. +function testPullSyncConsecutiveStatelessTransformFlush() { + const enc = new TextEncoder(); + const addAOnFlush = (chunks) => (chunks === null ? + [enc.encode('-A')] : chunks); + const addBOnFlush = (chunks) => (chunks === null ? + [enc.encode('-B')] : chunks); + + const data = new TextDecoder().decode(bytesSync( + pullSync(fromSync('x'), addAOnFlush, addBOnFlush))); + assert.strictEqual(data, 'x-A-B'); +} + +// Stateless transform flush error propagates +function testPullSyncStatelessTransformFlushError() { + const badFlush = (chunks) => { + if (chunks === null) { + throw new Error('flush boom'); + } + return chunks; + }; + assert.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const _ of pullSync(fromSync('hello'), badFlush)) { /* consume */ } + }, { message: 'flush boom' }); +} + +// Empty source result is a Uint8Array +function testPullSyncEmptySourceType() { + function* empty() {} + const result = bytesSync(pullSync(empty())); + assert.ok(result instanceof Uint8Array); + assert.strictEqual(result.byteLength, 0); +} + +// Invalid transform argument +function testPullSyncInvalidTransform() { + assert.throws( + () => { for (const _ of pullSync(fromSync('x'), 42)) { /* consume */ } }, // eslint-disable-line no-unused-vars + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + assert.throws( + () => { for (const _ of pullSync(fromSync('x'), null)) { /* consume */ } }, // eslint-disable-line no-unused-vars + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +Promise.all([ + testPullSyncIdentity(), + testPullSyncStatelessTransform(), + testPullSyncStatefulTransform(), + testPullSyncChainedTransforms(), + testPullSyncSourceError(), + testPullSyncEmptySource(), + testPullSyncEmptySourceType(), + testTapSyncCallbackError(), + testPullSyncStatelessTransformError(), + testPullSyncStatefulTransformError(), + testPullSyncStatelessTransformFlush(), + testPullSyncConsecutiveStatelessTransformFlush(), + testPullSyncStatelessTransformFlushError(), + testPullSyncInvalidTransform(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-push-backpressure.js b/test/parallel/test-stream-iter-push-backpressure.js new file mode 100644 index 00000000000000..7196b7cca5da71 --- /dev/null +++ b/test/parallel/test-stream-iter-push-backpressure.js @@ -0,0 +1,160 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { push, text } = require('stream/iter'); + +async function testStrictBackpressure() { + const { writer, readable } = push({ + highWaterMark: 1, + backpressure: 'strict', + }); + + // First write should succeed synchronously + assert.strictEqual(writer.writeSync('a'), true); + // Second write should fail synchronously (buffer full) + assert.strictEqual(writer.writeSync('b'), false); + + // Consume to free space, then end + const resultPromise = text(readable); + writer.end(); + const data = await resultPromise; + assert.strictEqual(data, 'a'); +} + +async function testDropOldest() { + const { writer, readable } = push({ + highWaterMark: 2, + backpressure: 'drop-oldest', + }); + + assert.strictEqual(writer.writeSync('first'), true); + assert.strictEqual(writer.writeSync('second'), true); + // This should drop 'first' — return value is true (write accepted via drop) + assert.strictEqual(writer.writeSync('third'), true); + writer.end(); + + const batches = []; + for await (const batch of readable) { + batches.push(batch); + } + // Should have 'second' and 'third' + const allBytes = []; + for (const batch of batches) { + for (const chunk of batch) { + allBytes.push(...chunk); + } + } + const result = new TextDecoder().decode(new Uint8Array(allBytes)); + assert.strictEqual(result, 'secondthird'); +} + +async function testDropNewest() { + const { writer, readable } = push({ + highWaterMark: 1, + backpressure: 'drop-newest', + }); + + assert.strictEqual(writer.writeSync('kept'), true); + // This is silently dropped — return value is true (accepted but discarded) + assert.strictEqual(writer.writeSync('dropped'), true); + writer.end(); + + const data = await text(readable); + assert.strictEqual(data, 'kept'); +} + +async function testBlockBackpressure() { + const { writer, readable } = push({ highWaterMark: 1, backpressure: 'block' }); + + // Fill the buffer + writer.writeSync('a'); + + // Next write should block (not throw, not drop) + let writeState = 'pending'; + const writePromise = writer.write('b').then(() => { writeState = 'resolved'; }); + + // The write cannot resolve until the buffer is drained, so a microtask + // tick is sufficient to confirm it is still blocked. + await new Promise(setImmediate); + assert.strictEqual(writeState, 'pending'); // Still blocked + + // Read from the consumer to drain + const iter = readable[Symbol.asyncIterator](); + const first = await iter.next(); // Drains 'a' + assert.strictEqual(first.done, false); + + // After draining, the pending write resolves as a microtask + await new Promise(setImmediate); + assert.strictEqual(writeState, 'resolved'); // Now unblocked + + writer.endSync(); + const second = await iter.next(); // Read 'b' + assert.strictEqual(second.done, false); + await writePromise; +} + +async function testBlockWriteSyncDoesNotEnqueue() { + // With block policy, writeSync returns false when the buffer is full. + // The data is NOT accepted — writeSync only operates on the slots buffer. + // The caller should fall back to write() which uses the pending queue. + const { writer, readable } = push({ highWaterMark: 1, backpressure: 'block' }); + + // Fill the buffer + assert.strictEqual(writer.writeSync('a'), true); + + // Buffer full: writeSync returns false, data NOT enqueued + assert.strictEqual(writer.writeSync('b'), false); + + // Use the async write (try-fallback pattern) — this goes to pending queue + const consuming = text(readable); + await writer.write('b'); + await writer.end(); + + // Both chunks should be delivered + const result = await consuming; + assert.strictEqual(result, 'ab'); +} + +async function testStrictPendingQueueOverflow() { + // With highWaterMark: 1 and strict, the pending writes queue is also limited to 1. + // Filling the buffer (1 sync write) + filling the pending queue (1 async write) + // should leave no room. A third write must reject with a RangeError. + const { writer, readable } = push({ + highWaterMark: 1, + backpressure: 'strict', + }); + + // Fill the buffer + assert.strictEqual(writer.writeSync('a'), true); + + // This async write goes into the pending queue (buffer full, queue has room) + const pendingWrite = writer.write('b'); + + // This write should reject: buffer full AND pending queue at capacity + await assert.rejects( + () => writer.write('c'), + { + code: 'ERR_INVALID_STATE', + name: 'RangeError', + }, + ); + + // Clean up: drain the readable + const iter = readable[Symbol.asyncIterator](); + await iter.next(); + await iter.next(); + await pendingWrite; + writer.endSync(); + await iter.return(); +} + +Promise.all([ + testStrictBackpressure(), + testDropOldest(), + testDropNewest(), + testBlockBackpressure(), + testBlockWriteSyncDoesNotEnqueue(), + testStrictPendingQueueOverflow(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-push-basic.js b/test/parallel/test-stream-iter-push-basic.js new file mode 100644 index 00000000000000..22d5b26c830a47 --- /dev/null +++ b/test/parallel/test-stream-iter-push-basic.js @@ -0,0 +1,182 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { push, text } = require('stream/iter'); + +async function testBasicWriteRead() { + const { writer, readable } = push(); + + writer.write('hello'); + writer.end(); + + const data = await text(readable); + assert.strictEqual(data, 'hello'); +} + +async function testMultipleWrites() { + const { writer, readable } = push({ highWaterMark: 10 }); + + writer.write('a'); + writer.write('b'); + writer.write('c'); + writer.end(); + + const data = await text(readable); + assert.strictEqual(data, 'abc'); +} + +async function testDesiredSize() { + const { writer } = push({ highWaterMark: 3 }); + + assert.strictEqual(writer.desiredSize, 3); + writer.writeSync('a'); + assert.strictEqual(writer.desiredSize, 2); + writer.writeSync('b'); + assert.strictEqual(writer.desiredSize, 1); + writer.writeSync('c'); + assert.strictEqual(writer.desiredSize, 0); + + writer.end(); + assert.strictEqual(writer.desiredSize, null); +} + +async function testWriterEnd() { + const { writer, readable } = push(); + + const totalBytes = writer.endSync(); + assert.strictEqual(totalBytes, 0); + + // Calling endSync again returns byte count (idempotent when closed) + assert.strictEqual(writer.endSync(), 0); + + const batches = []; + for await (const batch of readable) { + batches.push(batch); + } + assert.strictEqual(batches.length, 0); +} + +async function testWriterFail() { + const { writer, readable } = push(); + + writer.fail(new Error('test fail')); + + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { + assert.fail('Should not reach here'); + } + }, + { message: 'test fail' }, + ); +} + +async function testConsumerBreak() { + const { writer, readable } = push({ highWaterMark: 10 }); + + writer.writeSync('a'); + writer.writeSync('b'); + writer.writeSync('c'); + + // Break after first batch + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { + break; + } + + // Writer should now see null desiredSize + assert.strictEqual(writer.desiredSize, null); +} + +async function testAbortSignal() { + const ac = new AbortController(); + const { readable } = push({ signal: ac.signal }); + + ac.abort(); + + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { + assert.fail('Should not reach here'); + } + }, + { name: 'AbortError' }, + ); +} + +async function testPreAbortedSignal() { + const ac = new AbortController(); + ac.abort(); + const { readable } = push({ signal: ac.signal }); + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { + assert.fail('Should not reach here'); + } + }, { name: 'AbortError' }); +} + +async function testConsumerBreakWriteSyncReturnsFalse() { + const { writer, readable } = push({ highWaterMark: 10 }); + writer.writeSync('a'); + + // Break after first batch + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { + break; + } + + // After consumer break, writeSync should return false + assert.strictEqual(writer.writeSync('b'), false); + assert.strictEqual(writer.desiredSize, null); +} + +async function testPushWithTransforms() { + const upper = (chunks) => { + if (chunks === null) return null; + return chunks.map((c) => { + const str = new TextDecoder().decode(c); + return new TextEncoder().encode(str.toUpperCase()); + }); + }; + + const { writer, readable } = push(upper); + + writer.write('hello'); + writer.end(); + + const data = await text(readable); + assert.strictEqual(data, 'HELLO'); +} + +async function testInvalidBackpressure() { + assert.throws(() => push({ backpressure: 'banana' }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.throws(() => push({ backpressure: '' }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + + // Valid values should not throw + for (const bp of ['strict', 'block', 'drop-oldest', 'drop-newest']) { + push({ backpressure: bp }); + } +} + +Promise.all([ + testBasicWriteRead(), + testMultipleWrites(), + testDesiredSize(), + testWriterEnd(), + testWriterFail(), + testConsumerBreak(), + testAbortSignal(), + testPreAbortedSignal(), + testConsumerBreakWriteSyncReturnsFalse(), + testPushWithTransforms(), + testInvalidBackpressure(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-push-writer.js b/test/parallel/test-stream-iter-push-writer.js new file mode 100644 index 00000000000000..b1d702b14e52ac --- /dev/null +++ b/test/parallel/test-stream-iter-push-writer.js @@ -0,0 +1,521 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { push, ondrain, text } = require('stream/iter'); + +async function testOndrain() { + const { writer } = push({ highWaterMark: 1 }); + + // With space available, ondrain resolves immediately + const drainResult = ondrain(writer); + assert.ok(drainResult instanceof Promise); + const result = await drainResult; + assert.strictEqual(result, true); + + // After close, ondrain returns null + writer.end(); + assert.strictEqual(ondrain(writer), null); +} + +async function testOndrainNonDrainable() { + // Non-drainable objects return null + assert.strictEqual(ondrain(null), null); + assert.strictEqual(ondrain({}), null); + assert.strictEqual(ondrain('string'), null); +} + +async function testOndrainProtocolErrorPropagates() { + const badDrainable = { + [Symbol.for('Stream.drainableProtocol')]() { + throw new Error('protocol error'); + }, + }; + assert.throws( + () => ondrain(badDrainable), + { message: 'protocol error' }, + ); +} + +async function testWriteWithSignalRejects() { + const { writer, readable } = push({ highWaterMark: 1 }); + + // Fill the buffer so write will block + writer.writeSync('a'); + + const ac = new AbortController(); + const writePromise = writer.write('b', { signal: ac.signal }); + + // Signal fires while write is pending + ac.abort(); + + await assert.rejects(writePromise, { name: 'AbortError' }); + + // Clean up + writer.end(); + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { break; } +} + +async function testWriteWithPreAbortedSignal() { + const { writer, readable } = push({ highWaterMark: 1 }); + + const ac = new AbortController(); + ac.abort(); + + // Pre-aborted signal should reject immediately + await assert.rejects( + writer.write('data', { signal: ac.signal }), + { name: 'AbortError' }, + ); + + // Writer should still be usable for other writes + writer.write('ok'); + writer.end(); + const data = await text(readable); + assert.strictEqual(data, 'ok'); +} + +async function testCancelledWriteRemovedFromQueue() { + const { writer, readable } = push({ highWaterMark: 1 }); + + // Fill the buffer + writer.writeSync('first'); + + const ac = new AbortController(); + // This write should be queued since buffer is full + const cancelledWrite = writer.write('cancelled', { signal: ac.signal }); + + // Cancel it + ac.abort(); + await cancelledWrite.catch(() => {}); + + // Drain 'first' to make room for the replacement write + const iter = readable[Symbol.asyncIterator](); + await iter.next(); + + // The cancelled write should NOT occupy a pending slot. + // A new write should succeed now that the buffer has room. + await writer.write('second'); + writer.end(); + + const result = await iter.next(); + // 'second' should be the next (and only remaining) chunk + const decoder = new TextDecoder(); + let data = ''; + for (const chunk of result.value) { + data += decoder.decode(chunk, { stream: true }); + } + assert.strictEqual(data, 'second'); + await iter.return(); +} + +async function testOndrainResolvesFalseOnConsumerBreak() { + const { writer, readable } = push({ highWaterMark: 1 }); + + // Fill the buffer so desiredSize = 0 + writer.writeSync('a'); + + // Also queue a pending write so that reading one chunk + // doesn't clear backpressure (the pending write refills the slot) + const pendingWrite = writer.write('b'); + + // Start a drain wait - still at capacity + const drainPromise = ondrain(writer); + + // Consumer returns without draining enough to clear backpressure + const iter = readable[Symbol.asyncIterator](); + await iter.return(); + + // Ondrain should resolve false since the consumer terminated + const result = await drainPromise; + assert.strictEqual(result, false); + await pendingWrite.catch(() => {}); // Ignore write rejection +} + +async function testOndrainRejectsOnConsumerThrow() { + const { writer, readable } = push({ highWaterMark: 1 }); + + // Fill the buffer so desiredSize = 0 + writer.writeSync('a'); + + // Also queue a pending write so that reading one chunk + // doesn't clear backpressure (the pending write refills the slot) + const pendingWrite = writer.write('b'); + + // Start a drain wait - still at capacity + const drainPromise = ondrain(writer); + + // Consumer throws via iterator.throw() before draining enough + // to clear backpressure. The drain should reject. + const iter = readable[Symbol.asyncIterator](); + await iter.throw(new Error('consumer error')); + + await assert.rejects(drainPromise, /consumer error/); + await pendingWrite.catch(() => {}); // Ignore write rejection +} + +async function testWritev() { + const { writer, readable } = push({ highWaterMark: 10 }); + const enc = new TextEncoder(); + writer.writev([enc.encode('hel'), enc.encode('lo')]); + writer.endSync(); + const result = await text(readable); + assert.strictEqual(result, 'hello'); +} + +async function testWritevSync() { + const { writer, readable } = push({ highWaterMark: 10 }); + const enc = new TextEncoder(); + assert.strictEqual(writer.writevSync([enc.encode('hel'), enc.encode('lo')]), true); + writer.endSync(); + const result = await text(readable); + assert.strictEqual(result, 'hello'); +} + +async function testWritevMixedTypes() { + const { writer, readable } = push({ highWaterMark: 10 }); + // Mix strings and Uint8Arrays + writer.writev(['hel', new TextEncoder().encode('lo')]); + writer.endSync(); + const result = await text(readable); + assert.strictEqual(result, 'hello'); +} + +async function testWriteAfterEnd() { + const { writer } = push(); + writer.endSync(); + // Sync write after end returns false + assert.strictEqual(writer.writeSync('fail'), false); + // Async write after end rejects + await assert.rejects( + () => writer.write('fail'), + { code: 'ERR_INVALID_STATE' }, + ); +} + +async function testWriteAfterFail() { + const { writer } = push(); + writer.fail(new Error('failed')); + // Sync write after fail returns false + assert.strictEqual(writer.writeSync('fail'), false); + // Async write after fail rejects with the stored error + await assert.rejects( + () => writer.write('fail'), + { message: 'failed' }, + ); +} + +async function testFail() { + const { writer, readable } = push(); + writer.writeSync('hello'); + writer.fail(new Error('boom')); + // Second fail is a no-op (already errored) + writer.fail(new Error('boom2')); + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { /* consume */ } + }, { message: 'boom' }); +} + +async function testEndAsyncReturnValue() { + const { writer, readable } = push(); + writer.writeSync('hello'); + // Start consuming concurrently (end() waits for drain) + const consume = (async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { /* drain */ } + })(); + const total = await writer.end(); + assert.strictEqual(total, 5); + await consume; +} + +async function testEndAfterEndSyncWaitsForDrain() { + const { writer, readable } = push(); + writer.writeSync('hello'); + assert.strictEqual(writer.endSync(), -1); + + let ended = false; + const end = writer.end().then((n) => { + ended = true; + return n; + }); + + await Promise.resolve(); + assert.strictEqual(ended, false); + + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { /* drain */ } + assert.strictEqual(await end, 5); +} + +async function testWriteUint8Array() { + const { writer, readable } = push(); + writer.write(new Uint8Array([72, 73])); // 'HI' + writer.endSync(); + const result = await text(readable); + assert.strictEqual(result, 'HI'); +} + +async function testOndrainWaitsForDrain() { + const { writer, readable } = push({ highWaterMark: 1 }); + writer.writeSync('a'); // Fills buffer + + let drainState = 'pending'; + const drainPromise = ondrain(writer).then((v) => { drainState = v; }); + + await new Promise(setImmediate); + assert.strictEqual(drainState, 'pending'); // Still waiting + + // Read to drain + const iter = readable[Symbol.asyncIterator](); + await iter.next(); + + await drainPromise; + assert.strictEqual(drainState, true); + writer.endSync(); +} + +// Consumer throw causes subsequent writes to reject with consumer's error +async function testConsumerThrowRejectsWrites() { + const { writer, readable } = push({ highWaterMark: 1 }); + writer.writeSync('a'); + + const iter = readable[Symbol.asyncIterator](); + await iter.throw(new Error('consumer boom')); + + // Subsequent async writes should reject with the consumer's error + await assert.rejects( + () => writer.write('x'), + { message: 'consumer boom' }, + ); +} + +// end() resolves a pending read as done:true +async function testEndResolvesPendingRead() { + const { writer, readable } = push(); + + // Consumer starts reading — blocks because buffer is empty + const iter = readable[Symbol.asyncIterator](); + const readPromise = iter.next(); + + // Give the read a tick to enter the pending state + await new Promise(setImmediate); + + // End the writer — should resolve the pending read with done:true + writer.endSync(); + const result = await readPromise; + assert.strictEqual(result.done, true); +} + +// fail() rejects a pending read with the error +async function testFailRejectsPendingRead() { + const { writer, readable } = push(); + + const iter = readable[Symbol.asyncIterator](); + const readPromise = iter.next(); + + await new Promise(setImmediate); + + writer.fail(new Error('fail during read')); + await assert.rejects( + () => readPromise, + { message: 'fail during read' }, + ); +} + +// iterator.return() resolves a pending read with done:true +async function testConsumerReturnResolvesPendingRead() { + const { readable } = push(); + + const iter = readable[Symbol.asyncIterator](); + const readPromise = iter.next(); + + await new Promise(setImmediate); + + const returnResult = await iter.return(); + assert.strictEqual(returnResult.value, undefined); + assert.strictEqual(returnResult.done, true); + + const readResult = await readPromise; + assert.strictEqual(readResult.value, undefined); + assert.strictEqual(readResult.done, true); +} + +// iterator.throw() rejects a pending read with the thrown error +async function testConsumerThrowRejectsPendingRead() { + const { readable } = push(); + + const iter = readable[Symbol.asyncIterator](); + const readPromise = iter.next(); + + await new Promise(setImmediate); + + const err = new Error('consumer read boom'); + const throwResult = await iter.throw(err); + assert.strictEqual(throwResult.value, undefined); + assert.strictEqual(throwResult.done, true); + + await assert.rejects( + () => readPromise, + (e) => e === err, + ); +} + +// end() while writes are pending rejects those writes +async function testEndRejectsPendingWrites() { + const { writer, readable } = push({ highWaterMark: 1, backpressure: 'block' }); + writer.writeSync('a'); // fill buffer + + // This write blocks on backpressure + const writePromise = writer.write('b'); + + await new Promise(setImmediate); + + // Ending should reject the pending write + writer.endSync(); + + await assert.rejects( + () => writePromise, + { code: 'ERR_INVALID_STATE' }, + ); + + // Clean up: drain the readable + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { break; } +} + +async function testEndIdempotentWhenClosed() { + const { writer, readable } = push({ highWaterMark: 10 }); + await writer.write('hello'); + // Start consuming concurrently (end() waits for drain) + const consume = (async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { /* drain */ } + })(); + const first = await writer.end(); + assert.strictEqual(first, 5); + // Second end() should resolve with same byte count (idempotent) + const second = await writer.end(); + assert.strictEqual(second, 5); + await consume; +} + +async function testAsyncDispose() { + const { writer, readable } = push({ highWaterMark: 10 }); + writer.writeSync('hello'); + // Symbol.asyncDispose calls fail() with no argument + await writer[Symbol.asyncDispose](); + // Writer is now errored, writes should fail + assert.strictEqual(writer.writeSync('fail'), false); + // Drain readable + try { + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { /* consume */ } + } catch { + // Expected - reader sees the error + } +} + +async function testSyncDispose() { + const { writer, readable } = push({ highWaterMark: 10 }); + writer.writeSync('hello'); + // Symbol.dispose calls fail() with no argument + writer[Symbol.dispose](); + // Writer is now errored, writes should fail + assert.strictEqual(writer.writeSync('fail'), false); + // Drain readable + try { + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { /* consume */ } + } catch { + // Expected + } +} + +async function testEndRejectsWhenErrored() { + const { writer, readable } = push({ highWaterMark: 10 }); + await writer.write('hello'); + const err = new Error('boom'); + await writer.fail(err); + // end() after fail should reject with the stored error + await assert.rejects( + () => writer.end(), + (e) => e === err, + ); + // Drain readable + try { + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { break; } + } catch { + // Expected - reader may see the error + } +} + +async function testFailRejectsFutureReadWithFalsyReason() { + for (const reason of [0, null]) { + const { writer, readable } = push(); + + writer.fail(reason); + + const iter = readable[Symbol.asyncIterator](); + await iter.next().then( + common.mustNotCall(), + common.mustCall((rejection) => { + assert.strictEqual(rejection, reason); + }), + ); + } +} + +async function testFailRejectsPendingReadWithFalsyReason() { + const { writer, readable } = push(); + + const iter = readable[Symbol.asyncIterator](); + const readPromise = iter.next(); + + await new Promise(setImmediate); + + writer.fail(false); + await readPromise.then( + common.mustNotCall(), + common.mustCall((reason) => { + assert.strictEqual(reason, false); + }), + ); +} + +Promise.all([ + testOndrain(), + testOndrainNonDrainable(), + testWriteWithSignalRejects(), + testWriteWithPreAbortedSignal(), + testCancelledWriteRemovedFromQueue(), + testOndrainResolvesFalseOnConsumerBreak(), + testOndrainRejectsOnConsumerThrow(), + testWritev(), + testWritevSync(), + testWritevMixedTypes(), + testWriteAfterEnd(), + testWriteAfterFail(), + testOndrainProtocolErrorPropagates(), + testFail(), + testEndAsyncReturnValue(), + testEndAfterEndSyncWaitsForDrain(), + testWriteUint8Array(), + testOndrainWaitsForDrain(), + testConsumerThrowRejectsWrites(), + testEndResolvesPendingRead(), + testFailRejectsPendingRead(), + testFailRejectsFutureReadWithFalsyReason(), + testFailRejectsPendingReadWithFalsyReason(), + testConsumerReturnResolvesPendingRead(), + testConsumerThrowRejectsPendingRead(), + testEndRejectsPendingWrites(), + testEndIdempotentWhenClosed(), + testEndRejectsWhenErrored(), + testAsyncDispose(), + testSyncDispose(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-readable-interop-disabled.js b/test/parallel/test-stream-iter-readable-interop-disabled.js new file mode 100644 index 00000000000000..86c8cc141387cd --- /dev/null +++ b/test/parallel/test-stream-iter-readable-interop-disabled.js @@ -0,0 +1,57 @@ +'use strict'; + +// Tests that toAsyncStreamable throws ERR_STREAM_ITER_MISSING_FLAG +// when --experimental-stream-iter is not enabled. + +const common = require('../common'); +const assert = require('assert'); +const { spawnPromisified } = common; + +async function testToAsyncStreamableWithoutFlag() { + const { stderr, code } = await spawnPromisified(process.execPath, [ + '-e', + ` + const { Readable } = require('stream'); + const r = new Readable({ read() {} }); + r[Symbol.for('Stream.toAsyncStreamable')](); + `, + ]); + assert.notStrictEqual(code, 0); + assert.match(stderr, /ERR_STREAM_ITER_MISSING_FLAG/); +} + +async function testToAsyncStreamableWithFlag() { + const { code } = await spawnPromisified(process.execPath, [ + '--experimental-stream-iter', + '-e', + ` + const { Readable } = require('stream'); + const r = new Readable({ + read() { this.push(Buffer.from('ok')); this.push(null); } + }); + const sym = Symbol.for('Stream.toAsyncStreamable'); + const iter = r[sym](); + // Should not throw, and should have stream property + if (!iter.stream) process.exit(1); + `, + ]); + assert.strictEqual(code, 0); +} + +async function testStreamIterModuleWithoutFlag() { + // Requiring 'stream/iter' without the flag should not be possible + // since the module is gated behind --experimental-stream-iter. + const { code } = await spawnPromisified(process.execPath, [ + '-e', + ` + require('stream/iter'); + `, + ]); + assert.notStrictEqual(code, 0); +} + +Promise.all([ + testToAsyncStreamableWithoutFlag(), + testToAsyncStreamableWithFlag(), + testStreamIterModuleWithoutFlag(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-readable-interop.js b/test/parallel/test-stream-iter-readable-interop.js new file mode 100644 index 00000000000000..8100b54168be87 --- /dev/null +++ b/test/parallel/test-stream-iter-readable-interop.js @@ -0,0 +1,640 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Tests for classic Readable stream interop with the stream/iter API +// via the toAsyncStreamable protocol and kValidatedSource optimization. + +const common = require('../common'); +const assert = require('assert'); +const { Readable } = require('stream'); +const { + from, + pull, + bytes, + text, +} = require('stream/iter'); + +const toAsyncStreamable = Symbol.for('Stream.toAsyncStreamable'); + +// ============================================================================= +// toAsyncStreamable protocol is present on Readable.prototype +// ============================================================================= + +function testProtocolExists() { + assert.strictEqual(typeof Readable.prototype[toAsyncStreamable], 'function'); + + const readable = new Readable({ read() {} }); + assert.strictEqual(typeof readable[toAsyncStreamable], 'function'); +} + +// ============================================================================= +// Byte-mode Readable: basic round-trip through from() +// ============================================================================= + +async function testByteModeThroughFrom() { + const readable = new Readable({ + read() { + this.push(Buffer.from('hello')); + this.push(Buffer.from(' world')); + this.push(null); + }, + }); + + const result = await text(from(readable)); + assert.strictEqual(result, 'hello world'); +} + +// ============================================================================= +// Byte-mode Readable: basic round-trip through pull() +// ============================================================================= + +async function testByteModeThroughPull() { + const readable = new Readable({ + read() { + this.push(Buffer.from('pull ')); + this.push(Buffer.from('test')); + this.push(null); + }, + }); + + const result = await text(pull(readable)); + assert.strictEqual(result, 'pull test'); +} + +// ============================================================================= +// Byte-mode Readable: bytes consumer +// ============================================================================= + +async function testByteModeBytes() { + const data = Buffer.from('binary data here'); + const readable = new Readable({ + read() { + this.push(data); + this.push(null); + }, + }); + + const result = await bytes(from(readable)); + assert.deepStrictEqual(result, new Uint8Array(data)); +} + +// ============================================================================= +// Byte-mode Readable: batching - multiple buffered chunks yield as one batch +// ============================================================================= + +async function testBatchingBehavior() { + const readable = new Readable({ + read() { + // Push multiple chunks synchronously so they all buffer + for (let i = 0; i < 10; i++) { + this.push(Buffer.from(`chunk${i}`)); + } + this.push(null); + }, + }); + + const source = from(readable); + const batches = []; + for await (const batch of source) { + batches.push(batch); + } + + // All chunks were buffered synchronously, so they should come out + // as fewer batches than individual chunks (ideally one batch). + assert.ok(batches.length < 10, + `Expected fewer batches than chunks, got ${batches.length}`); + + // Total data should be correct + const allChunks = batches.flat(); + const combined = Buffer.concat(allChunks); + let expected = ''; + for (let i = 0; i < 10; i++) { + expected += `chunk${i}`; + } + assert.strictEqual(combined.toString(), expected); +} + +// ============================================================================= +// Byte-mode Readable: kValidatedSource is set +// ============================================================================= + +function testTrustedSourceByteMode() { + const readable = new Readable({ read() {} }); + const result = readable[toAsyncStreamable](); + // kValidatedSource is a private symbol, but we can verify the result + // is used directly by from() without wrapping by checking it has + // Symbol.asyncIterator + assert.strictEqual(typeof result[Symbol.asyncIterator], 'function'); + assert.strictEqual(result.stream, readable); +} + +// ============================================================================= +// Byte-mode Readable: multi-read with delayed pushes +// ============================================================================= + +async function testDelayedPushes() { + let pushCount = 0; + const readable = new Readable({ + read() { + if (pushCount < 3) { + setTimeout(() => { + this.push(Buffer.from(`delayed${pushCount}`)); + pushCount++; + if (pushCount === 3) { + this.push(null); + } + }, 10); + } + }, + }); + + const result = await text(from(readable)); + assert.strictEqual(result, 'delayed0delayed1delayed2'); +} + +// ============================================================================= +// Byte-mode Readable: empty stream +// ============================================================================= + +async function testEmptyStream() { + const readable = new Readable({ + read() { + this.push(null); + }, + }); + + const result = await text(from(readable)); + assert.strictEqual(result, ''); +} + +// ============================================================================= +// Byte-mode Readable: error propagation +// ============================================================================= + +async function testErrorPropagation() { + const readable = new Readable({ + read() { + process.nextTick(() => this.destroy(new Error('test error'))); + }, + }); + + await assert.rejects( + text(from(readable)), + (err) => err.message === 'test error', + ); +} + +// ============================================================================= +// Byte-mode Readable: with transforms +// ============================================================================= + +async function testWithTransform() { + const readable = new Readable({ + read() { + this.push(Buffer.from('hello')); + this.push(null); + }, + }); + + // Uppercase transform + function uppercase(chunks) { + if (chunks === null) return null; + return chunks.map((c) => { + const buf = Buffer.from(c); + for (let i = 0; i < buf.length; i++) { + if (buf[i] >= 97 && buf[i] <= 122) buf[i] -= 32; + } + return buf; + }); + } + + const result = await text(pull(readable, uppercase)); + assert.strictEqual(result, 'HELLO'); +} + +// ============================================================================= +// Object-mode Readable: strings are normalized to Uint8Array +// ============================================================================= + +async function testObjectModeStrings() { + const readable = new Readable({ + objectMode: true, + read() { + this.push('hello'); + this.push(' object'); + this.push(' mode'); + this.push(null); + }, + }); + + const result = await text(from(readable)); + assert.strictEqual(result, 'hello object mode'); +} + +// ============================================================================= +// Object-mode Readable: Uint8Array chunks pass through +// ============================================================================= + +async function testObjectModeUint8Array() { + const readable = new Readable({ + objectMode: true, + read() { + this.push(new Uint8Array([72, 73])); // "HI" + this.push(null); + }, + }); + + const result = await text(from(readable)); + assert.strictEqual(result, 'HI'); +} + +// ============================================================================= +// Object-mode Readable: mixed types (strings + Uint8Array) +// ============================================================================= + +async function testObjectModeMixed() { + const readable = new Readable({ + objectMode: true, + read() { + this.push('hello'); + this.push(Buffer.from(' ')); + this.push('world'); + this.push(null); + }, + }); + + const result = await text(from(readable)); + assert.strictEqual(result, 'hello world'); +} + +// ============================================================================= +// Object-mode Readable: toStreamable protocol objects +// ============================================================================= + +async function testObjectModeToStreamable() { + const toStreamableSym = Symbol.for('Stream.toStreamable'); + const readable = new Readable({ + objectMode: true, + read() { + this.push({ + [toStreamableSym]() { + return 'from-protocol'; + }, + }); + this.push(null); + }, + }); + + const result = await text(from(readable)); + assert.strictEqual(result, 'from-protocol'); +} + +// ============================================================================= +// Object-mode Readable: kValidatedSource is set +// ============================================================================= + +function testTrustedSourceObjectMode() { + const readable = new Readable({ objectMode: true, read() {} }); + const result = readable[toAsyncStreamable](); + assert.strictEqual(typeof result[Symbol.asyncIterator], 'function'); + assert.strictEqual(result.stream, readable); +} + +// ============================================================================= +// Encoded Readable: strings are re-encoded to Uint8Array +// ============================================================================= + +async function testEncodedReadable() { + const readable = new Readable({ + encoding: 'utf8', + read() { + this.push(Buffer.from('encoded')); + this.push(null); + }, + }); + + const result = await text(from(readable)); + assert.strictEqual(result, 'encoded'); +} + +// ============================================================================= +// Readable.from() source: verify interop with Readable.from() +// ============================================================================= + +async function testReadableFrom() { + const readable = Readable.from(['chunk1', 'chunk2', 'chunk3']); + + const result = await text(from(readable)); + assert.strictEqual(result, 'chunk1chunk2chunk3'); +} + +// ============================================================================= +// Byte-mode Readable: large data +// ============================================================================= + +async function testLargeData() { + const totalSize = 1024 * 1024; // 1 MB + const chunkSize = 16384; + let pushed = 0; + + const readable = new Readable({ + read() { + if (pushed < totalSize) { + const size = Math.min(chunkSize, totalSize - pushed); + const buf = Buffer.alloc(size, 0x41); // Fill with 'A' + this.push(buf); + pushed += size; + } else { + this.push(null); + } + }, + }); + + const result = await bytes(from(readable)); + assert.strictEqual(result.length, totalSize); + assert.strictEqual(result[0], 0x41); + assert.strictEqual(result[totalSize - 1], 0x41); +} + +// ============================================================================= +// Byte-mode Readable: consumer return (early termination) +// ============================================================================= + +async function testEarlyTermination() { + let pushCount = 0; + const readable = new Readable({ + read() { + this.push(Buffer.from(`chunk${pushCount++}`)); + // Never pushes null - infinite stream + }, + }); + + // Take only the first batch + const source = from(readable); + const batches = []; + for await (const batch of source) { + batches.push(batch); + break; // Stop after first batch + } + + assert.ok(batches.length >= 1); + // Stream should be destroyed after consumer return + // Give it a tick to clean up + await new Promise((resolve) => setTimeout(resolve, 50)); + assert.ok(readable.destroyed); +} + +// ============================================================================= +// Byte-mode Readable: pull() with compression transform +// ============================================================================= + +async function testWithCompression() { + const { + compressGzip, + decompressGzip, + } = require('zlib/iter'); + + const readable = new Readable({ + read() { + this.push(Buffer.from('compress me via classic Readable')); + this.push(null); + }, + }); + + const compressed = pull(readable, compressGzip()); + const result = await text(pull(compressed, decompressGzip())); + assert.strictEqual(result, 'compress me via classic Readable'); +} + +// ============================================================================= +// Object-mode Readable: error propagation +// ============================================================================= + +async function testObjectModeError() { + const readable = new Readable({ + objectMode: true, + read() { + process.nextTick(() => this.destroy(new Error('object error'))); + }, + }); + + await assert.rejects( + text(from(readable)), + (err) => err.message === 'object error', + ); +} + +// ============================================================================= +// Stream destroyed mid-iteration +// ============================================================================= + +async function testDestroyMidIteration() { + let pushCount = 0; + const readable = new Readable({ + read() { + this.push(Buffer.from(`chunk${pushCount++}`)); + // Never pushes null - infinite stream + }, + }); + + const chunks = []; + await assert.rejects(async () => { + for await (const batch of from(readable)) { + chunks.push(...batch); + if (chunks.length >= 3) { + readable.destroy(); + } + } + }, { code: 'ERR_STREAM_PREMATURE_CLOSE' }); + assert.ok(readable.destroyed); + assert.ok(chunks.length >= 3); +} + +// ============================================================================= +// Error after partial data +// ============================================================================= + +async function testErrorAfterPartialData() { + let count = 0; + const readable = new Readable({ + read() { + if (count < 3) { + this.push(Buffer.from(`ok${count++}`)); + } else { + this.destroy(new Error('late error')); + } + }, + }); + + const chunks = []; + await assert.rejects(async () => { + for await (const batch of from(readable)) { + chunks.push(...batch); + } + }, { message: 'late error' }); + assert.ok(chunks.length > 0, 'Should have received partial data'); +} + +// ============================================================================= +// Multiple consumers (second iteration yields empty) +// ============================================================================= + +async function testMultipleConsumers() { + const readable = new Readable({ + read() { + this.push(Buffer.from('data')); + this.push(null); + }, + }); + + const first = await text(from(readable)); + assert.strictEqual(first, 'data'); + + // Second consumption - stream is already consumed/destroyed + const second = await text(from(readable)); + assert.strictEqual(second, ''); +} + +// ============================================================================= +// highWaterMark: 0 - each chunk becomes its own batch +// ============================================================================= + +async function testHighWaterMarkZero() { + let pushCount = 0; + const readable = new Readable({ + highWaterMark: 0, + read() { + if (pushCount < 3) { + this.push(Buffer.from(`hwm0-${pushCount++}`)); + } else { + this.push(null); + } + }, + }); + + const batches = []; + for await (const batch of from(readable)) { + batches.push(batch); + } + + // With HWM=0, buffer is always empty so drain loop never fires. + // Each chunk should be its own batch. + assert.strictEqual(batches.length, 3); + const combined = Buffer.concat(batches.flat()); + assert.strictEqual(combined.toString(), 'hwm0-0hwm0-1hwm0-2'); +} + +// ============================================================================= +// Duplex stream (Duplex extends Readable, toAsyncStreamable should work) +// ============================================================================= + +async function testDuplexStream() { + const { Duplex } = require('stream'); + + const duplex = new Duplex({ + read() { + this.push(Buffer.from('duplex-data')); + this.push(null); + }, + write(chunk, enc, cb) { cb(); }, + }); + + const result = await text(from(duplex)); + assert.strictEqual(result, 'duplex-data'); +} + +// ============================================================================= +// setEncoding called dynamically after construction +// ============================================================================= + +async function testSetEncodingDynamic() { + const readable = new Readable({ + read() { + this.push(Buffer.from('dynamic-enc')); + this.push(null); + }, + }); + + readable.setEncoding('utf8'); + + const result = await text(from(readable)); + assert.strictEqual(result, 'dynamic-enc'); +} + +// ============================================================================= +// AbortSignal cancellation +// ============================================================================= + +async function testAbortSignal() { + let pushCount = 0; + const readable = new Readable({ + read() { + this.push(Buffer.from(`sig${pushCount++}`)); + // Never pushes null - infinite stream + }, + }); + + const ac = new AbortController(); + const chunks = []; + + await assert.rejects(async () => { + for await (const batch of pull(readable, { signal: ac.signal })) { + chunks.push(...batch); + if (chunks.length >= 2) { + ac.abort(); + } + } + }, { name: 'AbortError' }); + assert.ok(chunks.length >= 2); +} + +// ============================================================================= +// kValidatedSource identity - from() returns same object for validated sources +// ============================================================================= + +function testTrustedSourceIdentity() { + const readable = new Readable({ read() {} }); + const iter = readable[toAsyncStreamable](); + + // from() should return the validated iterator directly (same reference), + // not wrap it in another generator + const result = from(iter); + assert.strictEqual(result, iter); +} + +// ============================================================================= +// Run all tests +// ============================================================================= + +testProtocolExists(); +testTrustedSourceByteMode(); +testTrustedSourceObjectMode(); +testTrustedSourceIdentity(); + +Promise.all([ + testByteModeThroughFrom(), + testByteModeThroughPull(), + testByteModeBytes(), + testBatchingBehavior(), + testDelayedPushes(), + testEmptyStream(), + testErrorPropagation(), + testWithTransform(), + testObjectModeStrings(), + testObjectModeUint8Array(), + testObjectModeMixed(), + testObjectModeToStreamable(), + testEncodedReadable(), + testReadableFrom(), + testLargeData(), + testEarlyTermination(), + testWithCompression(), + testObjectModeError(), + testDestroyMidIteration(), + testErrorAfterPartialData(), + testMultipleConsumers(), + testHighWaterMarkZero(), + testDuplexStream(), + testSetEncodingDynamic(), + testAbortSignal(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-share-async.js b/test/parallel/test-stream-iter-share-async.js new file mode 100644 index 00000000000000..96b9bbc9c69bda --- /dev/null +++ b/test/parallel/test-stream-iter-share-async.js @@ -0,0 +1,373 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + from, + share, + text, +} = require('stream/iter'); + +const { setTimeout } = require('timers/promises'); + +// ============================================================================= +// Async share() +// ============================================================================= + +async function testBasicShare() { + const shared = share(from('hello shared')); + + const consumer = shared.pull(); + const data = await text(consumer); + assert.strictEqual(data, 'hello shared'); +} + +async function testShareMultipleConsumers() { + async function* gen() { + yield [new TextEncoder().encode('chunk1')]; + yield [new TextEncoder().encode('chunk2')]; + yield [new TextEncoder().encode('chunk3')]; + } + + const shared = share(gen(), { highWaterMark: 16 }); + + const c1 = shared.pull(); + const c2 = shared.pull(); + + assert.strictEqual(shared.consumerCount, 2); + + const [data1, data2] = await Promise.all([ + text(c1), + text(c2), + ]); + + assert.strictEqual(data1, 'chunk1chunk2chunk3'); + assert.strictEqual(data2, 'chunk1chunk2chunk3'); +} + +async function testShareConsumerCount() { + const shared = share(from('data')); + + assert.strictEqual(shared.consumerCount, 0); + + const c1 = shared.pull(); + assert.strictEqual(shared.consumerCount, 1); + + const c2 = shared.pull(); + assert.strictEqual(shared.consumerCount, 2); + + // Cancel detaches all consumers + shared.cancel(); + assert.strictEqual(shared.consumerCount, 0); + + // Both should complete immediately + const [data1, data2] = await Promise.all([ + text(c1), + text(c2), + ]); + assert.strictEqual(data1, ''); + assert.strictEqual(data2, ''); +} + +async function testShareCancel() { + const shared = share(from('data')); + const consumer = shared.pull(); + + shared.cancel(); + assert.strictEqual(shared.consumerCount, 0); + + const batches = []; + for await (const batch of consumer) { + batches.push(batch); + } + assert.strictEqual(batches.length, 0); +} + +async function testShareCancelMidIteration() { + // Verify that cancel during iteration stops data flow + let sourceReturnCalled = false; + const enc = new TextEncoder(); + async function* gen() { + try { + yield [enc.encode('a')]; + yield [enc.encode('b')]; + yield [enc.encode('c')]; + } finally { + sourceReturnCalled = true; + } + } + const shared = share(gen(), { highWaterMark: 16 }); + const consumer = shared.pull(); + + const items = []; + for await (const batch of consumer) { + for (const chunk of batch) { + items.push(new TextDecoder().decode(chunk)); + } + // Cancel after first batch + shared.cancel(); + } + assert.strictEqual(items.length, 1); + assert.strictEqual(items[0], 'a'); + + await new Promise(setImmediate); + assert.strictEqual(sourceReturnCalled, true); +} + +async function testShareCancelWithReason() { + const shared = share(from('data')); + const consumer = shared.pull(); + + shared.cancel(new Error('share cancelled')); + + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of consumer) { + assert.fail('Should not reach here'); + } + }, + { message: 'share cancelled' }, + ); +} + +async function testShareAbortSignal() { + const ac = new AbortController(); + const reason = new Error('share aborted'); + const enc = new TextEncoder(); + async function* source() { + yield [enc.encode('a')]; + yield [enc.encode('b')]; + } + const shared = share(source(), { + highWaterMark: 1, + backpressure: 'block', + signal: ac.signal, + }); + const fast = shared.pull()[Symbol.asyncIterator](); + shared.pull(); + + await fast.next(); + const read = fast.next(); + const rejected = assert.rejects(read, (error) => error === reason); + ac.abort(reason); + + await rejected; +} + +async function testShareAbortSignalWhileSourcePullPending() { + const ac = new AbortController(); + const { + promise: resumePromise, + resolve: resume, + } = Promise.withResolvers(); + const { + promise: sourceStartedPromise, + resolve: sourceStarted, + } = Promise.withResolvers(); + + const source = { + __proto__: null, + [Symbol.asyncIterator]() { + return { + __proto__: null, + async next() { + sourceStarted(); + await resumePromise; + return { __proto__: null, done: true, value: undefined }; + }, + }; + }, + }; + + const shared = share(source, { signal: ac.signal }); + const iter1 = shared.pull()[Symbol.asyncIterator](); + const iter2 = shared.pull()[Symbol.asyncIterator](); + const read1 = iter1.next(); + const read2 = iter2.next(); + const rejected1 = assert.rejects(read1, { name: 'AbortError' }); + const rejected2 = assert.rejects(read2, { name: 'AbortError' }); + + await sourceStartedPromise; + ac.abort(); + resume(); + + await Promise.all([rejected1, rejected2]); +} + +async function testSharePullAbortSignalRejectsPendingNext() { + const ac = new AbortController(); + const reason = new Error('pull aborted'); + const shared = share( + // eslint-disable-next-line require-yield + (async function* never() { + await new Promise(() => {}); + })(), + ); + const iter = shared.pull({ signal: ac.signal })[Symbol.asyncIterator](); + + const pendingNext = iter.next(); + const rejected = assert.rejects(pendingNext, (error) => error === reason); + ac.abort(reason); + + await rejected; + shared.cancel(); +} + +async function testShareAlreadyAborted() { + const ac = new AbortController(); + ac.abort(); + + const shared = share(from('data'), { signal: ac.signal }); + const consumer = shared.pull(); + + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of consumer) { + assert.fail('Should not reach here'); + } + }, { name: 'AbortError' }); +} + +// ============================================================================= +// Source error propagation +// ============================================================================= + +async function testShareSourceError() { + async function* failingSource() { + yield [new TextEncoder().encode('a')]; + throw new Error('share source boom'); + } + const shared = share(failingSource()); + const c1 = shared.pull(); + const c2 = shared.pull(); + + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of c1) { /* consume */ } + }, { message: 'share source boom' }); + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of c2) { /* consume */ } + }, { message: 'share source boom' }); +} + +async function testShareLateJoiningConsumer() { + // A consumer that joins after some data has been consumed should only + // see data remaining in the buffer (not items already trimmed). + const enc = new TextEncoder(); + async function* gen() { + yield [enc.encode('a')]; + yield [enc.encode('b')]; + yield [enc.encode('c')]; + } + const shared = share(gen(), { highWaterMark: 16 }); + + // First consumer reads all data + const c1 = shared.pull(); + const data1 = await text(c1); + assert.strictEqual(data1, 'abc'); + + // Late-joining consumer: source is exhausted, buffer has been trimmed + // past all data by c1's reads, so c2 gets nothing. + const c2 = shared.pull(); + const data2 = await text(c2); + assert.strictEqual(data2, ''); +} + +async function testShareConsumerBreak() { + // Verify that a consumer breaking mid-iteration detaches properly + const enc = new TextEncoder(); + async function* gen() { + yield [enc.encode('a')]; + yield [enc.encode('b')]; + yield [enc.encode('c')]; + } + const shared = share(gen(), { highWaterMark: 16 }); + const c1 = shared.pull(); + const c2 = shared.pull(); + + assert.strictEqual(shared.consumerCount, 2); + + // c1 breaks after first batch + // eslint-disable-next-line no-unused-vars + for await (const _ of c1) { + break; + } + // c1 should be detached + assert.strictEqual(shared.consumerCount, 1); + + // c2 should still get all data + const data2 = await text(c2); + assert.strictEqual(data2, 'abc'); +} + +async function testShareMultipleConsumersConcurrentPull() { + // Multiple consumers pulling concurrently should each receive all items + // even when only one item is pulled from source at a time. + async function* slowSource() { + const enc = new TextEncoder(); + for (let i = 0; i < 5; i++) { + await setTimeout(1); + yield [enc.encode(`item-${i}`)]; + } + } + const shared = share(slowSource()); + const c1 = shared.pull(); + const c2 = shared.pull(); + const c3 = shared.pull(); + + const [t1, t2, t3] = await Promise.all([ + text(c1), text(c2), text(c3), + ]); + + const expected = 'item-0item-1item-2item-3item-4'; + assert.strictEqual(t1, expected); + assert.strictEqual(t2, expected); + assert.strictEqual(t3, expected); +} + +async function testShareConsumerConcurrentNextCalls() { + async function* source() { + const enc = new TextEncoder(); + yield [enc.encode('first')]; + yield [enc.encode('second')]; + } + + const shared = share(source()); + const it = shared.pull()[Symbol.asyncIterator](); + const first = it.next(); + const second = it.next(); + + const [r1, r2] = await Promise.all([first, second]); + const dec = new TextDecoder(); + assert.strictEqual(dec.decode(r1.value[0]), 'first'); + assert.strictEqual(dec.decode(r2.value[0]), 'second'); +} + +// share() accepts string source directly (normalized via from()) +async function testShareStringSource() { + const shared = share('hello-share'); + const result = await text(shared.pull()); + assert.strictEqual(result, 'hello-share'); +} + +Promise.all([ + testBasicShare(), + testShareMultipleConsumers(), + testShareConsumerCount(), + testShareCancel(), + testShareCancelMidIteration(), + testShareCancelWithReason(), + testShareAbortSignal(), + testShareAbortSignalWhileSourcePullPending(), + testSharePullAbortSignalRejectsPendingNext(), + testShareAlreadyAborted(), + testShareSourceError(), + testShareLateJoiningConsumer(), + testShareConsumerBreak(), + testShareMultipleConsumersConcurrentPull(), + testShareConsumerConcurrentNextCalls(), + testShareStringSource(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-share-coverage.js b/test/parallel/test-stream-iter-share-coverage.js new file mode 100644 index 00000000000000..48866e61deab9b --- /dev/null +++ b/test/parallel/test-stream-iter-share-coverage.js @@ -0,0 +1,128 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Coverage tests for share.js: protocol happy path, dispose, throw, +// non-Error source throws. + +const common = require('../common'); +const assert = require('assert'); +const { + Share, + SyncShare, + share, + shareSync, + shareProtocol, + shareSyncProtocol, + from, + fromSync, + text, + textSync, +} = require('stream/iter'); + +// Share.from — protocol symbol happy path (returns valid share object) +async function testShareProtocolHappyPath() { + const obj = { + [shareProtocol](options) { + return share(from('protocol-data'), options); + }, + }; + const shared = Share.from(obj); + const result = await text(shared.pull()); + assert.strictEqual(result, 'protocol-data'); +} + +// SyncShare.fromSync — protocol symbol happy path +async function testSyncShareProtocolHappyPath() { + const obj = { + [shareSyncProtocol](options) { + return shareSync(fromSync('sync-protocol'), options); + }, + }; + const shared = SyncShare.fromSync(obj); + const result = textSync(shared.pull()); + assert.strictEqual(result, 'sync-protocol'); +} + +// Async share — Symbol.dispose cancels +async function testShareDispose() { + const shared = share(from('dispose-test')); + const consumer = shared.pull(); + shared[Symbol.dispose](); + assert.strictEqual(shared.consumerCount, 0); + // Consumer should yield nothing (cancelled before read) + const result = await text(consumer); + assert.strictEqual(result, ''); +} + +// Sync share — Symbol.dispose cancels +async function testSyncShareDispose() { + const shared = shareSync(fromSync('sync-dispose')); + const consumer = shared.pull(); + shared[Symbol.dispose](); + assert.strictEqual(shared.consumerCount, 0); + const result = textSync(consumer); + assert.strictEqual(result, ''); +} + +// Async consumer iterator throw() +async function testAsyncIteratorThrow() { + const shared = share(from('throw-test')); + const consumer = shared.pull(); + const iter = consumer[Symbol.asyncIterator](); + const first = await iter.next(); + assert.strictEqual(first.done, false); + const result = await iter.throw(new Error('test-throw')); + assert.strictEqual(result.done, true); + assert.strictEqual(shared.consumerCount, 0); +} + +// Sync consumer iterator throw() +async function testSyncIteratorThrow() { + const shared = shareSync(fromSync('throw-sync')); + const consumer = shared.pull(); + const iter = consumer[Symbol.iterator](); + const first = iter.next(); + assert.strictEqual(first.done, false); + const result = iter.throw(new Error('test-throw')); + assert.strictEqual(result.done, true); + assert.strictEqual(shared.consumerCount, 0); +} + +// Async source throws non-Error value → wrapError +async function testShareSourceThrowsNonError() { + async function* source() { + yield [new TextEncoder().encode('ok')]; + throw 'not an error'; // eslint-disable-line no-throw-literal + } + const shared = share(source()); + const consumer = shared.pull(); + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const batch of consumer) { /* consume */ } + }, { code: 'ERR_OPERATION_FAILED' }); +} + +// Sync source throws non-Error value → wrapError +async function testSyncShareSourceThrowsNonError() { + function* source() { + yield [new TextEncoder().encode('ok')]; + throw 42; // eslint-disable-line no-throw-literal + } + const shared = shareSync(source()); + const consumer = shared.pull(); + assert.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const batch of consumer) { /* consume */ } + }, { code: 'ERR_OPERATION_FAILED' }); +} + +Promise.all([ + testShareProtocolHappyPath(), + testSyncShareProtocolHappyPath(), + testShareDispose(), + testSyncShareDispose(), + testAsyncIteratorThrow(), + testSyncIteratorThrow(), + testShareSourceThrowsNonError(), + testSyncShareSourceThrowsNonError(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-share-from.js b/test/parallel/test-stream-iter-share-from.js new file mode 100644 index 00000000000000..362bfef78f1a2e --- /dev/null +++ b/test/parallel/test-stream-iter-share-from.js @@ -0,0 +1,242 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + from, + fromSync, + share, + Share, + SyncShare, + text, + textSync, + +} = require('stream/iter'); + +// ============================================================================= +// Share.from +// ============================================================================= + +async function testShareFrom() { + const shared = Share.from(from('share-from')); + const consumer = shared.pull(); + + const data = await text(consumer); + assert.strictEqual(data, 'share-from'); +} + +function testShareFromRejectsNonStreamable() { + assert.throws( + () => Share.from(12345), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +// ============================================================================= +// SyncShare.fromSync +// ============================================================================= + +async function testSyncShareFromSync() { + const shared = SyncShare.fromSync(fromSync('sync-share-from')); + const consumer = shared.pull(); + + const data = textSync(consumer); + assert.strictEqual(data, 'sync-share-from'); +} + +function testSyncShareFromRejectsNonStreamable() { + assert.throws( + () => SyncShare.fromSync(12345), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +// ============================================================================= +// Protocol validation +// ============================================================================= + +function testShareProtocolReturnsNull() { + const obj = { + [Symbol.for('Stream.shareProtocol')]() { return null; }, + }; + assert.throws( + () => Share.from(obj), + { code: 'ERR_INVALID_RETURN_VALUE' }, + ); +} + +function testShareProtocolReturnsNonObject() { + const obj = { + [Symbol.for('Stream.shareProtocol')]() { return 42; }, + }; + assert.throws( + () => Share.from(obj), + { code: 'ERR_INVALID_RETURN_VALUE' }, + ); +} + +function testSyncShareProtocolReturnsNull() { + const obj = { + [Symbol.for('Stream.shareSyncProtocol')]() { return null; }, + }; + assert.throws( + () => SyncShare.fromSync(obj), + { code: 'ERR_INVALID_RETURN_VALUE' }, + ); +} + +function testSyncShareProtocolReturnsNonObject() { + const obj = { + [Symbol.for('Stream.shareSyncProtocol')]() { return 'bad'; }, + }; + assert.throws( + () => SyncShare.fromSync(obj), + { code: 'ERR_INVALID_RETURN_VALUE' }, + ); +} + +// ============================================================================= +// Block backpressure: two consumers, slow consumer blocks the source +// ============================================================================= + +async function testShareBlockBackpressure() { + // A source that yields 5 items. With two consumers and highWaterMark: 2, + // the fast consumer drives the source forward. The slow consumer holds back + // trimming, causing the buffer to fill. 'block' mode should stall the + // source pull until the slow consumer catches up. + const enc = new TextEncoder(); + async function* source() { + for (let i = 0; i < 5; i++) { + yield [enc.encode(`item${i}`)]; + } + } + const shared = share(source(), { highWaterMark: 2, backpressure: 'block' }); + const fast = shared.pull(); + const slow = shared.pull(); + + // Both consumers should ultimately receive all 5 items + const [fastData, slowData] = await Promise.all([ + text(fast), + text(slow), + ]); + + assert.strictEqual(fastData, 'item0item1item2item3item4'); + assert.strictEqual(slowData, 'item0item1item2item3item4'); +} + +// ============================================================================= +// Drop backpressure modes: use a fast + stalled consumer to trigger drops +// ============================================================================= + +async function testShareDropOldest() { + // Two consumers, fast reads eagerly then slow reads. With drop-oldest, + // the slow consumer's cursor is advanced past dropped items, so it + // misses old data and only sees recent items. + async function* source() { + for (let i = 0; i < 4; i++) { + yield [new TextEncoder().encode(`${i}`)]; + } + } + const shared = share(source(), { highWaterMark: 2, backpressure: 'drop-oldest' }); + const fast = shared.pull(); + const slow = shared.pull(); + + // Fast consumer reads all items + const fastItems = []; + for await (const batch of fast) { + for (const chunk of batch) { + fastItems.push(new TextDecoder().decode(chunk)); + } + } + assert.strictEqual(fastItems.length, 4); + + // Slow consumer reads after fast is done — old items were dropped + const slowItems = []; + for await (const batch of slow) { + for (const chunk of batch) { + slowItems.push(new TextDecoder().decode(chunk)); + } + } + // The slow consumer should see fewer items than were produced + assert.ok(slowItems.length < 4, + `Expected < 4 items after drop-oldest, got ${slowItems.length}`); + assert.ok(slowItems.length > 0, + 'Expected at least some items after drop-oldest'); + // The last item should always be present (most recent items kept) + assert.strictEqual(slowItems[slowItems.length - 1], '3'); +} + +async function testShareDropNewest() { + // With drop-newest and a stalled consumer, the async path allows the + // buffer to grow beyond highWaterMark (the "drop" applies to the + // backpressure signal, not the buffer contents). Both consumers + // ultimately see all items. + async function* source() { + for (let i = 0; i < 4; i++) { + yield [new TextEncoder().encode(`${i}`)]; + } + } + const shared = share(source(), { highWaterMark: 2, backpressure: 'drop-newest' }); + const fast = shared.pull(); + const slow = shared.pull(); + + // Fast consumer reads all items + const fastItems = []; + for await (const batch of fast) { + for (const chunk of batch) { + fastItems.push(new TextDecoder().decode(chunk)); + } + } + assert.strictEqual(fastItems.length, 4); + + // Slow consumer also sees all items (buffer grew past hwm) + const slowItems = []; + for await (const batch of slow) { + for (const chunk of batch) { + slowItems.push(new TextDecoder().decode(chunk)); + } + } + assert.strictEqual(slowItems.length, 4); + assert.strictEqual(slowItems[0], '0'); + assert.strictEqual(slowItems[3], '3'); +} + +// ============================================================================= +// Strict backpressure: should throw when buffer overflows +// ============================================================================= + +async function testShareStrictBackpressure() { + async function* source() { + for (let i = 0; i < 10; i++) { + yield [new TextEncoder().encode(`${i}`)]; + } + } + const shared = share(source(), { highWaterMark: 2, backpressure: 'strict' }); + const fast = shared.pull(); + // Create a second consumer that never reads — this prevents buffer trimming + shared.pull(); + + // The fast consumer's pulls will eventually cause the buffer to exceed + // the highWaterMark (since the slow consumer prevents trimming), + // triggering an ERR_OUT_OF_RANGE error. + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of fast) { /* consume */ } + }, { code: 'ERR_OUT_OF_RANGE' }); +} + +Promise.all([ + testShareFrom(), + testShareFromRejectsNonStreamable(), + testSyncShareFromSync(), + testSyncShareFromRejectsNonStreamable(), + testShareProtocolReturnsNull(), + testShareProtocolReturnsNonObject(), + testSyncShareProtocolReturnsNull(), + testSyncShareProtocolReturnsNonObject(), + testShareBlockBackpressure(), + testShareDropOldest(), + testShareDropNewest(), + testShareStrictBackpressure(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-share-sync.js b/test/parallel/test-stream-iter-share-sync.js new file mode 100644 index 00000000000000..98ad74fe4f1c72 --- /dev/null +++ b/test/parallel/test-stream-iter-share-sync.js @@ -0,0 +1,162 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + shareSync, + fromSync, + textSync, + +} = require('stream/iter'); + +// ============================================================================= +// Sync share +// ============================================================================= + +async function testShareSyncBasic() { + const shared = shareSync(fromSync('sync shared')); + + const consumer = shared.pull(); + const data = textSync(consumer); + assert.strictEqual(data, 'sync shared'); +} + +async function testShareSyncMultipleConsumers() { + const enc = new TextEncoder(); + function* gen() { + yield [enc.encode('a')]; + yield [enc.encode('b')]; + yield [enc.encode('c')]; + } + + const shared = shareSync(gen(), { highWaterMark: 16 }); + + const c1 = shared.pull(); + const c2 = shared.pull(); + + const data1 = textSync(c1); + const data2 = textSync(c2); + + assert.strictEqual(data1, 'abc'); + assert.strictEqual(data2, 'abc'); +} + +function testShareSyncCancel() { + // Verify that cancel() on a pre-iteration share yields nothing + const shared = shareSync(fromSync('data')); + const consumer = shared.pull(); + + shared.cancel(); + assert.strictEqual(shared.consumerCount, 0); + + const batches = []; + for (const batch of consumer) { + batches.push(batch); + } + assert.strictEqual(batches.length, 0); +} + +function testShareSyncCancelMidIteration() { + // Verify cancel during iteration stops data flow and cleans up + const enc = new TextEncoder(); + let sourceReturnCalled = false; + function* gen() { + try { + yield [enc.encode('a')]; + yield [enc.encode('b')]; + yield [enc.encode('c')]; + } finally { + sourceReturnCalled = true; + } + } + const shared = shareSync(gen(), { highWaterMark: 16 }); + const consumer = shared.pull(); + + const items = []; + for (const batch of consumer) { + for (const chunk of batch) { + items.push(new TextDecoder().decode(chunk)); + } + // Cancel after first batch + shared.cancel(); + } + assert.strictEqual(items.length, 1); + assert.strictEqual(items[0], 'a'); + assert.strictEqual(sourceReturnCalled, true); +} + +function testShareSyncCancelWithReason() { + // When cancel(reason) is called, a consumer that hasn't started + // iterating is already detached, so it sees done:true (not the error). + // But a consumer that is mid-iteration when another consumer cancels + // with a reason will see the error on the next pull after cancel. + const enc = new TextEncoder(); + function* gen() { + yield [enc.encode('a')]; + yield [enc.encode('b')]; + yield [enc.encode('c')]; + } + const shared = shareSync(gen(), { highWaterMark: 16 }); + const c1 = shared.pull(); + const c2 = shared.pull(); + + // c1 reads one item, then c2 cancels with a reason + const iter1 = c1[Symbol.iterator](); + const first = iter1.next(); + assert.strictEqual(first.done, false); + + shared.cancel(new Error('sync cancel reason')); + + // c1 was already iterating, it's now detached → done + const next = iter1.next(); + assert.strictEqual(next.done, true); + + // c2 never started, also detached → done (not error) + const batches = []; + for (const batch of c2) { + batches.push(batch); + } + assert.strictEqual(batches.length, 0); +} + +// ============================================================================= +// Source error propagation +// ============================================================================= + +function testShareSyncSourceError() { + function* failingSource() { + yield [new TextEncoder().encode('ok')]; + throw new Error('sync share boom'); + } + const shared = shareSync(failingSource()); + const c1 = shared.pull(); + const c2 = shared.pull(); + + // Both consumers should see the error + assert.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const _ of c1) { /* consume */ } + }, { message: 'sync share boom' }); + assert.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const _ of c2) { /* consume */ } + }, { message: 'sync share boom' }); +} + +// shareSync() accepts string source directly (normalized via fromSync()) +function testShareSyncStringSource() { + const shared = shareSync('hello-sync-share'); + const result = textSync(shared.pull()); + assert.strictEqual(result, 'hello-sync-share'); +} + +Promise.all([ + testShareSyncBasic(), + testShareSyncMultipleConsumers(), + testShareSyncCancel(), + testShareSyncCancelMidIteration(), + testShareSyncCancelWithReason(), + testShareSyncSourceError(), + testShareSyncStringSource(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-sharedarraybuffer.js b/test/parallel/test-stream-iter-sharedarraybuffer.js new file mode 100644 index 00000000000000..7aff205bf7664a --- /dev/null +++ b/test/parallel/test-stream-iter-sharedarraybuffer.js @@ -0,0 +1,195 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + from, + fromSync, + bytes, + bytesSync, + text, + textSync, + arrayBuffer, + arrayBufferSync, + pipeTo, + pipeToSync, + pull, + pullSync, +} = require('stream/iter'); + +// ============================================================================= +// from() / fromSync() with SharedArrayBuffer +// ============================================================================= + +function testFromSyncSAB() { + const sab = new SharedArrayBuffer(4); + new Uint8Array(sab).set([10, 20, 30, 40]); + const batches = []; + for (const batch of fromSync(sab)) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], new Uint8Array([10, 20, 30, 40])); +} + +async function testFromAsyncSAB() { + const sab = new SharedArrayBuffer(4); + new Uint8Array(sab).set([10, 20, 30, 40]); + const batches = []; + for await (const batch of from(sab)) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], new Uint8Array([10, 20, 30, 40])); +} + +// ============================================================================= +// Consumers with SAB-backed source +// ============================================================================= + +function testBytesSyncSAB() { + const sab = new SharedArrayBuffer(5); + new Uint8Array(sab).set([104, 101, 108, 108, 111]); // 'hello' + const data = bytesSync(fromSync(sab)); + assert.deepStrictEqual(data, new Uint8Array([104, 101, 108, 108, 111])); +} + +async function testBytesAsyncSAB() { + const sab = new SharedArrayBuffer(5); + new Uint8Array(sab).set([104, 101, 108, 108, 111]); // 'hello' + const data = await bytes(from(sab)); + assert.deepStrictEqual(data, new Uint8Array([104, 101, 108, 108, 111])); +} + +function testTextSyncSAB() { + const sab = new SharedArrayBuffer(5); + new Uint8Array(sab).set([104, 101, 108, 108, 111]); // 'hello' + const result = textSync(fromSync(sab)); + assert.strictEqual(result, 'hello'); +} + +async function testTextAsyncSAB() { + const sab = new SharedArrayBuffer(5); + new Uint8Array(sab).set([104, 101, 108, 108, 111]); // 'hello' + const result = await text(from(sab)); + assert.strictEqual(result, 'hello'); +} + +function testArrayBufferSyncSAB() { + const sab = new SharedArrayBuffer(4); + new Uint8Array(sab).set([1, 2, 3, 4]); + const result = arrayBufferSync(fromSync(sab)); + assert.ok(result instanceof SharedArrayBuffer || result instanceof ArrayBuffer); + assert.deepStrictEqual(new Uint8Array(result), new Uint8Array([1, 2, 3, 4])); +} + +async function testArrayBufferAsyncSAB() { + const sab = new SharedArrayBuffer(4); + new Uint8Array(sab).set([1, 2, 3, 4]); + const result = await arrayBuffer(from(sab)); + assert.ok(result instanceof SharedArrayBuffer || result instanceof ArrayBuffer); + assert.deepStrictEqual(new Uint8Array(result), new Uint8Array([1, 2, 3, 4])); +} + +// ============================================================================= +// pipeTo / pipeToSync with SAB source +// ============================================================================= + +function testPipeToSyncSAB() { + const sab = new SharedArrayBuffer(3); + new Uint8Array(sab).set([65, 66, 67]); // 'ABC' + const written = []; + const writer = { + writeSync(chunk) { written.push(chunk); return true; }, + endSync() { return written.length; }, + }; + const totalBytes = pipeToSync(fromSync(sab), writer); + assert.strictEqual(totalBytes, 3); + assert.deepStrictEqual(written[0], new Uint8Array([65, 66, 67])); +} + +async function testPipeToAsyncSAB() { + const sab = new SharedArrayBuffer(3); + new Uint8Array(sab).set([65, 66, 67]); // 'ABC' + const written = []; + const writer = { + async write(chunk) { written.push(chunk); }, + async end() { return written.length; }, + }; + await pipeTo(from(sab), writer); + assert.deepStrictEqual(written[0], new Uint8Array([65, 66, 67])); +} + +// ============================================================================= +// pull / pullSync with SAB-yielding generator +// ============================================================================= + +function testPullSyncSABChunks() { + function* gen() { + const sab = new SharedArrayBuffer(2); + new Uint8Array(sab).set([1, 2]); + yield [new Uint8Array(sab)]; + } + const batches = []; + for (const batch of pullSync(gen())) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], new Uint8Array([1, 2])); +} + +async function testPullAsyncSABChunks() { + async function* gen() { + const sab = new SharedArrayBuffer(2); + new Uint8Array(sab).set([3, 4]); + yield [new Uint8Array(sab)]; + } + const batches = []; + for await (const batch of pull(gen())) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], new Uint8Array([3, 4])); +} + +// ============================================================================= +// Transform returning SAB +// ============================================================================= + +async function testTransformReturningSAB() { + function* source() { + yield [new Uint8Array([1, 2, 3])]; + } + const transform = (chunks) => { + if (chunks === null) return null; + // Transform returns a Uint8Array backed by a SharedArrayBuffer + const sab = new SharedArrayBuffer(chunks[0].length); + new Uint8Array(sab).set(chunks[0]); + return new Uint8Array(sab); + }; + const batches = []; + for await (const batch of pull(source(), transform)) { + batches.push(batch); + } + assert.strictEqual(batches.length, 1); + assert.deepStrictEqual(batches[0][0], new Uint8Array([1, 2, 3])); +} + +// ============================================================================= + +Promise.all([ + testFromSyncSAB(), + testFromAsyncSAB(), + testBytesSyncSAB(), + testBytesAsyncSAB(), + testTextSyncSAB(), + testTextAsyncSAB(), + testArrayBufferSyncSAB(), + testArrayBufferAsyncSAB(), + testPipeToSyncSAB(), + testPipeToAsyncSAB(), + testPullSyncSABChunks(), + testPullAsyncSABChunks(), + testTransformReturningSAB(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-to-readable.js b/test/parallel/test-stream-iter-to-readable.js new file mode 100644 index 00000000000000..836aa7179f00d7 --- /dev/null +++ b/test/parallel/test-stream-iter-to-readable.js @@ -0,0 +1,636 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Tests for toReadable() and toReadableSync() which create byte-mode +// Readable streams from stream/iter sources. + +const common = require('../common'); +const assert = require('assert'); +const { Writable } = require('stream'); +const { + from, + fromSync, + pull, + text, + toReadable, + toReadableSync, +} = require('stream/iter'); + +function collect(readable) { + return new Promise((resolve, reject) => { + const chunks = []; + readable.on('data', (chunk) => chunks.push(chunk)); + readable.on('end', () => resolve(Buffer.concat(chunks))); + readable.on('error', reject); + }); +} + +// ============================================================================= +// fromStreamIter: basic async round-trip +// ============================================================================= + +async function testBasicAsync() { + const source = from('hello world'); + const readable = toReadable(source); + + assert.strictEqual(readable.readableObjectMode, false); + + const result = await collect(readable); + assert.strictEqual(result.toString(), 'hello world'); +} + +// ============================================================================= +// fromStreamIter: multiple batches, all chunks arrive in order +// ============================================================================= + +async function testMultiBatchAsync() { + async function* gen() { + for (let i = 0; i < 5; i++) { + const batch = []; + for (let j = 0; j < 3; j++) { + batch.push(Buffer.from(`${i}-${j} `)); + } + yield batch; + } + } + + const readable = toReadable(gen()); + const result = await collect(readable); + assert.strictEqual(result.toString(), + '0-0 0-1 0-2 1-0 1-1 1-2 2-0 2-1 2-2 3-0 3-1 3-2 4-0 4-1 4-2 '); +} + +// ============================================================================= +// fromStreamIter: backpressure with small highWaterMark +// ============================================================================= + +async function testBackpressureAsync() { + let batchCount = 0; + async function* gen() { + for (let i = 0; i < 10; i++) { + batchCount++; + yield [Buffer.from(`chunk${i}`)]; + } + } + + const readable = toReadable(gen(), { highWaterMark: 1 }); + + // Read one chunk at a time with delays to exercise backpressure + const chunks = []; + for await (const chunk of readable) { + chunks.push(chunk); + } + + assert.strictEqual(chunks.length, 10); + assert.strictEqual(batchCount, 10); +} + +// ============================================================================= +// fromStreamIter: source error mid-stream +// ============================================================================= + +async function testErrorAsync() { + async function* gen() { + yield [Buffer.from('ok')]; + throw new Error('source failed'); + } + + const readable = toReadable(gen()); + + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const chunk of readable) { + // Consume until error + } + }, { message: 'source failed' }); +} + +// ============================================================================= +// fromStreamIter: empty source +// ============================================================================= + +async function testEmptyAsync() { + async function* gen() { + // yields nothing + } + + const readable = toReadable(gen()); + const result = await collect(readable); + assert.strictEqual(result.length, 0); +} + +// ============================================================================= +// fromStreamIter: empty batches are skipped +// ============================================================================= + +async function testEmptyBatchAsync() { + async function* gen() { + yield []; + yield [Buffer.from('real data')]; + yield []; + } + + const readable = toReadable(gen()); + const result = await collect(readable); + assert.strictEqual(result.toString(), 'real data'); +} + +// ============================================================================= +// fromStreamIter: destroy mid-stream cleans up iterator +// ============================================================================= + +async function testDestroyAsync() { + let returnCalled = false; + async function* gen() { + try { + for (let i = 0; ; i++) { + yield [Buffer.from(`chunk${i}`)]; + } + } finally { + returnCalled = true; + } + } + + const readable = toReadable(gen()); + + // Read a couple chunks then destroy + const chunks = []; + await new Promise((resolve, reject) => { + readable.on('data', (chunk) => { + chunks.push(chunk); + if (chunks.length >= 3) { + readable.destroy(); + } + }); + readable.on('close', resolve); + readable.on('error', reject); + }); + + assert.ok(chunks.length >= 3); + assert.ok(returnCalled, 'iterator.return() should have been called'); +} + +// ============================================================================= +// fromStreamIter: destroy while pump is waiting on backpressure +// ============================================================================= + +async function testDestroyDuringBackpressure() { + let returnCalled = false; + async function* gen() { + try { + // Yield a large batch that will trigger backpressure with HWM=1 + yield [Buffer.from('a'), Buffer.from('b'), Buffer.from('c')]; + // This should never be reached + yield [Buffer.from('d')]; + } finally { + returnCalled = true; + } + } + + const readable = toReadable(gen(), { highWaterMark: 1 }); + + // Read one chunk to start the pump, then destroy while it's waiting + const chunk = await new Promise((resolve) => { + readable.once('readable', () => resolve(readable.read())); + }); + assert.ok(chunk); + + // The pump should be waiting on backpressure now. Destroy the stream. + readable.destroy(); + + await new Promise((resolve) => readable.on('close', resolve)); + assert.ok(readable.destroyed); + assert.ok(returnCalled, 'iterator.return() should have been called'); +} + +// ============================================================================= +// fromStreamIter: large data integrity +// ============================================================================= + +async function testLargeDataAsync() { + const totalSize = 1024 * 1024; // 1 MB + const chunkSize = 16384; + + async function* gen() { + let remaining = totalSize; + while (remaining > 0) { + const size = Math.min(chunkSize, remaining); + yield [Buffer.alloc(size, 0x42)]; // Fill with 'B' + remaining -= size; + } + } + + const readable = toReadable(gen()); + const result = await collect(readable); + assert.strictEqual(result.length, totalSize); + assert.strictEqual(result[0], 0x42); + assert.strictEqual(result[totalSize - 1], 0x42); +} + +// ============================================================================= +// fromStreamIter: pipe to Writable +// ============================================================================= + +async function testPipeAsync() { + const source = from('pipe test data'); + const readable = toReadable(source); + + const chunks = []; + const writable = new Writable({ + write(chunk, enc, cb) { + chunks.push(chunk); + cb(); + }, + }); + + await new Promise((resolve, reject) => { + readable.pipe(writable); + writable.on('finish', resolve); + writable.on('error', reject); + }); + + assert.strictEqual(Buffer.concat(chunks).toString(), 'pipe test data'); +} + +// ============================================================================= +// fromStreamIter: not object mode +// ============================================================================= + +function testNotObjectMode() { + async function* gen() { yield [Buffer.from('x')]; } + const readable = toReadable(gen()); + assert.strictEqual(readable.readableObjectMode, false); +} + +// ============================================================================= +// fromStreamIter: invalid source throws +// ============================================================================= + +function testInvalidSourceAsync() { + assert.throws(() => toReadable(42), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => toReadable('not iterable'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => toReadable(null), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// ============================================================================= +// fromStreamIter: signal aborts the readable +// ============================================================================= + +async function testSignalAsync() { + async function* gen() { + for (let i = 0; ; i++) { + yield [Buffer.from(`chunk${i}`)]; + } + } + + const ac = new AbortController(); + const readable = toReadable(gen(), { signal: ac.signal }); + + const chunks = []; + await assert.rejects(async () => { + for await (const chunk of readable) { + chunks.push(chunk); + if (chunks.length >= 3) { + ac.abort(); + } + } + }, { name: 'AbortError' }); + assert.ok(chunks.length >= 3); + assert.ok(readable.destroyed); +} + +// ============================================================================= +// fromStreamIter: signal already aborted +// ============================================================================= + +async function testSignalAlreadyAborted() { + async function* gen() { + yield [Buffer.from('should not reach')]; + } + + const ac = new AbortController(); + ac.abort(); + const readable = toReadable(gen(), { signal: ac.signal }); + + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const chunk of readable) { + // Should not receive any data + } + }, { name: 'AbortError' }); + assert.ok(readable.destroyed); +} + +// ============================================================================= +// fromStreamIter: options validation +// ============================================================================= + +function testOptionsValidationAsync() { + async function* gen() { yield [Buffer.from('x')]; } + + // Options must be an object + assert.throws(() => toReadable(gen(), 'bad'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => toReadable(gen(), 42), + { code: 'ERR_INVALID_ARG_TYPE' }); + + // highWaterMark must be a non-negative integer + assert.throws(() => toReadable(gen(), { highWaterMark: -1 }), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => toReadable(gen(), { highWaterMark: 'big' }), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => toReadable(gen(), { highWaterMark: 1.5 }), + { code: 'ERR_OUT_OF_RANGE' }); + + // Signal must be an AbortSignal + assert.throws(() => toReadable(gen(), { signal: 'not a signal' }), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => toReadable(gen(), { signal: 42 }), + { code: 'ERR_INVALID_ARG_TYPE' }); + + // Valid options should work + const r = toReadable(gen(), { highWaterMark: 0 }); + assert.strictEqual(r.readableHighWaterMark, 0); + r.destroy(); + + // Default highWaterMark should be 16KB + const r2 = toReadable(gen()); + assert.strictEqual(r2.readableHighWaterMark, 64 * 1024); + r2.destroy(); + + // Explicit undefined options should work (uses defaults) + const r3 = toReadable(gen(), undefined); + assert.strictEqual(r3.readableHighWaterMark, 64 * 1024); + r3.destroy(); +} + +// ============================================================================= +// fromStreamIter: with stream/iter transform +// ============================================================================= + +async function testWithTransformAsync() { + // Uppercase transform + function upper(chunks) { + if (chunks === null) return null; + return chunks.map((c) => { + const buf = Buffer.from(c); + for (let i = 0; i < buf.length; i++) { + if (buf[i] >= 97 && buf[i] <= 122) buf[i] -= 32; + } + return buf; + }); + } + + const source = pull(from('hello world'), upper); + const readable = toReadable(source); + const result = await collect(readable); + assert.strictEqual(result.toString(), 'HELLO WORLD'); +} + +// ============================================================================= +// fromStreamIterSync: basic sync round-trip +// ============================================================================= + +async function testBasicSync() { + const source = fromSync('sync hello'); + const readable = toReadableSync(source); + + assert.strictEqual(readable.readableObjectMode, false); + + const result = await collect(readable); + assert.strictEqual(result.toString(), 'sync hello'); +} + +// ============================================================================= +// fromStreamIterSync: synchronous read() returns data immediately +// ============================================================================= + +function testSyncRead() { + const source = fromSync('immediate'); + const readable = toReadableSync(source); + + // Synchronous read should return data right away + const chunk = readable.read(); + assert.ok(Buffer.isBuffer(chunk)); + assert.strictEqual(chunk.toString(), 'immediate'); +} + +// ============================================================================= +// fromStreamIterSync: backpressure with small highWaterMark +// ============================================================================= + +async function testBackpressureSync() { + function* gen() { + for (let i = 0; i < 10; i++) { + yield [Buffer.from(`s${i}`)]; + } + } + + const readable = toReadableSync(gen(), { highWaterMark: 1 }); + + const chunks = []; + for await (const chunk of readable) { + chunks.push(chunk); + } + + assert.strictEqual(chunks.length, 9); +} + +// ============================================================================= +// fromStreamIterSync: backpressure within a batch +// ============================================================================= + +async function testBackpressureSyncMultiChunkBatch() { + function* gen() { + yield [Buffer.from('a'), Buffer.from('b'), Buffer.from('c')]; + } + + const readable = toReadableSync(gen(), { highWaterMark: 1 }); + const result = await collect(readable); + + assert.strictEqual(result.toString(), 'abc'); +} + +// ============================================================================= +// fromStreamIterSync: source error +// ============================================================================= + +async function testErrorSync() { + function* gen() { + yield [Buffer.from('ok')]; + throw new Error('sync source failed'); + } + + const readable = toReadableSync(gen()); + + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const chunk of readable) { + // Consume until error + } + }, { message: 'sync source failed' }); +} + +// ============================================================================= +// fromStreamIterSync: empty source +// ============================================================================= + +function testEmptySync() { + function* gen() { + // yields nothing + } + + const readable = toReadableSync(gen()); + const result = readable.read(); + assert.strictEqual(result, null); +} + +// ============================================================================= +// fromStreamIterSync: destroy calls iterator.return() +// ============================================================================= + +async function testDestroySync() { + let returnCalled = false; + function* gen() { + try { + for (let i = 0; ; i++) { + yield [Buffer.from(`chunk${i}`)]; + } + } finally { + returnCalled = true; + } + } + + const readable = toReadableSync(gen()); + readable.read(); // Start iteration + readable.destroy(); + + await new Promise((resolve) => readable.on('close', resolve)); + assert.ok(returnCalled, 'iterator.return() should have been called'); +} + +// ============================================================================= +// fromStreamIterSync: not object mode +// ============================================================================= + +function testNotObjectModeSync() { + function* gen() { yield [Buffer.from('x')]; } + const readable = toReadableSync(gen()); + assert.strictEqual(readable.readableObjectMode, false); +} + +// ============================================================================= +// fromStreamIterSync: invalid source throws +// ============================================================================= + +function testInvalidSourceSync() { + assert.throws(() => toReadableSync(42), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => toReadableSync(null), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// ============================================================================= +// fromStreamIterSync: options validation +// ============================================================================= + +function testOptionsValidationSync() { + function* gen() { yield [Buffer.from('x')]; } + + // Options must be an object + assert.throws(() => toReadableSync(gen(), 'bad'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => toReadableSync(gen(), 42), + { code: 'ERR_INVALID_ARG_TYPE' }); + + // highWaterMark must be a non-negative integer + assert.throws(() => toReadableSync(gen(), { highWaterMark: -1 }), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => toReadableSync(gen(), { highWaterMark: 'big' }), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => toReadableSync(gen(), { highWaterMark: 1.5 }), + { code: 'ERR_OUT_OF_RANGE' }); + + // Valid options should work + const r = toReadableSync(gen(), { highWaterMark: 0 }); + assert.strictEqual(r.readableHighWaterMark, 0); + r.destroy(); + + // Default highWaterMark should be 16KB + const r2 = toReadableSync(gen()); + assert.strictEqual(r2.readableHighWaterMark, 64 * 1024); + r2.destroy(); +} + +// ============================================================================= +// Round-trip: stream/iter -> Readable -> stream/iter +// ============================================================================= + +async function testRoundTrip() { + const original = 'round trip data test'; + + // stream/iter -> classic Readable + const source = from(original); + const readable = toReadable(source); + + // classic Readable -> stream/iter (via toAsyncStreamable) + const result = await text(from(readable)); + assert.strictEqual(result, original); +} + +// ============================================================================= +// Round-trip with compression +// ============================================================================= + +async function testRoundTripWithCompression() { + const { compressGzip, decompressGzip } = require('zlib/iter'); + + const original = 'compress through classic readable'; + + // Compress via stream/iter, bridge to classic Readable + const compressed = pull(from(original), compressGzip()); + const readable = toReadable(compressed); + + // Classic Readable back to stream/iter for decompression + const result = await text(pull(from(readable), decompressGzip())); + assert.strictEqual(result, original); +} + +// ============================================================================= +// Run all tests +// ============================================================================= + +testNotObjectMode(); +testNotObjectModeSync(); +testSyncRead(); +testEmptySync(); +testInvalidSourceAsync(); +testInvalidSourceSync(); +testOptionsValidationAsync(); +testOptionsValidationSync(); + +Promise.all([ + testBasicAsync(), + testMultiBatchAsync(), + testBackpressureAsync(), + testErrorAsync(), + testEmptyAsync(), + testEmptyBatchAsync(), + testDestroyAsync(), + testDestroyDuringBackpressure(), + testSignalAsync(), + testSignalAlreadyAborted(), + testLargeDataAsync(), + testPipeAsync(), + testWithTransformAsync(), + testBasicSync(), + testBackpressureSync(), + testBackpressureSyncMultiChunkBatch(), + testErrorSync(), + testDestroySync(), + testRoundTrip(), + testRoundTripWithCompression(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-transform-compat.js b/test/parallel/test-stream-iter-transform-compat.js new file mode 100644 index 00000000000000..b3180f41d98fa1 --- /dev/null +++ b/test/parallel/test-stream-iter-transform-compat.js @@ -0,0 +1,127 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); +const { promisify } = require('util'); +const { + from, + pull, + bytes, + text, +} = require('stream/iter'); +const { + compressGzip, + compressDeflate, + compressBrotli, + compressZstd, + decompressGzip, + decompressDeflate, + decompressBrotli, + decompressZstd, +} = require('zlib/iter'); + +// ============================================================================= +// Cross-compatibility: verify gzip/deflate output is compatible with zlib +// ============================================================================= + +async function testGzipCompatWithZlib() { + const gunzip = promisify(zlib.gunzip); + + const input = 'Cross-compat test with node:zlib. '.repeat(100); + const compressed = await bytes(pull(from(input), compressGzip())); + + // Decompress with standard zlib + const decompressed = await gunzip(compressed); + assert.strictEqual(decompressed.toString(), input); +} + +async function testDeflateCompatWithZlib() { + const inflate = promisify(zlib.inflate); + + const input = 'Cross-compat deflate test. '.repeat(100); + const compressed = await bytes(pull(from(input), compressDeflate())); + + // Decompress with standard zlib + const decompressed = await inflate(compressed); + assert.strictEqual(decompressed.toString(), input); +} + +async function testBrotliCompatWithZlib() { + const brotliDecompress = promisify(zlib.brotliDecompress); + + const input = 'Cross-compat brotli test. '.repeat(100); + const compressed = await bytes(pull(from(input), compressBrotli())); + + const decompressed = await brotliDecompress(compressed); + assert.strictEqual(decompressed.toString(), input); +} + +async function testZstdCompatWithZlib() { + const zstdDecompress = promisify(zlib.zstdDecompress); + + const input = 'Cross-compat zstd test. '.repeat(100); + const compressed = await bytes(pull(from(input), compressZstd())); + + const decompressed = await zstdDecompress(compressed); + assert.strictEqual(decompressed.toString(), input); +} + +// ============================================================================= +// Reverse compat: compress with zlib, decompress with new streams +// ============================================================================= + +async function testZlibGzipToNewStreams() { + const gzip = promisify(zlib.gzip); + + const input = 'Reverse compat gzip test. '.repeat(100); + const compressed = await gzip(input); + const result = await text(pull(from(compressed), decompressGzip())); + assert.strictEqual(result, input); +} + +async function testZlibDeflateToNewStreams() { + const deflate = promisify(zlib.deflate); + + const input = 'Reverse compat deflate test. '.repeat(100); + const compressed = await deflate(input); + const result = await text(pull(from(compressed), decompressDeflate())); + assert.strictEqual(result, input); +} + +async function testZlibBrotliToNewStreams() { + const brotliCompress = promisify(zlib.brotliCompress); + + const input = 'Reverse compat brotli test. '.repeat(100); + const compressed = await brotliCompress(input); + const result = await text(pull(from(compressed), decompressBrotli())); + assert.strictEqual(result, input); +} + +async function testZlibZstdToNewStreams() { + const zstdCompress = promisify(zlib.zstdCompress); + + const input = 'Reverse compat zstd test. '.repeat(100); + const compressed = await zstdCompress(input); + const result = await text(pull(from(compressed), decompressZstd())); + assert.strictEqual(result, input); +} + +// ============================================================================= +// Run all tests +// ============================================================================= + +(async () => { + // Cross-compat: new streams compress → zlib decompress + await testGzipCompatWithZlib(); + await testDeflateCompatWithZlib(); + await testBrotliCompatWithZlib(); + await testZstdCompatWithZlib(); + + // Reverse compat: zlib compress → new streams decompress + await testZlibGzipToNewStreams(); + await testZlibDeflateToNewStreams(); + await testZlibBrotliToNewStreams(); + await testZlibZstdToNewStreams(); +})().then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-transform-coverage.js b/test/parallel/test-stream-iter-transform-coverage.js new file mode 100644 index 00000000000000..d10766badd4dc6 --- /dev/null +++ b/test/parallel/test-stream-iter-transform-coverage.js @@ -0,0 +1,119 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Coverage tests for transform.js: abort cleanup, strategy/params options, +// early consumer exit triggering finally block. + +const common = require('../common'); +const assert = require('assert'); +const { + from, + pull, + bytes, + text, +} = require('stream/iter'); +const { + compressGzip, + decompressGzip, + compressDeflate, + decompressDeflate, + compressBrotli, + decompressBrotli, + compressZstd, + decompressZstd, +} = require('zlib/iter'); +const { constants } = require('zlib'); + +async function roundTrip(input, compress, decompress) { + return text(pull(from(input), compress, decompress)); +} + +// Abort mid-compression triggers finally cleanup +async function testAbortMidCompression() { + const ac = new AbortController(); + const largeInput = 'x'.repeat(100_000); + const compressed = pull(from(largeInput), compressGzip(), + { signal: ac.signal }); + const iter = compressed[Symbol.asyncIterator](); + + // Read one batch then abort + const first = await iter.next(); + assert.strictEqual(first.done, false); + ac.abort(); + await assert.rejects(iter.next(), { name: 'AbortError' }); +} + +// Early consumer exit (break from for-await) triggers finally +async function testEarlyConsumerExit() { + const largeInput = 'y'.repeat(100_000); + const compressed = pull(from(largeInput), compressGzip()); + + // eslint-disable-next-line no-unused-vars + for await (const batch of compressed) { + break; // Early exit — should trigger finally block cleanup + } + // If we get here without hanging or crashing, cleanup worked +} + +// Gzip with explicit strategy option +async function testGzipWithStrategy() { + const input = 'strategy test data '.repeat(100); + const c = compressGzip({ strategy: constants.Z_DEFAULT_STRATEGY }); + const result = await roundTrip(input, c, decompressGzip()); + assert.strictEqual(result, input); +} + +// Deflate with Z_FIXED strategy +async function testDeflateWithFixedStrategy() { + const input = 'fixed strategy '.repeat(100); + const c = compressDeflate({ strategy: constants.Z_FIXED }); + const result = await roundTrip(input, c, decompressDeflate()); + assert.strictEqual(result, input); +} + +// Brotli with custom quality param +async function testBrotliWithParams() { + const input = 'brotli params test '.repeat(100); + const params = { [constants.BROTLI_PARAM_QUALITY]: 5 }; + const result = await roundTrip(input, compressBrotli({ params }), + decompressBrotli()); + assert.strictEqual(result, input); +} + +// Zstd with custom compression level param +async function testZstdWithParams() { + const input = 'zstd params test '.repeat(100); + const params = { [constants.ZSTD_c_compressionLevel]: 10 }; + const result = await roundTrip(input, compressZstd({ params }), + decompressZstd()); + assert.strictEqual(result, input); +} + +// Gzip with custom chunkSize +async function testGzipWithChunkSize() { + const input = 'chunk size test'; + const c = compressGzip({ chunkSize: 256 }); + const d = decompressGzip({ chunkSize: 256 }); + const result = await roundTrip(input, c, d); + assert.strictEqual(result, input); +} + +// Invalid chunkSize throws when transform is invoked +async function testInvalidChunkSize() { + const tx = compressGzip({ chunkSize: 8 }); + await assert.rejects( + async () => await bytes(pull(from('data'), tx)), + { code: 'ERR_OUT_OF_RANGE' }, + ); +} + +Promise.all([ + testAbortMidCompression(), + testEarlyConsumerExit(), + testGzipWithStrategy(), + testDeflateWithFixedStrategy(), + testBrotliWithParams(), + testZstdWithParams(), + testGzipWithChunkSize(), + testInvalidChunkSize(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-transform-errors.js b/test/parallel/test-stream-iter-transform-errors.js new file mode 100644 index 00000000000000..62a3c178ac5f58 --- /dev/null +++ b/test/parallel/test-stream-iter-transform-errors.js @@ -0,0 +1,71 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + from, + pull, + bytes, +} = require('stream/iter'); +const { + decompressGzip, + decompressDeflate, + decompressBrotli, + decompressZstd, +} = require('zlib/iter'); + +// ============================================================================= +// Decompression of corrupt data +// ============================================================================= + +async function testCorruptGzipData() { + const corrupt = new Uint8Array([0x1F, 0x8B, 0xFF, 0xFF, 0xFF]); + + await assert.rejects( + async () => await bytes(pull(from(corrupt), decompressGzip())), { + name: 'Error', + code: 'Z_DATA_ERROR', + }); +} + +async function testCorruptDeflateData() { + const corrupt = new Uint8Array([0x78, 0xFF, 0xFF, 0xFF]); + + await assert.rejects( + async () => await bytes(pull(from(corrupt), decompressDeflate())), { + name: 'Error', + code: 'Z_DATA_ERROR', + }); +} + +async function testCorruptBrotliData() { + const corrupt = new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF]); + + await assert.rejects( + async () => await bytes(pull(from(corrupt), decompressBrotli())), { + name: 'Error', + code: 'ERR__ERROR_FORMAT_PADDING_2', + }); +} + +async function testCorruptZstdData() { + // Completely invalid data (not even valid magic bytes) + const corrupt = new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + await assert.rejects( + async () => await bytes(pull(from(corrupt), decompressZstd())), { + name: 'Error', + code: 'ZSTD_error_prefix_unknown', + }); +} + +// ============================================================================= +// Run all tests +// ============================================================================= + +(async () => { + await testCorruptGzipData(); + await testCorruptDeflateData(); + await testCorruptBrotliData(); + await testCorruptZstdData(); +})().then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-transform-output.js b/test/parallel/test-stream-iter-transform-output.js new file mode 100644 index 00000000000000..a37cf08dc9d53f --- /dev/null +++ b/test/parallel/test-stream-iter-transform-output.js @@ -0,0 +1,225 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Tests for transform output normalization edge cases: +// ArrayBuffer, ArrayBufferView, iterables, strings, invalid types. + +const common = require('../common'); +const assert = require('assert'); +const { + pull, + pullSync, + bytes, + bytesSync, + from, + fromSync, +} = require('stream/iter'); + +// Stateless transform returns ArrayBuffer (async) +async function testTransformReturnsArrayBuffer() { + const tx = (chunks) => { + if (chunks === null) return null; + const ab = new ArrayBuffer(chunks[0].length); + new Uint8Array(ab).set(chunks[0]); + return ab; + }; + const data = await bytes(pull(from('AB'), tx)); + assert.deepStrictEqual(data, new TextEncoder().encode('AB')); +} + +// Stateless transform returns ArrayBuffer (sync) +async function testSyncTransformReturnsArrayBuffer() { + const tx = (chunks) => { + if (chunks === null) return null; + const ab = new ArrayBuffer(chunks[0].length); + new Uint8Array(ab).set(chunks[0]); + return ab; + }; + const data = bytesSync(pullSync(fromSync('AB'), tx)); + assert.deepStrictEqual(data, new TextEncoder().encode('AB')); +} + +// Stateless transform returns Float32Array (non-Uint8Array ArrayBufferView) +async function testTransformReturnsFloat32Array() { + const tx = (chunks) => { + if (chunks === null) return null; + return new Float32Array([1.0]); + }; + const data = await bytes(pull(from('x'), tx)); + assert.strictEqual(data.byteLength, 4); // 1 float32 = 4 bytes +} + +// Stateless sync transform returns Float32Array +async function testSyncTransformReturnsFloat32Array() { + const tx = (chunks) => { + if (chunks === null) return null; + return new Float32Array([1.0]); + }; + const data = bytesSync(pullSync(fromSync('x'), tx)); + assert.strictEqual(data.byteLength, 4); +} + +// Stateless transform returns a sync generator (iterable) +async function testTransformReturnsGenerator() { + const tx = (chunks) => { + if (chunks === null) return null; + return (function*() { + yield new Uint8Array([65]); + yield new Uint8Array([66]); + })(); + }; + const data = await bytes(pull(from('x'), tx)); + assert.deepStrictEqual(data, new Uint8Array([65, 66])); +} + +// Stateless sync transform returns a generator +async function testSyncTransformReturnsGenerator() { + const tx = (chunks) => { + if (chunks === null) return null; + return (function*() { + yield new Uint8Array([67]); + yield new Uint8Array([68]); + })(); + }; + const data = bytesSync(pullSync(fromSync('x'), tx)); + assert.deepStrictEqual(data, new Uint8Array([67, 68])); +} + +// Stateless async transform returns an async generator +async function testTransformReturnsAsyncGenerator() { + const tx = (chunks) => { + if (chunks === null) return null; + return (async function*() { + yield new Uint8Array([69]); + yield new Uint8Array([70]); + })(); + }; + const data = await bytes(pull(from('x'), tx)); + assert.deepStrictEqual(data, new Uint8Array([69, 70])); +} + +// Stateful async transform yields string +async function testStatefulTransformYieldsString() { + const tx = { + async *transform(source) { + for await (const chunks of source) { + if (chunks === null) return; + yield 'hello'; + } + }, + }; + const data = await bytes(pull(from('x'), tx)); + assert.deepStrictEqual(data, new TextEncoder().encode('hello')); +} + +// Stateful async transform yields ArrayBuffer +async function testStatefulTransformYieldsArrayBuffer() { + const tx = { + async *transform(source) { + for await (const chunks of source) { + if (chunks === null) return; + const ab = new ArrayBuffer(2); + new Uint8Array(ab).set([71, 72]); + yield ab; + } + }, + }; + const data = await bytes(pull(from('x'), tx)); + assert.deepStrictEqual(data, new Uint8Array([71, 72])); +} + +// Stateful sync transform yields string +async function testStatefulSyncTransformYieldsString() { + const tx = { + *transform(source) { + for (const chunks of source) { + if (chunks === null) return; + yield 'world'; + } + }, + }; + const data = bytesSync(pullSync(fromSync('x'), tx)); + assert.deepStrictEqual(data, new TextEncoder().encode('world')); +} + +// Flush returns single Uint8Array (not batch) +async function testFlushReturnsSingleUint8Array() { + const tx = (chunks) => { + if (chunks === null) return new Uint8Array([99]); // Flush returns single + return chunks; + }; + const data = await bytes(pull(from('x'), tx)); + // Should contain both the original data and the flush byte + assert.ok(data.includes(99)); +} + +// Flush returns string +async function testFlushReturnsString() { + const tx = (chunks) => { + if (chunks === null) return 'trailer'; + return chunks; + }; + const data = await bytes(pull(from('x'), tx)); + const trailer = new TextEncoder().encode('trailer'); + // Last bytes should be the trailer + const tail = data.slice(data.length - trailer.length); + assert.deepStrictEqual(tail, trailer); +} + +// Sync flush returns single Uint8Array +async function testSyncFlushReturnsSingleUint8Array() { + const tx = (chunks) => { + if (chunks === null) return new Uint8Array([88]); + return chunks; + }; + const data = bytesSync(pullSync(fromSync('x'), tx)); + assert.ok(data.includes(88)); +} + +// Transform returns invalid type → ERR_INVALID_ARG_TYPE +async function testTransformReturnsInvalidType() { + const tx = (chunks) => { + if (chunks === null) return null; + return 42; // Invalid + }; + await assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars + for await (const batch of pull(from('x'), tx)) { /* consume */ } + }, + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +// Sync transform returns invalid type → ERR_INVALID_ARG_TYPE +async function testSyncTransformReturnsInvalidType() { + const tx = (chunks) => { + if (chunks === null) return null; + return 42; + }; + assert.throws( + () => { + // eslint-disable-next-line no-unused-vars + for (const batch of pullSync(fromSync('x'), tx)) { /* consume */ } + }, + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +Promise.all([ + testTransformReturnsArrayBuffer(), + testSyncTransformReturnsArrayBuffer(), + testTransformReturnsFloat32Array(), + testSyncTransformReturnsFloat32Array(), + testTransformReturnsGenerator(), + testSyncTransformReturnsGenerator(), + testTransformReturnsAsyncGenerator(), + testStatefulTransformYieldsString(), + testStatefulTransformYieldsArrayBuffer(), + testStatefulSyncTransformYieldsString(), + testFlushReturnsSingleUint8Array(), + testFlushReturnsString(), + testSyncFlushReturnsSingleUint8Array(), + testTransformReturnsInvalidType(), + testSyncTransformReturnsInvalidType(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-transform-roundtrip.js b/test/parallel/test-stream-iter-transform-roundtrip.js new file mode 100644 index 00000000000000..df63483d6c8535 --- /dev/null +++ b/test/parallel/test-stream-iter-transform-roundtrip.js @@ -0,0 +1,291 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + from, + pull, + bytes, + text, +} = require('stream/iter'); +const { + compressGzip, + compressDeflate, + compressBrotli, + compressZstd, + decompressGzip, + decompressDeflate, + decompressBrotli, + decompressZstd, +} = require('zlib/iter'); + +// ============================================================================= +// Helper: compress then decompress, verify round-trip equality +// ============================================================================= + +async function roundTrip(input, compress, decompress) { + return text(pull(pull(from(input), compress), decompress)); +} + +async function roundTripBytes(inputBuf, compress, decompress) { + return bytes(pull(pull(from(inputBuf), compress), decompress)); +} + +// ============================================================================= +// Gzip round-trip tests +// ============================================================================= + +async function testGzipRoundTrip() { + const input = 'Hello, gzip compression!'; + const result = await roundTrip(input, compressGzip(), decompressGzip()); + assert.strictEqual(result, input); +} + +async function testGzipLargeData() { + // 100KB of repeated text - exercises multi-chunk path + const input = 'gzip large data test. '.repeat(5000); + const result = await roundTrip(input, compressGzip(), decompressGzip()); + assert.strictEqual(result, input); +} + +async function testGzipActuallyCompresses() { + const input = 'Repeated data compresses well. '.repeat(1000); + const inputBuf = Buffer.from(input); + const source = from(inputBuf); + const compressed = await bytes(pull(source, compressGzip())); + assert.ok(compressed.byteLength < inputBuf.byteLength, + `Compressed ${compressed.byteLength} should be < original ${inputBuf.byteLength}`); +} + +// ============================================================================= +// Deflate round-trip tests +// ============================================================================= + +async function testDeflateRoundTrip() { + const input = 'Hello, deflate compression!'; + const result = await roundTrip(input, compressDeflate(), decompressDeflate()); + assert.strictEqual(result, input); +} + +async function testDeflateLargeData() { + const input = 'deflate large data test. '.repeat(5000); + const result = await roundTrip(input, compressDeflate(), decompressDeflate()); + assert.strictEqual(result, input); +} + +async function testDeflateActuallyCompresses() { + const input = 'Repeated data compresses well. '.repeat(1000); + const inputBuf = Buffer.from(input); + const source = from(inputBuf); + const compressed = await bytes(pull(source, compressDeflate())); + assert.ok(compressed.byteLength < inputBuf.byteLength, + `Compressed ${compressed.byteLength} should be < original ${inputBuf.byteLength}`); +} + +// ============================================================================= +// Brotli round-trip tests +// ============================================================================= + +async function testBrotliRoundTrip() { + const input = 'Hello, brotli compression!'; + const result = await roundTrip(input, compressBrotli(), decompressBrotli()); + assert.strictEqual(result, input); +} + +async function testBrotliLargeData() { + const input = 'brotli large data test. '.repeat(5000); + const result = await roundTrip(input, compressBrotli(), decompressBrotli()); + assert.strictEqual(result, input); +} + +async function testBrotliActuallyCompresses() { + const input = 'Repeated data compresses well. '.repeat(1000); + const inputBuf = Buffer.from(input); + const compressed = await bytes(pull(from(inputBuf), compressBrotli())); + assert.ok(compressed.byteLength < inputBuf.byteLength, + `Compressed ${compressed.byteLength} should be < original ${inputBuf.byteLength}`); +} + +// ============================================================================= +// Zstd round-trip tests +// ============================================================================= + +async function testZstdRoundTrip() { + const input = 'Hello, zstd compression!'; + const result = await roundTrip(input, compressZstd(), decompressZstd()); + assert.strictEqual(result, input); +} + +async function testZstdLargeData() { + const input = 'zstd large data test. '.repeat(5000); + const result = await roundTrip(input, compressZstd(), decompressZstd()); + assert.strictEqual(result, input); +} + +async function testZstdActuallyCompresses() { + const input = 'Repeated data compresses well. '.repeat(1000); + const inputBuf = Buffer.from(input); + const compressed = await bytes(pull(from(inputBuf), compressZstd())); + assert.ok(compressed.byteLength < inputBuf.byteLength, + `Compressed ${compressed.byteLength} should be < original ${inputBuf.byteLength}`); +} + +// ============================================================================= +// Binary data round-trip - verify no corruption on non-text data +// ============================================================================= + +// Create a buffer with a repeating byte pattern covering all 256 values. +function makeBinaryTestData(size = 1024) { + const buf = Buffer.alloc(size); + for (let i = 0; i < size; i++) buf[i] = i & 0xFF; + return buf; +} + +async function testBinaryRoundTripGzip() { + const input = makeBinaryTestData(); + const result = await roundTripBytes(input, compressGzip(), decompressGzip()); + assert.strictEqual(result.byteLength, input.byteLength); + assert.deepStrictEqual(Buffer.from(result), input); +} + +async function testBinaryRoundTripDeflate() { + const input = makeBinaryTestData(); + const result = await roundTripBytes(input, compressDeflate(), + decompressDeflate()); + assert.strictEqual(result.byteLength, input.byteLength); + assert.deepStrictEqual(Buffer.from(result), input); +} + +async function testBinaryRoundTripBrotli() { + const input = makeBinaryTestData(); + const result = await roundTripBytes(input, compressBrotli(), + decompressBrotli()); + assert.strictEqual(result.byteLength, input.byteLength); + assert.deepStrictEqual(Buffer.from(result), input); +} + +async function testBinaryRoundTripZstd() { + const input = makeBinaryTestData(); + const result = await roundTripBytes(input, compressZstd(), decompressZstd()); + assert.strictEqual(result.byteLength, input.byteLength); + assert.deepStrictEqual(Buffer.from(result), input); +} + +// ============================================================================= +// Empty input +// ============================================================================= + +async function testEmptyInputGzip() { + const result = await roundTrip('', compressGzip(), decompressGzip()); + assert.strictEqual(result, ''); +} + +async function testEmptyInputDeflate() { + const result = await roundTrip('', compressDeflate(), decompressDeflate()); + assert.strictEqual(result, ''); +} + +async function testEmptyInputBrotli() { + const result = await roundTrip('', compressBrotli(), decompressBrotli()); + assert.strictEqual(result, ''); +} + +async function testEmptyInputZstd() { + const result = await roundTrip('', compressZstd(), decompressZstd()); + assert.strictEqual(result, ''); +} + +// ============================================================================= +// Chained transforms - compress with one, then another, decompress in reverse +// ============================================================================= + +async function testChainedGzipDeflate() { + const input = 'Double compression test data. '.repeat(100); + // Compress: gzip then deflate + const compressed = pull(pull(from(input), compressGzip()), compressDeflate()); + // Decompress: deflate then gzip (reverse order) + const decompressed = pull(pull(compressed, decompressDeflate()), + decompressGzip()); + const result = await text(decompressed); + assert.strictEqual(result, input); +} + +// ============================================================================= +// Transform protocol: verify each factory returns a proper transform object +// ============================================================================= + +function testTransformProtocol() { + [ + compressGzip, compressDeflate, compressBrotli, compressZstd, + decompressGzip, decompressDeflate, decompressBrotli, decompressZstd, + ].forEach((factory) => { + const t = factory(); + assert.strictEqual(typeof t.transform, 'function', + `${factory.name}() should have a transform function`); + }); +} + +// ============================================================================= +// Compression with options +// ============================================================================= + +async function testGzipWithLevel() { + const data = 'a'.repeat(10000); + const level1 = await bytes(pull(from(data), compressGzip({ level: 1 }))); + const level9 = await bytes(pull(from(data), compressGzip({ level: 9 }))); + // Higher compression level should produce smaller output + assert.ok(level9.length <= level1.length); + // Both should decompress to original + const dec1 = await text(pull(from(level1), decompressGzip())); + const dec9 = await text(pull(from(level9), decompressGzip())); + assert.strictEqual(dec1, data); + assert.strictEqual(dec9, data); +} + +// ============================================================================= +// Run all tests +// ============================================================================= + +(async () => { + // Gzip + await testGzipRoundTrip(); + await testGzipLargeData(); + await testGzipActuallyCompresses(); + + // Deflate + await testDeflateRoundTrip(); + await testDeflateLargeData(); + await testDeflateActuallyCompresses(); + + // Brotli + await testBrotliRoundTrip(); + await testBrotliLargeData(); + await testBrotliActuallyCompresses(); + + // Zstd + await testZstdRoundTrip(); + await testZstdLargeData(); + await testZstdActuallyCompresses(); + + // Binary data + await testBinaryRoundTripGzip(); + await testBinaryRoundTripDeflate(); + await testBinaryRoundTripBrotli(); + await testBinaryRoundTripZstd(); + + // Empty input + await testEmptyInputGzip(); + await testEmptyInputDeflate(); + await testEmptyInputBrotli(); + await testEmptyInputZstd(); + + // Chained + await testChainedGzipDeflate(); + + // Protocol + testTransformProtocol(); + + // Compression with options + await testGzipWithLevel(); +})().then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-transform-sync.js b/test/parallel/test-stream-iter-transform-sync.js new file mode 100644 index 00000000000000..d674c26cca100a --- /dev/null +++ b/test/parallel/test-stream-iter-transform-sync.js @@ -0,0 +1,227 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + fromSync, + pullSync, + bytesSync, + textSync, +} = require('stream/iter'); +const { + compressGzipSync, + compressDeflateSync, + compressBrotliSync, + compressZstdSync, + decompressGzipSync, + decompressDeflateSync, + decompressBrotliSync, + decompressZstdSync, +} = require('zlib/iter'); + +// ============================================================================= +// Helper: sync compress then decompress, verify round-trip equality +// ============================================================================= + +function roundTrip(input, compress, decompress) { + return textSync(pullSync(pullSync(fromSync(input), compress), decompress)); +} + +function roundTripBytes(inputBuf, compress, decompress) { + return bytesSync(pullSync(pullSync(fromSync(inputBuf), compress), decompress)); +} + +// ============================================================================= +// Gzip sync round-trip tests +// ============================================================================= + +function testGzipRoundTrip() { + const input = 'Hello, sync gzip compression!'; + const result = roundTrip(input, compressGzipSync(), decompressGzipSync()); + assert.strictEqual(result, input); +} + +function testGzipLargeData() { + const input = 'gzip sync large data test. '.repeat(5000); + const result = roundTrip(input, compressGzipSync(), decompressGzipSync()); + assert.strictEqual(result, input); +} + +function testGzipActuallyCompresses() { + const input = 'Repeated data compresses well. '.repeat(1000); + const inputBuf = Buffer.from(input); + const compressed = bytesSync(pullSync(fromSync(inputBuf), + compressGzipSync())); + assert.ok(compressed.byteLength < inputBuf.byteLength, + `Compressed ${compressed.byteLength} should be < ` + + `original ${inputBuf.byteLength}`); +} + +function testGzipBinaryData() { + const inputBuf = Buffer.alloc(10000); + for (let i = 0; i < inputBuf.length; i++) inputBuf[i] = i & 0xff; + const result = roundTripBytes(inputBuf, compressGzipSync(), + decompressGzipSync()); + assert.deepStrictEqual(result, inputBuf); +} + +// ============================================================================= +// Deflate sync round-trip tests +// ============================================================================= + +function testDeflateRoundTrip() { + const input = 'Hello, sync deflate compression!'; + const result = roundTrip(input, compressDeflateSync(), + decompressDeflateSync()); + assert.strictEqual(result, input); +} + +function testDeflateLargeData() { + const input = 'deflate sync large data test. '.repeat(5000); + const result = roundTrip(input, compressDeflateSync(), + decompressDeflateSync()); + assert.strictEqual(result, input); +} + +// ============================================================================= +// Brotli sync round-trip tests +// ============================================================================= + +function testBrotliRoundTrip() { + const input = 'Hello, sync brotli compression!'; + const result = roundTrip(input, compressBrotliSync(), + decompressBrotliSync()); + assert.strictEqual(result, input); +} + +function testBrotliLargeData() { + const input = 'brotli sync large data test. '.repeat(5000); + const result = roundTrip(input, compressBrotliSync(), + decompressBrotliSync()); + assert.strictEqual(result, input); +} + +// ============================================================================= +// Zstd sync round-trip tests +// ============================================================================= + +function testZstdRoundTrip() { + const input = 'Hello, sync zstd compression!'; + const result = roundTrip(input, compressZstdSync(), decompressZstdSync()); + assert.strictEqual(result, input); +} + +function testZstdLargeData() { + const input = 'zstd sync large data test. '.repeat(5000); + const result = roundTrip(input, compressZstdSync(), decompressZstdSync()); + assert.strictEqual(result, input); +} + +// ============================================================================= +// Cross-algorithm: compress async-compatible, decompress sync (and vice versa) +// The sync transforms should produce output compatible with the standard format +// ============================================================================= + +function testGzipWithOptions() { + const input = 'options test data '.repeat(100); + const result = roundTrip(input, + compressGzipSync({ level: 1 }), + decompressGzipSync()); + assert.strictEqual(result, input); +} + +function testBrotliWithOptions() { + const zlib = require('zlib'); + const input = 'brotli options test data '.repeat(100); + const result = roundTrip(input, + compressBrotliSync({ + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: 3, + }, + }), + decompressBrotliSync()); + assert.strictEqual(result, input); +} + +// ============================================================================= +// Stateless + stateful sync transform pipeline +// ============================================================================= + +function testMixedStatelessAndStateful() { + // Uppercase stateless transform + gzip stateful transform + const upper = (chunks) => { + if (chunks === null) return null; + const out = new Array(chunks.length); + for (let j = 0; j < chunks.length; j++) { + const src = chunks[j]; + const buf = Buffer.allocUnsafe(src.length); + for (let i = 0; i < src.length; i++) { + const b = src[i]; + buf[i] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b; + } + out[j] = buf; + } + return out; + }; + + const input = 'hello world '.repeat(100); + const result = textSync( + pullSync( + pullSync(fromSync(input), upper, compressGzipSync()), + decompressGzipSync(), + ), + ); + assert.strictEqual(result, input.toUpperCase()); +} + +// ============================================================================= +// Early consumer exit (break from for-of) triggers cleanup +// ============================================================================= + +function testEarlyExit() { + const input = 'y'.repeat(100_000); + const compressed = pullSync(fromSync(input), compressGzipSync()); + + // eslint-disable-next-line no-unused-vars + for (const batch of compressed) { + break; // Early exit - should trigger finally block cleanup + } + // If we get here without crashing, cleanup worked +} + +// ============================================================================= +// Empty input +// ============================================================================= + +function testEmptyInput() { + const result = textSync( + pullSync( + pullSync(fromSync(''), compressGzipSync()), + decompressGzipSync(), + ), + ); + assert.strictEqual(result, ''); +} + +// ============================================================================= +// Run all tests +// ============================================================================= + +testGzipRoundTrip(); +testGzipLargeData(); +testGzipActuallyCompresses(); +testGzipBinaryData(); +testDeflateRoundTrip(); +testDeflateLargeData(); +testBrotliRoundTrip(); +testBrotliLargeData(); +testZstdRoundTrip(); +testZstdLargeData(); +testGzipWithOptions(); +testBrotliWithOptions(); +testMixedStatelessAndStateful(); +testEarlyExit(); +testEmptyInput(); + +common.mustCall()(); diff --git a/test/parallel/test-stream-iter-validation.js b/test/parallel/test-stream-iter-validation.js new file mode 100644 index 00000000000000..ecefaeb171ec41 --- /dev/null +++ b/test/parallel/test-stream-iter-validation.js @@ -0,0 +1,356 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + from, fromSync, pull, pullSync, pipeTo, + push, duplex, broadcast, Broadcast, share, shareSync, + Share, SyncShare, + bytes, bytesSync, text, textSync, + arrayBuffer, arrayBufferSync, array, arraySync, + tap, tapSync, +} = require('stream/iter'); +const { + compressGzip, compressBrotli, compressZstd, + decompressGzip, decompressBrotli, decompressZstd, +} = require('zlib/iter'); + +// ============================================================================= +// push() validation +// ============================================================================= + +// HighWaterMark must be integer >= 1 +assert.throws(() => push({ highWaterMark: 'bad' }), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => push({ highWaterMark: 1.5 }), { code: 'ERR_OUT_OF_RANGE' }); +// Values < 1 are clamped to 1 +assert.strictEqual(push({ highWaterMark: 0 }).writer.desiredSize, 1); +assert.strictEqual(push({ highWaterMark: -1 }).writer.desiredSize, 1); +assert.strictEqual(push({ highWaterMark: -100 }).writer.desiredSize, 1); +// MAX_SAFE_INTEGER is accepted +assert.strictEqual(push({ highWaterMark: Number.MAX_SAFE_INTEGER }).writer.desiredSize, + Number.MAX_SAFE_INTEGER); +// Values above MAX_SAFE_INTEGER are rejected by validateInteger +assert.throws(() => push({ highWaterMark: Number.MAX_SAFE_INTEGER + 1 }), + { code: 'ERR_OUT_OF_RANGE' }); + +// Signal must be AbortSignal +assert.throws(() => push({ signal: 'bad' }), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => push({ signal: {} }), { code: 'ERR_INVALID_ARG_TYPE' }); + +// Transforms must be functions or transform objects +assert.throws(() => push(42, {}), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => push('bad', {}), { code: 'ERR_INVALID_ARG_TYPE' }); + +// Writer.writev requires array +{ + const { writer } = push(); + assert.throws(() => writer.writev('bad'), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => writer.writev(42), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => writer.writevSync('bad'), { code: 'ERR_INVALID_ARG_TYPE' }); + writer.endSync(); +} + +// Writer.write rejects non-string/non-Uint8Array +{ + const { writer } = push(); + assert.throws(() => writer.writeSync(42), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => writer.writeSync({}), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => writer.writeSync(true), { code: 'ERR_INVALID_ARG_TYPE' }); + writer.endSync(); +} + +// ============================================================================= +// duplex() validation +// ============================================================================= + +assert.throws(() => duplex(42), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => duplex('bad'), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => duplex({ a: 42 }), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => duplex({ b: 'bad' }), { code: 'ERR_INVALID_ARG_TYPE' }); + +// highWaterMark validation (cascades through to push()) +assert.throws(() => duplex({ highWaterMark: 'bad' }), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => duplex({ highWaterMark: 1.5 }), { code: 'ERR_OUT_OF_RANGE' }); +assert.throws(() => duplex({ highWaterMark: Number.MAX_SAFE_INTEGER + 1 }), + { code: 'ERR_OUT_OF_RANGE' }); + +// Values < 1 are clamped to 1 (both directions) +{ + const [a, b] = duplex({ highWaterMark: 0 }); + assert.strictEqual(a.writer.desiredSize, 1); + assert.strictEqual(b.writer.desiredSize, 1); + a.close(); + b.close(); +} +// MAX_SAFE_INTEGER is accepted +{ + const [a, b] = duplex({ highWaterMark: Number.MAX_SAFE_INTEGER }); + assert.strictEqual(a.writer.desiredSize, Number.MAX_SAFE_INTEGER); + assert.strictEqual(b.writer.desiredSize, Number.MAX_SAFE_INTEGER); + a.close(); + b.close(); +} +// Per-direction overrides +{ + const [a, b] = duplex({ a: { highWaterMark: 0 }, b: { highWaterMark: 5 } }); + assert.strictEqual(a.writer.desiredSize, 1); // clamped + assert.strictEqual(b.writer.desiredSize, 5); + a.close(); + b.close(); +} + +assert.throws(() => duplex({ signal: {} }), { code: 'ERR_INVALID_ARG_TYPE' }); + +// ============================================================================= +// pull() / pullSync() validation +// ============================================================================= + +// Signal must be AbortSignal +assert.throws(() => pull(from('a'), { signal: 'bad' }), { code: 'ERR_INVALID_ARG_TYPE' }); + +// Transforms must be functions or transform objects +assert.throws(() => pull(from('a'), 42), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => pull(from('a'), 'bad'), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => pullSync(fromSync('a'), 42), { code: 'ERR_INVALID_ARG_TYPE' }); + +// ============================================================================= +// broadcast() validation +// ============================================================================= + +assert.throws(() => broadcast({ highWaterMark: 'bad' }), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => broadcast({ highWaterMark: 1.5 }), { code: 'ERR_OUT_OF_RANGE' }); +assert.throws(() => broadcast({ highWaterMark: Number.MAX_SAFE_INTEGER + 1 }), + { code: 'ERR_OUT_OF_RANGE' }); + +// Values < 1 are clamped to 1 (need a consumer for desiredSize to work) +{ + const bc = broadcast({ highWaterMark: 0 }); + bc.broadcast.push(); + assert.strictEqual(bc.writer.desiredSize, 1); + bc.writer.endSync(); +} +{ + const bc = broadcast({ highWaterMark: -1 }); + bc.broadcast.push(); + assert.strictEqual(bc.writer.desiredSize, 1); + bc.writer.endSync(); +} +// MAX_SAFE_INTEGER is accepted +{ + const bc = broadcast({ highWaterMark: Number.MAX_SAFE_INTEGER }); + bc.broadcast.push(); + assert.strictEqual(bc.writer.desiredSize, Number.MAX_SAFE_INTEGER); + bc.writer.endSync(); +} + +assert.throws(() => broadcast({ signal: {} }), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => broadcast({ backpressure: 'bad' }), { code: 'ERR_INVALID_ARG_VALUE' }); + +// BroadcastWriter.writev requires array +{ + const { writer } = broadcast(); + assert.throws(() => writer.writev('bad'), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => writer.writev(42), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => writer.writevSync('bad'), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => writer.writevSync(42), { code: 'ERR_INVALID_ARG_TYPE' }); + writer.endSync(); +} + +// Broadcast.from rejects non-streamable input +assert.throws(() => Broadcast.from(42), { code: 'ERR_INVALID_ARG_TYPE' }); + +// ============================================================================= +// share() / shareSync() validation +// ============================================================================= + +assert.throws(() => share(42), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => share(from('a'), { highWaterMark: 'bad' }), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => share(from('a'), { highWaterMark: 1.5 }), { code: 'ERR_OUT_OF_RANGE' }); +assert.throws(() => share(from('a'), { highWaterMark: Number.MAX_SAFE_INTEGER + 1 }), + { code: 'ERR_OUT_OF_RANGE' }); +assert.throws(() => share(from('a'), { signal: {} }), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => share(from('a'), { backpressure: 'bad' }), { code: 'ERR_INVALID_ARG_VALUE' }); + +// share() values < 1 are clamped (no desiredSize, but accepts the value) +share(from('a'), { highWaterMark: 0 }).cancel(); +share(from('a'), { highWaterMark: -1 }).cancel(); +share(from('a'), { highWaterMark: Number.MAX_SAFE_INTEGER }).cancel(); + +assert.throws(() => shareSync(42), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => shareSync(fromSync('a'), { highWaterMark: 'bad' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => shareSync(fromSync('a'), { highWaterMark: 1.5 }), + { code: 'ERR_OUT_OF_RANGE' }); +assert.throws(() => shareSync(fromSync('a'), { highWaterMark: Number.MAX_SAFE_INTEGER + 1 }), + { code: 'ERR_OUT_OF_RANGE' }); + +// shareSync() values < 1 are clamped (accepts the value) +shareSync(fromSync('a'), { highWaterMark: 0 }).cancel(); +shareSync(fromSync('a'), { highWaterMark: -1 }).cancel(); +shareSync(fromSync('a'), { highWaterMark: Number.MAX_SAFE_INTEGER }).cancel(); + +// Share.from / SyncShare.fromSync reject non-iterable +assert.throws(() => Share.from(42), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => SyncShare.fromSync(42), { code: 'ERR_INVALID_ARG_TYPE' }); + +// ============================================================================= +// Consumer validation (synchronous) +// ============================================================================= + +// tap / tapSync require function +assert.throws(() => tap(42), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => tap('bad'), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => tapSync(42), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => tapSync(null), { code: 'ERR_INVALID_ARG_TYPE' }); + +// Sync consumer options +assert.throws(() => bytesSync(fromSync('a'), { limit: 'bad' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => bytesSync(fromSync('a'), { limit: -1 }), + { code: 'ERR_OUT_OF_RANGE' }); +assert.throws(() => textSync(fromSync('a'), { encoding: 42 }), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => textSync(fromSync('a'), { encoding: 'bogus' }), + { code: 'ERR_INVALID_ARG_VALUE' }); +assert.throws(() => arrayBufferSync(fromSync('a'), { limit: 'bad' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => arraySync(fromSync('a'), { limit: -1 }), + { code: 'ERR_OUT_OF_RANGE' }); + +// Options must be object if provided +assert.throws(() => bytesSync(fromSync('a'), 42), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => textSync(fromSync('a'), 'bad'), { code: 'ERR_INVALID_ARG_TYPE' }); + +// Compression options must be object +assert.throws(() => compressGzip(42), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => decompressGzip('bad'), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => compressBrotli(42), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => decompressBrotli('bad'), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => compressZstd(42), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => decompressZstd('bad'), { code: 'ERR_INVALID_ARG_TYPE' }); + +// ============================================================================= +// Async consumer and compression validation +// ============================================================================= + +// Helper: consume a transform through a pipeline to trigger lazy validation. +const consume = (transform) => bytes(pull(from('test'), transform)); + +async function testAsyncValidation() { + // pipeTo signal + await assert.rejects( + () => pipeTo(from('a'), { write() {} }, { signal: 'bad' }), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + + // Async consumer options + await assert.rejects( + () => bytes(from('a'), 42), { code: 'ERR_INVALID_ARG_TYPE' }); + await assert.rejects( + () => bytes(from('a'), { signal: 'bad' }), { code: 'ERR_INVALID_ARG_TYPE' }); + await assert.rejects( + () => bytes(from('a'), { limit: 'bad' }), { code: 'ERR_INVALID_ARG_TYPE' }); + await assert.rejects( + () => bytes(from('a'), { limit: -1 }), { code: 'ERR_OUT_OF_RANGE' }); + await assert.rejects( + () => text(from('a'), { encoding: 42 }), { code: 'ERR_INVALID_ARG_TYPE' }); + await assert.rejects( + () => text(from('a'), { encoding: 'not-a-real-encoding' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + await assert.rejects( + () => arrayBuffer(from('a'), { limit: 'bad' }), + { code: 'ERR_INVALID_ARG_TYPE' }); + await assert.rejects( + () => array(from('a'), { limit: -1 }), { code: 'ERR_OUT_OF_RANGE' }); + + const TYPE = { code: 'ERR_INVALID_ARG_TYPE' }; + const RANGE = { code: 'ERR_OUT_OF_RANGE' }; + const BROTLI = { code: 'ERR_BROTLI_INVALID_PARAM' }; + const ZSTD = { code: 'ERR_ZSTD_INVALID_PARAM' }; + + // ChunkSize + await assert.rejects(consume(compressGzip({ chunkSize: 'bad' })), TYPE); + await assert.rejects(consume(compressGzip({ chunkSize: 0 })), RANGE); + await assert.rejects(consume(compressGzip({ chunkSize: 10 })), RANGE); + + // WindowBits + await assert.rejects(consume(compressGzip({ windowBits: 'bad' })), TYPE); + await assert.rejects(consume(compressGzip({ windowBits: 100 })), RANGE); + + // Level + await assert.rejects(consume(compressGzip({ level: 'bad' })), TYPE); + await assert.rejects(consume(compressGzip({ level: 100 })), RANGE); + + // MemLevel + await assert.rejects(consume(compressGzip({ memLevel: 'bad' })), TYPE); + await assert.rejects(consume(compressGzip({ memLevel: 100 })), RANGE); + + // Strategy + await assert.rejects(consume(compressGzip({ strategy: 'bad' })), TYPE); + await assert.rejects(consume(compressGzip({ strategy: 100 })), RANGE); + + // Dictionary + await assert.rejects(consume(compressGzip({ dictionary: 42 })), TYPE); + await assert.rejects(consume(compressGzip({ dictionary: 'bad' })), TYPE); + + // Brotli params + await assert.rejects(consume(compressBrotli({ params: 42 })), TYPE); + await assert.rejects(consume(compressBrotli({ params: { bad: 1 } })), BROTLI); + await assert.rejects(consume(compressBrotli({ params: { [-1]: 1 } })), BROTLI); + await assert.rejects(consume(compressBrotli({ params: { 0: 'bad' } })), TYPE); + + // Zstd params + await assert.rejects(consume(compressZstd({ params: 42 })), TYPE); + await assert.rejects(consume(compressZstd({ params: { bad: 1 } })), ZSTD); + await assert.rejects(consume(compressZstd({ params: { 0: 'bad' } })), TYPE); + + // Zstd pledgedSrcSize + await assert.rejects(consume(compressZstd({ pledgedSrcSize: 'bad' })), TYPE); + await assert.rejects(consume(compressZstd({ pledgedSrcSize: -1 })), RANGE); +} + +// ============================================================================= +// Valid calls still work +// ============================================================================= + +// Push with valid options +{ + const { writer } = push({ highWaterMark: 2 }); + writer.writeSync('hello'); + writer.endSync(); +} + +// Duplex with valid options +{ + const [a, b] = duplex({ highWaterMark: 2 }); + a.close(); + b.close(); +} + +// Broadcast with valid options +{ + const { writer } = broadcast({ highWaterMark: 4 }); + writer.endSync(); +} + +// Share with valid options +{ + const shared = share(from('hello'), { highWaterMark: 4 }); + shared.cancel(); +} + +// Compression with valid options +{ + const transform = compressGzip({ chunkSize: 1024, level: 6 }); + assert.strictEqual(typeof transform.transform, 'function'); +} + +// Brotli with valid params +{ + const { constants: { BROTLI_PARAM_QUALITY } } = require('zlib'); + const transform = compressBrotli({ params: { [BROTLI_PARAM_QUALITY]: 5 } }); + assert.strictEqual(typeof transform.transform, 'function'); +} + +testAsyncValidation().then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-writable-from.js b/test/parallel/test-stream-iter-writable-from.js new file mode 100644 index 00000000000000..529c88ad3662f5 --- /dev/null +++ b/test/parallel/test-stream-iter-writable-from.js @@ -0,0 +1,643 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Tests for toWritable() - creating a classic stream.Writable +// backed by a stream/iter Writer. + +const common = require('../common'); +const assert = require('assert'); +const { + push, + text, + toWritable, +} = require('stream/iter'); + +// ============================================================================= +// Basic: write through fromStreamIter writable, read from readable +// ============================================================================= + +async function testBasicWrite() { + const { writer, readable } = push({ backpressure: 'block' }); + const writable = toWritable(writer); + + writable.write('hello'); + writable.write(' world'); + writable.end(); + + const result = await text(readable); + assert.strictEqual(result, 'hello world'); +} + +// ============================================================================= +// _write delegates to writer.write() +// ============================================================================= + +async function testWriteDelegatesToWriter() { + const chunks = []; + // Create a minimal Writer that records writes. + const writer = { + write(chunk) { + chunks.push(Buffer.from(chunk)); + return Promise.resolve(); + }, + end() { return Promise.resolve(0); }, + fail() {}, + }; + + const writable = toWritable(writer); + + await new Promise((resolve, reject) => { + writable.write('hello', (err) => { + if (err) reject(err); + else resolve(); + }); + }); + + assert.strictEqual(Buffer.concat(chunks).toString(), 'hello'); +} + +// ============================================================================= +// _writev delegates to writer.writev() when available +// ============================================================================= + +async function testWritevDelegation() { + const batches = []; + const writer = { + write(chunk) { + return Promise.resolve(); + }, + writev(chunks) { + batches.push(chunks.map((c) => Buffer.from(c))); + return Promise.resolve(); + }, + writevSync(chunks) { + return false; + }, + end() { return Promise.resolve(0); }, + fail() {}, + }; + + const writable = toWritable(writer); + + // Cork to batch writes, then uncork to trigger _writev + writable.cork(); + writable.write('a'); + writable.write('b'); + writable.write('c'); + writable.uncork(); + + await new Promise((resolve) => writable.end(resolve)); + + // Writev should have been called with the batched chunks + assert.ok(batches.length > 0, 'writev should have been called'); +} + +// ============================================================================= +// _writev not defined when writer lacks writev +// ============================================================================= + +function testNoWritevWithoutWriterWritev() { + const writer = { + write(chunk) { return Promise.resolve(); }, + }; + + const writable = toWritable(writer); + // The _writev should be null (Writable default) when writer lacks writev + assert.strictEqual(writable._writev, null); +} + +// ============================================================================= +// Try-sync-first: writeSync is attempted before write +// ============================================================================= + +async function testWriteSyncFirst() { + let syncCalled = false; + let asyncCalled = false; + + const writer = { + writeSync(chunk) { + syncCalled = true; + return true; // Sync path accepted + }, + write(chunk) { + asyncCalled = true; + return Promise.resolve(); + }, + end() { return Promise.resolve(0); }, + fail() {}, + }; + + const writable = toWritable(writer); + + await new Promise((resolve) => { + writable.write('test', resolve); + }); + + assert.ok(syncCalled, 'writeSync should have been called'); + assert.ok(!asyncCalled, 'write should not have been called'); +} + +// ============================================================================= +// Try-sync-first: falls back to async when writeSync returns false +// ============================================================================= + +async function testWriteSyncFallback() { + let syncCalled = false; + let asyncCalled = false; + + const writer = { + writeSync(chunk) { + syncCalled = true; + return false; // Sync path rejected + }, + write(chunk) { + asyncCalled = true; + return Promise.resolve(); + }, + end() { return Promise.resolve(0); }, + fail() {}, + }; + + const writable = toWritable(writer); + + await new Promise((resolve) => { + writable.write('test', resolve); + }); + + assert.ok(syncCalled, 'writeSync should have been called'); + assert.ok(asyncCalled, 'write should have been called as fallback'); +} + +// ============================================================================= +// Try-sync-first: endSync attempted before end +// ============================================================================= + +async function testEndSyncFirst() { + let endSyncCalled = false; + let endAsyncCalled = false; + + const writer = { + write(chunk) { return Promise.resolve(); }, + endSync() { + endSyncCalled = true; + return 5; // Success, returns byte count + }, + end() { + endAsyncCalled = true; + return Promise.resolve(5); + }, + fail() {}, + }; + + const writable = toWritable(writer); + + await new Promise((resolve) => writable.end(resolve)); + + assert.ok(endSyncCalled, 'endSync should have been called'); + assert.ok(!endAsyncCalled, 'end should not have been called'); +} + +// ============================================================================= +// Try-sync-first: endSync returns -1, falls back to async end +// ============================================================================= + +async function testEndSyncFallback() { + let endSyncCalled = false; + let endAsyncCalled = false; + + const writer = { + write(chunk) { return Promise.resolve(); }, + endSync() { + endSyncCalled = true; + return -1; // Can't complete synchronously + }, + end() { + endAsyncCalled = true; + return Promise.resolve(0); + }, + fail() {}, + }; + + const writable = toWritable(writer); + + await new Promise((resolve) => writable.end(resolve)); + + assert.ok(endSyncCalled, 'endSync should have been called'); + assert.ok(endAsyncCalled, 'end should have been called as fallback'); +} + +// ============================================================================= +// _final delegates to writer.end() +// ============================================================================= + +async function testFinalDelegatesToEnd() { + let endCalled = false; + const writer = { + write(chunk) { return Promise.resolve(); }, + end() { + endCalled = true; + return Promise.resolve(0); + }, + fail() {}, + }; + + const writable = toWritable(writer); + + await new Promise((resolve) => writable.end(resolve)); + + assert.ok(endCalled, 'writer.end() should have been called'); +} + +// ============================================================================= +// _destroy delegates to writer.fail() +// ============================================================================= + +async function testDestroyDelegatesToFail() { + let failReason = null; + const writer = { + write(chunk) { return Promise.resolve(); }, + end() { return Promise.resolve(0); }, + fail(reason) { failReason = reason; }, + }; + + const writable = toWritable(writer); + writable.on('error', () => {}); // Prevent unhandled + + const testErr = new Error('destroy test'); + writable.destroy(testErr); + + // Give a tick for destroy to propagate + await new Promise((resolve) => setTimeout(resolve, 10)); + + assert.strictEqual(failReason, testErr); +} + +// ============================================================================= +// Error from writer.write() propagates to writable +// ============================================================================= + +async function testWriteErrorPropagation() { + const writer = { + write(chunk) { + return Promise.reject(new Error('write failed')); + }, + end() { return Promise.resolve(0); }, + fail() {}, + }; + + const writable = toWritable(writer); + + await assert.rejects(new Promise((resolve, reject) => { + writable.write('data', (err) => { + if (err) reject(err); + else resolve(); + }); + }), { message: 'write failed' }); +} + +// ============================================================================= +// Invalid writer argument throws +// ============================================================================= + +function testInvalidWriterThrows() { + assert.throws( + () => toWritable(null), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + assert.throws( + () => toWritable({}), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + assert.throws( + () => toWritable('not a writer'), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + // Object with write is valid (only write is required). + // This should not throw. + toWritable({ + write() { return Promise.resolve(); }, + }); +} + +// ============================================================================= +// Round-trip: push writer -> fromStreamIter -> write -> read from readable +// ============================================================================= + +async function testRoundTrip() { + const { writer, readable } = push({ backpressure: 'block' }); + const writable = toWritable(writer); + + const data = 'round trip test data'; + writable.write(data); + writable.end(); + + const result = await text(readable); + assert.strictEqual(result, data); +} + +// ============================================================================= +// PushWriter writeSync false accepted as backpressure is not retried +// ============================================================================= + +async function testPushWriterBlockBackpressureNoDuplicate() { + const { writer, readable } = push({ highWaterMark: 1, backpressure: 'block' }); + const writable = toWritable(writer); + + await new Promise((resolve, reject) => { + writable.write('a', (err) => { + if (err) reject(err); + else resolve(); + }); + }); + + writable.write('b'); + writable.end(); + + const result = await text(readable); + assert.strictEqual(result, 'ab'); +} + +// ============================================================================= +// PushWriter writevSync false accepted as backpressure is not retried +// ============================================================================= + +async function testPushWriterBlockBackpressureWritevNoDuplicate() { + const { writer, readable } = push({ highWaterMark: 1, backpressure: 'block' }); + const writable = toWritable(writer); + + await new Promise((resolve, reject) => { + writable.write('a', (err) => { + if (err) reject(err); + else resolve(); + }); + }); + + writable.cork(); + writable.write('b'); + writable.write('c'); + writable.uncork(); + writable.end(); + + const result = await text(readable); + assert.strictEqual(result, 'abc'); +} + +// ============================================================================= +// Multiple sequential writes +// ============================================================================= + +async function testSequentialWrites() { + const { writer, readable } = push({ backpressure: 'block' }); + const writable = toWritable(writer); + + for (let i = 0; i < 10; i++) { + writable.write(`chunk${i}`); + } + writable.end(); + + let expected = ''; + for (let i = 0; i < 10; i++) { + expected += `chunk${i}`; + } + const result = await text(readable); + assert.strictEqual(result, expected); +} + +// ============================================================================= +// Sync callback is deferred via queueMicrotask +// ============================================================================= + +async function testSyncCallbackDeferred() { + let callbackTick = false; + + const writer = { + writeSync(chunk) { + return true; + }, + write(chunk) { + return Promise.resolve(); + }, + end() { return Promise.resolve(0); }, + fail() {}, + }; + + const writable = toWritable(writer); + + const p = new Promise((resolve) => { + writable.write('test', () => { + callbackTick = true; + resolve(); + }); + // Callback should NOT have fired synchronously + assert.strictEqual(callbackTick, false); + }); + + await p; + assert.strictEqual(callbackTick, true); +} + +// ============================================================================= +// Minimal writer: only write() is required +// ============================================================================= + +async function testMinimalWriter() { + const chunks = []; + const writer = { + write(chunk) { + chunks.push(Buffer.from(chunk)); + return Promise.resolve(); + }, + // No end, fail, writeSync, writev, etc. + }; + + const writable = toWritable(writer); + + await new Promise((resolve) => { + writable.write('minimal'); + writable.end(resolve); + }); + + assert.strictEqual(Buffer.concat(chunks).toString(), 'minimal'); +} + +// ============================================================================= +// Destroy without error does not call fail() +// ============================================================================= + +async function testDestroyWithoutError() { + let failCalled = false; + const writer = { + write(chunk) { return Promise.resolve(); }, + fail() { failCalled = true; }, + }; + + const writable = toWritable(writer); + writable.destroy(); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + assert.ok(!failCalled, 'fail should not be called on clean destroy'); +} + +// ============================================================================= +// Destroy with error calls fail() when available +// ============================================================================= + +async function testDestroyWithError() { + let failReason = null; + const writer = { + write(chunk) { return Promise.resolve(); }, + fail(reason) { failReason = reason; }, + }; + + const writable = toWritable(writer); + writable.on('error', () => {}); + + const err = new Error('test'); + writable.destroy(err); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + assert.strictEqual(failReason, err); +} + +// ============================================================================= +// Destroy with error when writer lacks fail() +// ============================================================================= + +async function testDestroyWithoutFail() { + const writer = { + write(chunk) { return Promise.resolve(); }, + // No fail method + }; + + const writable = toWritable(writer); + writable.on('error', () => {}); + + // Should not throw even though writer has no fail() + writable.destroy(new Error('test')); + + await new Promise((resolve) => setTimeout(resolve, 10)); + assert.ok(writable.destroyed); +} + +// ============================================================================= +// Custom highWaterMark option +// ============================================================================= + +function testHighWaterMarkIsMaxSafeInt() { + const writer = { + write(chunk) { return Promise.resolve(); }, + }; + + // HWM is set to MAX_SAFE_INTEGER to disable Writable's internal + // buffering. The underlying Writer manages backpressure directly. + const writable = toWritable(writer); + assert.strictEqual(writable.writableHighWaterMark, Number.MAX_SAFE_INTEGER); +} + +// ============================================================================= +// writeSync throws -- error propagates, does NOT fall back to async +// ============================================================================= + +async function testWriteSyncThrowsPropagation() { + const writer = { + writeSync() { + throw new Error('sync broken'); + }, + write() { + return Promise.resolve(); + }, + end() { return Promise.resolve(0); }, + fail() {}, + }; + + const writable = toWritable(writer); + + await assert.rejects(new Promise((resolve, reject) => { + writable.write('test', (err) => { + if (err) reject(err); + else resolve(); + }); + }), { message: 'sync broken' }); +} + +// ============================================================================= +// ============================================================================= +// writer.write() throws synchronously -- error propagates to callback +// ============================================================================= + +async function testWriteThrowsSyncPropagation() { + const writer = { + write() { + throw new Error('sync throw from write'); + }, + }; + + const writable = toWritable(writer); + + await assert.rejects(new Promise((resolve, reject) => { + writable.write('data', (err) => { + if (err) reject(err); + else resolve(); + }); + }), { message: 'sync throw from write' }); +} + +// ============================================================================= +// writer.end() throws synchronously -- error propagates to callback +// ============================================================================= + +async function testEndThrowsSyncPropagation() { + const writer = { + write(chunk) { return Promise.resolve(); }, + endSync() { return -1; }, + end() { + throw new Error('sync throw from end'); + }, + }; + + const writable = toWritable(writer); + writable.on('error', () => {}); + + await new Promise((resolve) => { + writable.end(common.mustCall((err) => { + assert.ok(err); + assert.strictEqual(err.message, 'sync throw from end'); + resolve(); + })); + }); +} + +// ============================================================================= +// Run all tests +// ============================================================================= + +testInvalidWriterThrows(); +testNoWritevWithoutWriterWritev(); +testHighWaterMarkIsMaxSafeInt(); + +Promise.all([ + testBasicWrite(), + testWriteDelegatesToWriter(), + testWritevDelegation(), + testWriteSyncFirst(), + testWriteSyncFallback(), + testWriteSyncThrowsPropagation(), + testEndSyncFirst(), + testEndSyncFallback(), + testFinalDelegatesToEnd(), + testDestroyDelegatesToFail(), + testDestroyWithoutError(), + testDestroyWithError(), + testDestroyWithoutFail(), + testWriteErrorPropagation(), + testWriteThrowsSyncPropagation(), + testEndThrowsSyncPropagation(), + testRoundTrip(), + testPushWriterBlockBackpressureNoDuplicate(), + testPushWriterBlockBackpressureWritevNoDuplicate(), + testSequentialWrites(), + testSyncCallbackDeferred(), + testMinimalWriter(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-writable-interop.js b/test/parallel/test-stream-iter-writable-interop.js new file mode 100644 index 00000000000000..e7b83ac22841ce --- /dev/null +++ b/test/parallel/test-stream-iter-writable-interop.js @@ -0,0 +1,683 @@ +// Flags: --experimental-stream-iter +'use strict'; + +// Tests for classic Writable stream interop with the stream/iter API +// via fromWritable(). + +const common = require('../common'); +const assert = require('assert'); +const { Writable } = require('stream'); +const { + from, + fromWritable, + pipeTo, + text, + ondrain, +} = require('stream/iter'); + +// ============================================================================= +// fromWritable() is exported from stream/iter +// ============================================================================= + +function testFunctionExists() { + assert.strictEqual(typeof fromWritable, 'function'); +} + +// ============================================================================= +// Default policy is strict +// ============================================================================= + +async function testDefaultIsStrict() { + const writable = new Writable({ + highWaterMark: 1024, + write(chunk, encoding, cb) { cb(); }, + }); + + const writer = fromWritable(writable); + // Should work fine when buffer has room + await writer.write('hello'); + await writer.end(); +} + +// ============================================================================= +// Basic write: pipeTo through the adapter (block policy for pipeTo compat) +// ============================================================================= + +async function testBasicWrite() { + const chunks = []; + const writable = new Writable({ + write(chunk, encoding, cb) { + chunks.push(Buffer.from(chunk)); + cb(); + }, + }); + + const writer = fromWritable(writable, { backpressure: 'block' }); + await pipeTo(from('hello world'), writer); + + assert.strictEqual(Buffer.concat(chunks).toString(), 'hello world'); +} + +// ============================================================================= +// write() resolves when no backpressure (strict) +// ============================================================================= + +async function testWriteNoDrain() { + const chunks = []; + const writable = new Writable({ + highWaterMark: 1024, + write(chunk, encoding, cb) { + chunks.push(Buffer.from(chunk)); + cb(); + }, + }); + + const writer = fromWritable(writable); + await writer.write('hello'); + await writer.write(' world'); + await writer.end(); + + assert.strictEqual(Buffer.concat(chunks).toString(), 'hello world'); +} + +// ============================================================================= +// block: write() waits for drain when backpressure is active +// ============================================================================= + +async function testBlockWaitsForDrain() { + const chunks = []; + const writable = new Writable({ + highWaterMark: 1, // Very small buffer + write(chunk, encoding, cb) { + chunks.push(Buffer.from(chunk)); + // Delay callback to simulate slow consumer + setTimeout(cb, 10); + }, + }); + + const writer = fromWritable(writable, { backpressure: 'block' }); + + await writer.write('a'); + await writer.write('b'); + await writer.write('c'); + await writer.end(); + + assert.strictEqual(Buffer.concat(chunks).toString(), 'abc'); +} + +// ============================================================================= +// block: stream error rejects pending write +// ============================================================================= + +async function testBlockErrorRejectsPendingWrite() { + const writable = new Writable({ + highWaterMark: 1, + write(chunk, enc, cb) { + // Never call cb -- simulate stuck write + }, + }); + + const writer = fromWritable(writable, { backpressure: 'block' }); + + // First write fills the buffer, waits for drain + const writePromise = writer.write('data that will block'); + + // Destroy with error while write is pending + writable.destroy(new Error('stream broke')); + + await assert.rejects(writePromise, { message: 'stream broke' }); +} + +// ============================================================================= +// strict: rejects when buffer is full +// ============================================================================= + +async function testStrictRejectsWhenFull() { + const writable = new Writable({ + highWaterMark: 5, + write(chunk, enc, cb) { + // Never call cb -- data stays buffered + }, + }); + + const writer = fromWritable(writable); + + // First write fills the buffer (5 bytes = hwm) + await writer.write('12345'); + + // Second write should reject -- buffer is full + await assert.rejects( + writer.write('more'), + { code: 'ERR_INVALID_STATE' }, + ); +} + +// ============================================================================= +// strict: writev rejects when buffer is full +// ============================================================================= + +async function testStrictWritevRejectsWhenFull() { + const writable = new Writable({ + highWaterMark: 5, + write(chunk, enc, cb) { + // Never call cb + }, + }); + + const writer = fromWritable(writable); + + // Fill buffer + await writer.write('12345'); + + // Writev should reject entire batch + await assert.rejects( + writer.writev([ + new TextEncoder().encode('a'), + new TextEncoder().encode('b'), + ]), + { code: 'ERR_INVALID_STATE' }, + ); +} + +// ============================================================================= +// drop-newest: silently discards when buffer is full +// ============================================================================= + +async function testDropNewestDiscards() { + const chunks = []; + const writable = new Writable({ + highWaterMark: 5, + write(chunk, enc, cb) { + chunks.push(Buffer.from(chunk)); + // Never call cb -- data stays buffered + }, + }); + + const writer = fromWritable(writable, { backpressure: 'drop-newest' }); + + // First write fills the buffer + await writer.write('12345'); + + // Second write should be silently discarded (no reject, no block) + await writer.write('dropped'); + + // Only the first chunk was actually written to the writable + assert.strictEqual(chunks.length, 1); + assert.strictEqual(chunks[0].toString(), '12345'); +} + +// ============================================================================= +// drop-newest: writev discards entire batch when full +// ============================================================================= + +async function testDropNewestWritevDiscards() { + const chunks = []; + const writable = new Writable({ + highWaterMark: 5, + write(chunk, enc, cb) { + chunks.push(Buffer.from(chunk)); + // Never call cb + }, + }); + + const writer = fromWritable(writable, { backpressure: 'drop-newest' }); + + // Fill buffer + await writer.write('12345'); + + // Writev should discard entire batch + await writer.writev([ + new TextEncoder().encode('a'), + new TextEncoder().encode('b'), + ]); + + assert.strictEqual(chunks.length, 1); +} + +// ============================================================================= +// drop-newest: still counts bytes from dropped writes +// ============================================================================= + +async function testDropNewestCountsBytes() { + const writable = new Writable({ + highWaterMark: 5, + write(chunk, enc, cb) { + // Never call cb + }, + }); + + const writer = fromWritable(writable, { backpressure: 'drop-newest' }); + + await writer.write('12345'); // 5 bytes, accepted + await writer.write('67890'); // 5 bytes, dropped + + // desiredSize should be 0 (buffer is full) + assert.strictEqual(writer.desiredSize, 0); +} + +// ============================================================================= +// drop-oldest: throws on construction +// ============================================================================= + +function testDropOldestThrows() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + assert.throws( + () => fromWritable(writable, { backpressure: 'drop-oldest' }), + { code: 'ERR_INVALID_ARG_VALUE' }, + ); +} + +// ============================================================================= +// Invalid backpressure value throws +// ============================================================================= + +function testInvalidBackpressureThrows() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + assert.throws( + () => fromWritable(writable, { backpressure: 'invalid' }), + { code: 'ERR_INVALID_ARG_VALUE' }, + ); +} + +// ============================================================================= +// writev() corks and uncorks (block policy) +// ============================================================================= + +async function testWritev() { + const chunks = []; + const writable = new Writable({ + highWaterMark: 1024, + write(chunk, encoding, cb) { + chunks.push(Buffer.from(chunk)); + cb(); + }, + writev(entries, cb) { + for (const { chunk } of entries) { + chunks.push(Buffer.from(chunk)); + } + cb(); + }, + }); + + const writer = fromWritable(writable, { backpressure: 'block' }); + await writer.writev([ + new TextEncoder().encode('hello'), + new TextEncoder().encode(' '), + new TextEncoder().encode('world'), + ]); + await writer.end(); + + assert.strictEqual(Buffer.concat(chunks).toString(), 'hello world'); +} + +// ============================================================================= +// writeSync / writevSync always return false +// ============================================================================= + +function testSyncMethodsReturnFalse() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + const writer = fromWritable(writable); + + assert.strictEqual(writer.writeSync(new Uint8Array(1)), false); + assert.strictEqual(writer.writevSync([new Uint8Array(1)]), false); +} + +// ============================================================================= +// endSync returns -1 +// ============================================================================= + +function testEndSyncReturnsNegativeOne() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + const writer = fromWritable(writable); + + assert.strictEqual(writer.endSync(), -1); +} + +// ============================================================================= +// end() resolves with total bytes written +// ============================================================================= + +async function testEndReturnsByteCount() { + const writable = new Writable({ + write(chunk, encoding, cb) { cb(); }, + }); + + const writer = fromWritable(writable); + await writer.write('hello'); // 5 bytes + await writer.write(' world'); // 6 bytes + const total = await writer.end(); + + assert.strictEqual(total, 11); +} + +// ============================================================================= +// fail() destroys the writable +// ============================================================================= + +async function testFail() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + writable.on('error', () => {}); // Prevent unhandled error + const writer = fromWritable(writable); + + writer.fail(new Error('test fail')); + + assert.ok(writable.destroyed); +} + +// ============================================================================= +// desiredSize reflects buffer state +// ============================================================================= + +function testDesiredSize() { + const writable = new Writable({ + highWaterMark: 100, + write(chunk, enc, cb) { + // Don't call cb - keeps data buffered + }, + }); + + const writer = fromWritable(writable); + assert.strictEqual(writer.desiredSize, 100); +} + +// ============================================================================= +// desiredSize is null when destroyed +// ============================================================================= + +function testDesiredSizeNull() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + const writer = fromWritable(writable); + + writable.destroy(); + assert.strictEqual(writer.desiredSize, null); +} + +// ============================================================================= +// drainableProtocol: resolves immediately when no backpressure +// ============================================================================= + +async function testDrainableNoPressure() { + const writable = new Writable({ + highWaterMark: 1024, + write(chunk, enc, cb) { cb(); }, + }); + + const writer = fromWritable(writable); + const result = await ondrain(writer); + assert.strictEqual(result, true); +} + +// ============================================================================= +// drainableProtocol: returns null when destroyed +// ============================================================================= + +function testDrainableNull() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + const writer = fromWritable(writable); + + writable.destroy(); + assert.strictEqual(ondrain(writer), null); +} + +// ============================================================================= +// Error propagation: write after end rejects +// ============================================================================= + +async function testWriteAfterEnd() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + const writer = fromWritable(writable); + + await writer.end(); + + await assert.rejects( + writer.write('should fail'), + { code: 'ERR_STREAM_WRITE_AFTER_END' }, + ); +} + +// ============================================================================= +// Multiple sequential writes +// ============================================================================= + +async function testSequentialWrites() { + const chunks = []; + const writable = new Writable({ + write(chunk, encoding, cb) { + chunks.push(Buffer.from(chunk)); + cb(); + }, + }); + + const writer = fromWritable(writable); + + for (let i = 0; i < 10; i++) { + await writer.write(`chunk${i}`); + } + await writer.end(); + + let expected = ''; + for (let i = 0; i < 10; i++) { + expected += `chunk${i}`; + } + assert.strictEqual(Buffer.concat(chunks).toString(), expected); +} + +// ============================================================================= +// pipeTo with compression transform into writable (block policy) +// ============================================================================= + +async function testPipeToWithTransform() { + const { + compressGzip, + decompressGzip, + } = require('zlib/iter'); + const { pull } = require('stream/iter'); + + const compressed = []; + const writable = new Writable({ + write(chunk, encoding, cb) { + compressed.push(Buffer.from(chunk)); + cb(); + }, + }); + + const writer = fromWritable(writable, { backpressure: 'block' }); + await pipeTo(from('hello via transform'), compressGzip(), writer); + + const decompressed = await text( + pull(from(Buffer.concat(compressed)), decompressGzip()), + ); + assert.strictEqual(decompressed, 'hello via transform'); +} + +// ============================================================================= +// Dispose support +// ============================================================================= + +async function testDispose() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + const writer = fromWritable(writable); + + writer[Symbol.dispose](); + assert.ok(writable.destroyed); +} + +async function testAsyncDispose() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + const writer = fromWritable(writable); + + await writer[Symbol.asyncDispose](); + assert.ok(writable.destroyed); +} + +// ============================================================================= +// write() validates chunk type +// ============================================================================= + +async function testWriteInvalidChunkType() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + const writer = fromWritable(writable); + + await assert.rejects( + writer.write(42), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + await assert.rejects( + writer.write(null), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + await assert.rejects( + writer.write({}), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +// ============================================================================= +// writev() validates chunks is an array +// ============================================================================= + +function testWritevInvalidChunksType() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + const writer = fromWritable(writable); + + assert.throws( + () => writer.writev('not an array'), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + assert.throws( + () => writer.writev(42), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +} + +// ============================================================================= +// writev() uncorks when chunk validation throws +// ============================================================================= + +function testWritevInvalidChunkUncorks() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + const writer = fromWritable(writable); + + assert.throws( + () => writer.writev([new Uint8Array([1]), 42]), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + assert.strictEqual(writable.writableCorked, 0); +} + +// ============================================================================= +// Cached writer: second call returns same instance +// ============================================================================= + +function testCachedWriter() { + const writable = new Writable({ write(chunk, enc, cb) { cb(); } }); + const writer1 = fromWritable(writable); + const writer2 = fromWritable(writable); + + assert.strictEqual(writer1, writer2); +} + +// ============================================================================= +// fail() rejects pending block waiters +// ============================================================================= + +async function testFailRejectsPendingWaiters() { + const writable = new Writable({ + highWaterMark: 1, + write(chunk, enc, cb) { + // Never call cb -- stuck + }, + }); + writable.on('error', () => {}); // Prevent unhandled error + + const writer = fromWritable(writable, { backpressure: 'block' }); + + // This write will block on drain + const writePromise = writer.write('blocked data'); + + // fail() should reject the pending waiter, not orphan it + writer.fail(new Error('fail reason')); + + await assert.rejects(writePromise, { message: 'fail reason' }); +} + +// ============================================================================= +// dispose rejects pending block waiters +// ============================================================================= + +async function testDisposeRejectsPendingWaiters() { + const writable = new Writable({ + highWaterMark: 1, + write(chunk, enc, cb) { + // Never call cb -- stuck + }, + }); + + const writer = fromWritable(writable, { backpressure: 'block' }); + + // This write will block on drain + const writePromise = writer.write('blocked data'); + + writer[Symbol.dispose](); + + await assert.rejects(writePromise, { name: 'AbortError' }); +} + +// ============================================================================= +// Run all tests +// ============================================================================= + +testFunctionExists(); +testSyncMethodsReturnFalse(); +// ============================================================================= +// Object-mode Writable throws +// ============================================================================= + +function testObjectModeThrows() { + const writable = new Writable({ + objectMode: true, + write(chunk, enc, cb) { cb(); }, + }); + assert.throws( + () => fromWritable(writable), + { code: 'ERR_INVALID_STATE' }, + ); +} + +testFunctionExists(); +testSyncMethodsReturnFalse(); +testEndSyncReturnsNegativeOne(); +testDesiredSize(); +testDesiredSizeNull(); +testDrainableNull(); +testDropOldestThrows(); +testInvalidBackpressureThrows(); +testWritevInvalidChunksType(); +testWritevInvalidChunkUncorks(); +testCachedWriter(); +testObjectModeThrows(); + +Promise.all([ + testDefaultIsStrict(), + testBasicWrite(), + testWriteNoDrain(), + testBlockWaitsForDrain(), + testBlockErrorRejectsPendingWrite(), + testStrictRejectsWhenFull(), + testStrictWritevRejectsWhenFull(), + testDropNewestDiscards(), + testDropNewestWritevDiscards(), + testDropNewestCountsBytes(), + testWritev(), + testEndReturnsByteCount(), + testFail(), + testDrainableNoPressure(), + testWriteAfterEnd(), + testSequentialWrites(), + testPipeToWithTransform(), + testDispose(), + testAsyncDispose(), + testWriteInvalidChunkType(), + testFailRejectsPendingWaiters(), + testDisposeRejectsPendingWaiters(), +]).then(common.mustCall()); diff --git a/test/parallel/test-stream-readable-setEncoding-existing-buffers.js b/test/parallel/test-stream-readable-setEncoding-existing-buffers.js index eb75260bacfc45..38d50b5677579d 100644 --- a/test/parallel/test-stream-readable-setEncoding-existing-buffers.js +++ b/test/parallel/test-stream-readable-setEncoding-existing-buffers.js @@ -58,3 +58,48 @@ const assert = require('assert'); assert.deepStrictEqual(chunks, ['🎉']); }); } + +{ + // Call .setEncoding() before the stream starts flowing, after _read() + // has buffered the first chunk of a split character. + const chunks = [ + Buffer.from([0xf0, 0x9f]), + Buffer.from([0x8e, 0x89]), + null, + ]; + const r = new Readable({ + read() { + this.push(chunks.shift()); + }, + }); + + r.read(0); + assert.strictEqual(r.readableFlowing, null); + assert.strictEqual(r.readableLength, 2); + + r.setEncoding('utf8'); + const received = []; + r.on('data', (chunk) => received.push(chunk)); + + process.nextTick(() => { + assert.deepStrictEqual(received, ['🎉']); + }); +} + +{ + // Call .setEncoding() after EOF while the buffer contains an + // incomplete character. + const r = new Readable({ read() {} }); + + r.push(Buffer.from([0xf0])); + r.push(Buffer.from([0x9f])); + r.push(null); + + r.setEncoding('utf8'); + const chunks = []; + r.on('data', (chunk) => chunks.push(chunk)); + + process.nextTick(() => { + assert.deepStrictEqual(chunks, ['�']); + }); +} diff --git a/test/parallel/test-stream-readable-to-web-termination.js b/test/parallel/test-stream-readable-to-web-termination.js index 13fce9bc715e1e..f30cf721e14cd8 100644 --- a/test/parallel/test-stream-readable-to-web-termination.js +++ b/test/parallel/test-stream-readable-to-web-termination.js @@ -1,6 +1,8 @@ 'use strict'; -require('../common'); -const { Readable } = require('stream'); +const common = require('../common'); +const assert = require('assert'); +const { Duplex, Readable } = require('stream'); +const { setTimeout: delay } = require('timers/promises'); { const r = Readable.from([]); @@ -10,3 +12,33 @@ const { Readable } = require('stream'); const reader = Readable.toWeb(r).getReader(); reader.read(); } + +{ + const duplex = new Duplex({ + read() { + this.push(Buffer.from('x')); + this.push(null); + }, + write(_chunk, _encoding, callback) { + callback(); + }, + }); + + const reader = Readable.toWeb(duplex).getReader(); + + (async () => { + const result = await reader.read(); + assert.deepStrictEqual(result, { + value: new Uint8Array(Buffer.from('x')), + done: false, + }); + + const closeResult = await Promise.race([ + reader.read(), + delay(common.platformTimeout(100)).then(() => 'timeout'), + ]); + + assert.notStrictEqual(closeResult, 'timeout'); + assert.deepStrictEqual(closeResult, { value: undefined, done: true }); + })().then(common.mustCall()); +} diff --git a/test/parallel/test-stream-writable-write-error.js b/test/parallel/test-stream-writable-write-error.js index 069e32e1be8e3e..76f797fb1bd583 100644 --- a/test/parallel/test-stream-writable-write-error.js +++ b/test/parallel/test-stream-writable-write-error.js @@ -31,7 +31,7 @@ function test(autoDestroy) { { const w = new Writable({ autoDestroy, - _write() {} + write() {} }); w.end(); expectError(w, ['asd'], 'ERR_STREAM_WRITE_AFTER_END'); @@ -40,7 +40,7 @@ function test(autoDestroy) { { const w = new Writable({ autoDestroy, - _write() {} + write() {} }); w.destroy(); } @@ -48,7 +48,7 @@ function test(autoDestroy) { { const w = new Writable({ autoDestroy, - _write() {} + write() {} }); expectError(w, [null], 'ERR_STREAM_NULL_VALUES', true); } @@ -56,18 +56,26 @@ function test(autoDestroy) { { const w = new Writable({ autoDestroy, - _write() {} + write() {} }); expectError(w, [{}], 'ERR_INVALID_ARG_TYPE', true); } { const w = new Writable({ - decodeStrings: false, autoDestroy, - _write() {} + write() {} + }); + expectError(w, ['asd', 'buffer'], 'ERR_UNKNOWN_ENCODING', true); + } + + { + const w = new Writable({ + autoDestroy, + decodeStrings: false, + write() {} }); - expectError(w, ['asd', 'noencoding'], 'ERR_UNKNOWN_ENCODING', true); + expectError(w, ['asd', 'buffer'], 'ERR_UNKNOWN_ENCODING', true); } } diff --git a/test/parallel/test-tls-get-ca-certificates-worker-no-use-system-ca.js b/test/parallel/test-tls-get-ca-certificates-worker-no-use-system-ca.js new file mode 100644 index 00000000000000..16ed20e9a8c8c8 --- /dev/null +++ b/test/parallel/test-tls-get-ca-certificates-worker-no-use-system-ca.js @@ -0,0 +1,60 @@ +'use strict'; +// Flags: --no-use-system-ca + +// This tests that NODE_USE_SYSTEM_CA can be +// overridden by --no-use-system-ca. + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const { once } = require('events'); +const { Worker } = require('worker_threads'); +const fixtures = require('../common/fixtures'); + +const systemCerts = tls.getCACertificates('system'); +if (systemCerts.length === 0) { + common.skip('No trusted system certificates installed. Skip.'); +} + +async function runWorker({ execArgv, env }) { + const worker = new Worker( + fixtures.path('tls-get-ca-certificates-worker.js'), + { execArgv, env }, + ); + worker.once('error', common.mustNotCall()); + const exitPromise = once(worker, 'exit'); + const messagePromise = once(worker, 'message'); + const [message] = await messagePromise; + const [exitCode] = await exitPromise; + assert.strictEqual(exitCode, 0); + return message; +} + +(async () => { + // with --no-use-system-ca. + assert.strictEqual( + tls.getCACertificates('default').length, + tls.getCACertificates('bundled').length, + ); + + const envEnabled = await runWorker({ + execArgv: [], + env: { ...process.env, NODE_USE_SYSTEM_CA: '1' }, + }); + + assert.strictEqual(envEnabled.systemLen, systemCerts.length); + assert.strictEqual( + envEnabled.defaultLen, + envEnabled.bundledLen + envEnabled.systemLen, + ); + + const flagDisabled = await runWorker({ + execArgv: ['--no-use-system-ca'], + env: { ...process.env, NODE_USE_SYSTEM_CA: '1' }, + }); + + assert.strictEqual(flagDisabled.systemLen, systemCerts.length); + assert.strictEqual(flagDisabled.defaultLen, flagDisabled.bundledLen); +})().then(common.mustCall()); diff --git a/test/parallel/test-tls-get-ca-certificates-worker-use-system-ca.js b/test/parallel/test-tls-get-ca-certificates-worker-use-system-ca.js new file mode 100644 index 00000000000000..a07c4ffe93d4d0 --- /dev/null +++ b/test/parallel/test-tls-get-ca-certificates-worker-use-system-ca.js @@ -0,0 +1,47 @@ +'use strict'; +// Flags: --no-use-system-ca + +// This tests that --use-system-ca is an Environment option (i.e. can be +// enabled/disabled in workers) and that it affects tls.getCACertificates('default'). + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const { once } = require('events'); +const { Worker } = require('worker_threads'); +const fixtures = require('../common/fixtures'); + +const systemCerts = tls.getCACertificates('system'); +if (systemCerts.length === 0) { + common.skip('No trusted system certificates installed. Skip.'); +} +async function runWorker(execArgv) { + const worker = new Worker( + fixtures.path('tls-get-ca-certificates-worker.js'), + { execArgv }, + ); + worker.once('error', common.mustNotCall()); + const exitPromise = once(worker, 'exit'); + const messagePromise = once(worker, 'message'); + const [message] = await messagePromise; + const [exitCode] = await exitPromise; + assert.strictEqual(exitCode, 0); + return message; +} + +(async () => { + const withSystemCA = await runWorker(['--use-system-ca']); + assert.strictEqual(withSystemCA.systemLen, systemCerts.length); + + assert.strictEqual( + withSystemCA.defaultLen, + withSystemCA.bundledLen + withSystemCA.systemLen, + ); + + assert.strictEqual( + tls.getCACertificates('default').length, + tls.getCACertificates('bundled').length, + ); +})().then(common.mustCall()); diff --git a/test/parallel/test-whatwg-webstreams-adapters-to-readablestream.js b/test/parallel/test-whatwg-webstreams-adapters-to-readablestream.js index 66af7b128c4d5a..eb01a39d8dc168 100644 --- a/test/parallel/test-whatwg-webstreams-adapters-to-readablestream.js +++ b/test/parallel/test-whatwg-webstreams-adapters-to-readablestream.js @@ -4,6 +4,7 @@ const common = require('../common'); const assert = require('assert'); +const { once } = require('events'); const { newReadableStreamFromStreamReadable, @@ -11,6 +12,7 @@ const { const { Duplex, + PassThrough, Readable, } = require('stream'); @@ -188,11 +190,142 @@ const { } { - const readable = new Readable(); + /** + * Runs the same assertion across finalize-before/after and + * default/BYOB adapter creation orders. + * @param {(readable: Readable) => void | Promise} finalize + * Finalizes the source stream before or after adaptation. + * @param {(readAndAssert: () => Promise, reader: ReadableStreamReader) => Promise} postAssert + * Asserts the resulting web stream state for the current case. + * @param {() => Readable} [createReadable] + * Creates the source stream for each case. + */ + function testConfluence(finalize, postAssert, createReadable = () => new PassThrough()) { + const cases = [false, true].flatMap((finalizeFirst) => [false, true].map((isBYOB) => ({ finalizeFirst, isBYOB }))); + Promise.all(cases.map(async (case_) => { + try { + const { isBYOB } = case_; + const readable = createReadable(); + if (case_.finalizeFirst) { + await finalize(readable); + } + /** @type {ReadableStream} */ + const readableStream = newReadableStreamFromStreamReadable(readable, { type: isBYOB ? 'bytes' : undefined }); + const reader = readableStream.getReader({ mode: isBYOB ? 'byob' : undefined }); + if (!case_.finalizeFirst) { + await finalize(readable); + } + + const readAndAssert = common.mustCall(() => { + return reader.read(isBYOB ? new Uint8Array(1) : undefined).then((result) => { + assert.deepStrictEqual(result, { + value: isBYOB ? new Uint8Array(0) : undefined, + done: true, + }); + }); + }); + await postAssert(readAndAssert, reader); + } catch (cause) { + throw new Error(`Case failed: ${JSON.stringify(case_)}`, { cause }); + } + })).then(common.mustCall()); + } + const error = new Error('boom'); + // Ending the readable without an error => closes the readableStream without an error + testConfluence( + async (readable) => { + readable.resume(); + readable.end(); + await once(readable, 'end'); + }, + common.mustCall(async (readAndAssert, reader) => { + await readAndAssert(); + await reader.closed; + }, 4) + ); + // Prematurely destroying the stream.Readable without an error + // => errors the ReadableStream with a premature close error + testConfluence( + (readable) => readable.destroy(), + common.mustCall(async (readAndAssert, reader) => { + const errorPredicate = { code: 'ABORT_ERR' }; + await assert.rejects(readAndAssert(), errorPredicate); + await assert.rejects(reader.closed, errorPredicate); + }, 4) + ); + // Asynchronously destroyed readable => errors the ReadableStream with a + // premature close error regardless of adapter creation order. + class AsyncDestroyReadable extends Readable { + _read() {} + + _destroy(error, callback) { + setImmediate(callback, error); + } + } + testConfluence( + (readable) => readable.destroy(), + common.mustCall(async (readAndAssert, reader) => { + const errorPredicate = { code: 'ABORT_ERR' }; + await assert.rejects(readAndAssert(), errorPredicate); + await assert.rejects(reader.closed, errorPredicate); + }, 4), + () => new AsyncDestroyReadable() + ); + // Destroying the readable with an error => errors the readableStream + testConfluence( + common.mustCall((readable) => { + readable.on('error', common.mustCall((reason) => { + assert.strictEqual(reason, error); + })); + readable.destroy(error); + }, 4), + common.mustCall(async (readAndAssert, reader) => { + await assert.rejects(readAndAssert(), error); + await assert.rejects(reader.closed, error); + }, 4) + ); +} + +{ + const readable = new PassThrough(); + readable.end(); readable.destroy(); - const readableStream = newReadableStreamFromStreamReadable(readable); - const reader = readableStream.getReader(); - reader.closed.then(common.mustCall()); + + (async () => { + await new Promise((resolve) => readable.once('close', resolve)); + assert.strictEqual(readable.listenerCount('error'), 0); + + const readableStream = newReadableStreamFromStreamReadable(readable); + // Only one error listener from the adapter should be added + assert.strictEqual(readable.listenerCount('error'), 1); + newReadableStreamFromStreamReadable(readable); + // No duplicate listeners should be added. + assert.strictEqual(readable.listenerCount('error'), 1); + + const readResult = await readableStream.getReader().read(); + assert.deepStrictEqual(readResult, { value: undefined, done: true }); + })().then(common.mustCall()); +} + +{ + const readable = new PassThrough(); + const readableStream = newReadableStreamFromStreamReadable(readable, { + type: 'bytes', + }); + const reader = readableStream.getReader({ mode: 'byob' }); + + (async () => { + const readPromise = reader.read(new Uint8Array(8)); + + readable.end(); + await once(readable, 'end'); + + assert.deepStrictEqual(await readPromise, { + value: new Uint8Array(0), + done: true, + }); + await reader.closed; + })().then(common.mustCall()); } { diff --git a/test/system-ca/test-use-system-ca-worker-disable.mjs b/test/system-ca/test-use-system-ca-worker-disable.mjs new file mode 100644 index 00000000000000..43f3f2937602fd --- /dev/null +++ b/test/system-ca/test-use-system-ca-worker-disable.mjs @@ -0,0 +1,78 @@ +// Flags: --use-system-ca + +// Verify that a worker can disable the system CA store while the parent uses it. + +import * as common from '../common/index.mjs'; +import assert from 'node:assert/strict'; +import https from 'node:https'; +import fixtures from '../common/fixtures.js'; +import { it, beforeEach, afterEach, describe } from 'node:test'; +import { once } from 'events'; +import { Worker } from 'node:worker_threads'; + +if (!common.hasCrypto) { + common.skip('requires crypto'); +} + +// To run this test, the system needs to be configured to trust +// the CA certificate first (which needs an interactive GUI approval, e.g. TouchID): +// see the README.md in this folder for instructions on how to do this. +const handleRequest = (req, res) => { + const path = req.url; + switch (path) { + case '/hello-world': + res.writeHead(200); + res.end('hello world\n'); + break; + default: + common.mustNotCall(`Unexpected path: ${path}`)(); + } +}; + +describe('use-system-ca', function() { + async function setupServer(key, cert) { + const theServer = https.createServer( + { + key: fixtures.readKey(key), + cert: fixtures.readKey(cert), + }, + handleRequest, + ); + theServer.listen(0); + await once(theServer, 'listening'); + + return theServer; + } + + let server; + + beforeEach(async function() { + server = await setupServer('agent8-key.pem', 'agent8-cert.pem'); + }); + + it('trusts a valid root certificate', async function() { + const url = `https://localhost:${server.address().port}/hello-world`; + const worker = new Worker(fixtures.path('system-ca', 'fetch-worker.mjs'), { + execArgv: ['--no-use-system-ca'], + workerData: { url }, + }); + await fetch(url); + + const [message] = await once(worker, 'message'); + assert.strictEqual(message.ok, false); + assert( + message.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' || + message.code === 'SELF_SIGNED_CERT_IN_CHAIN', + `Expected certificate verification error but got: ${JSON.stringify( + message, + )}`, + ); + + const [exitCode] = await once(worker, 'exit'); + assert.strictEqual(exitCode, 0); + }); + + afterEach(async function() { + server?.close(); + }); +}); diff --git a/test/system-ca/test-use-system-ca-worker-enable.mjs b/test/system-ca/test-use-system-ca-worker-enable.mjs new file mode 100644 index 00000000000000..f5f3faab4814f1 --- /dev/null +++ b/test/system-ca/test-use-system-ca-worker-enable.mjs @@ -0,0 +1,78 @@ +// Flags: --no-use-system-ca + +// Verify that a worker can enable the system CA store while the parent disables it. + +import * as common from '../common/index.mjs'; +import assert from 'node:assert/strict'; +import https from 'node:https'; +import fixtures from '../common/fixtures.js'; +import { it, beforeEach, afterEach, describe } from 'node:test'; +import { once } from 'events'; +import { Worker } from 'node:worker_threads'; + +if (!common.hasCrypto) { + common.skip('requires crypto'); +} + +// To run this test, the system needs to be configured to trust +// the CA certificate first (which needs an interactive GUI approval, e.g. TouchID): +// see the README.md in this folder for instructions on how to do this. +const handleRequest = (req, res) => { + const path = req.url; + switch (path) { + case '/hello-world': + res.writeHead(200); + res.end('hello world\n'); + break; + default: + common.mustNotCall(`Unexpected path: ${path}`)(); + } +}; + +describe('use-system-ca', function() { + async function setupServer(key, cert) { + const theServer = https.createServer( + { + key: fixtures.readKey(key), + cert: fixtures.readKey(cert), + }, + handleRequest, + ); + theServer.listen(0); + await once(theServer, 'listening'); + + return theServer; + } + + let server; + + beforeEach(async function() { + server = await setupServer('agent8-key.pem', 'agent8-cert.pem'); + }); + + it('trusts a valid root certificate', async function() { + const url = `https://localhost:${server.address().port}/hello-world`; + const worker = new Worker(fixtures.path('system-ca', 'fetch-worker.mjs'), { + execArgv: ['--use-system-ca'], + workerData: { url }, + }); + await assert.rejects(fetch(url), (err) => { + assert.strictEqual(err.cause.code, 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'); + return true; + }); + + const [message] = await once(worker, 'message'); + assert.strictEqual(message.ok, true); + assert.strictEqual( + message.status, + 200, + `Expected status 200, got ${message.status}`, + ); + const [exitCode] = await once(worker, 'exit'); + assert.strictEqual(exitCode, 0); + }); + + afterEach(async function() { + server?.close(); + }); +}); diff --git a/tools/dep_updaters/update-ngtcp2.sh b/tools/dep_updaters/update-ngtcp2.sh index b892ecbab2c864..ea25e5a3b11e6b 100755 --- a/tools/dep_updaters/update-ngtcp2.sh +++ b/tools/dep_updaters/update-ngtcp2.sh @@ -10,19 +10,20 @@ DEPS_DIR="$BASE_DIR/deps" # shellcheck disable=SC1091 . "$BASE_DIR/tools/dep_updaters/utils.sh" -NEW_VERSION="$("$NODE" --input-type=module <<'EOF' -const res = await fetch('https://api.github.com/repos/ngtcp2/ngtcp2/releases', - process.env.GITHUB_TOKEN && { - headers: { - "Authorization": `Bearer ${process.env.GITHUB_TOKEN}` - }, - }); -if (!res.ok) throw new Error(`FetchError: ${res.status} ${res.statusText}`, { cause: res }); -const releases = await res.json() -const { tag_name } = releases.at(0); -console.log(tag_name.replace('v', '')); -EOF -)" +NEW_VERSION="1.15.1" +# NEW_VERSION="$("$NODE" --input-type=module <<'EOF' +# const res = await fetch('https://api.github.com/repos/ngtcp2/ngtcp2/releases', +# process.env.GITHUB_TOKEN && { +# headers: { +# "Authorization": `Bearer ${process.env.GITHUB_TOKEN}` +# }, +# }); +# if (!res.ok) throw new Error(`FetchError: ${res.status} ${res.statusText}`, { cause: res }); +# const releases = await res.json() +# const { tag_name } = releases.at(0); +# console.log(tag_name.replace('v', '')); +# EOF +# )" NGTCP2_VERSION_H="$DEPS_DIR/ngtcp2/ngtcp2/lib/includes/ngtcp2/version.h" @@ -42,29 +43,38 @@ cleanup () { trap cleanup INT TERM EXIT NGTCP2_REF="v$NEW_VERSION" -NGTCP2_ZIP="ngtcp2-$NEW_VERSION" cd "$WORKSPACE" echo "Fetching ngtcp2 source archive..." -curl -sL -o "$NGTCP2_ZIP.zip" "https://github.com/ngtcp2/ngtcp2/archive/refs/tags/$NGTCP2_REF.zip" -log_and_verify_sha256sum "ngtcp2" "$NGTCP2_ZIP.zip" -unzip "$NGTCP2_ZIP.zip" -rm "$NGTCP2_ZIP.zip" -mv "$NGTCP2_ZIP" ngtcp2 +git clone --depth 1 --branch "$NGTCP2_REF" https://github.com/ngtcp2/ngtcp2.git cd ngtcp2 +git submodule update --init --recursive + autoreconf -i # For Mac users who have installed libev with MacPorts, append # ',-L/opt/local/lib' to LDFLAGS, and also pass # CPPFLAGS="-I/opt/local/include" to ./configure. -./configure --prefix="$PWD/build" --enable-lib-only +./configure --prefix="$PWD/build" --enable-lib-only replace_dir "$DEPS_DIR/ngtcp2/ngtcp2/lib" "lib" replace_dir "$DEPS_DIR/ngtcp2/ngtcp2/crypto" "crypto" +replace_dir "$DEPS_DIR/ngtcp2/ngtcp2/examples" "examples" +replace_dir "$DEPS_DIR/ngtcp2/ngtcp2/third-party" "third-party" + +cd "$WORKSPACE" + +# Libev is a dependency of ngtcp2's examples. It really hasn't been updated +# in a long time, sticking with the same version should be fine. +curl -sL -o "libev.tar.gz" "https://dist.schmorp.de/libev/libev-4.33.tar.gz" +tar xzf libev.tar.gz +rm libev.tar.gz + +replace_dir "$DEPS_DIR/ngtcp2/ngtcp2/third-party/libev" "libev-4.33" # Update the version number on maintaining-dependencies.md # and print the new version as the last line of the script as we need diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index fe038842f2b408..4b8bd569c7764c 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -318,6 +318,11 @@ const customTypesMap = { 'TextDecoderStream': 'webstreams.html#class-textdecoderstream', + 'Shareable': 'stream_iter.html#interface-shareable', + 'Share': 'stream_iter.html#class-share', + 'SyncShareable': 'stream_iter.html#interface-syncshareable', + 'SyncShare': 'stream_iter.html#class-syncshare', + 'FormData': 'https://developer.mozilla.org/en-US/docs/Web/API/FormData', 'Headers': 'https://developer.mozilla.org/en-US/docs/Web/API/Headers', 'Response': 'https://developer.mozilla.org/en-US/docs/Web/API/Response',