diff --git a/crates/common/src/format.rs b/crates/common/src/format.rs index c8ab1651a83..5b416bbe804 100644 --- a/crates/common/src/format.rs +++ b/crates/common/src/format.rs @@ -20,7 +20,8 @@ pub struct LocaleInfo { pub decimal_point: String, /// Grouping pattern from `lconv.grouping`. /// Each element is a group size. The last non-zero element repeats. - /// e.g. `[3, 0]` means groups of 3 repeating forever. + /// e.g. `[3, 0]` means groups of 3 repeating forever. A `CHAR_MAX` + /// element ends grouping, e.g. `[3, CHAR_MAX]` groups only the last 3 digits. pub grouping: Vec, } @@ -617,10 +618,16 @@ impl FormatSpec { return int_part.to_string(); } + #[allow( + clippy::unnecessary_cast, + reason = "core::ffi::c_char is not u8 on all platforms" + )] + const CHAR_MAX: u8 = core::ffi::c_char::MAX as u8; + let mut group_idx = 0; let mut group_size = locale.grouping[0] as usize; - if group_size == 0 { + if group_size == 0 || locale.grouping[0] == CHAR_MAX { return int_part.to_string(); } @@ -640,9 +647,13 @@ impl FormatSpec { // Advance to next group size if group_idx + 1 < locale.grouping.len() { - let next = locale.grouping[group_idx + 1] as usize; + let next = locale.grouping[group_idx + 1]; + if next == CHAR_MAX { + groups.push(&int_part[..pos]); + break; + } if next != 0 { - group_size = next; + group_size = next as usize; group_idx += 1; } // 0 means repeat previous group size forever @@ -1897,6 +1908,33 @@ mod tests { ); } + #[test] + fn format_int_locale_stops_grouping_at_char_max() { + #[allow( + clippy::unnecessary_cast, + reason = "core::ffi::c_char is not u8 on all platforms" + )] + const CHAR_MAX: u8 = core::ffi::c_char::MAX as u8; + let locale = |grouping| LocaleInfo { + thousands_sep: ",".to_owned(), + decimal_point: ".".to_owned(), + grouping, + }; + let digits = "1".repeat(200); + assert_eq!( + FormatSpec::insert_locale_grouping(&digits, &locale(vec![CHAR_MAX])), + digits + ); + assert_eq!( + FormatSpec::insert_locale_grouping("1234567", &locale(vec![3, CHAR_MAX])), + "1234,567" + ); + assert_eq!( + FormatSpec::insert_locale_grouping("1234567", &locale(vec![3, 0])), + "1,234,567" + ); + } + #[test] fn format_complex_locale_rejects_zero_padding_before_after_sign_alignment() { let locale = LocaleInfo { diff --git a/crates/host_env/src/cert_store.rs b/crates/host_env/src/cert_store.rs index f95716faa99..ab5b92672a8 100644 --- a/crates/host_env/src/cert_store.rs +++ b/crates/host_env/src/cert_store.rs @@ -1,11 +1,15 @@ +use core::ffi::CStr; use std::io; use schannel::{RawPointer, cert_context::ValidUses, cert_store::CertStore}; use windows_sys::Win32::{ Foundation::{CRYPT_E_NOT_FOUND, GetLastError}, Security::Cryptography::{ - CERT_CONTEXT, CRL_CONTEXT, CertCloseStore, CertEnumCRLsInStore, CertOpenSystemStoreW, - PKCS_7_ASN_ENCODING, X509_ASN_ENCODING, + CERT_CONTEXT, CERT_FIND_EXT_ONLY_ENHKEY_USAGE_FLAG, CERT_FIND_PROP_ONLY_ENHKEY_USAGE_FLAG, + CERT_STORE_PROV_SYSTEM_A, CERT_STORE_READONLY_FLAG, CERT_SYSTEM_STORE_LOCAL_MACHINE, + CRL_CONTEXT, CTL_USAGE, CertCloseStore, CertEnumCRLsInStore, CertEnumCertificatesInStore, + CertFreeCertificateContext, CertGetEnhancedKeyUsage, CertOpenStore, CertOpenSystemStoreW, + HCERTSTORE, PKCS_7_ASN_ENCODING, X509_ASN_ENCODING, }, }; @@ -51,6 +55,158 @@ fn encoding_type(raw: u32) -> EncodingType { } } +fn encoding_type_exact(raw: u32) -> EncodingType { + match raw { + X509_ASN_ENCODING => EncodingType::X509Asn, + PKCS_7_ASN_ENCODING => EncodingType::Pkcs7Asn, + other => EncodingType::Other(other), + } +} + +fn open_local_machine(store_name: &CStr) -> io::Result { + let store = unsafe { + CertOpenStore( + CERT_STORE_PROV_SYSTEM_A, + 0, + 0, + CERT_SYSTEM_STORE_LOCAL_MACHINE | CERT_STORE_READONLY_FLAG, + store_name.as_ptr().cast(), + ) + }; + if store.is_null() { + Err(io::Error::last_os_error()) + } else { + Ok(store) + } +} + +unsafe fn certificate_uses_for_flags( + cert: *const CERT_CONTEXT, + flags: u32, +) -> io::Result> { + let mut size = 0; + if unsafe { CertGetEnhancedKeyUsage(cert, flags, core::ptr::null_mut(), &mut size) } == 0 { + let error = io::Error::last_os_error(); + if error.raw_os_error() == Some(CRYPT_E_NOT_FOUND) { + return Ok(None); + } + return Err(error); + } + + let words = (size as usize) + .max(core::mem::size_of::()) + .div_ceil(core::mem::size_of::()); + let mut buffer = vec![0usize; words]; + let usage = buffer.as_mut_ptr().cast::(); + if unsafe { CertGetEnhancedKeyUsage(cert, flags, usage, &mut size) } == 0 { + let error = io::Error::last_os_error(); + if error.raw_os_error() == Some(CRYPT_E_NOT_FOUND) { + return Ok(None); + } + return Err(error); + } + + let usage = unsafe { &*usage }; + let mut oids = Vec::with_capacity(usage.cUsageIdentifier as usize); + for index in 0..usage.cUsageIdentifier as usize { + let oid = unsafe { *usage.rgpszUsageIdentifier.add(index) }; + if !oid.is_null() { + oids.push( + unsafe { CStr::from_ptr(oid.cast()) } + .to_string_lossy() + .into_owned(), + ); + } + } + Ok(Some(CertificateUses::Oids(oids))) +} + +unsafe fn certificate_uses(cert: *const CERT_CONTEXT) -> io::Result { + if let Some(uses) = + unsafe { certificate_uses_for_flags(cert, CERT_FIND_PROP_ONLY_ENHKEY_USAGE_FLAG) }? + { + return Ok(uses); + } + Ok( + unsafe { certificate_uses_for_flags(cert, CERT_FIND_EXT_ONLY_ENHKEY_USAGE_FLAG) }? + .unwrap_or(CertificateUses::All), + ) +} + +/// Enumerate one read-only local-machine system store, matching the store +/// selected by CPython's Windows SSL helpers. Opening and closing the store +/// are both fallible; a close failure takes precedence over an enumeration +/// failure because it happens last. +pub fn enum_certificates_local_machine(store_name: &CStr) -> io::Result> { + let store = open_local_machine(store_name)?; + let result = (|| { + let mut entries = Vec::new(); + let mut context: *const CERT_CONTEXT = core::ptr::null(); + loop { + context = unsafe { CertEnumCertificatesInStore(store, context) }; + if context.is_null() { + let error = io::Error::last_os_error(); + if error.raw_os_error() == Some(CRYPT_E_NOT_FOUND) { + break; + } + return Err(error); + } + let cert = unsafe { &*context }; + let valid_uses = unsafe { certificate_uses(context) }; + if let Err(error) = valid_uses { + unsafe { CertFreeCertificateContext(context) }; + return Err(error); + } + entries.push(CertificateEntry { + der: unsafe { + core::slice::from_raw_parts(cert.pbCertEncoded, cert.cbCertEncoded as usize) + } + .to_vec(), + encoding: encoding_type_exact(cert.dwCertEncodingType), + valid_uses, + }); + } + Ok(entries) + })(); + if unsafe { CertCloseStore(store, 0) } == 0 { + return Err(io::Error::last_os_error()); + } + result +} + +/// Enumerate certificate-revocation lists from one read-only local-machine +/// system store. +pub fn enum_crls_local_machine(store_name: &CStr) -> io::Result> { + let store = open_local_machine(store_name)?; + let result = (|| { + let mut entries = Vec::new(); + let mut context: *const CRL_CONTEXT = core::ptr::null(); + loop { + context = unsafe { CertEnumCRLsInStore(store, context) }; + if context.is_null() { + let error = io::Error::last_os_error(); + if error.raw_os_error() == Some(CRYPT_E_NOT_FOUND) { + break; + } + return Err(error); + } + let crl = unsafe { &*context }; + entries.push(CrlEntry { + der: unsafe { + core::slice::from_raw_parts(crl.pbCrlEncoded, crl.cbCrlEncoded as usize) + } + .to_vec(), + encoding: encoding_type_exact(crl.dwCertEncodingType), + }); + } + Ok(entries) + })(); + if unsafe { CertCloseStore(store, 0) } == 0 { + return Err(io::Error::last_os_error()); + } + result +} + pub fn enum_certificates(store_name: &str) -> CertificateEntries { let open_fns = [CertStore::open_current_user, CertStore::open_local_machine]; let mut had_open_store = false; diff --git a/crates/host_env/src/locale.rs b/crates/host_env/src/locale.rs index c29110cc747..9809e2f3861 100644 --- a/crates/host_env/src/locale.rs +++ b/crates/host_env/src/locale.rs @@ -133,8 +133,12 @@ fn copy_grouping(ptr: *const libc::c_char) -> Vec { let mut out = Vec::new(); let mut cur = ptr; unsafe { - while ![0, libc::c_char::MAX].contains(&*cur) { - out.push(*cur); + while *cur != 0 { + let value = *cur; + out.push(value); + if value == libc::c_char::MAX { + break; + } cur = cur.add(1); } } @@ -707,3 +711,14 @@ mod snapshot { } } } + +#[cfg(test)] +mod tests { + use super::copy_grouping; + + #[test] + fn grouping_keeps_char_max_terminator() { + let grouping = [3, libc::c_char::MAX, 2, 0]; + assert_eq!(copy_grouping(grouping.as_ptr()), [3, libc::c_char::MAX]); + } +} diff --git a/crates/host_env/src/posix.rs b/crates/host_env/src/posix.rs index 02e95ef79a0..09d8d85bee1 100644 --- a/crates/host_env/src/posix.rs +++ b/crates/host_env/src/posix.rs @@ -2062,6 +2062,16 @@ pub fn sendfile( } } +#[cfg(target_os = "linux")] +/// Send from the input descriptor's current position and update that position. +pub fn sendfile_no_offset( + out_fd: BorrowedFd<'_>, + in_fd: BorrowedFd<'_>, + count: usize, +) -> std::io::Result { + nix::sys::sendfile::sendfile(out_fd, in_fd, None, count).map_err(std::io::Error::from) +} + #[cfg(target_os = "macos")] pub fn sendfile( in_fd: i32, diff --git a/crates/host_env/src/socket.rs b/crates/host_env/src/socket.rs index ed8b57595da..a46f4d8ce65 100644 --- a/crates/host_env/src/socket.rs +++ b/crates/host_env/src/socket.rs @@ -804,11 +804,30 @@ pub fn setsockopt_none(fd: libc::c_int, level: i32, name: i32, optlen: u32) -> i target_os = "openbsd", ))] pub fn if_nameindex() -> io::Result> { + Ok(if_nameindex_bytes()? + .into_iter() + .map(|(index, name)| (index, String::from_utf8_lossy(&name).into_owned())) + .collect()) +} + +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "fuchsia", + target_os = "ios", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", +))] +/// Return interface names as their original platform bytes without applying +/// a lossy text conversion. +pub fn if_nameindex_bytes() -> io::Result)>> { let list = nix::net::if_::if_nameindex().map_err(io::Error::from)?; Ok(list .to_slice() .iter() - .map(|iface| (iface.index(), iface.name().to_string_lossy().into_owned())) + .map(|iface| (iface.index(), iface.name().to_bytes().to_vec())) .collect()) } @@ -824,6 +843,12 @@ pub fn if_nametoindex_checked(name: &CStr) -> io::Result { #[cfg(unix)] pub fn if_indextoname_checked(index: u32) -> io::Result { + if_indextoname_bytes(index).map(|name| String::from_utf8_lossy(&name).into_owned()) +} + +#[cfg(unix)] +/// Return an interface name as its original platform bytes. +pub fn if_indextoname_bytes(index: u32) -> io::Result> { let mut buf = [0u8; libc::IF_NAMESIZE]; let ret = unsafe { libc::if_indextoname(index as libc::c_uint, buf.as_mut_ptr() as *mut libc::c_char) @@ -832,7 +857,7 @@ pub fn if_indextoname_checked(index: u32) -> io::Result { Err(io::Error::last_os_error()) } else { let buf = unsafe { CStr::from_ptr(buf.as_ptr() as *const libc::c_char) }; - Ok(buf.to_string_lossy().into_owned()) + Ok(buf.to_bytes().to_vec()) } } diff --git a/crates/host_env/src/winsound.rs b/crates/host_env/src/winsound.rs index b91a7a62490..a68759b4893 100644 --- a/crates/host_env/src/winsound.rs +++ b/crates/host_env/src/winsound.rs @@ -40,13 +40,20 @@ pub fn play_sound(source: PlaySoundSource<'_>, flags: u32) -> Result<(), PlaySou if matches!(source, PlaySoundSource::Memory(_)) && flags & SND_ASYNC != 0 { return Err(PlaySoundError::MemoryAsyncRejected); } - // `SND_MEMORY` requires a `Memory(_)` source; an empty pointer would - // dereference garbage. - if !matches!(source, PlaySoundSource::Memory(_)) && flags & SND_MEMORY != 0 { + // `SND_MEMORY` with a name would reinterpret the name's UTF-16 allocation + // as a RIFF buffer. A null source is still the documented stop operation, + // for which the remaining flags are ignored. + if matches!(source, PlaySoundSource::Name(_)) && flags & SND_MEMORY != 0 { return Err(PlaySoundError::MemoryFlagWithoutBuffer); } + // An empty slice's pointer is a dangling sentinel. Give WinMM zeroed + // storage instead; zero bytes do not start a RIFF header. + static EMPTY_IMAGE: [u8; 64] = [0; 64]; + // SND_MEMORY makes the `LPCWSTR` parameter point at a byte-oriented RIFF + // image. The pointer is only passed through, so it needs no `u16` alignment. let ptr: *const u16 = match source { PlaySoundSource::Stop => core::ptr::null(), + PlaySoundSource::Memory([]) => EMPTY_IMAGE.as_ptr().cast(), PlaySoundSource::Memory(buf) => buf.as_ptr().cast(), PlaySoundSource::Name(s) => s.as_ptr(), }; @@ -64,7 +71,7 @@ pub enum PlaySoundError { CallFailed, /// `Memory(_)` source cannot be combined with `SND_ASYNC` in the safe API. MemoryAsyncRejected, - /// `SND_MEMORY` set in `flags` but no `Memory(_)` buffer supplied. + /// `SND_MEMORY` set in `flags` for a named source. MemoryFlagWithoutBuffer, } diff --git a/crates/host_env/src/wmi.rs b/crates/host_env/src/wmi.rs index bf6d53264e6..100588640b8 100644 --- a/crates/host_env/src/wmi.rs +++ b/crates/host_env/src/wmi.rs @@ -566,10 +566,10 @@ unsafe fn query_thread_impl(param: *mut c_void) -> u32 { hr as u32 } -pub fn exec_query(query_str: &str) -> Result { - let query = WideCString::from_str(query_str) - .map(WideCString::into_vec_with_nul) - .map_err(|_| ExecQueryError::Code(ERROR_INVALID_NAME))?; +/// Execute a WMI query without translating unpaired UTF-16 surrogates in the +/// query or result. +pub fn exec_query_wide(query: &widestring::WideCStr) -> Result, ExecQueryError> { + let query = query.as_slice_with_nul().to_vec(); let mut h_thread: HANDLE = null_mut(); let mut err: u32 = 0; @@ -682,9 +682,15 @@ pub fn exec_query(query_str: &str) -> Result { return Err(ExecQueryError::Code(err)); } if offset == 0 { - return Ok(String::new()); + return Ok(Vec::new()); } let char_count = (offset as usize) / 2 - 1; - Ok(String::from_utf16_lossy(&buffer[..char_count])) + Ok(buffer[..char_count].to_vec()) +} + +pub fn exec_query(query_str: &str) -> Result { + let query = + WideCString::from_str(query_str).map_err(|_| ExecQueryError::Code(ERROR_INVALID_NAME))?; + exec_query_wide(&query).map(|result| String::from_utf16_lossy(&result)) } diff --git a/crates/stdlib/src/locale.rs b/crates/stdlib/src/locale.rs index f2966a49409..0333f20ffa9 100644 --- a/crates/stdlib/src/locale.rs +++ b/crates/stdlib/src/locale.rs @@ -49,7 +49,7 @@ mod _locale { group_vec.push(val.into()); } // https://github.com/python/cpython/blob/677320348728ce058fa3579017e985af74a236d4/Modules/_localemodule.c#L80 - if !group_vec.is_empty() { + if group.last().is_some_and(|&c| c != core::ffi::c_char::MAX) { group_vec.push(vm.ctx.new_int(0).into()); } vm.ctx.new_list(group_vec) diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index 34cb1c590e7..fa71f1182c2 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -3837,10 +3837,12 @@ mod _socket { #[cfg(not(target_os = "redox"))] #[pyfunction] - fn if_indextoname(if_index: IfIndex, vm: &VirtualMachine) -> PyResult { + fn if_indextoname(if_index: IfIndex, vm: &VirtualMachine) -> PyResult { #[cfg(windows)] { - host_socket::if_indextoname_checked(if_index).map_err(|_| vm.new_last_errno_error()) + host_socket::if_indextoname_checked(if_index) + .map(|name| vm.ctx.new_str(name)) + .map_err(|_| vm.new_last_errno_error()) } #[cfg(not(windows))] { @@ -3851,8 +3853,9 @@ mod _socket { if ret.is_null() { Err(vm.new_last_errno_error()) } else { + use std::os::unix::ffi::OsStrExt; let buf = unsafe { ffi::CStr::from_ptr(buf.as_ptr() as _) }; - Ok(buf.to_string_lossy().into_owned()) + vm.fsdecode(std::ffi::OsStr::from_bytes(buf.to_bytes())) } } } @@ -3872,13 +3875,15 @@ mod _socket { fn if_nameindex(vm: &VirtualMachine) -> PyResult> { #[cfg(not(windows))] { - let list = host_socket::if_nameindex() + use std::os::unix::ffi::OsStringExt; + host_socket::if_nameindex_bytes() .map_err(|err| err.into_pyexception(vm))? .into_iter() - .map(|tup| tup.to_pyobject(vm)) - .collect(); - - Ok(list) + .map(|(index, name)| { + let name = vm.fsdecode(std::ffi::OsString::from_vec(name))?; + Ok((index, name).to_pyobject(vm)) + }) + .collect() } #[cfg(windows)] { diff --git a/crates/vm/src/format.rs b/crates/vm/src/format.rs index 3ec7fb221e3..4c9b93bab09 100644 --- a/crates/vm/src/format.rs +++ b/crates/vm/src/format.rs @@ -15,7 +15,12 @@ pub(crate) fn get_locale_info() -> LocaleInfo { reason = "libc::c_char is not u8 on all platforms" )] let mut grouping = lc.grouping.iter().map(|&c| c as u8).collect::>(); - if !grouping.is_empty() { + // A CHAR_MAX terminator is already in the list; otherwise restore the NUL. + if lc + .grouping + .last() + .is_some_and(|&c| c != core::ffi::c_char::MAX) + { grouping.push(0); } LocaleInfo {