using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text.RegularExpressions; using System.Diagnostics; #if !HAVE_LINQ using LC.Newtonsoft.Json.Utilities.LinqBridge; #else using System.Linq; #endif using LC.Newtonsoft.Json.Utilities; namespace LC.Newtonsoft.Json.Linq.JsonPath { internal enum QueryOperator { None = 0, Equals = 1, NotEquals = 2, Exists = 3, LessThan = 4, LessThanOrEquals = 5, GreaterThan = 6, GreaterThanOrEquals = 7, And = 8, Or = 9, RegexEquals = 10, StrictEquals = 11, StrictNotEquals = 12 } internal abstract class QueryExpression { internal QueryOperator Operator; public QueryExpression(QueryOperator @operator) { Operator = @operator; } // For unit tests public bool IsMatch(JToken root, JToken t) { return IsMatch(root, t, null); } public abstract bool IsMatch(JToken root, JToken t, JsonSelectSettings? settings); } internal class CompositeExpression : QueryExpression { public List Expressions { get; set; } public CompositeExpression(QueryOperator @operator) : base(@operator) { Expressions = new List(); } public override bool IsMatch(JToken root, JToken t, JsonSelectSettings? settings) { switch (Operator) { case QueryOperator.And: foreach (QueryExpression e in Expressions) { if (!e.IsMatch(root, t, settings)) { return false; } } return true; case QueryOperator.Or: foreach (QueryExpression e in Expressions) { if (e.IsMatch(root, t, settings)) { return true; } } return false; default: throw new ArgumentOutOfRangeException(); } } } internal class BooleanQueryExpression : QueryExpression { public readonly object Left; public readonly object? Right; public BooleanQueryExpression(QueryOperator @operator, object left, object? right) : base(@operator) { Left = left; Right = right; } private IEnumerable GetResult(JToken root, JToken t, object? o) { if (o is JToken resultToken) { return new[] { resultToken }; } if (o is List pathFilters) { return JPath.Evaluate(pathFilters, root, t, null); } return CollectionUtils.ArrayEmpty(); } public override bool IsMatch(JToken root, JToken t, JsonSelectSettings? settings) { if (Operator == QueryOperator.Exists) { return GetResult(root, t, Left).Any(); } using (IEnumerator leftResults = GetResult(root, t, Left).GetEnumerator()) { if (leftResults.MoveNext()) { IEnumerable rightResultsEn = GetResult(root, t, Right); ICollection rightResults = rightResultsEn as ICollection ?? rightResultsEn.ToList(); do { JToken leftResult = leftResults.Current; foreach (JToken rightResult in rightResults) { if (MatchTokens(leftResult, rightResult, settings)) { return true; } } } while (leftResults.MoveNext()); } } return false; } private bool MatchTokens(JToken leftResult, JToken rightResult, JsonSelectSettings? settings) { if (leftResult is JValue leftValue && rightResult is JValue rightValue) { switch (Operator) { case QueryOperator.RegexEquals: if (RegexEquals(leftValue, rightValue, settings)) { return true; } break; case QueryOperator.Equals: if (EqualsWithStringCoercion(leftValue, rightValue)) { return true; } break; case QueryOperator.StrictEquals: if (EqualsWithStrictMatch(leftValue, rightValue)) { return true; } break; case QueryOperator.NotEquals: if (!EqualsWithStringCoercion(leftValue, rightValue)) { return true; } break; case QueryOperator.StrictNotEquals: if (!EqualsWithStrictMatch(leftValue, rightValue)) { return true; } break; case QueryOperator.GreaterThan: if (leftValue.CompareTo(rightValue) > 0) { return true; } break; case QueryOperator.GreaterThanOrEquals: if (leftValue.CompareTo(rightValue) >= 0) { return true; } break; case QueryOperator.LessThan: if (leftValue.CompareTo(rightValue) < 0) { return true; } break; case QueryOperator.LessThanOrEquals: if (leftValue.CompareTo(rightValue) <= 0) { return true; } break; case QueryOperator.Exists: return true; } } else { switch (Operator) { case QueryOperator.Exists: // you can only specify primitive types in a comparison // notequals will always be true case QueryOperator.NotEquals: return true; } } return false; } private static bool RegexEquals(JValue input, JValue pattern, JsonSelectSettings? settings) { if (input.Type != JTokenType.String || pattern.Type != JTokenType.String) { return false; } string regexText = (string)pattern.Value!; int patternOptionDelimiterIndex = regexText.LastIndexOf('/'); string patternText = regexText.Substring(1, patternOptionDelimiterIndex - 1); string optionsText = regexText.Substring(patternOptionDelimiterIndex + 1); #if HAVE_REGEX_TIMEOUTS TimeSpan timeout = settings?.RegexMatchTimeout ?? Regex.InfiniteMatchTimeout; return Regex.IsMatch((string)input.Value!, patternText, MiscellaneousUtils.GetRegexOptions(optionsText), timeout); #else return Regex.IsMatch((string)input.Value!, patternText, MiscellaneousUtils.GetRegexOptions(optionsText)); #endif } internal static bool EqualsWithStringCoercion(JValue value, JValue queryValue) { if (value.Equals(queryValue)) { return true; } // Handle comparing an integer with a float // e.g. Comparing 1 and 1.0 if ((value.Type == JTokenType.Integer && queryValue.Type == JTokenType.Float) || (value.Type == JTokenType.Float && queryValue.Type == JTokenType.Integer)) { return JValue.Compare(value.Type, value.Value, queryValue.Value) == 0; } if (queryValue.Type != JTokenType.String) { return false; } string queryValueString = (string)queryValue.Value!; string currentValueString; // potential performance issue with converting every value to string? switch (value.Type) { case JTokenType.Date: using (StringWriter writer = StringUtils.CreateStringWriter(64)) { #if HAVE_DATE_TIME_OFFSET if (value.Value is DateTimeOffset offset) { DateTimeUtils.WriteDateTimeOffsetString(writer, offset, DateFormatHandling.IsoDateFormat, null, CultureInfo.InvariantCulture); } else #endif { DateTimeUtils.WriteDateTimeString(writer, (DateTime)value.Value!, DateFormatHandling.IsoDateFormat, null, CultureInfo.InvariantCulture); } currentValueString = writer.ToString(); } break; case JTokenType.Bytes: currentValueString = Convert.ToBase64String((byte[])value.Value!); break; case JTokenType.Guid: case JTokenType.TimeSpan: currentValueString = value.Value!.ToString(); break; case JTokenType.Uri: currentValueString = ((Uri)value.Value!).OriginalString; break; default: return false; } return string.Equals(currentValueString, queryValueString, StringComparison.Ordinal); } internal static bool EqualsWithStrictMatch(JValue value, JValue queryValue) { MiscellaneousUtils.Assert(value != null); MiscellaneousUtils.Assert(queryValue != null); // Handle comparing an integer with a float // e.g. Comparing 1 and 1.0 if ((value.Type == JTokenType.Integer && queryValue.Type == JTokenType.Float) || (value.Type == JTokenType.Float && queryValue.Type == JTokenType.Integer)) { return JValue.Compare(value.Type, value.Value, queryValue.Value) == 0; } // we handle floats and integers the exact same way, so they are pseudo equivalent if (value.Type != queryValue.Type) { return false; } return value.Equals(queryValue); } } }