Skip to content

Commit fe5e46f

Browse files
committed
Fix Windows runtime compatibility issues
Correct socket address handling, untracked mappings, directory enumeration, winsound validation, and executable path discovery. Report the vendored Unicode version independently of the Rust toolchain. Assisted-by: Codex:GPT-5
1 parent 6934845 commit fe5e46f

10 files changed

Lines changed: 227 additions & 51 deletions

File tree

‎Lib/test/test_os/test_os.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5078,7 +5078,6 @@ def test_current_directory(self):
50785078
finally:
50795079
os.chdir(old_dir)
50805080

5081-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; FileNotFoundError")
50825081
@unittest.skipIf(sys.platform != 'win32', "Win32 specific test")
50835082
def test_windows_trailing_space_path(self):
50845083
import pathlib

‎Lib/test/test_winsound.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ def test_keyword_args(self):
100100

101101
class PlaySoundTest(unittest.TestCase):
102102

103-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'str'")
104103
def test_errors(self):
105104
self.assertRaises(TypeError, winsound.PlaySound)
106105
self.assertRaises(TypeError, winsound.PlaySound, "bad", "bad")

‎crates/host_env/src/mmap.rs‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -414,9 +414,9 @@ pub fn last_error() -> u32 {
414414
}
415415

416416
#[cfg(windows)]
417-
pub fn create_named_mapping(
417+
pub fn create_mapping(
418418
file_handle: Handle,
419-
tag: &str,
419+
tag: Option<&str>,
420420
access: AccessMode,
421421
offset: i64,
422422
map_size: usize,
@@ -432,7 +432,8 @@ pub fn create_named_mapping(
432432
.ok_or_else(|| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
433433
let size_hi = (total_size >> 32) as u32;
434434
let size_lo = total_size as u32;
435-
let tag_wide: Vec<u16> = tag.encode_utf16().chain(core::iter::once(0)).collect();
435+
let tag_wide: Option<Vec<u16>> =
436+
tag.map(|tag| tag.encode_utf16().chain(core::iter::once(0)).collect());
436437

437438
let map_handle = unsafe {
438439
CreateFileMappingW(
@@ -441,7 +442,9 @@ pub fn create_named_mapping(
441442
fl_protect,
442443
size_hi,
443444
size_lo,
444-
tag_wide.as_ptr(),
445+
tag_wide
446+
.as_ref()
447+
.map_or(core::ptr::null(), |tag| tag.as_ptr()),
445448
)
446449
}
447450
.into_owned()

‎crates/host_env/src/nt.rs‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,20 @@ impl core::fmt::Debug for Scandir {
665665
/// `rposix_scandir.opendir`: join `*.*` onto the directory and open the walk.
666666
pub fn scandir(path: &widestring::WideCStr) -> io::Result<Scandir> {
667667
let dir = path.as_slice().to_vec();
668-
let mut mask = join_path_filename(&dir, &[b'*' as u16, b'.' as u16, b'*' as u16]);
668+
// Normalize before appending the wildcard, as CPython does. Otherwise a
669+
// trailing space becomes an interior path component and is not stripped.
670+
// Extended paths deliberately retain their literal spelling.
671+
let normalized;
672+
let search_dir = if !dir.is_empty()
673+
&& !dir.starts_with(&[b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16])
674+
{
675+
use std::os::windows::ffi::OsStrExt;
676+
normalized = getfullpathname(path)?.encode_wide().collect::<Vec<_>>();
677+
&normalized
678+
} else {
679+
&dir
680+
};
681+
let mut mask = join_path_filename(search_dir, &[b'*' as u16, b'.' as u16, b'*' as u16]);
669682
mask.push(0);
670683
let mut data: WIN32_FIND_DATAW = unsafe { core::mem::zeroed() };
671684
let handle = unsafe { FindFirstFileW(mask.as_ptr(), &mut data) };
@@ -2499,6 +2512,24 @@ pub fn execve(
24992512
}
25002513
}
25012514

2515+
#[cfg(test)]
2516+
mod scandir_tests {
2517+
use super::*;
2518+
2519+
#[test]
2520+
fn trailing_space_is_normalized_except_in_extended_paths() {
2521+
let mut path = getfullpathname(widestring::widecstr!(".")).unwrap();
2522+
path.push(" ");
2523+
let wide = WideCString::from_os_str(&path).unwrap();
2524+
assert!(scandir(&wide).is_ok());
2525+
2526+
let mut extended = OsString::from("\\\\?\\");
2527+
extended.push(path);
2528+
let wide = WideCString::from_os_str(&extended).unwrap();
2529+
assert!(scandir(&wide).is_err());
2530+
}
2531+
}
2532+
25022533
#[cfg(test)]
25032534
mod find_last_utf8_boundary_tests {
25042535
use super::find_last_utf8_boundary;

‎crates/stdlib/src/mmap.rs‎

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ mod mmap {
199199
pos: AtomicCell<usize>, // relative to offset
200200
exports: AtomicCell<usize>,
201201
access: AccessMode,
202-
#[cfg(unix)]
203202
trackfd: bool,
204203
#[cfg(target_os = "linux")]
205204
anonymous: bool,
@@ -263,6 +262,8 @@ mod mmap {
263262
access: AccessMode,
264263
#[pyarg(any, default)]
265264
offset: i64,
265+
#[pyarg(named, default = true)]
266+
trackfd: bool,
266267
}
267268

268269
impl MmapNewArgs {
@@ -509,6 +510,7 @@ mod mmap {
509510
tagname,
510511
access,
511512
offset,
513+
trackfd,
512514
..
513515
} = args;
514516

@@ -589,8 +591,9 @@ mod mmap {
589591
}
590592
}
591593

592-
// When tagname is provided, use raw Win32 APIs for named shared memory
593-
if let Some(ref tag) = tag_str {
594+
// memmap2 retains a duplicated file handle, so untracked mappings
595+
// must use Win32 directly as well as named shared memory mappings.
596+
if tag_str.is_some() || !trackfd {
594597
let fh = if let Some(fh) = fh {
595598
// Close the duplicated handle - we'll use the original
596599
// file handle for CreateFileMappingW
@@ -602,9 +605,9 @@ mod mmap {
602605
host_mmap::INVALID_HANDLE
603606
};
604607

605-
let named = host_mmap::create_named_mapping(
608+
let named = host_mmap::create_mapping(
606609
fh,
607-
tag,
610+
tag_str.as_deref(),
608611
match access {
609612
AccessMode::Default => host_mmap::AccessMode::Default,
610613
AccessMode::Read => host_mmap::AccessMode::Read,
@@ -631,12 +634,16 @@ mod mmap {
631634
pos: AtomicCell::new(0),
632635
exports: AtomicCell::new(0),
633636
access,
637+
trackfd,
634638
});
635639
}
636640

637641
let (handle, mmap) = if duplicated_handle != host_mmap::INVALID_HANDLE {
638642
let mmap = Self::create_mmap_windows(duplicated_handle, offset, map_size, &access)
639-
.map_err(|e| e.to_pyexception(vm))?;
643+
.map_err(|e| {
644+
host_mmap::close_handle(duplicated_handle);
645+
e.to_pyexception(vm)
646+
})?;
640647
(duplicated_handle as isize, mmap)
641648
} else {
642649
// Anonymous mapping
@@ -653,6 +660,7 @@ mod mmap {
653660
pos: AtomicCell::new(0),
654661
exports: AtomicCell::new(0),
655662
access,
663+
trackfd,
656664
})
657665
}
658666
}
@@ -812,7 +820,6 @@ mod mmap {
812820
return Err(vm.new_buffer_error("mmap can't resize with extant buffers exported."));
813821
}
814822

815-
#[cfg(unix)]
816823
if !self.trackfd {
817824
drop(mmap);
818825
return Err(vm.new_value_error("mmap can't resize with trackfd=False."));
@@ -1312,6 +1319,9 @@ mod mmap {
13121319
#[pymethod]
13131320
fn size(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIntRef> {
13141321
let _mmap = zelf.check_valid(vm)?;
1322+
if !zelf.trackfd {
1323+
return Err(vm.new_value_error("can't get size with trackfd=False"));
1324+
}
13151325
let handle = zelf.handle.load();
13161326
if host_mmap::is_invalid_handle_value(handle) {
13171327
// Anonymous mapping, return the mmap size
@@ -1642,6 +1652,29 @@ mod mmap {
16421652
rejects_borrowed_buffer(false);
16431653
}
16441654

1655+
#[cfg(windows)]
1656+
#[test]
1657+
fn untracked_mapping_cannot_resize() {
1658+
crate::vm::Interpreter::without_stdlib(Default::default()).enter(|vm| {
1659+
let mapped = PyMmap::py_new(
1660+
vm.ctx.types.type_type,
1661+
MmapNewArgs {
1662+
fileno: -1,
1663+
length: 64,
1664+
tagname: None,
1665+
access: AccessMode::Default,
1666+
offset: 0,
1667+
trackfd: false,
1668+
},
1669+
vm,
1670+
)
1671+
.unwrap();
1672+
assert_eq!(mapped.__len__(), 64);
1673+
assert!(!mapped.trackfd);
1674+
assert!(mapped.check_resizeable(vm).is_err());
1675+
});
1676+
}
1677+
16451678
#[test]
16461679
fn resize_rejects_borrowed_buffer() {
16471680
rejects_borrowed_buffer(true);

‎crates/stdlib/src/socket.rs‎

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3485,17 +3485,9 @@ mod _socket {
34853485
flags: opts.flags,
34863486
};
34873487

3488-
// Encode host: str uses IDNA encoding, bytes must be valid UTF-8
3488+
// Encode non-ASCII hosts with IDNA; bytes must be valid UTF-8.
34893489
let host_encoded: Option<String> = match opts.host.as_ref() {
3490-
Some(ArgStrOrBytesLike::Str(s)) => {
3491-
let encoded =
3492-
vm.state
3493-
.codec_registry
3494-
.encode_text(s.to_owned(), "idna", None, vm)?;
3495-
let host_str = core::str::from_utf8(encoded.as_bytes())
3496-
.map_err(|_| vm.new_runtime_error("idna output is not utf8"))?;
3497-
Some(host_str.to_owned())
3498-
}
3490+
Some(ArgStrOrBytesLike::Str(s)) => Some(encode_hostname(s.as_wtf8(), vm)?),
34993491
Some(ArgStrOrBytesLike::Buf(b)) => {
35003492
let bytes = b.borrow_buf();
35013493
let host_str = core::str::from_utf8(&bytes).map_err(|e| {
@@ -3864,7 +3856,7 @@ mod _socket {
38643856
{
38653857
return Ok(SocketAddr::V4(net::SocketAddrV4::new(addr, 0)));
38663858
}
3867-
if matches!(af, c::AF_INET | c::AF_UNSPEC)
3859+
if matches!(af, c::AF_INET6 | c::AF_UNSPEC)
38683860
&& !name.contains('%')
38693861
&& let Ok(addr) = name.parse::<Ipv6Addr>()
38703862
{
@@ -3874,17 +3866,30 @@ mod _socket {
38743866
address: af,
38753867
..Default::default()
38763868
};
3877-
let name = vm
3878-
.state
3879-
.codec_registry
3880-
.encode_text(pyname.into_wtf8(), "idna", None, vm)?;
3881-
let name = core::str::from_utf8(name.as_bytes())
3882-
.map_err(|_| vm.new_runtime_error("idna output is not utf8"))?;
3883-
let mut res = host_socket::dns::getaddrinfo(Some(name), None, Some(hints))
3869+
let name = encode_hostname(pyname.as_wtf8(), vm)?;
3870+
let mut res = host_socket::dns::getaddrinfo(Some(&name), None, Some(hints))
38843871
.map_err(|e| convert_socket_error(vm, e, SocketError::GaiError))?;
38853872
Ok(res.next().unwrap().map(|ainfo| ainfo.sockaddr)?)
38863873
}
38873874

3875+
fn encode_hostname(name: &crate::common::wtf8::Wtf8, vm: &VirtualMachine) -> PyResult<String> {
3876+
// Like CPython's idna_converter, ASCII hostnames need no codec lookup.
3877+
if let Ok(name) = name.as_str()
3878+
&& name.is_ascii()
3879+
{
3880+
return Ok(name.to_owned());
3881+
}
3882+
let encoded = vm.state.codec_registry.encode_text(
3883+
vm.ctx.new_str(name.to_owned()),
3884+
"idna",
3885+
None,
3886+
vm,
3887+
)?;
3888+
core::str::from_utf8(encoded.as_bytes())
3889+
.map(str::to_owned)
3890+
.map_err(|_| vm.new_runtime_error("idna output is not utf8"))
3891+
}
3892+
38883893
fn sock_from_raw(fileno: RawSocket, vm: &VirtualMachine) -> PyResult<Socket> {
38893894
let invalid = cfg_select! {
38903895
windows => fileno == INVALID_SOCKET,
@@ -4064,6 +4069,24 @@ mod _socket {
40644069
.ok_or_else(|| vm.new_overflow_error("CMSG_SPACE() argument out of range"))
40654070
}
40664071

4072+
#[cfg(test)]
4073+
mod address_tests {
4074+
use super::*;
4075+
4076+
#[test]
4077+
fn ascii_hosts_do_not_require_idna() {
4078+
crate::vm::Interpreter::without_stdlib(Default::default()).enter(|vm| {
4079+
for host in ["localhost", "127.0.0.1", "::1", "fe80::1%1"] {
4080+
assert_eq!(encode_hostname(host.into(), vm).unwrap(), host);
4081+
}
4082+
let addr = get_addr(vm, vm.ctx.new_utf8_str("::1"), c::AF_INET6)
4083+
.map_err(|err| err.into_pyexception(vm))
4084+
.unwrap();
4085+
assert_eq!(addr.ip(), net::IpAddr::V6(Ipv6Addr::LOCALHOST));
4086+
});
4087+
}
4088+
}
4089+
40674090
#[cfg(test)]
40684091
#[cfg(all(feature = "ssl-openssl", unix, not(target_os = "aix")))]
40694092
mod ssl_identity_tests {

‎crates/unicode/src/data.rs‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,14 @@ fn lookup_property_diff<T: Copy>(
230230
/// library (`unicodedata.unidata_version`).
231231
#[must_use]
232232
pub fn unicode_version() -> String {
233-
format!(
234-
"{}.{}.{}",
235-
char::UNICODE_VERSION.0,
236-
char::UNICODE_VERSION.1,
237-
char::UNICODE_VERSION.2
238-
)
233+
// Report the vendored database version, not the compiler's Unicode version.
234+
include_str!("../unicode/latest/NameAliases.txt")
235+
.lines()
236+
.next()
237+
.and_then(|header| header.strip_prefix("# NameAliases-"))
238+
.and_then(|header| header.strip_suffix(".txt"))
239+
.expect("vendored NameAliases.txt must start with its Unicode version")
240+
.into()
239241
}
240242

241243
/// Look up a name in the latest UCD (`unicodedata.lookup`).
@@ -535,6 +537,12 @@ mod tests {
535537
CodePoint::from(ch)
536538
}
537539

540+
#[test]
541+
fn version_matches_vendored_database() {
542+
assert_eq!(super::unicode_version(), "17.0.0");
543+
assert_eq!(Ucd::new(true).unidata_version(), "17.0.0");
544+
}
545+
538546
#[test]
539547
fn data_queries_match_unicodedata_behavior() {
540548
let ucd = Ucd::new(true);

‎crates/vm/src/getpath.rs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,13 @@ fn program_name() -> String {
510510

511511
/// Get the current executable path
512512
fn get_executable_path() -> Option<PathBuf> {
513-
#[cfg(not(target_arch = "wasm32"))]
513+
#[cfg(windows)]
514+
{
515+
// CreateProcess permits argv[0] to differ from the executable filename.
516+
// Like CPython's GetModuleFileNameW, use the loaded image's path.
517+
env::current_exe().ok()
518+
}
519+
#[cfg(all(not(windows), not(target_arch = "wasm32")))]
514520
{
515521
let exec_arg = env::args_os().next()?;
516522
crate::host_env::fs::which(exec_arg)
@@ -545,6 +551,12 @@ fn parse_pyvenv_home(pyvenv_cfg: &Path) -> Option<Option<String>> {
545551
mod tests {
546552
use super::*;
547553

554+
#[cfg(windows)]
555+
#[test]
556+
fn executable_is_the_loaded_image() {
557+
assert_eq!(get_executable_path(), env::current_exe().ok());
558+
}
559+
548560
#[test]
549561
fn build_time_paths_follow_configured_paths_and_zip() {
550562
for configured in [vec![], vec!["first".to_owned(), "second".to_owned()]] {

0 commit comments

Comments
 (0)