pub const MAX_LAYER_NAME: usize = 24;
pub const PARTICIPANT_PREFIX: &str = "urn:collab:participant:";
pub const HOST_CLIENT_PREFIX: &str = "urn:iki:host:";
pub const MAX_SHORT_NAME: usize = 48;
pub const NEW_SUBJECT: &str = "urn:layer:new";
fn is_word(name: &str, max: usize) -> bool {
!name.is_empty()
&& name.len() <= max
&& name
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
&& !name.starts_with('-')
&& !name.ends_with('-')
&& !is_windows_device(name)
}
pub fn is_windows_device(name: &str) -> bool {
let stem = name
.split('.')
.next()
.unwrap_or_default()
.trim_end_matches(' ');
let lower = stem.to_lowercase();
if matches!(
lower.as_str(),
"con" | "prn" | "aux" | "nul" | "conin$" | "conout$"
) {
return true;
}
let mut chars = lower.chars();
let prefix: String = chars.by_ref().take(3).collect();
let rest: Vec<char> = chars.collect();
(prefix == "com" || prefix == "lpt")
&& rest.len() == 1
&& (rest[0].is_ascii_digit() || matches!(rest[0], '\u{b9}' | '\u{b2}' | '\u{b3}'))
}
pub fn is_plain_file_name(name: &str) -> bool {
!name.is_empty()
&& !name.starts_with('.')
&& !name.ends_with('.')
&& !name.ends_with(' ')
&& !name.chars().any(|c| {
c.is_control() || matches!(c, '/' | '\\' | ':' | '<' | '>' | '"' | '|' | '?' | '*')
})
&& !is_windows_device(name)
}
pub fn is_layer_name(name: &str) -> bool {
is_word(name, MAX_LAYER_NAME)
}
pub fn is_short_name(name: &str) -> bool {
is_word(name, MAX_SHORT_NAME)
}
pub fn layer_iri(layer: &str) -> String {
format!("urn:layer:{layer}")
}
pub fn collab_iri(name: &str) -> String {
format!("urn:collab:{name}")
}
pub fn layer_cap(verb: &str, layer: &str) -> String {
format!("urn:cap:layer:{verb}:{layer}")
}
pub fn collab_cap(name: &str) -> String {
format!("urn:cap:collab:read:{name}")
}
pub fn endpoint_id(layer: &str, action: &str) -> String {
format!("layer-{layer}-{action}")
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Participant {
iri: String,
slug: String,
fingerprint: Option<String>,
}
impl Participant {
pub fn parse(iri: &str) -> Result<Participant, String> {
if let Some(slug) = iri.strip_prefix(PARTICIPANT_PREFIX) {
if !is_short_name(slug) {
return Err(format!(
"`{iri}`: a participant's short name is lowercase [a-z0-9-], at most \
{MAX_SHORT_NAME} characters, and no Windows device name"
));
}
return Ok(Participant {
iri: iri.to_string(),
slug: slug.to_string(),
fingerprint: None,
});
}
if let Some(rest) = iri.strip_prefix(HOST_CLIENT_PREFIX) {
if let Some((host, fp)) = rest.split_once(":client:") {
let hex = fp.len() == 64
&& fp
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
if is_short_name(host) && hex {
return Ok(Participant {
iri: iri.to_string(),
slug: format!("client.{host}.{fp}"),
fingerprint: Some(fp.to_string()),
});
}
}
return Err(format!(
"`{iri}`: a host client is urn:iki:host:{{host}}:client:<64 lowercase hex>"
));
}
Err(format!(
"`{iri}`: a participant is {PARTICIPANT_PREFIX}{{name}} or \
{HOST_CLIENT_PREFIX}{{host}}:client:<fingerprint>"
))
}
pub fn from_manifest_key(key: &str) -> Result<Participant, String> {
if key.contains(':') {
Participant::parse(key)
} else {
Participant::parse(&format!("{PARTICIPANT_PREFIX}{key}"))
}
}
pub fn iri(&self) -> &str {
&self.iri
}
pub fn slug(&self) -> &str {
&self.slug
}
pub fn fingerprint(&self) -> Option<&str> {
self.fingerprint.as_deref()
}
}
pub fn fold_fingerprint(fp: &str) -> Option<String> {
let folded: String = fp
.chars()
.filter(|c| *c != ':' && !c.is_whitespace())
.map(|c| c.to_ascii_lowercase())
.collect();
(folded.len() == 64 && folded.bytes().all(|b| b.is_ascii_hexdigit())).then_some(folded)
}