|
5 | 5 | * file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
6 | 6 | */
|
7 | 7 |
|
8 |
| -use crate::framework::itest; |
| 8 | +use crate::framework::{expect_panic, itest}; |
9 | 9 | use godot::prelude::*;
|
10 | 10 |
|
11 | 11 | #[itest(skip)]
|
@@ -105,6 +105,86 @@ fn base_gd_self() {
|
105 | 105 | obj.free();
|
106 | 106 | }
|
107 | 107 |
|
| 108 | +// Hardening against https://github.com/godot-rust/gdext/issues/711. |
| 109 | +#[itest] |
| 110 | +fn base_smuggling() { |
| 111 | + let (mut obj, extracted_base) = create_object_with_extracted_base(); |
| 112 | + |
| 113 | + // This works because Gd<T> additionally stores the instance ID (through cached_rtti). |
| 114 | + assert_eq!(extracted_base.to_gd().instance_id(), obj.instance_id()); |
| 115 | + |
| 116 | + // This _also_ works because Gd<T> has the direct object pointer to the Godot object. |
| 117 | + obj.set_position(Vector2::new(1.0, 2.0)); |
| 118 | + assert_eq!( |
| 119 | + extracted_base.to_gd().get_position(), |
| 120 | + Vector2::new(1.0, 2.0) |
| 121 | + ); |
| 122 | + |
| 123 | + // Destroy base externally. |
| 124 | + extracted_base.to_gd().free(); |
| 125 | + |
| 126 | + // Access to object should now fail. |
| 127 | + expect_panic("object with dead base: calling base methods", || { |
| 128 | + obj.get_position(); |
| 129 | + }); |
| 130 | + expect_panic("object with dead base: bind()", || { |
| 131 | + obj.bind(); |
| 132 | + }); |
| 133 | + expect_panic("object with dead base: instance_id()", || { |
| 134 | + obj.instance_id(); |
| 135 | + }); |
| 136 | + expect_panic("object with dead base: clone()", || { |
| 137 | + let _ = obj.clone(); |
| 138 | + }); |
| 139 | + expect_panic("object with dead base: upcast()", || { |
| 140 | + obj.upcast::<Object>(); |
| 141 | + }); |
| 142 | + |
| 143 | + // Now vice versa: destroy object, access base. |
| 144 | + let (obj, extracted_base) = create_object_with_extracted_base(); |
| 145 | + obj.free(); |
| 146 | + |
| 147 | + expect_panic("accessing extracted base of dead object", || { |
| 148 | + extracted_base.to_gd().get_position(); |
| 149 | + }); |
| 150 | +} |
| 151 | + |
| 152 | +// While base swapping isn't an encouraged workflow, it can also be regarded as a quicker way to swap all individual properties of two base |
| 153 | +// objects -- which is also allowed. It's also similar to slicing in C++. So this is a Ship-of-Theseus problem, and we don't install ergonomic |
| 154 | +// obstacles to prevent it. Here, we test that results are expected and safe. |
| 155 | +#[itest] |
| 156 | +fn base_swapping() { |
| 157 | + let (one, mut one_ext_base) = create_object_with_extracted_base(); |
| 158 | + let one_id = one.instance_id(); |
| 159 | + |
| 160 | + let mut two = Based::new_alloc(); |
| 161 | + let two_id = two.instance_id(); |
| 162 | + |
| 163 | + std::mem::swap(&mut one_ext_base, &mut two.bind_mut().base); |
| 164 | + |
| 165 | + // Gd<T> itself isn't affected (it stores the ID separately). |
| 166 | + assert_eq!(one_id, one.instance_id()); |
| 167 | + assert_eq!(two_id, two.instance_id()); |
| 168 | + |
| 169 | + // However, the base now has the other object's ID. Gd<T> and T.base having distinct IDs is a bit unintuitive and could lead to follow-up |
| 170 | + // logic errors. One option to prevent this would be to add a base integrity check on the entire Gd<T> API (it can't be done from the |
| 171 | + // Base<T> side, since that only has direct access to the object pointer, while Gd<T> has access to the object pointer _and_ the base field). |
| 172 | + // Not sure if this is worth the effort + complexity though, given that it almost requires malice to get into such a situation. |
| 173 | + assert_eq!(one.instance_id(), two.bind().base().instance_id()); |
| 174 | + assert_eq!(two.instance_id(), one_ext_base.to_gd().instance_id()); |
| 175 | + |
| 176 | + one.free(); |
| 177 | + two.free(); |
| 178 | +} |
| 179 | + |
| 180 | +fn create_object_with_extracted_base() -> (Gd<Baseless>, Base<Node2D>) { |
| 181 | + let mut extracted_base = None; |
| 182 | + let obj = Baseless::smuggle_out(&mut extracted_base); |
| 183 | + let extracted_base = extracted_base.expect("smuggling didn't work"); |
| 184 | + |
| 185 | + (obj, extracted_base) |
| 186 | +} |
| 187 | + |
108 | 188 | // ----------------------------------------------------------------------------------------------------------------------------------------------
|
109 | 189 |
|
110 | 190 | use renamed_bases::Based;
|
@@ -139,3 +219,13 @@ impl Based {
|
139 | 219 | struct Baseless {
|
140 | 220 | // No need for fields, we just test if we can access this as Gd<Node2D>.
|
141 | 221 | }
|
| 222 | + |
| 223 | +impl Baseless { |
| 224 | + /// Steals the `Base<T>` from a newly constructed object and stores it in the output parameter. |
| 225 | + fn smuggle_out(other_base: &mut Option<Base<Node2D>>) -> Gd<Self> { |
| 226 | + Gd::from_init_fn(|base| { |
| 227 | + *other_base = Some(base); |
| 228 | + Self {} |
| 229 | + }) |
| 230 | + } |
| 231 | +} |
0 commit comments