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/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/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/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/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 1b33c55..0000000 Binary files a/bindings/kotlin/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/bindings/kotlin/gradle/wrapper/gradle-wrapper.properties b/bindings/kotlin/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index d4081da..0000000 --- a/bindings/kotlin/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/bindings/kotlin/gradlew b/bindings/kotlin/gradlew deleted file mode 100755 index 23d15a9..0000000 --- a/bindings/kotlin/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - 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/benchmarks.html b/site/benchmarks.html index 33ff909..63aea66 100644 --- a/site/benchmarks.html +++ b/site/benchmarks.html @@ -15,10 +15,8 @@ Glyph11