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