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 SESSION_PATH_SEPARATOR: char = '/';
49}
50
51#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum PermissionLevel {
59 Mute,
61 #[default]
63 Notify,
64 Converse,
66 Act,
68}
69
70impl PermissionLevel {
71 #[must_use]
76 pub const fn may_emit(self) -> bool {
77 matches!(self, Self::Converse | Self::Act)
78 }
79}
80
81#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
83#[error("unknown permission level `{0}` (expected mute, notify, converse, or act)")]
84pub struct ParsePermissionError(pub String);
85
86impl FromStr for PermissionLevel {
87 type Err = ParsePermissionError;
88
89 fn from_str(s: &str) -> Result<Self, Self::Err> {
90 match s {
91 "mute" => Ok(Self::Mute),
92 "notify" => Ok(Self::Notify),
93 "converse" => Ok(Self::Converse),
94 "act" => Ok(Self::Act),
95 other => Err(ParsePermissionError(other.to_owned())),
96 }
97 }
98}
99
100#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
102#[serde(rename_all = "lowercase")]
103pub enum Visibility {
104 Public,
106 Unlisted,
108 Private,
110}
111
112impl Visibility {
113 #[must_use]
115 pub const fn as_str(self) -> &'static str {
116 match self {
117 Self::Public => "public",
118 Self::Unlisted => "unlisted",
119 Self::Private => "private",
120 }
121 }
122}
123
124#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
126#[error("unknown visibility tier `{0}` (expected public, unlisted, or private)")]
127pub struct ParseVisibilityError(pub String);
128
129impl FromStr for Visibility {
130 type Err = ParseVisibilityError;
131
132 fn from_str(s: &str) -> Result<Self, Self::Err> {
133 match s {
134 "public" => Ok(Self::Public),
135 "unlisted" => Ok(Self::Unlisted),
136 "private" => Ok(Self::Private),
137 other => Err(ParseVisibilityError(other.to_owned())),
138 }
139 }
140}
141
142#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
148pub struct SessionPath {
149 pub user: String,
151 pub machine: String,
153 pub session: String,
155}
156
157impl SessionPath {
158 #[must_use]
160 pub fn new(user: impl Into<String>, machine: impl Into<String>, session: impl Into<String>) -> Self {
161 Self {
162 user: user.into(),
163 machine: machine.into(),
164 session: session.into(),
165 }
166 }
167
168 pub fn validate_component(component: &str) -> Result<(), ParsePathError> {
176 if component.is_empty() || component.contains(Constant::SESSION_PATH_SEPARATOR) {
177 return Err(ParsePathError::Malformed(component.to_owned()));
178 }
179 Ok(())
180 }
181}
182
183impl fmt::Display for SessionPath {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 let sep = Constant::SESSION_PATH_SEPARATOR;
186 write!(f, "{}{sep}{}{sep}{}", self.user, self.machine, self.session)
187 }
188}
189
190#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
192pub enum ParsePathError {
193 #[error("session path must be `user/machine/session`, got `{0}`")]
195 Malformed(String),
196}
197
198impl FromStr for SessionPath {
199 type Err = ParsePathError;
200
201 fn from_str(s: &str) -> Result<Self, Self::Err> {
202 let mut parts = s.split(Constant::SESSION_PATH_SEPARATOR);
203 let (Some(user), Some(machine), Some(session), None) = (parts.next(), parts.next(), parts.next(), parts.next()) else {
204 return Err(ParsePathError::Malformed(s.to_owned()));
205 };
206
207 if user.is_empty() || machine.is_empty() || session.is_empty() {
208 return Err(ParsePathError::Malformed(s.to_owned()));
209 }
210
211 Ok(Self::new(user, machine, session))
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 #![allow(clippy::unwrap_used)]
219
220 use super::*;
221 use pretty_assertions::assert_eq;
222
223 #[test]
224 fn permission_levels_order_by_ascending_autonomy() {
225 assert!(PermissionLevel::Mute < PermissionLevel::Notify);
226 assert!(PermissionLevel::Notify < PermissionLevel::Converse);
227 assert!(PermissionLevel::Converse < PermissionLevel::Act);
228 }
229
230 #[test]
231 fn default_permission_level_is_notify() {
232 assert_eq!(PermissionLevel::default(), PermissionLevel::Notify);
233 }
234
235 #[test]
236 fn only_converse_and_above_may_emit() {
237 assert!(!PermissionLevel::Mute.may_emit());
238 assert!(!PermissionLevel::Notify.may_emit());
239 assert!(PermissionLevel::Converse.may_emit());
240 assert!(PermissionLevel::Act.may_emit());
241 }
242
243 #[test]
244 fn permission_level_parses_from_its_lowercase_token() {
245 for (token, level) in [
246 ("mute", PermissionLevel::Mute),
247 ("notify", PermissionLevel::Notify),
248 ("converse", PermissionLevel::Converse),
249 ("act", PermissionLevel::Act),
250 ] {
251 assert_eq!(token.parse::<PermissionLevel>().unwrap(), level);
252 }
253 assert!("bogus".parse::<PermissionLevel>().is_err());
254 }
255
256 #[test]
257 fn session_path_displays_as_slash_separated_triple() {
258 let path = SessionPath::new("aaron", "workstation", "razel");
259 assert_eq!(path.to_string(), "aaron/workstation/razel");
260 }
261
262 #[test]
263 fn session_path_parses_a_slash_separated_triple() {
264 let path: SessionPath = "aaron/workstation/razel".parse().unwrap();
265 assert_eq!(path, SessionPath::new("aaron", "workstation", "razel"));
266 }
267
268 #[test]
269 fn session_path_round_trips_through_display_and_parse() {
270 let path = SessionPath::new("aaron", "sno-box", "dotagent");
271 assert_eq!(path.to_string().parse::<SessionPath>().unwrap(), path);
272 }
273
274 #[test]
275 fn session_path_rejects_malformed_strings() {
276 for bad in ["", "a", "a/b", "a/b/c/d", "a//c", "/b/c", "a/b/"] {
277 assert!(bad.parse::<SessionPath>().is_err(), "expected `{bad}` to be rejected");
278 }
279 }
280
281 #[test]
282 fn session_path_component_validation_rejects_empty_and_separators() {
283 for good in ["aaron", "sno-box", "repo.name", "a_b"] {
284 assert!(SessionPath::validate_component(good).is_ok(), "expected `{good}` to be accepted");
285 }
286 for bad in ["", "a/b", "/", "a/", "/b"] {
287 assert!(SessionPath::validate_component(bad).is_err(), "expected `{bad}` to be rejected");
288 }
289 }
290
291 #[test]
292 fn visibility_round_trips_through_its_wire_token() {
293 for tier in [Visibility::Public, Visibility::Unlisted, Visibility::Private] {
294 assert_eq!(tier.as_str().parse::<Visibility>().unwrap(), tier);
295 }
296 assert!("bogus".parse::<Visibility>().is_err());
297 }
298}