From a9321baf897c9b3941ed7a98d7b6fe0b8e0f97cb Mon Sep 17 00:00:00 2001 From: Yacine Petitprez Date: Mon, 8 Jun 2026 17:07:32 +0700 Subject: [PATCH 1/3] fix compilation issue --- src/lua/stack/util.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lua/stack/util.cr b/src/lua/stack/util.cr index df7aa58..a8f626a 100644 --- a/src/lua/stack/util.cr +++ b/src/lua/stack/util.cr @@ -35,7 +35,7 @@ module Lua end protected def pick_results(start, finish = size) - elements = (start..finish).map { pop } + elements = (start..finish).map { pop.as(::Lua::Type) } elements.size > 1 ? elements : elements.first? end end From 1c843c1719b17e3d5c235cf74089b7a569915181 Mon Sep 17 00:00:00 2001 From: Yacine Petitprez Date: Sun, 9 Aug 2026 13:31:52 +0700 Subject: [PATCH 2/3] Fix non-ASCII chunk truncation: pass bytesize, not size, to loadbufferx --- spec/lua/stack/chunk_spec.cr | 61 ++++++++++++++++++++++++++++++++++++ src/lua/stack/chunk.cr | 2 +- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/spec/lua/stack/chunk_spec.cr b/spec/lua/stack/chunk_spec.cr index 763f119..2d669f6 100644 --- a/spec/lua/stack/chunk_spec.cr +++ b/spec/lua/stack/chunk_spec.cr @@ -43,6 +43,67 @@ module Lua::StackMixin )).should be_a Function s.close end + + # Regression: luaL_loadbufferx must be passed the byte length of the + # chunk, not the codepoint count. Passing `String#size` (codepoints) + # silently truncates the buffer for any non-ASCII byte, which then + # surfaces as a spurious "" / "unfinished string" syntax error + # at the *tail* of the chunk rather than at the offending character. + context "with non-ASCII characters (multibyte UTF-8)" do + it "keeps the full chunk when a comment contains Latin-1 supplement" do + chunk = String.build do |io| + io << "-- comment with é, à, ç\n" + io << "return 42\n" + end + Stack.new.run(chunk).should eq 42 + end + + it "keeps the full chunk when a comment contains Thai" do + # สวัสดี : each codepoint is 3 bytes in UTF-8 + chunk = String.build do |io| + io << "-- สวัสดีครับ\n" + io << "return 7\n" + end + Stack.new.run(chunk).should eq 7 + end + + it "keeps the full chunk when a comment contains CJK" do + # 你好世界 : each codepoint is 3 bytes in UTF-8 + chunk = String.build do |io| + io << "-- 你好世界\n" + io << "return 11\n" + end + Stack.new.run(chunk).should eq 11 + end + + it "keeps the full chunk when a comment contains an em-dash" do + chunk = String.build do |io| + io << "-- an em-dash — here\n" + io << "return 99\n" + end + Stack.new.run(chunk).should eq 99 + end + + it "passes non-ASCII bytes through inside string literals" do + # `"é — สวัสดี 你好"` written via string concatenation so the Lua + # source contains a literal double-quoted string with multibyte + # bytes inside it. + inner = "\u00E9 — \u0E2A\u0E27\u0E31\u0E2A\u0E14\u0E35 \u4F60\u597D" + chunk = "return \"" + inner + "\"\n" + Stack.new.run(chunk).should eq inner + end + + it "loads a chunk whose entire trailing portion is multibyte" do + # If the loader truncates by even one byte, the final `return` + # statement gets mangled and we get a tail-side syntax error + # instead of the intended value. + chunk = String.build do |io| + io << "-- 你好สวัสดีé—\n" + io << "return 123\n" + end + Stack.new.run(chunk).should eq 123 + end + end end describe "#run" do diff --git a/src/lua/stack/chunk.cr b/src/lua/stack/chunk.cr index 948256c..d575a43 100644 --- a/src/lua/stack/chunk.cr +++ b/src/lua/stack/chunk.cr @@ -13,7 +13,7 @@ module Lua # } # => 8 # ``` def run(buff : String, name : String? = nil) - call = CALL.new LibLua.l_loadbufferx @state, buff, buff.size, name || buff.strip, nil + call = CALL.new LibLua.l_loadbufferx @state, buff, buff.bytesize, name || buff.strip, nil raise error(call, pop) if call != CALL::OK call_and_return size end From 9d3e2a9aa9ad3cdebbc20b19bb3e260d7b246041 Mon Sep 17 00:00:00 2001 From: Vitalii Elenhaupt Date: Sun, 9 Aug 2026 10:42:31 +0200 Subject: [PATCH 3/3] test: simplify multibyte chunk specs and close stacks --- spec/lua/stack/chunk_spec.cr | 68 +++++++----------------------------- 1 file changed, 12 insertions(+), 56 deletions(-) diff --git a/spec/lua/stack/chunk_spec.cr b/spec/lua/stack/chunk_spec.cr index 2d669f6..0495f67 100644 --- a/spec/lua/stack/chunk_spec.cr +++ b/spec/lua/stack/chunk_spec.cr @@ -44,64 +44,20 @@ module Lua::StackMixin s.close end - # Regression: luaL_loadbufferx must be passed the byte length of the - # chunk, not the codepoint count. Passing `String#size` (codepoints) - # silently truncates the buffer for any non-ASCII byte, which then - # surfaces as a spurious "" / "unfinished string" syntax error - # at the *tail* of the chunk rather than at the offending character. - context "with non-ASCII characters (multibyte UTF-8)" do - it "keeps the full chunk when a comment contains Latin-1 supplement" do - chunk = String.build do |io| - io << "-- comment with é, à, ç\n" - io << "return 42\n" - end - Stack.new.run(chunk).should eq 42 - end - - it "keeps the full chunk when a comment contains Thai" do - # สวัสดี : each codepoint is 3 bytes in UTF-8 - chunk = String.build do |io| - io << "-- สวัสดีครับ\n" - io << "return 7\n" - end - Stack.new.run(chunk).should eq 7 - end - - it "keeps the full chunk when a comment contains CJK" do - # 你好世界 : each codepoint is 3 bytes in UTF-8 - chunk = String.build do |io| - io << "-- 你好世界\n" - io << "return 11\n" - end - Stack.new.run(chunk).should eq 11 + context "with multibyte UTF-8 characters" do + it "keeps the full chunk when a comment contains multibyte characters" do + s = Stack.new + s.run(%q( + -- é à ç — สวัสดี 你好 + return 42 + )).should eq 42 + s.close end - it "keeps the full chunk when a comment contains an em-dash" do - chunk = String.build do |io| - io << "-- an em-dash — here\n" - io << "return 99\n" - end - Stack.new.run(chunk).should eq 99 - end - - it "passes non-ASCII bytes through inside string literals" do - # `"é — สวัสดี 你好"` written via string concatenation so the Lua - # source contains a literal double-quoted string with multibyte - # bytes inside it. - inner = "\u00E9 — \u0E2A\u0E27\u0E31\u0E2A\u0E14\u0E35 \u4F60\u597D" - chunk = "return \"" + inner + "\"\n" - Stack.new.run(chunk).should eq inner - end - - it "loads a chunk whose entire trailing portion is multibyte" do - # If the loader truncates by even one byte, the final `return` - # statement gets mangled and we get a tail-side syntax error - # instead of the intended value. - chunk = String.build do |io| - io << "-- 你好สวัสดีé—\n" - io << "return 123\n" - end - Stack.new.run(chunk).should eq 123 + it "passes multibyte bytes through inside string literals" do + s = Stack.new + s.run(%q( return "é — สวัสดี 你好" )).should eq "é — สวัสดี 你好" + s.close end end end