diff --git a/Lib/os.py b/Lib/os.py index b4c9f84c36d..ac03b416390 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -10,7 +10,7 @@ - os.extsep is the extension separator (always '.') - os.altsep is the alternate pathname separator (None or '/') - os.pathsep is the component separator used in $PATH etc - - os.linesep is the line separator in text files ('\r' or '\n' or '\r\n') + - os.linesep is the line separator in text files ('\n' or '\r\n') - os.defpath is the default search path for executables - os.devnull is the file path of the null device ('/dev/null', etc.) @@ -64,6 +64,10 @@ def _get_exports_list(module): from posix import _have_functions except ImportError: pass + try: + from posix import _create_environ + except ImportError: + pass import posix __all__.extend(_get_exports_list(posix)) @@ -88,6 +92,10 @@ def _get_exports_list(module): from nt import _have_functions except ImportError: pass + try: + from nt import _create_environ + except ImportError: + pass else: raise ImportError('no os specific module found') @@ -366,61 +374,45 @@ def walk(top, topdown=True, onerror=None, followlinks=False): # minor reason when (say) a thousand readable directories are still # left to visit. try: - scandir_it = scandir(top) + with scandir(top) as entries: + for entry in entries: + try: + if followlinks is _walk_symlinks_as_files: + is_dir = entry.is_dir(follow_symlinks=False) and not entry.is_junction() + else: + is_dir = entry.is_dir() + except OSError: + # If is_dir() raises an OSError, consider the entry not to + # be a directory, same behaviour as os.path.isdir(). + is_dir = False + + if is_dir: + dirs.append(entry.name) + else: + nondirs.append(entry.name) + + if not topdown and is_dir: + # Bottom-up: traverse into sub-directory, but exclude + # symlinks to directories if followlinks is False + if followlinks: + walk_into = True + else: + try: + is_symlink = entry.is_symlink() + except OSError: + # If is_symlink() raises an OSError, consider the + # entry not to be a symbolic link, same behaviour + # as os.path.islink(). + is_symlink = False + walk_into = not is_symlink + + if walk_into: + walk_dirs.append(entry.path) except OSError as error: if onerror is not None: onerror(error) continue - cont = False - with scandir_it: - while True: - try: - try: - entry = next(scandir_it) - except StopIteration: - break - except OSError as error: - if onerror is not None: - onerror(error) - cont = True - break - - try: - if followlinks is _walk_symlinks_as_files: - is_dir = entry.is_dir(follow_symlinks=False) and not entry.is_junction() - else: - is_dir = entry.is_dir() - except OSError: - # If is_dir() raises an OSError, consider the entry not to - # be a directory, same behaviour as os.path.isdir(). - is_dir = False - - if is_dir: - dirs.append(entry.name) - else: - nondirs.append(entry.name) - - if not topdown and is_dir: - # Bottom-up: traverse into sub-directory, but exclude - # symlinks to directories if followlinks is False - if followlinks: - walk_into = True - else: - try: - is_symlink = entry.is_symlink() - except OSError: - # If is_symlink() raises an OSError, consider the - # entry not to be a symbolic link, same behaviour - # as os.path.islink(). - is_symlink = False - walk_into = not is_symlink - - if walk_into: - walk_dirs.append(entry.path) - if cont: - continue - if topdown: # Yield before sub-directory traversal if going top down yield top, dirs, nondirs @@ -774,7 +766,7 @@ def __ror__(self, other): new.update(self) return new -def _createenviron(): +def _create_environ_mapping(): if name == 'nt': # Where Env Var Names Must Be UPPERCASE def check_str(value): @@ -804,9 +796,24 @@ def decode(value): encode, decode) # unicode environ -environ = _createenviron() -del _createenviron +environ = _create_environ_mapping() +del _create_environ_mapping + + +if _exists("_create_environ"): + def reload_environ(): + data = _create_environ() + if name == 'nt': + encodekey = environ.encodekey + data = {encodekey(key): value + for key, value in data.items()} + + # modify in-place to keep os.environb in sync + env_data = environ._data + env_data.clear() + env_data.update(data) + __all__.append("reload_environ") def getenv(key, default=None): """Get an environment variable, return None if it doesn't exist. diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py index ab580dfad0f..1a44cedcd36 100644 --- a/Lib/test/test_genericpath.py +++ b/Lib/test/test_genericpath.py @@ -170,7 +170,6 @@ def test_exists_fd(self): os.close(w) self.assertFalse(self.pathmodule.exists(r)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_exists_bool(self): for fd in False, True: with self.assertWarnsRegex(RuntimeWarning, diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 653a05dd011..f1119d2d1d8 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -104,7 +104,7 @@ def create_file(filename, content=b'content'): def tearDownModule(): - asyncio.set_event_loop_policy(None) + asyncio.events._set_event_loop_policy(None) class MiscTests(unittest.TestCase): @@ -187,10 +187,6 @@ def test_access(self): os.close(f) self.assertTrue(os.access(os_helper.TESTFN, os.W_OK)) - @unittest.skipIf(sys.platform == 'win32', "TODO: RUSTPYTHON; BrokenPipeError: (32, 'The process cannot access the file because it is being used by another process. (os error 32)')") - @unittest.skipIf( - support.is_emscripten, "Test is unstable under Emscripten." - ) @unittest.skipIf( support.is_wasi, "WASI does not support dup." ) @@ -233,6 +229,94 @@ def test_read(self): self.assertEqual(type(s), bytes) self.assertEqual(s, b"spam") + def test_readinto(self): + with open(os_helper.TESTFN, "w+b") as fobj: + fobj.write(b"spam") + fobj.flush() + fd = fobj.fileno() + os.lseek(fd, 0, 0) + # Oversized so readinto without hitting end. + buffer = bytearray(7) + s = os.readinto(fd, buffer) + self.assertEqual(type(s), int) + self.assertEqual(s, 4) + # Should overwrite the first 4 bytes of the buffer. + self.assertEqual(buffer[:4], b"spam") + + # Readinto at EOF should return 0 and not touch buffer. + buffer[:] = b"notspam" + s = os.readinto(fd, buffer) + self.assertEqual(type(s), int) + self.assertEqual(s, 0) + self.assertEqual(bytes(buffer), b"notspam") + s = os.readinto(fd, buffer) + self.assertEqual(s, 0) + self.assertEqual(bytes(buffer), b"notspam") + + # Readinto a 0 length bytearray when at EOF should return 0 + self.assertEqual(os.readinto(fd, bytearray()), 0) + + # Readinto a 0 length bytearray with data available should return 0. + os.lseek(fd, 0, 0) + self.assertEqual(os.readinto(fd, bytearray()), 0) + + @unittest.skipUnless(hasattr(os, 'get_blocking'), + 'needs os.get_blocking() and os.set_blocking()') + @unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()") + @unittest.skipIf(support.is_emscripten, "set_blocking does not work correctly") + def test_readinto_non_blocking(self): + # Verify behavior of a readinto which would block on a non-blocking fd. + r, w = os.pipe() + try: + os.set_blocking(r, False) + with self.assertRaises(BlockingIOError): + os.readinto(r, bytearray(5)) + + # Pass some data through + os.write(w, b"spam") + self.assertEqual(os.readinto(r, bytearray(4)), 4) + + # Still don't block or return 0. + with self.assertRaises(BlockingIOError): + os.readinto(r, bytearray(5)) + + # At EOF should return size 0 + os.close(w) + w = None + self.assertEqual(os.readinto(r, bytearray(5)), 0) + self.assertEqual(os.readinto(r, bytearray(5)), 0) # Still EOF + + finally: + os.close(r) + if w is not None: + os.close(w) + + def test_readinto_badarg(self): + with open(os_helper.TESTFN, "w+b") as fobj: + fobj.write(b"spam") + fobj.flush() + fd = fobj.fileno() + os.lseek(fd, 0, 0) + + for bad_arg in ("test", bytes(), 14): + with self.subTest(f"bad buffer {type(bad_arg)}"): + with self.assertRaises(TypeError): + os.readinto(fd, bad_arg) + + with self.subTest("doesn't work on file objects"): + with self.assertRaises(TypeError): + os.readinto(fobj, bytearray(5)) + + # takes two args + with self.assertRaises(TypeError): + os.readinto(fd) + + # No data should have been read with the bad arguments. + buffer = bytearray(4) + s = os.readinto(fd, buffer) + self.assertEqual(s, 4) + self.assertEqual(buffer, b"spam") + @support.cpython_only # Skip the test on 32-bit platforms: the number of bytes must fit in a # Py_ssize_t type @@ -252,6 +336,29 @@ def test_large_read(self, size): # operating system is free to return less bytes than requested. self.assertEqual(data, b'test') + + @support.cpython_only + # Skip the test on 32-bit platforms: the number of bytes must fit in a + # Py_ssize_t type + @unittest.skipUnless(INT_MAX < PY_SSIZE_T_MAX, + "needs INT_MAX < PY_SSIZE_T_MAX") + @support.bigmemtest(size=INT_MAX + 10, memuse=1, dry_run=False) + def test_large_readinto(self, size): + self.addCleanup(os_helper.unlink, os_helper.TESTFN) + create_file(os_helper.TESTFN, b'test') + + # Issue #21932: For readinto the buffer contains the length rather than + # a length being passed explicitly to read, should still get capped to a + # valid size / not raise an OverflowError for sizes larger than INT_MAX. + buffer = bytearray(INT_MAX + 10) + with open(os_helper.TESTFN, "rb") as fp: + length = os.readinto(fp.fileno(), buffer) + + # The test does not try to read more than 2 GiB at once because the + # operating system is free to return less bytes than requested. + self.assertEqual(length, 4) + self.assertEqual(buffer[:4], b'test') + def test_write(self): # os.write() accepts bytes- and buffer-like objects but not strings fd = os.open(os_helper.TESTFN, os.O_CREAT | os.O_WRONLY) @@ -710,7 +817,7 @@ def test_15261(self): self.assertEqual(ctx.exception.errno, errno.EBADF) def check_file_attributes(self, result): - self.assertTrue(hasattr(result, 'st_file_attributes')) + self.assertHasAttr(result, 'st_file_attributes') self.assertTrue(isinstance(result.st_file_attributes, int)) self.assertTrue(0 <= result.st_file_attributes <= 0xFFFFFFFF) @@ -805,14 +912,28 @@ def _test_utime(self, set_time, filename=None): set_time(filename, (atime_ns, mtime_ns)) st = os.stat(filename) - if support_subsecond: - self.assertAlmostEqual(st.st_atime, atime_ns * 1e-9, delta=1e-6) - self.assertAlmostEqual(st.st_mtime, mtime_ns * 1e-9, delta=1e-6) + if support.is_emscripten: + # Emscripten timestamps are roundtripped through a 53 bit integer of + # nanoseconds. If we want to represent ~50 years which is an 11 + # digits number of seconds: + # 2*log10(60) + log10(24) + log10(365) + log10(60) + log10(50) + # is about 11. Because 53 * log10(2) is about 16, we only have 5 + # digits worth of sub-second precision. + # Some day it would be good to fix this upstream. + delta=1e-5 + self.assertAlmostEqual(st.st_atime, atime_ns * 1e-9, delta=1e-5) + self.assertAlmostEqual(st.st_mtime, mtime_ns * 1e-9, delta=1e-5) + self.assertAlmostEqual(st.st_atime_ns, atime_ns, delta=1e9 * 1e-5) + self.assertAlmostEqual(st.st_mtime_ns, mtime_ns, delta=1e9 * 1e-5) else: - self.assertEqual(st.st_atime, atime_ns * 1e-9) - self.assertEqual(st.st_mtime, mtime_ns * 1e-9) - self.assertEqual(st.st_atime_ns, atime_ns) - self.assertEqual(st.st_mtime_ns, mtime_ns) + if support_subsecond: + self.assertAlmostEqual(st.st_atime, atime_ns * 1e-9, delta=1e-6) + self.assertAlmostEqual(st.st_mtime, mtime_ns * 1e-9, delta=1e-6) + else: + self.assertEqual(st.st_atime, atime_ns * 1e-9) + self.assertEqual(st.st_mtime, mtime_ns * 1e-9) + self.assertEqual(st.st_atime_ns, atime_ns) + self.assertEqual(st.st_mtime_ns, mtime_ns) def test_utime(self): def set_time(filename, ns): @@ -825,9 +946,7 @@ def ns_to_sec(ns): # Convert a number of nanosecond (int) to a number of seconds (float). # Round towards infinity by adding 0.5 nanosecond to avoid rounding # issue, os.utime() rounds towards minus infinity. - # XXX: RUSTPYTHON os.utime() use `[Duration::from_secs_f64](https://doc.rust-lang.org/std/time/struct.Duration.html#method.try_from_secs_f64)` - # return (ns * 1e-9) + 0.5e-9 - return (ns * 1e-9) + return (ns * 1e-9) + 0.5e-9 def test_utime_by_indexed(self): # pass times as floating-point seconds as the second indexed parameter @@ -1300,6 +1419,52 @@ def test_ror_operator(self): self._test_underlying_process_env('_A_', '') self._test_underlying_process_env(overridden_key, original_value) + def test_reload_environ(self): + # Test os.reload_environ() + has_environb = hasattr(os, 'environb') + + # Test with putenv() which doesn't update os.environ + os.environ['test_env'] = 'python_value' + os.putenv("test_env", "new_value") + self.assertEqual(os.environ['test_env'], 'python_value') + if has_environb: + self.assertEqual(os.environb[b'test_env'], b'python_value') + + os.reload_environ() + self.assertEqual(os.environ['test_env'], 'new_value') + if has_environb: + self.assertEqual(os.environb[b'test_env'], b'new_value') + + # Test with unsetenv() which doesn't update os.environ + os.unsetenv('test_env') + self.assertEqual(os.environ['test_env'], 'new_value') + if has_environb: + self.assertEqual(os.environb[b'test_env'], b'new_value') + + os.reload_environ() + self.assertNotIn('test_env', os.environ) + if has_environb: + self.assertNotIn(b'test_env', os.environb) + + if has_environb: + # test reload_environ() on os.environb with putenv() + os.environb[b'test_env'] = b'python_value2' + os.putenv("test_env", "new_value2") + self.assertEqual(os.environb[b'test_env'], b'python_value2') + self.assertEqual(os.environ['test_env'], 'python_value2') + + os.reload_environ() + self.assertEqual(os.environb[b'test_env'], b'new_value2') + self.assertEqual(os.environ['test_env'], 'new_value2') + + # test reload_environ() on os.environb with unsetenv() + os.unsetenv('test_env') + self.assertEqual(os.environb[b'test_env'], b'new_value2') + self.assertEqual(os.environ['test_env'], 'new_value2') + + os.reload_environ() + self.assertNotIn(b'test_env', os.environb) + self.assertNotIn('test_env', os.environ) class WalkTests(unittest.TestCase): """Tests for os.walk().""" @@ -1370,9 +1535,7 @@ def setUp(self): else: self.sub2_tree = (sub2_path, ["SUB21"], ["tmp3"]) - if not support.is_emscripten: - # Emscripten fails with inaccessible directory - os.chmod(sub21_path, 0) + os.chmod(sub21_path, 0) try: os.listdir(sub21_path) except PermissionError: @@ -1668,9 +1831,6 @@ def test_yields_correct_dir_fd(self): # check that listdir() returns consistent information self.assertEqual(set(os.listdir(rootfd)), set(dirs) | set(files)) - @unittest.skipIf( - support.is_emscripten, "Cannot dup stdout on Emscripten" - ) @unittest.skipIf( support.is_android, "dup return value is unpredictable on Android" ) @@ -1687,9 +1847,6 @@ def test_fd_leak(self): self.addCleanup(os.close, newfd) self.assertEqual(newfd, minfd) - @unittest.skipIf( - support.is_emscripten, "Cannot dup stdout on Emscripten" - ) @unittest.skipIf( support.is_android, "dup return value is unpredictable on Android" ) @@ -1725,17 +1882,6 @@ def walk(self, top, **kwargs): bdirs[:] = list(map(os.fsencode, dirs)) bfiles[:] = list(map(os.fsencode, files)) - @unittest.expectedFailure # TODO: RUSTPYTHON; WalkTests doesn't have these methods - def test_compare_to_walk(self): - return super().test_compare_to_walk() - - @unittest.expectedFailure # TODO: RUSTPYTHON; WalkTests doesn't have these methods - def test_dir_fd(self): - return super().test_dir_fd() - - @unittest.expectedFailure # TODO: RUSTPYTHON; WalkTests doesn't have these methods - def test_yields_correct_dir_fd(self): - return super().test_yields_correct_dir_fd() @unittest.skipUnless(hasattr(os, 'fwalk'), "Test needs os.fwalk()") class BytesFwalkTests(FwalkTests): @@ -1770,10 +1916,12 @@ def test_makedir(self): os.makedirs(path) @unittest.skipIf( - support.is_emscripten or support.is_wasi, - "Emscripten's/WASI's umask is a stub." + support.is_wasi, + "WASI's umask is a stub." ) def test_mode(self): + # Note: in some cases, the umask might already be 2 in which case this + # will pass even if os.umask is actually broken. with os_helper.temp_umask(0o002): base = os_helper.TESTFN parent = os.path.join(base, 'dir1') @@ -1786,8 +1934,8 @@ def test_mode(self): self.assertEqual(os.stat(parent).st_mode & 0o777, 0o775) @unittest.skipIf( - support.is_emscripten or support.is_wasi, - "Emscripten's/WASI's umask is a stub." + support.is_wasi, + "WASI's umask is a stub." ) def test_exist_ok_existing_directory(self): path = os.path.join(os_helper.TESTFN, 'dir1') @@ -1804,8 +1952,8 @@ def test_exist_ok_existing_directory(self): os.makedirs(os.path.abspath('/'), exist_ok=True) @unittest.skipIf( - support.is_emscripten or support.is_wasi, - "Emscripten's/WASI's umask is a stub." + support.is_wasi, + "WASI's umask is a stub." ) def test_exist_ok_s_isgid_directory(self): path = os.path.join(os_helper.TESTFN, 'dir1') @@ -2035,7 +2183,7 @@ def test_getrandom0(self): self.assertEqual(empty, b'') def test_getrandom_random(self): - self.assertTrue(hasattr(os, 'GRND_RANDOM')) + self.assertHasAttr(os, 'GRND_RANDOM') # Don't test os.getrandom(1, os.GRND_RANDOM) to not consume the rare # resource /dev/random @@ -2319,9 +2467,13 @@ def test_chmod(self): @unittest.skipIf(support.is_wasi, "Cannot create invalid FD on WASI.") class TestInvalidFD(unittest.TestCase): - singles = ["fchdir", "dup", "fdatasync", "fstat", - "fstatvfs", "fsync", "tcgetpgrp", "ttyname"] - singles_fildes = {"fchdir", "fdatasync", "fsync"} + singles = ["fchdir", "dup", "fstat", "fstatvfs", "tcgetpgrp", "ttyname"] + singles_fildes = {"fchdir"} + # systemd-nspawn --suppress-sync=true does not verify fd passed + # fdatasync() and fsync(), and always returns success + if not support.in_systemd_nspawn_sync_suppressed(): + singles += ["fdatasync", "fsync"] + singles_fildes |= {"fdatasync", "fsync"} #singles.append("close") #We omit close because it doesn't raise an exception on some platforms def get_single(f): @@ -2379,10 +2531,6 @@ def test_dup2(self): self.check(os.dup2, 20) @unittest.skipUnless(hasattr(os, 'dup2'), 'test needs os.dup2()') - @unittest.skipIf( - support.is_emscripten, - "dup2() with negative fds is broken on Emscripten (see gh-102179)" - ) def test_dup2_negative_fd(self): valid_fd = os.open(__file__, os.O_RDONLY) self.addCleanup(os.close, valid_fd) @@ -2406,20 +2554,21 @@ def test_fchmod(self): def test_fchown(self): self.check(os.fchown, -1, -1) - @unittest.expectedFailure # TODO: RUSTPYTHON @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()') - @unittest.skipIf( - support.is_emscripten or support.is_wasi, - "musl libc issue on Emscripten/WASI, bpo-46390" - ) def test_fpathconf(self): self.assertIn("PC_NAME_MAX", os.pathconf_names) - self.check(os.pathconf, "PC_NAME_MAX") - self.check(os.fpathconf, "PC_NAME_MAX") self.check_bool(os.pathconf, "PC_NAME_MAX") self.check_bool(os.fpathconf, "PC_NAME_MAX") - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()') + @unittest.skipIf( + support.linked_to_musl(), + 'musl pathconf ignores the file descriptor and returns a constant', + ) + def test_fpathconf_bad_fd(self): + self.check(os.pathconf, "PC_NAME_MAX") + self.check(os.fpathconf, "PC_NAME_MAX") + @unittest.skipUnless(hasattr(os, 'ftruncate'), 'test needs os.ftruncate()') def test_ftruncate(self): self.check(os.truncate, 0) @@ -2434,6 +2583,10 @@ def test_lseek(self): def test_read(self): self.check(os.read, 1) + @unittest.skipUnless(hasattr(os, 'readinto'), 'test needs os.readinto()') + def test_readinto(self): + self.check(os.readinto, bytearray(5)) + @unittest.skipUnless(hasattr(os, 'readv'), 'test needs os.readv()') def test_readv(self): buf = bytearray(10) @@ -2462,13 +2615,8 @@ def test_blocking(self): self.check(os.get_blocking) self.check(os.set_blocking, True) - @unittest.expectedFailure # TODO: RUSTPYTHON - def test_fchdir(self): - return super().test_fchdir() - @unittest.expectedFailure # TODO: RUSTPYTHON - def test_fsync(self): - return super().test_fsync() + @unittest.skipUnless(hasattr(os, 'link'), 'requires os.link') @@ -3355,9 +3503,6 @@ def test_bad_fd(self): @unittest.skipUnless(os.isatty(0) and not win32_is_iot() and (sys.platform.startswith('win') or (hasattr(locale, 'nl_langinfo') and hasattr(locale, 'CODESET'))), 'test requires a tty and either Windows or nl_langinfo(CODESET)') - @unittest.skipIf( - support.is_emscripten, "Cannot get encoding of stdin on Emscripten" - ) def test_device_encoding(self): encoding = os.device_encoding(0) self.assertIsNotNone(encoding) @@ -3485,8 +3630,8 @@ def test_spawnl(self): exitcode = os.spawnl(os.P_WAIT, program, *args) self.assertEqual(exitcode, self.exitcode) + @unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; fix spawnve on Windows") @requires_os_func('spawnle') - @unittest.skipIf(sys.platform == 'win32', "TODO: RUSTPYTHON; fix spawnve on Windows") def test_spawnle(self): program, args = self.create_args(with_env=True) exitcode = os.spawnle(os.P_WAIT, program, *args, self.env) @@ -3514,8 +3659,8 @@ def test_spawnv(self): exitcode = os.spawnv(os.P_WAIT, FakePath(program), args) self.assertEqual(exitcode, self.exitcode) + @unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; fix spawnve on Windows") @requires_os_func('spawnve') - @unittest.skipIf(sys.platform == 'win32', "TODO: RUSTPYTHON; fix spawnve on Windows") def test_spawnve(self): program, args = self.create_args(with_env=True) exitcode = os.spawnve(os.P_WAIT, program, args, self.env) @@ -3539,7 +3684,6 @@ def test_nowait(self): pid = os.spawnv(os.P_NOWAIT, program, args) support.wait_process(pid, exitcode=self.exitcode) - @unittest.expectedFailure # TODO: RUSTPYTHON; fix spawnv bytes @requires_os_func('spawnve') def test_spawnve_bytes(self): # Test bytes handling in parse_arglist and parse_envlist (#28114) @@ -3623,8 +3767,8 @@ def _test_invalid_env(self, spawn): exitcode = spawn(os.P_WAIT, program, args, newenv) self.assertEqual(exitcode, 0) + @unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; fix spawnve on Windows") @requires_os_func('spawnve') - @unittest.skipIf(sys.platform == 'win32', "TODO: RUSTPYTHON; fix spawnve on Windows") def test_spawnve_invalid_env(self): self._test_invalid_env(os.spawnve) @@ -4989,7 +5133,7 @@ def check_entry(self, entry, name, is_dir, is_file, is_symlink): entry_lstat, os.name == 'nt') - @unittest.skipIf(sys.platform == 'linux', 'TODO: RUSTPYTHON; flaky test') + @unittest.skipIf(sys.platform == "linux", "TODO: RUSTPYTHON; flaky test") def test_attributes(self): link = os_helper.can_hardlink() symlink = os_helper.can_symlink() @@ -5171,7 +5315,7 @@ def test_bytes_like(self): with self.assertRaises(TypeError): os.scandir(path_bytes) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: not found in {, , , , , , , , , , , } @unittest.skipUnless(os.listdir in os.supports_fd, 'fd support for listdir required for this test.') def test_fd(self): @@ -5253,7 +5397,6 @@ def test_context_manager_exception(self): with self.check_no_resource_warning(): del iterator - @unittest.expectedFailure # TODO: RUSTPYTHON def test_resource_warning(self): self.create_file("file.txt") self.create_file("file2.txt") @@ -5293,8 +5436,8 @@ def test_fsencode_fsdecode(self): def test_pathlike(self): self.assertEqual('#feelthegil', self.fspath(FakePath('#feelthegil'))) - self.assertTrue(issubclass(FakePath, os.PathLike)) - self.assertTrue(isinstance(FakePath('x'), os.PathLike)) + self.assertIsSubclass(FakePath, os.PathLike) + self.assertIsInstance(FakePath('x'), os.PathLike) def test_garbage_in_exception_out(self): vapor = type('blah', (), {}) @@ -5320,8 +5463,8 @@ def test_pathlike_subclasshook(self): # true on abstract implementation. class A(os.PathLike): pass - self.assertFalse(issubclass(FakePath, A)) - self.assertTrue(issubclass(FakePath, os.PathLike)) + self.assertNotIsSubclass(FakePath, A) + self.assertIsSubclass(FakePath, os.PathLike) def test_pathlike_class_getitem(self): self.assertIsInstance(os.PathLike[bytes], types.GenericAlias) @@ -5331,7 +5474,7 @@ class A(os.PathLike): __slots__ = () def __fspath__(self): return '' - self.assertFalse(hasattr(A(), '__dict__')) + self.assertNotHasAttr(A(), '__dict__') def test_fspath_set_to_None(self): class Foo: @@ -5435,7 +5578,7 @@ def test_fork_warns_when_non_python_thread_exists(self): self.assertEqual(err.decode("utf-8"), "") self.assertEqual(out.decode("utf-8"), "") - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b"can't fork at interpreter shutdown" not found in b"Exception ignored in: \nAttributeError: 'NoneType' object has no attribute 'fork'\n" def test_fork_at_finalization(self): code = """if 1: import atexit diff --git a/Lib/test/test_popen.py b/Lib/test/test_popen.py index e6bfc480cbd..34cda35b17b 100644 --- a/Lib/test/test_popen.py +++ b/Lib/test/test_popen.py @@ -57,14 +57,21 @@ def test_return_code(self): def test_contextmanager(self): with os.popen("echo hello") as f: self.assertEqual(f.read(), "hello\n") + self.assertFalse(f.closed) + self.assertTrue(f.closed) def test_iterating(self): with os.popen("echo hello") as f: self.assertEqual(list(f), ["hello\n"]) + self.assertFalse(f.closed) + self.assertTrue(f.closed) def test_keywords(self): - with os.popen(cmd="exit 0", mode="w", buffering=-1): - pass + with os.popen(cmd="echo hello", mode="r", buffering=-1) as f: + self.assertEqual(f.read(), "hello\n") + self.assertFalse(f.closed) + self.assertTrue(f.closed) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py index a4809a23798..4589180d7ae 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -583,7 +583,6 @@ def test_confstr(self): self.assertGreater(len(path), 0) self.assertEqual(posix.confstr(posix.confstr_names["CS_PATH"]), path) - @unittest.expectedFailureIf(sys.platform in ('darwin', 'linux'), '''TODO: RUSTPYTHON; AssertionError: "configuration names must be strings or integers" does not match "Expected type 'str' but 'float' found."''') @unittest.skipUnless(hasattr(posix, 'sysconf'), 'test needs posix.sysconf()') def test_sysconf(self): @@ -1018,7 +1017,7 @@ def test_chmod_dir(self): target = self.tempdir() self.check_chmod(posix.chmod, target) - @unittest.skipIf(sys.platform in ('darwin', 'linux'), 'TODO: RUSTPYTHON; crash') + @unittest.skipIf(sys.platform in ("darwin", "linux"), "TODO: RUSTPYTHON; crash") @os_helper.skip_unless_working_chmod def test_fchmod_file(self): with open(os_helper.TESTFN, 'wb+') as f: @@ -1075,7 +1074,7 @@ def test_chmod_file_symlink(self): self.check_chmod_link(posix.chmod, target, link) self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True) - @unittest.skipIf(sys.platform == 'win32', 'TODO: RUSTPYTHON; flaky') + @unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; flaky") @os_helper.skip_unless_symlink def test_chmod_dir_symlink(self): target = self.tempdir() @@ -1110,7 +1109,7 @@ def test_lchmod_dir_symlink(self): def _test_chflags_regular_file(self, chflags_func, target_file, **kwargs): st = os.stat(target_file) - self.assertTrue(hasattr(st, 'st_flags')) + self.assertHasAttr(st, 'st_flags') # ZFS returns EOPNOTSUPP when attempting to set flag UF_IMMUTABLE. flags = st.st_flags | stat.UF_IMMUTABLE @@ -1146,7 +1145,7 @@ def test_lchflags_regular_file(self): def test_lchflags_symlink(self): testfn_st = os.stat(os_helper.TESTFN) - self.assertTrue(hasattr(testfn_st, 'st_flags')) + self.assertHasAttr(testfn_st, 'st_flags') self.addCleanup(os_helper.unlink, _DUMMY_SYMLINK) os.symlink(os_helper.TESTFN, _DUMMY_SYMLINK) @@ -1350,7 +1349,6 @@ def test_get_and_set_scheduler_and_param(self): param = posix.sched_param(sched_priority=-large) self.assertRaises(OverflowError, posix.sched_setparam, 0, param) - @unittest.expectedFailureIf(sys.platform == 'linux', "TODO: RUSTPYTHON; TypeError: cannot pickle 'sched_param' object") @requires_sched def test_sched_param(self): param = posix.sched_param(1) @@ -1370,6 +1368,14 @@ def test_sched_param(self): self.assertNotEqual(newparam, param) self.assertEqual(newparam.sched_priority, 0) + @requires_sched + def test_bug_140634(self): + sched_priority = float('inf') # any new reference + param = posix.sched_param(sched_priority) + param.__reduce__() + del sched_priority, param # should not crash + support.gc_collect() # just to be sure + @unittest.skipUnless(hasattr(posix, "sched_rr_get_interval"), "no function") def test_sched_rr_get_interval(self): try: @@ -1525,6 +1531,51 @@ def test_pidfd_open(self): self.assertEqual(cm.exception.errno, errno.EINVAL) os.close(os.pidfd_open(os.getpid(), 0)) + @os_helper.skip_unless_hardlink + @os_helper.skip_unless_symlink + def test_link_follow_symlinks(self): + default_follow = sys.platform.startswith( + ('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly', 'sunos5')) + default_no_follow = sys.platform.startswith(('win32', 'linux')) + orig = os_helper.TESTFN + symlink = orig + 'symlink' + posix.symlink(orig, symlink) + self.addCleanup(os_helper.unlink, symlink) + + with self.subTest('no follow_symlinks'): + # no follow_symlinks -> platform depending + link = orig + 'link' + posix.link(symlink, link) + self.addCleanup(os_helper.unlink, link) + if os.link in os.supports_follow_symlinks or default_follow: + self.assertEqual(posix.lstat(link), posix.lstat(orig)) + elif default_no_follow: + self.assertEqual(posix.lstat(link), posix.lstat(symlink)) + + with self.subTest('follow_symlinks=False'): + # follow_symlinks=False -> duplicate the symlink itself + link = orig + 'link_nofollow' + try: + posix.link(symlink, link, follow_symlinks=False) + except NotImplementedError: + if os.link in os.supports_follow_symlinks or default_no_follow: + raise + else: + self.addCleanup(os_helper.unlink, link) + self.assertEqual(posix.lstat(link), posix.lstat(symlink)) + + with self.subTest('follow_symlinks=True'): + # follow_symlinks=True -> duplicate the target file + link = orig + 'link_following' + try: + posix.link(symlink, link, follow_symlinks=True) + except NotImplementedError: + if os.link in os.supports_follow_symlinks or default_follow: + raise + else: + self.addCleanup(os_helper.unlink, link) + self.assertEqual(posix.lstat(link), posix.lstat(orig)) + # tests for the posix *at functions follow class TestPosixDirFd(unittest.TestCase): @@ -1570,7 +1621,6 @@ def test_chown_dir_fd(self): with self.prepare_file() as (dir_fd, name, fullname): posix.chown(name, os.getuid(), os.getgid(), dir_fd=dir_fd) - @unittest.expectedFailureIf(sys.platform in ('darwin', 'linux'), 'TODO: RUSTPYTHON; AssertionError: RuntimeWarning not triggered') @unittest.skipUnless(os.stat in os.supports_dir_fd, "test needs dir_fd support in os.stat()") def test_stat_dir_fd(self): with self.prepare() as (dir_fd, name, fullname): @@ -1973,7 +2023,7 @@ def test_setsigdef_wrong_type(self): [sys.executable, "-c", "pass"], os.environ, setsigdef=[signal.NSIG, signal.NSIG+1]) - @unittest.expectedFailureIf(sys.platform in ('darwin', 'linux'), 'TODO: RUSTPYTHON; NotImplementedError: scheduler parameter is not yet implemented') + @unittest.expectedFailureIf(sys.platform in ("darwin", "linux"), "TODO: RUSTPYTHON; NotImplementedError: scheduler parameter is not yet implemented") @requires_sched @unittest.skipIf(sys.platform.startswith(('freebsd', 'netbsd')), "bpo-34685: test can fail on BSD") @@ -1994,14 +2044,15 @@ def test_setscheduler_only_param(self): ) support.wait_process(pid, exitcode=0) - @unittest.expectedFailureIf(sys.platform in ('darwin', 'linux'), 'TODO: RUSTPYTHON; NotImplementedError: scheduler parameter is not yet implemented') + @unittest.expectedFailureIf(sys.platform in ("darwin", "linux"), "TODO: RUSTPYTHON; NotImplementedError: scheduler parameter is not yet implemented") @requires_sched @unittest.skipIf(sys.platform.startswith(('freebsd', 'netbsd')), "bpo-34685: test can fail on BSD") @unittest.skipIf(platform.libc_ver()[0] == 'glibc' and os.sched_getscheduler(0) in [ os.SCHED_BATCH, - os.SCHED_IDLE], + os.SCHED_IDLE, + os.SCHED_DEADLINE], "Skip test due to glibc posix_spawn policy") def test_setscheduler_with_policy(self): policy = os.sched_getscheduler(0) @@ -2081,7 +2132,7 @@ def test_open_file(self): with open(outfile, encoding="utf-8") as f: self.assertEqual(f.read(), 'hello') - @unittest.expectedFailure # TODO: RUSTPYTHON; the rust runtime reopens closed stdio fds at startup, so this test fails, even though POSIX_SPAWN_CLOSE does actually have an effect + @unittest.expectedFailure # TODO: RUSTPYTHON; the rust runtime reopens closed stdio fds at startup, so this test fails, even though POSIX_SPAWN_CLOSE does actually have an effect def test_close_file(self): closefile = os_helper.TESTFN self.addCleanup(os_helper.unlink, closefile) @@ -2186,12 +2237,12 @@ def _verify_available(self, name): def test_pwritev(self): self._verify_available("HAVE_PWRITEV") if self.mac_ver >= (10, 16): - self.assertTrue(hasattr(os, "pwritev"), "os.pwritev is not available") - self.assertTrue(hasattr(os, "preadv"), "os.readv is not available") + self.assertHasAttr(os, "pwritev") + self.assertHasAttr(os, "preadv") else: - self.assertFalse(hasattr(os, "pwritev"), "os.pwritev is available") - self.assertFalse(hasattr(os, "preadv"), "os.readv is available") + self.assertNotHasAttr(os, "pwritev") + self.assertNotHasAttr(os, "preadv") def test_stat(self): self._verify_available("HAVE_FSTATAT") diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py index 5bb7cb5df31..07fc97cb6a1 100644 --- a/Lib/test/test_signal.py +++ b/Lib/test/test_signal.py @@ -192,7 +192,6 @@ def test_valid_signals(self): self.assertNotIn(signal.NSIG, s) self.assertLess(len(s), signal.NSIG) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_issue9324(self): # Updated for issue #10003, adding SIGBREAK handler = lambda x, y: None @@ -1414,7 +1413,6 @@ def test_sigint(self): with self.assertRaises(KeyboardInterrupt): signal.raise_signal(signal.SIGINT) - @unittest.expectedFailure # TODO: RUSTPYTHON @unittest.skipIf(sys.platform != "win32", "Windows specific test") def test_invalid_argument(self): try: diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index d95c7857d98..5f3b3c321ae 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -800,7 +800,6 @@ def test_env(self): stdout, stderr = p.communicate() self.assertEqual(stdout, b"orange") - @unittest.expectedFailure # TODO: RUSTPYTHON @unittest.skipUnless(sys.platform == "win32", "Windows only issue") def test_win32_duplicate_envs(self): newenv = os.environ.copy() diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index 6b33fe52e37..0d67d3680ad 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -16,8 +16,8 @@ mod _socket { common::os::ErrorExt, convert::{IntoPyException, ToPyObject, TryFromBorrowedObject, TryFromObject}, function::{ - ArgBytesLike, ArgMemoryBuffer, ArgStrOrBytesLike, Either, FsPath, OptionalArg, - OptionalOption, + ArgBytesLike, ArgIntoFloat, ArgMemoryBuffer, ArgStrOrBytesLike, Either, FsPath, + OptionalArg, OptionalOption, }, types::{Constructor, DefaultConstructor, Initializer, Representable}, utils::ToCString, @@ -2201,12 +2201,29 @@ mod _socket { } #[pymethod] - fn settimeout(&self, timeout: Option) -> io::Result<()> { - self.timeout - .store(timeout.map_or(-1.0, |d| d.as_secs_f64())); + fn settimeout(&self, timeout: Option, vm: &VirtualMachine) -> PyResult<()> { + let timeout = match timeout { + Some(t) => { + let f = t.into_float(); + if f.is_nan() { + return Err( + vm.new_value_error("Invalid value NaN (not a number)".to_owned()) + ); + } + if f < 0.0 || !f.is_finite() { + return Err(vm.new_value_error("Timeout value out of range".to_owned())); + } + Some(f) + } + None => None, + }; + self.timeout.store(timeout.unwrap_or(-1.0)); // even if timeout is > 0 the socket needs to be nonblocking in order for us to select() on // it - self.sock()?.set_nonblocking(timeout.is_some()) + self.sock() + .map_err(|e| e.into_pyexception(vm))? + .set_nonblocking(timeout.is_some()) + .map_err(|e| e.into_pyexception(vm)) } #[pymethod] @@ -3366,8 +3383,22 @@ mod _socket { } #[pyfunction] - fn setdefaulttimeout(timeout: Option) { - DEFAULT_TIMEOUT.store(timeout.map_or(-1.0, |d| d.as_secs_f64())); + fn setdefaulttimeout(timeout: Option, vm: &VirtualMachine) -> PyResult<()> { + let val = match timeout { + Some(t) => { + let f = t.into_float(); + if f.is_nan() { + return Err(vm.new_value_error("Invalid value NaN (not a number)".to_owned())); + } + if f < 0.0 || !f.is_finite() { + return Err(vm.new_value_error("Timeout value out of range".to_owned())); + } + f + } + None => -1.0, + }; + DEFAULT_TIMEOUT.store(val); + Ok(()) } #[pyfunction] diff --git a/crates/vm/src/convert/try_from.rs b/crates/vm/src/convert/try_from.rs index ceb7d003e9b..b8d1b53e2e7 100644 --- a/crates/vm/src/convert/try_from.rs +++ b/crates/vm/src/convert/try_from.rs @@ -126,10 +126,23 @@ impl TryFromObject for core::time::Duration { fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { if let Some(float) = obj.downcast_ref::() { let f = float.to_f64(); + if f.is_nan() { + return Err(vm.new_value_error("Invalid value NaN (not a number)".to_owned())); + } if f < 0.0 { - return Err(vm.new_value_error("negative duration")); + return Err(vm.new_value_error("negative duration".to_owned())); + } + if !f.is_finite() || f > u64::MAX as f64 { + return Err(vm.new_overflow_error( + "timestamp too large to convert to C PyTime_t".to_owned(), + )); } - Ok(Self::from_secs_f64(f)) + // Convert float to Duration using floor rounding (_PyTime_ROUND_FLOOR) + let secs = f.trunc() as u64; + let frac = f.fract(); + // Use floor to round down the nanoseconds + let nanos = (frac * 1_000_000_000.0).floor() as u32; + Ok(Self::new(secs, nanos)) } else if let Some(int) = obj.try_index_opt(vm) { let int = int?; let bigint = int.as_bigint(); diff --git a/crates/vm/src/ospath.rs b/crates/vm/src/ospath.rs index 25fcafb74c5..b9efccde399 100644 --- a/crates/vm/src/ospath.rs +++ b/crates/vm/src/ospath.rs @@ -1,8 +1,9 @@ use rustpython_common::crt_fd; use crate::{ - PyObjectRef, PyResult, VirtualMachine, + AsObject, PyObjectRef, PyResult, VirtualMachine, builtins::{PyBytes, PyStr}, + class::StaticType, convert::{IntoPyException, ToPyException, ToPyObject, TryFromObject}, function::FsPath, }; @@ -80,6 +81,18 @@ impl PathConverter { ) -> PyResult> { // Handle fd (before __fspath__ check, like CPython) if let Some(int) = obj.try_index_opt(vm) { + // Warn if bool is used as a file descriptor + if obj + .class() + .is(crate::builtins::bool_::PyBool::static_type()) + { + crate::stdlib::warnings::warn( + vm.ctx.exceptions.runtime_warning, + "bool is used as a file descriptor".to_owned(), + 1, + vm, + )?; + } let fd = int?.try_to_primitive(vm)?; return unsafe { crt_fd::Borrowed::try_borrow_raw(fd) } .map(OsPathOrFd::Fd) diff --git a/crates/vm/src/stdlib/nt.rs b/crates/vm/src/stdlib/nt.rs index c8c699fb3fa..ae74d611085 100644 --- a/crates/vm/src/stdlib/nt.rs +++ b/crates/vm/src/stdlib/nt.rs @@ -183,6 +183,18 @@ pub(crate) mod module { environ } + #[pyfunction] + fn _create_environ(vm: &VirtualMachine) -> PyDictRef { + let environ = vm.ctx.new_dict(); + for (key, value) in env::vars() { + if key.starts_with('=') { + continue; + } + environ.set_item(&key, vm.new_pyobj(value), vm).unwrap(); + } + environ + } + #[derive(FromArgs)] struct ChmodArgs<'a> { #[pyarg(any)] @@ -903,16 +915,14 @@ pub(crate) mod module { argv: Either, vm: &VirtualMachine, ) -> PyResult { + use crate::function::FsPath; use std::iter::once; - let make_widestring = - |s: &str| widestring::WideCString::from_os_str(s).map_err(|err| err.to_pyexception(vm)); - let path = path.to_wide_cstring(vm)?; let argv = vm.extract_elements_with(argv.as_ref(), |obj| { - let arg = PyStrRef::try_from_object(vm, obj)?; - make_widestring(arg.as_str()) + let fspath = FsPath::try_from_path_like(obj, true, vm)?; + fspath.to_wide_cstring(vm) })?; let first = argv @@ -946,16 +956,14 @@ pub(crate) mod module { env: PyDictRef, vm: &VirtualMachine, ) -> PyResult { + use crate::function::FsPath; use std::iter::once; - let make_widestring = - |s: &str| widestring::WideCString::from_os_str(s).map_err(|err| err.to_pyexception(vm)); - let path = path.to_wide_cstring(vm)?; let argv = vm.extract_elements_with(argv.as_ref(), |obj| { - let arg = PyStrRef::try_from_object(vm, obj)?; - make_widestring(arg.as_str()) + let fspath = FsPath::try_from_path_like(obj, true, vm)?; + fspath.to_wide_cstring(vm) })?; let first = argv @@ -975,15 +983,11 @@ pub(crate) mod module { // Build environment strings as "KEY=VALUE\0" wide strings let mut env_strings: Vec = Vec::new(); for (key, value) in env.into_iter() { - let key = PyStrRef::try_from_object(vm, key)?; - let value = PyStrRef::try_from_object(vm, value)?; - let key_str = key.as_str(); - let value_str = value.as_str(); + let key = FsPath::try_from_path_like(key, true, vm)?; + let value = FsPath::try_from_path_like(value, true, vm)?; + let key_str = key.to_string_lossy(); + let value_str = value.to_string_lossy(); - // Validate: no null characters in key or value - if key_str.contains('\0') || value_str.contains('\0') { - return Err(vm.new_value_error("embedded null character")); - } // Validate: no '=' in key (search from index 1 because on Windows // starting '=' is allowed for defining hidden environment variables) if key_str.get(1..).is_some_and(|s| s.contains('=')) { @@ -991,7 +995,10 @@ pub(crate) mod module { } let env_str = format!("{}={}", key_str, value_str); - env_strings.push(make_widestring(&env_str)?); + env_strings.push( + widestring::WideCString::from_os_str(&*std::ffi::OsString::from(env_str)) + .map_err(|err| err.to_pyexception(vm))?, + ); } let envp: Vec<*const u16> = env_strings diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index 95836e38337..c2630f7f8f3 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -87,6 +87,7 @@ impl FromArgs for DirFd<'_, AVAILABLE> { Some(o) if vm.is_none(&o) => Ok(DEFAULT_DIR_FD), None => Ok(DEFAULT_DIR_FD), Some(o) => { + warn_if_bool_fd(&o, vm).map_err(Into::::into)?; let fd = o.try_index_opt(vm).unwrap_or_else(|| { Err(vm.new_type_error(format!( "argument should be integer or None, not {}", @@ -118,8 +119,25 @@ fn bytes_as_os_str<'a>(b: &'a [u8], vm: &VirtualMachine) -> PyResult<&'a std::ff .map_err(|_| vm.new_unicode_decode_error("can't decode path for utf-8")) } +pub(crate) fn warn_if_bool_fd(obj: &PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + use crate::class::StaticType; + if obj + .class() + .is(crate::builtins::bool_::PyBool::static_type()) + { + crate::stdlib::warnings::warn( + vm.ctx.exceptions.runtime_warning, + "bool is used as a file descriptor".to_owned(), + 1, + vm, + )?; + } + Ok(()) +} + impl TryFromObject for crt_fd::Owned { fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + warn_if_bool_fd(&obj, vm)?; let fd = crt_fd::Raw::try_from_object(vm, obj)?; unsafe { crt_fd::Owned::try_from_raw(fd) }.map_err(|e| e.into_pyexception(vm)) } @@ -127,6 +145,7 @@ impl TryFromObject for crt_fd::Owned { impl TryFromObject for crt_fd::Borrowed<'_> { fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + warn_if_bool_fd(&obj, vm)?; let fd = crt_fd::Raw::try_from_object(vm, obj)?; unsafe { crt_fd::Borrowed::try_borrow_raw(fd) }.map_err(|e| e.into_pyexception(vm)) } @@ -165,11 +184,11 @@ pub(super) mod _os { }, convert::{IntoPyException, ToPyObject}, exceptions::OSErrorBuilder, - function::{ArgBytesLike, FsPath, FuncArgs, OptionalArg}, + function::{ArgBytesLike, ArgMemoryBuffer, FsPath, FuncArgs, OptionalArg}, ospath::{OsPath, OsPathOrFd, OutputMode, PathConverter}, protocol::PyIterReturn, recursion::ReprGuard, - types::{IterNext, Iterable, PyStructSequence, Representable, SelfIter}, + types::{Destructor, IterNext, Iterable, PyStructSequence, Representable, SelfIter}, vm::VirtualMachine, }; use core::time::Duration; @@ -296,6 +315,26 @@ pub(super) mod _os { } } + #[pyfunction] + fn readinto( + fd: crt_fd::Borrowed<'_>, + buffer: ArgMemoryBuffer, + vm: &VirtualMachine, + ) -> PyResult { + buffer.with_ref(|buf| { + loop { + match crt_fd::read(fd, buf) { + Ok(n) => return Ok(n), + Err(e) if e.raw_os_error() == Some(libc::EINTR) => { + vm.check_signals()?; + continue; + } + Err(e) => return Err(e.into_pyexception(vm)), + } + } + }) + } + #[pyfunction] fn write(fd: crt_fd::Borrowed<'_>, data: ArgBytesLike) -> io::Result { data.with_ref(|b| crt_fd::write(fd, b)) @@ -754,7 +793,7 @@ pub(super) mod _os { mode: OutputMode, } - #[pyclass(flags(DISALLOW_INSTANTIATION), with(IterNext, Iterable))] + #[pyclass(flags(DISALLOW_INSTANTIATION), with(Destructor, IterNext, Iterable))] impl ScandirIterator { #[pymethod] fn close(&self) { @@ -777,6 +816,21 @@ pub(super) mod _os { Err(vm.new_type_error("cannot pickle 'ScandirIterator' object".to_owned())) } } + impl Destructor for ScandirIterator { + fn del(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { + // Emit ResourceWarning if the iterator is not yet exhausted/closed + if zelf.entries.read().is_some() { + let _ = crate::stdlib::warnings::warn( + vm.ctx.exceptions.resource_warning, + format!("unclosed scandir iterator {:?}", zelf.as_object()), + 1, + vm, + ); + zelf.close(); + } + Ok(()) + } + } impl SelfIter for ScandirIterator {} impl IterNext for ScandirIterator { fn next(zelf: &crate::Py, vm: &VirtualMachine) -> PyResult { @@ -1267,13 +1321,24 @@ pub(super) mod _os { } } - #[pyfunction] - fn link( + #[derive(FromArgs)] + struct LinkArgs { + #[pyarg(any)] src: OsPath, + #[pyarg(any)] dst: OsPath, - follow_symlinks: FollowSymlinks, - vm: &VirtualMachine, - ) -> PyResult<()> { + #[pyarg(named, name = "follow_symlinks", optional)] + follow_symlinks: OptionalArg, + } + + #[pyfunction] + fn link(args: LinkArgs, vm: &VirtualMachine) -> PyResult<()> { + let LinkArgs { + src, + dst, + follow_symlinks, + } = args; + #[cfg(unix)] { use std::os::unix::ffi::OsStrExt; @@ -1282,11 +1347,8 @@ pub(super) mod _os { let dst_cstr = std::ffi::CString::new(dst.path.as_os_str().as_bytes()) .map_err(|_| vm.new_value_error("embedded null byte"))?; - let flags = if follow_symlinks.0 { - libc::AT_SYMLINK_FOLLOW - } else { - 0 - }; + let follow = follow_symlinks.into_option().unwrap_or(true); + let flags = if follow { libc::AT_SYMLINK_FOLLOW } else { 0 }; let ret = unsafe { libc::linkat( @@ -1311,15 +1373,18 @@ pub(super) mod _os { #[cfg(not(unix))] { - // On non-Unix platforms, ignore follow_symlinks if it's the default value - // or raise NotImplementedError if explicitly set to False - if !follow_symlinks.0 { - return Err(vm.new_not_implemented_error( - "link: follow_symlinks unavailable on this platform", - )); - } + let src_path = match follow_symlinks.into_option() { + Some(true) => { + // Explicit follow_symlinks=True: resolve symlinks + fs::canonicalize(&src.path).unwrap_or_else(|_| PathBuf::from(src.path.clone())) + } + Some(false) | None => { + // Default or explicit no-follow: native hard_link behavior + PathBuf::from(src.path.clone()) + } + }; - fs::hard_link(&src.path, &dst.path).map_err(|err| { + fs::hard_link(&src_path, &dst.path).map_err(|err| { let builder = err.to_os_error_builder(vm); let builder = builder.filename(src.filename(vm)); let builder = builder.filename2(dst.filename(vm)); @@ -1657,8 +1722,10 @@ pub(super) mod _os { #[pyfunction] fn truncate(path: PyObjectRef, length: crt_fd::Offset, vm: &VirtualMachine) -> PyResult<()> { - if let Ok(fd) = path.clone().try_into_value(vm) { - return ftruncate(fd, length).map_err(|e| e.into_pyexception(vm)); + match path.clone().try_into_value::>(vm) { + Ok(fd) => return ftruncate(fd, length).map_err(|e| e.into_pyexception(vm)), + Err(e) if e.fast_isinstance(vm.ctx.exceptions.warning) => return Err(e), + Err(_) => {} } #[cold] diff --git a/crates/vm/src/stdlib/posix.rs b/crates/vm/src/stdlib/posix.rs index 5bbfef0f93b..6af6e62221e 100644 --- a/crates/vm/src/stdlib/posix.rs +++ b/crates/vm/src/stdlib/posix.rs @@ -15,19 +15,36 @@ pub fn set_inheritable(fd: BorrowedFd<'_>, inheritable: bool) -> nix::Result<()> Ok(()) } -#[pymodule(name = "posix", with(super::os::_os))] +#[pymodule(name = "posix", with( + super::os::_os, + #[cfg(any( + target_os = "linux", + target_os = "netbsd", + target_os = "freebsd", + target_os = "android" + ))] + posix_sched +))] pub mod module { use crate::{ - AsObject, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine, - builtins::{PyDictRef, PyInt, PyListRef, PyStrRef, PyTupleRef, PyType, PyUtf8StrRef}, + AsObject, Py, PyObjectRef, PyResult, VirtualMachine, + builtins::{PyDictRef, PyInt, PyListRef, PyStr, PyTupleRef}, convert::{IntoPyException, ToPyObject, TryFromObject}, exceptions::OSErrorBuilder, function::{Either, KwArgs, OptionalArg}, ospath::{OsPath, OsPathOrFd}, - stdlib::os::{_os, DirFd, FollowSymlinks, SupportFunc, TargetIsDirectory, fs_metadata}, - types::{Constructor, Representable}, - utils::ToCString, + stdlib::os::{ + _os, DirFd, FollowSymlinks, SupportFunc, TargetIsDirectory, fs_metadata, + warn_if_bool_fd, + }, }; + #[cfg(any( + target_os = "android", + target_os = "freebsd", + target_os = "linux", + target_os = "openbsd" + ))] + use crate::{builtins::PyStrRef, utils::ToCString}; use alloc::ffi::CString; use bitflags::bitflags; use core::ffi::CStr; @@ -41,7 +58,7 @@ pub mod module { }; use strum_macros::{EnumIter, EnumString}; - #[cfg(target_os = "android")] + #[cfg(any(target_os = "android", target_os = "linux"))] #[pyattr] use libc::{SCHED_DEADLINE, SCHED_NORMAL}; @@ -278,6 +295,7 @@ pub mod module { impl TryFromObject for BorrowedFd<'_> { fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + crate::stdlib::os::warn_if_bool_fd(&obj, vm)?; let fd = i32::try_from_object(vm, obj)?; if fd == -1 { return Err(io::Error::from_raw_os_error(libc::EBADF).into_pyexception(vm)); @@ -443,6 +461,19 @@ pub mod module { environ } + #[pyfunction] + fn _create_environ(vm: &VirtualMachine) -> PyDictRef { + use rustpython_common::os::ffi::OsStringExt; + + let environ = vm.ctx.new_dict(); + for (key, value) in env::vars_os() { + let key: PyObjectRef = vm.ctx.new_bytes(key.into_vec()).into(); + let value: PyObjectRef = vm.ctx.new_bytes(value.into_vec()).into(); + environ.set_item(&*key, value, vm).unwrap(); + } + environ + } + #[derive(FromArgs)] pub(super) struct SymlinkArgs<'fd> { src: OsPath, @@ -483,8 +514,15 @@ pub mod module { #[cfg(not(target_os = "redox"))] #[pyfunction] - fn fchdir(fd: BorrowedFd<'_>, vm: &VirtualMachine) -> PyResult<()> { - nix::unistd::fchdir(fd).map_err(|err| err.into_pyexception(vm)) + fn fchdir(fd: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + warn_if_bool_fd(&fd, vm)?; + let fd = i32::try_from_object(vm, fd)?; + let ret = unsafe { libc::fchdir(fd) }; + if ret == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error().into_pyexception(vm)) + } } #[cfg(not(target_os = "redox"))] @@ -859,175 +897,6 @@ pub mod module { nix::sched::sched_yield().map_err(|e| e.into_pyexception(vm)) } - #[pyattr] - #[pyclass(name = "sched_param")] - #[derive(Debug, PyPayload)] - struct SchedParam { - sched_priority: PyObjectRef, - } - - impl TryFromObject for SchedParam { - fn try_from_object(_vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { - Ok(Self { - sched_priority: obj, - }) - } - } - - #[pyclass(with(Constructor, Representable))] - impl SchedParam { - #[pygetset] - fn sched_priority(&self, vm: &VirtualMachine) -> PyObjectRef { - self.sched_priority.clone().to_pyobject(vm) - } - - #[cfg(any( - target_os = "linux", - target_os = "netbsd", - target_os = "freebsd", - target_os = "android" - ))] - #[cfg(not(target_env = "musl"))] - fn try_to_libc(&self, vm: &VirtualMachine) -> PyResult { - use crate::AsObject; - let priority_class = self.sched_priority.class(); - let priority_type = priority_class.name(); - let priority = self.sched_priority.clone(); - let value = priority.downcast::().map_err(|_| { - vm.new_type_error(format!("an integer is required (got type {priority_type})")) - })?; - let sched_priority = value.try_to_primitive(vm)?; - Ok(libc::sched_param { sched_priority }) - } - } - - #[derive(FromArgs)] - pub struct SchedParamArg { - sched_priority: PyObjectRef, - } - - impl Constructor for SchedParam { - type Args = SchedParamArg; - - fn py_new(_cls: &Py, arg: Self::Args, _vm: &VirtualMachine) -> PyResult { - Ok(Self { - sched_priority: arg.sched_priority, - }) - } - } - - impl Representable for SchedParam { - #[inline] - fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let sched_priority_repr = zelf.sched_priority.repr(vm)?; - Ok(format!( - "posix.sched_param(sched_priority = {})", - sched_priority_repr.as_str() - )) - } - } - - #[cfg(any( - target_os = "linux", - target_os = "netbsd", - target_os = "freebsd", - target_os = "android" - ))] - #[pyfunction] - fn sched_getscheduler(pid: libc::pid_t, vm: &VirtualMachine) -> PyResult { - let policy = unsafe { libc::sched_getscheduler(pid) }; - if policy == -1 { - Err(vm.new_last_errno_error()) - } else { - Ok(policy) - } - } - - #[cfg(any( - target_os = "linux", - target_os = "netbsd", - target_os = "freebsd", - target_os = "android" - ))] - #[derive(FromArgs)] - struct SchedSetschedulerArgs { - #[pyarg(positional)] - pid: i32, - #[pyarg(positional)] - policy: i32, - #[pyarg(positional)] - sched_param_obj: crate::PyRef, - } - - #[cfg(any( - target_os = "linux", - target_os = "netbsd", - target_os = "freebsd", - target_os = "android" - ))] - #[cfg(not(target_env = "musl"))] - #[pyfunction] - fn sched_setscheduler(args: SchedSetschedulerArgs, vm: &VirtualMachine) -> PyResult { - let libc_sched_param = args.sched_param_obj.try_to_libc(vm)?; - let policy = unsafe { libc::sched_setscheduler(args.pid, args.policy, &libc_sched_param) }; - if policy == -1 { - Err(vm.new_last_errno_error()) - } else { - Ok(policy) - } - } - #[cfg(any( - target_os = "linux", - target_os = "netbsd", - target_os = "freebsd", - target_os = "android" - ))] - #[pyfunction] - fn sched_getparam(pid: libc::pid_t, vm: &VirtualMachine) -> PyResult { - let param = unsafe { - let mut param = core::mem::MaybeUninit::uninit(); - if -1 == libc::sched_getparam(pid, param.as_mut_ptr()) { - return Err(vm.new_last_errno_error()); - } - param.assume_init() - }; - Ok(SchedParam { - sched_priority: param.sched_priority.to_pyobject(vm), - }) - } - - #[cfg(any( - target_os = "linux", - target_os = "netbsd", - target_os = "freebsd", - target_os = "android" - ))] - #[derive(FromArgs)] - struct SchedSetParamArgs { - #[pyarg(positional)] - pid: i32, - #[pyarg(positional)] - sched_param_obj: crate::PyRef, - } - - #[cfg(any( - target_os = "linux", - target_os = "netbsd", - target_os = "freebsd", - target_os = "android" - ))] - #[cfg(not(target_env = "musl"))] - #[pyfunction] - fn sched_setparam(args: SchedSetParamArgs, vm: &VirtualMachine) -> PyResult { - let libc_sched_param = args.sched_param_obj.try_to_libc(vm)?; - let ret = unsafe { libc::sched_setparam(args.pid, &libc_sched_param) }; - if ret == -1 { - Err(vm.new_last_errno_error()) - } else { - Ok(ret) - } - } - #[pyfunction] fn get_inheritable(fd: BorrowedFd<'_>, vm: &VirtualMachine) -> PyResult { let flags = fcntl::fcntl(fd, fcntl::FcntlArg::F_GETFD); @@ -1183,7 +1052,7 @@ pub mod module { let path = path.into_cstring(vm)?; let argv = vm.extract_elements_with(argv.as_ref(), |obj| { - PyStrRef::try_from_object(vm, obj)?.to_cstring(vm) + OsPath::try_from_object(vm, obj)?.into_cstring(vm) })?; let argv: Vec<&CStr> = argv.iter().map(|entry| entry.as_c_str()).collect(); @@ -1209,7 +1078,7 @@ pub mod module { let path = path.into_cstring(vm)?; let argv = vm.extract_elements_with(argv.as_ref(), |obj| { - PyStrRef::try_from_object(vm, obj)?.to_cstring(vm) + OsPath::try_from_object(vm, obj)?.into_cstring(vm) })?; let argv: Vec<&CStr> = argv.iter().map(|entry| entry.as_c_str()).collect(); @@ -2068,7 +1937,11 @@ pub mod module { let i = match obj.downcast::() { Ok(int) => int.try_to_primitive(vm)?, Err(obj) => { - let s = PyStrRef::try_from_object(vm, obj)?; + let s = obj.downcast::().map_err(|_| { + vm.new_type_error( + "configuration names must be strings or integers".to_owned(), + ) + })?; s.as_str() .parse::() .map_err(|_| vm.new_value_error("unrecognized configuration name"))? @@ -2462,7 +2335,11 @@ pub mod module { let i = match obj.downcast::() { Ok(int) => int.try_to_primitive(vm)?, Err(obj) => { - let s = PyUtf8StrRef::try_from_object(vm, obj)?; + let s = obj.downcast::().map_err(|_| { + vm.new_type_error( + "configuration names must be strings or integers".to_owned(), + ) + })?; s.as_str().parse::().or_else(|_| { if s.as_str() == "SC_PAGESIZE" { Ok(SysconfVar::SC_PAGESIZE) @@ -2630,3 +2507,159 @@ pub mod module { Ok(()) } } + +#[cfg(any( + target_os = "linux", + target_os = "netbsd", + target_os = "freebsd", + target_os = "android" +))] +#[pymodule(sub)] +mod posix_sched { + use crate::{ + AsObject, Py, PyObjectRef, PyResult, VirtualMachine, + builtins::{PyInt, PyTupleRef}, + convert::ToPyObject, + function::FuncArgs, + types::PyStructSequence, + }; + + #[derive(FromArgs)] + struct SchedParamArgs { + #[pyarg(any)] + sched_priority: PyObjectRef, + } + + #[pystruct_sequence_data] + struct SchedParamData { + pub sched_priority: PyObjectRef, + } + + #[pyattr] + #[pystruct_sequence(name = "sched_param", module = "posix", data = "SchedParamData")] + struct PySchedParam; + + #[pyclass(with(PyStructSequence))] + impl PySchedParam { + #[pyslot] + fn slot_new( + cls: crate::builtins::PyTypeRef, + args: FuncArgs, + vm: &VirtualMachine, + ) -> PyResult { + use crate::PyPayload; + let SchedParamArgs { sched_priority } = args.bind(vm)?; + let items = vec![sched_priority]; + crate::builtins::PyTuple::new_unchecked(items.into_boxed_slice()) + .into_ref_with_type(vm, cls) + .map(Into::into) + } + + #[extend_class] + fn extend_pyclass(ctx: &crate::vm::Context, class: &'static Py) { + // Override __reduce__ to return (type, (sched_priority,)) + // instead of the generic structseq (type, ((sched_priority,),)). + // The trait's extend_class checks contains_key before setting default. + const SCHED_PARAM_REDUCE: crate::function::PyMethodDef = + crate::function::PyMethodDef::new_const( + "__reduce__", + |zelf: crate::PyRef, + vm: &VirtualMachine| + -> PyTupleRef { + vm.new_tuple((zelf.class().to_owned(), (zelf[0].clone(),))) + }, + crate::function::PyMethodFlags::METHOD, + None, + ); + class.set_attr( + ctx.intern_str("__reduce__"), + SCHED_PARAM_REDUCE.to_proper_method(class, ctx), + ); + } + } + + #[cfg(not(target_env = "musl"))] + fn convert_sched_param(obj: &PyObjectRef, vm: &VirtualMachine) -> PyResult { + use crate::{builtins::PyTuple, class::StaticType}; + if !obj.fast_isinstance(PySchedParam::static_type()) { + return Err(vm.new_type_error("must have a sched_param object".to_owned())); + } + let tuple = obj.downcast_ref::().unwrap(); + let priority = tuple[0].clone(); + let priority_type = priority.class().name().to_string(); + let value = priority.downcast::().map_err(|_| { + vm.new_type_error(format!("an integer is required (got type {priority_type})")) + })?; + let sched_priority = value.try_to_primitive(vm)?; + Ok(libc::sched_param { sched_priority }) + } + + #[pyfunction] + fn sched_getscheduler(pid: libc::pid_t, vm: &VirtualMachine) -> PyResult { + let policy = unsafe { libc::sched_getscheduler(pid) }; + if policy == -1 { + Err(vm.new_last_errno_error()) + } else { + Ok(policy) + } + } + + #[derive(FromArgs)] + struct SchedSetschedulerArgs { + #[pyarg(positional)] + pid: i32, + #[pyarg(positional)] + policy: i32, + #[pyarg(positional)] + sched_param: PyObjectRef, + } + + #[cfg(not(target_env = "musl"))] + #[pyfunction] + fn sched_setscheduler(args: SchedSetschedulerArgs, vm: &VirtualMachine) -> PyResult { + let libc_sched_param = convert_sched_param(&args.sched_param, vm)?; + let policy = unsafe { libc::sched_setscheduler(args.pid, args.policy, &libc_sched_param) }; + if policy == -1 { + Err(vm.new_last_errno_error()) + } else { + Ok(policy) + } + } + + #[pyfunction] + fn sched_getparam(pid: libc::pid_t, vm: &VirtualMachine) -> PyResult { + let param = unsafe { + let mut param = core::mem::MaybeUninit::uninit(); + if -1 == libc::sched_getparam(pid, param.as_mut_ptr()) { + return Err(vm.new_last_errno_error()); + } + param.assume_init() + }; + Ok(PySchedParam::from_data( + SchedParamData { + sched_priority: param.sched_priority.to_pyobject(vm), + }, + vm, + )) + } + + #[derive(FromArgs)] + struct SchedSetParamArgs { + #[pyarg(positional)] + pid: i32, + #[pyarg(positional)] + sched_param: PyObjectRef, + } + + #[cfg(not(target_env = "musl"))] + #[pyfunction] + fn sched_setparam(args: SchedSetParamArgs, vm: &VirtualMachine) -> PyResult { + let libc_sched_param = convert_sched_param(&args.sched_param, vm)?; + let ret = unsafe { libc::sched_setparam(args.pid, &libc_sched_param) }; + if ret == -1 { + Err(vm.new_last_errno_error()) + } else { + Ok(ret) + } + } +} diff --git a/crates/vm/src/stdlib/signal.rs b/crates/vm/src/stdlib/signal.rs index 1a73c454b8f..8b747e04786 100644 --- a/crates/vm/src/stdlib/signal.rs +++ b/crates/vm/src/stdlib/signal.rs @@ -101,6 +101,10 @@ pub(crate) mod _signal { #[pyattr] pub use libc::{SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM}; + #[cfg(windows)] + #[pyattr] + const SIGBREAK: i32 = 21; // _SIGBREAK + // Windows-specific control events for GenerateConsoleCtrlEvent #[cfg(windows)] #[pyattr] @@ -201,6 +205,21 @@ pub(crate) mod _signal { vm: &VirtualMachine, ) -> PyResult> { signal::assert_in_range(signalnum, vm)?; + #[cfg(windows)] + { + const VALID_SIGNALS: &[i32] = &[ + libc::SIGINT, + libc::SIGILL, + libc::SIGFPE, + libc::SIGSEGV, + libc::SIGTERM, + SIGBREAK, + libc::SIGABRT, + ]; + if !VALID_SIGNALS.contains(&signalnum) { + return Err(vm.new_value_error(format!("signal number {} out of range", signalnum))); + } + } let signal_handlers = vm .signal_handlers .as_deref() @@ -482,18 +501,20 @@ pub(crate) mod _signal { // On Windows, only certain signals are supported #[cfg(windows)] { - use crate::convert::IntoPyException; - // Windows supports: SIGINT(2), SIGILL(4), SIGFPE(8), SIGSEGV(11), SIGTERM(15), SIGABRT(22) + // Windows supports: SIGINT(2), SIGILL(4), SIGFPE(8), SIGSEGV(11), SIGTERM(15), SIGBREAK(21), SIGABRT(22) const VALID_SIGNALS: &[i32] = &[ libc::SIGINT, libc::SIGILL, libc::SIGFPE, libc::SIGSEGV, libc::SIGTERM, + SIGBREAK, libc::SIGABRT, ]; if !VALID_SIGNALS.contains(&signalnum) { - return Err(std::io::Error::from_raw_os_error(libc::EINVAL).into_pyexception(vm)); + return Err(vm + .new_errno_error(libc::EINVAL, "Invalid argument") + .upcast()); } } @@ -537,6 +558,7 @@ pub(crate) mod _signal { libc::SIGFPE => "Floating-point exception", libc::SIGSEGV => "Segmentation fault", libc::SIGTERM => "Terminated", + SIGBREAK => "Break", libc::SIGABRT => "Aborted", _ => return Ok(None), }; @@ -573,6 +595,7 @@ pub(crate) mod _signal { libc::SIGFPE, libc::SIGSEGV, libc::SIGTERM, + SIGBREAK, libc::SIGABRT, ] { set.add(vm.ctx.new_int(signum).into(), vm)?; diff --git a/crates/vm/src/stdlib/winapi.rs b/crates/vm/src/stdlib/winapi.rs index c1fe32aadfc..c58a55476a7 100644 --- a/crates/vm/src/stdlib/winapi.rs +++ b/crates/vm/src/stdlib/winapi.rs @@ -437,7 +437,9 @@ mod _winapi { return Err(vm.new_runtime_error("environment changed size during iteration")); } - let mut out = widestring::WideString::new(); + // Deduplicate case-insensitive keys, keeping the last value + use std::collections::HashMap; + let mut last_entry: HashMap = HashMap::new(); for (k, v) in keys.into_iter().zip(values.into_iter()) { let k = PyStrRef::try_from_object(vm, k)?; let k = k.as_str(); @@ -449,10 +451,22 @@ mod _winapi { if k.is_empty() || k[1..].contains('=') { return Err(vm.new_value_error("illegal environment variable name")); } - out.push_str(k); - out.push_str("="); - out.push_str(v); - out.push_str("\0"); + let key_upper = k.to_uppercase(); + let mut entry = widestring::WideString::new(); + entry.push_str(k); + entry.push_str("="); + entry.push_str(v); + entry.push_str("\0"); + last_entry.insert(key_upper, entry); + } + + // Sort by uppercase key for case-insensitive ordering + let mut entries: Vec<(String, widestring::WideString)> = last_entry.into_iter().collect(); + entries.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut out = widestring::WideString::new(); + for (_, entry) in entries { + out.push(entry); } out.push_str("\0"); Ok(out.into_vec())