From 42139ea6cd8d500790bf27c9afc69fd66eab60ad Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 17 Jan 2021 16:40:13 +1300 Subject: [PATCH] Add JsonSelectSettings and regex timeout --- Src/Directory.Build.props | 2 - .../Linq/JsonPath/JPathExecuteTests.cs | 68 ++--------------- .../Newtonsoft.Json.Tests.csproj | 11 +-- Src/Newtonsoft.Json/Linq/JToken.cs | 75 +++++-------------- .../Linq/JsonPath/ArrayIndexFilter.cs | 6 +- .../Linq/JsonPath/ArrayMultipleIndexFilter.cs | 4 +- .../Linq/JsonPath/ArraySliceFilter.cs | 6 +- .../Linq/JsonPath/FieldFilter.cs | 6 +- .../Linq/JsonPath/FieldMultipleFilter.cs | 6 +- Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs | 17 ++--- .../Linq/JsonPath/PathFilter.cs | 10 +-- .../Linq/JsonPath/QueryExpression.cs | 39 +++++----- .../Linq/JsonPath/QueryFilter.cs | 4 +- .../Linq/JsonPath/QueryScanFilter.cs | 6 +- .../Linq/JsonPath/RootFilter.cs | 2 +- .../Linq/JsonPath/ScanFilter.cs | 2 +- .../Linq/JsonPath/ScanMultipleFilter.cs | 2 +- .../Linq/JsonSelectSettings.cs | 28 +++++++ Src/Newtonsoft.Json/Newtonsoft.Json.csproj | 2 +- .../Utilities/MiscellaneousUtils.cs | 5 -- 20 files changed, 108 insertions(+), 193 deletions(-) create mode 100644 Src/Newtonsoft.Json/Linq/JsonSelectSettings.cs diff --git a/Src/Directory.Build.props b/Src/Directory.Build.props index cfaf3745..11ecc418 100644 --- a/Src/Directory.Build.props +++ b/Src/Directory.Build.props @@ -22,7 +22,5 @@ 4.4.0 2.3.1 2.3.1 - 32.0.2 - 5.1.0 diff --git a/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs index 6b3a512e..10611fed 100644 --- a/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs +++ b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs @@ -32,11 +32,7 @@ using System.Numerics; using Newtonsoft.Json.Linq.JsonPath; using Newtonsoft.Json.Tests.Bson; #if HAVE_REGEX_TIMEOUTS -using Bogus; -using Nito.AsyncEx; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Threading; #endif #if DNXCORE50 using Xunit; @@ -81,69 +77,21 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath [Test] public void BacktrackingRegex_SingleMatch_TimeoutRespected() { - var RegexBacktrackingPattern = "(?(.*?))[|].*(?(.*?))[|].*(?(.*?))[|].*(?[1-3])[|].*(?(.*?))[|].*[|].*[|].*(?(.*?))[|].*[|].*(?(.*?))[|].*(?(.*))"; + const string RegexBacktrackingPattern = "(?(.*?))[|].*(?(.*?))[|].*(?(.*?))[|].*(?[1-3])[|].*(?(.*?))[|].*[|].*[|].*(?(.*?))[|].*[|].*(?(.*?))[|].*(?(.*))"; - var faker = new Faker(); var regexBacktrackingData = new JArray(); + regexBacktrackingData.Add(new JObject(new JProperty("b", @"15/04/2020 8:18:03 PM|1|System.String[]|3|Libero eligendi magnam ut inventore.. Quaerat et sit voluptatibus repellendus blanditiis aliquam ut.. Quidem qui ut sint in ex et tempore.|||.\iste.cpp||46018|-1"))); - for (var i = 0; i < 1000; i++) + ExceptionAssert.Throws(() => { - var value = $"{faker.Date.Past()}|1|{faker.Lorem.Words()}|3|{faker.Lorem.Sentences(3, ". ")}|||.\\{faker.Lorem.Word()}.cpp||{faker.Random.UShort()}|-1"; - regexBacktrackingData.Add(new JObject(new JProperty("b", value))); - } - - Xunit.Assert.Throws(() => - { - var tokens = regexBacktrackingData.SelectTokens( + regexBacktrackingData.SelectTokens( $"[?(@.b =~ /{RegexBacktrackingPattern}/)]", - errorWhenNoMatch: false, - singleRegexMatchTimeout: TimeSpan.FromSeconds(.01)).ToArray(); + new JsonSelectSettings + { + RegexMatchTimeout = TimeSpan.FromSeconds(0.01) + }).ToArray(); }); } - - [Test] - public void BacktrackingRegex_GlobalMatch_TimeoutRespected() - { - var faker = new Faker(); - var regexBacktrackingData = new JArray(); - for (var i = 0; i < 1000; i++) - { - var value = $"{faker.Date.Past()}|1|{faker.Lorem.Words()}|3|{faker.Lorem.Sentences(3, ". ")}|||.\\{faker.Lorem.Word()}.cpp||{faker.Random.UShort()}|-1"; - regexBacktrackingData.Add(new JObject(new JProperty("b", value))); - } - var RegexBacktrackingPattern = "(?(.*?))[|].*(?(.*?))[|].*(?(.*?))[|].*(?[1-3])[|].*(?(.*?))[|].*[|].*[|].*(?(.*?))[|].*[|].*(?(.*?))[|].*(?(.*))"; - var jpathExpression = $"[?(@.b =~ /{RegexBacktrackingPattern}/c)]"; - - var exceptionThrow = Xunit.Assert.ThrowsAny(() => - { - var tokens = regexBacktrackingData.SelectTokens( - jpathExpression, - errorWhenNoMatch: false, - singleRegexMatchTimeout: TimeSpan.FromSeconds(2), - globalRegexMatchTimeout: TimeSpan.FromSeconds(.5)).ToArray(); - }); - Assert.IsTrue(exceptionThrow is OperationCanceledException or RegexMatchTimeoutException); - } - - [Test] - public async Task BacktrackingRegexCanBeVerySlowWithoutTimeouts() - { - var RegexBacktrackingPattern = "(?(.*?))[|].*(?(.*?))[|].*(?(.*?))[|].*(?[1-3])[|].*(?(.*?))[|].*[|].*[|].*(?(.*?))[|].*[|].*(?(.*?))[|].*(?(.*))"; - var faker = new Faker(); - var regexBacktrackingData = new JArray(); - - for (var i = 0; i < 10; i++) - { - var value = $"{faker.Date.Past()}|1|{faker.Lorem.Words()}|3|{faker.Lorem.Sentences(3, ". ")}|||.\\{faker.Lorem.Word()}.cpp||{faker.Random.UShort()}|-1"; - regexBacktrackingData.Add(new JObject(new JProperty("b", value))); - } - - var selectTokenTask = Task.Run(() => regexBacktrackingData.SelectTokens($"[?(@.b =~ /{RegexBacktrackingPattern}/)]").ToArray()); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(.3)); - using var ctts = new CancellationTokenTaskSource(cts.Token); - var finishedTask = await Task.WhenAny(selectTokenTask, ctts.Task); - Assert.AreEqual(finishedTask, ctts.Task); - } #endif [Test] diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj index 7989fe9f..c074971d 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj @@ -62,7 +62,7 @@ Json.NET Tests .NETFramework,Version=v4.5 - NET45;HAVE_BENCHMARKS;$(AdditionalConstants) + NET45;HAVE_BENCHMARKS;HAVE_REGEX_TIMEOUTS;$(AdditionalConstants) @@ -76,7 +76,6 @@ - Json.NET Tests .NET 4.0 @@ -122,10 +121,7 @@ - - - Json.NET Tests .NET Standard 2.0 .NETStandard,Version=v2.0 @@ -146,8 +142,6 @@ - - Json.NET Tests .NET Standard 1.3 @@ -168,10 +162,7 @@ - - - Json.NET Tests .NET Standard 1.0 .NETStandard,Version=v1.0 diff --git a/Src/Newtonsoft.Json/Linq/JToken.cs b/Src/Newtonsoft.Json/Linq/JToken.cs index 252f846c..31b7fb36 100644 --- a/Src/Newtonsoft.Json/Linq/JToken.cs +++ b/Src/Newtonsoft.Json/Linq/JToken.cs @@ -44,7 +44,6 @@ using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json.Utilities.LinqBridge; #else using System.Linq; -using System.Threading; #endif namespace Newtonsoft.Json.Linq @@ -2308,7 +2307,7 @@ namespace Newtonsoft.Json.Linq /// A , or null. public JToken? SelectToken(string path) { - return SelectToken(path, false); + return SelectToken(path, settings: null); } /// @@ -2321,38 +2320,27 @@ namespace Newtonsoft.Json.Linq /// A . public JToken? SelectToken(string path, bool errorWhenNoMatch) { - JPath p = new JPath(path); + JsonSelectSettings? settings = errorWhenNoMatch + ? new JsonSelectSettings { ErrorWhenNoMatch = true } + : null; - JToken? token = null; - foreach (JToken t in p.Evaluate(this, this, errorWhenNoMatch)) - { - if (token != null) - { - throw new JsonException("Path returned multiple tokens."); - } - - token = t; - } - - return token; + return SelectToken(path, settings); } -#if HAVE_REGEX_TIMEOUTS /// /// Selects a using a JSONPath expression. Selects the token that matches the object path. /// /// /// A that contains a JSONPath expression. /// - /// A flag to indicate whether an error should be thrown if no tokens are found when evaluating part of the expression. - /// the time after which a single call to regex.ismatch must complete, default is forever + /// The used to select tokens. /// A . - public JToken? SelectToken(string path, bool errorWhenNoMatch, TimeSpan? singleMatchTimeout = default) + public JToken? SelectToken(string path, JsonSelectSettings? settings) { - JPath p = new JPath(path, singleMatchTimeout); + JPath p = new JPath(path); JToken? token = null; - foreach (JToken t in p.Evaluate(this, this, errorWhenNoMatch)) + foreach (JToken t in p.Evaluate(this, this, settings)) { if (token != null) { @@ -2364,7 +2352,6 @@ namespace Newtonsoft.Json.Linq return token; } -#endif /// /// Selects a collection of elements using a JSONPath expression. @@ -2375,7 +2362,7 @@ namespace Newtonsoft.Json.Linq /// An of that contains the selected elements. public IEnumerable SelectTokens(string path) { - return SelectTokens(path, false); + return SelectTokens(path, settings: null); } /// @@ -2388,50 +2375,26 @@ namespace Newtonsoft.Json.Linq /// An of that contains the selected elements. public IEnumerable SelectTokens(string path, bool errorWhenNoMatch) { - var p = new JPath(path); - return p.Evaluate(this, this, errorWhenNoMatch); + JsonSelectSettings? settings = errorWhenNoMatch + ? new JsonSelectSettings { ErrorWhenNoMatch = true } + : null; + + return SelectTokens(path, settings); } -#if HAVE_REGEX_TIMEOUTS /// /// Selects a collection of elements using a JSONPath expression. /// /// /// A that contains a JSONPath expression. /// - /// A flag to indicate whether an error should be thrown if no tokens are found when evaluating part of the expression. - /// - /// for every token that matches a jpath, the time a regex is permitted to run - /// against that token before timeout - /// - /// - /// the time this method should wait for the given jpath regex to match all tokens. - /// worst case expected execution time roughly globalRegexMatchTimeout + Min(singleRegexMatchTimeout, globalRegexMatchTimeout) - /// - /// if single call timeout exceeded" + /// The used to select tokens. /// An of that contains the selected elements. - public IEnumerable SelectTokens(string path, - bool errorWhenNoMatch, - TimeSpan? singleRegexMatchTimeout = default, - TimeSpan? globalRegexMatchTimeout = default) + public IEnumerable SelectTokens(string path, JsonSelectSettings? settings) { - var singleTimeout = singleRegexMatchTimeout ?? Timeout.InfiniteTimeSpan; - var globalTimeout = globalRegexMatchTimeout ?? Timeout.InfiniteTimeSpan; - if (globalTimeout != Timeout.InfiniteTimeSpan && globalTimeout < singleTimeout) - { - singleTimeout = globalTimeout; - } - - using var cts = new CancellationTokenSource(globalTimeout); - var p = new JPath(path, singleTimeout); - var results = p.Evaluate(this, this, errorWhenNoMatch); - foreach (var result in results) - { - cts.Token.ThrowIfCancellationRequested(); - yield return result; - } + var p = new JPath(path); + return p.Evaluate(this, this, settings); } -#endif #if HAVE_DYNAMIC /// diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/ArrayIndexFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/ArrayIndexFilter.cs index 653a2ea6..0378713d 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/ArrayIndexFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/ArrayIndexFilter.cs @@ -8,13 +8,13 @@ namespace Newtonsoft.Json.Linq.JsonPath { public int? Index { get; set; } - public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, bool errorWhenNoMatch) + public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, JsonSelectSettings? settings) { foreach (JToken t in current) { if (Index != null) { - JToken? v = GetTokenIndex(t, errorWhenNoMatch, Index.GetValueOrDefault()); + JToken? v = GetTokenIndex(t, settings, Index.GetValueOrDefault()); if (v != null) { @@ -32,7 +32,7 @@ namespace Newtonsoft.Json.Linq.JsonPath } else { - if (errorWhenNoMatch) + if (settings?.ErrorWhenNoMatch ?? false) { throw new JsonException("Index * not valid on {0}.".FormatWith(CultureInfo.InvariantCulture, t.GetType().Name)); } diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/ArrayMultipleIndexFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/ArrayMultipleIndexFilter.cs index ddf15206..e1492298 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/ArrayMultipleIndexFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/ArrayMultipleIndexFilter.cs @@ -11,13 +11,13 @@ namespace Newtonsoft.Json.Linq.JsonPath Indexes = indexes; } - public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, bool errorWhenNoMatch) + public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, JsonSelectSettings? settings) { foreach (JToken t in current) { foreach (int i in Indexes) { - JToken? v = GetTokenIndex(t, errorWhenNoMatch, i); + JToken? v = GetTokenIndex(t, settings, i); if (v != null) { diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/ArraySliceFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/ArraySliceFilter.cs index e6ee0b7b..c948b5e0 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/ArraySliceFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/ArraySliceFilter.cs @@ -11,7 +11,7 @@ namespace Newtonsoft.Json.Linq.JsonPath public int? End { get; set; } public int? Step { get; set; } - public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, bool errorWhenNoMatch) + public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, JsonSelectSettings? settings) { if (Step == 0) { @@ -56,7 +56,7 @@ namespace Newtonsoft.Json.Linq.JsonPath } else { - if (errorWhenNoMatch) + if (settings?.ErrorWhenNoMatch ?? false) { throw new JsonException("Array slice of {0} to {1} returned no results.".FormatWith(CultureInfo.InvariantCulture, Start != null ? Start.GetValueOrDefault().ToString(CultureInfo.InvariantCulture) : "*", @@ -66,7 +66,7 @@ namespace Newtonsoft.Json.Linq.JsonPath } else { - if (errorWhenNoMatch) + if (settings?.ErrorWhenNoMatch ?? false) { throw new JsonException("Array slice is not valid on {0}.".FormatWith(CultureInfo.InvariantCulture, t.GetType().Name)); } diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/FieldFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/FieldFilter.cs index d1374e5a..97f63816 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/FieldFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/FieldFilter.cs @@ -13,7 +13,7 @@ namespace Newtonsoft.Json.Linq.JsonPath Name = name; } - public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, bool errorWhenNoMatch) + public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, JsonSelectSettings? settings) { foreach (JToken t in current) { @@ -27,7 +27,7 @@ namespace Newtonsoft.Json.Linq.JsonPath { yield return v; } - else if (errorWhenNoMatch) + else if (settings?.ErrorWhenNoMatch ?? false) { throw new JsonException("Property '{0}' does not exist on JObject.".FormatWith(CultureInfo.InvariantCulture, Name)); } @@ -42,7 +42,7 @@ namespace Newtonsoft.Json.Linq.JsonPath } else { - if (errorWhenNoMatch) + if (settings?.ErrorWhenNoMatch ?? false) { throw new JsonException("Property '{0}' not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, Name ?? "*", t.GetType().Name)); } diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/FieldMultipleFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/FieldMultipleFilter.cs index 20cd0688..c3eb7033 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/FieldMultipleFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/FieldMultipleFilter.cs @@ -18,7 +18,7 @@ namespace Newtonsoft.Json.Linq.JsonPath Names = names; } - public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, bool errorWhenNoMatch) + public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, JsonSelectSettings? settings) { foreach (JToken t in current) { @@ -33,7 +33,7 @@ namespace Newtonsoft.Json.Linq.JsonPath yield return v; } - if (errorWhenNoMatch) + if (settings?.ErrorWhenNoMatch ?? false) { throw new JsonException("Property '{0}' does not exist on JObject.".FormatWith(CultureInfo.InvariantCulture, name)); } @@ -41,7 +41,7 @@ namespace Newtonsoft.Json.Linq.JsonPath } else { - if (errorWhenNoMatch) + if (settings?.ErrorWhenNoMatch ?? false) { throw new JsonException("Properties {0} not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, string.Join(", ", Names.Select(n => "'" + n + "'") #if !HAVE_STRING_JOIN_WITH_ENUMERABLE diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs b/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs index d5bc4e89..c29fcd06 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs @@ -27,8 +27,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Text; -using System.Text.RegularExpressions; -using System.Threading; using Newtonsoft.Json.Utilities; namespace Newtonsoft.Json.Linq.JsonPath @@ -39,16 +37,14 @@ namespace Newtonsoft.Json.Linq.JsonPath private readonly string _expression; public List Filters { get; } - public readonly TimeSpan? RegexSingleMatchTimeout = default; private int _currentIndex; - public JPath(string expression, TimeSpan? regexSingleMatchTimeout = default) + public JPath(string expression) { ValidationUtils.ArgumentNotNull(expression, nameof(expression)); _expression = expression; Filters = new List(); - RegexSingleMatchTimeout = regexSingleMatchTimeout; ParseMain(); } @@ -513,8 +509,7 @@ namespace Newtonsoft.Json.Linq.JsonPath right = ParseSide(); } - - var booleanExpression = new BooleanQueryExpression(op, left, right, RegexSingleMatchTimeout); + BooleanQueryExpression booleanExpression = new BooleanQueryExpression(op, left, right); if (_expression[_currentIndex] == ')') { @@ -879,17 +874,17 @@ namespace Newtonsoft.Json.Linq.JsonPath } } - internal IEnumerable Evaluate(JToken root, JToken t, bool errorWhenNoMatch) + internal IEnumerable Evaluate(JToken root, JToken t, JsonSelectSettings? settings) { - return Evaluate(Filters, root, t, errorWhenNoMatch); + return Evaluate(Filters, root, t, settings); } - internal static IEnumerable Evaluate(List filters, JToken root, JToken t, bool errorWhenNoMatch) + internal static IEnumerable Evaluate(List filters, JToken root, JToken t, JsonSelectSettings? settings) { IEnumerable current = new[] { t }; foreach (PathFilter filter in filters) { - current = filter.ExecuteFilter(root, current, errorWhenNoMatch); + current = filter.ExecuteFilter(root, current, settings); } return current; diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/PathFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/PathFilter.cs index abba44ee..45a1683e 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/PathFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/PathFilter.cs @@ -6,15 +6,15 @@ namespace Newtonsoft.Json.Linq.JsonPath { internal abstract class PathFilter { - public abstract IEnumerable ExecuteFilter(JToken root, IEnumerable current, bool errorWhenNoMatch); + public abstract IEnumerable ExecuteFilter(JToken root, IEnumerable current, JsonSelectSettings? settings); - protected static JToken? GetTokenIndex(JToken t, bool errorWhenNoMatch, int index) + protected static JToken? GetTokenIndex(JToken t, JsonSelectSettings? settings, int index) { if (t is JArray a) { if (a.Count <= index) { - if (errorWhenNoMatch) + if (settings?.ErrorWhenNoMatch ?? false) { throw new JsonException("Index {0} outside the bounds of JArray.".FormatWith(CultureInfo.InvariantCulture, index)); } @@ -28,7 +28,7 @@ namespace Newtonsoft.Json.Linq.JsonPath { if (c.Count <= index) { - if (errorWhenNoMatch) + if (settings?.ErrorWhenNoMatch ?? false) { throw new JsonException("Index {0} outside the bounds of JConstructor.".FormatWith(CultureInfo.InvariantCulture, index)); } @@ -40,7 +40,7 @@ namespace Newtonsoft.Json.Linq.JsonPath } else { - if (errorWhenNoMatch) + if (settings?.ErrorWhenNoMatch ?? false) { throw new JsonException("Index {0} not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, index, t.GetType().Name)); } diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs b/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs index f9aa84d1..1ab97041 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.IO; using System.Text.RegularExpressions; using System.Diagnostics; -using System.Threading; #if !HAVE_LINQ using Newtonsoft.Json.Utilities.LinqBridge; #else @@ -40,7 +39,13 @@ namespace Newtonsoft.Json.Linq.JsonPath Operator = @operator; } - public abstract bool IsMatch(JToken root, JToken t); + // 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 @@ -52,14 +57,14 @@ namespace Newtonsoft.Json.Linq.JsonPath Expressions = new List(); } - public override bool IsMatch(JToken root, JToken t) + 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)) + if (!e.IsMatch(root, t, settings)) { return false; } @@ -68,7 +73,7 @@ namespace Newtonsoft.Json.Linq.JsonPath case QueryOperator.Or: foreach (QueryExpression e in Expressions) { - if (e.IsMatch(root, t)) + if (e.IsMatch(root, t, settings)) { return true; } @@ -84,17 +89,11 @@ namespace Newtonsoft.Json.Linq.JsonPath { public readonly object Left; public readonly object? Right; - public readonly TimeSpan? SingleMatchTimeout; - public BooleanQueryExpression(QueryOperator @operator, - object left, - object? right, - TimeSpan? regexSingleMatchTimeout = default) - : base(@operator) + public BooleanQueryExpression(QueryOperator @operator, object left, object? right) : base(@operator) { Left = left; Right = right; - SingleMatchTimeout = regexSingleMatchTimeout; } private IEnumerable GetResult(JToken root, JToken t, object? o) @@ -106,13 +105,13 @@ namespace Newtonsoft.Json.Linq.JsonPath if (o is List pathFilters) { - return JPath.Evaluate(pathFilters, root, t, false); + return JPath.Evaluate(pathFilters, root, t, null); } return CollectionUtils.ArrayEmpty(); } - public override bool IsMatch(JToken root, JToken t) + public override bool IsMatch(JToken root, JToken t, JsonSelectSettings? settings) { if (Operator == QueryOperator.Exists) { @@ -131,7 +130,7 @@ namespace Newtonsoft.Json.Linq.JsonPath JToken leftResult = leftResults.Current; foreach (JToken rightResult in rightResults) { - if (MatchTokens(leftResult, rightResult)) + if (MatchTokens(leftResult, rightResult, settings)) { return true; } @@ -143,14 +142,14 @@ namespace Newtonsoft.Json.Linq.JsonPath return false; } - private bool MatchTokens(JToken leftResult, JToken rightResult) + 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, SingleMatchTimeout)) + if (RegexEquals(leftValue, rightValue, settings)) { return true; } @@ -222,9 +221,7 @@ namespace Newtonsoft.Json.Linq.JsonPath return false; } - private static bool RegexEquals(JValue input, - JValue pattern, - TimeSpan? regexMatchTimeout = default) + private static bool RegexEquals(JValue input, JValue pattern, JsonSelectSettings? settings) { if (input.Type != JTokenType.String || pattern.Type != JTokenType.String) { @@ -238,7 +235,7 @@ namespace Newtonsoft.Json.Linq.JsonPath string optionsText = regexText.Substring(patternOptionDelimiterIndex + 1); #if HAVE_REGEX_TIMEOUTS - var timeout = regexMatchTimeout ?? Regex.InfiniteMatchTimeout; + 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)); diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/QueryFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/QueryFilter.cs index c528623f..e844e113 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/QueryFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/QueryFilter.cs @@ -12,13 +12,13 @@ namespace Newtonsoft.Json.Linq.JsonPath Expression = expression; } - public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, bool errorWhenNoMatch) + public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, JsonSelectSettings? settings) { foreach (JToken t in current) { foreach (JToken v in t) { - if (Expression.IsMatch(root, v)) + if (Expression.IsMatch(root, v, settings)) { yield return v; } diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/QueryScanFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/QueryScanFilter.cs index 42237671..a1b5270a 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/QueryScanFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/QueryScanFilter.cs @@ -12,7 +12,7 @@ namespace Newtonsoft.Json.Linq.JsonPath Expression = expression; } - public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, bool errorWhenNoMatch) + public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, JsonSelectSettings? settings) { foreach (JToken t in current) { @@ -20,7 +20,7 @@ namespace Newtonsoft.Json.Linq.JsonPath { foreach (JToken d in c.DescendantsAndSelf()) { - if (Expression.IsMatch(root, d)) + if (Expression.IsMatch(root, d, settings)) { yield return d; } @@ -28,7 +28,7 @@ namespace Newtonsoft.Json.Linq.JsonPath } else { - if (Expression.IsMatch(root, t)) + if (Expression.IsMatch(root, t, settings)) { yield return t; } diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/RootFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/RootFilter.cs index 00f5b725..164ba0c0 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/RootFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/RootFilter.cs @@ -10,7 +10,7 @@ namespace Newtonsoft.Json.Linq.JsonPath { } - public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, bool errorWhenNoMatch) + public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, JsonSelectSettings? settings) { return new[] { root }; } diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/ScanFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/ScanFilter.cs index 1938c1b0..a3f6adcc 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/ScanFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/ScanFilter.cs @@ -11,7 +11,7 @@ namespace Newtonsoft.Json.Linq.JsonPath Name = name; } - public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, bool errorWhenNoMatch) + public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, JsonSelectSettings? settings) { foreach (JToken c in current) { diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/ScanMultipleFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/ScanMultipleFilter.cs index ff9a560a..40e9334a 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/ScanMultipleFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/ScanMultipleFilter.cs @@ -11,7 +11,7 @@ namespace Newtonsoft.Json.Linq.JsonPath _names = names; } - public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, bool errorWhenNoMatch) + public override IEnumerable ExecuteFilter(JToken root, IEnumerable current, JsonSelectSettings? settings) { foreach (JToken c in current) { diff --git a/Src/Newtonsoft.Json/Linq/JsonSelectSettings.cs b/Src/Newtonsoft.Json/Linq/JsonSelectSettings.cs new file mode 100644 index 00000000..495b2dc0 --- /dev/null +++ b/Src/Newtonsoft.Json/Linq/JsonSelectSettings.cs @@ -0,0 +1,28 @@ +using System; + +namespace Newtonsoft.Json.Linq +{ + /// + /// Specifies the settings used when selecting JSON. + /// + public class JsonSelectSettings + { +#if HAVE_REGEX_TIMEOUTS + /// + /// Gets or sets a timeout that will be used when executing regular expressions. + /// + /// The timeout that will be used when executing regular expressions. + public TimeSpan? RegexMatchTimeout { get; set; } +#endif + + /// + /// Gets or sets a flag that indicates whether an error should be thrown if + /// no tokens are found when evaluating part of the expression. + /// + /// + /// A flag that indicates whether an error should be thrown if + /// no tokens are found when evaluating part of the expression. + /// + public bool ErrorWhenNoMatch { get; set; } + } +} diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.csproj index f6cc6cc7..dfac4a04 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.csproj @@ -45,7 +45,7 @@ Json.NET - HAVE_ADO_NET;HAVE_APP_DOMAIN;HAVE_ASYNC;HAVE_BIG_INTEGER;HAVE_BINARY_FORMATTER;HAVE_BINARY_SERIALIZATION;HAVE_BINARY_EXCEPTION_SERIALIZATION;HAVE_CAS;HAVE_CHAR_TO_LOWER_WITH_CULTURE;HAVE_CHAR_TO_STRING_WITH_CULTURE;HAVE_COM_ATTRIBUTES;HAVE_COMPONENT_MODEL;HAVE_CONCURRENT_COLLECTIONS;HAVE_COVARIANT_GENERICS;HAVE_DATA_CONTRACTS;HAVE_DATE_TIME_OFFSET;HAVE_DB_NULL_TYPE_CODE;HAVE_DYNAMIC;HAVE_EMPTY_TYPES;HAVE_ENTITY_FRAMEWORK;HAVE_EXPRESSIONS;HAVE_FAST_REVERSE;HAVE_FSHARP_TYPES;HAVE_FULL_REFLECTION;HAVE_GUID_TRY_PARSE;HAVE_HASH_SET;HAVE_ICLONEABLE;HAVE_ICONVERTIBLE;HAVE_IGNORE_DATA_MEMBER_ATTRIBUTE;HAVE_INOTIFY_COLLECTION_CHANGED;HAVE_INOTIFY_PROPERTY_CHANGING;HAVE_ISET;HAVE_LINQ;HAVE_MEMORY_BARRIER;HAVE_METHOD_IMPL_ATTRIBUTE;HAVE_NON_SERIALIZED_ATTRIBUTE;HAVE_READ_ONLY_COLLECTIONS;HAVE_REFLECTION_EMIT;HAVE_SECURITY_SAFE_CRITICAL_ATTRIBUTE;HAVE_SERIALIZATION_BINDER_BIND_TO_NAME;HAVE_STREAM_READER_WRITER_CLOSE;HAVE_STRING_JOIN_WITH_ENUMERABLE;HAVE_TIME_SPAN_PARSE_WITH_CULTURE;HAVE_TIME_SPAN_TO_STRING_WITH_CULTURE;HAVE_TIME_ZONE_INFO;HAVE_TRACE_WRITER;HAVE_TYPE_DESCRIPTOR;HAVE_UNICODE_SURROGATE_DETECTION;HAVE_VARIANT_TYPE_PARAMETERS;HAVE_VERSION_TRY_PARSE;HAVE_XLINQ;HAVE_XML_DOCUMENT;HAVE_XML_DOCUMENT_TYPE;HAVE_CONCURRENT_DICTIONARY;$(AdditionalConstants) + HAVE_ADO_NET;HAVE_APP_DOMAIN;HAVE_ASYNC;HAVE_BIG_INTEGER;HAVE_BINARY_FORMATTER;HAVE_BINARY_SERIALIZATION;HAVE_BINARY_EXCEPTION_SERIALIZATION;HAVE_CAS;HAVE_CHAR_TO_LOWER_WITH_CULTURE;HAVE_CHAR_TO_STRING_WITH_CULTURE;HAVE_COM_ATTRIBUTES;HAVE_COMPONENT_MODEL;HAVE_CONCURRENT_COLLECTIONS;HAVE_COVARIANT_GENERICS;HAVE_DATA_CONTRACTS;HAVE_DATE_TIME_OFFSET;HAVE_DB_NULL_TYPE_CODE;HAVE_DYNAMIC;HAVE_EMPTY_TYPES;HAVE_ENTITY_FRAMEWORK;HAVE_EXPRESSIONS;HAVE_FAST_REVERSE;HAVE_FSHARP_TYPES;HAVE_FULL_REFLECTION;HAVE_GUID_TRY_PARSE;HAVE_HASH_SET;HAVE_ICLONEABLE;HAVE_ICONVERTIBLE;HAVE_IGNORE_DATA_MEMBER_ATTRIBUTE;HAVE_INOTIFY_COLLECTION_CHANGED;HAVE_INOTIFY_PROPERTY_CHANGING;HAVE_ISET;HAVE_LINQ;HAVE_MEMORY_BARRIER;HAVE_METHOD_IMPL_ATTRIBUTE;HAVE_NON_SERIALIZED_ATTRIBUTE;HAVE_READ_ONLY_COLLECTIONS;HAVE_REFLECTION_EMIT;HAVE_REGEX_TIMEOUTS;HAVE_SECURITY_SAFE_CRITICAL_ATTRIBUTE;HAVE_SERIALIZATION_BINDER_BIND_TO_NAME;HAVE_STREAM_READER_WRITER_CLOSE;HAVE_STRING_JOIN_WITH_ENUMERABLE;HAVE_TIME_SPAN_PARSE_WITH_CULTURE;HAVE_TIME_SPAN_TO_STRING_WITH_CULTURE;HAVE_TIME_ZONE_INFO;HAVE_TRACE_WRITER;HAVE_TYPE_DESCRIPTOR;HAVE_UNICODE_SURROGATE_DETECTION;HAVE_VARIANT_TYPE_PARAMETERS;HAVE_VERSION_TRY_PARSE;HAVE_XLINQ;HAVE_XML_DOCUMENT;HAVE_XML_DOCUMENT_TYPE;HAVE_CONCURRENT_DICTIONARY;$(AdditionalConstants) Json.NET .NET 4.0 diff --git a/Src/Newtonsoft.Json/Utilities/MiscellaneousUtils.cs b/Src/Newtonsoft.Json/Utilities/MiscellaneousUtils.cs index d61ea5d7..137ec445 100644 --- a/Src/Newtonsoft.Json/Utilities/MiscellaneousUtils.cs +++ b/Src/Newtonsoft.Json/Utilities/MiscellaneousUtils.cs @@ -165,11 +165,6 @@ namespace Newtonsoft.Json.Utilities case 'x': options |= RegexOptions.ExplicitCapture; break; -#if HAVE_REGEX_TIMEOUTS && (NETSTANDARD1_3 || NETSTANDARD2_0) - case 'c': - options |= RegexOptions.Compiled; - break; -#endif } }