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);
+ }
+ }
+}