ssh_key/
authorized_keys.rs1use crate::{Error, PublicKey, Result};
4use core::{
5 fmt::{self, Debug},
6 str,
7};
8
9#[cfg(feature = "alloc")]
10use alloc::string::{String, ToString};
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 #[must_use]
48 pub fn new(input: &'a str) -> Self {
49 Self {
50 lines: input.lines(),
51 }
52 }
53
54 #[cfg(feature = "std")]
61 pub fn read_file(path: impl AsRef<Path>) -> Result<Vec<Entry>> {
62 let input = fs::read_to_string(path)?;
64 AuthorizedKeys::new(&input).collect()
65 }
66
67 fn next_line_trimmed(&mut self) -> Option<&'a str> {
69 loop {
70 let mut line = self.lines.next()?;
71
72 if let Some((l, _)) = line.split_once(COMMENT_DELIMITER) {
74 line = l;
75 }
76
77 line = line.trim_end();
79
80 if !line.is_empty() {
81 return Some(line);
82 }
83 }
84 }
85}
86
87impl Debug for AuthorizedKeys<'_> {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 f.debug_struct("AuthorizedKeys").finish_non_exhaustive()
90 }
91}
92
93impl Iterator for AuthorizedKeys<'_> {
94 type Item = Result<Entry>;
95
96 fn next(&mut self) -> Option<Result<Entry>> {
97 self.next_line_trimmed().map(str::parse)
98 }
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct Entry {
104 #[cfg(feature = "alloc")]
106 config_opts: ConfigOpts,
107
108 public_key: PublicKey,
110}
111
112impl Entry {
113 #[cfg(feature = "alloc")]
115 #[must_use]
116 pub fn config_opts(&self) -> &ConfigOpts {
117 &self.config_opts
118 }
119
120 #[must_use]
122 pub fn public_key(&self) -> &PublicKey {
123 &self.public_key
124 }
125}
126
127#[cfg(feature = "alloc")]
128impl From<Entry> for ConfigOpts {
129 fn from(entry: Entry) -> ConfigOpts {
130 entry.config_opts
131 }
132}
133
134impl From<Entry> for PublicKey {
135 fn from(entry: Entry) -> PublicKey {
136 entry.public_key
137 }
138}
139
140impl From<PublicKey> for Entry {
141 fn from(public_key: PublicKey) -> Entry {
142 Entry {
143 #[cfg(feature = "alloc")]
144 config_opts: ConfigOpts::default(),
145 public_key,
146 }
147 }
148}
149
150impl str::FromStr for Entry {
151 type Err = Error;
152
153 fn from_str(line: &str) -> Result<Self> {
154 match line.matches(' ').count() {
155 1..=2 => Ok(Self {
156 #[cfg(feature = "alloc")]
157 config_opts: Default::default(),
158 public_key: line.parse()?,
159 }),
160 3.. => {
161 match line.parse() {
166 Ok(public_key) => Ok(Self {
167 #[cfg(feature = "alloc")]
168 config_opts: Default::default(),
169 public_key,
170 }),
171 Err(_) => line
172 .split_once(' ')
173 .map(|(config_opts_str, public_key_str)| {
174 ConfigOptsIter(config_opts_str).validate()?;
175
176 Ok(Self {
177 #[cfg(feature = "alloc")]
178 config_opts: ConfigOpts(config_opts_str.to_string()),
179 public_key: public_key_str.parse()?,
180 })
181 })
182 .ok_or(Error::FormatEncoding)?,
183 }
184 }
185 _ => Err(Error::FormatEncoding),
186 }
187 }
188}
189
190#[cfg(feature = "alloc")]
191#[allow(clippy::to_string_trait_impl)]
192impl ToString for Entry {
193 fn to_string(&self) -> String {
194 let mut s = String::new();
195
196 if !self.config_opts.is_empty() {
197 s.push_str(self.config_opts.as_str());
198 s.push(' ');
199 }
200
201 s.push_str(&self.public_key.to_string());
202 s
203 }
204}
205
206#[cfg(feature = "alloc")]
214#[derive(Clone, Debug, Default, Eq, PartialEq)]
215pub struct ConfigOpts(String);
216
217#[cfg(feature = "alloc")]
218impl ConfigOpts {
219 pub fn new(string: impl Into<String>) -> Result<Self> {
224 let ret = Self(string.into());
225 ret.iter().validate()?;
226 Ok(ret)
227 }
228
229 #[must_use]
231 pub fn as_str(&self) -> &str {
232 self.0.as_str()
233 }
234
235 #[must_use]
237 pub fn is_empty(&self) -> bool {
238 self.0.is_empty()
239 }
240
241 #[must_use]
243 pub fn iter(&self) -> ConfigOptsIter<'_> {
244 ConfigOptsIter(self.as_str())
245 }
246}
247
248#[cfg(feature = "alloc")]
249impl AsRef<str> for ConfigOpts {
250 fn as_ref(&self) -> &str {
251 self.as_str()
252 }
253}
254
255#[cfg(feature = "alloc")]
256impl str::FromStr for ConfigOpts {
257 type Err = Error;
258
259 fn from_str(s: &str) -> Result<Self> {
260 Self::new(s)
261 }
262}
263
264#[cfg(feature = "alloc")]
265impl fmt::Display for ConfigOpts {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 f.write_str(&self.0)
268 }
269}
270
271#[derive(Clone, Debug)]
273pub struct ConfigOptsIter<'a>(&'a str);
274
275impl<'a> ConfigOptsIter<'a> {
276 pub fn new(s: &'a str) -> Result<Self> {
283 let ret = Self(s);
284 ret.clone().validate()?;
285 Ok(ret)
286 }
287
288 fn validate(&mut self) -> Result<()> {
290 while self.try_next()?.is_some() {}
291 Ok(())
292 }
293
294 fn try_next(&mut self) -> Result<Option<&'a str>> {
296 if self.0.is_empty() {
297 return Ok(None);
298 }
299
300 let mut quoted = false;
301 let mut index = 0;
302
303 while let Some(byte) = self.0.as_bytes().get(index).cloned() {
304 match byte {
305 b',' => {
306 if !quoted {
308 let (next, rest) = self.0.split_at(index);
309 self.0 = &rest[1..]; return Ok(Some(next));
311 }
312 }
313 b'"' => {
315 quoted = !quoted;
317 }
318 b'A'..=b'Z'
320 | b'a'..=b'z'
321 | b'0'..=b'9'
322 | b'!'..=b'/'
323 | b':'..=b'@'
324 | b'['..=b'_'
325 | b'{'
326 | b'}'
327 | b'|'
328 | b'~' => (),
329 _ => return Err(encoding::Error::CharacterEncoding.into()),
330 }
331
332 index = index.checked_add(1).ok_or(encoding::Error::Length)?;
333 }
334
335 let remaining = self.0;
336 self.0 = "";
337 Ok(Some(remaining))
338 }
339}
340
341impl<'a> Iterator for ConfigOptsIter<'a> {
342 type Item = &'a str;
343
344 fn next(&mut self) -> Option<&'a str> {
345 self.try_next().expect("malformed options string")
347 }
348}
349
350#[cfg(all(test, feature = "alloc"))]
351mod tests {
352 use super::ConfigOptsIter;
353
354 #[test]
355 fn options_empty() {
356 assert_eq!(ConfigOptsIter("").try_next(), Ok(None));
357 }
358
359 #[test]
360 fn options_no_comma() {
361 let mut opts = ConfigOptsIter("foo");
362 assert_eq!(opts.try_next(), Ok(Some("foo")));
363 assert_eq!(opts.try_next(), Ok(None));
364 }
365
366 #[test]
367 fn options_no_comma_quoted() {
368 let mut opts = ConfigOptsIter("foo=\"bar\"");
369 assert_eq!(opts.try_next(), Ok(Some("foo=\"bar\"")));
370 assert_eq!(opts.try_next(), Ok(None));
371
372 let mut opts = ConfigOptsIter("foo=\"bar,baz\"");
374 assert_eq!(opts.try_next(), Ok(Some("foo=\"bar,baz\"")));
375 assert_eq!(opts.try_next(), Ok(None));
376 }
377
378 #[test]
379 fn options_comma_delimited() {
380 let mut opts = ConfigOptsIter("foo,bar");
381 assert_eq!(opts.try_next(), Ok(Some("foo")));
382 assert_eq!(opts.try_next(), Ok(Some("bar")));
383 assert_eq!(opts.try_next(), Ok(None));
384 }
385
386 #[test]
387 fn options_comma_delimited_quoted() {
388 let mut opts = ConfigOptsIter("foo=\"bar\",baz");
389 assert_eq!(opts.try_next(), Ok(Some("foo=\"bar\"")));
390 assert_eq!(opts.try_next(), Ok(Some("baz")));
391 assert_eq!(opts.try_next(), Ok(None));
392 }
393
394 #[test]
395 fn options_invalid_character() {
396 let mut opts = ConfigOptsIter("❌");
397 assert_eq!(
398 opts.try_next(),
399 Err(encoding::Error::CharacterEncoding.into())
400 );
401
402 let mut opts = ConfigOptsIter("x,❌");
403 assert_eq!(opts.try_next(), Ok(Some("x")));
404 assert_eq!(
405 opts.try_next(),
406 Err(encoding::Error::CharacterEncoding.into())
407 );
408 }
409}