Skip to content
File

Blob: src/rust/jsg-test/tests/string.rs

rust594 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5use jsg::v8::Local;
6use jsg::v8::MaybeLocal;
7use jsg::v8::String as JsString;
8use jsg::v8::ToLocalValue;
9use jsg::v8::Utf8Value;
10use jsg::v8::Value;
11use jsg::v8::WriteFlags;
12 
13// Convenience: create a Local<String> from a UTF-8 str literal.
14fn from_utf8<'a>(lock: &mut jsg::Lock, s: &str) -> Local<'a, JsString> {
15 JsString::new_from_utf8(lock, s.as_bytes()).unwrap(lock)
16}
17 
18// =============================================================================
19// Local<String> — static constructors
20// =============================================================================
21 
22#[test]
23fn string_empty_has_zero_length() {
24 let harness = crate::Harness::new();
25 harness.run_in_context(|lock, _ctx| {
26 let s = JsString::empty(lock);
27 assert_eq!(s.length(), 0);
28 assert!(s.is_one_byte());
29 Ok(())
30 });
31}
32 
33#[test]
34fn string_new_from_utf8_roundtrips_ascii() {
35 let harness = crate::Harness::new();
36 harness.run_in_context(|lock, _ctx| {
37 let maybe = JsString::new_from_utf8(lock, b"hello");
38 assert!(!maybe.is_empty());
39 let s = maybe.unwrap(lock);
40 assert_eq!(s.length(), 5);
41 assert_eq!(s.to_string(lock), "hello");
42 Ok(())
43 });
44}
45 
46#[test]
47fn string_new_from_utf8_roundtrips_unicode() {
48 let harness = crate::Harness::new();
49 harness.run_in_context(|lock, _ctx| {
50 let input = "こんにちは";
51 let s = from_utf8(lock, input);
52 assert_eq!(s.to_string(lock), input);
53 Ok(())
54 });
55}
56 
57#[test]
58fn string_new_from_utf8_empty_slice() {
59 let harness = crate::Harness::new();
60 harness.run_in_context(|lock, _ctx| {
61 let maybe = JsString::new_from_utf8(lock, b"");
62 assert!(!maybe.is_empty());
63 let s = maybe.unwrap(lock);
64 assert_eq!(s.length(), 0);
65 Ok(())
66 });
67}
68 
69#[test]
70fn string_new_from_one_byte_roundtrips() {
71 let harness = crate::Harness::new();
72 harness.run_in_context(|lock, _ctx| {
73 let maybe = JsString::new_from_one_byte(lock, b"latin1");
74 assert!(!maybe.is_empty());
75 let s = maybe.unwrap(lock);
76 assert_eq!(s.length(), 6);
77 assert!(s.is_one_byte());
78 assert_eq!(s.to_string(lock), "latin1");
79 Ok(())
80 });
81}
82 
83#[test]
84fn string_new_from_two_byte_roundtrips() {
85 let harness = crate::Harness::new();
86 harness.run_in_context(|lock, _ctx| {
87 let utf16: Vec<u16> = "hello".encode_utf16().collect();
88 let maybe = JsString::new_from_two_byte(lock, &utf16);
89 assert!(!maybe.is_empty());
90 let s = maybe.unwrap(lock);
91 assert_eq!(s.length(), 5);
92 assert_eq!(s.to_string(lock), "hello");
93 Ok(())
94 });
95}
96 
97#[test]
98fn string_new_from_two_byte_unicode() {
99 let harness = crate::Harness::new();
100 harness.run_in_context(|lock, _ctx| {
101 let input = "日本語";
102 let utf16: Vec<u16> = input.encode_utf16().collect();
103 let s = JsString::new_from_two_byte(lock, &utf16).unwrap(lock);
104 assert_eq!(s.to_string(lock), input);
105 Ok(())
106 });
107}
108 
109#[test]
110fn string_new_from_utf8_internalized_roundtrips() {
111 let harness = crate::Harness::new();
112 harness.run_in_context(|lock, _ctx| {
113 let s1 = JsString::new_internalized_from_utf8(lock, b"intern_me").unwrap(lock);
114 let s2 = JsString::new_internalized_from_utf8(lock, b"intern_me").unwrap(lock);
115 // Both strings should have the same content.
116 assert_eq!(s1.to_string(lock), "intern_me");
117 assert_eq!(s2.to_string(lock), "intern_me");
118 // Internalized strings with equal content compare equal.
119 assert_eq!(s1, s2);
120 Ok(())
121 });
122}
123 
124#[test]
125fn string_new_from_one_byte_internalized_roundtrips() {
126 let harness = crate::Harness::new();
127 harness.run_in_context(|lock, _ctx| {
128 let s = JsString::new_internalized_from_one_byte(lock, b"latin1_intern").unwrap(lock);
129 assert_eq!(s.to_string(lock), "latin1_intern");
130 assert!(s.is_one_byte());
131 Ok(())
132 });
133}
134 
135#[test]
136fn string_new_from_two_byte_internalized_roundtrips() {
137 let harness = crate::Harness::new();
138 harness.run_in_context(|lock, _ctx| {
139 let utf16: Vec<u16> = "two_byte_intern".encode_utf16().collect();
140 let s = JsString::new_internalized_from_two_byte(lock, &utf16).unwrap(lock);
141 assert_eq!(s.to_string(lock), "two_byte_intern");
142 Ok(())
143 });
144}
145 
146// =============================================================================
147// Local<String> — instance methods
148// =============================================================================
149 
150#[test]
151fn string_length_matches_utf16_code_units() {
152 let harness = crate::Harness::new();
153 harness.run_in_context(|lock, _ctx| {
154 // ASCII: 1 byte per char = 1 UTF-16 code unit
155 assert_eq!(from_utf8(lock, "hello").length(), 5);
156 // Each BMP kanji = 1 UTF-16 code unit
157 assert_eq!(from_utf8(lock, "日本語").length(), 3);
158 Ok(())
159 });
160}
161 
162#[test]
163fn string_is_one_byte_for_ascii() {
164 let harness = crate::Harness::new();
165 harness.run_in_context(|lock, _ctx| {
166 let s = from_utf8(lock, "ascii only");
167 assert!(s.is_one_byte());
168 assert!(s.contains_only_one_byte());
169 Ok(())
170 });
171}
172 
173#[test]
174fn string_is_not_one_byte_for_multibyte() {
175 let harness = crate::Harness::new();
176 harness.run_in_context(|lock, _ctx| {
177 let s = from_utf8(lock, "こんにちは");
178 assert!(!s.is_one_byte());
179 assert!(!s.contains_only_one_byte());
180 Ok(())
181 });
182}
183 
184#[test]
185fn string_utf8_length_matches_byte_count() {
186 let harness = crate::Harness::new();
187 harness.run_in_context(|lock, _ctx| {
188 let ascii = "hello";
189 assert_eq!(from_utf8(lock, ascii).utf8_length(lock), ascii.len());
190 
191 let unicode = "こんにちは";
192 assert_eq!(from_utf8(lock, unicode).utf8_length(lock), unicode.len());
193 Ok(())
194 });
195}
196 
197// =============================================================================
198// write / write_one_byte / write_utf8
199// =============================================================================
200 
201#[test]
202fn string_write_utf16_into_buffer() {
203 let harness = crate::Harness::new();
204 harness.run_in_context(|lock, _ctx| {
205 let s = from_utf8(lock, "hi");
206 let mut buf = vec![0u16; 2];
207 s.write(lock, 0, 2, &mut buf, WriteFlags::None);
208 assert_eq!(buf, vec!['h' as u16, 'i' as u16]);
209 Ok(())
210 });
211}
212 
213#[test]
214fn string_write_utf16_with_offset() {
215 let harness = crate::Harness::new();
216 harness.run_in_context(|lock, _ctx| {
217 let s = from_utf8(lock, "hello");
218 let mut buf = vec![0u16; 3];
219 s.write(lock, 2, 3, &mut buf, WriteFlags::None);
220 assert_eq!(buf, vec!['l' as u16, 'l' as u16, 'o' as u16]);
221 Ok(())
222 });
223}
224 
225#[test]
226fn string_write_one_byte_into_buffer() {
227 let harness = crate::Harness::new();
228 harness.run_in_context(|lock, _ctx| {
229 let s = from_utf8(lock, "abc");
230 let mut buf = vec![0u8; 3];
231 s.write_one_byte(lock, 0, 3, &mut buf, WriteFlags::None);
232 assert_eq!(buf, b"abc");
233 Ok(())
234 });
235}
236 
237#[test]
238fn string_write_utf8_into_buffer() {
239 let harness = crate::Harness::new();
240 harness.run_in_context(|lock, _ctx| {
241 let input = "hello";
242 let s = from_utf8(lock, input);
243 let mut buf = vec![0u8; input.len()];
244 let written = s.write_utf8(lock, &mut buf, WriteFlags::None);
245 assert_eq!(written, input.len());
246 assert_eq!(&buf, input.as_bytes());
247 Ok(())
248 });
249}
250 
251#[test]
252fn string_write_utf8_null_terminate() {
253 let harness = crate::Harness::new();
254 harness.run_in_context(|lock, _ctx| {
255 let input = "hi";
256 let s = from_utf8(lock, input);
257 let mut buf = vec![0u8; input.len() + 1];
258 let written = s.write_utf8(lock, &mut buf, WriteFlags::NullTerminate);
259 assert_eq!(written, input.len() + 1);
260 assert_eq!(buf[input.len()], 0);
261 Ok(())
262 });
263}
264 
265#[test]
266fn string_to_string_roundtrips_ascii() {
267 let harness = crate::Harness::new();
268 harness.run_in_context(|lock, _ctx| {
269 let input = "Hello, world!";
270 assert_eq!(from_utf8(lock, input).to_string(lock), input);
271 Ok(())
272 });
273}
274 
275#[test]
276fn string_to_string_roundtrips_unicode() {
277 let harness = crate::Harness::new();
278 harness.run_in_context(|lock, _ctx| {
279 let input = "🦀 Rust";
280 assert_eq!(from_utf8(lock, input).to_string(lock), input);
281 Ok(())
282 });
283}
284 
285// =============================================================================
286// PartialEq / Eq
287// =============================================================================
288 
289#[test]
290fn string_eq_same_content() {
291 let harness = crate::Harness::new();
292 harness.run_in_context(|lock, _ctx| {
293 let a = from_utf8(lock, "equal");
294 let b = from_utf8(lock, "equal");
295 assert_eq!(a, b);
296 Ok(())
297 });
298}
299 
300#[test]
301fn string_ne_different_content() {
302 let harness = crate::Harness::new();
303 harness.run_in_context(|lock, _ctx| {
304 let a = from_utf8(lock, "foo");
305 let b = from_utf8(lock, "bar");
306 assert_ne!(a, b);
307 Ok(())
308 });
309}
310 
311#[test]
312fn string_empty_eq_empty() {
313 let harness = crate::Harness::new();
314 harness.run_in_context(|lock, _ctx| {
315 let a = JsString::empty(lock);
316 let b = JsString::empty(lock);
317 assert_eq!(a, b);
318 Ok(())
319 });
320}
321 
322// =============================================================================
323// MaybeLocal
324// =============================================================================
325 
326#[test]
327fn maybe_local_is_not_empty_on_success() {
328 let harness = crate::Harness::new();
329 harness.run_in_context(|lock, _ctx| {
330 let maybe = JsString::new_from_utf8(lock, b"hello");
331 assert!(!maybe.is_empty());
332 Ok(())
333 });
334}
335 
336#[test]
337fn maybe_local_into_option_some() {
338 let harness = crate::Harness::new();
339 harness.run_in_context(|lock, _ctx| {
340 let maybe = JsString::new_from_utf8(lock, b"test");
341 let opt = maybe.into_option(lock);
342 assert!(opt.is_some());
343 Ok(())
344 });
345}
346 
347#[test]
348fn maybe_local_from_none_is_empty() {
349 let harness = crate::Harness::new();
350 harness.run_in_context(|lock, _ctx| {
351 let _ = lock; // ensure we're inside the context
352 let maybe: MaybeLocal<'_, JsString> = None::<Local<'_, JsString>>.into();
353 assert!(maybe.is_empty());
354 Ok(())
355 });
356}
357 
358#[test]
359fn maybe_local_unwrap_or_returns_default_when_empty() {
360 let harness = crate::Harness::new();
361 harness.run_in_context(|lock, _ctx| {
362 let default = JsString::empty(lock);
363 let maybe: MaybeLocal<'_, JsString> = None::<Local<'_, JsString>>.into();
364 let result = maybe.unwrap_or(lock, default);
365 assert_eq!(result.length(), 0);
366 Ok(())
367 });
368}
369 
370// =============================================================================
371// WriteFlags
372// =============================================================================
373 
374#[test]
375fn write_flags_none_is_zero() {
376 assert_eq!(WriteFlags::None.bits(), 0);
377}
378 
379#[test]
380fn write_flags_null_terminate_is_one() {
381 assert_eq!(WriteFlags::NullTerminate.bits(), 1);
382}
383 
384#[test]
385fn write_flags_replace_invalid_utf8_is_two() {
386 assert_eq!(WriteFlags::ReplaceInvalidUtf8.bits(), 2);
387}
388 
389#[test]
390fn write_flags_bitor_combines_values() {
391 let combined = WriteFlags::NullTerminate | WriteFlags::ReplaceInvalidUtf8;
392 assert_eq!(combined.bits(), 3);
393 assert_eq!(combined, WriteFlags::NullTerminateAndReplaceInvalidUtf8);
394}
395 
396#[test]
397fn write_flags_combined_variant_equals_bitor() {
398 assert_eq!(
399 WriteFlags::NullTerminateAndReplaceInvalidUtf8.bits(),
400 WriteFlags::NullTerminate.bits() | WriteFlags::ReplaceInvalidUtf8.bits()
401 );
402}
403 
404// =============================================================================
405// Utf8Value
406// =============================================================================
407 
408#[test]
409fn utf8_value_length_matches_byte_count() {
410 let harness = crate::Harness::new();
411 harness.run_in_context(|lock, _ctx| {
412 let val: Local<'_, Value> = "hello".to_local(lock);
413 let utf8 = Utf8Value::new(lock, &val);
414 assert_eq!(utf8.length(), 5);
415 Ok(())
416 });
417}
418 
419#[test]
420fn utf8_value_as_str_returns_correct_content() {
421 let harness = crate::Harness::new();
422 harness.run_in_context(|lock, _ctx| {
423 let val: Local<'_, Value> = "hello world".to_local(lock);
424 let utf8 = Utf8Value::new(lock, &val);
425 assert_eq!(utf8.as_str(), Some("hello world"));
426 Ok(())
427 });
428}
429 
430#[test]
431fn utf8_value_as_bytes_matches_utf8_encoding() {
432 let harness = crate::Harness::new();
433 harness.run_in_context(|lock, _ctx| {
434 let input = "Rust 🦀";
435 let val: Local<'_, Value> = input.to_local(lock);
436 let utf8 = Utf8Value::new(lock, &val);
437 assert_eq!(utf8.as_bytes(), input.as_bytes());
438 Ok(())
439 });
440}
441 
442#[test]
443fn utf8_value_from_unicode_string() {
444 let harness = crate::Harness::new();
445 harness.run_in_context(|lock, _ctx| {
446 let input = "こんにちは";
447 let val: Local<'_, Value> = input.to_local(lock);
448 let utf8 = Utf8Value::new(lock, &val);
449 assert_eq!(utf8.length(), input.len());
450 assert_eq!(utf8.as_str(), Some(input));
451 Ok(())
452 });
453}
454 
455#[test]
456fn utf8_value_from_empty_string() {
457 let harness = crate::Harness::new();
458 harness.run_in_context(|lock, _ctx| {
459 let val: Local<'_, Value> = "".to_local(lock);
460 let utf8 = Utf8Value::new(lock, &val);
461 assert_eq!(utf8.length(), 0);
462 assert_eq!(utf8.as_str(), Some(""));
463 Ok(())
464 });
465}
466 
467#[test]
468fn utf8_value_as_ptr_is_non_null() {
469 let harness = crate::Harness::new();
470 harness.run_in_context(|lock, _ctx| {
471 let val: Local<'_, Value> = "test".to_local(lock);
472 let utf8 = Utf8Value::new(lock, &val);
473 assert!(!utf8.as_ptr().is_null());
474 Ok(())
475 });
476}
477 
478#[test]
479fn utf8_value_coerces_number_to_string() {
480 let harness = crate::Harness::new();
481 harness.run_in_context(|lock, ctx| {
482 let val = ctx.eval_raw("42").map_err(|e| e.unwrap_jsg_err(lock))?;
483 let utf8 = Utf8Value::new(lock, &val);
484 assert_eq!(utf8.as_str(), Some("42"));
485 Ok(())
486 });
487}
488 
489// =============================================================================
490// Local<String> cast from/to Local<Value>
491// =============================================================================
492 
493#[test]
494fn string_try_as_from_value() {
495 let harness = crate::Harness::new();
496 harness.run_in_context(|lock, _ctx| {
497 let val: Local<'_, Value> = "cast me".to_local(lock);
498 let s = val.try_as::<JsString>().expect("should cast to String");
499 assert_eq!(s.to_string(lock), "cast me");
500 Ok(())
501 });
502}
503 
504#[test]
505fn string_try_as_fails_for_non_string() {
506 let harness = crate::Harness::new();
507 harness.run_in_context(|lock, _ctx| {
508 let val: Local<'_, Value> = jsg::Number::new(42.0).to_local(lock);
509 assert!(val.try_as::<JsString>().is_none());
510 Ok(())
511 });
512}
513 
514#[test]
515fn string_upcast_to_value() {
516 let harness = crate::Harness::new();
517 harness.run_in_context(|lock, _ctx| {
518 let s = from_utf8(lock, "upcast");
519 let val: Local<'_, Value> = s.into();
520 assert!(val.is_string());
521 Ok(())
522 });
523}
524 
525// =============================================================================
526// String::concat, internalize, get_identity_hash, is_flat, K_MAX_LENGTH
527// =============================================================================
528 
529#[test]
530fn string_concat_produces_concatenated_string() {
531 let harness = crate::Harness::new();
532 harness.run_in_context(|lock, _ctx| {
533 let left = from_utf8(lock, "hello, ");
534 let right = from_utf8(lock, "world");
535 let result = JsString::concat(lock, left, right);
536 assert_eq!(result.to_string(lock), "hello, world");
537 Ok(())
538 });
539}
540 
541#[test]
542fn string_concat_with_empty_string() {
543 let harness = crate::Harness::new();
544 harness.run_in_context(|lock, _ctx| {
545 let s = from_utf8(lock, "abc");
546 let empty = JsString::empty(lock);
547 let result = JsString::concat(lock, s, empty);
548 assert_eq!(result.to_string(lock), "abc");
549 Ok(())
550 });
551}
552 
553#[test]
554fn string_internalize_returns_equal_string() {
555 let harness = crate::Harness::new();
556 harness.run_in_context(|lock, _ctx| {
557 let s = from_utf8(lock, "intern-me");
558 let interned = s.internalize(lock);
559 assert_eq!(interned.to_string(lock), "intern-me");
560 Ok(())
561 });
562}
563 
564#[test]
565fn string_internalize_twice_returns_equal_content() {
566 let harness = crate::Harness::new();
567 harness.run_in_context(|lock, _ctx| {
568 let s = from_utf8(lock, "dedup");
569 let a = s.internalize(lock);
570 let b = s.internalize(lock);
571 assert_eq!(a.to_string(lock), b.to_string(lock));
572 Ok(())
573 });
574}
575 
576#[test]
577fn string_is_flat_for_simple_string() {
578 let harness = crate::Harness::new();
579 harness.run_in_context(|lock, _ctx| {
580 let s = from_utf8(lock, "flat");
581 assert!(s.is_flat());
582 Ok(())
583 });
584}
585 
586#[test]
587fn string_max_length_matches_v8() {
588 // V8's kMaxLength is (1<<28)-16 on 32-bit and (1<<29)-24 on 64-bit.
589 #[cfg(target_pointer_width = "32")]
590 assert_eq!(JsString::MAX_LENGTH, (1 << 28) - 16);
591 #[cfg(target_pointer_width = "64")]
592 assert_eq!(JsString::MAX_LENGTH, (1 << 29) - 24);
593}