ssh_key/
comment.rs

1//! SSH key comment support.
2
3use alloc::{borrow::ToOwned, boxed::Box, string::String, vec::Vec};
4use core::{
5    convert::Infallible,
6    fmt,
7    str::{self, FromStr},
8};
9use encoding::{Decode, Encode, Error, Reader, Writer};
10
11/// SSH key comment (e.g. email address of owner)
12///
13/// Comments may be found in both the binary serialization of  [`PrivateKey`] as well as the text
14/// serialization of [`PublicKey`].
15///
16/// The binary serialization of [`PrivateKey`] stores the comment encoded as an [RFC4251]
17/// `string` type which can contain arbitrary binary data and does not necessarily represent valid
18/// UTF-8. To support round trip encoding of such comments.
19///
20/// To support round-trip encoding of such comments, this type also supports arbitrary binary data.
21///
22/// [RFC4251]: https://datatracker.ietf.org/doc/html/rfc4251#section-5
23/// [`PrivateKey`]: crate::PrivateKey
24/// [`PublicKey`]: crate::PublicKey
25#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
26pub struct Comment(Box<[u8]>);
27
28impl AsRef<[u8]> for Comment {
29    fn as_ref(&self) -> &[u8] {
30        self.as_bytes()
31    }
32}
33
34impl AsRef<str> for Comment {
35    fn as_ref(&self) -> &str {
36        self.as_str_lossy()
37    }
38}
39
40impl Decode for Comment {
41    type Error = Error;
42
43    fn decode(reader: &mut impl Reader) -> encoding::Result<Self> {
44        Vec::<u8>::decode(reader).map(Into::into)
45    }
46}
47
48impl Encode for Comment {
49    fn encoded_len(&self) -> Result<usize, Error> {
50        self.0.encoded_len()
51    }
52
53    fn encode(&self, writer: &mut impl Writer) -> Result<(), Error> {
54        self.0.encode(writer)
55    }
56}
57
58impl FromStr for Comment {
59    type Err = Infallible;
60
61    fn from_str(s: &str) -> Result<Comment, Infallible> {
62        Ok(s.into())
63    }
64}
65
66impl From<&str> for Comment {
67    fn from(s: &str) -> Comment {
68        s.to_owned().into()
69    }
70}
71
72impl From<String> for Comment {
73    fn from(s: String) -> Self {
74        s.into_bytes().into()
75    }
76}
77
78impl From<&[u8]> for Comment {
79    fn from(bytes: &[u8]) -> Comment {
80        bytes.to_owned().into()
81    }
82}
83
84impl From<Vec<u8>> for Comment {
85    fn from(vec: Vec<u8>) -> Self {
86        Self(vec.into_boxed_slice())
87    }
88}
89
90impl From<Comment> for Vec<u8> {
91    fn from(comment: Comment) -> Vec<u8> {
92        comment.0.into()
93    }
94}
95
96impl TryFrom<Comment> for String {
97    type Error = Error;
98
99    fn try_from(comment: Comment) -> Result<String, Error> {
100        comment.as_str().map(ToOwned::to_owned)
101    }
102}
103
104impl fmt::Display for Comment {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        f.write_str(self.as_str_lossy())
107    }
108}
109
110impl Comment {
111    /// Interpret the comment as raw binary data.
112    pub fn as_bytes(&self) -> &[u8] {
113        &self.0
114    }
115
116    /// Interpret the comment as a UTF-8 string.
117    pub fn as_str(&self) -> Result<&str, Error> {
118        Ok(str::from_utf8(&self.0)?)
119    }
120
121    /// Interpret the comment as a UTF-8 string.
122    ///
123    /// This is the maximal prefix of the comment which can be interpreted as valid UTF-8.
124    // TODO(tarcieri): precompute and store the offset which represents this prefix?
125    #[cfg(feature = "alloc")]
126    pub fn as_str_lossy(&self) -> &str {
127        for i in (1..=self.len()).rev() {
128            if let Ok(s) = str::from_utf8(&self.0[..i]) {
129                return s;
130            }
131        }
132
133        ""
134    }
135
136    /// Is the comment empty?
137    pub fn is_empty(&self) -> bool {
138        self.0.is_empty()
139    }
140
141    /// Get the length of this comment in bytes.
142    pub fn len(&self) -> usize {
143        self.0.len()
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::Comment;
150
151    #[test]
152    fn as_str_lossy_ignores_non_utf8_data() {
153        const EXAMPLE: &[u8] = b"hello world\xc3\x28";
154
155        let comment = Comment::from(EXAMPLE);
156        assert_eq!(comment.as_str_lossy(), "hello world");
157    }
158}