use std::{fmt, str::FromStr};
use serde::{Deserialize, Serialize};
pub type Err = anyhow::Error;
pub type Res<T> = anyhow::Result<T, Err>;
pub type Void = Res<()>;
pub(crate) fn ensure_tls_provider() {
use std::sync::Once;
static INSTALL: Once = Once::new();
INSTALL.call_once(|| {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
});
}
pub struct Constant;
impl Constant {
pub const CHALLENGE_SIZE: usize = 32;
pub const CONFIG_DIR_NAME: &'static str = "conclave";
pub const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
pub const PROTOCOL_VERSION: u32 = 1;
pub const SERVER_ID_HEADER: &'static str = "x-conclave-server-id";
pub const SESSION_PATH_SEPARATOR: char = '/';
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PermissionLevel {
Mute,
#[default]
Notify,
Converse,
Act,
}
impl PermissionLevel {
#[must_use]
pub const fn may_emit(self) -> bool {
matches!(self, Self::Converse | Self::Act)
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("unknown permission level `{0}` (expected mute, notify, converse, or act)")]
pub struct ParsePermissionError(pub String);
impl FromStr for PermissionLevel {
type Err = ParsePermissionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"mute" => Ok(Self::Mute),
"notify" => Ok(Self::Notify),
"converse" => Ok(Self::Converse),
"act" => Ok(Self::Act),
other => Err(ParsePermissionError(other.to_owned())),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Visibility {
Public,
Unlisted,
Private,
}
impl Visibility {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Public => "public",
Self::Unlisted => "unlisted",
Self::Private => "private",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("unknown visibility tier `{0}` (expected public, unlisted, or private)")]
pub struct ParseVisibilityError(pub String);
impl FromStr for Visibility {
type Err = ParseVisibilityError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"public" => Ok(Self::Public),
"unlisted" => Ok(Self::Unlisted),
"private" => Ok(Self::Private),
other => Err(ParseVisibilityError(other.to_owned())),
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub struct SessionPath {
pub user: String,
pub machine: String,
pub session: String,
}
impl SessionPath {
#[must_use]
pub fn new(user: impl Into<String>, machine: impl Into<String>, session: impl Into<String>) -> Self {
Self {
user: user.into(),
machine: machine.into(),
session: session.into(),
}
}
pub fn validate_component(component: &str) -> Result<(), ParsePathError> {
if component.is_empty() || component.contains(Constant::SESSION_PATH_SEPARATOR) {
return Err(ParsePathError::Malformed(component.to_owned()));
}
Ok(())
}
}
impl fmt::Display for SessionPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sep = Constant::SESSION_PATH_SEPARATOR;
write!(f, "{}{sep}{}{sep}{}", self.user, self.machine, self.session)
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ParsePathError {
#[error("session path must be `user/machine/session`, got `{0}`")]
Malformed(String),
}
impl FromStr for SessionPath {
type Err = ParsePathError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(Constant::SESSION_PATH_SEPARATOR);
let (Some(user), Some(machine), Some(session), None) = (parts.next(), parts.next(), parts.next(), parts.next()) else {
return Err(ParsePathError::Malformed(s.to_owned()));
};
if user.is_empty() || machine.is_empty() || session.is_empty() {
return Err(ParsePathError::Malformed(s.to_owned()));
}
Ok(Self::new(user, machine, session))
}
}
pub fn parse_duration_secs(value: &str) -> Res<u64> {
use anyhow::Context as _;
let value = value.trim();
let (digits, mult) = match value.chars().last() {
Some('s') => (&value[..value.len() - 1], 1),
Some('m') => (&value[..value.len() - 1], 60),
Some('h') => (&value[..value.len() - 1], 3600),
Some('d') => (&value[..value.len() - 1], 86_400),
_ => (value, 1),
};
let count: u64 = digits.trim().parse().with_context(|| format!("invalid duration `{value}`"))?;
Ok(count * mult)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn permission_levels_order_by_ascending_autonomy() {
assert!(PermissionLevel::Mute < PermissionLevel::Notify);
assert!(PermissionLevel::Notify < PermissionLevel::Converse);
assert!(PermissionLevel::Converse < PermissionLevel::Act);
}
#[test]
fn default_permission_level_is_notify() {
assert_eq!(PermissionLevel::default(), PermissionLevel::Notify);
}
#[test]
fn only_converse_and_above_may_emit() {
assert!(!PermissionLevel::Mute.may_emit());
assert!(!PermissionLevel::Notify.may_emit());
assert!(PermissionLevel::Converse.may_emit());
assert!(PermissionLevel::Act.may_emit());
}
#[test]
fn permission_level_parses_from_its_lowercase_token() {
for (token, level) in [
("mute", PermissionLevel::Mute),
("notify", PermissionLevel::Notify),
("converse", PermissionLevel::Converse),
("act", PermissionLevel::Act),
] {
assert_eq!(token.parse::<PermissionLevel>().unwrap(), level);
}
assert!("bogus".parse::<PermissionLevel>().is_err());
}
#[test]
fn session_path_displays_as_slash_separated_triple() {
let path = SessionPath::new("aaron", "workstation", "razel");
assert_eq!(path.to_string(), "aaron/workstation/razel");
}
#[test]
fn session_path_parses_a_slash_separated_triple() {
let path: SessionPath = "aaron/workstation/razel".parse().unwrap();
assert_eq!(path, SessionPath::new("aaron", "workstation", "razel"));
}
#[test]
fn session_path_round_trips_through_display_and_parse() {
let path = SessionPath::new("aaron", "sno-box", "dotagent");
assert_eq!(path.to_string().parse::<SessionPath>().unwrap(), path);
}
#[test]
fn session_path_rejects_malformed_strings() {
for bad in ["", "a", "a/b", "a/b/c/d", "a//c", "/b/c", "a/b/"] {
assert!(bad.parse::<SessionPath>().is_err(), "expected `{bad}` to be rejected");
}
}
#[test]
fn session_path_component_validation_rejects_empty_and_separators() {
for good in ["aaron", "sno-box", "repo.name", "a_b"] {
assert!(SessionPath::validate_component(good).is_ok(), "expected `{good}` to be accepted");
}
for bad in ["", "a/b", "/", "a/", "/b"] {
assert!(SessionPath::validate_component(bad).is_err(), "expected `{bad}` to be rejected");
}
}
#[test]
fn visibility_round_trips_through_its_wire_token() {
for tier in [Visibility::Public, Visibility::Unlisted, Visibility::Private] {
assert_eq!(tier.as_str().parse::<Visibility>().unwrap(), tier);
}
assert!("bogus".parse::<Visibility>().is_err());
}
}