diff --git a/ColorCode.Core/Compilation/Languages/Json.cs b/ColorCode.Core/Compilation/Languages/Json.cs index ab568d9..b5c5518 100644 --- a/ColorCode.Core/Compilation/Languages/Json.cs +++ b/ColorCode.Core/Compilation/Languages/Json.cs @@ -13,7 +13,7 @@ namespace ColorCode.Compilation.Languages /// public class Json : ILanguage { - private const string Regex_String = @"""[^""\\]*(?:\\[^\r\n]|[^""\\]*)*"""; + private const string Regex_String = @"""[^""\\]*(?:\\.[^""\\]*)*"""; private const string Regex_Number = @"-?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][-+]?[0-9]+)?"; public string Id diff --git a/ColorCode.sln b/ColorCode.sln index b67cc75..dcd5d87 100644 --- a/ColorCode.sln +++ b/ColorCode.sln @@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ColorCode.UWP", "ColorCode. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorCode.UWPTests", "Tests\ColorCode.UWPTests\ColorCode.UWPTests.csproj", "{A3D5A8A5-1D1F-412D-A829-C3DC1C7D4DF3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorCode.Core.UnitTests", "Tests\ColorCode.Core.UnitTests\ColorCode.Core.UnitTests.csproj", "{277FBC4B-50A4-440A-811F-39AEEB088978}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -201,6 +203,26 @@ Global {A3D5A8A5-1D1F-412D-A829-C3DC1C7D4DF3}.Release|x86.ActiveCfg = Release|x86 {A3D5A8A5-1D1F-412D-A829-C3DC1C7D4DF3}.Release|x86.Build.0 = Release|x86 {A3D5A8A5-1D1F-412D-A829-C3DC1C7D4DF3}.Release|x86.Deploy.0 = Release|x86 + {277FBC4B-50A4-440A-811F-39AEEB088978}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Debug|Any CPU.Build.0 = Debug|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Debug|ARM.ActiveCfg = Debug|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Debug|ARM.Build.0 = Debug|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Debug|ARM64.Build.0 = Debug|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Debug|x64.ActiveCfg = Debug|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Debug|x64.Build.0 = Debug|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Debug|x86.ActiveCfg = Debug|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Debug|x86.Build.0 = Debug|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Release|Any CPU.ActiveCfg = Release|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Release|Any CPU.Build.0 = Release|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Release|ARM.ActiveCfg = Release|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Release|ARM.Build.0 = Release|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Release|ARM64.ActiveCfg = Release|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Release|ARM64.Build.0 = Release|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Release|x64.ActiveCfg = Release|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Release|x64.Build.0 = Release|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Release|x86.ActiveCfg = Release|Any CPU + {277FBC4B-50A4-440A-811F-39AEEB088978}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -210,6 +232,7 @@ Global {F1ED6BD6-2690-4BF3-835C-C3927C33424F} = {5B9F207C-2EAB-4F77-95C7-206D65C87137} {DD20D31A-915E-43A2-B819-3A7AE39CA25C} = {5B9F207C-2EAB-4F77-95C7-206D65C87137} {A3D5A8A5-1D1F-412D-A829-C3DC1C7D4DF3} = {5B9F207C-2EAB-4F77-95C7-206D65C87137} + {277FBC4B-50A4-440A-811F-39AEEB088978} = {5B9F207C-2EAB-4F77-95C7-206D65C87137} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5A764590-3191-47F4-9257-5D5F63BC5713} diff --git a/Tests/ColorCode.Core.UnitTests/ColorCode.Core.UnitTests.csproj b/Tests/ColorCode.Core.UnitTests/ColorCode.Core.UnitTests.csproj new file mode 100644 index 0000000..3a38847 --- /dev/null +++ b/Tests/ColorCode.Core.UnitTests/ColorCode.Core.UnitTests.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + false + disable + + + + + + + + + + + + + diff --git a/Tests/ColorCode.Core.UnitTests/JsonBacktrackingTests.cs b/Tests/ColorCode.Core.UnitTests/JsonBacktrackingTests.cs new file mode 100644 index 0000000..828f73e --- /dev/null +++ b/Tests/ColorCode.Core.UnitTests/JsonBacktrackingTests.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; +using Xunit; + +namespace ColorCode.Core.UnitTests +{ + /// + /// Regression tests for the JSON language's Regex_String catastrophic backtracking + /// (issue #45): malformed JSON must not send the highlighter into exponential-time matching. + /// + public class JsonBacktrackingTests + { + [Fact] + public void Partial_json_does_not_catastrophically_backtrack() + { + // A JSON array whose last string value is unterminated. With the old Regex_String the + // key rule explores O(2^n) ways to partition the string body and never returns; with + // the fix it is linear. Bound the call so the pre-fix state fails fast (this test + // running long IS the bug) instead of hanging the whole run. + var partial = "[\n { \"field\": \"" + new string('a', 40) + "\n"; + + string html = null; + var task = Task.Run(() => html = new HtmlClassFormatter().GetHtmlString(partial, Languages.FindById("json"))); + + Assert.True( + task.Wait(TimeSpan.FromSeconds(10)), + "Highlighting partial JSON did not finish in 10s — Regex_String is backtracking catastrophically (issue #45)."); + Assert.NotNull(html); + } + + [Fact] + public void Valid_json_still_highlights_keys_strings_and_escapes() + { + // The fix must not change how valid JSON is tokenised: keys, string values, and strings + // containing escaped quotes/backslashes all still colour correctly. + var json = "{ \"name\": \"John Doe\", \"path\": \"C:\\\\a\\\"b\" }"; + + var html = new HtmlClassFormatter().GetHtmlString(json, Languages.FindById("json")); + + Assert.Contains("jsonKey", html); + Assert.Contains("jsonString", html); + Assert.Contains("John Doe", html); + } + } +}