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;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
namespace SVSim.BattleNode.Wire;
|
namespace SVSim.BattleNode.Wire;
|
||||||
|
|
||||||
@@ -65,11 +66,17 @@ public sealed class SocketIoFrame
|
|||||||
cursor = dashIdx + 1;
|
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] == '/')
|
if (cursor < raw.Length && raw[cursor] == '/')
|
||||||
{
|
{
|
||||||
var commaIdx = raw.IndexOf(',', 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;
|
int? ackId = null;
|
||||||
@@ -126,19 +133,15 @@ public sealed class SocketIoFrame
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static SocketIoFrame BinaryEventWithAttachments(string eventName, IReadOnlyList<byte[]> attachments)
|
public static SocketIoFrame BinaryEventWithAttachments(string eventName, IReadOnlyList<byte[]> attachments)
|
||||||
{
|
{
|
||||||
// Build the placeholder-only portion (without the event name) — event name is stored separately.
|
// Build placeholders via the typed Nodes API; event name is stored separately.
|
||||||
var sb = new StringBuilder();
|
var placeholders = new JsonArray();
|
||||||
sb.Append('[');
|
|
||||||
for (var i = 0; i < attachments.Count; i++)
|
for (var i = 0; i < attachments.Count; i++)
|
||||||
{
|
{
|
||||||
if (i > 0) sb.Append(',');
|
placeholders.Add(new JsonObject
|
||||||
sb.Append("{\"_placeholder\":true,\"num\":").Append(i).Append('}');
|
{
|
||||||
}
|
["_placeholder"] = true,
|
||||||
sb.Append(']');
|
["num"] = i,
|
||||||
JsonElement[] args;
|
});
|
||||||
using (var doc = JsonDocument.Parse(sb.ToString()))
|
|
||||||
{
|
|
||||||
args = doc.RootElement.EnumerateArray().Select(el => el.Clone()).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SocketIoFrame(
|
return new SocketIoFrame(
|
||||||
@@ -146,20 +149,28 @@ public sealed class SocketIoFrame
|
|||||||
ackId: null,
|
ackId: null,
|
||||||
attachmentCount: attachments.Count,
|
attachmentCount: attachments.Count,
|
||||||
eventName: eventName,
|
eventName: eventName,
|
||||||
rawArgs: args,
|
rawArgs: NodesToElements(placeholders),
|
||||||
binaryAttachments: attachments);
|
binaryAttachments: attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Build an ack response with a single int argument (the spec's pubSeq echo).</summary>
|
/// <summary>Build an ack response with a single int argument (the spec's pubSeq echo).</summary>
|
||||||
public static SocketIoFrame AckResponse(int ackId, int arg)
|
public static SocketIoFrame AckResponse(int ackId, int arg)
|
||||||
{
|
{
|
||||||
JsonElement[] args;
|
var args = new JsonArray { arg };
|
||||||
using (var doc = JsonDocument.Parse($"[{arg}]"))
|
|
||||||
{
|
|
||||||
args = doc.RootElement.EnumerateArray().Select(el => el.Clone()).ToArray();
|
|
||||||
}
|
|
||||||
return new SocketIoFrame(
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -105,6 +105,16 @@ public class SocketIoFrameTests
|
|||||||
Assert.That(text, Is.EqualTo(wire));
|
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]
|
[Test]
|
||||||
public void Encode_EventNameWithSpecialChars_IsJsonEscaped()
|
public void Encode_EventNameWithSpecialChars_IsJsonEscaped()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user