xso: Import the FixedHex text codec from xmpp-parsers

Emmanuel Gil Peyrot created

This codec converts from a fixed-size case-insensitive hexadecimal
string into an array of bytes, and back to a lowercase hexadecimal
string.

Change summary

xso/src/text.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 49 insertions(+)

Detailed changes

xso/src/text.rs 🔗

@@ -243,3 +243,52 @@ impl<Filter: TextFilter> TextCodec<Option<Vec<u8>>> for Base64<Filter> {
             .map(Option::flatten)
     }
 }
+
+/// Text codec transforming text to binary using hexadecimal nibbles.
+///
+/// The length must be known at compile-time.
+pub struct FixedHex<const N: usize>;
+
+impl<const N: usize> TextCodec<[u8; N]> for FixedHex<N> {
+    fn decode(s: String) -> Result<[u8; N], Error> {
+        if s.len() != 2 * N {
+            return Err(Error::Other("Invalid length"));
+        }
+
+        let mut bytes = [0u8; N];
+        for i in 0..N {
+            bytes[i] =
+                u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(Error::text_parse_error)?;
+        }
+
+        Ok(bytes)
+    }
+
+    fn encode(value: &[u8; N]) -> Result<Option<Cow<'_, str>>, Error> {
+        let mut bytes = String::with_capacity(N * 2);
+        for byte in value {
+            bytes.extend(format!("{:02x}", byte).chars());
+        }
+        Ok(Some(Cow::Owned(bytes)))
+    }
+}
+
+impl<T, const N: usize> TextCodec<Option<T>> for FixedHex<N>
+where
+    FixedHex<N>: TextCodec<T>,
+{
+    fn decode(s: String) -> Result<Option<T>, Error> {
+        if s.is_empty() {
+            return Ok(None);
+        }
+        Ok(Some(Self::decode(s)?))
+    }
+
+    fn encode(decoded: &Option<T>) -> Result<Option<Cow<'_, str>>, Error> {
+        decoded
+            .as_ref()
+            .map(Self::encode)
+            .transpose()
+            .map(Option::flatten)
+    }
+}