From a2d30cf610ba777883f0faf1c7b547f89c0f15d0 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Thu, 4 Jan 2024 01:31:34 -0600 Subject: [PATCH 1/8] Add MSDP Support (WIP) --- .../Interpreters/TelnetMSDPInterpreter.cs | 207 ++++++++++++++++++ .../Interpreters/TelnetMSSPInterpreter.cs | 36 +-- TelnetNegotiationCore/Models/MSDPConfig.cs | 6 + TelnetNegotiationCore/Models/State.cs | 13 +- 4 files changed, 243 insertions(+), 19 deletions(-) create mode 100644 TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs create mode 100644 TelnetNegotiationCore/Models/MSDPConfig.cs diff --git a/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs new file mode 100644 index 0000000..8ea4f7a --- /dev/null +++ b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Dynamic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using MoreLinq; +using OneOf; +using Stateless; +using TelnetNegotiationCore.Models; + +namespace TelnetNegotiationCore.Interpreters +{ + /// + /// Implements http://www.faqs.org/rfcs/rfc1073.html + /// + /// + /// TODO: Implement Client Side + /// + public partial class TelnetInterpreter + { + private Func _MSDPConfig; + + private List _currentMSDPInfo; + + public Func SignalOnMSDPAsync { get; init; } + + /// + /// Mud Server Status Protocol will provide information to the requestee about the server's contents. + /// + /// The state machine. + /// Itself + private StateMachine SetupMSDPNegotiation(StateMachine tsm) + { + if (Mode == TelnetMode.Server) + { + tsm.Configure(State.Do) + .Permit(Trigger.MSDP, State.DoMSDP); + + tsm.Configure(State.Dont) + .Permit(Trigger.MSDP, State.DontMSDP); + + tsm.Configure(State.DoMSDP) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnDoMSDPAsync); + + tsm.Configure(State.DontMSDP) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.Debug("Connection: {ConnectionState}", "Client won't do MSDP - do nothing")); + + RegisterInitialWilling(WillingMSDPAsync); + } + else + { + tsm.Configure(State.Willing) + .Permit(Trigger.MSDP, State.WillMSDP); + + tsm.Configure(State.Refusing) + .Permit(Trigger.MSDP, State.WontMSDP); + + tsm.Configure(State.WillMSDP) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnWillMSDPAsync); + + tsm.Configure(State.WontMSDP) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.Debug("Connection: {ConnectionState}", "Server won't do MSDP - do nothing")); + + tsm.Configure(State.SubNegotiation) + .Permit(Trigger.MSDP, State.AlmostNegotiatingMSDP) + .OnEntry(() => + { + _currentMSDPInfo = []; + }); + + tsm.Configure(State.AlmostNegotiatingMSDP) + .Permit(Trigger.MSDP_VAR, State.EvaluatingMSDP) + .Permit(Trigger.MSDP_VAL, State.EvaluatingMSDP) + .Permit(Trigger.MSDP_ARRAY_OPEN, State.EvaluatingMSDP) + .Permit(Trigger.MSDP_ARRAY_CLOSE, State.EvaluatingMSDP) + .Permit(Trigger.MSDP_TABLE_OPEN, State.EvaluatingMSDP) + .Permit(Trigger.MSDP_TABLE_CLOSE, State.EvaluatingMSDP); + + tsm.Configure(State.EvaluatingMSDP) + .Permit(Trigger.IAC, State.EscapingMSDP); + + tsm.Configure(State.EscapingMSDP) + .Permit(Trigger.IAC, State.EvaluatingMSDP) + .Permit(Trigger.SE, State.CompletingMSDP); + + tsm.Configure(State.CompletingMSDP) + .SubstateOf(State.Accepting) + .OnEntryAsync(ReadMSDPValues); + + TriggerHelper.ForAllTriggersExcept([Trigger.IAC], t => + tsm.Configure(State.EvaluatingMSDP).OnEntryFrom(ParameterizedTrigger(t), CaptureMSDPValue).PermitReentry(t)); + } + + return tsm; + } + + private void CaptureMSDPValue(OneOf b) + { + _currentMSDPInfo.Add(b.AsT0); + } + + private async Task ReadMSDPValues() + { + dynamic root = new ExpandoObject(); + var array = _currentMSDPInfo.Skip(1).ToImmutableArray(); + + MSDPScan(root, array, Trigger.MSDP_VAR); + await Task.CompletedTask; + } + + private dynamic MSDPScan(dynamic root, ImmutableArray array, Trigger type) + { + if (array.Length == 0) return root; + + if (type == Trigger.MSDP_VAR) + { + var variableName = array.TakeUntil(x => x is (byte)Trigger.MSDP_VAL).ToArray(); + ((IDictionary)root).Add(CurrentEncoding.GetString(variableName), + MSDPScan(root, array.SkipUntil(x => x is (byte)Trigger.MSDP_VAL).ToImmutableArray(), Trigger.MSDP_VAL)); + } + else if (type == Trigger.MSDP_VAL) + { + var nextType = array.FirstOrDefault(x => x is + (byte)Trigger.MSDP_ARRAY_OPEN or + (byte)Trigger.MSDP_TABLE_OPEN or + (byte)Trigger.MSDP_ARRAY_CLOSE or + (byte)Trigger.MSDP_TABLE_CLOSE or + (byte)Trigger.MSDP_VAL); + dynamic result = root; + + if (nextType == default) // We have hit the end. + { + result = CurrentEncoding.GetString(array.ToArray()); + } + else if (nextType is (byte)Trigger.MSDP_VAL) + { + var value = array.TakeWhile(x => x != nextType); + var startOfRest = array.SkipUntil(x => x is (byte)Trigger.MSDP_VAL).ToImmutableArray(); + result = MSDPScan(((ImmutableList)root).Add(CurrentEncoding.GetString(value.ToArray())), startOfRest, Trigger.MSDP_VAL); + } + else if (nextType is (byte)Trigger.MSDP_ARRAY_OPEN) + { + var startOfArray = array.SkipUntil(x => x is (byte)Trigger.MSDP_ARRAY_OPEN).ToImmutableArray(); + result = MSDPScan(root, startOfArray, Trigger.MSDP_ARRAY_OPEN); + } + else if (nextType is (byte)Trigger.MSDP_TABLE_OPEN) + { + var startOfTable = array.SkipUntil(x => x is (byte)Trigger.MSDP_TABLE_OPEN).ToImmutableArray(); + result = MSDPScan(root, startOfTable, Trigger.MSDP_ARRAY_OPEN); + } + else if (nextType is (byte)Trigger.MSDP_ARRAY_CLOSE) + { + result = root; + } + else if (nextType is (byte)Trigger.MSDP_TABLE_CLOSE) + { + result = root; + } + + return result; + } + else if (type == Trigger.MSDP_ARRAY_OPEN) + { + return MSDPScan(ImmutableList.Empty, array.Skip(1).ToImmutableArray(), Trigger.MSDP_VAL); + } + else if (type == Trigger.MSDP_TABLE_OPEN) + { + return MSDPScan(new ExpandoObject(), array.Skip(1).ToImmutableArray(), Trigger.MSDP_VAR); + } + + return root; + } + + /// + /// Announce we do MSDP negotiation to the client. + /// + private async Task WillingMSDPAsync() + { + _Logger.Debug("Connection: {ConnectionState}", "Announcing willingness to MSDP!"); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.MSDP]); + } + + /// + /// Announce the MSDP we support to the client after getting a Do. + /// + private async Task OnDoMSDPAsync(StateMachine.Transition _) + { + _Logger.Debug("Connection: {ConnectionState}", "Client will do MSDP output"); + await Task.CompletedTask; + } + + /// + /// Announce we do MSDP negotiation to the server. + /// + private async Task OnWillMSDPAsync() + { + _Logger.Debug("Connection: {ConnectionState}", "Announcing willingness to MSDP!"); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.MSDP]); + } + } +} diff --git a/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs index 15926dd..2a97a5a 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs @@ -15,10 +15,10 @@ public partial class TelnetInterpreter { private Func _msspConfig = () => new(); - private List _currentVariable; - private List> _currentValueList; - private List _currentValue; - private List> _currentVariableList; + private List _currentMSSPVariable; + private List _currentMSSPValue; + private List> _currentMSSPVariableList; + private List> _currentMSSPValueList; public Func SignalOnMSSPAsync { get; init; } @@ -73,10 +73,10 @@ private StateMachine SetupMSSPNegotiation(StateMachine { - _currentValue = []; - _currentVariable = []; - _currentValueList = []; - _currentVariableList = []; + _currentMSDPValue = []; + _currentMSDPInfo = []; + _currentMSDPValueList = []; + _currentMSDPVariableList = []; }); tsm.Configure(State.AlmostNegotiatingMSSP) @@ -117,18 +117,18 @@ private StateMachine SetupMSSPNegotiation(StateMachine(); + _currentMSDPValueList.Add(_currentMSDPValue); + _currentMSDPValue = new List(); } private void RegisterMSSPVar() { - if (!_currentVariable.Any()) return; + if (!_currentMSDPInfo.Any()) return; - _currentVariableList.Add(_currentVariable); - _currentVariable = new List(); + _currentMSDPVariableList.Add(_currentMSDPInfo); + _currentMSDPInfo = new List(); } private async Task ReadMSSPValues() @@ -136,8 +136,8 @@ private async Task ReadMSSPValues() RegisterMSSPVal(); RegisterMSSPVar(); - var grouping = _currentVariableList - .Zip(_currentValueList) + var grouping = _currentMSDPVariableList + .Zip(_currentMSDPValueList) .GroupBy(x => CurrentEncoding.GetString(x.First.ToArray())); foreach (var group in grouping) @@ -205,14 +205,14 @@ private void CaptureMSSPVariable(OneOf b) { // We could increment here based on having switched... Somehow? // We need a better state tracking for this, to indicate the transition. - _currentVariable.Add(b.AsT0); + _currentMSDPInfo.Add(b.AsT0); } private void CaptureMSSPValue(OneOf b) { // We could increment here based on having switched... Somehow? // We need a better state tracking for this, to indicate the transition. - _currentValue.Add(b.AsT0); + _currentMSDPValue.Add(b.AsT0); } /// diff --git a/TelnetNegotiationCore/Models/MSDPConfig.cs b/TelnetNegotiationCore/Models/MSDPConfig.cs new file mode 100644 index 0000000..4618f19 --- /dev/null +++ b/TelnetNegotiationCore/Models/MSDPConfig.cs @@ -0,0 +1,6 @@ +namespace TelnetNegotiationCore.Models +{ + public class MSDPConfig + { + } +} \ No newline at end of file diff --git a/TelnetNegotiationCore/Models/State.cs b/TelnetNegotiationCore/Models/State.cs index 8e68fe8..6b00673 100644 --- a/TelnetNegotiationCore/Models/State.cs +++ b/TelnetNegotiationCore/Models/State.cs @@ -91,7 +91,18 @@ public enum State : sbyte AlmostNegotiatingGMCP, EvaluatingGMCPValue, EscapingGMCPValue, - CompletingGMCPValue + CompletingGMCPValue, #endregion GMCP Negotiation + #region MSDP Negotiation + DontMSDP, + DoMSDP, + WillMSDP, + WontMSDP, + NegotiatingMSDP, + EvaluatingMSDP, + CompletingMSDP, + AlmostNegotiatingMSDP, + EscapingMSDP + #endregion MSDP Negotiation } } From 6c177ec886be62c700e816e1c60a679ae07ca90e Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Thu, 4 Jan 2024 01:34:48 -0600 Subject: [PATCH 2/8] Fix build. --- .../KestrelMockServer.cs | 2 +- .../Interpreters/TelnetMSDPInterpreter.cs | 2 -- .../Interpreters/TelnetMSSPInterpreter.cs | 30 +++++++++---------- .../TelnetTerminalTypeInterpreter.cs | 2 +- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/TelnetNegotiationCore.TestServer/KestrelMockServer.cs b/TelnetNegotiationCore.TestServer/KestrelMockServer.cs index c0949d5..666c621 100644 --- a/TelnetNegotiationCore.TestServer/KestrelMockServer.cs +++ b/TelnetNegotiationCore.TestServer/KestrelMockServer.cs @@ -14,7 +14,7 @@ namespace TelnetNegotiationCore.TestServer { public class KestrelMockServer : ConnectionHandler { - private ILogger _Logger; + private readonly ILogger _Logger; public KestrelMockServer(ILogger logger = null): base() { diff --git a/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs index 8ea4f7a..41bef4a 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs @@ -20,8 +20,6 @@ namespace TelnetNegotiationCore.Interpreters /// public partial class TelnetInterpreter { - private Func _MSDPConfig; - private List _currentMSDPInfo; public Func SignalOnMSDPAsync { get; init; } diff --git a/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs index 2a97a5a..a624a22 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs @@ -16,9 +16,9 @@ public partial class TelnetInterpreter private Func _msspConfig = () => new(); private List _currentMSSPVariable; + private List> _currentMSSPValueList; private List _currentMSSPValue; private List> _currentMSSPVariableList; - private List> _currentMSSPValueList; public Func SignalOnMSSPAsync { get; init; } @@ -73,10 +73,10 @@ private StateMachine SetupMSSPNegotiation(StateMachine { - _currentMSDPValue = []; - _currentMSDPInfo = []; - _currentMSDPValueList = []; - _currentMSDPVariableList = []; + _currentMSSPValue = []; + _currentMSSPVariable = []; + _currentMSSPValueList = []; + _currentMSSPVariableList = []; }); tsm.Configure(State.AlmostNegotiatingMSSP) @@ -117,18 +117,18 @@ private StateMachine SetupMSSPNegotiation(StateMachine(); + _currentMSSPValueList.Add(_currentMSSPValue); + _currentMSSPValue = []; } private void RegisterMSSPVar() { - if (!_currentMSDPInfo.Any()) return; + if (_currentMSSPVariable.Count == 0) return; - _currentMSDPVariableList.Add(_currentMSDPInfo); - _currentMSDPInfo = new List(); + _currentMSSPVariableList.Add(_currentMSSPVariable); + _currentMSSPVariable = []; } private async Task ReadMSSPValues() @@ -136,8 +136,8 @@ private async Task ReadMSSPValues() RegisterMSSPVal(); RegisterMSSPVar(); - var grouping = _currentMSDPVariableList - .Zip(_currentMSDPValueList) + var grouping = _currentMSSPVariableList + .Zip(_currentMSSPValueList) .GroupBy(x => CurrentEncoding.GetString(x.First.ToArray())); foreach (var group in grouping) @@ -205,14 +205,14 @@ private void CaptureMSSPVariable(OneOf b) { // We could increment here based on having switched... Somehow? // We need a better state tracking for this, to indicate the transition. - _currentMSDPInfo.Add(b.AsT0); + _currentMSSPVariable.Add(b.AsT0); } private void CaptureMSSPValue(OneOf b) { // We could increment here based on having switched... Somehow? // We need a better state tracking for this, to indicate the transition. - _currentMSDPValue.Add(b.AsT0); + _currentMSSPValue.Add(b.AsT0); } /// diff --git a/TelnetNegotiationCore/Interpreters/TelnetTerminalTypeInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetTerminalTypeInterpreter.cs index 225b0ba..27e0636 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetTerminalTypeInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetTerminalTypeInterpreter.cs @@ -22,7 +22,7 @@ public partial class TelnetInterpreter /// /// A list of terminal types for this connection. /// - public ImmutableList TerminalTypes { get; private set; } = ImmutableList.Empty; + public ImmutableList TerminalTypes { get; private set; } = []; /// /// The current selected Terminal Type. Use RequestTerminalTypeAsync if you want the client to switch to the next mode. From 3bfae1cb79bd07623b370b69aa3e9bdb497ba8ff Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Thu, 4 Jan 2024 13:59:28 -0600 Subject: [PATCH 3/8] C# doesn't support Tail Recursion Optimization. So time to bring out F#. --- .../MSDPLibrary.fs | 49 +++++++++++++ .../TelnetNegotiationCore.Functional.fsproj | 12 ++++ .../CreateDotGraph.cs | 1 + .../TelnetNegotiationCore.UnitTests.csproj | 2 + TelnetNegotiationCore.sln | 6 ++ .../Interpreters/TelnetMSDPInterpreter.cs | 72 ++----------------- .../TelnetNegotiationCore.csproj | 4 ++ 7 files changed, 79 insertions(+), 67 deletions(-) create mode 100644 TelnetNegotiationCore.Functional/MSDPLibrary.fs create mode 100644 TelnetNegotiationCore.Functional/TelnetNegotiationCore.Functional.fsproj diff --git a/TelnetNegotiationCore.Functional/MSDPLibrary.fs b/TelnetNegotiationCore.Functional/MSDPLibrary.fs new file mode 100644 index 0000000..d64636d --- /dev/null +++ b/TelnetNegotiationCore.Functional/MSDPLibrary.fs @@ -0,0 +1,49 @@ +namespace TelnetNegotiationCore.Functional + +open System.Text + +type Trigger = + | NULL = 0uy + | MSDP_VAR = 1uy + | MSDP_VAL = 2uy + | MSDP_TABLE_OPEN = 3uy + | MSDP_TABLE_CLOSE = 4uy + | MSDP_ARRAY_OPEN = 5uy + | MSDP_ARRAY_CLOSE = 6uy + +module MSDPLibrary = + let rec MSDPScan (root: obj, array: seq, type_: Trigger, encoding : Encoding) = + if Seq.length(array) = 0 then root + else + match type_ with + | Trigger.MSDP_VAR -> + (root :?> Map).Add(encoding.GetString(array |> Seq.takeWhile(fun x -> x <> byte Trigger.MSDP_VAL) |> Array.ofSeq), MSDPScan(root, array |> Seq.skipWhile(fun x -> x <> byte Trigger.MSDP_VAL) |> Seq.skip(1), Trigger.MSDP_VAL,encoding)) + | Trigger.MSDP_VAL -> + let nextType = + try + array |> Seq.find(fun x -> + x = byte Trigger.MSDP_ARRAY_OPEN || + x = byte Trigger.MSDP_TABLE_OPEN || + x = byte Trigger.MSDP_ARRAY_CLOSE || + x = byte Trigger.MSDP_TABLE_CLOSE || + x = byte Trigger.MSDP_VAL) + with + | :? System.Collections.Generic.KeyNotFoundException -> 0uy + + match LanguagePrimitives.EnumOfValue nextType : Trigger with + | Trigger.NULL -> + encoding.GetString(array |> Array.ofSeq) + | Trigger.MSDP_VAL -> + MSDPScan((root :?> List) @ [encoding.GetString(array |> Seq.takeWhile(fun x -> x <> nextType) |> Array.ofSeq)], array |> Seq.skipWhile(fun x -> x <> byte Trigger.MSDP_VAL) |> Seq.skip(1), Trigger.MSDP_VAL, encoding) + | Trigger.MSDP_TABLE_CLOSE -> + (root :?> List) @ [encoding.GetString(array |> Seq.takeWhile(fun x -> x <> nextType) |> Array.ofSeq)] + | Trigger.MSDP_ARRAY_OPEN -> + MSDPScan(root, array |> Seq.skipWhile(fun x -> x <> byte Trigger.MSDP_ARRAY_OPEN) |> Seq.skip(1), Trigger.MSDP_ARRAY_OPEN,encoding) + | Trigger.MSDP_TABLE_OPEN -> + MSDPScan(root, array |> Seq.skipWhile(fun x -> x <> byte Trigger.MSDP_TABLE_OPEN) |> Seq.skip(1), Trigger.MSDP_TABLE_OPEN,encoding) + | _ -> root + | Trigger.MSDP_ARRAY_OPEN -> + MSDPScan(List.Empty, array |> Seq.skip(1), Trigger.MSDP_VAL, encoding) + | Trigger.MSDP_TABLE_OPEN -> + MSDPScan(Map [], array |> Seq.skip(1), Trigger.MSDP_VAR, encoding) + | _ -> failwith "Failure in MSDPScan." \ No newline at end of file diff --git a/TelnetNegotiationCore.Functional/TelnetNegotiationCore.Functional.fsproj b/TelnetNegotiationCore.Functional/TelnetNegotiationCore.Functional.fsproj new file mode 100644 index 0000000..78d27df --- /dev/null +++ b/TelnetNegotiationCore.Functional/TelnetNegotiationCore.Functional.fsproj @@ -0,0 +1,12 @@ + + + + net8.0 + true + + + + + + + diff --git a/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs b/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs index fcd9566..30e4e48 100644 --- a/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs +++ b/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs @@ -3,6 +3,7 @@ using Stateless.Graph; using System.Collections.Generic; using System.IO; +using System.Numerics; using System.Text; using System.Threading.Tasks; using TelnetNegotiationCore.Interpreters; diff --git a/TelnetNegotiationCore.UnitTests/TelnetNegotiationCore.UnitTests.csproj b/TelnetNegotiationCore.UnitTests/TelnetNegotiationCore.UnitTests.csproj index 74e1576..0c50924 100644 --- a/TelnetNegotiationCore.UnitTests/TelnetNegotiationCore.UnitTests.csproj +++ b/TelnetNegotiationCore.UnitTests/TelnetNegotiationCore.UnitTests.csproj @@ -4,6 +4,8 @@ net8.0 false + + AnyCPU diff --git a/TelnetNegotiationCore.sln b/TelnetNegotiationCore.sln index c7c48a7..d450995 100644 --- a/TelnetNegotiationCore.sln +++ b/TelnetNegotiationCore.sln @@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelnetNegotiationCore.UnitT EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelnetNegotiationCore.TestClient", "TelnetNegotiationCore.TestClient\TelnetNegotiationCore.TestClient.csproj", "{E1A76C78-8246-472F-824E-9F7F9B3778B7}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TelnetNegotiationCore.Functional", "TelnetNegotiationCore.Functional\TelnetNegotiationCore.Functional.fsproj", "{0307D0B8-AAA7-40C8-9E3E-34BE64F93BB7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,6 +43,10 @@ Global {E1A76C78-8246-472F-824E-9F7F9B3778B7}.Debug|Any CPU.Build.0 = Debug|Any CPU {E1A76C78-8246-472F-824E-9F7F9B3778B7}.Release|Any CPU.ActiveCfg = Release|Any CPU {E1A76C78-8246-472F-824E-9F7F9B3778B7}.Release|Any CPU.Build.0 = Release|Any CPU + {0307D0B8-AAA7-40C8-9E3E-34BE64F93BB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0307D0B8-AAA7-40C8-9E3E-34BE64F93BB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0307D0B8-AAA7-40C8-9E3E-34BE64F93BB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0307D0B8-AAA7-40C8-9E3E-34BE64F93BB7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs index 41bef4a..b6370b1 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; +using Microsoft.FSharp.Collections; using MoreLinq; using OneOf; using Stateless; @@ -105,76 +106,13 @@ private void CaptureMSDPValue(OneOf b) private async Task ReadMSDPValues() { - dynamic root = new ExpandoObject(); - var array = _currentMSDPInfo.Skip(1).ToImmutableArray(); - - MSDPScan(root, array, Trigger.MSDP_VAR); + Functional.MSDPLibrary.MSDPScan(new FSharpMap(Enumerable.Empty>()), + _currentMSDPInfo.Skip(1), + Functional.Trigger.MSDP_VAL, + CurrentEncoding); await Task.CompletedTask; } - private dynamic MSDPScan(dynamic root, ImmutableArray array, Trigger type) - { - if (array.Length == 0) return root; - - if (type == Trigger.MSDP_VAR) - { - var variableName = array.TakeUntil(x => x is (byte)Trigger.MSDP_VAL).ToArray(); - ((IDictionary)root).Add(CurrentEncoding.GetString(variableName), - MSDPScan(root, array.SkipUntil(x => x is (byte)Trigger.MSDP_VAL).ToImmutableArray(), Trigger.MSDP_VAL)); - } - else if (type == Trigger.MSDP_VAL) - { - var nextType = array.FirstOrDefault(x => x is - (byte)Trigger.MSDP_ARRAY_OPEN or - (byte)Trigger.MSDP_TABLE_OPEN or - (byte)Trigger.MSDP_ARRAY_CLOSE or - (byte)Trigger.MSDP_TABLE_CLOSE or - (byte)Trigger.MSDP_VAL); - dynamic result = root; - - if (nextType == default) // We have hit the end. - { - result = CurrentEncoding.GetString(array.ToArray()); - } - else if (nextType is (byte)Trigger.MSDP_VAL) - { - var value = array.TakeWhile(x => x != nextType); - var startOfRest = array.SkipUntil(x => x is (byte)Trigger.MSDP_VAL).ToImmutableArray(); - result = MSDPScan(((ImmutableList)root).Add(CurrentEncoding.GetString(value.ToArray())), startOfRest, Trigger.MSDP_VAL); - } - else if (nextType is (byte)Trigger.MSDP_ARRAY_OPEN) - { - var startOfArray = array.SkipUntil(x => x is (byte)Trigger.MSDP_ARRAY_OPEN).ToImmutableArray(); - result = MSDPScan(root, startOfArray, Trigger.MSDP_ARRAY_OPEN); - } - else if (nextType is (byte)Trigger.MSDP_TABLE_OPEN) - { - var startOfTable = array.SkipUntil(x => x is (byte)Trigger.MSDP_TABLE_OPEN).ToImmutableArray(); - result = MSDPScan(root, startOfTable, Trigger.MSDP_ARRAY_OPEN); - } - else if (nextType is (byte)Trigger.MSDP_ARRAY_CLOSE) - { - result = root; - } - else if (nextType is (byte)Trigger.MSDP_TABLE_CLOSE) - { - result = root; - } - - return result; - } - else if (type == Trigger.MSDP_ARRAY_OPEN) - { - return MSDPScan(ImmutableList.Empty, array.Skip(1).ToImmutableArray(), Trigger.MSDP_VAL); - } - else if (type == Trigger.MSDP_TABLE_OPEN) - { - return MSDPScan(new ExpandoObject(), array.Skip(1).ToImmutableArray(), Trigger.MSDP_VAR); - } - - return root; - } - /// /// Announce we do MSDP negotiation to the client. /// diff --git a/TelnetNegotiationCore/TelnetNegotiationCore.csproj b/TelnetNegotiationCore/TelnetNegotiationCore.csproj index f020ffa..470a225 100644 --- a/TelnetNegotiationCore/TelnetNegotiationCore.csproj +++ b/TelnetNegotiationCore/TelnetNegotiationCore.csproj @@ -53,4 +53,8 @@ + + + + From 754e2575348cba27b434491d06af7eb4e6b84ca6 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Thu, 4 Jan 2024 23:55:48 -0600 Subject: [PATCH 4/8] F# Recursion fixed. Now to move to transforming this into Tail Recursion. --- .../MSDPLibrary.fs | 90 ++++++++++--------- TelnetNegotiationCore.UnitTests/BaseTest.cs | 18 ++++ .../CHARSETTests.cs | 9 +- .../CreateDotGraph.cs | 2 +- TelnetNegotiationCore.UnitTests/MSDPTests.cs | 82 +++++++++++++++++ TelnetNegotiationCore.UnitTests/TTypeTests.cs | 8 -- .../Interpreters/TelnetMSDPInterpreter.cs | 17 ++-- 7 files changed, 156 insertions(+), 70 deletions(-) create mode 100644 TelnetNegotiationCore.UnitTests/BaseTest.cs create mode 100644 TelnetNegotiationCore.UnitTests/MSDPTests.cs diff --git a/TelnetNegotiationCore.Functional/MSDPLibrary.fs b/TelnetNegotiationCore.Functional/MSDPLibrary.fs index d64636d..4eb68e5 100644 --- a/TelnetNegotiationCore.Functional/MSDPLibrary.fs +++ b/TelnetNegotiationCore.Functional/MSDPLibrary.fs @@ -2,48 +2,54 @@ open System.Text -type Trigger = - | NULL = 0uy - | MSDP_VAR = 1uy - | MSDP_VAL = 2uy - | MSDP_TABLE_OPEN = 3uy - | MSDP_TABLE_CLOSE = 4uy - | MSDP_ARRAY_OPEN = 5uy - | MSDP_ARRAY_CLOSE = 6uy - module MSDPLibrary = - let rec MSDPScan (root: obj, array: seq, type_: Trigger, encoding : Encoding) = - if Seq.length(array) = 0 then root + type Trigger = + | NULL = 0uy + | MSDP_VAR = 1uy + | MSDP_VAL = 2uy + | MSDP_TABLE_OPEN = 3uy + | MSDP_TABLE_CLOSE = 4uy + | MSDP_ARRAY_OPEN = 5uy + | MSDP_ARRAY_CLOSE = 6uy + + let rec private MSDPScanRec (root: obj, array: seq, encoding : Encoding) = + if Seq.length(array) = 0 then (root, array) else - match type_ with - | Trigger.MSDP_VAR -> - (root :?> Map).Add(encoding.GetString(array |> Seq.takeWhile(fun x -> x <> byte Trigger.MSDP_VAL) |> Array.ofSeq), MSDPScan(root, array |> Seq.skipWhile(fun x -> x <> byte Trigger.MSDP_VAL) |> Seq.skip(1), Trigger.MSDP_VAL,encoding)) - | Trigger.MSDP_VAL -> - let nextType = - try - array |> Seq.find(fun x -> - x = byte Trigger.MSDP_ARRAY_OPEN || - x = byte Trigger.MSDP_TABLE_OPEN || - x = byte Trigger.MSDP_ARRAY_CLOSE || - x = byte Trigger.MSDP_TABLE_CLOSE || - x = byte Trigger.MSDP_VAL) - with - | :? System.Collections.Generic.KeyNotFoundException -> 0uy + match array |> Seq.head with + | 1uy -> + let key = encoding.GetString(array |> Seq.skip(1) |> Seq.takeWhile(fun x -> x <> byte Trigger.MSDP_VAL) |> Array.ofSeq) + let (calculatedValue, leftoverArray) = MSDPScanRec(root, array |> Seq.skip(1) |> Seq.skipWhile(fun x -> x <> byte Trigger.MSDP_VAL), encoding) + MSDPScanRec((root :?> Map).Add(key, calculatedValue), leftoverArray, encoding) + | 2uy -> + if root :? Map then + MSDPScanRec(Map [], array |> Seq.skip(1), encoding) + elif root :? List then + let (calculatedValue, leftoverArray) = MSDPScanRec(Map [], array |> Seq.skip(1), encoding) + MSDPScanRec((root :?> List) @ [calculatedValue], leftoverArray, encoding) + else + MSDPScanRec(root, array |> Seq.skip(1), encoding) + | 3uy -> + MSDPScanRec(Map [], array |> Seq.skip(1), encoding) + | 4uy -> + ( + root, + array |> Seq.skip(1) + ) + | 5uy -> + MSDPScanRec(List.Empty, array |> Seq.skip(1), encoding) + | 6uy -> + ( + root, + array |> Seq.skip(1) + ) + | _ -> + let result = encoding.GetString(array |> Seq.takeWhile(fun x -> x > byte Trigger.MSDP_ARRAY_CLOSE) |> Array.ofSeq) + ( + result, + array |> Seq.skipWhile(fun x -> x > byte Trigger.MSDP_ARRAY_CLOSE) + ) - match LanguagePrimitives.EnumOfValue nextType : Trigger with - | Trigger.NULL -> - encoding.GetString(array |> Array.ofSeq) - | Trigger.MSDP_VAL -> - MSDPScan((root :?> List) @ [encoding.GetString(array |> Seq.takeWhile(fun x -> x <> nextType) |> Array.ofSeq)], array |> Seq.skipWhile(fun x -> x <> byte Trigger.MSDP_VAL) |> Seq.skip(1), Trigger.MSDP_VAL, encoding) - | Trigger.MSDP_TABLE_CLOSE -> - (root :?> List) @ [encoding.GetString(array |> Seq.takeWhile(fun x -> x <> nextType) |> Array.ofSeq)] - | Trigger.MSDP_ARRAY_OPEN -> - MSDPScan(root, array |> Seq.skipWhile(fun x -> x <> byte Trigger.MSDP_ARRAY_OPEN) |> Seq.skip(1), Trigger.MSDP_ARRAY_OPEN,encoding) - | Trigger.MSDP_TABLE_OPEN -> - MSDPScan(root, array |> Seq.skipWhile(fun x -> x <> byte Trigger.MSDP_TABLE_OPEN) |> Seq.skip(1), Trigger.MSDP_TABLE_OPEN,encoding) - | _ -> root - | Trigger.MSDP_ARRAY_OPEN -> - MSDPScan(List.Empty, array |> Seq.skip(1), Trigger.MSDP_VAL, encoding) - | Trigger.MSDP_TABLE_OPEN -> - MSDPScan(Map [], array |> Seq.skip(1), Trigger.MSDP_VAR, encoding) - | _ -> failwith "Failure in MSDPScan." \ No newline at end of file + let public MSDPScan(array: seq, encoding) = + let a = [byte Trigger.MSDP_ARRAY_CLOSE] |> Seq.append array |> Seq.append [byte Trigger.MSDP_VAL; byte Trigger.MSDP_TABLE_OPEN] + let (result, _) = MSDPScanRec(null, a, encoding) + result \ No newline at end of file diff --git a/TelnetNegotiationCore.UnitTests/BaseTest.cs b/TelnetNegotiationCore.UnitTests/BaseTest.cs new file mode 100644 index 0000000..246e8c5 --- /dev/null +++ b/TelnetNegotiationCore.UnitTests/BaseTest.cs @@ -0,0 +1,18 @@ +using Serilog; + +namespace TelnetNegotiationCore.UnitTests +{ + public class BaseTest + { + static BaseTest() + { + var log = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] ({TelnetMode}) {Message:lj}{NewLine}{Exception}") + .MinimumLevel.Verbose() + .CreateLogger(); + + Log.Logger = log; + } + } +} diff --git a/TelnetNegotiationCore.UnitTests/CHARSETTests.cs b/TelnetNegotiationCore.UnitTests/CHARSETTests.cs index cb7833e..25b1319 100644 --- a/TelnetNegotiationCore.UnitTests/CHARSETTests.cs +++ b/TelnetNegotiationCore.UnitTests/CHARSETTests.cs @@ -11,7 +11,7 @@ namespace TelnetNegotiationCore.UnitTests { [TestFixture] - public class CHARSETTests + public class CHARSETTests: BaseTest { private byte[] _negotiationOutput; @@ -28,13 +28,6 @@ public void Setup() { _negotiationOutput = null; - var log = new LoggerConfiguration() - .Enrich.FromLogContext() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] ({TelnetMode}) {Message:lj}{NewLine}{Exception}") - .MinimumLevel.Verbose() - .CreateLogger(); - - Log.Logger = log; } [TestCaseSource(nameof(ServerCHARSETSequences), Category = nameof(TelnetInterpreter.TelnetMode.Server))] diff --git a/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs b/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs index 30e4e48..446b8a2 100644 --- a/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs +++ b/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs @@ -14,7 +14,7 @@ namespace TelnetNegotiationCore.UnitTests [TestFixture( Category = "Tool", Description = "Creates the DotGraph files for Server and Client forms. Some of these are combined.")] - public class CreateDotGraph + public class CreateDotGraph : BaseTest { readonly ILogger _Logger; diff --git a/TelnetNegotiationCore.UnitTests/MSDPTests.cs b/TelnetNegotiationCore.UnitTests/MSDPTests.cs new file mode 100644 index 0000000..2488cea --- /dev/null +++ b/TelnetNegotiationCore.UnitTests/MSDPTests.cs @@ -0,0 +1,82 @@ +using NUnit.Framework; +using Serilog; +using System.Collections.Generic; +using System.Text; +using TelnetNegotiationCore.Models; + +namespace TelnetNegotiationCore.UnitTests +{ + [TestFixture] + public class MSDPTests : BaseTest + { + static Encoding encoding = Encoding.ASCII; + + [TestCaseSource(nameof(FSharpTestSequences))] + public void TestFSharp(byte[] testcase) + { + var result = Functional.MSDPLibrary.MSDPScan(testcase, encoding); + Log.Logger.Information("{$Result}", result); + Assert.True(true); + } + + public static IEnumerable FSharpTestSequences + { + get + { + yield return new TestCaseData((byte[])[ + (byte)Trigger.MSDP_VAR, + .. encoding.GetBytes("LIST"), + (byte)Trigger.MSDP_VAL, + .. encoding.GetBytes("COMMANDS")]); + + yield return new TestCaseData((byte[])[ + (byte)Trigger.MSDP_VAR, + .. encoding.GetBytes("COMMANDS"), + (byte)Trigger.MSDP_VAL, + (byte)Trigger.MSDP_ARRAY_OPEN, + (byte)Trigger.MSDP_VAL, + .. encoding.GetBytes("LIST"), + (byte)Trigger.MSDP_VAL, + .. encoding.GetBytes("REPORT"), + (byte)Trigger.MSDP_VAL, + .. encoding.GetBytes("SEND"), + (byte)Trigger.MSDP_ARRAY_CLOSE]); + + ///MSDP_VAR "EXITS" MSDP_VAL MSDP_TABLE_OPEN MSDP_VAR "n" MSDP_VAL "6011" MSDP_VAR "e" MSDP_VAL "6007" MSDP_TABLE_CLOSE MSDP_TABLE_CLOSE IAC SE + + yield return new TestCaseData((byte[])[ + (byte)Trigger.MSDP_VAR, + .. encoding.GetBytes("ROOM"), + (byte)Trigger.MSDP_VAL, + (byte)Trigger.MSDP_TABLE_OPEN, + (byte)Trigger.MSDP_VAR, + .. encoding.GetBytes("VNUM"), + (byte)Trigger.MSDP_VAL, + .. encoding.GetBytes("6008"), + (byte)Trigger.MSDP_VAR, + .. encoding.GetBytes("NAME"), + (byte)Trigger.MSDP_VAL, + .. encoding.GetBytes("The Forest clearing"), + (byte)Trigger.MSDP_VAR, + .. encoding.GetBytes("AREA"), + (byte)Trigger.MSDP_VAL, + .. encoding.GetBytes("Haon Dor"), + (byte)Trigger.MSDP_VAR, + .. encoding.GetBytes("EXITS"), + (byte)Trigger.MSDP_VAL, + (byte)Trigger.MSDP_TABLE_OPEN, + (byte)Trigger.MSDP_VAR, + .. encoding.GetBytes("n"), + (byte)Trigger.MSDP_VAL, + .. encoding.GetBytes("6011"), + (byte)Trigger.MSDP_VAR, + .. encoding.GetBytes("e"), + (byte)Trigger.MSDP_VAL, + .. encoding.GetBytes("6012"), + (byte)Trigger.MSDP_TABLE_CLOSE, + (byte)Trigger.MSDP_TABLE_CLOSE + ]); + } + } + } +} diff --git a/TelnetNegotiationCore.UnitTests/TTypeTests.cs b/TelnetNegotiationCore.UnitTests/TTypeTests.cs index cae91b6..944a625 100644 --- a/TelnetNegotiationCore.UnitTests/TTypeTests.cs +++ b/TelnetNegotiationCore.UnitTests/TTypeTests.cs @@ -27,14 +27,6 @@ public class TTypeTests [SetUp] public async Task Setup() { - var log = new LoggerConfiguration() - .Enrich.FromLogContext() - .WriteTo.Console() - .MinimumLevel.Verbose() - .CreateLogger(); - - Log.Logger = log; - _server_ti = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server) { CallbackNegotiationAsync = WriteBackToNegotiate, diff --git a/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs index b6370b1..21ed324 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs @@ -99,18 +99,13 @@ private StateMachine SetupMSDPNegotiation(StateMachine b) - { + private void CaptureMSDPValue(OneOf b) => _currentMSDPInfo.Add(b.AsT0); - } - private async Task ReadMSDPValues() + private Task ReadMSDPValues() { - Functional.MSDPLibrary.MSDPScan(new FSharpMap(Enumerable.Empty>()), - _currentMSDPInfo.Skip(1), - Functional.Trigger.MSDP_VAL, - CurrentEncoding); - await Task.CompletedTask; + Functional.MSDPLibrary.MSDPScan(_currentMSDPInfo.Skip(1), CurrentEncoding); + return Task.CompletedTask; } /// @@ -125,10 +120,10 @@ private async Task WillingMSDPAsync() /// /// Announce the MSDP we support to the client after getting a Do. /// - private async Task OnDoMSDPAsync(StateMachine.Transition _) + private Task OnDoMSDPAsync(StateMachine.Transition _) { _Logger.Debug("Connection: {ConnectionState}", "Client will do MSDP output"); - await Task.CompletedTask; + return Task.CompletedTask; } /// From deeef0dec2d0938820431e8f82e62dd89951c06f Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Fri, 5 Jan 2024 00:10:54 -0600 Subject: [PATCH 5/8] Better tests for FSharp MSDP parser. --- TelnetNegotiationCore.UnitTests/MSDPTests.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/TelnetNegotiationCore.UnitTests/MSDPTests.cs b/TelnetNegotiationCore.UnitTests/MSDPTests.cs index 2488cea..61f7ac3 100644 --- a/TelnetNegotiationCore.UnitTests/MSDPTests.cs +++ b/TelnetNegotiationCore.UnitTests/MSDPTests.cs @@ -2,6 +2,7 @@ using Serilog; using System.Collections.Generic; using System.Text; +using System.Text.Json; using TelnetNegotiationCore.Models; namespace TelnetNegotiationCore.UnitTests @@ -12,11 +13,11 @@ public class MSDPTests : BaseTest static Encoding encoding = Encoding.ASCII; [TestCaseSource(nameof(FSharpTestSequences))] - public void TestFSharp(byte[] testcase) + public void TestFSharp(byte[] testcase, dynamic expectedObject) { var result = Functional.MSDPLibrary.MSDPScan(testcase, encoding); - Log.Logger.Information("{$Result}", result); - Assert.True(true); + Log.Logger.Information("Serialized: {NewLine} {Serialized}", JsonSerializer.Serialize(result)); + Assert.AreEqual(JsonSerializer.Serialize(expectedObject), JsonSerializer.Serialize(result)); } public static IEnumerable FSharpTestSequences @@ -27,7 +28,8 @@ public static IEnumerable FSharpTestSequences (byte)Trigger.MSDP_VAR, .. encoding.GetBytes("LIST"), (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("COMMANDS")]); + .. encoding.GetBytes("COMMANDS")], + new { LIST = "COMMANDS" }); yield return new TestCaseData((byte[])[ (byte)Trigger.MSDP_VAR, @@ -40,9 +42,8 @@ .. encoding.GetBytes("LIST"), .. encoding.GetBytes("REPORT"), (byte)Trigger.MSDP_VAL, .. encoding.GetBytes("SEND"), - (byte)Trigger.MSDP_ARRAY_CLOSE]); - - ///MSDP_VAR "EXITS" MSDP_VAL MSDP_TABLE_OPEN MSDP_VAR "n" MSDP_VAL "6011" MSDP_VAR "e" MSDP_VAL "6007" MSDP_TABLE_CLOSE MSDP_TABLE_CLOSE IAC SE + (byte)Trigger.MSDP_ARRAY_CLOSE], + new { COMMANDS = (string[])["LIST", "REPORT", "SEND"] }); yield return new TestCaseData((byte[])[ (byte)Trigger.MSDP_VAR, @@ -75,7 +76,8 @@ .. encoding.GetBytes("e"), .. encoding.GetBytes("6012"), (byte)Trigger.MSDP_TABLE_CLOSE, (byte)Trigger.MSDP_TABLE_CLOSE - ]); + ], + new { ROOM = new { AREA = "Haon Dor", EXITS = new { e = "6012", n = "6011" }, NAME = "The Forest clearing", VNUM = "6008" } }); } } } From 0b41f2182129dee90daf586443f5d6d61c7234cd Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Fri, 5 Jan 2024 00:22:32 -0600 Subject: [PATCH 6/8] It's tail Recursive now. --- .../MSDPLibrary.fs | 66 ++++++++----------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/TelnetNegotiationCore.Functional/MSDPLibrary.fs b/TelnetNegotiationCore.Functional/MSDPLibrary.fs index 4eb68e5..e2b88c9 100644 --- a/TelnetNegotiationCore.Functional/MSDPLibrary.fs +++ b/TelnetNegotiationCore.Functional/MSDPLibrary.fs @@ -12,44 +12,34 @@ module MSDPLibrary = | MSDP_ARRAY_OPEN = 5uy | MSDP_ARRAY_CLOSE = 6uy - let rec private MSDPScanRec (root: obj, array: seq, encoding : Encoding) = - if Seq.length(array) = 0 then (root, array) - else - match array |> Seq.head with - | 1uy -> - let key = encoding.GetString(array |> Seq.skip(1) |> Seq.takeWhile(fun x -> x <> byte Trigger.MSDP_VAL) |> Array.ofSeq) - let (calculatedValue, leftoverArray) = MSDPScanRec(root, array |> Seq.skip(1) |> Seq.skipWhile(fun x -> x <> byte Trigger.MSDP_VAL), encoding) - MSDPScanRec((root :?> Map).Add(key, calculatedValue), leftoverArray, encoding) - | 2uy -> - if root :? Map then - MSDPScanRec(Map [], array |> Seq.skip(1), encoding) - elif root :? List then - let (calculatedValue, leftoverArray) = MSDPScanRec(Map [], array |> Seq.skip(1), encoding) - MSDPScanRec((root :?> List) @ [calculatedValue], leftoverArray, encoding) - else - MSDPScanRec(root, array |> Seq.skip(1), encoding) - | 3uy -> - MSDPScanRec(Map [], array |> Seq.skip(1), encoding) - | 4uy -> - ( - root, - array |> Seq.skip(1) - ) - | 5uy -> - MSDPScanRec(List.Empty, array |> Seq.skip(1), encoding) - | 6uy -> - ( - root, - array |> Seq.skip(1) - ) - | _ -> - let result = encoding.GetString(array |> Seq.takeWhile(fun x -> x > byte Trigger.MSDP_ARRAY_CLOSE) |> Array.ofSeq) - ( - result, - array |> Seq.skipWhile(fun x -> x > byte Trigger.MSDP_ARRAY_CLOSE) - ) + [] + let private MSDPScanTailRec (root: obj, array: seq, encoding: Encoding) = + let rec scan accRoot accArray = + if Seq.length accArray = 0 then (accRoot, accArray) + else + match accArray |> Seq.head with + | 1uy -> + let key = encoding.GetString(accArray |> Seq.skip(1) |> Seq.takeWhile(fun x -> x <> byte Trigger.MSDP_VAL) |> Array.ofSeq) + let (calculatedValue, leftoverArray) = scan root (accArray |> Seq.skip(1) |> Seq.skipWhile(fun x -> x <> byte Trigger.MSDP_VAL)) + scan ((accRoot :?> Map).Add(key, calculatedValue)) leftoverArray + | 2uy -> + if accRoot :? Map then + scan (Map []) (accArray |> Seq.skip(1)) + elif accRoot :? List then + let (calculatedValue, leftoverArray) = scan (Map []) (accArray |> Seq.skip(1)) + scan ((accRoot :?> List) @ [calculatedValue]) leftoverArray + else + scan accRoot (accArray |> Seq.skip(1)) + | 3uy -> + scan (Map []) (accArray |> Seq.skip(1)) + | 5uy -> + scan (List.Empty) (accArray |> Seq.skip(1)) + | 4uy | 6uy -> + (accRoot, accArray |> Seq.skip(1)) + | _ -> + (encoding.GetString(accArray |> Seq.takeWhile(fun x -> x > 6uy) |> Array.ofSeq), accArray |> Seq.skipWhile(fun x -> x > 6uy)) + scan root array let public MSDPScan(array: seq, encoding) = - let a = [byte Trigger.MSDP_ARRAY_CLOSE] |> Seq.append array |> Seq.append [byte Trigger.MSDP_VAL; byte Trigger.MSDP_TABLE_OPEN] - let (result, _) = MSDPScanRec(null, a, encoding) + let (result, _) = MSDPScanTailRec(Map [], array, encoding) result \ No newline at end of file From 2f48dfaa329e5f5d7dddb0ee5c9d9b91aa1a9624 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Sat, 6 Jan 2024 00:44:06 -0600 Subject: [PATCH 7/8] MSDP support in GMCP. Readme update. --- README.md | 17 ++++++++--------- .../Interpreters/TelnetGMCPInterpreter.cs | 11 +++++++++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 426061d..30526f2 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,20 @@ This library is in a state where breaking changes to the interface are expected. | RFC | Description | Supported | Comments | | ------------------------------------------- | ---------------------------------- |------------| ------------------ | | http://www.faqs.org/rfcs/rfc855.html | Telnet Option Specification | Full | | -| http://www.faqs.org/rfcs/rfc858.html | Suppress GOAHEAD Negotiation | Full | Untested | | http://www.faqs.org/rfcs/rfc1091.html | Terminal Type Negotiation | Full | | | https://tintin.mudhalla.net/protocols/mtts | MTTS Negotiation (Extends TTYPE) | Full | | -| http://www.faqs.org/rfcs/rfc885.html | End Of Record Negotiation | Full | Untested | -| https://tintin.mudhalla.net/protocols/eor | End Of Record Negotiation | Full | Untested | | http://www.faqs.org/rfcs/rfc1073.html | Window Size Negotiation (NAWS) | Full | | -| http://www.faqs.org/rfcs/rfc2066.html | Charset Negotiation | Partial | No TTABLE support | +| https://tintin.mudhalla.net/protocols/gmcp | Generic Mud Communication Protocol | Full | | | https://tintin.mudhalla.net/protocols/mssp | MSSP Negotiation (Extents 855) | Full | Untested | +| http://www.faqs.org/rfcs/rfc885.html | End Of Record Negotiation | Full | Untested, Error | +| https://tintin.mudhalla.net/protocols/eor | End Of Record Negotiation | Full | Untested, Error | +| http://www.faqs.org/rfcs/rfc858.html | Suppress GOAHEAD Negotiation | Full | Untested | +| https://tintin.mudhalla.net/protocols/msdp | Mud Server Data Protocol | Partial | Planned | +| http://www.faqs.org/rfcs/rfc2066.html | Charset Negotiation | Partial | No TTABLE support | | http://www.faqs.org/rfcs/rfc1572.html | New Environment Negotiation | No | Planned | | https://tintin.mudhalla.net/protocols/mnes | Mud New Environment Negotiation | No | Planned | -| https://tintin.mudhalla.net/protocols/msdp | Mud Server Data Protocol | No | Planned | -| https://tintin.mudhalla.net/rfc/rfc1950/ | ZLIB Compression | No | Planned | -| https://tintin.mudhalla.net/protocols/mccp | Mud Client Compression Protocol | No | Planned | -| https://tintin.mudhalla.net/protocols/gmcp | Generic Mud Communication Protocol | Partial | MSDP Planned | -| https://tintin.mudhalla.net/protocols/mccp | Mud Client Compression Protocol | No | Planned | +| https://tintin.mudhalla.net/protocols/mccp | Mud Client Compression Protocol | No | Rejects | +| https://tintin.mudhalla.net/rfc/rfc1950/ | ZLIB Compression | No | Rejects | | http://www.faqs.org/rfcs/rfc857.html | Echo Negotiation | No | Rejects | | http://www.faqs.org/rfcs/rfc1079.html | Terminal Speed Negotiation | No | Rejects | | http://www.faqs.org/rfcs/rfc1372.html | Flow Control Negotiation | No | Rejects | diff --git a/TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs index b41fcde..b250469 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs @@ -7,6 +7,7 @@ using OneOf; using MoreLinq; using System.Linq; +using System.Text.Json; namespace TelnetNegotiationCore.Interpreters { @@ -121,9 +122,15 @@ private async Task CompleteGMCPNegotiation(StateMachine.Transiti var firstSpace = _GMCPBytes.FindIndex(x => x == space); var packageBytes = _GMCPBytes.Take(firstSpace).ToArray(); var rest = _GMCPBytes.Skip(firstSpace + 1).ToArray(); - // TODO: Check for MSDP information, if so, convert to JSON first, then send as Info. + // TODO: Consideration: a version of this that sends back a Dynamic or other similar object. - await (SignalOnGMCPAsync?.Invoke((Package: CurrentEncoding.GetString(packageBytes), Info: CurrentEncoding.GetString(packageBytes))) ?? Task.CompletedTask); + var package = CurrentEncoding.GetString(packageBytes); + var info = + package == "MSDP" + ? JsonSerializer.Serialize(Functional.MSDPLibrary.MSDPScan(packageBytes, CurrentEncoding)) + : CurrentEncoding.GetString(packageBytes); + + await (SignalOnGMCPAsync?.Invoke((Package: package, Info: info)) ?? Task.CompletedTask); } /// From 5d0237a722899d2af4aa98b9a54efd3074068883 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Sun, 7 Jan 2024 15:39:09 -0600 Subject: [PATCH 8/8] Basic EOR work, and separate functions for telnet safe sends. --- .../Interpreters/TelnetEORInterpreter.cs | 22 ++++----- .../Interpreters/TelnetMSDPInterpreter.cs | 5 -- .../Interpreters/TelnetSafeInterpreter.cs | 48 ++++++++++++++++++- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs index a433e4d..2afaae9 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs @@ -113,31 +113,31 @@ private async Task OnWillEORAsync(StateMachine.Transition _) } /// - /// Sends a byte message as a Prompt, adding EOR if desired. + /// Sends a byte message as a Prompt, if supported, by not sending an EOR at the end. /// /// Byte array /// A completed Task public async Task SendPromptAsync(byte[] send) { - if (_doEOR is null or false) - { await CallbackNegotiationAsync(send); - } - else - { - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.EOR]); - await CallbackNegotiationAsync(send); - } } /// - /// Sends a byte message as a Prompt, adding EOR if desired. + /// Sends a byte message, adding an EOR at the end if needed. /// /// Byte array /// A completed Task public async Task SendAsync(byte[] send) { - await CallbackNegotiationAsync(send); + if (_doEOR is null or false) + { + await CallbackNegotiationAsync(send); + } + else + { + await CallbackNegotiationAsync(send); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.EOR]); + } } } } diff --git a/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs index 21ed324..b2c8ae8 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs @@ -1,12 +1,7 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Dynamic; using System.Linq; -using System.Reflection; using System.Threading.Tasks; -using Microsoft.FSharp.Collections; -using MoreLinq; using OneOf; using Stateless; using TelnetNegotiationCore.Models; diff --git a/TelnetNegotiationCore/Interpreters/TelnetSafeInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetSafeInterpreter.cs index 556dd2c..4a14705 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetSafeInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetSafeInterpreter.cs @@ -4,6 +4,7 @@ using MoreLinq; using System; using TelnetNegotiationCore.Models; +using System.IO; namespace TelnetNegotiationCore.Interpreters { @@ -12,6 +13,51 @@ namespace TelnetNegotiationCore.Interpreters /// public partial class TelnetInterpreter { + /// + /// Create a byte[] that is safe to send over telnet by repeating 255s. + /// + /// The string intent to be sent across the wire. + /// The new byte[] with 255s duplicated. + public byte[] TelnetSafeString(string str) + { + byte[] result; + var x = CurrentEncoding.GetBytes(str).AsSpan(); + + using (var memStream = new MemoryStream()) + { + foreach (byte bt in x) + { + memStream.Write(bt == 255 ? ([255, 255]) : (byte[])[bt]); + } + result = memStream.ToArray(); + } + + return result; + } + + /// + /// Create a byte[] that is safe to send over telnet by repeating 255s. + /// Only use this function if you do not intend to send any kind of negotiation. + /// + /// The original bytes intent to be sent. + /// The new byte[] with 255s duplicated. + public byte[] TelnetSafeBytes(byte[] str) + { + byte[] result; + var x = str; + + using (var memStream = new MemoryStream()) + { + foreach (byte bt in x) + { + memStream.Write(bt == 255 ? ([255, 255]) : (byte[])[bt]); + } + result = memStream.ToArray(); + } + + return result; + } + /// /// Protect against State Transitions and Telnet Negotiations we do not recognize. /// @@ -26,7 +72,7 @@ private StateMachine SetupSafeNegotiation(StateMachine(); var refuseThese = new List { State.Willing, State.Refusing, State.Do, State.Dont }; - foreach (var stateInfo in info.States.Join(refuseThese, x => x.UnderlyingState, y => y, (x,y) => x)) + foreach (var stateInfo in info.States.Join(refuseThese, x => x.UnderlyingState, y => y, (x, y) => x)) { var state = (State)stateInfo.UnderlyingState; var outboundUnhandledTriggers = triggers.Except(stateInfo.Transitions.Select(x => (Trigger)x.Trigger.UnderlyingTrigger));