Skip to main content

kg_js/
ctx.rs

1use std::borrow::Cow;
2use std::ops::DerefMut;
3use super::*;
4
5macro_rules! try_exec_success {
6    ($res:expr) => {
7        if $res != DUK_EXEC_SUCCESS {
8            return Err($res)
9        }
10    }
11}
12
13/// Wrapper for Duktape context
14#[derive(Debug)]
15pub struct DukContext {
16    pub (crate) ctx: *mut duk_context,
17}
18
19impl DukContext {
20    pub (crate) unsafe fn from_raw(ctx: *mut duk_context) -> Self {
21        Self { ctx }
22    }
23
24    #[inline]
25    pub fn normalize_index(&self, index: i32) -> i32 {
26        unsafe {
27            duk_normalize_index(self.ctx, index)
28        }
29    }
30
31    #[inline]
32    pub fn get_top(&self) -> i32 {
33        unsafe { duk_get_top(self.ctx) }
34    }
35
36    #[inline]
37    pub fn dup(&self, index: i32) {
38        unsafe {
39            duk_dup(self.ctx, index);
40        }
41    }
42
43    #[inline]
44    pub fn remove(&self, index: i32) {
45        unsafe {
46            duk_remove(self.ctx, index);
47        }
48    }
49
50    #[inline]
51    pub fn pop(&self) {
52        unsafe {
53            duk_pop(self.ctx);
54        }
55    }
56
57    #[inline]
58    pub fn pop_n(&self, n: i32) {
59        unsafe {
60            duk_pop_n(self.ctx, n);
61        }
62    }
63
64    #[inline]
65    pub fn swap(&self, idx1: i32, idx2: i32) {
66        unsafe {
67            duk_swap(self.ctx, idx1, idx2);
68        }
69    }
70
71    #[inline]
72    pub fn push_this(&self) {
73        unsafe { duk_push_this(self.ctx); }
74    }
75
76    #[inline]
77    pub fn push_thread(&self) -> i32 {
78        unsafe { duk_push_thread_raw(self.ctx, 0) }
79    }
80
81    #[inline]
82    pub fn push_thread_new_globalenv(&self) -> i32 {
83        unsafe { duk_push_thread_raw(self.ctx, DukThreadFlags::DUK_THREAD_NEW_GLOBAL_ENV.bits()) }
84    }
85
86    #[inline]
87    pub fn push_global_object(&self) {
88        unsafe { duk_push_global_object(self.ctx); }
89    }
90
91    #[inline]
92    pub fn push_boolean(&self, value: bool) {
93        unsafe { duk_push_boolean(self.ctx, value as i32) }
94    }
95
96    #[inline]
97    pub fn push_null(&self) {
98        unsafe { duk_push_null(self.ctx) }
99    }
100
101    #[inline]
102    pub fn push_undefined(&self) {
103        unsafe { duk_push_undefined(self.ctx) }
104    }
105
106    #[inline]
107    pub fn push_i32(&self, value: i32) {
108        unsafe { duk_push_int(self.ctx, value) }
109    }
110
111    #[inline]
112    pub fn push_u32(&self, value: u32) {
113        unsafe { duk_push_uint(self.ctx, value) }
114    }
115
116    #[inline]
117    pub fn push_number(&self, value: f64) {
118        unsafe { duk_push_number(self.ctx, value) }
119    }
120
121    #[inline]
122    pub fn push_string(&self, value: &str) {
123        unsafe {
124            duk_push_lstring(self.ctx, value.as_ptr() as *const c_char, value.len());
125        }
126    }
127
128    #[inline]
129    pub fn push_object(&self) -> i32 {
130        unsafe { duk_push_object(self.ctx) }
131    }
132
133    #[inline]
134    pub fn push_ext_buffer(&self, data: &[u8]) {
135        unsafe {
136            duk_push_buffer_raw(self.ctx, 0, (DukBufFlags::DUK_BUF_FLAG_DYNAMIC | DukBufFlags::DUK_BUF_FLAG_EXTERNAL).bits());
137            duk_config_buffer(self.ctx, -1, data.as_ptr() as *mut c_void, data.len());
138        }
139    }
140
141    #[inline]
142    pub fn push_array(&self) -> i32 {
143        unsafe { duk_push_array(self.ctx) }
144    }
145
146    pub fn push_function(&self, func_name: &str, nargs: i32) {
147        unsafe {
148            duk_push_c_function(self.ctx, Some(func_dispatch), nargs);
149            duk_push_lstring(self.ctx, FUNC_NAME_PROP.as_ptr() as *const c_char, FUNC_NAME_PROP.len());
150            duk_push_lstring(self.ctx, func_name.as_ptr() as *const c_char, func_name.len());
151            duk_def_prop(self.ctx, -3, (DukDefpropFlags::DUK_DEFPROP_ENUMERABLE | DukDefpropFlags::DUK_DEFPROP_HAVE_VALUE).bits())
152        }
153    }
154
155    pub fn put_prop_function(&self, obj_index: i32, func_name: &str, nargs: i32) {
156        let obj_index = self.normalize_index(obj_index);
157        self.push_function(func_name, nargs);
158        unsafe {
159            duk_put_prop_lstring(self.ctx, obj_index, func_name.as_ptr() as *const c_char, func_name.len());
160        }
161    }
162
163    pub fn put_global_function(&self, func_name: &str, nargs: i32) {
164        self.push_function(func_name, nargs);
165        self.put_global_string(func_name);
166    }
167
168    #[inline]
169    pub fn get_type(&self, index: i32) -> DukType {
170        DukType::from(unsafe { duk_get_type(self.ctx, index) })
171    }
172
173    #[inline]
174    pub fn is_string(&self, index: i32) -> bool {
175        unsafe { duk_is_string(self.ctx, index) == 1 }
176    }
177
178    #[inline]
179    pub fn is_number(&self, index: i32) -> bool {
180        unsafe { duk_is_number(self.ctx, index) == 1 }
181    }
182
183    #[inline]
184    pub fn is_object(&self, index: i32) -> bool {
185        unsafe { duk_is_object(self.ctx, index) == 1 }
186    }
187
188    #[inline]
189    pub fn is_array(&self, index: i32) -> bool {
190        unsafe { duk_is_array(self.ctx, index) == 1 }
191    }
192
193    #[inline]
194    pub fn is_pure_object(&self, index: i32) -> bool {
195        unsafe {
196            duk_is_object(self.ctx, index) == 1
197                && duk_is_array(self.ctx, index) == 0
198                && duk_is_function(self.ctx, index) == 0
199                && duk_is_thread(self.ctx, index) == 0
200        }
201    }
202
203    #[inline]
204    pub fn get_string(&self, index: i32) -> Cow<'_, str> {
205        use std::slice;
206        let bytes = unsafe {
207            let mut len: usize = 0;
208            let ptr = duk_get_lstring(self.ctx, index, Some(&mut len)) as *const u8;
209            slice::from_raw_parts(ptr, len)
210        };
211        // Duktape strings are CESU-8, which can encode lone UTF-16 surrogates (legal
212        // ECMAScript content) as byte sequences that are not valid UTF-8. Lossy-decode
213        // instead of assuming validity; valid input (the common case) is still returned
214        // as a zero-copy borrow of this buffer via Cow::Borrowed.
215        String::from_utf8_lossy(bytes)
216    }
217
218    #[inline]
219    pub fn get_buffer(&self, index: i32) -> &[u8] {
220        use std::slice;
221        unsafe {
222            let mut len: usize = 0;
223            let ptr = duk_get_buffer(self.ctx, index, Some(&mut len)) as *const u8;
224            slice::from_raw_parts(ptr, len)
225        }
226    }
227
228    #[inline]
229    pub fn get_number(&self, index: i32) -> f64 {
230        unsafe { duk_get_number(self.ctx, index) }
231    }
232
233    #[inline]
234    pub fn get_boolean(&self, index: i32) -> bool {
235        unsafe { duk_get_boolean(self.ctx, index) != 0 }
236    }
237
238    pub fn get_context(&self, index: i32) -> Result<DukContextGuard<'_>, JsError> {
239        let new_ctx = unsafe { duk_get_context(self.ctx, index) };
240        if new_ctx.is_null() {
241            return Err(JsError::from(format!("could not get context from index {}", index)));
242        }
243        Ok(DukContextGuard::new(unsafe { DukContext::from_raw(new_ctx) }))
244    }
245
246    #[inline]
247    pub fn get_prop(&self, obj_index: i32) -> bool {
248        unsafe { duk_get_prop(self.ctx, obj_index) == 1 }
249    }
250
251    #[inline]
252    pub fn put_prop(&self, obj_index: i32) {
253        unsafe { duk_put_prop(self.ctx, obj_index); }
254    }
255
256    #[inline]
257    pub fn get_prop_string(&self, obj_index: i32, key: &str) -> bool {
258        unsafe {
259            duk_get_prop_lstring(self.ctx, obj_index, key.as_ptr() as *const c_char, key.len()) == 1
260        }
261    }
262
263    #[inline]
264    pub fn put_prop_string(&self, obj_index: i32, key: &str) {
265        unsafe {
266            duk_put_prop_lstring(self.ctx,
267                                 obj_index,
268                                 key.as_ptr() as *const c_char,
269                                 key.len());
270        }
271    }
272
273    #[inline]
274    pub fn get_prop_index(&self, obj_index: i32, index: u32) -> bool {
275        unsafe { duk_get_prop_index(self.ctx, obj_index, index) == 1 }
276    }
277
278    #[inline]
279    pub fn put_prop_index(&self, obj_index: i32, index: u32) {
280        unsafe {
281            duk_put_prop_index(self.ctx, obj_index, index);
282        }
283    }
284
285    #[inline]
286    pub fn get_global_string(&self, key: &str) -> bool {
287        unsafe {
288            duk_get_global_lstring(self.ctx, key.as_ptr() as *const c_char, key.len()) == 1
289        }
290    }
291
292    #[inline]
293    pub fn put_global_string(&self, key: &str) {
294        unsafe {
295            duk_put_global_lstring(self.ctx, key.as_ptr() as *const c_char, key.len());
296        }
297    }
298
299    #[inline]
300    pub fn get_length(&self, obj_index: i32) -> usize {
301        unsafe {
302            duk_get_length(self.ctx, obj_index)
303        }
304    }
305
306    #[inline]
307    pub fn enum_indices(&self, obj_index: i32) {
308        unsafe {
309            duk_enum(self.ctx, obj_index, DukEnumFlags::DUK_ENUM_ARRAY_INDICES_ONLY.bits());
310        }
311    }
312
313    #[inline]
314    pub fn enum_keys(&self, obj_index: i32) {
315        unsafe {
316            duk_enum(self.ctx, obj_index, DukEnumFlags::DUK_ENUM_OWN_PROPERTIES_ONLY.bits());
317        }
318    }
319
320    #[inline]
321    pub fn next(&self, obj_index: i32) -> bool {
322        unsafe {
323            duk_next(self.ctx, obj_index, 1) == 1
324        }
325    }
326
327    #[inline]
328    pub fn call_prop(&self, obj_index: i32, nargs: usize) {
329        unsafe {
330            duk_call_prop(self.ctx, obj_index, nargs as i32);
331        }
332    }
333
334    #[inline]
335    pub fn pcall(&self, nargs: usize) -> Result<(), i32> {
336        let res = unsafe {
337            duk_pcall(self.ctx, nargs as i32)
338        };
339        try_exec_success!(res);
340        Ok(())
341    }
342
343    #[inline]
344    pub fn pcall_method(&self, nargs: usize) -> Result<(), i32> {
345        let res = unsafe {
346            duk_pcall_method(self.ctx, nargs as i32)
347        };
348        try_exec_success!(res);
349        Ok(())
350    }
351
352    #[inline]
353    pub fn pcall_prop(&self, obj_index: i32, nargs: usize) -> Result<(), i32> {
354        let res = unsafe {
355            duk_pcall_prop(self.ctx, obj_index, nargs as i32)
356        };
357        try_exec_success!(res);
358        Ok(())
359    }
360
361    #[inline]
362    pub fn safe_to_lstring(&self, obj_index: i32) -> String {
363        unsafe {
364            let mut len: usize = 0;
365            let msg = duk_safe_to_lstring(self.ctx, obj_index, &mut len);
366            String::from(std::str::from_utf8_unchecked(std::slice::from_raw_parts(msg as *const u8, len)))
367        }
368    }
369
370    #[inline]
371    pub fn throw(&self) {
372        unsafe {
373            duk_throw_raw(self.ctx);
374        }
375    }
376
377    #[inline]
378    pub fn push_context_dump(&self) {
379        unsafe {
380            duk_push_context_dump(self.ctx);
381        }
382    }
383
384    pub fn get_stack_dump(&self) -> String {
385        self.push_context_dump();
386        unsafe {
387            let dump = CStr::from_ptr(duk_to_string(self.ctx, -1)).to_string_lossy().to_string();
388            duk_pop(self.ctx);
389            dump
390        }
391    }
392
393    /// Propagate JS error to Rust, popping the error from the stack.
394    /// js_res: Result<(), i32> - JS result returned by protected call functions.
395    /// If it is an error, it will be converted to JsError.
396    /// This method should be called immediately after a protected call to handle the error.
397    pub fn propagate_js_error<T>(&self, js_res: Result<T, i32>) -> Result<T, JsError> {
398        unsafe {
399            match js_res {
400                Ok(v) => Ok(v),
401                Err(_err) => {
402                    let mut len: usize = 0;
403                    let msg = duk_safe_to_lstring(self.ctx, -1, &mut len);
404                    let s = String::from(std::str::from_utf8_unchecked(std::slice::from_raw_parts(msg as *const u8, len)));
405                    duk_pop(self.ctx);
406                    Err(JsError::from(s))
407                }
408            }
409        }
410    }
411
412    #[inline]
413    pub fn eval(&self, code: &str) -> Result<(), JsError> {
414        unsafe {
415            if duk_eval_raw(self.ctx,
416                            code.as_ptr() as *const c_char,
417                            code.len(),
418                            0 | (DukCompileFlags::DUK_COMPILE_SAFE | DukCompileFlags::DUK_COMPILE_NOSOURCE | DukCompileFlags::DUK_COMPILE_NOFILENAME).bits()) != 0 {
419                let mut len: usize = 0;
420                let msg = duk_safe_to_lstring(self.ctx, -1, &mut len);
421                let s = String::from(std::str::from_utf8_unchecked(std::slice::from_raw_parts(msg as *const u8, len)));
422                duk_pop(self.ctx);
423                Err(JsError::from(s))
424            } else {
425                Ok(())
426            }
427        }
428    }
429
430    #[inline]
431    pub fn eval_file(&self, filename: &str, code: &str) -> Result<(), JsError> {
432        unsafe {
433            duk_push_lstring(self.ctx, filename.as_ptr() as *const c_char, filename.len());
434            if duk_eval_raw(self.ctx,
435                            code.as_ptr() as *const c_char,
436                            code.len(),
437                            1 | (DukCompileFlags::DUK_COMPILE_SAFE | DukCompileFlags::DUK_COMPILE_NOSOURCE).bits()) != 0 {
438                let s = self.safe_to_lstring(-1);
439                duk_pop(self.ctx);
440                Err(JsError::from(s))
441            } else {
442                Ok(())
443            }
444        }
445    }
446
447    #[inline]
448    pub fn compile(&self, code: &str) -> Result<(), JsError> {
449        unsafe {
450            if duk_compile_raw(self.ctx,
451                               code.as_ptr() as *const c_char,
452                               code.len(),
453                               0 | (DukCompileFlags::DUK_COMPILE_NORESULT | DukCompileFlags::DUK_COMPILE_NOFILENAME).bits()) != 0 {
454                let s = self.safe_to_lstring(-1);
455                duk_pop(self.ctx);
456                Err(JsError::from(s))
457            } else {
458                Ok(())
459            }
460        }
461    }
462
463    #[inline]
464    pub fn compile_file(&self, filename: &str, code: &str) -> Result<(), JsError> {
465        unsafe {
466            duk_push_lstring(self.ctx, filename.as_ptr() as *const c_char, filename.len());
467            if duk_compile_raw(self.ctx,
468                               code.as_ptr() as *const c_char,
469                               code.len(),
470                               1 | DukCompileFlags::DUK_COMPILE_NORESULT.bits()) != 0 {
471                let s = self.safe_to_lstring(-1);
472                duk_pop(self.ctx);
473                Err(JsError::from(s))
474            } else {
475                Ok(())
476            }
477        }
478    }
479
480    #[inline]
481    pub fn write<O: WriteJs>(&self, obj: &O) -> Result<(), JsError> {
482        obj.write_js(self)
483    }
484
485    #[inline]
486    pub fn read<O: ReadJs>(&self, obj_index: i32) -> Result<O, JsError> {
487        let obj_index = self.normalize_index(obj_index);
488        O::read_js(self, obj_index)
489    }
490
491    #[inline]
492    pub fn read_top<O: ReadJs>(&self) -> Result<O, JsError> {
493        self.read( -1)
494    }
495
496    /// Initialize console functions.
497    #[inline]
498    pub fn init_console(&self) {
499        unsafe {
500            duk_api_console_init(self.ctx, Some(console_func));
501        }
502    }
503
504    #[inline]
505    pub fn xcopy_top(&self, from: &DukContext, count: i32) {
506        unsafe {
507            duk_xcopymove_raw(self.ctx, from.ctx, count, 1);
508        }
509    }
510
511    #[inline]
512    pub fn xmove_top(&self, from: &mut DukContext, count: i32) {
513        unsafe {
514            duk_xcopymove_raw(self.ctx, from.ctx, count, 0);
515        }
516    }
517
518    #[inline]
519    pub fn check_stack(&self, extra: i32) -> Result<(), JsError> {
520        let res = unsafe {
521            duk_check_stack(self.ctx, extra)
522        };
523
524        if res {
525            Ok(())
526        } else {
527            Err(JsError::from("failed to reserve enough stack space".to_string()))
528        }
529    }
530
531    #[inline]
532    pub fn check_stack_top(&self, top: i32) -> Result<(), JsError> {
533        let res = unsafe {
534            duk_check_stack_top(self.ctx, top)
535        };
536        if res {
537            Ok(())
538        } else {
539            Err(JsError::from("failed to reserve enough stack space".to_string()))
540        }
541    }
542
543    #[inline]
544    pub fn set_global_object(&self) {
545        unsafe {
546            duk_set_global_object(self.ctx);
547        }
548    }
549
550    pub fn gc(&self) {
551        unsafe {
552            duk_gc(self.ctx, DukGcFlags::NONE.bits());
553        }
554    }
555}
556
557pub struct DukContextGuard<'a> {
558    ctx: DukContext,
559    _marker: std::marker::PhantomData<&'a JsEngine>,
560}
561
562impl std::fmt::Debug for DukContextGuard<'_> {
563    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
564        f.debug_struct("DukContextGuard").finish()
565    }
566}
567
568impl Deref for DukContextGuard<'_> {
569    type Target = DukContext;
570
571    fn deref(&self) -> &Self::Target {
572        &self.ctx
573    }
574}
575
576impl DerefMut for DukContextGuard<'_> {
577    fn deref_mut(&mut self) -> &mut Self::Target {
578        &mut self.ctx
579    }
580}
581
582impl <'a> DukContextGuard<'a> {
583    pub fn new(ctx: DukContext) -> Self {
584        Self {
585            ctx,
586            _marker: std::marker::PhantomData,
587        }
588    }
589}
590
591#[cfg(test)]
592mod tests {
593    use serde::Deserialize;
594    use super::*;
595
596    #[test]
597    fn test_eval() {
598        let engine = JsEngine::new().unwrap();
599        //language=js
600        engine.eval(r#" var tmp =  {
601            "foo": 1,
602            "bar": "baz"
603        }
604        tmp
605        "#).unwrap();
606
607        #[derive(Deserialize)]
608        struct TestStruct {
609            foo: i32,
610        }
611        assert_eq!(engine.read_top::<TestStruct>().unwrap().foo, 1);
612        engine.pop();
613    }
614
615    #[test]
616    fn test_get_invalid_context() {
617        let engine = JsEngine::new().unwrap();
618        let res = engine.get_context(0);
619        assert!(res.is_err());
620    }
621
622    #[test]
623    fn test_push_thread() {
624        let engine = JsEngine::new().unwrap();
625        let new_idx = engine.push_thread();
626        let new_ctx = engine.get_context(new_idx).unwrap();
627        new_ctx.push_string("test");
628        assert_eq!(&*new_ctx.get_string(-1), "test");
629        new_ctx.pop();
630        assert!(new_ctx.get_stack_dump().contains("ctx: top=0"));
631
632        drop(new_ctx);
633        engine.pop();
634        assert!(engine.get_stack_dump().contains("ctx: top=0"));
635    }
636
637    #[test]
638    fn test_nested_push_thread() {
639        let engine = JsEngine::new().unwrap();
640        let new_idx = engine.push_thread();
641        let new_ctx = engine.get_context(new_idx).unwrap();
642
643        let nested_id = new_ctx.push_thread();
644        let nested_ctx = new_ctx.get_context(nested_id).unwrap();
645        nested_ctx.push_string("test");
646
647        assert_eq!(&*nested_ctx.get_string(-1), "test");
648
649        drop(nested_ctx);
650        drop(new_ctx);
651
652        engine.pop();
653
654        assert!(engine.get_stack_dump().contains("ctx: top=0"));
655    }
656
657    #[test]
658    fn test_push_thread_new_globalenv() {
659        let engine = JsEngine::new().unwrap();
660
661        let new_idx = engine.push_thread_new_globalenv();
662        let new_idx2 = engine.push_thread_new_globalenv();
663
664        let new_ctx = engine.get_context(new_idx).unwrap();
665        let new_ctx2 = engine.get_context(new_idx2).unwrap();
666
667        // Test first context
668        new_ctx.push_string("test");
669        assert_eq!(&*new_ctx.get_string(-1), "test");
670        new_ctx.pop();
671        assert!(new_ctx.get_stack_dump().contains("ctx: top=0"));
672
673        // Test second context
674        new_ctx2.push_string("test2");
675        new_ctx2.push_string("test2");
676        assert_eq!(&*new_ctx2.get_string(-1), "test2");
677        // Pop only one string
678        new_ctx2.pop();
679        assert!(new_ctx2.get_stack_dump().contains("ctx: top=1"));
680
681        drop(new_ctx);
682        drop(new_ctx2);
683
684        // Pop both contexts
685        engine.pop_n(2);
686
687        assert!(engine.get_stack_dump().contains("ctx: top=0"));
688    }
689
690    #[test]
691    fn test_to_lstring_safety() {
692        let engine = JsEngine::new().unwrap();
693        engine.push_string("test");
694        let s = engine.safe_to_lstring(-1);
695        assert_eq!(s, "test");
696        engine.pop();
697        assert_eq!(s, "test");
698        drop(engine);
699        assert_eq!(s, "test");
700    }
701
702    #[test]
703    fn test_check_stack_error() {
704        let engine = JsEngine::new().unwrap();
705        let res = engine.check_stack(i32::MAX);
706        assert!(res.is_err());
707    }
708
709    #[test]
710    fn test_xcopy_top() {
711        let engine = JsEngine::new().unwrap();
712
713        //language=javascript
714        engine.eval("GLOBAL_TEST = { a: 1, b: 2 }").unwrap();
715
716        let ctx1_idx = engine.push_thread_new_globalenv();
717        let ctx1 = engine.get_context(ctx1_idx).unwrap();
718        // Create a new global variable
719        assert!(engine.get_global_string("GLOBAL_TEST"));
720
721        ctx1.check_stack(1).unwrap();
722        // Copy the global variable to the new context
723        ctx1.xcopy_top(&engine, 1);
724        ctx1.put_global_string("GLOBAL_TEST");
725
726        // Check if the global variable is available in the new context
727        ctx1.eval("GLOBAL_TEST.b").unwrap();
728        assert_eq!(ctx1.get_number(-1), 2.0);
729
730        // Change the value of the global variable in the new context
731        ctx1.eval("GLOBAL_TEST.b = 5").unwrap();
732
733        // Check if the value has changed in original context
734        engine.eval("GLOBAL_TEST.b").unwrap();
735        assert_eq!(engine.get_number(-1), 5.0);
736        engine.pop();
737    }
738
739    #[test]
740    fn test_set_global_object() {
741        let engine = JsEngine::new().unwrap();
742
743        engine.eval("Math.abs(-1)").unwrap();
744        assert_eq!(engine.get_number(-1), 1.0);
745
746        engine.pop();
747
748        //language=javascript
749        let _: () = engine.eval("var obj = { a: 1, b: 2 }; obj").unwrap();
750        engine.set_global_object();
751
752        engine.eval("typeof Math").unwrap();
753
754        assert_eq!(&*engine.get_string(-1), "undefined");
755
756    }
757
758    #[test]
759    fn test_gc() {
760        let engine = JsEngine::new().unwrap();
761
762        engine.eval("Math.abs(-1)").unwrap();
763        assert_eq!(engine.get_number(-1), 1.0);
764        engine.pop();
765        engine.gc();
766    }
767
768    /// Duktape strings are CESU-8 / "extended UTF-8" encoded, which can represent lone
769    /// (unpaired) UTF-16 surrogates - legal ECMAScript string content that is not valid
770    /// UTF-8 (e.g. `"\uD800"`, a lone high surrogate, CESU-8-encodes to the 3 bytes
771    /// 0xED 0xA0 0x80, which decode to the forbidden codepoint U+D800). `get_string`
772    /// used to hand these bytes to a `&str` via `from_utf8_unchecked` - immediate UB.
773    /// It now sanitizes via `String::from_utf8_lossy`, replacing invalid sequences with
774    /// U+FFFD, so the result is always a genuinely valid `&str`/`Cow<str>`.
775    #[test]
776    fn get_string_sanitizes_lone_surrogate() {
777        let engine = JsEngine::new().unwrap();
778        // A lone high surrogate: valid ECMAScript string, not valid Unicode text.
779        //language=js
780        engine.eval(r#"value = "\uD800""#).unwrap();
781        engine.get_global_string("value");
782
783        let s = engine.get_string(-1);
784
785        // Sanitized to replacement characters rather than the raw CESU-8 bytes. Three,
786        // not one: per the Unicode UTF-8 validity table, 0xED's valid continuation
787        // range is 0x80..=0x9F specifically to exclude surrogates, so 0xA0 makes 0xED
788        // an ill-formed 1-byte subpart on its own; 0xA0 and 0x80 are then each a stray
789        // continuation byte with no valid lead - three separate 1-byte error subparts.
790        assert_eq!(&*s, "\u{FFFD}\u{FFFD}\u{FFFD}");
791        assert!(std::str::from_utf8(s.as_bytes()).is_ok());
792
793        engine.pop();
794    }
795
796    /// Valid strings must still be returned as a zero-copy borrow of the duktape stack
797    /// buffer (the whole point of `Cow`: `from_utf8_lossy` returns `Cow::Borrowed` when
798    /// the input is already valid UTF-8, so the common case pays for a validation scan
799    /// but not an allocation).
800    #[test]
801    fn get_string_borrows_for_valid_input() {
802        let engine = JsEngine::new().unwrap();
803        engine.push_string("hello");
804
805        let s = engine.get_string(-1);
806
807        assert_eq!(&*s, "hello");
808        assert!(matches!(s, Cow::Borrowed(_)), "valid UTF-8 should not be copied");
809
810        engine.pop();
811    }
812
813    /// Same sanitization, exercised through the public `serde`-based deserialization
814    /// path instead of the raw `get_string` accessor - confirms the fix covers the
815    /// actually-exploitable route (e.g. a `#[derive(Deserialize)] struct { ...: String }`
816    /// field read from a script, like `HttpRequest.url` in the original bug report).
817    #[test]
818    fn deserialized_string_is_sanitized() {
819        let engine = JsEngine::new().unwrap();
820        //language=js
821        engine.eval(r#"value = "\uD800""#).unwrap();
822        engine.get_global_string("value");
823
824        let s: String = engine.read_top().unwrap();
825
826        assert_eq!(s, "\u{FFFD}\u{FFFD}\u{FFFD}");
827        assert!(std::str::from_utf8(s.as_bytes()).is_ok());
828    }
829
830    /// Regression check for the crash this fix closes: previously, `.chars()` on a
831    /// lone-surrogate string decoded an invalid `char` and hit a hard process abort
832    /// (SIGABRT) via libstd's "unsafe precondition" check - see git history for the
833    /// pre-fix version of this test. With sanitization in place, the string only ever
834    /// contains U+FFFD, an ordinary valid `char`, so no downstream operation on it can
835    /// resurrect that crash.
836    #[test]
837    fn lone_surrogate_no_longer_crashes_on_chars_iteration() {
838        let engine = JsEngine::new().unwrap();
839        //language=js
840        engine.eval(r#"value = "\uD800""#).unwrap();
841        engine.get_global_string("value");
842        let s = engine.get_string(-1);
843
844        let c = s.chars().next().expect("one (sanitized) char");
845        assert_eq!(c, '\u{FFFD}');
846        assert!(Some(c).is_some());
847
848        engine.pop();
849    }
850
851    #[test]
852    fn tes_propagate_js_error() {
853        let engine = JsEngine::new().unwrap();
854        //language=js
855        engine.eval(r#" var tmp =  {
856            "foo": 1,
857            "bar": "baz",
858            "some_fn": function() {
859                throw new Error("test error");
860            }
861        }
862        tmp
863        "#).unwrap();
864
865        engine.push_string("some_fn");
866
867        let call_res = engine.pcall_prop(0, 0);
868        assert!(call_res.is_err());
869        let res = engine.propagate_js_error(call_res);
870
871        assert!(res.is_err());
872        let err = res.unwrap_err();
873        assert!(err.to_string().contains("test error"));
874    }
875}
876