agent_migrate_core/
endpoint.rs1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::{AgentKind, AgentParseError};
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
10#[serde(try_from = "String", into = "String")]
11pub struct SshHost(String);
12
13impl SshHost {
14 #[must_use]
15 pub fn as_str(&self) -> &str {
16 &self.0
17 }
18}
19
20impl TryFrom<String> for SshHost {
21 type Error = EndpointError;
22
23 fn try_from(value: String) -> Result<Self, Self::Error> {
24 Self::from_str(&value)
25 }
26}
27
28impl From<SshHost> for String {
29 fn from(value: SshHost) -> Self {
30 value.0
31 }
32}
33
34impl FromStr for SshHost {
35 type Err = EndpointError;
36
37 fn from_str(value: &str) -> Result<Self, Self::Err> {
38 let mut bytes = value.bytes();
39 let first = bytes.next().ok_or(EndpointError::InvalidHost)?;
40 let valid_first = first.is_ascii_alphanumeric();
41 let valid_rest =
42 bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'));
43 if value.len() > 64 || !valid_first || !valid_rest || value == "local" {
44 return Err(EndpointError::InvalidHost);
45 }
46 Ok(Self(value.to_owned()))
47 }
48}
49
50impl fmt::Display for SshHost {
51 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52 formatter.write_str(&self.0)
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum Host {
59 Local,
60 Ssh(SshHost),
61}
62
63impl fmt::Display for Host {
64 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::Local => formatter.write_str("local"),
67 Self::Ssh(host) => host.fmt(formatter),
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct Endpoint {
74 agent: AgentKind,
75 host: Host,
76}
77
78impl Endpoint {
79 #[must_use]
80 pub fn new(agent: AgentKind, host: Host) -> Self {
81 Self { agent, host }
82 }
83
84 #[must_use]
85 pub fn agent(&self) -> AgentKind {
86 self.agent
87 }
88
89 #[must_use]
90 pub fn host(&self) -> &Host {
91 &self.host
92 }
93}
94
95impl FromStr for Endpoint {
96 type Err = EndpointError;
97
98 fn from_str(value: &str) -> Result<Self, Self::Err> {
99 let (agent, host) = value.split_once('@').ok_or(EndpointError::InvalidFormat)?;
100 if host.contains('@') {
101 return Err(EndpointError::InvalidFormat);
102 }
103 let agent = AgentKind::from_str(agent)?;
104 let host = match host {
105 "local" => Host::Local,
106 remote => Host::Ssh(SshHost::from_str(remote)?),
107 };
108 Ok(Self { agent, host })
109 }
110}
111
112impl fmt::Display for Endpoint {
113 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
114 write!(formatter, "{}@{}", self.agent, self.host)
115 }
116}
117
118#[derive(Debug, Error, PartialEq, Eq)]
119pub enum EndpointError {
120 #[error("endpoint must use <agent>@<host>")]
121 InvalidFormat,
122 #[error("invalid SSH config host alias")]
123 InvalidHost,
124 #[error(transparent)]
125 Agent(#[from] AgentParseError),
126}