use crate::types::string::AtStrError;
use crate::types::{DISALLOWED_TLDS, ends_with};
use crate::{CowStr, IntoStatic};
use alloc::string::{String, ToString};
use core::fmt;
use core::ops::Deref;
use core::str::FromStr;
#[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
use regex::Regex;
#[cfg(all(not(target_arch = "wasm32"), not(feature = "std")))]
use regex_automata::meta::Regex;
#[cfg(target_arch = "wasm32")]
use regex_lite::Regex;
use serde::{Deserialize, Deserializer, Serialize, de::Error};
use smol_str::{SmolStr, StrExt};
use super::Lazy;
#[derive(Clone, PartialEq, Eq, Serialize, Hash)]
#[serde(transparent)]
#[repr(transparent)]
pub struct Handle<'h>(pub(crate) CowStr<'h>);
pub static HANDLE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$").unwrap()
});
impl<'h> Handle<'h> {
pub fn new(handle: &'h str) -> Result<Self, AtStrError> {
if handle.contains(|c: char| c.is_ascii_uppercase()) {
return Self::new_owned(handle);
}
let stripped = handle
.strip_prefix("at://")
.or_else(|| handle.strip_prefix('@'))
.unwrap_or(handle);
if stripped.len() > 253 {
Err(AtStrError::too_long(
"handle",
stripped,
253,
stripped.len(),
))
} else if !HANDLE_REGEX.is_match(stripped) {
Err(AtStrError::regex(
"handle",
stripped,
SmolStr::new_static("invalid"),
))
} else if ends_with(stripped, DISALLOWED_TLDS) {
if handle == "handle.invalid" {
Ok(Self(CowStr::Borrowed(stripped)))
} else {
Err(AtStrError::disallowed("handle", stripped, DISALLOWED_TLDS))
}
} else {
Ok(Self(CowStr::Borrowed(stripped)))
}
}
pub fn is_valid(&self) -> bool {
self.0.len() <= 253
&& HANDLE_REGEX.is_match(&self.0)
&& !ends_with(&self.0, DISALLOWED_TLDS)
}
pub fn new_owned(handle: impl AsRef<str>) -> Result<Self, AtStrError> {
let handle = handle.as_ref();
let stripped = handle
.strip_prefix("at://")
.or_else(|| handle.strip_prefix('@'))
.unwrap_or(handle);
let normalized = stripped.to_lowercase_smolstr();
let handle = normalized.as_str();
if handle.len() > 253 {
Err(AtStrError::too_long("handle", handle, 253, handle.len()))
} else if !HANDLE_REGEX.is_match(handle) {
Err(AtStrError::regex(
"handle",
handle,
SmolStr::new_static("invalid"),
))
} else if ends_with(handle, DISALLOWED_TLDS) {
if handle == "handle.invalid" {
Ok(Self(CowStr::Owned(normalized)))
} else {
Err(AtStrError::disallowed(
"handle",
normalized.as_str(),
DISALLOWED_TLDS,
))
}
} else {
Ok(Self(CowStr::Owned(normalized)))
}
}
pub fn new_static(handle: &'static str) -> Result<Self, AtStrError> {
let stripped = handle
.strip_prefix("at://")
.or_else(|| handle.strip_prefix('@'))
.unwrap_or(handle);
let handle = if handle.contains(|c: char| c.is_ascii_uppercase()) {
stripped.to_lowercase_smolstr()
} else {
SmolStr::new_static(stripped)
};
if handle.len() > 253 {
Err(AtStrError::too_long("handle", &handle, 253, handle.len()))
} else if !HANDLE_REGEX.is_match(&handle) {
Err(AtStrError::regex(
"handle",
&handle,
SmolStr::new_static("invalid"),
))
} else if ends_with(&handle, DISALLOWED_TLDS) {
if handle == "handle.invalid" {
Ok(Self(CowStr::Owned(handle)))
} else {
Err(AtStrError::disallowed("handle", stripped, DISALLOWED_TLDS))
}
} else {
Ok(Self(CowStr::Owned(handle)))
}
}
pub fn new_cow(handle: CowStr<'h>) -> Result<Self, AtStrError> {
if handle.contains(|c: char| c.is_ascii_uppercase()) {
return Self::new_owned(handle);
}
let handle = if let Some(stripped) = handle.strip_prefix("at://") {
CowStr::copy_from_str(stripped)
} else if let Some(stripped) = handle.strip_prefix('@') {
CowStr::copy_from_str(stripped)
} else {
handle
};
if handle.len() > 253 {
Err(AtStrError::too_long("handle", &handle, 253, handle.len()))
} else if !HANDLE_REGEX.is_match(&handle) {
Err(AtStrError::regex(
"handle",
&handle,
SmolStr::new_static("invalid"),
))
} else if ends_with(&handle, DISALLOWED_TLDS) {
if handle == "handle.invalid" {
Ok(Self(handle))
} else {
Err(AtStrError::disallowed(
"handle",
handle.as_str(),
DISALLOWED_TLDS,
))
}
} else {
Ok(Self(handle))
}
}
pub fn raw(handle: &'h str) -> Self {
if handle.contains(|c: char| c.is_ascii_uppercase()) {
return Self::new_owned(handle).expect("Invalid handle");
}
let stripped = handle
.strip_prefix("at://")
.or_else(|| handle.strip_prefix('@'))
.unwrap_or(handle);
let handle = stripped;
if handle.len() > 253 {
panic!("handle too long")
} else if !HANDLE_REGEX.is_match(handle) {
panic!("Invalid handle")
} else if ends_with(handle, DISALLOWED_TLDS) {
if handle == "handle.invalid" {
Self(CowStr::Borrowed(stripped))
} else {
panic!("top-level domain not allowed in handles")
}
} else {
Self(CowStr::Borrowed(handle))
}
}
pub unsafe fn unchecked(handle: &'h str) -> Self {
let stripped = handle
.strip_prefix("at://")
.or_else(|| handle.strip_prefix('@'))
.unwrap_or(handle);
if stripped.contains(|c: char| c.is_ascii_uppercase()) {
return Self(CowStr::Owned(stripped.to_lowercase_smolstr()));
}
Self(CowStr::Borrowed(stripped))
}
pub fn as_str(&self) -> &str {
{
let this = &self.0;
this
}
}
}
impl FromStr for Handle<'_> {
type Err = AtStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new_owned(s)
}
}
impl IntoStatic for Handle<'_> {
type Output = Handle<'static>;
fn into_static(self) -> Self::Output {
Handle(self.0.into_static())
}
}
impl<'de, 'a> Deserialize<'de> for Handle<'a>
where
'de: 'a,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Deserialize::deserialize(deserializer)?;
Self::new_cow(value).map_err(D::Error::custom)
}
}
impl fmt::Display for Handle<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl fmt::Debug for Handle<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "at://{}", self.0)
}
}
impl<'h> From<Handle<'h>> for String {
fn from(value: Handle<'h>) -> Self {
value.0.to_string()
}
}
impl<'h> From<Handle<'h>> for CowStr<'h> {
fn from(value: Handle<'h>) -> Self {
value.0
}
}
impl From<String> for Handle<'static> {
fn from(value: String) -> Self {
Self::new_owned(value).unwrap()
}
}
impl<'h> From<CowStr<'h>> for Handle<'h> {
fn from(value: CowStr<'h>) -> Self {
Self::new_owned(value).unwrap()
}
}
impl AsRef<str> for Handle<'_> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Deref for Handle<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_handles() {
assert!(Handle::new("alice.test").is_ok());
assert!(Handle::new("foo.bsky.social").is_ok());
assert!(Handle::new("a.b.c.d.e").is_ok());
assert!(Handle::new("a1.b2.c3").is_ok());
assert!(Handle::new("name-with-dash.com").is_ok());
}
#[test]
fn prefix_stripping() {
assert_eq!(Handle::new("@alice.test").unwrap().as_str(), "alice.test");
assert_eq!(
Handle::new("at://alice.test").unwrap().as_str(),
"alice.test"
);
assert_eq!(Handle::new("alice.test").unwrap().as_str(), "alice.test");
}
#[test]
fn max_length() {
let s1 = format!("a{}a", "b".repeat(61)); let s2 = format!("c{}c", "d".repeat(61)); let s3 = format!("e{}e", "f".repeat(61)); let s4 = format!("g{}g", "h".repeat(59)); let valid_253 = format!("{}.{}.{}.{}", s1, s2, s3, s4);
assert_eq!(valid_253.len(), 253);
assert!(Handle::new(&valid_253).is_ok());
let s4_long = format!("g{}g", "h".repeat(60)); let too_long_254 = format!("{}.{}.{}.{}", s1, s2, s3, s4_long);
assert_eq!(too_long_254.len(), 254);
assert!(Handle::new(&too_long_254).is_err());
}
#[test]
fn segment_length_constraints() {
let valid_63_char_segment = format!("{}.com", "a".repeat(63));
assert!(Handle::new(&valid_63_char_segment).is_ok());
let too_long_64_char_segment = format!("{}.com", "a".repeat(64));
assert!(Handle::new(&too_long_64_char_segment).is_err());
}
#[test]
fn hyphen_placement() {
assert!(Handle::new("valid-label.com").is_ok());
assert!(Handle::new("-nope.com").is_err());
assert!(Handle::new("nope-.com").is_err());
}
#[test]
fn tld_must_start_with_letter() {
assert!(Handle::new("foo.bar").is_ok());
assert!(Handle::new("foo.9bar").is_err());
}
#[test]
fn disallowed_tlds() {
assert!(Handle::new("foo.local").is_err());
assert!(Handle::new("foo.localhost").is_err());
assert!(Handle::new("foo.arpa").is_err());
assert!(Handle::new("foo.invalid").is_err());
assert!(Handle::new("foo.internal").is_err());
assert!(Handle::new("foo.example").is_err());
assert!(Handle::new("foo.alt").is_err());
assert!(Handle::new("foo.onion").is_err());
}
#[test]
fn minimum_segments() {
assert!(Handle::new("a.b").is_ok());
assert!(Handle::new("a").is_err());
assert!(Handle::new("com").is_err());
}
#[test]
fn invalid_characters() {
assert!(Handle::new("foo!bar.com").is_err());
assert!(Handle::new("foo_bar.com").is_err());
assert!(Handle::new("foo bar.com").is_err());
assert!(Handle::new("foo@bar.com").is_err());
}
#[test]
fn empty_segments() {
assert!(Handle::new("foo..com").is_err());
assert!(Handle::new(".foo.com").is_err());
assert!(Handle::new("foo.com.").is_err());
}
}