File
Blob: src/rust/jsg-test/tests/unwrap.rs
| 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 | |
| 5 | use jsg::Number; |
| 6 | use jsg::v8::ToLocalValue; |
| 7 | |
| 8 | #[test] |
| 9 | fn v8_is_string_returns_true_for_strings() { |
| 10 | let harness = crate::Harness::new(); |
| 11 | harness.run_in_context(|lock, _ctx| { |
| 12 | let str_val = "hello".to_local(lock); |
| 13 | assert!(str_val.is_string()); |
| 14 | Ok(()) |
| 15 | }); |
| 16 | } |
| 17 | |
| 18 | #[test] |
| 19 | fn v8_is_boolean_returns_true_for_booleans() { |
| 20 | let harness = crate::Harness::new(); |
| 21 | harness.run_in_context(|lock, _ctx| { |
| 22 | let bool_true = true.to_local(lock); |
| 23 | assert!(bool_true.is_boolean()); |
| 24 | |
| 25 | let bool_false = false.to_local(lock); |
| 26 | assert!(bool_false.is_boolean()); |
| 27 | Ok(()) |
| 28 | }); |
| 29 | } |
| 30 | |
| 31 | #[test] |
| 32 | fn v8_is_number_returns_true_for_numbers() { |
| 33 | let harness = crate::Harness::new(); |
| 34 | harness.run_in_context(|lock, _ctx| { |
| 35 | let float_val = Number::new(2.5).to_local(lock); |
| 36 | assert!(float_val.is_number()); |
| 37 | |
| 38 | let int_val = 42u32.to_local(lock); |
| 39 | assert!(int_val.is_number()); |
| 40 | |
| 41 | let num_u8 = 255u8.to_local(lock); |
| 42 | assert!(num_u8.is_number()); |
| 43 | Ok(()) |
| 44 | }); |
| 45 | } |
| 46 | |
| 47 | #[test] |
| 48 | fn v8_type_checks_are_mutually_exclusive() { |
| 49 | let harness = crate::Harness::new(); |
| 50 | harness.run_in_context(|lock, _ctx| { |
| 51 | // String is only string |
| 52 | let s = "test".to_local(lock); |
| 53 | assert!(s.is_string() && !s.is_boolean() && !s.is_number()); |
| 54 | |
| 55 | // Boolean is only boolean |
| 56 | let b = true.to_local(lock); |
| 57 | assert!(!b.is_string() && b.is_boolean() && !b.is_number()); |
| 58 | |
| 59 | // Number is only number |
| 60 | let n = Number::new(42.0).to_local(lock); |
| 61 | assert!(!n.is_string() && !n.is_boolean() && n.is_number()); |
| 62 | Ok(()) |
| 63 | }); |
| 64 | } |
| 65 | |
| 66 | #[test] |
| 67 | fn v8_unwrap_boolean_returns_correct_values() { |
| 68 | let harness = crate::Harness::new(); |
| 69 | harness.run_in_context(|lock, _ctx| { |
| 70 | let bool_true = true.to_local(lock); |
| 71 | // SAFETY: The isolate is locked and the value is a valid V8 local handle. |
| 72 | let unwrapped_true = |
| 73 | unsafe { jsg::v8::ffi::unwrap_boolean(lock.isolate().as_ffi(), bool_true.into_ffi()) }; |
| 74 | assert!(unwrapped_true); |
| 75 | |
| 76 | let bool_false = false.to_local(lock); |
| 77 | // SAFETY: The isolate is locked and the value is a valid V8 local handle. |
| 78 | let unwrapped_false = |
| 79 | unsafe { jsg::v8::ffi::unwrap_boolean(lock.isolate().as_ffi(), bool_false.into_ffi()) }; |
| 80 | assert!(!unwrapped_false); |
| 81 | Ok(()) |
| 82 | }); |
| 83 | } |
| 84 | |
| 85 | #[test] |
| 86 | fn v8_unwrap_number_returns_correct_values() { |
| 87 | let harness = crate::Harness::new(); |
| 88 | harness.run_in_context(|lock, _ctx| { |
| 89 | let num = Number::new(2.5).to_local(lock); |
| 90 | // SAFETY: The isolate is locked and the value is a valid V8 local handle. |
| 91 | let unwrapped = |
| 92 | unsafe { jsg::v8::ffi::unwrap_number(lock.isolate().as_ffi(), num.into_ffi()) }; |
| 93 | assert!((unwrapped - 2.5).abs() < f64::EPSILON); |
| 94 | |
| 95 | let zero = Number::new(0.0).to_local(lock); |
| 96 | // SAFETY: The isolate is locked and the value is a valid V8 local handle. |
| 97 | let unwrapped_zero = |
| 98 | unsafe { jsg::v8::ffi::unwrap_number(lock.isolate().as_ffi(), zero.into_ffi()) }; |
| 99 | assert!(unwrapped_zero.abs() < f64::EPSILON); |
| 100 | |
| 101 | let negative = Number::new(-42.5).to_local(lock); |
| 102 | // SAFETY: The isolate is locked and the value is a valid V8 local handle. |
| 103 | let unwrapped_neg = |
| 104 | unsafe { jsg::v8::ffi::unwrap_number(lock.isolate().as_ffi(), negative.into_ffi()) }; |
| 105 | assert!((unwrapped_neg - (-42.5)).abs() < f64::EPSILON); |
| 106 | Ok(()) |
| 107 | }); |
| 108 | } |
| 109 | |
| 110 | #[test] |
| 111 | fn v8_unwrap_string_returns_correct_values() { |
| 112 | let harness = crate::Harness::new(); |
| 113 | harness.run_in_context(|lock, _ctx| { |
| 114 | let s = "hello world".to_local(lock); |
| 115 | // SAFETY: The isolate is locked and the value is a valid V8 local handle. |
| 116 | let unwrapped = |
| 117 | unsafe { jsg::v8::ffi::unwrap_string(lock.isolate().as_ffi(), s.into_ffi()) }; |
| 118 | assert_eq!(unwrapped.as_str(), "hello world"); |
| 119 | |
| 120 | let empty = "".to_local(lock); |
| 121 | // SAFETY: The isolate is locked and the value is a valid V8 local handle. |
| 122 | let unwrapped_empty = |
| 123 | unsafe { jsg::v8::ffi::unwrap_string(lock.isolate().as_ffi(), empty.into_ffi()) }; |
| 124 | assert_eq!(unwrapped_empty.as_str(), ""); |
| 125 | |
| 126 | let unicode = "こんにちは".to_local(lock); |
| 127 | // SAFETY: The isolate is locked and the value is a valid V8 local handle. |
| 128 | let unwrapped_unicode = |
| 129 | unsafe { jsg::v8::ffi::unwrap_string(lock.isolate().as_ffi(), unicode.into_ffi()) }; |
| 130 | assert_eq!(unwrapped_unicode.as_str(), "こんにちは"); |
| 131 | Ok(()) |
| 132 | }); |
| 133 | } |
| 134 | |
| 135 | /// Tests that `unwrap_resource` rejects a C++ JSG object (tagged with `WORKERD_WRAPPABLE_TAG`). |
| 136 | /// |
| 137 | /// Rust wrappables use `WORKERD_RUST_WRAPPABLE_TAG` (0xeb05), while C++ JSG objects use |
| 138 | /// `WORKERD_WRAPPABLE_TAG` (0xeb04). Attempting to unwrap a C++ object through the Rust path |
| 139 | /// must return nullptr to prevent reading garbage from non-existent `data[2]` fields. |
| 140 | #[test] |
| 141 | fn unwrap_resource_rejects_cpp_tagged_object() { |
| 142 | let harness = crate::Harness::new(); |
| 143 | harness.run_in_context(|lock, _ctx| { |
| 144 | let cpp_obj = crate::Harness::create_cpp_tagged_object(lock); |
| 145 | |
| 146 | // unwrap_resource returns nullptr because the object has the C++ tag, not the Rust tag. |
| 147 | let result = |
| 148 | // SAFETY: isolate is valid and locked, value is a valid Local. |
| 149 | unsafe { jsg::v8::ffi::unwrap_resource(lock.isolate().as_ffi(), cpp_obj.into_ffi()) }; |
| 150 | assert!( |
| 151 | result.get().is_null(), |
| 152 | "unwrap_resource should return null for a C++ tagged object" |
| 153 | ); |
| 154 | |
| 155 | Ok(()) |
| 156 | }); |
| 157 | } |
| 158 | |
| 159 | /// Tests that `unwrap_resource` rejects a plain JS object with no internal fields. |
| 160 | /// |
| 161 | /// A plain `{}` object has no wrappable tag at all. This verifies we don't crash on |
| 162 | /// objects that were never wrapped by either the C++ or Rust JSG layer. |
| 163 | #[test] |
| 164 | fn unwrap_resource_rejects_plain_js_object() { |
| 165 | let harness = crate::Harness::new(); |
| 166 | harness.run_in_context(|lock, ctx| { |
| 167 | let plain_obj = ctx.eval_raw("({})").unwrap(); |
| 168 | |
| 169 | let result = |
| 170 | // SAFETY: isolate is valid and locked, value is a valid Local. |
| 171 | unsafe { jsg::v8::ffi::unwrap_resource(lock.isolate().as_ffi(), plain_obj.into_ffi()) }; |
| 172 | assert!( |
| 173 | result.get().is_null(), |
| 174 | "unwrap_resource should return null for a plain JS object" |
| 175 | ); |
| 176 | |
| 177 | Ok(()) |
| 178 | }); |
| 179 | } |