Skip to content
Merged
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
6 changes: 6 additions & 0 deletions changelog.d/8448-centralize-string-readers.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 4 additions & 8 deletions crates/perry-ext-fastify/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> {
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::<StringHeader>());
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.
Expand Down
13 changes: 4 additions & 9 deletions crates/perry-ext-fastify/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -1412,13 +1412,8 @@ unsafe fn gc_obj_type(ptr: *const u8) -> u8 {
}

unsafe fn string_header_to_string(ptr: *const StringHeader) -> Option<String> {
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::<StringHeader>());
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<u8>, bytes: &[u8]) {
Expand Down
24 changes: 5 additions & 19 deletions crates/perry-ext-http/src/server/types.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -311,29 +311,15 @@ pub fn jsvalue_to_body_bytes(value: f64) -> Option<Vec<u8>> {

/// Read a `StringHeader` as a Rust `String`, copying its bytes.
pub(crate) fn read_string_header(ptr: *mut StringHeader) -> Option<String> {
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::<StringHeader>());
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<Vec<u8>> {
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::<StringHeader>());
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)]
Expand Down
9 changes: 3 additions & 6 deletions crates/perry-ext-net/src/jsvalue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
Expand All @@ -23,11 +23,8 @@ pub(crate) unsafe fn string_from_header_i64(ptr: i64) -> Option<String> {
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::<StringHeader>());
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
Expand Down
12 changes: 1 addition & 11 deletions crates/perry-stdlib/src/argon2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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::<StringHeader>());
let bytes = std::slice::from_raw_parts(data_ptr, len);
Some(String::from_utf8_lossy(bytes).to_string())
}

/// argon2.hash(password) -> Promise<string>
///
/// Hash a password using Argon2id with default parameters.
Expand Down
16 changes: 4 additions & 12 deletions crates/perry-stdlib/src/axios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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::<StringHeader>());
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
Expand Down
12 changes: 1 addition & 11 deletions crates/perry-stdlib/src/bcrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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::<StringHeader>());
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<string>
Expand Down
14 changes: 3 additions & 11 deletions crates/perry-stdlib/src/cheerio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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::<StringHeader>());
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,
Expand Down
15 changes: 4 additions & 11 deletions crates/perry-stdlib/src/commander.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String> {
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::<StringHeader>());
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 <number>"` → `(Some('p'), "port", false)`.
/// `"-v, --verbose"` → `(Some('v'), "verbose", true)`.
Expand Down
102 changes: 102 additions & 0 deletions crates/perry-stdlib/src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<String> {
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<String> {
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<Vec<u8>> {
map_string_header_bytes(ptr, <[u8]>::to_vec)
}

unsafe fn map_string_header_bytes<T>(
ptr: *const StringHeader,
map: impl FnOnce(&[u8]) -> T,
) -> Option<T> {
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())
);
}
}
11 changes: 1 addition & 10 deletions crates/perry-stdlib/src/container/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> {
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::<StringHeader>());
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();
Expand Down
Loading
Loading