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