ssh_key/
authorized_keys.rs1use crate::{Error, PublicKey, Result};
4use core::str;
5
6#[cfg(feature = "alloc")]
7use {
8 alloc::string::{String, ToString},
9 core::fmt,
10};
11
12#[cfg(feature = "std")]
13use {
14 alloc::vec::Vec,
15 std::{fs, path::Path},
16};
17
18const COMMENT_DELIMITER: char = '#';
20
21pub struct AuthorizedKeys<'a> {
41 lines: str::Lines<'a>,
43}
44
45impl<'a> AuthorizedKeys<'a> {
46 pub fn new(input: &'a str) -> Self {
48 Self {
49 lines: input.lines(),
50 }
51 }
52
53 #[cfg(feature = "std")]
56 pub fn read_file(path: impl AsRef<Path>) -> Result<Vec<Entry>> {
57 let input = fs::read_to_string(path)?;
59 AuthorizedKeys::new(&input).collect()
60 }
61
62 fn next_line_trimmed(&mut self) -> Option<&'a str> {
66 loop {
67 let mut line = self.lines.next()?;
68
69 if let Some((l, _)) = line.split_once(COMMENT_DELIMITER) {
71 line = l;
72 }
73
74 line = line.trim_end();
76
77 if !line.is_empty() {
78 return Some(line);
79 }
80 }
81 }
82}
83
84impl Iterator for AuthorizedKeys<'_> {
85 type Item = Result<Entry>;
86
87 fn next(&mut self) -> Option<Result<Entry>> {
88 self.next_line_trimmed().map(|line| line.parse())
89 }
90}
91
92#[derive(Clone, Debug, Eq, PartialEq)]
94pub struct Entry {
95 #[cfg(feature = "alloc")]
97 config_opts: ConfigOpts,
98
99 public_key: PublicKey,
101}
102
103impl Entry {
104 #[cfg(feature = "alloc")]
106 pub fn config_opts(&self) -> &ConfigOpts {
107 &self.config_opts
108 }
109
110 pub fn public_key(&self) -> &PublicKey {
112 &self.public_key
113 }
114}
115
116#[cfg(feature = "alloc")]
117impl From<Entry> for ConfigOpts {
118 fn from(entry: Entry) -> ConfigOpts {
119 entry.config_opts
120 }
121}
122
123impl From<Entry> for PublicKey {
124 fn from(entry: Entry) -> PublicKey {
125 entry.public_key
126 }
127}
128
129impl From<PublicKey> for Entry {
130 fn from(public_key: PublicKey) -> Entry {
131 Entry {
132 #[cfg(feature = "alloc")]
133 config_opts: ConfigOpts::default(),
134 public_key,
135 }
136 }
137}
138
139impl str::FromStr for Entry {
140 type Err = Error;
141
142 fn from_str(line: &str) -> Result<Self> {
143 match line.matches(' ').count() {
144 1..=2 => Ok(Self {
145 #[cfg(feature = "alloc")]
146 config_opts: Default::default(),
147 public_key: line.parse()?,
148 }),
149 3.. => {
150 match line.parse() {
155 Ok(public_key) => Ok(Self {
156 #[cfg(feature = "alloc")]
157 config_opts: Default::default(),
158 public_key,
159 }),
160 Err(_) => line
161 .split_once(' ')
162 .map(|(config_opts_str, public_key_str)| {
163 ConfigOptsIter(config_opts_str).validate()?;
164
165 Ok(Self {
166 #[cfg(feature = "alloc")]
167 config_opts: ConfigOpts(config_opts_str.to_string()),
168 public_key: public_key_str.parse()?,
169 })
170 })
171 .ok_or(Error::FormatEncoding)?,
172 }
173 }
174 _ => Err(Error::FormatEncoding),
175 }
176 }
177}
178
179#[cfg(feature = "alloc")]
180#[allow(clippy::to_string_trait_impl)]
181impl ToString for Entry {
182 fn to_string(&self) -> String {
183 let mut s = String::new();
184
185 if !self.config_opts.is_empty() {
186 s.push_str(self.config_opts.as_str());
187 s.push(' ');
188 }
189
190 s.push_str(&self.public_key.to_string());
191 s
192 }
193}
194
195#[cfg(feature = "alloc")]
203#[derive(Clone, Debug, Default, Eq, PartialEq)]
204pub struct ConfigOpts(String);
205
206#[cfg(feature = "alloc")]
207impl ConfigOpts {
208 pub fn new(string: impl Into<String>) -> Result<Self> {
210 let ret = Self(string.into());
211 ret.iter().validate()?;
212 Ok(ret)
213 }
214
215 pub fn as_str(&self) -> &str {
217 self.0.as_str()
218 }
219
220 pub fn is_empty(&self) -> bool {
222 self.0.is_empty()
223 }
224
225 pub fn iter(&self) -> ConfigOptsIter<'_> {
227 ConfigOptsIter(self.as_str())
228 }
229}
230
231#[cfg(feature = "alloc")]
232impl AsRef<str> for ConfigOpts {
233 fn as_ref(&self) -> &str {
234 self.as_str()
235 }
236}
237
238#[cfg(feature = "alloc")]
239impl str::FromStr for ConfigOpts {
240 type Err = Error;
241
242 fn from_str(s: &str) -> Result<Self> {
243 Self::new(s)
244 }
245}
246
247#[cfg(feature = "alloc")]
248impl fmt::Display for ConfigOpts {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 f.write_str(&self.0)
251 }
252}
253
254#[derive(Clone, Debug)]
256pub struct ConfigOptsIter<'a>(&'a str);
257
258impl<'a> ConfigOptsIter<'a> {
259 pub fn new(s: &'a str) -> Result<Self> {
263 let ret = Self(s);
264 ret.clone().validate()?;
265 Ok(ret)
266 }
267
268 fn validate(&mut self) -> Result<()> {
270 while self.try_next()?.is_some() {}
271 Ok(())
272 }
273
274 fn try_next(&mut self) -> Result<Option<&'a str>> {
276 if self.0.is_empty() {
277 return Ok(None);
278 }
279
280 let mut quoted = false;
281 let mut index = 0;
282
283 while let Some(byte) = self.0.as_bytes().get(index).cloned() {
284 match byte {
285 b',' => {
286 if !quoted {
288 let (next, rest) = self.0.split_at(index);
289 self.0 = &rest[1..]; return Ok(Some(next));
291 }
292 }
293 b'"' => {
295 quoted = !quoted;
297 }
298 b'A'..=b'Z'
300 | b'a'..=b'z'
301 | b'0'..=b'9'
302 | b'!'..=b'/'
303 | b':'..=b'@'
304 | b'['..=b'_'
305 | b'{'
306 | b'}'
307 | b'|'
308 | b'~' => (),
309 _ => return Err(encoding::Error::CharacterEncoding.into()),
310 }
311
312 index = index.checked_add(1).ok_or(encoding::Error::Length)?;
313 }
314
315 let remaining = self.0;
316 self.0 = "";
317 Ok(Some(remaining))
318 }
319}
320
321impl<'a> Iterator for ConfigOptsIter<'a> {
322 type Item = &'a str;
323
324 fn next(&mut self) -> Option<&'a str> {
325 self.try_next().expect("malformed options string")
327 }
328}
329
330#[cfg(all(test, feature = "alloc"))]
331mod tests {
332 use super::ConfigOptsIter;
333
334 #[test]
335 fn options_empty() {
336 assert_eq!(ConfigOptsIter("").try_next(), Ok(None));
337 }
338
339 #[test]
340 fn options_no_comma() {
341 let mut opts = ConfigOptsIter("foo");
342 assert_eq!(opts.try_next(), Ok(Some("foo")));
343 assert_eq!(opts.try_next(), Ok(None));
344 }
345
346 #[test]
347 fn options_no_comma_quoted() {
348 let mut opts = ConfigOptsIter("foo=\"bar\"");
349 assert_eq!(opts.try_next(), Ok(Some("foo=\"bar\"")));
350 assert_eq!(opts.try_next(), Ok(None));
351
352 let mut opts = ConfigOptsIter("foo=\"bar,baz\"");
354 assert_eq!(opts.try_next(), Ok(Some("foo=\"bar,baz\"")));
355 assert_eq!(opts.try_next(), Ok(None));
356 }
357
358 #[test]
359 fn options_comma_delimited() {
360 let mut opts = ConfigOptsIter("foo,bar");
361 assert_eq!(opts.try_next(), Ok(Some("foo")));
362 assert_eq!(opts.try_next(), Ok(Some("bar")));
363 assert_eq!(opts.try_next(), Ok(None));
364 }
365
366 #[test]
367 fn options_comma_delimited_quoted() {
368 let mut opts = ConfigOptsIter("foo=\"bar\",baz");
369 assert_eq!(opts.try_next(), Ok(Some("foo=\"bar\"")));
370 assert_eq!(opts.try_next(), Ok(Some("baz")));
371 assert_eq!(opts.try_next(), Ok(None));
372 }
373
374 #[test]
375 fn options_invalid_character() {
376 let mut opts = ConfigOptsIter("❌");
377 assert_eq!(
378 opts.try_next(),
379 Err(encoding::Error::CharacterEncoding.into())
380 );
381
382 let mut opts = ConfigOptsIter("x,❌");
383 assert_eq!(opts.try_next(), Ok(Some("x")));
384 assert_eq!(
385 opts.try_next(),
386 Err(encoding::Error::CharacterEncoding.into())
387 );
388 }
389}