From c242dabb55dc885c74cc3b8d0fb90ef15dc2439c Mon Sep 17 00:00:00 2001 From: Tsur Devson Date: Tue, 10 Jun 2025 13:15:47 +0200 Subject: [PATCH] Implement Encode for sequences of string-like types With this change, various types like `&[&str]`, `Vec<&String>, &[Vec`, etc. are now `Encode` by default, and encode as an RFC4251 string of the encoded entries. Custom types can opt into this by implementing the marker trait `Rfc4251String`. Implementation note: This would be more general and cover more types if we could blanket `impl Encode for &T`, as this would cover any level of references in the trait bound for the `Rfc4251String` blanket implenentation. However, this would collide with the `Label` trait, so instead this adds explicit impls for the immediate types that we implement `Encode` for. --- ssh-encoding/src/encode.rs | 137 +++++++++++++++++++++++------------ ssh-encoding/tests/encode.rs | 33 +++++++++ 2 files changed, 124 insertions(+), 46 deletions(-) diff --git a/ssh-encoding/src/encode.rs b/ssh-encoding/src/encode.rs index e7296f3..09fd725 100644 --- a/ssh-encoding/src/encode.rs +++ b/ssh-encoding/src/encode.rs @@ -175,7 +175,76 @@ impl Encode for [u8; N] { } } -/// Encode a `string` as described in [RFC4251 § 5]: +/// A macro to implement `Encode` for a type by delegating to some transformed version of `self`. +macro_rules! impl_by_delegation { + ( + $( + $(#[$attr:meta])* + impl $( ($($generics:tt)+) )? Encode for $type:ty where $self:ident -> $delegate:expr; + )+ + ) => { + $( + $(#[$attr])* + impl $(< $($generics)* >)? Encode for $type { + fn encoded_len(&$self) -> Result { + $delegate.encoded_len() + } + + fn encode(&$self, writer: &mut impl Writer) -> Result<(), Error> { + $delegate.encode(writer) + } + } + )+ + }; +} + +impl_by_delegation!( + /// Encode a `string` as described in [RFC4251 § 5]: + /// + /// > Arbitrary length binary string. Strings are allowed to contain + /// > arbitrary binary data, including null characters and 8-bit + /// > characters. They are stored as a uint32 containing its length + /// > (number of bytes that follow) and zero (= empty string) or more + /// > bytes that are the value of the string. Terminating null + /// > characters are not used. + /// > + /// > Strings are also used to store text. In that case, US-ASCII is + /// > used for internal names, and ISO-10646 UTF-8 for text that might + /// > be displayed to the user. The terminating null character SHOULD + /// > NOT normally be stored in the string. For example: the US-ASCII + /// > string "testing" is represented as 00 00 00 07 t e s t i n g. The + /// > UTF-8 mapping does not alter the encoding of US-ASCII characters. + /// + /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 + impl Encode for str where self -> self.as_bytes(); + + #[cfg(feature = "alloc")] + impl Encode for Vec where self -> self.as_slice(); + #[cfg(feature = "alloc")] + impl Encode for String where self -> self.as_bytes(); + #[cfg(feature = "bytes")] + impl Encode for Bytes where self -> self.as_ref(); + + // While deref coercion ensures that `&E` can use the `Encode` trait methods, it will not be + // allowd in trait bounds, as `&E` does not implement `Encode` itself just because `E: Encode`. + // A blanket impl for `&E` would be the most generic, but that collides with the `Label` trait's + // blanket impl. Instead, we can do it explicitly for the immediatley relevant base types. + impl Encode for &str where self -> **self; + impl Encode for &[u8] where self -> **self; + #[cfg(feature = "alloc")] + impl Encode for &Vec where self -> **self; + #[cfg(feature = "alloc")] + impl Encode for &String where self -> **self; + #[cfg(feature = "bytes")] + impl Encode for &Bytes where self -> **self; + +); + +/// A trait indicating that the type is encoded like an RFC4251 string. +/// +/// Implementing this trait allows encoding sequences of the type as a string of strings. +/// +/// A `string` is described in [RFC4251 § 5]: /// /// > Arbitrary length binary string. Strings are allowed to contain /// > arbitrary binary data, including null characters and 8-bit @@ -192,40 +261,27 @@ impl Encode for [u8; N] { /// > UTF-8 mapping does not alter the encoding of US-ASCII characters. /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 -impl Encode for &str { - fn encoded_len(&self) -> Result { - self.as_bytes().encoded_len() - } - - fn encode(&self, writer: &mut impl Writer) -> Result<(), Error> { - self.as_bytes().encode(writer) - } -} +pub trait Rfc4251String: Encode {} +impl Rfc4251String for str {} +impl Rfc4251String for [u8] {} #[cfg(feature = "alloc")] -impl Encode for Vec { - fn encoded_len(&self) -> Result { - self.as_slice().encoded_len() - } - - fn encode(&self, writer: &mut impl Writer) -> Result<(), Error> { - self.as_slice().encode(writer) - } -} - +impl Rfc4251String for String {} #[cfg(feature = "alloc")] -impl Encode for String { - fn encoded_len(&self) -> Result { - self.as_str().encoded_len() - } - - fn encode(&self, writer: &mut impl Writer) -> Result<(), Error> { - self.as_str().encode(writer) - } +impl Rfc4251String for Vec {} +#[cfg(feature = "bytes")] +impl Rfc4251String for Bytes {} + +/// Any reference to [`Rfc4251String`] is itself [`Rfc4251String`] if `&T: Encode`. +impl<'a, T> Rfc4251String for &'a T +where + T: Rfc4251String + ?Sized, + &'a T: Encode, +{ } -#[cfg(feature = "alloc")] -impl Encode for Vec { +/// Encode a slice of string-like types as a string wrapping all the entries. +impl Encode for [T] { fn encoded_len(&self) -> Result { self.iter().try_fold(4usize, |acc, string| { acc.checked_add(string.encoded_len()?).ok_or(Error::Length) @@ -237,22 +293,11 @@ impl Encode for Vec { .checked_sub(4) .ok_or(Error::Length)? .encode(writer)?; - - for entry in self { - entry.encode(writer)?; - } - - Ok(()) + self.iter().try_fold((), |(), entry| entry.encode(writer)) } } -#[cfg(feature = "bytes")] -impl Encode for Bytes { - fn encoded_len(&self) -> Result { - self.as_ref().encoded_len() - } - - fn encode(&self, writer: &mut impl Writer) -> Result<(), Error> { - self.as_ref().encode(writer) - } -} +impl_by_delegation!( + #[cfg(feature = "alloc")] + impl (T: Rfc4251String) Encode for Vec where self -> self.as_slice(); +); diff --git a/ssh-encoding/tests/encode.rs b/ssh-encoding/tests/encode.rs index 0611363..257cc7f 100644 --- a/ssh-encoding/tests/encode.rs +++ b/ssh-encoding/tests/encode.rs @@ -89,4 +89,37 @@ fn encode_string_vec() { out, hex!("0000001500000003666f6f000000036261720000000362617a") ); + + // Should also work with a Vec of references to Strings. + let vec: Vec<&String> = vec.iter().collect(); + let mut out = Vec::new(); + vec.encode(&mut out).unwrap(); + + assert_eq!( + out, + hex!("0000001500000003666f6f000000036261720000000362617a") + ); +} + +#[test] +fn encode_str_vec() { + let vec = vec!["foo", "bar", "baz"]; + + let mut out = Vec::new(); + vec.encode(&mut out).unwrap(); + + assert_eq!( + out, + hex!("0000001500000003666f6f000000036261720000000362617a") + ); +} + +#[test] +fn encode_slice_vec() { + let vec = vec![[1u8].as_slice(), [2u8, 3u8].as_slice(), [4u8].as_slice()]; + + let mut out = Vec::new(); + vec.encode(&mut out).unwrap(); + + assert_eq!(out, hex!("0000001000000001010000000202030000000104")); }