From 6a3bfd314d87bed96611d24440b0961a06bf7e91 Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Mon, 20 Apr 2020 17:24:59 -0400 Subject: [PATCH] stateful in progress --- IrcTokens/Hostmask.cs | 16 +++- IrcTokens/Line.cs | 24 ++++-- IrcTokens/StatefulDecoder.cs | 33 +++++++++ IrcTokens/StatefulEncoder.cs | 35 +++++++++ IrcTokens/Tests/StatefulDecoderTests.cs | 97 +++++++++++++++++++++++++ IrcTokens/Tests/StatefulEncoderTests.cs | 72 ++++++++++++++++++ 6 files changed, 266 insertions(+), 11 deletions(-) create mode 100644 IrcTokens/StatefulDecoder.cs create mode 100644 IrcTokens/StatefulEncoder.cs create mode 100644 IrcTokens/Tests/StatefulDecoderTests.cs create mode 100644 IrcTokens/Tests/StatefulEncoderTests.cs diff --git a/IrcTokens/Hostmask.cs b/IrcTokens/Hostmask.cs index 05470ef..9f935b0 100644 --- a/IrcTokens/Hostmask.cs +++ b/IrcTokens/Hostmask.cs @@ -11,8 +11,18 @@ public override string ToString() => _source; + public override int GetHashCode() => _source.GetHashCode(); + + public override bool Equals(object obj) + { + if (obj == null || GetType() != obj.GetType()) + return false; + + return _source == ((Hostmask) obj)._source; + } + private readonly string _source; - + public Hostmask(string source) { if (source == null) return; @@ -30,7 +40,7 @@ { NickName = source; } - + if (NickName.Contains('!')) { var userSplit = NickName.Split('!'); @@ -39,4 +49,4 @@ } } } -} +} \ No newline at end of file diff --git a/IrcTokens/Line.cs b/IrcTokens/Line.cs index 7592376..6198c04 100644 --- a/IrcTokens/Line.cs +++ b/IrcTokens/Line.cs @@ -15,10 +15,20 @@ namespace IrcTokens public List Params { get; set; } private Hostmask _hostmask; - private string _rawLine; + private readonly string _rawLine; - public override string ToString() => - $"Line(tags={string.Join(";", Tags.Select(kvp => $"{kvp.Key}={kvp.Value}"))}, params={string.Join(",", Params)})"; + public override string ToString() => + $"Line(source={Source}, command={Command}, tags={string.Join(";", Tags.Select(kvp => $"{kvp.Key}={kvp.Value}"))}, params={string.Join(",", Params)})"; + + public override int GetHashCode() => Format().GetHashCode(); + + public override bool Equals(object obj) + { + if (obj == null || GetType() != obj.GetType()) + return false; + + return Format() == ((Line) obj).Format(); + } public Hostmask Hostmask => _hostmask ??= new Hostmask(Source); @@ -108,9 +118,7 @@ namespace IrcTokens } if (Source != null) - { outs.Add($":{Source}"); - } outs.Add(Command); @@ -122,13 +130,13 @@ namespace IrcTokens foreach (var p in Params) { if (p.Contains(' ')) - throw new ArgumentException("non-last parameters cannot have spaces"); + throw new ArgumentException("non-last parameters cannot have spaces", p); if (p.StartsWith(':')) - throw new ArgumentException("non-last parameters cannot start with colon"); + throw new ArgumentException("non-last parameters cannot start with colon", p); } outs.AddRange(Params); - if (last == null || string.IsNullOrWhiteSpace(last) || last.Contains(' ') || last.StartsWith(':')) + if (string.IsNullOrWhiteSpace(last) || last.Contains(' ') || last.StartsWith(':')) last = $":{last}"; outs.Add(last); } diff --git a/IrcTokens/StatefulDecoder.cs b/IrcTokens/StatefulDecoder.cs new file mode 100644 index 0000000..65dd3de --- /dev/null +++ b/IrcTokens/StatefulDecoder.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace IrcTokens +{ + public class StatefulDecoder + { + private string _buffer; + public EncodingInfo Encoding { get; set; } + public EncodingInfo Fallback { get; set; } + + public string Pending => _buffer; + + public void Clear() + { + _buffer = ""; + } + + public List Push(string data) + { + if (string.IsNullOrEmpty(data)) + return null; + + _buffer += data; + return _buffer + .Split('\n') + .Select(l => l.TrimEnd('\r')) + .Select(l => new Line(l)) + .ToList(); + } + } +} diff --git a/IrcTokens/StatefulEncoder.cs b/IrcTokens/StatefulEncoder.cs new file mode 100644 index 0000000..0c8b5f9 --- /dev/null +++ b/IrcTokens/StatefulEncoder.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace IrcTokens +{ + public class StatefulEncoder + { + private string _buffer; + public EncodingInfo Encoding { get; set; } + private List _bufferedLines; + + public string Pending => _buffer; + + public void Clear() + { + _buffer = ""; + _bufferedLines.Clear(); + } + + public void Push(Line line) + { + _buffer += $"{line.Format()}\r\n"; + _bufferedLines.Add(line); + } + + public List Pop(int byteCount) + { + var sent = _buffer.Substring(byteCount).Count(c => c == '\n'); + _buffer = _buffer.Substring(byteCount); + _bufferedLines = _bufferedLines.Skip(sent).ToList(); + return _bufferedLines.Take(sent).ToList(); + } + } +} diff --git a/IrcTokens/Tests/StatefulDecoderTests.cs b/IrcTokens/Tests/StatefulDecoderTests.cs new file mode 100644 index 0000000..e0c2143 --- /dev/null +++ b/IrcTokens/Tests/StatefulDecoderTests.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IrcTokens.Tests +{ + [TestClass] + public class StatefulDecoderTests + { + private StatefulDecoder _decoder; + + [TestInitialize] + public void TestInitialize() + { + _decoder = new StatefulDecoder(); + } + + [TestMethod] + public void TestPartial() + { + var lines = _decoder.Push("PRIVMSG "); + Assert.AreEqual(new List(), lines); + + lines = _decoder.Push("#channel hello\r\n"); + Assert.AreEqual(1, lines.Count); + + var line = new Line("PRIVMSG #channel hello"); + CollectionAssert.AreEqual(new List {line}, lines); + } + + [TestMethod] + public void TestMultiple() + { + _decoder.Push("PRIVMSG #channel1 hello\r\n"); + var lines = _decoder.Push("PRIVMSG #channel2 hello\r\n"); + Assert.AreEqual(2, lines.Count); + + var line1 = new Line("PRIVMSG #channel1 hello"); + var line2 = new Line("PRIVMSG #channel2 hello"); + Assert.AreEqual(line1, lines[0]); + Assert.AreEqual(line2, lines[1]); + } + + [TestMethod] + public void TestEncoding() + { + var iso8859 = Encoding.GetEncodings().Single(ei => ei.Name == "iso-8859-1"); + _decoder = new StatefulDecoder {Encoding = iso8859}; + var lines = _decoder.Push("PRIVMSG #channel :hello Č\r\n"); + var line = new Line("PRIVMSG #channel :hello Č"); + Assert.AreEqual(line, lines[0]); + } + + [TestMethod] + public void TestEncodingFallback() + { + var latin1 = Encoding.GetEncodings().Single(ei => ei.Name == "latin-1"); + _decoder = new StatefulDecoder {Fallback = latin1}; + var lines = _decoder.Push("PRIVMSG #channel hélló\r\n"); + Assert.AreEqual(1, lines.Count); + Assert.AreEqual(new Line("PRIVMSG #channel hélló"), lines[0]); + } + + [TestMethod] + public void TestEmpty() + { + var lines = _decoder.Push(string.Empty); + Assert.IsNull(lines); + } + + [TestMethod] + public void TestBufferUnfinished() + { + _decoder.Push("PRIVMSG #channel hello"); + var lines = _decoder.Push(string.Empty); + Assert.IsNull(lines); + } + + [TestMethod] + public void TestClear() + { + _decoder.Push("PRIVMSG "); + _decoder.Clear(); + Assert.AreEqual(string.Empty, _decoder.Pending); + } + + [TestMethod] + public void TestTagEncodingMismatch() + { + _decoder.Push("@asd=á "); + var lines = _decoder.Push("PRIVMSG #chan :á\r\n"); + Assert.AreEqual("á", lines[0].Params[0]); + Assert.AreEqual("á", lines[0].Tags["asd"]); + } + } +} diff --git a/IrcTokens/Tests/StatefulEncoderTests.cs b/IrcTokens/Tests/StatefulEncoderTests.cs new file mode 100644 index 0000000..4732573 --- /dev/null +++ b/IrcTokens/Tests/StatefulEncoderTests.cs @@ -0,0 +1,72 @@ +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IrcTokens.Tests +{ + [TestClass] + public class StatefulEncoderTests + { + private StatefulEncoder _encoder; + + [TestInitialize] + public void TestInitialize() + { + _encoder = new StatefulEncoder(); + } + + [TestMethod] + public void TestPush() + { + var line = new Line("PRIVMSG #channel hello"); + _encoder.Push(line); + Assert.AreEqual("PRIVMSG #channel hello\r\n", _encoder.Pending); + } + + [TestMethod] + public void TestPopPartial() + { + var line = new Line("PRIVMSG #channel hello"); + _encoder.Push(line); + _encoder.Pop("PRIVMSG #channel hello".Length); + Assert.AreEqual("\r\n", _encoder.Pending); + } + + [TestMethod] + public void TestPopReturned() + { + var line = new Line("PRIVMSG #channel hello"); + _encoder.Push(line); + _encoder.Push(line); + var lines = _encoder.Pop("PRIVMSG #channel hello\r\n".Length); + Assert.AreEqual(1, lines.Count); + Assert.AreEqual(line, lines[0]); + } + + [TestMethod] + public void TestPopNoneReturned() + { + var line = new Line("PRIVMSG #channel hello"); + _encoder.Push(line); + var lines = _encoder.Pop(1); + Assert.AreEqual(0, lines.Count); + } + + [TestMethod] + public void TestClear() + { + _encoder.Push(new Line("PRIVMSG #channel hello")); + _encoder.Clear(); + Assert.AreEqual(string.Empty, _encoder.Pending); + } + + [TestMethod] + public void TestEncoding() + { + var iso88592 = Encoding.GetEncodings().Single(ei => ei.Name == "iso-8859-2"); + _encoder = new StatefulEncoder {Encoding = iso88592}; + _encoder.Push(new Line("PRIVMSG #channel :hello Č")); + Assert.AreEqual("PRIVMSG #channel :hello Č\r\n", _encoder.Pending); + } + } +}