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 struct Constant;
22
23impl Constant {
24 pub const CHALLENGE_SIZE: usize = 32;
26 pub const CONFIG_DIR_NAME: &'static str = "conclave";
29 pub const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
32 pub const PROTOCOL_VERSION: u32 = 1;
35 pub const SESSION_PATH_SEPARATOR: char = '/';
37}
38
39#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize)]
45#[serde(rename_all = "lowercase")]
46pub enum PermissionLevel {
47 Mute,
49 #[default]
51 Notify,
52 Converse,
54 Act,
56}
57
58impl PermissionLevel {
59 #[must_use]
64 pub const fn may_emit(self) -> bool {
65 matches!(self, Self::Converse | Self::Act)
66 }
67}
68
69#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
71#[error("unknown permission level `{0}` (expected mute, notify, converse, or act)")]
72pub struct ParsePermissionError(pub String);
73
74impl FromStr for PermissionLevel {
75 type Err = ParsePermissionError;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 match s {
79 "mute" => Ok(Self::Mute),
80 "notify" => Ok(Self::Notify),
81 "converse" => Ok(Self::Converse),
82 "act" => Ok(Self::Act),
83 other => Err(ParsePermissionError(other.to_owned())),
84 }
85 }
86}
87
88#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
90#[serde(rename_all = "lowercase")]
91pub enum Visibility {
92 Public,
94 Unlisted,
96 Private,
98}
99
100impl Visibility {
101 #[must_use]
103 pub const fn as_str(self) -> &'static str {
104 match self {
105 Self::Public => "public",
106 Self::Unlisted => "unlisted",
107 Self::Private => "private",
108 }
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
114#[error("unknown visibility tier `{0}` (expected public, unlisted, or private)")]
115pub struct ParseVisibilityError(pub String);
116
117impl FromStr for Visibility {
118 type Err = ParseVisibilityError;
119
120 fn from_str(s: &str) -> Result<Self, Self::Err> {
121 match s {
122 "public" => Ok(Self::Public),
123 "unlisted" => Ok(Self::Unlisted),
124 "private" => Ok(Self::Private),
125 other => Err(ParseVisibilityError(other.to_owned())),
126 }
127 }
128}
129
130#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
136pub struct SessionPath {
137 pub user: String,
139 pub machine: String,
141 pub session: String,
143}
144
145impl SessionPath {
146 #[must_use]
148 pub fn new(user: impl Into<String>, machine: impl Into<String>, session: impl Into<String>) -> Self {
149 Self {
150 user: user.into(),
151 machine: machine.into(),
152 session: session.into(),
153 }
154 }
155}
156
157impl fmt::Display for SessionPath {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 let sep = Constant::SESSION_PATH_SEPARATOR;
160 write!(f, "{}{sep}{}{sep}{}", self.user, self.machine, self.session)
161 }
162}
163
164#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
166pub enum ParsePathError {
167 #[error("session path must be `user/machine/session`, got `{0}`")]
169 Malformed(String),
170}
171
172impl FromStr for SessionPath {
173 type Err = ParsePathError;
174
175 fn from_str(s: &str) -> Result<Self, Self::Err> {
176 let mut parts = s.split(Constant::SESSION_PATH_SEPARATOR);
177 let (Some(user), Some(machine), Some(session), None) = (parts.next(), parts.next(), parts.next(), parts.next()) else {
178 return Err(ParsePathError::Malformed(s.to_owned()));
179 };
180
181 if user.is_empty() || machine.is_empty() || session.is_empty() {
182 return Err(ParsePathError::Malformed(s.to_owned()));
183 }
184
185 Ok(Self::new(user, machine, session))
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 #![allow(clippy::unwrap_used)]
193
194 use super::*;
195 use pretty_assertions::assert_eq;
196
197 #[test]
198 fn permission_levels_order_by_ascending_autonomy() {
199 assert!(PermissionLevel::Mute < PermissionLevel::Notify);
200 assert!(PermissionLevel::Notify < PermissionLevel::Converse);
201 assert!(PermissionLevel::Converse < PermissionLevel::Act);
202 }
203
204 #[test]
205 fn default_permission_level_is_notify() {
206 assert_eq!(PermissionLevel::default(), PermissionLevel::Notify);
207 }
208
209 #[test]
210 fn only_converse_and_above_may_emit() {
211 assert!(!PermissionLevel::Mute.may_emit());
212 assert!(!PermissionLevel::Notify.may_emit());
213 assert!(PermissionLevel::Converse.may_emit());
214 assert!(PermissionLevel::Act.may_emit());
215 }
216
217 #[test]
218 fn permission_level_parses_from_its_lowercase_token() {
219 for (token, level) in [
220 ("mute", PermissionLevel::Mute),
221 ("notify", PermissionLevel::Notify),
222 ("converse", PermissionLevel::Converse),
223 ("act", PermissionLevel::Act),
224 ] {
225 assert_eq!(token.parse::<PermissionLevel>().unwrap(), level);
226 }
227 assert!("bogus".parse::<PermissionLevel>().is_err());
228 }
229
230 #[test]
231 fn session_path_displays_as_slash_separated_triple() {
232 let path = SessionPath::new("aaron", "workstation", "razel");
233 assert_eq!(path.to_string(), "aaron/workstation/razel");
234 }
235
236 #[test]
237 fn session_path_parses_a_slash_separated_triple() {
238 let path: SessionPath = "aaron/workstation/razel".parse().unwrap();
239 assert_eq!(path, SessionPath::new("aaron", "workstation", "razel"));
240 }
241
242 #[test]
243 fn session_path_round_trips_through_display_and_parse() {
244 let path = SessionPath::new("aaron", "sno-box", "dotagent");
245 assert_eq!(path.to_string().parse::<SessionPath>().unwrap(), path);
246 }
247
248 #[test]
249 fn session_path_rejects_malformed_strings() {
250 for bad in ["", "a", "a/b", "a/b/c/d", "a//c", "/b/c", "a/b/"] {
251 assert!(bad.parse::<SessionPath>().is_err(), "expected `{bad}` to be rejected");
252 }
253 }
254
255 #[test]
256 fn visibility_round_trips_through_its_wire_token() {
257 for tier in [Visibility::Public, Visibility::Unlisted, Visibility::Private] {
258 assert_eq!(tier.as_str().parse::<Visibility>().unwrap(), tier);
259 }
260 assert!("bogus".parse::<Visibility>().is_err());
261 }
262}