IRC parsing, tokenization, and state handling in C#

stateful in progress

+266 -11
+13 -3
IrcTokens/Hostmask.cs
··· 11 11 12 12 public override string ToString() => _source; 13 13 14 + public override int GetHashCode() => _source.GetHashCode(); 15 + 16 + public override bool Equals(object obj) 17 + { 18 + if (obj == null || GetType() != obj.GetType()) 19 + return false; 20 + 21 + return _source == ((Hostmask) obj)._source; 22 + } 23 + 14 24 private readonly string _source; 15 - 25 + 16 26 public Hostmask(string source) 17 27 { 18 28 if (source == null) return; ··· 30 40 { 31 41 NickName = source; 32 42 } 33 - 43 + 34 44 if (NickName.Contains('!')) 35 45 { 36 46 var userSplit = NickName.Split('!'); ··· 39 49 } 40 50 } 41 51 } 42 - } 52 + }
+16 -8
IrcTokens/Line.cs
··· 15 15 public List<string> Params { get; set; } 16 16 17 17 private Hostmask _hostmask; 18 - private string _rawLine; 18 + private readonly string _rawLine; 19 19 20 - public override string ToString() => 21 - $"Line(tags={string.Join(";", Tags.Select(kvp => $"{kvp.Key}={kvp.Value}"))}, params={string.Join(",", Params)})"; 20 + public override string ToString() => 21 + $"Line(source={Source}, command={Command}, tags={string.Join(";", Tags.Select(kvp => $"{kvp.Key}={kvp.Value}"))}, params={string.Join(",", Params)})"; 22 + 23 + public override int GetHashCode() => Format().GetHashCode(); 24 + 25 + public override bool Equals(object obj) 26 + { 27 + if (obj == null || GetType() != obj.GetType()) 28 + return false; 29 + 30 + return Format() == ((Line) obj).Format(); 31 + } 22 32 23 33 public Hostmask Hostmask => 24 34 _hostmask ??= new Hostmask(Source); ··· 108 118 } 109 119 110 120 if (Source != null) 111 - { 112 121 outs.Add($":{Source}"); 113 - } 114 122 115 123 outs.Add(Command); 116 124 ··· 122 130 foreach (var p in Params) 123 131 { 124 132 if (p.Contains(' ')) 125 - throw new ArgumentException("non-last parameters cannot have spaces"); 133 + throw new ArgumentException("non-last parameters cannot have spaces", p); 126 134 if (p.StartsWith(':')) 127 - throw new ArgumentException("non-last parameters cannot start with colon"); 135 + throw new ArgumentException("non-last parameters cannot start with colon", p); 128 136 } 129 137 outs.AddRange(Params); 130 138 131 - if (last == null || string.IsNullOrWhiteSpace(last) || last.Contains(' ') || last.StartsWith(':')) 139 + if (string.IsNullOrWhiteSpace(last) || last.Contains(' ') || last.StartsWith(':')) 132 140 last = $":{last}"; 133 141 outs.Add(last); 134 142 }
+33
IrcTokens/StatefulDecoder.cs
··· 1 + using System.Collections.Generic; 2 + using System.Linq; 3 + using System.Text; 4 + 5 + namespace IrcTokens 6 + { 7 + public class StatefulDecoder 8 + { 9 + private string _buffer; 10 + public EncodingInfo Encoding { get; set; } 11 + public EncodingInfo Fallback { get; set; } 12 + 13 + public string Pending => _buffer; 14 + 15 + public void Clear() 16 + { 17 + _buffer = ""; 18 + } 19 + 20 + public List<Line> Push(string data) 21 + { 22 + if (string.IsNullOrEmpty(data)) 23 + return null; 24 + 25 + _buffer += data; 26 + return _buffer 27 + .Split('\n') 28 + .Select(l => l.TrimEnd('\r')) 29 + .Select(l => new Line(l)) 30 + .ToList(); 31 + } 32 + } 33 + }
+35
IrcTokens/StatefulEncoder.cs
··· 1 + using System.Collections.Generic; 2 + using System.Linq; 3 + using System.Text; 4 + 5 + namespace IrcTokens 6 + { 7 + public class StatefulEncoder 8 + { 9 + private string _buffer; 10 + public EncodingInfo Encoding { get; set; } 11 + private List<Line> _bufferedLines; 12 + 13 + public string Pending => _buffer; 14 + 15 + public void Clear() 16 + { 17 + _buffer = ""; 18 + _bufferedLines.Clear(); 19 + } 20 + 21 + public void Push(Line line) 22 + { 23 + _buffer += $"{line.Format()}\r\n"; 24 + _bufferedLines.Add(line); 25 + } 26 + 27 + public List<Line> Pop(int byteCount) 28 + { 29 + var sent = _buffer.Substring(byteCount).Count(c => c == '\n'); 30 + _buffer = _buffer.Substring(byteCount); 31 + _bufferedLines = _bufferedLines.Skip(sent).ToList(); 32 + return _bufferedLines.Take(sent).ToList(); 33 + } 34 + } 35 + }
+97
IrcTokens/Tests/StatefulDecoderTests.cs
··· 1 + using System.Collections.Generic; 2 + using System.Linq; 3 + using System.Text; 4 + using Microsoft.VisualStudio.TestTools.UnitTesting; 5 + 6 + namespace IrcTokens.Tests 7 + { 8 + [TestClass] 9 + public class StatefulDecoderTests 10 + { 11 + private StatefulDecoder _decoder; 12 + 13 + [TestInitialize] 14 + public void TestInitialize() 15 + { 16 + _decoder = new StatefulDecoder(); 17 + } 18 + 19 + [TestMethod] 20 + public void TestPartial() 21 + { 22 + var lines = _decoder.Push("PRIVMSG "); 23 + Assert.AreEqual(new List<string>(), lines); 24 + 25 + lines = _decoder.Push("#channel hello\r\n"); 26 + Assert.AreEqual(1, lines.Count); 27 + 28 + var line = new Line("PRIVMSG #channel hello"); 29 + CollectionAssert.AreEqual(new List<Line> {line}, lines); 30 + } 31 + 32 + [TestMethod] 33 + public void TestMultiple() 34 + { 35 + _decoder.Push("PRIVMSG #channel1 hello\r\n"); 36 + var lines = _decoder.Push("PRIVMSG #channel2 hello\r\n"); 37 + Assert.AreEqual(2, lines.Count); 38 + 39 + var line1 = new Line("PRIVMSG #channel1 hello"); 40 + var line2 = new Line("PRIVMSG #channel2 hello"); 41 + Assert.AreEqual(line1, lines[0]); 42 + Assert.AreEqual(line2, lines[1]); 43 + } 44 + 45 + [TestMethod] 46 + public void TestEncoding() 47 + { 48 + var iso8859 = Encoding.GetEncodings().Single(ei => ei.Name == "iso-8859-1"); 49 + _decoder = new StatefulDecoder {Encoding = iso8859}; 50 + var lines = _decoder.Push("PRIVMSG #channel :hello Č\r\n"); 51 + var line = new Line("PRIVMSG #channel :hello Č"); 52 + Assert.AreEqual(line, lines[0]); 53 + } 54 + 55 + [TestMethod] 56 + public void TestEncodingFallback() 57 + { 58 + var latin1 = Encoding.GetEncodings().Single(ei => ei.Name == "latin-1"); 59 + _decoder = new StatefulDecoder {Fallback = latin1}; 60 + var lines = _decoder.Push("PRIVMSG #channel hélló\r\n"); 61 + Assert.AreEqual(1, lines.Count); 62 + Assert.AreEqual(new Line("PRIVMSG #channel hélló"), lines[0]); 63 + } 64 + 65 + [TestMethod] 66 + public void TestEmpty() 67 + { 68 + var lines = _decoder.Push(string.Empty); 69 + Assert.IsNull(lines); 70 + } 71 + 72 + [TestMethod] 73 + public void TestBufferUnfinished() 74 + { 75 + _decoder.Push("PRIVMSG #channel hello"); 76 + var lines = _decoder.Push(string.Empty); 77 + Assert.IsNull(lines); 78 + } 79 + 80 + [TestMethod] 81 + public void TestClear() 82 + { 83 + _decoder.Push("PRIVMSG "); 84 + _decoder.Clear(); 85 + Assert.AreEqual(string.Empty, _decoder.Pending); 86 + } 87 + 88 + [TestMethod] 89 + public void TestTagEncodingMismatch() 90 + { 91 + _decoder.Push("@asd=á "); 92 + var lines = _decoder.Push("PRIVMSG #chan :á\r\n"); 93 + Assert.AreEqual("á", lines[0].Params[0]); 94 + Assert.AreEqual("á", lines[0].Tags["asd"]); 95 + } 96 + } 97 + }
+72
IrcTokens/Tests/StatefulEncoderTests.cs
··· 1 + using System.Linq; 2 + using System.Text; 3 + using Microsoft.VisualStudio.TestTools.UnitTesting; 4 + 5 + namespace IrcTokens.Tests 6 + { 7 + [TestClass] 8 + public class StatefulEncoderTests 9 + { 10 + private StatefulEncoder _encoder; 11 + 12 + [TestInitialize] 13 + public void TestInitialize() 14 + { 15 + _encoder = new StatefulEncoder(); 16 + } 17 + 18 + [TestMethod] 19 + public void TestPush() 20 + { 21 + var line = new Line("PRIVMSG #channel hello"); 22 + _encoder.Push(line); 23 + Assert.AreEqual("PRIVMSG #channel hello\r\n", _encoder.Pending); 24 + } 25 + 26 + [TestMethod] 27 + public void TestPopPartial() 28 + { 29 + var line = new Line("PRIVMSG #channel hello"); 30 + _encoder.Push(line); 31 + _encoder.Pop("PRIVMSG #channel hello".Length); 32 + Assert.AreEqual("\r\n", _encoder.Pending); 33 + } 34 + 35 + [TestMethod] 36 + public void TestPopReturned() 37 + { 38 + var line = new Line("PRIVMSG #channel hello"); 39 + _encoder.Push(line); 40 + _encoder.Push(line); 41 + var lines = _encoder.Pop("PRIVMSG #channel hello\r\n".Length); 42 + Assert.AreEqual(1, lines.Count); 43 + Assert.AreEqual(line, lines[0]); 44 + } 45 + 46 + [TestMethod] 47 + public void TestPopNoneReturned() 48 + { 49 + var line = new Line("PRIVMSG #channel hello"); 50 + _encoder.Push(line); 51 + var lines = _encoder.Pop(1); 52 + Assert.AreEqual(0, lines.Count); 53 + } 54 + 55 + [TestMethod] 56 + public void TestClear() 57 + { 58 + _encoder.Push(new Line("PRIVMSG #channel hello")); 59 + _encoder.Clear(); 60 + Assert.AreEqual(string.Empty, _encoder.Pending); 61 + } 62 + 63 + [TestMethod] 64 + public void TestEncoding() 65 + { 66 + var iso88592 = Encoding.GetEncodings().Single(ei => ei.Name == "iso-8859-2"); 67 + _encoder = new StatefulEncoder {Encoding = iso88592}; 68 + _encoder.Push(new Line("PRIVMSG #channel :hello Č")); 69 + Assert.AreEqual("PRIVMSG #channel :hello Č\r\n", _encoder.Pending); 70 + } 71 + } 72 + }