IRC parsing, tokenization, and state handling in C#

Fix example

+50 -29
+1 -1
IrcStates/Server.cs
··· 57 57 58 58 public List<(Line, Emit)> Recv(byte[] data) 59 59 { 60 - var lines = _decoder.Push(data); 60 + var lines = _decoder.Push(data, data.Length); 61 61 if (lines == null) throw new ServerDisconnectedException(); 62 62 63 63 var emits = lines.Select(ParseTokens).ToList();
+4 -1
IrcTokens/Extensions.cs
··· 36 36 37 37 public static byte[] Trim(this IEnumerable<byte> bytes, byte separator) 38 38 { 39 - if (bytes == null || !bytes.Any()) return Array.Empty<byte>(); 39 + if (bytes == null) return Array.Empty<byte>(); 40 + 40 41 var byteList = new List<byte>(bytes); 41 42 var i = 0; 43 + 44 + if (!byteList.Any()) return byteList.ToArray(); 42 45 43 46 while (byteList[i] == separator) 44 47 {
+5 -4
IrcTokens/StatefulDecoder.cs
··· 49 49 50 50 public List<Line> Push(string data) 51 51 { 52 - return Push(Encoding.GetBytes(data)); 52 + var bytes = Encoding.GetBytes(data); 53 + return Push(bytes, bytes.Length); 53 54 } 54 55 55 - public List<Line> Push(byte[] data) 56 + public List<Line> Push(byte[] data, int bytesReceived) 56 57 { 57 - if (data == null || data.Length == 0) return null; 58 + if (data == null) return null; 58 59 59 - _buffer = _buffer == null ? Array.Empty<byte>() : _buffer.Concat(data).ToArray(); 60 + _buffer = _buffer == null ? Array.Empty<byte>() : _buffer.Concat(data.Take(bytesReceived)).ToArray(); 60 61 61 62 var listLines = _buffer.Split((byte) '\n').Select(l => l.Trim((byte) '\r')).ToList(); 62 63 _buffer = listLines.Last();
+7 -7
IrcTokens/StatefulEncoder.cs
··· 7 7 { 8 8 public class StatefulEncoder 9 9 { 10 - private Queue<Line> _bufferedLines; 10 + private List<Line> _bufferedLines; 11 11 private Encoding _encoding; 12 12 13 13 public StatefulEncoder() ··· 45 45 public void Clear() 46 46 { 47 47 PendingBytes = Array.Empty<byte>(); 48 - _bufferedLines = new Queue<Line>(); 48 + _bufferedLines = new List<Line>(); 49 49 } 50 50 51 51 public void Push(Line line) ··· 53 53 if (line == null) throw new ArgumentNullException(nameof(line)); 54 54 55 55 PendingBytes = PendingBytes.Concat(Encoding.GetBytes($"{line.Format()}\r\n")).ToArray(); 56 - _bufferedLines.Enqueue(line); 56 + _bufferedLines.Add(line); 57 57 } 58 58 59 59 public List<Line> Pop(int byteCount) ··· 61 61 var sent = PendingBytes.Take(byteCount).Count(c => c == '\n'); 62 62 63 63 PendingBytes = PendingBytes.Skip(byteCount).ToArray(); 64 - _bufferedLines = new Queue<Line>(_bufferedLines.Take(sent)); 64 + 65 + var sentLines = _bufferedLines.Take(sent).ToList(); 66 + _bufferedLines = _bufferedLines.Skip(sent).ToList(); 65 67 66 - return Enumerable.Range(0, sent) 67 - .Select(_ => _bufferedLines.Dequeue()) 68 - .ToList(); 68 + return sentLines; 69 69 } 70 70 } 71 71 }
+4 -2
IrcTokens/Tests/StatefulDecoder.cs
··· 45 45 { 46 46 var iso8859 = Encoding.GetEncoding("iso-8859-1"); 47 47 _decoder = new IrcTokens.StatefulDecoder {Encoding = iso8859}; 48 - var lines = _decoder.Push(iso8859.GetBytes("PRIVMSG #channel :hello Ç\r\n")); 48 + var bytes = iso8859.GetBytes("PRIVMSG #channel :hello Ç\r\n"); 49 + var lines = _decoder.Push(bytes, bytes.Length); 49 50 var line = new Line("PRIVMSG #channel :hello Ç"); 50 51 Assert.IsTrue(line.Equals(lines[0])); 51 52 } ··· 55 56 { 56 57 var latin1 = Encoding.GetEncoding("iso-8859-1"); 57 58 _decoder = new IrcTokens.StatefulDecoder {Encoding = null, Fallback = latin1}; 58 - var lines = _decoder.Push(latin1.GetBytes("PRIVMSG #channel hélló\r\n")); 59 + var bytes = latin1.GetBytes("PRIVMSG #channel hélló\r\n"); 60 + var lines = _decoder.Push(bytes, bytes.Length); 59 61 Assert.AreEqual(1, lines.Count); 60 62 Assert.IsTrue(new Line("PRIVMSG #channel hélló").Equals(lines[0])); 61 63 }
+19 -10
README.md
··· 29 29 30 30 public class Client 31 31 { 32 - private readonly Socket _socket; 32 + private readonly byte[] _bytes; 33 33 private readonly StatefulDecoder _decoder; 34 34 private readonly StatefulEncoder _encoder; 35 - private readonly byte[] _bytes; 35 + private readonly Socket _socket; 36 36 37 37 public Client() 38 38 { 39 39 _decoder = new StatefulDecoder(); 40 40 _encoder = new StatefulEncoder(); 41 - _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP); 42 - _bytes = new byte[1024]; 41 + _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP); 42 + _bytes = new byte[1024]; 43 43 } 44 44 45 45 public void Start() 46 46 { 47 47 _socket.Connect("127.0.0.1", 6667); 48 + while (!_socket.Connected) Thread.Sleep(1000); 48 49 49 - Send(new Line {Command = "USER", Params = new List<string> {"username", "0", "*", "real name"}}); 50 - Send(new Line {Command = "NICK", Params = new List<string> {"statefulbot"}}); 50 + Send(new Line {Command = "NICK", Params = new List<string> {"tokensbot"}}); 51 + Send(new Line {Command = "USER", Params = new List<string> {"tokensbot", "0", "*", "real name"}}); 51 52 52 53 while (true) 53 54 { 54 55 var bytesReceived = _socket.Receive(_bytes); 55 - var lines = _decoder.Push(_bytes); 56 56 57 - if (lines.Count == 0) 57 + if (bytesReceived == 0) 58 58 { 59 59 Console.WriteLine("! disconnected"); 60 60 _socket.Shutdown(SocketShutdown.Both); 61 + _socket.Close(); 61 62 break; 62 63 } 64 + 65 + var lines = _decoder.Push(_bytes, bytesReceived); 63 66 64 67 foreach (var line in lines) 65 68 { ··· 71 74 Send(new Line {Command = "PONG", Params = line.Params}); 72 75 break; 73 76 case "001": 74 - Send(new Line {Command = "JOIN", Params = new List<string> {"#channel"}}); 77 + Send(new Line {Command = "JOIN", Params = new List<string> {"#test"}}); 78 + break; 79 + case "PRIVMSG": 80 + Send(new Line 81 + { 82 + Command = "PRIVMSG", Params = new List<string> {line.Params[0], "hello there"} 83 + }); 75 84 break; 76 85 } 77 86 } ··· 83 92 Console.WriteLine($"> {line.Format()}"); 84 93 _encoder.Push(line); 85 94 while (_encoder.PendingBytes.Length > 0) 86 - _encoder.Pop(_socket.Send(_encoder.PendingBytes)); 95 + _encoder.Pop(_socket.Send(_encoder.PendingBytes, SocketFlags.None)); 87 96 } 88 97 } 89 98
+10 -4
TokensSample/Client.cs
··· 1 1 using System; 2 2 using System.Collections.Generic; 3 3 using System.Net.Sockets; 4 + using System.Threading; 4 5 using IrcTokens; 5 6 6 7 namespace TokensSample ··· 23 24 public void Start() 24 25 { 25 26 _socket.Connect("127.0.0.1", 6667); 27 + while (!_socket.Connected) Thread.Sleep(1000); 26 28 27 29 Send(new Line {Command = "NICK", Params = new List<string> {"tokensbot"}}); 28 30 Send(new Line {Command = "USER", Params = new List<string> {"tokensbot", "0", "*", "real name"}}); ··· 35 37 { 36 38 Console.WriteLine("! disconnected"); 37 39 _socket.Shutdown(SocketShutdown.Both); 40 + _socket.Close(); 38 41 break; 39 42 } 40 43 41 - var lines = _decoder.Push(_bytes); 42 - 44 + var lines = _decoder.Push(_bytes, bytesReceived); 45 + 43 46 foreach (var line in lines) 44 47 { 45 48 Console.WriteLine($"< {line.Format()}"); ··· 53 56 Send(new Line {Command = "JOIN", Params = new List<string> {"#test"}}); 54 57 break; 55 58 case "PRIVMSG": 56 - Send(new Line {Command = "PRIVMSG", Params = new List<string> {line.Params[0], "hello there"}}); 59 + Send(new Line 60 + { 61 + Command = "PRIVMSG", Params = new List<string> {line.Params[0], "hello there"} 62 + }); 57 63 break; 58 64 } 59 65 } ··· 65 71 Console.WriteLine($"> {line.Format()}"); 66 72 _encoder.Push(line); 67 73 while (_encoder.PendingBytes.Length > 0) 68 - _encoder.Pop(_socket.Send(_encoder.PendingBytes)); 74 + _encoder.Pop(_socket.Send(_encoder.PendingBytes, SocketFlags.None)); 69 75 } 70 76 } 71 77 }