diff options
Diffstat (limited to 'IrcTokens/Line.cs')
-rw-r--r-- | IrcTokens/Line.cs | 194 |
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); } |