Skip to content

Commit 6cdbfde

Browse files
authored
Merge pull request #994 from godot-rust/feature/packedbytearray
Add encoding, string conversion and compression methods to `PackedByteArray`
2 parents db5eb85 + a0d308f commit 6cdbfde

File tree

4 files changed

+330
-13
lines changed

4 files changed

+330
-13
lines changed

godot-codegen/src/special_cases/special_cases.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ pub fn is_builtin_method_exposed(builtin_ty: &TyName, godot_method_name: &str) -
399399
| ("NodePath", "is_empty")
400400
| ("NodePath", "get_concatenated_names")
401401
| ("NodePath", "get_concatenated_subnames")
402-
//| ("NodePath", "get_as_property_path")
402+
| ("NodePath", "get_as_property_path")
403403

404404
// Callable
405405
| ("Callable", "call")
@@ -409,7 +409,13 @@ pub fn is_builtin_method_exposed(builtin_ty: &TyName, godot_method_name: &str) -
409409
| ("Callable", "rpc")
410410
| ("Callable", "rpc_id")
411411

412-
// (add more builtin types below)
412+
// PackedByteArray
413+
| ("PackedByteArray", "get_string_from_ascii")
414+
| ("PackedByteArray", "get_string_from_utf8")
415+
| ("PackedByteArray", "get_string_from_utf16")
416+
| ("PackedByteArray", "get_string_from_utf32")
417+
| ("PackedByteArray", "get_string_from_wchar")
418+
| ("PackedByteArray", "hex_encode")
413419

414420
// Vector2i
415421
| ("Vector2i", "clampi")

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

+251-5
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::*;
@@ -13,8 +17,11 @@ use std::{fmt, ops, ptr};
1317
use sys::types::*;
1418
use sys::{ffi_methods, interface_fn, GodotFfi};
1519

16-
// FIXME remove dependency on these types
20+
use crate::classes::file_access::CompressionMode;
1721
use crate::meta;
22+
use crate::obj::EngineEnum;
23+
24+
// FIXME remove dependency on these types.
1825
use sys::{__GdextString, __GdextType};
1926
// TODO(bromeon): ensure and test that all element types can be packed.
2027
// Many builtin types don't have a #[repr] themselves, but they are used in packed arrays, which assumes certain size and alignment.
@@ -112,6 +119,7 @@ macro_rules! impl_packed_array {
112119
}
113120

114121
/// Returns the number of elements in the array. Equivalent of `size()` in Godot.
122+
#[doc(alias = "size")]
115123
pub fn len(&self) -> usize {
116124
to_usize(self.as_inner().size())
117125
}
@@ -310,7 +318,7 @@ macro_rules! impl_packed_array {
310318
}
311319

312320
// Include specific functions in the code only if the Packed*Array provides the function.
313-
impl_specific_packed_array_functions!($PackedArray);
321+
declare_packed_array_conversion_fns!($PackedArray);
314322

315323
/// # Panics
316324
///
@@ -544,7 +552,7 @@ macro_rules! impl_packed_array {
544552
}
545553

546554
// Helper macro to only include specific functions in the code if the Packed*Array provides the function.
547-
macro_rules! impl_specific_packed_array_functions {
555+
macro_rules! declare_packed_array_conversion_fns {
548556
(PackedByteArray) => {
549557
/// Returns a copy of the data converted to a `PackedFloat32Array`, where each block of 4 bytes has been converted to a 32-bit float.
550558
///
@@ -818,10 +826,248 @@ impl_packed_trait_as_into!(Vector3);
818826
impl_packed_trait_as_into!(Vector4);
819827
impl_packed_trait_as_into!(Color);
820828

821-
impl<'r> PackedTraits for crate::meta::CowArg<'r, GString> {
822-
type ArgType = crate::meta::CowArg<'r, GString>;
829+
impl<'r> PackedTraits for meta::CowArg<'r, GString> {
830+
type ArgType = meta::CowArg<'r, GString>;
823831

824832
fn into_packed_arg(self) -> Self::ArgType {
825833
self
826834
}
827835
}
836+
837+
// ----------------------------------------------------------------------------------------------------------------------------------------------
838+
// Specific API for PackedByteArray
839+
840+
macro_rules! declare_encode_decode {
841+
// $Via could be inferred, but ensures we have the correct type expectations.
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.
846+
///
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+
857+
self.as_inner()
858+
.$encode_fn(byte_offset as i64, value as $Via);
859+
Ok(())
860+
}
861+
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.
866+
///
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+
875+
let decoded: $Via = self.as_inner().$decode_fn(byte_offset as i64);
876+
Ok(decoded as $Ty)
877+
}
878+
};
879+
}
880+
881+
impl PackedByteArray {
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.
895+
///
896+
/// Sufficient space must be allocated, depending on the encoded variant's size. If `allow_objects` is false, [`VariantType::OBJECT`] values
897+
/// are not permitted and will instead be serialized as ID-only. You should set `allow_objects` to false by default.
898+
pub fn encode_var(
899+
&mut self,
900+
byte_offset: usize,
901+
value: impl AsArg<Variant>,
902+
allow_objects: bool,
903+
) -> Result<usize, ()> {
904+
meta::arg_into_ref!(value);
905+
906+
let bytes_written: i64 =
907+
self.as_inner()
908+
.encode_var(byte_offset as i64, value, allow_objects);
909+
910+
if bytes_written == -1 {
911+
Err(())
912+
} else {
913+
Ok(bytes_written as usize)
914+
}
915+
}
916+
917+
/// Decodes a `Variant` from bytes and returns it, alongside the number of bytes read.
918+
///
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.
921+
///
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.
938+
///
939+
/// # Security
940+
/// You should set `allow_objects` to `false` unless you have a good reason not to. Decoding objects (e.g. coming from remote sources)
941+
/// can cause arbitrary code execution.
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))
968+
}
969+
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>
975+
///
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>
980+
///
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.
985+
///
986+
/// # Security
987+
/// You should set `allow_objects` to `false` unless you have a good reason not to. Decoding objects (e.g. coming from remote sources)
988+
/// can cause arbitrary code execution.
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+
});
1002+
1003+
(variant, decoded_size)
1004+
}
1005+
1006+
/// Returns a new `PackedByteArray`, with the data of this array compressed.
1007+
///
1008+
/// On failure, Godot prints an error and this method returns `Err`. (Note that any empty results coming from Godot are mapped to `Err`
1009+
/// in Rust.)
1010+
pub fn compress(&self, compression_mode: CompressionMode) -> Result<PackedByteArray, ()> {
1011+
let compressed: PackedByteArray = self.as_inner().compress(compression_mode.ord() as i64);
1012+
populated_or_err(compressed)
1013+
}
1014+
1015+
/// Returns a new `PackedByteArray`, with the data of this array decompressed.
1016+
///
1017+
/// Set `buffer_size` to the size of the uncompressed data.
1018+
///
1019+
/// On failure, Godot prints an error and this method returns `Err`. (Note that any empty results coming from Godot are mapped to `Err`
1020+
/// in Rust.)
1021+
///
1022+
/// **Note:** Decompression is not guaranteed to work with data not compressed by Godot, for example if data compressed with the deflate
1023+
/// compression mode lacks a checksum or header.
1024+
pub fn decompress(
1025+
&self,
1026+
buffer_size: usize,
1027+
compression_mode: CompressionMode,
1028+
) -> Result<PackedByteArray, ()> {
1029+
let decompressed: PackedByteArray = self
1030+
.as_inner()
1031+
.decompress(buffer_size as i64, compression_mode.ord() as i64);
1032+
1033+
populated_or_err(decompressed)
1034+
}
1035+
1036+
/// Returns a new `PackedByteArray`, with the data of this array decompressed, and without fixed decompression buffer.
1037+
///
1038+
/// This method only accepts `BROTLI`, `GZIP`, and `DEFLATE` compression modes.
1039+
///
1040+
/// This method is potentially slower than [`decompress()`][Self::decompress], as it may have to re-allocate its output buffer multiple
1041+
/// times while decompressing, whereas `decompress()` knows its output buffer size from the beginning.
1042+
///
1043+
/// GZIP has a maximal compression ratio of 1032:1, meaning it's very possible for a small compressed payload to decompress to a potentially
1044+
/// very large output. To guard against this, you may provide a maximum size this function is allowed to allocate in bytes via
1045+
/// `max_output_size`. Passing `None` will allow for unbounded output. If any positive value is passed, and the decompression exceeds that
1046+
/// amount in bytes, then an error will be returned.
1047+
///
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+
///
1051+
/// **Note:** Decompression is not guaranteed to work with data not compressed by Godot, for example if data compressed with the deflate
1052+
/// compression mode lacks a checksum or header.
1053+
pub fn decompress_dynamic(
1054+
&self,
1055+
max_output_size: Option<usize>,
1056+
compression_mode: CompressionMode,
1057+
) -> Result<PackedByteArray, ()> {
1058+
let max_output_size = max_output_size.map(|i| i as i64).unwrap_or(-1);
1059+
let decompressed: PackedByteArray = self
1060+
.as_inner()
1061+
.decompress_dynamic(max_output_size, compression_mode.ord() as i64);
1062+
1063+
populated_or_err(decompressed)
1064+
}
1065+
}
1066+
1067+
fn populated_or_err(array: PackedByteArray) -> Result<PackedByteArray, ()> {
1068+
if array.is_empty() {
1069+
Err(())
1070+
} else {
1071+
Ok(array)
1072+
}
1073+
}

godot-core/src/lib.rs

-4
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,6 @@ mod gen {
5454
include!(concat!(env!("OUT_DIR"), "/mod.rs"));
5555
}
5656

57-
pub mod inners {
58-
pub use crate::gen::*;
59-
}
60-
6157
// ----------------------------------------------------------------------------------------------------------------------------------------------
6258
// Hidden but accessible symbols
6359

0 commit comments

Comments
 (0)