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#[derive(Clone)]
17pub struct JsString(Rc<StringRepr>);
18
19#[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#[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 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
151enum BorrowedUtf16Units<'a> {
154 Latin1(std::slice::Iter<'a, u8>),
155 Utf16(std::slice::Iter<'a, u16>),
156 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 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 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 #[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 pub fn try_from_utf8(value: &str) -> Result<Self, JsStringError> {
507 Self::try_from_utf16(value.encode_utf16())
508 }
509
510 pub fn try_from_bytes(value: &[u8]) -> Result<Self, JsStringError> {
524 Self::try_from_bytes_with_limit(value, Self::MAX_LEN)
525 }
526
527 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 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 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 #[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 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 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 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 #[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 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 #[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 #[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 #[must_use]
747 pub fn is_well_formed(&self) -> bool {
748 self.first_unpaired_surrogate().is_none()
749 }
750
751 #[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 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 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 #[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 #[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 pub fn repeat_with_limit(&self, count: usize, max_len: usize) -> Result<Self, JsStringError> {
1074 let source = self.linearize();
1075 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 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 #[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 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 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 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 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 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 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 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 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 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 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 pub fn try_to_wtf8_bytes(&self) -> Result<Vec<u8>, TryReserveError> {
1442 self.try_to_quickjs_utf8_bytes(false)
1443 }
1444
1445 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 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 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 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 #[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 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#[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 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#[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 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}