Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Lib/test/test_unicodedata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,6 @@ def test_failed_import_during_compiling(self):
"(can't load unicodedata module)"
self.assertIn(error, result.err.decode("ascii"))

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError Process return code is 1
def test_unicodedata_unload_reload(self):
# gh-149449: dropping unicodedata and running gc must not leave the
# cached _ucnhash_CAPI pointer dangling.
Expand Down
11 changes: 7 additions & 4 deletions crates/capi/src/pycapsule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use rustpython_vm::{PyObjectRef, PyResult, VirtualMachine};
pub type PyCapsule_Destructor = unsafe extern "C" fn(capsule: *mut PyObject);

#[unsafe(no_mangle)]
pub extern "C" fn PyCapsule_New(
pub unsafe extern "C" fn PyCapsule_New(
pointer: *mut c_void,
name: *const c_char,
destructor: Option<PyCapsule_Destructor>,
Expand All @@ -20,7 +20,8 @@ pub extern "C" fn PyCapsule_New(
return Err(vm.new_value_error("PyCapsule_New called with null pointer"));
}
let name = NonNull::new(name.cast_mut()).map(|ptr| unsafe { CStr::from_ptr(ptr.as_ptr()) });
Ok(vm.ctx.new_capsule(pointer, name, destructor))
// SAFETY: the C API caller provides a value valid for its capsule protocol.
Ok(unsafe { vm.ctx.new_capsule(pointer, name, destructor) })
})
}

Expand Down Expand Up @@ -66,7 +67,8 @@ pub unsafe extern "C" fn PyCapsule_SetContext(
let capsule = unsafe { capsule.assume_borrowed() }
.downcast_ref_if_exact::<PyCapsule>(vm)
.ok_or_else(|| vm.new_value_error("Invalid capsule"))?;
let _: () = capsule.set_context(context);
// SAFETY: the C API caller must preserve the capsule protocol.
unsafe { capsule.set_context(context) };
Ok(())
})
}
Expand All @@ -80,7 +82,8 @@ pub unsafe extern "C" fn PyCapsule_SetPointer(
let capsule = unsafe { capsule.assume_borrowed() }
.downcast_ref_if_exact::<PyCapsule>(vm)
.ok_or_else(|| vm.new_value_error("Invalid capsule"))?;
let _: () = capsule.set_pointer(pointer);
// SAFETY: the C API caller must preserve the capsule protocol.
unsafe { capsule.set_pointer(pointer) };
Ok(())
})
}
Expand Down
30 changes: 27 additions & 3 deletions crates/common/src/encodings.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use alloc::borrow::Cow;
use core::ops::{self, Range};

use num_traits::ToPrimitive;
Expand Down Expand Up @@ -27,13 +28,34 @@ pub trait StrBuffer: AsRef<Wtf8> {
}
}

/// Name services used by Unicode codecs. Runtime contexts can import a provider
/// lazily; standalone codecs use the bundled database.
pub trait UnicodeNames {
fn name<'a>(&self, code: u32, buffer: &'a mut [u8; 256]) -> Option<Cow<'a, str>>;
fn character(&self, name: &[u8]) -> Option<CodePoint>;
}

struct BundledUnicodeNames;
impl UnicodeNames for BundledUnicodeNames {
fn name<'a>(&self, code: u32, _buffer: &'a mut [u8; 256]) -> Option<Cow<'a, str>> {
rustpython_unicode::data::name_for_namereplace(code).map(Cow::Owned)
}
fn character(&self, name: &[u8]) -> Option<CodePoint> {
rustpython_unicode::lookup_character(core::str::from_utf8(name).ok()?).map(CodePoint::from)
}
}

pub trait CodecContext: Sized {
type Error;
type StrBuf: StrBuffer;
type BytesBuf: AsRef<[u8]>;

fn string(&self, s: Wtf8Buf) -> Self::StrBuf;
fn bytes(&self, b: Vec<u8>) -> Self::BytesBuf;

fn unicode_names(&self) -> Result<&dyn UnicodeNames, Self::Error> {
Ok(&BundledUnicodeNames)
}
}

pub trait EncodeContext: CodecContext {
Expand Down Expand Up @@ -411,21 +433,23 @@ pub mod errors {
}

#[derive(Clone, Copy)]
pub struct NameReplace;
pub struct NameReplace<'a>(pub &'a dyn UnicodeNames);

impl<Ctx: EncodeContext> EncodeErrorHandler<Ctx> for NameReplace {
impl<Ctx: EncodeContext> EncodeErrorHandler<Ctx> for NameReplace<'_> {
fn handle_encode_error(
&self,
ctx: &mut Ctx,
range: Range<StrSize>,
_reason: Option<&str>,
) -> Result<(EncodeReplace<Ctx>, StrSize), Ctx::Error> {
let names = self.0;
let mut name_buffer = [0; 256];
let err_str = &ctx.full_data()[range.start.bytes..range.end.bytes];
let num_chars = range.end.chars - range.start.chars;
let mut out = String::with_capacity(num_chars * 4);
for c in err_str.code_points() {
let c_u32 = c.to_u32();
if let Some(c_name) = rustpython_unicode::data::name_for_namereplace(c_u32) {
if let Some(c_name) = names.name(c_u32, &mut name_buffer) {
write!(out, "\\N{{{c_name}}}").unwrap();
} else if c_u32 >= 0x10000 {
write!(out, "\\U{c_u32:08x}").unwrap();
Expand Down
7 changes: 4 additions & 3 deletions crates/common/src/encodings/unicode_escape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ where
out.push_wtf8(replace.as_ref());
}
b'N' => {
let names = ctx.unicode_names()?;
if rest.len() == 2 && !final_decode {
consumed = ctx.position();
break;
Expand Down Expand Up @@ -232,10 +233,10 @@ where
out.push_wtf8(replace.as_ref());
continue;
}
let name = core::str::from_utf8(&rest[name_start..look]).ok();
match name.and_then(rustpython_unicode::lookup_character) {
let name = &rest[name_start..look];
match names.character(name) {
Some(ch) => {
out.push_char(ch);
out.push(ch);
ctx.advance(look + 1);
continue;
}
Expand Down
8 changes: 7 additions & 1 deletion crates/stdlib/src/unicodedata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
See also: https://docs.python.org/3/library/unicodedata.html
*/

// spell-checker:ignore nfkc unistr unidata
// spell-checker:ignore nfkc unistr unidata ucnhash

pub(crate) use unicodedata::module_def;

Expand Down Expand Up @@ -506,6 +506,12 @@ mod unicodedata {
Ucd::new(false).into_ref(&vm.ctx)
}

#[pyattr]
#[allow(non_snake_case)]
fn _ucnhash_CAPI(vm: &VirtualMachine) -> crate::vm::PyRef<crate::vm::builtins::PyCapsule> {
vm.new_unicode_name_capsule()
}

#[pyattr]
fn unidata_version(_vm: &VirtualMachine) -> String {
UNICODE_VERSION.into()
Expand Down
13 changes: 13 additions & 0 deletions crates/unicode/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,19 @@ pub fn name_for_namereplace(code: u32) -> Option<String> {
char::from_u32(code).and_then(character_name)
}

/// The private Unicode-name API can return a named-sequence pseudo codepoint.
#[must_use]
pub fn lookup_name_code(name: &str, with_named_seq: bool) -> Option<u32> {
match lookup_name(name)? {
LookupResult::Character(ch) => Some(ch as u32),
LookupResult::Sequence(_) if with_named_seq => CAPI_SEQUENCE_NAMES
.iter()
.position(|candidate| cmp_caseless(candidate, name) == Ordering::Equal)
.map(|i| 0xF0200 + i as u32),
LookupResult::Sequence(_) => None,
}
}

fn lookup_alias(name: &str) -> Option<char> {
NAME_ALIASES
.binary_search_by(|(a, _)| cmp_caseless(a, name))
Expand Down
20 changes: 17 additions & 3 deletions crates/vm/src/builtins/capsule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ impl PyPayload for PyCapsule {

#[pyclass(with(Representable, Destructor), flags(DISALLOW_INSTANTIATION))]
impl PyCapsule {
pub fn new(
/// Construct a capsule carrying a native protocol value.
///
/// # Safety
/// `ptr`, `context` (initially null), and `destructor` must satisfy the
/// protocol identified by `name` or by the receiving API, including valid
/// pointed-to data, callback signatures, synchronization and required lifetimes.
/// This also applies to unnamed capsules. Consumers may retain imported
/// pointers beyond the capsule lifetime when their protocol permits it.
pub unsafe fn new(
ptr: *mut c_void,
name: Option<&'static CStr>,
destructor: Option<unsafe extern "C" fn(_: *mut PyObject)>,
Expand All @@ -43,7 +51,10 @@ impl PyCapsule {
self.ptr.load(core::sync::atomic::Ordering::Relaxed)
}

pub fn set_pointer(&self, pointer: *mut c_void) {
/// # Safety
/// The replacement must preserve the capsule protocol and destructor
/// invariants, including any previously exported pointer lifetimes.
pub unsafe fn set_pointer(&self, pointer: *mut c_void) {
self.ptr
.store(pointer, core::sync::atomic::Ordering::Relaxed);
}
Expand All @@ -52,7 +63,10 @@ impl PyCapsule {
self.context.load(core::sync::atomic::Ordering::Relaxed)
}

pub fn set_context(&self, context: *mut c_void) {
/// # Safety
/// The context must preserve the capsule protocol and destructor invariants.
/// It can identify the pointee's type even for an unnamed capsule.
pub unsafe fn set_context(&self, context: *mut c_void) {
self.context
.store(context, core::sync::atomic::Ordering::Relaxed);
}
Expand Down
16 changes: 14 additions & 2 deletions crates/vm/src/codecs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use rustpython_common::{
ascii,
borrow::BorrowedValue,
encodings::{
CodecContext, DecodeContext, DecodeErrorHandler, EncodeContext, EncodeErrorHandler,
self, CodecContext, DecodeContext, DecodeErrorHandler, EncodeContext, EncodeErrorHandler,
EncodeReplace, StrBuffer, StrSize, errors,
},
lock::{OnceCell, PyRwLock},
Expand Down Expand Up @@ -1211,6 +1211,17 @@ impl CodecContext for PyDecodeContext<'_> {
fn bytes(&self, b: Vec<u8>) -> Self::BytesBuf {
self.vm.ctx.new_bytes(b)
}
fn unicode_names(&self) -> PyResult<&dyn encodings::UnicodeNames> {
self.vm
.unicode_names()
.map(|names| names as &dyn encodings::UnicodeNames)
.map_err(|_| {
self.vm.new_exception_msg(
self.vm.ctx.exceptions.unicode_error.to_owned(),
"\\N escapes not supported (can't load unicodedata module)".into(),
)
})
}
}

impl DecodeContext for PyDecodeContext<'_> {
Expand Down Expand Up @@ -1730,7 +1741,8 @@ fn backslashreplace_errors(

fn namereplace_errors(err: PyObjectRef, vm: &VirtualMachine) -> PyResult<(PyObjectRef, usize)> {
if is_encode_err(&err, vm) {
call_native_encode_error(errors::NameReplace, err, vm)
let names = vm.unicode_names()?;
call_native_encode_error(errors::NameReplace(names), err, vm)
} else {
Err(bad_err_type(&err, vm))
}
Expand Down
1 change: 1 addition & 0 deletions crates/vm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub mod convert;
mod coroutine;
pub mod datastack;
mod dict_inner;
mod unicode_names;

#[cfg(feature = "rustpython-compiler")]
pub mod eval;
Expand Down
16 changes: 11 additions & 5 deletions crates/vm/src/stdlib/_testinternalcapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ mod _testinternalcapi {
return;
}
let data = capsule.pointer().cast::<CrossinterpData>();
capsule.set_pointer(core::ptr::null_mut());
// SAFETY: null marks the transferred allocation as already released.
unsafe { capsule.set_pointer(core::ptr::null_mut()) };
if !data.is_null() {
// SAFETY: get_crossinterp_data transferred this Box to the capsule.
// Clearing the pointer above prevents a second release.
Expand All @@ -233,10 +234,15 @@ mod _testinternalcapi {
let mode = args.mode.into_option();
let data = CrossinterpData::from_object(&args.obj, mode.as_deref(), vm)?;
let data = Box::into_raw(Box::new(data)).cast();
let capsule = vm
.ctx
.new_capsule(data, None, Some(release_crossinterp_data));
capsule.set_context(crossinterp_data_tag());
// SAFETY: data is the owned CrossinterpData allocation expected by the
// destructor and restore_crossinterp_data; the tag identifies this type.
let capsule = unsafe {
let capsule = vm
.ctx
.new_capsule(data, None, Some(release_crossinterp_data));
capsule.set_context(crossinterp_data_tag());
capsule
};
Ok(capsule.into())
}

Expand Down
9 changes: 4 additions & 5 deletions crates/vm/src/stdlib/nt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1133,10 +1133,8 @@ pub(crate) mod module {
let wide = path.to_wide_cstring(vm)?;
let cookie = host_winapi::add_dll_directory(&wide)
.map_err(|err| OSErrorBuilder::with_filename(&err, path, vm))?;
Ok(vm
.ctx
.new_capsule(cookie, Some(DLL_DIRECTORY_COOKIE), None)
.into())
// SAFETY: cookie is the live native DLL-directory handle returned above.
Ok(unsafe { vm.ctx.new_capsule(cookie, Some(DLL_DIRECTORY_COOKIE), None) }.into())
}

#[derive(FromArgs)]
Expand All @@ -1158,7 +1156,8 @@ pub(crate) mod module {
}
host_winapi::remove_dll_directory(capsule.pointer())
.map_err(|err| err.to_pyexception(vm))?;
capsule.set_pointer(core::ptr::null_mut());
// SAFETY: null marks the native handle as closed and prevents reuse.
unsafe { capsule.set_pointer(core::ptr::null_mut()) };
Ok(())
}

Expand Down
Loading
Loading