From fb6aeaba2d3a38c7febd0a39cabd89685de11b9d Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:36:43 -0400 Subject: [PATCH] feat(eval): treat Lua string as "blob" in writefile() #39098 Problem: vim.fn.writefile() treats Lua strings as Vimscript strings instead of a "binary clean" string. Solution: Treat Lua-originated strings as blob data. --- src/nvim/eval/fs.c | 23 ++++++++++++++++---- src/nvim/lua/executor.c | 2 ++ test/functional/vimscript/writefile_spec.lua | 6 +++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/nvim/eval/fs.c b/src/nvim/eval/fs.c index 1235d78783..7e4dc1801b 100644 --- a/src/nvim/eval/fs.c +++ b/src/nvim/eval/fs.c @@ -42,6 +42,7 @@ #include "nvim/os/os.h" #include "nvim/path.h" #include "nvim/pos_defs.h" +#include "nvim/runtime.h" #include "nvim/strings.h" #include "nvim/types_defs.h" #include "nvim/vim_defs.h" @@ -1716,13 +1717,12 @@ write_list_error: /// @param[in] blob Blob to write. /// /// @return true on success, or false on failure. -static bool write_blob(FileDescriptor *const fp, const blob_T *const blob) +static bool write_data(FileDescriptor *const fp, const char *const data, const size_t len) FUNC_ATTR_NONNULL_ARG(1) { int error = 0; - const int len = tv_blob_len(blob); if (len > 0) { - const ptrdiff_t written = file_write(fp, blob->bv_ga.ga_data, (size_t)len); + const ptrdiff_t written = file_write(fp, data, len); if (written < (ptrdiff_t)len) { error = (int)written; goto write_blob_error; @@ -1738,6 +1738,18 @@ write_blob_error: return false; } +static bool write_blob(FileDescriptor *const fp, const blob_T *const blob) + FUNC_ATTR_NONNULL_ARG(1) +{ + return write_data(fp, blob->bv_ga.ga_data, (size_t)tv_blob_len(blob)); +} + +static bool write_string(FileDescriptor *const fp, const char *const data) + FUNC_ATTR_NONNULL_ARG(1) +{ + return write_data(fp, data, strlen(data)); +} + /// "writefile()" function void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { @@ -1753,7 +1765,8 @@ void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) return; } }); - } else if (argvars[0].v_type != VAR_BLOB) { + } else if (argvars[0].v_type != VAR_BLOB + && !(argvars[0].v_type == VAR_STRING && script_is_lua(current_sctx.sc_sid))) { semsg(_(e_invarg2), _("writefile() first argument must be a List or a Blob")); return; @@ -1823,6 +1836,8 @@ void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) bool write_ok; if (argvars[0].v_type == VAR_BLOB) { write_ok = write_blob(&fp, argvars[0].vval.v_blob); + } else if (argvars[0].v_type == VAR_STRING) { + write_ok = write_string(&fp, argvars[0].vval.v_string); } else { write_ok = write_list(&fp, argvars[0].vval.v_list, binary); } diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 1771a01565..02a95d5f6d 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -1366,12 +1366,14 @@ int nlua_call(lua_State *lstate) funcexe.fe_firstline = curwin->w_cursor.lnum; funcexe.fe_lastline = curwin->w_cursor.lnum; funcexe.fe_evaluate = true; + const sctx_T save_current_sctx = api_set_sctx(LUA_INTERNAL_CALL); TRY_WRAP(&err, { // call_func() retval is deceptive, ignore it. Instead we set `msg_list` // (TRY_WRAP) to capture abort-causing non-exception errors. (void)call_func(name, (int)name_len, &rettv, nargs, vim_args, &funcexe); }); + current_sctx = save_current_sctx; if (!ERROR_SET(&err)) { nlua_push_typval(lstate, &rettv, 0); diff --git a/test/functional/vimscript/writefile_spec.lua b/test/functional/vimscript/writefile_spec.lua index b81cb43691..f406e5a469 100644 --- a/test/functional/vimscript/writefile_spec.lua +++ b/test/functional/vimscript/writefile_spec.lua @@ -6,6 +6,7 @@ local clear = n.clear local eq = t.eq local fn = n.fn local api = n.api +local exec_lua = n.exec_lua local read_file = t.read_file local write_file = t.write_file local pcall_err = t.pcall_err @@ -99,6 +100,11 @@ describe('writefile()', function() eq('a\0', read_file(fname)) end) + it('writes Lua strings to a file', function() + eq(0, exec_lua([[return vim.fn.writefile('foo\0bar', ..., 'b')]], fname)) + eq('foo\0bar', read_file(fname)) + end) + it('shows correct file name when supplied numbers', function() api.nvim_set_current_dir(dname) eq(