Skip to content

Commit d688332

Browse files
committed
Fix Dictionary::get unsoundness, and make it return Option<Variant>
Also adds `get_or` and `get_or_nil` convenience functions.
1 parent 206f710 commit d688332

File tree

5 files changed

+58
-23
lines changed

5 files changed

+58
-23
lines changed

gdnative-core/src/core_types/dictionary.rs

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,21 +69,45 @@ impl<Access: ThreadAccess> Dictionary<Access> {
6969
unsafe { (get_api().godot_dictionary_has_all)(self.sys(), keys.sys()) }
7070
}
7171

72-
/// Returns a copy of the value corresponding to the key.
72+
/// Returns a copy of the value corresponding to the key if it exists.
7373
#[inline]
74-
pub fn get<K>(&self, key: K) -> Variant
74+
pub fn get<K>(&self, key: K) -> Option<Variant>
7575
where
7676
K: ToVariant + ToVariantEq,
77+
{
78+
let key = key.to_variant();
79+
// This should never return the default Nil, but there isn't a safe way to otherwise check
80+
// if the entry exists in a single API call.
81+
self.contains(&key).then(|| self.get_or_nil(key))
82+
}
83+
84+
/// Returns a copy of the value corresponding to the key, or `default` if it doesn't exist
85+
#[inline]
86+
pub fn get_or<K, D>(&self, key: K, default: D) -> Variant
87+
where
88+
K: ToVariant + ToVariantEq,
89+
D: ToVariant,
7790
{
7891
unsafe {
79-
Variant((get_api().godot_dictionary_get)(
92+
Variant((get_api().godot_dictionary_get_with_default)(
8093
self.sys(),
8194
key.to_variant().sys(),
95+
default.to_variant().sys(),
8296
))
8397
}
8498
}
8599

86-
/// Update an existing element corresponding ot the key.
100+
/// Returns a copy of the element corresponding to the key, or `Nil` if it doesn't exist.
101+
/// Shorthand for `self.get_or(key, Variant::new())`.
102+
#[inline]
103+
pub fn get_or_nil<K>(&self, key: K) -> Variant
104+
where
105+
K: ToVariant + ToVariantEq,
106+
{
107+
self.get_or(key, Variant::new())
108+
}
109+
110+
/// Update an existing element corresponding to the key.
87111
///
88112
/// # Panics
89113
///
@@ -106,12 +130,14 @@ impl<Access: ThreadAccess> Dictionary<Access> {
106130
}
107131
}
108132

109-
/// Returns a reference to the value corresponding to the key.
133+
/// Returns a reference to the value corresponding to the key, inserting a `Nil` value first if
134+
/// it does not exist.
110135
///
111136
/// # Safety
112137
///
113138
/// The returned reference is invalidated if the same container is mutated through another
114-
/// reference.
139+
/// reference, and other references may be invalidated if the entry does not already exist
140+
/// (which causes this function to insert `Nil` and thus possibly re-allocate).
115141
///
116142
/// `Variant` is reference-counted and thus cheaply cloned. Consider using `get` instead.
117143
#[inline]
@@ -125,13 +151,16 @@ impl<Access: ThreadAccess> Dictionary<Access> {
125151
))
126152
}
127153

128-
/// Returns a mutable reference to the value corresponding to the key.
154+
/// Returns a mutable reference to the value corresponding to the key, inserting a `Nil` value
155+
/// first if it does not exist.
129156
///
130157
/// # Safety
131158
///
132159
/// The returned reference is invalidated if the same container is mutated through another
133-
/// reference. It is possible to create two mutable references to the same memory location
134-
/// if the same `key` is provided, causing undefined behavior.
160+
/// reference, and other references may be invalidated if the `key` does not already exist
161+
/// (which causes this function to insert `Nil` and thus possibly re-allocate). It is also
162+
/// possible to create two mutable references to the same memory location if the same `key`
163+
/// is provided, causing undefined behavior.
135164
#[inline]
136165
#[allow(clippy::mut_from_ref)]
137166
pub unsafe fn get_mut_ref<K>(&self, key: K) -> &mut Variant
@@ -425,7 +454,7 @@ unsafe fn iter_next<Access: ThreadAccess>(
425454
None
426455
} else {
427456
let key = Variant::cast_ref(next_ptr).clone();
428-
let value = dic.get(&key);
457+
let value = dic.get_or_nil(&key);
429458
*last_key = Some(key.clone());
430459
Some((key, value))
431460
}
@@ -591,7 +620,7 @@ godot_test!(test_dictionary {
591620
let mut iter_keys = HashSet::new();
592621
let expected_keys = ["foo", "bar"].iter().map(|&s| s.to_string()).collect::<HashSet<_>>();
593622
for (key, value) in &dict {
594-
assert_eq!(value, dict.get(&key));
623+
assert_eq!(Some(value), dict.get(&key));
595624
if !iter_keys.insert(key.to_string()) {
596625
panic!("key is already contained in set: {:?}", key);
597626
}

gdnative-core/src/core_types/variant.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,7 +1753,7 @@ impl<T: FromVariant, E: FromVariant> FromVariant for Result<T, E> {
17531753

17541754
match key.as_str() {
17551755
"Ok" => {
1756-
let val = T::from_variant(&dict.get(key_variant)).map_err(|err| {
1756+
let val = T::from_variant(&dict.get_or_nil(key_variant)).map_err(|err| {
17571757
FVE::InvalidEnumVariant {
17581758
variant: "Ok",
17591759
error: Box::new(err),
@@ -1762,7 +1762,7 @@ impl<T: FromVariant, E: FromVariant> FromVariant for Result<T, E> {
17621762
Ok(Ok(val))
17631763
}
17641764
"Err" => {
1765-
let err = E::from_variant(&dict.get(key_variant)).map_err(|err| {
1765+
let err = E::from_variant(&dict.get_or_nil(key_variant)).map_err(|err| {
17661766
FVE::InvalidEnumVariant {
17671767
variant: "Err",
17681768
error: Box::new(err),
@@ -1912,11 +1912,11 @@ godot_test!(
19121912
test_variant_result {
19131913
let variant = Result::<i64, ()>::Ok(42_i64).to_variant();
19141914
let dict = variant.try_to_dictionary().expect("should be dic");
1915-
assert_eq!(Some(42), dict.get("Ok").try_to_i64());
1915+
assert_eq!(Some(42), dict.get("Ok").and_then(|v| v.try_to_i64()));
19161916

19171917
let variant = Result::<(), i64>::Err(54_i64).to_variant();
19181918
let dict = variant.try_to_dictionary().expect("should be dic");
1919-
assert_eq!(Some(54), dict.get("Err").try_to_i64());
1919+
assert_eq!(Some(54), dict.get("Err").and_then(|v| v.try_to_i64()));
19201920

19211921
let variant = Variant::from_bool(true);
19221922
assert_eq!(

gdnative-derive/src/variant/from.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub(crate) fn expand_from_variant(derive_data: DeriveData) -> Result<TokenStream
8787
match __key.as_str() {
8888
#(
8989
#ref_var_ident_string_literals => {
90-
let #var_input_ident_iter = &__dict.get(&__keys.get(0));
90+
let #var_input_ident_iter = &__dict.get_or_nil(&__keys.get(0));
9191
(#var_from_variants).map_err(|err| FVE::InvalidEnumVariant {
9292
variant: #ref_var_ident_string_literals,
9393
error: Box::new(err),

gdnative-derive/src/variant/repr.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ impl VariantRepr {
280280
let name_string_literals =
281281
name_strings.iter().map(|string| Literal::string(&string));
282282

283-
let expr_variant = &quote!(&__dict.get(&__key));
283+
let expr_variant = &quote!(&__dict.get_or_nil(&__key));
284284
let exprs = non_skipped_fields
285285
.iter()
286286
.map(|f| f.from_variant(expr_variant));

test/src/test_derive.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,19 +83,25 @@ fn test_derive_to_variant() -> bool {
8383

8484
let variant = data.to_variant();
8585
let dictionary = variant.try_to_dictionary().expect("should be dictionary");
86-
assert_eq!(Some(42), dictionary.get("foo").try_to_i64());
87-
assert_eq!(Some(54.0), dictionary.get("bar").try_to_f64());
86+
assert_eq!(Some(42), dictionary.get("foo").and_then(|v| v.try_to_i64()));
87+
assert_eq!(
88+
Some(54.0),
89+
dictionary.get("bar").and_then(|v| v.try_to_f64())
90+
);
8891
assert_eq!(
8992
Some("*mut ()".into()),
90-
dictionary.get("ptr").try_to_string()
93+
dictionary.get("ptr").and_then(|v| v.try_to_string())
9194
);
9295
assert!(!dictionary.contains("skipped"));
9396

9497
let enum_dict = dictionary
9598
.get("baz")
96-
.try_to_dictionary()
99+
.and_then(|v| v.try_to_dictionary())
97100
.expect("should be dictionary");
98-
assert_eq!(Some(true), enum_dict.get("Foo").try_to_bool());
101+
assert_eq!(
102+
Some(true),
103+
enum_dict.get("Foo").and_then(|v| v.try_to_bool())
104+
);
99105

100106
assert_eq!(
101107
Ok(ToVar::<f64, i128> {
@@ -146,7 +152,7 @@ fn test_derive_owned_to_variant() -> bool {
146152
let dictionary = variant.try_to_dictionary().expect("should be dictionary");
147153
let array = dictionary
148154
.get("arr")
149-
.try_to_array()
155+
.and_then(|v| v.try_to_array())
150156
.expect("should be array");
151157
assert_eq!(3, array.len());
152158
assert_eq!(

0 commit comments

Comments
 (0)