ssh_key/
authorized_keys.rs
1use 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 std::{fs, path::Path, vec::Vec};
14
15const COMMENT_DELIMITER: char = '#';
17
18pub struct AuthorizedKeys<'a> {
38 lines: core::str::Lines<'a>,
40}
41
42impl<'a> AuthorizedKeys<'a> {
43 pub fn new(input: &'a str) -> Self {
45 Self {
46 lines: input.lines(),
47 }
48 }
49
50 #[cfg(feature = "std")]
53 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
54 pub fn read_file(path: impl AsRef<Path>) -> Result<Vec<Entry>> {
55 let input = fs::read_to_string(path)?;
57 AuthorizedKeys::new(&input).collect()
58 }
59
60 fn next_line_trimmed(&mut self) -> Option<&'a str> {
64 loop {
65 let mut line = self.lines.next()?;
66
67 if let Some((l, _)) = line.split_once(COMMENT_DELIMITER) {
69 line = l;
70 }
71
72 line = line.trim_end();
74
75 if !line.is_empty() {
76 return Some(line);
77 }
78 }
79 }
80}
81
82impl Iterator for AuthorizedKeys<'_> {
83 type Item = Result<Entry>;
84
85 fn next(&mut self) -> Option<Result<Entry>> {
86 self.next_line_trimmed().map(|line| line.parse())
87 }
88}
89
90#[derive(Clone, Debug, Eq, PartialEq)]
92pub struct Entry {
93 #[cfg(feature = "alloc")]
95 config_opts: ConfigOpts,
96
97 public_key: PublicKey,
99}
100
101impl Entry {
102 #[cfg(feature = "alloc")]
104 #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
105 pub fn config_opts(&self) -> &ConfigOpts {
106 &self.config_opts
107 }
108
109 pub fn public_key(&self) -> &PublicKey {
111 &self.public_key
112 }
113}
114
115#[cfg(feature = "alloc")]
116impl From<Entry> for ConfigOpts {
117 fn from(entry: Entry) -> ConfigOpts {
118 entry.config_opts
119 }
120}
121
122impl From<Entry> for PublicKey {
123 fn from(entry: Entry) -> PublicKey {
124 entry.public_key
125 }
126}
127
128impl From<PublicKey> for Entry {
129 fn from(public_key: PublicKey) -> Entry {
130 Entry {
131 #[cfg(feature = "alloc")]
132 config_opts: ConfigOpts::default(),
133 public_key,
134 }
135 }
136}
137
138impl str::FromStr for Entry {
139 type Err = Error;
140
141 fn from_str(line: &str) -> Result<Self> {
142 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 => line
150 .split_once(' ')
151 .map(|(config_opts_str, public_key_str)| {
152 ConfigOptsIter(config_opts_str).validate()?;
153
154 Ok(Self {
155 #[cfg(feature = "alloc")]
156 config_opts: ConfigOpts(config_opts_str.to_string()),
157 public_key: public_key_str.parse()?,
158 })
159 })
160 .ok_or(Error::FormatEncoding)?,
161 _ => Err(Error::FormatEncoding),
162 }
163 }
164}
165
166#[cfg(feature = "alloc")]
167impl ToString for Entry {
168 fn to_string(&self) -> String {
169 let mut s = String::new();
170
171 if !self.config_opts.is_empty() {
172 s.push_str(self.config_opts.as_str());
173 s.push(' ');
174 }
175
176 s.push_str(&self.public_key.to_string());
177 s
178 }
179}
180
181#[cfg(feature = "alloc")]
189#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
190#[derive(Clone, Debug, Default, Eq, PartialEq)]
191pub struct ConfigOpts(String);
192
193#[cfg(feature = "alloc")]
194impl ConfigOpts {
195 pub fn new(string: impl Into<String>) -> Result<Self> {
197 let ret = Self(string.into());
198 ret.iter().validate()?;
199 Ok(ret)
200 }
201
202 pub fn as_str(&self) -> &str {
204 self.0.as_str()
205 }
206
207 pub fn is_empty(&self) -> bool {
209 self.0.is_empty()
210 }
211
212 pub fn iter(&self) -> ConfigOptsIter<'_> {
214 ConfigOptsIter(self.as_str())
215 }
216}
217
218#[cfg(feature = "alloc")]
219impl AsRef<str> for ConfigOpts {
220 fn as_ref(&self) -> &str {
221 self.as_str()
222 }
223}
224
225#[cfg(feature = "alloc")]
226impl str::FromStr for ConfigOpts {
227 type Err = Error;
228
229 fn from_str(s: &str) -> Result<Self> {
230 Self::new(s)
231 }
232}
233
234#[cfg(feature = "alloc")]
235impl fmt::Display for ConfigOpts {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 f.write_str(&self.0)
238 }
239}
240
241#[derive(Clone, Debug)]
243pub struct ConfigOptsIter<'a>(&'a str);
244
245impl<'a> ConfigOptsIter<'a> {
246 pub fn new(s: &'a str) -> Result<Self> {
250 let ret = Self(s);
251 ret.clone().validate()?;
252 Ok(ret)
253 }
254
255 fn validate(&mut self) -> Result<()> {
257 while self.try_next()?.is_some() {}
258 Ok(())
259 }
260
261 fn try_next(&mut self) -> Result<Option<&'a str>> {
263 if self.0.is_empty() {
264 return Ok(None);
265 }
266
267 let mut quoted = false;
268 let mut index = 0;
269
270 while let Some(byte) = self.0.as_bytes().get(index).cloned() {
271 match byte {
272 b',' => {
273 if !quoted {
275 let (next, rest) = self.0.split_at(index);
276 self.0 = &rest[1..]; return Ok(Some(next));
278 }
279 }
280 b'"' => {
282 quoted = !quoted;
284 }
285 b'A'..=b'Z'
287 | b'a'..=b'z'
288 | b'0'..=b'9'
289 | b'!'..=b'/'
290 | b':'..=b'@'
291 | b'['..=b'_'
292 | b'{'
293 | b'}'
294 | b'|'
295 | b'~' => (),
296 _ => return Err(Error::CharacterEncoding),
297 }
298
299 index = index.checked_add(1).ok_or(Error::Length)?;
300 }
301
302 let remaining = self.0;
303 self.0 = "";
304 Ok(Some(remaining))
305 }
306}
307
308impl<'a> Iterator for ConfigOptsIter<'a> {
309 type Item = &'a str;
310
311 fn next(&mut self) -> Option<&'a str> {
312 self.try_next().expect("malformed options string")
314 }
315}
316
317#[cfg(all(test, feature = "alloc"))]
318mod tests {
319 use super::ConfigOptsIter;
320 use crate::Error;
321
322 #[test]
323 fn options_empty() {
324 assert_eq!(ConfigOptsIter("").try_next(), Ok(None));
325 }
326
327 #[test]
328 fn options_no_comma() {
329 let mut opts = ConfigOptsIter("foo");
330 assert_eq!(opts.try_next(), Ok(Some("foo")));
331 assert_eq!(opts.try_next(), Ok(None));
332 }
333
334 #[test]
335 fn options_no_comma_quoted() {
336 let mut opts = ConfigOptsIter("foo=\"bar\"");
337 assert_eq!(opts.try_next(), Ok(Some("foo=\"bar\"")));
338 assert_eq!(opts.try_next(), Ok(None));
339
340 let mut opts = ConfigOptsIter("foo=\"bar,baz\"");
342 assert_eq!(opts.try_next(), Ok(Some("foo=\"bar,baz\"")));
343 assert_eq!(opts.try_next(), Ok(None));
344 }
345
346 #[test]
347 fn options_comma_delimited() {
348 let mut opts = ConfigOptsIter("foo,bar");
349 assert_eq!(opts.try_next(), Ok(Some("foo")));
350 assert_eq!(opts.try_next(), Ok(Some("bar")));
351 assert_eq!(opts.try_next(), Ok(None));
352 }
353
354 #[test]
355 fn options_comma_delimited_quoted() {
356 let mut opts = ConfigOptsIter("foo=\"bar\",baz");
357 assert_eq!(opts.try_next(), Ok(Some("foo=\"bar\"")));
358 assert_eq!(opts.try_next(), Ok(Some("baz")));
359 assert_eq!(opts.try_next(), Ok(None));
360 }
361
362 #[test]
363 fn options_invalid_character() {
364 let mut opts = ConfigOptsIter("❌");
365 assert_eq!(opts.try_next(), Err(Error::CharacterEncoding));
366
367 let mut opts = ConfigOptsIter("x,❌");
368 assert_eq!(opts.try_next(), Ok(Some("x")));
369 assert_eq!(opts.try_next(), Err(Error::CharacterEncoding));
370 }
371}