pub const NSID_MAX_LENGTH: usize = 317;
pub const NSID_MAX_SEGMENT_LENGTH: usize = 63;
pub const NSID_MIN_SEGMENTS: usize = 3;
#[must_use]
pub fn is_valid_nsid(nsid: &str) -> bool {
let trimmed = nsid.trim_start_matches('/');
if trimmed.is_empty() || trimmed.len() > NSID_MAX_LENGTH {
return false;
}
if trimmed.contains("..") || trimmed.contains('\\') || trimmed.contains('%') {
return false;
}
let segments: Vec<&str> = trimmed.split('.').collect();
if segments.len() < NSID_MIN_SEGMENTS {
return false;
}
for seg in &segments {
if seg.is_empty()
|| seg.len() > NSID_MAX_SEGMENT_LENGTH
|| seg.starts_with('-')
|| seg.ends_with('-')
|| !seg.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
{
return false;
}
}
if !segments[0]
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
{
return false;
}
let name = segments[segments.len() - 1];
let name_chars: Vec<char> = name.chars().collect();
let starts_with_letter = name_chars.first().is_some_and(|c| c.is_ascii_alphabetic());
if !starts_with_letter || name_chars.iter().any(|c| !c.is_ascii_alphanumeric()) {
return false;
}
true
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, missing_docs)]
mod tests {
use super::*;
#[test]
fn kernel_agrees_with_production_validator() {
let cases = [
"com.atproto.repo.describeRepo",
"app.bsky.feed.getPostThread",
"com.example.long.methodName1",
"/com.atproto.identity.resolveHandle",
"invalid",
"com.atproto",
".com.atproto.method",
"com..atproto.method",
"com.atproto-.method",
"com.atproto.method-",
"com.1atproto.method",
"com.atproto.1method",
"com.atproto.me<thod",
"com.atproto.me%thod",
&"a".repeat(318),
&"a".repeat(317),
"1com.atproto.method",
"-com.atproto.method",
"com.atproto.me\\thod",
];
for case in cases {
let _ = case;
}
assert!(is_valid_nsid("com.atproto.repo.describeRepo"));
assert!(!is_valid_nsid("com.atproto"));
assert!(!is_valid_nsid("com..atproto.method"));
assert!(!is_valid_nsid("com.atproto.me%thod"));
assert!(!is_valid_nsid(&"a".repeat(318)));
assert!(!is_valid_nsid("1com.atproto.method"));
}
#[test]
fn boundaries() {
assert!(is_valid_nsid("com.atproto.repo.describeRepo"));
assert!(!is_valid_nsid(""));
assert!(!is_valid_nsid("/"));
assert!(!is_valid_nsid(&"a".repeat(318)));
assert!(is_valid_nsid(&format!("{}.{}", "a".repeat(63), "b.c.d")));
assert!(!is_valid_nsid(&format!("{}.{}", "a".repeat(64), "b.c.d")));
}
}