1pub const HOST_MAX: usize = 253;
3pub const DEFAULT_PORT: u16 = 21837;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
6#[repr(u8)]
7pub enum State {
8 #[default]
9 Disabled = 0,
10 Unconfigured = 1,
11 WaitingForNetwork = 2,
12 Connecting = 3,
13 Connected = 4,
14 Retrying = 5,
15}
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18#[repr(u8)]
19pub enum Reason {
20 #[default]
21 None = 0,
22 Dns = 1,
23 Tcp = 2,
24 Tls = 3,
25 Authentication = 4,
26 IdleTimeout = 5,
27 IdentityUnavailable = 6,
28}
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub struct Link {
32 pub state: State,
33 pub reason: Reason,
34}
35
36impl Link {
37 pub const fn new(state: State, reason: Reason) -> Self {
38 Self { state, reason }
39 }
40 pub const fn encode(self) -> [u8; 2] {
41 [self.state as u8, self.reason as u8]
42 }
43 pub fn decode(bytes: &[u8]) -> Option<Self> {
44 let [state, reason] = bytes else {
45 return None;
46 };
47 Some(Self {
48 state: match state {
49 0 => State::Disabled,
50 1 => State::Unconfigured,
51 2 => State::WaitingForNetwork,
52 3 => State::Connecting,
53 4 => State::Connected,
54 5 => State::Retrying,
55 _ => return None,
56 },
57 reason: match reason {
58 0 => Reason::None,
59 1 => Reason::Dns,
60 2 => Reason::Tcp,
61 3 => Reason::Tls,
62 4 => Reason::Authentication,
63 5 => Reason::IdleTimeout,
64 6 => Reason::IdentityUnavailable,
65 _ => return None,
66 },
67 })
68 }
69}
70
71pub fn valid_host(host: &str) -> bool {
73 if host.is_empty() {
74 return true;
75 }
76 if host.len() > HOST_MAX || !host.is_ascii() {
77 return false;
78 }
79 if host.contains(':') {
80 return host.parse::<core::net::Ipv6Addr>().is_ok();
81 }
82 if host.bytes().all(|b| b.is_ascii_digit() || b == b'.') {
83 return host.parse::<core::net::Ipv4Addr>().is_ok();
84 }
85 host.strip_suffix('.')
86 .unwrap_or(host)
87 .split('.')
88 .all(|label| {
89 !label.is_empty()
90 && label.len() <= 63
91 && label.as_bytes()[0].is_ascii_alphanumeric()
92 && label.as_bytes()[label.len() - 1].is_ascii_alphanumeric()
93 && label
94 .bytes()
95 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
96 })
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 #[test]
103 fn hosts_are_endpoints_not_urls() {
104 for host in [
105 "",
106 "bridge.example.net",
107 "bridge.example.net.",
108 "127.0.0.1",
109 "::1",
110 "2001:db8::1",
111 ] {
112 assert!(valid_host(host), "{host}");
113 }
114 for host in [
115 "https://bridge",
116 "bridge:21837",
117 "[::1]",
118 "a..b",
119 "-a",
120 "a-",
121 "999.0.0.1",
122 "a\0b",
123 "b ridge",
124 ] {
125 assert!(!valid_host(host), "{host}");
126 }
127 }
128 #[test]
129 fn status_round_trips_and_rejects_unknown_codes() {
130 let link = Link::new(State::Retrying, Reason::Authentication);
131 assert_eq!(Link::decode(&link.encode()), Some(link));
132 for bytes in [&[][..], &[0], &[6, 0], &[0, 7], &[0, 0, 0]] {
133 assert!(Link::decode(bytes).is_none());
134 }
135 }
136}