about summary refs log tree commit diff
diff options
context:
space:
mode:
authorBen Harris <ben@tilde.team>2020-05-16 22:08:40 -0400
committerBen Harris <ben@tilde.team>2020-05-16 22:08:40 -0400
commita1e1922e62419de75d2909dfae1ee7caa9ed487f (patch)
tree9198d3edbe1363df38fc03e30e4d247be9dd8e60
parenta03daca3d4f52af4fecbb263526a0d61d17776fd (diff)
Add xmldocs, and handle null bytes
-rw-r--r--Examples/States/Client.cs5
-rw-r--r--IRCStates/Channel.cs10
-rw-r--r--IRCStates/ChannelUser.cs7
-rw-r--r--IRCStates/Extensions.cs7
-rw-r--r--IRCStates/IRCStates.csproj1
-rw-r--r--IRCStates/ISupport.cs32
-rw-r--r--IRCStates/ISupportChanModes.cs12
-rw-r--r--IRCStates/ISupportPrefix.cs9
-rw-r--r--IRCStates/Numeric.cs3
-rw-r--r--IRCStates/Server.cs399
-rw-r--r--IRCStates/User.cs6
-rw-r--r--IRCTokens/IRCTokens.csproj1
-rw-r--r--IRCTokens/StatefulDecoder.cs9
-rw-r--r--IRCTokens/Tests/Format.cs24
-rw-r--r--IRCTokens/Tests/Hostmask.cs12
-rw-r--r--IRCTokens/Tests/Parser.cs4
-rw-r--r--IRCTokens/Tests/StatefulDecoder.cs16
-rw-r--r--IRCTokens/Tests/StatefulEncoder.cs14
-rw-r--r--IRCTokens/Tests/Tokenization.cs45
19 files changed, 441 insertions, 175 deletions
diff --git a/Examples/States/Client.cs b/Examples/States/Client.cs
index e98457d..0cd7722 100644
--- a/Examples/States/Client.cs
+++ b/Examples/States/Client.cs
@@ -45,9 +45,8 @@ namespace StatesSample
             {
                 while (_encoder.PendingBytes.Any())
                 {
-                    var bytesSent = _socket.Send(_encoder.PendingBytes);
-                    var sentLines = _encoder.Pop(bytesSent);
-                    foreach (var line in sentLines) Console.WriteLine($"> {line.Format()}");
+                    foreach (var line in _encoder.Pop(_socket.Send(_encoder.PendingBytes)))
+                        Console.WriteLine($"> {line.Format()}");
                 }
 
                 var bytesReceived = _socket.Receive(_bytes);
diff --git a/IRCStates/Channel.cs b/IRCStates/Channel.cs
index 60ca3fb..a9d2302 100644
--- a/IRCStates/Channel.cs
+++ b/IRCStates/Channel.cs
@@ -13,15 +13,15 @@ namespace IRCStates
             Modes     = new Dictionary<string, string>();
         }
 
-        public string Name { get; set; }
-        public string NameLower { get; set; }
-        public Dictionary<string, ChannelUser> Users { get; set; }
+        public string Name { get; private set; }
+        public string NameLower { get; private set; }
+        public Dictionary<string, ChannelUser> Users { get; private set; }
         public string Topic { get; set; }
         public string TopicSetter { get; set; }
         public DateTime TopicTime { get; set; }
         public DateTime Created { get; set; }
-        public Dictionary<string, List<string>> ListModes { get; set; }
-        public Dictionary<string, string> Modes { get; set; }
+        public Dictionary<string, List<string>> ListModes { get; private set; }
+        public Dictionary<string, string> Modes { get; private set; }
 
         public override string ToString()
         {
diff --git a/IRCStates/ChannelUser.cs b/IRCStates/ChannelUser.cs
index 8c2298b..bc5e99b 100644
--- a/IRCStates/ChannelUser.cs
+++ b/IRCStates/ChannelUser.cs
@@ -9,9 +9,9 @@ namespace IRCStates
             Modes = new List<string>();
         }
 
-        public List<string> Modes { get; set; }
+        public List<string> Modes { get; }
 
-        protected bool Equals(ChannelUser other)
+        private bool Equals(ChannelUser other)
         {
             return other != null && Equals(Modes, other.Modes);
         }
@@ -20,8 +20,7 @@ namespace IRCStates
         {
             if (ReferenceEquals(null, obj)) return false;
             if (ReferenceEquals(this, obj)) return true;
-            if (obj.GetType() != GetType()) return false;
-            return Equals((ChannelUser) obj);
+            return obj.GetType() == GetType() && Equals((ChannelUser) obj);
         }
 
         public override int GetHashCode()
diff --git a/IRCStates/Extensions.cs b/IRCStates/Extensions.cs
index c807dbb..6cce3d4 100644
--- a/IRCStates/Extensions.cs
+++ b/IRCStates/Extensions.cs
@@ -5,6 +5,13 @@ namespace IRCStates
 {
     public static class Extensions
     {
+        /// <summary>
+        /// Update the dictionary with <see cref="other"/>'s keys and values
+        /// </summary>
+        /// <param name="dict"></param>
+        /// <param name="other"></param>
+        /// <typeparam name="TKey"></typeparam>
+        /// <typeparam name="TValue"></typeparam>
         public static void UpdateWith<TKey, TValue>(this Dictionary<TKey, TValue> dict, Dictionary<TKey, TValue> other)
         {
             if (dict == null || other == null || !other.Any()) return;
diff --git a/IRCStates/IRCStates.csproj b/IRCStates/IRCStates.csproj
index 76c4bf4..162f592 100644
--- a/IRCStates/IRCStates.csproj
+++ b/IRCStates/IRCStates.csproj
@@ -13,6 +13,7 @@
     <RepositoryUrl>https://tildegit.org/ben/ircsharp/src/branch/master/IRCStates</RepositoryUrl>
     <RepositoryType>git</RepositoryType>
     <PackageTags>irc</PackageTags>
+    <PackageVersion>1.0.1</PackageVersion>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/IRCStates/ISupport.cs b/IRCStates/ISupport.cs
index b8a5651..8dfcb0c 100644
--- a/IRCStates/ISupport.cs
+++ b/IRCStates/ISupport.cs
@@ -21,21 +21,25 @@ namespace IRCStates
             Whox        = false;
         }
 
-        public Dictionary<string, string> Raw { get; set; }
-        public string Network { get; set; }
-        public ISupportChanModes ChanModes { get; set; }
-        public ISupportPrefix Prefix { get; set; }
-        public int? Modes { get; set; }
-        public Casemap.CaseMapping CaseMapping { get; set; }
-        public List<string> ChanTypes { get; set; }
-        public List<string> StatusMsg { get; set; }
-        public string CallerId { get; set; }
-        public string Excepts { get; set; }
-        public string Invex { get; set; }
-        public int? Monitor { get; set; }
-        public int? Watch { get; set; }
-        public bool Whox { get; set; }
+        public Dictionary<string, string> Raw { get; }
+        public string Network { get; private set; }
+        public ISupportChanModes ChanModes { get; private set; }
+        public ISupportPrefix Prefix { get; private set; }
+        public int? Modes { get; private set; }
+        public Casemap.CaseMapping CaseMapping { get; private set; }
+        public List<string> ChanTypes { get; private set; }
+        public List<string> StatusMsg { get; private set; }
+        public string CallerId { get; private set; }
+        public string Excepts { get; private set; }
+        public string Invex { get; private set; }
+        public int? Monitor { get; private set; }
+        public int? Watch { get; private set; }
+        public bool Whox { get; private set; }
 
+        /// <summary>
+        /// Parse the ISupport values from the line's parameters
+        /// </summary>
+        /// <param name="tokens"></param>
         public void Parse(IEnumerable<string> tokens)
         {
             if (tokens == null) return;
diff --git a/IRCStates/ISupportChanModes.cs b/IRCStates/ISupportChanModes.cs
index e63c15f..5a175da 100644
--- a/IRCStates/ISupportChanModes.cs
+++ b/IRCStates/ISupportChanModes.cs
@@ -6,6 +6,10 @@ namespace IRCStates
 {
     public class ISupportChanModes
     {
+        /// <summary>
+        /// Split the chanmodes and add to our known <see cref="ListModes"/>
+        /// </summary>
+        /// <param name="splitVal"></param>
         public ISupportChanModes(string splitVal)
         {
             if (splitVal == null) return;
@@ -25,9 +29,9 @@ namespace IRCStates
             SettingDModes.AddRange(split[3].Select(c => c.ToString(CultureInfo.InvariantCulture)));
         }
 
-        public List<string> ListModes { get; set; }
-        public List<string> SettingBModes { get; set; }
-        public List<string> SettingCModes { get; set; }
-        public List<string> SettingDModes { get; set; }
+        public List<string> ListModes { get; }
+        public List<string> SettingBModes { get; }
+        public List<string> SettingCModes { get; }
+        public List<string> SettingDModes { get; }
     }
 }
diff --git a/IRCStates/ISupportPrefix.cs b/IRCStates/ISupportPrefix.cs
index 35c5344..146bdcc 100644
--- a/IRCStates/ISupportPrefix.cs
+++ b/IRCStates/ISupportPrefix.cs
@@ -7,6 +7,11 @@ namespace IRCStates
 {
     public class ISupportPrefix
     {
+        /// <summary>
+        /// Split the prefix value and add them to our known <see cref="Modes"/> and <see cref="Prefixes"/>
+        /// </summary>
+        /// <param name="splitVal"></param>
+        /// <exception cref="ArgumentNullException"></exception>
         public ISupportPrefix(string splitVal)
         {
             if (splitVal == null) throw new ArgumentNullException(nameof(splitVal));
@@ -18,8 +23,8 @@ namespace IRCStates
             Prefixes.AddRange(split[1].Select(c => c.ToString(CultureInfo.InvariantCulture)));
         }
 
-        public List<string> Modes { get; set; }
-        public List<string> Prefixes { get; set; }
+        public List<string> Modes { get; }
+        public List<string> Prefixes { get; }
 
         public string FromMode(char mode)
         {
diff --git a/IRCStates/Numeric.cs b/IRCStates/Numeric.cs
index 1ccbd76..674e313 100644
--- a/IRCStates/Numeric.cs
+++ b/IRCStates/Numeric.cs
@@ -2,6 +2,9 @@
 
 namespace IRCStates
 {
+    /// <summary>
+    /// Known numeric response codes
+    /// </summary>
     public static class Numeric
     {
 #pragma warning disable CA1707 // Identifiers should not contain underscores
diff --git a/IRCStates/Server.cs b/IRCStates/Server.cs
index b4a0145..2cbbb92 100644
--- a/IRCStates/Server.cs
+++ b/IRCStates/Server.cs
@@ -11,7 +11,7 @@ namespace IRCStates
         public const string WhoType = "525"; // randomly generated
         private readonly StatefulDecoder _decoder;
 
-        private readonly Dictionary<string, string> TempCaps;
+        private readonly Dictionary<string, string> _tempCaps;
 
         public Server(string name)
         {
@@ -24,7 +24,7 @@ namespace IRCStates
             Channels      = new Dictionary<string, Channel>();
             ISupport      = new ISupport();
             HasCap        = false;
-            TempCaps      = new Dictionary<string, string>();
+            _tempCaps     = new Dictionary<string, string>();
             AvailableCaps = new Dictionary<string, string>();
             AgreedCaps    = new List<string>();
         }
@@ -54,56 +54,107 @@ namespace IRCStates
             return $"Server(name={Name})";
         }
 
+        /// <summary>
+        /// Use <see cref="ISupport"/>'s case mapping to convert to lowercase
+        /// </summary>
+        /// <param name="str"></param>
+        /// <returns></returns>
         public string CaseFold(string str)
         {
             return Casemap.CaseFold(ISupport.CaseMapping, str);
         }
 
-        private bool CaseFoldEquals(string s1, string s2)
-        {
-            return CaseFold(s1) == CaseFold(s2);
-        }
-
+        /// <summary>
+        /// Is the current nickname this client?
+        /// </summary>
+        /// <param name="nickname"></param>
+        /// <returns></returns>
         private bool IsMe(string nickname)
         {
             return CaseFold(nickname) == NickNameLower;
         }
 
+        /// <summary>
+        /// Check for a user - not case sensitive
+        /// </summary>
+        /// <param name="nickname"></param>
+        /// <returns></returns>
         private bool HasUser(string nickname)
         {
             return Users.ContainsKey(CaseFold(nickname));
         }
 
-        private User AddUser(string nickname, string nicknameLower)
+        /// <summary>
+        /// Get existing user by case-insensitive nickname
+        /// </summary>
+        /// <param name="nickname"></param>
+        /// <returns></returns>
+        private User GetUser(string nickname)
         {
-            var user = CreateUser(nickname, nicknameLower);
-            Users[nicknameLower] = user;
+            return HasUser(nickname) ? Users[CaseFold(nickname)] : null;
+        }
+
+        /// <summary>
+        /// Create and add user
+        /// </summary>
+        /// <param name="nickname"></param>
+        /// <returns></returns>
+        private User AddUser(string nickname)
+        {
+            var user = CreateUser(nickname);
+            Users[CaseFold(nickname)] = user;
             return user;
         }
 
-        private User CreateUser(string nickname, string nicknameLower)
+        /// <summary>
+        /// Build a new <see cref="User"/> and update correct case-mapped nick
+        /// </summary>
+        /// <param name="nickname"></param>
+        /// <returns></returns>
+        private User CreateUser(string nickname)
         {
             var user = new User();
-            user.SetNickName(nickname, nicknameLower);
+            user.SetNickName(nickname, CaseFold(nickname));
             return user;
         }
 
+        /// <summary>
+        /// Is the channel a valid ISupport type?
+        /// </summary>
+        /// <param name="target"></param>
+        /// <returns></returns>
         private bool IsChannel(string target)
         {
             return !string.IsNullOrEmpty(target) &&
                    ISupport.ChanTypes.Contains(target[0].ToString(CultureInfo.InvariantCulture));
         }
 
+        /// <summary>
+        /// Is the channel known to this client?
+        /// </summary>
+        /// <param name="name"></param>
+        /// <returns></returns>
         public bool HasChannel(string name)
         {
-            return Channels.ContainsKey(CaseFold(name));
+            return IsChannel(name) && Channels.ContainsKey(CaseFold(name));
         }
 
+        /// <summary>
+        /// Get the channel if it's known to us
+        /// </summary>
+        /// <param name="name"></param>
+        /// <returns></returns>
         private Channel GetChannel(string name)
         {
-            return HasChannel(name) ? Channels[name] : null;
+            return HasChannel(name) ? Channels[CaseFold(name)] : null;
         }
 
+        /// <summary>
+        /// Add a <see cref="User"/> to a <see cref="Channel"/>
+        /// </summary>
+        /// <param name="channel"></param>
+        /// <param name="user"></param>
+        /// <returns>the <see cref="ChannelUser"/> that was added</returns>
         private ChannelUser UserJoin(Channel channel, User user)
         {
             var channelUser = new ChannelUser();
@@ -112,6 +163,11 @@ namespace IRCStates
             return channelUser;
         }
 
+        /// <summary>
+        /// Set own <see cref="NickName"/>, <see cref="UserName"/>, and <see cref="HostName"/>
+        /// from a given <see cref="Hostmask"/>
+        /// </summary>
+        /// <param name="hostmask"></param>
         private void SelfHostmask(Hostmask hostmask)
         {
             NickName = hostmask.NickName;
@@ -119,6 +175,19 @@ namespace IRCStates
             if (hostmask.HostName != null) HostName = hostmask.HostName;
         }
 
+        private void SelfHostmask(string raw)
+        {
+            SelfHostmask(new Hostmask(raw));
+        }
+
+        /// <summary>
+        /// Remove a user from a channel. Used to handle PART and KICK
+        /// </summary>
+        /// <param name="line"></param>
+        /// <param name="nickName"></param>
+        /// <param name="channelName"></param>
+        /// <param name="reasonIndex"></param>
+        /// <returns></returns>
         private (Emit, User) UserPart(Line line, string nickName, string channelName, int reasonIndex)
         {
             var emit                                            = new Emit();
@@ -128,7 +197,7 @@ namespace IRCStates
             User user = null;
             if (HasChannel(channelName))
             {
-                var channel = Channels[channelLower];
+                var channel = GetChannel(channelName);
                 emit.Channel = channel;
                 var nickLower = CaseFold(nickName);
                 if (HasUser(nickLower))
@@ -153,6 +222,12 @@ namespace IRCStates
             return (emit, user);
         }
 
+        /// <summary>
+        /// Update modes on a <see cref="Channel"/> given modes and parameters
+        /// </summary>
+        /// <param name="channel"></param>
+        /// <param name="modes"></param>
+        /// <param name="parameters"></param>
         private void SetChannelModes(Channel channel, IEnumerable<(bool, string)> modes, IList<string> parameters)
         {
             foreach (var (add, c) in modes)
@@ -197,6 +272,13 @@ namespace IRCStates
             }
         }
 
+        /// <summary>
+        /// Handle incoming bytes
+        /// </summary>
+        /// <param name="data"></param>
+        /// <param name="length"></param>
+        /// <returns>parsed lines and emits</returns>
+        /// <exception cref="ServerDisconnectedException"></exception>
         public IEnumerable<(Line, Emit)> Receive(byte[] data, int length)
         {
             if (data == null) return null;
@@ -204,9 +286,14 @@ namespace IRCStates
             var lines = _decoder.Push(data, length);
             if (lines == null) throw new ServerDisconnectedException();
 
-            return lines.Select(l => (l, Parse(l))).ToList();
+            return lines.Select(l => (l, Parse(l)));
         }
 
+        /// <summary>
+        /// Delegate a <see cref="Line"/> to the correct handler
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         public Emit Parse(Line line)
         {
             if (line == null) return null;
@@ -256,6 +343,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles SETNAME command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleSetname(Line line)
         {
             var emit          = new Emit();
@@ -278,6 +370,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles AWAY command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleAway(Line line)
         {
             var emit          = new Emit();
@@ -300,6 +397,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles ACCOUNT command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleAccount(Line line)
         {
             var emit          = new Emit();
@@ -322,6 +424,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles CAP command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleCap(Line line)
         {
             HasCap = true;
@@ -343,11 +450,11 @@ namespace IRCStates
             switch (subcommand)
             {
                 case "LS":
-                    TempCaps.UpdateWith(tokens);
+                    _tempCaps.UpdateWith(tokens);
                     if (!multiline)
                     {
-                        AvailableCaps.UpdateWith(TempCaps);
-                        TempCaps.Clear();
+                        AvailableCaps.UpdateWith(_tempCaps);
+                        _tempCaps.Clear();
                     }
 
                     break;
@@ -380,6 +487,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_LOGGEDIN numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleLoggedIn(Line line)
         {
             SelfHostmask(new Hostmask(line.Params[1]));
@@ -387,6 +499,11 @@ namespace IRCStates
             return new Emit();
         }
 
+        /// <summary>
+        /// Handles CHGHOST command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleChghost(Line line)
         {
             var emit          = new Emit();
@@ -412,6 +529,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_WHOISUSER numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleWhoIsUser(Line line)
         {
             var emit     = new Emit();
@@ -440,6 +562,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_WHOSPCRPL numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleWhox(Line line)
         {
             var emit = new Emit();
@@ -474,6 +601,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_WHOREPLY numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleWhoReply(Line line)
         {
             var emit     = new Emit {Target = line.Params[1]};
@@ -502,6 +634,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_VISIBLEHOST numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleVisibleHost(Line line)
         {
             var split = line.Params[1].Split('@', 2);
@@ -519,22 +656,25 @@ namespace IRCStates
             return new Emit();
         }
 
+        /// <summary>
+        /// Handles PRIVMSG, NOTICE, and TAGMSG commands
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleMessage(Line line)
         {
             var emit                       = new Emit();
             var message                    = line.Params.Count > 1 ? line.Params[1] : null;
             if (message != null) emit.Text = message;
 
-            var nickLower = CaseFold(line.Hostmask.NickName);
-            if (IsMe(nickLower))
+            var nick = CaseFold(line.Hostmask.NickName);
+            if (IsMe(nick))
             {
                 emit.SelfSource = true;
                 SelfHostmask(line.Hostmask);
             }
 
-            var user = HasUser(nickLower)
-                ? Users[nickLower]
-                : AddUser(line.Hostmask.NickName, nickLower);
+            var user = GetUser(nick) ?? AddUser(nick);
             emit.User = user;
 
             if (line.Hostmask.UserName != null) user.UserName = line.Hostmask.UserName;
@@ -559,12 +699,17 @@ namespace IRCStates
             emit.Target = line.Params[0];
 
             if (IsChannel(target) && HasChannel(target))
-                emit.Channel                       = Channels[CaseFold(target)];
+                emit.Channel                       = GetChannel(target);
             else if (IsMe(target)) emit.SelfTarget = true;
 
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_UMODEIS numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleUModeIs(Line line)
         {
             foreach (var c in line.Params[1]
@@ -576,12 +721,17 @@ namespace IRCStates
             return new Emit();
         }
 
+        /// <summary>
+        /// Handles RPL_CHANNELMODEIS numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleChannelModeIs(Line line)
         {
             var emit = new Emit();
             if (HasChannel(line.Params[1]))
             {
-                var channel = Channels[CaseFold(line.Params[1])];
+                var channel = GetChannel(line.Params[1]);
                 emit.Channel = channel;
                 var modes = line.Params[2]
                     .TrimStart('+')
@@ -593,6 +743,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles MODE command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleMode(Line line)
         {
             var emit       = new Emit();
@@ -635,13 +790,17 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_TOPICWHOTIME numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleTopicTime(Line line)
         {
-            var emit         = new Emit();
-            var channelLower = CaseFold(line.Params[1]);
-            if (Channels.ContainsKey(channelLower))
+            var emit = new Emit();
+            if (HasChannel(line.Params[1]))
             {
-                var channel = Channels[channelLower];
+                var channel = GetChannel(line.Params[1]);
                 emit.Channel        = channel;
                 channel.TopicSetter = line.Params[2];
                 channel.TopicTime = DateTimeOffset
@@ -651,27 +810,35 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_TOPIC numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleTopicNumeric(Line line)
         {
-            var emit         = new Emit();
-            var channelLower = CaseFold(line.Params[1]);
-            if (Channels.ContainsKey(channelLower))
+            var emit = new Emit();
+            if (HasChannel(line.Params[1]))
             {
-                var channel = Channels[channelLower];
-                emit.Channel                 = channel;
-                Channels[channelLower].Topic = line.Params[2];
+                var channel = GetChannel(line.Params[1]);
+                emit.Channel  = channel;
+                channel.Topic = line.Params[2];
             }
 
             return emit;
         }
 
+        /// <summary>
+        /// Handles TOPIC command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleTopic(Line line)
         {
-            var emit         = new Emit();
-            var channelLower = CaseFold(line.Params[0]);
-            if (Channels.ContainsKey(channelLower))
+            var emit = new Emit();
+            if (HasChannel(line.Params[0]))
             {
-                var channel = Channels[channelLower];
+                var channel = GetChannel(line.Params[0]);
                 emit.Channel        = channel;
                 channel.Topic       = line.Params[1];
                 channel.TopicSetter = line.Hostmask.ToString();
@@ -681,13 +848,17 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_CREATIONTIME numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleCreationTime(Line line)
         {
-            var emit         = new Emit();
-            var channelLower = CaseFold(line.Params[1]);
-            if (Channels.ContainsKey(channelLower))
+            var emit = new Emit();
+            if (HasChannel(line.Params[1]))
             {
-                var channel = Channels[channelLower];
+                var channel = GetChannel(line.Params[1]);
                 emit.Channel = channel;
                 channel.Created = DateTimeOffset
                     .FromUnixTimeSeconds(int.Parse(line.Params[2], CultureInfo.InvariantCulture)).DateTime;
@@ -696,13 +867,17 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_NAMREPLY numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleNames(Line line)
         {
-            var emit         = new Emit();
-            var channelLower = CaseFold(line.Params[2]);
+            var emit = new Emit();
+            if (!HasChannel(line.Params[2])) return emit;
 
-            if (!Channels.ContainsKey(channelLower)) return emit;
-            var channel = Channels[channelLower];
+            var channel = GetChannel(line.Params[2]);
             emit.Channel = channel;
             var nicknames = line.Params[3].Split(' ', StringSplitOptions.RemoveEmptyEntries);
             var users     = new List<User>();
@@ -721,17 +896,15 @@ namespace IRCStates
                 }
 
                 var hostmask  = new Hostmask(nick.Substring(modes.Length));
-                var nickLower = CaseFold(hostmask.NickName);
-                if (!Users.ContainsKey(nickLower)) AddUser(hostmask.NickName, nickLower);
+                var user = GetUser(hostmask.NickName) ?? AddUser(hostmask.NickName);
 
-                var user = Users[nickLower];
                 users.Add(user);
                 var channelUser = UserJoin(channel, user);
 
                 if (hostmask.UserName != null) user.UserName = hostmask.UserName;
                 if (hostmask.HostName != null) user.HostName = hostmask.HostName;
 
-                if (IsMe(nickLower)) SelfHostmask(hostmask);
+                if (IsMe(hostmask.NickName)) SelfHostmask(hostmask);
 
                 foreach (var mode in modes.Select(c => c.ToString(CultureInfo.InvariantCulture)))
                     if (!channelUser.Modes.Contains(mode))
@@ -741,6 +914,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles ERROR command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleError(Line line)
         {
             Users.Clear();
@@ -748,22 +926,27 @@ namespace IRCStates
             return new Emit();
         }
 
+        /// <summary>
+        /// Handles QUIT command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleQuit(Line line)
         {
             var emit                         = new Emit();
-            var nickLower                    = CaseFold(line.Hostmask.NickName);
+            var nick                         = line.Hostmask.NickName;
             if (line.Params.Any()) emit.Text = line.Params[0];
 
-            if (IsMe(nickLower) || line.Source == null)
+            if (IsMe(nick) || line.Source == null)
             {
                 emit.Self = true;
                 Users.Clear();
                 Channels.Clear();
             }
-            else if (Users.ContainsKey(nickLower))
+            else if (HasUser(nick))
             {
-                var user = Users[nickLower];
-                Users.Remove(nickLower);
+                var user = GetUser(nick);
+                Users.Remove(user.NickNameLower);
                 emit.User = user;
                 foreach (var channel in user.Channels.Select(c => Channels[c]))
                     channel.Users.Remove(user.NickNameLower);
@@ -772,13 +955,23 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_LOGGEDOUT numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleLoggedOut(Line line)
         {
             Account = null;
-            SelfHostmask(new Hostmask(line.Params[1]));
+            SelfHostmask(line.Params[1]);
             return new Emit();
         }
 
+        /// <summary>
+        /// Handles KICK command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleKick(Line line)
         {
             var (emit, kicked) = UserPart(line, line.Params[1], line.Params[0], 2);
@@ -787,30 +980,37 @@ namespace IRCStates
                 emit.UserTarget = kicked;
                 if (IsMe(kicked.NickName)) emit.Self = true;
 
-                var kickerLower                        = CaseFold(line.Hostmask.NickName);
-                if (IsMe(kickerLower)) emit.SelfSource = true;
+                var kicker                        = line.Hostmask.NickName;
+                if (IsMe(kicker)) emit.SelfSource = true;
 
-                emit.UserSource = Users.ContainsKey(kickerLower)
-                    ? Users[kickerLower]
-                    : CreateUser(line.Hostmask.NickName, kickerLower);
+                emit.UserSource = GetUser(kicker) ?? CreateUser(kicker);
             }
 
             return emit;
         }
 
+        /// <summary>
+        /// Handles PART command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandlePart(Line line)
         {
             var (emit, user) = UserPart(line, line.Hostmask.NickName, line.Params[0], 1);
             if (user != null)
             {
                 emit.User = user;
-                if (IsMe(user.NickName)) emit.Self = true;
+                emit.Self = IsMe(user.NickName);
             }
 
             return emit;
         }
 
-
+        /// <summary>
+        /// Handles JOIN command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleJoin(Line line)
         {
             var extended = line.Params.Count == 3;
@@ -818,18 +1018,18 @@ namespace IRCStates
             var realname = extended ? line.Params[2] : null;
             var emit     = new Emit();
 
-            var channelLower = CaseFold(line.Params[0]);
-            var nickLower    = CaseFold(line.Hostmask.NickName);
+            var channelName = line.Params[0];
+            var nick        = line.Hostmask.NickName;
 
             // handle own join
-            if (IsMe(nickLower))
+            if (IsMe(nick))
             {
                 emit.Self = true;
-                if (!HasChannel(channelLower))
+                if (!HasChannel(channelName))
                 {
                     var channel = new Channel();
-                    channel.SetName(line.Params[0], channelLower);
-                    Channels[channelLower] = channel;
+                    channel.SetName(channelName, CaseFold(channelName));
+                    Channels[CaseFold(channelName)] = channel;
                 }
 
                 SelfHostmask(line.Hostmask);
@@ -840,14 +1040,14 @@ namespace IRCStates
                 }
             }
 
-            if (HasChannel(channelLower))
+            if (HasChannel(channelName))
             {
-                var channel = Channels[channelLower];
+                var channel = GetChannel(channelName);
                 emit.Channel = channel;
 
-                if (!HasUser(nickLower)) AddUser(line.Hostmask.NickName, nickLower);
+                if (!HasUser(nick)) AddUser(nick);
 
-                var user = Users[nickLower];
+                var user = GetUser(nick);
                 emit.User = user;
                 if (line.Hostmask.UserName != null) user.UserName = line.Hostmask.UserName;
                 if (line.Hostmask.HostName != null) user.HostName = line.Hostmask.HostName;
@@ -863,43 +1063,53 @@ namespace IRCStates
             return emit;
         }
 
-
+        /// <summary>
+        /// Handles NICK command
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleNick(Line line)
         {
-            var nick      = line.Params[0];
-            var nickLower = CaseFold(line.Hostmask.NickName);
+            var newNick = line.Params[0];
+            var oldNick = line.Hostmask.NickName;
 
             var emit = new Emit();
 
-            if (Users.ContainsKey(nickLower))
+            if (HasUser(oldNick))
             {
-                var user = Users[nickLower];
-                Users.Remove(nickLower);
-                emit.User = user;
-
+                var user         = GetUser(oldNick);
                 var oldNickLower = user.NickNameLower;
-                var newNickLower = CaseFold(nick);
-                user.SetNickName(nick, newNickLower);
+                var newNickLower = CaseFold(newNick);
+
+                emit.User = user;
+                Users.Remove(oldNickLower);
                 Users[newNickLower] = user;
+                user.SetNickName(newNick, newNickLower);
+
                 foreach (var channelLower in user.Channels)
                 {
-                    var channel     = Channels[channelLower];
+                    var channel     = GetChannel(channelLower);
                     var channelUser = channel.Users[oldNickLower];
                     channel.Users.Remove(oldNickLower);
                     channel.Users[newNickLower] = channelUser;
                 }
             }
 
-            if (IsMe(nickLower))
+            if (IsMe(oldNick))
             {
                 emit.Self     = true;
-                NickName      = nick;
-                NickNameLower = CaseFold(nick);
+                NickName      = newNick;
+                NickNameLower = CaseFold(newNick);
             }
 
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_MOTDSTART and RPL_MOTD numerics
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleMotd(Line line)
         {
             if (line.Command == Numeric.RPL_MOTDSTART) Motd.Clear();
@@ -909,6 +1119,11 @@ namespace IRCStates
             return emit;
         }
 
+        /// <summary>
+        /// Handles RPL_ISUPPORT numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleISupport(Line line)
         {
             ISupport = new ISupport();
@@ -916,7 +1131,11 @@ namespace IRCStates
             return new Emit();
         }
 
-
+        /// <summary>
+        /// Handles RPL_WELCOME numeric
+        /// </summary>
+        /// <param name="line"></param>
+        /// <returns></returns>
         private Emit HandleWelcome(Line line)
         {
             NickName      = line.Params[0];
diff --git a/IRCStates/User.cs b/IRCStates/User.cs
index 5e18443..97abb15 100644
--- a/IRCStates/User.cs
+++ b/IRCStates/User.cs
@@ -9,15 +9,15 @@ namespace IRCStates
             Channels = new HashSet<string>();
         }
 
-        public string NickName { get; set; }
-        public string NickNameLower { get; set; }
+        public string NickName { get; private set; }
+        public string NickNameLower { get; private 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 HashSet<string> Channels { get; set; }
+        public HashSet<string> Channels { get; private set; }
 
         public override string ToString()
         {
diff --git a/IRCTokens/IRCTokens.csproj b/IRCTokens/IRCTokens.csproj
index ed2e0c2..cbee30e 100644
--- a/IRCTokens/IRCTokens.csproj
+++ b/IRCTokens/IRCTokens.csproj
@@ -13,6 +13,7 @@
     <RepositoryUrl>https://tildegit.org/ben/ircsharp/src/branch/master/IRCTokens</RepositoryUrl>
     <RepositoryType>git</RepositoryType>
     <PackageTags>irc</PackageTags>
+    <PackageVersion>1.0.1</PackageVersion>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/IRCTokens/StatefulDecoder.cs b/IRCTokens/StatefulDecoder.cs
index 82630f6..619253b 100644
--- a/IRCTokens/StatefulDecoder.cs
+++ b/IRCTokens/StatefulDecoder.cs
@@ -59,6 +59,15 @@ namespace IRCTokens
 
             _buffer = _buffer == null ? Array.Empty<byte>() : _buffer.Concat(data.Take(bytesReceived)).ToArray();
 
+            // truncate message at NUL if found
+            if (_buffer.Contains((byte) 0))
+            {
+                _buffer = _buffer
+                    .Take(Array.IndexOf(_buffer, (byte) 0))
+                    .Concat(new []{(byte) '\n'})
+                    .ToArray();
+            }
+
             var listLines = _buffer.Split((byte) '\n').Select(l => l.Trim((byte) '\r')).ToList();
             _buffer = listLines.LastOrDefault() ?? Array.Empty<byte>();
 
diff --git a/IRCTokens/Tests/Format.cs b/IRCTokens/Tests/Format.cs
index 8319069..7224f97 100644
--- a/IRCTokens/Tests/Format.cs
+++ b/IRCTokens/Tests/Format.cs
@@ -8,7 +8,7 @@ namespace IRCTokens.Tests
     public class Format
     {
         [TestMethod]
-        public void TestTags()
+        public void Tags()
         {
             var line = new Line("PRIVMSG", "#channel", "hello")
             {
@@ -19,7 +19,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestMissingTag()
+        public void MissingTag()
         {
             var line = new Line("PRIVMSG", "#channel", "hello").Format();
 
@@ -27,7 +27,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestNullTag()
+        public void NullTag()
         {
             var line = new Line("PRIVMSG", "#channel", "hello") {Tags = new Dictionary<string, string> {{"a", null}}}
                 .Format();
@@ -36,7 +36,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestEmptyTag()
+        public void EmptyTag()
         {
             var line = new Line("PRIVMSG", "#channel", "hello") {Tags = new Dictionary<string, string> {{"a", ""}}}
                 .Format();
@@ -45,7 +45,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestSource()
+        public void Source()
         {
             var line = new Line("PRIVMSG", "#channel", "hello") {Source = "nick!user@host"}.Format();
 
@@ -53,21 +53,21 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestCommandLowercase()
+        public void CommandLowercase()
         {
             var line = new Line {Command = "privmsg"}.Format();
             Assert.AreEqual("privmsg", line);
         }
 
         [TestMethod]
-        public void TestCommandUppercase()
+        public void CommandUppercase()
         {
             var line = new Line {Command = "PRIVMSG"}.Format();
             Assert.AreEqual("PRIVMSG", line);
         }
 
         [TestMethod]
-        public void TestTrailingSpace()
+        public void TrailingSpace()
         {
             var line = new Line("PRIVMSG", "#channel", "hello world").Format();
 
@@ -75,7 +75,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestTrailingNoSpace()
+        public void TrailingNoSpace()
         {
             var line = new Line("PRIVMSG", "#channel", "helloworld").Format();
 
@@ -83,7 +83,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestTrailingDoubleColon()
+        public void TrailingDoubleColon()
         {
             var line = new Line("PRIVMSG", "#channel", ":helloworld").Format();
 
@@ -91,13 +91,13 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestInvalidNonLastSpace()
+        public void InvalidNonLastSpace()
         {
             Assert.ThrowsException<ArgumentException>(() => { new Line("USER", "user", "0 *", "real name").Format(); });
         }
 
         [TestMethod]
-        public void TestInvalidNonLastColon()
+        public void InvalidNonLastColon()
         {
             Assert.ThrowsException<ArgumentException>(() => { new Line("PRIVMSG", ":#channel", "hello").Format(); });
         }
diff --git a/IRCTokens/Tests/Hostmask.cs b/IRCTokens/Tests/Hostmask.cs
index 2446013..17c5ad7 100644
--- a/IRCTokens/Tests/Hostmask.cs
+++ b/IRCTokens/Tests/Hostmask.cs
@@ -6,7 +6,7 @@ namespace IRCTokens.Tests
     public class Hostmask
     {
         [TestMethod]
-        public void TestHostmask()
+        public void FullHostmask()
         {
             var hostmask = new IRCTokens.Hostmask("nick!user@host");
             Assert.AreEqual("nick", hostmask.NickName);
@@ -15,7 +15,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestNoHostName()
+        public void NoHostName()
         {
             var hostmask = new IRCTokens.Hostmask("nick!user");
             Assert.AreEqual("nick", hostmask.NickName);
@@ -24,7 +24,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestNoUserName()
+        public void NoUserName()
         {
             var hostmask = new IRCTokens.Hostmask("nick@host");
             Assert.AreEqual("nick", hostmask.NickName);
@@ -33,7 +33,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestOnlyNickName()
+        public void OnlyNickName()
         {
             var hostmask = new IRCTokens.Hostmask("nick");
             Assert.AreEqual("nick", hostmask.NickName);
@@ -42,7 +42,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestHostmaskFromLine()
+        public void HostmaskFromLine()
         {
             var line     = new Line(":nick!user@host PRIVMSG #channel hello");
             var hostmask = new IRCTokens.Hostmask("nick!user@host");
@@ -53,7 +53,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestEmptyHostmaskFromLine()
+        public void EmptyHostmaskFromLine()
         {
             var line = new Line("PRIVMSG #channel hello");
             Assert.IsNull(line.Hostmask.HostName);
diff --git a/IRCTokens/Tests/Parser.cs b/IRCTokens/Tests/Parser.cs
index bd0a92d..a560793 100644
--- a/IRCTokens/Tests/Parser.cs
+++ b/IRCTokens/Tests/Parser.cs
@@ -21,7 +21,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestSplit()
+        public void Split()
         {
             foreach (var test in LoadYaml<SplitModel>("Tests/Data/msg-split.yaml").Tests)
             {
@@ -38,7 +38,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestJoin()
+        public void Join()
         {
             foreach (var test in LoadYaml<JoinModel>("Tests/Data/msg-join.yaml").Tests)
             {
diff --git a/IRCTokens/Tests/StatefulDecoder.cs b/IRCTokens/Tests/StatefulDecoder.cs
index d37310f..4da7690 100644
--- a/IRCTokens/Tests/StatefulDecoder.cs
+++ b/IRCTokens/Tests/StatefulDecoder.cs
@@ -10,13 +10,13 @@ namespace IRCTokens.Tests
         private IRCTokens.StatefulDecoder _decoder;
 
         [TestInitialize]
-        public void TestInitialize()
+        public void Initialize()
         {
             _decoder = new IRCTokens.StatefulDecoder();
         }
 
         [TestMethod]
-        public void TestPartial()
+        public void Partial()
         {
             var lines = _decoder.Push("PRIVMSG ");
             Assert.AreEqual(0, lines.Count);
@@ -29,7 +29,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestMultiple()
+        public void Multiple()
         {
             var lines = _decoder.Push("PRIVMSG #channel1 hello\r\nPRIVMSG #channel2 hello\r\n");
             Assert.AreEqual(2, lines.Count);
@@ -41,7 +41,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestEncoding()
+        public void EncodingIso8859()
         {
             var iso8859 = Encoding.GetEncoding("iso-8859-1");
             _decoder = new IRCTokens.StatefulDecoder {Encoding = iso8859};
@@ -52,7 +52,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestEncodingFallback()
+        public void EncodingFallback()
         {
             var latin1 = Encoding.GetEncoding("iso-8859-1");
             _decoder = new IRCTokens.StatefulDecoder {Encoding = null, Fallback = latin1};
@@ -63,14 +63,14 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestEmpty()
+        public void Empty()
         {
             var lines = _decoder.Push(string.Empty);
             Assert.AreEqual(0, lines.Count);
         }
 
         [TestMethod]
-        public void TestBufferUnfinished()
+        public void BufferUnfinished()
         {
             _decoder.Push("PRIVMSG #channel hello");
             var lines = _decoder.Push(string.Empty);
@@ -78,7 +78,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestClear()
+        public void Clear()
         {
             _decoder.Push("PRIVMSG ");
             _decoder.Clear();
diff --git a/IRCTokens/Tests/StatefulEncoder.cs b/IRCTokens/Tests/StatefulEncoder.cs
index 5ced4d2..d1e1e3e 100644
--- a/IRCTokens/Tests/StatefulEncoder.cs
+++ b/IRCTokens/Tests/StatefulEncoder.cs
@@ -9,13 +9,13 @@ namespace IRCTokens.Tests
         private IRCTokens.StatefulEncoder _encoder;
 
         [TestInitialize]
-        public void TestInitialize()
+        public void Initialize()
         {
             _encoder = new IRCTokens.StatefulEncoder();
         }
 
         [TestMethod]
-        public void TestPush()
+        public void Push()
         {
             var line = new Line("PRIVMSG #channel hello");
             _encoder.Push(line);
@@ -23,7 +23,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestPopPartial()
+        public void PopPartial()
         {
             var line = new Line("PRIVMSG #channel hello");
             _encoder.Push(line);
@@ -43,7 +43,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestPopNoneReturned()
+        public void PopNoneReturned()
         {
             var line = new Line("PRIVMSG #channel hello");
             _encoder.Push(line);
@@ -52,7 +52,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestPopMultipleLines()
+        public void PopMultipleLines()
         {
             var line1 = new Line("PRIVMSG #channel1 hello");
             _encoder.Push(line1);
@@ -65,7 +65,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestClear()
+        public void Clear()
         {
             _encoder.Push(new Line("PRIVMSG #channel hello"));
             _encoder.Clear();
@@ -73,7 +73,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestEncoding()
+        public void EncodingIso8859()
         {
             var iso8859 = Encoding.GetEncoding("iso-8859-1");
             _encoder = new IRCTokens.StatefulEncoder {Encoding = iso8859};
diff --git a/IRCTokens/Tests/Tokenization.cs b/IRCTokens/Tests/Tokenization.cs
index 03959de..c4c5c5a 100644
--- a/IRCTokens/Tests/Tokenization.cs
+++ b/IRCTokens/Tests/Tokenization.cs
@@ -1,4 +1,6 @@
 using System.Collections.Generic;
+using System.Linq;
+using System.Text;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 
 namespace IRCTokens.Tests
@@ -7,98 +9,98 @@ namespace IRCTokens.Tests
     public class Tokenization
     {
         [TestMethod]
-        public void TestTagsMissing()
+        public void TagsMissing()
         {
             var line = new Line("PRIVMSG #channel");
             Assert.IsNull(line.Tags);
         }
 
         [TestMethod]
-        public void TestTagsMissingValue()
+        public void TagsMissingValue()
         {
             var line = new Line("@id= PRIVMSG #channel");
             Assert.AreEqual(string.Empty, line.Tags["id"]);
         }
 
         [TestMethod]
-        public void TestTagsMissingEqual()
+        public void TagsMissingEqual()
         {
             var line = new Line("@id PRIVMSG #channel");
             Assert.AreEqual(string.Empty, line.Tags["id"]);
         }
 
         [TestMethod]
-        public void TestTagsUnescape()
+        public void TagsUnescape()
         {
             var line = new Line(@"@id=1\\\:\r\n\s2 PRIVMSG #channel");
             Assert.AreEqual("1\\;\r\n 2", line.Tags["id"]);
         }
 
         [TestMethod]
-        public void TestTagsOverlap()
+        public void TagsOverlap()
         {
             var line = new Line(@"@id=1\\\s\\s PRIVMSG #channel");
             Assert.AreEqual("1\\ \\s", line.Tags["id"]);
         }
 
         [TestMethod]
-        public void TestTagsLoneEndSlash()
+        public void TagsLoneEndSlash()
         {
             var line = new Line("@id=1\\ PRIVMSG #channel");
             Assert.AreEqual("1", line.Tags["id"]);
         }
 
         [TestMethod]
-        public void TestSourceWithoutTags()
+        public void SourceWithoutTags()
         {
             var line = new Line(":nick!user@host PRIVMSG #channel");
             Assert.AreEqual("nick!user@host", line.Source);
         }
 
         [TestMethod]
-        public void TestSourceWithTags()
+        public void SourceWithTags()
         {
             var line = new Line("@id=123 :nick!user@host PRIVMSG #channel");
             Assert.AreEqual("nick!user@host", line.Source);
         }
 
         [TestMethod]
-        public void TestSourceMissingWithoutTags()
+        public void SourceMissingWithoutTags()
         {
             var line = new Line("PRIVMSG #channel");
             Assert.IsNull(line.Source);
         }
 
         [TestMethod]
-        public void TestSourceMissingWithTags()
+        public void SourceMissingWithTags()
         {
             var line = new Line("@id=123 PRIVMSG #channel");
             Assert.IsNull(line.Source);
         }
 
         [TestMethod]
-        public void TestCommand()
+        public void Command()
         {
             var line = new Line("privmsg #channel");
             Assert.AreEqual("PRIVMSG", line.Command);
         }
 
         [TestMethod]
-        public void TestParamsTrailing()
+        public void ParamsTrailing()
         {
             var line = new Line("PRIVMSG #channel :hello world");
             CollectionAssert.AreEqual(new List<string> {"#channel", "hello world"}, line.Params);
         }
 
         [TestMethod]
-        public void TestParamsOnlyTrailing()
+        public void ParamsOnlyTrailing()
         {
             var line = new Line("PRIVMSG :hello world");
             CollectionAssert.AreEqual(new List<string> {"hello world"}, line.Params);
         }
 
         [TestMethod]
-        public void TestParamsMissing()
+        public void ParamsMissing()
         {
             var line = new Line("PRIVMSG");
             Assert.AreEqual("PRIVMSG", line.Command);
@@ -106,7 +108,7 @@ namespace IRCTokens.Tests
         }
 
         [TestMethod]
-        public void TestAllTokens()
+        public void AllTokens()
         {
             var line = new Line("@id=123 :nick!user@host PRIVMSG #channel :hello world");
             CollectionAssert.AreEqual(new Dictionary<string, string> {{"id", "123"}}, line.Tags);
@@ -114,5 +116,18 @@ namespace IRCTokens.Tests
             Assert.AreEqual("PRIVMSG", line.Command);
             CollectionAssert.AreEqual(new List<string> {"#channel", "hello world"}, line.Params);
         }
+
+        [TestMethod]
+        public void NulByte()
+        {
+            var decoder = new IRCTokens.StatefulDecoder();
+            var bytes = Encoding.UTF8.GetBytes(":nick!user@host PRIVMSG #channel :hello")
+                .Concat(Encoding.UTF8.GetBytes("\0"))
+                .Concat(Encoding.UTF8.GetBytes("world"))
+                .ToArray();
+            var line = decoder.Push(bytes, bytes.Length).First();
+            
+            CollectionAssert.AreEqual(new List<string> {"#channel", "hello"}, line.Params);
+        }
     }
 }