Skip to main content

boa_string/
lib.rs

1//! A Latin1 or UTF-16 encoded, reference counted, immutable string.
2
3// Required per unsafe code standards to ensure every unsafe usage is properly documented.
4// - `unsafe_op_in_unsafe_fn` will be warn-by-default in edition 2024:
5//   https://github.com/rust-lang/rust/issues/71668#issuecomment-1189396860
6// - `undocumented_unsafe_blocks` and `missing_safety_doc` requires a `Safety:` section in the
7//   comment or doc of the unsafe block or function, respectively.
8#![deny(
9    unsafe_op_in_unsafe_fn,
10    clippy::undocumented_unsafe_blocks,
11    clippy::missing_safety_doc
12)]
13#![allow(clippy::module_name_repetitions)]
14
15mod builder;
16mod code_point;
17mod common;
18mod display;
19mod iter;
20mod str;
21mod r#type;
22mod vtable;
23
24#[cfg(test)]
25mod tests;
26
27use self::iter::Windows;
28use crate::display::{JsStrDisplayEscaped, JsStrDisplayLossy, JsStringDebugInfo};
29use crate::iter::CodePointsIter;
30use crate::r#type::{Latin1, Utf16};
31pub use crate::vtable::StaticString;
32use crate::vtable::{SequenceString, SliceString};
33#[doc(inline)]
34pub use crate::{
35    builder::{CommonJsStringBuilder, Latin1JsStringBuilder, Utf16JsStringBuilder},
36    code_point::CodePoint,
37    common::StaticJsStrings,
38    iter::Iter,
39    str::{JsStr, JsStrVariant},
40};
41use std::marker::PhantomData;
42use std::{borrow::Cow, mem::ManuallyDrop};
43use std::{
44    convert::Infallible,
45    hash::{Hash, Hasher},
46    ptr::{self, NonNull},
47    str::FromStr,
48};
49use vtable::JsStringVTable;
50
51fn alloc_overflow() -> ! {
52    panic!("detected overflow during string allocation")
53}
54
55/// Helper function to check if a `char` is trimmable.
56pub(crate) const fn is_trimmable_whitespace(c: char) -> bool {
57    // The rust implementation of `trim` does not regard the same characters whitespace as
58    // ecma standard does.
59    //
60    // Rust uses \p{White_Space} by default, which also includes:
61    // `\u{0085}' (next line)
62    // And does not include:
63    // '\u{FEFF}' (zero width non-breaking space)
64    // Explicit whitespace: https://tc39.es/ecma262/#sec-white-space
65    matches!(
66        c,
67        '\u{0009}' | '\u{000B}' | '\u{000C}' | '\u{0020}' | '\u{00A0}' | '\u{FEFF}' |
68    // Unicode Space_Separator category
69    '\u{1680}' | '\u{2000}'
70            ..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' |
71    // Line terminators: https://tc39.es/ecma262/#sec-line-terminators
72    '\u{000A}' | '\u{000D}' | '\u{2028}' | '\u{2029}'
73    )
74}
75
76/// Helper function to check if a `u8` latin1 character is trimmable.
77pub(crate) const fn is_trimmable_whitespace_latin1(c: u8) -> bool {
78    // The rust implementation of `trim` does not regard the same characters whitespace as
79    // ecma standard does.
80    //
81    // Rust uses \p{White_Space} by default, which also includes:
82    // `\u{0085}' (next line)
83    // And does not include:
84    // '\u{FEFF}' (zero width non-breaking space)
85    // Explicit whitespace: https://tc39.es/ecma262/#sec-white-space
86    matches!(
87        c,
88        0x09 | 0x0B | 0x0C | 0x20 | 0xA0 |
89        // Line terminators: https://tc39.es/ecma262/#sec-line-terminators
90        0x0A | 0x0D
91    )
92}
93
94/// Opaque type of a raw string pointer.
95#[allow(missing_copy_implementations, missing_debug_implementations)]
96pub struct RawJsString {
97    // Make this non-send, non-sync, invariant and unconstructable.
98    phantom_data: PhantomData<*mut ()>,
99}
100
101/// Strings can be represented internally by multiple kinds. This is used to identify
102/// the storage kind of string.
103#[derive(Debug, Clone, Copy, Eq, PartialEq)]
104#[repr(u8)]
105pub(crate) enum JsStringKind {
106    /// A sequential memory slice of Latin1 bytes. See [`SequenceString`].
107    Latin1Sequence = 0,
108
109    /// A sequential memory slice of UTF-16 code units. See [`SequenceString`].
110    Utf16Sequence = 1,
111
112    /// A slice of an existing string. See [`SliceString`].
113    Slice = 2,
114
115    /// A static string that is valid for `'static` lifetime.
116    Static = 3,
117}
118
119/// A Latin1 or UTF-16–encoded, reference counted, immutable string.
120///
121/// This is pretty similar to a <code>[Rc][std::rc::Rc]\<[\[u16\]][slice]\></code>, but without the
122/// length metadata associated with the `Rc` fat pointer. Instead, the length of every string is
123/// stored on the heap, along with its reference counter and its data.
124///
125/// The string can be latin1 (stored as a byte for space efficiency) or U16 encoding.
126///
127/// We define some commonly used string constants in an interner. For these strings, we don't allocate
128/// memory on the heap to reduce the overhead of memory allocation and reference counting.
129///
130/// # Internal representation
131///
132/// The `ptr` field always points to a structure whose first field is a `JsStringVTable`.
133/// This enables uniform vtable dispatch for all string operations without branching.
134///
135/// Because we ensure this invariant at every construction, we can directly point to this
136/// type to allow for better optimization (and simpler code).
137#[allow(clippy::module_name_repetitions)]
138pub struct JsString {
139    /// Pointer to the string data. Always points to a struct whose first field is
140    /// `JsStringVTable`.
141    ptr: NonNull<JsStringVTable>,
142}
143
144// `JsString` should always be thin-pointer sized.
145static_assertions::assert_eq_size!(JsString, *const ());
146
147impl<'a> From<&'a JsString> for JsStr<'a> {
148    #[inline]
149    fn from(value: &'a JsString) -> Self {
150        value.as_str()
151    }
152}
153
154impl<'a> IntoIterator for &'a JsString {
155    type Item = u16;
156    type IntoIter = Iter<'a>;
157
158    #[inline]
159    fn into_iter(self) -> Self::IntoIter {
160        self.iter()
161    }
162}
163
164impl JsString {
165    /// Create an iterator over the [`JsString`].
166    #[inline]
167    #[must_use]
168    pub fn iter(&self) -> Iter<'_> {
169        self.as_str().iter()
170    }
171
172    /// Create an iterator over overlapping subslices of length size.
173    #[inline]
174    #[must_use]
175    pub fn windows(&self, size: usize) -> Windows<'_> {
176        self.as_str().windows(size)
177    }
178
179    /// Decodes a [`JsString`] into a [`String`], replacing invalid data with its escaped representation
180    /// in 4 digit hexadecimal.
181    #[inline]
182    #[must_use]
183    pub fn to_std_string_escaped(&self) -> String {
184        self.display_escaped().to_string()
185    }
186
187    /// Decodes a [`JsString`] into a [`String`], replacing invalid data with the
188    /// replacement character U+FFFD.
189    #[inline]
190    #[must_use]
191    pub fn to_std_string_lossy(&self) -> String {
192        self.display_lossy().to_string()
193    }
194
195    /// Decodes a [`JsString`] into a [`String`], returning an error if the string contains unpaired
196    /// surrogates.
197    ///
198    /// # Errors
199    ///
200    /// [`FromUtf16Error`][std::string::FromUtf16Error] if it contains any invalid data.
201    #[inline]
202    pub fn to_std_string(&self) -> Result<String, std::string::FromUtf16Error> {
203        self.as_str().to_std_string()
204    }
205
206    /// Decodes a [`JsString`] into an iterator of [`Result<String, u16>`], returning surrogates as
207    /// errors.
208    #[inline]
209    #[allow(clippy::missing_panics_doc)]
210    pub fn to_std_string_with_surrogates(
211        &self,
212    ) -> impl Iterator<Item = Result<String, u16>> + use<'_> {
213        let mut iter = self.code_points().peekable();
214
215        std::iter::from_fn(move || {
216            let cp = iter.next()?;
217            let char = match cp {
218                CodePoint::Unicode(c) => c,
219                CodePoint::UnpairedSurrogate(surr) => return Some(Err(surr)),
220            };
221
222            let mut string = String::from(char);
223
224            while let Some(cp) = iter.peek().and_then(|cp| match cp {
225                CodePoint::Unicode(c) => Some(*c),
226                CodePoint::UnpairedSurrogate(_) => None,
227            }) {
228                string.push(cp);
229                iter.next().expect("iter.peek() ensures that next is Some");
230            }
231
232            Some(Ok(string))
233        })
234    }
235
236    /// Maps the valid segments of an UTF16 string and leaves the unpaired surrogates unchanged.
237    #[inline]
238    #[must_use]
239    pub fn map_valid_segments<F>(&self, mut f: F) -> Self
240    where
241        F: FnMut(String) -> String,
242    {
243        let mut text = Vec::new();
244
245        for part in self.to_std_string_with_surrogates() {
246            match part {
247                Ok(string) => text.extend(f(string).encode_utf16()),
248                Err(surr) => text.push(surr),
249            }
250        }
251
252        Self::from(&text[..])
253    }
254
255    /// Gets an iterator of all the Unicode codepoints of a [`JsString`].
256    #[inline]
257    #[must_use]
258    pub fn code_points(&self) -> CodePointsIter<'_> {
259        (self.vtable().code_points)(self.ptr)
260    }
261
262    /// Get the variant of this string.
263    #[inline]
264    #[must_use]
265    pub fn variant(&self) -> JsStrVariant<'_> {
266        self.as_str().variant()
267    }
268
269    /// Abstract operation `StringIndexOf ( string, searchValue, fromIndex )`
270    ///
271    /// Note: Instead of returning an isize with `-1` as the "not found" value, we make use of the
272    /// type system and return <code>[Option]\<usize\></code> with [`None`] as the "not found" value.
273    ///
274    /// More information:
275    ///  - [ECMAScript reference][spec]
276    ///
277    /// [spec]: https://tc39.es/ecma262/#sec-stringindexof
278    #[inline]
279    #[must_use]
280    pub fn index_of(&self, search_value: JsStr<'_>, from_index: usize) -> Option<usize> {
281        self.as_str().index_of(search_value, from_index)
282    }
283
284    /// Abstract operation `CodePointAt( string, position )`.
285    ///
286    /// The abstract operation `CodePointAt` takes arguments `string` (a String) and `position` (a
287    /// non-negative integer) and returns a Record with fields `[[CodePoint]]` (a code point),
288    /// `[[CodeUnitCount]]` (a positive integer), and `[[IsUnpairedSurrogate]]` (a Boolean). It
289    /// interprets string as a sequence of UTF-16 encoded code points, as described in 6.1.4, and reads
290    /// from it a single code point starting with the code unit at index `position`.
291    ///
292    /// More information:
293    ///  - [ECMAScript reference][spec]
294    ///
295    /// [spec]: https://tc39.es/ecma262/#sec-codepointat
296    ///
297    /// # Panics
298    ///
299    /// If `position` is smaller than size of string.
300    #[inline]
301    #[must_use]
302    pub fn code_point_at(&self, position: usize) -> CodePoint {
303        self.as_str().code_point_at(position)
304    }
305
306    /// Abstract operation `StringToNumber ( str )`
307    ///
308    /// More information:
309    /// - [ECMAScript reference][spec]
310    ///
311    /// [spec]: https://tc39.es/ecma262/#sec-stringtonumber
312    #[inline]
313    #[must_use]
314    pub fn to_number(&self) -> f64 {
315        self.as_str().to_number()
316    }
317
318    /// Get the length of the [`JsString`].
319    #[inline]
320    #[must_use]
321    pub fn len(&self) -> usize {
322        self.vtable().len
323    }
324
325    /// Return true if the [`JsString`] is empty.
326    #[inline]
327    #[must_use]
328    pub fn is_empty(&self) -> bool {
329        self.len() == 0
330    }
331
332    /// Convert the [`JsString`] into a [`Vec<U16>`].
333    #[inline]
334    #[must_use]
335    pub fn to_vec(&self) -> Vec<u16> {
336        self.as_str().to_vec()
337    }
338
339    /// Check if the [`JsString`] contains a byte.
340    #[inline]
341    #[must_use]
342    pub fn contains(&self, element: u8) -> bool {
343        self.as_str().contains(element)
344    }
345
346    /// Trim whitespace from the start and end of the [`JsString`].
347    #[inline]
348    #[must_use]
349    pub fn trim(&self) -> JsString {
350        // Calculate both bounds directly to avoid intermediate allocations.
351        let (start, end) = match self.variant() {
352            JsStrVariant::Latin1(v) => {
353                let Some(start) = v.iter().position(|c| !is_trimmable_whitespace_latin1(*c)) else {
354                    return StaticJsStrings::EMPTY_STRING;
355                };
356                let end = v
357                    .iter()
358                    .rposition(|c| !is_trimmable_whitespace_latin1(*c))
359                    .unwrap_or(start);
360                (start, end)
361            }
362            JsStrVariant::Utf16(v) => {
363                let Some(start) = v.iter().copied().position(|r| {
364                    !char::from_u32(u32::from(r)).is_some_and(is_trimmable_whitespace)
365                }) else {
366                    return StaticJsStrings::EMPTY_STRING;
367                };
368                let end = v
369                    .iter()
370                    .copied()
371                    .rposition(|r| {
372                        !char::from_u32(u32::from(r)).is_some_and(is_trimmable_whitespace)
373                    })
374                    .unwrap_or(start);
375                (start, end)
376            }
377        };
378
379        // SAFETY: `position(...)` and `rposition(...)` cannot exceed the length of the string.
380        unsafe { Self::slice_unchecked(self, start, end + 1) }
381    }
382
383    /// Trim whitespace from the start of the [`JsString`].
384    #[inline]
385    #[must_use]
386    pub fn trim_start(&self) -> JsString {
387        let Some(start) = (match self.variant() {
388            JsStrVariant::Latin1(v) => v.iter().position(|c| !is_trimmable_whitespace_latin1(*c)),
389            JsStrVariant::Utf16(v) => v
390                .iter()
391                .copied()
392                .position(|r| !char::from_u32(u32::from(r)).is_some_and(is_trimmable_whitespace)),
393        }) else {
394            return StaticJsStrings::EMPTY_STRING;
395        };
396
397        // SAFETY: `position(...)` cannot exceed the length of the string.
398        unsafe { Self::slice_unchecked(self, start, self.len()) }
399    }
400
401    /// Trim whitespace from the end of the [`JsString`].
402    #[inline]
403    #[must_use]
404    pub fn trim_end(&self) -> JsString {
405        let Some(end) = (match self.variant() {
406            JsStrVariant::Latin1(v) => v.iter().rposition(|c| !is_trimmable_whitespace_latin1(*c)),
407            JsStrVariant::Utf16(v) => v
408                .iter()
409                .copied()
410                .rposition(|r| !char::from_u32(u32::from(r)).is_some_and(is_trimmable_whitespace)),
411        }) else {
412            return StaticJsStrings::EMPTY_STRING;
413        };
414
415        // SAFETY: `rposition(...)` cannot exceed the length of the string. `end` is the first
416        //         character that is not trimmable, therefore we need to add 1 to it.
417        unsafe { Self::slice_unchecked(self, 0, end + 1) }
418    }
419
420    /// Returns true if needle is a prefix of the [`JsStr`].
421    #[inline]
422    #[must_use]
423    // We check the size, so this should never panic.
424    #[allow(clippy::missing_panics_doc)]
425    pub fn starts_with(&self, needle: JsStr<'_>) -> bool {
426        self.as_str().starts_with(needle)
427    }
428
429    /// Returns `true` if `needle` is a suffix of the [`JsStr`].
430    #[inline]
431    #[must_use]
432    // We check the size, so this should never panic.
433    #[allow(clippy::missing_panics_doc)]
434    pub fn ends_with(&self, needle: JsStr<'_>) -> bool {
435        self.as_str().ends_with(needle)
436    }
437
438    /// Get the `u16` code unit at index. This does not parse any characters if there
439    /// are pairs, it is simply the index of the `u16` elements.
440    #[inline]
441    #[must_use]
442    pub fn code_unit_at(&self, index: usize) -> Option<u16> {
443        self.as_str().get(index)
444    }
445
446    /// Get the element at the given index, or [`None`] if the index is out of range.
447    #[inline]
448    #[must_use]
449    pub fn get<I>(&self, index: I) -> Option<JsString>
450    where
451        I: JsStringSliceIndex,
452    {
453        index.get(self)
454    }
455
456    /// Get the element at the given index, or panic.
457    ///
458    /// # Panics
459    /// If the index returns `None`, this will panic.
460    #[inline]
461    #[must_use]
462    pub fn get_expect<I>(&self, index: I) -> JsString
463    where
464        I: JsStringSliceIndex,
465    {
466        index.get(self).expect("Unexpected get()")
467    }
468
469    /// Gets a displayable escaped string. This may be faster and has fewer
470    /// allocations than `format!("{}", str.to_string_escaped())` when
471    /// displaying.
472    #[inline]
473    #[must_use]
474    pub fn display_escaped(&self) -> JsStrDisplayEscaped<'_> {
475        JsStrDisplayEscaped::from(self)
476    }
477
478    /// Gets a displayable lossy string. This may be faster and has fewer
479    /// allocations than `format!("{}", str.to_string_lossy())` when displaying.
480    #[inline]
481    #[must_use]
482    pub fn display_lossy(&self) -> JsStrDisplayLossy<'_> {
483        self.as_str().display_lossy()
484    }
485
486    /// Get a debug displayable info and metadata for this string.
487    #[inline]
488    #[must_use]
489    pub fn debug_info(&self) -> JsStringDebugInfo<'_> {
490        self.into()
491    }
492
493    /// Consumes the [`JsString`], returning the internal pointer.
494    ///
495    /// To avoid a memory leak the pointer must be converted back to a `JsString` using
496    /// [`JsString::from_raw`].
497    #[inline]
498    #[must_use]
499    pub fn into_raw(self) -> NonNull<RawJsString> {
500        ManuallyDrop::new(self).ptr.cast()
501    }
502
503    /// Constructs a `JsString` from the internal pointer.
504    ///
505    /// The raw pointer must have been previously returned by a call to
506    /// [`JsString::into_raw`].
507    ///
508    /// # Safety
509    ///
510    /// This function is unsafe because improper use may lead to memory unsafety,
511    /// even if the returned `JsString` is never accessed.
512    #[inline]
513    #[must_use]
514    pub const unsafe fn from_raw(ptr: NonNull<RawJsString>) -> Self {
515        Self { ptr: ptr.cast() }
516    }
517
518    /// Constructs a `JsString` from a reference to a `VTable`.
519    ///
520    /// # Safety
521    ///
522    /// This function is unsafe because improper use may lead to memory unsafety,
523    /// even if the returned `JsString` is never accessed.
524    #[inline]
525    #[must_use]
526    pub(crate) const unsafe fn from_ptr(ptr: NonNull<JsStringVTable>) -> Self {
527        Self { ptr }
528    }
529}
530
531// `&JsStr<'static>` must always be aligned so it can be tagged.
532static_assertions::const_assert!(align_of::<*const JsStr<'static>>() >= 2);
533
534/// Dealing with inner types.
535impl JsString {
536    /// Check if this is a static string.
537    #[inline]
538    #[must_use]
539    pub fn is_static(&self) -> bool {
540        // Check the vtable kind tag
541        self.vtable().kind == JsStringKind::Static
542    }
543
544    /// Get the vtable for this string.
545    #[inline]
546    #[must_use]
547    const fn vtable(&self) -> &JsStringVTable {
548        // SAFETY: All JsString variants have vtable as the first field (embedded directly).
549        unsafe { self.ptr.as_ref() }
550    }
551
552    /// Create a [`JsString`] from a [`StaticString`] instance. This is assumed that the
553    /// static string referenced is available for the duration of the `JsString` instance
554    /// returned.
555    #[inline]
556    #[must_use]
557    pub const fn from_static(str: &'static StaticString) -> Self {
558        Self {
559            ptr: NonNull::from_ref(str).cast(),
560        }
561    }
562
563    /// Create a [`JsString`] from an existing `JsString` and start, end
564    /// range. `end` is 1 past the last character (or `== data.len()`
565    /// for the last character).
566    ///
567    /// # Safety
568    /// It is the responsibility of the caller to ensure:
569    ///   - `start` <= `end`. If `start` == `end`, the string is empty.
570    ///   - `end` <= `data.len()`.
571    #[inline]
572    #[must_use]
573    pub unsafe fn slice_unchecked(data: &JsString, start: usize, end: usize) -> Self {
574        // Safety: invariant stated by this whole function.
575        let slice = Box::new(unsafe { SliceString::new(data, start, end) });
576
577        Self {
578            ptr: NonNull::from(Box::leak(slice)).cast(),
579        }
580    }
581
582    /// Create a [`JsString`] from an existing `JsString` and start, end
583    /// range. Returns None if the start/end is invalid.
584    #[inline]
585    #[must_use]
586    pub fn slice(&self, p1: usize, mut p2: usize) -> JsString {
587        if p2 > self.len() {
588            p2 = self.len();
589        }
590        if p1 >= p2 {
591            StaticJsStrings::EMPTY_STRING
592        } else {
593            // SAFETY: We just checked the conditions.
594            unsafe { Self::slice_unchecked(self, p1, p2) }
595        }
596    }
597
598    /// Get the kind of this string (for debugging/introspection).
599    #[inline]
600    #[must_use]
601    pub(crate) fn kind(&self) -> JsStringKind {
602        self.vtable().kind
603    }
604
605    /// Get the inner pointer as a reference of type T.
606    ///
607    /// # Safety
608    /// This should only be used when the inner type has been validated via `kind()`.
609    /// Using an unvalidated inner type is undefined behaviour.
610    #[inline]
611    pub(crate) unsafe fn as_inner<T>(&self) -> &T {
612        // SAFETY: Caller must ensure the type matches.
613        unsafe { self.ptr.cast::<T>().as_ref() }
614    }
615}
616
617impl JsString {
618    /// Obtains the underlying [`&[u16]`][slice] slice of a [`JsString`]
619    #[inline]
620    #[must_use]
621    pub fn as_str(&self) -> JsStr<'_> {
622        (self.vtable().as_str)(self.ptr)
623    }
624
625    /// Creates a new [`JsString`] from the concatenation of `x` and `y`.
626    #[inline]
627    #[must_use]
628    pub fn concat(x: JsStr<'_>, y: JsStr<'_>) -> Self {
629        Self::concat_array(&[x, y])
630    }
631
632    /// Creates a new [`JsString`] from the concatenation of every element of
633    /// `strings`.
634    #[inline]
635    #[must_use]
636    pub fn concat_array(strings: &[JsStr<'_>]) -> Self {
637        let mut latin1_encoding = true;
638        let mut full_count = 0usize;
639        for string in strings {
640            let Some(sum) = full_count.checked_add(string.len()) else {
641                alloc_overflow()
642            };
643            if !string.is_latin1() {
644                latin1_encoding = false;
645            }
646            full_count = sum;
647        }
648
649        let (ptr, data_offset) = if latin1_encoding {
650            let p = SequenceString::<Latin1>::allocate(full_count);
651            (p.cast::<u8>(), size_of::<SequenceString<Latin1>>())
652        } else {
653            let p = SequenceString::<Utf16>::allocate(full_count);
654            (p.cast::<u8>(), size_of::<SequenceString<Utf16>>())
655        };
656
657        let string = {
658            // SAFETY: `allocate_*_seq` guarantees that `ptr` is a valid pointer to a sequence string.
659            let mut data = unsafe {
660                let seq_ptr = ptr.as_ptr();
661                seq_ptr.add(data_offset)
662            };
663            for &string in strings {
664                // SAFETY:
665                // The sum of all `count` for each `string` equals `full_count`, and since we're
666                // iteratively writing each of them to `data`, `copy_non_overlapping` always stays
667                // in-bounds for `count` reads of each string and `full_count` writes to `data`.
668                //
669                // Each `string` must be properly aligned to be a valid slice, and `data` must be
670                // properly aligned by `allocate_seq`.
671                //
672                // `allocate_seq` must return a valid pointer to newly allocated memory, meaning
673                // `ptr` and all `string`s should never overlap.
674                unsafe {
675                    // NOTE: The alignment is checked when we allocate the array.
676                    #[allow(clippy::cast_ptr_alignment)]
677                    match (latin1_encoding, string.variant()) {
678                        (true, JsStrVariant::Latin1(s)) => {
679                            let count = s.len();
680                            ptr::copy_nonoverlapping(s.as_ptr(), data.cast::<u8>(), count);
681                            data = data.cast::<u8>().add(count).cast::<u8>();
682                        }
683                        (false, JsStrVariant::Latin1(s)) => {
684                            let count = s.len();
685                            for (i, byte) in s.iter().enumerate() {
686                                *data.cast::<u16>().add(i) = u16::from(*byte);
687                            }
688                            data = data.cast::<u16>().add(count).cast::<u8>();
689                        }
690                        (false, JsStrVariant::Utf16(s)) => {
691                            let count = s.len();
692                            ptr::copy_nonoverlapping(s.as_ptr(), data.cast::<u16>(), count);
693                            data = data.cast::<u16>().add(count).cast::<u8>();
694                        }
695                        (true, JsStrVariant::Utf16(_)) => {
696                            unreachable!("Already checked that it's latin1 encoding")
697                        }
698                    }
699                }
700            }
701
702            Self { ptr: ptr.cast() }
703        };
704
705        StaticJsStrings::get_string(&string.as_str()).unwrap_or(string)
706    }
707
708    /// Creates a new [`JsString`] from `data`, without checking if the string is in the interner.
709    fn from_slice_skip_interning(string: JsStr<'_>) -> Self {
710        let count = string.len();
711
712        // SAFETY:
713        // - We read `count = data.len()` elements from `data`, which is within the bounds of the slice.
714        // - `allocate_*_seq` must allocate at least `count` elements, which allows us to safely
715        //   write at least `count` elements.
716        // - `allocate_*_seq` should already take care of the alignment of `ptr`, and `data` must be
717        //   aligned to be a valid slice.
718        // - `allocate_*_seq` must return a valid pointer to newly allocated memory, meaning `ptr`
719        //   and `data` should never overlap.
720        unsafe {
721            // NOTE: The alignment is checked when we allocate the array.
722            #[allow(clippy::cast_ptr_alignment)]
723            match string.variant() {
724                JsStrVariant::Latin1(s) => {
725                    let ptr = SequenceString::<Latin1>::allocate(count);
726                    let data = (&raw mut (*ptr.as_ptr()).data)
727                        .cast::<<Latin1 as r#type::StringType>::Byte>();
728                    ptr::copy_nonoverlapping(s.as_ptr(), data, count);
729                    Self { ptr: ptr.cast() }
730                }
731                JsStrVariant::Utf16(s) => {
732                    let ptr = SequenceString::<Utf16>::allocate(count);
733                    let data = (&raw mut (*ptr.as_ptr()).data)
734                        .cast::<<Utf16 as r#type::StringType>::Byte>();
735                    ptr::copy_nonoverlapping(s.as_ptr(), data, count);
736                    Self { ptr: ptr.cast() }
737                }
738            }
739        }
740    }
741
742    /// Creates a new [`JsString`] from `data`.
743    fn from_js_str(string: JsStr<'_>) -> Self {
744        if let Some(s) = StaticJsStrings::get_string(&string) {
745            return s;
746        }
747        Self::from_slice_skip_interning(string)
748    }
749
750    /// Gets the number of `JsString`s which point to this allocation.
751    #[inline]
752    #[must_use]
753    pub fn refcount(&self) -> Option<usize> {
754        (self.vtable().refcount)(self.ptr)
755    }
756}
757
758impl Clone for JsString {
759    #[inline]
760    fn clone(&self) -> Self {
761        (self.vtable().clone)(self.ptr)
762    }
763}
764
765impl Default for JsString {
766    #[inline]
767    fn default() -> Self {
768        StaticJsStrings::EMPTY_STRING
769    }
770}
771
772impl Drop for JsString {
773    #[inline]
774    fn drop(&mut self) {
775        (self.vtable().drop)(self.ptr);
776    }
777}
778
779impl std::fmt::Debug for JsString {
780    #[inline]
781    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
782        f.debug_tuple("JsString")
783            .field(&self.display_escaped().to_string())
784            .finish()
785    }
786}
787
788impl Eq for JsString {}
789
790macro_rules! impl_from_number_for_js_string {
791    ($($module: ident => $($ty:ty),+)+) => {
792        $(
793            $(
794                impl From<$ty> for JsString {
795                    #[inline]
796                    fn from(value: $ty) -> Self {
797                        JsString::from_slice_skip_interning(JsStr::latin1(
798                            $module::Buffer::new().format(value).as_bytes(),
799                        ))
800                    }
801                }
802            )+
803        )+
804    };
805}
806
807impl_from_number_for_js_string!(
808    itoa => i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, isize, usize
809    ryu_js => f32, f64
810);
811
812impl From<&[u16]> for JsString {
813    #[inline]
814    fn from(s: &[u16]) -> Self {
815        JsString::from_js_str(JsStr::utf16(s))
816    }
817}
818
819impl From<&str> for JsString {
820    #[inline]
821    fn from(s: &str) -> Self {
822        if s.is_ascii() {
823            let js_str = JsStr::latin1(s.as_bytes());
824            return StaticJsStrings::get_string(&js_str)
825                .unwrap_or_else(|| JsString::from_slice_skip_interning(js_str));
826        }
827        // Non-ASCII but still Latin1-encodable (U+0080..=U+00FF): chars map 1-to-1 to u8.
828        if s.chars().all(|c| c as u32 <= 0xFF) {
829            let bytes: Vec<u8> = s.chars().map(|c| c as u8).collect();
830            let js_str = JsStr::latin1(&bytes);
831            return StaticJsStrings::get_string(&js_str)
832                .unwrap_or_else(|| JsString::from_slice_skip_interning(js_str));
833        }
834        let s = s.encode_utf16().collect::<Vec<_>>();
835        JsString::from_slice_skip_interning(JsStr::utf16(&s[..]))
836    }
837}
838
839impl From<JsStr<'_>> for JsString {
840    #[inline]
841    fn from(value: JsStr<'_>) -> Self {
842        StaticJsStrings::get_string(&value)
843            .unwrap_or_else(|| JsString::from_slice_skip_interning(value))
844    }
845}
846
847impl From<&[JsString]> for JsString {
848    #[inline]
849    fn from(value: &[JsString]) -> Self {
850        Self::concat_array(&value.iter().map(Self::as_str).collect::<Vec<_>>()[..])
851    }
852}
853
854impl<const N: usize> From<&[JsString; N]> for JsString {
855    #[inline]
856    fn from(value: &[JsString; N]) -> Self {
857        Self::concat_array(&value.iter().map(Self::as_str).collect::<Vec<_>>()[..])
858    }
859}
860
861impl From<String> for JsString {
862    #[inline]
863    fn from(s: String) -> Self {
864        Self::from(s.as_str())
865    }
866}
867
868impl<'a> From<Cow<'a, str>> for JsString {
869    #[inline]
870    fn from(s: Cow<'a, str>) -> Self {
871        match s {
872            Cow::Borrowed(s) => s.into(),
873            Cow::Owned(s) => s.into(),
874        }
875    }
876}
877
878impl<const N: usize> From<&[u16; N]> for JsString {
879    #[inline]
880    fn from(s: &[u16; N]) -> Self {
881        Self::from(&s[..])
882    }
883}
884
885impl Hash for JsString {
886    #[inline]
887    fn hash<H: Hasher>(&self, state: &mut H) {
888        self.as_str().hash(state);
889    }
890}
891
892impl PartialOrd for JsStr<'_> {
893    #[inline]
894    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
895        Some(self.cmp(other))
896    }
897}
898
899impl Ord for JsString {
900    #[inline]
901    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
902        self.as_str().cmp(&other.as_str())
903    }
904}
905
906impl PartialEq for JsString {
907    #[inline]
908    fn eq(&self, other: &Self) -> bool {
909        self.as_str() == other.as_str()
910    }
911}
912
913impl PartialEq<JsString> for [u16] {
914    #[inline]
915    fn eq(&self, other: &JsString) -> bool {
916        if self.len() != other.len() {
917            return false;
918        }
919        for (x, y) in self.iter().copied().zip(other.iter()) {
920            if x != y {
921                return false;
922            }
923        }
924        true
925    }
926}
927
928impl<const N: usize> PartialEq<JsString> for [u16; N] {
929    #[inline]
930    fn eq(&self, other: &JsString) -> bool {
931        self[..] == *other
932    }
933}
934
935impl PartialEq<[u16]> for JsString {
936    #[inline]
937    fn eq(&self, other: &[u16]) -> bool {
938        other == self
939    }
940}
941
942impl<const N: usize> PartialEq<[u16; N]> for JsString {
943    #[inline]
944    fn eq(&self, other: &[u16; N]) -> bool {
945        *self == other[..]
946    }
947}
948
949impl PartialEq<str> for JsString {
950    #[inline]
951    fn eq(&self, other: &str) -> bool {
952        self.as_str() == other
953    }
954}
955
956impl PartialEq<&str> for JsString {
957    #[inline]
958    fn eq(&self, other: &&str) -> bool {
959        self.as_str() == *other
960    }
961}
962
963impl PartialEq<JsString> for str {
964    #[inline]
965    fn eq(&self, other: &JsString) -> bool {
966        other == self
967    }
968}
969
970impl PartialEq<JsStr<'_>> for JsString {
971    #[inline]
972    fn eq(&self, other: &JsStr<'_>) -> bool {
973        self.as_str() == *other
974    }
975}
976
977impl PartialEq<JsString> for JsStr<'_> {
978    #[inline]
979    fn eq(&self, other: &JsString) -> bool {
980        other == self
981    }
982}
983
984impl PartialOrd for JsString {
985    #[inline]
986    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
987        Some(self.cmp(other))
988    }
989}
990
991impl FromStr for JsString {
992    type Err = Infallible;
993
994    #[inline]
995    fn from_str(s: &str) -> Result<Self, Self::Err> {
996        Ok(Self::from(s))
997    }
998}
999
1000/// Similar to [`std::ops::RangeBounds`] but custom implemented for getting direct indices.
1001// TODO: remove [`str::JsSliceIndex`] and rename this when `JsStr` is no more.
1002pub trait JsStringSliceIndex {
1003    /// Get the substring (or `None` if outside the string).
1004    fn get(self, str: &JsString) -> Option<JsString>;
1005}
1006
1007macro_rules! impl_js_string_slice_index {
1008    ($($type:ty),+ $(,)?) => {
1009        $(
1010        impl JsStringSliceIndex for $type {
1011            fn get(self, str: &JsString) -> Option<JsString> {
1012                let start = match std::ops::RangeBounds::<usize>::start_bound(&self) {
1013                    std::ops::Bound::Included(start) => *start,
1014                    std::ops::Bound::Excluded(start) => *start + 1,
1015                    std::ops::Bound::Unbounded => 0,
1016                };
1017
1018                let end = match std::ops::RangeBounds::<usize>::end_bound(&self) {
1019                    std::ops::Bound::Included(end) => *end + 1,
1020                    std::ops::Bound::Excluded(end) => *end,
1021                    std::ops::Bound::Unbounded => str.len(),
1022                };
1023
1024                if end > str.len() || start > end {
1025                    None
1026                } else {
1027                    // SAFETY: we just checked the indices.
1028                    Some(unsafe { JsString::slice_unchecked(str, start, end) })
1029                }
1030            }
1031        }
1032        )+
1033    };
1034}
1035
1036impl_js_string_slice_index!(
1037    std::ops::Range<usize>,
1038    std::ops::RangeInclusive<usize>,
1039    std::ops::RangeTo<usize>,
1040    std::ops::RangeToInclusive<usize>,
1041    std::ops::RangeFrom<usize>,
1042    std::ops::RangeFull,
1043);