From 489764c5883d4428060c76be457e783301706f90 Mon Sep 17 00:00:00 2001 From: Artemis Kearney Date: Wed, 14 Jul 2021 15:40:45 -0700 Subject: [PATCH] add getPath to DictionaryExtensions also changes all DictionaryExtensions methods to work on IDictionary --- CustomJSONData/Utils/DictionaryExtensions.cs | 146 ++++++++++++++++++- 1 file changed, 144 insertions(+), 2 deletions(-) diff --git a/CustomJSONData/Utils/DictionaryExtensions.cs b/CustomJSONData/Utils/DictionaryExtensions.cs index de2ea64..de52531 100644 --- a/CustomJSONData/Utils/DictionaryExtensions.cs +++ b/CustomJSONData/Utils/DictionaryExtensions.cs @@ -2,15 +2,19 @@ { using System; using System.Collections.Generic; + using System.Text.RegularExpressions; + using System.Linq; + using System.Runtime.CompilerServices; + using IPA.Logging; public static class DictionaryExtensions { - public static Dictionary Copy(this Dictionary dictionary) + public static IDictionary Copy(this IDictionary dictionary) { return dictionary != null ? new Dictionary(dictionary) : new Dictionary(); } - public static T Get(this Dictionary dictionary, string key) + public static T Get(this IDictionary dictionary, string key) { if (dictionary.TryGetValue(key, out object value)) { @@ -34,5 +38,143 @@ public static T Get(this Dictionary dictionary, string key) return default; } + public static readonly Regex dotRegex = new Regex( + @"\.(?# literal . + )((?# capture the identifier + )[$_\p{L}](?# $, _, or any Unicode letter + )[$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\u200C\u200D]*(?# any character allowed after the first character of an identifier, any number of times + ))", RegexOptions.Compiled); + public static readonly Regex bracketRegex = new Regex( + @"\[(?# literal [ + )(?:(?# first option: a quoted string + )(?['""])(?# the starting quote for the string + )(?(?# capture the string's contents as id + )(?:(?# either... + )(?:(?# ... a normal character ... + )(?!\k)(?# negative lookahead; asserts the next character isn't the closing quote + )[^\\](?# any character but a backslash + ))|(?:(?# ... or an escaped quote ... + )\\(?# literal \ + )\k(?# the same type of quote that started the string + ))|(?:(?# ... or an escaped backslash ... + )\\\\(?# two literal \s + ))(?# + ))*(?# ... any number of times + ))(?# done capturing id + )\k(?# closing quote + ))|(?# second option: any number of digits + )(?(?# capture the digits as id + )\p{Nd}(?# Unicode decimal digit)+(?# one or more times + ))(?# done capturing id + )\](?# literal ])", RegexOptions.Compiled); + /// + /// Retrieves a value using a sequence of JS accesses of the following forms: + /// - ".identifier", where identifier is a valid JS identifier (not excluding reserved words) + /// - "['key']" / "[\"key\"]", where key is any sequence of characters (with quotes matching the surrounding quotes backslash-escaped) + /// - "[index]", where index is a sequence of digits + /// For example, ".foo['bar \\' baz'].qux[\"quux\"][3]". + /// Descends through both objects (assumed to be s) and arrays (assumed to be s). + /// For arrays, numeric indices and string indices are both parsed with . + /// Returns default() if this parse fails while indexing an array, if a nonfinal path step is neither an object nor an array, + /// or if the value at the path can't be converted to . + /// An empty resolves to itself. + /// If is nonempty and starts with a character other than '.' or '[', a '.' is added. + /// + /// The type of the value to retrieve. + /// The object to retrieve a value from. + /// The path to follow within + /// The value of type at within , if present. + public static T getPath(this IDictionary dictionary, string path) + { + if (path == "") + { + if (dictionary is IConvertible) + { + return (T)Convert.ChangeType(dictionary, typeof(T)); + } + else + { + return (T)dictionary; + } + } + // allow omitting starting . + if (!path.StartsWith(".") && !path.StartsWith("[")) path = $".{path}"; + // replace all . accesses with [''] accesses + path = dotRegex.Replace(path, match => $"['{match.Groups[0]}']"); + // extract keys from [''] / [""] / [] (numeric) accesses + var segments = bracketRegex.Matches(path) + .Cast() + .Select(m => m.Groups["quote"].Success + ? m.Groups["id"].Value + .Replace($"\\{m.Groups["quote"].Value}", m.Groups["quote"].Value) // if quotes were used, unescape escaped quotes + .Replace(@"\\", @"\") // and escaped backslashes + : m.Groups["id"].Value) // otherwise, nothing to unescape + .ToList(); + + // validate path + var badSegments = bracketRegex.Replace(path, m => "\uE000") // replace all valid bracket accesses with a Unicode private use character + .Split('\uE000') // split on that private use character + .Where(seg => seg != "") // remove all empty segments, leaving only non-empty segments that weren't valid bracket accesses + .ToList(); + if (badSegments.Count > 0) + { + Logger.Log($"Warning: getPath path \"{path}\" contains bad segments:", level: IPA.Logging.Logger.Level.Warning); + foreach (var seg in badSegments) + { + Logger.Log($" {seg}", level: IPA.Logging.Logger.Level.Warning); + } + Logger.Log("Pretending they don't exist, and probably accessing a bad path.", level: IPA.Logging.Logger.Level.Warning); + } + + // paths can pass through both objects and arrays + List array = null; + // for each segment before the last, descend into the object/array at that path + for (int i = 0; i < (segments.Count - 1); i++) + { + if (dictionary != null) + { + array = dictionary.Get>(segments[i]); + dictionary = dictionary.Get>(segments[i]); + } + else if (array != null && int.TryParse(segments[i], out int index)) + { + dictionary = array[index] as IDictionary; + array = array[index] as List; + } + else return default; + } + // we're at the last segment; + if (dictionary != null) + { + return dictionary.Get(segments[segments.Count - 1]); + } + else if (array != null && int.TryParse(segments[segments.Count - 1], out int index_)) + { + object value = array[index_]; + Type underlyingType = Nullable.GetUnderlyingType(typeof(T)); + if (underlyingType != null) + { + return (T)Convert.ChangeType(value, underlyingType); + } + else + { + if (value is IConvertible) + { + return (T)Convert.ChangeType(value, typeof(T)); + } + else + { + return (T)value; + } + } + } + else return default; + } + /// + /// The UpperCamelCase form of + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T GetPath(this IDictionary dictionary, string path) => dictionary.getPath(path); } }