refactor(battle-node): SocketIoFrame throws on namespace; typed JSON construction
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace SVSim.BattleNode.Wire;
|
||||
|
||||
@@ -65,11 +66,17 @@ public sealed class SocketIoFrame
|
||||
cursor = dashIdx + 1;
|
||||
}
|
||||
|
||||
// Skip namespace (only present if a '/' starts here, terminated by ',').
|
||||
// Namespace prefix (only present if '/' starts here, terminated by ','). v1 only
|
||||
// uses the default namespace; anything else is a protocol surprise we should
|
||||
// surface rather than silently route to default. If we ever support non-default
|
||||
// namespaces, capture into a property and let callers branch.
|
||||
if (cursor < raw.Length && raw[cursor] == '/')
|
||||
{
|
||||
var commaIdx = raw.IndexOf(',', cursor);
|
||||
cursor = commaIdx >= 0 ? commaIdx + 1 : raw.Length;
|
||||
var ns = commaIdx >= 0 ? raw.Substring(cursor, commaIdx - cursor) : raw.Substring(cursor);
|
||||
throw new ArgumentException(
|
||||
$"Socket.IO namespaces aren't supported — got '{ns}'. v1 expects default namespace only.",
|
||||
nameof(raw));
|
||||
}
|
||||
|
||||
int? ackId = null;
|
||||
@@ -126,19 +133,15 @@ public sealed class SocketIoFrame
|
||||
/// </summary>
|
||||
public static SocketIoFrame BinaryEventWithAttachments(string eventName, IReadOnlyList<byte[]> attachments)
|
||||
{
|
||||
// Build the placeholder-only portion (without the event name) — event name is stored separately.
|
||||
var sb = new StringBuilder();
|
||||
sb.Append('[');
|
||||
// Build placeholders via the typed Nodes API; event name is stored separately.
|
||||
var placeholders = new JsonArray();
|
||||
for (var i = 0; i < attachments.Count; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(',');
|
||||
sb.Append("{\"_placeholder\":true,\"num\":").Append(i).Append('}');
|
||||
}
|
||||
sb.Append(']');
|
||||
JsonElement[] args;
|
||||
using (var doc = JsonDocument.Parse(sb.ToString()))
|
||||
{
|
||||
args = doc.RootElement.EnumerateArray().Select(el => el.Clone()).ToArray();
|
||||
placeholders.Add(new JsonObject
|
||||
{
|
||||
["_placeholder"] = true,
|
||||
["num"] = i,
|
||||
});
|
||||
}
|
||||
|
||||
return new SocketIoFrame(
|
||||
@@ -146,20 +149,28 @@ public sealed class SocketIoFrame
|
||||
ackId: null,
|
||||
attachmentCount: attachments.Count,
|
||||
eventName: eventName,
|
||||
rawArgs: args,
|
||||
rawArgs: NodesToElements(placeholders),
|
||||
binaryAttachments: attachments);
|
||||
}
|
||||
|
||||
/// <summary>Build an ack response with a single int argument (the spec's pubSeq echo).</summary>
|
||||
public static SocketIoFrame AckResponse(int ackId, int arg)
|
||||
{
|
||||
JsonElement[] args;
|
||||
using (var doc = JsonDocument.Parse($"[{arg}]"))
|
||||
{
|
||||
args = doc.RootElement.EnumerateArray().Select(el => el.Clone()).ToArray();
|
||||
}
|
||||
var args = new JsonArray { arg };
|
||||
return new SocketIoFrame(
|
||||
SocketIoPacketType.Ack, ackId, 0, null, args, Array.Empty<byte[]>());
|
||||
SocketIoPacketType.Ack, ackId, 0, null, NodesToElements(args), Array.Empty<byte[]>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a <see cref="JsonArray"/> into the <see cref="JsonElement"/>[] that
|
||||
/// <see cref="RawArgs"/> stores. The current storage type is <see cref="JsonElement"/>
|
||||
/// because <see cref="Parse"/> produces it from <see cref="JsonDocument"/>; this helper
|
||||
/// keeps the typed-construction call sites without changing <see cref="RawArgs"/>.
|
||||
/// </summary>
|
||||
private static JsonElement[] NodesToElements(JsonArray nodes)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(nodes.ToJsonString());
|
||||
return doc.RootElement.EnumerateArray().Select(el => el.Clone()).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -105,6 +105,16 @@ public class SocketIoFrameTests
|
||||
Assert.That(text, Is.EqualTo(wire));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_NamespacePrefix_Throws()
|
||||
{
|
||||
// v1 only supports the default namespace. A "/foo," prefix used to be silently
|
||||
// skipped, which would route a frame meant for namespace /foo to the default
|
||||
// handler. Fail loud instead so we'd notice if the client ever started using one.
|
||||
var ex = Assert.Throws<ArgumentException>(() => SocketIoFrame.Parse("2/foo,[\"msg\"]"));
|
||||
Assert.That(ex!.Message, Does.Contain("/foo"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Encode_EventNameWithSpecialChars_IsJsonEscaped()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user