Skip to content

Commit a0d308f

Browse files
committed
More robust encode/decode APIs with error handling and clear docs
1 parent 0414deb commit a0d308f

File tree

2 files changed

+209
-68
lines changed

2 files changed

+209
-68
lines changed

godot-core/src/builtin/collections/packed_array.rs

+138-66
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8+
// Result<..., ()> is used. But we don't have more error info. https://rust-lang.github.io/rust-clippy/master/index.html#result_unit_err.
9+
// We may want to change () to something like godot::meta::IoError, or a domain-specific one, in the future.
10+
#![allow(clippy::result_unit_err)]
11+
812
use godot_ffi as sys;
913

1014
use crate::builtin::*;
@@ -835,119 +839,184 @@ impl<'r> PackedTraits for meta::CowArg<'r, GString> {
835839

836840
macro_rules! declare_encode_decode {
837841
// $Via could be inferred, but ensures we have the correct type expectations.
838-
($Ty:ty, $encode_fn:ident, $decode_fn:ident, $Via:ty) => {
839-
#[doc = concat!("Encodes a value of type `", stringify!($Ty), "` at position `byte_offset`.")]
842+
($Ty:ty, $bytes:literal, $encode_fn:ident, $decode_fn:ident, $Via:ty) => {
843+
#[doc = concat!("Encodes `", stringify!($Ty), "` as ", stringify!($bytes), " byte(s) at position `byte_offset`.")]
844+
///
845+
/// Returns `Err` if there is not enough space left to write the value, and does nothing in that case.
840846
///
841-
/// Godot will print an error on encode failure (not enough space left). If you want to detect this programmatically,
842-
/// you need to check the offset manually -- or write directly to a Rust data structure, such as `Vec<u8>`.
843-
pub fn $encode_fn(&self, byte_offset: usize, value: $Ty) {
844-
// We trust Godot that indeed only fitting values are returned.
847+
/// **Note:** byte order and encoding pattern is an implementation detail. For portable byte representation and faster encoding, use
848+
/// [`as_mut_slice()`][Self::as_mut_slice] and the various Rust standard APIs such as
849+
#[doc = concat!("[`", stringify!($Ty), "::to_be_bytes()`].")]
850+
pub fn $encode_fn(&mut self, byte_offset: usize, value: $Ty) -> Result<(), ()> {
851+
// sys::static_assert!(std::mem::size_of::<$Ty>() == $bytes); -- used for testing, can't keep enabled due to half-floats.
852+
853+
if byte_offset + $bytes > self.len() {
854+
return Err(());
855+
}
856+
845857
self.as_inner()
846858
.$encode_fn(byte_offset as i64, value as $Via);
859+
Ok(())
847860
}
848861

849-
#[doc = concat!("Decodes a value of type `", stringify!($Ty), "` at position `byte_offset`.")]
862+
#[doc = concat!("Decodes `", stringify!($Ty), "` from ", stringify!($bytes), " byte(s) at position `byte_offset`.")]
863+
///
864+
/// Returns `Err` if there is not enough space left to read the value. In case Godot has other error conditions for decoding, it may
865+
/// return zero and print an error.
850866
///
851-
/// Godot will print an error on decode failure (not enough space left), and return `0`. If you want to detect this programmatically,
852-
/// you need to check the offset manually -- or convert the packed array to a Rust data structure, such as `Vec<u8>`.
853-
pub fn $decode_fn(&self, byte_offset: usize) -> $Ty {
854-
// We trust Godot that indeed only fitting values are returned.
867+
/// **Note:** byte order and encoding pattern is an implementation detail. For portable byte representation and faster decoding, use
868+
/// [`as_slice()`][Self::as_slice] and the various Rust standard APIs such as
869+
#[doc = concat!("[`", stringify!($Ty), "::from_be_bytes()`].")]
870+
pub fn $decode_fn(&self, byte_offset: usize) -> Result<$Ty, ()> {
871+
if byte_offset + $bytes > self.len() {
872+
return Err(());
873+
}
874+
855875
let decoded: $Via = self.as_inner().$decode_fn(byte_offset as i64);
856-
decoded as $Ty
876+
Ok(decoded as $Ty)
857877
}
858878
};
859879
}
860880

861881
impl PackedByteArray {
862-
declare_encode_decode!(u8, encode_u8, decode_u8, i64);
863-
declare_encode_decode!(i8, encode_s8, decode_s8, i64);
864-
declare_encode_decode!(u16, encode_u16, decode_u16, i64);
865-
declare_encode_decode!(i16, encode_s16, decode_s16, i64);
866-
declare_encode_decode!(u32, encode_u32, decode_u32, i64);
867-
declare_encode_decode!(i32, encode_s32, decode_s32, i64);
868-
declare_encode_decode!(u64, encode_u64, decode_u64, i64);
869-
declare_encode_decode!(i64, encode_s64, decode_s64, i64);
870-
declare_encode_decode!(f32, encode_half, decode_half, f64);
871-
declare_encode_decode!(f32, encode_float, decode_float, f64);
872-
declare_encode_decode!(f64, encode_double, decode_double, f64);
873-
874-
/// Encodes a `Variant` as bytes. Returns number of bytes written.
882+
declare_encode_decode!(u8, 1, encode_u8, decode_u8, i64);
883+
declare_encode_decode!(i8, 1, encode_s8, decode_s8, i64);
884+
declare_encode_decode!(u16, 2, encode_u16, decode_u16, i64);
885+
declare_encode_decode!(i16, 2, encode_s16, decode_s16, i64);
886+
declare_encode_decode!(u32, 4, encode_u32, decode_u32, i64);
887+
declare_encode_decode!(i32, 4, encode_s32, decode_s32, i64);
888+
declare_encode_decode!(u64, 8, encode_u64, decode_u64, i64);
889+
declare_encode_decode!(i64, 8, encode_s64, decode_s64, i64);
890+
declare_encode_decode!(f32, 2, encode_half, decode_half, f64);
891+
declare_encode_decode!(f32, 4, encode_float, decode_float, f64);
892+
declare_encode_decode!(f64, 8, encode_double, decode_double, f64);
893+
894+
/// Encodes a `Variant` as bytes. Returns number of bytes written, or `Err` on encoding failure.
875895
///
876896
/// Sufficient space must be allocated, depending on the encoded variant's size. If `allow_objects` is false, [`VariantType::OBJECT`] values
877897
/// are not permitted and will instead be serialized as ID-only. You should set `allow_objects` to false by default.
878898
pub fn encode_var(
879-
&self,
899+
&mut self,
880900
byte_offset: usize,
881901
value: impl AsArg<Variant>,
882902
allow_objects: bool,
883-
) -> usize {
903+
) -> Result<usize, ()> {
884904
meta::arg_into_ref!(value);
885905

886906
let bytes_written: i64 =
887907
self.as_inner()
888908
.encode_var(byte_offset as i64, value, allow_objects);
889-
bytes_written as usize
890-
}
891909

892-
/// Returns `true` if a valid `Variant` value can be decoded at `byte_offset`.
893-
///
894-
/// Returns `false` otherwise, or when the value is of type [`VariantType::OBJECT`] and `allow_objects` is `false`.
895-
///
896-
/// # Security
897-
/// You should set `allow_objects` to `false` unless you have a good reason not to. Decoding objects (e.g. coming from remote sources)
898-
/// can cause arbitrary code execution.
899-
pub fn has_encoded_var(&self, byte_offset: usize, allow_objects: bool) -> bool {
900-
self.as_inner()
901-
.has_encoded_var(byte_offset as i64, allow_objects)
910+
if bytes_written == -1 {
911+
Err(())
912+
} else {
913+
Ok(bytes_written as usize)
914+
}
902915
}
903916

904-
/// Decodes a `Variant` from bytes and returns it.
917+
/// Decodes a `Variant` from bytes and returns it, alongside the number of bytes read.
905918
///
906-
/// Returns [`Variant::nil()`] if a valid variant can't be decoded, or the value is of type [`VariantType::OBJECT`] and `allow_objects`
907-
/// is `false`.
919+
/// Returns `Err` on decoding error. If you store legit `NIL` variants inside the byte array, use
920+
/// [`decode_var_allow_nil()`][Self::decode_var_allow_nil] instead.
908921
///
909-
/// To know how many bytes the decoded variant took, you need to separately call [`decode_var_size()`][Self::decode_var_size].
922+
/// # API design
923+
/// Godot offers three separate methods `decode_var()`, `decode_var_size()` and `has_encoded_var()`. That comes with several problems:
924+
/// - `has_encoded_var()` is practically useless, because it performs the full decoding work and then throws away the variant.
925+
/// `decode_var()` can do all that and more.
926+
/// - Both `has_encoded_var()` and `decode_var_size()` are unreliable. They don't tell whether an actual variant has been written at
927+
/// the location. They interpret garbage as `Variant::nil()` and return `true` or `4`, respectively. This can very easily cause bugs
928+
/// because surprisingly, some users may expect that `has_encoded_var()` returns _whether a variant has been encoded_.
929+
/// - The underlying C++ implementation has all the necessary information (whether a variant is there, how big it is and its value) but the
930+
/// GDExtension API returns only one info at a time, requiring re-decoding on each call.
931+
///
932+
/// godot-rust mitigates this somewhat, with the following design:
933+
/// - `decode_var()` treats all `NIL`s as errors. This is most often the desired behavior, and if not, `decode_var_allow_nil()` can be used.
934+
/// It's also the only way to detect errors at all -- once you store legit `NIL` values, you can no longer differentiate them from garbage.
935+
/// - `decode_var()` returns both the decoded variant and its size. This requires two decoding runs, but only if the variant is actually
936+
/// valid. Again, in many cases, a user needs the size to know where follow-up data in the buffer starts.
937+
/// - `decode_var_size()` and `has_encoded_var()` are not exposed.
910938
///
911939
/// # Security
912940
/// You should set `allow_objects` to `false` unless you have a good reason not to. Decoding objects (e.g. coming from remote sources)
913941
/// can cause arbitrary code execution.
914-
pub fn decode_var(&self, byte_offset: usize, allow_objects: bool) -> Variant {
915-
self.as_inner()
916-
.decode_var(byte_offset as i64, allow_objects)
942+
#[doc(alias = "has_encoded_var", alias = "decode_var_size")]
943+
#[inline]
944+
pub fn decode_var(
945+
&self,
946+
byte_offset: usize,
947+
allow_objects: bool,
948+
) -> Result<(Variant, usize), ()> {
949+
let variant = self
950+
.as_inner()
951+
.decode_var(byte_offset as i64, allow_objects);
952+
953+
if variant.is_nil() {
954+
return Err(());
955+
}
956+
957+
// It's unfortunate that this does another full decoding, but decode_var() is barely useful without also knowing the size, as it won't
958+
// be possible to know where to start reading any follow-up data. Furthermore, decode_var_size() often returns true when there's in fact
959+
// no variant written at that place, it just interprets "nil", treats it as valid, and happily returns 4 bytes.
960+
//
961+
// So we combine the two calls for the sake of convenience and to avoid accidental usage.
962+
let size: i64 = self
963+
.as_inner()
964+
.decode_var_size(byte_offset as i64, allow_objects);
965+
debug_assert_ne!(size, -1); // must not happen if we just decoded variant.
966+
967+
Ok((variant, size as usize))
917968
}
918969

919-
/// Decodes a `Variant` from bytes and returns it.
970+
/// Unreliable `Variant` decoding, allowing `NIL`.
971+
///
972+
/// <div class="warning">
973+
/// <p>This method is highly unreliable and will try to interpret anything into variants, even zeroed memory or random byte patterns.
974+
/// Only use it if you need a 1:1 equivalent of Godot's <code>decode_var()</code> and <code>decode_var_size()</code> functions.</p>
920975
///
921-
/// Returns [`Variant::nil()`] if a valid variant can't be decoded, or the value is of type [`VariantType::OBJECT`] and `allow_objects`
922-
/// is `false`.
976+
/// <p>In the majority of cases, <a href="struct.PackedByteArray.html#method.decode_var" title="method godot::builtin::PackedByteArray::decode_var">
977+
/// <code>decode_var()</code></a> is the better choice, as it’s much easier to use correctly. See also its section about the rationale
978+
/// behind the current API design.</p>
979+
/// </div>
923980
///
924-
/// This method is designed to be called in combination with [`decode_var()`][Self::decode_var].
981+
/// Returns a tuple of two elements:
982+
/// 1. the decoded variant. This is [`Variant::nil()`] if a valid variant can't be decoded, or the value is of type [`VariantType::OBJECT`]
983+
/// and `allow_objects` is `false`.
984+
/// 2. The number of bytes the variant occupies. This is `0` if running out of space, but most other failures are not recognized.
925985
///
926986
/// # Security
927987
/// You should set `allow_objects` to `false` unless you have a good reason not to. Decoding objects (e.g. coming from remote sources)
928988
/// can cause arbitrary code execution.
929-
pub fn decode_var_size(&self, byte_offset: usize, allow_objects: bool) -> usize {
930-
let size: i64 = self
931-
.as_inner()
932-
.decode_var_size(byte_offset as i64, allow_objects);
989+
#[inline]
990+
pub fn decode_var_allow_nil(
991+
&self,
992+
byte_offset: usize,
993+
allow_objects: bool,
994+
) -> (Variant, usize) {
995+
let byte_offset = byte_offset as i64;
996+
997+
let variant = self.as_inner().decode_var(byte_offset, allow_objects);
998+
let decoded_size = self.as_inner().decode_var_size(byte_offset, allow_objects);
999+
let decoded_size = decoded_size.try_into().unwrap_or_else(|_| {
1000+
panic!("unexpected value {decoded_size} returned from decode_var_size()")
1001+
});
9331002

934-
size as usize
1003+
(variant, decoded_size)
9351004
}
9361005

9371006
/// Returns a new `PackedByteArray`, with the data of this array compressed.
9381007
///
939-
/// On failure, Godot prints an error and this method returns `None`. (Note that any empty results coming from Godot are mapped to `None`
1008+
/// On failure, Godot prints an error and this method returns `Err`. (Note that any empty results coming from Godot are mapped to `Err`
9401009
/// in Rust.)
941-
pub fn compress(&self, compression_mode: CompressionMode) -> Option<PackedByteArray> {
1010+
pub fn compress(&self, compression_mode: CompressionMode) -> Result<PackedByteArray, ()> {
9421011
let compressed: PackedByteArray = self.as_inner().compress(compression_mode.ord() as i64);
943-
populated_or_none(compressed)
1012+
populated_or_err(compressed)
9441013
}
9451014

9461015
/// Returns a new `PackedByteArray`, with the data of this array decompressed.
9471016
///
9481017
/// Set `buffer_size` to the size of the uncompressed data.
9491018
///
950-
/// On failure, Godot prints an error and this method returns `None`. (Note that any empty results coming from Godot are mapped to `None`
1019+
/// On failure, Godot prints an error and this method returns `Err`. (Note that any empty results coming from Godot are mapped to `Err`
9511020
/// in Rust.)
9521021
///
9531022
/// **Note:** Decompression is not guaranteed to work with data not compressed by Godot, for example if data compressed with the deflate
@@ -956,12 +1025,12 @@ impl PackedByteArray {
9561025
&self,
9571026
buffer_size: usize,
9581027
compression_mode: CompressionMode,
959-
) -> Option<PackedByteArray> {
1028+
) -> Result<PackedByteArray, ()> {
9601029
let decompressed: PackedByteArray = self
9611030
.as_inner()
9621031
.decompress(buffer_size as i64, compression_mode.ord() as i64);
9631032

964-
populated_or_none(decompressed)
1033+
populated_or_err(decompressed)
9651034
}
9661035

9671036
/// Returns a new `PackedByteArray`, with the data of this array decompressed, and without fixed decompression buffer.
@@ -976,26 +1045,29 @@ impl PackedByteArray {
9761045
/// `max_output_size`. Passing `None` will allow for unbounded output. If any positive value is passed, and the decompression exceeds that
9771046
/// amount in bytes, then an error will be returned.
9781047
///
1048+
/// On failure, Godot prints an error and this method returns `Err`. (Note that any empty results coming from Godot are mapped to `Err`
1049+
/// in Rust.)
1050+
///
9791051
/// **Note:** Decompression is not guaranteed to work with data not compressed by Godot, for example if data compressed with the deflate
9801052
/// compression mode lacks a checksum or header.
9811053
pub fn decompress_dynamic(
9821054
&self,
9831055
max_output_size: Option<usize>,
9841056
compression_mode: CompressionMode,
985-
) -> Option<PackedByteArray> {
1057+
) -> Result<PackedByteArray, ()> {
9861058
let max_output_size = max_output_size.map(|i| i as i64).unwrap_or(-1);
9871059
let decompressed: PackedByteArray = self
9881060
.as_inner()
9891061
.decompress_dynamic(max_output_size, compression_mode.ord() as i64);
9901062

991-
populated_or_none(decompressed)
1063+
populated_or_err(decompressed)
9921064
}
9931065
}
9941066

995-
fn populated_or_none(array: PackedByteArray) -> Option<PackedByteArray> {
1067+
fn populated_or_err(array: PackedByteArray) -> Result<PackedByteArray, ()> {
9961068
if array.is_empty() {
997-
None
1069+
Err(())
9981070
} else {
999-
Some(array)
1071+
Ok(array)
10001072
}
10011073
}

itest/rust/src/builtin_tests/containers/packed_array_test.rs

+71-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77

88
use crate::framework::{expect_panic, itest};
99
use godot::builtin::{
10-
Color, GString, PackedByteArray, PackedColorArray, PackedFloat32Array, PackedInt32Array,
11-
PackedStringArray,
10+
dict, Color, GString, PackedByteArray, PackedColorArray, PackedFloat32Array, PackedInt32Array,
11+
PackedStringArray, Variant,
1212
};
13+
use godot::prelude::ToGodot;
1314

1415
#[itest]
1516
fn packed_array_default() {
@@ -305,3 +306,71 @@ fn packed_array_format() {
305306
let a = PackedByteArray::new();
306307
assert_eq!(format!("{a}"), "[]");
307308
}
309+
310+
#[itest]
311+
fn packed_byte_array_encode_decode() {
312+
let a = PackedByteArray::from(&[0xAB, 0xCD, 0x12]);
313+
314+
assert_eq!(a.decode_u8(0), Ok(0xAB));
315+
assert_eq!(a.decode_u8(2), Ok(0x12));
316+
assert_eq!(a.decode_u16(1), Ok(0x12CD)); // Currently little endian, but this may change.
317+
assert_eq!(a.decode_u16(2), Err(()));
318+
assert_eq!(a.decode_u32(0), Err(()));
319+
320+
let mut a = a;
321+
a.encode_u16(1, 0xEF34).unwrap();
322+
assert_eq!(a.decode_u8(0), Ok(0xAB));
323+
assert_eq!(a.decode_u8(1), Ok(0x34));
324+
assert_eq!(a.decode_u8(2), Ok(0xEF));
325+
}
326+
327+
#[itest]
328+
fn packed_byte_array_encode_decode_variant() {
329+
let variant = dict! {
330+
"s": "some string",
331+
"i": -12345,
332+
}
333+
.to_variant();
334+
335+
let mut a = PackedByteArray::new();
336+
a.resize(40);
337+
338+
// NIL is a valid, encodable value.
339+
let nil = a.encode_var(3, &Variant::nil(), false);
340+
assert_eq!(nil, Ok(4));
341+
342+
let bytes = a.encode_var(3, &variant, false);
343+
assert_eq!(bytes, Err(()));
344+
345+
a.resize(80);
346+
let bytes = a.encode_var(3, &variant, false);
347+
assert_eq!(bytes, Ok(60)); // Size may change; in that case we only need to verify is_ok().
348+
349+
// Decoding. Detects garbage.
350+
let decoded = a.decode_var(3, false).expect("decode_var() succeeds");
351+
assert_eq!(decoded.0, variant);
352+
assert_eq!(decoded.1, 60);
353+
354+
let decoded = a.decode_var(4, false);
355+
assert_eq!(decoded, Err(()));
356+
357+
// Decoding with NILs.
358+
let decoded = a.decode_var_allow_nil(3, false);
359+
assert_eq!(decoded.0, variant);
360+
assert_eq!(decoded.1, 60);
361+
362+
// Interprets garbage as NIL Variant with size 4.
363+
let decoded = a.decode_var_allow_nil(4, false);
364+
assert_eq!(decoded.0, Variant::nil());
365+
assert_eq!(decoded.1, 4);
366+
367+
// Even last 4 bytes (still zeroed memory) is allegedly a variant.
368+
let decoded = a.decode_var_allow_nil(76, false);
369+
assert_eq!(decoded.0, Variant::nil());
370+
assert_eq!(decoded.1, 4);
371+
372+
// Only running out of size "fails".
373+
let decoded = a.decode_var_allow_nil(77, false);
374+
assert_eq!(decoded.0, Variant::nil());
375+
assert_eq!(decoded.1, 0);
376+
}

0 commit comments

Comments
 (0)