captcha_sdk/
provider_id.rs1use std::{borrow::Cow, error::Error, fmt};
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5pub const MAX_PROVIDER_ID_LEN: usize = 128;
7
8pub const MAX_PROVIDER_ID_SEGMENT_LEN: usize = 63;
10
11const RESERVED_AUTHORITIES: &[&str] = &[
12 "altcha",
13 "captchafox",
14 "cloudflare",
15 "friendlycaptcha",
16 "google",
17 "hcaptcha",
18 "mtcaptcha",
19 "prosopo",
20];
21
22const BUILTIN_IDS: &[&str] = &[
23 "altcha.custom",
24 "altcha.sentinel",
25 "captchafox.standard",
26 "cloudflare.turnstile",
27 "friendlycaptcha.v2",
28 "google.recaptcha.enterprise",
29 "google.recaptcha.v2",
30 "google.recaptcha.v3",
31 "hcaptcha.enterprise",
32 "hcaptcha.standard",
33 "mtcaptcha.standard",
34 "prosopo.procaptcha",
35];
36
37#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
44#[serde(transparent)]
45pub struct ProviderId(Cow<'static, str>);
46
47impl ProviderId {
48 pub const ALTCHA_CUSTOM: Self = Self::builtin("altcha.custom");
50 pub const ALTCHA_SENTINEL: Self = Self::builtin("altcha.sentinel");
52 pub const CAPTCHAFOX_STANDARD: Self = Self::builtin("captchafox.standard");
54 pub const CLOUDFLARE_TURNSTILE: Self = Self::builtin("cloudflare.turnstile");
56 pub const FRIENDLYCAPTCHA_V2: Self = Self::builtin("friendlycaptcha.v2");
58 pub const GOOGLE_RECAPTCHA_ENTERPRISE: Self = Self::builtin("google.recaptcha.enterprise");
60 pub const GOOGLE_RECAPTCHA_V2: Self = Self::builtin("google.recaptcha.v2");
62 pub const GOOGLE_RECAPTCHA_V3: Self = Self::builtin("google.recaptcha.v3");
64 pub const HCAPTCHA_ENTERPRISE: Self = Self::builtin("hcaptcha.enterprise");
66 pub const HCAPTCHA_STANDARD: Self = Self::builtin("hcaptcha.standard");
68 pub const MTCAPTCHA_STANDARD: Self = Self::builtin("mtcaptcha.standard");
70 pub const PROSOPO_PROCAPTCHA: Self = Self::builtin("prosopo.procaptcha");
72
73 const fn builtin(value: &'static str) -> Self {
74 Self(Cow::Borrowed(value))
75 }
76
77 pub fn new(value: impl Into<String>) -> Result<Self, InvalidProviderId> {
87 let value = value.into();
88 validate(&value)?;
89 Ok(Self(Cow::Owned(value)))
90 }
91
92 #[must_use]
94 pub fn as_str(&self) -> &str {
95 self.0.as_ref()
96 }
97}
98
99impl fmt::Debug for ProviderId {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 formatter
102 .debug_tuple("ProviderId")
103 .field(&self.as_str())
104 .finish()
105 }
106}
107
108impl fmt::Display for ProviderId {
109 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110 formatter.write_str(self.as_str())
111 }
112}
113
114impl TryFrom<&str> for ProviderId {
115 type Error = InvalidProviderId;
116
117 fn try_from(value: &str) -> Result<Self, Self::Error> {
118 Self::new(value)
119 }
120}
121
122impl<'de> Deserialize<'de> for ProviderId {
123 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124 where
125 D: Deserializer<'de>,
126 {
127 let value = String::deserialize(deserializer)?;
128 Self::new(value).map_err(serde::de::Error::custom)
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134#[non_exhaustive]
135pub enum InvalidProviderId {
136 Empty,
138 TooLong,
140 SegmentTooLong,
142 InvalidSyntax,
144 ReservedAuthority,
146}
147
148impl fmt::Display for InvalidProviderId {
149 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150 let message = match self {
151 Self::Empty => "provider identifier must not be empty",
152 Self::TooLong => "provider identifier is too long",
153 Self::SegmentTooLong => "provider identifier segment is too long",
154 Self::InvalidSyntax => "provider identifier has invalid syntax",
155 Self::ReservedAuthority => "provider identifier uses a reserved authority",
156 };
157 formatter.write_str(message)
158 }
159}
160
161impl Error for InvalidProviderId {}
162
163fn validate(value: &str) -> Result<(), InvalidProviderId> {
164 if value.is_empty() {
165 return Err(InvalidProviderId::Empty);
166 }
167 if value.len() > MAX_PROVIDER_ID_LEN {
168 return Err(InvalidProviderId::TooLong);
169 }
170 if !value.contains('.') {
171 return Err(InvalidProviderId::InvalidSyntax);
172 }
173
174 for segment in value.split('.') {
175 validate_segment(segment)?;
176 }
177
178 let authority = value.split('.').next().expect("non-empty identifier");
179 if RESERVED_AUTHORITIES.contains(&authority) && !BUILTIN_IDS.contains(&value) {
180 return Err(InvalidProviderId::ReservedAuthority);
181 }
182
183 Ok(())
184}
185
186fn validate_segment(segment: &str) -> Result<(), InvalidProviderId> {
187 if segment.is_empty() {
188 return Err(InvalidProviderId::InvalidSyntax);
189 }
190 if segment.len() > MAX_PROVIDER_ID_SEGMENT_LEN {
191 return Err(InvalidProviderId::SegmentTooLong);
192 }
193
194 let bytes = segment.as_bytes();
195 let is_alphanumeric = |byte: &u8| byte.is_ascii_lowercase() || byte.is_ascii_digit();
196 if !is_alphanumeric(&bytes[0]) || !is_alphanumeric(&bytes[bytes.len() - 1]) {
197 return Err(InvalidProviderId::InvalidSyntax);
198 }
199 if !bytes
200 .iter()
201 .all(|byte| is_alphanumeric(byte) || *byte == b'-')
202 {
203 return Err(InvalidProviderId::InvalidSyntax);
204 }
205
206 Ok(())
207}