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