From 909b2a2a592eec1bd36fd7f85d8b21a0c7a4be12 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Sat, 22 Feb 2025 20:48:47 +0100 Subject: [PATCH 1/6] create tests for a valid and an invalid BOM both fail with the current implementation --- Lib/test/test_launcher.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_launcher.py b/Lib/test/test_launcher.py index 58baae25df3df7..173fc743cf68ae 100644 --- a/Lib/test/test_launcher.py +++ b/Lib/test/test_launcher.py @@ -271,7 +271,10 @@ def py_ini(self, content): @contextlib.contextmanager def script(self, content, encoding="utf-8"): file = Path(tempfile.mktemp(dir=os.getcwd()) + ".py") - file.write_text(content, encoding=encoding) + if isinstance(content, bytes): + file.write_bytes(content) + else: + file.write_text(content, encoding=encoding) try: yield file finally: @@ -624,6 +627,25 @@ def test_py_shebang_short_argv0(self): self.assertEqual("3.100", data["SearchInfo.tag"]) self.assertEqual(f'X.Y.exe -prearg "{script}" -postarg', data["stdout"].strip()) + def test_py_shebang_valid_bom(self): + with self.py_ini(TEST_PY_DEFAULTS): + content = "#! /usr/bin/python -prearg".encode("utf-8") + with self.script(b"\xEF\xBB\xBF" + content) as script: + data = self.run_py([script, "-postarg"]) + self.assertEqual("PythonTestSuite", data["SearchInfo.company"]) + self.assertEqual("3.100", data["SearchInfo.tag"]) + self.assertEqual(f"X.Y.exe -prearg {quote(script)} -postarg", data["stdout"].strip()) + + def test_py_shebang_invalid_bom(self): + with self.py_ini(TEST_PY_DEFAULTS): + content = "#! /usr/bin/python3 -prearg".encode("utf-8") + with self.script(b"\xEF\xAA\xBF" + content) as script: + data = self.run_py([script, "-postarg"]) + self.assertIn("Invalid BOM", data["stderr"]) + self.assertEqual("PythonTestSuite", data["SearchInfo.company"]) + self.assertEqual("3.100", data["SearchInfo.tag"]) + self.assertEqual(f"X.Y.exe {quote(script)} -postarg", data["stdout"].strip()) + def test_py_handle_64_in_ini(self): with self.py_ini("\n".join(["[defaults]", "python=3.999-64"])): # Expect this to fail, but should get oldStyleTag flipped on From f1cbe4f88c6e5a96d3cc3a3c8fd2f0091f77d87f Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Sat, 22 Feb 2025 20:53:23 +0100 Subject: [PATCH 2/6] fix launcher2.c --- PC/launcher2.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PC/launcher2.c b/PC/launcher2.c index befcbe30600f2c..72121724726ccb 100644 --- a/PC/launcher2.c +++ b/PC/launcher2.c @@ -1062,7 +1062,7 @@ checkShebang(SearchInfo *search) } DWORD bytesRead = 0; - char buffer[4096]; + unsigned char buffer[4096]; if (!ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, NULL)) { debug(L"# Failed to read %s for shebang parsing (0x%08X)\n", scriptFile, GetLastError()); @@ -1075,7 +1075,7 @@ checkShebang(SearchInfo *search) free(scriptFile); - char *b = buffer; + unsigned char *b = buffer; bool onlyUtf8 = false; if (bytesRead > 3 && *b == 0xEF) { if (*++b == 0xBB && *++b == 0xBF) { @@ -1096,13 +1096,13 @@ checkShebang(SearchInfo *search) ++b; --bytesRead; while (--bytesRead > 0 && isspace(*++b)) { } - char *start = b; + const unsigned char *start = b; while (--bytesRead > 0 && *++b != '\r' && *b != '\n') { } wchar_t *shebang; int shebangLength; // We add 1 when bytesRead==0, as in that case we hit EOF and b points // to the last character in the file, not the newline - int exitCode = _decodeShebang(search, start, (int)(b - start + (bytesRead == 0)), onlyUtf8, &shebang, &shebangLength); + int exitCode = _decodeShebang(search, (const char*)start, (int)(b - start + (bytesRead == 0)), onlyUtf8, &shebang, &shebangLength); if (exitCode) { return exitCode; } From 1861cc231d8721662ea4ccee5cdc9dd9fcf40f33 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Sun, 9 Mar 2025 19:58:34 +0100 Subject: [PATCH 3/6] blurb it --- .../next/Windows/2025-03-09-19-57-35.gh-issue-131020._c87wf.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Windows/2025-03-09-19-57-35.gh-issue-131020._c87wf.rst diff --git a/Misc/NEWS.d/next/Windows/2025-03-09-19-57-35.gh-issue-131020._c87wf.rst b/Misc/NEWS.d/next/Windows/2025-03-09-19-57-35.gh-issue-131020._c87wf.rst new file mode 100644 index 00000000000000..fd30bd3367b4f5 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2025-03-09-19-57-35.gh-issue-131020._c87wf.rst @@ -0,0 +1,2 @@ +pylauncher (py) does not correctly detect a BOM when searching for the +shebang. Fix by Chris Eibl. From 342c4af67fdd469fc25ec9077fbbf3658de273d1 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Sun, 9 Mar 2025 20:13:17 +0100 Subject: [PATCH 4/6] Update Misc/NEWS.d/next/Windows/2025-03-09-19-57-35.gh-issue-131020._c87wf.rst Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- .../next/Windows/2025-03-09-19-57-35.gh-issue-131020._c87wf.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Windows/2025-03-09-19-57-35.gh-issue-131020._c87wf.rst b/Misc/NEWS.d/next/Windows/2025-03-09-19-57-35.gh-issue-131020._c87wf.rst index fd30bd3367b4f5..35cd1dd63e75b0 100644 --- a/Misc/NEWS.d/next/Windows/2025-03-09-19-57-35.gh-issue-131020._c87wf.rst +++ b/Misc/NEWS.d/next/Windows/2025-03-09-19-57-35.gh-issue-131020._c87wf.rst @@ -1,2 +1,2 @@ -pylauncher (py) does not correctly detect a BOM when searching for the +:source:`pylauncher ` correctly detects a BOM when searching for the shebang. Fix by Chris Eibl. From 1a245faba9875dc90eb834794b29b927c511aa49 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Mon, 10 Mar 2025 07:52:10 +0100 Subject: [PATCH 5/6] add test_py_default_with_valid_bom and mark as expected failure until GH-99620 is fixed --- Lib/test/test_launcher.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_launcher.py b/Lib/test/test_launcher.py index 173fc743cf68ae..31896a3823a5fd 100644 --- a/Lib/test/test_launcher.py +++ b/Lib/test/test_launcher.py @@ -157,7 +157,10 @@ def __enter__(self): self._preserved = self.path.read_bytes() except FileNotFoundError: self._preserved = None - self.path.write_text(self.content, encoding="utf-16") + if isinstance(self.content, bytes): + self.path.write_bytes(self.content) + else: + self.path.write_text(self.content, encoding="utf-16") def __exit__(self, *exc_info): if self._preserved is None: @@ -472,6 +475,15 @@ def test_py_default(self): self.assertEqual("3.100", data["SearchInfo.tag"]) self.assertEqual("X.Y.exe -arg", data["stdout"].strip()) + @unittest.expectedFailure # fails until GH-99620 is fixed + def test_py_default_with_valid_bom(self): + content = TEST_PY_DEFAULTS.encode("utf-8") + with self.py_ini(b"\xEF\xBB\xBF" + content): + data = self.run_py(["-arg"]) + self.assertEqual("PythonTestSuite", data["SearchInfo.company"]) + self.assertEqual("3.100", data["SearchInfo.tag"]) + self.assertEqual("X.Y.exe -arg", data["stdout"].strip()) + def test_py2_default(self): with self.py_ini(TEST_PY_DEFAULTS): data = self.run_py(["-2", "-arg"]) From 6a501dc8ccb0ea0da5b2e8b010d6f96fac857900 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Mon, 10 Mar 2025 18:33:47 +0100 Subject: [PATCH 6/6] Revert "add test_py_default_with_valid_bom and mark as expected failure" This reverts commit 1a245faba9875dc90eb834794b29b927c511aa49. Was merely a demo to show that this PR does not fix the other issue :) --- Lib/test/test_launcher.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Lib/test/test_launcher.py b/Lib/test/test_launcher.py index 31896a3823a5fd..173fc743cf68ae 100644 --- a/Lib/test/test_launcher.py +++ b/Lib/test/test_launcher.py @@ -157,10 +157,7 @@ def __enter__(self): self._preserved = self.path.read_bytes() except FileNotFoundError: self._preserved = None - if isinstance(self.content, bytes): - self.path.write_bytes(self.content) - else: - self.path.write_text(self.content, encoding="utf-16") + self.path.write_text(self.content, encoding="utf-16") def __exit__(self, *exc_info): if self._preserved is None: @@ -475,15 +472,6 @@ def test_py_default(self): self.assertEqual("3.100", data["SearchInfo.tag"]) self.assertEqual("X.Y.exe -arg", data["stdout"].strip()) - @unittest.expectedFailure # fails until GH-99620 is fixed - def test_py_default_with_valid_bom(self): - content = TEST_PY_DEFAULTS.encode("utf-8") - with self.py_ini(b"\xEF\xBB\xBF" + content): - data = self.run_py(["-arg"]) - self.assertEqual("PythonTestSuite", data["SearchInfo.company"]) - self.assertEqual("3.100", data["SearchInfo.tag"]) - self.assertEqual("X.Y.exe -arg", data["stdout"].strip()) - def test_py2_default(self): with self.py_ini(TEST_PY_DEFAULTS): data = self.run_py(["-2", "-arg"])