feat: add NatsHeaderParser for MIME header parsing
This commit is contained in:
101
src/NATS.Server/Protocol/NatsHeaderParser.cs
Normal file
101
src/NATS.Server/Protocol/NatsHeaderParser.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace NATS.Server.Protocol;
|
||||||
|
|
||||||
|
public readonly struct NatsHeaders
|
||||||
|
{
|
||||||
|
public int Status { get; init; }
|
||||||
|
public string Description { get; init; }
|
||||||
|
public Dictionary<string, string[]> Headers { get; init; }
|
||||||
|
|
||||||
|
public static readonly NatsHeaders Invalid = new() { Status = -1, Description = string.Empty, Headers = new() };
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NatsHeaderParser
|
||||||
|
{
|
||||||
|
private static readonly byte[] CrLf = "\r\n"u8.ToArray();
|
||||||
|
private static readonly byte[] Prefix = "NATS/1.0"u8.ToArray();
|
||||||
|
|
||||||
|
public static NatsHeaders Parse(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
if (data.Length < Prefix.Length)
|
||||||
|
return NatsHeaders.Invalid;
|
||||||
|
|
||||||
|
if (!data[..Prefix.Length].SequenceEqual(Prefix))
|
||||||
|
return NatsHeaders.Invalid;
|
||||||
|
|
||||||
|
int pos = Prefix.Length;
|
||||||
|
int status = 0;
|
||||||
|
string description = string.Empty;
|
||||||
|
|
||||||
|
// Parse status line: NATS/1.0[ status[ description]]\r\n
|
||||||
|
int lineEnd = data[pos..].IndexOf(CrLf);
|
||||||
|
if (lineEnd < 0)
|
||||||
|
return NatsHeaders.Invalid;
|
||||||
|
|
||||||
|
var statusLine = data[pos..(pos + lineEnd)];
|
||||||
|
pos += lineEnd + 2; // skip \r\n
|
||||||
|
|
||||||
|
if (statusLine.Length > 0)
|
||||||
|
{
|
||||||
|
int si = 0;
|
||||||
|
while (si < statusLine.Length && statusLine[si] == (byte)' ')
|
||||||
|
si++;
|
||||||
|
|
||||||
|
int numStart = si;
|
||||||
|
while (si < statusLine.Length && statusLine[si] >= (byte)'0' && statusLine[si] <= (byte)'9')
|
||||||
|
si++;
|
||||||
|
|
||||||
|
if (si > numStart)
|
||||||
|
{
|
||||||
|
status = int.Parse(Encoding.ASCII.GetString(statusLine[numStart..si]));
|
||||||
|
|
||||||
|
while (si < statusLine.Length && statusLine[si] == (byte)' ')
|
||||||
|
si++;
|
||||||
|
if (si < statusLine.Length)
|
||||||
|
description = Encoding.ASCII.GetString(statusLine[si..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse key-value headers until empty line
|
||||||
|
var headers = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
while (pos < data.Length)
|
||||||
|
{
|
||||||
|
var remaining = data[pos..];
|
||||||
|
if (remaining.Length >= 2 && remaining[0] == (byte)'\r' && remaining[1] == (byte)'\n')
|
||||||
|
break;
|
||||||
|
|
||||||
|
lineEnd = remaining.IndexOf(CrLf);
|
||||||
|
if (lineEnd < 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var headerLine = remaining[..lineEnd];
|
||||||
|
pos += lineEnd + 2;
|
||||||
|
|
||||||
|
int colon = headerLine.IndexOf((byte)':');
|
||||||
|
if (colon < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var key = Encoding.ASCII.GetString(headerLine[..colon]).Trim();
|
||||||
|
var value = Encoding.ASCII.GetString(headerLine[(colon + 1)..]).Trim();
|
||||||
|
|
||||||
|
if (!headers.TryGetValue(key, out var values))
|
||||||
|
{
|
||||||
|
values = [];
|
||||||
|
headers[key] = values;
|
||||||
|
}
|
||||||
|
values.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new Dictionary<string, string[]>(headers.Count, StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var (k, v) in headers)
|
||||||
|
result[k] = v.ToArray();
|
||||||
|
|
||||||
|
return new NatsHeaders
|
||||||
|
{
|
||||||
|
Status = status,
|
||||||
|
Description = description,
|
||||||
|
Headers = result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
51
tests/NATS.Server.Tests/NatsHeaderParserTests.cs
Normal file
51
tests/NATS.Server.Tests/NatsHeaderParserTests.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using NATS.Server.Protocol;
|
||||||
|
|
||||||
|
namespace NATS.Server.Tests;
|
||||||
|
|
||||||
|
public class NatsHeaderParserTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Parse_status_line_only()
|
||||||
|
{
|
||||||
|
var input = "NATS/1.0 503\r\n\r\n"u8;
|
||||||
|
var result = NatsHeaderParser.Parse(input);
|
||||||
|
result.Status.ShouldBe(503);
|
||||||
|
result.Description.ShouldBeEmpty();
|
||||||
|
result.Headers.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_status_with_description()
|
||||||
|
{
|
||||||
|
var input = "NATS/1.0 503 No Responders\r\n\r\n"u8;
|
||||||
|
var result = NatsHeaderParser.Parse(input);
|
||||||
|
result.Status.ShouldBe(503);
|
||||||
|
result.Description.ShouldBe("No Responders");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_headers_with_values()
|
||||||
|
{
|
||||||
|
var input = "NATS/1.0\r\nFoo: bar\r\nBaz: qux\r\n\r\n"u8;
|
||||||
|
var result = NatsHeaderParser.Parse(input);
|
||||||
|
result.Status.ShouldBe(0);
|
||||||
|
result.Headers["Foo"].ShouldBe(["bar"]);
|
||||||
|
result.Headers["Baz"].ShouldBe(["qux"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_multi_value_header()
|
||||||
|
{
|
||||||
|
var input = "NATS/1.0\r\nX-Tag: a\r\nX-Tag: b\r\n\r\n"u8;
|
||||||
|
var result = NatsHeaderParser.Parse(input);
|
||||||
|
result.Headers["X-Tag"].ShouldBe(["a", "b"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_invalid_returns_defaults()
|
||||||
|
{
|
||||||
|
var input = "GARBAGE\r\n\r\n"u8;
|
||||||
|
var result = NatsHeaderParser.Parse(input);
|
||||||
|
result.Status.ShouldBe(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user