diff --git a/changelog.d/8448-centralize-string-readers.md b/changelog.d/8448-centralize-string-readers.md new file mode 100644 index 0000000000..6063a350ed --- /dev/null +++ b/changelog.d/8448-centralize-string-readers.md @@ -0,0 +1,6 @@ +### Changed + +- Centralized native stdlib and extension string-header reads behind the + runtime and `perry-ffi` accessors, preserving strict UTF-8, lossy UTF-8, and + raw-byte behavior while keeping results owned across allocations and async + boundaries. diff --git a/crates/perry-ext-fastify/src/context.rs b/crates/perry-ext-fastify/src/context.rs index 8aada9ef3e..3e16891f02 100644 --- a/crates/perry-ext-fastify/src/context.rs +++ b/crates/perry-ext-fastify/src/context.rs @@ -8,7 +8,8 @@ use std::sync::atomic::{AtomicU64, Ordering}; use perry_ffi::{ alloc_string, build_object_shape, get_handle, get_handle_mut, js_object_alloc_with_shape, - js_object_set_field, ArrayHeader, Handle, JsValue, ObjectHeader, StringHeader, + js_object_set_field, read_bytes, ArrayHeader, Handle, JsString, JsValue, ObjectHeader, + StringHeader, }; const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; @@ -211,13 +212,8 @@ fn urlencoding_decode(s: &str) -> String { /// Read a `*const StringHeader` into an owned `String`. pub(crate) unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) + let handle = JsString::from_raw(ptr as *mut StringHeader); + read_bytes(handle).map(|bytes| String::from_utf8_lossy(bytes).into_owned()) } /// Pull a string out of a raw i64 NaN-boxed value. diff --git a/crates/perry-ext-fastify/src/server.rs b/crates/perry-ext-fastify/src/server.rs index 5237c85dfe..23e9461908 100644 --- a/crates/perry-ext-fastify/src/server.rs +++ b/crates/perry-ext-fastify/src/server.rs @@ -24,8 +24,8 @@ use tokio::net::TcpListener; use tokio::sync::{mpsc, oneshot}; use perry_ffi::{ - alloc_string, get_handle, get_handle_mut, iter_handle_ids_of, register_handle, Handle, - JsClosure, JsValue, RawClosureHeader, StringHeader, + alloc_string, get_handle, get_handle_mut, iter_handle_ids_of, read_bytes, register_handle, + Handle, JsClosure, JsString, JsValue, RawClosureHeader, StringHeader, }; use crate::app::{ClosurePtr, FastifyApp}; @@ -1412,13 +1412,8 @@ unsafe fn gc_obj_type(ptr: *const u8) -> u8 { } unsafe fn string_header_to_string(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) + let handle = JsString::from_raw(ptr as *mut StringHeader); + read_bytes(handle).map(|bytes| String::from_utf8_lossy(bytes).into_owned()) } fn push_json_string(out: &mut Vec, bytes: &[u8]) { diff --git a/crates/perry-ext-http/src/server/types.rs b/crates/perry-ext-http/src/server/types.rs index 05a14b764c..0664053c59 100644 --- a/crates/perry-ext-http/src/server/types.rs +++ b/crates/perry-ext-http/src/server/types.rs @@ -1,7 +1,7 @@ //! Shared NaN-boxing constants, runtime extern declarations, and //! port/host extraction helpers. -use perry_ffi::{ArrayHeader, BufferHeader, JsValue, StringHeader}; +use perry_ffi::{read_bytes, ArrayHeader, BufferHeader, JsString, JsValue, StringHeader}; pub const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; pub const PTR_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; @@ -311,29 +311,15 @@ pub fn jsvalue_to_body_bytes(value: f64) -> Option> { /// Read a `StringHeader` as a Rust `String`, copying its bytes. pub(crate) fn read_string_header(ptr: *mut StringHeader) -> Option { - if ptr.is_null() { - return None; - } - unsafe { - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - let slice = std::slice::from_raw_parts(data, len); - Some(String::from_utf8_lossy(slice).into_owned()) - } + let handle = unsafe { JsString::from_raw(ptr) }; + read_bytes(handle).map(|bytes| String::from_utf8_lossy(bytes).into_owned()) } /// Read a `StringHeader` as raw bytes — used when the payload is /// not necessarily UTF-8 (Buffer / Uint8Array round-trip). pub(crate) fn read_string_header_bytes(ptr: *mut StringHeader) -> Option> { - if ptr.is_null() { - return None; - } - unsafe { - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - let slice = std::slice::from_raw_parts(data, len); - Some(slice.to_vec()) - } + let handle = unsafe { JsString::from_raw(ptr) }; + read_bytes(handle).map(<[u8]>::to_vec) } #[allow(dead_code)] diff --git a/crates/perry-ext-net/src/jsvalue.rs b/crates/perry-ext-net/src/jsvalue.rs index 90072cb243..b1bd4929ea 100644 --- a/crates/perry-ext-net/src/jsvalue.rs +++ b/crates/perry-ext-net/src/jsvalue.rs @@ -11,7 +11,7 @@ use perry_ffi::{ alloc_string, build_object_shape, js_object_alloc_with_shape, js_object_set_field, - nanbox_string_bits, BufferHeader, JsValue, ObjectHeader, StringHeader, + nanbox_string_bits, read_string, BufferHeader, JsString, JsValue, ObjectHeader, StringHeader, }; pub(crate) unsafe fn string_from_header_i64(ptr: i64) -> Option { @@ -23,11 +23,8 @@ pub(crate) unsafe fn string_from_header_i64(ptr: i64) -> Option { if p < 0x100000 { return None; } - let hdr = ptr as *const StringHeader; - let len = (*hdr).byte_len as usize; - let data_ptr = (hdr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) + let handle = JsString::from_raw(ptr as *mut StringHeader); + read_string(handle).map(str::to_owned) } // Runtime entrypoints provided by perry-runtime (declared as extern so diff --git a/crates/perry-stdlib/src/argon2.rs b/crates/perry-stdlib/src/argon2.rs index 12129d129e..72a74c25cc 100644 --- a/crates/perry-stdlib/src/argon2.rs +++ b/crates/perry-stdlib/src/argon2.rs @@ -4,23 +4,13 @@ //! Provides secure password hashing using Argon2id algorithm. use crate::common::spawn_for_promise; +use crate::common::string_from_header_lossy as string_from_header; use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; use perry_runtime::{js_promise_new, js_string_from_bytes, Promise, StringHeader}; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - /// argon2.hash(password) -> Promise /// /// Hash a password using Argon2id with default parameters. diff --git a/crates/perry-stdlib/src/axios.rs b/crates/perry-stdlib/src/axios.rs index 9af3b1e81f..23c812df1b 100644 --- a/crates/perry-stdlib/src/axios.rs +++ b/crates/perry-stdlib/src/axios.rs @@ -3,20 +3,12 @@ //! Native implementation of the 'axios' npm package using reqwest. //! Provides HTTP client functionality with a promise-based API. -use crate::common::{get_handle, register_handle, spawn_for_promise, Handle}; +use crate::common::{ + get_handle, register_handle, spawn_for_promise, string_from_header_lossy as string_from_header, + Handle, +}; use perry_runtime::{js_promise_new, js_string_from_bytes, Promise, StringHeader}; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() || (ptr as usize) < 0x1000 { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - /// #598: read the body argument as a JSON string. Strings pass /// through as-is; everything else is JSON.stringify'd via the /// runtime's `js_json_stringify`. See perry-ext-axios's parallel diff --git a/crates/perry-stdlib/src/bcrypt.rs b/crates/perry-stdlib/src/bcrypt.rs index 5ed278c743..661300456e 100644 --- a/crates/perry-stdlib/src/bcrypt.rs +++ b/crates/perry-stdlib/src/bcrypt.rs @@ -6,17 +6,7 @@ use perry_runtime::{js_string_from_bytes, JSValue, StringHeader}; use crate::common::async_bridge::{queue_deferred_resolution, queue_promise_resolution, spawn}; - -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} +use crate::common::string_from_header; /// Hash a password with the given cost factor /// bcrypt.hash(password, saltRounds) -> Promise diff --git a/crates/perry-stdlib/src/cheerio.rs b/crates/perry-stdlib/src/cheerio.rs index 389452f0bc..463aecf3b4 100644 --- a/crates/perry-stdlib/src/cheerio.rs +++ b/crates/perry-stdlib/src/cheerio.rs @@ -3,21 +3,13 @@ //! Native implementation of the 'cheerio' npm package using scraper. //! Provides jQuery-like HTML parsing and manipulation. -use crate::common::{get_handle, register_handle, Handle}; +use crate::common::{ + get_handle, register_handle, string_from_header_lossy as string_from_header, Handle, +}; use perry_runtime::{js_array_alloc, js_array_push, js_string_from_bytes, JSValue, StringHeader}; use scraper::{ElementRef, Html, Selector}; /// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - /// Cheerio document handle (stores HTML string for thread safety) pub struct CheerioHandle { pub html: String, diff --git a/crates/perry-stdlib/src/commander.rs b/crates/perry-stdlib/src/commander.rs index eb373fa5a7..0e96c9a664 100644 --- a/crates/perry-stdlib/src/commander.rs +++ b/crates/perry-stdlib/src/commander.rs @@ -18,7 +18,10 @@ use perry_runtime::{ }; use std::collections::HashMap; -use crate::common::{for_each_handle_mut_of, get_handle_mut, register_handle, Handle}; +use crate::common::{ + for_each_handle_mut_of, get_handle_mut, register_handle, + string_from_header_lossy as string_from_header, Handle, +}; // NaN-box tags. Mirror perry-runtime/src/value.rs constants. Duplicated // here because they're not exported across crate boundaries; if either @@ -118,16 +121,6 @@ fn scan_commander_roots_mut(visitor: &mut perry_runtime::gc::RuntimeRootVisitor< // --------------------------------------------------------------------------- // Helpers -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() || (ptr as usize) < 4096 { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - /// Parse the commander flag-spec mini-language used in `.option(...)`: /// `"-p, --port "` → `(Some('p'), "port", false)`. /// `"-v, --verbose"` → `(Some('v'), "verbose", true)`. diff --git a/crates/perry-stdlib/src/common/mod.rs b/crates/perry-stdlib/src/common/mod.rs index dc375daa99..6e522d9860 100644 --- a/crates/perry-stdlib/src/common/mod.rs +++ b/crates/perry-stdlib/src/common/mod.rs @@ -1,5 +1,7 @@ //! Common utilities for stdlib modules +use perry_runtime::{string::str_bytes_from_jsvalue, value::JSValue, StringHeader}; + pub mod handle; // Tokio-backed promise/runtime bridge — only needed when an async feature // (http-server/client, websocket, databases, email, scheduler, rate-limit, @@ -17,3 +19,103 @@ mod net_socket_bridge; pub use async_bridge::*; pub use dispatch::*; pub use handle::*; + +/// Copy a runtime string header into an owned UTF-8 string. +/// +/// Invalid UTF-8 and values from Perry's native-handle address band are +/// rejected. The owned return is intentional: callers may allocate or cross +/// an async boundary after this function returns, so no GC-managed payload +/// borrow may escape this accessor. +/// +/// # Safety +/// +/// `ptr` must be null, a native handle-band value, or point to a live Perry +/// [`StringHeader`]. +pub(crate) unsafe fn string_from_header(ptr: *const StringHeader) -> Option { + map_string_header_bytes(ptr, |bytes| { + std::str::from_utf8(bytes).ok().map(str::to_owned) + }) + .flatten() +} + +/// Copy a runtime string header into an owned string, replacing invalid UTF-8. +/// +/// # Safety +/// +/// `ptr` has the same requirements as [`string_from_header`]. +pub(crate) unsafe fn string_from_header_lossy(ptr: *const StringHeader) -> Option { + map_string_header_bytes(ptr, |bytes| String::from_utf8_lossy(bytes).into_owned()) +} + +/// Copy a runtime string header's payload without UTF-8 validation. +/// +/// # Safety +/// +/// `ptr` has the same requirements as [`string_from_header`]. +pub(crate) unsafe fn bytes_from_header(ptr: *const StringHeader) -> Option> { + map_string_header_bytes(ptr, <[u8]>::to_vec) +} + +unsafe fn map_string_header_bytes( + ptr: *const StringHeader, + map: impl FnOnce(&[u8]) -> T, +) -> Option { + if ptr.is_null() || perry_runtime::value::addr_class::is_handle_band(ptr as usize) { + return None; + } + + let value = f64::from_bits(JSValue::string_ptr(ptr as *mut StringHeader).bits()); + let mut scratch = [0; perry_runtime::value::SHORT_STRING_MAX_LEN]; + let (data, len) = str_bytes_from_jsvalue(value, &mut scratch)?; + let bytes = std::slice::from_raw_parts(data, len as usize); + Some(map(bytes)) +} + +#[cfg(test)] +mod string_header_tests { + use super::*; + + #[test] + fn string_readers_reject_null_and_handle_band_values() { + for ptr in [ + std::ptr::null(), + 1usize as *const StringHeader, + (perry_runtime::value::addr_class::HANDLE_BAND_MAX - 1) as *const StringHeader, + ] { + assert!(unsafe { string_from_header(ptr) }.is_none()); + assert!(unsafe { string_from_header_lossy(ptr) }.is_none()); + assert!(unsafe { bytes_from_header(ptr) }.is_none()); + } + } + + #[test] + fn string_readers_preserve_text_and_raw_bytes() { + let input = b"Perry \xf0\x9f\xa6\x86"; + let ptr = perry_runtime::js_string_from_bytes(input.as_ptr(), input.len() as u32); + + assert_eq!( + unsafe { string_from_header(ptr) }.as_deref(), + Some("Perry \u{1f986}") + ); + assert_eq!( + unsafe { string_from_header_lossy(ptr) }.as_deref(), + Some("Perry \u{1f986}") + ); + assert_eq!( + unsafe { bytes_from_header(ptr) }.as_deref(), + Some(input.as_slice()) + ); + + let invalid = b"\xff"; + let ptr = perry_runtime::js_string_from_bytes(invalid.as_ptr(), invalid.len() as u32); + assert!(unsafe { string_from_header(ptr) }.is_none()); + assert_eq!( + unsafe { string_from_header_lossy(ptr) }.as_deref(), + Some("\u{fffd}") + ); + assert_eq!( + unsafe { bytes_from_header(ptr) }.as_deref(), + Some(invalid.as_slice()) + ); + } +} diff --git a/crates/perry-stdlib/src/container/mod.rs b/crates/perry-stdlib/src/container/mod.rs index 7f19861ab8..56d92c61c1 100644 --- a/crates/perry-stdlib/src/container/mod.rs +++ b/crates/perry-stdlib/src/container/mod.rs @@ -47,6 +47,7 @@ pub use types::{ ContainerSpec, ImageInfo, ListOrDict, }; +pub(crate) use crate::common::string_from_header_lossy as string_from_header; pub use backend::{detect_backend, ContainerBackend}; use perry_runtime::{js_promise_new, Promise, StringHeader}; use std::collections::HashMap; @@ -98,16 +99,6 @@ pub(crate) async fn get_global_backend( } /// Helper to extract string from StringHeader pointer -pub(crate) unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() || (ptr as usize) < 0x1000 { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - /// Helper to create a JS string from a Rust string pub(crate) unsafe fn string_to_js(s: &str) -> *const StringHeader { let bytes = s.as_bytes(); diff --git a/crates/perry-stdlib/src/container/types.rs b/crates/perry-stdlib/src/container/types.rs index 5efe1a65e2..dbaec24c35 100644 --- a/crates/perry-stdlib/src/container/types.rs +++ b/crates/perry-stdlib/src/container/types.rs @@ -1,5 +1,6 @@ //! Type definitions for the perry/container module. +pub(crate) use crate::common::string_from_header_lossy as string_from_header; use dashmap::DashMap; use perry_runtime::StringHeader; use serde::{Deserialize, Serialize}; @@ -125,13 +126,3 @@ pub use perry_container_compose::types::{ }; // ============ Helper for StringHeader ============ - -pub unsafe fn string_from_header(header: *const StringHeader) -> Option { - if header.is_null() || (header as usize) < 0x1000 { - return None; - } - let byte_len = (*header).byte_len as usize; - let data_ptr = (header as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, byte_len); - Some(String::from_utf8_lossy(bytes).into_owned()) -} diff --git a/crates/perry-stdlib/src/cron.rs b/crates/perry-stdlib/src/cron.rs index 0587c87d4b..c812b49c80 100644 --- a/crates/perry-stdlib/src/cron.rs +++ b/crates/perry-stdlib/src/cron.rs @@ -22,7 +22,9 @@ //! noted "we'd invoke js_callback_invoke(callback_id) here" — callbacks //! never fired in user code. -use crate::common::{get_handle, register_handle, Handle, RUNTIME}; +use crate::common::{ + get_handle, register_handle, string_from_header_lossy as string_from_header, Handle, RUNTIME, +}; use cron::Schedule; use perry_runtime::closure::{js_closure_call0, ClosureHeader}; use perry_runtime::gc::{gc_register_mutable_root_scanner_named, RuntimeRootVisitor}; @@ -32,17 +34,6 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex as StdMutex, Once}; use std::time::Instant; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - /// Cron job handle. /// /// `running` is shared with the global `CRON_TIMERS` queue so `start()` / diff --git a/crates/perry-stdlib/src/crypto/util.rs b/crates/perry-stdlib/src/crypto/util.rs index 2b28476f18..be1dd1dd4e 100644 --- a/crates/perry-stdlib/src/crypto/util.rs +++ b/crates/perry-stdlib/src/crypto/util.rs @@ -49,16 +49,7 @@ pub(super) use sha2::{Digest as Sha256Digest, Sha224, Sha256, Sha384, Sha512, Sh // as of sha3 0.12 (RustCrypto/hashes#869). pub(super) use shake::{ExtendableOutput, Shake128, Shake256, XofReader}; -/// Helper to extract string from StringHeader pointer -pub(super) unsafe fn string_from_header(ptr: *const StringHeader) -> Option> { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(bytes.to_vec()) -} +pub(super) use crate::common::bytes_from_header as string_from_header; /// Extract the raw bytes from a pointer that might be a Buffer, a /// StringHeader, or anything that uses the `[u32 byte-length prefix][bytes]` diff --git a/crates/perry-stdlib/src/dayjs.rs b/crates/perry-stdlib/src/dayjs.rs index ce3c2ff56b..cca17651e7 100644 --- a/crates/perry-stdlib/src/dayjs.rs +++ b/crates/perry-stdlib/src/dayjs.rs @@ -6,18 +6,7 @@ use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, TimeZone, Timelike, Utc}; use perry_runtime::{js_string_from_bytes, StringHeader}; -use crate::common::{register_handle, Handle}; - -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} +use crate::common::{register_handle, string_from_header, Handle}; /// Wrapper around DateTime for handle storage pub struct DayjsHandle { diff --git a/crates/perry-stdlib/src/decimal.rs b/crates/perry-stdlib/src/decimal.rs index d7c7caef21..89b3f99153 100644 --- a/crates/perry-stdlib/src/decimal.rs +++ b/crates/perry-stdlib/src/decimal.rs @@ -7,7 +7,9 @@ use perry_runtime::{js_string_from_bytes, StringHeader}; use rust_decimal::prelude::*; use rust_decimal::Decimal; -use crate::common::{get_handle_mut, register_handle, Handle}; +use crate::common::{ + get_handle_mut, register_handle, string_from_header_lossy as string_from_header, Handle, +}; /// DecimalHandle stores a Decimal value pub struct DecimalHandle { @@ -20,17 +22,6 @@ impl DecimalHandle { } } -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - /// Create a new Decimal from a number #[no_mangle] pub extern "C" fn js_decimal_from_number(value: f64) -> Handle { diff --git a/crates/perry-stdlib/src/domain.rs b/crates/perry-stdlib/src/domain.rs index d603cd7228..a505eb06d1 100644 --- a/crates/perry-stdlib/src/domain.rs +++ b/crates/perry-stdlib/src/domain.rs @@ -1,6 +1,9 @@ //! Minimal node:domain surface. -use crate::common::{for_each_handle_mut_of, get_handle, get_handle_mut, register_handle, Handle}; +use crate::common::{ + for_each_handle_mut_of, get_handle, get_handle_mut, register_handle, + string_from_header_lossy as string_from_header, Handle, +}; use perry_runtime::{ js_array_alloc, js_array_length, js_array_push_f64, js_nanbox_get_pointer, js_nanbox_pointer, js_string_from_bytes, ArrayHeader, ClosureHeader, JSValue, ObjectHeader, StringHeader, @@ -160,16 +163,6 @@ fn handle_from_value(value: f64) -> Handle { } } -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - fn event_name_from_value(value: f64) -> Option<*const StringHeader> { let ptr = perry_runtime::value::js_get_string_pointer_unified(value) as *const StringHeader; if ptr.is_null() { diff --git a/crates/perry-stdlib/src/dotenv.rs b/crates/perry-stdlib/src/dotenv.rs index 0bf1473ad1..fb17c93c27 100644 --- a/crates/perry-stdlib/src/dotenv.rs +++ b/crates/perry-stdlib/src/dotenv.rs @@ -8,21 +8,12 @@ use std::collections::HashMap; use std::fs; use std::sync::Mutex; +use crate::common::string_from_header; + lazy_static::lazy_static! { static ref DOTENV_LOADED: Mutex = Mutex::new(false); } -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} - /// Parse a .env file content into key-value pairs fn parse_dotenv_content(content: &str) -> HashMap { let mut vars = HashMap::new(); diff --git a/crates/perry-stdlib/src/events.rs b/crates/perry-stdlib/src/events.rs index 7d10e6141b..28e72d83b6 100644 --- a/crates/perry-stdlib/src/events.rs +++ b/crates/perry-stdlib/src/events.rs @@ -454,7 +454,7 @@ fn remove_one_matching_listener( /// Helper to extract string from StringHeader pointer unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { + if ptr.is_null() || perry_runtime::value::addr_class::is_handle_band(ptr as usize) { return None; } @@ -465,10 +465,7 @@ unsafe fn string_from_header(ptr: *const StringHeader) -> Option { return string_from_header(rendered as *const StringHeader); } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) + crate::common::string_from_header_lossy(ptr) } fn value_from_bits(bits: i64) -> f64 { diff --git a/crates/perry-stdlib/src/fetch/mod.rs b/crates/perry-stdlib/src/fetch/mod.rs index a5ba23e785..d03d4c0a31 100644 --- a/crates/perry-stdlib/src/fetch/mod.rs +++ b/crates/perry-stdlib/src/fetch/mod.rs @@ -323,32 +323,7 @@ pub(crate) fn handle_to_f64(id: usize) -> f64 { perry_runtime::value::js_nanbox_pointer(id as i64) } -/// Helper to extract string from StringHeader pointer -pub(crate) unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - // NaN-boxed TAG_UNDEFINED (0x7FFC_0000_0000_0001) unboxes to 0x1 - // after POINTER_MASK. Treat any pointer below page size as invalid. - if ptr.is_null() || (ptr as usize) < 0x1000 { - return None; - } - // A handle-band value (`< 0x100000`: Web Fetch Headers/Request/Response/Blob - // ids, net/http small handles, zlib/proxy ids) is a registry id, NOT a - // `StringHeader` pointer. It reaches here when `fetch()` is called with a - // non-string first argument such as a `Request`/`Headers` object — the - // codegen passes the bare handle id into the `url_ptr` `*StringHeader` - // slot. Reading `(*ptr).byte_len` at `id + 4` then dereferences an - // unmapped low address → SIGSEGV (the doctor / mcp-list startup crash at - // the fetch-handle address). The `< 0x1000` floor above only catches the - // TAG_UNDEFINED `0x1` remnant; widen it to the whole handle band so any - // native handle is treated as "not a string" (`None`) rather than - // dereferenced. - if perry_runtime::value::addr_class::is_handle_band(ptr as usize) { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} +pub(crate) use crate::common::string_from_header; /// Diagnostic: return the number of FETCH_RESPONSES entries. /// Useful for detecting response handle leaks in long-running services. diff --git a/crates/perry-stdlib/src/framework/multipart.rs b/crates/perry-stdlib/src/framework/multipart.rs index 2b1c0aa4ee..8b43123fcf 100644 --- a/crates/perry-stdlib/src/framework/multipart.rs +++ b/crates/perry-stdlib/src/framework/multipart.rs @@ -5,17 +5,9 @@ use perry_runtime::{js_string_from_bytes, StringHeader}; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} +use crate::common::string_from_header_lossy as string_from_header; +/// Helper to extract string from StringHeader pointer /// A single part from a multipart/form-data body #[derive(Debug, Clone)] pub struct MultipartPart { diff --git a/crates/perry-stdlib/src/framework/request.rs b/crates/perry-stdlib/src/framework/request.rs index 5cbd3433f6..e2a71667a5 100644 --- a/crates/perry-stdlib/src/framework/request.rs +++ b/crates/perry-stdlib/src/framework/request.rs @@ -6,18 +6,7 @@ use perry_runtime::{js_string_from_bytes, StringHeader}; use std::collections::HashMap; use super::server::RequestHandle; -use crate::common::{get_handle, Handle}; - -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} +use crate::common::{get_handle, string_from_header_lossy as string_from_header, Handle}; /// Get request ID (for debugging/logging) #[no_mangle] diff --git a/crates/perry-stdlib/src/framework/response.rs b/crates/perry-stdlib/src/framework/response.rs index 86e4bf03a4..9477cfba42 100644 --- a/crates/perry-stdlib/src/framework/response.rs +++ b/crates/perry-stdlib/src/framework/response.rs @@ -6,18 +6,7 @@ use perry_runtime::{js_string_from_bytes, StringHeader}; use std::collections::HashMap; use super::server::{HttpResponse, RequestHandle, PENDING_RESPONSES}; -use crate::common::{get_handle, Handle}; - -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} +use crate::common::{get_handle, string_from_header_lossy as string_from_header, Handle}; /// Send a text response #[no_mangle] diff --git a/crates/perry-stdlib/src/framework/server.rs b/crates/perry-stdlib/src/framework/server.rs index cca18ab56a..f5e284ac8f 100644 --- a/crates/perry-stdlib/src/framework/server.rs +++ b/crates/perry-stdlib/src/framework/server.rs @@ -16,19 +16,11 @@ use std::sync::Arc; use tokio::net::TcpListener; use tokio::sync::mpsc; -use crate::common::{get_handle, register_handle, Handle, RUNTIME}; +use crate::common::{ + get_handle, register_handle, string_from_header_lossy as string_from_header, Handle, RUNTIME, +}; /// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - /// Request ID counter static REQUEST_ID_COUNTER: AtomicU64 = AtomicU64::new(1); diff --git a/crates/perry-stdlib/src/ioredis.rs b/crates/perry-stdlib/src/ioredis.rs index ca52457ae7..d094f218a7 100644 --- a/crates/perry-stdlib/src/ioredis.rs +++ b/crates/perry-stdlib/src/ioredis.rs @@ -10,7 +10,7 @@ use std::sync::Mutex; use std::time::Duration; use crate::common::async_bridge::{queue_deferred_resolution, queue_promise_resolution, spawn}; -use crate::common::{register_handle, Handle}; +use crate::common::{register_handle, string_from_header, Handle}; /// Default timeout for Redis operations const DEFAULT_TIMEOUT_SECS: u64 = 10; @@ -30,17 +30,6 @@ lazy_static::lazy_static! { static ref URLS: Mutex> = Mutex::new(HashMap::new()); } -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} - /// Create a new Redis client (synchronous, connects lazily like real ioredis) /// new Redis() or new Redis(options) #[no_mangle] diff --git a/crates/perry-stdlib/src/jsonwebtoken.rs b/crates/perry-stdlib/src/jsonwebtoken.rs index d7b9f7cbba..0bfb6dd0bd 100644 --- a/crates/perry-stdlib/src/jsonwebtoken.rs +++ b/crates/perry-stdlib/src/jsonwebtoken.rs @@ -10,16 +10,7 @@ use perry_runtime::{ use serde::{Deserialize, Serialize}; use std::collections::HashMap; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} +use crate::common::string_from_header; /// Generic claims structure that can hold any JSON #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/perry-stdlib/src/lodash.rs b/crates/perry-stdlib/src/lodash.rs index 0cf5fb188c..2adf8fda91 100644 --- a/crates/perry-stdlib/src/lodash.rs +++ b/crates/perry-stdlib/src/lodash.rs @@ -9,16 +9,7 @@ use perry_runtime::{ }; use std::collections::HashSet; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} +use crate::common::string_from_header_lossy as string_from_header; // ============================================================================ // Array functions diff --git a/crates/perry-stdlib/src/moment.rs b/crates/perry-stdlib/src/moment.rs index 3cda2edec4..085821577a 100644 --- a/crates/perry-stdlib/src/moment.rs +++ b/crates/perry-stdlib/src/moment.rs @@ -3,21 +3,12 @@ //! Native implementation of the 'moment' npm package using chrono. //! Provides date/time manipulation with moment.js compatible API. -use crate::common::{get_handle, register_handle, Handle}; +use crate::common::{ + get_handle, register_handle, string_from_header_lossy as string_from_header, Handle, +}; use chrono::{DateTime, Datelike, Duration, NaiveDateTime, TimeZone, Timelike, Utc}; use perry_runtime::{js_string_from_bytes, StringHeader}; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - /// Moment handle pub struct MomentHandle { pub datetime: DateTime, diff --git a/crates/perry-stdlib/src/mongodb.rs b/crates/perry-stdlib/src/mongodb.rs index d63c22c3c1..7122602d96 100644 --- a/crates/perry-stdlib/src/mongodb.rs +++ b/crates/perry-stdlib/src/mongodb.rs @@ -3,6 +3,7 @@ //! Native implementation of the 'mongodb' npm package. //! Provides MongoDB client functionality. +use crate::common::string_from_header_lossy as string_from_header; use crate::common::{ get_handle, register_handle, spawn_for_promise, spawn_for_promise_deferred, Handle, }; @@ -33,16 +34,6 @@ unsafe fn jsvalue_to_json_string(value: f64) -> String { } /// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - /// MongoDB client handle. /// /// Lives in two states like PgConnectionHandle: pre-connect (`pending_uri` diff --git a/crates/perry-stdlib/src/nanoid.rs b/crates/perry-stdlib/src/nanoid.rs index cba7b1ec72..b39c64441a 100644 --- a/crates/perry-stdlib/src/nanoid.rs +++ b/crates/perry-stdlib/src/nanoid.rs @@ -6,16 +6,7 @@ use nanoid::nanoid; use perry_runtime::{js_string_from_bytes, StringHeader}; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} +use crate::common::string_from_header; /// Generate a nanoid with default settings (21 chars, URL-safe alphabet) /// nanoid() -> string diff --git a/crates/perry-stdlib/src/net/mod.rs b/crates/perry-stdlib/src/net/mod.rs index 0ded4f746d..6c4c9a2e4e 100644 --- a/crates/perry-stdlib/src/net/mod.rs +++ b/crates/perry-stdlib/src/net/mod.rs @@ -175,15 +175,7 @@ enum PendingNetEvent { // ─── Helpers ───────────────────────────────────────────────────────────────── unsafe fn string_from_header_i64(ptr: i64) -> Option { - let p = ptr as usize; - if p < 0x1000 { - return None; - } - let hdr = ptr as *const StringHeader; - let len = (*hdr).byte_len as usize; - let data_ptr = (hdr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) + crate::common::string_from_header(ptr as *const StringHeader) } /// Issue #770 — true iff `val_f64` carries `POINTER_TAG` (0x7FFD), i.e. diff --git a/crates/perry-stdlib/src/ratelimit.rs b/crates/perry-stdlib/src/ratelimit.rs index 63ec9e8490..f9e71d73e8 100644 --- a/crates/perry-stdlib/src/ratelimit.rs +++ b/crates/perry-stdlib/src/ratelimit.rs @@ -14,7 +14,9 @@ //! flip's copy); this bundled copy links when the ext staticlib is //! unavailable or `PERRY_DISABLE_WELL_KNOWN` is set. -use crate::common::{get_handle, register_handle, Handle}; +use crate::common::{ + get_handle, register_handle, string_from_header_lossy as string_from_header, Handle, +}; use governor::{ clock::DefaultClock, state::{InMemoryState, NotKeyed}, @@ -29,17 +31,6 @@ use std::num::NonZeroU32; use std::sync::Mutex; use std::time::{Duration, Instant}; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - /// Legacy direct (non-keyed) limiter — kept for the pre-existing /// `js_ratelimit_new` / `js_ratelimit_check` / `js_ratelimit_remaining` /// symbol surface. diff --git a/crates/perry-stdlib/src/readline/mod.rs b/crates/perry-stdlib/src/readline/mod.rs index 3268937961..4841d7b306 100644 --- a/crates/perry-stdlib/src/readline/mod.rs +++ b/crates/perry-stdlib/src/readline/mod.rs @@ -406,14 +406,7 @@ fn boxed_str(bytes: &[u8]) -> f64 { } fn string_header_to_string(ptr: *const StringHeader) -> String { - if ptr.is_null() { - return String::new(); - } - unsafe { - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() - } + unsafe { crate::common::string_from_header_lossy(ptr) }.unwrap_or_default() } fn value_to_string(value: f64) -> String { diff --git a/crates/perry-stdlib/src/sharp.rs b/crates/perry-stdlib/src/sharp.rs index 057aa78cf7..5883cdeff1 100644 --- a/crates/perry-stdlib/src/sharp.rs +++ b/crates/perry-stdlib/src/sharp.rs @@ -3,33 +3,14 @@ //! Native implementation of the 'sharp' npm package using the image crate. //! Provides image processing functionality. -use crate::common::{get_handle, register_handle, spawn_for_promise, Handle}; +use crate::common::{ + bytes_from_header, get_handle, register_handle, spawn_for_promise, + string_from_header_lossy as string_from_header, Handle, +}; use image::{imageops::FilterType, DynamicImage, GenericImageView, ImageFormat}; use perry_runtime::{js_promise_new, js_string_from_bytes, JSValue, Promise, StringHeader}; use std::io::Cursor; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - -/// Helper to extract bytes from StringHeader pointer -unsafe fn bytes_from_header(ptr: *const StringHeader) -> Option> { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(bytes.to_vec()) -} - /// Sharp image handle with pending operations pub struct SharpHandle { pub image: DynamicImage, diff --git a/crates/perry-stdlib/src/slugify.rs b/crates/perry-stdlib/src/slugify.rs index 44f1206955..3ae8d850d5 100644 --- a/crates/perry-stdlib/src/slugify.rs +++ b/crates/perry-stdlib/src/slugify.rs @@ -23,17 +23,9 @@ use perry_runtime::{js_string_from_bytes, JSValue, ObjectHeader, StringHeader}; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} +use crate::common::string_from_header; +/// Helper to extract string from StringHeader pointer /// Subset of npm slugify's charMap. Case-preserving, may expand to /// multiple chars ('ß' → "ss", '&' → "and") — exactly like the npm map. pub(crate) fn char_map(c: char) -> Option<&'static str> { diff --git a/crates/perry-stdlib/src/sqlite/options.rs b/crates/perry-stdlib/src/sqlite/options.rs index d76f290ef5..2c942ebad0 100644 --- a/crates/perry-stdlib/src/sqlite/options.rs +++ b/crates/perry-stdlib/src/sqlite/options.rs @@ -1,4 +1,5 @@ use super::*; +pub(crate) use crate::common::string_from_header_lossy as string_from_header; use perry_runtime::{ closure::{is_closure_ptr, ClosureHeader}, js_get_string_pointer_unified, js_nanbox_pointer, js_object_get_field_by_name, @@ -7,17 +8,6 @@ use perry_runtime::{ use rusqlite::{ffi, limits::Limit, Connection}; use std::ffi::{CStr, CString}; -/// Helper to extract string from StringHeader pointer -pub(crate) unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - pub(crate) fn undefined_f64() -> f64 { f64::from_bits(TAG_UNDEFINED_BITS) } diff --git a/crates/perry-stdlib/src/tls.rs b/crates/perry-stdlib/src/tls.rs index 4874910f96..b99c5a31ef 100644 --- a/crates/perry-stdlib/src/tls.rs +++ b/crates/perry-stdlib/src/tls.rs @@ -11,6 +11,7 @@ use std::pin::Pin; use std::sync::{Arc, Mutex, Once, OnceLock}; use std::task::{Context, Poll}; +use crate::common::string_from_header; use perry_runtime::{ js_array_alloc, js_array_is_array, js_array_push, js_closure_call0, js_closure_call1, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, js_object_get_field_by_name, @@ -128,16 +129,6 @@ fn nanbox_str(s: &str) -> f64 { } } -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() || (ptr as usize) < 0x1000 { - return None; - } - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} - unsafe fn value_to_string(value: f64) -> Option { let ptr = js_get_string_pointer_unified(value); if ptr != 0 { diff --git a/crates/perry-stdlib/src/uuid.rs b/crates/perry-stdlib/src/uuid.rs index dc29de8084..1fa8b90020 100644 --- a/crates/perry-stdlib/src/uuid.rs +++ b/crates/perry-stdlib/src/uuid.rs @@ -37,22 +37,12 @@ pub extern "C" fn js_uuid_v7() -> *mut StringHeader { js_string_from_bytes(uuid_str.as_ptr(), uuid_str.len() as u32) } -/// Read a `*const StringHeader` into a `&str`, or `""` if null / invalid UTF-8. -unsafe fn read_str<'a>(str_ptr: *const StringHeader) -> &'a str { - if str_ptr.is_null() { - return ""; - } - let len = (*str_ptr).byte_len as usize; - let data_ptr = (str_ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).unwrap_or("") -} - /// Parse a namespace argument (string-UUID form) into a `Uuid`, falling /// back to the nil UUID when the argument isn't a parseable UUID string. /// The array-namespace form is only reachable via `perry.compilePackages`. unsafe fn parse_namespace(ns_ptr: *const StringHeader) -> Uuid { - Uuid::parse_str(read_str(ns_ptr)).unwrap_or_else(|_| Uuid::nil()) + let namespace = crate::common::string_from_header(ns_ptr).unwrap_or_default(); + Uuid::parse_str(&namespace).unwrap_or_else(|_| Uuid::nil()) } /// Generate a v5 (SHA-1 name-based) UUID and return it as a string @@ -63,7 +53,8 @@ pub unsafe extern "C" fn js_uuid_v5( ns_ptr: *const StringHeader, ) -> *mut StringHeader { let namespace = parse_namespace(ns_ptr); - let uuid = Uuid::new_v5(&namespace, read_str(name_ptr).as_bytes()); + let name = crate::common::string_from_header(name_ptr).unwrap_or_default(); + let uuid = Uuid::new_v5(&namespace, name.as_bytes()); let uuid_str = uuid.to_string(); js_string_from_bytes(uuid_str.as_ptr(), uuid_str.len() as u32) } @@ -76,7 +67,8 @@ pub unsafe extern "C" fn js_uuid_v3( ns_ptr: *const StringHeader, ) -> *mut StringHeader { let namespace = parse_namespace(ns_ptr); - let uuid = Uuid::new_v3(&namespace, read_str(name_ptr).as_bytes()); + let name = crate::common::string_from_header(name_ptr).unwrap_or_default(); + let uuid = Uuid::new_v3(&namespace, name.as_bytes()); let uuid_str = uuid.to_string(); js_string_from_bytes(uuid_str.as_ptr(), uuid_str.len() as u32) } diff --git a/crates/perry-stdlib/src/validator.rs b/crates/perry-stdlib/src/validator.rs index c057d1cfa9..8aaf8785b0 100644 --- a/crates/perry-stdlib/src/validator.rs +++ b/crates/perry-stdlib/src/validator.rs @@ -5,16 +5,7 @@ use perry_runtime::StringHeader; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} +use crate::common::string_from_header; /// Check if a string is a valid email address /// validator.isEmail(str) -> boolean diff --git a/crates/perry-stdlib/src/worker_threads.rs b/crates/perry-stdlib/src/worker_threads.rs index 58739474bc..50c7868bad 100644 --- a/crates/perry-stdlib/src/worker_threads.rs +++ b/crates/perry-stdlib/src/worker_threads.rs @@ -291,15 +291,7 @@ fn closure_ptr_from_bits(bits: u64) -> *const ClosureHeader { } fn string_header_to_string(str_ptr: *const StringHeader) -> Option { - if str_ptr.is_null() || (str_ptr as usize) < 0x1000 { - return None; - } - unsafe { - let len = (*str_ptr).byte_len as usize; - let data_ptr = (str_ptr as *const u8).add(std::mem::size_of::()); - let slice = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(slice).into_owned()) - } + unsafe { crate::common::string_from_header_lossy(str_ptr) } } fn string_value_to_string(value: f64) -> Option { diff --git a/crates/perry-stdlib/src/ws.rs b/crates/perry-stdlib/src/ws.rs index 5c699cf7eb..5ccd1ddfea 100644 --- a/crates/perry-stdlib/src/ws.rs +++ b/crates/perry-stdlib/src/ws.rs @@ -18,6 +18,7 @@ use tokio_tungstenite::{connect_async, tungstenite::Message}; #[cfg(not(target_os = "ios"))] use crate::common::async_bridge::{queue_deferred_resolution, queue_promise_resolution, spawn}; +use crate::common::string_from_header; use crate::common::{for_each_handle_mut_of, get_handle_mut, register_handle, Handle}; /// #6117 — rustls panics resolving the process-level CryptoProvider on the @@ -217,17 +218,6 @@ fn cleanup_ws_client(ws_id: usize) { } } -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} - /// Create a new WebSocket connection /// new WebSocket(url) -> Promise #[cfg(not(target_os = "ios"))]