From d250905c69c0a8d2ac99c4ade9cfb574a011faaf Mon Sep 17 00:00:00 2001 From: James McCoy Date: Thu, 13 May 2021 20:56:25 -0400 Subject: [PATCH 1/2] test(job_spec): Test handling of case-insensitively matching env vars Since Windows is case-insensitive, there should only be a single env var in visible in the resulting job. However, non-Windows should see both. --- test/functional/core/job_spec.lua | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index 9de0d08e79..34ab90d760 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -118,6 +118,32 @@ describe('jobs', function() end end) + it('handles case-insensitively matching #env vars', function() + nvim('command', "let $TOTO = 'abc'") + -- Since $Toto is being set in the job, it should take precedence over the + -- global $TOTO on Windows + nvim('command', "let g:job_opts = {'env': {'Toto': 'def'}, 'stdout_buffered': v:true}") + if iswin() then + nvim('command', [[let j = jobstart('set | find /I "toto="', g:job_opts)]]) + else + nvim('command', [[let j = jobstart('env | grep -i toto=', g:job_opts)]]) + end + nvim('command', "call jobwait([j])") + nvim('command', "let g:output = Normalize(g:job_opts.stdout)") + local actual = eval('g:output') + local expected + if iswin() then + -- Toto is normalized to TOTO so we can detect duplicates, and because + -- Windows doesn't care about case + expected = {'TOTO=def', ''} + else + expected = {'TOTO=abc', 'Toto=def', ''} + end + table.sort(actual) + table.sort(expected) + eq(expected, actual) + end) + it('uses &shell and &shellcmdflag if passed a string', function() nvim('command', "let $VAR = 'abc'") if iswin() then From 88ee095d42e222ec2353b1eaae48a2a9a65b1d9d Mon Sep 17 00:00:00 2001 From: James McCoy Date: Tue, 18 May 2021 06:54:24 -0400 Subject: [PATCH 2/2] fix(job): Deduplicate env var names on Windows Windows handles environment variable names case-insensitively (i.e., %Path% can be accessed via %PATH%). Since nvim explicitly creates the environment block for jobs, we need to ensure it doesn't contain duplicate names with different cases (e.g., both %Path% and %PATH%) as that can confuse processes using the environment. `f_environ()` now converts all env var names to upper-case on Windows, since it does duplicate detection and also provides the base environment for jobs. Similarly, `create_environment()` converts any env var names supplied in the job opts to upper-case so they can be compared against what we get from `f_environ()`. Closes #14541 --- src/nvim/eval/funcs.c | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 072d206ecb..caaf675db2 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -1850,15 +1850,30 @@ static void f_environ(typval_T *argvars, typval_T *rettv, FunPtr fptr) ptrdiff_t len = end - str; assert(len > 0); const char * value = str + len + 1; - if (tv_dict_find(rettv->vval.v_dict, str, len) != NULL) { + + char c = env[i][len]; + env[i][len] = NUL; + +#ifdef WIN32 + // Upper-case all the keys for Windows so we can detect duplicates + char *const key = strcase_save(str, true); +#else + char *const key = xstrdup(str); +#endif + + env[i][len] = c; + + if (tv_dict_find(rettv->vval.v_dict, key, len) != NULL) { // Since we're traversing from the end of the env block to the front, any // duplicate names encountered should be ignored. This preserves the // semantics of env vars defined later in the env block taking precedence. + xfree(key); continue; } tv_dict_add_str(rettv->vval.v_dict, - str, len, + key, len, value); + xfree(key); } os_free_fullenv(env); } @@ -5096,7 +5111,21 @@ static dict_T *create_environment(const dictitem_T *job_env, } if (job_env) { +#ifdef WIN32 + 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((const char *)var->di_key, true); + size_t len = strlen(key); + dictitem_T *dv = tv_dict_find(env, key, len); + if (dv) { + tv_dict_item_remove(env, dv); + } + tv_dict_add_str(env, key, len, tv_get_string(&var->di_tv)); + xfree(key); + }); +#else tv_dict_extend(env, job_env->di_tv.vval.v_dict, "force"); +#endif } if (pty) {