From 2b555dbfaf074af7392c123c09b9dcdf030cde7b Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Mon, 2 Nov 2026 15:52:55 +0000 Subject: [PATCH 1/3] Remove the bindings and the C core Glyph11 is managed-only from here on: the bindings are no longer supported, so carrying them means maintaining a second and third implementation of every rule (and a Gradle toolchain) for packages that will not be released again. Removed: bindings/ (Kotlin, Glyph11.Native, Glyph11.Pico with the vendored picohttpparser), core/ (the C parser, its tests and the C-vs-C# differential harness), the two native example projects, and the core-tests / package-native / package-pico workflows. release.yml now publishes only Glyph11, and the solution drops the four binding projects. The benchmark keeps the Glyph11 measurements and loses the native columns and decoder. InternalsVisibleTo("Glyph11.Pico") stays in Glyph11.csproj. The published Glyph11.Pico 0.0.1 sets BinaryRequest's internal setters and calls KeyValueList.Add, and the runtime enforces friend access, so dropping the attribute would make Pico fail with MethodAccessException against the next Glyph11 release - GenHTTP still ships it next to Glyph11. Closes #57, closes #66, closes #69, closes #70, closes #72. --- .github/workflows/core-tests.yml | 44 -- .github/workflows/package-native.yml | 170 ----- .github/workflows/package-pico.yml | 170 ----- .github/workflows/release.yml | 29 +- .gitignore | 8 - Benchmarks/Benchmarks.csproj | 3 - Benchmarks/Program.cs | 60 +- .../Glyph11.Native.Example.csproj | 19 - Examples/Glyph11.Native.Example/Program.cs | 180 ----- .../Glyph11.Pico.Example.csproj | 19 - Examples/Glyph11.Pico.Example/Program.cs | 139 ---- .../Glyph11.Native/Glyph11.Native.csproj | 42 - .../dotnet/Glyph11.Native/Glyph11Chunked.cs | 77 -- .../dotnet/Glyph11.Native/Glyph11Parser.cs | 175 ----- .../dotnet/Glyph11.Native/NativeResolver.cs | 29 - bindings/dotnet/Glyph11.Native/README.md | 64 -- .../dotnet/Glyph11.Pico/Glyph11.Pico.csproj | 44 -- .../dotnet/Glyph11.Pico/NativeResolver.cs | 28 - bindings/dotnet/Glyph11.Pico/PicoInterop.cs | 33 - bindings/dotnet/Glyph11.Pico/PicoParser.cs | 129 ---- bindings/dotnet/Glyph11.Pico/README.md | 63 -- .../dotnet/Glyph11.Pico/native/CMakeLists.txt | 26 - .../dotnet/Glyph11.Pico/native/pico_shim.c | 39 - .../dotnet/Glyph11.Pico/native/pico_shim.h | 37 - .../Glyph11.Pico/native/picohttpparser.c | 707 ----------------- .../Glyph11.Pico/native/picohttpparser.h | 90 --- bindings/kotlin/.gitignore | 2 - bindings/kotlin/README.md | 42 - bindings/kotlin/build.gradle.kts | 24 - .../kotlin/gradle/wrapper/gradle-wrapper.jar | Bin 43764 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - bindings/kotlin/gradlew | 251 ------ bindings/kotlin/gradlew.bat | 94 --- bindings/kotlin/settings.gradle.kts | 1 - .../src/main/kotlin/io/glyph11/Glyph11.kt | 135 ---- .../kotlin/src/main/kotlin/io/glyph11/Main.kt | 43 -- core/.gitignore | 3 - core/CMakeLists.txt | 76 -- core/diff/Glyph11.Diff.csproj | 17 - core/diff/Native.cs | 48 -- core/diff/Program.cs | 197 ----- core/diff/README.md | 35 - core/diff/run-diff.sh | 13 - core/include/glyph11.h | 209 ----- core/src/glyph11.c | 719 ------------------ core/tests/fuzz_smoke.c | 52 -- core/tests/test_glyph11.c | 256 ------- site/glyph11-native.html | 206 ----- site/glyph11-pico.html | 152 ---- src/Glyph11.sln | 56 -- src/Glyph11/Glyph11.csproj | 6 +- 51 files changed, 25 insertions(+), 5043 deletions(-) delete mode 100644 .github/workflows/core-tests.yml delete mode 100644 .github/workflows/package-native.yml delete mode 100644 .github/workflows/package-pico.yml delete mode 100644 Examples/Glyph11.Native.Example/Glyph11.Native.Example.csproj delete mode 100644 Examples/Glyph11.Native.Example/Program.cs delete mode 100644 Examples/Glyph11.Pico.Example/Glyph11.Pico.Example.csproj delete mode 100644 Examples/Glyph11.Pico.Example/Program.cs delete mode 100644 bindings/dotnet/Glyph11.Native/Glyph11.Native.csproj delete mode 100644 bindings/dotnet/Glyph11.Native/Glyph11Chunked.cs delete mode 100644 bindings/dotnet/Glyph11.Native/Glyph11Parser.cs delete mode 100644 bindings/dotnet/Glyph11.Native/NativeResolver.cs delete mode 100644 bindings/dotnet/Glyph11.Native/README.md delete mode 100644 bindings/dotnet/Glyph11.Pico/Glyph11.Pico.csproj delete mode 100644 bindings/dotnet/Glyph11.Pico/NativeResolver.cs delete mode 100644 bindings/dotnet/Glyph11.Pico/PicoInterop.cs delete mode 100644 bindings/dotnet/Glyph11.Pico/PicoParser.cs delete mode 100644 bindings/dotnet/Glyph11.Pico/README.md delete mode 100644 bindings/dotnet/Glyph11.Pico/native/CMakeLists.txt delete mode 100644 bindings/dotnet/Glyph11.Pico/native/pico_shim.c delete mode 100644 bindings/dotnet/Glyph11.Pico/native/pico_shim.h delete mode 100644 bindings/dotnet/Glyph11.Pico/native/picohttpparser.c delete mode 100644 bindings/dotnet/Glyph11.Pico/native/picohttpparser.h delete mode 100644 bindings/kotlin/.gitignore delete mode 100644 bindings/kotlin/README.md delete mode 100644 bindings/kotlin/build.gradle.kts delete mode 100644 bindings/kotlin/gradle/wrapper/gradle-wrapper.jar delete mode 100644 bindings/kotlin/gradle/wrapper/gradle-wrapper.properties delete mode 100755 bindings/kotlin/gradlew delete mode 100644 bindings/kotlin/gradlew.bat delete mode 100644 bindings/kotlin/settings.gradle.kts delete mode 100644 bindings/kotlin/src/main/kotlin/io/glyph11/Glyph11.kt delete mode 100644 bindings/kotlin/src/main/kotlin/io/glyph11/Main.kt delete mode 100644 core/.gitignore delete mode 100644 core/CMakeLists.txt delete mode 100644 core/diff/Glyph11.Diff.csproj delete mode 100644 core/diff/Native.cs delete mode 100644 core/diff/Program.cs delete mode 100644 core/diff/README.md delete mode 100755 core/diff/run-diff.sh delete mode 100644 core/include/glyph11.h delete mode 100644 core/src/glyph11.c delete mode 100644 core/tests/fuzz_smoke.c delete mode 100644 core/tests/test_glyph11.c delete mode 100644 site/glyph11-native.html delete mode 100644 site/glyph11-pico.html diff --git a/.github/workflows/core-tests.yml b/.github/workflows/core-tests.yml deleted file mode 100644 index 5cf6415..0000000 --- a/.github/workflows/core-tests.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Core Tests - -# The C core's test suite (previously only run locally). Covers the portable SSE2 -# build, the AVX2 build shipped for linux-x64, and an ASan/UBSan run. -on: - push: - paths: - - 'core/**' - - '.github/workflows/core-tests.yml' - pull_request: - paths: - - 'core/**' - - '.github/workflows/core-tests.yml' - -permissions: - contents: read - -jobs: - test: - name: ${{ matrix.name }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - name: sse2 (portable baseline) - cmake: -DCMAKE_BUILD_TYPE=Release - - name: avx2 (linux-x64 package) - cmake: -DCMAKE_BUILD_TYPE=Release -DGLYPH11_X86_AVX2=ON - need_avx2: true - - name: asan+ubsan - cmake: -DCMAKE_BUILD_TYPE=Debug -DGLYPH11_SANITIZE=ON - steps: - - name: Checkout source - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - - name: Build & test - run: | - if [ "${{ matrix.need_avx2 }}" = "true" ] && ! grep -q avx2 /proc/cpuinfo; then - echo "::warning::runner lacks AVX2 — skipping AVX2 test"; exit 0 - fi - cmake -S core -B core/build ${{ matrix.cmake }} - cmake --build core/build - ctest --test-dir core/build --output-on-failure diff --git a/.github/workflows/package-native.yml b/.github/workflows/package-native.yml deleted file mode 100644 index 1c50cf5..0000000 --- a/.github/workflows/package-native.yml +++ /dev/null @@ -1,170 +0,0 @@ -name: Package Native - -# Builds libglyph11 for every supported RID, bundles them into the Glyph11.Native -# NuGet (runtimes//native/), and — only on a `native-v*` tag or an explicit -# dispatch — pushes to NuGet.org. PRs touching the core/binding build + pack as a -# smoke test (artifact only, never published). -on: - workflow_dispatch: - inputs: - publish: - description: Push the packed NuGet to NuGet.org - type: boolean - default: false - workflow_call: # called by release.yml to publish alongside the other packages - inputs: - publish: - type: boolean - default: false - push: - tags: - - 'native-v*' - pull_request: - paths: - - 'core/**' - - 'bindings/dotnet/**' - - '.github/workflows/package-native.yml' - -permissions: - contents: read - -jobs: - - build: - name: native ${{ matrix.rid }} - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - include: - - rid: linux-x64 - runner: ubuntu-latest - # AVX2 microarch level: the 256-bit scanners inline (~15% faster on large - # headers). Requires a 2013+ (Haswell) CPU — fine for modern linux-x64. - cmake: -DGLYPH11_X86_AVX2=ON - - rid: linux-arm64 - runner: ubuntu-latest - apt: gcc-aarch64-linux-gnu - cmake: -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=aarch64 -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc - - rid: win-x64 - runner: windows-latest - cmake: -A x64 - - rid: win-arm64 - runner: windows-latest - cmake: -A ARM64 - - rid: osx-x64 - runner: macos-14 - cmake: -DCMAKE_OSX_ARCHITECTURES=x86_64 - - rid: osx-arm64 - runner: macos-14 - cmake: -DCMAKE_OSX_ARCHITECTURES=arm64 - steps: - - name: Checkout source - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - - name: Install cross toolchain - if: matrix.apt != '' - run: sudo apt-get update && sudo apt-get install -y ${{ matrix.apt }} - - - name: Configure - run: cmake -S core -B core/build-pkg -DCMAKE_BUILD_TYPE=Release -DGLYPH11_BUILD_TESTS=OFF ${{ matrix.cmake }} - - - name: Build - run: cmake --build core/build-pkg --config Release --target glyph11_shared - - - name: Stage (cmake --install -> stage//native/) - run: cmake --install core/build-pkg --config Release --prefix stage/${{ matrix.rid }} - - - name: Upload native artifact - uses: actions/upload-artifact@v4 - with: - name: native-${{ matrix.rid }} - # `path: stage` keeps artifact paths relative to stage/, i.e. /native/, - # so the pack job can merge all six straight into runtimes/. - path: stage - if-no-files-found: error - - pack: - name: Pack Glyph11.Native - needs: build - runs-on: ubuntu-latest - outputs: - version: ${{ steps.ver.outputs.version }} - steps: - - name: Checkout source - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 0 - - - name: Setup .NET SDKs - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 - with: - dotnet-version: | - 8.0 - 9.0 - 10.0 - - - name: Gather native binaries -> runtimes/ - uses: actions/download-artifact@v4 - with: - pattern: native-* - merge-multiple: true - path: bindings/dotnet/Glyph11.Native/runtimes - - - name: Show staged runtimes - run: find bindings/dotnet/Glyph11.Native/runtimes -type f | sort - - - name: Resolve package version - id: ver - # tag native-vX.Y.Z -> X.Y.Z; publish (dispatch/release call) -> the csproj ; - # PR smoke test -> a throwaway CI prerelease. - run: | - ref='${{ github.ref }}' - if [[ "$ref" == refs/tags/native-v* ]]; then - echo "args=-p:Version=${ref#refs/tags/native-v}" >> "$GITHUB_OUTPUT" - elif [[ "${{ inputs.publish }}" == "true" ]]; then - echo "args=" >> "$GITHUB_OUTPUT" - else - echo "args=-p:Version=0.0.0-ci.${{ github.run_number }}" >> "$GITHUB_OUTPUT" - fi - - - name: Pack - run: > - dotnet pack bindings/dotnet/Glyph11.Native/Glyph11.Native.csproj - -c Release -p:ContinuousIntegrationBuild=true - ${{ steps.ver.outputs.args }} - -o artifacts - - - name: Upload package - uses: actions/upload-artifact@v4 - with: - name: Glyph11.Native-nupkg - path: | - artifacts/*.nupkg - artifacts/*.snupkg - if-no-files-found: error - - publish: - name: Publish to NuGet.org - needs: pack - # Only on a native-v* tag, or an explicit dispatch with publish=true. - if: startsWith(github.ref, 'refs/tags/native-v') || ((github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.publish) - runs-on: ubuntu-latest - environment: - name: production - steps: - - name: Setup .NET SDK - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 - with: - dotnet-version: '10.0' - - - name: Download package - uses: actions/download-artifact@v4 - with: - name: Glyph11.Native-nupkg - path: artifacts - - - name: Push to NuGet.org - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - run: dotnet nuget push "artifacts/*.nupkg" -k "$NUGET_API_KEY" -s https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.github/workflows/package-pico.yml b/.github/workflows/package-pico.yml deleted file mode 100644 index 6bc84a4..0000000 --- a/.github/workflows/package-pico.yml +++ /dev/null @@ -1,170 +0,0 @@ -name: Package Pico - -# Builds libglyph11pico (picohttpparser + shim) for every supported RID, bundles them -# into the Glyph11.Pico NuGet (runtimes//native/), and — only on a `pico-v*` tag or -# an explicit dispatch — pushes to NuGet.org. PRs touching the package build + pack as a -# smoke test (artifact only, never published). -on: - workflow_dispatch: - inputs: - publish: - description: Push the packed NuGet to NuGet.org - type: boolean - default: false - workflow_call: # called by release.yml to publish alongside the other packages - inputs: - publish: - type: boolean - default: false - push: - tags: - - 'pico-v*' - pull_request: - paths: - - 'bindings/dotnet/Glyph11.Pico/**' - - '.github/workflows/package-pico.yml' - -permissions: - contents: read - -env: - NATIVE_DIR: bindings/dotnet/Glyph11.Pico/native - -jobs: - - build: - name: pico ${{ matrix.rid }} - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - include: - - rid: linux-x64 - runner: ubuntu-latest - # SSE4.2 for picohttpparser's findchar_fast (via -march=x86-64-v3, Haswell+). - cmake: -DGLYPH11_PICO_X86_AVX2=ON - - rid: linux-arm64 - runner: ubuntu-latest - apt: gcc-aarch64-linux-gnu - cmake: -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=aarch64 -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc - - rid: win-x64 - runner: windows-latest - cmake: -A x64 - - rid: win-arm64 - runner: windows-latest - cmake: -A ARM64 - - rid: osx-x64 - runner: macos-14 - cmake: -DCMAKE_OSX_ARCHITECTURES=x86_64 -DGLYPH11_PICO_X86_AVX2=ON - - rid: osx-arm64 - runner: macos-14 - cmake: -DCMAKE_OSX_ARCHITECTURES=arm64 - steps: - - name: Checkout source - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - - name: Install cross toolchain - if: matrix.apt != '' - run: sudo apt-get update && sudo apt-get install -y ${{ matrix.apt }} - - - name: Configure - run: cmake -S "$NATIVE_DIR" -B "$NATIVE_DIR/build-pkg" -DCMAKE_BUILD_TYPE=Release ${{ matrix.cmake }} - shell: bash - - - name: Build - run: cmake --build "$NATIVE_DIR/build-pkg" --config Release --target glyph11pico - shell: bash - - - name: Stage (cmake --install -> stage//native/) - run: cmake --install "$NATIVE_DIR/build-pkg" --config Release --prefix stage/${{ matrix.rid }} - shell: bash - - - name: Upload native artifact - uses: actions/upload-artifact@v4 - with: - name: pico-${{ matrix.rid }} - path: stage - if-no-files-found: error - - pack: - name: Pack Glyph11.Pico - needs: build - runs-on: ubuntu-latest - steps: - - name: Checkout source - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 0 - - - name: Setup .NET SDKs - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 - with: - dotnet-version: | - 8.0 - 9.0 - 10.0 - - - name: Gather native binaries -> runtimes/ - uses: actions/download-artifact@v4 - with: - pattern: pico-* - merge-multiple: true - path: bindings/dotnet/Glyph11.Pico/runtimes - - - name: Show staged runtimes - run: find bindings/dotnet/Glyph11.Pico/runtimes -type f | sort - - - name: Resolve package version - id: ver - # tag pico-vX.Y.Z -> X.Y.Z; publish (dispatch/release call) -> the csproj ; - # PR smoke test -> a throwaway CI prerelease. - run: | - ref='${{ github.ref }}' - if [[ "$ref" == refs/tags/pico-v* ]]; then - echo "args=-p:Version=${ref#refs/tags/pico-v}" >> "$GITHUB_OUTPUT" - elif [[ "${{ inputs.publish }}" == "true" ]]; then - echo "args=" >> "$GITHUB_OUTPUT" - else - echo "args=-p:Version=0.0.1-ci.${{ github.run_number }}" >> "$GITHUB_OUTPUT" - fi - - - name: Pack - run: > - dotnet pack bindings/dotnet/Glyph11.Pico/Glyph11.Pico.csproj - -c Release -p:ContinuousIntegrationBuild=true - ${{ steps.ver.outputs.args }} - -o artifacts - - - name: Upload package - uses: actions/upload-artifact@v4 - with: - name: Glyph11.Pico-nupkg - path: | - artifacts/*.nupkg - artifacts/*.snupkg - if-no-files-found: error - - publish: - name: Publish to NuGet.org - needs: pack - # Only on a pico-v* tag, or an explicit dispatch with publish=true. - if: startsWith(github.ref, 'refs/tags/pico-v') || ((github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.publish) - runs-on: ubuntu-latest - environment: - name: production - steps: - - name: Setup .NET SDK - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 - with: - dotnet-version: '10.0' - - - name: Download package - uses: actions/download-artifact@v4 - with: - name: Glyph11.Pico-nupkg - path: artifacts - - - name: Push to NuGet.org - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - run: dotnet nuget push "artifacts/*.nupkg" -k "$NUGET_API_KEY" -s https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21706a0..60344d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,8 @@ name: Release -# Publishes all three NuGet packages at their csproj versions: -# Glyph11 (managed) here, and Glyph11.Native / Glyph11.Pico via their package -# workflows (which run the full native build matrices, then pack + push). -# Every push uses --skip-duplicate, so re-running only publishes versions not yet -# on NuGet.org — bump a package's and re-run to release it. +# Publishes the Glyph11 NuGet package at its csproj version. +# The push uses --skip-duplicate, so re-running only publishes a version not yet +# on NuGet.org — bump the and re-run to release it. on: workflow_dispatch: @@ -33,9 +31,8 @@ jobs: 9.0 10.0 - # Pack only the Glyph11 package, not the whole src solution — the solution now also - # contains the binding projects (so the examples resolve in IDEs), and those publish - # via package-native.yml / package-pico.yml with their native binaries bundled. + # Pack only the Glyph11 package, not the whole src solution — the solution also + # holds the tests, the benchmark and the example, none of which ship. - name: Restore dependencies run: dotnet restore Glyph11/Glyph11.csproj working-directory: src @@ -57,19 +54,3 @@ jobs: env: NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} run: dotnet nuget push *.nupkg -k "$env:NUGET_API_KEY" -s https://api.nuget.org/v3/index.json --skip-duplicate - - # Build the 6-RID native matrix, pack, and publish Glyph11.Native at its csproj version. - native: - name: Glyph11.Native - uses: ./.github/workflows/package-native.yml - with: - publish: true - secrets: inherit - - # Build the 6-RID native matrix, pack, and publish Glyph11.Pico at its csproj version. - pico: - name: Glyph11.Pico - uses: ./.github/workflows/package-pico.yml - with: - publish: true - secrets: inherit diff --git a/.gitignore b/.gitignore index 3dea208..6560ca8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,14 +3,6 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore -# Native binaries staged per-RID for the Glyph11.Native NuGet (produced by the -# package-native CI matrix, never committed). -bindings/dotnet/Glyph11.Native/runtimes/ - -# Glyph11.Pico native build + staged binaries (CMake build dir, per-RID staging). -bindings/dotnet/Glyph11.Pico/native/build/ -bindings/dotnet/Glyph11.Pico/runtimes/ - # User-specific files *.rsuser *.suo diff --git a/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj index c410783..39d87dd 100644 --- a/Benchmarks/Benchmarks.csproj +++ b/Benchmarks/Benchmarks.csproj @@ -9,10 +9,7 @@ - - - diff --git a/Benchmarks/Program.cs b/Benchmarks/Program.cs index 2b8715e..ca71dc4 100644 --- a/Benchmarks/Program.cs +++ b/Benchmarks/Program.cs @@ -1,25 +1,16 @@ -// Reproduces the benchmark tables shown on the docs site: the three packages -// (Glyph11 managed, Glyph11.Native, Glyph11.Pico) on contiguous request parsing, -// multi-segment request parsing, and chunked-body decoding. Best-of-5, ns/op. +// Reproduces the benchmark tables shown on the docs site: Glyph11 on contiguous +// request parsing, multi-segment request parsing, and chunked-body decoding. +// Best-of-5, ns/op. // -// # build the native cores, then point at them and run: -// cmake -S core -B core/build -DGLYPH11_BUILD_TESTS=OFF && cmake --build core/build -// cmake -S bindings/dotnet/Glyph11.Pico/native -B pico-build && cmake --build pico-build -// GLYPH11_NATIVE_PATH="$PWD/core/build/libglyph11.so" \ -// GLYPH11_PICO_NATIVE_PATH="$PWD/pico-build/libglyph11pico.so" \ -// dotnet run -c Release --project Benchmarks -// -// (Glyph11 — pure managed — needs no native library.) +// dotnet run -c Release --project Benchmarks using System.Buffers; using System.Diagnostics; -using Glyph11.Native; -using Glyph11.Pico; using Glyph11.Protocol; using Glyph11.Parser; using Glyph11.Parser.UltraHardened; -// ---- payloads (identical bytes for every parser) ---- +// ---- payloads (identical bytes to the docs-site tables) ---- byte[] small = Payloads.Small; (string label, byte[] bytes)[] requests = { @@ -36,13 +27,7 @@ // ---- policy + reusable storage (no per-request allocation in the parse itself) ---- var managedLimits = ParserLimits.Default with { MaxHeaderCount = 200, MaxTotalHeaderBytes = 64 * 1024 }; -var nativeLimits = Glyph11Limits.Default; -nativeLimits.MaxHeaderCount = 200; -nativeLimits.MaxTotalHeaderBytes = 64 * 1024; -var headers = new Glyph11Field[256]; -var query = new Glyph11Field[256]; -var picoRequest = new BinaryRequest(); var managedRequest = new BinaryRequest(); var chunkOutput = new byte[64 * 1024]; @@ -55,9 +40,7 @@ var rom = (ReadOnlyMemory)bytes; long it = Iters(bytes.Length); double glyph = Best(it, () => { managedRequest.Clear(); var r = rom; UltraHardenedParser.TryExtractFullHeaderROM(ref r, managedRequest, in managedLimits, out _); }); - double native = Best(it, () => Glyph11Parser.Parse(bytes, headers, query, nativeLimits, out _)); - double pico = Best(it, () => PicoParser.TryParse(rom, picoRequest, out _)); - return (glyph, native, pico); + return glyph; }); // ---- multi-segment request parsing (3 segments, linearized per request, then parsed) ---- @@ -65,34 +48,27 @@ Table(requests, (label, bytes) => { var seq = ThreeSegments(bytes); - int len = bytes.Length; - long it = Iters(len); + long it = Iters(bytes.Length); double glyph = Best(it, () => { managedRequest.Clear(); var s = seq; UltraHardenedParser.TryExtractFullHeaderValidated(ref s, managedRequest, in managedLimits, out _); }); - double native = Best(it, () => { var buf = new byte[len]; seq.CopyTo(buf); Glyph11Parser.Parse(buf, headers, query, nativeLimits, out _); }); - double pico = Best(it, () => { var buf = new byte[len]; seq.CopyTo(buf); PicoParser.TryParse(buf, picoRequest, out _); }); - return (glyph, native, pico); + return glyph; }); -// ---- chunked body decoding (Glyph11 == Pico, which reuses ChunkedBodyStream) ---- -Console.WriteLine("\nChunked body decoding (Glyph11 / Pico share the decoder)"); -Console.WriteLine($" {"decoded",-8}{"Glyph11/Pico",16}{"Glyph11.Native",16}"); +// ---- chunked body decoding ---- +Console.WriteLine("\nChunked body decoding"); +Console.WriteLine($" {"decoded",-8}{"Glyph11",13}"); foreach (var (label, body) in chunked) { long it = body.Length < 4096 ? 1_000_000 : body.Length < 32_768 ? 300_000 : 50_000; double glyph = Best(it, () => DecodeManaged(body, chunkOutput)); - double native = Best(it, () => DecodeNative(body, chunkOutput)); - Console.WriteLine($" {label,-8}{glyph,13:F0} ns{native,13:F0} ns"); + Console.WriteLine($" {label,-8}{glyph,10:F0} ns"); } // ───────────────────────────────────────────────────────────────────────────── -void Table((string label, byte[] bytes)[] cases, Func run) +void Table((string label, byte[] bytes)[] cases, Func run) { - Console.WriteLine($" {"payload",-8}{"Glyph11",13}{"Glyph11.Native",17}{"Glyph11.Pico",15}"); + Console.WriteLine($" {"payload",-8}{"Glyph11",13}"); foreach (var (label, bytes) in cases) - { - var (g, n, p) = run(label, bytes); - Console.WriteLine($" {label,-8}{g,10:F0} ns{n,14:F0} ns{p,12:F0} ns"); - } + Console.WriteLine($" {label,-8}{run(label, bytes),10:F0} ns"); } static void DecodeManaged(byte[] body, byte[] output) @@ -109,12 +85,6 @@ static void DecodeManaged(byte[] body, byte[] output) } } -static void DecodeNative(byte[] body, byte[] output) -{ - Glyph11Chunked.Init(out var decoder); - Glyph11Chunked.Decode(ref decoder, body, output, out _, out _); -} - // best of N timed trials (after warmup) — filters scheduling / turbo interference static double Best(long iters, Action body) { diff --git a/Examples/Glyph11.Native.Example/Glyph11.Native.Example.csproj b/Examples/Glyph11.Native.Example/Glyph11.Native.Example.csproj deleted file mode 100644 index 49f77db..0000000 --- a/Examples/Glyph11.Native.Example/Glyph11.Native.Example.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - false - - - - - - - - diff --git a/Examples/Glyph11.Native.Example/Program.cs b/Examples/Glyph11.Native.Example/Program.cs deleted file mode 100644 index e16bcce..0000000 --- a/Examples/Glyph11.Native.Example/Program.cs +++ /dev/null @@ -1,180 +0,0 @@ -// Glyph11.Native (the C core via P/Invoke) — every option, end to end. -// -// dotnet run --project Examples/Glyph11.Native.Example -// -// The NuGet package bundles the native libglyph11. Running in-repo, point at a built core: -// GLYPH11_NATIVE_PATH=core/build/libglyph11.so dotnet run --project Examples/Glyph11.Native.Example -// (see Examples/README.md). Output is zero-copy: the parser writes (offset,length) spans -// into arrays you provide; you slice the input buffer to read them. - -using System.Buffers; -using System.Text; -using Glyph11.Native; - -Console.WriteLine($"libglyph11 ABI = 0x{Glyph11Parser.AbiVersion():x6}\n"); - -ContiguousParse(); -SequenceParse(); -CustomLimitsAndPooling(); -StatusCodes(); -ChunkedDecode(); - -// ───────────────────────────────────────────────────────────────────────────── -// 1. Parse a contiguous buffer. You supply the field storage; nothing is allocated. -// ───────────────────────────────────────────────────────────────────────────── -void ContiguousParse() -{ - Console.WriteLine("== 1. Contiguous parse =="); - - byte[] request = Encoding.ASCII.GetBytes( - "GET /api/users?page=1 HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n"); - - var limits = Glyph11Limits.Default; - - // Size storage to the limits so any request the policy accepts fits. The parser - // bounds-checks every write — a smaller array just lowers your effective limit. - Span headers = stackalloc Glyph11Field[(int)limits.MaxHeaderCount]; - Span query = stackalloc Glyph11Field[(int)limits.MaxQueryParameterCount]; - - int status = Glyph11Parser.Parse(request, headers, query, limits, out Glyph11Result r); - if (status == Glyph11Parser.Ok) - { - // Offsets index into `request` — slice it to read. - string Slice(Glyph11Span s) => Encoding.ASCII.GetString(request, (int)s.Offset, (int)s.Length); - Console.WriteLine($" method = {Slice(r.Method)}"); - Console.WriteLine($" path = {Slice(r.Path)}"); // query stripped - Console.WriteLine($" target = {Slice(r.Target)}"); // full target, as received - Console.WriteLine($" version = {Slice(r.Version)}"); - - for (int i = 0; i < r.HeaderCount; i++) - Console.WriteLine($" header {Slice(headers[i].Name)}: {Slice(headers[i].Value)}"); - for (int i = 0; i < r.QueryCount; i++) - Console.WriteLine($" query {Slice(query[i].Name)} = {Slice(query[i].Value)}"); - - Console.WriteLine($" body begins at byte {r.Consumed}"); - } - Console.WriteLine(); -} - -// ───────────────────────────────────────────────────────────────────────────── -// 2. Parse a fragmented ReadOnlySequence. The C core needs one contiguous buffer, -// so multi-segment input is linearized into a scratch buffer YOU provide -// (keeping the zero-allocation contract). `parsed` tells you which buffer the -// offsets index into. -// ───────────────────────────────────────────────────────────────────────────── -void SequenceParse() -{ - Console.WriteLine("== 2. ReadOnlySequence parse =="); - - byte[] all = Encoding.ASCII.GetBytes("GET /x?a=1 HTTP/1.1\r\nHost: h\r\n\r\n"); - ReadOnlySequence seq = ThreeSegments(all); // simulate fragmented reads - - var limits = Glyph11Limits.Default; - Span headers = stackalloc Glyph11Field[(int)limits.MaxHeaderCount]; - Span query = stackalloc Glyph11Field[(int)limits.MaxQueryParameterCount]; - - // scratch only needs to hold a request when the input is fragmented (single-segment - // input is parsed in place). Size it to MaxTotalHeaderBytes. - Span scratch = stackalloc byte[8 * 1024]; - - int status = Glyph11Parser.Parse( - seq, scratch, headers, query, limits, - out Glyph11Result r, - out ReadOnlySpan parsed); // ← the contiguous bytes the offsets index into - - if (status == Glyph11Parser.Ok) - { - // Slice against `parsed` (input's first segment if single, else `scratch`) — NOT `seq`. - Console.WriteLine($" method = {Encoding.ASCII.GetString(parsed.Slice((int)r.Method.Offset, (int)r.Method.Length))}"); - Console.WriteLine($" path = {Encoding.ASCII.GetString(parsed.Slice((int)r.Path.Offset, (int)r.Path.Length))}\n"); - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// 3. A custom policy, and pooled storage for large limits (instead of stackalloc). -// ───────────────────────────────────────────────────────────────────────────── -void CustomLimitsAndPooling() -{ - Console.WriteLine("== 3. Custom limits + pooled storage =="); - - var limits = Glyph11Limits.Default; // Glyph11Limits is a struct — copy and set fields - limits.MaxHeaderCount = 200; - limits.MaxTotalHeaderBytes = 64 * 1024; - - byte[] request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost: x\r\n\r\n"); - - // Big limits → rent from ArrayPool rather than blowing the stack. - Glyph11Field[] headers = ArrayPool.Shared.Rent((int)limits.MaxHeaderCount); - Glyph11Field[] query = ArrayPool.Shared.Rent((int)limits.MaxQueryParameterCount); - try - { - int status = Glyph11Parser.Parse(request, headers, query, limits, out var r); - Console.WriteLine($" status={status} headers={r.HeaderCount}\n"); - } - finally - { - ArrayPool.Shared.Return(headers); - ArrayPool.Shared.Return(query); - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// 4. The status int: 0 = OK, 1 = incomplete, anything else maps to an HTTP code. -// ───────────────────────────────────────────────────────────────────────────── -void StatusCodes() -{ - Console.WriteLine("== 4. Status codes =="); - var limits = Glyph11Limits.Default; - Span h = stackalloc Glyph11Field[(int)limits.MaxHeaderCount]; - Span q = stackalloc Glyph11Field[(int)limits.MaxQueryParameterCount]; - - // Incomplete: header block not terminated → status 1 (read more, retry). - int incomplete = Glyph11Parser.Parse( - Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost: x\r\n"), h, q, limits, out _); - Console.WriteLine($" incomplete → status {incomplete} (Incomplete = {Glyph11Parser.Incomplete})"); - - // Invalid: missing Host → an error status that maps to HTTP 400. - int bad = Glyph11Parser.Parse( - Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\n\r\n"), h, q, limits, out _); - Console.WriteLine($" no-Host → status {bad} → HTTP {Glyph11Parser.HttpCode(bad)}\n"); -} - -// ───────────────────────────────────────────────────────────────────────────── -// 5. Decode a chunked body with the streaming native decoder. Feed each read; a -// chunk's payload may span calls and the decoder carries the partial state. -// ───────────────────────────────────────────────────────────────────────────── -void ChunkedDecode() -{ - Console.WriteLine("== 5. Chunked decode =="); - - byte[] chunked = Encoding.ASCII.GetBytes("5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n"); - - Glyph11Chunked.Init(out Glyph11ChunkDecoder decoder); // one decoder per body (zeroed state) - Span output = stackalloc byte[256]; // decoded bytes land here - - Glyph11ChunkResult r = Glyph11Chunked.Decode( - ref decoder, chunked, output, out int inConsumed, out int outWritten); - - Console.WriteLine($" result={r} consumed={inConsumed} wrote={outWritten}"); - Console.WriteLine($" decoded = \"{Encoding.ASCII.GetString(output[..outWritten])}\"\n"); // Hello World -} - -// ───────────────────────────────────────────────────────────────────────────── -static ReadOnlySequence ThreeSegments(byte[] data) -{ - int s1 = data.Length / 3, s2 = 2 * data.Length / 3; - var first = new Seg(data.AsMemory(0, s1)); - var last = first.Append(data.AsMemory(s1, s2 - s1)).Append(data.AsMemory(s2)); - return new ReadOnlySequence(first, 0, last, last.Memory.Length); -} - -sealed class Seg : ReadOnlySequenceSegment -{ - public Seg(ReadOnlyMemory memory) => Memory = memory; - public Seg Append(ReadOnlyMemory memory) - { - var next = new Seg(memory) { RunningIndex = RunningIndex + Memory.Length }; - Next = next; - return next; - } -} diff --git a/Examples/Glyph11.Pico.Example/Glyph11.Pico.Example.csproj b/Examples/Glyph11.Pico.Example/Glyph11.Pico.Example.csproj deleted file mode 100644 index 0265017..0000000 --- a/Examples/Glyph11.Pico.Example/Glyph11.Pico.Example.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - false - - - - - - - - diff --git a/Examples/Glyph11.Pico.Example/Program.cs b/Examples/Glyph11.Pico.Example/Program.cs deleted file mode 100644 index 227c2b0..0000000 --- a/Examples/Glyph11.Pico.Example/Program.cs +++ /dev/null @@ -1,139 +0,0 @@ -// Glyph11.Pico (picohttpparser + managed glue) — every option, end to end. -// -// dotnet run --project Examples/Glyph11.Pico.Example -// -// Fills the SAME BinaryRequest as the managed Glyph11 parser, but only picohttpparser's -// validation — fastest path to a request, no hardening. The NuGet bundles libglyph11pico; -// running in-repo, point at a built lib (see Examples/README.md): -// GLYPH11_PICO_NATIVE_PATH=<...>/libglyph11pico.so dotnet run --project Examples/Glyph11.Pico.Example - -using System.Buffers; -using System.Text; -using Glyph11.Pico; -using Glyph11.Protocol; // BinaryRequest -using Glyph11.Parser; // ChunkedBodyStream (chunked decoding is glyph11's) - -// Same reusable BinaryRequest as the managed parser — allocate once, Clear() per request. -var request = new BinaryRequest(); - -ContiguousParse(); -SequenceParse(); -ChunkedBody(); -ValidationTradeoff(); - -request.Dispose(); - -// ───────────────────────────────────────────────────────────────────────────── -// 1. Parse a contiguous buffer into a BinaryRequest (identical shape to Glyph11). -// ───────────────────────────────────────────────────────────────────────────── -void ContiguousParse() -{ - Console.WriteLine("== 1. Contiguous parse =="); - - byte[] input = "GET /api/users?page=1&sort=asc HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n"u8.ToArray(); - - request.Clear(); - - // true → parsed; false → malformed or incomplete. `consumed` follows glyph11's -1 - // convention (the body begins at consumed + 1). - if (PicoParser.TryParse(input, request, out int consumed)) - { - Console.WriteLine($" method = {Ascii(request.Method)}"); - Console.WriteLine($" path = {Ascii(request.Path)}"); // query stripped - Console.WriteLine($" version = {Ascii(request.Version)}"); - - for (int i = 0; i < request.Headers.Count; i++) - { - var (name, value) = request.Headers[i]; - Console.WriteLine($" header {Ascii(name)}: {Ascii(value)}"); - } - for (int i = 0; i < request.QueryParameters.Count; i++) - { - var (key, val) = request.QueryParameters[i]; - Console.WriteLine($" query {Ascii(key)} = {Ascii(val)}"); - } - Console.WriteLine($" body begins at byte {consumed + 1}"); - } - Console.WriteLine(); -} - -// ───────────────────────────────────────────────────────────────────────────── -// 2. Parse a fragmented ReadOnlySequence. Single-segment is zero-copy; multi-segment -// is linearized into a fresh array internally — which the BinaryRequest slices keep -// alive, so there's no buffer for you to manage. -// ───────────────────────────────────────────────────────────────────────────── -void SequenceParse() -{ - Console.WriteLine("== 2. ReadOnlySequence parse =="); - - byte[] all = "GET /x?a=1 HTTP/1.1\r\nHost: h\r\n\r\n"u8.ToArray(); - ReadOnlySequence seq = ThreeSegments(all); - - request.Clear(); - if (PicoParser.TryParse(seq, request, out _)) - Console.WriteLine($" parsed {Ascii(request.Method)} {Ascii(request.Path)} from 3 segments\n"); -} - -// ───────────────────────────────────────────────────────────────────────────── -// 3. Chunked bodies use glyph11's decoder (this package depends on Glyph11). -// ───────────────────────────────────────────────────────────────────────────── -void ChunkedBody() -{ - Console.WriteLine("== 3. Chunked body (via Glyph11.Parser.ChunkedBodyStream) =="); - - byte[] body = "5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n"u8.ToArray(); - var decoder = new ChunkedBodyStream(); - var decoded = new ArrayBufferWriter(); - int offset = 0; - - while (true) - { - ChunkResult r = decoder.TryReadChunk( - body.AsSpan(offset), out int consumed, out int dataOffset, out int dataLength); - if (r == ChunkResult.Chunk) { decoded.Write(body.AsSpan(offset + dataOffset, dataLength)); offset += consumed; continue; } - break; // Completed (done) or NeedMoreData (read more) - } - Console.WriteLine($" decoded = \"{Encoding.ASCII.GetString(decoded.WrittenSpan)}\"\n"); -} - -// ───────────────────────────────────────────────────────────────────────────── -// 4. The trade-off: Pico does NOT do glyph11's hardening. A path-traversal request -// that the managed/native parser rejects is happily tokenized here — so only use -// Pico when you validate elsewhere or trust the source. -// ───────────────────────────────────────────────────────────────────────────── -void ValidationTradeoff() -{ - Console.WriteLine("== 4. Validation trade-off =="); - - // Glyph11 / Glyph11.Native would throw / reject this (dot-segment traversal). Pico parses it. - byte[] evil = "GET /a/../../etc/passwd HTTP/1.1\r\nHost: x\r\n\r\n"u8.ToArray(); - - request.Clear(); - if (PicoParser.TryParse(evil, request, out _)) - { - Console.WriteLine($" Pico accepted path = {Ascii(request.Path)}"); - Console.WriteLine(" → validate paths/tokens yourself when using Pico on untrusted input.\n"); - } -} - -// ───────────────────────────────────────────────────────────────────────────── -static string Ascii(ReadOnlyMemory m) => Encoding.ASCII.GetString(m.Span); - -static ReadOnlySequence ThreeSegments(byte[] data) -{ - int s1 = data.Length / 3, s2 = 2 * data.Length / 3; - var first = new Seg(data.AsMemory(0, s1)); - var last = first.Append(data.AsMemory(s1, s2 - s1)).Append(data.AsMemory(s2)); - return new ReadOnlySequence(first, 0, last, last.Memory.Length); -} - -sealed class Seg : ReadOnlySequenceSegment -{ - public Seg(ReadOnlyMemory memory) => Memory = memory; - public Seg Append(ReadOnlyMemory memory) - { - var next = new Seg(memory) { RunningIndex = RunningIndex + Memory.Length }; - Next = next; - return next; - } -} diff --git a/bindings/dotnet/Glyph11.Native/Glyph11.Native.csproj b/bindings/dotnet/Glyph11.Native/Glyph11.Native.csproj deleted file mode 100644 index 5f09062..0000000 --- a/bindings/dotnet/Glyph11.Native/Glyph11.Native.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - net8.0;net9.0;net10.0 - true - enable - latest - Glyph11.Native - - - Glyph11.Native - 0.1.0 - 0.1.0.0 - 0.1.0.0 - Native (P/Invoke) binding for the Glyph11 hardened HTTP/1.1 parser — the C core (libglyph11) reachable from .NET, with native binaries bundled for linux-x64/arm64, win-x64/arm64 and osx-x64/arm64. - C HTTP Protocol Parser P/Invoke Native FFI Interop - - https://github.com/MDA2AV/Glyph11 - MIT - README.md - git - https://github.com/MDA2AV/Glyph11 - true - - true - snupkg - true - true - - - - - - - - - diff --git a/bindings/dotnet/Glyph11.Native/Glyph11Chunked.cs b/bindings/dotnet/Glyph11.Native/Glyph11Chunked.cs deleted file mode 100644 index afce1cd..0000000 --- a/bindings/dotnet/Glyph11.Native/Glyph11Chunked.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Glyph11.Native; - -/// Streaming chunked transfer-encoding decoder state (mirrors the C glyph11_chunk_decoder). -[StructLayout(LayoutKind.Sequential)] -public struct Glyph11ChunkDecoder -{ - public uint Phase; - public uint DigitCount; - public uint ExtBytes; - public uint Reserved; - public ulong ChunkSize; - public ulong Remaining; -} - -/// Result of . -public enum Glyph11ChunkResult -{ - /// Progress made — drain the output, supply more input, and call again. - Ok = 0, - - /// Terminal chunk and trailers consumed; the body is complete. - Done = 1, - - /// Malformed chunked encoding — the server should respond 400. - Error = 2, -} - -/// -/// Managed wrapper over the Glyph11 C streaming chunked-body decoder (libglyph11). -/// Strips chunk framing and writes the decoded body into a caller-provided buffer — no -/// allocation. Reuse one across calls for a single body. -/// -public static class Glyph11Chunked -{ - private const string Lib = "glyph11"; - - /// Reset a decoder to the start state (its zero value is the start state). - public static void Init(out Glyph11ChunkDecoder decoder) => decoder = default; - - /// - /// Decode chunked data: strip the framing, writing decoded body bytes into . - /// Call repeatedly as data arrives; a chunk's payload may be split across calls. - /// - /// - /// when the body is complete, - /// on a malformed encoding, otherwise - /// — the call stopped because input was exhausted or output - /// filled; advance by , drain , call again. - /// - public static unsafe Glyph11ChunkResult Decode( - ref Glyph11ChunkDecoder decoder, - ReadOnlySpan input, - Span output, - out int inConsumed, - out int outWritten) - { - fixed (Glyph11ChunkDecoder* pd = &decoder) - fixed (byte* pin = input) - fixed (byte* pout = output) - { - nuint consumed, written; - int r = glyph11_chunk_decode(pd, pin, (nuint)input.Length, pout, (nuint)output.Length, &consumed, &written); - inConsumed = (int)consumed; - outWritten = (int)written; - return (Glyph11ChunkResult)r; - } - } - - [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] - private static extern unsafe int glyph11_chunk_decode( - Glyph11ChunkDecoder* dec, - byte* @in, nuint inLen, - byte* @out, nuint outCap, - nuint* inConsumed, nuint* outWritten); -} diff --git a/bindings/dotnet/Glyph11.Native/Glyph11Parser.cs b/bindings/dotnet/Glyph11.Native/Glyph11Parser.cs deleted file mode 100644 index 8e6987a..0000000 --- a/bindings/dotnet/Glyph11.Native/Glyph11Parser.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System.Buffers; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Glyph11.Native; - -/// A byte range (offset + length) into the parsed input buffer (zero-copy). -[StructLayout(LayoutKind.Sequential)] -public struct Glyph11Span -{ - public uint Offset; - public uint Length; -} - -/// A parsed name/value pair (header or query parameter). -[StructLayout(LayoutKind.Sequential)] -public struct Glyph11Field -{ - public Glyph11Span Name; - public Glyph11Span Value; -} - -/// Resource limits; mirrors the C glyph11_limits struct. -[StructLayout(LayoutKind.Sequential)] -public struct Glyph11Limits -{ - public uint StructSize; - public uint MaxHeaderCount; - public uint MaxHeaderNameLength; - public uint MaxHeaderValueLength; - public uint MaxUrlLength; - public uint MaxQueryParameterCount; - public uint MaxMethodLength; - public uint MaxTotalHeaderBytes; - - public static unsafe Glyph11Limits Default => new() - { - StructSize = (uint)sizeof(Glyph11Limits), - MaxHeaderCount = 100, - MaxHeaderNameLength = 256, - MaxHeaderValueLength = 8192, - MaxUrlLength = 8192, - MaxQueryParameterCount = 128, - MaxMethodLength = 16, - MaxTotalHeaderBytes = 1048576, - }; -} - -/// Parsed request fields (spans into the input). Headers/query live in caller storage. -public struct Glyph11Result -{ - public Glyph11Span Method; - public Glyph11Span Target; - public Glyph11Span Path; - public Glyph11Span Version; - public int HeaderCount; - public int QueryCount; - public long Consumed; -} - -/// Managed wrapper over the Glyph11 C hardened parser (libglyph11). -public static class Glyph11Parser -{ - public const int Ok = 0; - public const int Incomplete = 1; - - /// - /// Parse one HTTP/1.1 request header block. and - /// are caller-provided storage the parser fills. - /// Returns the C glyph11_status (0 = OK, 1 = incomplete, else an error). - /// - public static unsafe int Parse( - ReadOnlySpan input, - Span headers, - Span query, - in Glyph11Limits limits, - out Glyph11Result result) - { - result = default; - Glyph11Limits lim = limits; // local so we can take its address - fixed (byte* pin = input) - fixed (Glyph11Field* ph = headers) - fixed (Glyph11Field* pq = query) - { - NRequest req; - req.Method = req.Target = req.Path = req.Version = default; - req.Headers = ph; req.HeaderCap = (uint)headers.Length; req.HeaderCount = 0; - req.Query = pq; req.QueryCap = (uint)query.Length; req.QueryCount = 0; - - nuint consumed; - int st = glyph11_parse_request(pin, (nuint)input.Length, &lim, &req, &consumed); - if (st == Ok) - { - result.Method = req.Method; - result.Target = req.Target; - result.Path = req.Path; - result.Version = req.Version; - result.HeaderCount = (int)req.HeaderCount; - result.QueryCount = (int)req.QueryCount; - result.Consumed = (long)consumed; - } - return st; - } - } - - /// Parse with the default resource limits. - public static int Parse(ReadOnlySpan input, Span headers, - Span query, out Glyph11Result result) - => Parse(input, headers, query, Glyph11Limits.Default, out result); - - /// - /// Parse from a (possibly fragmented) . Single-segment - /// input is parsed in place (zero-copy); multi-segment input is first linearized into - /// — the C core needs one contiguous buffer. The result's - /// offsets index into (the input's first segment, or - /// ); use it as the base when slicing fields. - /// must hold at least input.Length bytes when the - /// input is multi-segment (rent from , or size it to your - /// MaxTotalHeaderBytes limit); it is untouched for single-segment input. - /// - public static int Parse(in ReadOnlySequence input, Span scratch, - Span headers, Span query, - Glyph11Limits limits, out Glyph11Result result, - out ReadOnlySpan parsed) - { - if (input.IsSingleSegment) - { - parsed = input.FirstSpan; - } - else - { - int len = checked((int)input.Length); - input.CopyTo(scratch); // throws if scratch is smaller than the request - parsed = scratch.Slice(0, len); - } - return Parse(parsed, headers, query, limits, out result); - } - - /// Parse a with the default resource limits. - public static int Parse(in ReadOnlySequence input, Span scratch, - Span headers, Span query, - out Glyph11Result result, out ReadOnlySpan parsed) - => Parse(input, scratch, headers, query, Glyph11Limits.Default, out result, out parsed); - - /// HTTP response code for a status (400 / 431, or 0 for OK / incomplete). - public static int HttpCode(int status) => glyph11_status_http_code(status); - - /// Packed ABI version of the loaded native library. - public static uint AbiVersion() => glyph11_abi_version(); - - // ---- interop ---- - - [StructLayout(LayoutKind.Sequential)] - private unsafe struct NRequest - { - public Glyph11Span Method, Target, Path, Version; - public Glyph11Field* Headers; public uint HeaderCap; public uint HeaderCount; - public Glyph11Field* Query; public uint QueryCap; public uint QueryCount; - } - - private const string Lib = "glyph11"; - - [SuppressGCTransition] - [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] - private static extern unsafe int glyph11_parse_request( - byte* buf, nuint len, Glyph11Limits* limits, NRequest* req, nuint* consumed); - - [SuppressGCTransition] - [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] - private static extern int glyph11_status_http_code(int status); - - [SuppressGCTransition] - [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] - private static extern uint glyph11_abi_version(); -} diff --git a/bindings/dotnet/Glyph11.Native/NativeResolver.cs b/bindings/dotnet/Glyph11.Native/NativeResolver.cs deleted file mode 100644 index fd62dc5..0000000 --- a/bindings/dotnet/Glyph11.Native/NativeResolver.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Glyph11.Native; - -/// -/// Resolves the native glyph11 library from the GLYPH11_NATIVE_PATH -/// environment variable when set (an explicit path to libglyph11.so/.dll/.dylib), -/// falling back to the default OS search otherwise. Lets tests and benchmarks point -/// at a freshly-built library without installing it. -/// -internal static class NativeResolver -{ - [ModuleInitializer] - internal static void Init() - => NativeLibrary.SetDllImportResolver(typeof(Glyph11Parser).Assembly, Resolve); - - private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? searchPath) - { - if (name == "glyph11") - { - var path = Environment.GetEnvironmentVariable("GLYPH11_NATIVE_PATH"); - if (!string.IsNullOrEmpty(path) && NativeLibrary.TryLoad(path, out var handle)) - return handle; - } - return IntPtr.Zero; // fall back to default resolution - } -} diff --git a/bindings/dotnet/Glyph11.Native/README.md b/bindings/dotnet/Glyph11.Native/README.md deleted file mode 100644 index 9aca72c..0000000 --- a/bindings/dotnet/Glyph11.Native/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Glyph11.Native - -Native (P/Invoke) binding for [**Glyph11**](https://github.com/MDA2AV/Glyph11) — a -zero-allocation, hardened HTTP/1.1 request parser. This package wraps the C core -(`libglyph11`) and **bundles the native binary for every supported platform**, so a -plain `` works with no extra setup: - -| OS | RIDs | -|----|------| -| Linux | `linux-x64`, `linux-arm64` | -| Windows | `win-x64`, `win-arm64` | -| macOS | `osx-x64`, `osx-arm64` | - -The right binary is resolved from `runtimes//native/` automatically at build/publish. - -> **`linux-x64` requires AVX2** (a 2013+ "Haswell" or newer CPU — universal on modern -> servers and cloud). It's built at the `x86-64-v3` microarchitecture level so the -> SIMD scanners inline into the parse loop (~15% faster on large headers). All other -> RIDs use the portable baseline. - -> Looking for the dependency-free, pure-managed parser? Use the -> [**`Glyph11`**](https://www.nuget.org/packages/Glyph11) package instead. This one -> trades portability for the native core (and a per-platform binary). - -## Install - -```sh -dotnet add package Glyph11.Native -``` - -## Parse a request - -```csharp -using Glyph11.Native; - -var request = "GET /api/users?a=1 HTTP/1.1\r\nHost: example.com\r\n\r\n"u8.ToArray(); -var headers = new Glyph11Field[64]; -var query = new Glyph11Field[16]; - -int status = Glyph11Parser.Parse(request, headers, query, Glyph11Limits.Default, out Glyph11Result result); -// status 0 == OK. result.HeaderCount / .Consumed; every span (result.Path, -// headers[i].Name, ...) is an Offset/Length into `request` — zero-copy. -``` - -## Decode a chunked body - -A streaming decoder — feed each network read; it strips the chunk framing and writes the -decoded payload into your buffer, carrying partial-chunk state across calls (one call per -read, not per chunk, no allocation): - -```csharp -Glyph11Chunked.Init(out var decoder); -var result = Glyph11Chunked.Decode(ref decoder, input, output, out int inConsumed, out int outWritten); -// result: Ok (need more input) | Done (terminal chunk seen) | Error (malformed framing) -``` - -## Notes - -- Zero-allocation: the parser writes into caller-provided arrays/spans; the C core takes a - single contiguous slab. Multi-segment input must be linearized by the caller first. -- `GLYPH11_NATIVE_PATH` overrides native resolution with an explicit path to - `libglyph11.{so,dll,dylib}` — handy for tests and benchmarks against a fresh local build. - -MIT licensed. Source and issues: . diff --git a/bindings/dotnet/Glyph11.Pico/Glyph11.Pico.csproj b/bindings/dotnet/Glyph11.Pico/Glyph11.Pico.csproj deleted file mode 100644 index 1866e53..0000000 --- a/bindings/dotnet/Glyph11.Pico/Glyph11.Pico.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - net8.0;net9.0;net10.0 - true - enable - latest - Glyph11.Pico - - - Glyph11.Pico - 0.0.1 - 0.0.1.0 - 0.0.1.0 - A fast HTTP/1.x request parser: picohttpparser (native, SSE4.2) tokenizes, then fills the same BinaryRequest glyph11 returns. No extra compliance checks beyond picohttpparser's — built for speed. Chunked decoding reuses glyph11's ChunkedBodyStream. - HTTP Parser picohttpparser Native FFI Fast - - https://github.com/MDA2AV/Glyph11 - MIT - README.md - git - https://github.com/MDA2AV/Glyph11 - true - - true - snupkg - true - true - - - - - - - - - - - - - - diff --git a/bindings/dotnet/Glyph11.Pico/NativeResolver.cs b/bindings/dotnet/Glyph11.Pico/NativeResolver.cs deleted file mode 100644 index 2f83c50..0000000 --- a/bindings/dotnet/Glyph11.Pico/NativeResolver.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Glyph11.Pico; - -/// -/// Resolves the native glyph11pico library from the GLYPH11_PICO_NATIVE_PATH -/// environment variable when set (an explicit path to libglyph11pico.so/.dll/.dylib), -/// falling back to the default OS search otherwise. -/// -internal static class NativeResolver -{ - [ModuleInitializer] - internal static void Init() - => NativeLibrary.SetDllImportResolver(typeof(PicoNative).Assembly, Resolve); - - private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? searchPath) - { - if (name == "glyph11pico") - { - var path = Environment.GetEnvironmentVariable("GLYPH11_PICO_NATIVE_PATH"); - if (!string.IsNullOrEmpty(path) && NativeLibrary.TryLoad(path, out var handle)) - return handle; - } - return IntPtr.Zero; // fall back to default resolution - } -} diff --git a/bindings/dotnet/Glyph11.Pico/PicoInterop.cs b/bindings/dotnet/Glyph11.Pico/PicoInterop.cs deleted file mode 100644 index f6c9af2..0000000 --- a/bindings/dotnet/Glyph11.Pico/PicoInterop.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Glyph11.Pico; - -/// A byte range (offset + length) into the parsed input buffer. -[StructLayout(LayoutKind.Sequential)] -internal struct PicoSpan -{ - public uint Off; - public uint Len; -} - -/// A header name/value pair as two spans into the input buffer. -[StructLayout(LayoutKind.Sequential)] -internal struct PicoField -{ - public PicoSpan Name; - public PicoSpan Value; -} - -internal static class PicoNative -{ - private const string Lib = "glyph11pico"; - - /// - /// Offset-based wrapper over picohttpparser's phr_parse_request. - /// Returns >= 0 header-block length (consumed), -1 parse error, -2 incomplete. - /// - [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] - internal static extern unsafe int pico_parse_request( - byte* buf, nuint len, PicoSpan* method, PicoSpan* target, int* minorVersion, - PicoField* headers, nuint* numHeaders); -} diff --git a/bindings/dotnet/Glyph11.Pico/PicoParser.cs b/bindings/dotnet/Glyph11.Pico/PicoParser.cs deleted file mode 100644 index 00317d8..0000000 --- a/bindings/dotnet/Glyph11.Pico/PicoParser.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System.Buffers; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Glyph11.Protocol; - -namespace Glyph11.Pico; - -/// -/// A fast HTTP/1.x request-header parser: picohttpparser (native, SSE4.2) does the -/// tokenizing, then minimal managed post-processing fills a -/// with the same shape glyph11 produces — method, path, version, query parameters, -/// headers, body offset. -/// -/// Unlike glyph11's hardened parser this does no extra compliance checking beyond -/// what picohttpparser does (no token / field-value validation, no path normalization, -/// no limits) — the trade is raw speed. For chunked request bodies, use glyph11's -/// (available via the Glyph11 dependency). -/// -/// -public static class PicoParser -{ - // Synthesized version bytes (picohttpparser returns only the minor digit; major is 1). - private static readonly byte[] Http11 = "HTTP/1.1"u8.ToArray(); - private static readonly byte[] Http10 = "HTTP/1.0"u8.ToArray(); - - /// Header capacity per parse; requests with more headers are rejected (false). - public const int MaxHeaders = 256; - - /// - /// Parse one request header block from into - /// (cleared first). Returns false on a malformed or - /// incomplete header block. On success is the header-block - /// length and request.Body is the remainder of the buffer. - /// - [SkipLocalsInit] // the scratch header array is written by the native call, not read uninitialized - public static unsafe bool TryParse(ReadOnlyMemory input, BinaryRequest request, out int consumed) - { - request.Clear(); - consumed = 0; - if (input.IsEmpty) return false; - - PicoSpan method, target; - int minor; - Span headers = stackalloc PicoField[MaxHeaders]; - nuint count = MaxHeaders; - int ret; - fixed (PicoField* hp = headers) - { - // Cheap pin: `fixed` on the backing array, not Memory.Pin() (a pinned GCHandle - // costs tens of ns — dominant on small requests). Fall back to Pin() for the - // rare non-array-backed memory. - if (MemoryMarshal.TryGetArray(input, out ArraySegment seg)) - { - fixed (byte* basePtr = seg.Array) - ret = PicoNative.pico_parse_request(basePtr + seg.Offset, (nuint)input.Length, - &method, &target, &minor, hp, &count); - } - else - { - using var handle = input.Pin(); - ret = PicoNative.pico_parse_request((byte*)handle.Pointer, (nuint)input.Length, - &method, &target, &minor, hp, &count); - } - } - if (ret < 0) return false; // -1 parse error, -2 incomplete - - // Match glyph11's managed convention: `consumed` is headerLen - 1 (callers add +1 - // to find the body); Body below is sliced at the true header-block length. - consumed = ret - 1; - request.Method = input.Slice((int)method.Off, (int)method.Len); - request.Version = minor == 0 ? Http10 : Http11; - - // Split the request-target into path + query (picohttpparser returns it whole). - var targetMem = input.Slice((int)target.Off, (int)target.Len); - int q = targetMem.Span.IndexOf((byte)'?'); - if (q < 0) - { - request.Path = targetMem; - } - else - { - request.Path = targetMem.Slice(0, q); - ParseQuery(targetMem.Slice(q + 1), request.QueryParameters); - } - - int n = (int)count; - for (int i = 0; i < n; i++) - { - PicoSpan name = headers[i].Name, value = headers[i].Value; - request.Headers.Add(input.Slice((int)name.Off, (int)name.Len), - input.Slice((int)value.Off, (int)value.Len)); - } - - request.Body = input.Slice(ret); - return true; - } - - /// - /// Parse from a (possibly fragmented) . Single-segment - /// input is parsed in place (zero-copy); multi-segment input is linearized into a fresh - /// array first — picohttpparser needs one contiguous buffer — which the resulting - /// slices reference (so it stays alive, like glyph11's managed - /// parser). Use the overload when you already have a - /// contiguous buffer. - /// - public static bool TryParse(in ReadOnlySequence input, BinaryRequest request, out int consumed) - { - if (input.IsSingleSegment) - return TryParse(input.First, request, out consumed); - return TryParse((ReadOnlyMemory)input.ToArray(), request, out consumed); - } - - // Minimal query split on '&' / '=' — no percent-decoding, no validation (just be fast). - private static void ParseQuery(ReadOnlyMemory query, KeyValueList into) - { - while (!query.IsEmpty) - { - int amp = query.Span.IndexOf((byte)'&'); - ReadOnlyMemory pair = amp < 0 ? query : query.Slice(0, amp); - if (!pair.IsEmpty) - { - int eq = pair.Span.IndexOf((byte)'='); - if (eq < 0) into.Add(pair, default); - else into.Add(pair.Slice(0, eq), pair.Slice(eq + 1)); - } - query = amp < 0 ? default : query.Slice(amp + 1); - } - } -} diff --git a/bindings/dotnet/Glyph11.Pico/README.md b/bindings/dotnet/Glyph11.Pico/README.md deleted file mode 100644 index 2f8d98e..0000000 --- a/bindings/dotnet/Glyph11.Pico/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# Glyph11.Pico - -A **fast** HTTP/1.x request-header parser. [picohttpparser](https://github.com/h2o/picohttpparser) -(native, SSE4.2) does the tokenizing; minimal managed post-processing fills the same -[`BinaryRequest`](https://www.nuget.org/packages/Glyph11) that -[**Glyph11**](https://github.com/MDA2AV/Glyph11) produces — method, path, version, query -parameters, headers, body offset. - -The trade vs the hardened `Glyph11` parser: **no extra compliance checking** beyond what -picohttpparser does (no token / field-value validation, no path normalization, no limit -enforcement, no smuggling checks). You get raw speed and the same request shape. - -| vs. glyph11 managed parser (same `BinaryRequest`) | ~95 B | 4 KB | 32 KB | -|---|---|---|---| -| speedup | ~1.2× | ~1.8× | ~2.0× | - -## Install - -```sh -dotnet add package Glyph11.Pico -``` - -Depends on `Glyph11` (for `BinaryRequest`, `KeyValueList`, and chunked decoding) and -bundles the native `libglyph11pico` per platform. - -## Parse a request - -```csharp -using Glyph11.Pico; -using Glyph11.Protocol; - -var request = new BinaryRequest(); -var input = "GET /api/users?a=1&b=2 HTTP/1.1\r\nHost: example.com\r\n\r\n"u8.ToArray(); - -if (PicoParser.TryParse(input, request, out int consumed)) -{ - // request.Method / .Path / .Version / .Headers / .QueryParameters — same shape as glyph11. - // request.Body is the remainder of the buffer; `consumed` follows glyph11's convention. -} -``` - -## Chunked bodies - -Reuses glyph11's decoder (this package depends on `Glyph11`): - -```csharp -using Glyph11.Parser; - -var stream = new ChunkedBodyStream(); -var r = stream.TryReadChunk(input, out int consumed, out int dataOffset, out int dataLength); -// r: Chunk | Completed | NeedMoreData | Error -``` - -## When to use which - -- **`Glyph11.Pico`** — you want the fastest path to a `BinaryRequest` and will validate - elsewhere (or trust the source). -- **`Glyph11`** — you want the hardened, validating parser (token/field-value checks, - limits, smuggling defenses) in pure managed code. -- **`Glyph11.Native`** — the hardened parser via the C core (raw field spans, not `BinaryRequest`). - -MIT licensed; bundles picohttpparser (MIT/Perl, © Kazuho Oku et al.). -Source: . diff --git a/bindings/dotnet/Glyph11.Pico/native/CMakeLists.txt b/bindings/dotnet/Glyph11.Pico/native/CMakeLists.txt deleted file mode 100644 index 87a4f4c..0000000 --- a/bindings/dotnet/Glyph11.Pico/native/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.15) -project(glyph11pico LANGUAGES C VERSION 0.1.0) - -set(CMAKE_C_STANDARD 11) -set(CMAKE_C_STANDARD_REQUIRED ON) - -# x86-64: SSE4.2 enables picohttpparser's findchar_fast fast path. -march=x86-64-v3 -# (the same level glyph11's linux-x64 package uses) includes it. -option(GLYPH11_PICO_X86_AVX2 "x86-64: build -march=x86-64-v3 (enables pico's SSE4.2)" OFF) -if(GLYPH11_PICO_X86_AVX2) - include(CheckCCompilerFlag) - check_c_compiler_flag("-march=x86-64-v3" GLYPH11_PICO_HAS_V3) - if(GLYPH11_PICO_HAS_V3) - add_compile_options(-march=x86-64-v3) - endif() -endif() - -# picohttpparser is vendored third-party (MIT/Perl) — don't impose -Werror on it. -add_library(glyph11pico SHARED picohttpparser.c pico_shim.c) -set_target_properties(glyph11pico PROPERTIES - POSITION_INDEPENDENT_CODE ON - WINDOWS_EXPORT_ALL_SYMBOLS ON) - -install(TARGETS glyph11pico - RUNTIME DESTINATION native - LIBRARY DESTINATION native) diff --git a/bindings/dotnet/Glyph11.Pico/native/pico_shim.c b/bindings/dotnet/Glyph11.Pico/native/pico_shim.c deleted file mode 100644 index 1078f10..0000000 --- a/bindings/dotnet/Glyph11.Pico/native/pico_shim.c +++ /dev/null @@ -1,39 +0,0 @@ -#include "pico_shim.h" -#include "picohttpparser.h" - -/* Scratch for picohttpparser's pointer-based header array. 512 headers is well above - any sane request; the caller's capacity is honoured for what we copy back out. */ -#define PICO_SHIM_MAX_HEADERS 512 - -int pico_parse_request(const uint8_t* buf, size_t len, - pico_span* method, pico_span* target, int* minor_version, - pico_field* headers, size_t* num_headers) -{ - struct phr_header hdr[PICO_SHIM_MAX_HEADERS]; - const char* m; - const char* p; - size_t ml, pl; - int minor; - size_t nh = PICO_SHIM_MAX_HEADERS; - - int ret = phr_parse_request((const char*)buf, len, &m, &ml, &p, &pl, &minor, - hdr, &nh, 0); - if (ret < 0) { *num_headers = 0; return ret; } /* -1 error, -2 incomplete */ - - const char* base = (const char*)buf; - method->off = (uint32_t)(m - base); method->len = (uint32_t)ml; - target->off = (uint32_t)(p - base); target->len = (uint32_t)pl; - *minor_version = minor; - - size_t cap = *num_headers; - size_t out = nh < cap ? nh : cap; - for (size_t i = 0; i < out; i++) { - /* obs-fold continuation lines have name == NULL; report a zero span. */ - headers[i].name.off = hdr[i].name ? (uint32_t)(hdr[i].name - base) : 0u; - headers[i].name.len = (uint32_t)hdr[i].name_len; - headers[i].value.off = hdr[i].value ? (uint32_t)(hdr[i].value - base) : 0u; - headers[i].value.len = (uint32_t)hdr[i].value_len; - } - *num_headers = out; - return ret; -} diff --git a/bindings/dotnet/Glyph11.Pico/native/pico_shim.h b/bindings/dotnet/Glyph11.Pico/native/pico_shim.h deleted file mode 100644 index 6e2f00a..0000000 --- a/bindings/dotnet/Glyph11.Pico/native/pico_shim.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Thin offset-based wrapper over picohttpparser's phr_parse_request, so the .NET - * binding marshals plain (offset,len) spans instead of raw pointers. The heavy - * lifting is picohttpparser's; this only converts its returned pointers to offsets - * relative to the input buffer. - */ -#ifndef GLYPH11_PICO_SHIM_H -#define GLYPH11_PICO_SHIM_H - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct { uint32_t off; uint32_t len; } pico_span; -typedef struct { pico_span name; pico_span value; } pico_field; - -/* - * Parse one HTTP/1.x request header block. All offsets are relative to `buf`. - * method - out: request method span - * target - out: full request-target span (path + query; NOT split) - * minor_version - out: HTTP minor version (0 or 1; major is 1) - * headers - out: header name/value spans; caller-provided storage - * num_headers - in: capacity of headers[]; out: number written - * Returns: >= 0 header-block length in bytes (consumed); -1 parse error; -2 incomplete. - */ -int pico_parse_request(const uint8_t* buf, size_t len, - pico_span* method, pico_span* target, int* minor_version, - pico_field* headers, size_t* num_headers); - -#ifdef __cplusplus -} -#endif - -#endif /* GLYPH11_PICO_SHIM_H */ diff --git a/bindings/dotnet/Glyph11.Pico/native/picohttpparser.c b/bindings/dotnet/Glyph11.Pico/native/picohttpparser.c deleted file mode 100644 index 26e4ccf..0000000 --- a/bindings/dotnet/Glyph11.Pico/native/picohttpparser.c +++ /dev/null @@ -1,707 +0,0 @@ -/* - * Copyright (c) 2009-2014 Kazuho Oku, Tokuhiro Matsuno, Daisuke Murase, - * Shigeo Mitsunari - * - * The software is licensed under either the MIT License (below) or the Perl - * license. - * - * 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 -#ifdef __SSE4_2__ -#ifdef _MSC_VER -#include -#else -#include -#endif -#endif -#include "picohttpparser.h" - -#if __GNUC__ >= 3 -#define likely(x) __builtin_expect(!!(x), 1) -#define unlikely(x) __builtin_expect(!!(x), 0) -#else -#define likely(x) (x) -#define unlikely(x) (x) -#endif - -#ifdef _MSC_VER -#define ALIGNED(n) _declspec(align(n)) -#else -#define ALIGNED(n) __attribute__((aligned(n))) -#endif - -#define IS_PRINTABLE_ASCII(c) ((unsigned char)(c)-040u < 0137u) - -#define CHECK_EOF() \ - if (buf == buf_end) { \ - *ret = -2; \ - return NULL; \ - } - -#define EXPECT_CHAR_NO_CHECK(ch) \ - if (*buf++ != ch) { \ - *ret = -1; \ - return NULL; \ - } - -#define EXPECT_CHAR(ch) \ - CHECK_EOF(); \ - EXPECT_CHAR_NO_CHECK(ch); - -#define ADVANCE_TOKEN(tok, toklen) \ - do { \ - const char *tok_start = buf; \ - static const char ALIGNED(16) ranges2[16] = "\000\040\177\177"; \ - int found2; \ - buf = findchar_fast(buf, buf_end, ranges2, 4, &found2); \ - if (!found2) { \ - CHECK_EOF(); \ - } \ - while (1) { \ - if (*buf == ' ') { \ - break; \ - } else if (unlikely(!IS_PRINTABLE_ASCII(*buf))) { \ - if ((unsigned char)*buf < '\040' || *buf == '\177') { \ - *ret = -1; \ - return NULL; \ - } \ - } \ - ++buf; \ - CHECK_EOF(); \ - } \ - tok = tok_start; \ - toklen = buf - tok_start; \ - } while (0) - -static const char *token_char_map = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" - "\0\1\0\1\1\1\1\1\0\0\1\1\0\1\1\0\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0" - "\0\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\1\1" - "\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\1\0\1\0" - "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" - "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" - "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" - "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; - -static const char *findchar_fast(const char *buf, const char *buf_end, const char *ranges, size_t ranges_size, int *found) -{ - *found = 0; -#if __SSE4_2__ - if (likely(buf_end - buf >= 16)) { - __m128i ranges16 = _mm_loadu_si128((const __m128i *)ranges); - - size_t left = (buf_end - buf) & ~15; - do { - __m128i b16 = _mm_loadu_si128((const __m128i *)buf); - int r = _mm_cmpestri(ranges16, ranges_size, b16, 16, _SIDD_LEAST_SIGNIFICANT | _SIDD_CMP_RANGES | _SIDD_UBYTE_OPS); - if (unlikely(r != 16)) { - buf += r; - *found = 1; - break; - } - buf += 16; - left -= 16; - } while (likely(left != 0)); - } -#else - /* suppress unused parameter warning */ - (void)buf_end; - (void)ranges; - (void)ranges_size; -#endif - return buf; -} - -static const char *get_token_to_eol(const char *buf, const char *buf_end, const char **token, size_t *token_len, int *ret) -{ - const char *token_start = buf; - -#ifdef __SSE4_2__ - static const char ALIGNED(16) ranges1[16] = "\0\010" /* allow HT */ - "\012\037" /* allow SP and up to but not including DEL */ - "\177\177"; /* allow chars w. MSB set */ - int found; - buf = findchar_fast(buf, buf_end, ranges1, 6, &found); - if (found) - goto FOUND_CTL; -#else - /* find non-printable char within the next 8 bytes, this is the hottest code; manually inlined */ - while (likely(buf_end - buf >= 8)) { -#define DOIT() \ - do { \ - if (unlikely(!IS_PRINTABLE_ASCII(*buf))) \ - goto NonPrintable; \ - ++buf; \ - } while (0) - DOIT(); - DOIT(); - DOIT(); - DOIT(); - DOIT(); - DOIT(); - DOIT(); - DOIT(); -#undef DOIT - continue; - NonPrintable: - if ((likely((unsigned char)*buf < '\040') && likely(*buf != '\011')) || unlikely(*buf == '\177')) { - goto FOUND_CTL; - } - ++buf; - } -#endif - for (;; ++buf) { - CHECK_EOF(); - if (unlikely(!IS_PRINTABLE_ASCII(*buf))) { - if ((likely((unsigned char)*buf < '\040') && likely(*buf != '\011')) || unlikely(*buf == '\177')) { - goto FOUND_CTL; - } - } - } -FOUND_CTL: - if (likely(*buf == '\015')) { - ++buf; - EXPECT_CHAR('\012'); - *token_len = buf - 2 - token_start; - } else if (*buf == '\012') { - *token_len = buf - token_start; - ++buf; - } else { - *ret = -1; - return NULL; - } - *token = token_start; - - return buf; -} - -static const char *is_complete(const char *buf, const char *buf_end, size_t last_len, int *ret) -{ - int ret_cnt = 0; - buf = last_len < 3 ? buf : buf + last_len - 3; - - while (1) { - CHECK_EOF(); - if (*buf == '\015') { - ++buf; - CHECK_EOF(); - EXPECT_CHAR('\012'); - ++ret_cnt; - } else if (*buf == '\012') { - ++buf; - ++ret_cnt; - } else { - ++buf; - ret_cnt = 0; - } - if (ret_cnt == 2) { - return buf; - } - } - - *ret = -2; - return NULL; -} - -#define PARSE_INT(valp_, mul_) \ - if (*buf < '0' || '9' < *buf) { \ - buf++; \ - *ret = -1; \ - return NULL; \ - } \ - *(valp_) = (mul_) * (*buf++ - '0'); - -#define PARSE_INT_3(valp_) \ - do { \ - int res_ = 0; \ - PARSE_INT(&res_, 100) \ - *valp_ = res_; \ - PARSE_INT(&res_, 10) \ - *valp_ += res_; \ - PARSE_INT(&res_, 1) \ - *valp_ += res_; \ - } while (0) - -/* returned pointer is always within [buf, buf_end), or null */ -static const char *parse_token(const char *buf, const char *buf_end, const char **token, size_t *token_len, char next_char, - int *ret) -{ - /* We use pcmpestri to detect non-token characters. This instruction can take no more than eight character ranges (8*2*8=128 - * bits that is the size of a SSE register). Due to this restriction, characters `|` and `~` are handled in the slow loop. */ - static const char ALIGNED(16) ranges[] = "\x00 " /* control chars and up to SP */ - "\"\"" /* 0x22 */ - "()" /* 0x28,0x29 */ - ",," /* 0x2c */ - "//" /* 0x2f */ - ":@" /* 0x3a-0x40 */ - "[]" /* 0x5b-0x5d */ - "{\xff"; /* 0x7b-0xff */ - const char *buf_start = buf; - int found; - buf = findchar_fast(buf, buf_end, ranges, sizeof(ranges) - 1, &found); - if (!found) { - CHECK_EOF(); - } - while (1) { - if (*buf == next_char) { - break; - } else if (!token_char_map[(unsigned char)*buf]) { - *ret = -1; - return NULL; - } - ++buf; - CHECK_EOF(); - } - *token = buf_start; - *token_len = buf - buf_start; - return buf; -} - -/* returned pointer is always within [buf, buf_end), or null */ -static const char *parse_http_version(const char *buf, const char *buf_end, int *minor_version, int *ret) -{ - /* we want at least [HTTP/1.] to try to parse */ - if (buf_end - buf < 9) { - *ret = -2; - return NULL; - } - EXPECT_CHAR_NO_CHECK('H'); - EXPECT_CHAR_NO_CHECK('T'); - EXPECT_CHAR_NO_CHECK('T'); - EXPECT_CHAR_NO_CHECK('P'); - EXPECT_CHAR_NO_CHECK('/'); - EXPECT_CHAR_NO_CHECK('1'); - EXPECT_CHAR_NO_CHECK('.'); - PARSE_INT(minor_version, 1); - return buf; -} - -static const char *parse_headers(const char *buf, const char *buf_end, struct phr_header *headers, size_t *num_headers, - size_t max_headers, int *ret) -{ - for (;; ++*num_headers) { - CHECK_EOF(); - if (*buf == '\015') { - ++buf; - EXPECT_CHAR('\012'); - break; - } else if (*buf == '\012') { - ++buf; - break; - } - if (*num_headers == max_headers) { - *ret = -1; - return NULL; - } - if (!(*num_headers != 0 && (*buf == ' ' || *buf == '\t'))) { - /* parsing name, but do not discard SP before colon, see - * http://www.mozilla.org/security/announce/2006/mfsa2006-33.html */ - if ((buf = parse_token(buf, buf_end, &headers[*num_headers].name, &headers[*num_headers].name_len, ':', ret)) == NULL) { - return NULL; - } - if (headers[*num_headers].name_len == 0) { - *ret = -1; - return NULL; - } - ++buf; - for (;; ++buf) { - CHECK_EOF(); - if (!(*buf == ' ' || *buf == '\t')) { - break; - } - } - } else { - headers[*num_headers].name = NULL; - headers[*num_headers].name_len = 0; - } - const char *value; - size_t value_len; - if ((buf = get_token_to_eol(buf, buf_end, &value, &value_len, ret)) == NULL) { - return NULL; - } - /* remove trailing SPs and HTABs */ - const char *value_end = value + value_len; - for (; value_end != value; --value_end) { - const char c = *(value_end - 1); - if (!(c == ' ' || c == '\t')) { - break; - } - } - headers[*num_headers].value = value; - headers[*num_headers].value_len = value_end - value; - } - return buf; -} - -static const char *parse_request(const char *buf, const char *buf_end, const char **method, size_t *method_len, const char **path, - size_t *path_len, int *minor_version, struct phr_header *headers, size_t *num_headers, - size_t max_headers, int *ret) -{ - /* skip first empty line (some clients add CRLF after POST content) */ - CHECK_EOF(); - if (*buf == '\015') { - ++buf; - EXPECT_CHAR('\012'); - } else if (*buf == '\012') { - ++buf; - } - - /* parse request line */ - if ((buf = parse_token(buf, buf_end, method, method_len, ' ', ret)) == NULL) { - return NULL; - } - do { - ++buf; - CHECK_EOF(); - } while (*buf == ' '); - ADVANCE_TOKEN(*path, *path_len); - do { - ++buf; - CHECK_EOF(); - } while (*buf == ' '); - if (*method_len == 0 || *path_len == 0) { - *ret = -1; - return NULL; - } - if ((buf = parse_http_version(buf, buf_end, minor_version, ret)) == NULL) { - return NULL; - } - if (*buf == '\015') { - ++buf; - EXPECT_CHAR('\012'); - } else if (*buf == '\012') { - ++buf; - } else { - *ret = -1; - return NULL; - } - - return parse_headers(buf, buf_end, headers, num_headers, max_headers, ret); -} - -int phr_parse_request(const char *buf_start, size_t len, const char **method, size_t *method_len, const char **path, - size_t *path_len, int *minor_version, struct phr_header *headers, size_t *num_headers, size_t last_len) -{ - const char *buf = buf_start, *buf_end = buf_start + len; - size_t max_headers = *num_headers; - int r; - - *method = NULL; - *method_len = 0; - *path = NULL; - *path_len = 0; - *minor_version = -1; - *num_headers = 0; - - /* if last_len != 0, check if the request is complete (a fast countermeasure - againt slowloris */ - if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { - return r; - } - - if ((buf = parse_request(buf, buf_end, method, method_len, path, path_len, minor_version, headers, num_headers, max_headers, - &r)) == NULL) { - return r; - } - - return (int)(buf - buf_start); -} - -static const char *parse_response(const char *buf, const char *buf_end, int *minor_version, int *status, const char **msg, - size_t *msg_len, struct phr_header *headers, size_t *num_headers, size_t max_headers, int *ret) -{ - /* parse "HTTP/1.x" */ - if ((buf = parse_http_version(buf, buf_end, minor_version, ret)) == NULL) { - return NULL; - } - /* skip space */ - if (*buf != ' ') { - *ret = -1; - return NULL; - } - do { - ++buf; - CHECK_EOF(); - } while (*buf == ' '); - /* parse status code, we want at least [:digit:][:digit:][:digit:] to try to parse */ - if (buf_end - buf < 4) { - *ret = -2; - return NULL; - } - PARSE_INT_3(status); - - /* get message including preceding space */ - if ((buf = get_token_to_eol(buf, buf_end, msg, msg_len, ret)) == NULL) { - return NULL; - } - if (*msg_len == 0) { - /* ok */ - } else if (**msg == ' ') { - /* Remove preceding space. Successful return from `get_token_to_eol` guarantees that we would hit something other than SP - * before running past the end of the given buffer. */ - do { - ++*msg; - --*msg_len; - } while (**msg == ' '); - } else { - /* garbage found after status code */ - *ret = -1; - return NULL; - } - - return parse_headers(buf, buf_end, headers, num_headers, max_headers, ret); -} - -int phr_parse_response(const char *buf_start, size_t len, int *minor_version, int *status, const char **msg, size_t *msg_len, - struct phr_header *headers, size_t *num_headers, size_t last_len) -{ - const char *buf = buf_start, *buf_end = buf + len; - size_t max_headers = *num_headers; - int r; - - *minor_version = -1; - *status = 0; - *msg = NULL; - *msg_len = 0; - *num_headers = 0; - - /* if last_len != 0, check if the response is complete (a fast countermeasure - against slowloris */ - if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { - return r; - } - - if ((buf = parse_response(buf, buf_end, minor_version, status, msg, msg_len, headers, num_headers, max_headers, &r)) == NULL) { - return r; - } - - return (int)(buf - buf_start); -} - -int phr_parse_headers(const char *buf_start, size_t len, struct phr_header *headers, size_t *num_headers, size_t last_len) -{ - const char *buf = buf_start, *buf_end = buf + len; - size_t max_headers = *num_headers; - int r; - - *num_headers = 0; - - /* if last_len != 0, check if the response is complete (a fast countermeasure - against slowloris */ - if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { - return r; - } - - if ((buf = parse_headers(buf, buf_end, headers, num_headers, max_headers, &r)) == NULL) { - return r; - } - - return (int)(buf - buf_start); -} - -enum { - CHUNKED_IN_CHUNK_SIZE, - CHUNKED_IN_CHUNK_EXT, - CHUNKED_IN_CHUNK_HEADER_EXPECT_LF, - CHUNKED_IN_CHUNK_DATA, - CHUNKED_IN_CHUNK_DATA_EXPECT_CR, - CHUNKED_IN_CHUNK_DATA_EXPECT_LF, - CHUNKED_IN_TRAILERS_LINE_HEAD, - CHUNKED_IN_TRAILERS_LINE_MIDDLE -}; - -static int decode_hex(int ch) -{ - if ('0' <= ch && ch <= '9') { - return ch - '0'; - } else if ('A' <= ch && ch <= 'F') { - return ch - 'A' + 0xa; - } else if ('a' <= ch && ch <= 'f') { - return ch - 'a' + 0xa; - } else { - return -1; - } -} - -ssize_t phr_decode_chunked(struct phr_chunked_decoder *decoder, char *buf, size_t *_bufsz) -{ - size_t dst = 0, src = 0, bufsz = *_bufsz; - ssize_t ret = -2; /* incomplete */ - - decoder->_total_read += bufsz; - - while (1) { - switch (decoder->_state) { - case CHUNKED_IN_CHUNK_SIZE: - for (;; ++src) { - int v; - if (src == bufsz) - goto Exit; - if ((v = decode_hex(buf[src])) == -1) { - if (decoder->_hex_count == 0) { - ret = -1; - goto Exit; - } - /* the only characters that may appear after the chunk size are BWS, semicolon, or CRLF */ - switch (buf[src]) { - case ' ': - case '\011': - case ';': - case '\012': - case '\015': - break; - default: - ret = -1; - goto Exit; - } - break; - } - if (decoder->_hex_count == sizeof(size_t) * 2) { - ret = -1; - goto Exit; - } - decoder->bytes_left_in_chunk = decoder->bytes_left_in_chunk * 16 + v; - ++decoder->_hex_count; - } - decoder->_hex_count = 0; - decoder->_state = CHUNKED_IN_CHUNK_EXT; - /* fallthru */ - case CHUNKED_IN_CHUNK_EXT: - /* RFC 7230 A.2 "Line folding in chunk extensions is disallowed" */ - for (;; ++src) { - if (src == bufsz) - goto Exit; - if (buf[src] == '\015') { - break; - } else if (buf[src] == '\012') { - ret = -1; - goto Exit; - } - } - ++src; - decoder->_state = CHUNKED_IN_CHUNK_HEADER_EXPECT_LF; - /* fallthru */ - case CHUNKED_IN_CHUNK_HEADER_EXPECT_LF: - if (src == bufsz) - goto Exit; - if (buf[src] != '\012') { - ret = -1; - goto Exit; - } - ++src; - if (decoder->bytes_left_in_chunk == 0) { - if (decoder->consume_trailer) { - decoder->_state = CHUNKED_IN_TRAILERS_LINE_HEAD; - break; - } else { - goto Complete; - } - } - decoder->_state = CHUNKED_IN_CHUNK_DATA; - /* fallthru */ - case CHUNKED_IN_CHUNK_DATA: { - size_t avail = bufsz - src; - if (avail < decoder->bytes_left_in_chunk) { - if (dst != src) - memmove(buf + dst, buf + src, avail); - src += avail; - dst += avail; - decoder->bytes_left_in_chunk -= avail; - goto Exit; - } - if (dst != src) - memmove(buf + dst, buf + src, decoder->bytes_left_in_chunk); - src += decoder->bytes_left_in_chunk; - dst += decoder->bytes_left_in_chunk; - decoder->bytes_left_in_chunk = 0; - decoder->_state = CHUNKED_IN_CHUNK_DATA_EXPECT_CR; - } - /* fallthru */ - case CHUNKED_IN_CHUNK_DATA_EXPECT_CR: - if (src == bufsz) - goto Exit; - if (buf[src] != '\015') { - ret = -1; - goto Exit; - } - ++src; - decoder->_state = CHUNKED_IN_CHUNK_DATA_EXPECT_LF; - /* fallthru */ - case CHUNKED_IN_CHUNK_DATA_EXPECT_LF: - if (src == bufsz) - goto Exit; - if (buf[src] != '\012') { - ret = -1; - goto Exit; - } - ++src; - decoder->_state = CHUNKED_IN_CHUNK_SIZE; - break; - case CHUNKED_IN_TRAILERS_LINE_HEAD: - for (;; ++src) { - if (src == bufsz) - goto Exit; - if (buf[src] != '\015') - break; - } - if (buf[src++] == '\012') - goto Complete; - decoder->_state = CHUNKED_IN_TRAILERS_LINE_MIDDLE; - /* fallthru */ - case CHUNKED_IN_TRAILERS_LINE_MIDDLE: - for (;; ++src) { - if (src == bufsz) - goto Exit; - if (buf[src] == '\012') - break; - } - ++src; - decoder->_state = CHUNKED_IN_TRAILERS_LINE_HEAD; - break; - default: - assert(!"decoder is corrupt"); - } - } - -Complete: - ret = bufsz - src; -Exit: - if (dst != src) - memmove(buf + dst, buf + src, bufsz - src); - *_bufsz = dst; - /* if incomplete but the overhead of the chunked encoding is >=100KB and >80%, signal an error */ - if (ret == -2) { - decoder->_total_overhead += bufsz - dst; - if (decoder->_total_overhead >= 100 * 1024 && decoder->_total_read - decoder->_total_overhead < decoder->_total_read / 4) - ret = -1; - } - return ret; -} - -int phr_decode_chunked_is_in_data(struct phr_chunked_decoder *decoder) -{ - return decoder->_state == CHUNKED_IN_CHUNK_DATA; -} - -#undef CHECK_EOF -#undef EXPECT_CHAR -#undef ADVANCE_TOKEN diff --git a/bindings/dotnet/Glyph11.Pico/native/picohttpparser.h b/bindings/dotnet/Glyph11.Pico/native/picohttpparser.h deleted file mode 100644 index 13bc855..0000000 --- a/bindings/dotnet/Glyph11.Pico/native/picohttpparser.h +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2009-2014 Kazuho Oku, Tokuhiro Matsuno, Daisuke Murase, - * Shigeo Mitsunari - * - * The software is licensed under either the MIT License (below) or the Perl - * license. - * - * 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 picohttpparser_h -#define picohttpparser_h - -#include -#include - -#ifdef _MSC_VER -#define ssize_t intptr_t -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -/* contains name and value of a header (name == NULL if is a continuing line - * of a multiline header */ -struct phr_header { - const char *name; - size_t name_len; - const char *value; - size_t value_len; -}; - -/* returns number of bytes consumed if successful, -2 if request is partial, - * -1 if failed */ -int phr_parse_request(const char *buf, size_t len, const char **method, size_t *method_len, const char **path, size_t *path_len, - int *minor_version, struct phr_header *headers, size_t *num_headers, size_t last_len); - -/* ditto */ -int phr_parse_response(const char *_buf, size_t len, int *minor_version, int *status, const char **msg, size_t *msg_len, - struct phr_header *headers, size_t *num_headers, size_t last_len); - -/* ditto */ -int phr_parse_headers(const char *buf, size_t len, struct phr_header *headers, size_t *num_headers, size_t last_len); - -/* should be zero-filled before start */ -struct phr_chunked_decoder { - size_t bytes_left_in_chunk; /* number of bytes left in current chunk */ - char consume_trailer; /* if trailing headers should be consumed */ - char _hex_count; - char _state; - uint64_t _total_read; - uint64_t _total_overhead; -}; - -/* the function rewrites the buffer given as (buf, bufsz) removing the chunked- - * encoding headers. When the function returns without an error, bufsz is - * updated to the length of the decoded data available. Applications should - * repeatedly call the function while it returns -2 (incomplete) every time - * supplying newly arrived data. If the end of the chunked-encoded data is - * found, the function returns a non-negative number indicating the number of - * octets left undecoded, that starts from the offset returned by `*bufsz`. - * Returns -1 on error. - */ -ssize_t phr_decode_chunked(struct phr_chunked_decoder *decoder, char *buf, size_t *bufsz); - -/* returns if the chunked decoder is in middle of chunked data */ -int phr_decode_chunked_is_in_data(struct phr_chunked_decoder *decoder); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/bindings/kotlin/.gitignore b/bindings/kotlin/.gitignore deleted file mode 100644 index 67bcc2f..0000000 --- a/bindings/kotlin/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.gradle/ -build/ diff --git a/bindings/kotlin/README.md b/bindings/kotlin/README.md deleted file mode 100644 index f2b1e5b..0000000 --- a/bindings/kotlin/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Kotlin / JVM binding (Panama FFM) - -Calls the Glyph11 C core (`libglyph11`) from Kotlin via the Foreign Function & -Memory API — no JNI glue, pure managed downcalls. - -```kotlin -val r = Glyph11.parse(requestBytes) -when { - r.isOk -> { - val method = String(requestBytes, r.method.offset, r.method.length) - // also: r.path, r.version, r.headerCount, r.queryCount, r.consumed - } - r.isIncomplete -> { /* read more bytes */ } - else -> { val httpStatus = Glyph11.httpCode(r.status) /* 400 / 431 */ } -} -``` - -## Build & run - -FFM is a preview feature on JDK 21 (finalized in JDK 22), so the build passes -`--enable-preview`. Point at the native library with `GLYPH11_LIB`: - -```sh -# build the .so first (from the repo root) -cmake -S core -B core/build-rel -DGLYPH11_SANITIZE=OFF -DGLYPH11_BUILD_TESTS=OFF -cmake --build core/build-rel - -cd bindings/kotlin -GLYPH11_LIB="$PWD/../../core/build-rel/libglyph11.so" gradle run -``` - -`Main.kt` is a smoke test that parses valid and malformed requests and checks the -status, parsed spans, and HTTP codes against the native core. - -## Notes - -- Requires JDK 21+ (FFM). On JDK 22+ you can drop `--enable-preview`. -- The library is resolved from `-Dglyph11.lib` (the `run` task sets it from the - `GLYPH11_LIB` env var) or the default OS library search path. -- The C `glyph11_request` struct layout is mirrored by byte offset in - `Glyph11.kt` (LP64); the binding allocates the header/query storage off-heap - via an `Arena` per call. diff --git a/bindings/kotlin/build.gradle.kts b/bindings/kotlin/build.gradle.kts deleted file mode 100644 index 8350e71..0000000 --- a/bindings/kotlin/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -plugins { - kotlin("jvm") version "2.0.21" - application -} - -repositories { mavenCentral() } - -kotlin { jvmToolchain(21) } - -application { - mainClass.set("io.glyph11.MainKt") -} - -// java.lang.foreign (Panama / FFM) is a preview feature on JDK 21. -val previewArgs = listOf("--enable-preview", "--enable-native-access=ALL-UNNAMED") - -tasks.withType().configureEach { - compilerOptions { freeCompilerArgs.add("-Xjvm-enable-preview") } -} - -tasks.named("run") { - jvmArgs(previewArgs) - System.getenv("GLYPH11_LIB")?.let { systemProperty("glyph11.lib", it) } -} diff --git a/bindings/kotlin/gradle/wrapper/gradle-wrapper.jar b/bindings/kotlin/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 1b33c55baabb587c669f562ae36f953de2481846..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43764 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 '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH="\\\"\\\"" - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/bindings/kotlin/gradlew.bat b/bindings/kotlin/gradlew.bat deleted file mode 100644 index 5eed7ee..0000000 --- a/bindings/kotlin/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH= - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/bindings/kotlin/settings.gradle.kts b/bindings/kotlin/settings.gradle.kts deleted file mode 100644 index aa0bcf7..0000000 --- a/bindings/kotlin/settings.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = "glyph11-kotlin" diff --git a/bindings/kotlin/src/main/kotlin/io/glyph11/Glyph11.kt b/bindings/kotlin/src/main/kotlin/io/glyph11/Glyph11.kt deleted file mode 100644 index 4ca844a..0000000 --- a/bindings/kotlin/src/main/kotlin/io/glyph11/Glyph11.kt +++ /dev/null @@ -1,135 +0,0 @@ -package io.glyph11 - -import java.lang.foreign.Arena -import java.lang.foreign.FunctionDescriptor -import java.lang.foreign.Linker -import java.lang.foreign.MemorySegment -import java.lang.foreign.SymbolLookup -import java.lang.foreign.ValueLayout -import java.lang.invoke.MethodHandle - -/** A byte range (offset + length) into the parsed input buffer (zero-copy). */ -data class Glyph11Span(val offset: Int, val length: Int) - -/** A parsed name/value pair (header or query parameter); spans index into the input. */ -data class Glyph11Field(val name: Glyph11Span, val value: Glyph11Span) - -/** Parsed request fields. Spans index into the input passed to [Glyph11.parse]. */ -data class Glyph11Result( - val status: Int, - val method: Glyph11Span, - val target: Glyph11Span, - val path: Glyph11Span, - val version: Glyph11Span, - val headers: List, - val query: List, - val consumed: Long, -) { - val isOk: Boolean get() = status == 0 - val isIncomplete: Boolean get() = status == 1 - val headerCount: Int get() = headers.size - val queryCount: Int get() = query.size -} - -/** - * Kotlin/JVM binding for the Glyph11 hardened HTTP/1.1 request parser - * (`libglyph11`), via the Foreign Function & Memory API (Panama). - * - * Point at the native library with `-Dglyph11.lib=/path/to/libglyph11.so`, - * otherwise it is resolved from the default library search path. - */ -object Glyph11 { - // glyph11_request field offsets (see glyph11.h; LP64 layout). - private const val OFF_HEADERS = 32L - private const val OFF_HEADER_CAP = 40L - private const val OFF_HEADER_COUNT = 44L - private const val OFF_QUERY = 48L - private const val OFF_QUERY_CAP = 56L - private const val OFF_QUERY_COUNT = 60L - private const val SIZEOF_REQUEST = 64L - private const val SIZEOF_FIELD = 16L - private const val CAPACITY = 256 - - private val linker = Linker.nativeLinker() - - private val lookup: SymbolLookup = run { - val path = System.getProperty("glyph11.lib") - if (path != null) SymbolLookup.libraryLookup(path, Arena.global()) - else SymbolLookup.libraryLookup("glyph11", Arena.global()) - } - - private fun handle(name: String, desc: FunctionDescriptor): MethodHandle = - linker.downcallHandle(lookup.find(name).orElseThrow { UnsatisfiedLinkError(name) }, desc) - - private val parseHandle = handle( - "glyph11_parse_request", - FunctionDescriptor.of( - ValueLayout.JAVA_INT, - ValueLayout.ADDRESS, // const uint8_t* buf - ValueLayout.JAVA_LONG, // size_t len - ValueLayout.ADDRESS, // const glyph11_limits* (null -> defaults) - ValueLayout.ADDRESS, // glyph11_request* - ValueLayout.ADDRESS, // size_t* consumed - ), - ) - - private val httpCodeHandle = handle( - "glyph11_status_http_code", - FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT), - ) - - private val abiHandle = handle( - "glyph11_abi_version", - FunctionDescriptor.of(ValueLayout.JAVA_INT), - ) - - /** Packed ABI version of the loaded native library. */ - val abiVersion: Int get() = abiHandle.invoke() as Int - - /** HTTP response code for a status (400 / 431, or 0 for OK / incomplete). */ - fun httpCode(status: Int): Int = httpCodeHandle.invoke(status) as Int - - /** Parse one HTTP/1.1 request header block from [input]. */ - fun parse(input: ByteArray): Glyph11Result { - Arena.ofConfined().use { arena -> - val buf = arena.allocate(maxOf(input.size, 1).toLong()) - MemorySegment.copy(input, 0, buf, ValueLayout.JAVA_BYTE, 0L, input.size) - - val headers = arena.allocate(SIZEOF_FIELD * CAPACITY) - val query = arena.allocate(SIZEOF_FIELD * CAPACITY) - val req = arena.allocate(SIZEOF_REQUEST) // zero-initialized - req.set(ValueLayout.ADDRESS, OFF_HEADERS, headers) - req.set(ValueLayout.JAVA_INT, OFF_HEADER_CAP, CAPACITY) - req.set(ValueLayout.ADDRESS, OFF_QUERY, query) - req.set(ValueLayout.JAVA_INT, OFF_QUERY_CAP, CAPACITY) - val consumed = arena.allocate(ValueLayout.JAVA_LONG) - - val status = parseHandle.invoke( - buf, input.size.toLong(), MemorySegment.NULL, req, consumed, - ) as Int - - fun span(off: Long) = - Glyph11Span(req.get(ValueLayout.JAVA_INT, off), req.get(ValueLayout.JAVA_INT, off + 4)) - fun fields(seg: MemorySegment, count: Int): List = - (0 until count).map { i -> - val b = i.toLong() * SIZEOF_FIELD - Glyph11Field( - Glyph11Span(seg.get(ValueLayout.JAVA_INT, b), seg.get(ValueLayout.JAVA_INT, b + 4)), - Glyph11Span(seg.get(ValueLayout.JAVA_INT, b + 8), seg.get(ValueLayout.JAVA_INT, b + 12)), - ) - } - - return Glyph11Result( - status = status, - method = span(0L), - target = span(8L), - path = span(16L), - version = span(24L), - headers = if (status == 0) fields(headers, req.get(ValueLayout.JAVA_INT, OFF_HEADER_COUNT)) else emptyList(), - query = if (status == 0) fields(query, req.get(ValueLayout.JAVA_INT, OFF_QUERY_COUNT)) else emptyList(), - consumed = if (status == 0) consumed.get(ValueLayout.JAVA_LONG, 0L) else 0L, - ) - } - } - -} diff --git a/bindings/kotlin/src/main/kotlin/io/glyph11/Main.kt b/bindings/kotlin/src/main/kotlin/io/glyph11/Main.kt deleted file mode 100644 index 54736c6..0000000 --- a/bindings/kotlin/src/main/kotlin/io/glyph11/Main.kt +++ /dev/null @@ -1,43 +0,0 @@ -package io.glyph11 - -import kotlin.system.exitProcess - -fun main() { - smoke() -} - -/** Smoke test: parse a few requests via the native core and verify the results. */ -private fun smoke() { - println("glyph11 abi 0x%06x".format(Glyph11.abiVersion)) - - var fails = 0 - fun check(name: String, cond: Boolean) { - if (!cond) { fails++; println(" FAIL $name") } - } - fun bytes(s: String) = s.toByteArray(Charsets.ISO_8859_1) - fun slice(b: ByteArray, sp: Glyph11Span) = String(b, sp.offset, sp.length, Charsets.ISO_8859_1) - - val valid = bytes("GET /api/users?a=1&b=2 HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n") - val r = Glyph11.parse(valid) - check("valid ok", r.isOk) - check("method", slice(valid, r.method) == "GET") - check("path", slice(valid, r.path) == "/api/users") - check("version", slice(valid, r.version) == "HTTP/1.1") - check("headerCount", r.headerCount == 2) - check("header name/value", r.headers[0].let { slice(valid, it.name) == "Host" && slice(valid, it.value) == "example.com" }) - check("queryCount", r.queryCount == 2) - check("consumed", r.consumed.toInt() == valid.size) - - check("no-host -> 400", Glyph11.httpCode(Glyph11.parse(bytes("GET / HTTP/1.1\r\n\r\n")).status) == 400) - check("te+cl -> 400", Glyph11.httpCode(Glyph11.parse( - bytes("POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n")).status) == 400) - check("traversal -> 400", Glyph11.httpCode(Glyph11.parse(bytes("GET /a/../b HTTP/1.1\r\nHost: x\r\n\r\n")).status) == 400) - check("incomplete", Glyph11.parse(bytes("GET / HTTP/1.1\r\nHost: x\r\n")).isIncomplete) - - if (fails == 0) { - println("kotlin binding: all checks passed") - } else { - println("kotlin binding: $fails check(s) FAILED") - exitProcess(1) - } -} diff --git a/core/.gitignore b/core/.gitignore deleted file mode 100644 index 72f0219..0000000 --- a/core/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -build*/ -**/bin/ -**/obj/ diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt deleted file mode 100644 index 0d5b482..0000000 --- a/core/CMakeLists.txt +++ /dev/null @@ -1,76 +0,0 @@ -cmake_minimum_required(VERSION 3.15) -project(glyph11 LANGUAGES C VERSION 0.1.0) - -set(CMAKE_C_STANDARD 11) -set(CMAKE_C_STANDARD_REQUIRED ON) - -option(GLYPH11_SANITIZE "Build with AddressSanitizer + UBSan" OFF) -option(GLYPH11_BUILD_TESTS "Build the test suite" ON) -option(GLYPH11_X86_AVX2 "x86-64: require AVX2 (build -march=x86-64-v3)" OFF) - -if(GLYPH11_SANITIZE) - add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) - add_link_options(-fsanitize=address,undefined) -endif() - -# Build for the AVX2 microarchitecture level so the 256-bit scanner tier (gated on -# __AVX2__ in glyph11.c) inlines into the parse loop — ~15% faster on large headers. -# This makes the binary REQUIRE a 2013+ (Haswell) CPU; only enable it for RIDs where -# that's acceptable (the linux-x64 package). Off by default keeps the binary portable. -if(GLYPH11_X86_AVX2) - include(CheckCCompilerFlag) - check_c_compiler_flag("-march=x86-64-v3" GLYPH11_HAS_V3) - if(GLYPH11_HAS_V3) - add_compile_options(-march=x86-64-v3) - else() - message(WARNING "GLYPH11_X86_AVX2 set but -march=x86-64-v3 unsupported; building baseline") - endif() -endif() - -# GCC/Clang warning flags are not MSVC-compatible (cl treats -Wextra as /Wextra -> -# error D8021). Use /W4 on MSVC; no /WX there since the cross-platform packaging -# build just needs to compile (MSVC takes the scalar path — the SSE2 fast path is -# guarded by __SSE2__, which MSVC doesn't define). -if(MSVC) - set(GLYPH11_WARNINGS /W4) -else() - set(GLYPH11_WARNINGS -Wall -Wextra -Werror) -endif() - -# Static + shared from the same source. -add_library(glyph11 STATIC src/glyph11.c) -add_library(glyph11_shared SHARED src/glyph11.c) -# OUTPUT_NAME glyph11 -> libglyph11.so / libglyph11.dylib / glyph11.dll, matching -# the [DllImport("glyph11")] name on every platform. WINDOWS_EXPORT_ALL_SYMBOLS makes -# MSVC export the public C API from the DLL (it exports nothing by default), so the -# P/Invoke binding can find the entry points without per-function __declspec annotations. -set_target_properties(glyph11_shared PROPERTIES - OUTPUT_NAME glyph11 - WINDOWS_EXPORT_ALL_SYMBOLS ON) - -foreach(t glyph11 glyph11_shared) - target_include_directories(${t} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) - target_compile_options(${t} PRIVATE ${GLYPH11_WARNINGS}) - set_target_properties(${t} PROPERTIES POSITION_INDEPENDENT_CODE ON) -endforeach() - -# Install just the shared library into a `native/` subdir so `cmake --install` stages it -# to a predictable path on every platform (RUNTIME = glyph11.dll, LIBRARY = libglyph11.so -# /.dylib) — the package-native CI matrix installs each RID then drops it into the NuGet's -# runtimes//native/. The import .lib (ARCHIVE) is intentionally not installed. -install(TARGETS glyph11_shared - RUNTIME DESTINATION native - LIBRARY DESTINATION native) - -if(GLYPH11_BUILD_TESTS) - enable_testing() - add_executable(test_glyph11 tests/test_glyph11.c) - target_link_libraries(test_glyph11 PRIVATE glyph11) - target_compile_options(test_glyph11 PRIVATE ${GLYPH11_WARNINGS}) - add_test(NAME glyph11_unit COMMAND test_glyph11) - - add_executable(fuzz_smoke tests/fuzz_smoke.c) - target_link_libraries(fuzz_smoke PRIVATE glyph11) - target_compile_options(fuzz_smoke PRIVATE ${GLYPH11_WARNINGS}) - add_test(NAME glyph11_fuzz_smoke COMMAND fuzz_smoke) -endif() diff --git a/core/diff/Glyph11.Diff.csproj b/core/diff/Glyph11.Diff.csproj deleted file mode 100644 index 4403fd2..0000000 --- a/core/diff/Glyph11.Diff.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - Exe - net10.0 - true - enable - enable - glyph11-diff - true - - - - - - - diff --git a/core/diff/Native.cs b/core/diff/Native.cs deleted file mode 100644 index bd217b7..0000000 --- a/core/diff/Native.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Glyph11.Diff; - -[StructLayout(LayoutKind.Sequential)] -internal struct NSpan { public uint Off; public uint Len; } - -[StructLayout(LayoutKind.Sequential)] -internal struct NField { public NSpan Name; public NSpan Value; } - -[StructLayout(LayoutKind.Sequential)] -internal struct NLimits -{ - public uint StructSize, MaxHeaderCount, MaxHeaderNameLen, MaxHeaderValueLen, - MaxUrlLen, MaxQueryParamCount, MaxMethodLen, MaxTotalHeaderBytes; - - public static NLimits Default() => new() - { - StructSize = (uint)Marshal.SizeOf(), - MaxHeaderCount = 100, - MaxHeaderNameLen = 256, - MaxHeaderValueLen = 8192, - MaxUrlLen = 8192, - MaxQueryParamCount = 128, - MaxMethodLen = 16, - MaxTotalHeaderBytes = 1048576, - }; -} - -[StructLayout(LayoutKind.Sequential)] -internal unsafe struct NRequest -{ - public NSpan Method, Target, Path, Version; - public NField* Headers; public uint HeaderCap; public uint HeaderCount; - public NField* Query; public uint QueryCap; public uint QueryCount; -} - -internal static unsafe class Native -{ - public const int OK = 0, INCOMPLETE = 1; - - [DllImport("glyph11", CallingConvention = CallingConvention.Cdecl)] - public static extern int glyph11_parse_request( - byte* buf, nuint len, NLimits* limits, NRequest* req, nuint* consumed); - - [DllImport("glyph11", CallingConvention = CallingConvention.Cdecl)] - public static extern int glyph11_status_http_code(int status); -} diff --git a/core/diff/Program.cs b/core/diff/Program.cs deleted file mode 100644 index e887d8b..0000000 --- a/core/diff/Program.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System.Text; -using Glyph11; -using Glyph11.Parser; -using Glyph11.Parser.UltraHardened; -using Glyph11.Protocol; - -namespace Glyph11.Diff; - -internal enum Kind { Parsed, Incomplete, Error } - -internal sealed class Result -{ - public Kind Kind; - public int HttpCode; - public string? Note; - public byte[] Method = [], Path = [], Version = []; - public readonly List<(byte[] n, byte[] v)> Headers = new(); - public readonly List<(byte[] n, byte[] v)> Query = new(); - public long Consumed; -} - -internal static class Program -{ - // ---- C side (P/Invoke) ---- - private static unsafe Result RunC(byte[] input) - { - var lim = NLimits.Default(); - var hdrs = stackalloc NField[256]; - var qry = stackalloc NField[256]; - var req = new NRequest { Headers = hdrs, HeaderCap = 256, Query = qry, QueryCap = 256 }; - nuint consumed = 0; - int st; - fixed (byte* p = input) - st = Native.glyph11_parse_request(p, (nuint)input.Length, &lim, &req, &consumed); - - var r = new Result(); - if (st == Native.OK) - { - r.Kind = Kind.Parsed; - r.Method = Slice(input, req.Method); - r.Path = Slice(input, req.Path); - r.Version = Slice(input, req.Version); - for (uint i = 0; i < req.HeaderCount; i++) - r.Headers.Add((Slice(input, hdrs[i].Name), Slice(input, hdrs[i].Value))); - for (uint i = 0; i < req.QueryCount; i++) - r.Query.Add((Slice(input, qry[i].Name), Slice(input, qry[i].Value))); - r.Consumed = (long)consumed; - } - else if (st == Native.INCOMPLETE) r.Kind = Kind.Incomplete; - else { r.Kind = Kind.Error; r.HttpCode = Native.glyph11_status_http_code(st); } - return r; - } - - private static byte[] Slice(byte[] buf, NSpan s) - { - var r = new byte[s.Len]; - if (s.Len != 0) Array.Copy(buf, (int)s.Off, r, 0, (int)s.Len); - return r; - } - - // ---- C# side (reference) ---- - private static Result RunCs(byte[] input) - { - var r = new Result(); - var req = new BinaryRequest(); - var lim = ParserLimits.Default; - try - { - var rom = (ReadOnlyMemory)input; - bool ok = UltraHardenedParser.TryExtractFullHeaderROM(ref rom, req, in lim, out int br); - if (ok) - { - r.Kind = Kind.Parsed; - r.Method = req.Method.ToArray(); - r.Path = req.Path.ToArray(); - r.Version = req.Version.ToArray(); - var hs = req.Headers; - for (int i = 0; i < hs.Count; i++) r.Headers.Add((hs[i].Key.ToArray(), hs[i].Value.ToArray())); - var qs = req.QueryParameters; - for (int i = 0; i < qs.Count; i++) r.Query.Add((qs[i].Key.ToArray(), qs[i].Value.ToArray())); - r.Consumed = br + 1; // C# returns total-1; the C ABI returns the clean total - } - else r.Kind = Kind.Incomplete; - } - catch (HttpParseException ex) { r.Kind = Kind.Error; r.HttpCode = ex.StatusCode; } - catch (Exception ex) { r.Kind = Kind.Error; r.HttpCode = -1; r.Note = ex.GetType().Name; } - finally { req.Dispose(); } - return r; - } - - private static bool Eq(byte[] a, byte[] b) => a.AsSpan().SequenceEqual(b); - - private static string? Compare(Result c, Result cs) - { - if (cs.Note != null) return $"C# threw {cs.Note}"; - if (c.Kind != cs.Kind) return $"outcome C={c.Kind} C#={cs.Kind}"; - if (c.Kind == Kind.Error) - return c.HttpCode == cs.HttpCode ? null : $"http C={c.HttpCode} C#={cs.HttpCode}"; - if (c.Kind != Kind.Parsed) return null; - if (!Eq(c.Method, cs.Method)) return "method"; - if (!Eq(c.Path, cs.Path)) return "path"; - if (!Eq(c.Version, cs.Version)) return "version"; - if (c.Headers.Count != cs.Headers.Count) return $"hcount C={c.Headers.Count} C#={cs.Headers.Count}"; - for (int i = 0; i < c.Headers.Count; i++) - if (!Eq(c.Headers[i].n, cs.Headers[i].n) || !Eq(c.Headers[i].v, cs.Headers[i].v)) return $"header[{i}]"; - if (c.Query.Count != cs.Query.Count) return $"qcount C={c.Query.Count} C#={cs.Query.Count}"; - for (int i = 0; i < c.Query.Count; i++) - if (!Eq(c.Query[i].n, cs.Query[i].n) || !Eq(c.Query[i].v, cs.Query[i].v)) return $"query[{i}]"; - if (c.Consumed != cs.Consumed) return $"consumed C={c.Consumed} C#={cs.Consumed}"; - return null; - } - - private static int Main(string[] args) - { - int iterations = args.Length > 0 && int.TryParse(args[0], out var n) ? n : 1_000_000; - int total = 0, diverged = 0; - var firsts = new List(); - - void Check(byte[] input) - { - total++; - var d = Compare(RunC(input), RunCs(input)); - if (d != null) { diverged++; if (firsts.Count < 25) firsts.Add($" [{Show(input)}] : {d}"); } - } - - var seeds = Curated.Select(Bytes).ToArray(); - foreach (var s in seeds) Check(s); - - var rng = new Random(1234567); - for (int it = 0; it < iterations; it++) - { - var buf = (byte[])seeds[rng.Next(seeds.Length)].Clone(); - int flips = rng.Next(6); - for (int k = 0; k < flips && buf.Length > 0; k++) buf[rng.Next(buf.Length)] = (byte)rng.Next(256); - Check(buf[..rng.Next(buf.Length + 1)]); - } - - Console.WriteLine($"differential: {total - diverged}/{total} agree, {diverged} diverged"); - foreach (var f in firsts) Console.WriteLine(f); - return diverged == 0 ? 0 : 1; - } - - private static byte[] Bytes(string s) => Encoding.Latin1.GetBytes(s); - - private static string Show(byte[] b) - { - var s = Encoding.Latin1.GetString(b) - .Replace("\r", "\\r").Replace("\n", "\\n").Replace("\0", "\\0"); - return s.Length > 70 ? s[..70] + "…" : s; - } - - private static readonly string[] Curated = - { - // valid - "GET /api/users?a=1&b=2 HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n", - "GET / HTTP/1.1\r\nHost: x\r\n\r\n", - "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\n\r\n", - "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n", - "POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n", - "OPTIONS * HTTP/1.1\r\nHost: x\r\n\r\n", - "GET / HTTP/1.0\r\nHost: x\r\n\r\n", - "GET /?flag HTTP/1.1\r\nHost: x\r\n\r\n", - "GET /?a=1&&b=2&=v&c HTTP/1.1\r\nHost: x\r\n\r\n", - "GET /p HTTP/1.1\r\nHost: x\r\nX-Trailing: val \r\n\r\n", - // incomplete - "GET / HTTP/1.1\r\nHost: x\r\n", - "", - // structural - "GET / HTTP/1.1\r\nHost: x\r\n\r\n", - "GET / HTTP/2.0\r\nHost: x\r\n\r\n", - "G(T / HTTP/1.1\r\nHost: x\r\n\r\n", - "GET /\n/ HTTP/1.1\r\nHost: x\r\n\r\n", - "GET /\x01 HTTP/1.1\r\nHost: x\r\n\r\n", - "GET / HTTP/1.1\r\nHost : x\r\n\r\n", - "GET / HTTP/1.1\r\nHost: x\r\n more\r\n\r\n", - "GET / HTTP/1.1\r\n: x\r\nHost: x\r\n\r\n", - "GET / HTTP/1.1\r\nHostx\r\n\r\n", - "GET / HTTP/1.1\r\nHost: x\r\nA: b\nc\r\n\r\n", - // semantic - "POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n", - "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\nContent-Length: 6\r\n\r\n", - "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 5, 6\r\n\r\n", - "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 05\r\n\r\n", - "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: abc\r\n\r\n", - "POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: gzip\r\n\r\n", - "GET / HTTP/1.1\r\n\r\n", - "GET / HTTP/1.1\r\nHost: a\r\nHost: b\r\n\r\n", - "GET / HTTP/1.1\r\nHost: a/b\r\n\r\n", - "CONNECT x:443 HTTP/1.1\r\nHost: x\r\n\r\n", - "GET * HTTP/1.1\r\nHost: x\r\n\r\n", - "GET /a/../b HTTP/1.1\r\nHost: x\r\n\r\n", - "GET /a\\b HTTP/1.1\r\nHost: x\r\n\r\n", - "GET /%25 HTTP/1.1\r\nHost: x\r\n\r\n", - "GET /%00 HTTP/1.1\r\nHost: x\r\n\r\n", - "GET /a#b HTTP/1.1\r\nHost: x\r\n\r\n", - }; -} diff --git a/core/diff/README.md b/core/diff/README.md deleted file mode 100644 index 8d5c988..0000000 --- a/core/diff/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Differential harness — C core vs C# reference - -Runs every input through **both** parsers in one process and asserts identical -results: - -- the C core `glyph11_parse_request` (via P/Invoke into `libglyph11.so`), and -- the C# reference `UltraHardenedParser`. - -It compares the outcome (parsed / incomplete / error), the HTTP status (400/431) -on rejection, and on success the **method / path / version** bytes, every -**header** and **query** pair, and the **consumed** byte count. - -Inputs are a set of curated vectors plus a deterministic, seeded fuzzer that -mutates and truncates them — so any failure is reproducible. - -## Run - -```sh -./core/diff/run-diff.sh # 1,000,000 fuzz iterations (default) -./core/diff/run-diff.sh 50000 # fewer -``` - -Exit code is non-zero if any divergence is found; up to 25 are printed with the -offending input. - -## Notes - -- The C ABI returns the **clean** header byte count in `consumed`; the C# - `bytesReadCount` is `total - 1`. The harness adds 1 to the C# value before - comparing — the only intentional difference between the two. -- `target` (the full request-target) is a C-only convenience field and is not - compared (the C# `BinaryRequest` exposes only the query-stripped `Path`). -- Build the C side with `-DGLYPH11_SANITIZE=ON` for the standalone C tests - (`ctest`); the differential harness uses a plain Release `.so` so the CLR can - load it without preloading the ASan runtime. diff --git a/core/diff/run-diff.sh b/core/diff/run-diff.sh deleted file mode 100755 index fdb8275..0000000 --- a/core/diff/run-diff.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -# Build the release shared library and run the C-vs-C# differential harness. -# ./run-diff.sh [iterations] (default 1,000,000) -set -euo pipefail -here="$(cd "$(dirname "$0")" && pwd)" -core="$(dirname "$here")" - -cmake -S "$core" -B "$core/build-rel" \ - -DGLYPH11_SANITIZE=OFF -DGLYPH11_BUILD_TESTS=OFF -DCMAKE_BUILD_TYPE=Release >/dev/null -cmake --build "$core/build-rel" >/dev/null - -LD_LIBRARY_PATH="$core/build-rel" \ - dotnet run -c Release --project "$here" -- "${1:-1000000}" diff --git a/core/include/glyph11.h b/core/include/glyph11.h deleted file mode 100644 index 374354b..0000000 --- a/core/include/glyph11.h +++ /dev/null @@ -1,209 +0,0 @@ -/* - * glyph11.h — C ABI for the Glyph11 HTTP/1.1 request parser core. - * - * A single hardened parser: full RFC 9110/9112 validation, configurable resource - * limits, and every semantic / smuggling check, fused into one pass. (The C# - * library also ships a no-validation FlexibleParser; that fast path is intentionally - * NOT ported — the C core exists for the security-critical validation.) - * - * Zero-copy, zero-allocation, dependency-free. All results are byte ranges - * (offset + length) into the caller's input buffer; the parser never copies - * payload bytes and never allocates. The caller owns the buffer and must keep - * it alive for as long as it reads the parsed spans. - * - * This is the single source of truth shared by every language binding. It is - * a faithful port of the Glyph11 C# reference implementation; the two are kept - * in lock-step via differential testing. - */ -#ifndef GLYPH11_H -#define GLYPH11_H - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* ---- Versioning -------------------------------------------------------- */ - -#define GLYPH11_VERSION_MAJOR 0 -#define GLYPH11_VERSION_MINOR 2 -#define GLYPH11_VERSION_PATCH 0 - -/* Packed ABI version: (major << 16) | (minor << 8) | patch. - * Bindings can compare this against the macros they compiled against. */ -uint32_t glyph11_abi_version(void); - -/* ---- Resource limits --------------------------------------------------- */ - -/* Zero-initialize and set fields, or call glyph11_limits_default(). The - * struct_size field must be set to sizeof(glyph11_limits) by the caller so the - * ABI can grow without breaking older callers. */ -typedef struct { - uint32_t struct_size; /* = sizeof(glyph11_limits) */ - uint32_t max_header_count; /* default 100 */ - uint32_t max_header_name_len; /* default 256 */ - uint32_t max_header_value_len; /* default 8192 */ - uint32_t max_url_len; /* default 8192 */ - uint32_t max_query_param_count; /* default 128 */ - uint32_t max_method_len; /* default 16 */ - uint32_t max_total_header_bytes; /* default 1048576 */ -} glyph11_limits; - -/* Fills *out with production defaults (and sets struct_size). */ -void glyph11_limits_default(glyph11_limits* out); - -/* ---- Result types ------------------------------------------------------ */ - -/* A byte range into the original input buffer (zero-copy). */ -typedef struct { uint32_t off; uint32_t len; } glyph11_span; - -typedef struct { - glyph11_span name; - glyph11_span value; -} glyph11_field; - -/* Parse target. The caller provides the headers (and optional query) arrays - * and their capacities; the parser writes spans and the *_count fields. If an - * array is too small the parse fails with GLYPH11_ERR_TOO_MANY_HEADERS / - * _TOO_MANY_QUERY_PARAMS (nothing is allocated or grown). */ -typedef struct { - glyph11_span method; - glyph11_span target; /* full request-target, as received */ - glyph11_span path; /* request-target with the query string removed */ - glyph11_span version; /* e.g. "HTTP/1.1" */ - - glyph11_field* headers; /* caller-provided storage (required) */ - uint32_t header_cap; /* in: capacity of headers[] */ - uint32_t header_count; /* out: number of headers written */ - - glyph11_field* query; /* caller-provided storage (may be NULL) */ - uint32_t query_cap; /* in: capacity of query[] */ - uint32_t query_count; /* out: number of query params written */ -} glyph11_request; - -/* ---- Status codes ------------------------------------------------------ */ - -typedef enum { - GLYPH11_OK = 0, /* a complete, valid header block was parsed */ - GLYPH11_INCOMPLETE = 1, /* no CRLFCRLF yet — read more (not an error) */ - - /* Structural / syntax violations (HTTP 400). */ - GLYPH11_ERR_REQUEST_LINE = 100, - GLYPH11_ERR_METHOD_TOKEN, - GLYPH11_ERR_METHOD_LENGTH, /* method longer than max_method_len (HTTP 400, matches C#) */ - GLYPH11_ERR_VERSION, - GLYPH11_ERR_TARGET_CHAR, - GLYPH11_ERR_BARE_LF, - GLYPH11_ERR_OBS_FOLD, - GLYPH11_ERR_WS_BEFORE_COLON, - GLYPH11_ERR_MULTIPLE_SP, - GLYPH11_ERR_HEADER_NAME, - GLYPH11_ERR_HEADER_VALUE, - GLYPH11_ERR_EMPTY_NAME, - GLYPH11_ERR_NO_COLON, - - /* Semantic violations (HTTP 400). */ - GLYPH11_ERR_TE_AND_CL = 200, - GLYPH11_ERR_CL_CONFLICT, - GLYPH11_ERR_CL_FORMAT, - GLYPH11_ERR_TE_VALUE, - GLYPH11_ERR_HOST_COUNT, - GLYPH11_ERR_HOST_FORMAT, - GLYPH11_ERR_DOT_SEGMENT, - GLYPH11_ERR_BACKSLASH, - GLYPH11_ERR_DOUBLE_ENCODING, - GLYPH11_ERR_NULL_BYTE, - GLYPH11_ERR_OVERLONG_UTF8, - GLYPH11_ERR_FRAGMENT, - GLYPH11_ERR_CONNECT, - GLYPH11_ERR_ASTERISK_FORM, - - /* Resource-limit breaches (HTTP 431). */ - GLYPH11_ERR_TOO_LARGE = 300, /* total header bytes over max_total_header_bytes */ - GLYPH11_ERR_URL_TOO_LONG, - GLYPH11_ERR_HEADER_NAME_TOO_LONG, - GLYPH11_ERR_HEADER_VALUE_TOO_LONG, - GLYPH11_ERR_TOO_MANY_HEADERS, - GLYPH11_ERR_TOO_MANY_QUERY_PARAMS -} glyph11_status; - -/* The HTTP response code a server should return for a status: 400, 431, or 0 - * for GLYPH11_OK / GLYPH11_INCOMPLETE. */ -int glyph11_status_http_code(glyph11_status status); - -/* A short, static, human-readable description. Never NULL; never freed. */ -const char* glyph11_status_message(glyph11_status status); - -/* ---- Parse ------------------------------------------------------------- */ - -/* - * Parse one complete HTTP/1.1 request header block from a single contiguous - * buffer. - * - * buf, len : bytes received so far. NOT required to be NUL-terminated and - * may contain NUL bytes. - * limits : resource limits to enforce; may be NULL to use defaults. - * req : caller sets headers/header_cap (and optionally query/query_cap); - * the parser fills the spans and *_count fields. - * consumed : optional out-param; on GLYPH11_OK, set to the number of bytes - * up to and including the terminating CRLFCRLF. - * - * Returns GLYPH11_OK, GLYPH11_INCOMPLETE, or a specific error. Performs no - * allocation and writes nothing outside the caller-provided arrays. - */ -glyph11_status glyph11_parse_request( - const uint8_t* buf, size_t len, - const glyph11_limits* limits, - glyph11_request* req, size_t* consumed); - -/* ---- Chunked transfer-encoding decoder (RFC 9112 §7.1) ----------------- */ - -/* Result of glyph11_chunk_decode. */ -typedef enum { - GLYPH11_CHUNK_OK = 0, /* progress made — drain `out`, supply more input, call again */ - GLYPH11_CHUNK_DONE = 1, /* terminal chunk and trailers consumed; the body is complete */ - GLYPH11_CHUNK_ERROR = 2, /* malformed chunked encoding — the server should respond 400 */ -} glyph11_chunk_result; - -/* Streaming decoder state. Zero-initialize (or call glyph11_chunk_decoder_init) - * before the first call, then reuse the same instance across calls for one body. */ -typedef struct { - uint32_t phase; - uint32_t digit_count; - uint32_t ext_bytes; - uint32_t reserved; - uint64_t chunk_size; - uint64_t remaining; -} glyph11_chunk_decoder; - -/* Reset a decoder to the start state. */ -void glyph11_chunk_decoder_init(glyph11_chunk_decoder* dec); - -/* - * Decode chunked transfer-encoding incrementally: strip the framing and write the - * decoded body bytes into `out`. Call repeatedly as data arrives — a chunk's - * payload may be split across calls (no re-parsing; only the payload is copied). - * - * dec : decoder state, reused across calls for one body. - * in, in_len : the next bytes of the chunked stream. - * out, out_cap : caller buffer to receive decoded body bytes. - * in_consumed : out — bytes consumed from `in` (advance your reader by this). - * out_written : out — decoded body bytes written to `out`. - * - * Returns GLYPH11_CHUNK_DONE when the body is complete, GLYPH11_CHUNK_ERROR on a - * malformed encoding, otherwise GLYPH11_CHUNK_OK — meaning the call stopped because - * `in` was exhausted or `out` filled; drain/refill and call again. No allocation. - */ -glyph11_chunk_result glyph11_chunk_decode( - glyph11_chunk_decoder* dec, - const uint8_t* in, size_t in_len, - uint8_t* out, size_t out_cap, - size_t* in_consumed, size_t* out_written); - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -#endif /* GLYPH11_H */ diff --git a/core/src/glyph11.c b/core/src/glyph11.c deleted file mode 100644 index 90fb9ad..0000000 --- a/core/src/glyph11.c +++ /dev/null @@ -1,719 +0,0 @@ -/* - * glyph11.c — the Glyph11 hardened HTTP/1.1 request parser. - * - * A faithful port of the C# UltraHardenedParser: full RFC 9110/9112 structural - * validation, configurable resource limits, and every semantic / smuggling - * check, fused into one pass. Scalar (correctness-first); SIMD is a follow-on. - * - * Discipline: no allocation; length-bounded scanning only (memchr, never - * strlen/strchr); every index checked against `len`. - */ -#include "glyph11.h" -#include /* memchr, memcmp */ -#if defined(__SSE2__) -# include -#endif -#if defined(__AVX2__) -# include -#endif - -/* ======================================================================== */ -/* Versioning / limits / status */ -/* ======================================================================== */ - -uint32_t glyph11_abi_version(void) -{ - return ((uint32_t)GLYPH11_VERSION_MAJOR << 16) - | ((uint32_t)GLYPH11_VERSION_MINOR << 8) - | (uint32_t)GLYPH11_VERSION_PATCH; -} - -void glyph11_limits_default(glyph11_limits* out) -{ - if (!out) return; - out->struct_size = (uint32_t)sizeof(glyph11_limits); - out->max_header_count = 100; - out->max_header_name_len = 256; - out->max_header_value_len = 8192; - out->max_url_len = 8192; - out->max_query_param_count = 128; - out->max_method_len = 16; - out->max_total_header_bytes = 1048576u; -} - -int glyph11_status_http_code(glyph11_status s) -{ - if (s == GLYPH11_OK || s == GLYPH11_INCOMPLETE) return 0; - if ((int)s >= 300) return 431; /* resource-limit breaches */ - return 400; /* structural + semantic */ -} - -const char* glyph11_status_message(glyph11_status s) -{ - switch (s) { - case GLYPH11_OK: return "ok"; - case GLYPH11_INCOMPLETE: return "incomplete: need more data"; - case GLYPH11_ERR_REQUEST_LINE: return "invalid request line"; - case GLYPH11_ERR_METHOD_TOKEN: return "method contains invalid token characters"; - case GLYPH11_ERR_METHOD_LENGTH: return "method length exceeds limit"; - case GLYPH11_ERR_VERSION: return "invalid HTTP version"; - case GLYPH11_ERR_TARGET_CHAR: return "request-target contains invalid characters"; - case GLYPH11_ERR_BARE_LF: return "bare LF; only CRLF line endings are allowed"; - case GLYPH11_ERR_OBS_FOLD: return "obsolete line folding (obs-fold) is not allowed"; - case GLYPH11_ERR_WS_BEFORE_COLON: return "whitespace between header name and colon"; - case GLYPH11_ERR_MULTIPLE_SP: return "multiple spaces in request line"; - case GLYPH11_ERR_HEADER_NAME: return "header name contains invalid token characters"; - case GLYPH11_ERR_HEADER_VALUE: return "header value contains invalid characters"; - case GLYPH11_ERR_EMPTY_NAME: return "header name is empty"; - case GLYPH11_ERR_NO_COLON: return "malformed header line: missing colon"; - case GLYPH11_ERR_TE_AND_CL: return "both Transfer-Encoding and Content-Length present"; - case GLYPH11_ERR_CL_CONFLICT: return "conflicting Content-Length values"; - case GLYPH11_ERR_CL_FORMAT: return "invalid Content-Length format"; - case GLYPH11_ERR_TE_VALUE: return "invalid Transfer-Encoding value; only 'chunked' is accepted"; - case GLYPH11_ERR_HOST_COUNT: return "request must have exactly one Host header"; - case GLYPH11_ERR_HOST_FORMAT: return "invalid Host header: contains '@' or '/'"; - case GLYPH11_ERR_DOT_SEGMENT: return "dot segment in request path"; - case GLYPH11_ERR_BACKSLASH: return "backslash in request path"; - case GLYPH11_ERR_DOUBLE_ENCODING: return "double-encoded percent (%25) in path"; - case GLYPH11_ERR_NULL_BYTE: return "encoded null byte (%00) in path"; - case GLYPH11_ERR_OVERLONG_UTF8: return "overlong UTF-8 sequence in path"; - case GLYPH11_ERR_FRAGMENT: return "fragment indicator (#) in request-target"; - case GLYPH11_ERR_CONNECT: return "CONNECT method is not allowed on origin servers"; - case GLYPH11_ERR_ASTERISK_FORM: return "asterisk-form request-target is only valid for OPTIONS"; - case GLYPH11_ERR_TOO_LARGE: return "total header size exceeds limit"; - case GLYPH11_ERR_URL_TOO_LONG: return "URL length exceeds limit"; - case GLYPH11_ERR_HEADER_NAME_TOO_LONG: return "header name length exceeds limit"; - case GLYPH11_ERR_HEADER_VALUE_TOO_LONG: return "header value length exceeds limit"; - case GLYPH11_ERR_TOO_MANY_HEADERS: return "header count exceeds limit"; - case GLYPH11_ERR_TOO_MANY_QUERY_PARAMS: return "query parameter count exceeds limit"; - default: return "unknown"; - } -} - -/* ======================================================================== */ -/* Character classes (RFC 9110/9112) */ -/* ======================================================================== */ - -static int is_digit(uint8_t c) { return c >= '0' && c <= '9'; } -static int is_ows(uint8_t c) { return c == ' ' || c == '\t'; } - -/* token = ALPHA / DIGIT / "!#$%&'*+-.^_`|~" (RFC 9110 §5.6.2) */ -static int is_token(uint8_t c) -{ - if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) - return 1; - switch (c) { - case '!': case '#': case '$': case '%': case '&': case '\'': case '*': - case '+': case '-': case '.': case '^': case '_': case '`': case '|': case '~': - return 1; - default: - return 0; - } -} - -/* field-value = HTAB / SP / VCHAR(0x21-0x7E) / obs-text(0x80-0xFF) (RFC 9110 §5.5) */ -static int is_fieldvalue(uint8_t c) -{ - return c == 0x09 || c == 0x20 || (c >= 0x21 && c <= 0x7E) || c >= 0x80; -} - -/* request-target chars: SP(0x20) + VCHAR(0x21-0x7E), ASCII only (RFC 9112 §3.2) */ -static int is_reqtarget(uint8_t c) -{ - return c >= 0x20 && c <= 0x7E; -} - -/* index of first byte failing pred, or -1 if all pass (scalar; used for the - short token scans — method and header names) */ -static long first_invalid(const uint8_t* p, size_t len, int (*pred)(uint8_t)) -{ - for (size_t i = 0; i < len; i++) - if (!pred(p[i])) return (long)i; - return -1; -} - -/* Range-class scanners for the long, hot fields (header value, request-target). - AVX2 (32 B/iter) when built -march=x86-64-v3 (GLYPH11_X86_AVX2 — the linux-x64 - package), else SSE2 (16 B/iter); scalar fallback on non-x86. The AVX2 tier is - compile-time gated so it INLINES into the parse loop (a runtime-dispatched call - was measured slower — the call overhead beat the width). (NEON is a TODO pending - ARM CI; the scalar fallback keeps non-x86 correct, just not vectorized.) */ - -/* index of first byte not a valid field-value char (RFC 9110 §5.5), or -1. - valid = HTAB(0x09) | (b >= 0x20 & b != 0x7F) [obs-text 0x80-0xFF allowed] */ -static long scan_fieldvalue(const uint8_t* p, size_t len) -{ - size_t i = 0; -#if defined(__AVX2__) - { - const __m256i c09 = _mm256_set1_epi8(0x09); - const __m256i c20 = _mm256_set1_epi8(0x20); - const __m256i c7f = _mm256_set1_epi8(0x7F); - for (; i + 32 <= len; i += 32) { - __m256i v = _mm256_loadu_si256((const __m256i*)(p + i)); - __m256i ge20 = _mm256_cmpeq_epi8(_mm256_max_epu8(v, c20), v); /* b >= 0x20 */ - __m256i htab = _mm256_cmpeq_epi8(v, c09); - __m256i del = _mm256_cmpeq_epi8(v, c7f); - __m256i valid = _mm256_or_si256(htab, _mm256_andnot_si256(del, ge20)); - unsigned mask = (unsigned)_mm256_movemask_epi8(valid); - if (mask != 0xFFFFFFFFu) return (long)(i + (size_t)__builtin_ctz(~mask)); - } - } -#endif -#if defined(__SSE2__) - const __m128i c09 = _mm_set1_epi8(0x09); - const __m128i c20 = _mm_set1_epi8(0x20); - const __m128i c7f = _mm_set1_epi8(0x7F); - for (; i + 16 <= len; i += 16) { - __m128i v = _mm_loadu_si128((const __m128i*)(p + i)); - __m128i ge20 = _mm_cmpeq_epi8(_mm_max_epu8(v, c20), v); /* b >= 0x20 */ - __m128i htab = _mm_cmpeq_epi8(v, c09); - __m128i del = _mm_cmpeq_epi8(v, c7f); - __m128i valid = _mm_or_si128(htab, _mm_andnot_si128(del, ge20)); - unsigned mask = (unsigned)_mm_movemask_epi8(valid) & 0xFFFFu; - if (mask != 0xFFFFu) - return (long)(i + (size_t)__builtin_ctz((~mask) & 0xFFFFu)); - } -#endif - for (; i < len; i++) if (!is_fieldvalue(p[i])) return (long)i; - return -1; -} - -/* index of first byte not a valid request-target char (SP + VCHAR), or -1. - valid = (b >= 0x20 & b <= 0x7E) */ -static long scan_reqtarget(const uint8_t* p, size_t len) -{ - size_t i = 0; -#if defined(__AVX2__) - { - const __m256i c20 = _mm256_set1_epi8(0x20); - const __m256i c7e = _mm256_set1_epi8(0x7E); - for (; i + 32 <= len; i += 32) { - __m256i v = _mm256_loadu_si256((const __m256i*)(p + i)); - __m256i ge20 = _mm256_cmpeq_epi8(_mm256_max_epu8(v, c20), v); /* b >= 0x20 */ - __m256i le7e = _mm256_cmpeq_epi8(_mm256_min_epu8(v, c7e), v); /* b <= 0x7E */ - __m256i valid = _mm256_and_si256(ge20, le7e); - unsigned mask = (unsigned)_mm256_movemask_epi8(valid); - if (mask != 0xFFFFFFFFu) return (long)(i + (size_t)__builtin_ctz(~mask)); - } - } -#endif -#if defined(__SSE2__) - const __m128i c20 = _mm_set1_epi8(0x20); - const __m128i c7e = _mm_set1_epi8(0x7E); - for (; i + 16 <= len; i += 16) { - __m128i v = _mm_loadu_si128((const __m128i*)(p + i)); - __m128i ge20 = _mm_cmpeq_epi8(_mm_max_epu8(v, c20), v); /* b >= 0x20 */ - __m128i le7e = _mm_cmpeq_epi8(_mm_min_epu8(v, c7e), v); /* b <= 0x7E */ - __m128i valid = _mm_and_si128(ge20, le7e); - unsigned mask = (unsigned)_mm_movemask_epi8(valid) & 0xFFFFu; - if (mask != 0xFFFFu) - return (long)(i + (size_t)__builtin_ctz((~mask) & 0xFFFFu)); - } -#endif - for (; i < len; i++) if (!is_reqtarget(p[i])) return (long)i; - return -1; -} - -/* case-insensitive ASCII compare of a span against a lowercase literal */ -static int ci_eq(const uint8_t* a, size_t alen, const char* lower, size_t llen) -{ - if (alen != llen) return 0; - for (size_t i = 0; i < alen; i++) - if ((uint8_t)(a[i] | 0x20) != (uint8_t)lower[i]) return 0; - return 1; -} - -/* HTTP-version = "HTTP/" DIGIT "." DIGIT; accept only 1.0 / 1.1 (exactly 8 bytes) */ -static int is_http_version(const uint8_t* p, size_t len) -{ - return len == 8 - && p[0]=='H' && p[1]=='T' && p[2]=='T' && p[3]=='P' && p[4]=='/' - && p[5]=='1' && p[6]=='.' && (p[7]=='0' || p[7]=='1'); -} - -/* ======================================================================== */ -/* Delimiter search (length-bounded) */ -/* ======================================================================== */ - -/* index of the '\r' of the first CRLF in p[0..len), else -1 */ -static long find_crlf(const uint8_t* p, size_t len) -{ - size_t i = 0; - while (i < len) { - const uint8_t* cr = (const uint8_t*)memchr(p + i, '\r', len - i); - if (!cr) return -1; - size_t idx = (size_t)(cr - p); - if (idx + 1 >= len) return -1; /* CR at end, no LF can follow */ - if (p[idx + 1] == '\n') return (long)idx; - i = idx + 1; - } - return -1; -} - -/* index of the first '\r' of CRLFCRLF in p[0..len), else -1 */ -static long find_crlfcrlf(const uint8_t* p, size_t len) -{ - if (len < 4) return -1; - size_t i = 0, limit = len - 3; /* idx must satisfy idx+3 < len */ - while (i < limit) { - const uint8_t* cr = (const uint8_t*)memchr(p + i, '\r', limit - i); - if (!cr) return -1; - size_t idx = (size_t)(cr - p); - if (p[idx+1]=='\n' && p[idx+2]=='\r' && p[idx+3]=='\n') return (long)idx; - i = idx + 1; - } - return -1; -} - -/* ======================================================================== */ -/* Semantic helpers (ported from UltraHardenedParser) */ -/* ======================================================================== */ - -/* trim leading + trailing OWS; reports the trimmed sub-range via out params */ -static void trim_ows(const uint8_t* v, size_t len, size_t* out_off, size_t* out_len) -{ - size_t s = 0, e = len; - while (s < e && is_ows(v[s])) s++; - while (e > s && is_ows(v[e-1])) e--; - *out_off = s; *out_len = e - s; -} - -/* Content-Length value syntax (RFC 9110 §8.6): 1*DIGIT, optional comma-separated - duplicates. Rejects empty, non-digit, leading zeros, > uint64 max, trailing comma. */ -static int cl_value_valid(const uint8_t* v, size_t len) -{ - static const char ULMAX[20] = {'1','8','4','4','6','7','4','4','0','7', - '3','7','0','9','5','5','1','6','1','5'}; - const int MAXD = 20; - if (len == 0) return 0; - size_t pos = 0; - while (pos < len) { - while (pos < len && is_ows(v[pos])) pos++; - if (pos >= len) return 0; - if (!is_digit(v[pos])) return 0; - size_t dstart = pos; - if (v[pos] == '0') { - pos++; - if (pos < len && is_digit(v[pos])) return 0; /* leading zero */ - } else { - pos++; - while (pos < len && is_digit(v[pos])) pos++; - } - size_t dcount = pos - dstart; - if (dcount > (size_t)MAXD) return 0; - if (dcount == (size_t)MAXD) { - for (int j = 0; j < MAXD; j++) { - if (v[dstart + (size_t)j] > (uint8_t)ULMAX[j]) return 0; - if (v[dstart + (size_t)j] < (uint8_t)ULMAX[j]) break; - } - } - while (pos < len && is_ows(v[pos])) pos++; - if (pos >= len) return 1; - if (v[pos] != ',') return 0; - pos++; - } - return 0; /* trailing comma with nothing after */ -} - -/* true if one CL value has comma-separated segments that are not all identical */ -static int cl_comma_conflict(const uint8_t* v, size_t len) -{ - if (memchr(v, ',', len) == NULL) return 0; - const uint8_t* fseg = NULL; size_t fseglen = 0; - size_t start = 0; - while (start < len) { - while (start < len && is_ows(v[start])) start++; - size_t end = start; - while (end < len && v[end] != ',') end++; - size_t te = end; - while (te > start && is_ows(v[te-1])) te--; - const uint8_t* seg = v + start; size_t seglen = te - start; - if (fseg == NULL) { fseg = seg; fseglen = seglen; } - else if (!(seglen == fseglen && memcmp(seg, fseg, seglen) == 0)) return 1; - start = end + 1; - } - return 0; -} - -/* single-pass path semantics: fragment, backslash, %25, %00, dot segments, - overlong UTF-8. Returns GLYPH11_OK if clean, else the specific error. */ -static glyph11_status validate_path(const uint8_t* p, size_t len) -{ - for (size_t i = 0; i < len; i++) { - uint8_t b = p[i]; - if (b == '#') return GLYPH11_ERR_FRAGMENT; - if (b == '\\') return GLYPH11_ERR_BACKSLASH; - if (b == '%' && i + 2 < len) { - uint8_t h1 = p[i+1], h2 = p[i+2]; - if (h1 == '2' && h2 == '5') return GLYPH11_ERR_DOUBLE_ENCODING; - if (h1 == '0' && h2 == '0') return GLYPH11_ERR_NULL_BYTE; - } - if (b == '/') { - size_t rem = len - i - 1; - if (rem >= 1 && p[i+1] == '.') { - if (rem == 1 || p[i+2] == '/') return GLYPH11_ERR_DOT_SEGMENT; - if (p[i+2] == '.' && (rem == 2 || p[i+3] == '/')) return GLYPH11_ERR_DOT_SEGMENT; - } - } - /* Overlong UTF-8 (RFC 3629). Mostly unreachable — is_reqtarget already - rejects bytes > 0x7E — but ported for completeness. */ - if (b == 0xC0 || b == 0xC1) return GLYPH11_ERR_OVERLONG_UTF8; - if (b == 0xE0 && i+1 < len && p[i+1] < 0xA0) return GLYPH11_ERR_OVERLONG_UTF8; - if (b == 0xF0 && i+1 < len && p[i+1] < 0x90) return GLYPH11_ERR_OVERLONG_UTF8; - } - return GLYPH11_OK; -} - -/* ======================================================================== */ -/* Parse */ -/* ======================================================================== */ - -glyph11_status glyph11_parse_request( - const uint8_t* buf, size_t len, - const glyph11_limits* limits, - glyph11_request* req, size_t* consumed) -{ - glyph11_limits L; - if (limits) L = *limits; else glyph11_limits_default(&L); - - if (consumed) *consumed = 0; - if (req) { - req->method = (glyph11_span){0, 0}; - req->target = (glyph11_span){0, 0}; - req->path = (glyph11_span){0, 0}; - req->version = (glyph11_span){0, 0}; - req->header_count = 0; - req->query_count = 0; - } - if (!buf || !req || !req->headers) return GLYPH11_INCOMPLETE; - - long he = find_crlfcrlf(buf, len); - if (he < 0) return GLYPH11_INCOMPLETE; - size_t header_end = (size_t)he; - size_t total = header_end + 4; - if (total > L.max_total_header_bytes) return GLYPH11_ERR_TOO_LARGE; - - /* ---- request line: buf[0 .. rl_end) ---- */ - long rl = find_crlf(buf, len); - if (rl < 0) return GLYPH11_ERR_REQUEST_LINE; - size_t rl_end = (size_t)rl; - - if (memchr(buf, '\n', rl_end) != NULL) return GLYPH11_ERR_BARE_LF; - - const uint8_t* sp1 = (const uint8_t*)memchr(buf, ' ', rl_end); - if (!sp1) return GLYPH11_ERR_REQUEST_LINE; - size_t first_space = (size_t)(sp1 - buf); - - const uint8_t* sp2 = (const uint8_t*)memchr(buf + first_space + 1, ' ', - rl_end - (first_space + 1)); - if (!sp2) return GLYPH11_ERR_REQUEST_LINE; - size_t second_space = (size_t)(sp2 - buf); - - /* reject multiple spaces (RFC 9112 §3) */ - if (second_space > first_space + 1 && buf[first_space + 1] == ' ') - return GLYPH11_ERR_MULTIPLE_SP; - if (second_space + 1 < rl_end && buf[second_space + 1] == ' ') - return GLYPH11_ERR_MULTIPLE_SP; - - /* ---- method ---- */ - size_t method_len = first_space; - if (method_len == 0 || method_len > L.max_method_len) return GLYPH11_ERR_METHOD_LENGTH; - if (first_invalid(buf, method_len, is_token) >= 0) return GLYPH11_ERR_METHOD_TOKEN; - req->method = (glyph11_span){0, (uint32_t)method_len}; - - int method_is_connect = ci_eq(buf, method_len, "connect", 7); - int method_is_options = ci_eq(buf, method_len, "options", 7); - if (method_is_connect) return GLYPH11_ERR_CONNECT; - - /* ---- url / request-target ---- */ - size_t url_start = first_space + 1; - size_t url_len = second_space - url_start; - if (url_len > L.max_url_len) return GLYPH11_ERR_URL_TOO_LONG; - const uint8_t* url = buf + url_start; - if (scan_reqtarget(url, url_len) >= 0) return GLYPH11_ERR_TARGET_CHAR; - req->target = (glyph11_span){(uint32_t)url_start, (uint32_t)url_len}; - - /* ---- version ---- */ - size_t ver_start = second_space + 1; - size_t ver_len = rl_end - ver_start; - if (!is_http_version(buf + ver_start, ver_len)) return GLYPH11_ERR_VERSION; - req->version = (glyph11_span){(uint32_t)ver_start, (uint32_t)ver_len}; - - /* ---- path + query ---- */ - size_t path_off, path_len; - const uint8_t* qmark = (const uint8_t*)memchr(url, '?', url_len); - if (qmark) { - size_t qrel = (size_t)(qmark - url); - path_off = url_start; path_len = qrel; - - size_t qa_start = url_start + qrel + 1; - size_t qlen = url_len - (qrel + 1); - const uint8_t* query = buf + qa_start; - uint32_t pcount = 0; - size_t cur = 0; - while (cur < qlen) { - size_t pair_abs = qa_start + cur; - const uint8_t* amp = (const uint8_t*)memchr(query + cur, '&', qlen - cur); - size_t pair_len = amp ? (size_t)(amp - (query + cur)) : (qlen - cur); - const uint8_t* pair = query + cur; - const uint8_t* eqp = (const uint8_t*)memchr(pair, '=', pair_len); - size_t eq = eqp ? (size_t)(eqp - pair) : 0; - int has_param = (eqp && eq > 0) || (!eqp && pair_len > 0); - if (has_param) { - if (pcount >= L.max_query_param_count) return GLYPH11_ERR_TOO_MANY_QUERY_PARAMS; - pcount++; - if (req->query) { - if (req->query_count >= req->query_cap) return GLYPH11_ERR_TOO_MANY_QUERY_PARAMS; - glyph11_field* f = &req->query[req->query_count]; - if (eqp && eq > 0) { - f->name = (glyph11_span){(uint32_t)pair_abs, (uint32_t)eq}; - f->value = (glyph11_span){(uint32_t)(pair_abs + eq + 1), - (uint32_t)(pair_len - (eq + 1))}; - } else { - f->name = (glyph11_span){(uint32_t)pair_abs, (uint32_t)pair_len}; - f->value = (glyph11_span){0, 0}; - } - req->query_count++; - } - } - cur += pair_len + (amp ? 1 : 0); - } - } else { - path_off = url_start; path_len = url_len; - } - req->path = (glyph11_span){(uint32_t)path_off, (uint32_t)path_len}; - - /* path semantics + asterisk-form */ - glyph11_status ps = validate_path(buf + path_off, path_len); - if (ps != GLYPH11_OK) return ps; - if (path_len == 1 && buf[path_off] == '*' && !method_is_options) - return GLYPH11_ERR_ASTERISK_FORM; - - /* ---- headers ---- */ - size_t line_start = rl_end + 2; - uint32_t hcount = 0; - int has_cl = 0, has_te = 0, host_count = 0; - const uint8_t* first_cl = NULL; size_t first_cl_len = 0; - - for (;;) { - long ll = find_crlf(buf + line_start, len - line_start); - if (ll < 0) return GLYPH11_ERR_REQUEST_LINE; /* unreachable: block ends in CRLFCRLF */ - size_t line_len = (size_t)ll; - if (line_len == 0) break; /* empty line → end of headers */ - - const uint8_t* line = buf + line_start; - if (memchr(line, '\n', line_len) != NULL) return GLYPH11_ERR_BARE_LF; - if (line[0] == ' ' || line[0] == '\t') return GLYPH11_ERR_OBS_FOLD; - - const uint8_t* col = (const uint8_t*)memchr(line, ':', line_len); - if (!col) return GLYPH11_ERR_NO_COLON; - size_t colon = (size_t)(col - line); - if (colon == 0) return GLYPH11_ERR_EMPTY_NAME; - if (line[colon - 1] == ' ' || line[colon - 1] == '\t') return GLYPH11_ERR_WS_BEFORE_COLON; - - /* name */ - size_t name_len = colon; - if (name_len > L.max_header_name_len) return GLYPH11_ERR_HEADER_NAME_TOO_LONG; - if (first_invalid(line, name_len, is_token) >= 0) return GLYPH11_ERR_HEADER_NAME; - - /* value (trim leading OWS) */ - size_t line_end = line_start + line_len; - size_t val_abs = line_start + colon + 1; - while (val_abs < line_end && (buf[val_abs] == ' ' || buf[val_abs] == '\t')) val_abs++; - size_t val_len = line_end - val_abs; - const uint8_t* val = buf + val_abs; - if (val_len > L.max_header_value_len) return GLYPH11_ERR_HEADER_VALUE_TOO_LONG; - if (scan_fieldvalue(val, val_len) >= 0) return GLYPH11_ERR_HEADER_VALUE; - - /* count + store */ - if (hcount >= L.max_header_count) return GLYPH11_ERR_TOO_MANY_HEADERS; - if (hcount >= req->header_cap) return GLYPH11_ERR_TOO_MANY_HEADERS; - req->headers[hcount].name = (glyph11_span){(uint32_t)line_start, (uint32_t)name_len}; - req->headers[hcount].value = (glyph11_span){(uint32_t)val_abs, (uint32_t)val_len}; - hcount++; - - /* inline semantic checks, keyed by name length then case-insensitive name */ - if (name_len == 14 && ci_eq(line, name_len, "content-length", 14)) { - if (!cl_value_valid(val, val_len)) return GLYPH11_ERR_CL_FORMAT; - if (cl_comma_conflict(val, val_len)) return GLYPH11_ERR_CL_CONFLICT; - if (has_cl) { - if (!(val_len == first_cl_len && memcmp(val, first_cl, val_len) == 0)) - return GLYPH11_ERR_CL_CONFLICT; - } else { - first_cl = val; first_cl_len = val_len; has_cl = 1; - } - } else if (name_len == 17 && ci_eq(line, name_len, "transfer-encoding", 17)) { - has_te = 1; - size_t toff, tlen; trim_ows(val, val_len, &toff, &tlen); - if (!ci_eq(val + toff, tlen, "chunked", 7)) return GLYPH11_ERR_TE_VALUE; - } else if (name_len == 4 && ci_eq(line, name_len, "host", 4)) { - host_count++; - if (memchr(val, '@', val_len) || memchr(val, '/', val_len)) - return GLYPH11_ERR_HOST_FORMAT; - } - - line_start = line_end + 2; - } - - /* cross-header semantics */ - if (has_te && has_cl) return GLYPH11_ERR_TE_AND_CL; - if (host_count != 1) return GLYPH11_ERR_HOST_COUNT; - - req->header_count = hcount; - if (consumed) *consumed = total; - return GLYPH11_OK; -} - -/* ===================== Chunked transfer-encoding decoder ================= */ - -enum { - CH_SIZE = 0, /* reading hex chunk size */ - CH_EXT, /* reading chunk extension (after ';') */ - CH_SIZE_LF, /* size/ext CR seen, expect LF */ - CH_DATA, /* copying chunk payload */ - CH_DATA_CR, /* payload done, expect CR */ - CH_DATA_LF, /* payload CR seen, expect LF */ - CH_TRAILER, /* start of a trailer line (or the final empty line) */ - CH_TRAILER_END_LF, /* empty-line CR seen, expect LF -> done */ - CH_TRAILER_LINE, /* scanning a trailer line */ - CH_TRAILER_LINE_LF, /* trailer-line CR seen, expect LF */ - CH_DONE /* body complete */ -}; - -#define GLYPH11_MAX_CHUNK_EXT 4096 - -void glyph11_chunk_decoder_init(glyph11_chunk_decoder* dec) -{ - dec->phase = CH_SIZE; - dec->digit_count = 0; - dec->ext_bytes = 0; - dec->reserved = 0; - dec->chunk_size = 0; - dec->remaining = 0; -} - -static int ch_hexval(uint8_t b) -{ - if (b >= '0' && b <= '9') return b - '0'; - if (b >= 'a' && b <= 'f') return b - 'a' + 10; - if (b >= 'A' && b <= 'F') return b - 'A' + 10; - return -1; -} - -glyph11_chunk_result glyph11_chunk_decode( - glyph11_chunk_decoder* dec, - const uint8_t* in, size_t in_len, - uint8_t* out, size_t out_cap, - size_t* in_consumed, size_t* out_written) -{ - size_t ip = 0, op = 0; - glyph11_chunk_result result = GLYPH11_CHUNK_OK; - - if (dec->phase == CH_DONE) { - if (in_consumed) *in_consumed = 0; - if (out_written) *out_written = 0; - return GLYPH11_CHUNK_DONE; - } - - while (ip < in_len) { - uint8_t b = in[ip]; - switch (dec->phase) { - case CH_SIZE: { - int hv = ch_hexval(b); - if (dec->digit_count == 0 && (b == ' ' || b == '\t')) { result = GLYPH11_CHUNK_ERROR; goto done; } - if (dec->digit_count == 0 && b == '-') { result = GLYPH11_CHUNK_ERROR; goto done; } - if (dec->digit_count == 1 && dec->chunk_size == 0 && (b == 'x' || b == 'X')) { result = GLYPH11_CHUNK_ERROR; goto done; } - if (hv >= 0) { - if (dec->digit_count >= 16) { result = GLYPH11_CHUNK_ERROR; goto done; } /* overflow */ - dec->chunk_size = (dec->chunk_size << 4) | (uint64_t)hv; - dec->digit_count++; - ip++; - break; - } - if (dec->digit_count == 0) { result = GLYPH11_CHUNK_ERROR; goto done; } /* no digits */ - if (b == '_') { result = GLYPH11_CHUNK_ERROR; goto done; } - if (b == ';') { dec->ext_bytes = 0; dec->phase = CH_EXT; ip++; break; } - if (b == '\n') { result = GLYPH11_CHUNK_ERROR; goto done; } /* bare LF */ - if (b == '\r') { dec->phase = CH_SIZE_LF; ip++; break; } - result = GLYPH11_CHUNK_ERROR; goto done; /* missing CRLF */ - } - - case CH_EXT: { - if (dec->ext_bytes > GLYPH11_MAX_CHUNK_EXT) { result = GLYPH11_CHUNK_ERROR; goto done; } - if (b == 0) { result = GLYPH11_CHUNK_ERROR; goto done; } /* NUL */ - if (b == '\n') { result = GLYPH11_CHUNK_ERROR; goto done; } /* bare LF */ - if (b == '\r') { - if (dec->ext_bytes == 0) { result = GLYPH11_CHUNK_ERROR; goto done; } /* bare ';' */ - dec->phase = CH_SIZE_LF; ip++; break; - } - dec->ext_bytes++; ip++; break; - } - - case CH_SIZE_LF: { - if (b != '\n') { result = GLYPH11_CHUNK_ERROR; goto done; } - ip++; - if (dec->chunk_size == 0) dec->phase = CH_TRAILER; - else { dec->remaining = dec->chunk_size; dec->phase = CH_DATA; } - break; - } - - case CH_DATA: { - size_t avail = in_len - ip; - size_t want = dec->remaining < avail ? (size_t)dec->remaining : avail; - size_t room = out_cap - op; - size_t n = want < room ? want : room; - if (n) { memcpy(out + op, in + ip, n); ip += n; op += n; dec->remaining -= n; } - if (dec->remaining == 0) { dec->phase = CH_DATA_CR; break; } - goto done; /* input exhausted or output full; result stays OK */ - } - - case CH_DATA_CR: { - if (b == '\n') { result = GLYPH11_CHUNK_ERROR; goto done; } /* bare LF after data */ - if (b != '\r') { result = GLYPH11_CHUNK_ERROR; goto done; } /* missing CRLF */ - dec->phase = CH_DATA_LF; ip++; break; - } - - case CH_DATA_LF: { - if (b != '\n') { result = GLYPH11_CHUNK_ERROR; goto done; } - ip++; - dec->phase = CH_SIZE; - dec->chunk_size = 0; dec->digit_count = 0; dec->ext_bytes = 0; - break; - } - - case CH_TRAILER: { - if (b == '\r') { dec->phase = CH_TRAILER_END_LF; ip++; break; } - if (b == '\n') { result = GLYPH11_CHUNK_ERROR; goto done; } /* bare LF */ - dec->phase = CH_TRAILER_LINE; ip++; break; /* first content byte */ - } - - case CH_TRAILER_END_LF: { - if (b != '\n') { result = GLYPH11_CHUNK_ERROR; goto done; } - ip++; - dec->phase = CH_DONE; - result = GLYPH11_CHUNK_DONE; - goto done; - } - - case CH_TRAILER_LINE: { - if (b == '\r') { dec->phase = CH_TRAILER_LINE_LF; ip++; break; } - if (b == '\n') { result = GLYPH11_CHUNK_ERROR; goto done; } /* bare LF */ - ip++; break; /* trailer content */ - } - - case CH_TRAILER_LINE_LF: { - if (b != '\n') { result = GLYPH11_CHUNK_ERROR; goto done; } - ip++; dec->phase = CH_TRAILER; break; - } - - default: - result = GLYPH11_CHUNK_ERROR; goto done; - } - } - -done: - if (in_consumed) *in_consumed = ip; - if (out_written) *out_written = op; - return result; -} diff --git a/core/tests/fuzz_smoke.c b/core/tests/fuzz_smoke.c deleted file mode 100644 index cdf38d5..0000000 --- a/core/tests/fuzz_smoke.c +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Deterministic fuzz smoke. Mutates and truncates seed requests and runs them - * through the parser; the win condition is "never reads out of bounds", which - * is enforced by building this under ASan/UBSan. Reproducible (fixed LCG seed) - * so any failure is debuggable. A coverage-guided libFuzzer/AFL target is a - * follow-on (needs clang); this catches the obvious OOB classes with gcc. - */ -#include "glyph11.h" -#include - -static unsigned int rng = 0x9e3779b9u; -static unsigned int nxt(void) { rng = rng * 1103515245u + 12345u; return rng; } - -int main(void) -{ - static const char* seeds[] = { - "GET /api/users?a=1&b=2 HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n", - "POST /x HTTP/1.1\r\nHost: y\r\nTransfer-Encoding: chunked\r\n\r\n", - "OPTIONS * HTTP/1.1\r\nHost: z\r\nAccept: */*\r\n\r\n", - "GET /a/../b%2e%2e\\c?p=1 HTTP/1.1\r\nHost: h\r\nContent-Length: 0, 1\r\n\r\n", - }; - enum { NSEED = (int)(sizeof seeds / sizeof seeds[0]) }; - static glyph11_field h[256], q[256]; - glyph11_request r; - unsigned char buf[1024]; - - /* 1. every prefix of every seed (exercises the incomplete paths) */ - for (int s = 0; s < NSEED; s++) { - size_t n = strlen(seeds[s]); - for (size_t L = 0; L <= n; L++) { - memset(&r, 0, sizeof r); - r.headers = h; r.header_cap = 256; r.query = q; r.query_cap = 256; - glyph11_parse_request((const uint8_t*)seeds[s], L, NULL, &r, NULL); - } - } - - /* 2. mutated + randomly-truncated copies */ - for (int it = 0; it < 500000; it++) { - int s = (int)(nxt() % (unsigned)NSEED); - size_t n = strlen(seeds[s]); - if (n > sizeof buf) n = sizeof buf; - memcpy(buf, seeds[s], n); - int flips = (int)(nxt() % 6); - for (int k = 0; k < flips && n; k++) - buf[nxt() % n] = (unsigned char)(nxt() >> 13); - size_t len = nxt() % (n + 1); - memset(&r, 0, sizeof r); - r.headers = h; r.header_cap = 256; r.query = q; r.query_cap = 256; - glyph11_parse_request(buf, len, NULL, &r, NULL); - } - return 0; -} diff --git a/core/tests/test_glyph11.c b/core/tests/test_glyph11.c deleted file mode 100644 index 5273792..0000000 --- a/core/tests/test_glyph11.c +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Conformance tests for the Glyph11 C parser. - * - * These encode the same accept/reject decisions as the C# reference suite. The - * live C-vs-C# differential/fuzz harness is a follow-on; this verifies the port - * against known cases and runs clean under ASan/UBSan. - */ -#include "glyph11.h" -#include -#include - -static int g_total = 0, g_fail = 0; - -static glyph11_status run(const void* data, size_t len, - const glyph11_limits* L, glyph11_request* out) -{ - static glyph11_field hdrs[256], qry[256]; - memset(out, 0, sizeof *out); - out->headers = hdrs; out->header_cap = 256; - out->query = qry; out->query_cap = 256; - return glyph11_parse_request((const uint8_t*)data, len, L, out, NULL); -} - -static void expect(const char* name, const void* data, size_t len, - const glyph11_limits* L, glyph11_status want) -{ - glyph11_request r; - glyph11_status got = run(data, len, L, &r); - g_total++; - if (got != want) { - g_fail++; - printf(" FAIL %-26s got=%d (%s) want=%d (%s)\n", - name, got, glyph11_status_message(got), - want, glyph11_status_message(want)); - } -} - -/* string-literal helper: sizeof-1 includes embedded NULs, excludes the trailing \0 */ -#define E(name, lit, want) expect(name, lit, sizeof(lit) - 1, NULL, want) -#define EL(name, lit, L, want) expect(name, lit, sizeof(lit) - 1, L, want) - -/* chunked decoder: decode `in` in one call; check the result and (on DONE) the body */ -static void chk(const char* name, const char* in, glyph11_chunk_result want, const char* want_out) -{ - glyph11_chunk_decoder d; - glyph11_chunk_decoder_init(&d); - unsigned char out[256]; - size_t written; - glyph11_chunk_result r = glyph11_chunk_decode( - &d, (const uint8_t*)in, strlen(in), out, sizeof out, NULL, &written); - g_total++; - int ok = (r == want); - if (ok && want == GLYPH11_CHUNK_DONE) - ok = (written == strlen(want_out)) && (memcmp(out, want_out, written) == 0); - if (!ok) { - g_fail++; - printf(" FAIL chunk %-20s result=%d written=%zu\n", name, (int)r, written); - } -} - -/* feed `in` one byte per call — exercises every cross-call boundary (size, data, - * terminal, trailers) with the streaming decoder, which persists state (no rollback) */ -static void chk_stream(const char* name, const char* in, glyph11_chunk_result want, const char* want_out) -{ - glyph11_chunk_decoder d; - glyph11_chunk_decoder_init(&d); - unsigned char out[256]; - size_t total = 0; - size_t n = strlen(in); - glyph11_chunk_result r = GLYPH11_CHUNK_OK; - for (size_t i = 0; i < n; i++) { - size_t w; - r = glyph11_chunk_decode(&d, (const uint8_t*)(in + i), 1, out + total, sizeof out - total, NULL, &w); - total += w; - if (r != GLYPH11_CHUNK_OK) break; - } - g_total++; - int ok = (r == want); - if (ok && want == GLYPH11_CHUNK_DONE) - ok = (total == strlen(want_out)) && (memcmp(out, want_out, total) == 0); - if (!ok) { - g_fail++; - printf(" FAIL chunk-stream %-16s result=%d total=%zu\n", name, (int)r, total); - } -} - -static void test_valid_spans(void) -{ - const char* in = "GET /api/users?a=1&b=2 HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n"; - size_t len = strlen(in); - static glyph11_field hdrs[16], qry[16]; - glyph11_request r; - memset(&r, 0, sizeof r); - r.headers = hdrs; r.header_cap = 16; - r.query = qry; r.query_cap = 16; - size_t consumed = 0; - glyph11_status s = glyph11_parse_request((const uint8_t*)in, len, NULL, &r, &consumed); - - g_total++; - int ok = (s == GLYPH11_OK) - && r.method.len == 3 && memcmp(in + r.method.off, "GET", 3) == 0 - && r.path.len == 10 && memcmp(in + r.path.off, "/api/users", 10) == 0 - && r.target.len == 18 && memcmp(in + r.target.off, "/api/users?a=1&b=2", 18) == 0 - && r.version.len == 8 && memcmp(in + r.version.off, "HTTP/1.1", 8) == 0 - && r.query_count == 2 - && r.query[0].name.len == 1 && in[r.query[0].name.off] == 'a' - && r.query[0].value.len == 1 && in[r.query[0].value.off] == '1' - && r.query[1].name.len == 1 && in[r.query[1].name.off] == 'b' - && r.query[1].value.len == 1 && in[r.query[1].value.off] == '2' - && r.header_count == 2 - && r.headers[0].name.len == 4 && memcmp(in + r.headers[0].name.off, "Host", 4) == 0 - && r.headers[0].value.len == 11 && memcmp(in + r.headers[0].value.off, "example.com", 11) == 0 - && r.headers[1].name.len == 6 && memcmp(in + r.headers[1].name.off, "Accept", 6) == 0 - && r.headers[1].value.len == 3 && memcmp(in + r.headers[1].value.off, "*/*", 3) == 0 - && consumed == len; - if (!ok) { - g_fail++; - printf(" FAIL valid-spans (status=%d %s, hc=%u qc=%u consumed=%zu)\n", - s, glyph11_status_message(s), r.header_count, r.query_count, consumed); - } -} - -int main(void) -{ - printf("glyph11 v%u.%u.%u (abi 0x%06x)\n", - GLYPH11_VERSION_MAJOR, GLYPH11_VERSION_MINOR, GLYPH11_VERSION_PATCH, - glyph11_abi_version()); - - /* ---- valid ---- */ - test_valid_spans(); - E("valid-minimal", "GET / HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_OK); - E("valid-cl", "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\n\r\n", GLYPH11_OK); - E("valid-cl-zero", "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n", GLYPH11_OK); - E("valid-te-chunked","POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n", GLYPH11_OK); - E("valid-options-*", "OPTIONS * HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_OK); - E("valid-http10", "GET / HTTP/1.0\r\nHost: x\r\n\r\n", GLYPH11_OK); - E("valid-flagparam", "GET /?flag HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_OK); - - /* ---- incomplete ---- */ - E("incomplete", "GET / HTTP/1.1\r\nHost: x\r\n", GLYPH11_INCOMPLETE); - E("incomplete-empty","", GLYPH11_INCOMPLETE); - - /* ---- request line ---- */ - E("multiple-sp", "GET / HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_MULTIPLE_SP); - E("bad-version", "GET / HTTP/2.0\r\nHost: x\r\n\r\n", GLYPH11_ERR_VERSION); - E("method-token", "G(T / HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_METHOD_TOKEN); - E("bare-lf-reqline", "GET /\n/ HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_BARE_LF); - E("target-control", "GET /\x01 HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_TARGET_CHAR); - - /* ---- headers (structural) ---- */ - E("ws-before-colon", "GET / HTTP/1.1\r\nHost : x\r\n\r\n", GLYPH11_ERR_WS_BEFORE_COLON); - E("obs-fold", "GET / HTTP/1.1\r\nHost: x\r\n more\r\n\r\n", GLYPH11_ERR_OBS_FOLD); - E("empty-name", "GET / HTTP/1.1\r\n: x\r\nHost: x\r\n\r\n", GLYPH11_ERR_EMPTY_NAME); - E("no-colon", "GET / HTTP/1.1\r\nHostx\r\n\r\n", GLYPH11_ERR_NO_COLON); - E("header-name-tok", "GET / HTTP/1.1\r\nHost: x\r\nBa d: y\r\n\r\n", GLYPH11_ERR_HEADER_NAME); - E("header-value-nul","GET / HTTP/1.1\r\nHost: x\r\nA: b\x00" "c\r\n\r\n", GLYPH11_ERR_HEADER_VALUE); - E("bare-lf-header", "GET / HTTP/1.1\r\nHost: x\r\nA: b\nc\r\n\r\n", GLYPH11_ERR_BARE_LF); - - /* ---- semantic: smuggling ---- */ - E("te-and-cl", "POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n", GLYPH11_ERR_TE_AND_CL); - E("cl-conflict", "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\nContent-Length: 6\r\n\r\n", GLYPH11_ERR_CL_CONFLICT); - E("cl-comma-confl", "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 5, 6\r\n\r\n", GLYPH11_ERR_CL_CONFLICT); - E("cl-leading-zero", "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 05\r\n\r\n", GLYPH11_ERR_CL_FORMAT); - E("cl-nondigit", "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: abc\r\n\r\n", GLYPH11_ERR_CL_FORMAT); - E("te-bad-value", "POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: gzip\r\n\r\n", GLYPH11_ERR_TE_VALUE); - - /* ---- semantic: host / method / target ---- */ - E("no-host", "GET / HTTP/1.1\r\n\r\n", GLYPH11_ERR_HOST_COUNT); - E("two-host", "GET / HTTP/1.1\r\nHost: a\r\nHost: b\r\n\r\n", GLYPH11_ERR_HOST_COUNT); - E("host-format", "GET / HTTP/1.1\r\nHost: a/b\r\n\r\n", GLYPH11_ERR_HOST_FORMAT); - E("connect", "CONNECT x:443 HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_CONNECT); - E("asterisk-nonopt", "GET * HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_ASTERISK_FORM); - - /* ---- semantic: path traversal ---- */ - E("dot-segment", "GET /a/../b HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_DOT_SEGMENT); - E("dot-trailing", "GET /a/.. HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_DOT_SEGMENT); - E("backslash", "GET /a\\b HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_BACKSLASH); - E("double-encoding", "GET /%25 HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_DOUBLE_ENCODING); - E("encoded-null", "GET /%00 HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_NULL_BYTE); - E("fragment", "GET /a#b HTTP/1.1\r\nHost: x\r\n\r\n", GLYPH11_ERR_FRAGMENT); - - /* ---- limits (custom) ---- */ - { - glyph11_limits L; glyph11_limits_default(&L); L.max_total_header_bytes = 10; - EL("too-large", "GET / HTTP/1.1\r\nHost: x\r\n\r\n", &L, GLYPH11_ERR_TOO_LARGE); - } - { - glyph11_limits L; glyph11_limits_default(&L); L.max_method_len = 2; - EL("method-length", "GET / HTTP/1.1\r\nHost: x\r\n\r\n", &L, GLYPH11_ERR_METHOD_LENGTH); - } - { - glyph11_limits L; glyph11_limits_default(&L); L.max_url_len = 3; - EL("url-too-long", "GET /toolong HTTP/1.1\r\nHost: x\r\n\r\n", &L, GLYPH11_ERR_URL_TOO_LONG); - } - { - glyph11_limits L; glyph11_limits_default(&L); L.max_header_name_len = 3; - EL("name-too-long", "GET / HTTP/1.1\r\nHost: x\r\n\r\n", &L, GLYPH11_ERR_HEADER_NAME_TOO_LONG); - } - { - glyph11_limits L; glyph11_limits_default(&L); L.max_header_value_len = 2; - EL("value-too-long", "GET / HTTP/1.1\r\nHost: example\r\n\r\n", &L, GLYPH11_ERR_HEADER_VALUE_TOO_LONG); - } - { - glyph11_limits L; glyph11_limits_default(&L); L.max_header_count = 1; - EL("too-many-headers", "GET / HTTP/1.1\r\nHost: x\r\nA: b\r\n\r\n", &L, GLYPH11_ERR_TOO_MANY_HEADERS); - } - { - glyph11_limits L; glyph11_limits_default(&L); L.max_query_param_count = 1; - EL("too-many-query", "GET /?a=1&b=2 HTTP/1.1\r\nHost: x\r\n\r\n", &L, GLYPH11_ERR_TOO_MANY_QUERY_PARAMS); - } - - /* ---- chunked transfer-encoding decoder (parity with the C# managed suite) ---- */ - /* single-call */ - chk("chunk-single", "5\r\nHello\r\n0\r\n\r\n", GLYPH11_CHUNK_DONE, "Hello"); - chk("chunk-multi", "5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n", GLYPH11_CHUNK_DONE, "Hello World"); - chk("chunk-multi2", "3\r\nFoo\r\n4\r\nBar!\r\n0\r\n\r\n", GLYPH11_CHUNK_DONE, "FooBar!"); - chk("chunk-empty", "0\r\n\r\n", GLYPH11_CHUNK_DONE, ""); - chk("chunk-ext", "5;name=value\r\nHello\r\n0\r\n\r\n", GLYPH11_CHUNK_DONE, "Hello"); - chk("chunk-trailer", "5\r\nHello\r\n0\r\nTrailer: value\r\n\r\n", GLYPH11_CHUNK_DONE, "Hello"); - chk("chunk-empty-trail", "0\r\nTrailer: value\r\n\r\n", GLYPH11_CHUNK_DONE, ""); - chk("chunk-hex-size", "a\r\n0123456789\r\n0\r\n\r\n", GLYPH11_CHUNK_DONE, "0123456789"); - chk("chunk-incomplete", "5\r\nHel", GLYPH11_CHUNK_OK, ""); - /* malformed */ - chk("chunk-bad-hex", "G\r\n\r\n", GLYPH11_CHUNK_ERROR, ""); - chk("chunk-bare-lf", "5\r\nHello\nX", GLYPH11_CHUNK_ERROR, ""); - chk("chunk-leading-ws", " 5\r\n", GLYPH11_CHUNK_ERROR, ""); - chk("chunk-hex-prefix", "0x5\r\n", GLYPH11_CHUNK_ERROR, ""); - chk("chunk-negative", "-5\r\nHello\r\n0\r\n\r\n", GLYPH11_CHUNK_ERROR, ""); - chk("chunk-underscore", "5_\r\n", GLYPH11_CHUNK_ERROR, ""); - - /* incremental: byte-by-byte feeding splits size/data/terminal/trailer across calls */ - chk_stream("stream-data", "5\r\nHello\r\n0\r\n\r\n", GLYPH11_CHUNK_DONE, "Hello"); - chk_stream("stream-multi", "3\r\nFoo\r\n2\r\nHi\r\n0\r\n\r\n", GLYPH11_CHUNK_DONE, "FooHi"); - chk_stream("stream-size", "1a\r\nabcdefghijklmnopqrstuvwxyz\r\n0\r\n\r\n", GLYPH11_CHUNK_DONE, "abcdefghijklmnopqrstuvwxyz"); - chk_stream("stream-terminal", "0\r\n\r\n", GLYPH11_CHUNK_DONE, ""); - chk_stream("stream-trailer", "5\r\nHello\r\n0\r\nX: y\r\n\r\n", GLYPH11_CHUNK_DONE, "Hello"); - chk_stream("stream-bad-after", "5\r\nHello\r\n-3\r\nFoo\r\n0\r\n\r\n", GLYPH11_CHUNK_ERROR, ""); - - /* explicit two-call split: a chunk's data crosses the call boundary */ - { - glyph11_chunk_decoder d; - glyph11_chunk_decoder_init(&d); - unsigned char out[64]; - size_t w1, w2; - glyph11_chunk_result r1 = glyph11_chunk_decode(&d, (const uint8_t*)"5\r\nHel", 6, out, sizeof out, NULL, &w1); - glyph11_chunk_result r2 = glyph11_chunk_decode(&d, (const uint8_t*)"lo\r\n0\r\n\r\n", 9, out + w1, sizeof out - w1, NULL, &w2); - g_total++; - if (!(r1 == GLYPH11_CHUNK_OK && r2 == GLYPH11_CHUNK_DONE && w1 + w2 == 5 && memcmp(out, "Hello", 5) == 0)) { - g_fail++; - printf(" FAIL chunk-split\n"); - } - } - - printf("%d/%d passed%s\n", g_total - g_fail, g_total, g_fail ? " *** FAILURES ***" : ""); - return g_fail ? 1 : 0; -} diff --git a/site/glyph11-native.html b/site/glyph11-native.html deleted file mode 100644 index 5b89d64..0000000 --- a/site/glyph11-native.html +++ /dev/null @@ -1,206 +0,0 @@ - - - - - -Glyph11.Native — Glyph11 docs - - - - - - -
- - -
- native · P/Invoke -

Glyph11.Native

-

The same hardened parser as Glyph11, running in the C core - (libglyph11) via P/Invoke. Native speed, zero allocation — you provide the field - storage and the parser writes byte-offset spans into it. Native binaries are bundled for six RIDs.

- -

Install

-

dotnet add package Glyph11.Native

-

No native setup — the right libglyph11 is bundled per platform and resolved automatically.

- -

Quick start

-
using System.Text;
-using Glyph11.Native;
-
-byte[] request = Encoding.ASCII.GetBytes(
-    "GET /api/users?page=1 HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n");
-
-// Your security policy. Default caps MaxHeaderCount at 100 (see "Storage & limits").
-var limits = Glyph11Limits.Default;
-
-// Caller-provided storage the parser fills with (offset,length) spans — no allocation.
-// Size to the limits so any request the policy accepts fits (see the note below).
-Span<Glyph11Field> headers = stackalloc Glyph11Field[(int)limits.MaxHeaderCount];
-Span<Glyph11Field> query   = stackalloc Glyph11Field[(int)limits.MaxQueryParameterCount];
-
-// Returns a status int; `r` holds the request-line spans + counts.
-int status = Glyph11Parser.Parse(request, headers, query, limits, out Glyph11Result r);
-
-if (status == Glyph11Parser.Ok)            // 0 = OK
-{
-    // Fields are offsets into `request` — slice it to read them (see "Reading fields").
-    Console.WriteLine(Encoding.ASCII.GetString(request, (int)r.Path.Offset, (int)r.Path.Length)); // /api/users
-}
- -

Storage & limits

-

Glyph11Limits mirrors the C policy (all uint): MaxHeaderCount, - MaxHeaderNameLength, MaxHeaderValueLength, MaxUrlLength, - MaxQueryParameterCount, MaxMethodLength, MaxTotalHeaderBytes.

-
// Derive a custom policy (it's a struct — copy and set fields):
-var limits = Glyph11Limits.Default;
-limits.MaxHeaderCount      = 64;
-limits.MaxTotalHeaderBytes = 16 * 1024;
-
Size the arrays to your limits. The parser bounds-checks every write — it never - overflows. But a headers array smaller than MaxHeaderCount silently lowers your - effective limit: once it fills, parsing stops with TOO_MANY_HEADERS. For large limits, rent - from ArrayPool<Glyph11Field> instead of stackalloc.
-
// Large limits → pool instead of stackalloc:
-Glyph11Field[] headers = ArrayPool<Glyph11Field>.Shared.Rent((int)limits.MaxHeaderCount);
-try
-{
-    int status = Glyph11Parser.Parse(request, headers, query, limits, out var r);
-    // ...
-}
-finally { ArrayPool<Glyph11Field>.Shared.Return(headers); }
- -

Reading fields

-

The output is offset-based: Glyph11Result and each Glyph11Field hold - Glyph11Span values (Offset + Length, uint) that index - into the buffer you passed. Nothing is copied; slice the buffer to read.

-
// A tiny helper to turn a span into a string against the input buffer.
-string Slice(Glyph11Span s) => Encoding.ASCII.GetString(request, (int)s.Offset, (int)s.Length);
-
-// --- request line (Glyph11Result) ---
-Slice(r.Method);   // GET
-Slice(r.Target);   // /api/users?page=1   (full request target, as received)
-Slice(r.Path);     // /api/users          (query stripped)
-Slice(r.Version);  // HTTP/1.1
-long bodyOffset = r.Consumed; // where the body begins (bytes consumed by the header block)
-
-// --- headers: read r.HeaderCount entries from the array you passed ---
-for (int i = 0; i < r.HeaderCount; i++)
-    Console.WriteLine($"{Slice(headers[i].Name)}: {Slice(headers[i].Value)}");
-
-// --- query parameters: r.QueryCount entries ---
-for (int i = 0; i < r.QueryCount; i++)
-    Console.WriteLine($"{Slice(query[i].Name)} = {Slice(query[i].Value)}");
-
Lifetime: the offsets are valid only while the input buffer is alive and - unmodified. Read (or copy out) the fields you need before reusing the buffer for the next request.
- -

ReadOnlySequence input

-

The C core needs one contiguous buffer, so a fragmented ReadOnlySequence<byte> uses a - dedicated overload: single-segment is parsed in place (zero-copy); multi-segment is linearized into a - scratch buffer you provide (keeping the package's zero-allocation contract). The out - parsed span tells you which buffer the offsets index into.

-
// `scratch` only needs to hold a request when the input is fragmented; size it to your
-// MaxTotalHeaderBytes. Single-segment input leaves it untouched.
-Span<byte> scratch = stackalloc byte[16 * 1024];
-
-int status = Glyph11Parser.Parse(
-    sequence, scratch, headers, query, limits,
-    out Glyph11Result r,
-    out ReadOnlySpan<byte> parsed); // ← the contiguous bytes the offsets index into
-
-if (status == Glyph11Parser.Ok)
-{
-    // Slice against `parsed` (NOT the original sequence): it is either the input's first
-    // segment (single-segment) or `scratch` (multi-segment).
-    var method = parsed.Slice((int)r.Method.Offset, (int)r.Method.Length);
-    var path   = parsed.Slice((int)r.Path.Offset,   (int)r.Path.Length);
-}
-// Don't overwrite `scratch` while still reading fields from a multi-segment parse.
- -

Status codes

-
int status = Glyph11Parser.Parse(request, headers, query, limits, out var r);
-
-if (status == Glyph11Parser.Ok)          { /* 0 — valid request parsed */ }
-else if (status == Glyph11Parser.Incomplete) { /* 1 — need more bytes, read & retry */ }
-else
-{
-    // Any other value is a protocol/limit error. Map it to an HTTP response code:
-    int http = Glyph11Parser.HttpCode(status); // 400 (malformed) or 431 (limit exceeded)
-}
-
-// Sanity-check the loaded native library at startup if you like:
-uint abi = Glyph11Parser.AbiVersion();   // packed major.minor.patch of libglyph11
- -

Chunked decoding

-

The C core's streaming chunked decoder strips the framing and writes the decoded body into an output - buffer you own. Feed it each network read — a chunk's payload may span calls; the decoder carries the - partial state.

-
// One decoder per body. Init zeroes it (the struct is the whole state — no allocation).
-Glyph11Chunked.Init(out Glyph11ChunkDecoder decoder);
-
-Span<byte> output = stackalloc byte[64 * 1024]; // your drain buffer
-
-Glyph11ChunkResult r = Glyph11Chunked.Decode(
-    ref decoder,
-    input,                 // a chunk-encoded network read
-    output,                // decoded body bytes are written here
-    out int inConsumed,    // input bytes consumed this call
-    out int outWritten);   // decoded bytes written to `output`
-
-switch (r)
-{
-    case Glyph11ChunkResult.Ok:    break; // input exhausted or output full — advance, drain, call again
-    case Glyph11ChunkResult.Done:  break; // terminal chunk seen — body complete
-    case Glyph11ChunkResult.Error: break; // malformed framing — reject the request
-}
- -

Platforms & native resolution

-

Bundled native binaries (resolved automatically from runtimes/<rid>/native/):

- - - - - - - -
OSRIDs
Linuxlinux-x64, linux-arm64
Windowswin-x64, win-arm64
macOSosx-x64, osx-arm64
-
linux-x64 requires AVX2 (Haswell / 2013+ — universal on modern - servers and cloud): the SIMD scanners inline into the parse loop for ~15% on large headers. Other RIDs - use the portable baseline.
-

To point at a hand-built core (tests, local builds), set GLYPH11_NATIVE_PATH to an explicit - path to libglyph11.{so,dll,dylib}; otherwise the bundled binary is used.

- - - -
-
- - - - diff --git a/site/glyph11-pico.html b/site/glyph11-pico.html deleted file mode 100644 index b8ac1bd..0000000 --- a/site/glyph11-pico.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - -Glyph11.Pico — Glyph11 docs - - - - - - -
- - -
- native · fastest -

Glyph11.Pico

-

The fastest path to a BinaryRequest. - picohttpparser (native, SSE4.2) tokenizes; a thin - managed layer fills the same BinaryRequest as Glyph11. The - trade: no validation beyond picohttpparser's — raw speed, same request shape.

- -

Install

-

dotnet add package Glyph11.Pico

-

Depends on Glyph11 (for BinaryRequest and chunked decoding) and bundles the - native libglyph11pico per platform.

- -

Quick start

-
using System.Text;
-using Glyph11.Pico;
-using Glyph11.Protocol; // BinaryRequest
-
-// Same reusable BinaryRequest as the managed parser — allocate once, Clear() per request.
-var request = new BinaryRequest();
-
-// Contiguous input (a byte[] / ReadOnlyMemory<byte> you already have buffered).
-byte[] input = "GET /api/users?page=1 HTTP/1.1\r\nHost: example.com\r\n\r\n"u8.ToArray();
-
-// Returns true on a parsed header block; false if malformed or incomplete.
-// `consumed` marks the header block (see "The trade-off" for the off-by-one note).
-if (PicoParser.TryParse(input, request, out int consumed))
-{
-    // Identical BinaryRequest to Glyph11 — Method / Path / Version / Headers / QueryParameters.
-    Console.WriteLine(Encoding.ASCII.GetString(request.Method.Span)); // GET
-    Console.WriteLine(Encoding.ASCII.GetString(request.Path.Span));   // /api/users
-}
- -

Reading the result

-

Because it fills the same BinaryRequest, reading is identical to - Glyph11 — every field is a zero-copy - ReadOnlyMemory<byte> slice of your input.

-
request.Method;          // "GET"
-request.Path;            // "/api/users"  (query stripped)
-request.Version;         // "HTTP/1.1"
-request.Body;            // remainder of the buffer after the header block
-
-for (int i = 0; i < request.Headers.Count; i++)
-{
-    var (name, value) = request.Headers[i];  // KeyValuePair of byte slices
-    // ...
-}
-for (int i = 0; i < request.QueryParameters.Count; i++)
-{
-    var (key, val) = request.QueryParameters[i];
-    // ...
-}
-

The header capacity per parse is PicoParser.MaxHeaders (256); a request with more headers - is rejected (false).

- -

ReadOnlySequence input

-

picohttpparser needs one contiguous buffer, so a fragmented sequence uses a dedicated overload: - single-segment is parsed in place (zero-copy); multi-segment is linearized into a fresh array, which the - resulting BinaryRequest slices keep alive (just like the managed parser — no buffer for you - to manage).

-
// `sequence` is a ReadOnlySequence<byte> from a PipeReader, socket, etc.
-if (PicoParser.TryParse(sequence, request, out int consumed))
-{
-    // request.* exactly as in the contiguous case.
-}
-// Single-segment → zero-copy. Multi-segment → one ToArray() internally; the BinaryRequest
-// references that array, so it stays alive as long as you hold the request.
- -

Chunked bodies

-

Chunked decoding is glyph11's — this package depends on Glyph11, so use - ChunkedBodyStream directly (see the Glyph11 page).

-
using Glyph11.Parser;
-
-var decoder = new ChunkedBodyStream();
-var r = decoder.TryReadChunk(input, out int consumed, out int dataOffset, out int dataLength);
-// r: Chunk | Completed | NeedMoreData | Error
-
- -

The trade-off

-

Glyph11.Pico does the same parsing as Glyph11, but only picohttpparser's validation. It does - not do glyph11's hardening:

- - - - - - - - - -
Glyph11 / Glyph11.NativeGlyph11.Pico
Method / path / version / headers / query✅✅ (same BinaryRequest)
Token / field-value character validation✅❌
Path normalization & traversal checks✅❌
Host / limits / Transfer-Encoding+Content-Length conflict✅❌
Request-smuggling defenses✅❌
-
Use it when you validate elsewhere or trust the source. For untrusted, - internet-facing input that you want hardened in the parser, use Glyph11 or - Glyph11.Native.
-

consumed follows glyph11's convention: it's the header-block length minus one, so the body - begins at consumed + 1 — a drop-in match for the managed parser.

- -

Platforms & native resolution

-

Native libglyph11pico is bundled for linux-x64/arm64, - win-x64/arm64, and osx-x64/arm64, resolved automatically. The x86-64 builds use - picohttpparser's SSE4.2 fast path. Override with GLYPH11_PICO_NATIVE_PATH (an explicit path - to libglyph11pico.{so,dll,dylib}) for local builds.

- - - -
-
- - - - diff --git a/src/Glyph11.sln b/src/Glyph11.sln index ddbd871..dba17e9 100644 --- a/src/Glyph11.sln +++ b/src/Glyph11.sln @@ -8,14 +8,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "..\Benchmarks EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Glyph11.Example", "..\Examples\Glyph11.Example\Glyph11.Example.csproj", "{9BB41491-CA01-4D1B-93CC-BEF6C449179A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Glyph11.Native.Example", "..\Examples\Glyph11.Native.Example\Glyph11.Native.Example.csproj", "{3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Glyph11.Pico.Example", "..\Examples\Glyph11.Pico.Example\Glyph11.Pico.Example.csproj", "{CE3462D0-F32A-4ED0-91E1-6B03980FB656}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Glyph11.Native", "..\bindings\dotnet\Glyph11.Native\Glyph11.Native.csproj", "{C6187C79-0229-4E86-9D90-7613A368D7D9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Glyph11.Pico", "..\bindings\dotnet\Glyph11.Pico\Glyph11.Pico.csproj", "{D4818609-70E0-4F92-8638-A5B096C54CAB}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -74,54 +66,6 @@ Global {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Release|x64.Build.0 = Release|Any CPU {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Release|x86.ActiveCfg = Release|Any CPU {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Release|x86.Build.0 = Release|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|x64.ActiveCfg = Debug|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|x64.Build.0 = Debug|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|x86.ActiveCfg = Debug|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|x86.Build.0 = Debug|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|Any CPU.Build.0 = Release|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|x64.ActiveCfg = Release|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|x64.Build.0 = Release|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|x86.ActiveCfg = Release|Any CPU - {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|x86.Build.0 = Release|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|x64.ActiveCfg = Debug|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|x64.Build.0 = Debug|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|x86.ActiveCfg = Debug|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|x86.Build.0 = Debug|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|Any CPU.Build.0 = Release|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|x64.ActiveCfg = Release|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|x64.Build.0 = Release|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|x86.ActiveCfg = Release|Any CPU - {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|x86.Build.0 = Release|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Debug|x64.ActiveCfg = Debug|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Debug|x64.Build.0 = Debug|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Debug|x86.ActiveCfg = Debug|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Debug|x86.Build.0 = Debug|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Release|Any CPU.Build.0 = Release|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Release|x64.ActiveCfg = Release|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Release|x64.Build.0 = Release|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Release|x86.ActiveCfg = Release|Any CPU - {C6187C79-0229-4E86-9D90-7613A368D7D9}.Release|x86.Build.0 = Release|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Debug|x64.ActiveCfg = Debug|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Debug|x64.Build.0 = Debug|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Debug|x86.ActiveCfg = Debug|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Debug|x86.Build.0 = Debug|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Release|Any CPU.Build.0 = Release|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Release|x64.ActiveCfg = Release|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Release|x64.Build.0 = Release|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Release|x86.ActiveCfg = Release|Any CPU - {D4818609-70E0-4F92-8638-A5B096C54CAB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Glyph11/Glyph11.csproj b/src/Glyph11/Glyph11.csproj index 06acc97..325b737 100644 --- a/src/Glyph11/Glyph11.csproj +++ b/src/Glyph11/Glyph11.csproj @@ -29,7 +29,11 @@ - + From 42c6f188f00697e116b58379ca0a3254f78d84fb Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Mon, 2 Nov 2026 15:54:50 +0000 Subject: [PATCH 2/3] Docs: Glyph11 is one managed package With the bindings gone, the README, the docs site, the examples index and the benchmark README described three packages and their native build steps. They now describe the managed parser only: the three-package table, the "all three produce the same data" claim, the Native / Pico / Kotlin sections and the native build instructions are removed; the benchmark tables keep the existing Glyph11 numbers; the site loses the Native and Pico pages from its navigation. A short note says that Glyph11.Native and Glyph11.Pico are discontinued: their published versions stay on NuGet but get no further releases. Also fixes the harness link, which pointed at a bench/ directory that does not exist (the harness lives in Benchmarks/). Refs #57, #66, #69, #70, #72. --- Benchmarks/README.md | 18 +---- Examples/README.md | 41 +++-------- README.md | 157 +++++++------------------------------------ site/benchmarks.html | 47 ++++++------- site/glyph11.html | 6 +- site/index.html | 56 +++------------ 6 files changed, 67 insertions(+), 258 deletions(-) diff --git a/Benchmarks/README.md b/Benchmarks/README.md index f007418..00a5d26 100644 --- a/Benchmarks/README.md +++ b/Benchmarks/README.md @@ -1,7 +1,7 @@ # Benchmarks Reproduces the benchmark tables on the [docs site](https://dotnet-web-stack.github.io/Glyph11/benchmarks.html): -the three packages — **Glyph11** (managed), **Glyph11.Native**, **Glyph11.Pico** — on +**Glyph11** on - **request header parsing** (contiguous and multi-segment), and - **chunked body decoding** @@ -11,20 +11,8 @@ in-process and are identical to what the site reports. ## Run -`Glyph11.Native` and `Glyph11.Pico` need their native libraries on the load path (the NuGet -packages bundle them; in-repo, build the cores and point at them): - ```sh -cmake -S core -B core/build -DGLYPH11_BUILD_TESTS=OFF && cmake --build core/build -cmake -S bindings/dotnet/Glyph11.Pico/native -B pico-build && cmake --build pico-build - -GLYPH11_NATIVE_PATH="$PWD/core/build/libglyph11.so" \ -GLYPH11_PICO_NATIVE_PATH="$PWD/pico-build/libglyph11pico.so" \ - dotnet run -c Release --project Benchmarks +dotnet run -c Release --project Benchmarks ``` -(`Glyph11` — pure managed — needs no native library; its columns work with no env vars.) - -Numbers vary run-to-run and by hardware; treat them as relative. For the lowest `linux-x64` -native numbers, build the cores with AVX2/SSE4.2 (`-DGLYPH11_X86_AVX2=ON` / -`-DGLYPH11_PICO_X86_AVX2=ON`) as the shipped packages do. +Numbers vary run-to-run and by hardware; treat them as relative. diff --git a/Examples/README.md b/Examples/README.md index 96ff487..129c5a9 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -1,47 +1,22 @@ # Examples -Runnable, fully-commented examples for each Glyph11 NuGet package. Each project mirrors what a -consumer writes (the same `using`s and APIs) — in-repo they use a `ProjectReference`; a real app -uses `dotnet add package `. +A runnable, fully-commented example for the Glyph11 NuGet package. The project mirrors what a +consumer writes (the same `using`s and APIs) — in-repo it uses a `ProjectReference`; a real app +uses `dotnet add package Glyph11`. -| Project | Package | Native lib needed to run? | -|---|---|---| -| [`Glyph11.Example`](Glyph11.Example) | [`Glyph11`](https://www.nuget.org/packages/Glyph11/) | No — pure managed | -| [`Glyph11.Native.Example`](Glyph11.Native.Example) | [`Glyph11.Native`](https://www.nuget.org/packages/Glyph11.Native/) | Yes — `libglyph11` | -| [`Glyph11.Pico.Example`](Glyph11.Pico.Example) | [`Glyph11.Pico`](https://www.nuget.org/packages/Glyph11.Pico/) | Yes — `libglyph11pico` | +| Project | Package | +|---|---| +| [`Glyph11.Example`](Glyph11.Example) | [`Glyph11`](https://www.nuget.org/packages/Glyph11/) | -Each `Program.cs` walks through **every option**: contiguous parse, `ReadOnlySequence` parse, +`Program.cs` walks through **every option**: contiguous parse, `ReadOnlySequence` parse, custom limits, reading fields/headers/query, chunked decoding, status/error handling, and reuse. ## Run -The managed example needs nothing extra: +Pure managed — nothing extra needed: ```sh dotnet run --project Examples/Glyph11.Example ``` -The **native** examples need the matching native library on the load path. With the NuGet packages -it's bundled automatically; running in-repo, build the core and point at it with an env var: - -```sh -# Glyph11.Native — build libglyph11 and run -cmake -S core -B core/build -DGLYPH11_BUILD_TESTS=OFF && cmake --build core/build -GLYPH11_NATIVE_PATH="$PWD/core/build/libglyph11.so" \ - dotnet run --project Examples/Glyph11.Native.Example - -# Glyph11.Pico — build libglyph11pico and run -cmake -S bindings/dotnet/Glyph11.Pico/native -B pico-build && cmake --build pico-build -GLYPH11_PICO_NATIVE_PATH="$PWD/pico-build/libglyph11pico.so" \ - dotnet run --project Examples/Glyph11.Pico.Example -``` - -(On Windows/macOS the library is `glyph11.dll` / `libglyph11.dylib`, etc.) - -## Which package? - -- **Glyph11** — hardened, dependency-free, runs anywhere. Returns a `BinaryRequest`. -- **Glyph11.Native** — the same hardening via the C core, native speed, zero allocation. Raw spans. -- **Glyph11.Pico** — fastest to a `BinaryRequest`, picohttpparser-level validation only. - See the [docs site](https://dotnet-web-stack.github.io/Glyph11/) for the full reference. diff --git a/README.md b/README.md index 84b39d0..d5d23cd 100644 --- a/README.md +++ b/README.md @@ -2,27 +2,16 @@ A zero-allocation, hardened HTTP/1.1 request parser for .NET — RFC 9110/9112 validation, configurable resource limits, and request-smuggling / semantic checks fused into a single -zero-copy pass. Available as three NuGet packages that trade portability, native speed, and -raw throughput. +zero-copy pass. Pure managed and dependency-free. [![Glyph11](https://img.shields.io/nuget/v/Glyph11.svg?label=Glyph11)](https://www.nuget.org/packages/Glyph11/) -[![Glyph11.Native](https://img.shields.io/nuget/v/Glyph11.Native.svg?label=Glyph11.Native)](https://www.nuget.org/packages/Glyph11.Native/) -[![Glyph11.Pico](https://img.shields.io/nuget/v/Glyph11.Pico.svg?label=Glyph11.Pico)](https://www.nuget.org/packages/Glyph11.Pico/) ![.NET](https://img.shields.io/badge/.NET-8.0%20%7C%209.0%20%7C%2010.0-512bd4) [![Docs](https://img.shields.io/badge/docs%20%26%20benchmarks-online-blue)](https://dotnet-web-stack.github.io/Glyph11/) -## Three packages +> **`Glyph11.Native` and `Glyph11.Pico` are discontinued.** Their published versions stay on +> NuGet, but they get no further releases — Glyph11 is the managed parser only. -| Package | What it is | Output | Validation | Dependencies | Best for | -|---|---|---|---|---|---| -| **[Glyph11](https://www.nuget.org/packages/Glyph11/)** | pure-managed hardened parser | `BinaryRequest` | full (RFC + smuggling) | none — runs anywhere | portability, zero deps | -| **[Glyph11.Native](https://www.nuget.org/packages/Glyph11.Native/)** | the C core via P/Invoke | raw field spans | full (same as managed) | native `libglyph11` per RID | native speed, zero-alloc | -| **[Glyph11.Pico](https://www.nuget.org/packages/Glyph11.Pico/)** | picohttpparser + managed glue | `BinaryRequest` | picohttpparser only | native `libglyph11pico` + `Glyph11` | maximum speed, validate elsewhere | - -All three produce the same data; `Glyph11` and `Glyph11.Pico` fill the identical -`BinaryRequest`, so swapping between them is a one-line change. - -## Glyph11 — pure managed +## Usage Dependency-free, runs on any .NET target. Parses a `ReadOnlySequence` (from a `PipeReader`, socket, …) or a contiguous `ReadOnlyMemory`. @@ -57,85 +46,9 @@ if (UltraHardenedParser.TryExtractFullHeaderValidated(ref buffer, request, in li `TryExtractFullHeaderROM(ref ReadOnlyMemory, …)` is the single-buffer fast path. -## Glyph11.Native — C core via P/Invoke - -Same validation, native speed, **zero allocation** (you provide the field storage). Bundles -`libglyph11` for `linux-x64/arm64`, `win-x64/arm64`, `osx-x64/arm64`. - -```csharp -using System.Text; -using Glyph11.Native; - -byte[] request = Encoding.ASCII.GetBytes( - "GET /api/users?page=1 HTTP/1.1\r\nHost: example.com\r\n\r\n"); - -var limits = Glyph11Limits.Default; -Span headers = stackalloc Glyph11Field[(int)limits.MaxHeaderCount]; -Span query = stackalloc Glyph11Field[(int)limits.MaxQueryParameterCount]; - -int status = Glyph11Parser.Parse(request, headers, query, limits, out var r); -if (status == Glyph11Parser.Ok) -{ - string Slice(Glyph11Span s) => Encoding.ASCII.GetString(request, (int)s.Offset, (int)s.Length); - Console.WriteLine(Slice(r.Method)); // GET - Console.WriteLine(Slice(r.Path)); // /api/users - for (int i = 0; i < r.HeaderCount; i++) - Console.WriteLine($"{Slice(headers[i].Name)}: {Slice(headers[i].Value)}"); -} -// status: 0 = OK, 1 = incomplete, otherwise a protocol/limit error (→ HTTP 400 / 431). -``` - -A `ReadOnlySequence` overload handles fragmented input — single-segment is parsed in -place (zero-copy), multi-segment is linearized into a caller-provided scratch (the C core -needs one contiguous slab). `parsed` is the contiguous span the offsets index into: - -```csharp -Span scratch = stackalloc byte[8192]; // sized to your max header block -int status = Glyph11Parser.Parse(sequence, scratch, headers, query, limits, - out var r, out ReadOnlySpan parsed); -// slice fields against `parsed` — the input's first segment, or `scratch`. -``` - -> **`linux-x64` requires AVX2** (Haswell / 2013+ — universal on modern servers): the SIMD -> scanners inline into the parse loop for ~15% on large headers. Other RIDs use the portable -> baseline. - -## Glyph11.Pico — fastest path to a `BinaryRequest` - -[picohttpparser](https://github.com/h2o/picohttpparser) (native, SSE4.2) tokenizes; minimal -managed glue fills the same `BinaryRequest` as `Glyph11`. **No validation beyond -picohttpparser's** — the trade is raw speed. - -```csharp -using System.Text; -using Glyph11.Pico; -using Glyph11.Protocol; - -var request = new BinaryRequest(); -byte[] input = "GET /api/users?page=1 HTTP/1.1\r\nHost: example.com\r\n\r\n"u8.ToArray(); - -if (PicoParser.TryParse(input, request, out int consumed)) -{ - Console.WriteLine(Encoding.ASCII.GetString(request.Method.Span)); // GET - Console.WriteLine(Encoding.ASCII.GetString(request.Path.Span)); // /api/users - // request.Version / .Headers / .QueryParameters — same shape as Glyph11. -} -``` - -A `ReadOnlySequence` overload is also available — single-segment zero-copy, -multi-segment linearized into a fresh array (the `BinaryRequest` slices keep it alive): - -```csharp -if (PicoParser.TryParse(sequence, request, out int consumed)) { /* request.* as usual */ } -``` - -Use it when you want the fastest path to a `BinaryRequest` and validate elsewhere (or trust -the source). For the hardened parser, use `Glyph11`. - ## Chunked bodies -Both `Glyph11` and `Glyph11.Pico` (which depends on `Glyph11`) decode chunked -transfer-encoding via `ChunkedBodyStream`; `Glyph11.Native` exposes the C decoder. +`ChunkedBodyStream` decodes chunked transfer-encoding incrementally. ```csharp using Glyph11.Parser; @@ -147,50 +60,32 @@ var r = stream.TryReadChunk(input, out int consumed, out int dataOffset, out int ## Benchmarks -`linux-x64`, best-of-5, ns/op (lower is better). `Glyph11.Native` / `Glyph11.Pico` are the -AVX2 / SSE4.2 native builds. The same tables and usage docs are at -****; the harnesses are in -[`bench/`](bench). +`linux-x64`, best-of-5, ns/op (lower is better). The same tables and usage docs are at +****; the harness is in +[`Benchmarks/`](Benchmarks). -**Request header parsing** — contiguous buffer (→ `BinaryRequest` for Glyph11 / Pico; raw spans for Native): +**Request header parsing** — contiguous buffer (→ `BinaryRequest`): -| Payload | Glyph11 | Glyph11.Native | Glyph11.Pico | -|---|---:|---:|---:| -| ~95 B | 120 ns | 99 ns | **80 ns** | -| 4 KB | 750 ns | 502 ns | **487 ns** | -| 32 KB | 5251 ns | 3752 ns | **3370 ns** | +| Payload | Glyph11 | +|---|---:| +| ~95 B | 120 ns | +| 4 KB | 750 ns | +| 32 KB | 5251 ns | **Multi-segment** — fragmented request (3 segments) linearized into a buffer per request, then parsed: -| Payload | Glyph11 | Glyph11.Native | Glyph11.Pico | -|---|---:|---:|---:| -| ~95 B | 245 ns | **116 ns** | 118 ns | -| 4 KB | 1258 ns | 721 ns | **674 ns** | -| 32 KB | 8695 ns | 4969 ns | **4627 ns** | - -**Chunked body decoding** (decoded size; `Glyph11.Pico` reuses `Glyph11`'s `ChunkedBodyStream`, so they share a column): +| Payload | Glyph11 | +|---|---:| +| ~95 B | 245 ns | +| 4 KB | 1258 ns | +| 32 KB | 8695 ns | -| Decoded | Glyph11 / Pico | Glyph11.Native | -|---|---:|---:| -| 256 B | 19 ns | 21 ns | -| 4 KB | 115 ns | **70 ns** | -| 32 KB | 826 ns | **808 ns** | +**Chunked body decoding** (decoded size): -`Glyph11.Pico` is the fastest to a full `BinaryRequest` — it skips glyph11's hardening, so -pick it only when you validate elsewhere. - -## Kotlin / JVM - -The C core is also reachable from the JVM via Panama FFM (JDK 21+) — see -[`bindings/kotlin`](bindings/kotlin). Not a NuGet package. - -## Build the native cores - -```sh -cmake -S core -B core/build-rel -DGLYPH11_BUILD_TESTS=OFF # libglyph11 -cmake --build core/build-rel -cmake -S bindings/dotnet/Glyph11.Pico/native -B pico-build # libglyph11pico -cmake --build pico-build -``` +| Decoded | Glyph11 | +|---|---:| +| 256 B | 19 ns | +| 4 KB | 115 ns | +| 32 KB | 826 ns | -MIT licensed. `Glyph11.Pico` bundles picohttpparser (MIT/Perl, © Kazuho Oku et al.). +MIT licensed. diff --git a/site/benchmarks.html b/site/benchmarks.html index 33ff909..63aea66 100644 --- a/site/benchmarks.html +++ b/site/benchmarks.html @@ -15,10 +15,8 @@ Glyph11