Support GLib's bytestring as Rust UTF8 &str
zeenix opened this issue · comments
In GitLab by @Vanadiae on Dec 19, 2022, 22:45
GLib has a feature in GVariant that outputs byte arrays (ay
signature) for strings instead of the regular string type (s
signature). This allows passing non UTF8 strings through DBus while still having the convenience of only really handling char*
on the C code side. The format string is described at https://docs.gtk.org/glib/gvariant-format-strings.html#convenience-conversions (with ^ay
and ^&ay
), with bytestring arrays also existing. Bytestrings are at the end created through g_variant_new_bytestring(). One peculiar point here is that the nul terminator byte for C-like strings is kept as part of the bytes array, so this needs to be taken into account when creating a Rust &str from a bytestring. GVfs uses bytestrings extensively to pass paths through DBus, as can be seen with the first argument of pretty much every method in its introspection XML, and it's probably not the only one to use those I guess? For now I am using the following code, which is untested while I'm still working out the rest of my code:
#[derive(Deserialize, Serialize, Type)]
#[zvariant(signature = "ay")]
struct MountPath<'a>(#[serde(with = "mount_path_deser")] pub &'a str);
mod mount_path_deser {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(s: &str, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// The extra nul byte is required. See https://docs.gtk.org/glib/method.Variant.get_bytestring.html
serializer.serialize_bytes(&[s.as_bytes(), &[0u8]].concat())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<&'de str, D::Error>
where
D: Deserializer<'de>,
{
let bytes = <&[u8]>::deserialize(deserializer)?;
Ok(
std::str::from_utf8(bytes.strip_suffix(&[0u8]).unwrap_or(bytes))
.map_err(serde::de::Error::custom)?,
)
}
}
impl AsRef<str> for MountPath<'_> {
fn as_ref(&self) -> &str {
self.0
}
}
I think there's a better and more straightforward that could be done in zbus/zvariant itself, such as having a #[zvariant(bytestring)]
attribute, which would do the same logic as the code above (i.e. remove nul byte and parse as UTF8 when deserializing, and output as bytes with the nul byte when serializing).
I think there's a better and more straightforward that could be done in zbus/zvariant itself
The (de)serialization is left entirely to serde so zbus/zvariant can't really help you here. zvariant::Type
already allows you to manually specify signature using the signature
attribute.