diff --git a/.github/workflows/package-native.yml b/.github/workflows/package-native.yml index f1dd4bb..1c50cf5 100644 --- a/.github/workflows/package-native.yml +++ b/.github/workflows/package-native.yml @@ -11,6 +11,11 @@ on: 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*' @@ -111,20 +116,23 @@ jobs: - name: Resolve package version id: ver - # native-vX.Y.Z tag -> X.Y.Z (a release); otherwise a CI prerelease. + # 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 "version=${ref#refs/tags/native-v}" >> "$GITHUB_OUTPUT" + echo "args=-p:Version=${ref#refs/tags/native-v}" >> "$GITHUB_OUTPUT" + elif [[ "${{ inputs.publish }}" == "true" ]]; then + echo "args=" >> "$GITHUB_OUTPUT" else - echo "version=0.1.0-ci.${{ github.run_number }}" >> "$GITHUB_OUTPUT" + 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 - -p:Version=${{ steps.ver.outputs.version }} + ${{ steps.ver.outputs.args }} -o artifacts - name: Upload package @@ -140,7 +148,7 @@ jobs: 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' && inputs.publish) + 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 diff --git a/.github/workflows/package-pico.yml b/.github/workflows/package-pico.yml index b8c7342..6bc84a4 100644 --- a/.github/workflows/package-pico.yml +++ b/.github/workflows/package-pico.yml @@ -11,6 +11,11 @@ on: 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*' @@ -111,20 +116,23 @@ jobs: - name: Resolve package version id: ver - # pico-vX.Y.Z tag -> X.Y.Z (a release); otherwise a CI prerelease. + # 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 "version=${ref#refs/tags/pico-v}" >> "$GITHUB_OUTPUT" + echo "args=-p:Version=${ref#refs/tags/pico-v}" >> "$GITHUB_OUTPUT" + elif [[ "${{ inputs.publish }}" == "true" ]]; then + echo "args=" >> "$GITHUB_OUTPUT" else - echo "version=0.0.1-ci.${{ github.run_number }}" >> "$GITHUB_OUTPUT" + 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 - -p:Version=${{ steps.ver.outputs.version }} + ${{ steps.ver.outputs.args }} -o artifacts - name: Upload package @@ -140,7 +148,7 @@ jobs: 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' && inputs.publish) + 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9406d73..c4585de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,10 @@ 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. on: workflow_dispatch: @@ -9,15 +14,11 @@ permissions: jobs: - publish: - - name: Build & Publish Packages - + managed: + name: Glyph11 (managed) runs-on: windows-latest - environment: name: production - steps: - name: Checkout source uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -32,9 +33,6 @@ jobs: 9.0 10.0 - - name: Setup NuGet CLI - uses: NuGet/setup-nuget@323ab0502cd38fdc493335025a96c8fdb0edc71f # v2 - - name: Restore dependencies run: dotnet restore working-directory: src @@ -56,3 +54,19 @@ 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/Benchmarks/BenchmarkData.cs b/Benchmarks/BenchmarkData.cs index ec27955..1b4abe3 100644 --- a/Benchmarks/BenchmarkData.cs +++ b/Benchmarks/BenchmarkData.cs @@ -15,7 +15,7 @@ public static byte[] BuildSmallHeader() "GET /route?p1=1&p2=2&p3=3&p4=4 HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 100\r\n" + - "Server: GenHTTP\r\n\r\n"); + "Server: Glyph11\r\n\r\n"); } /// diff --git a/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj index 622c2b1..7ffba4a 100644 --- a/Benchmarks/Benchmarks.csproj +++ b/Benchmarks/Benchmarks.csproj @@ -7,7 +7,6 @@ - diff --git a/Benchmarks/FlexibleParserBenchmark.cs b/Benchmarks/FlexibleParserBenchmark.cs index 1c9657c..12d9bc4 100644 --- a/Benchmarks/FlexibleParserBenchmark.cs +++ b/Benchmarks/FlexibleParserBenchmark.cs @@ -7,8 +7,8 @@ using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; -using GenHTTP.Types; using Glyph11.Parser.FlexibleParser; +using Glyph11.Protocol; namespace Benchmarks; @@ -40,14 +40,14 @@ public static void Main(string[] args) [MemoryDiagnoser] public class FlexibleParserBenchmark { - private readonly Request _into = new(); + private readonly BinaryRequest _into = new(); // ---- Small (~80B) ---- private readonly ReadOnlySequence _buffer = new(("GET /route?p1=1&p2=2&p3=3&p4=4 HTTP/1.1\r\n"u8 + "Content-Length: 100\r\n"u8 + - "Server: GenHTTP\r\n\r\n"u8).ToArray()); + "Server: Glyph11\r\n\r\n"u8).ToArray()); private ReadOnlySequence _segmentedBuffer = CreateMultiSegment(); @@ -79,7 +79,7 @@ private static ReadOnlySequence CreateMultiSegment() { var seg1 = "GET /route?p1=1&p2=2&p3=3&p4=4 HT"u8.ToArray(); var seg2 = "TP/1.1\r\nContent-Length: 100\r\nServer: "u8.ToArray(); - var seg3 = "GenHTTP\r\n\r\n"u8.ToArray(); + var seg3 = "Glyph11\r\n\r\n"u8.ToArray(); var first = new Glyph11.Utils.BufferSegment(seg1); var last = first.Append(seg2).Append(seg3); @@ -92,15 +92,15 @@ private static ReadOnlySequence CreateMultiSegment() [Benchmark] public void Small_ROM() { - _into.Reset(); - FlexibleParser.TryExtractFullHeaderReadOnlyMemory(ref _memory, _into.Source, out _); + _into.Clear(); + FlexibleParser.TryExtractFullHeaderReadOnlyMemory(ref _memory, _into, out _); } [Benchmark] public void Small_MultiSegment() { - _into.Reset(); - FlexibleParser.TryExtractFullHeader(ref _segmentedBuffer, _into.Source, out _); + _into.Clear(); + FlexibleParser.TryExtractFullHeader(ref _segmentedBuffer, _into, out _); } // ---- 4KB ---- @@ -108,15 +108,15 @@ public void Small_MultiSegment() [Benchmark] public void Header4K_ROM() { - _into.Reset(); - FlexibleParser.TryExtractFullHeaderReadOnlyMemory(ref _rom4K, _into.Source, out _); + _into.Clear(); + FlexibleParser.TryExtractFullHeaderReadOnlyMemory(ref _rom4K, _into, out _); } [Benchmark] public void Header4K_MultiSegment() { - _into.Reset(); - FlexibleParser.TryExtractFullHeader(ref _seg4K, _into.Source, out _); + _into.Clear(); + FlexibleParser.TryExtractFullHeader(ref _seg4K, _into, out _); } // ---- 32KB ---- @@ -124,14 +124,14 @@ public void Header4K_MultiSegment() [Benchmark] public void Header32K_ROM() { - _into.Reset(); - FlexibleParser.TryExtractFullHeaderReadOnlyMemory(ref _rom32K, _into.Source, out _); + _into.Clear(); + FlexibleParser.TryExtractFullHeaderReadOnlyMemory(ref _rom32K, _into, out _); } [Benchmark] public void Header32K_MultiSegment() { - _into.Reset(); - FlexibleParser.TryExtractFullHeader(ref _seg32K, _into.Source, out _); + _into.Clear(); + FlexibleParser.TryExtractFullHeader(ref _seg32K, _into, out _); } } diff --git a/Benchmarks/UltraHardenedParserBenchmark.cs b/Benchmarks/UltraHardenedParserBenchmark.cs index f20da61..c8d6395 100644 --- a/Benchmarks/UltraHardenedParserBenchmark.cs +++ b/Benchmarks/UltraHardenedParserBenchmark.cs @@ -1,15 +1,15 @@ using System.Buffers; using BenchmarkDotNet.Attributes; -using GenHTTP.Types; using Glyph11.Parser; using Glyph11.Parser.UltraHardened; +using Glyph11.Protocol; namespace Benchmarks; [MemoryDiagnoser] public class UltraHardenedParserBenchmark { - private readonly Request _into = new(); + private readonly BinaryRequest _into = new(); private static readonly ParserLimits Limits = ParserLimits.Default with { MaxTotalHeaderBytes = 64 * 1024, MaxHeaderCount = 200 }; @@ -19,7 +19,7 @@ public class UltraHardenedParserBenchmark new(("GET /route?p1=1&p2=2&p3=3&p4=4 HTTP/1.1\r\n"u8 + "Host: localhost\r\n"u8 + "Content-Length: 100\r\n"u8 + - "Server: GenHTTP\r\n\r\n"u8).ToArray()); + "Server: Glyph11\r\n\r\n"u8).ToArray()); private ReadOnlySequence _segmentedBuffer = CreateMultiSegment(); @@ -51,7 +51,7 @@ private static ReadOnlySequence CreateMultiSegment() { var seg1 = "GET /route?p1=1&p2=2&p3=3&p4=4 HT"u8.ToArray(); var seg2 = "TP/1.1\r\nHost: localhost\r\nContent-Length: 100\r\nServer: "u8.ToArray(); - var seg3 = "GenHTTP\r\n\r\n"u8.ToArray(); + var seg3 = "Glyph11\r\n\r\n"u8.ToArray(); var first = new Glyph11.Utils.BufferSegment(seg1); var last = first.Append(seg2).Append(seg3); @@ -64,15 +64,15 @@ private static ReadOnlySequence CreateMultiSegment() [Benchmark] public void Small_ROM() { - _into.Reset(); - UltraHardenedParser.TryExtractFullHeaderROM(ref _memory, _into.Source, in Limits, out _); + _into.Clear(); + UltraHardenedParser.TryExtractFullHeaderROM(ref _memory, _into, in Limits, out _); } [Benchmark] public void Small_MultiSegment() { - _into.Reset(); - UltraHardenedParser.TryExtractFullHeaderValidated(ref _segmentedBuffer, _into.Source, in Limits, out _); + _into.Clear(); + UltraHardenedParser.TryExtractFullHeaderValidated(ref _segmentedBuffer, _into, in Limits, out _); } // ---- 4KB ---- @@ -80,15 +80,15 @@ public void Small_MultiSegment() [Benchmark] public void Header4K_ROM() { - _into.Reset(); - UltraHardenedParser.TryExtractFullHeaderROM(ref _rom4K, _into.Source, in Limits, out _); + _into.Clear(); + UltraHardenedParser.TryExtractFullHeaderROM(ref _rom4K, _into, in Limits, out _); } [Benchmark] public void Header4K_MultiSegment() { - _into.Reset(); - UltraHardenedParser.TryExtractFullHeaderValidated(ref _seg4K, _into.Source, in Limits, out _); + _into.Clear(); + UltraHardenedParser.TryExtractFullHeaderValidated(ref _seg4K, _into, in Limits, out _); } // ---- 32KB ---- @@ -96,14 +96,14 @@ public void Header4K_MultiSegment() [Benchmark] public void Header32K_ROM() { - _into.Reset(); - UltraHardenedParser.TryExtractFullHeaderROM(ref _rom32K, _into.Source, in Limits, out _); + _into.Clear(); + UltraHardenedParser.TryExtractFullHeaderROM(ref _rom32K, _into, in Limits, out _); } [Benchmark] public void Header32K_MultiSegment() { - _into.Reset(); - UltraHardenedParser.TryExtractFullHeaderValidated(ref _seg32K, _into.Source, in Limits, out _); + _into.Clear(); + UltraHardenedParser.TryExtractFullHeaderValidated(ref _seg32K, _into, in Limits, out _); } } \ No newline at end of file diff --git a/Examples/Glyph11.Example/Glyph11.Example.csproj b/Examples/Glyph11.Example/Glyph11.Example.csproj new file mode 100644 index 0000000..69417ad --- /dev/null +++ b/Examples/Glyph11.Example/Glyph11.Example.csproj @@ -0,0 +1,17 @@ + + + + Exe + net10.0 + enable + enable + false + false + + + + + + + + diff --git a/Examples/Glyph11.Example/Program.cs b/Examples/Glyph11.Example/Program.cs new file mode 100644 index 0000000..46062bc --- /dev/null +++ b/Examples/Glyph11.Example/Program.cs @@ -0,0 +1,238 @@ +// Glyph11 (pure managed) — every option, end to end. +// +// dotnet run --project Examples/Glyph11.Example +// +// Glyph11 is dependency-free and runs anywhere .NET runs — no native library needed. + +using System.Buffers; +using System.Text; +using Glyph11; // HttpParseException +using Glyph11.Protocol; // BinaryRequest, KeyValueList +using Glyph11.Parser; // ParserLimits, ChunkedBodyStream +using Glyph11.Parser.UltraHardened; // UltraHardenedParser + +// A BinaryRequest is reusable storage — allocate it once and Clear() it between requests +// to stay allocation-free across a connection (see Example 6). +var request = new BinaryRequest(); + +ContiguousParse(); +MultiSegmentParse(); +CustomLimits(); +ChunkedBody(); +ErrorHandling(); +ReuseAndDispose(); + +request.Dispose(); // returns the pooled header/query arrays to the ArrayPool + +// ───────────────────────────────────────────────────────────────────────────── +// 1. Parse a request that's already in one contiguous buffer (the fast path). +// ───────────────────────────────────────────────────────────────────────────── +void ContiguousParse() +{ + Console.WriteLine("== 1. Contiguous parse =="); + + // The request bytes as one block. TryExtractFullHeaderROM takes a ReadOnlyMemory. + ReadOnlyMemory input = Encoding.ASCII.GetBytes( + "GET /api/users?page=1&sort=asc HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Accept: */*\r\n\r\n"); + + request.Clear(); + var limits = ParserLimits.Default; + + // Returns true on a complete, valid header block; false if the buffer is incomplete; + // throws HttpParseException if the bytes are a complete but invalid/malicious request. + if (UltraHardenedParser.TryExtractFullHeaderROM(ref input, request, in limits, out int bytesRead)) + { + // Every field is a zero-copy ReadOnlyMemory slice of `input`. + Console.WriteLine($" method = {Ascii(request.Method)}"); // GET + Console.WriteLine($" path = {Ascii(request.Path)}"); // /api/users (query stripped) + Console.WriteLine($" version = {Ascii(request.Version)}"); // HTTP/1.1 + + // Headers are an ordered list of (name, value) byte-slice pairs. + Console.WriteLine($" headers ({request.Headers.Count}):"); + for (int i = 0; i < request.Headers.Count; i++) + { + var (name, value) = request.Headers[i]; + Console.WriteLine($" {Ascii(name)}: {Ascii(value)}"); + } + + // Query parameters are parsed out of the request target. + Console.WriteLine($" query ({request.QueryParameters.Count}):"); + for (int i = 0; i < request.QueryParameters.Count; i++) + { + var (key, val) = request.QueryParameters[i]; + Console.WriteLine($" {Ascii(key)} = {Ascii(val)}"); + } + + // bytesRead follows glyph11's -1 convention: the body begins at bytesRead + 1. + Console.WriteLine($" header block length = {bytesRead + 1} bytes"); + } + Console.WriteLine(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 2. Parse a fragmented request (a ReadOnlySequence split across buffers) — what +// you get from a PipeReader/socket. The parser walks the segments for you. +// ───────────────────────────────────────────────────────────────────────────── +void MultiSegmentParse() +{ + Console.WriteLine("== 2. Multi-segment parse =="); + + byte[] all = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost: x\r\n\r\n"); + + // Build a 3-segment ReadOnlySequence to simulate fragmented network reads. + ReadOnlySequence buffer = ThreeSegments(all); + + request.Clear(); + var limits = ParserLimits.Default; + + // Same contract as Example 1, but for a (possibly) multi-segment sequence. It takes the + // contiguous fast path automatically when the sequence happens to be single-segment. + if (UltraHardenedParser.TryExtractFullHeaderValidated(ref buffer, request, in limits, out int bytesRead)) + Console.WriteLine($" parsed {Ascii(request.Method)} {Ascii(request.Path)} from 3 segments\n"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 3. Tighten the security policy with a `with` expression (ParserLimits is a +// readonly record struct). Exceeding a limit is a rejection (HTTP 431), never +// an overflow. +// ───────────────────────────────────────────────────────────────────────────── +void CustomLimits() +{ + Console.WriteLine("== 3. Custom limits =="); + + var strict = ParserLimits.Default with + { + MaxHeaderCount = 2, // allow at most 2 headers + MaxHeaderValueLength = 1024, + MaxUrlLength = 256, + MaxTotalHeaderBytes = 4 * 1024, + }; + + // This request has 3 headers — more than MaxHeaderCount = 2, so it's rejected. + ReadOnlyMemory tooMany = Encoding.ASCII.GetBytes( + "GET / HTTP/1.1\r\nHost: x\r\nA: 1\r\nB: 2\r\n\r\n"); + + request.Clear(); + try + { + UltraHardenedParser.TryExtractFullHeaderROM(ref tooMany, request, in strict, out _); + Console.WriteLine(" unexpectedly accepted"); + } + catch (HttpParseException ex) + { + // A limit breach maps to HTTP 431; a structural/semantic error maps to 400. + Console.WriteLine($" rejected: {ex.Message}\n"); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 4. Decode a chunked (Transfer-Encoding: chunked) body with ChunkedBodyStream. +// ───────────────────────────────────────────────────────────────────────────── +void ChunkedBody() +{ + Console.WriteLine("== 4. Chunked body =="); + + // "Hello" + " World" framed as two chunks, then the terminal 0-length chunk. + byte[] body = Encoding.ASCII.GetBytes("5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n"); + + var decoder = new ChunkedBodyStream(); + var decoded = new ArrayBufferWriter(); + int offset = 0; + + while (true) + { + // consumed = input bytes used (framing + payload) + // dataOffset = where the payload starts, relative to the slice we passed + // dataLength = payload byte count + 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; + } + // Completed = terminal 0-chunk seen (body done); NeedMoreData = read more, then retry. + // (Malformed framing surfaces as an HttpParseException from the parse path.) + break; + } + + Console.WriteLine($" decoded body = \"{Encoding.ASCII.GetString(decoded.WrittenSpan)}\"\n"); // Hello World +} + +// ───────────────────────────────────────────────────────────────────────────── +// 5. The three outcomes: valid (true), incomplete (false), invalid (throws). +// ───────────────────────────────────────────────────────────────────────────── +void ErrorHandling() +{ + Console.WriteLine("== 5. Error handling =="); + var limits = ParserLimits.Default; + + // (a) Incomplete — the header block isn't terminated yet. Returns false: read more. + ReadOnlyMemory partial = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost: x\r\n"); + request.Clear(); + bool ok = UltraHardenedParser.TryExtractFullHeaderROM(ref partial, request, in limits, out _); + Console.WriteLine($" incomplete request → returned {ok} (need more bytes)"); + + // (b) Invalid — a path-traversal attempt. A complete but malicious request → throws. + ReadOnlyMemory evil = Encoding.ASCII.GetBytes("GET /a/../../etc/passwd HTTP/1.1\r\nHost: x\r\n\r\n"); + request.Clear(); + try + { + UltraHardenedParser.TryExtractFullHeaderROM(ref evil, request, in limits, out _); + } + catch (HttpParseException ex) + { + Console.WriteLine($" malicious request → threw: {ex.Message}\n"); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 6. Reuse across requests, then dispose. Clear() keeps the pooled arrays; Dispose() +// returns them. Not disposing won't leak — it just skips the pooling. +// ───────────────────────────────────────────────────────────────────────────── +void ReuseAndDispose() +{ + Console.WriteLine("== 6. Reuse =="); + var limits = ParserLimits.Default; + var shared = new BinaryRequest(); + + foreach (var path in new[] { "/a", "/b", "/c" }) + { + ReadOnlyMemory req = Encoding.ASCII.GetBytes($"GET {path} HTTP/1.1\r\nHost: x\r\n\r\n"); + shared.Clear(); // reset before each parse; reuses the same pooled storage + if (UltraHardenedParser.TryExtractFullHeaderROM(ref req, shared, in limits, out _)) + Console.WriteLine($" parsed {Ascii(shared.Path)}"); + } + + shared.Dispose(); + Console.WriteLine(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────────────────────── +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); +} + +// Minimal ReadOnlySequenceSegment to chain memory blocks into one sequence. +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.Native.Example/Glyph11.Native.Example.csproj b/Examples/Glyph11.Native.Example/Glyph11.Native.Example.csproj new file mode 100644 index 0000000..49f77db --- /dev/null +++ b/Examples/Glyph11.Native.Example/Glyph11.Native.Example.csproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + enable + enable + false + false + + + + + + + + diff --git a/Examples/Glyph11.Native.Example/Program.cs b/Examples/Glyph11.Native.Example/Program.cs new file mode 100644 index 0000000..e16bcce --- /dev/null +++ b/Examples/Glyph11.Native.Example/Program.cs @@ -0,0 +1,180 @@ +// 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 new file mode 100644 index 0000000..0265017 --- /dev/null +++ b/Examples/Glyph11.Pico.Example/Glyph11.Pico.Example.csproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + enable + enable + false + false + + + + + + + + diff --git a/Examples/Glyph11.Pico.Example/Program.cs b/Examples/Glyph11.Pico.Example/Program.cs new file mode 100644 index 0000000..227c2b0 --- /dev/null +++ b/Examples/Glyph11.Pico.Example/Program.cs @@ -0,0 +1,139 @@ +// 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 new file mode 100644 index 0000000..96ff487 --- /dev/null +++ b/Examples/README.md @@ -0,0 +1,47 @@ +# 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 `. + +| 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` | + +Each `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: + +```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/src/Examples/GenHTTP/Assembly.cs b/src/Examples/GenHTTP/Assembly.cs deleted file mode 100644 index d7b7f8c..0000000 --- a/src/Examples/GenHTTP/Assembly.cs +++ /dev/null @@ -1,4 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Tests")] -[assembly: InternalsVisibleTo("Benchmarks")] diff --git a/src/Examples/GenHTTP/ClientHandler.cs b/src/Examples/GenHTTP/ClientHandler.cs deleted file mode 100644 index 1bf4106..0000000 --- a/src/Examples/GenHTTP/ClientHandler.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace GenHTTP; - -public class ClientHandler -{ - -} \ No newline at end of file diff --git a/src/Examples/GenHTTP/Context/ClientContext.cs b/src/Examples/GenHTTP/Context/ClientContext.cs deleted file mode 100644 index 0577300..0000000 --- a/src/Examples/GenHTTP/Context/ClientContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -using GenHTTP.Types; - -namespace GenHTTP.Context; - -public class ClientContext -{ - public Request Request { get; set; } = null!; - - internal void Clear() - { - Request.Reset(); - } -} diff --git a/src/Examples/GenHTTP/Context/ClientContextPolicy.cs b/src/Examples/GenHTTP/Context/ClientContextPolicy.cs deleted file mode 100644 index 9456bf9..0000000 --- a/src/Examples/GenHTTP/Context/ClientContextPolicy.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.Extensions.ObjectPool; - -namespace GenHTTP.Context; - -public class ClientContextPolicy : PooledObjectPolicy -{ - - public override ClientContext Create() => new(); - - public override bool Return(ClientContext obj) - { - obj.Clear(); - return true; - } - -} \ No newline at end of file diff --git a/src/Examples/GenHTTP/GenHTTP.csproj b/src/Examples/GenHTTP/GenHTTP.csproj deleted file mode 100644 index b78143a..0000000 --- a/src/Examples/GenHTTP/GenHTTP.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net8.0;net9.0;net10.0 - true - false - GenHTTP - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Examples/GenHTTP/Parser/RequestParser.cs b/src/Examples/GenHTTP/Parser/RequestParser.cs deleted file mode 100644 index ddc5937..0000000 --- a/src/Examples/GenHTTP/Parser/RequestParser.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Buffers; -using GenHTTP.Types; -using Glyph11.Parser; -using Glyph11.Parser.UltraHardened; - -namespace GenHTTP.Parser; - -public static class RequestParser -{ - private static readonly ParserLimits Limits = ParserLimits.Default; - - public static bool TryParse(ReadOnlySequence buffer, Request into, out int bytesRead) - => TryParse(buffer, into, in Limits, out bytesRead); - - public static bool TryParse(ReadOnlySequence buffer, Request into, in ParserLimits limits, out int bytesRead) - { - var raw = into.Source; - - // UltraHardenedParser enforces all structural and semantic checks during - // parsing and throws HttpParseException on any violation. - if (UltraHardenedParser.TryExtractFullHeaderValidated(ref buffer, raw, in limits, out bytesRead)) - { - into.Apply(); - return true; - } - - return false; - } - -} diff --git a/src/Examples/GenHTTP/Protocol/IRequest.cs b/src/Examples/GenHTTP/Protocol/IRequest.cs deleted file mode 100644 index 224cceb..0000000 --- a/src/Examples/GenHTTP/Protocol/IRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GenHTTP.Protocol.Raw; - -namespace GenHTTP.Protocol; - -public interface IRequest -{ - - IRawRequest Raw { get; } - - RequestMethod Method { get; } - -} diff --git a/src/Examples/GenHTTP/Protocol/IResponse.cs b/src/Examples/GenHTTP/Protocol/IResponse.cs deleted file mode 100644 index d9c5f28..0000000 --- a/src/Examples/GenHTTP/Protocol/IResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace GenHTTP.Protocol; - -public interface IResponse -{ - -} diff --git a/src/Examples/GenHTTP/Protocol/Raw/IRawKeyValueList.cs b/src/Examples/GenHTTP/Protocol/Raw/IRawKeyValueList.cs deleted file mode 100644 index cba7632..0000000 --- a/src/Examples/GenHTTP/Protocol/Raw/IRawKeyValueList.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace GenHTTP.Protocol.Raw; - -public interface IRawKeyValueList -{ - - int Count { get; } - - KeyValuePair, ReadOnlyMemory> this[int index] { get; } - -} diff --git a/src/Examples/GenHTTP/Protocol/Raw/IRawRequest.cs b/src/Examples/GenHTTP/Protocol/Raw/IRawRequest.cs deleted file mode 100644 index 880fac0..0000000 --- a/src/Examples/GenHTTP/Protocol/Raw/IRawRequest.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace GenHTTP.Protocol.Raw; - -public interface IRawRequest -{ - - ReadOnlyMemory Method { get; } - - ReadOnlyMemory Path { get; } - - IRawRequestTarget Target { get; } - - IRawKeyValueList Query { get; } - - ReadOnlyMemory Version { get; } - - IRawKeyValueList Headers { get; } - -} diff --git a/src/Examples/GenHTTP/Protocol/Raw/IRawRequestTarget.cs b/src/Examples/GenHTTP/Protocol/Raw/IRawRequestTarget.cs deleted file mode 100644 index a1cdacd..0000000 --- a/src/Examples/GenHTTP/Protocol/Raw/IRawRequestTarget.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace GenHTTP.Protocol.Raw; - -public interface IRawRequestTarget -{ - - ReadOnlyMemory? Current { get; } - - void Advance(int segments = 1); - -} diff --git a/src/Examples/GenHTTP/Protocol/RequestMethod.cs b/src/Examples/GenHTTP/Protocol/RequestMethod.cs deleted file mode 100644 index f1b4fb2..0000000 --- a/src/Examples/GenHTTP/Protocol/RequestMethod.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace GenHTTP.Protocol; - -public enum RequestMethod -{ - Get, - Head, - Post, - Put, - Delete, - Connect, - Options, - Trace, - Patch, - - // if it cannot be parsed into one of the ones above - Other - -} diff --git a/src/Examples/GenHTTP/Types/RawKeyValueList.cs b/src/Examples/GenHTTP/Types/RawKeyValueList.cs deleted file mode 100644 index 0f802bb..0000000 --- a/src/Examples/GenHTTP/Types/RawKeyValueList.cs +++ /dev/null @@ -1,13 +0,0 @@ -using GenHTTP.Protocol.Raw; -using Glyph11.Protocol; - -namespace GenHTTP.Types; - -public sealed class RawKeyValueList(KeyValueList source) : IRawKeyValueList -{ - - public int Count => source.Count; - - public KeyValuePair, ReadOnlyMemory> this[int index] => source[index]; - -} diff --git a/src/Examples/GenHTTP/Types/RawRequest.cs b/src/Examples/GenHTTP/Types/RawRequest.cs deleted file mode 100644 index 7281b93..0000000 --- a/src/Examples/GenHTTP/Types/RawRequest.cs +++ /dev/null @@ -1,52 +0,0 @@ -using GenHTTP.Protocol.Raw; -using Glyph11.Protocol; - -namespace GenHTTP.Types; - -public sealed class RawRequest : IRawRequest -{ - private readonly BinaryRequest _source; - - private readonly RawKeyValueList _headers; - - private readonly RawKeyValueList _query; - - private readonly RawRequestTarget _target; - - internal BinaryRequest Source => _source; - - public ReadOnlyMemory Method => _source.Method; - - public ReadOnlyMemory Path => _source.Path; - - public IRawRequestTarget Target => _target; - - public ReadOnlyMemory Version => _source.Version; - - public IRawKeyValueList Headers => _headers; - - public IRawKeyValueList Query => _query; - - public ReadOnlyMemory Body { get; set; } - - public RawRequest() - { - _source = new(); - - _headers = new(_source.Headers); - _query = new(_source.QueryParameters); - - _target = new(); - } - - public void Reset() - { - _source.Clear(); - } - - public void Apply() - { - _target.Apply(Path); - } - -} diff --git a/src/Examples/GenHTTP/Types/RawRequestTarget.cs b/src/Examples/GenHTTP/Types/RawRequestTarget.cs deleted file mode 100644 index 30fb27b..0000000 --- a/src/Examples/GenHTTP/Types/RawRequestTarget.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Runtime.CompilerServices; -using GenHTTP.Protocol.Raw; - -namespace GenHTTP.Types; - -public sealed class RawRequestTarget : IRawRequestTarget -{ - private ReadOnlyMemory _path = ReadOnlyMemory.Empty; - - private int _offset; - - public ReadOnlyMemory? Current { get; private set; } - - public void Apply(ReadOnlyMemory path) - { - _path = path; - - _offset = 0; - Current = null; - - MoveNext(); - } - - public void Advance(int segments = 1) - { - while (segments-- > 0 && Current != null) - { - MoveNext(); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void MoveNext() - { - var span = _path.Span; - var length = span.Length; - - if (length < 2) - { - Current = null; - return; - } - - while (_offset < length && span[_offset] == (byte)'/') - { - _offset++; - } - - if (_offset >= length) - { - Current = null; - return; - } - - var start = _offset; - - var idx = span[_offset..].IndexOf((byte)'/'); - - if (idx < 0) - { - _offset = length; - Current = _path.Slice(start, length - start); - } - else - { - _offset += idx; - Current = _path.Slice(start, idx); - } - } - -} diff --git a/src/Examples/GenHTTP/Types/Request.cs b/src/Examples/GenHTTP/Types/Request.cs deleted file mode 100644 index 410a629..0000000 --- a/src/Examples/GenHTTP/Types/Request.cs +++ /dev/null @@ -1,57 +0,0 @@ -using GenHTTP.Protocol; -using GenHTTP.Protocol.Raw; -using GenHTTP.Utils; -using Glyph11.Protocol; - -namespace GenHTTP.Types; - -public sealed class Request : IRequest -{ - private readonly RawRequest _raw = new(); - - private RequestMethod? _method; - - public IRawRequest Raw => _raw; - - internal BinaryRequest Source => _raw.Source; - - public RequestMethod Method - { - get - { - if (_method == null) - { - var m = _raw.Method.Span; - - _method = m.Length switch - { - 3 when AsciiComparer.EqualsIgnoreCase(m, "GET"u8) => RequestMethod.Get, - 4 when AsciiComparer.EqualsIgnoreCase(m, "POST"u8) => RequestMethod.Post, - 3 when AsciiComparer.EqualsIgnoreCase(m, "PUT"u8) => RequestMethod.Put, - 6 when AsciiComparer.EqualsIgnoreCase(m, "DELETE"u8) => RequestMethod.Delete, - 4 when AsciiComparer.EqualsIgnoreCase(m, "HEAD"u8) => RequestMethod.Head, - 7 when AsciiComparer.EqualsIgnoreCase(m, "OPTIONS"u8) => RequestMethod.Options, - 5 when AsciiComparer.EqualsIgnoreCase(m, "PATCH"u8) => RequestMethod.Patch, - 5 when AsciiComparer.EqualsIgnoreCase(m, "TRACE"u8) => RequestMethod.Trace, - 7 when AsciiComparer.EqualsIgnoreCase(m, "CONNECT"u8) => RequestMethod.Connect, - _ => RequestMethod.Other - }; - } - - return _method.Value; - } - } - - public void Reset() - { - _raw.Reset(); - - _method = null; - } - - public void Apply() - { - _raw.Apply(); - } - -} diff --git a/src/Examples/GenHTTP/Utils/AsciiComparer.cs b/src/Examples/GenHTTP/Utils/AsciiComparer.cs deleted file mode 100644 index dc8f7a0..0000000 --- a/src/Examples/GenHTTP/Utils/AsciiComparer.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace GenHTTP.Utils; - -public static class AsciiComparer -{ - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EqualsIgnoreCase(ReadOnlyMemory a, ReadOnlyMemory b) - => EqualsIgnoreCase(a.Span, b.Span); - - public static bool EqualsIgnoreCase(ReadOnlySpan a, ReadOnlySpan b) - { - if (a.Length != b.Length) - { - return false; - } - - for (var i = 0; i < a.Length; i++) - { - var x = a[i]; - var y = b[i]; - - if ((uint)(x - 'A') <= 25) x = (byte)(x + 32); - if ((uint)(y - 'A') <= 25) y = (byte)(y + 32); - - if (x != y) - { - return false; - } - } - - return true; - } - -} diff --git a/src/Examples/ServerTest/Program.cs b/src/Examples/ServerTest/Program.cs deleted file mode 100644 index 7267216..0000000 --- a/src/Examples/ServerTest/Program.cs +++ /dev/null @@ -1,250 +0,0 @@ -using System.Buffers; -using System.IO.Pipelines; -using System.Net; -using System.Net.Sockets; -using System.Text; -using Glyph11; -using Glyph11.Parser; -using Glyph11.Parser.UltraHardened; -using Glyph11.Protocol; -using Glyph11.Validation; - -var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; - -var listener = new TcpListener(IPAddress.Any, port); -listener.Start(); - -Console.WriteLine($"GlyphServer listening on http://localhost:{port}"); - -using var cts = new CancellationTokenSource(); -Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; - -try -{ - while (!cts.Token.IsCancellationRequested) - { - var client = await listener.AcceptTcpClientAsync(cts.Token); - _ = HandleClientAsync(client, cts.Token); - } -} -catch (OperationCanceledException) { } - -listener.Stop(); -Console.WriteLine("Server stopped."); - -static async Task HandleClientAsync(TcpClient client, CancellationToken ct) -{ - using (client) - await using (var stream = client.GetStream()) - { - var limits = ParserLimits.Default; - var reader = PipeReader.Create(stream); - using var request = new BinaryRequest(); - - try - { - while (!ct.IsCancellationRequested) - { - // ── Phase 1: parse header ────────────────────────── - // Loop until we have a complete header. Do NOT advance - // the pipe yet — request holds ReadOnlyMemory slices - // into the pipe buffer. - ReadOnlySequence headerBuffer; - int headerByteCount; - while (true) - { - request.Clear(); - var result = await reader.ReadAsync(ct); - var buffer = result.Buffer; - - if (result.IsCompleted && buffer.IsEmpty) - { - await reader.CompleteAsync(); - return; - } - - var sequence = buffer; - try - { - // TODO FOR SINGLE SEQUENCE THERE ARE NO ALLOCATIONS, FOR MULTI SEGMENT THERE ARE, THAT INTERFERES THE BEHAVIOR - // TODO MEANING WE CANT ADVANCE FOR SINGLE SEGMENT CASE - - if (UltraHardenedParser.TryExtractFullHeaderValidated(ref sequence, request, in limits, out var bytesRead)) - { - Console.WriteLine(Encoding.UTF8.GetString(sequence)); - - headerByteCount = bytesRead + 1; - headerBuffer = buffer; - break; - } - - if (buffer.Length > limits.MaxTotalHeaderBytes) - { - reader.AdvanceTo(buffer.End); - await stream.WriteAsync(MakeErrorResponse(431, "Request Header Fields Too Large"), ct); - await reader.CompleteAsync(); - return; - } - - reader.AdvanceTo(buffer.Start, buffer.End); - - if (result.IsCompleted) - { - await reader.CompleteAsync(); - return; - } - } - catch (HttpParseException ex) - { - var code = ex.StatusCode; - var reason = code switch - { - 431 => "Request Header Fields Too Large", - _ => "Bad Request" - }; - reader.AdvanceTo(buffer.End); - await stream.WriteAsync(MakeErrorResponse(code, reason), ct); - await reader.CompleteAsync(); - return; - } - } - - // ── Phase 2: validation ──────────────────────────── - // No work needed — UltraHardenedParser enforced all structural and - // semantic checks during Phase 1 parsing (it throws on any violation). - - // ── Phase 3: extract values & detect framing ─────── - // Copy what we need out of the pipe buffer, then release it. - var method = Encoding.ASCII.GetString(request.Method.Span); - var path = Encoding.ASCII.GetString(request.Path.Span); - var framing = BodyFramingDetector.DetectBodyFraming(request); - - // Now safe to advance past the header bytes. - reader.AdvanceTo(headerBuffer.GetPosition(headerByteCount)); - - // ── Phase 4: consume body ────────────────────────── - switch (framing.Framing) - { - case BodyFraming.ContentLength: - { - long remaining = framing.ContentLength; - while (remaining > 0) - { - var result = await reader.ReadAsync(ct); - var buffer = result.Buffer; - long available = Math.Min(buffer.Length, remaining); - remaining -= available; - reader.AdvanceTo(buffer.GetPosition(available)); - - if (result.IsCompleted && remaining > 0) - { - await reader.CompleteAsync(); - return; - } - } - break; - } - - case BodyFraming.Chunked: - { - var chunked = new ChunkedBodyStream(); - while (true) - { - var result = await reader.ReadAsync(ct); - var buffer = result.Buffer; - - ReadOnlySpan span; - byte[]? linearized = null; - if (buffer.IsSingleSegment) - { - span = buffer.FirstSpan; - } - else - { - linearized = new byte[buffer.Length]; - buffer.CopyTo(linearized); - span = linearized; - } - - bool done = false; - int totalConsumed = 0; - while (true) - { - var cr = chunked.TryReadChunk(span[totalConsumed..], out var consumed, out _, out _); - totalConsumed += consumed; - - if (cr == ChunkResult.Completed) - { - done = true; - break; - } - if (cr == ChunkResult.NeedMoreData) - break; - // ChunkResult.Chunk — loop to consume next chunk - } - - reader.AdvanceTo(buffer.GetPosition(totalConsumed)); - - if (done) - break; - - if (result.IsCompleted) - { - await reader.CompleteAsync(); - return; - } - } - break; - } - - case BodyFraming.None: - default: - break; - } - - // ── Phase 5: send response ───────────────────────── - var responseBytes = BuildResponse(method, path); - await stream.WriteAsync(responseBytes, ct); - } - } - catch (OperationCanceledException) { } - catch (IOException) { } - catch (HttpParseException ex) - { - var code = ex.StatusCode; - var reason = code switch - { - 431 => "Request Header Fields Too Large", - _ => "Bad Request" - }; - try { await stream.WriteAsync(MakeErrorResponse(code, reason), ct); } catch { } - } - finally - { - await reader.CompleteAsync(); - } - } -} - -static byte[] BuildResponse(string method, string path) -{ - var body = $"Hello from GlyphServer\r\nMethod: {method}\r\nPath: {path}\r\n"; - return MakeResponse(200, "OK", body); -} - -static byte[] MakeResponse(int status, string reason, string body) -{ - var bodyBytes = Encoding.UTF8.GetBytes(body); - var header = $"HTTP/1.1 {status} {reason}\r\nContent-Type: text/plain\r\nContent-Length: {bodyBytes.Length}\r\nConnection: keep-alive\r\n\r\n"; - var headerBytes = Encoding.ASCII.GetBytes(header); - - var result = new byte[headerBytes.Length + bodyBytes.Length]; - Buffer.BlockCopy(headerBytes, 0, result, 0, headerBytes.Length); - Buffer.BlockCopy(bodyBytes, 0, result, headerBytes.Length, bodyBytes.Length); - return result; -} - -static byte[] MakeErrorResponse(int status, string reason) -{ - return MakeResponse(status, reason, $"{status} {reason}\r\n"); -} diff --git a/src/Examples/ServerTest/ServerTest.csproj b/src/Examples/ServerTest/ServerTest.csproj deleted file mode 100644 index 52c5f75..0000000 --- a/src/Examples/ServerTest/ServerTest.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - Exe - net10.0 - enable - enable - - - - - - - diff --git a/src/Glyph11.sln b/src/Glyph11.sln index c2f330d..0004c8f 100644 --- a/src/Glyph11.sln +++ b/src/Glyph11.sln @@ -4,13 +4,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Glyph11", "Glyph11\Glyph11. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "..\tests\Tests\Tests.csproj", "{8DD133D0-C052-4CF9-8A09-6E948E68311B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP", "Examples\GenHTTP\GenHTTP.csproj", "{D6936307-4A8C-4636-ADA2-4761D101970E}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "..\Benchmarks\Benchmarks.csproj", "{0DF18FBF-F275-4CA3-ACA6-E9D60E21DE6D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{B1832E06-A86C-42E2-BB12-CE83BDE6D77A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Glyph11.Example", "..\Examples\Glyph11.Example\Glyph11.Example.csproj", "{9BB41491-CA01-4D1B-93CC-BEF6C449179A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Glyph11.Native.Example", "..\Examples\Glyph11.Native.Example\Glyph11.Native.Example.csproj", "{3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServerTest", "Examples\ServerTest\ServerTest.csproj", "{CE2C01E2-C498-45EE-9EA9-67F43787D00D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Glyph11.Pico.Example", "..\Examples\Glyph11.Pico.Example\Glyph11.Pico.Example.csproj", "{CE3462D0-F32A-4ED0-91E1-6B03980FB656}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -46,18 +46,6 @@ Global {8DD133D0-C052-4CF9-8A09-6E948E68311B}.Release|x64.Build.0 = Release|Any CPU {8DD133D0-C052-4CF9-8A09-6E948E68311B}.Release|x86.ActiveCfg = Release|Any CPU {8DD133D0-C052-4CF9-8A09-6E948E68311B}.Release|x86.Build.0 = Release|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Debug|x64.ActiveCfg = Debug|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Debug|x64.Build.0 = Debug|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Debug|x86.ActiveCfg = Debug|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Debug|x86.Build.0 = Debug|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Release|Any CPU.Build.0 = Release|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Release|x64.ActiveCfg = Release|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Release|x64.Build.0 = Release|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Release|x86.ActiveCfg = Release|Any CPU - {D6936307-4A8C-4636-ADA2-4761D101970E}.Release|x86.Build.0 = Release|Any CPU {0DF18FBF-F275-4CA3-ACA6-E9D60E21DE6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0DF18FBF-F275-4CA3-ACA6-E9D60E21DE6D}.Debug|Any CPU.Build.0 = Debug|Any CPU {0DF18FBF-F275-4CA3-ACA6-E9D60E21DE6D}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -70,24 +58,44 @@ Global {0DF18FBF-F275-4CA3-ACA6-E9D60E21DE6D}.Release|x64.Build.0 = Release|Any CPU {0DF18FBF-F275-4CA3-ACA6-E9D60E21DE6D}.Release|x86.ActiveCfg = Release|Any CPU {0DF18FBF-F275-4CA3-ACA6-E9D60E21DE6D}.Release|x86.Build.0 = Release|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Debug|x64.ActiveCfg = Debug|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Debug|x64.Build.0 = Debug|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Debug|x86.ActiveCfg = Debug|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Debug|x86.Build.0 = Debug|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Release|Any CPU.Build.0 = Release|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Release|x64.ActiveCfg = Release|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Release|x64.Build.0 = Release|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Release|x86.ActiveCfg = Release|Any CPU - {CE2C01E2-C498-45EE-9EA9-67F43787D00D}.Release|x86.Build.0 = Release|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Debug|x64.ActiveCfg = Debug|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Debug|x64.Build.0 = Debug|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Debug|x86.ActiveCfg = Debug|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Debug|x86.Build.0 = Debug|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Release|Any CPU.Build.0 = Release|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Release|x64.ActiveCfg = Release|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Release|x64.Build.0 = Release|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Release|x86.ActiveCfg = Release|Any CPU + {9BB41491-CA01-4D1B-93CC-BEF6C449179A}.Release|x86.Build.0 = Release|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|x64.ActiveCfg = Debug|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|x64.Build.0 = Debug|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|x86.ActiveCfg = Debug|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Debug|x86.Build.0 = Debug|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|Any CPU.Build.0 = Release|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|x64.ActiveCfg = Release|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|x64.Build.0 = Release|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|x86.ActiveCfg = Release|Any CPU + {3E9D42DA-8C26-4C4A-99F6-E275121BDBA9}.Release|x86.Build.0 = Release|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|x64.ActiveCfg = Debug|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|x64.Build.0 = Debug|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|x86.ActiveCfg = Debug|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Debug|x86.Build.0 = Debug|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|Any CPU.Build.0 = Release|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|x64.ActiveCfg = Release|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|x64.Build.0 = Release|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|x86.ActiveCfg = Release|Any CPU + {CE3462D0-F32A-4ED0-91E1-6B03980FB656}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {D6936307-4A8C-4636-ADA2-4761D101970E} = {B1832E06-A86C-42E2-BB12-CE83BDE6D77A} - {CE2C01E2-C498-45EE-9EA9-67F43787D00D} = {B1832E06-A86C-42E2-BB12-CE83BDE6D77A} - EndGlobalSection EndGlobal diff --git a/tests/Tests/FlexibleParser.TryExtractFullHeader.cs b/tests/Tests/FlexibleParser.TryExtractFullHeader.cs index f8ba054..ed097cc 100644 --- a/tests/Tests/FlexibleParser.TryExtractFullHeader.cs +++ b/tests/Tests/FlexibleParser.TryExtractFullHeader.cs @@ -1,7 +1,5 @@ using System.Buffers; using System.Text; -using GenHTTP.Protocol; -using GenHTTP.Types; using Glyph11.Parser.FlexibleParser; using Glyph11.Protocol; @@ -17,14 +15,14 @@ public void ParseSingleSegmentRequest() var request = "GET /route?p1=1&p2=2&p3=3&p4=4 HTTP/1.1\r\n" + "Content-Length: 100\r\n" + - "Server: GenHTTP\r\n" + + "Server: Glyph11\r\n" + "\r\n"; ReadOnlyMemory rom = Encoding.ASCII.GetBytes(request); - var data = new Request(); + var data = new BinaryRequest(); - var parsed = FlexibleParser.TryExtractFullHeaderReadOnlyMemory(ref rom, data.Source, out var position); + var parsed = FlexibleParser.TryExtractFullHeaderReadOnlyMemory(ref rom, data, out var position); Assert.True(parsed); AssertRequestParsedCorrectly(data); @@ -38,9 +36,9 @@ public void ParseMultiSegmentRequest() { ReadOnlySequence segmented = CreateMultiSegment(); - var data = new Request(); + var data = new BinaryRequest(); - var parsed = FlexibleParser.TryExtractFullHeader(ref segmented, data.Source, out var position); + var parsed = FlexibleParser.TryExtractFullHeader(ref segmented, data, out var position); Assert.True(parsed); AssertRequestParsedCorrectly(data); @@ -48,17 +46,14 @@ public void ParseMultiSegmentRequest() Assert.Equal((int)segmented.Length - 1, position); } - private static void AssertRequestParsedCorrectly(Request data) + private static void AssertRequestParsedCorrectly(BinaryRequest data) { - // Method (enum + raw bytes) - Assert.Equal(RequestMethod.Get, data.Method); - AssertAscii.Equal("GET", data.Source.Method); - - // Route (path only) - AssertAscii.Equal(ExpectedPath, data.Source.Path); + // Method + path (path only, query stripped) + AssertAscii.Equal("GET", data.Method); + AssertAscii.Equal(ExpectedPath, data.Path); // Query params - var qp = data.Source.QueryParameters; + var qp = data.QueryParameters; Assert.Equal(4, qp.Count); AssertKeyValue(qp, "p1", "1"); @@ -67,11 +62,11 @@ private static void AssertRequestParsedCorrectly(Request data) AssertKeyValue(qp, "p4", "4"); // Headers - var headers = data.Source.Headers; + var headers = data.Headers; Assert.Equal(2, headers.Count); AssertKeyValue(headers, "Content-Length", "100"); - AssertKeyValue(headers, "Server", "GenHTTP"); + AssertKeyValue(headers, "Server", "Glyph11"); } private static void AssertKeyValue(KeyValueList list, string expectedKey, string expectedValue) @@ -106,7 +101,7 @@ private static ReadOnlySequence CreateMultiSegment() { var seg1 = "GET /route?p1=1&p2=2&p3=3&p4=4 HT"u8.ToArray(); var seg2 = "TP/1.1\r\nContent-Length: 100\r\nServer: "u8.ToArray(); - var seg3 = "GenHTTP\r\n\r\n"u8.ToArray(); + var seg3 = "Glyph11\r\n\r\n"u8.ToArray(); var first = new Glyph11.Utils.BufferSegment(seg1); var last = first.Append(seg2).Append(seg3); diff --git a/tests/Tests/Tests.csproj b/tests/Tests/Tests.csproj index b65ad4b..9268718 100644 --- a/tests/Tests/Tests.csproj +++ b/tests/Tests/Tests.csproj @@ -20,7 +20,6 @@ -