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
219pub fn parse_duration_secs(value: &str) -> Res<u64> {
226 use anyhow::Context as _;
227 let value = value.trim();
228 let (digits, mult) = match value.chars().last() {
229 Some('s') => (&value[..value.len() - 1], 1),
230 Some('m') => (&value[..value.len() - 1], 60),
231 Some('h') => (&value[..value.len() - 1], 3600),
232 Some('d') => (&value[..value.len() - 1], 86_400),
233 _ => (value, 1),
234 };
235 let count: u64 = digits.trim().parse().with_context(|| format!("invalid duration `{value}`"))?;
236 Ok(count * mult)
237}
238
239#[cfg(test)]
240mod tests {
241 #![allow(clippy::unwrap_used)]
243
244 use super::*;
245 use pretty_assertions::assert_eq;
246
247 #[test]
248 fn permission_levels_order_by_ascending_autonomy() {
249 assert!(PermissionLevel::Mute < PermissionLevel::Notify);
250 assert!(PermissionLevel::Notify < PermissionLevel::Converse);
251 assert!(PermissionLevel::Converse < PermissionLevel::Act);
252 }
253
254 #[test]
255 fn default_permission_level_is_notify() {
256 assert_eq!(PermissionLevel::default(), PermissionLevel::Notify);
257 }
258
259 #[test]
260 fn only_converse_and_above_may_emit() {
261 assert!(!PermissionLevel::Mute.may_emit());
262 assert!(!PermissionLevel::Notify.may_emit());
263 assert!(PermissionLevel::Converse.may_emit());
264 assert!(PermissionLevel::Act.may_emit());
265 }
266
267 #[test]
268 fn permission_level_parses_from_its_lowercase_token() {
269 for (token, level) in [
270 ("mute", PermissionLevel::Mute),
271 ("notify", PermissionLevel::Notify),
272 ("converse", PermissionLevel::Converse),
273 ("act", PermissionLevel::Act),
274 ] {
275 assert_eq!(token.parse::<PermissionLevel>().unwrap(), level);
276 }
277 assert!("bogus".parse::<PermissionLevel>().is_err());
278 }
279
280 #[test]
281 fn session_path_displays_as_slash_separated_triple() {
282 let path = SessionPath::new("aaron", "workstation", "razel");
283 assert_eq!(path.to_string(), "aaron/workstation/razel");
284 }
285
286 #[test]
287 fn session_path_parses_a_slash_separated_triple() {
288 let path: SessionPath = "aaron/workstation/razel".parse().unwrap();
289 assert_eq!(path, SessionPath::new("aaron", "workstation", "razel"));
290 }
291
292 #[test]
293 fn session_path_round_trips_through_display_and_parse() {
294 let path = SessionPath::new("aaron", "sno-box", "dotagent");
295 assert_eq!(path.to_string().parse::<SessionPath>().unwrap(), path);
296 }
297
298 #[test]
299 fn session_path_rejects_malformed_strings() {
300 for bad in ["", "a", "a/b", "a/b/c/d", "a//c", "/b/c", "a/b/"] {
301 assert!(bad.parse::<SessionPath>().is_err(), "expected `{bad}` to be rejected");
302 }
303 }
304
305 #[test]
306 fn session_path_component_validation_rejects_empty_and_separators() {
307 for good in ["aaron", "sno-box", "repo.name", "a_b"] {
308 assert!(SessionPath::validate_component(good).is_ok(), "expected `{good}` to be accepted");
309 }
310 for bad in ["", "a/b", "/", "a/", "/b"] {
311 assert!(SessionPath::validate_component(bad).is_err(), "expected `{bad}` to be rejected");
312 }
313 }
314
315 #[test]
316 fn visibility_round_trips_through_its_wire_token() {
317 for tier in [Visibility::Public, Visibility::Unlisted, Visibility::Private] {
318 assert_eq!(tier.as_str().parse::<Visibility>().unwrap(), tier);
319 }
320 assert!("bogus".parse::<Visibility>().is_err());
321 }
322}