[Python-checkins] gh-99442: Fix handling in py.exe launcher when argv[0] does not include a file extension (GH-99542)

zooba webhook-mailer at python.org
Fri Nov 18 09:15:02 EST 2022


https://github.com/python/cpython/commit/a220c6d1ee3053895f502b43b47dc3a9c55fa6a3
commit: a220c6d1ee3053895f502b43b47dc3a9c55fa6a3
branch: main
author: Steve Dower <steve.dower at python.org>
committer: zooba <steve.dower at microsoft.com>
date: 2022-11-18T14:14:56Z
summary:

gh-99442: Fix handling in py.exe launcher when argv[0] does not include a file extension (GH-99542)

files:
A Misc/NEWS.d/next/Windows/2022-11-16-19-03-21.gh-issue-99442.6Dgk3Q.rst
M Lib/test/test_launcher.py
M PC/launcher2.c

diff --git a/Lib/test/test_launcher.py b/Lib/test/test_launcher.py
index 6ad85dc9c300..47152d4a3c00 100644
--- a/Lib/test/test_launcher.py
+++ b/Lib/test/test_launcher.py
@@ -173,7 +173,7 @@ def find_py(cls):
                     errors="ignore",
                 ) as p:
                     p.stdin.close()
-                    version = next(p.stdout).splitlines()[0].rpartition(" ")[2]
+                    version = next(p.stdout, "\n").splitlines()[0].rpartition(" ")[2]
                     p.stdout.read()
                     p.wait(10)
                 if not sys.version.startswith(version):
@@ -467,6 +467,15 @@ def test_py3_default_env(self):
         self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
         self.assertEqual("X.Y-arm64.exe -X fake_arg_for_test -arg", data["stdout"].strip())
 
+    def test_py_default_short_argv0(self):
+        with self.py_ini(TEST_PY_COMMANDS):
+            for argv0 in ['"py.exe"', 'py.exe', '"py"', 'py']:
+                with self.subTest(argv0):
+                    data = self.run_py(["--version"], argv=f'{argv0} --version')
+                    self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
+                    self.assertEqual("3.100", data["SearchInfo.tag"])
+                    self.assertEqual(f'X.Y.exe --version', data["stdout"].strip())
+
     def test_py_default_in_list(self):
         data = self.run_py(["-0"], env=TEST_PY_ENV)
         default = None
diff --git a/Misc/NEWS.d/next/Windows/2022-11-16-19-03-21.gh-issue-99442.6Dgk3Q.rst b/Misc/NEWS.d/next/Windows/2022-11-16-19-03-21.gh-issue-99442.6Dgk3Q.rst
new file mode 100644
index 000000000000..8e19366c4297
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2022-11-16-19-03-21.gh-issue-99442.6Dgk3Q.rst
@@ -0,0 +1,2 @@
+Fix handling in :ref:`launcher` when ``argv[0]`` does not include a file
+extension.
diff --git a/PC/launcher2.c b/PC/launcher2.c
index 5bcd2ba8a067..9b3db04aa48b 100644
--- a/PC/launcher2.c
+++ b/PC/launcher2.c
@@ -491,62 +491,39 @@ dumpSearchInfo(SearchInfo *search)
 
 
 int
-findArgumentLength(const wchar_t *buffer, int bufferLength)
+findArgv0Length(const wchar_t *buffer, int bufferLength)
 {
-    if (bufferLength < 0) {
-        bufferLength = (int)wcsnlen_s(buffer, MAXLEN);
-    }
-    if (bufferLength == 0) {
-        return 0;
-    }
-    const wchar_t *end;
-    int i;
-
-    if (buffer[0] != L'"') {
-        end = wcschr(buffer, L' ');
-        if (!end) {
-            return bufferLength;
-        }
-        i = (int)(end - buffer);
-        return i < bufferLength ? i : bufferLength;
-    }
-
-    i = 0;
-    while (i < bufferLength) {
-        end = wcschr(&buffer[i + 1], L'"');
-        if (!end) {
-            return bufferLength;
-        }
-
-        i = (int)(end - buffer);
-        if (i >= bufferLength) {
-            return bufferLength;
-        }
-
-        int j = i;
-        while (j > 1 && buffer[--j] == L'\\') {
-            if (j > 0 && buffer[--j] == L'\\') {
-                // Even number, so back up and keep counting
-            } else {
-                // Odd number, so it's escaped and we want to keep searching
-                continue;
+    // Note: this implements semantics that are only valid for argv0.
+    // Specifically, there is no escaping of quotes, and quotes within
+    // the argument have no effect. A quoted argv0 must start and end
+    // with a double quote character; otherwise, it ends at the first
+    // ' ' or '\t'.
+    int quoted = buffer[0] == L'"';
+    for (int i = 1; bufferLength < 0 || i < bufferLength; ++i) {
+        switch (buffer[i]) {
+        case L'\0':
+            return i;
+        case L' ':
+        case L'\t':
+            if (!quoted) {
+                return i;
             }
-        }
-
-        // Non-escaped quote with space after it - end of the argument!
-        if (i + 1 >= bufferLength || isspace(buffer[i + 1])) {
-            return i + 1;
+            break;
+        case L'"':
+            if (quoted) {
+                return i + 1;
+            }
+            break;
         }
     }
-
     return bufferLength;
 }
 
 
 const wchar_t *
-findArgumentEnd(const wchar_t *buffer, int bufferLength)
+findArgv0End(const wchar_t *buffer, int bufferLength)
 {
-    return &buffer[findArgumentLength(buffer, bufferLength)];
+    return &buffer[findArgv0Length(buffer, bufferLength)];
 }
 
 
@@ -562,11 +539,16 @@ parseCommandLine(SearchInfo *search)
         return RC_NO_COMMANDLINE;
     }
 
-    const wchar_t *tail = findArgumentEnd(search->originalCmdLine, -1);
-    const wchar_t *end = tail;
-    search->restOfCmdLine = tail;
+    const wchar_t *argv0End = findArgv0End(search->originalCmdLine, -1);
+    const wchar_t *tail = argv0End; // will be start of the executable name
+    const wchar_t *end = argv0End;  // will be end of the executable name
+    search->restOfCmdLine = argv0End;   // will be first space after argv0
     while (--tail != search->originalCmdLine) {
-        if (*tail == L'.' && end == search->restOfCmdLine) {
+        if (*tail == L'"' && end == argv0End) {
+            // Move the "end" up to the quote, so we also allow moving for
+            // a period later on.
+            end = argv0End = tail;
+        } else if (*tail == L'.' && end == argv0End) {
             end = tail;
         } else if (*tail == L'\\' || *tail == L'/') {
             ++tail;



More information about the Python-checkins mailing list