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