Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/clients/ioxide.file/ioxide.file.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.file</RootNamespace>

<PackageId>ioxide.file</PackageId>
<Version>0.4.186</Version>
<Version>0.4.187</Version>
<Authors>MDA2AV</Authors>
<Description>File serving for the ioxide io_uring runtime: immutable asset snapshots with baked responses, pooled positional ring reads, atomic reloads.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.httpclient/ioxide.httpclient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.httpclient</RootNamespace>

<PackageId>ioxide.httpclient</PackageId>
<Version>0.4.186</Version>
<Version>0.4.187</Version>
<Authors>MDA2AV</Authors>
<Description>The ring-native HTTP/1.1 client for the ioxide io_uring runtime - the upstream leg between a proxy and an origin. Connections are opened on the reactor thread that will use them, so a request never crosses a thread on its way out or back, and every response resumes the awaiting handler inline on its own reactor. Includes client-side TLS (SNI, ALPN, certificate verification and client certificates for mutual TLS) for https:// origins. Depends on ioxide core alone: no protocol package, no native asset.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.pg/ioxide.pg.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.pg</RootNamespace>

<PackageId>ioxide.pg</PackageId>
<Version>0.4.186</Version>
<Version>0.4.187</Version>
<Authors>MDA2AV</Authors>
<Description>Postgres driver for the ioxide io_uring runtime: pooled ring-native connections per reactor, ring-native connect and handshake, inline completion resume.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.redis/ioxide.redis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.redis</RootNamespace>

<PackageId>ioxide.redis</PackageId>
<Version>0.4.186</Version>
<Version>0.4.187</Version>
<Authors>MDA2AV</Authors>
<Description>Redis client for the ioxide io_uring runtime: pooled ring-native connections per reactor, full RESP2 protocol, a generic command API plus typed helpers (strings, keys, hashes, lists, sets, sorted sets, pub/sub, transactions, scripting), and pipelining. Inline completion resume.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/ioxide/ioxide.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide</RootNamespace>

<PackageId>ioxide</PackageId>
<Version>0.4.186</Version>
<Version>0.4.187</Version>
<Authors>MDA2AV</Authors>
<Description>A shared-nothing io_uring runtime for .NET: one ring per reactor thread, inline completions, zero native dependencies. The engine - reactor, connection, and the IRingHost client seam. Includes TLS termination: the OpenSSL handshake driven over the ring, then kernel TLS (kTLS) transmit offload, so handlers keep writing plaintext. TLS needs OpenSSL 3 and the Linux tls module; nothing else does, and neither is loaded unless you use it.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.http2/ioxide.http2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.http2</RootNamespace>

<PackageId>ioxide.http2</PackageId>
<Version>0.4.186</Version>
<Version>0.4.187</Version>
<Authors>MDA2AV</Authors>
<Description>Pure-C# HTTP/2 for the ioxide io_uring runtime: framing, HPACK (static and dynamic tables, Huffman) and flow control, with zero native code. Serves h2c with prior knowledge and h2 over TLS by ALPN, buffered or streamed in either direction.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
181 changes: 171 additions & 10 deletions src/protocols/ioxide.http3/Qpack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,176 @@ private static readonly (int Status, byte Index)[] IndexedStatuses =
];

/// <summary>Encode a response's field section (prefix + :status + headers) into a pooled buffer.</summary>
/// <summary>
/// The static table indexed for encoding: distinct names, each with the entries sharing it.
/// </summary>
/// <remarks>
/// Grouped rather than flat so a lookup rejects most candidates on length alone. Built once from
/// the 99 fixed entries.
/// </remarks>
private static readonly (byte[] Name, int NameIndex, (byte[] Value, int Index)[] Values)[][] StaticByLength = BuildStaticNames();

/// <summary>Longest field name in the static table, so a longer name skips the lookup entirely.</summary>
private static readonly int LongestStaticName = StaticByLength.Length - 1;

private static (byte[] Name, int NameIndex, (byte[] Value, int Index)[] Values)[][] BuildStaticNames()
{
var byName = new List<(byte[] Name, int NameIndex, List<(byte[] Value, int Index)> Values)>();

for (int i = 0; i < QpackStatic.Table.Length; i++)
{
(byte[] name, byte[] value) = QpackStatic.Table[i];

int at = -1;
for (int j = 0; j < byName.Count; j++)
{
if (byName[j].Name.AsSpan().SequenceEqual(name))
{
at = j;
break;
}
}

if (at < 0)
{
byName.Add((name, i, [(value, i)]));
}
else
{
byName[at].Values.Add((value, i));
}
}

// Bucketed by name length. A per-header linear scan of every distinct name is far too
// expensive at this request rate - it cost about 6% of throughput - and length alone
// narrows 50-odd candidates down to one or two.
int longest = 0;

foreach ((byte[] name, int _, List<(byte[] Value, int Index)> _) in byName)
{
longest = Math.Max(longest, name.Length);
}

var buckets = new List<(byte[], int, (byte[], int)[])>[longest + 1];

foreach ((byte[] name, int nameIndex, List<(byte[] Value, int Index)> values) in byName)
{
buckets[name.Length] ??= [];
buckets[name.Length].Add((name, nameIndex, values.ToArray()));
}

var result = new (byte[], int, (byte[], int)[])[longest + 1][];

for (int i = 0; i <= longest; i++)
{
result[i] = buckets[i]?.ToArray() ?? [];
}

return result;
}

/// <summary>
/// Finds a header in the static table: an exact name and value match, or failing that a name.
/// </summary>
/// <remarks>
/// Names match case-insensitively, so a caller holding HTTP's conventional capitalisation still
/// resolves - and then never writes the name at all. Values match exactly, because a field value
/// is case-sensitive.
/// </remarks>
private static bool TryFindStatic(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value, out int exact, out int nameIndex)
{
exact = -1;
nameIndex = -1;

if (name.Length > LongestStaticName)
{
return false;
}

foreach ((byte[] candidate, int candidateIndex, (byte[] Value, int Index)[] values) in StaticByLength[name.Length])
{
if (!EqualsIgnoreCase(candidate, name))
{
continue;
}

nameIndex = candidateIndex;

foreach ((byte[] entryValue, int entryIndex) in values)
{
if (entryValue.AsSpan().SequenceEqual(value))
{
exact = entryIndex;
break;
}
}

return true;
}

return false;
}

private static bool EqualsIgnoreCase(ReadOnlySpan<byte> lowercase, ReadOnlySpan<byte> other)
{
for (int i = 0; i < lowercase.Length; i++)
{
byte c = other[i];

if (c is >= (byte)'A' and <= (byte)'Z')
{
c |= 0x20;
}

if (c != lowercase[i])
{
return false;
}
}

return true;
}

/// <summary>
/// Writes one header, preferring the static table over spelling the name out.
/// </summary>
/// <remarks>
/// Three tiers, cheapest first: name and value both in the table cost a single byte; a known
/// name costs an index plus the literal value; anything else falls back to the full literal.
/// Unlike the dynamic table this needs nothing from the peer, so it applies to every client.
/// </remarks>
private static int WriteHeader(Span<byte> buf, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
if (TryFindStatic(name, value, out int exact, out int nameIndex))
{
if (exact >= 0)
{
// Indexed Field Line: 1 T=1 index(6+).
return WriteInt(buf, 0xC0, 6, exact);
}

// Literal With Name Reference: 01 N=0 T=1 nameindex(4+), then H=0 value(7+).
int written = WriteInt(buf, 0x50, 4, nameIndex);
written += WriteInt(buf[written..], 0x00, 7, value.Length);
value.CopyTo(buf[written..]);

return written + value.Length;
}

// Literal With Literal Name: 001 N=0 H=0 namelen(3+), lowercased name, H=0 value(7+).
int w = WriteInt(buf, 0x20, 3, name.Length);

foreach (byte b in name)
{
buf[w++] = b is >= (byte)'A' and <= (byte)'Z' ? (byte)(b | 0x20) : b;
}

w += WriteInt(buf[w..], 0x00, 7, value.Length);
value.CopyTo(buf[w..]);

return w + value.Length;
}

public static byte[] EncodeResponseFields(Http3Response response, out int written)
{
int cap = 2 + 8;
Expand Down Expand Up @@ -258,16 +428,7 @@ public static byte[] EncodeResponseFields(Http3Response response, out int writte

foreach ((ReadOnlyMemory<byte> nameM, ReadOnlyMemory<byte> valueM) in response.Headers)
{
// Literal With Literal Name: 001 N=0 H=0 namelen(3+), lowercased name, H=0 value(7+).
ReadOnlySpan<byte> name = nameM.Span;
w += WriteInt(buf.AsSpan(w), 0x20, 3, name.Length);
foreach (byte b in name)
{
buf[w++] = b is >= (byte)'A' and <= (byte)'Z' ? (byte)(b | 0x20) : b;
}
w += WriteInt(buf.AsSpan(w), 0x00, 7, valueM.Length);
valueM.Span.CopyTo(buf.AsSpan(w));
w += valueM.Length;
w += WriteHeader(buf.AsSpan(w), nameM.Span, valueM.Span);
}

written = w;
Expand Down
9 changes: 8 additions & 1 deletion src/protocols/ioxide.http3/ioxide.http3.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.http3</RootNamespace>

<PackageId>ioxide.http3</PackageId>
<Version>0.4.186</Version>
<Version>0.4.187</Version>
<Authors>MDA2AV</Authors>
<Description>Pure C# HTTP/3 for the ioxide io_uring runtime: frame parsing, QPACK (static table + Huffman) and request dispatch with zero native dependencies. Rides any QuicConnection via its stream read surface - engine-agnostic, drop-in alternative to ioxide.nghttp3.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand All @@ -24,4 +24,11 @@
<ProjectReference Include="../../ioxide/ioxide.csproj" />
</ItemGroup>

<ItemGroup>
<!-- The QPACK encoder is driven directly by the unit tests: a wrong static-table index is
invisible from outside, since the response still decodes to the right thing on a peer
that happens to agree. -->
<InternalsVisibleTo Include="Ioxide.Tests.Unit" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/protocols/ioxide.nghttp2/ioxide.nghttp2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.nghttp2</RootNamespace>

<PackageId>ioxide.nghttp2</PackageId>
<Version>0.4.186</Version>
<Version>0.4.187</Version>
<Authors>MDA2AV</Authors>
<Description>HTTP/2 for the ioxide io_uring runtime: framing, HPACK and flow control from nghttp2, statically linked behind a small shim with no external dependencies beyond libc. Serves HTTP/2 over any TcpConnection - h2c with prior knowledge, or h2 over TLS via ALPN. nghttp2 is sans-I/O, so ioxide keeps the ring and the loop.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.nghttp3/ioxide.nghttp3.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.nghttp3</RootNamespace>

<PackageId>ioxide.nghttp3</PackageId>
<Version>0.4.186</Version>
<Version>0.4.187</Version>
<Authors>MDA2AV</Authors>
<Description>HTTP/3 layer for the ioxide io_uring runtime: nghttp3 (H3 + QPACK) bundled as a single self-contained native library with no external dependencies. Rides any QuicConnection via its stream read surface - engine-agnostic, no ioxide.ngtcp2 dependency.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.ngtcp2/ioxide.ngtcp2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.ngtcp2</RootNamespace>

<PackageId>ioxide.ngtcp2</PackageId>
<Version>0.4.186</Version>
<Version>0.4.187</Version>
<Authors>MDA2AV</Authors>
<Description>QUIC engine for the ioxide io_uring runtime: ngtcp2 + picotls bundled as a single self-contained native library (only system dependency: libcrypto.so.3 / OpenSSL 3.x). Plugs into the reactor's QUIC transport via QuicConnection. Server side; engine bindings in progress.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/serving/ioxide.Kestrel/ioxide.Kestrel.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.Kestrel</RootNamespace>

<PackageId>ioxide.Kestrel</PackageId>
<Version>0.4.186</Version>
<Version>0.4.187</Version>
<Authors>MDA2AV</Authors>
<Description>ASP.NET Core Kestrel transport backed by the ioxide io_uring runtime: one reactor (ring) per core, SO_REUSEPORT load-balanced, with Kestrel's HTTP request loop pinned to the reactor thread. Drop-in via UseIoxide().</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
1 change: 1 addition & 0 deletions tests/Ioxide.Tests.Unit/Ioxide.Tests.Unit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<ProjectReference Include="../Ioxide.Tests.Harness/Ioxide.Tests.Harness.csproj" />
<ProjectReference Include="../../src/clients/ioxide.httpclient/ioxide.httpclient.csproj" />
<ProjectReference Include="../../src/protocols/ioxide.http2/ioxide.http2.csproj" />
<ProjectReference Include="../../src/protocols/ioxide.http3/ioxide.http3.csproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions tests/Ioxide.Tests.Unit/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ private static int Main()
ResponseCapTests.Register(runner);
Http2OutputQueueTests.Register(runner);
Http2StreamedRequestTests.Register(runner);
QpackStaticEncodeTests.Register(runner);

return runner.Summary();
}
Expand Down
Loading
Loading