1use std::{fmt, str::FromStr};
10
11use serde::{Deserialize, Serialize};
12
13pub type Err = anyhow::Error;
15pub type Res<T> = anyhow::Result<T, Err>;
17pub type Void = Res<()>;
19
20pub(crate) fn ensure_tls_provider() {
25 use std::sync::Once;
26 static INSTALL: Once = Once::new();
27 INSTALL.call_once(|| {
28 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
29 });
30}
31
32pub struct Constant;
34
35impl Constant {
36 pub const CHALLENGE_SIZE: usize = 32;
38 pub const CONFIG_DIR_NAME: &'static str = "conclave";
41 pub const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
44 pub const PROTOCOL_VERSION: u32 = 1;
47 pub const SERVER_ID_HEADER: &'static str = "x-conclave-server-id";
51 pub const SESSION_PATH_SEPARATOR: char = '/';
53}
54
55#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize)]
61#[serde(rename_all = "lowercase")]
62pub enum PermissionLevel {
63 Mute,
65 #[default]
67 Notify,
68 Converse,
70 Act,
72}
73
74impl PermissionLevel {
75 #[must_use]
80 pub const fn may_emit(self) -> bool {
81 matches!(self, Self::Converse | Self::Act)
82 }
83}
84
85#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
87#[error("unknown permission level `{0}` (expected mute, notify, converse, or act)")]
88pub struct ParsePermissionError(pub String);
89
90impl FromStr for PermissionLevel {
91 type Err = ParsePermissionError;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 match s {
95 "mute" => Ok(Self::Mute),
96 "notify" => Ok(Self::Notify),
97 "converse" => Ok(Self::Converse),
98 "act" => Ok(Self::Act),
99 other => Err(ParsePermissionError(other.to_owned())),
100 }
101 }
102}
103
104#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
106#[serde(rename_all = "lowercase")]
107pub enum Visibility {
108 Public,
110 Unlisted,
112 Private,
114}
115
116impl Visibility {
117 #[must_use]
119 pub const fn as_str(self) -> &'static str {
120 match self {
121 Self::Public => "public",
122 Self::Unlisted => "unlisted",
123 Self::Private => "private",
124 }
125 }
126}
127
128#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
130#[error("unknown visibility tier `{0}` (expected public, unlisted, or private)")]
131pub struct ParseVisibilityError(pub String);
132
133impl FromStr for Visibility {
134 type Err = ParseVisibilityError;
135
136 fn from_str(s: &str) -> Result<Self, Self::Err> {
137 match s {
138 "public" => Ok(Self::Public),
139 "unlisted" => Ok(Self::Unlisted),
140 "private" => Ok(Self::Private),
141 other => Err(ParseVisibilityError(other.to_owned())),
142 }
143 }
144}
145
146#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
152pub struct SessionPath {
153 pub user: String,
155 pub machine: String,
157 pub session: String,
159}
160
161impl SessionPath {
162 #[must_use]
164 pub fn new(user: impl Into<String>, machine: impl Into<String>, session: impl Into<String>) -> Self {
165 Self {
166 user: user.into(),
167 machine: machine.into(),
168 session: session.into(),
169 }
170 }
171
172 pub fn validate_component(component: &str) -> Result<(), ParsePathError> {
180 if component.is_empty() || component.contains(Constant::SESSION_PATH_SEPARATOR) {
181 return Err(ParsePathError::Malformed(component.to_owned()));
182 }
183 Ok(())
184 }
185}
186
187impl fmt::Display for SessionPath {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 let sep = Constant::SESSION_PATH_SEPARATOR;
190 write!(f, "{}{sep}{}{sep}{}", self.user, self.machine, self.session)
191 }
192}
193
194#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
196pub enum ParsePathError {
197 #[error("session path must be `user/machine/session`, got `{0}`")]
199 Malformed(String),
200}
201
202impl FromStr for SessionPath {
203 type Err = ParsePathError;
204
205 fn from_str(s: &str) -> Result<Self, Self::Err> {
206 let mut parts = s.split(Constant::SESSION_PATH_SEPARATOR);
207 let (Some(user), Some(machine), Some(session), None) = (parts.next(), parts.next(), parts.next(), parts.next()) else {
208 return Err(ParsePathError::Malformed(s.to_owned()));
209 };
210
211 if user.is_empty() || machine.is_empty() || session.is_empty() {
212 return Err(ParsePathError::Malformed(s.to_owned()));
213 }
214
215 Ok(Self::new(user, machine, session))
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 #![allow(clippy::unwrap_used)]
223
224 use super::*;
225 use pretty_assertions::assert_eq;
226
227 #[test]
228 fn permission_levels_order_by_ascending_autonomy() {
229 assert!(PermissionLevel::Mute < PermissionLevel::Notify);
230 assert!(PermissionLevel::Notify < PermissionLevel::Converse);
231 assert!(PermissionLevel::Converse < PermissionLevel::Act);
232 }
233
234 #[test]
235 fn default_permission_level_is_notify() {
236 assert_eq!(PermissionLevel::default(), PermissionLevel::Notify);
237 }
238
239 #[test]
240 fn only_converse_and_above_may_emit() {
241 assert!(!PermissionLevel::Mute.may_emit());
242 assert!(!PermissionLevel::Notify.may_emit());
243 assert!(PermissionLevel::Converse.may_emit());
244 assert!(PermissionLevel::Act.may_emit());
245 }
246
247 #[test]
248 fn permission_level_parses_from_its_lowercase_token() {
249 for (token, level) in [
250 ("mute", PermissionLevel::Mute),
251 ("notify", PermissionLevel::Notify),
252 ("converse", PermissionLevel::Converse),
253 ("act", PermissionLevel::Act),
254 ] {
255 assert_eq!(token.parse::<PermissionLevel>().unwrap(), level);
256 }
257 assert!("bogus".parse::<PermissionLevel>().is_err());
258 }
259
260 #[test]
261 fn session_path_displays_as_slash_separated_triple() {
262 let path = SessionPath::new("aaron", "workstation", "razel");
263 assert_eq!(path.to_string(), "aaron/workstation/razel");
264 }
265
266 #[test]
267 fn session_path_parses_a_slash_separated_triple() {
268 let path: SessionPath = "aaron/workstation/razel".parse().unwrap();
269 assert_eq!(path, SessionPath::new("aaron", "workstation", "razel"));
270 }
271
272 #[test]
273 fn session_path_round_trips_through_display_and_parse() {
274 let path = SessionPath::new("aaron", "sno-box", "dotagent");
275 assert_eq!(path.to_string().parse::<SessionPath>().unwrap(), path);
276 }
277
278 #[test]
279 fn session_path_rejects_malformed_strings() {
280 for bad in ["", "a", "a/b", "a/b/c/d", "a//c", "/b/c", "a/b/"] {
281 assert!(bad.parse::<SessionPath>().is_err(), "expected `{bad}` to be rejected");
282 }
283 }
284
285 #[test]
286 fn session_path_component_validation_rejects_empty_and_separators() {
287 for good in ["aaron", "sno-box", "repo.name", "a_b"] {
288 assert!(SessionPath::validate_component(good).is_ok(), "expected `{good}` to be accepted");
289 }
290 for bad in ["", "a/b", "/", "a/", "/b"] {
291 assert!(SessionPath::validate_component(bad).is_err(), "expected `{bad}` to be rejected");
292 }
293 }
294
295 #[test]
296 fn visibility_round_trips_through_its_wire_token() {
297 for tier in [Visibility::Public, Visibility::Unlisted, Visibility::Private] {
298 assert_eq!(tier.as_str().parse::<Visibility>().unwrap(), tier);
299 }
300 assert!("bogus".parse::<Visibility>().is_err());
301 }
302}