fix(messages): disallow source="nvim" progress msg #39315

Problem:  Internal progress messages use the "nvim" source (since
          ff68fd6b), plugins shouldn't be allowed to set the progress
          message source to "nvim". The message ID used for internal
          progress messages is not identifiable as such.
Solution: Disallow setting opts->source to "nvim" with nvim_echo().
          Refactor msg_progress() and callees to bypass nvim_echo().
          Prepend message id for internal progress messages with "nvim.".
This commit is contained in:
luukvbaal
2026-05-06 18:25:25 +02:00
committed by GitHub
parent 27e7aba982
commit dda30fdfbb
9 changed files with 79 additions and 66 deletions

View File

@@ -856,7 +856,11 @@ must handle.
"list_cmd" List output for various commands (|:ls|, |:set|, …)
"lua_error" Error in |:lua| code
"lua_print" |print()| from |:lua| code
"progress" Progress message emitted by |nvim_echo()|
"progress" Progress message emitted by |nvim_echo()|.
Internal progress messages use these ids:
- `nvim.bufwrite "<filename>"`
- `nvim.completion`
- `nvim.indent`
"quickfix" Quickfix navigation message
"rpc_error" Error response from |rpcrequest()|
"search_cmd" Entered search command

View File

@@ -864,10 +864,10 @@ Union(Integer, String) nvim_echo(ArrayOf(Tuple(String, *HLGroupID)) chunks, Bool
goto error;
});
VALIDATE_EXP((!is_progress || strequal(opts->status.data, "success")
|| strequal(opts->status.data, "failed")
|| strequal(opts->status.data, "running")
|| strequal(opts->status.data, "cancel")),
VALIDATE_EXP(!is_progress || strequal(opts->status.data, "success")
|| strequal(opts->status.data, "failed")
|| strequal(opts->status.data, "running")
|| strequal(opts->status.data, "cancel"),
"status", "success|failed|running|cancel", opts->status.data, {
goto error;
});
@@ -878,13 +878,16 @@ Union(Integer, String) nvim_echo(ArrayOf(Tuple(String, *HLGroupID)) chunks, Bool
goto error;
});
VALIDATE_R((!is_progress || opts->source.size != 0), "opts.source", {
VALIDATE_R(!is_progress || opts->source.size != 0, "opts.source", {
goto error;
});
VALIDATE_S(!is_progress || !strequal(opts->source.data, "nvim"), "source", opts->source.data, {
goto error;
});
// Message-id may be user-defined only if String, not Integer.
VALIDATE(opts->id.type != kObjectTypeInteger || msg_id_exists(opts->id.data.integer),
"Invalid 'id': %" PRId64, opts->id.data.integer, {
VALIDATE_INT(opts->id.type != kObjectTypeInteger || msg_id_exists(opts->id.data.integer),
"id", opts->id.data.integer, {
goto error;
});
@@ -917,10 +920,6 @@ Union(Integer, String) nvim_echo(ArrayOf(Tuple(String, *HLGroupID)) chunks, Bool
verbose_stop(); // flush now
}
if (is_progress) {
do_autocmd_progress(id, hl_msg, &msg_data);
}
if (!needs_clear) {
// history takes ownership of `hl_msg`
return id;

View File

@@ -1669,6 +1669,9 @@ restore_backup:
#endif
if (!filtering) {
add_quoted_fname(IObuff, IOSIZE, buf, fname);
// Append the filename to the message ID.
char msg_id[IOSIZE + 14] = "nvim.bufwrite ";
strncat(msg_id + 14, IObuff, strlen(IObuff) - 1);
bool insert_space = false;
if (write_info.bw_conv_error) {
xstrlcat(IObuff, _(" CONVERSION ERROR"), IOSIZE);
@@ -1707,7 +1710,7 @@ restore_backup:
xstrlcat(IObuff, shortmess(SHM_WRI) ? _(" [w]") : _(" written"), IOSIZE);
}
}
set_keep_msg(msg_progress(IObuff, "bufwrite", "success", 0, true, true), 0);
set_keep_msg(msg_progress(IObuff, msg_id, "success", 0, true, true), 0);
}
// When written everything correctly: reset 'modified'. Unless not

View File

@@ -132,7 +132,10 @@ void filemess(buf_T *buf, char *name, char *s)
msg_scrolled_ign = true;
// may truncate the message to avoid a hit-return prompt
if (*s == NUL) {
msg_progress(IObuff, "bufwrite", "running", 0, false, true);
// Append the filename to the message ID.
char msg_id[IOSIZE + 14] = "nvim.bufwrite ";
strncat(msg_id + 14, IObuff, strlen(IObuff) - 1);
msg_progress(IObuff, msg_id, "running", 0, false, true);
} else {
msg_outtrans(msg_may_trunc(false, IObuff), 0, false);
}

View File

@@ -1008,7 +1008,7 @@ void op_reindent(oparg_T *oap, Indenter how)
// Restore cursor to avoid redrawing curwin in msg_show callback.
linenr_T save_lnum = curwin->w_cursor.lnum;
curwin->w_cursor.lnum = start_lnum;
msg_progress(IObuff, "indent", "running", 0, true, false);
msg_progress(IObuff, "nvim.indent", "running", 0, true, false);
curwin->w_cursor.lnum = save_lnum;
}
@@ -1050,7 +1050,7 @@ void op_reindent(oparg_T *oap, Indenter how)
i = oap->line_count - (i + 1);
snprintf(IObuff, IOSIZE,
NGETTEXT("%" PRId64 " line indented ", "%" PRId64 " lines indented ", i), (int64_t)i);
msg_progress(IObuff, "indent", "success", 0, true, false);
msg_progress(IObuff, "nvim.indent", "success", 0, true, false);
}
if ((cmdmod.cmod_flags & CMOD_LOCKMARKS) == 0) {
// set '[ and '] marks

View File

@@ -1945,7 +1945,7 @@ static void ins_compl_files(int count, char **files, bool thesaurus, int flags,
FILE *fp = os_fopen(files[i], "r"); // open dictionary file
if (flags != DICT_EXACT && !shortmess(SHM_COMPLETIONSCAN) && !compl_autocomplete) {
vim_snprintf(IObuff, IOSIZE, _("Scanning dictionary: %s"), files[i]);
msg_progress(IObuff, "completion", "running", HLF_R, false, true);
msg_progress(IObuff, "nvim.completion", "running", HLF_R, false, true);
}
if (fp == NULL) {
@@ -3834,7 +3834,7 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar
: st->ins_buf->b_sfname == NULL
? st->ins_buf->b_fname
: st->ins_buf->b_sfname);
msg_progress(IObuff, "completion", "running", HLF_R, false, true);
msg_progress(IObuff, "nvim.completion", "running", HLF_R, false, true);
}
} else if (*st->e_cpt == NUL) {
status = INS_COMPL_CPT_END;
@@ -3868,7 +3868,7 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar
compl_type = CTRL_X_TAGS;
if (!shortmess(SHM_COMPLETIONSCAN) && !compl_autocomplete) {
vim_snprintf(IObuff, IOSIZE, "%s", _("Scanning tags."));
msg_progress(IObuff, "completion", "running", HLF_R, false, true);
msg_progress(IObuff, "nvim.completion", "running", HLF_R, false, true);
}
}
}

View File

@@ -315,7 +315,7 @@ static int is_multihl = 0;
///
/// @param hl_msg Message chunks
/// @param msg_data Additional data for progress messages
static HlMessage format_progress_message(HlMessage hl_msg, MessageData *msg_data)
static bool format_progress_message(HlMessage *hl_msg, MessageData *msg_data)
{
HlMessage updated_msg = KV_INITIAL_VALUE;
// progress messages are special. displayed as "title: percent% msg"
@@ -333,28 +333,26 @@ static HlMessage format_progress_message(HlMessage hl_msg, MessageData *msg_data
} else if (strequal(msg_data->status.data, "cancel")) {
hl_id = syn_check_group("WarningMsg", STRLEN_LITERAL("WarningMsg"));
}
kv_push(updated_msg,
((HlMessageChunk){ .text = copy_string(msg_data->title, NULL), .hl_id = hl_id }));
kv_push(updated_msg, ((HlMessageChunk){ .text = cstr_to_string(": "), .hl_id = 0 }));
kv_push(updated_msg, ((HlMessageChunk){ copy_string(msg_data->title, NULL), hl_id }));
kv_push(updated_msg, ((HlMessageChunk){ cstr_to_string(": "), 0 }));
}
if (msg_data->percent >= 0) {
char percent_buf[10];
vim_snprintf(percent_buf, sizeof(percent_buf), "%3ld%% ", (long)msg_data->percent);
String percent = cstr_to_string(percent_buf);
int hl_id = syn_check_group("WarningMsg", STRLEN_LITERAL("WarningMsg"));
kv_push(updated_msg, ((HlMessageChunk){ .text = percent, .hl_id = hl_id }));
kv_push(updated_msg, ((HlMessageChunk){ percent, hl_id }));
}
if (kv_size(updated_msg) != 0) {
for (uint32_t i = 0; i < kv_size(hl_msg); i++) {
kv_push(updated_msg,
((HlMessageChunk){ .text = copy_string(kv_A(hl_msg, i).text, NULL),
.hl_id = kv_A(hl_msg, i).hl_id }));
for (uint32_t i = 0; i < kv_size(*hl_msg); i++) {
HlMessageChunk chunk = kv_A(*hl_msg, i);
kv_push(updated_msg, ((HlMessageChunk){ copy_string(chunk.text, NULL), chunk.hl_id }));
}
return updated_msg;
} else {
return hl_msg;
*hl_msg = updated_msg;
return true;
}
return false;
}
/// Print message chunks, each with their own highlight ID.
@@ -377,33 +375,30 @@ MsgID msg_multihl(MsgID id, HlMessage hl_msg, const char *kind, bool history, bo
abort();
}
bool hl_msg_updated = false;
// don't display progress message in cmd when target doesn't have cmd
if (strequal(kind, "progress") && (progress_msg_target & PROGRESS_TARGET_CMD) == 0) {
*needs_msg_clear = true;
return id;
if (strequal(kind, "progress")) {
do_autocmd_progress(id, hl_msg, msg_data);
if ((progress_msg_target & PROGRESS_TARGET_CMD) == 0) {
*needs_msg_clear = true;
return id;
}
if (msg_data && format_progress_message(&hl_msg, msg_data)) {
*needs_msg_clear = true;
hl_msg_updated = true;
}
}
no_wait_return++;
msg_start();
msg_clr_eos();
bool need_clear = false;
bool hl_msg_updated = false;
if (kind != NULL) {
msg_ext_set_kind(kind);
}
msg_ext_skip_flush = true;
msg_ext_id = id;
// progress message are special displayed as "title: percent% msg"
if (strequal(kind, "progress") && msg_data) {
HlMessage formated_message = format_progress_message(hl_msg, msg_data);
if (formated_message.items != hl_msg.items) {
*needs_msg_clear = true;
hl_msg_updated = true;
hl_msg = formated_message;
}
}
for (uint32_t i = 0; i < kv_size(hl_msg); i++) {
HlMessageChunk chunk = kv_A(hl_msg, i);
is_multihl++;
@@ -416,7 +411,7 @@ MsgID msg_multihl(MsgID id, HlMessage hl_msg, const char *kind, bool history, bo
}
if (history && kv_size(hl_msg)) {
msg_hist_add_multihl(hl_msg, false, msg_data);
msg_hist_add_multihl(hl_msg, false);
}
msg_ext_skip_flush = false;
@@ -1101,25 +1096,22 @@ char *msg_may_trunc(bool force, char *s)
char *msg_progress(char *s, char *id, char *status, int hl_id, bool hist, bool trunc)
{
Error err = ERROR_INIT;
Dict(echo_opts) opts = {
.kind = cstr_as_string("progress"),
.source = cstr_as_string("nvim"),
.status = cstr_as_string(status),
.id = CSTR_AS_OBJ(id),
};
if (hist && (!trunc || ui_has(kUIMessages))) {
msg_hist_add(s, -1, 0);
}
if (trunc) {
s = msg_may_trunc(false, s);
}
MAXSIZE_TEMP_ARRAY(chunk, 2);
MAXSIZE_TEMP_ARRAY(chunks, 1);
ADD_C(chunk, CSTR_AS_OBJ(s));
ADD_C(chunk, INTEGER_OBJ(hl_id));
ADD_C(chunks, ARRAY_OBJ(chunk));
nvim_echo(chunks, false, &opts, &err);
bool clear = false;
MessageData data = {
.source = cstr_as_string("nvim"),
.status = cstr_as_string(status),
.percent = -1,
};
HlMessage chunks = KV_INITIAL_VALUE;
kv_push(chunks, ((HlMessageChunk){ cstr_as_string(s), hl_id }));
msg_multihl(CSTR_AS_OBJ(id), chunks, "progress", false, false, &data, &clear);
kv_destroy(chunks);
ui_flush();
return s;
}
@@ -1153,7 +1145,7 @@ static void msg_hist_add(const char *s, int len, int hl_id)
HlMessage msg = KV_INITIAL_VALUE;
kv_push(msg, ((HlMessageChunk){ text, hl_id }));
msg_hist_add_multihl(msg, false, NULL);
msg_hist_add_multihl(msg, false);
}
static bool do_clear_hist_temp = true;
@@ -1190,7 +1182,7 @@ void do_autocmd_progress(MsgID msg_id, HlMessage msg, MessageData *msg_data)
kv_destroy(messages);
}
static void msg_hist_add_multihl(HlMessage msg, bool temp, MessageData *msg_data)
static void msg_hist_add_multihl(HlMessage msg, bool temp)
{
if (do_clear_hist_temp) {
msg_hist_clear_temp();
@@ -3417,7 +3409,7 @@ void msg_ext_ui_flush(void)
xfree(chunk);
}
xfree(tofree->items);
msg_hist_add_multihl(msg, true, NULL);
msg_hist_add_multihl(msg, true);
}
xfree(tofree);
msg_ext_overwrite = false;

View File

@@ -3888,6 +3888,8 @@ describe('API', function()
eq("Invalid 'id': -1", pcall_err(api.nvim_echo, { { 'foo' } }, false, { id = -1 }))
-- String ids are always allowed (user-defined).
eq('my.msg.id', api.nvim_echo({ { 'foo' } }, false, { id = 'my.msg.id' }))
local opts = { kind = 'progress', source = 'nvim', status = 'success' }
eq("Invalid 'source': 'nvim'", pcall_err(api.nvim_echo, { { '' } }, 1, opts))
end)
it('should clear cmdline message before echo', function()

View File

@@ -95,7 +95,12 @@ describe('ui/ext_messages', function()
{1:~ }|*3
]],
messages = {
{ content = { { writemsg } }, history = true, id = 'bufwrite', kind = 'progress' },
{
content = { { writemsg } },
history = true,
id = 'nvim.bufwrite "Xtest_functional_ui_messages_spec"',
kind = 'progress',
},
{
content = { { 'W10: Warning: Changing a readonly file', 19, 'WarningMsg' } },
history = true,
@@ -587,7 +592,12 @@ describe('ui/ext_messages', function()
{1:~ }|*2
]],
messages = {
{ content = { { '3 lines indented ' } }, history = true, id = 'indent', kind = 'progress' },
{
content = { { '3 lines indented ' } },
kind = 'progress',
id = 'nvim.indent',
history = true,
},
},
})
end)
@@ -1398,7 +1408,7 @@ stack traceback:
{
content = { { string.format('"%s" [New] 0L, 0B written', fname) } },
kind = 'progress',
id = 'bufwrite',
id = 'nvim.bufwrite "Xtest_functional_ui_messages_spec"',
history = true,
},
},
@@ -1645,7 +1655,7 @@ stack traceback:
{
content = { { 'Scanning tags.', 6, 'Question' } },
kind = 'progress',
id = 'completion',
id = 'nvim.completion',
},
},
showmode = {