From f37a17f8bcd3dc3b457d09150014ca9a073ddd52 Mon Sep 17 00:00:00 2001 From: Vadim1987 Date: Thu, 2 Jul 2026 13:00:18 +0200 Subject: [PATCH] fix(input): sanitize incoming text, balance the draw stack (A2, A3) --- src/model/input/userInputModel.lua | 21 +++++++++++++++++++- src/view/input/userInputView.lua | 5 ++++- tests/input/user_input_model_spec.lua | 28 +++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/model/input/userInputModel.lua b/src/model/input/userInputModel.lua index 4613feec..d3786f17 100644 --- a/src/model/input/userInputModel.lua +++ b/src/model/input/userInputModel.lua @@ -82,6 +82,19 @@ function UserInputModel:init_visible(text) self.visible:set_default_range() end +--- Drop bytes that do not form valid UTF-8 +--- @param s string +--- @return string +local function sanitize_utf8(s) + local r = s + local n, pos = utf8.len(r) + while not n do + r = string.sub(r, 1, pos - 1) .. string.sub(r, pos + 1) + n, pos = utf8.len(r) + end + return r +end + ---------------- -- entered -- ---------------- @@ -89,6 +102,7 @@ end --- @param text string function UserInputModel:add_text(text) if type(text) == 'string' then + text = sanitize_utf8(text) self:pop_selected_text() local sl, cc = self:get_cursor_pos() local cur_line = self:get_text_line(sl) @@ -127,6 +141,7 @@ end --- @param keep_cursor boolean function UserInputModel:set_text(text, keep_cursor) if type(text) == 'string' then + text = sanitize_utf8(text) local lines = string.lines(text) local n_added = #lines if n_added == 1 then @@ -136,7 +151,11 @@ function UserInputModel:set_text(text, keep_cursor) self:_update_cursor(true) end elseif type(text) == 'table' then - self.entered = InputText(text) + local clean = {} + for i, l in ipairs(text) do + clean[i] = sanitize_utf8(l) + end + self.entered = InputText(clean) end self:text_change() if not keep_cursor then diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 73cbbd83..a9375317 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -160,7 +160,10 @@ function UserInputView:render_input(input, status, time) for l, s in ipairs(visible) do local ln = l + vc.offset local tl = string.ulen(s) - if not tl then return end + if not tl then + gfx.pop() + return + end for c = 1, tl do local char = string.usub(s, c, c) diff --git a/tests/input/user_input_model_spec.lua b/tests/input/user_input_model_spec.lua index f001b02f..9fbb4a49 100644 --- a/tests/input/user_input_model_spec.lua +++ b/tests/input/user_input_model_spec.lua @@ -27,6 +27,34 @@ describe("input model spec #input", function() } mock.mock_love(love) + describe('invalid UTF-8', function() + local invalid = 'abc\195(' + + it('add_text drops invalid bytes', function() + local model = UserInputModel(mockConf, luaEval) + model:add_text(invalid) + assert.same({ 'abc(' }, model:get_text()) + end) + + it('paste drops invalid bytes across lines', function() + local model = UserInputModel(mockConf, luaEval) + model:paste('abc\ndef' .. invalid) + assert.same({ 'abc', 'defabc(' }, model:get_text()) + end) + + it('set_text drops invalid bytes', function() + local model = UserInputModel(mockConf, luaEval) + model:set_text(invalid) + assert.same({ 'abc(' }, model:get_text()) + end) + + it('set_text drops invalid bytes in tables', function() + local model = UserInputModel(mockConf, luaEval) + model:set_text({ 'ok', invalid }) + assert.same({ 'ok', 'abc(' }, model:get_text()) + end) + end) + ----------------- -- ASCII -- -----------------