#![deny(
unsafe_op_in_unsafe_fn,
clippy::undocumented_unsafe_blocks,
clippy::missing_safety_doc
)]
#![allow(clippy::module_name_repetitions)]
mod builder;
mod code_point;
mod common;
mod display;
mod iter;
mod str;
mod r#type;
mod vtable;
#[cfg(test)]
mod tests;
use self::iter::Windows;
use crate::display::{JsStrDisplayEscaped, JsStrDisplayLossy, JsStringDebugInfo};
use crate::iter::CodePointsIter;
use crate::r#type::{Latin1, Utf16};
pub use crate::vtable::StaticString;
use crate::vtable::{SequenceString, SliceString};
#[doc(inline)]
pub use crate::{
builder::{CommonJsStringBuilder, Latin1JsStringBuilder, Utf16JsStringBuilder},
code_point::CodePoint,
common::StaticJsStrings,
iter::Iter,
str::{JsStr, JsStrVariant},
};
use std::marker::PhantomData;
use std::{borrow::Cow, mem::ManuallyDrop};
use std::{
convert::Infallible,
hash::{Hash, Hasher},
ptr::{self, NonNull},
str::FromStr,
};
use vtable::JsStringVTable;
fn alloc_overflow() -> ! {
panic!("detected overflow during string allocation")
}
pub(crate) const fn is_trimmable_whitespace(c: char) -> bool {
matches!(
c,
'\u{0009}' | '\u{000B}' | '\u{000C}' | '\u{0020}' | '\u{00A0}' | '\u{FEFF}' |
'\u{1680}' | '\u{2000}'
..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' |
'\u{000A}' | '\u{000D}' | '\u{2028}' | '\u{2029}'
)
}
pub(crate) const fn is_trimmable_whitespace_latin1(c: u8) -> bool {
matches!(
c,
0x09 | 0x0B | 0x0C | 0x20 | 0xA0 |
0x0A | 0x0D
)
}
#[allow(missing_copy_implementations, missing_debug_implementations)]
pub struct RawJsString {
phantom_data: PhantomData<*mut ()>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[repr(u8)]
pub(crate) enum JsStringKind {
Latin1Sequence = 0,
Utf16Sequence = 1,
Slice = 2,
Static = 3,
}
#[allow(clippy::module_name_repetitions)]
pub struct JsString {
ptr: NonNull<JsStringVTable>,
}
static_assertions::assert_eq_size!(JsString, *const ());
impl<'a> From<&'a JsString> for JsStr<'a> {
#[inline]
fn from(value: &'a JsString) -> Self {
value.as_str()
}
}
impl<'a> IntoIterator for &'a JsString {
type Item = u16;
type IntoIter = Iter<'a>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl JsString {
#[inline]
#[must_use]
pub fn iter(&self) -> Iter<'_> {
self.as_str().iter()
}
#[inline]
#[must_use]
pub fn windows(&self, size: usize) -> Windows<'_> {
self.as_str().windows(size)
}
#[inline]
#[must_use]
pub fn to_std_string_escaped(&self) -> String {
self.display_escaped().to_string()
}
#[inline]
#[must_use]
pub fn to_std_string_lossy(&self) -> String {
self.display_lossy().to_string()
}
#[inline]
pub fn to_std_string(&self) -> Result<String, std::string::FromUtf16Error> {
self.as_str().to_std_string()
}
#[inline]
#[allow(clippy::missing_panics_doc)]
pub fn to_std_string_with_surrogates(
&self,
) -> impl Iterator<Item = Result<String, u16>> + use<'_> {
let mut iter = self.code_points().peekable();
std::iter::from_fn(move || {
let cp = iter.next()?;
let char = match cp {
CodePoint::Unicode(c) => c,
CodePoint::UnpairedSurrogate(surr) => return Some(Err(surr)),
};
let mut string = String::from(char);
while let Some(cp) = iter.peek().and_then(|cp| match cp {
CodePoint::Unicode(c) => Some(*c),
CodePoint::UnpairedSurrogate(_) => None,
}) {
string.push(cp);
iter.next().expect("iter.peek() ensures that next is Some");
}
Some(Ok(string))
})
}
#[inline]
#[must_use]
pub fn map_valid_segments<F>(&self, mut f: F) -> Self
where
F: FnMut(String) -> String,
{
let mut text = Vec::new();
for part in self.to_std_string_with_surrogates() {
match part {
Ok(string) => text.extend(f(string).encode_utf16()),
Err(surr) => text.push(surr),
}
}
Self::from(&text[..])
}
#[inline]
#[must_use]
pub fn code_points(&self) -> CodePointsIter<'_> {
(self.vtable().code_points)(self.ptr)
}
#[inline]
#[must_use]
pub fn variant(&self) -> JsStrVariant<'_> {
self.as_str().variant()
}
#[inline]
#[must_use]
pub fn index_of(&self, search_value: JsStr<'_>, from_index: usize) -> Option<usize> {
self.as_str().index_of(search_value, from_index)
}
#[inline]
#[must_use]
pub fn code_point_at(&self, position: usize) -> CodePoint {
self.as_str().code_point_at(position)
}
#[inline]
#[must_use]
pub fn to_number(&self) -> f64 {
self.as_str().to_number()
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.vtable().len
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[inline]
#[must_use]
pub fn to_vec(&self) -> Vec<u16> {
self.as_str().to_vec()
}
#[inline]
#[must_use]
pub fn contains(&self, element: u8) -> bool {
self.as_str().contains(element)
}
#[inline]
#[must_use]
pub fn trim(&self) -> JsString {
let (start, end) = match self.variant() {
JsStrVariant::Latin1(v) => {
let Some(start) = v.iter().position(|c| !is_trimmable_whitespace_latin1(*c)) else {
return StaticJsStrings::EMPTY_STRING;
};
let end = v
.iter()
.rposition(|c| !is_trimmable_whitespace_latin1(*c))
.unwrap_or(start);
(start, end)
}
JsStrVariant::Utf16(v) => {
let Some(start) = v.iter().copied().position(|r| {
!char::from_u32(u32::from(r)).is_some_and(is_trimmable_whitespace)
}) else {
return StaticJsStrings::EMPTY_STRING;
};
let end = v
.iter()
.copied()
.rposition(|r| {
!char::from_u32(u32::from(r)).is_some_and(is_trimmable_whitespace)
})
.unwrap_or(start);
(start, end)
}
};
unsafe { Self::slice_unchecked(self, start, end + 1) }
}
#[inline]
#[must_use]
pub fn trim_start(&self) -> JsString {
let Some(start) = (match self.variant() {
JsStrVariant::Latin1(v) => v.iter().position(|c| !is_trimmable_whitespace_latin1(*c)),
JsStrVariant::Utf16(v) => v
.iter()
.copied()
.position(|r| !char::from_u32(u32::from(r)).is_some_and(is_trimmable_whitespace)),
}) else {
return StaticJsStrings::EMPTY_STRING;
};
unsafe { Self::slice_unchecked(self, start, self.len()) }
}
#[inline]
#[must_use]
pub fn trim_end(&self) -> JsString {
let Some(end) = (match self.variant() {
JsStrVariant::Latin1(v) => v.iter().rposition(|c| !is_trimmable_whitespace_latin1(*c)),
JsStrVariant::Utf16(v) => v
.iter()
.copied()
.rposition(|r| !char::from_u32(u32::from(r)).is_some_and(is_trimmable_whitespace)),
}) else {
return StaticJsStrings::EMPTY_STRING;
};
unsafe { Self::slice_unchecked(self, 0, end + 1) }
}
#[inline]
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn starts_with(&self, needle: JsStr<'_>) -> bool {
self.as_str().starts_with(needle)
}
#[inline]
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn ends_with(&self, needle: JsStr<'_>) -> bool {
self.as_str().ends_with(needle)
}
#[inline]
#[must_use]
pub fn code_unit_at(&self, index: usize) -> Option<u16> {
self.as_str().get(index)
}
#[inline]
#[must_use]
pub fn get<I>(&self, index: I) -> Option<JsString>
where
I: JsStringSliceIndex,
{
index.get(self)
}
#[inline]
#[must_use]
pub fn get_expect<I>(&self, index: I) -> JsString
where
I: JsStringSliceIndex,
{
index.get(self).expect("Unexpected get()")
}
#[inline]
#[must_use]
pub fn display_escaped(&self) -> JsStrDisplayEscaped<'_> {
JsStrDisplayEscaped::from(self)
}
#[inline]
#[must_use]
pub fn display_lossy(&self) -> JsStrDisplayLossy<'_> {
self.as_str().display_lossy()
}
#[inline]
#[must_use]
pub fn debug_info(&self) -> JsStringDebugInfo<'_> {
self.into()
}
#[inline]
#[must_use]
pub fn into_raw(self) -> NonNull<RawJsString> {
ManuallyDrop::new(self).ptr.cast()
}
#[inline]
#[must_use]
pub const unsafe fn from_raw(ptr: NonNull<RawJsString>) -> Self {
Self { ptr: ptr.cast() }
}
#[inline]
#[must_use]
pub(crate) const unsafe fn from_ptr(ptr: NonNull<JsStringVTable>) -> Self {
Self { ptr }
}
}
static_assertions::const_assert!(align_of::<*const JsStr<'static>>() >= 2);
impl JsString {
#[inline]
#[must_use]
pub fn is_static(&self) -> bool {
self.vtable().kind == JsStringKind::Static
}
#[inline]
#[must_use]
const fn vtable(&self) -> &JsStringVTable {
unsafe { self.ptr.as_ref() }
}
#[inline]
#[must_use]
pub const fn from_static(str: &'static StaticString) -> Self {
Self {
ptr: NonNull::from_ref(str).cast(),
}
}
#[inline]
#[must_use]
pub unsafe fn slice_unchecked(data: &JsString, start: usize, end: usize) -> Self {
let slice = Box::new(unsafe { SliceString::new(data, start, end) });
Self {
ptr: NonNull::from(Box::leak(slice)).cast(),
}
}
#[inline]
#[must_use]
pub fn slice(&self, p1: usize, mut p2: usize) -> JsString {
if p2 > self.len() {
p2 = self.len();
}
if p1 >= p2 {
StaticJsStrings::EMPTY_STRING
} else {
unsafe { Self::slice_unchecked(self, p1, p2) }
}
}
#[inline]
#[must_use]
pub(crate) fn kind(&self) -> JsStringKind {
self.vtable().kind
}
#[inline]
pub(crate) unsafe fn as_inner<T>(&self) -> &T {
unsafe { self.ptr.cast::<T>().as_ref() }
}
}
impl JsString {
#[inline]
#[must_use]
pub fn as_str(&self) -> JsStr<'_> {
(self.vtable().as_str)(self.ptr)
}
#[inline]
#[must_use]
pub fn concat(x: JsStr<'_>, y: JsStr<'_>) -> Self {
Self::concat_array(&[x, y])
}
#[inline]
#[must_use]
pub fn concat_array(strings: &[JsStr<'_>]) -> Self {
let mut latin1_encoding = true;
let mut full_count = 0usize;
for string in strings {
let Some(sum) = full_count.checked_add(string.len()) else {
alloc_overflow()
};
if !string.is_latin1() {
latin1_encoding = false;
}
full_count = sum;
}
let (ptr, data_offset) = if latin1_encoding {
let p = SequenceString::<Latin1>::allocate(full_count);
(p.cast::<u8>(), size_of::<SequenceString<Latin1>>())
} else {
let p = SequenceString::<Utf16>::allocate(full_count);
(p.cast::<u8>(), size_of::<SequenceString<Utf16>>())
};
let string = {
let mut data = unsafe {
let seq_ptr = ptr.as_ptr();
seq_ptr.add(data_offset)
};
for &string in strings {
unsafe {
#[allow(clippy::cast_ptr_alignment)]
match (latin1_encoding, string.variant()) {
(true, JsStrVariant::Latin1(s)) => {
let count = s.len();
ptr::copy_nonoverlapping(s.as_ptr(), data.cast::<u8>(), count);
data = data.cast::<u8>().add(count).cast::<u8>();
}
(false, JsStrVariant::Latin1(s)) => {
let count = s.len();
for (i, byte) in s.iter().enumerate() {
*data.cast::<u16>().add(i) = u16::from(*byte);
}
data = data.cast::<u16>().add(count).cast::<u8>();
}
(false, JsStrVariant::Utf16(s)) => {
let count = s.len();
ptr::copy_nonoverlapping(s.as_ptr(), data.cast::<u16>(), count);
data = data.cast::<u16>().add(count).cast::<u8>();
}
(true, JsStrVariant::Utf16(_)) => {
unreachable!("Already checked that it's latin1 encoding")
}
}
}
}
Self { ptr: ptr.cast() }
};
StaticJsStrings::get_string(&string.as_str()).unwrap_or(string)
}
fn from_slice_skip_interning(string: JsStr<'_>) -> Self {
let count = string.len();
unsafe {
#[allow(clippy::cast_ptr_alignment)]
match string.variant() {
JsStrVariant::Latin1(s) => {
let ptr = SequenceString::<Latin1>::allocate(count);
let data = (&raw mut (*ptr.as_ptr()).data)
.cast::<<Latin1 as r#type::StringType>::Byte>();
ptr::copy_nonoverlapping(s.as_ptr(), data, count);
Self { ptr: ptr.cast() }
}
JsStrVariant::Utf16(s) => {
let ptr = SequenceString::<Utf16>::allocate(count);
let data = (&raw mut (*ptr.as_ptr()).data)
.cast::<<Utf16 as r#type::StringType>::Byte>();
ptr::copy_nonoverlapping(s.as_ptr(), data, count);
Self { ptr: ptr.cast() }
}
}
}
}
fn from_js_str(string: JsStr<'_>) -> Self {
if let Some(s) = StaticJsStrings::get_string(&string) {
return s;
}
Self::from_slice_skip_interning(string)
}
#[inline]
#[must_use]
pub fn refcount(&self) -> Option<usize> {
(self.vtable().refcount)(self.ptr)
}
}
impl Clone for JsString {
#[inline]
fn clone(&self) -> Self {
(self.vtable().clone)(self.ptr)
}
}
impl Default for JsString {
#[inline]
fn default() -> Self {
StaticJsStrings::EMPTY_STRING
}
}
impl Drop for JsString {
#[inline]
fn drop(&mut self) {
(self.vtable().drop)(self.ptr);
}
}
impl std::fmt::Debug for JsString {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("JsString")
.field(&self.display_escaped().to_string())
.finish()
}
}
impl Eq for JsString {}
macro_rules! impl_from_number_for_js_string {
($($module: ident => $($ty:ty),+)+) => {
$(
$(
impl From<$ty> for JsString {
#[inline]
fn from(value: $ty) -> Self {
JsString::from_slice_skip_interning(JsStr::latin1(
$module::Buffer::new().format(value).as_bytes(),
))
}
}
)+
)+
};
}
impl_from_number_for_js_string!(
itoa => i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, isize, usize
ryu_js => f32, f64
);
impl From<&[u16]> for JsString {
#[inline]
fn from(s: &[u16]) -> Self {
JsString::from_js_str(JsStr::utf16(s))
}
}
impl From<&str> for JsString {
#[inline]
fn from(s: &str) -> Self {
if s.is_ascii() {
let js_str = JsStr::latin1(s.as_bytes());
return StaticJsStrings::get_string(&js_str)
.unwrap_or_else(|| JsString::from_slice_skip_interning(js_str));
}
if s.chars().all(|c| c as u32 <= 0xFF) {
let bytes: Vec<u8> = s.chars().map(|c| c as u8).collect();
let js_str = JsStr::latin1(&bytes);
return StaticJsStrings::get_string(&js_str)
.unwrap_or_else(|| JsString::from_slice_skip_interning(js_str));
}
let s = s.encode_utf16().collect::<Vec<_>>();
JsString::from_slice_skip_interning(JsStr::utf16(&s[..]))
}
}
impl From<JsStr<'_>> for JsString {
#[inline]
fn from(value: JsStr<'_>) -> Self {
StaticJsStrings::get_string(&value)
.unwrap_or_else(|| JsString::from_slice_skip_interning(value))
}
}
impl From<&[JsString]> for JsString {
#[inline]
fn from(value: &[JsString]) -> Self {
Self::concat_array(&value.iter().map(Self::as_str).collect::<Vec<_>>()[..])
}
}
impl<const N: usize> From<&[JsString; N]> for JsString {
#[inline]
fn from(value: &[JsString; N]) -> Self {
Self::concat_array(&value.iter().map(Self::as_str).collect::<Vec<_>>()[..])
}
}
impl From<String> for JsString {
#[inline]
fn from(s: String) -> Self {
Self::from(s.as_str())
}
}
impl<'a> From<Cow<'a, str>> for JsString {
#[inline]
fn from(s: Cow<'a, str>) -> Self {
match s {
Cow::Borrowed(s) => s.into(),
Cow::Owned(s) => s.into(),
}
}
}
impl<const N: usize> From<&[u16; N]> for JsString {
#[inline]
fn from(s: &[u16; N]) -> Self {
Self::from(&s[..])
}
}
impl Hash for JsString {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
impl PartialOrd for JsStr<'_> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for JsString {
#[inline]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_str().cmp(&other.as_str())
}
}
impl PartialEq for JsString {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}
impl PartialEq<JsString> for [u16] {
#[inline]
fn eq(&self, other: &JsString) -> bool {
if self.len() != other.len() {
return false;
}
for (x, y) in self.iter().copied().zip(other.iter()) {
if x != y {
return false;
}
}
true
}
}
impl<const N: usize> PartialEq<JsString> for [u16; N] {
#[inline]
fn eq(&self, other: &JsString) -> bool {
self[..] == *other
}
}
impl PartialEq<[u16]> for JsString {
#[inline]
fn eq(&self, other: &[u16]) -> bool {
other == self
}
}
impl<const N: usize> PartialEq<[u16; N]> for JsString {
#[inline]
fn eq(&self, other: &[u16; N]) -> bool {
*self == other[..]
}
}
impl PartialEq<str> for JsString {
#[inline]
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for JsString {
#[inline]
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<JsString> for str {
#[inline]
fn eq(&self, other: &JsString) -> bool {
other == self
}
}
impl PartialEq<JsStr<'_>> for JsString {
#[inline]
fn eq(&self, other: &JsStr<'_>) -> bool {
self.as_str() == *other
}
}
impl PartialEq<JsString> for JsStr<'_> {
#[inline]
fn eq(&self, other: &JsString) -> bool {
other == self
}
}
impl PartialOrd for JsString {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl FromStr for JsString {
type Err = Infallible;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from(s))
}
}
pub trait JsStringSliceIndex {
fn get(self, str: &JsString) -> Option<JsString>;
}
macro_rules! impl_js_string_slice_index {
($($type:ty),+ $(,)?) => {
$(
impl JsStringSliceIndex for $type {
fn get(self, str: &JsString) -> Option<JsString> {
let start = match std::ops::RangeBounds::<usize>::start_bound(&self) {
std::ops::Bound::Included(start) => *start,
std::ops::Bound::Excluded(start) => *start + 1,
std::ops::Bound::Unbounded => 0,
};
let end = match std::ops::RangeBounds::<usize>::end_bound(&self) {
std::ops::Bound::Included(end) => *end + 1,
std::ops::Bound::Excluded(end) => *end,
std::ops::Bound::Unbounded => str.len(),
};
if end > str.len() || start > end {
None
} else {
Some(unsafe { JsString::slice_unchecked(str, start, end) })
}
}
}
)+
};
}
impl_js_string_slice_index!(
std::ops::Range<usize>,
std::ops::RangeInclusive<usize>,
std::ops::RangeTo<usize>,
std::ops::RangeToInclusive<usize>,
std::ops::RangeFrom<usize>,
std::ops::RangeFull,
);