about summary refs log tree commit diff
diff options
context:
space:
mode:
authorBen Harris <ben@tilde.team>2020-04-28 00:35:52 -0400
committerBen Harris <ben@tilde.team>2020-04-28 00:35:52 -0400
commit80afa2c0aec37b7f98cc22615417c36672e695da (patch)
tree63ca3e309a5daa5093e54bdfdb493115c7a3d942
parent933a4f85604e21445c9bac8272d64cf3e6f65e00 (diff)
tidy up, work on stateful
-rw-r--r--IrcStates/Casemap.cs11
-rw-r--r--IrcStates/Numeric.cs3
-rw-r--r--IrcStates/Server.cs75
-rw-r--r--IrcStates/ServerDisconnectedException.cs9
-rw-r--r--IrcStates/ServerException.cs9
-rw-r--r--IrcStates/User.cs2
-rw-r--r--IrcTokens/Hostmask.cs64
-rw-r--r--IrcTokens/Line.cs194
-rw-r--r--IrcTokens/Protocol.cs76
-rw-r--r--IrcTokens/StatefulDecoder.cs25
-rw-r--r--IrcTokens/StatefulEncoder.cs26
-rw-r--r--IrcTokens/Tests/Format.cs60
-rw-r--r--IrcTokens/Tests/Hostmask.cs2
-rw-r--r--IrcTokens/Tests/Parser.cs16
-rw-r--r--IrcTokens/Tests/StatefulDecoder.cs12
-rw-r--r--IrcTokens/Tests/StatefulEncoder.cs19
-rw-r--r--IrcTokens/Tests/Tokenization.cs12
17 files changed, 312 insertions, 303 deletions
diff --git a/IrcStates/Casemap.cs b/IrcStates/Casemap.cs
index 484c490..67867c5 100644
--- a/IrcStates/Casemap.cs
+++ b/IrcStates/Casemap.cs
@@ -17,10 +17,7 @@ namespace IrcStates
 
         private static string Replace(string s, string upper, string lower)
         {
-            for (var i = 0; i < upper.Length; ++i)
-            {
-                s = s.Replace(upper[i], lower[i]);
-            }
+            for (var i = 0; i < upper.Length; ++i) s = s.Replace(upper[i], lower[i]);
 
             return s;
         }
@@ -28,14 +25,12 @@ namespace IrcStates
         public static string CaseFold(CaseMapping mapping, string s)
         {
             if (s != null)
-            {
                 return mapping switch
                 {
                     CaseMapping.Rfc1459 => Replace(s, Rfc1459UpperChars, Rfc1459LowerChars),
-                    CaseMapping.Ascii => Replace(s, AsciiUpperChars, AsciiLowerChars),
-                    _ => throw new ArgumentOutOfRangeException(nameof(mapping), mapping, null)
+                    CaseMapping.Ascii   => Replace(s, AsciiUpperChars, AsciiLowerChars),
+                    _                   => throw new ArgumentOutOfRangeException(nameof(mapping), mapping, null)
                 };
-            }
 
             return string.Empty;
         }
diff --git a/IrcStates/Numeric.cs b/IrcStates/Numeric.cs
index f5525f6..3cd63b3 100644
--- a/IrcStates/Numeric.cs
+++ b/IrcStates/Numeric.cs
@@ -1,8 +1,10 @@
 // ReSharper disable InconsistentNaming
+
 namespace IrcStates
 {
     public static class Numeric
     {
+#pragma warning disable CA1707 // Identifiers should not contain underscores
         public const string RPL_WELCOME = "001";
         public const string RPL_ISUPPORT = "005";
         public const string RPL_MOTD = "372";
@@ -45,5 +47,6 @@ namespace IrcStates
         public const string RPL_ENDOFWHOIS = "318";
 
         public const string ERR_NOSUCHCHANNEL = "403";
+#pragma warning restore CA1707 // Identifiers should not contain underscores
     }
 }
diff --git a/IrcStates/Server.cs b/IrcStates/Server.cs
index 9f97f47..c20e041 100644
--- a/IrcStates/Server.cs
+++ b/IrcStates/Server.cs
@@ -1,6 +1,79 @@
-namespace IrcStates
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using IrcTokens;
+
+namespace IrcStates
 {
     public class Server
     {
+        private readonly StatefulDecoder _decoder;
+
+        public Dictionary<string, List<Func<string, Line, Emit>>> LineHandlers;
+
+        private Dictionary<string, string> TempCaps;
+
+        public Server(string name)
+        {
+            Name          = name;
+            Registered    = false;
+            Modes         = new List<string>();
+            Motd          = new List<string>();
+            _decoder      = new StatefulDecoder();
+            LineHandlers  = new Dictionary<string, List<Func<string, Line, Emit>>>();
+            Users         = new Dictionary<string, User>();
+            Channels      = new Dictionary<string, Channel>();
+            ISupport      = new ISupport();
+            HasCap        = false;
+            TempCaps      = new Dictionary<string, string>();
+            AvailableCaps = new Dictionary<string, string>();
+            AgreedCaps    = new List<string>();
+        }
+
+        public string Name { get; set; }
+        public string NickName { get; set; }
+        public string NickNameLower { get; set; }
+        public string UserName { get; set; }
+        public string HostName { get; set; }
+        public string RealName { get; set; }
+        public string Account { get; set; }
+        public string Away { get; set; }
+
+        public bool Registered { get; set; }
+        public List<string> Modes { get; set; }
+        public List<string> Motd { get; set; }
+        public Dictionary<string, User> Users { get; set; }
+        public Dictionary<string, Channel> Channels { get; set; }
+        public Dictionary<string, string> AvailableCaps { get; set; }
+        public List<string> AgreedCaps { get; set; }
+
+        public ISupport ISupport { get; set; }
+        public bool HasCap { get; set; }
+
+        public override string ToString()
+        {
+            return $"Server(name={Name})";
+        }
+
+        public List<(Line, Emit)> Recv(byte[] data)
+        {
+            var lines = _decoder.Push(data);
+            if (lines == null) throw new ServerDisconnectedException();
+
+            var emits = lines.Select(ParseTokens).ToList();
+            return null;
+        }
+
+        private Emit ParseTokens(Line line)
+        {
+            Emit ret;
+            if (LineHandlers.ContainsKey(line.Command))
+                foreach (var callback in LineHandlers[line.Command])
+                {
+                    var emit = callback(line.Command, line);
+                }
+
+            throw new NotImplementedException();
+        }
     }
 }
diff --git a/IrcStates/ServerDisconnectedException.cs b/IrcStates/ServerDisconnectedException.cs
new file mode 100644
index 0000000..636acec
--- /dev/null
+++ b/IrcStates/ServerDisconnectedException.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace IrcStates
+{
+    public class ServerDisconnectedException : Exception
+    {
+
+    }
+}
diff --git a/IrcStates/ServerException.cs b/IrcStates/ServerException.cs
new file mode 100644
index 0000000..328b141
--- /dev/null
+++ b/IrcStates/ServerException.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace IrcStates
+{
+    public class ServerException : Exception
+    {
+
+    }
+}
diff --git a/IrcStates/User.cs b/IrcStates/User.cs
index 9bc5ce8..3fea024 100644
--- a/IrcStates/User.cs
+++ b/IrcStates/User.cs
@@ -21,7 +21,7 @@ namespace IrcStates
 
         public void SetNickName(string nick, string nickLower)
         {
-            NickName = nick;
+            NickName      = nick;
             NickNameLower = nickLower;
         }
     }
diff --git a/IrcTokens/Hostmask.cs b/IrcTokens/Hostmask.cs
index 01fe7d5..3c0b7f2 100644
--- a/IrcTokens/Hostmask.cs
+++ b/IrcTokens/Hostmask.cs
@@ -3,47 +3,15 @@
 namespace IrcTokens
 {
     /// <summary>
-    /// Represents the three parts of a hostmask. Parse with the constructor.
+    ///     Represents the three parts of a hostmask. Parse with the constructor.
     /// </summary>
     public class Hostmask : IEquatable<Hostmask>
     {
-        public string NickName { get; set; }
-        public string UserName { get; set; }
-        public string HostName { get; set; }
-
-        public override string ToString()
-        {
-            return _source;
-        }
-
-        public override int GetHashCode()
-        {
-            return _source.GetHashCode(StringComparison.Ordinal);
-        }
-
-        public bool Equals(Hostmask other)
-        {
-            if (other == null)
-            {
-                return false;
-            }
-
-            return _source == other._source;
-        }
-
-        public override bool Equals(object obj)
-        {
-            return Equals(obj as Hostmask);
-        }
-
         private readonly string _source;
 
         public Hostmask(string source)
         {
-            if (source == null)
-            {
-                return;
-            }
+            if (source == null) return;
 
             _source = source;
 
@@ -66,5 +34,31 @@ namespace IrcTokens
                 UserName = userSplit[1];
             }
         }
+
+        public string NickName { get; set; }
+        public string UserName { get; set; }
+        public string HostName { get; set; }
+
+        public bool Equals(Hostmask other)
+        {
+            if (other == null) return false;
+
+            return _source == other._source;
+        }
+
+        public override string ToString()
+        {
+            return _source;
+        }
+
+        public override int GetHashCode()
+        {
+            return _source.GetHashCode(StringComparison.Ordinal);
+        }
+
+        public override bool Equals(object obj)
+        {
+            return Equals(obj as Hostmask);
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/IrcTokens/Line.cs b/IrcTokens/Line.cs
index 24efe4a..2e6f696 100644
--- a/IrcTokens/Line.cs
+++ b/IrcTokens/Line.cs
@@ -2,83 +2,34 @@
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Text;
 
 namespace IrcTokens
 {
     /// <summary>
-    /// Tools to represent, parse, and format IRC lines
+    ///     Tools to represent, parse, and format IRC lines
     /// </summary>
     public class Line : IEquatable<Line>
     {
-        public Dictionary<string, string> Tags { get; set; }
-        public string Source { get; set; }
-        public string Command { get; set; }
-        public List<string> Params { get; set; }
-
-        private Hostmask _hostmask;
-
-        public override string ToString()
-        {
-            var vars = new List<string>();
-
-            if (Command != null)
-            {
-                vars.Add($"command={Command}");
-            }
-
-            if (Source != null)
-            {
-                vars.Add($"source={Source}");
-            }
-
-            if (Params != null && Params.Any())
-            {
-                vars.Add($"params=[{string.Join(",", Params)}]");
-            }
-
-            if (Tags != null && Tags.Any())
-            {
-                vars.Add($"tags=[{string.Join(";", Tags.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]");
-            }
-
-            return $"Line({string.Join(", ", vars)})";
-        }
-
-        public override int GetHashCode()
-        {
-            return Format().GetHashCode(StringComparison.Ordinal);
-        }
+        private static readonly string[] TagUnescaped = {"\\", " ", ";", "\r", "\n"};
 
-        public bool Equals(Line other)
-        {
-            if (other == null)
-            {
-                return false;
-            }
+        private static readonly string[] TagEscaped = {"\\\\", "\\s", "\\:", "\\r", "\\n"};
 
-            return Format() == other.Format();
-        }
+        private Hostmask _hostmask;
 
-        public override bool Equals(object obj)
+        public Line()
         {
-            return Equals(obj as Line);
         }
 
-        public Hostmask Hostmask =>
-            _hostmask ??= new Hostmask(Source);
-
-        public Line() { }
-
         /// <summary>
-        /// Build new <see cref="Line"/> object parsed from <param name="line">a string</param>. Analogous to irctokens.tokenise()
+        ///     Build new <see cref="Line" /> object parsed from
+        ///     <param name="line">a string</param>
+        ///     . Analogous to irctokens.tokenise()
         /// </summary>
         /// <param name="line">irc line to parse</param>
         public Line(string line)
         {
-            if (string.IsNullOrWhiteSpace(line))
-            {
-                throw new ArgumentNullException(nameof(line));
-            }
+            if (string.IsNullOrWhiteSpace(line)) throw new ArgumentNullException(nameof(line));
 
             string[] split;
 
@@ -91,24 +42,22 @@ namespace IrcTokens
                 line = string.Join(" ", split.Skip(1));
 
                 foreach (var part in messageTags.Substring(1).Split(';'))
-                {
                     if (part.Contains('=', StringComparison.Ordinal))
                     {
-                        split = part.Split('=', 2);
-                        Tags[split[0]] = Protocol.UnescapeTag(split[1]);
+                        split          = part.Split('=', 2);
+                        Tags[split[0]] = UnescapeTag(split[1]);
                     }
                     else
                     {
                         Tags[part] = string.Empty;
                     }
-                }
             }
 
             string trailing;
             if (line.Contains(" :", StringComparison.Ordinal))
             {
-                split = line.Split(" :", 2);
-                line = split[0];
+                split    = line.Split(" :", 2);
+                line     = split[0];
                 trailing = split[1];
             }
             else
@@ -118,7 +67,7 @@ namespace IrcTokens
 
             Params = line.Contains(' ', StringComparison.Ordinal)
                 ? line.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList()
-                : new List<string> { line };
+                : new List<string> {line};
 
             if (Params[0].StartsWith(':'))
             {
@@ -132,14 +81,102 @@ namespace IrcTokens
                 Params.RemoveAt(0);
             }
 
-            if (trailing != null)
+            if (trailing != null) Params.Add(trailing);
+        }
+
+        public Dictionary<string, string> Tags { get; set; }
+        public string Source { get; set; }
+        public string Command { get; set; }
+        public List<string> Params { get; set; }
+
+        public Hostmask Hostmask =>
+            _hostmask ??= new Hostmask(Source);
+
+        public bool Equals(Line other)
+        {
+            if (other == null) return false;
+
+            return Format() == other.Format();
+        }
+
+        /// <summary>
+        ///     Unescape ircv3 tag
+        /// </summary>
+        /// <param name="val">escaped string</param>
+        /// <returns>unescaped string</returns>
+        public static string UnescapeTag(string val)
+        {
+            var unescaped = new StringBuilder();
+
+            var graphemeIterator = StringInfo.GetTextElementEnumerator(val);
+            graphemeIterator.Reset();
+
+            while (graphemeIterator.MoveNext())
             {
-                Params.Add(trailing);
+                var current = graphemeIterator.GetTextElement();
+
+                if (current == @"\")
+                    try
+                    {
+                        graphemeIterator.MoveNext();
+                        var next = graphemeIterator.GetTextElement();
+                        var pair = current + next;
+                        unescaped.Append(TagEscaped.Contains(pair)
+                            ? TagUnescaped[Array.IndexOf(TagEscaped, pair)]
+                            : next);
+                    }
+                    catch (InvalidOperationException)
+                    {
+                        // ignored
+                    }
+                else
+                    unescaped.Append(current);
             }
+
+            return unescaped.ToString();
         }
 
         /// <summary>
-        /// Format a <see cref="Line"/> as a standards-compliant IRC line
+        ///     Escape strings for use in ircv3 tags
+        /// </summary>
+        /// <param name="val">string to escape</param>
+        /// <returns>escaped string</returns>
+        public static string EscapeTag(string val)
+        {
+            for (var i = 0; i < TagUnescaped.Length; ++i)
+                val = val?.Replace(TagUnescaped[i], TagEscaped[i], StringComparison.Ordinal);
+
+            return val;
+        }
+
+        public override string ToString()
+        {
+            var vars = new List<string>();
+
+            if (Command != null) vars.Add($"command={Command}");
+
+            if (Source != null) vars.Add($"source={Source}");
+
+            if (Params != null && Params.Any()) vars.Add($"params=[{string.Join(",", Params)}]");
+
+            if (Tags != null && Tags.Any())
+                vars.Add($"tags=[{string.Join(";", Tags.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]");
+
+            return $"Line({string.Join(", ", vars)})";
+        }
+
+        public override int GetHashCode()
+        {
+            return Format().GetHashCode(StringComparison.Ordinal);
+        }
+
+        public override bool Equals(object obj)
+        {
+            return Equals(obj as Line);
+        }
+
+        /// <summary>
+        ///     Format a <see cref="Line" /> as a standards-compliant IRC line
         /// </summary>
         /// <returns>formatted irc line</returns>
         public string Format()
@@ -149,16 +186,15 @@ namespace IrcTokens
             if (Tags != null && Tags.Any())
             {
                 var tags = Tags.Keys
-                    .Select(key => string.IsNullOrWhiteSpace(Tags[key]) ? key : $"{key}={Protocol.EscapeTag(Tags[key])}")
+                    .OrderBy(k => k)
+                    .Select(key =>
+                        string.IsNullOrWhiteSpace(Tags[key]) ? key : $"{key}={EscapeTag(Tags[key])}")
                     .ToList();
 
                 outs.Add($"@{string.Join(";", tags)}");
             }
 
-            if (Source != null)
-            {
-                outs.Add($":{Source}");
-            }
+            if (Source != null) outs.Add($":{Source}");
 
             outs.Add(Command);
 
@@ -170,21 +206,17 @@ namespace IrcTokens
                 foreach (var p in Params)
                 {
                     if (p.Contains(' ', StringComparison.Ordinal))
-                    {
                         throw new ArgumentException(@"non-last parameters cannot have spaces", p);
-                    }
 
                     if (p.StartsWith(':'))
-                    {
                         throw new ArgumentException(@"non-last parameters cannot start with colon", p);
-                    }
                 }
+
                 outs.AddRange(Params);
 
-                if (string.IsNullOrWhiteSpace(last) || last.Contains(' ', StringComparison.Ordinal) || last.StartsWith(':'))
-                {
+                if (string.IsNullOrWhiteSpace(last) || last.Contains(' ', StringComparison.Ordinal) ||
+                    last.StartsWith(':'))
                     last = $":{last}";
-                }
 
                 outs.Add(last);
             }
diff --git a/IrcTokens/Protocol.cs b/IrcTokens/Protocol.cs
deleted file mode 100644
index 6ddb079..0000000
--- a/IrcTokens/Protocol.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-
-namespace IrcTokens
-{
-    internal class Protocol
-    {
-        private static readonly string[] TagUnescaped =
-        {
-            "\\", " ", ";", "\r", "\n"
-        };
-
-        private static readonly string[] TagEscaped =
-        {
-            "\\\\", "\\s", "\\:", "\\r", "\\n"
-        };
-
-        /// <summary>
-        /// Unescape ircv3 tag
-        /// </summary>
-        /// <param name="val">escaped string</param>
-        /// <returns>unescaped string</returns>
-        public static string UnescapeTag(string val)
-        {
-            var unescaped = new StringBuilder();
-
-            var graphemeIterator = StringInfo.GetTextElementEnumerator(val);
-            graphemeIterator.Reset();
-
-            while (graphemeIterator.MoveNext())
-            {
-                var current = graphemeIterator.GetTextElement();
-
-                if (current == @"\")
-                {
-                    try
-                    {
-                        graphemeIterator.MoveNext();
-                        var next = graphemeIterator.GetTextElement();
-                        var pair = current + next;
-                        unescaped.Append(TagEscaped.Contains(pair)
-                            ? TagUnescaped[Array.IndexOf(TagEscaped, pair)]
-                            : next);
-                    }
-                    catch (InvalidOperationException)
-                    {
-                        // ignored
-                    }
-                }
-                else
-                {
-                    unescaped.Append(current);
-                }
-            }
-
-            return unescaped.ToString();
-        }
-
-        /// <summary>
-        /// Escape strings for use in ircv3 tags
-        /// </summary>
-        /// <param name="val">string to escape</param>
-        /// <returns>escaped string</returns>
-        public static string EscapeTag(string val)
-        {
-            for (var i = 0; i < TagUnescaped.Length; ++i)
-            {
-                val = val.Replace(TagUnescaped[i], TagEscaped[i], StringComparison.Ordinal);
-            }
-
-            return val;
-        }
-    }
-}
diff --git a/IrcTokens/StatefulDecoder.cs b/IrcTokens/StatefulDecoder.cs
index 47106d9..bdaf81d 100644
--- a/IrcTokens/StatefulDecoder.cs
+++ b/IrcTokens/StatefulDecoder.cs
@@ -11,17 +11,20 @@ namespace IrcTokens
         private Encoding _encoding;
         private Encoding _fallback;
 
+        public StatefulDecoder()
+        {
+            Clear();
+        }
+
         public Encoding Encoding
         {
             get => _encoding ?? Encoding.GetEncoding(Encoding.UTF8.CodePage, EncoderFallback.ExceptionFallback,
-                       DecoderFallback.ExceptionFallback);
+                DecoderFallback.ExceptionFallback);
             set
             {
                 if (value != null)
-                {
                     _encoding = Encoding.GetEncoding(value.CodePage, EncoderFallback.ExceptionFallback,
                         DecoderFallback.ReplacementFallback);
-                }
             }
         }
 
@@ -32,20 +35,13 @@ namespace IrcTokens
             set
             {
                 if (value != null)
-                {
                     _fallback = Encoding.GetEncoding(value.CodePage, EncoderFallback.ReplacementFallback,
                         DecoderFallback.ReplacementFallback);
-                }
             }
         }
 
         public string Pending => Encoding.GetString(_buffer);
 
-        public StatefulDecoder()
-        {
-            Clear();
-        }
-
         public void Clear()
         {
             _buffer = Array.Empty<byte>();
@@ -58,16 +54,13 @@ namespace IrcTokens
 
         public List<Line> Push(byte[] data)
         {
-            if (data == null || data.Length == 0)
-            {
-                return null;
-            }
+            if (data == null || data.Length == 0) return null;
 
             _buffer = _buffer.Concat(data).ToArray();
 
             // simulate string.Split('\n') before decoding
             var newLineIndices = _buffer.Select((b, i) => b == '\n' ? i : -1).Where(i => i != -1).ToArray();
-            var lines = new List<byte[]>();
+            var lines          = new List<byte[]>();
 
             for (int i = 0, currentIndex = 0; i < newLineIndices.Length; ++i)
             {
@@ -99,7 +92,6 @@ namespace IrcTokens
 
             var decodeLines = new List<string>();
             foreach (var line in listLines.Select(l => l.ToArray()))
-            {
                 try
                 {
                     decodeLines.Add(Encoding.GetString(line));
@@ -108,7 +100,6 @@ namespace IrcTokens
                 {
                     decodeLines.Add(Fallback.GetString(line));
                 }
-            }
 
             return decodeLines
                 .Select(l => new Line(l))
diff --git a/IrcTokens/StatefulEncoder.cs b/IrcTokens/StatefulEncoder.cs
index c036400..b486736 100644
--- a/IrcTokens/StatefulEncoder.cs
+++ b/IrcTokens/StatefulEncoder.cs
@@ -7,8 +7,14 @@ namespace IrcTokens
 {
     public class StatefulEncoder
     {
+        private Queue<Line> _bufferedLines;
         private Encoding _encoding;
 
+        public StatefulEncoder()
+        {
+            Clear();
+        }
+
         public Encoding Encoding
         {
             get => _encoding ?? Encoding.GetEncoding(Encoding.UTF8.CodePage, EncoderFallback.ExceptionFallback,
@@ -16,15 +22,11 @@ namespace IrcTokens
             set
             {
                 if (value != null)
-                {
                     _encoding = Encoding.GetEncoding(value.CodePage, EncoderFallback.ExceptionFallback,
                         DecoderFallback.ExceptionFallback);
-                }
             }
         }
 
-        private Queue<Line> _bufferedLines;
-
         public byte[] PendingBytes { get; private set; }
 
         public string Pending()
@@ -40,23 +42,15 @@ namespace IrcTokens
             }
         }
 
-        public StatefulEncoder()
-        {
-            Clear();
-        }
-
         public void Clear()
         {
-            PendingBytes = Array.Empty<byte>();
+            PendingBytes   = Array.Empty<byte>();
             _bufferedLines = new Queue<Line>();
         }
 
         public void Push(Line line)
         {
-            if (line == null)
-            {
-                throw new ArgumentNullException(nameof(line));
-            }
+            if (line == null) throw new ArgumentNullException(nameof(line));
 
             PendingBytes = PendingBytes.Concat(Encoding.GetBytes($"{line.Format()}\r\n")).ToArray();
             _bufferedLines.Enqueue(line);
@@ -66,8 +60,8 @@ namespace IrcTokens
         {
             var sent = PendingBytes.Take(byteCount).Count(c => c == '\n');
 
-            PendingBytes = PendingBytes.Skip(byteCount).ToArray();
-            _bufferedLines = new Queue<Line>(_bufferedLines.Skip(sent));
+            PendingBytes   = PendingBytes.Skip(byteCount).ToArray();
+            _bufferedLines = new Queue<Line>(_bufferedLines.Take(sent));
 
             return Enumerable.Range(0, sent)
                 .Select(_ => _bufferedLines.Dequeue())
diff --git a/IrcTokens/Tests/Format.cs b/IrcTokens/Tests/Format.cs
index 64f974a..8ef5344 100644
--- a/IrcTokens/Tests/Format.cs
+++ b/IrcTokens/Tests/Format.cs
@@ -1,6 +1,6 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System;
+using System;
 using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
 
 namespace IrcTokens.Tests
 {
@@ -13,8 +13,8 @@ namespace IrcTokens.Tests
             var line = new Line
             {
                 Command = "PRIVMSG",
-                Params = new List<string> { "#channel", "hello" },
-                Tags = new Dictionary<string, string> { { "id", "\\" + " " + ";" + "\r\n" } }
+                Params  = new List<string> {"#channel", "hello"},
+                Tags    = new Dictionary<string, string> {{"id", "\\" + " " + ";" + "\r\n"}}
             }.Format();
 
             Assert.AreEqual("@id=\\\\\\s\\:\\r\\n PRIVMSG #channel hello", line);
@@ -23,11 +23,7 @@ namespace IrcTokens.Tests
         [TestMethod]
         public void TestMissingTag()
         {
-            var line = new Line
-            {
-                Command = "PRIVMSG",
-                Params = new List<string> { "#channel", "hello" }
-            }.Format();
+            var line = new Line {Command = "PRIVMSG", Params = new List<string> {"#channel", "hello"}}.Format();
 
             Assert.AreEqual("PRIVMSG #channel hello", line);
         }
@@ -38,8 +34,8 @@ namespace IrcTokens.Tests
             var line = new Line
             {
                 Command = "PRIVMSG",
-                Params = new List<string> { "#channel", "hello" },
-                Tags = new Dictionary<string, string> { { "a", null } }
+                Params  = new List<string> {"#channel", "hello"},
+                Tags    = new Dictionary<string, string> {{"a", null}}
             }.Format();
 
             Assert.AreEqual("@a PRIVMSG #channel hello", line);
@@ -51,8 +47,8 @@ namespace IrcTokens.Tests
             var line = new Line
             {
                 Command = "PRIVMSG",
-                Params = new List<string> { "#channel", "hello" },
-                Tags = new Dictionary<string, string> { { "a", "" } }
+                Params  = new List<string> {"#channel", "hello"},
+                Tags    = new Dictionary<string, string> {{"a", ""}}
             }.Format();
 
             Assert.AreEqual("@a PRIVMSG #channel hello", line);
@@ -63,9 +59,7 @@ namespace IrcTokens.Tests
         {
             var line = new Line
             {
-                Command = "PRIVMSG",
-                Params = new List<string> { "#channel", "hello" },
-                Source = "nick!user@host"
+                Command = "PRIVMSG", Params = new List<string> {"#channel", "hello"}, Source = "nick!user@host"
             }.Format();
 
             Assert.AreEqual(":nick!user@host PRIVMSG #channel hello", line);
@@ -74,25 +68,21 @@ namespace IrcTokens.Tests
         [TestMethod]
         public void TestCommandLowercase()
         {
-            var line = new Line { Command = "privmsg" }.Format();
+            var line = new Line {Command = "privmsg"}.Format();
             Assert.AreEqual("privmsg", line);
         }
 
         [TestMethod]
         public void TestCommandUppercase()
         {
-            var line = new Line { Command = "PRIVMSG" }.Format();
+            var line = new Line {Command = "PRIVMSG"}.Format();
             Assert.AreEqual("PRIVMSG", line);
         }
 
         [TestMethod]
         public void TestTrailingSpace()
         {
-            var line = new Line
-            {
-                Command = "PRIVMSG",
-                Params = new List<string> { "#channel", "hello world" }
-            }.Format();
+            var line = new Line {Command = "PRIVMSG", Params = new List<string> {"#channel", "hello world"}}.Format();
 
             Assert.AreEqual("PRIVMSG #channel :hello world", line);
         }
@@ -100,11 +90,7 @@ namespace IrcTokens.Tests
         [TestMethod]
         public void TestTrailingNoSpace()
         {
-            var line = new Line
-            {
-                Command = "PRIVMSG",
-                Params = new List<string> { "#channel", "helloworld" }
-            }.Format();
+            var line = new Line {Command = "PRIVMSG", Params = new List<string> {"#channel", "helloworld"}}.Format();
 
             Assert.AreEqual("PRIVMSG #channel helloworld", line);
         }
@@ -112,11 +98,7 @@ namespace IrcTokens.Tests
         [TestMethod]
         public void TestTrailingDoubleColon()
         {
-            var line = new Line
-            {
-                Command = "PRIVMSG",
-                Params = new List<string> { "#channel", ":helloworld" }
-            }.Format();
+            var line = new Line {Command = "PRIVMSG", Params = new List<string> {"#channel", ":helloworld"}}.Format();
 
             Assert.AreEqual("PRIVMSG #channel ::helloworld", line);
         }
@@ -126,11 +108,7 @@ namespace IrcTokens.Tests
         {
             Assert.ThrowsException<ArgumentException>(() =>
             {
-                new Line
-                {
-                    Command = "USER",
-                    Params = new List<string> { "user", "0 *", "real name" }
-                }.Format();
+                new Line {Command = "USER", Params = new List<string> {"user", "0 *", "real name"}}.Format();
             });
         }
 
@@ -139,11 +117,7 @@ namespace IrcTokens.Tests
         {
             Assert.ThrowsException<ArgumentException>(() =>
             {
-                new Line
-                {
-                    Command = "PRIVMSG",
-                    Params = new List<string> { ":#channel", "hello" }
-                }.Format();
+                new Line {Command = "PRIVMSG", Params = new List<string> {":#channel", "hello"}}.Format();
             });
         }
     }
diff --git a/IrcTokens/Tests/Hostmask.cs b/IrcTokens/Tests/Hostmask.cs
index 51bc182..6a5cf65 100644
--- a/IrcTokens/Tests/Hostmask.cs
+++ b/IrcTokens/Tests/Hostmask.cs
@@ -44,7 +44,7 @@ namespace IrcTokens.Tests
         [TestMethod]
         public void TestHostmaskFromLine()
         {
-            var line = new Line(":nick!user@host PRIVMSG #channel hello");
+            var line     = new Line(":nick!user@host PRIVMSG #channel hello");
             var hostmask = new IrcTokens.Hostmask("nick!user@host");
             Assert.AreEqual(hostmask.ToString(), line.Hostmask.ToString());
             Assert.AreEqual("nick", line.Hostmask.NickName);
diff --git a/IrcTokens/Tests/Parser.cs b/IrcTokens/Tests/Parser.cs
index df70b92..ed4e406 100644
--- a/IrcTokens/Tests/Parser.cs
+++ b/IrcTokens/Tests/Parser.cs
@@ -1,8 +1,8 @@
-using IrcTokens.Tests.Data;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
+using IrcTokens.Tests.Data;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
 using YamlDotNet.Serialization;
 using YamlDotNet.Serialization.NamingConventions;
 
@@ -26,13 +26,14 @@ namespace IrcTokens.Tests
             foreach (var test in LoadYaml<SplitModel>("Tests/Data/msg-split.yaml").Tests)
             {
                 var tokens = new Line(test.Input);
-                var atoms = test.Atoms;
+                var atoms  = test.Atoms;
 
                 Assert.AreEqual(atoms.Verb.ToUpper(CultureInfo.InvariantCulture), tokens.Command,
                     $"command failed on: '{test.Input}'");
                 Assert.AreEqual(atoms.Source, tokens.Source, $"source failed on: '{test.Input}'");
                 CollectionAssert.AreEqual(atoms.Tags, tokens.Tags, $"tags failed on: '{test.Input}'");
-                CollectionAssert.AreEqual(atoms.Params ?? new List<string>(), tokens.Params, $"params failed on: '{test.Input}'");
+                CollectionAssert.AreEqual(atoms.Params ?? new List<string>(), tokens.Params,
+                    $"params failed on: '{test.Input}'");
             }
         }
 
@@ -44,10 +45,7 @@ namespace IrcTokens.Tests
                 var atoms = test.Atoms;
                 var line = new Line
                 {
-                    Command = atoms.Verb,
-                    Params = atoms.Params,
-                    Source = atoms.Source,
-                    Tags = atoms.Tags
+                    Command = atoms.Verb, Params = atoms.Params, Source = atoms.Source, Tags = atoms.Tags
                 }.Format();
 
                 Assert.IsTrue(test.Matches.Contains(line), test.Description);
diff --git a/IrcTokens/Tests/StatefulDecoder.cs b/IrcTokens/Tests/StatefulDecoder.cs
index 209a6cf..da4009e 100644
--- a/IrcTokens/Tests/StatefulDecoder.cs
+++ b/IrcTokens/Tests/StatefulDecoder.cs
@@ -1,6 +1,6 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
 
 namespace IrcTokens.Tests
 {
@@ -25,7 +25,7 @@ namespace IrcTokens.Tests
             Assert.AreEqual(1, lines.Count);
 
             var line = new Line("PRIVMSG #channel hello");
-            CollectionAssert.AreEqual(new List<Line> { line }, lines);
+            CollectionAssert.AreEqual(new List<Line> {line}, lines);
         }
 
         [TestMethod]
@@ -44,9 +44,9 @@ namespace IrcTokens.Tests
         public void TestEncoding()
         {
             var iso8859 = Encoding.GetEncoding("iso-8859-1");
-            _decoder = new IrcTokens.StatefulDecoder { Encoding = iso8859 };
+            _decoder = new IrcTokens.StatefulDecoder {Encoding = iso8859};
             var lines = _decoder.Push(iso8859.GetBytes("PRIVMSG #channel :hello Ç\r\n"));
-            var line = new Line("PRIVMSG #channel :hello Ç");
+            var line  = new Line("PRIVMSG #channel :hello Ç");
             Assert.IsTrue(line.Equals(lines[0]));
         }
 
@@ -54,7 +54,7 @@ namespace IrcTokens.Tests
         public void TestEncodingFallback()
         {
             var latin1 = Encoding.GetEncoding("iso-8859-1");
-            _decoder = new IrcTokens.StatefulDecoder { Encoding = null, Fallback = latin1 };
+            _decoder = new IrcTokens.StatefulDecoder {Encoding = null, Fallback = latin1};
             var lines = _decoder.Push(latin1.GetBytes("PRIVMSG #channel hélló\r\n"));
             Assert.AreEqual(1, lines.Count);
             Assert.IsTrue(new Line("PRIVMSG #channel hélló").Equals(lines[0]));
diff --git a/IrcTokens/Tests/StatefulEncoder.cs b/IrcTokens/Tests/StatefulEncoder.cs
index e3ed70d..f2cd6c4 100644
--- a/IrcTokens/Tests/StatefulEncoder.cs
+++ b/IrcTokens/Tests/StatefulEncoder.cs
@@ -1,5 +1,5 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System.Text;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
 
 namespace IrcTokens.Tests
 {
@@ -52,6 +52,19 @@ namespace IrcTokens.Tests
         }
 
         [TestMethod]
+        public void TestPopMultipleLines()
+        {
+            var line1 = new Line("PRIVMSG #channel1 hello");
+            _encoder.Push(line1);
+            var line2 = new Line("PRIVMSG #channel2 hello");
+            _encoder.Push(line2);
+
+            var lines = _encoder.Pop(_encoder.Pending().Length);
+            Assert.AreEqual(2, lines.Count);
+            Assert.AreEqual(string.Empty, _encoder.Pending());
+        }
+
+        [TestMethod]
         public void TestClear()
         {
             _encoder.Push(new Line("PRIVMSG #channel hello"));
@@ -63,7 +76,7 @@ namespace IrcTokens.Tests
         public void TestEncoding()
         {
             var iso8859 = Encoding.GetEncoding("iso-8859-1");
-            _encoder = new IrcTokens.StatefulEncoder { Encoding = iso8859 };
+            _encoder = new IrcTokens.StatefulEncoder {Encoding = iso8859};
             _encoder.Push(new Line("PRIVMSG #channel :hello Ç"));
             CollectionAssert.AreEqual(iso8859.GetBytes("PRIVMSG #channel :hello Ç\r\n"), _encoder.PendingBytes);
         }
diff --git a/IrcTokens/Tests/Tokenization.cs b/IrcTokens/Tests/Tokenization.cs
index c4970ed..7e2139d 100644
--- a/IrcTokens/Tests/Tokenization.cs
+++ b/IrcTokens/Tests/Tokenization.cs
@@ -1,5 +1,5 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System.Collections.Generic;
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
 
 namespace IrcTokens.Tests
 {
@@ -87,14 +87,14 @@ namespace IrcTokens.Tests
         public void TestParamsTrailing()
         {
             var line = new Line("PRIVMSG #channel :hello world");
-            CollectionAssert.AreEqual(new List<string> { "#channel", "hello world" }, line.Params);
+            CollectionAssert.AreEqual(new List<string> {"#channel", "hello world"}, line.Params);
         }
 
         [TestMethod]
         public void TestParamsOnlyTrailing()
         {
             var line = new Line("PRIVMSG :hello world");
-            CollectionAssert.AreEqual(new List<string> { "hello world" }, line.Params);
+            CollectionAssert.AreEqual(new List<string> {"hello world"}, line.Params);
         }
 
         [TestMethod]
@@ -109,10 +109,10 @@ namespace IrcTokens.Tests
         public void TestAllTokens()
         {
             var line = new Line("@id=123 :nick!user@host PRIVMSG #channel :hello world");
-            CollectionAssert.AreEqual(new Dictionary<string, string> { { "id", "123" } }, line.Tags);
+            CollectionAssert.AreEqual(new Dictionary<string, string> {{"id", "123"}}, line.Tags);
             Assert.AreEqual("nick!user@host", line.Source);
             Assert.AreEqual("PRIVMSG", line.Command);
-            CollectionAssert.AreEqual(new List<string> { "#channel", "hello world" }, line.Params);
+            CollectionAssert.AreEqual(new List<string> {"#channel", "hello world"}, line.Params);
         }
     }
 }