use crate::{Error, PublicKey, Result};
use core::str;
#[cfg(feature = "alloc")]
use {
alloc::string::{String, ToString},
core::fmt,
};
#[cfg(feature = "std")]
use std::{fs, path::Path, vec::Vec};
const COMMENT_DELIMITER: char = '#';
pub struct AuthorizedKeys<'a> {
lines: core::str::Lines<'a>,
}
impl<'a> AuthorizedKeys<'a> {
pub fn new(input: &'a str) -> Self {
Self {
lines: input.lines(),
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn read_file(path: impl AsRef<Path>) -> Result<Vec<Entry>> {
let input = fs::read_to_string(path)?;
AuthorizedKeys::new(&input).collect()
}
fn next_line_trimmed(&mut self) -> Option<&'a str> {
loop {
let mut line = self.lines.next()?;
if let Some((l, _)) = line.split_once(COMMENT_DELIMITER) {
line = l;
}
line = line.trim_end();
if !line.is_empty() {
return Some(line);
}
}
}
}
impl Iterator for AuthorizedKeys<'_> {
type Item = Result<Entry>;
fn next(&mut self) -> Option<Result<Entry>> {
self.next_line_trimmed().map(|line| line.parse())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Entry {
#[cfg(feature = "alloc")]
config_opts: ConfigOpts,
public_key: PublicKey,
}
impl Entry {
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn config_opts(&self) -> &ConfigOpts {
&self.config_opts
}
pub fn public_key(&self) -> &PublicKey {
&self.public_key
}
}
#[cfg(feature = "alloc")]
impl From<Entry> for ConfigOpts {
fn from(entry: Entry) -> ConfigOpts {
entry.config_opts
}
}
impl From<Entry> for PublicKey {
fn from(entry: Entry) -> PublicKey {
entry.public_key
}
}
impl From<PublicKey> for Entry {
fn from(public_key: PublicKey) -> Entry {
Entry {
#[cfg(feature = "alloc")]
config_opts: ConfigOpts::default(),
public_key,
}
}
}
impl str::FromStr for Entry {
type Err = Error;
fn from_str(line: &str) -> Result<Self> {
match line.matches(' ').count() {
1..=2 => Ok(Self {
#[cfg(feature = "alloc")]
config_opts: Default::default(),
public_key: line.parse()?,
}),
3 => line
.split_once(' ')
.map(|(config_opts_str, public_key_str)| {
ConfigOptsIter(config_opts_str).validate()?;
Ok(Self {
#[cfg(feature = "alloc")]
config_opts: ConfigOpts(config_opts_str.to_string()),
public_key: public_key_str.parse()?,
})
})
.ok_or(Error::FormatEncoding)?,
_ => Err(Error::FormatEncoding),
}
}
}
#[cfg(feature = "alloc")]
impl ToString for Entry {
fn to_string(&self) -> String {
let mut s = String::new();
if !self.config_opts.is_empty() {
s.push_str(self.config_opts.as_str());
s.push(' ');
}
s.push_str(&self.public_key.to_string());
s
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ConfigOpts(String);
#[cfg(feature = "alloc")]
impl ConfigOpts {
pub fn new(string: impl Into<String>) -> Result<Self> {
let ret = Self(string.into());
ret.iter().validate()?;
Ok(ret)
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> ConfigOptsIter<'_> {
ConfigOptsIter(self.as_str())
}
}
#[cfg(feature = "alloc")]
impl AsRef<str> for ConfigOpts {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(feature = "alloc")]
impl str::FromStr for ConfigOpts {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::new(s)
}
}
#[cfg(feature = "alloc")]
impl fmt::Display for ConfigOpts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug)]
pub struct ConfigOptsIter<'a>(&'a str);
impl<'a> ConfigOptsIter<'a> {
pub fn new(s: &'a str) -> Result<Self> {
let ret = Self(s);
ret.clone().validate()?;
Ok(ret)
}
fn validate(&mut self) -> Result<()> {
while self.try_next()?.is_some() {}
Ok(())
}
fn try_next(&mut self) -> Result<Option<&'a str>> {
if self.0.is_empty() {
return Ok(None);
}
let mut quoted = false;
let mut index = 0;
while let Some(byte) = self.0.as_bytes().get(index).cloned() {
match byte {
b',' => {
if !quoted {
let (next, rest) = self.0.split_at(index);
self.0 = &rest[1..]; return Ok(Some(next));
}
}
b'"' => {
quoted = !quoted;
}
b'A'..=b'Z'
| b'a'..=b'z'
| b'0'..=b'9'
| b'!'..=b'/'
| b':'..=b'@'
| b'['..=b'_'
| b'{'
| b'}'
| b'|'
| b'~' => (),
_ => return Err(Error::CharacterEncoding),
}
index = index.checked_add(1).ok_or(Error::Length)?;
}
let remaining = self.0;
self.0 = "";
Ok(Some(remaining))
}
}
impl<'a> Iterator for ConfigOptsIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
self.try_next().expect("malformed options string")
}
}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::ConfigOptsIter;
use crate::Error;
#[test]
fn options_empty() {
assert_eq!(ConfigOptsIter("").try_next(), Ok(None));
}
#[test]
fn options_no_comma() {
let mut opts = ConfigOptsIter("foo");
assert_eq!(opts.try_next(), Ok(Some("foo")));
assert_eq!(opts.try_next(), Ok(None));
}
#[test]
fn options_no_comma_quoted() {
let mut opts = ConfigOptsIter("foo=\"bar\"");
assert_eq!(opts.try_next(), Ok(Some("foo=\"bar\"")));
assert_eq!(opts.try_next(), Ok(None));
let mut opts = ConfigOptsIter("foo=\"bar,baz\"");
assert_eq!(opts.try_next(), Ok(Some("foo=\"bar,baz\"")));
assert_eq!(opts.try_next(), Ok(None));
}
#[test]
fn options_comma_delimited() {
let mut opts = ConfigOptsIter("foo,bar");
assert_eq!(opts.try_next(), Ok(Some("foo")));
assert_eq!(opts.try_next(), Ok(Some("bar")));
assert_eq!(opts.try_next(), Ok(None));
}
#[test]
fn options_comma_delimited_quoted() {
let mut opts = ConfigOptsIter("foo=\"bar\",baz");
assert_eq!(opts.try_next(), Ok(Some("foo=\"bar\"")));
assert_eq!(opts.try_next(), Ok(Some("baz")));
assert_eq!(opts.try_next(), Ok(None));
}
#[test]
fn options_invalid_character() {
let mut opts = ConfigOptsIter("❌");
assert_eq!(opts.try_next(), Err(Error::CharacterEncoding));
let mut opts = ConfigOptsIter("x,❌");
assert_eq!(opts.try_next(), Ok(Some("x")));
assert_eq!(opts.try_next(), Err(Error::CharacterEncoding));
}
}