Skip to content

Commit c242dab

Browse files
committed
Implement Encode for sequences of string-like types
With this change, various types like `&[&str]`, `Vec<&String>, &[Vec<u8>`, 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<T: Encode> 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.
1 parent aac3aee commit c242dab

File tree

2 files changed

+124
-46
lines changed

2 files changed

+124
-46
lines changed

ssh-encoding/src/encode.rs

Lines changed: 91 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,76 @@ impl<const N: usize> Encode for [u8; N] {
175175
}
176176
}
177177

178-
/// Encode a `string` as described in [RFC4251 § 5]:
178+
/// A macro to implement `Encode` for a type by delegating to some transformed version of `self`.
179+
macro_rules! impl_by_delegation {
180+
(
181+
$(
182+
$(#[$attr:meta])*
183+
impl $( ($($generics:tt)+) )? Encode for $type:ty where $self:ident -> $delegate:expr;
184+
)+
185+
) => {
186+
$(
187+
$(#[$attr])*
188+
impl $(< $($generics)* >)? Encode for $type {
189+
fn encoded_len(&$self) -> Result<usize, Error> {
190+
$delegate.encoded_len()
191+
}
192+
193+
fn encode(&$self, writer: &mut impl Writer) -> Result<(), Error> {
194+
$delegate.encode(writer)
195+
}
196+
}
197+
)+
198+
};
199+
}
200+
201+
impl_by_delegation!(
202+
/// Encode a `string` as described in [RFC4251 § 5]:
203+
///
204+
/// > Arbitrary length binary string. Strings are allowed to contain
205+
/// > arbitrary binary data, including null characters and 8-bit
206+
/// > characters. They are stored as a uint32 containing its length
207+
/// > (number of bytes that follow) and zero (= empty string) or more
208+
/// > bytes that are the value of the string. Terminating null
209+
/// > characters are not used.
210+
/// >
211+
/// > Strings are also used to store text. In that case, US-ASCII is
212+
/// > used for internal names, and ISO-10646 UTF-8 for text that might
213+
/// > be displayed to the user. The terminating null character SHOULD
214+
/// > NOT normally be stored in the string. For example: the US-ASCII
215+
/// > string "testing" is represented as 00 00 00 07 t e s t i n g. The
216+
/// > UTF-8 mapping does not alter the encoding of US-ASCII characters.
217+
///
218+
/// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5
219+
impl Encode for str where self -> self.as_bytes();
220+
221+
#[cfg(feature = "alloc")]
222+
impl Encode for Vec<u8> where self -> self.as_slice();
223+
#[cfg(feature = "alloc")]
224+
impl Encode for String where self -> self.as_bytes();
225+
#[cfg(feature = "bytes")]
226+
impl Encode for Bytes where self -> self.as_ref();
227+
228+
// While deref coercion ensures that `&E` can use the `Encode` trait methods, it will not be
229+
// allowd in trait bounds, as `&E` does not implement `Encode` itself just because `E: Encode`.
230+
// A blanket impl for `&E` would be the most generic, but that collides with the `Label` trait's
231+
// blanket impl. Instead, we can do it explicitly for the immediatley relevant base types.
232+
impl Encode for &str where self -> **self;
233+
impl Encode for &[u8] where self -> **self;
234+
#[cfg(feature = "alloc")]
235+
impl Encode for &Vec<u8> where self -> **self;
236+
#[cfg(feature = "alloc")]
237+
impl Encode for &String where self -> **self;
238+
#[cfg(feature = "bytes")]
239+
impl Encode for &Bytes where self -> **self;
240+
241+
);
242+
243+
/// A trait indicating that the type is encoded like an RFC4251 string.
244+
///
245+
/// Implementing this trait allows encoding sequences of the type as a string of strings.
246+
///
247+
/// A `string` is described in [RFC4251 § 5]:
179248
///
180249
/// > Arbitrary length binary string. Strings are allowed to contain
181250
/// > arbitrary binary data, including null characters and 8-bit
@@ -192,40 +261,27 @@ impl<const N: usize> Encode for [u8; N] {
192261
/// > UTF-8 mapping does not alter the encoding of US-ASCII characters.
193262
///
194263
/// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5
195-
impl Encode for &str {
196-
fn encoded_len(&self) -> Result<usize, Error> {
197-
self.as_bytes().encoded_len()
198-
}
199-
200-
fn encode(&self, writer: &mut impl Writer) -> Result<(), Error> {
201-
self.as_bytes().encode(writer)
202-
}
203-
}
264+
pub trait Rfc4251String: Encode {}
204265

266+
impl Rfc4251String for str {}
267+
impl Rfc4251String for [u8] {}
205268
#[cfg(feature = "alloc")]
206-
impl Encode for Vec<u8> {
207-
fn encoded_len(&self) -> Result<usize, Error> {
208-
self.as_slice().encoded_len()
209-
}
210-
211-
fn encode(&self, writer: &mut impl Writer) -> Result<(), Error> {
212-
self.as_slice().encode(writer)
213-
}
214-
}
215-
269+
impl Rfc4251String for String {}
216270
#[cfg(feature = "alloc")]
217-
impl Encode for String {
218-
fn encoded_len(&self) -> Result<usize, Error> {
219-
self.as_str().encoded_len()
220-
}
221-
222-
fn encode(&self, writer: &mut impl Writer) -> Result<(), Error> {
223-
self.as_str().encode(writer)
224-
}
271+
impl Rfc4251String for Vec<u8> {}
272+
#[cfg(feature = "bytes")]
273+
impl Rfc4251String for Bytes {}
274+
275+
/// Any reference to [`Rfc4251String`] is itself [`Rfc4251String`] if `&T: Encode`.
276+
impl<'a, T> Rfc4251String for &'a T
277+
where
278+
T: Rfc4251String + ?Sized,
279+
&'a T: Encode,
280+
{
225281
}
226282

227-
#[cfg(feature = "alloc")]
228-
impl Encode for Vec<String> {
283+
/// Encode a slice of string-like types as a string wrapping all the entries.
284+
impl<T: Rfc4251String> Encode for [T] {
229285
fn encoded_len(&self) -> Result<usize, Error> {
230286
self.iter().try_fold(4usize, |acc, string| {
231287
acc.checked_add(string.encoded_len()?).ok_or(Error::Length)
@@ -237,22 +293,11 @@ impl Encode for Vec<String> {
237293
.checked_sub(4)
238294
.ok_or(Error::Length)?
239295
.encode(writer)?;
240-
241-
for entry in self {
242-
entry.encode(writer)?;
243-
}
244-
245-
Ok(())
296+
self.iter().try_fold((), |(), entry| entry.encode(writer))
246297
}
247298
}
248299

249-
#[cfg(feature = "bytes")]
250-
impl Encode for Bytes {
251-
fn encoded_len(&self) -> Result<usize, Error> {
252-
self.as_ref().encoded_len()
253-
}
254-
255-
fn encode(&self, writer: &mut impl Writer) -> Result<(), Error> {
256-
self.as_ref().encode(writer)
257-
}
258-
}
300+
impl_by_delegation!(
301+
#[cfg(feature = "alloc")]
302+
impl (T: Rfc4251String) Encode for Vec<T> where self -> self.as_slice();
303+
);

ssh-encoding/tests/encode.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,37 @@ fn encode_string_vec() {
8989
out,
9090
hex!("0000001500000003666f6f000000036261720000000362617a")
9191
);
92+
93+
// Should also work with a Vec of references to Strings.
94+
let vec: Vec<&String> = vec.iter().collect();
95+
let mut out = Vec::new();
96+
vec.encode(&mut out).unwrap();
97+
98+
assert_eq!(
99+
out,
100+
hex!("0000001500000003666f6f000000036261720000000362617a")
101+
);
102+
}
103+
104+
#[test]
105+
fn encode_str_vec() {
106+
let vec = vec!["foo", "bar", "baz"];
107+
108+
let mut out = Vec::new();
109+
vec.encode(&mut out).unwrap();
110+
111+
assert_eq!(
112+
out,
113+
hex!("0000001500000003666f6f000000036261720000000362617a")
114+
);
115+
}
116+
117+
#[test]
118+
fn encode_slice_vec() {
119+
let vec = vec![[1u8].as_slice(), [2u8, 3u8].as_slice(), [4u8].as_slice()];
120+
121+
let mut out = Vec::new();
122+
vec.encode(&mut out).unwrap();
123+
124+
assert_eq!(out, hex!("0000001000000001010000000202030000000104"));
92125
}

0 commit comments

Comments
 (0)