Skip to main content

quickjs_oxide/engine/value/
primitive.rs

1use std::cell::Cell;
2use std::cell::RefCell;
3use std::collections::TryReserveError;
4use std::fmt;
5
6use std::hash::{Hash, Hasher};
7use std::iter::FusedIterator;
8use std::rc::{Rc, Weak};
9
10use crate::engine::api::error::{Error, ErrorKind, NativeErrorMessage};
11use crate::engine::value::bigint::JsBigInt;
12
13/// ECMAScript strings are sequences of UTF-16 code units, not UTF-8 scalar
14/// values. Compact Latin-1/UTF-16 leaves and bounded ropes mirror QuickJS's
15/// current representation while preserving lone surrogates.
16#[derive(Clone)]
17pub struct JsString(Rc<StringRepr>);
18
19/// Non-owning handle to a String value ([`JsString::downgrade`]): `upgrade`
20/// recovers the same string while another handle keeps it alive.
21#[derive(Clone)]
22pub struct WeakJsString(Weak<StringRepr>);
23
24impl WeakJsString {
25    #[must_use]
26    pub fn upgrade(&self) -> Option<JsString> {
27        self.0.upgrade().map(JsString)
28    }
29}
30
31impl fmt::Debug for WeakJsString {
32    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33        formatter
34            .debug_tuple("WeakJsString")
35            .field(&self.0.as_ptr())
36            .finish()
37    }
38}
39
40/// Why a string-kernel operation failed. It becomes a QuickJS InternalError
41/// (`ErrorKind::JsInternal`) in the front end's errors, and a
42/// [`RuntimeError`](crate::engine::api::core::RuntimeError) in the embedding
43/// API.
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum JsStringError {
46    TooLong,
47    OutOfMemory,
48}
49
50impl fmt::Display for JsStringError {
51    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::TooLong => formatter.write_str("string too long"),
54            Self::OutOfMemory => formatter.write_str("out of memory"),
55        }
56    }
57}
58
59impl std::error::Error for JsStringError {}
60
61impl From<JsStringError> for Error {
62    fn from(error: JsStringError) -> Self {
63        match error {
64            JsStringError::TooLong | JsStringError::OutOfMemory => {
65                Error::new(ErrorKind::JsInternal, error.to_string())
66            }
67        }
68    }
69}
70
71#[cfg(test)]
72thread_local! {
73    static FAIL_NEXT_CONCAT_RESERVATION: Cell<bool> = const { Cell::new(false) };
74    static FAIL_NEXT_REPEAT_RESERVATION: Cell<bool> = const { Cell::new(false) };
75    static FAIL_NEXT_PAD_RESERVATION: Cell<bool> = const { Cell::new(false) };
76    static FAIL_NEXT_TRIM_RESERVATION: Cell<bool> = const { Cell::new(false) };
77    static FAIL_NEXT_REPLACEMENT_RESERVATION: Cell<bool> = const { Cell::new(false) };
78}
79
80#[cfg(test)]
81fn arm_reservation_failure(flag: &'static std::thread::LocalKey<Cell<bool>>, label: &str) {
82    flag.with(|armed| {
83        assert!(
84            !armed.replace(true),
85            "{label} reservation failure was already armed"
86        )
87    });
88}
89
90#[cfg(test)]
91pub fn fail_next_repeat_reservation_for_test() {
92    arm_reservation_failure(&FAIL_NEXT_REPEAT_RESERVATION, "repeat");
93}
94
95#[cfg(test)]
96pub fn fail_next_pad_reservation_for_test() {
97    arm_reservation_failure(&FAIL_NEXT_PAD_RESERVATION, "pad");
98}
99
100#[cfg(test)]
101pub fn fail_next_trim_reservation_for_test() {
102    arm_reservation_failure(&FAIL_NEXT_TRIM_RESERVATION, "trim");
103}
104
105struct StringRepr {
106    content_hash: Cell<Option<u32>>,
107    data: StringData,
108}
109
110impl StringRepr {
111    fn allocate(data: StringData) -> Rc<Self> {
112        Rc::new(Self {
113            content_hash: Cell::new(None),
114            data,
115        })
116    }
117
118    fn data(&self) -> &StringData {
119        &self.data
120    }
121}
122
123enum StringData {
124    Latin1(Vec<u8>),
125    Utf16(Vec<u16>),
126    Rope(RopeRepr),
127}
128
129struct RopeRepr {
130    len: usize,
131    is_wide: bool,
132    depth: u8,
133    /// QuickJS rewrites a shared rope to `flat + empty` after ToString while
134    /// retaining its tag and cached depth. The state transition releases the
135    /// old children and preserves that exact observable performance shape.
136    state: RefCell<RopeState>,
137}
138
139enum RopeState {
140    Tree { left: JsString, right: JsString },
141    Linearized { flat: JsString },
142}
143
144struct Utf16Units {
145    stack: [Option<JsString>; 61],
146    stack_len: usize,
147    current_flat: Option<(JsString, usize)>,
148    remaining: usize,
149}
150
151/// Borrowed code-unit iterator for [`JsString::utf16_units`]. Flat strings
152/// iterate their storage slice in place; ropes keep the owning traversal.
153enum BorrowedUtf16Units<'a> {
154    Latin1(std::slice::Iter<'a, u8>),
155    Utf16(std::slice::Iter<'a, u16>),
156    // Boxed: the traversal stack dwarfs the flat variants, and only ropes
157    // pay the allocation.
158    Rope(Box<Utf16Units>),
159}
160
161impl Iterator for BorrowedUtf16Units<'_> {
162    type Item = u16;
163
164    #[inline]
165    fn next(&mut self) -> Option<u16> {
166        match self {
167            Self::Latin1(units) => units.next().copied().map(u16::from),
168            Self::Utf16(units) => units.next().copied(),
169            Self::Rope(units) => units.next(),
170        }
171    }
172
173    #[inline]
174    fn size_hint(&self) -> (usize, Option<usize>) {
175        match self {
176            Self::Latin1(units) => units.size_hint(),
177            Self::Utf16(units) => units.size_hint(),
178            Self::Rope(units) => units.size_hint(),
179        }
180    }
181
182    #[inline]
183    fn nth(&mut self, n: usize) -> Option<u16> {
184        match self {
185            Self::Latin1(units) => units.nth(n).copied().map(u16::from),
186            Self::Utf16(units) => units.nth(n).copied(),
187            Self::Rope(units) => units.nth(n),
188        }
189    }
190}
191
192impl ExactSizeIterator for BorrowedUtf16Units<'_> {}
193
194struct QuickJsUtf8Bytes {
195    units: std::iter::Peekable<Utf16Units>,
196    cesu8: bool,
197    pending: [u8; 4],
198    pending_index: usize,
199    pending_len: usize,
200}
201
202pub struct JsStringBuilder {
203    units: Vec<u16>,
204    limit: usize,
205    failed: bool,
206}
207
208impl JsStringBuilder {
209    pub fn with_limit(capacity: usize, limit: usize) -> Self {
210        let limit = limit.min(JsString::MAX_LEN);
211        Self {
212            units: Vec::with_capacity(capacity.min(limit).min(4096)),
213            limit,
214            failed: false,
215        }
216    }
217
218    fn push_latin1(&mut self, value: &[u8]) -> Result<(), JsStringError> {
219        self.ensure_additional(value.len())?;
220        self.units.extend(value.iter().copied().map(u16::from));
221        Ok(())
222    }
223
224    pub fn push_code_point(&mut self, value: u32) -> Result<(), JsStringError> {
225        debug_assert!(value <= 0x10_ffff);
226        let additional = if value <= 0xffff { 1 } else { 2 };
227        self.ensure_additional(additional)?;
228        if value <= 0xffff {
229            self.units.push(value as u16);
230        } else {
231            let adjusted = value - 0x1_0000;
232            self.units.push(0xd800 | ((adjusted >> 10) as u16));
233            self.units.push(0xdc00 | ((adjusted & 0x3ff) as u16));
234        }
235        Ok(())
236    }
237
238    fn ensure_additional(&mut self, additional: usize) -> Result<(), JsStringError> {
239        if self.failed {
240            return Err(JsStringError::TooLong);
241        }
242        if JsString::checked_length_with_limit(self.units.len(), additional, self.limit).is_err() {
243            self.units = Vec::new();
244            self.failed = true;
245            return Err(JsStringError::TooLong);
246        }
247        Ok(())
248    }
249
250    pub fn finish(self) -> Result<JsString, JsStringError> {
251        if self.failed {
252            return Err(JsStringError::TooLong);
253        }
254        debug_assert!(self.units.len() <= self.limit);
255        Ok(JsString::from_validated_utf16(self.units))
256    }
257}
258
259impl Utf16Units {
260    fn new(string: &JsString) -> Self {
261        let mut iterator = Self {
262            // Inline-const init compiles to a plain zeroing store; the closure
263            // form lowered to one non-inlined call per slot on this hot path.
264            stack: [const { None }; 61],
265            stack_len: 0,
266            current_flat: None,
267            remaining: string.len(),
268        };
269        iterator.push(string.clone());
270        iterator
271    }
272
273    fn push(&mut self, string: JsString) {
274        assert!(
275            self.stack_len < self.stack.len(),
276            "String rope exceeded its bounded iterator depth"
277        );
278        self.stack[self.stack_len] = Some(string);
279        self.stack_len += 1;
280    }
281
282    fn pop(&mut self) -> Option<JsString> {
283        self.stack_len = self.stack_len.checked_sub(1)?;
284        self.stack[self.stack_len].take()
285    }
286}
287
288impl Iterator for Utf16Units {
289    type Item = u16;
290
291    fn next(&mut self) -> Option<Self::Item> {
292        loop {
293            if let Some((flat, index)) = &mut self.current_flat {
294                let unit = match flat.0.data() {
295                    StringData::Latin1(units) => units.get(*index).copied().map(u16::from),
296                    StringData::Utf16(units) => units.get(*index).copied(),
297                    StringData::Rope(_) => {
298                        unreachable!("UTF-16 iterator current node must be flat")
299                    }
300                };
301                if let Some(unit) = unit {
302                    *index += 1;
303                    self.remaining -= 1;
304                    return Some(unit);
305                }
306                self.current_flat = None;
307            }
308
309            let node = self.pop()?;
310            match node.0.data() {
311                StringData::Latin1(_) | StringData::Utf16(_) => {
312                    self.current_flat = Some((node, 0));
313                }
314                StringData::Rope(rope) => match &*rope.state.borrow() {
315                    RopeState::Tree { left, right } => {
316                        self.push(right.clone());
317                        self.push(left.clone());
318                    }
319                    RopeState::Linearized { flat } => self.push(flat.clone()),
320                },
321            }
322        }
323    }
324
325    fn nth(&mut self, mut n: usize) -> Option<Self::Item> {
326        while n > 0 {
327            if let Some((flat, index)) = &mut self.current_flat {
328                let skip = n.min(flat.len() - *index);
329                *index += skip;
330                self.remaining -= skip;
331                n -= skip;
332                if n == 0 {
333                    break;
334                }
335                self.current_flat = None;
336            }
337            let node = self.pop()?;
338            if node.len() <= n {
339                n -= node.len();
340                self.remaining -= node.len();
341                continue;
342            }
343            match node.0.data() {
344                StringData::Latin1(_) | StringData::Utf16(_) => self.current_flat = Some((node, 0)),
345                StringData::Rope(rope) => match &*rope.state.borrow() {
346                    RopeState::Tree { left, right } => {
347                        self.push(right.clone());
348                        self.push(left.clone());
349                    }
350                    RopeState::Linearized { flat } => self.push(flat.clone()),
351                },
352            }
353        }
354        self.next()
355    }
356
357    fn size_hint(&self) -> (usize, Option<usize>) {
358        (self.remaining, Some(self.remaining))
359    }
360}
361
362impl ExactSizeIterator for Utf16Units {
363    fn len(&self) -> usize {
364        self.remaining
365    }
366}
367
368impl FusedIterator for Utf16Units {}
369
370impl QuickJsUtf8Bytes {
371    fn new(string: &JsString, cesu8: bool) -> Self {
372        Self {
373            units: Utf16Units::new(string).peekable(),
374            cesu8,
375            pending: [0; 4],
376            pending_index: 0,
377            pending_len: 0,
378        }
379    }
380}
381
382impl Iterator for QuickJsUtf8Bytes {
383    type Item = u8;
384
385    fn next(&mut self) -> Option<Self::Item> {
386        if self.pending_index < self.pending_len {
387            let byte = self.pending[self.pending_index];
388            self.pending_index += 1;
389            return Some(byte);
390        }
391
392        let unit = self.units.next()?;
393        let code_point = if !self.cesu8
394            && (0xd800..=0xdbff).contains(&unit)
395            && self
396                .units
397                .peek()
398                .is_some_and(|next| (0xdc00..=0xdfff).contains(next))
399        {
400            let low = self
401                .units
402                .next()
403                .expect("peeked low surrogate disappeared from UTF-16 iterator");
404            0x1_0000 + ((u32::from(unit) - 0xd800) << 10) + (u32::from(low) - 0xdc00)
405        } else {
406            u32::from(unit)
407        };
408        self.pending_len = encode_quickjs_utf8(&mut self.pending, code_point);
409        self.pending_index = 1;
410        Some(self.pending[0])
411    }
412}
413
414impl FusedIterator for QuickJsUtf8Bytes {}
415
416impl NativeErrorMessage {
417    pub fn to_js_string(&self) -> Result<JsString, JsStringError> {
418        JsString::try_from_bytes(self.visible_bytes())
419    }
420
421    pub fn to_utf8_lossy(&self) -> String {
422        self.to_js_string()
423            .expect("a 255-byte native Error message cannot exceed the String length limit")
424            .to_utf8_lossy()
425    }
426}
427
428impl JsString {
429    pub(crate) fn flat_latin1(&self) -> Option<&[u8]> {
430        if let StringData::Latin1(units) = self.0.data() {
431            Some(units)
432        } else {
433            None
434        }
435    }
436
437    pub(crate) fn flat_utf16(&self) -> Option<&[u16]> {
438        if let StringData::Utf16(units) = self.0.data() {
439            Some(units)
440        } else {
441            None
442        }
443    }
444
445    /// QuickJS reserves 30 bits for a string's UTF-16 code-unit length.
446    pub const MAX_LEN: usize = (1 << 30) - 1;
447    const ROPE_SHORT_LEN: usize = 512;
448    const ROPE_SHORT2_LEN: usize = 8192;
449    const ROPE_MAX_DEPTH: u8 = 60;
450    const ROPE_BUCKET_LENGTHS: [usize; 44] = [
451        1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946,
452        17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309,
453        3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141,
454        267914296, 433494437, 701408733, 1134903170,
455    ];
456
457    #[must_use]
458    pub fn downgrade(&self) -> WeakJsString {
459        WeakJsString(Rc::downgrade(&self.0))
460    }
461
462    #[must_use]
463    pub fn same_representation(&self, other: &Self) -> bool {
464        Rc::ptr_eq(&self.0, &other.0)
465    }
466
467    /// The address of the representation, which [`Self::same_representation`]
468    /// compares; it identifies the string while a clone of it is alive.
469    #[must_use]
470    pub(crate) fn address(&self) -> usize {
471        Rc::as_ptr(&self.0).addr()
472    }
473
474    #[must_use]
475    pub fn content_hash(&self) -> u32 {
476        if let Some(hash) = self.0.content_hash.get() {
477            return hash;
478        }
479        let hash = self.quickjs_hash(0);
480        self.0.content_hash.set(Some(hash));
481        hash
482    }
483
484    pub fn checked_length_with_limit(
485        current: usize,
486        additional: usize,
487        limit: usize,
488    ) -> Result<usize, JsStringError> {
489        current
490            .checked_add(additional)
491            .filter(|length| *length <= limit)
492            .ok_or(JsStringError::TooLong)
493    }
494
495    pub fn checked_length(current: usize, additional: usize) -> Result<usize, JsStringError> {
496        Self::checked_length_with_limit(current, additional, Self::MAX_LEN)
497    }
498
499    /// Construct a dynamically supplied UTF-8 string while enforcing
500    /// QuickJS's 30-bit UTF-16 length limit.
501    ///
502    /// # Errors
503    /// Returns [`JsStringError::TooLong`] when the decoded value exceeds
504    /// [`Self::MAX_LEN`]. Recoverable allocator failure remains a separate
505    /// parity gap.
506    pub fn try_from_utf8(value: &str) -> Result<Self, JsStringError> {
507        if value.is_ascii() {
508            return Self::try_from_latin1(value.as_bytes());
509        }
510        Self::try_from_utf16(value.encode_utf16())
511    }
512
513    /// Decode the byte-oriented input accepted by QuickJS `JS_NewStringLen`.
514    ///
515    /// This is deliberately not Rust's strict or standard lossy UTF-8
516    /// decoder. Three-byte surrogate encodings are preserved as UTF-16 code
517    /// units, non-BMP scalars become surrogate pairs, and malformed sequences
518    /// use QuickJS's release-pinned replacement-and-skip algorithm.
519    /// Embedded NUL bytes are ordinary U+0000 code units because the input
520    /// length is explicit.
521    ///
522    /// # Errors
523    /// Returns [`JsStringError::TooLong`] when the decoded value exceeds
524    /// [`Self::MAX_LEN`] UTF-16 code units. Recoverable allocation failure for
525    /// String storage remains a separate parity gap.
526    pub fn try_from_bytes(value: &[u8]) -> Result<Self, JsStringError> {
527        Self::try_from_bytes_with_limit(value, Self::MAX_LEN)
528    }
529
530    /// Construct one of the engine's trusted static table/literal strings.
531    /// Dynamic host input must use [`Self::try_from_utf8`].
532    pub fn from_static(value: &'static str) -> Self {
533        Self::try_from_utf8(value).expect("static ECMAScript String exceeded QuickJS's length cap")
534    }
535
536    /// Construct a dynamically supplied UTF-16 string while enforcing
537    /// QuickJS's 30-bit code-unit length limit.
538    ///
539    /// The iterator is consumed only after its lower size bound is validated;
540    /// a dishonest or unbounded upper hint is never used for an enormous eager
541    /// allocation.
542    ///
543    /// # Errors
544    /// Returns [`JsStringError::TooLong`] when the value exceeds
545    /// [`Self::MAX_LEN`]. Recoverable allocator failure remains a separate
546    /// parity gap.
547    pub fn try_from_utf16(units: impl IntoIterator<Item = u16>) -> Result<Self, JsStringError> {
548        Self::try_from_utf16_with_limit(units, Self::MAX_LEN)
549    }
550
551    pub fn try_from_utf16_with_limit(
552        units: impl IntoIterator<Item = u16>,
553        max_len: usize,
554    ) -> Result<Self, JsStringError> {
555        let max_len = max_len.min(Self::MAX_LEN);
556        let mut iterator = units.into_iter();
557        let (lower, upper) = iterator.size_hint();
558        if lower > max_len {
559            return Err(JsStringError::TooLong);
560        }
561        let initial_capacity = upper.unwrap_or(lower).min(max_len).min(4096);
562        let mut collected = Vec::with_capacity(initial_capacity);
563        for unit in &mut iterator {
564            if collected.len() == max_len {
565                return Err(JsStringError::TooLong);
566            }
567            collected.push(unit);
568        }
569        Ok(Self::from_validated_utf16(collected))
570    }
571
572    fn try_from_bytes_with_limit(value: &[u8], max_len: usize) -> Result<Self, JsStringError> {
573        let max_len = max_len.min(Self::MAX_LEN);
574        let ascii_len = value.iter().take_while(|byte| **byte < 0x80).count();
575        if ascii_len > max_len {
576            return Err(JsStringError::TooLong);
577        }
578        if ascii_len == value.len() {
579            return Ok(Self::from_owned_latin1(value.to_vec()));
580        }
581
582        let mut output = JsStringBuilder::with_limit(value.len(), max_len);
583        output.push_latin1(&value[..ascii_len])?;
584        let mut index = ascii_len;
585        while index < value.len() {
586            if value[index] < 0x80 {
587                output.push_code_point(u32::from(value[index]))?;
588                index += 1;
589                continue;
590            }
591
592            match decode_quickjs_utf8(&value[index..]) {
593                Some((code_point, consumed)) if code_point <= 0x10_ffff => {
594                    output.push_code_point(code_point)?;
595                    index += consumed;
596                }
597                Some(_) | None => {
598                    index = skip_quickjs_invalid_utf8(value, index);
599                    output.push_code_point(0xfffd)?;
600                }
601            }
602        }
603        output.finish()
604    }
605
606    fn from_validated_utf16(units: Vec<u16>) -> Self {
607        debug_assert!(units.len() <= Self::MAX_LEN);
608        if units.iter().all(|&unit| unit <= 0xff) {
609            let latin1 = units.iter().map(|&unit| unit as u8).collect();
610            Self(StringRepr::allocate(StringData::Latin1(latin1)))
611        } else {
612            Self(StringRepr::allocate(StringData::Utf16(units)))
613        }
614    }
615
616    /// Adopt a checked narrow result buffer without collecting its contents a
617    /// second time. Callers must have enforced [`Self::MAX_LEN`].
618    pub fn from_owned_latin1(units: Vec<u8>) -> Self {
619        debug_assert!(units.len() <= Self::MAX_LEN);
620        Self(StringRepr::allocate(StringData::Latin1(units)))
621    }
622
623    /// Materialize QuickJS's `JS_AtomToString` result for one tagged integer
624    /// atom. The returned narrow String deliberately owns a fresh
625    /// representation on every call; routing this spelling through an atom or
626    /// constant pool would make QuickJS's representation identity observable.
627    #[must_use]
628    pub fn from_fresh_decimal_u32(value: u32) -> Self {
629        let mut buffer = fmt::NumBuffer::new();
630        let digits = value.format_into(&mut buffer).as_bytes().to_vec();
631        Self(StringRepr::allocate(StringData::Latin1(digits)))
632    }
633
634    /// A copy of Latin-1 code units.
635    ///
636    /// # Errors
637    /// Returns [`JsStringError::TooLong`] past [`Self::MAX_LEN`].
638    pub fn try_from_latin1(units: &[u8]) -> Result<Self, JsStringError> {
639        if units.len() > Self::MAX_LEN {
640            return Err(JsStringError::TooLong);
641        }
642        Ok(Self::from_owned_latin1(units.to_vec()))
643    }
644
645    /// A copy of UTF-16 code units, stored narrow when every unit fits in a
646    /// byte.
647    ///
648    /// # Errors
649    /// Returns [`JsStringError::TooLong`] past [`Self::MAX_LEN`].
650    pub fn try_from_utf16_slice(units: &[u16]) -> Result<Self, JsStringError> {
651        if units.len() > Self::MAX_LEN {
652            return Err(JsStringError::TooLong);
653        }
654        if units.iter().all(|&unit| unit <= 0xff) {
655            return Ok(Self::from_owned_latin1(
656                units.iter().map(|&unit| unit as u8).collect(),
657            ));
658        }
659        Ok(Self::from_owned_utf16(units.to_vec()))
660    }
661
662    /// Adopt a checked wide result buffer without collecting its contents a
663    /// second time. Callers must have enforced [`Self::MAX_LEN`].
664    pub fn from_owned_utf16(units: Vec<u16>) -> Self {
665        debug_assert!(units.len() <= Self::MAX_LEN);
666        Self(StringRepr::allocate(StringData::Utf16(units)))
667    }
668
669    /// Build the compact one-code-unit form used by String exotic indices and
670    /// character methods, equivalent to QuickJS's `js_new_string_char`.
671    #[must_use]
672    pub fn from_code_unit(unit: u16) -> Self {
673        match u8::try_from(unit) {
674            Ok(unit) => Self(StringRepr::allocate(StringData::Latin1(vec![unit]))),
675            Err(_) => Self(StringRepr::allocate(StringData::Utf16(vec![unit]))),
676        }
677    }
678
679    #[must_use]
680    pub fn len(&self) -> usize {
681        match self.0.data() {
682            StringData::Latin1(units) => units.len(),
683            StringData::Utf16(units) => units.len(),
684            StringData::Rope(rope) => rope.len,
685        }
686    }
687
688    #[must_use]
689    pub fn is_empty(&self) -> bool {
690        self.len() == 0
691    }
692
693    #[inline]
694    pub fn utf16_units(&self) -> impl ExactSizeIterator<Item = u16> + '_ {
695        // Flat strings walk their storage slice directly; only a live rope
696        // pays for the owning traversal iterator and its bounded stack.
697        match self.0.data() {
698            StringData::Latin1(units) => BorrowedUtf16Units::Latin1(units.iter()),
699            StringData::Utf16(units) => BorrowedUtf16Units::Utf16(units.iter()),
700            StringData::Rope(_) => BorrowedUtf16Units::Rope(Box::new(Utf16Units::new(self))),
701        }
702    }
703
704    /// Return one UTF-16 code unit without decoding or normalizing surrogate
705    /// pairs. This is the equivalent of QuickJS's `string_get` fast path.
706    #[must_use]
707    pub fn code_unit_at(&self, index: usize) -> Option<u16> {
708        if index >= self.len() {
709            return None;
710        }
711        match self.0.data() {
712            StringData::Latin1(units) => units.get(index).copied().map(u16::from),
713            StringData::Utf16(units) => units.get(index).copied(),
714            StringData::Rope(rope) => match &*rope.state.borrow() {
715                RopeState::Linearized { flat } => flat.code_unit_at(index),
716                RopeState::Tree { left, right } => {
717                    if index < left.len() {
718                        left.code_unit_at(index)
719                    } else {
720                        right.code_unit_at(index - left.len())
721                    }
722                }
723            },
724        }
725    }
726
727    /// Return the code point beginning at one UTF-16 code-unit index. A lead
728    /// surrogate is combined only with an immediately following trail
729    /// surrogate; every other code unit is returned unchanged.
730    #[must_use]
731    pub fn code_point_at(&self, index: usize) -> Option<u32> {
732        let first = self.code_unit_at(index)?;
733        if !(0xd800..=0xdbff).contains(&first) {
734            return Some(u32::from(first));
735        }
736        let Some(second) = index
737            .checked_add(1)
738            .and_then(|next| self.code_unit_at(next))
739        else {
740            return Some(u32::from(first));
741        };
742        if !(0xdc00..=0xdfff).contains(&second) {
743            return Some(u32::from(first));
744        }
745        Some(0x1_0000 + ((u32::from(first) - 0xd800) << 10) + (u32::from(second) - 0xdc00))
746    }
747
748    /// Whether every surrogate participates in a valid UTF-16 pair.
749    #[must_use]
750    pub fn is_well_formed(&self) -> bool {
751        self.first_unpaired_surrogate().is_none()
752    }
753
754    /// Replace each unpaired surrogate with U+FFFD. Well-formed strings retain
755    /// their existing compact allocation, matching QuickJS's no-copy path.
756    #[must_use]
757    pub fn to_well_formed(&self) -> Self {
758        let Some(first_invalid) = self.first_unpaired_surrogate() else {
759            return self.clone();
760        };
761        let mut units = self.utf16_units().collect::<Vec<_>>();
762        let mut index = first_invalid;
763        while index < units.len() {
764            let unit = units[index];
765            if (0xd800..=0xdbff).contains(&unit)
766                && units
767                    .get(index + 1)
768                    .is_some_and(|next| (0xdc00..=0xdfff).contains(next))
769            {
770                index += 2;
771            } else {
772                if (0xd800..=0xdfff).contains(&unit) {
773                    units[index] = 0xfffd;
774                }
775                index += 1;
776            }
777        }
778        Self::from_validated_utf16(units)
779    }
780
781    fn first_unpaired_surrogate(&self) -> Option<usize> {
782        if !self.is_wide() {
783            return None;
784        }
785        let mut units = self.utf16_units().enumerate().peekable();
786        while let Some((index, unit)) = units.next() {
787            if (0xd800..=0xdbff).contains(&unit) {
788                if units
789                    .peek()
790                    .is_some_and(|(_, next)| (0xdc00..=0xdfff).contains(next))
791                {
792                    units.next();
793                    continue;
794                }
795                return Some(index);
796            }
797            if (0xd800..=0xdfff).contains(&unit) {
798                return Some(index);
799            }
800        }
801        None
802    }
803
804    pub fn is_wide(&self) -> bool {
805        match self.0.data() {
806            StringData::Latin1(_) => false,
807            StringData::Utf16(_) => true,
808            StringData::Rope(rope) => rope.is_wide,
809        }
810    }
811
812    fn depth(&self) -> u8 {
813        match self.0.data() {
814            StringData::Latin1(_) | StringData::Utf16(_) => 0,
815            StringData::Rope(rope) => rope.depth,
816        }
817    }
818
819    pub fn is_flat(&self) -> bool {
820        matches!(self.0.data(), StringData::Latin1(_) | StringData::Utf16(_))
821    }
822
823    /// Visit the current flat leaves in logical order without linearizing a
824    /// rope. QuickJS's diagnostic String printer decodes each leaf
825    /// independently, so surrogate pairs do not cross a live rope boundary.
826    /// A previously linearized rope exposes its replacement flat value as one
827    /// leaf, matching QuickJS's in-place `flat + empty` transition.
828    pub fn for_each_flat_leaf(&self, mut visitor: impl FnMut(&Self)) {
829        let mut stack: [Option<Self>; 61] = [const { None }; 61];
830        stack[0] = Some(self.clone());
831        let mut stack_len = 1;
832        while stack_len != 0 {
833            stack_len -= 1;
834            let node = stack[stack_len]
835                .take()
836                .expect("String flat-leaf traversal stack contained a hole");
837            match node.0.data() {
838                StringData::Latin1(_) | StringData::Utf16(_) => visitor(&node),
839                StringData::Rope(rope) => match &*rope.state.borrow() {
840                    RopeState::Tree { left, right } => {
841                        assert!(
842                            stack_len + 2 <= stack.len(),
843                            "String rope exceeded its bounded flat-leaf traversal depth"
844                        );
845                        stack[stack_len] = Some(right.clone());
846                        stack[stack_len + 1] = Some(left.clone());
847                        stack_len += 2;
848                    }
849                    RopeState::Linearized { flat } => {
850                        assert!(
851                            stack_len < stack.len(),
852                            "String rope exceeded its bounded flat-leaf traversal depth"
853                        );
854                        stack[stack_len] = Some(flat.clone());
855                        stack_len += 1;
856                    }
857                },
858            }
859        }
860    }
861
862    fn quickjs_hash(&self, seed: u32) -> u32 {
863        let step = |hash: u32, unit: u16| hash.wrapping_mul(263).wrapping_add(u32::from(unit));
864        match self.0.data() {
865            StringData::Latin1(units) => units.iter().copied().map(u16::from).fold(seed, step),
866            StringData::Utf16(units) => units.iter().copied().fold(seed, step),
867            StringData::Rope(_) => self.utf16_units().fold(seed, step),
868        }
869    }
870
871    fn rope_children(rope: &RopeRepr) -> (Self, Self) {
872        match &*rope.state.borrow() {
873            RopeState::Tree { left, right } => (left.clone(), right.clone()),
874            RopeState::Linearized { flat } => (flat.clone(), Self::from_static("")),
875        }
876    }
877
878    fn flat_concat(left: &Self, right: &Self) -> Self {
879        debug_assert!(left.is_flat() && right.is_flat());
880        let len = left
881            .len()
882            .checked_add(right.len())
883            .expect("validated flat String concatenation length overflowed");
884        debug_assert!(len <= Self::MAX_LEN);
885        match (left.0.data(), right.0.data()) {
886            (StringData::Latin1(left), StringData::Latin1(right)) => {
887                let mut units = Vec::with_capacity(len);
888                units.extend_from_slice(left);
889                units.extend_from_slice(right);
890                Self(StringRepr::allocate(StringData::Latin1(units)))
891            }
892            (
893                StringData::Latin1(_) | StringData::Utf16(_),
894                StringData::Latin1(_) | StringData::Utf16(_),
895            ) => {
896                let units = left
897                    .utf16_units()
898                    .chain(right.utf16_units())
899                    .collect::<Vec<_>>();
900                debug_assert_eq!(units.len(), len);
901                Self(StringRepr::allocate(StringData::Utf16(units)))
902            }
903            (StringData::Rope(_), _) | (_, StringData::Rope(_)) => {
904                unreachable!("flat String concatenation received a rope")
905            }
906        }
907    }
908
909    fn new_rope_unbalanced(left: Self, right: Self) -> Result<Self, JsStringError> {
910        let len = left
911            .len()
912            .checked_add(right.len())
913            .filter(|len| *len <= Self::MAX_LEN)
914            .ok_or(JsStringError::TooLong)?;
915        let depth = left
916            .depth()
917            .max(right.depth())
918            .checked_add(1)
919            .expect("String rope depth invariant overflowed");
920        Ok(Self(StringRepr::allocate(StringData::Rope(RopeRepr {
921            len,
922            is_wide: left.is_wide() || right.is_wide(),
923            depth,
924            state: RefCell::new(RopeState::Tree { left, right }),
925        }))))
926    }
927
928    fn new_rope(left: Self, right: Self) -> Result<Self, JsStringError> {
929        let rope = Self::new_rope_unbalanced(left, right)?;
930        if rope.depth() > Self::ROPE_MAX_DEPTH {
931            rope.rebalance()
932        } else {
933            Ok(rope)
934        }
935    }
936
937    fn rebalance(&self) -> Result<Self, JsStringError> {
938        let mut buckets: [Option<Self>; Self::ROPE_BUCKET_LENGTHS.len()] =
939            [const { None }; Self::ROPE_BUCKET_LENGTHS.len()];
940        // Rebalancing is entered for the one temporary depth-61 rope. A
941        // depth-first traversal therefore needs at most 62 pending nodes.
942        let mut pending: [Option<Self>; 62] = [const { None }; 62];
943        pending[0] = Some(self.clone());
944        let mut pending_len = 1;
945        while pending_len != 0 {
946            pending_len -= 1;
947            let node = pending[pending_len]
948                .take()
949                .expect("String rope rebalance stack contained a hole");
950            match node.0.data() {
951                StringData::Rope(rope) => {
952                    match &*rope.state.borrow() {
953                        RopeState::Tree { left, right } => {
954                            assert!(
955                                pending_len + 2 <= pending.len(),
956                                "String rope exceeded its bounded rebalance depth"
957                            );
958                            pending[pending_len] = Some(right.clone());
959                            pending[pending_len + 1] = Some(left.clone());
960                            pending_len += 2;
961                        }
962                        RopeState::Linearized { flat } => {
963                            assert!(
964                                pending_len < pending.len(),
965                                "String rope exceeded its bounded rebalance depth"
966                            );
967                            pending[pending_len] = Some(flat.clone());
968                            pending_len += 1;
969                        }
970                    }
971                    continue;
972                }
973                StringData::Latin1(_) | StringData::Utf16(_) if node.is_empty() => continue,
974                StringData::Latin1(_) | StringData::Utf16(_) => {}
975            }
976
977            let len = node.len();
978            let mut index = 0;
979            let mut prefix = None;
980            while len >= Self::ROPE_BUCKET_LENGTHS[index + 1] {
981                if let Some(bucket) = buckets[index].take() {
982                    prefix = Some(match prefix {
983                        None => bucket,
984                        Some(prefix) => Self::new_rope_unbalanced(bucket, prefix)?,
985                    });
986                }
987                index += 1;
988            }
989            let mut value = match prefix {
990                Some(prefix) => Self::new_rope_unbalanced(prefix, node)?,
991                None => node,
992            };
993            while let Some(bucket) = buckets[index].take() {
994                value = Self::new_rope_unbalanced(bucket, value)?;
995                index += 1;
996            }
997            buckets[index] = Some(value);
998        }
999
1000        let mut result = None;
1001        for bucket in buckets.into_iter().flatten() {
1002            result = Some(match result {
1003                None => bucket,
1004                Some(result) => Self::new_rope_unbalanced(bucket, result)?,
1005            });
1006        }
1007        Ok(result.unwrap_or_else(|| Self::from_static("")))
1008    }
1009
1010    /// Return a compact flat string, caching the result on a rope. This is the
1011    /// safe-Rust equivalent of QuickJS `js_linearize_string_rope`.
1012    #[must_use]
1013    pub fn linearize(&self) -> Self {
1014        let StringData::Rope(rope) = self.0.data() else {
1015            return self.clone();
1016        };
1017        if let RopeState::Linearized { flat } = &*rope.state.borrow() {
1018            return flat.clone();
1019        }
1020        let flattened = if rope.is_wide {
1021            Self(StringRepr::allocate(StringData::Utf16(
1022                self.utf16_units().collect::<Vec<_>>(),
1023            )))
1024        } else {
1025            let units = self
1026                .utf16_units()
1027                .map(|unit| {
1028                    u8::try_from(unit).expect("a narrow String rope contained a wide code unit")
1029                })
1030                .collect::<Vec<_>>();
1031            Self(StringRepr::allocate(StringData::Latin1(units)))
1032        };
1033        *rope.state.borrow_mut() = RopeState::Linearized {
1034            flat: flattened.clone(),
1035        };
1036        flattened
1037    }
1038
1039    /// Copy one validated UTF-16 code-unit range with QuickJS
1040    /// `js_sub_string` representation rules. Ropes are linearized once, a
1041    /// full-range request reuses that flat handle, Latin-1 ranges copy their
1042    /// bytes directly, and UTF-16 ranges scan only the selected units before
1043    /// choosing a single Latin-1 or UTF-16 allocation.
1044    #[must_use]
1045    pub fn sub_string(&self, start: usize, end: usize) -> Self {
1046        let source = self.linearize();
1047        assert!(start <= end, "String subrange start exceeded end");
1048        assert!(
1049            end <= source.len(),
1050            "String subrange exceeded source length"
1051        );
1052        if start == 0 && end == source.len() {
1053            return source;
1054        }
1055        match source.0.data() {
1056            StringData::Latin1(units) => Self(StringRepr::allocate(StringData::Latin1(
1057                units[start..end].to_vec(),
1058            ))),
1059            StringData::Utf16(units) => {
1060                let selected = &units[start..end];
1061                if selected.iter().all(|unit| *unit <= u16::from(u8::MAX)) {
1062                    let narrow = selected.iter().map(|unit| *unit as u8).collect::<Vec<_>>();
1063                    Self(StringRepr::allocate(StringData::Latin1(narrow)))
1064                } else {
1065                    Self(StringRepr::allocate(StringData::Utf16(selected.to_vec())))
1066                }
1067            }
1068            StringData::Rope(_) => unreachable!("linearized String remained a rope"),
1069        }
1070    }
1071
1072    /// Repeat one linearized String into the single flat allocation produced by
1073    /// QuickJS `js_string_repeat`. The checked product uses the caller-supplied
1074    /// limit so native white-box tests can exercise the length failure without
1075    /// attempting a near-gigabyte allocation.
1076    pub fn repeat_with_limit(&self, count: usize, max_len: usize) -> Result<Self, JsStringError> {
1077        let source = self.linearize();
1078        // QuickJS returns the already-converted receiver before checking the
1079        // product. Count conversion and validation happen outside this kernel.
1080        if source.is_empty() || count == 1 {
1081            return Ok(source);
1082        }
1083        let output_len = source
1084            .len()
1085            .checked_mul(count)
1086            .filter(|length| *length <= max_len.min(Self::MAX_LEN))
1087            .ok_or(JsStringError::TooLong)?;
1088        if output_len == 0 {
1089            // `string_buffer_end` releases the temporary zero-length buffer
1090            // and returns QuickJS's canonical narrow empty String.
1091            return Ok(Self::from_static(""));
1092        }
1093
1094        fn grow_repetition<T: Copy>(
1095            source: &[T],
1096            output_len: usize,
1097        ) -> Result<Vec<T>, JsStringError> {
1098            // `string_buffer_init2` performs one exact fallible allocation.
1099            // Reserve the complete buffer up front so later doubling copies
1100            // cannot allocate and allocator failure remains catchable.
1101            #[cfg(test)]
1102            if FAIL_NEXT_REPEAT_RESERVATION.with(|armed| armed.replace(false)) {
1103                return Err(JsStringError::OutOfMemory);
1104            }
1105            let mut output = Vec::new();
1106            output
1107                .try_reserve_exact(output_len)
1108                .map_err(|_| JsStringError::OutOfMemory)?;
1109            if source.len() == 1 {
1110                output.resize(output_len, source[0]);
1111                return Ok(output);
1112            }
1113            output.extend_from_slice(source);
1114            while output.len() < output_len {
1115                let copied = output.len().min(output_len - output.len());
1116                output.extend_from_within(..copied);
1117            }
1118            Ok(output)
1119        }
1120
1121        Ok(match source.0.data() {
1122            StringData::Latin1(units) => Self(StringRepr::allocate(StringData::Latin1(
1123                grow_repetition(units, output_len)?,
1124            ))),
1125            StringData::Utf16(units) => Self(StringRepr::allocate(StringData::Utf16(
1126                grow_repetition(units, output_len)?,
1127            ))),
1128            StringData::Rope(_) => unreachable!("linearized String remained a rope"),
1129        })
1130    }
1131
1132    /// Build the one flat result of pinned QuickJS `js_string_pad` while
1133    /// retaining its narrow-first `StringBuffer` behavior. A wide code unit
1134    /// triggers one fallible widening reservation at the point it is copied;
1135    /// unused wide units in a truncated filler therefore do not widen the
1136    /// result.
1137    pub fn pad_with_limit(
1138        &self,
1139        target_len: usize,
1140        filler: Option<&Self>,
1141        pad_at_end: bool,
1142        max_len: usize,
1143    ) -> Result<Self, JsStringError> {
1144        let source = self.linearize();
1145        if source.len() >= target_len {
1146            return Ok(source);
1147        }
1148        let filler = filler.map(Self::linearize);
1149        if filler.as_ref().is_some_and(Self::is_empty) {
1150            return Ok(source);
1151        }
1152        if target_len > max_len.min(Self::MAX_LEN) {
1153            return Err(JsStringError::TooLong);
1154        }
1155
1156        fn reserve_pad_buffer<T>(capacity: usize) -> Result<Vec<T>, JsStringError> {
1157            #[cfg(test)]
1158            if FAIL_NEXT_PAD_RESERVATION.with(|armed| armed.replace(false)) {
1159                return Err(JsStringError::OutOfMemory);
1160            }
1161            let mut output = Vec::new();
1162            output
1163                .try_reserve_exact(capacity)
1164                .map_err(|_| JsStringError::OutOfMemory)?;
1165            Ok(output)
1166        }
1167
1168        enum PadBuffer {
1169            Latin1(Vec<u8>),
1170            Utf16(Vec<u16>),
1171        }
1172
1173        fn push_pad_unit(
1174            buffer: &mut PadBuffer,
1175            unit: u16,
1176            target_len: usize,
1177        ) -> Result<(), JsStringError> {
1178            match buffer {
1179                PadBuffer::Latin1(units) if unit <= u16::from(u8::MAX) => {
1180                    units.push(unit as u8);
1181                }
1182                PadBuffer::Latin1(units) => {
1183                    // QuickJS first allocates a complete narrow buffer, then
1184                    // fallibly widens it when the first copied unit needs 16
1185                    // bits. Preserve both recoverable allocation points.
1186                    let mut wide = reserve_pad_buffer(target_len)?;
1187                    wide.extend(units.iter().map(|unit| u16::from(*unit)));
1188                    wide.push(unit);
1189                    *buffer = PadBuffer::Utf16(wide);
1190                }
1191                PadBuffer::Utf16(units) => units.push(unit),
1192            }
1193            Ok(())
1194        }
1195
1196        fn append_source(
1197            buffer: &mut PadBuffer,
1198            source: &JsString,
1199            target_len: usize,
1200        ) -> Result<(), JsStringError> {
1201            for unit in source.utf16_units() {
1202                push_pad_unit(buffer, unit, target_len)?;
1203            }
1204            Ok(())
1205        }
1206
1207        fn append_padding(
1208            buffer: &mut PadBuffer,
1209            filler: Option<&JsString>,
1210            padding_len: usize,
1211            target_len: usize,
1212        ) -> Result<(), JsStringError> {
1213            let filler_len = filler.map_or(1, JsString::len);
1214            for index in 0..padding_len {
1215                let unit = match filler {
1216                    Some(filler) => filler
1217                        .code_unit_at(index % filler_len)
1218                        .expect("non-empty flat pad filler lost a code unit"),
1219                    None => u16::from(b' '),
1220                };
1221                push_pad_unit(buffer, unit, target_len)?;
1222            }
1223            Ok(())
1224        }
1225
1226        // string_buffer_init(ctx, b, n) always starts narrow, even if either
1227        // input is wide. Subsequent writes widen only for copied code units.
1228        let mut output = PadBuffer::Latin1(reserve_pad_buffer(target_len)?);
1229        let padding_len = target_len - source.len();
1230        if pad_at_end {
1231            append_source(&mut output, &source, target_len)?;
1232            append_padding(&mut output, filler.as_ref(), padding_len, target_len)?;
1233        } else {
1234            append_padding(&mut output, filler.as_ref(), padding_len, target_len)?;
1235            append_source(&mut output, &source, target_len)?;
1236        }
1237
1238        Ok(match output {
1239            PadBuffer::Latin1(units) => {
1240                debug_assert_eq!(units.len(), target_len);
1241                Self(StringRepr::allocate(StringData::Latin1(units)))
1242            }
1243            PadBuffer::Utf16(units) => {
1244                debug_assert_eq!(units.len(), target_len);
1245                Self(StringRepr::allocate(StringData::Utf16(units)))
1246            }
1247        })
1248    }
1249
1250    /// Trim the selected ends with pinned QuickJS `js_string_trim` and
1251    /// `js_sub_string` representation rules. Full-range and empty results do
1252    /// not enter or consume the partial-substring reservation path; a partial
1253    /// wide range narrows when every retained UTF-16 code unit fits Latin-1.
1254    pub fn trim_whitespace(&self, trim_start: bool, trim_end: bool) -> Result<Self, JsStringError> {
1255        let source = self.linearize();
1256        let mut start = 0;
1257        let mut end = source.len();
1258        if trim_start {
1259            while start < end
1260                && source
1261                    .code_unit_at(start)
1262                    .is_some_and(is_ecmascript_whitespace)
1263            {
1264                start += 1;
1265            }
1266        }
1267        if trim_end {
1268            while end > start
1269                && source
1270                    .code_unit_at(end - 1)
1271                    .is_some_and(is_ecmascript_whitespace)
1272            {
1273                end -= 1;
1274            }
1275        }
1276        if start == 0 && end == source.len() {
1277            return Ok(source);
1278        }
1279        if start == end {
1280            // `js_new_string8_len(..., 0)` returns the canonical narrow empty
1281            // atom without performing the substring allocation.
1282            return Ok(Self::from_static(""));
1283        }
1284
1285        fn reserve_trim_buffer<T>(capacity: usize) -> Result<Vec<T>, JsStringError> {
1286            #[cfg(test)]
1287            if FAIL_NEXT_TRIM_RESERVATION.with(|armed| armed.replace(false)) {
1288                return Err(JsStringError::OutOfMemory);
1289            }
1290            let mut selected = Vec::new();
1291            selected
1292                .try_reserve_exact(capacity)
1293                .map_err(|_| JsStringError::OutOfMemory)?;
1294            Ok(selected)
1295        }
1296
1297        let selected_len = end - start;
1298        match source.0.data() {
1299            StringData::Latin1(units) => {
1300                let mut selected = reserve_trim_buffer(selected_len)?;
1301                selected.extend_from_slice(&units[start..end]);
1302                Ok(Self(StringRepr::allocate(StringData::Latin1(selected))))
1303            }
1304            StringData::Utf16(units) => {
1305                let range = &units[start..end];
1306                if range.iter().all(|unit| *unit <= u16::from(u8::MAX)) {
1307                    let mut selected = reserve_trim_buffer(selected_len)?;
1308                    selected.extend(range.iter().map(|unit| *unit as u8));
1309                    Ok(Self(StringRepr::allocate(StringData::Latin1(selected))))
1310                } else {
1311                    let mut selected = reserve_trim_buffer(selected_len)?;
1312                    selected.extend_from_slice(range);
1313                    Ok(Self(StringRepr::allocate(StringData::Utf16(selected))))
1314                }
1315            }
1316            StringData::Rope(_) => unreachable!("linearized String remained a rope"),
1317        }
1318    }
1319
1320    /// Concatenate with QuickJS's short-flat/rope thresholds, bounded depth,
1321    /// Fibonacci rebalance and 30-bit length cap.
1322    ///
1323    /// # Errors
1324    /// Returns [`JsStringError::TooLong`] when the result would exceed
1325    /// [`Self::MAX_LEN`] UTF-16 code units.
1326    pub fn try_concat(&self, other: &Self) -> Result<Self, JsStringError> {
1327        Self::checked_length(self.len(), other.len())?;
1328
1329        if other.is_flat() {
1330            if other.is_empty() {
1331                return Ok(self.clone());
1332            }
1333            if self.is_empty() && self.is_flat() {
1334                return Ok(other.clone());
1335            }
1336            if other.len() <= Self::ROPE_SHORT_LEN {
1337                if self.is_flat() {
1338                    if self.len() <= Self::ROPE_SHORT2_LEN {
1339                        return Ok(Self::flat_concat(self, other));
1340                    }
1341                    return Self::new_rope(self.clone(), other.clone());
1342                }
1343                if let StringData::Rope(rope) = self.0.data() {
1344                    let (left, right) = Self::rope_children(rope);
1345                    if right.is_flat() && right.len() <= Self::ROPE_SHORT_LEN {
1346                        let tail = Self::flat_concat(&right, other);
1347                        return Self::new_rope(left, tail);
1348                    }
1349                }
1350            }
1351        } else if self.is_flat() {
1352            if self.is_empty() {
1353                return Ok(other.clone());
1354            }
1355            if let StringData::Rope(rope) = other.0.data() {
1356                let (left, right) = Self::rope_children(rope);
1357                if left.is_flat() && left.len() <= Self::ROPE_SHORT_LEN {
1358                    let head = Self::flat_concat(self, &left);
1359                    return Self::new_rope(head, right);
1360                }
1361            }
1362        }
1363        Self::new_rope(self.clone(), other.clone())
1364    }
1365
1366    /// Append to an exclusively owned compatible flat buffer. Shared strings
1367    /// and ropes retain the ordinary concatenation representation rules. The
1368    /// same short-flat thresholds apply even to an exclusively owned buffer.
1369    ///
1370    /// # Errors
1371    /// Rejects lengths above `MAX_LEN` and reports reservation failure.
1372    pub fn concat_owned(mut self, other: &Self) -> Result<Self, JsStringError> {
1373        Self::checked_length(self.len(), other.len())?;
1374        if other.is_empty() && other.is_flat() {
1375            return Ok(self);
1376        }
1377        if self.is_empty() && self.is_flat() {
1378            return Ok(other.clone());
1379        }
1380        if self.try_concat_in_place(other)? {
1381            return Ok(self);
1382        }
1383        self.try_concat(other)
1384    }
1385
1386    /// Append only if this flat buffer has one strong owner and no weak owners.
1387    /// Reservation precedes every mutation: a failed allocation leaves the
1388    /// original local binding intact. A declined append likewise changes nothing.
1389    pub(crate) fn try_concat_in_place(&mut self, other: &Self) -> Result<bool, JsStringError> {
1390        fn reserve<T>(buffer: &mut Vec<T>, additional: usize) -> Result<(), JsStringError> {
1391            #[cfg(test)]
1392            if FAIL_NEXT_CONCAT_RESERVATION.with(|armed| armed.replace(false)) {
1393                return Err(JsStringError::OutOfMemory);
1394            }
1395            buffer
1396                .try_reserve(additional)
1397                .map_err(|_| JsStringError::OutOfMemory)
1398        }
1399        Self::checked_length(self.len(), other.len())?;
1400        // QuickJS exposes rope/flat distinctions in host value printing. Keep
1401        // its flat-concat admission even when we own spare buffer capacity.
1402        if self.len() > Self::ROPE_SHORT2_LEN || other.len() > Self::ROPE_SHORT_LEN {
1403            return Ok(false);
1404        }
1405
1406        if let Some(repr) = Rc::get_mut(&mut self.0) {
1407            // Only this exclusive mutation can change code units. Shared rope
1408            // linearization preserves content and therefore preserves its hash.
1409            repr.content_hash.set(None);
1410            match (&mut repr.data, other.0.data()) {
1411                (StringData::Latin1(left), StringData::Latin1(right)) => {
1412                    reserve(left, right.len())?;
1413                    left.extend_from_slice(right);
1414                    return Ok(true);
1415                }
1416                (StringData::Utf16(left), StringData::Utf16(right)) => {
1417                    reserve(left, right.len())?;
1418                    left.extend_from_slice(right);
1419                    return Ok(true);
1420                }
1421                (StringData::Utf16(left), StringData::Latin1(right)) => {
1422                    reserve(left, right.len())?;
1423                    left.extend(right.iter().copied().map(u16::from));
1424                    return Ok(true);
1425                }
1426                _ => {}
1427            }
1428        }
1429        Ok(false)
1430    }
1431
1432    /// Encode the byte slice exposed by QuickJS `JS_ToCStringLen2` when its
1433    /// `cesu8` flag is false.
1434    ///
1435    /// Valid surrogate pairs become standard four-byte UTF-8. Unpaired
1436    /// surrogates are deliberately retained as three-byte WTF-8 sequences.
1437    /// Embedded U+0000 is returned as an ordinary zero byte; the terminating C
1438    /// NUL owned by QuickJS is not part of its reported length and is therefore
1439    /// not included in this safe Rust byte vector. As in QuickJS, converting a
1440    /// rope caches its flat representation on the shared String identity.
1441    ///
1442    /// # Errors
1443    /// Returns [`TryReserveError`] if the output buffer cannot be reserved.
1444    pub fn try_to_wtf8_bytes(&self) -> Result<Vec<u8>, TryReserveError> {
1445        self.try_to_quickjs_utf8_bytes(false)
1446    }
1447
1448    /// Append the WTF-8 bytes exposed by `JS_ToCStringLen2(..., false)` to an
1449    /// existing buffer while preserving the same rope-linearization side
1450    /// effect as [`Self::try_to_wtf8_bytes`].
1451    pub fn try_append_wtf8_bytes(&self, output: &mut Vec<u8>) -> Result<(), TryReserveError> {
1452        self.try_append_quickjs_utf8_bytes(output, false)
1453    }
1454
1455    /// Encode the byte slice exposed by QuickJS `JS_ToCStringLen2` when its
1456    /// `cesu8` flag is true.
1457    ///
1458    /// Every UTF-16 code unit is encoded independently, so a valid surrogate
1459    /// pair occupies two three-byte CESU-8 sequences. Embedded U+0000 is
1460    /// retained and no trailing C NUL is included in the returned vector. Rope
1461    /// conversion has the same shared flat-cache transition as the WTF-8 path.
1462    ///
1463    /// # Errors
1464    /// Returns [`TryReserveError`] if the output buffer cannot be reserved.
1465    pub fn try_to_cesu8_bytes(&self) -> Result<Vec<u8>, TryReserveError> {
1466        self.try_to_quickjs_utf8_bytes(true)
1467    }
1468
1469    fn try_to_quickjs_utf8_bytes(&self, cesu8: bool) -> Result<Vec<u8>, TryReserveError> {
1470        let mut output = Vec::new();
1471        self.try_append_quickjs_utf8_bytes(&mut output, cesu8)?;
1472        Ok(output)
1473    }
1474
1475    fn try_append_quickjs_utf8_bytes(
1476        &self,
1477        output: &mut Vec<u8>,
1478        cesu8: bool,
1479    ) -> Result<(), TryReserveError> {
1480        // JS_ToCStringLen2 first rewrites a rope to its cached flat form. The
1481        // root keeps its rope identity, but later diagnostics observe one
1482        // logical leaf instead of the former tree boundary.
1483        let value = self.linearize();
1484        let encoded_len = quickjs_utf8_length(value.utf16_units(), cesu8);
1485        let initial_len = output.len();
1486        output.try_reserve_exact(encoded_len)?;
1487        output.extend(QuickJsUtf8Bytes::new(&value, cesu8));
1488        debug_assert_eq!(output.len(), initial_len + encoded_len);
1489        Ok(())
1490    }
1491
1492    pub fn wtf8_bytes(&self) -> impl Iterator<Item = u8> {
1493        QuickJsUtf8Bytes::new(self, false)
1494    }
1495
1496    pub fn push_c_string_to(&self, output: &mut NativeErrorMessage) {
1497        output.push_c_string_bytes(self.wtf8_bytes());
1498    }
1499
1500    /// Reproduce `JS_AtomGetStr(..., char buf[64], 64)` as consumed by a C
1501    /// `%s` native-error argument. Narrow all-ASCII atoms bypass the scratch
1502    /// buffer; every other text atom is encoded one UTF-16 code unit at a time
1503    /// and stops before starting a unit once 58 bytes have already been written.
1504    pub fn push_atom_get_str_to(&self, output: &mut NativeErrorMessage) {
1505        let atom = self.linearize();
1506        if !atom.is_wide() && atom.utf16_units().all(|unit| unit < 0x80) {
1507            atom.push_c_string_to(output);
1508            return;
1509        }
1510
1511        const ATOM_BUFFER_SIZE: usize = 64;
1512        const UTF8_CHAR_LEN_MAX: usize = 6;
1513        let mut scratch = [0_u8; ATOM_BUFFER_SIZE];
1514        let mut len = 0;
1515        for unit in atom.utf16_units() {
1516            if len >= ATOM_BUFFER_SIZE - UTF8_CHAR_LEN_MAX {
1517                break;
1518            }
1519            let mut encoded = [0_u8; 4];
1520            let encoded_len = encode_quickjs_utf8(&mut encoded, u32::from(unit));
1521            scratch[len..len + encoded_len].copy_from_slice(&encoded[..encoded_len]);
1522            len += encoded_len;
1523        }
1524        output.push_c_string_bytes(scratch[..len].iter().copied());
1525    }
1526
1527    /// Lossy conversion is suitable for terminal diagnostics. It must not be
1528    /// used for language-level string comparison or indexing.
1529    #[must_use]
1530    pub fn to_utf8_lossy(&self) -> String {
1531        match self.0.data() {
1532            StringData::Latin1(units) if units.is_ascii() => {
1533                String::from_utf8(units.clone()).expect("ASCII is UTF-8")
1534            }
1535            StringData::Latin1(units) => units.iter().map(|&unit| char::from(unit)).collect(),
1536            StringData::Utf16(units) => String::from_utf16_lossy(units),
1537            StringData::Rope(_) => char::decode_utf16(self.utf16_units())
1538                .map(|result| result.unwrap_or(char::REPLACEMENT_CHARACTER))
1539                .collect(),
1540        }
1541    }
1542}
1543
1544pub fn decode_quickjs_utf8(value: &[u8]) -> Option<(u32, usize)> {
1545    let first = *value.first()?;
1546    if first < 0x80 {
1547        return Some((u32::from(first), 1));
1548    }
1549
1550    let (continuation_count, mask, minimum): (usize, u8, u32) = match first {
1551        0x00..=0x7f => unreachable!("ASCII byte escaped the fast path"),
1552        0xc0..=0xdf => (1, 0x1f, 0x80),
1553        0xe0..=0xef => (2, 0x0f, 0x800),
1554        0xf0..=0xf7 => (3, 0x07, 0x1_0000),
1555        0xf8..=0xfb => (4, 0x03, 0x20_0000),
1556        0xfc..=0xfd => (5, 0x01, 0x400_0000),
1557        0x80..=0xbf | 0xfe..=0xff => return None,
1558    };
1559    if value.len() <= continuation_count {
1560        return None;
1561    }
1562
1563    let mut code_point = u32::from(first & mask);
1564    for &byte in &value[1..=continuation_count] {
1565        if !(0x80..=0xbf).contains(&byte) {
1566            return None;
1567        }
1568        code_point = (code_point << 6) | u32::from(byte & 0x3f);
1569    }
1570    (code_point >= minimum).then_some((code_point, continuation_count + 1))
1571}
1572
1573fn skip_quickjs_invalid_utf8(value: &[u8], mut index: usize) -> usize {
1574    while value
1575        .get(index)
1576        .is_some_and(|byte| (0x80..=0xbf).contains(byte))
1577    {
1578        index += 1;
1579    }
1580    if index < value.len() {
1581        index += 1;
1582        while value
1583            .get(index)
1584            .is_some_and(|byte| (0x80..=0xbf).contains(byte))
1585        {
1586            index += 1;
1587        }
1588    }
1589    index
1590}
1591
1592fn quickjs_utf8_length(units: impl Iterator<Item = u16>, cesu8: bool) -> usize {
1593    let mut units = units.peekable();
1594    let mut length = 0;
1595    while let Some(unit) = units.next() {
1596        if !cesu8
1597            && (0xd800..=0xdbff).contains(&unit)
1598            && units
1599                .peek()
1600                .is_some_and(|next| (0xdc00..=0xdfff).contains(next))
1601        {
1602            units.next();
1603            length += 4;
1604        } else {
1605            length += match unit {
1606                0x0000..=0x007f => 1,
1607                0x0080..=0x07ff => 2,
1608                0x0800..=0xffff => 3,
1609            };
1610        }
1611    }
1612    length
1613}
1614
1615fn encode_quickjs_utf8(output: &mut [u8; 4], code_point: u32) -> usize {
1616    match code_point {
1617        0x0000..=0x007f => {
1618            output[0] = code_point as u8;
1619            1
1620        }
1621        0x0080..=0x07ff => {
1622            output[0] = (0xc0 | (code_point >> 6)) as u8;
1623            output[1] = (0x80 | (code_point & 0x3f)) as u8;
1624            2
1625        }
1626        0x0800..=0xffff => {
1627            output[0] = (0xe0 | (code_point >> 12)) as u8;
1628            output[1] = (0x80 | ((code_point >> 6) & 0x3f)) as u8;
1629            output[2] = (0x80 | (code_point & 0x3f)) as u8;
1630            3
1631        }
1632        0x1_0000..=0x10_ffff => {
1633            output[0] = (0xf0 | (code_point >> 18)) as u8;
1634            output[1] = (0x80 | ((code_point >> 12) & 0x3f)) as u8;
1635            output[2] = (0x80 | ((code_point >> 6) & 0x3f)) as u8;
1636            output[3] = (0x80 | (code_point & 0x3f)) as u8;
1637            4
1638        }
1639        _ => unreachable!("QuickJS byte encoder received an invalid code point"),
1640    }
1641}
1642
1643impl PartialEq for JsString {
1644    fn eq(&self, other: &Self) -> bool {
1645        if Rc::ptr_eq(&self.0, &other.0) {
1646            return true;
1647        }
1648        if self.len() != other.len() {
1649            return false;
1650        }
1651        // Flat strings need neither owned traversal handles nor a rope stack.
1652        // Width is a representation choice, so mixed-width equality compares
1653        // exact code units rather than treating Latin-1 as UTF-8.
1654        match (self.0.data(), other.0.data()) {
1655            (StringData::Latin1(left), StringData::Latin1(right)) => left == right,
1656            (StringData::Utf16(left), StringData::Utf16(right)) => left == right,
1657            (StringData::Latin1(narrow), StringData::Utf16(wide))
1658            | (StringData::Utf16(wide), StringData::Latin1(narrow)) => narrow
1659                .iter()
1660                .copied()
1661                .map(u16::from)
1662                .eq(wide.iter().copied()),
1663            _ => self.utf16_units().eq(other.utf16_units()),
1664        }
1665    }
1666}
1667
1668impl Eq for JsString {}
1669
1670impl Hash for JsString {
1671    fn hash<H: Hasher>(&self, state: &mut H) {
1672        state.write_u32(self.content_hash());
1673    }
1674}
1675
1676impl fmt::Debug for JsString {
1677    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1678        formatter
1679            .debug_tuple("JsString")
1680            .field(&self.to_utf8_lossy())
1681            .finish()
1682    }
1683}
1684
1685impl fmt::Display for JsString {
1686    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1687        formatter.write_str(&self.to_utf8_lossy())
1688    }
1689}
1690
1691/// A constant-pool value of the front end. Numbers follow `QuickJS`'s split
1692/// between immediate 32-bit integers and IEEE-754 doubles.
1693#[derive(Clone, Debug)]
1694pub enum PrimitiveValue {
1695    Int(i32),
1696    Float(f64),
1697    BigInt(JsBigInt),
1698    String(JsString),
1699}
1700
1701impl PrimitiveValue {
1702    #[must_use]
1703    pub fn number(value: f64) -> Self {
1704        if value == f64::from(value as i32) && !(value == 0.0 && value.is_sign_negative()) {
1705            Self::Int(value as i32)
1706        } else {
1707            Self::Float(value)
1708        }
1709    }
1710
1711    #[must_use]
1712    pub const fn as_number(&self) -> Option<f64> {
1713        match self {
1714            Self::Int(value) => Some(*value as f64),
1715            Self::Float(value) => Some(*value),
1716            _ => None,
1717        }
1718    }
1719
1720    /// Apply ECMAScript `ToString` to the value kinds implemented by the
1721    /// runtime kernel.
1722    ///
1723    /// # Errors
1724    /// Symbol conversion throws, an extended-limit BigInt can fail the pinned
1725    /// QuickJS decimal-conversion allocation guard, and object conversion must
1726    /// be routed through a context so `ToPrimitive` can execute user code.
1727    pub fn to_js_string(&self) -> Result<JsString, Error> {
1728        let text = match self {
1729            Self::Int(value) => value.to_string(),
1730            Self::Float(value) => number_to_string(*value),
1731            Self::BigInt(value) => {
1732                if value.exceeds_allocation_limit() {
1733                    return Err(Error::new(
1734                        ErrorKind::Range,
1735                        "BigInt is too large to allocate",
1736                    ));
1737                }
1738                value.to_string()
1739            }
1740            Self::String(value) => return Ok(value.linearize()),
1741        };
1742        Ok(JsString::try_from_utf8(&text)?)
1743    }
1744
1745    #[must_use]
1746    #[expect(clippy::float_cmp)]
1747    pub fn strict_equal(&self, other: &Self) -> bool {
1748        match (self, other) {
1749            (Self::String(left), Self::String(right)) => left == right,
1750            (Self::BigInt(left), Self::BigInt(right)) => left == right,
1751            (Self::Int(left), Self::Int(right)) => left == right,
1752            (left, right) => match (left.as_number(), right.as_number()) {
1753                (Some(left), Some(right)) => left == right,
1754                _ => false,
1755            },
1756        }
1757    }
1758}
1759
1760/// `Number::toString(value, 10)`, as the core prints it.
1761#[must_use]
1762pub fn number_to_string(value: f64) -> String {
1763    oxide_core::vm::radix::to_string_radix(value, 10)
1764}
1765
1766pub(super) const fn is_ecmascript_whitespace(unit: u16) -> bool {
1767    matches!(
1768        unit,
1769        0x0009..=0x000d
1770            | 0x0020
1771            | 0x00a0
1772            | 0x1680
1773            | 0x2000..=0x200a
1774            | 0x2028
1775            | 0x2029
1776            | 0x202f
1777            | 0x205f
1778            | 0x3000
1779            | 0xfeff
1780    )
1781}
1782
1783impl PartialEq for PrimitiveValue {
1784    fn eq(&self, other: &Self) -> bool {
1785        self.strict_equal(other)
1786    }
1787}
1788
1789#[cfg(test)]
1790mod tests {
1791    use std::cell::Cell;
1792    use std::collections::hash_map::DefaultHasher;
1793
1794    use std::hash::{Hash, Hasher};
1795    use std::rc::Rc;
1796
1797    use super::{
1798        JsString, JsStringError, PrimitiveValue, StringData, StringRepr, number_to_string,
1799    };
1800    use crate::engine::api::error::{Error, ErrorKind};
1801
1802    #[test]
1803    fn cached_content_hash_survives_linearization_and_invalidates_on_append() {
1804        let mut flat = JsString::from_static("abc");
1805        assert_eq!(
1806            flat.content_hash(),
1807            JsString::from_static("abc").content_hash()
1808        );
1809        assert!(flat.0.content_hash.get().is_some());
1810        assert!(
1811            flat.try_concat_in_place(&JsString::from_static("def"))
1812                .unwrap()
1813        );
1814        assert_eq!(
1815            flat.content_hash(),
1816            JsString::from_static("abcdef").content_hash()
1817        );
1818        let left = JsString::from_owned_latin1(vec![b'a'; 1024]);
1819        let right = JsString::from_owned_utf16(vec![0xd800; 1024]);
1820        let rope = left.try_concat(&right).unwrap();
1821        let hash = rope.content_hash();
1822        let flat = rope.linearize();
1823        assert_eq!(flat.content_hash(), hash);
1824        assert_eq!(rope.content_hash(), hash);
1825        assert!(rope.same_representation(&rope.clone()));
1826    }
1827
1828    #[test]
1829    fn utf16_nth_skips_leaf_and_rope_ranges() {
1830        let left = JsString::from_owned_latin1(vec![b'a'; 1024]);
1831        let right = JsString::from_owned_utf16(vec![0x1234; 1024]);
1832        let joined = left.try_concat(&right).unwrap();
1833        assert!(matches!(joined.0.data(), StringData::Rope(_)));
1834        for source in [&left, &joined] {
1835            let expected = source.utf16_units().collect::<Vec<_>>();
1836            for skip in 0..=expected.len() + 1 {
1837                let mut iter = source.utf16_units();
1838                assert_eq!(iter.nth(skip), expected.get(skip).copied());
1839                assert_eq!(
1840                    iter.collect::<Vec<_>>(),
1841                    expected.get(skip.saturating_add(1)..).unwrap_or(&[])
1842                );
1843            }
1844        }
1845    }
1846
1847    #[test]
1848    fn owned_concat_preserves_aliases_width_identity_and_rope_content() {
1849        let empty = JsString::from_static("");
1850        let narrow = JsString::from_owned_latin1(b"abc".to_vec());
1851        assert!(Rc::ptr_eq(&empty.try_concat(&narrow).unwrap().0, &narrow.0));
1852        assert!(Rc::ptr_eq(&narrow.try_concat(&empty).unwrap().0, &narrow.0));
1853        let identity = Rc::as_ptr(&narrow.0);
1854        let grown = narrow.concat_owned(&JsString::from_static("d")).unwrap();
1855        assert_eq!(Rc::as_ptr(&grown.0), identity);
1856        assert_eq!(grown.to_string(), "abcd");
1857        let alias = grown.clone();
1858        let copied = grown.concat_owned(&JsString::from_static("e")).unwrap();
1859        assert_eq!(alias.to_string(), "abcd");
1860        assert_eq!(copied.to_string(), "abcde");
1861        assert!(!Rc::ptr_eq(&alias.0, &copied.0));
1862        let wide = JsString::from_owned_utf16(vec![0xd800]);
1863        let identity = Rc::as_ptr(&wide.0);
1864        let wide = wide.concat_owned(&JsString::from_static("x")).unwrap();
1865        assert_eq!(Rc::as_ptr(&wide.0), identity);
1866        assert_eq!(wide.utf16_units().collect::<Vec<_>>(), vec![0xd800, 120]);
1867        let wide = wide
1868            .concat_owned(&JsString::from_owned_utf16(vec![0xdc00]))
1869            .unwrap();
1870        assert_eq!(Rc::as_ptr(&wide.0), identity);
1871        let widened = JsString::from_static("a").concat_owned(&wide).unwrap();
1872        assert_eq!(
1873            widened.utf16_units().collect::<Vec<_>>(),
1874            vec![97, 0xd800, 120, 0xdc00]
1875        );
1876        let weak_owner = JsString::from_owned_latin1(b"weak".to_vec());
1877        let weak = Rc::downgrade(&weak_owner.0);
1878        let identity = Rc::as_ptr(&weak_owner.0);
1879        let weak_result = weak_owner
1880            .concat_owned(&JsString::from_static("!"))
1881            .unwrap();
1882        assert_ne!(Rc::as_ptr(&weak_result.0), identity);
1883        assert!(weak.upgrade().is_none());
1884        let suffix = JsString::from_static("xy");
1885        let mut owned = JsString::from_owned_latin1(b"start".to_vec());
1886        let identity = Rc::as_ptr(&owned.0);
1887        let mut reference = owned.clone();
1888        // Release the temporary alias before beginning owned append.
1889        reference = reference.try_concat(&suffix).unwrap();
1890        owned = owned.concat_owned(&suffix).unwrap();
1891        for _ in 0..100 {
1892            owned = owned.concat_owned(&suffix).unwrap();
1893            reference = reference.try_concat(&suffix).unwrap();
1894        }
1895        assert_eq!(Rc::as_ptr(&owned.0), identity);
1896        assert!(owned.is_flat());
1897        for _ in 0..8900 {
1898            owned = owned.concat_owned(&suffix).unwrap();
1899            reference = reference.try_concat(&suffix).unwrap();
1900        }
1901        assert_eq!(owned, reference);
1902        assert_eq!(content_hash(&owned), content_hash(&reference));
1903        assert!(!owned.is_flat());
1904        assert!(!reference.is_flat());
1905        let rope = reference.clone();
1906        let appended = reference.concat_owned(&suffix).unwrap();
1907        assert_eq!(appended, rope.try_concat(&suffix).unwrap());
1908    }
1909
1910    #[test]
1911    fn owned_concat_preserves_observable_flat_rope_thresholds() {
1912        for (left_len, right_len, expected_in_place) in [
1913            (600, 600, false),
1914            (8192, 512, true),
1915            (8193, 512, false),
1916            (8192, 513, false),
1917        ] {
1918            let mut left = JsString::from_owned_latin1(vec![b'a'; left_len]);
1919            let right = JsString::from_owned_latin1(vec![b'b'; right_len]);
1920            let identity = Rc::as_ptr(&left.0);
1921            assert_eq!(left.try_concat_in_place(&right).unwrap(), expected_in_place);
1922            assert_eq!(Rc::as_ptr(&left.0), identity);
1923            if expected_in_place {
1924                assert!(left.is_flat());
1925                assert_eq!(left.len(), left_len + right_len);
1926            } else {
1927                assert_eq!(left.len(), left_len);
1928                let result = left.concat_owned(&right).unwrap();
1929                assert!(!result.is_flat());
1930                assert_eq!(result.len(), left_len + right_len);
1931            }
1932        }
1933    }
1934
1935    fn content_hash(value: &JsString) -> u64 {
1936        let mut state = DefaultHasher::new();
1937        value.hash(&mut state);
1938        state.finish()
1939    }
1940
1941    #[test]
1942    fn tagged_atom_decimal_strings_are_narrow_and_fresh() {
1943        for (value, expected) in [(0, "0"), (42, "42"), (2_147_483_647, "2147483647")] {
1944            let left = JsString::from_fresh_decimal_u32(value);
1945            let right = JsString::from_fresh_decimal_u32(value);
1946            assert_eq!(left, JsString::from_static(expected));
1947            assert_eq!(right, left);
1948            assert!(!left.same_representation(&right));
1949            assert!(matches!(left.0.data(), StringData::Latin1(_)));
1950        }
1951    }
1952
1953    #[test]
1954    fn checked_string_construction_rejects_limits_and_hostile_hints_without_overpolling() {
1955        struct PanicOnPoll;
1956        impl Iterator for PanicOnPoll {
1957            type Item = u16;
1958
1959            fn next(&mut self) -> Option<Self::Item> {
1960                panic!("oversized lower bound should reject before polling")
1961            }
1962
1963            fn size_hint(&self) -> (usize, Option<usize>) {
1964                (usize::MAX, None)
1965            }
1966        }
1967
1968        struct LateOverflow {
1969            polls: Rc<Cell<usize>>,
1970        }
1971        impl Iterator for LateOverflow {
1972            type Item = u16;
1973
1974            fn next(&mut self) -> Option<Self::Item> {
1975                let poll = self.polls.get();
1976                self.polls.set(poll + 1);
1977                match poll {
1978                    0..=3 => Some(u16::from(b'a') + u16::try_from(poll).unwrap()),
1979                    _ => panic!("constructor polled after observing the overflowing unit"),
1980                }
1981            }
1982
1983            fn size_hint(&self) -> (usize, Option<usize>) {
1984                (0, Some(0))
1985            }
1986        }
1987
1988        assert_eq!(JsString::MAX_LEN, 0x3fff_ffff);
1989        assert_eq!(
1990            JsString::checked_length(JsString::MAX_LEN - 1, 1),
1991            Ok(JsString::MAX_LEN)
1992        );
1993        assert_eq!(
1994            JsString::checked_length(JsString::MAX_LEN, 1),
1995            Err(JsStringError::TooLong)
1996        );
1997        assert_eq!(
1998            JsString::checked_length(usize::MAX, 1),
1999            Err(JsStringError::TooLong)
2000        );
2001        assert_eq!(
2002            JsString::try_from_utf16_with_limit(PanicOnPoll, 3),
2003            Err(JsStringError::TooLong)
2004        );
2005
2006        struct UpperHint<I>(I);
2007        impl<I: Iterator<Item = u16>> Iterator for UpperHint<I> {
2008            type Item = u16;
2009
2010            fn next(&mut self) -> Option<Self::Item> {
2011                self.0.next()
2012            }
2013
2014            fn size_hint(&self) -> (usize, Option<usize>) {
2015                (0, Some(usize::MAX))
2016            }
2017        }
2018        let accepted =
2019            JsString::try_from_utf16_with_limit(UpperHint([0x61, 0x100].into_iter()), 2).unwrap();
2020        assert_eq!(accepted.utf16_units().collect::<Vec<_>>(), [0x61, 0x100]);
2021
2022        let polls = Rc::new(Cell::new(0));
2023        assert_eq!(
2024            JsString::try_from_utf16_with_limit(
2025                LateOverflow {
2026                    polls: polls.clone(),
2027                },
2028                3,
2029            ),
2030            Err(JsStringError::TooLong)
2031        );
2032        assert_eq!(polls.get(), 4);
2033        assert!(JsString::try_from_utf16_with_limit([], 0).is_ok());
2034        assert_eq!(
2035            JsString::try_from_utf16_with_limit([0x61], 0),
2036            Err(JsStringError::TooLong)
2037        );
2038        assert!(JsString::try_from_utf16_with_limit("😀".encode_utf16(), 2).is_ok());
2039        assert_eq!(
2040            JsString::try_from_utf16_with_limit("😀".encode_utf16(), 1),
2041            Err(JsStringError::TooLong)
2042        );
2043    }
2044
2045    #[test]
2046    fn quickjs_byte_constructor_preserves_wtf8_and_invalid_skip_rules() {
2047        let cases = [
2048            (vec![], vec![]),
2049            (vec![0x00, 0x41], vec![0x0000, 0x0041]),
2050            (vec![0xc3, 0xa9], vec![0x00e9]),
2051            (vec![0xc4, 0x80], vec![0x0100]),
2052            (vec![0xed, 0xa0, 0x80], vec![0xd800]),
2053            (vec![0xed, 0xb0, 0x80], vec![0xdc00]),
2054            (vec![0xf0, 0x9f, 0x98, 0x80], vec![0xd83d, 0xde00]),
2055            (vec![0x80, 0x41], vec![0xfffd]),
2056            (vec![0xff, 0x41], vec![0xfffd, 0x0041]),
2057            (vec![0x80, 0x80, 0x41, 0x80, 0x42], vec![0xfffd, 0x0042]),
2058            (vec![0x80, 0xc2, 0xa2], vec![0xfffd]),
2059            (vec![0xe2, 0x28, 0xa1], vec![0xfffd, 0x0028, 0xfffd]),
2060            (vec![0xe2, 0x82], vec![0xfffd]),
2061            (vec![0xc0, 0x80, 0x41], vec![0xfffd, 0x0041]),
2062            (vec![0xf4, 0x90, 0x80, 0x80, 0x41], vec![0xfffd, 0x0041]),
2063            (vec![0xfe, 0x80, 0x41], vec![0xfffd, 0x0041]),
2064            (vec![0xf8, 0x88, 0x80, 0x80, 0x80], vec![0xfffd]),
2065        ];
2066        for (bytes, expected) in cases {
2067            let actual = JsString::try_from_bytes(&bytes).unwrap();
2068            assert_eq!(
2069                actual.utf16_units().collect::<Vec<_>>(),
2070                expected,
2071                "{bytes:02x?}"
2072            );
2073        }
2074
2075        assert!(JsString::try_from_bytes_with_limit(&[0xf0, 0x9f, 0x98, 0x80], 2).is_ok());
2076        assert_eq!(
2077            JsString::try_from_bytes_with_limit(&[0xf0, 0x9f, 0x98, 0x80], 1),
2078            Err(JsStringError::TooLong)
2079        );
2080        assert!(JsString::try_from_bytes_with_limit(&[0x80, 0x41], 1).is_ok());
2081        assert_eq!(
2082            JsString::try_from_bytes_with_limit(&[0xff, 0x41], 1),
2083            Err(JsStringError::TooLong)
2084        );
2085        assert_eq!(
2086            JsString::try_from_bytes_with_limit(b"ab", 1),
2087            Err(JsStringError::TooLong)
2088        );
2089    }
2090
2091    #[test]
2092    fn quickjs_byte_exports_distinguish_wtf8_from_cesu8_and_roundtrip_utf16() {
2093        let value = JsString::try_from_utf16([
2094            0x0041, 0x0000, 0x00e9, 0x0800, 0xd800, 0x0042, 0xdc00, 0xd83d, 0xde00,
2095        ])
2096        .unwrap();
2097        assert_eq!(
2098            value.try_to_wtf8_bytes().unwrap(),
2099            [
2100                0x41, 0x00, 0xc3, 0xa9, 0xe0, 0xa0, 0x80, 0xed, 0xa0, 0x80, 0x42, 0xed, 0xb0, 0x80,
2101                0xf0, 0x9f, 0x98, 0x80,
2102            ]
2103        );
2104        assert_eq!(
2105            value.try_to_cesu8_bytes().unwrap(),
2106            [
2107                0x41, 0x00, 0xc3, 0xa9, 0xe0, 0xa0, 0x80, 0xed, 0xa0, 0x80, 0x42, 0xed, 0xb0, 0x80,
2108                0xed, 0xa0, 0xbd, 0xed, 0xb8, 0x80,
2109            ]
2110        );
2111
2112        let all_units = JsString::try_from_utf16(0_u16..=u16::MAX).unwrap();
2113        for bytes in [
2114            all_units.try_to_wtf8_bytes().unwrap(),
2115            all_units.try_to_cesu8_bytes().unwrap(),
2116        ] {
2117            assert_eq!(JsString::try_from_bytes(&bytes).unwrap(), all_units);
2118        }
2119
2120        let high = JsString::try_from_utf16([0xd83d]).unwrap();
2121        let low_and_tail = JsString::try_from_utf16(
2122            [0xde00]
2123                .into_iter()
2124                .chain(std::iter::repeat_n(u16::from(b'b'), 512)),
2125        )
2126        .unwrap();
2127        let across_leaf = JsString::try_from_utf8(&"a".repeat(8193))
2128            .unwrap()
2129            .try_concat(&high)
2130            .unwrap()
2131            .try_concat(&low_and_tail)
2132            .unwrap();
2133        assert!(!across_leaf.is_flat());
2134        let mut leaf_lengths = Vec::new();
2135        across_leaf.for_each_flat_leaf(|leaf| leaf_lengths.push(leaf.len()));
2136        assert_eq!(leaf_lengths, [8193, 1, 513]);
2137        let flat = JsString::try_from_utf16(
2138            std::iter::repeat_n(u16::from(b'a'), 8193)
2139                .chain([0xd83d, 0xde00])
2140                .chain(std::iter::repeat_n(u16::from(b'b'), 512)),
2141        )
2142        .unwrap();
2143        let wtf8 = across_leaf.try_to_wtf8_bytes().unwrap();
2144        assert_eq!(&wtf8[8193..8197], &[0xf0, 0x9f, 0x98, 0x80]);
2145        assert_eq!(wtf8, flat.try_to_wtf8_bytes().unwrap());
2146        leaf_lengths.clear();
2147        across_leaf.for_each_flat_leaf(|leaf| leaf_lengths.push(leaf.len()));
2148        assert_eq!(leaf_lengths, [across_leaf.len()]);
2149        let cesu8 = across_leaf.try_to_cesu8_bytes().unwrap();
2150        assert_eq!(&cesu8[8193..8199], &[0xed, 0xa0, 0xbd, 0xed, 0xb8, 0x80]);
2151        assert_eq!(cesu8, flat.try_to_cesu8_bytes().unwrap());
2152    }
2153
2154    #[test]
2155    fn string_length_counts_utf16_code_units() {
2156        let text = JsString::from_static("a🚀");
2157        assert_eq!(text.len(), 3);
2158        assert_eq!(
2159            text.utf16_units().collect::<Vec<_>>(),
2160            vec![0x61, 0xd83d, 0xde80]
2161        );
2162    }
2163
2164    #[test]
2165    fn strings_preserve_lone_surrogates() {
2166        let text = JsString::try_from_utf16([0xd800, 0x61]).unwrap();
2167        assert_eq!(text.utf16_units().collect::<Vec<_>>(), vec![0xd800, 0x61]);
2168        assert_eq!(text.to_utf8_lossy(), "�a");
2169    }
2170
2171    #[test]
2172    fn utf16_index_code_point_and_well_formed_helpers_preserve_quickjs_rules() {
2173        let text = JsString::try_from_utf16([0x41, 0xd83d, 0xde80, 0xd800, 0x42, 0xdc00]).unwrap();
2174        assert_eq!(text.code_unit_at(0), Some(0x41));
2175        assert_eq!(text.code_unit_at(6), None);
2176        assert_eq!(text.code_point_at(1), Some(0x1f680));
2177        assert_eq!(text.code_point_at(2), Some(0xde80));
2178        assert_eq!(text.code_point_at(3), Some(0xd800));
2179        assert!(!text.is_well_formed());
2180        assert_eq!(
2181            text.to_well_formed().utf16_units().collect::<Vec<_>>(),
2182            vec![0x41, 0xd83d, 0xde80, 0xfffd, 0x42, 0xfffd]
2183        );
2184
2185        let well_formed = JsString::try_from_utf16([0xd83d, 0xde80]).unwrap();
2186        assert!(well_formed.is_well_formed());
2187        assert_eq!(well_formed.to_well_formed(), well_formed);
2188    }
2189
2190    #[test]
2191    fn sub_string_linearizes_ranges_and_preserves_quickjs_width_rules() {
2192        let latin1 = JsString::from_static("abcdef");
2193        let latin1_full = latin1.sub_string(0, latin1.len());
2194        assert!(latin1_full.same_representation(&latin1));
2195        let latin1_range = latin1.sub_string(1, 4);
2196        assert_eq!(latin1_range, JsString::from_static("bcd"));
2197        assert!(matches!(latin1_range.0.data(), StringData::Latin1(_)));
2198
2199        let forced_wide = JsString(StringRepr::allocate(StringData::Utf16(
2200            [u16::from(b'a'), u16::from(b'b'), 0xd800, u16::from(b'c')]
2201                .into_iter()
2202                .collect::<Vec<_>>(),
2203        )));
2204        let narrowed = forced_wide.sub_string(0, 2);
2205        assert!(matches!(narrowed.0.data(), StringData::Latin1(_)));
2206        assert_eq!(
2207            narrowed.utf16_units().collect::<Vec<_>>(),
2208            [u16::from(b'a'), u16::from(b'b')]
2209        );
2210        let still_wide = forced_wide.sub_string(1, 3);
2211        assert!(matches!(still_wide.0.data(), StringData::Utf16(_)));
2212        assert_eq!(
2213            still_wide.utf16_units().collect::<Vec<_>>(),
2214            [u16::from(b'b'), 0xd800]
2215        );
2216        let empty = forced_wide.sub_string(2, 2);
2217        assert!(matches!(empty.0.data(), StringData::Latin1(units) if units.is_empty()));
2218
2219        let left = JsString::try_from_utf8(&"x".repeat(8_193)).unwrap();
2220        let right = JsString::try_from_utf16([0xd83d, 0xde00, u16::from(b'z')]).unwrap();
2221        let rope = left.try_concat(&right).unwrap();
2222        assert!(!rope.is_flat());
2223        let full = rope.sub_string(0, rope.len());
2224        assert!(full.is_flat());
2225        assert_eq!(full, rope);
2226        let cached_flat = rope.linearize();
2227        assert!(full.same_representation(&cached_flat));
2228        let crossed = rope.sub_string(8_193, 8_196);
2229        assert!(crossed.is_flat());
2230        assert_eq!(
2231            crossed.utf16_units().collect::<Vec<_>>(),
2232            [0xd83d, 0xde00, u16::from(b'z')]
2233        );
2234    }
2235
2236    #[test]
2237    fn repeat_builds_one_flat_string_with_quickjs_width_and_limit_rules() {
2238        let latin1 = JsString::from_static("ab");
2239        let repeated = latin1.repeat_with_limit(3, JsString::MAX_LEN).unwrap();
2240        assert_eq!(repeated, JsString::from_static("ababab"));
2241        assert!(repeated.is_flat());
2242        assert!(matches!(repeated.0.data(), StringData::Latin1(_)));
2243        let once = latin1.repeat_with_limit(1, 1).unwrap();
2244        assert!(once.same_representation(&latin1));
2245
2246        let wide = JsString(StringRepr::allocate(StringData::Utf16(
2247            [u16::from(b'A'), 0xd800].into_iter().collect(),
2248        )));
2249        let repeated = wide.repeat_with_limit(3, JsString::MAX_LEN).unwrap();
2250        assert_eq!(
2251            repeated.utf16_units().collect::<Vec<_>>(),
2252            [0x41, 0xd800, 0x41, 0xd800, 0x41, 0xd800],
2253        );
2254        assert!(matches!(repeated.0.data(), StringData::Utf16(_)));
2255        let zero = wide.repeat_with_limit(0, JsString::MAX_LEN).unwrap();
2256        assert!(matches!(zero.0.data(), StringData::Latin1(units) if units.is_empty()));
2257
2258        let left = JsString::try_from_utf8(&"x".repeat(8_193)).unwrap();
2259        let rope = left.try_concat(&JsString::from_static("yz")).unwrap();
2260        assert!(!rope.is_flat());
2261        let once = rope.repeat_with_limit(1, JsString::MAX_LEN).unwrap();
2262        assert!(once.is_flat());
2263        assert!(once.same_representation(&rope.linearize()));
2264        let twice = rope.repeat_with_limit(2, JsString::MAX_LEN).unwrap();
2265        assert!(twice.is_flat());
2266        assert_eq!(twice.len(), rope.len() * 2);
2267        assert_eq!(twice.code_unit_at(rope.len()), Some(u16::from(b'x')));
2268
2269        let empty = JsString::from_static("");
2270        let empty_repeat = empty.repeat_with_limit(2_147_483_647, 0).unwrap();
2271        assert!(empty_repeat.same_representation(&empty));
2272        assert_eq!(latin1.repeat_with_limit(3, 5), Err(JsStringError::TooLong));
2273
2274        super::fail_next_repeat_reservation_for_test();
2275        let forced_fast = latin1.repeat_with_limit(1, 1).unwrap();
2276        assert!(forced_fast.same_representation(&latin1));
2277        assert_eq!(
2278            latin1.repeat_with_limit(2, JsString::MAX_LEN),
2279            Err(JsStringError::OutOfMemory),
2280        );
2281        assert_eq!(
2282            latin1.repeat_with_limit(2, JsString::MAX_LEN).unwrap(),
2283            JsString::from_static("abab"),
2284            "the fail-next reservation hook was not consumed exactly once",
2285        );
2286    }
2287
2288    #[test]
2289    fn pad_uses_quickjs_order_width_limit_and_recoverable_reservations() {
2290        let source = JsString::from_static("ab");
2291        let filler = JsString::from_static("xy");
2292        assert_eq!(
2293            source
2294                .pad_with_limit(7, Some(&filler), true, JsString::MAX_LEN)
2295                .unwrap(),
2296            JsString::from_static("abxyxyx"),
2297        );
2298        assert_eq!(
2299            source
2300                .pad_with_limit(7, Some(&filler), false, JsString::MAX_LEN)
2301                .unwrap(),
2302            JsString::from_static("xyxyxab"),
2303        );
2304        assert_eq!(
2305            source
2306                .pad_with_limit(4, None, true, JsString::MAX_LEN)
2307                .unwrap(),
2308            JsString::from_static("ab  "),
2309        );
2310
2311        let forced_wide_filler = JsString(StringRepr::allocate(StringData::Utf16(
2312            [u16::from(b'z'), 0x100].into_iter().collect(),
2313        )));
2314        let narrow = source
2315            .pad_with_limit(3, Some(&forced_wide_filler), false, JsString::MAX_LEN)
2316            .unwrap();
2317        assert_eq!(narrow, JsString::from_static("zab"));
2318        assert!(matches!(narrow.0.data(), StringData::Latin1(_)));
2319        let wide = source
2320            .pad_with_limit(4, Some(&forced_wide_filler), false, JsString::MAX_LEN)
2321            .unwrap();
2322        assert_eq!(
2323            wide.utf16_units().collect::<Vec<_>>(),
2324            [u16::from(b'z'), 0x100, u16::from(b'a'), u16::from(b'b')],
2325        );
2326        assert!(matches!(wide.0.data(), StringData::Utf16(_)));
2327
2328        let early = source
2329            .pad_with_limit(2, Some(&filler), true, 0)
2330            .expect("source-length early return must precede the cap");
2331        assert!(early.same_representation(&source));
2332        let empty = JsString::from_static("");
2333        let empty_filler = source
2334            .pad_with_limit(100, Some(&empty), false, 0)
2335            .expect("empty-filler early return must precede the cap");
2336        assert!(empty_filler.same_representation(&source));
2337        assert_eq!(
2338            source.pad_with_limit(3, Some(&filler), true, 2),
2339            Err(JsStringError::TooLong),
2340        );
2341
2342        super::fail_next_pad_reservation_for_test();
2343        let forced_fast = source.pad_with_limit(2, Some(&filler), true, 0).unwrap();
2344        assert!(forced_fast.same_representation(&source));
2345        let forced_empty = source.pad_with_limit(100, Some(&empty), false, 0).unwrap();
2346        assert!(forced_empty.same_representation(&source));
2347        assert_eq!(
2348            source.pad_with_limit(3, Some(&filler), true, 2),
2349            Err(JsStringError::TooLong),
2350            "length rejection must precede and preserve the reservation hook",
2351        );
2352        assert_eq!(
2353            source.pad_with_limit(3, Some(&filler), true, JsString::MAX_LEN),
2354            Err(JsStringError::OutOfMemory),
2355        );
2356        assert_eq!(
2357            source
2358                .pad_with_limit(3, Some(&filler), true, JsString::MAX_LEN)
2359                .unwrap(),
2360            JsString::from_static("abx"),
2361            "the fail-next pad reservation hook was not consumed exactly once",
2362        );
2363    }
2364
2365    #[test]
2366    fn trim_uses_quickjs_whitespace_width_identity_and_reservation_rules() {
2367        let whitespace = [
2368            0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x0020, 0x00a0, 0x1680, 0x2000, 0x2001, 0x2002,
2369            0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200a, 0x2028, 0x2029, 0x202f,
2370            0x205f, 0x3000, 0xfeff,
2371        ];
2372        for unit in whitespace {
2373            let source = JsString::try_from_utf16([unit, u16::from(b'x'), unit]).unwrap();
2374            assert_eq!(
2375                source.trim_whitespace(true, true).unwrap(),
2376                JsString::from_static("x"),
2377                "U+{unit:04X} was not trimmed",
2378            );
2379        }
2380        for unit in [0x0000, 0x0085, 0x180e, 0x200b] {
2381            let source = JsString::try_from_utf16([unit, u16::from(b'x'), unit]).unwrap();
2382            assert!(
2383                source
2384                    .trim_whitespace(true, true)
2385                    .unwrap()
2386                    .same_representation(&source),
2387                "U+{unit:04X} was incorrectly trimmed",
2388            );
2389        }
2390
2391        let source = JsString::from_static("  value  ");
2392        assert_eq!(
2393            source.trim_whitespace(true, false).unwrap(),
2394            JsString::from_static("value  "),
2395        );
2396        assert_eq!(
2397            source.trim_whitespace(false, true).unwrap(),
2398            JsString::from_static("  value"),
2399        );
2400        assert_eq!(
2401            source.trim_whitespace(true, true).unwrap(),
2402            JsString::from_static("value"),
2403        );
2404
2405        let forced_wide_latin1 = JsString(StringRepr::allocate(StringData::Utf16(
2406            [0x20, u16::from(b'a'), u16::from(b'b'), 0x20]
2407                .into_iter()
2408                .collect(),
2409        )));
2410        let narrowed = forced_wide_latin1.trim_whitespace(true, true).unwrap();
2411        assert_eq!(narrowed, JsString::from_static("ab"));
2412        assert!(matches!(narrowed.0.data(), StringData::Latin1(_)));
2413        let forced_wide = JsString::try_from_utf16([0x20, 0x100, 0x20]).unwrap();
2414        let still_wide = forced_wide.trim_whitespace(true, true).unwrap();
2415        assert_eq!(still_wide.utf16_units().collect::<Vec<_>>(), [0x100]);
2416        assert!(matches!(still_wide.0.data(), StringData::Utf16(_)));
2417
2418        let empty = JsString::from_static(" \u{feff}\u{2029}")
2419            .trim_whitespace(true, true)
2420            .unwrap();
2421        assert!(matches!(empty.0.data(), StringData::Latin1(units) if units.is_empty()));
2422
2423        let rope = JsString::try_from_utf8(&" ".repeat(8_193))
2424            .unwrap()
2425            .try_concat(&JsString::from_static("x "))
2426            .unwrap();
2427        assert!(!rope.is_flat());
2428        assert_eq!(
2429            rope.trim_whitespace(true, true).unwrap(),
2430            JsString::from_static("x"),
2431        );
2432
2433        super::fail_next_trim_reservation_for_test();
2434        let unchanged = JsString::from_static("value");
2435        assert!(
2436            unchanged
2437                .trim_whitespace(true, true)
2438                .unwrap()
2439                .same_representation(&unchanged),
2440            "full-range return must not consume the reservation hook",
2441        );
2442        let all_space = JsString::from_static("  ");
2443        assert!(all_space.trim_whitespace(true, true).unwrap().is_empty());
2444        assert_eq!(
2445            source.trim_whitespace(true, true),
2446            Err(JsStringError::OutOfMemory),
2447        );
2448        assert_eq!(
2449            source.trim_whitespace(true, true).unwrap(),
2450            JsString::from_static("value"),
2451            "the fail-next trim reservation hook was not consumed exactly once",
2452        );
2453    }
2454
2455    #[test]
2456    fn rope_thresholds_fringe_merges_and_content_identity_match_quickjs() {
2457        let flat_8192 = JsString::try_from_utf8(&"a".repeat(8192)).unwrap();
2458        let short_512 = JsString::try_from_utf8(&"b".repeat(512)).unwrap();
2459        let merged_flat = flat_8192.try_concat(&short_512).unwrap();
2460        assert!(merged_flat.is_flat());
2461        assert_eq!(merged_flat.len(), 8704);
2462
2463        let flat_8193 = JsString::try_from_utf8(&"a".repeat(8193)).unwrap();
2464        let rope = flat_8193.try_concat(&short_512).unwrap();
2465        assert!(!rope.is_flat());
2466        assert_eq!(rope.depth(), 1);
2467        assert_eq!(rope.code_unit_at(8192), Some(u16::from(b'a')));
2468        assert_eq!(rope.code_unit_at(8193), Some(u16::from(b'b')));
2469
2470        let expected = JsString::try_from_utf8(&("a".repeat(8193) + &"b".repeat(512))).unwrap();
2471        assert_eq!(rope, expected);
2472        assert_eq!(content_hash(&rope), content_hash(&expected));
2473        let mut units = rope.utf16_units();
2474        assert_eq!(units.len(), rope.len());
2475        assert_eq!(units.next(), Some(u16::from(b'a')));
2476        assert_eq!(units.len(), rope.len() - 1);
2477
2478        let one = JsString::from_static("c");
2479        let tail_merged = rope.try_concat(&one).unwrap();
2480        assert_eq!(tail_merged.depth(), 1);
2481        let super::StringData::Rope(tail) = tail_merged.0.data() else {
2482            panic!("short right fringe did not remain a rope");
2483        };
2484        let (_, tail_right) = JsString::rope_children(tail);
2485        assert_eq!(tail_right.len(), 513);
2486        assert!(tail_right.is_flat());
2487
2488        let right_513 = JsString::try_from_utf8(&"z".repeat(513)).unwrap();
2489        let threshold_rope = flat_8192.try_concat(&right_513).unwrap();
2490        assert!(!threshold_rope.is_flat());
2491        let no_tail_merge = threshold_rope.try_concat(&one).unwrap();
2492        assert_eq!(no_tail_merge.depth(), 2);
2493        let super::StringData::Rope(no_tail_merge_rope) = no_tail_merge.0.data() else {
2494            panic!("513-unit right fringe did not remain nested");
2495        };
2496        let (unmerged_left, _) = JsString::rope_children(no_tail_merge_rope);
2497        assert!(Rc::ptr_eq(&unmerged_left.0, &threshold_rope.0));
2498
2499        let left_513_rope = right_513.try_concat(&flat_8192).unwrap();
2500        let no_head_merge = one.try_concat(&left_513_rope).unwrap();
2501        assert_eq!(no_head_merge.depth(), 2);
2502        let super::StringData::Rope(no_head_merge_rope) = no_head_merge.0.data() else {
2503            panic!("513-unit left fringe did not remain nested");
2504        };
2505        let (_, unmerged_right) = JsString::rope_children(no_head_merge_rope);
2506        assert!(Rc::ptr_eq(&unmerged_right.0, &left_513_rope.0));
2507
2508        let right_rope = short_512.try_concat(&right_513).unwrap();
2509        assert_eq!(right_rope.depth(), 1);
2510        let head_merged = one.try_concat(&right_rope).unwrap();
2511        assert_eq!(head_merged.depth(), 1);
2512        let super::StringData::Rope(head) = head_merged.0.data() else {
2513            panic!("short left fringe did not remain a rope");
2514        };
2515        let (head_left, _) = JsString::rope_children(head);
2516        assert_eq!(head_left.len(), 513);
2517        assert!(head_left.is_flat());
2518
2519        let empty = JsString::from_static("");
2520        let large_flat = JsString::try_from_utf8(&"q".repeat(8193)).unwrap();
2521        let empty_plus_flat = empty.try_concat(&large_flat).unwrap();
2522        assert!(empty_plus_flat.is_flat());
2523        assert!(Rc::ptr_eq(&empty_plus_flat.0, &large_flat.0));
2524        let rope = large_flat.try_concat(&one).unwrap();
2525        let empty_plus_rope = empty.try_concat(&rope).unwrap();
2526        assert!(Rc::ptr_eq(&empty_plus_rope.0, &rope.0));
2527        let rope_plus_empty = rope.try_concat(&empty).unwrap();
2528        assert!(Rc::ptr_eq(&rope_plus_empty.0, &rope.0));
2529
2530        let cached_flat = rope.linearize();
2531        assert!(cached_flat.is_flat());
2532        let cached_concat = rope.try_concat(&JsString::from_static("!")).unwrap();
2533        assert!(!cached_concat.is_flat());
2534        assert_eq!(
2535            cached_concat.code_unit_at(cached_concat.len() - 1),
2536            Some(0x21)
2537        );
2538    }
2539
2540    #[test]
2541    fn rope_code_points_and_well_formed_scans_cross_leaf_boundaries() {
2542        let mut left = vec![u16::from(b'a'); 8192];
2543        left.push(0xd83d);
2544        let mut right = vec![0xde80];
2545        right.extend(std::iter::repeat_n(u16::from(b'b'), 512));
2546        let valid = JsString::try_from_utf16(left)
2547            .unwrap()
2548            .try_concat(&JsString::try_from_utf16(right).unwrap())
2549            .unwrap();
2550        assert!(!valid.is_flat());
2551        assert_eq!(valid.code_point_at(8192), Some(0x1f680));
2552        assert!(valid.is_well_formed());
2553
2554        let invalid =
2555            JsString::try_from_utf16(std::iter::repeat_n(u16::from(b'a'), 8192).chain([0xd800]))
2556                .unwrap()
2557                .try_concat(
2558                    &JsString::try_from_utf16(
2559                        [u16::from(b'x')]
2560                            .into_iter()
2561                            .chain(std::iter::repeat_n(u16::from(b'b'), 512)),
2562                    )
2563                    .unwrap(),
2564                )
2565                .unwrap();
2566        assert!(!invalid.is_well_formed());
2567        let repaired = invalid.to_well_formed();
2568        assert_eq!(repaired.code_unit_at(8192), Some(0xfffd));
2569        assert_eq!(repaired.len(), invalid.len());
2570    }
2571
2572    #[test]
2573    fn rope_rebalances_and_reaches_the_quickjs_length_guard_without_flattening() {
2574        let marker_chunk = |index: usize| {
2575            let marker = char::from(b'A' + u8::try_from(index % 26).unwrap());
2576            JsString::try_from_utf8(&marker.to_string().repeat(8193)).unwrap()
2577        };
2578        let mut deep = marker_chunk(0);
2579        for index in 1..101 {
2580            deep = deep.try_concat(&marker_chunk(index)).unwrap();
2581            assert!(deep.depth() <= JsString::ROPE_MAX_DEPTH);
2582        }
2583        for index in 0..101 {
2584            let marker = u16::from(b'A' + u8::try_from(index % 26).unwrap());
2585            assert_eq!(deep.code_unit_at(index * 8193), Some(marker));
2586            assert_eq!(deep.code_unit_at((index + 1) * 8193 - 1), Some(marker));
2587        }
2588
2589        let mut prepended = marker_chunk(100);
2590        for index in (0..100).rev() {
2591            prepended = marker_chunk(index).try_concat(&prepended).unwrap();
2592            assert!(prepended.depth() <= JsString::ROPE_MAX_DEPTH);
2593        }
2594        for index in 0..101 {
2595            let marker = u16::from(b'A' + u8::try_from(index % 26).unwrap());
2596            assert_eq!(prepended.code_unit_at(index * 8193), Some(marker));
2597            assert_eq!(prepended.code_unit_at((index + 1) * 8193 - 1), Some(marker));
2598        }
2599
2600        let before_hash = content_hash(&deep);
2601        let flat = deep.linearize();
2602        assert!(flat.is_flat());
2603        assert_eq!(deep, flat);
2604        assert_eq!(before_hash, content_hash(&deep));
2605        assert_eq!(before_hash, content_hash(&flat));
2606
2607        let chunk = JsString::try_from_utf8(&"x".repeat(8193)).unwrap();
2608        let mut near_limit = chunk;
2609        for _ in 0..16 {
2610            near_limit = near_limit.try_concat(&near_limit).unwrap();
2611        }
2612        assert_eq!(near_limit.len(), 536_936_448);
2613        assert!(matches!(
2614            near_limit.try_concat(&near_limit),
2615            Err(JsStringError::TooLong)
2616        ));
2617        assert!(matches!(
2618            near_limit.clone().concat_owned(&near_limit),
2619            Err(JsStringError::TooLong)
2620        ));
2621        let error = Error::from(JsStringError::TooLong);
2622        assert_eq!(error.kind(), ErrorKind::JsInternal);
2623        assert_eq!(error.message(), "string too long");
2624
2625        let mut powers = Vec::with_capacity(30);
2626        let mut power = JsString::from_static("x");
2627        powers.push(power.clone());
2628        for _ in 1..30 {
2629            power = power.try_concat(&power).unwrap();
2630            powers.push(power.clone());
2631        }
2632        let mut exact_max = JsString::from_static("");
2633        for power in powers.into_iter().rev() {
2634            exact_max = exact_max.try_concat(&power).unwrap();
2635        }
2636        assert_eq!(exact_max.len(), JsString::MAX_LEN);
2637        assert!(matches!(
2638            exact_max.try_concat(&JsString::from_static("x")),
2639            Err(JsStringError::TooLong)
2640        ));
2641        let exact_plus_empty = exact_max.try_concat(&JsString::from_static("")).unwrap();
2642        assert!(Rc::ptr_eq(&exact_plus_empty.0, &exact_max.0));
2643    }
2644
2645    #[test]
2646    fn number_uses_int_fast_path_without_losing_negative_zero() {
2647        assert!(matches!(
2648            PrimitiveValue::number(42.0),
2649            PrimitiveValue::Int(42)
2650        ));
2651        assert!(
2652            matches!(PrimitiveValue::number(-0.0), PrimitiveValue::Float(value) if value.is_sign_negative())
2653        );
2654    }
2655
2656    #[test]
2657    fn number_formatting_uses_ecmascript_thresholds() {
2658        assert_eq!(number_to_string(-0.0), "0");
2659        assert_eq!(number_to_string(f64::NAN), "NaN");
2660        assert_eq!(number_to_string(1e20), "100000000000000000000");
2661        assert_eq!(number_to_string(1e21), "1e+21");
2662    }
2663}