diff --git a/runtime/lua/vim/_core/vimfn.lua b/runtime/lua/vim/_core/vimfn.lua index e972e7cedb..8cb07d3bbc 100644 --- a/runtime/lua/vim/_core/vimfn.lua +++ b/runtime/lua/vim/_core/vimfn.lua @@ -14,7 +14,15 @@ end --- Returns all environment variables as a dictionary. --- @return table function M.f_environ() - return vim.uv.os_environ() + local env = vim.uv.os_environ() ---@type table + if vim.fn.has('win32') == 1 then -- Vim/legacy behavior: force uppercase keys on Windows. #39443 + local upper = {} --- @type table + for k, v in pairs(env) do + upper[k:upper()] = v + end + return upper + end + return env end return M diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index a04671030a..20147a48cb 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -3386,7 +3386,16 @@ dict_T *create_environment(const dictitem_T *job_env, const bool clear_env, cons int envcount; if (uv_os_environ(&envitems, &envcount) == 0) { for (int i = 0; i < envcount; i++) { - tv_dict_add_str(env, envitems[i].name, strlen(envitems[i].name), envitems[i].value); + const char *name = envitems[i].name; + size_t namelen = strlen(name); +#ifdef MSWIN + // Force uppercase keys on Windows (for dedup, below). #39443 + if (namelen < MAXPATHL / MB_MAXBYTES) { + namelen = mb_strup_buf(name, NameBuff); + name = NameBuff; + } +#endif + tv_dict_add_str(env, name, namelen, envitems[i].value); } uv_os_free_environ(envitems, envcount); } @@ -3434,15 +3443,18 @@ dict_T *create_environment(const dictitem_T *job_env, const bool clear_env, cons if (job_env) { #ifdef MSWIN TV_DICT_ITER(job_env->di_tv.vval.v_dict, var, { - // Always use upper-case keys for Windows so we detect duplicate keys - char *const key = strcase_save(var->di_key, true); - size_t len = strlen(key); - dictitem_T *dv = tv_dict_find(env, key, len); + // Force uppercase keys on Windows (dedup). + const char *name = var->di_key; + size_t namelen = strlen(name); + if (namelen < MAXPATHL / MB_MAXBYTES) { + namelen = mb_strup_buf(name, NameBuff); + name = NameBuff; + } + dictitem_T *dv = tv_dict_find(env, name, (ptrdiff_t)namelen); if (dv) { tv_dict_item_remove(env, dv); } - tv_dict_add_str(env, key, len, tv_get_string(&var->di_tv)); - xfree(key); + tv_dict_add_str(env, name, namelen, tv_get_string(&var->di_tv)); }); #else tv_dict_extend(env, job_env->di_tv.vval.v_dict, "force"); diff --git a/src/nvim/strings.c b/src/nvim/strings.c index c48f971192..22770a9ba5 100644 --- a/src/nvim/strings.c +++ b/src/nvim/strings.c @@ -358,6 +358,24 @@ void vim_memcpy_up(char *restrict dst, const char *restrict src, size_t n) } } +/// Multi-byte uppercase `src` into `dst`. `dst` must have room for the result. +/// In the worst case, the uppercased form needs `strlen(src) * MB_MAXBYTES` bytes plus NUL. +/// +/// @return number of bytes written to `dst`, not including the NUL terminator. +size_t mb_strup_buf(const char *src, char *dst) + FUNC_ATTR_NONNULL_ALL +{ + size_t i = 0; + for (const char *p = src; *p != NUL;) { + CharInfo ci = utf_ptr2CharInfo(p); + int c = ci.value < 0 ? (uint8_t)(*p) : ci.value; + i += (size_t)utf_char2bytes(mb_toupper(c), dst + i); + p += ci.len; + } + dst[i] = NUL; + return i; +} + /// Make given string all upper-case or all lower-case /// /// Handles multi-byte characters as good as possible. diff --git a/test/functional/vimscript/environ_spec.lua b/test/functional/vimscript/environ_spec.lua index 50756f4c8e..30763d3157 100644 --- a/test/functional/vimscript/environ_spec.lua +++ b/test/functional/vimscript/environ_spec.lua @@ -27,6 +27,15 @@ describe('vim.fn.environ()', function() eq(vim.NIL, n.exec_lua('return vim.env.DOES_NOT_EXIST')) end) + it('forces uppercase keys on Windows #39443', function() + if not t.is_os('win') then + return + end + clear({ env = { mixed_Case_Var = 'val' } }) + eq('val', environ()['MIXED_CASE_VAR']) + eq(nil, environ()['mixed_Case_Var']) + end) + it('results match getenv()', function() clear() eq(