using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; public class TwitchMessageTags : IDictionary { public Dictionary Tags = new(); public TwitchMessageTags() { } public TwitchMessageTags(TwitchMessageTags other) { Tags = new(other); } private enum ParseState { FindingKey, FindingValue, ValueEscaped, } //TODO this should be unit tested /// /// /// /// /// public static TwitchMessageTags Parse(string s) { s = s.TrimStart('@'); TwitchMessageTags tags = new(); string key = ""; string value = ""; var state = ParseState.FindingKey; foreach (char c in s) { switch (state) { case ParseState.FindingKey: if (c == '=') state = ParseState.FindingValue; else if (c == ';') { state = ParseState.FindingKey; tags.Add(key, ""); key = ""; } else if (c == ' ') { tags.Add(key, ""); goto EndParse; } else key += c; break; case ParseState.FindingValue: if (c == '\\') { state = ParseState.ValueEscaped; } else if (c == ';') { tags.Add(key, value); key = value = ""; state = ParseState.FindingKey; } else if (c == ' ') { tags.Add(key, value); goto EndParse; } else if ("\r\n\0".Contains(c)) throw new ArgumentException("Invalid character in tag string", nameof(s)); else { value += c; } break; case ParseState.ValueEscaped: if (c == ':') { value += ';'; state = ParseState.FindingValue; } else if (c == 's') { value += ' '; state = ParseState.FindingValue; } else if (c == '\\') { value += '\\'; state = ParseState.FindingValue; } else if (c == 'r') { value += '\r'; state = ParseState.FindingValue; } else if (c == 'n') { value += '\n'; state = ParseState.FindingValue; } else if (c == ';') { tags.Add(key, value); key = value = ""; state = ParseState.FindingKey; } //spaces should already be stripped, but handle this as end of tags just in case else if (c == ' ') { tags.Add(key, value); key = value = ""; goto EndParse; } else if ("\r\n\0".Contains(c)) throw new ArgumentException("Invalid character in tag string", nameof(s)); else { value += c; state = ParseState.FindingValue; } break; default: throw new InvalidEnumArgumentException("Invalid state enum"); } } //this is reached after processing the last character without hitting a space tags.Add(key, value); EndParse: return tags; } #region IDictionary public string this[string key] { get => ((IDictionary)Tags)[key]; set => ((IDictionary)Tags)[key] = value; } public ICollection Keys => ((IDictionary)Tags).Keys; public ICollection Values => ((IDictionary)Tags).Values; public int Count => ((ICollection>)Tags).Count; public bool IsReadOnly => ((ICollection>)Tags).IsReadOnly; public void Add(string key, string value) { ((IDictionary)Tags).Add(key, value); } public void Add(KeyValuePair item) { ((ICollection>)Tags).Add(item); } public void Clear() { ((ICollection>)Tags).Clear(); } public bool Contains(KeyValuePair item) { return ((ICollection>)Tags).Contains(item); } public bool ContainsKey(string key) { return ((IDictionary)Tags).ContainsKey(key); } public void CopyTo(KeyValuePair[] array, int arrayIndex) { ((ICollection>)Tags).CopyTo(array, arrayIndex); } public IEnumerator> GetEnumerator() { return ((IEnumerable>)Tags).GetEnumerator(); } public bool Remove(string key) { return ((IDictionary)Tags).Remove(key); } public bool Remove(KeyValuePair item) { return ((ICollection>)Tags).Remove(item); } public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) { return ((IDictionary)Tags).TryGetValue(key, out value); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)Tags).GetEnumerator(); } #endregion //IDictionary }