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