diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 6a6de4c15d..f185afd259 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -79,6 +79,7 @@ struct TUIData { Loop *loop; unibi_var_t params[9]; char buf[OUTBUF_SIZE]; + char *buf_to_flush; ///< If non-null, flush this instead of buf[]. size_t bufpos; TermInput input; uv_loop_t write_loop; @@ -1892,7 +1893,7 @@ static void unibi_goto(TUIData *tui, int row, int col) unibi_out(tui, unibi_cursor_address); } -#define UNIBI_OUT(fn) \ +#define UNIBI_OUT(fn, name_fn) \ do { \ const char *str = NULL; \ if (unibi_index >= 0) { \ @@ -1910,11 +1911,14 @@ retry: \ unibi_format(vars, vars + 26, str, params, out, tui, pad, tui); \ if (tui->overflow) { \ tui->bufpos = orig_pos; \ - /* If orig_pos is 0, there's nothing to flush and retrying won't work. */ \ - /* TODO(zeertzjq): should this situation still be handled? */ \ if (orig_pos > 0) { \ flush_buf(tui); \ + orig_pos = 0; \ goto retry; \ + } else { /* orig_pos == 0 */ \ + /* There's nothing to flush and retrying won't work. */ \ + ELOG("TUI: escape sequence for %s too long", name_fn(unibi_index)); \ + tui->overflow = false; \ } \ } \ tui->cork = false; \ @@ -1922,11 +1926,15 @@ retry: \ } while (0) static void unibi_out(TUIData *tui, int unibi_index) { - UNIBI_OUT(unibi_get_str); +#define UNIBI_NAME_STR(i) unibi_name_str((unsigned)(i)) + UNIBI_OUT(unibi_get_str, UNIBI_NAME_STR); +#undef UNIBI_NAME_STR } static void unibi_out_ext(TUIData *tui, int unibi_index) { - UNIBI_OUT(unibi_get_ext_str); +#define UNIBI_GET_EXT_STR_NAME(i) unibi_get_ext_str_name(tui->ut, (unsigned)(i)) + UNIBI_OUT(unibi_get_ext_str, UNIBI_GET_EXT_STR_NAME); +#undef UNIBI_GET_EXT_STR_NAME } #undef UNIBI_OUT @@ -1946,8 +1954,14 @@ static void out(void *ctx, const char *str, size_t len) return; } flush_buf(tui); + if (len > sizeof(tui->buf)) { + // Don't use tui->buf[] when the string to output is too long. #30794 + tui->buf_to_flush = (char *)str; + tui->bufpos = len; + flush_buf(tui); + return; + } } - // TODO(zeertzjq): handle string longer than buffer size? #30794 memcpy(tui->buf + tui->bufpos, str, len); tui->bufpos += len; @@ -2635,7 +2649,7 @@ static void flush_buf(TUIData *tui) bufs[0].base = pre; bufs[0].len = UV_BUF_LEN(flush_buf_start(tui, pre, sizeof(pre))); - bufs[1].base = tui->buf; + bufs[1].base = tui->buf_to_flush != NULL ? tui->buf_to_flush : tui->buf; bufs[1].len = UV_BUF_LEN(tui->bufpos); bufs[2].base = post; @@ -2653,6 +2667,7 @@ static void flush_buf(TUIData *tui) } uv_run(&tui->write_loop, UV_RUN_DEFAULT); } + tui->buf_to_flush = NULL; tui->bufpos = 0; tui->overflow = false; } diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 2b5f455c85..618f779b70 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -2297,8 +2297,8 @@ describe('TUI', function() ]]) child_exec_lua([[ vim.api.nvim_buf_set_lines(0, 0, 0, true, {'Hello'}) - local ns = vim.api.nvim_create_namespace('test') - vim.api.nvim_buf_set_extmark(0, ns, 0, 1, { + _G.NS = vim.api.nvim_create_namespace('test') + vim.api.nvim_buf_set_extmark(0, _G.NS, 0, 1, { end_col = 3, url = 'https://example.com', }) @@ -2306,6 +2306,19 @@ describe('TUI', function() retry(nil, 1000, function() eq({ { id = 0xE1EA0000, url = 'https://example.com' } }, exec_lua([[return _G.urls]])) end) + -- No crash with very long URL #30794 + child_exec_lua([[ + vim.api.nvim_buf_set_extmark(0, _G.NS, 0, 3, { + end_col = 5, + url = 'https://example.com/' .. ('a'):rep(65536), + }) + ]]) + retry(nil, nil, function() + eq({ + { id = 0xE1EA0000, url = 'https://example.com' }, + { id = 0xE1EA0001, url = 'https://example.com/' .. ('a'):rep(65536) }, + }, exec_lua([[return _G.urls]])) + end) end) it('TermResponse works with vim.wait() from another autocommand #32706', function() @@ -3708,6 +3721,44 @@ describe('TUI client', function() end end) + it('does not crash or hang with a very long title', function() + local server, _, screen_client = start_headless_server_and_client() + + local server_exec_lua = tt.make_lua_executor(server) + if not server_exec_lua('return pcall(require, "ffi")') then + pending('missing LuaJIT FFI') + end + + local bufname = api.nvim_buf_get_name(0) + -- Normally a title cannot be longer than the 65535-byte buffer as maketitle() + -- limits it length. Use FFI to send a very long title directly. + server_exec_lua([=[ + local ffi = require('ffi') + local cstr = ffi.typeof('char[?]') + local function to_cstr(string) + return cstr(#string + 1, string) + end + + ffi.cdef([[ + typedef struct { char *data; size_t size; } String; + void ui_call_set_title(String title); + ]]) + + local len = 65536 + local title = ffi.new('String', { data = to_cstr(('a'):rep(len)), size = len }) + ffi.C.ui_call_set_title(title) + ]=]) + screen_client:expect_unchanged() + assert_log('TUI: escape sequence for ext%.set_title too long', testlog) + eq(bufname, api.nvim_buf_get_var(0, 'term_title')) + + -- Following escape sequences are not affected. + server:request('nvim_set_option_value', 'title', true, {}) + retry(nil, nil, function() + eq('[No Name] + - Nvim', api.nvim_buf_get_var(0, 'term_title')) + end) + end) + it('throws error when no server exists', function() clear() local screen = tt.setup_child_nvim({