Skip to content

Commit d5e6f9b

Browse files
gh-99442: Fix handling in py.exe launcher when argv[0] does not include a file extension (GH-99542)
(cherry picked from commit a220c6d)
1 parent 82ab9e6 commit d5e6f9b

File tree

3 files changed

+44
-51
lines changed

3 files changed

+44
-51
lines changed

Lib/test/test_launcher.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def find_py(cls):
174174
errors="ignore",
175175
) as p:
176176
p.stdin.close()
177-
version = next(p.stdout).splitlines()[0].rpartition(" ")[2]
177+
version = next(p.stdout, "\n").splitlines()[0].rpartition(" ")[2]
178178
p.stdout.read()
179179
p.wait(10)
180180
if not sys.version.startswith(version):
@@ -468,6 +468,15 @@ def test_py3_default_env(self):
468468
self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
469469
self.assertEqual("X.Y-arm64.exe -X fake_arg_for_test -arg", data["stdout"].strip())
470470

471+
def test_py_default_short_argv0(self):
472+
with self.py_ini(TEST_PY_COMMANDS):
473+
for argv0 in ['"py.exe"', 'py.exe', '"py"', 'py']:
474+
with self.subTest(argv0):
475+
data = self.run_py(["--version"], argv=f'{argv0} --version')
476+
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
477+
self.assertEqual("3.100", data["SearchInfo.tag"])
478+
self.assertEqual(f'X.Y.exe --version', data["stdout"].strip())
479+
471480
def test_py_default_in_list(self):
472481
data = self.run_py(["-0"], env=TEST_PY_ENV)
473482
default = None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix handling in :ref:`launcher` when ``argv[0]`` does not include a file
2+
extension.

PC/launcher2.c

+32-50
Original file line numberDiff line numberDiff line change
@@ -491,62 +491,39 @@ dumpSearchInfo(SearchInfo *search)
491491

492492

493493
int
494-
findArgumentLength(const wchar_t *buffer, int bufferLength)
494+
findArgv0Length(const wchar_t *buffer, int bufferLength)
495495
{
496-
if (bufferLength < 0) {
497-
bufferLength = (int)wcsnlen_s(buffer, MAXLEN);
498-
}
499-
if (bufferLength == 0) {
500-
return 0;
501-
}
502-
const wchar_t *end;
503-
int i;
504-
505-
if (buffer[0] != L'"') {
506-
end = wcschr(buffer, L' ');
507-
if (!end) {
508-
return bufferLength;
509-
}
510-
i = (int)(end - buffer);
511-
return i < bufferLength ? i : bufferLength;
512-
}
513-
514-
i = 0;
515-
while (i < bufferLength) {
516-
end = wcschr(&buffer[i + 1], L'"');
517-
if (!end) {
518-
return bufferLength;
519-
}
520-
521-
i = (int)(end - buffer);
522-
if (i >= bufferLength) {
523-
return bufferLength;
524-
}
525-
526-
int j = i;
527-
while (j > 1 && buffer[--j] == L'\\') {
528-
if (j > 0 && buffer[--j] == L'\\') {
529-
// Even number, so back up and keep counting
530-
} else {
531-
// Odd number, so it's escaped and we want to keep searching
532-
continue;
496+
// Note: this implements semantics that are only valid for argv0.
497+
// Specifically, there is no escaping of quotes, and quotes within
498+
// the argument have no effect. A quoted argv0 must start and end
499+
// with a double quote character; otherwise, it ends at the first
500+
// ' ' or '\t'.
501+
int quoted = buffer[0] == L'"';
502+
for (int i = 1; bufferLength < 0 || i < bufferLength; ++i) {
503+
switch (buffer[i]) {
504+
case L'\0':
505+
return i;
506+
case L' ':
507+
case L'\t':
508+
if (!quoted) {
509+
return i;
533510
}
534-
}
535-
536-
// Non-escaped quote with space after it - end of the argument!
537-
if (i + 1 >= bufferLength || isspace(buffer[i + 1])) {
538-
return i + 1;
511+
break;
512+
case L'"':
513+
if (quoted) {
514+
return i + 1;
515+
}
516+
break;
539517
}
540518
}
541-
542519
return bufferLength;
543520
}
544521

545522

546523
const wchar_t *
547-
findArgumentEnd(const wchar_t *buffer, int bufferLength)
524+
findArgv0End(const wchar_t *buffer, int bufferLength)
548525
{
549-
return &buffer[findArgumentLength(buffer, bufferLength)];
526+
return &buffer[findArgv0Length(buffer, bufferLength)];
550527
}
551528

552529

@@ -562,11 +539,16 @@ parseCommandLine(SearchInfo *search)
562539
return RC_NO_COMMANDLINE;
563540
}
564541

565-
const wchar_t *tail = findArgumentEnd(search->originalCmdLine, -1);
566-
const wchar_t *end = tail;
567-
search->restOfCmdLine = tail;
542+
const wchar_t *argv0End = findArgv0End(search->originalCmdLine, -1);
543+
const wchar_t *tail = argv0End; // will be start of the executable name
544+
const wchar_t *end = argv0End; // will be end of the executable name
545+
search->restOfCmdLine = argv0End; // will be first space after argv0
568546
while (--tail != search->originalCmdLine) {
569-
if (*tail == L'.' && end == search->restOfCmdLine) {
547+
if (*tail == L'"' && end == argv0End) {
548+
// Move the "end" up to the quote, so we also allow moving for
549+
// a period later on.
550+
end = argv0End = tail;
551+
} else if (*tail == L'.' && end == argv0End) {
570552
end = tail;
571553
} else if (*tail == L'\\' || *tail == L'/') {
572554
++tail;

0 commit comments

Comments
 (0)