| From 633555735a023d3e4d92ba31da35b1205f9ecbd7 Mon Sep 17 00:00:00 2001 |
| From: Victor Stinner <vstinner@python.org> |
| Date: Mon, 4 Nov 2024 16:16:35 +0100 |
| Subject: [PATCH] [3.9] gh-124651: Quote template strings in `venv` activation |
| scripts (GH-124712) (GH-126185) (GH-126269) (GH-126301) |
| |
| (cherry picked from commit ae961ae94bf19c8f8c7fbea3d1c25cc55ce8ae97) |
| --- |
| Lib/test/test_venv.py | 81 +++++++++++++++++++ |
| Lib/venv/__init__.py | 42 ++++++++-- |
| Lib/venv/scripts/common/activate | 6 +- |
| Lib/venv/scripts/nt/activate.bat | 4 +- |
| Lib/venv/scripts/posix/activate.csh | 6 +- |
| Lib/venv/scripts/posix/activate.fish | 6 +- |
| ...-09-28-02-03-04.gh-issue-124651.bLBGtH.rst | 1 + |
| 7 files changed, 130 insertions(+), 16 deletions(-) |
| create mode 100644 Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst |
| |
| diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py |
| index 480cb29f35a6a4..871b8314b90b05 100644 |
| --- a/Lib/test/test_venv.py |
| +++ b/Lib/test/test_venv.py |
| @@ -14,6 +14,7 @@ |
| import subprocess |
| import sys |
| import tempfile |
| +import shlex |
| from test.support import (captured_stdout, captured_stderr, requires_zlib, |
| can_symlink, EnvironmentVarGuard, rmtree, |
| import_module, |
| @@ -85,6 +86,10 @@ def get_text_file_contents(self, *args, encoding='utf-8'): |
| result = f.read() |
| return result |
| |
| + def assertEndsWith(self, string, tail): |
| + if not string.endswith(tail): |
| + self.fail(f"String {string!r} does not end with {tail!r}") |
| + |
| class BasicTest(BaseTest): |
| """Test venv module functionality.""" |
| |
| @@ -342,6 +347,82 @@ def test_executable_symlinks(self): |
| 'import sys; print(sys.executable)']) |
| self.assertEqual(out.strip(), envpy.encode()) |
| |
| + # gh-124651: test quoted strings |
| + @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows') |
| + def test_special_chars_bash(self): |
| + """ |
| + Test that the template strings are quoted properly (bash) |
| + """ |
| + rmtree(self.env_dir) |
| + bash = shutil.which('bash') |
| + if bash is None: |
| + self.skipTest('bash required for this test') |
| + env_name = '"\';&&$e|\'"' |
| + env_dir = os.path.join(os.path.realpath(self.env_dir), env_name) |
| + builder = venv.EnvBuilder(clear=True) |
| + builder.create(env_dir) |
| + activate = os.path.join(env_dir, self.bindir, 'activate') |
| + test_script = os.path.join(self.env_dir, 'test_special_chars.sh') |
| + with open(test_script, "w") as f: |
| + f.write(f'source {shlex.quote(activate)}\n' |
| + 'python -c \'import sys; print(sys.executable)\'\n' |
| + 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n' |
| + 'deactivate\n') |
| + out, err = check_output([bash, test_script]) |
| + lines = out.splitlines() |
| + self.assertTrue(env_name.encode() in lines[0]) |
| + self.assertEndsWith(lines[1], env_name.encode()) |
| + |
| + # gh-124651: test quoted strings |
| + @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows') |
| + def test_special_chars_csh(self): |
| + """ |
| + Test that the template strings are quoted properly (csh) |
| + """ |
| + rmtree(self.env_dir) |
| + csh = shutil.which('tcsh') or shutil.which('csh') |
| + if csh is None: |
| + self.skipTest('csh required for this test') |
| + env_name = '"\';&&$e|\'"' |
| + env_dir = os.path.join(os.path.realpath(self.env_dir), env_name) |
| + builder = venv.EnvBuilder(clear=True) |
| + builder.create(env_dir) |
| + activate = os.path.join(env_dir, self.bindir, 'activate.csh') |
| + test_script = os.path.join(self.env_dir, 'test_special_chars.csh') |
| + with open(test_script, "w") as f: |
| + f.write(f'source {shlex.quote(activate)}\n' |
| + 'python -c \'import sys; print(sys.executable)\'\n' |
| + 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n' |
| + 'deactivate\n') |
| + out, err = check_output([csh, test_script]) |
| + lines = out.splitlines() |
| + self.assertTrue(env_name.encode() in lines[0]) |
| + self.assertEndsWith(lines[1], env_name.encode()) |
| + |
| + # gh-124651: test quoted strings on Windows |
| + @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows') |
| + def test_special_chars_windows(self): |
| + """ |
| + Test that the template strings are quoted properly on Windows |
| + """ |
| + rmtree(self.env_dir) |
| + env_name = "'&&^$e" |
| + env_dir = os.path.join(os.path.realpath(self.env_dir), env_name) |
| + builder = venv.EnvBuilder(clear=True) |
| + builder.create(env_dir) |
| + activate = os.path.join(env_dir, self.bindir, 'activate.bat') |
| + test_batch = os.path.join(self.env_dir, 'test_special_chars.bat') |
| + with open(test_batch, "w") as f: |
| + f.write('@echo off\n' |
| + f'"{activate}" & ' |
| + f'{self.exe} -c "import sys; print(sys.executable)" & ' |
| + f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & ' |
| + 'deactivate') |
| + out, err = check_output([test_batch]) |
| + lines = out.splitlines() |
| + self.assertTrue(env_name.encode() in lines[0]) |
| + self.assertEndsWith(lines[1], env_name.encode()) |
| + |
| @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows') |
| def test_unicode_in_batch_file(self): |
| """ |
| diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py |
| index 6f1af294ae63e3..299633117e6fbe 100644 |
| --- a/Lib/venv/__init__.py |
| +++ b/Lib/venv/__init__.py |
| @@ -11,6 +11,7 @@ |
| import sys |
| import sysconfig |
| import types |
| +import shlex |
| |
| |
| CORE_VENV_DEPS = ('pip', 'setuptools') |
| @@ -348,11 +349,41 @@ def replace_variables(self, text, context): |
| :param context: The information for the environment creation request |
| being processed. |
| """ |
| - text = text.replace('__VENV_DIR__', context.env_dir) |
| - text = text.replace('__VENV_NAME__', context.env_name) |
| - text = text.replace('__VENV_PROMPT__', context.prompt) |
| - text = text.replace('__VENV_BIN_NAME__', context.bin_name) |
| - text = text.replace('__VENV_PYTHON__', context.env_exe) |
| + replacements = { |
| + '__VENV_DIR__': context.env_dir, |
| + '__VENV_NAME__': context.env_name, |
| + '__VENV_PROMPT__': context.prompt, |
| + '__VENV_BIN_NAME__': context.bin_name, |
| + '__VENV_PYTHON__': context.env_exe, |
| + } |
| + |
| + def quote_ps1(s): |
| + """ |
| + This should satisfy PowerShell quoting rules [1], unless the quoted |
| + string is passed directly to Windows native commands [2]. |
| + [1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules |
| + [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters |
| + """ |
| + s = s.replace("'", "''") |
| + return f"'{s}'" |
| + |
| + def quote_bat(s): |
| + return s |
| + |
| + # gh-124651: need to quote the template strings properly |
| + quote = shlex.quote |
| + script_path = context.script_path |
| + if script_path.endswith('.ps1'): |
| + quote = quote_ps1 |
| + elif script_path.endswith('.bat'): |
| + quote = quote_bat |
| + else: |
| + # fallbacks to POSIX shell compliant quote |
| + quote = shlex.quote |
| + |
| + replacements = {key: quote(s) for key, s in replacements.items()} |
| + for key, quoted in replacements.items(): |
| + text = text.replace(key, quoted) |
| return text |
| |
| def install_scripts(self, context, path): |
| @@ -392,6 +423,7 @@ def install_scripts(self, context, path): |
| with open(srcfile, 'rb') as f: |
| data = f.read() |
| if not srcfile.endswith(('.exe', '.pdb')): |
| + context.script_path = srcfile |
| try: |
| data = data.decode('utf-8') |
| data = self.replace_variables(data, context) |
| diff --git a/Lib/venv/scripts/common/activate b/Lib/venv/scripts/common/activate |
| index 45af3536aa191d..1d116ca6eda4ed 100644 |
| --- a/Lib/venv/scripts/common/activate |
| +++ b/Lib/venv/scripts/common/activate |
| @@ -37,11 +37,11 @@ deactivate () { |
| # unset irrelevant variables |
| deactivate nondestructive |
| |
| -VIRTUAL_ENV="__VENV_DIR__" |
| +VIRTUAL_ENV=__VENV_DIR__ |
| export VIRTUAL_ENV |
| |
| _OLD_VIRTUAL_PATH="$PATH" |
| -PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH" |
| +PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH" |
| export PATH |
| |
| # unset PYTHONHOME if set |
| @@ -55,7 +55,7 @@ fi |
| if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then |
| _OLD_VIRTUAL_PS1="${PS1:-}" |
| if [ "x__VENV_PROMPT__" != x ] ; then |
| - PS1="__VENV_PROMPT__${PS1:-}" |
| + PS1=__VENV_PROMPT__"${PS1:-}" |
| else |
| if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then |
| # special case for Aspen magic directories |
| diff --git a/Lib/venv/scripts/nt/activate.bat b/Lib/venv/scripts/nt/activate.bat |
| index af4c7e0abacb1c..5ca475a6e81879 100644 |
| --- a/Lib/venv/scripts/nt/activate.bat |
| +++ b/Lib/venv/scripts/nt/activate.bat |
| @@ -8,7 +8,7 @@ if defined _OLD_CODEPAGE ( |
| "%SystemRoot%\System32\chcp.com" 65001 > nul
|
| )
|
|
|
| -set VIRTUAL_ENV=__VENV_DIR__
|
| +set "VIRTUAL_ENV=__VENV_DIR__"
|
|
|
| if not defined PROMPT set PROMPT=$P$G
|
|
|
| @@ -24,7 +24,7 @@ set PYTHONHOME= |
| if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
|
| if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
|
|
|
| -set PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%
|
| +set "PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%"
|
|
|
| :END
|
| if defined _OLD_CODEPAGE (
|
| diff --git a/Lib/venv/scripts/posix/activate.csh b/Lib/venv/scripts/posix/activate.csh |
| index 68a0dc74e1a3c7..51301139517f10 100644 |
| --- a/Lib/venv/scripts/posix/activate.csh |
| +++ b/Lib/venv/scripts/posix/activate.csh |
| @@ -8,17 +8,17 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA |
| # Unset irrelevant variables. |
| deactivate nondestructive |
| |
| -setenv VIRTUAL_ENV "__VENV_DIR__" |
| +setenv VIRTUAL_ENV __VENV_DIR__ |
| |
| set _OLD_VIRTUAL_PATH="$PATH" |
| -setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH" |
| +setenv PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH" |
| |
| |
| set _OLD_VIRTUAL_PROMPT="$prompt" |
| |
| if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then |
| - if ("__VENV_NAME__" != "") then |
| - set env_name = "__VENV_NAME__" |
| + if (__VENV_NAME__ != "") then |
| + set env_name = __VENV_NAME__ |
| else |
| if (`basename "VIRTUAL_ENV"` == "__") then |
| # special case for Aspen magic directories |
| diff --git a/Lib/venv/scripts/posix/activate.fish b/Lib/venv/scripts/posix/activate.fish |
| index 54b9ea5676b66b..62ab5312d6121b 100644 |
| --- a/Lib/venv/scripts/posix/activate.fish |
| +++ b/Lib/venv/scripts/posix/activate.fish |
| @@ -29,10 +29,10 @@ end |
| # Unset irrelevant variables. |
| deactivate nondestructive |
| |
| -set -gx VIRTUAL_ENV "__VENV_DIR__" |
| +set -gx VIRTUAL_ENV __VENV_DIR__ |
| |
| set -gx _OLD_VIRTUAL_PATH $PATH |
| -set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH |
| +set -gx PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__ $PATH |
| |
| # Unset PYTHONHOME if set. |
| if set -q PYTHONHOME |
| @@ -52,8 +52,8 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" |
| set -l old_status $status |
| |
| # Prompt override? |
| - if test -n "__VENV_PROMPT__" |
| - printf "%s%s" "__VENV_PROMPT__" (set_color normal) |
| + if test -n __VENV_PROMPT__ |
| + printf "%s%s" __VENV_PROMPT__ (set_color normal) |
| else |
| # ...Otherwise, prepend env |
| set -l _checkbase (basename "$VIRTUAL_ENV") |
| diff --git a/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst b/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst |
| new file mode 100644 |
| index 00000000000000..17fc9171390dd9 |
| --- /dev/null |
| +++ b/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst |
| @@ -0,0 +1 @@ |
| +Properly quote template strings in :mod:`venv` activation scripts. |
| |