about summary refs log tree commit diff
path: root/IrcTokens/Line.cs
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 /IrcTokens/Line.cs
parent933a4f85604e21445c9bac8272d64cf3e6f65e00 (diff)
tidy up, work on stateful
Diffstat (limited to 'IrcTokens/Line.cs')
-rw-r--r--IrcTokens/Line.cs194
1 files changed, 113 insertions, 81 deletions
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);
             }