Skip to content

Commit

Permalink
Fix Dictionary::get unsoundness, and make it return Option<Variant>
Browse files Browse the repository at this point in the history
Also adds `get_or` and `get_or_nil` convenience functions.
  • Loading branch information
Waridley committed Jun 14, 2021
1 parent 206f710 commit d688332
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 23 deletions.
51 changes: 40 additions & 11 deletions gdnative-core/src/core_types/dictionary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,45 @@ impl<Access: ThreadAccess> Dictionary<Access> {
unsafe { (get_api().godot_dictionary_has_all)(self.sys(), keys.sys()) }
}

/// Returns a copy of the value corresponding to the key.
/// Returns a copy of the value corresponding to the key if it exists.
#[inline]
pub fn get<K>(&self, key: K) -> Variant
pub fn get<K>(&self, key: K) -> Option<Variant>
where
K: ToVariant + ToVariantEq,
{
let key = key.to_variant();
// This should never return the default Nil, but there isn't a safe way to otherwise check
// if the entry exists in a single API call.
self.contains(&key).then(|| self.get_or_nil(key))
}

/// Returns a copy of the value corresponding to the key, or `default` if it doesn't exist
#[inline]
pub fn get_or<K, D>(&self, key: K, default: D) -> Variant
where
K: ToVariant + ToVariantEq,
D: ToVariant,
{
unsafe {
Variant((get_api().godot_dictionary_get)(
Variant((get_api().godot_dictionary_get_with_default)(
self.sys(),
key.to_variant().sys(),
default.to_variant().sys(),
))
}
}

/// Update an existing element corresponding ot the key.
/// Returns a copy of the element corresponding to the key, or `Nil` if it doesn't exist.
/// Shorthand for `self.get_or(key, Variant::new())`.
#[inline]
pub fn get_or_nil<K>(&self, key: K) -> Variant
where
K: ToVariant + ToVariantEq,
{
self.get_or(key, Variant::new())
}

/// Update an existing element corresponding to the key.
///
/// # Panics
///
Expand All @@ -106,12 +130,14 @@ impl<Access: ThreadAccess> Dictionary<Access> {
}
}

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

/// Returns a mutable reference to the value corresponding to the key.
/// Returns a mutable reference to the value corresponding to the key, inserting a `Nil` value
/// first if it does not exist.
///
/// # Safety
///
/// The returned reference is invalidated if the same container is mutated through another
/// reference. It is possible to create two mutable references to the same memory location
/// if the same `key` is provided, causing undefined behavior.
/// reference, and other references may be invalidated if the `key` does not already exist
/// (which causes this function to insert `Nil` and thus possibly re-allocate). It is also
/// possible to create two mutable references to the same memory location if the same `key`
/// is provided, causing undefined behavior.
#[inline]
#[allow(clippy::mut_from_ref)]
pub unsafe fn get_mut_ref<K>(&self, key: K) -> &mut Variant
Expand Down Expand Up @@ -425,7 +454,7 @@ unsafe fn iter_next<Access: ThreadAccess>(
None
} else {
let key = Variant::cast_ref(next_ptr).clone();
let value = dic.get(&key);
let value = dic.get_or_nil(&key);
*last_key = Some(key.clone());
Some((key, value))
}
Expand Down Expand Up @@ -591,7 +620,7 @@ godot_test!(test_dictionary {
let mut iter_keys = HashSet::new();
let expected_keys = ["foo", "bar"].iter().map(|&s| s.to_string()).collect::<HashSet<_>>();
for (key, value) in &dict {
assert_eq!(value, dict.get(&key));
assert_eq!(Some(value), dict.get(&key));
if !iter_keys.insert(key.to_string()) {
panic!("key is already contained in set: {:?}", key);
}
Expand Down
8 changes: 4 additions & 4 deletions gdnative-core/src/core_types/variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1753,7 +1753,7 @@ impl<T: FromVariant, E: FromVariant> FromVariant for Result<T, E> {

match key.as_str() {
"Ok" => {
let val = T::from_variant(&dict.get(key_variant)).map_err(|err| {
let val = T::from_variant(&dict.get_or_nil(key_variant)).map_err(|err| {
FVE::InvalidEnumVariant {
variant: "Ok",
error: Box::new(err),
Expand All @@ -1762,7 +1762,7 @@ impl<T: FromVariant, E: FromVariant> FromVariant for Result<T, E> {
Ok(Ok(val))
}
"Err" => {
let err = E::from_variant(&dict.get(key_variant)).map_err(|err| {
let err = E::from_variant(&dict.get_or_nil(key_variant)).map_err(|err| {
FVE::InvalidEnumVariant {
variant: "Err",
error: Box::new(err),
Expand Down Expand Up @@ -1912,11 +1912,11 @@ godot_test!(
test_variant_result {
let variant = Result::<i64, ()>::Ok(42_i64).to_variant();
let dict = variant.try_to_dictionary().expect("should be dic");
assert_eq!(Some(42), dict.get("Ok").try_to_i64());
assert_eq!(Some(42), dict.get("Ok").and_then(|v| v.try_to_i64()));

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

let variant = Variant::from_bool(true);
assert_eq!(
Expand Down
2 changes: 1 addition & 1 deletion gdnative-derive/src/variant/from.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub(crate) fn expand_from_variant(derive_data: DeriveData) -> Result<TokenStream
match __key.as_str() {
#(
#ref_var_ident_string_literals => {
let #var_input_ident_iter = &__dict.get(&__keys.get(0));
let #var_input_ident_iter = &__dict.get_or_nil(&__keys.get(0));
(#var_from_variants).map_err(|err| FVE::InvalidEnumVariant {
variant: #ref_var_ident_string_literals,
error: Box::new(err),
Expand Down
2 changes: 1 addition & 1 deletion gdnative-derive/src/variant/repr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ impl VariantRepr {
let name_string_literals =
name_strings.iter().map(|string| Literal::string(&string));

let expr_variant = &quote!(&__dict.get(&__key));
let expr_variant = &quote!(&__dict.get_or_nil(&__key));
let exprs = non_skipped_fields
.iter()
.map(|f| f.from_variant(expr_variant));
Expand Down
18 changes: 12 additions & 6 deletions test/src/test_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,25 @@ fn test_derive_to_variant() -> bool {

let variant = data.to_variant();
let dictionary = variant.try_to_dictionary().expect("should be dictionary");
assert_eq!(Some(42), dictionary.get("foo").try_to_i64());
assert_eq!(Some(54.0), dictionary.get("bar").try_to_f64());
assert_eq!(Some(42), dictionary.get("foo").and_then(|v| v.try_to_i64()));
assert_eq!(
Some(54.0),
dictionary.get("bar").and_then(|v| v.try_to_f64())
);
assert_eq!(
Some("*mut ()".into()),
dictionary.get("ptr").try_to_string()
dictionary.get("ptr").and_then(|v| v.try_to_string())
);
assert!(!dictionary.contains("skipped"));

let enum_dict = dictionary
.get("baz")
.try_to_dictionary()
.and_then(|v| v.try_to_dictionary())
.expect("should be dictionary");
assert_eq!(Some(true), enum_dict.get("Foo").try_to_bool());
assert_eq!(
Some(true),
enum_dict.get("Foo").and_then(|v| v.try_to_bool())
);

assert_eq!(
Ok(ToVar::<f64, i128> {
Expand Down Expand Up @@ -146,7 +152,7 @@ fn test_derive_owned_to_variant() -> bool {
let dictionary = variant.try_to_dictionary().expect("should be dictionary");
let array = dictionary
.get("arr")
.try_to_array()
.and_then(|v| v.try_to_array())
.expect("should be array");
assert_eq!(3, array.len());
assert_eq!(
Expand Down

0 comments on commit d688332

Please sign in to comment.