use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{AgentKind, AgentParseError};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct SshHost(String);
impl SshHost {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for SshHost {
type Error = EndpointError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::from_str(&value)
}
}
impl From<SshHost> for String {
fn from(value: SshHost) -> Self {
value.0
}
}
impl FromStr for SshHost {
type Err = EndpointError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let mut bytes = value.bytes();
let first = bytes.next().ok_or(EndpointError::InvalidHost)?;
let valid_first = first.is_ascii_alphanumeric();
let valid_rest =
bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'));
if value.len() > 64 || !valid_first || !valid_rest || value == "local" {
return Err(EndpointError::InvalidHost);
}
Ok(Self(value.to_owned()))
}
}
impl fmt::Display for SshHost {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Host {
Local,
Ssh(SshHost),
}
impl fmt::Display for Host {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Local => formatter.write_str("local"),
Self::Ssh(host) => host.fmt(formatter),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Endpoint {
agent: AgentKind,
host: Host,
}
impl Endpoint {
#[must_use]
pub fn new(agent: AgentKind, host: Host) -> Self {
Self { agent, host }
}
#[must_use]
pub fn agent(&self) -> AgentKind {
self.agent
}
#[must_use]
pub fn host(&self) -> &Host {
&self.host
}
}
impl FromStr for Endpoint {
type Err = EndpointError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let (agent, host) = value.split_once('@').ok_or(EndpointError::InvalidFormat)?;
if host.contains('@') {
return Err(EndpointError::InvalidFormat);
}
let agent = AgentKind::from_str(agent)?;
let host = match host {
"local" => Host::Local,
remote => Host::Ssh(SshHost::from_str(remote)?),
};
Ok(Self { agent, host })
}
}
impl fmt::Display for Endpoint {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}@{}", self.agent, self.host)
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum EndpointError {
#[error("endpoint must use <agent>@<host>")]
InvalidFormat,
#[error("invalid SSH config host alias")]
InvalidHost,
#[error(transparent)]
Agent(#[from] AgentParseError),
}