diff --git a/SVSim.BattleNode/Lifecycle/ServerBattleFrames.cs b/SVSim.BattleNode/Lifecycle/ServerBattleFrames.cs index 9f1b106..a7754bd 100644 --- a/SVSim.BattleNode/Lifecycle/ServerBattleFrames.cs +++ b/SVSim.BattleNode/Lifecycle/ServerBattleFrames.cs @@ -23,7 +23,7 @@ public static class ServerBattleFrames public static MsgEnvelope BuildMatched( MatchContext selfCtx, MatchContext oppoCtx, long selfViewerId, long oppoViewerId, - string battleId, long seed, IReadOnlyList selfDeckOrder) => + string battleId, int seed, IReadOnlyList selfDeckOrder) => EnvelopeForPush(NetworkBattleUri.Matched, new MatchedBody( SelfInfo: new MatchedSelfInfo( @@ -34,7 +34,7 @@ public static class ServerBattleFrames DegreeId: selfCtx.DegreeId, FieldId: selfCtx.FieldId, IsOfficial: selfCtx.IsOfficial != 0, - OppoId: oppoViewerId, + OppoId: (int)oppoViewerId, Seed: seed), OppoInfo: new MatchedOppoInfo( CountryCode: oppoCtx.CountryCode, @@ -44,7 +44,7 @@ public static class ServerBattleFrames DegreeId: oppoCtx.DegreeId, FieldId: oppoCtx.FieldId, IsOfficial: oppoCtx.IsOfficial != 0, - OppoId: selfViewerId, + OppoId: (int)selfViewerId, Seed: seed, OppoDeckCount: oppoCtx.SelfDeckCardIds.Count), SelfDeck: BuildPlayerDeck(selfDeckOrder)), diff --git a/SVSim.BattleNode/Protocol/Bodies/MatchedBody.cs b/SVSim.BattleNode/Protocol/Bodies/MatchedBody.cs index f4021da..8b6a13f 100644 --- a/SVSim.BattleNode/Protocol/Bodies/MatchedBody.cs +++ b/SVSim.BattleNode/Protocol/Bodies/MatchedBody.cs @@ -19,8 +19,8 @@ public sealed record MatchedSelfInfo( [property: JsonPropertyName("fieldId")] int FieldId, [property: JsonPropertyName("isOfficial")] [property: JsonConverter(typeof(NumericBoolJsonConverter))] bool IsOfficial, - [property: JsonPropertyName("oppoId")] long OppoId, - [property: JsonPropertyName("seed")] long Seed); + [property: JsonPropertyName("oppoId")] int OppoId, + [property: JsonPropertyName("seed")] int Seed); public sealed record MatchedOppoInfo( [property: JsonPropertyName("country_code")] string CountryCode, @@ -31,8 +31,8 @@ public sealed record MatchedOppoInfo( [property: JsonPropertyName("fieldId")] int FieldId, [property: JsonPropertyName("isOfficial")] [property: JsonConverter(typeof(NumericBoolJsonConverter))] bool IsOfficial, - [property: JsonPropertyName("oppoId")] long OppoId, - [property: JsonPropertyName("seed")] long Seed, + [property: JsonPropertyName("oppoId")] int OppoId, + [property: JsonPropertyName("seed")] int Seed, [property: JsonPropertyName("oppoDeckCount")] int OppoDeckCount); public sealed record DeckCardRef( diff --git a/SVSim.BattleNode/Sessions/Participants/RealParticipant.cs b/SVSim.BattleNode/Sessions/Participants/RealParticipant.cs index fd2de61..e4c72ab 100644 --- a/SVSim.BattleNode/Sessions/Participants/RealParticipant.cs +++ b/SVSim.BattleNode/Sessions/Participants/RealParticipant.cs @@ -134,7 +134,15 @@ public sealed class RealParticipant : IBattleParticipant, IHasHandshakePhase { var text = Encoding.UTF8.GetString(msg.Value.Bytes); if (text.Length == 0) continue; - var eio = EngineIoFrame.Parse(text); + + EngineIoFrame eio; + try { eio = EngineIoFrame.Parse(text); } + catch (ArgumentException ex) + { + _log.LogWarning(ex, "Dropping unparseable EIO frame from viewer {Vid}", ViewerId); + continue; + } + if (_diagnosticLogging) { _log.LogInformation( @@ -149,7 +157,13 @@ public sealed class RealParticipant : IBattleParticipant, IHasHandshakePhase } if (eio.Type != EngineIoPacketType.Message) continue; - var sio = SocketIoFrame.Parse(eio.Payload); + SocketIoFrame sio; + try { sio = SocketIoFrame.Parse(eio.Payload); } + catch (ArgumentException ex) + { + _log.LogWarning(ex, "Dropping unparseable SIO frame from viewer {Vid}", ViewerId); + continue; + } if (sio.AttachmentCount > 0) { pendingFrame = sio; diff --git a/SVSim.BattleNode/Wire/SocketIoFrame.cs b/SVSim.BattleNode/Wire/SocketIoFrame.cs index 1cd56b1..d3cc359 100644 --- a/SVSim.BattleNode/Wire/SocketIoFrame.cs +++ b/SVSim.BattleNode/Wire/SocketIoFrame.cs @@ -52,7 +52,10 @@ public sealed class SocketIoFrame if (string.IsNullOrEmpty(raw)) throw new ArgumentException("Empty SIO payload", nameof(raw)); - var type = (SocketIoPacketType)(raw[0] - '0'); + var typeChar = raw[0]; + if (typeChar < '0' || typeChar > '6') + throw new ArgumentException($"Invalid SIO type char '{typeChar}'", nameof(raw)); + var type = (SocketIoPacketType)(typeChar - '0'); var cursor = 1; var attachmentCount = 0; @@ -84,7 +87,9 @@ public sealed class SocketIoFrame { var start = cursor; while (cursor < raw.Length && char.IsDigit(raw[cursor])) cursor++; - ackId = int.Parse(raw.AsSpan(start, cursor - start)); + if (!int.TryParse(raw.AsSpan(start, cursor - start), out var parsedAckId)) + throw new ArgumentException("SIO ack-id overflows int32", nameof(raw)); + ackId = parsedAckId; } var argsJson = cursor < raw.Length ? raw.Substring(cursor) : string.Empty; diff --git a/SVSim.UnitTests/BattleNode/Lifecycle/ServerBattleFramesTests.cs b/SVSim.UnitTests/BattleNode/Lifecycle/ServerBattleFramesTests.cs index 90865b0..8bdd2af 100644 --- a/SVSim.UnitTests/BattleNode/Lifecycle/ServerBattleFramesTests.cs +++ b/SVSim.UnitTests/BattleNode/Lifecycle/ServerBattleFramesTests.cs @@ -14,19 +14,19 @@ public class ServerBattleFramesTests { var env = ServerBattleFrames.BuildMatched(FixtureCtx(), FakeOpponentCtx(), selfViewerId: 906243102, oppoViewerId: 847666884, - battleId: "b", seed: 17_548_138L, selfDeckOrder: FixtureCtx().SelfDeckCardIds); + battleId: "b", seed: 17_548_138, selfDeckOrder: FixtureCtx().SelfDeckCardIds); Assert.That(env.Uri, Is.EqualTo(NetworkBattleUri.Matched)); var body = (MatchedBody)env.Body; - Assert.That(body.SelfInfo.OppoId, Is.EqualTo(847666884L)); - Assert.That(body.OppoInfo.OppoId, Is.EqualTo(906243102L)); + Assert.That(body.SelfInfo.OppoId, Is.EqualTo(847666884)); + Assert.That(body.OppoInfo.OppoId, Is.EqualTo(906243102)); Assert.That(env.Bid, Is.EqualTo("b")); } [Test] public void BuildMatched_ContainsThirtyCardSelfDeck() { - var env = ServerBattleFrames.BuildMatched(FixtureCtx(), FakeOpponentCtx(), 1, 2, "b", 17_548_138L, FixtureCtx().SelfDeckCardIds); + var env = ServerBattleFrames.BuildMatched(FixtureCtx(), FakeOpponentCtx(), 1, 2, "b", 17_548_138, FixtureCtx().SelfDeckCardIds); var body = (MatchedBody)env.Body; Assert.That(body.SelfDeck.Count, Is.EqualTo(30)); } @@ -35,7 +35,7 @@ public class ServerBattleFramesTests public void BuildMatched_deck_idxs_pair_1to30_with_context_card_ids() { var draftedDeck = Enumerable.Range(1, 30).Select(i => 200_000_000L + i).ToList(); - var env = ServerBattleFrames.BuildMatched(FixtureCtx(draftedDeck), FakeOpponentCtx(), 1, 2, "b", 17_548_138L, draftedDeck); + var env = ServerBattleFrames.BuildMatched(FixtureCtx(draftedDeck), FakeOpponentCtx(), 1, 2, "b", 17_548_138, draftedDeck); var body = (MatchedBody)env.Body; for (int i = 0; i < 30; i++) @@ -56,7 +56,7 @@ public class ServerBattleFramesTests EmblemId = "888", DegreeId = "777", FieldId = 42, IsOfficial = 1, }; - var env = ServerBattleFrames.BuildMatched(ctx, FakeOpponentCtx(), 1, 2, "b", 17_548_138L, ctx.SelfDeckCardIds); + var env = ServerBattleFrames.BuildMatched(ctx, FakeOpponentCtx(), 1, 2, "b", 17_548_138, ctx.SelfDeckCardIds); var body = (MatchedBody)env.Body; Assert.That(body.SelfInfo.CountryCode, Is.EqualTo("JPN")); diff --git a/SVSim.UnitTests/BattleNode/Lifecycle/TypedBodyWireShapeTests.cs b/SVSim.UnitTests/BattleNode/Lifecycle/TypedBodyWireShapeTests.cs index fe20320..2eb2417 100644 --- a/SVSim.UnitTests/BattleNode/Lifecycle/TypedBodyWireShapeTests.cs +++ b/SVSim.UnitTests/BattleNode/Lifecycle/TypedBodyWireShapeTests.cs @@ -28,7 +28,7 @@ public class TypedBodyWireShapeTests // with "Value cannot be null. Parameter name: source". The prod wire format // emits envelope keys (uri first) before body keys; we must too. var env = ServerBattleFrames.BuildMatched(FixtureCtx(), FakeOpponentCtx(), - selfViewerId: 1, oppoViewerId: 2, battleId: "b", seed: 17_548_138L, + selfViewerId: 1, oppoViewerId: 2, battleId: "b", seed: 17_548_138, selfDeckOrder: FixtureCtx().SelfDeckCardIds); var json = MsgEnvelope.ToJson(env); @@ -48,7 +48,7 @@ public class TypedBodyWireShapeTests { var env = ServerBattleFrames.BuildMatched(FixtureCtx(), FakeOpponentCtx(), selfViewerId: 906243102, oppoViewerId: 847666884, battleId: "597830888107", - seed: 17_548_138L, selfDeckOrder: FixtureCtx().SelfDeckCardIds); + seed: 17_548_138, selfDeckOrder: FixtureCtx().SelfDeckCardIds); var json = MsgEnvelope.ToJson(env); var node = JsonNode.Parse(json)!.AsObject(); diff --git a/SVSim.UnitTests/BattleNode/Protocol/Bodies/MatchedBodyTests.cs b/SVSim.UnitTests/BattleNode/Protocol/Bodies/MatchedBodyTests.cs index 39823b6..c62f1a5 100644 --- a/SVSim.UnitTests/BattleNode/Protocol/Bodies/MatchedBodyTests.cs +++ b/SVSim.UnitTests/BattleNode/Protocol/Bodies/MatchedBodyTests.cs @@ -15,11 +15,11 @@ public class MatchedBodyTests SelfInfo: new MatchedSelfInfo( CountryCode: "KOR", UserName: "Player", SleeveId: "3000011", EmblemId: "701441011", DegreeId: "300003", FieldId: 43, - IsOfficial: false, OppoId: 847666884L, Seed: 17_548_138L), + IsOfficial: false, OppoId: 847666884, Seed: 17_548_138), OppoInfo: new MatchedOppoInfo( CountryCode: "JPN", UserName: "Opponent", SleeveId: "704141010", EmblemId: "400001100", DegreeId: "120027", FieldId: 5, - IsOfficial: false, OppoId: 906243102L, Seed: 17_548_138L, OppoDeckCount: 30), + IsOfficial: false, OppoId: 906243102, Seed: 17_548_138, OppoDeckCount: 30), SelfDeck: new[] { new DeckCardRef(Idx: 1, CardId: 100011010L) }); var node = (JsonObject)JsonSerializer.SerializeToNode(body)!; @@ -32,16 +32,16 @@ public class MatchedBodyTests Assert.That(selfInfo["degreeId"]!.GetValue(), Is.EqualTo("300003")); Assert.That(selfInfo["fieldId"]!.GetValue(), Is.EqualTo(43)); Assert.That(selfInfo["isOfficial"]!.GetValue(), Is.EqualTo(0)); - Assert.That(selfInfo["oppoId"]!.GetValue(), Is.EqualTo(847666884L)); - Assert.That(selfInfo["seed"]!.GetValue(), Is.EqualTo(17_548_138L)); + Assert.That(selfInfo["oppoId"]!.GetValue(), Is.EqualTo(847666884)); + Assert.That(selfInfo["seed"]!.GetValue(), Is.EqualTo(17_548_138)); } [Test] public void OppoInfo_HasOppoDeckCount_OnTheWire() { var body = new MatchedBody( - SelfInfo: new MatchedSelfInfo("KOR","P","s","e","d",0,false,1L,1L), - OppoInfo: new MatchedOppoInfo("JPN","O","s","e","d",0,false,1L,1L, OppoDeckCount: 30), + SelfInfo: new MatchedSelfInfo("KOR","P","s","e","d",0,false,1,1), + OppoInfo: new MatchedOppoInfo("JPN","O","s","e","d",0,false,1,1, OppoDeckCount: 30), SelfDeck: System.Array.Empty()); var node = (JsonObject)JsonSerializer.SerializeToNode(body)!; @@ -54,8 +54,8 @@ public class MatchedBodyTests public void SelfInfo_DoesNotHaveOppoDeckCount_OnTheWire() { var body = new MatchedBody( - SelfInfo: new MatchedSelfInfo("KOR","P","s","e","d",0,false,1L,1L), - OppoInfo: new MatchedOppoInfo("JPN","O","s","e","d",0,false,1L,1L,30), + SelfInfo: new MatchedSelfInfo("KOR","P","s","e","d",0,false,1,1), + OppoInfo: new MatchedOppoInfo("JPN","O","s","e","d",0,false,1,1,30), SelfDeck: System.Array.Empty()); var node = (JsonObject)JsonSerializer.SerializeToNode(body)!; @@ -68,8 +68,8 @@ public class MatchedBodyTests public void ResultCode_DefaultsToOne_OnConstruction() { var body = new MatchedBody( - SelfInfo: new MatchedSelfInfo("KOR","P","s","e","d",0,false,1L,1L), - OppoInfo: new MatchedOppoInfo("JPN","O","s","e","d",0,false,1L,1L,30), + SelfInfo: new MatchedSelfInfo("KOR","P","s","e","d",0,false,1,1), + OppoInfo: new MatchedOppoInfo("JPN","O","s","e","d",0,false,1,1,30), SelfDeck: System.Array.Empty()); Assert.That(body.ResultCode, Is.EqualTo(1)); @@ -81,8 +81,8 @@ public class MatchedBodyTests public void SelfDeck_SerializesAsArray_WithIdxAndCardIdKeys() { var body = new MatchedBody( - SelfInfo: new MatchedSelfInfo("KOR","P","s","e","d",0,false,1L,1L), - OppoInfo: new MatchedOppoInfo("JPN","O","s","e","d",0,false,1L,1L,30), + SelfInfo: new MatchedSelfInfo("KOR","P","s","e","d",0,false,1,1), + OppoInfo: new MatchedOppoInfo("JPN","O","s","e","d",0,false,1,1,30), SelfDeck: new[] { new DeckCardRef(Idx: 1, CardId: 100011010L), diff --git a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs index b7b499d..1615ad1 100644 --- a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs +++ b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs @@ -99,8 +99,8 @@ public class BattleSessionDispatchTests var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitBattle)); var body = (MatchedBody)routes[0].Frame.Body; - Assert.That(body.SelfInfo.Seed, Is.EqualTo((long)BattleSeeds.Stable(s.MasterSeed))); - Assert.That(body.OppoInfo.Seed, Is.EqualTo((long)BattleSeeds.Stable(s.MasterSeed))); + Assert.That(body.SelfInfo.Seed, Is.EqualTo(BattleSeeds.Stable(s.MasterSeed))); + Assert.That(body.OppoInfo.Seed, Is.EqualTo(BattleSeeds.Stable(s.MasterSeed))); } [Test] diff --git a/SVSim.UnitTests/BattleNode/Wire/SocketIoFrameTests.cs b/SVSim.UnitTests/BattleNode/Wire/SocketIoFrameTests.cs index f0e9d50..a147cdd 100644 --- a/SVSim.UnitTests/BattleNode/Wire/SocketIoFrameTests.cs +++ b/SVSim.UnitTests/BattleNode/Wire/SocketIoFrameTests.cs @@ -125,4 +125,17 @@ public class SocketIoFrameTests // The event name must be JSON-escaped: each " becomes \", and the literal \ becomes \\. Assert.That(text, Does.Contain("\"weird \\\"name\\\" with \\\\ backslash\"")); } + + [Test] + public void Parse_InvalidTypeChar_Throws() + { + var ex = Assert.Throws(() => SocketIoFrame.Parse("9[\"msg\"]")); + Assert.That(ex!.Message, Does.Contain("Invalid SIO type char")); + } + + [Test] + public void Parse_OverflowingAckId_Throws() + { + Assert.Throws(() => SocketIoFrame.Parse("2999999999999[\"msg\"]")); + } }