diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 5e0286cd80..b724bb8e3c 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -279,19 +279,19 @@ static void schedule_termrequest(Terminal *term, char *sequence, size_t sequence (void *)(intptr_t)term->cursor.col); } -static int parse_osc8(VTermStringFragment frag, int *attr) +static int parse_osc8(const char *str, size_t len, int *attr) FUNC_ATTR_NONNULL_ALL { // Parse the URI from the OSC 8 sequence and add the URL to our URL set. // Skip the ID, we don't use it (for now) size_t i = 0; - for (; i < frag.len; i++) { - if (frag.str[i] == ';') { + for (; i < len; i++) { + if (str[i] == ';') { break; } } - if (frag.str[i] != ';') { + if (str[i] != ';') { // Invalid OSC sequence return 0; } @@ -299,14 +299,13 @@ static int parse_osc8(VTermStringFragment frag, int *attr) // Move past the semicolon i++; - if (i >= frag.len) { + if (i >= len) { // Empty OSC 8, no URL *attr = 0; return 1; } - char *url = xmemdupz(&frag.str[i], frag.len - i + 1); - url[frag.len - i] = 0; + char *url = xmemdupz(str + i, len - i); *attr = hl_add_url(0, url); xfree(url); @@ -322,16 +321,7 @@ static int on_osc(int command, VTermStringFragment frag, void *user) return 0; } - if (command == 8) { - int attr = 0; - if (parse_osc8(frag, &attr)) { - VTermState *state = vterm_obtain_state(term->vt); - VTermValue value = { .number = attr }; - vterm_state_set_penattr(state, VTERM_ATTR_URI, VTERM_VALUETYPE_INT, &value); - } - } - - if (!has_event(EVENT_TERMREQUEST)) { + if (command != 8 && !has_event(EVENT_TERMREQUEST)) { return 1; } @@ -341,8 +331,20 @@ static int on_osc(int command, VTermStringFragment frag, void *user) } kv_concat_len(term->termrequest_buffer, frag.str, frag.len); if (frag.final) { - char *sequence = xmemdup(term->termrequest_buffer.items, term->termrequest_buffer.size); - schedule_termrequest(user, sequence, term->termrequest_buffer.size); + if (command == 8) { + int attr = 0; + const int off = STRLEN_LITERAL("\x1b]8;"); + if (parse_osc8(term->termrequest_buffer.items + off, + term->termrequest_buffer.size - off, &attr)) { + VTermState *state = vterm_obtain_state(term->vt); + VTermValue value = { .number = attr }; + vterm_state_set_penattr(state, VTERM_ATTR_URI, VTERM_VALUETYPE_INT, &value); + } + } + if (has_event(EVENT_TERMREQUEST)) { + char *sequence = xmemdup(term->termrequest_buffer.items, term->termrequest_buffer.size); + schedule_termrequest(user, sequence, term->termrequest_buffer.size); + } } return 1; } diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 9e6427c040..a1a9122777 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; @@ -1895,7 +1896,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) { \ @@ -1913,11 +1914,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; \ @@ -1925,11 +1929,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 @@ -1949,8 +1957,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; @@ -2591,7 +2605,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; @@ -2609,6 +2623,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/highlight_spec.lua b/test/functional/terminal/highlight_spec.lua index 1fc0f8deb5..1c6493cc8e 100644 --- a/test/functional/terminal/highlight_spec.lua +++ b/test/functional/terminal/highlight_spec.lua @@ -388,6 +388,19 @@ describe(':terminal', function() ^This is an {100:example} of a link | |*6 ]]) + -- Also works if OSC 8 is split into multiple fragments. + api.nvim_chan_send(chan, '\nThis is another \027]8;;https') + n.poke_eventloop() + api.nvim_chan_send(chan, '://') + n.poke_eventloop() + api.nvim_chan_send(chan, 'example') + n.poke_eventloop() + api.nvim_chan_send(chan, '.com\027\\EXAMPLE\027]8;;\027\\ of a link') + screen:expect([[ + ^This is an {100:example} of a link | + This is another {100:EXAMPLE} of a link | + |*5 + ]]) end) it('zoomout with large horizontal output #30374', function() diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index aba0ee38f9..1adbf5cd96 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -2513,8 +2513,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', }) @@ -2522,6 +2522,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() @@ -3996,6 +4009,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({