From 1f06735fcc8012512369c63121c6bb5d862c4666 Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:34:03 +0900 Subject: [PATCH 1/2] fix: handle escaped closing brackets in pattern classes --- src/Lua/Standard/Internal/MatchState.cs | 11 ++- tests/Lua.Tests/PatternClassTests.cs | 89 +++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 tests/Lua.Tests/PatternClassTests.cs diff --git a/src/Lua/Standard/Internal/MatchState.cs b/src/Lua/Standard/Internal/MatchState.cs index 15874841..3dc20f82 100644 --- a/src/Lua/Standard/Internal/MatchState.cs +++ b/src/Lua/Standard/Internal/MatchState.cs @@ -420,8 +420,13 @@ int ClassEnd(ReadOnlySpan pattern, int pIdx) do { - pIdx++; - if (pIdx < pattern.Length && pattern[pIdx] == LEsc) + if (pIdx >= pattern.Length) + { + throw new LuaRuntimeException(State, "malformed pattern (missing ']')"); + } + + // Check the consumed character so an escaped ']' cannot end the class. + if (pattern[pIdx++] == LEsc && pIdx < pattern.Length) { pIdx++; } @@ -430,7 +435,7 @@ int ClassEnd(ReadOnlySpan pattern, int pIdx) { throw new LuaRuntimeException(State, "malformed pattern (missing ']')"); } - } while (pIdx < pattern.Length && pattern[pIdx] != ']'); + } while (pattern[pIdx] != ']'); return pIdx + 1; diff --git a/tests/Lua.Tests/PatternClassTests.cs b/tests/Lua.Tests/PatternClassTests.cs new file mode 100644 index 00000000..6daf5286 --- /dev/null +++ b/tests/Lua.Tests/PatternClassTests.cs @@ -0,0 +1,89 @@ +using Lua.Standard; + +namespace Lua.Tests; + +public class PatternClassTests +{ + const string SingleElementArray = """{"SomeArray":["Alice"], "Text":"some_text"}"""; + const string MultipleElementArray = """{"SomeArray":["Alice","Bob","Denis"], "Text":"some_text"}"""; + + [TestCase(SingleElementArray, "%[[^%]]*,", -1, -1)] + [TestCase(MultipleElementArray, "%[[^%]]*,", 14, 28)] + [TestCase(SingleElementArray, "[[^%]]*", 1, 0)] + [TestCase(SingleElementArray, "[^%]]*", 1, 21)] + [TestCase("[ab,c]", "%[[^%]]*,", 1, 4)] + [TestCase("abc", "[[^%]]*", 1, 0)] + [TestCase("]", "[%]]", 1, 1)] + [TestCase("]]x", "[%]]+", 1, 2)] + [TestCase("abc]", "[^%]]+", 1, 3)] + [TestCase("]", "[a%]]", 1, 1)] + [TestCase("]", "[%%%]]", 1, 1)] + [TestCase("]", "[]]", 1, 1)] + [TestCase("abc]", "[^]]+", 1, 3)] + [TestCase("[", "[[]", 1, 1)] + [TestCase("12x", "[%d]+", 1, 2)] + [TestCase("%", "[%%]", 1, 1)] + public async Task FindCharacterClass(string source, string pattern, int start, int end) + { + using var state = LuaState.Create(); + state.OpenStringLibrary(); + + var result = await state.DoStringAsync($"return string.find('{source}', '{pattern}')"); + + if (start < 0) + { + Assert.That(result, Is.EqualTo(new LuaValue[] { LuaValue.Nil })); + return; + } + + Assert.That(result, Is.EqualTo(new LuaValue[] { start, end })); + } + + [TestCase("[%]")] + [TestCase("[^%]")] + [TestCase("[")] + [TestCase("[^")] + [TestCase("[%")] + [TestCase("[]")] + public void RejectUnterminatedCharacterClass(string pattern) + { + using var state = LuaState.Create(); + state.OpenStringLibrary(); + + var exception = Assert.ThrowsAsync(async () => + await state.DoStringAsync($"return string.find('x]', '{pattern}')") + ); + + Assert.That(exception!.Message, Does.Contain("malformed pattern (missing ']')")); + } + + [Test] + public async Task EscapedClosingBracketInMatchGMatchAndGSub() + { + using var state = LuaState.Create(); + state.OpenStandardLibraries(); + + var result = await state.DoStringAsync( + """ + local s = '[ab,c]' + local p = '%[[^%]]*,' + local f = string.gmatch(s, p) + local replaced, count = string.gsub(s, p, 'X') + return string.match(s, p), f(), replaced, count + """ + ); + + Assert.That(result, Is.EqualTo(new LuaValue[] { "[ab,", "[ab,", "Xc]", 1 })); + } + + [Test] + public async Task EscapedClosingBracketInFrontier() + { + using var state = LuaState.Create(); + state.OpenStringLibrary(); + + var result = await state.DoStringAsync("return string.find(']a', '%f[%]]')"); + + Assert.That(result, Is.EqualTo(new LuaValue[] { 1, 0 })); + } +} From dfacfe881c4651ad4cdf285978036a7ee03e62ac Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:36:20 +0900 Subject: [PATCH 2/2] style: format pattern class test constant --- tests/Lua.Tests/PatternClassTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Lua.Tests/PatternClassTests.cs b/tests/Lua.Tests/PatternClassTests.cs index 6daf5286..f183eba2 100644 --- a/tests/Lua.Tests/PatternClassTests.cs +++ b/tests/Lua.Tests/PatternClassTests.cs @@ -5,7 +5,8 @@ namespace Lua.Tests; public class PatternClassTests { const string SingleElementArray = """{"SomeArray":["Alice"], "Text":"some_text"}"""; - const string MultipleElementArray = """{"SomeArray":["Alice","Bob","Denis"], "Text":"some_text"}"""; + const string MultipleElementArray = + """{"SomeArray":["Alice","Bob","Denis"], "Text":"some_text"}"""; [TestCase(SingleElementArray, "%[[^%]]*,", -1, -1)] [TestCase(MultipleElementArray, "%[[^%]]*,", 14, 28)]