1use std::borrow::Cow;
7
8use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
9use serde::de::DeserializeOwned;
10
11use crate::prelude::*;
12use rand::RngExt;
13
14pub const ID_LENGTH: usize = 24;
15pub const SAFE: [char; 62] = [
16 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
17 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
18 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
19 'V', 'W', 'X', 'Y', 'Z',
20];
21
22pub fn derive_name_from_id_tag(id_tag: &str) -> String {
31 let first_part = id_tag.split('.').next().unwrap_or(id_tag);
32 let mut chars = first_part.chars();
33 match chars.next() {
34 Some(c) => c.to_uppercase().chain(chars).collect(),
35 None => id_tag.to_string(),
36 }
37}
38
39pub fn normalize_id_tag(id_tag: &str) -> Cow<'_, str> {
54 crate::validation::canonicalize_id_tag(id_tag).unwrap_or(Cow::Borrowed(id_tag.trim()))
55}
56
57pub fn random_id() -> ClResult<String> {
58 let mut rng = rand::rng();
59 let mut result = String::with_capacity(ID_LENGTH);
60
61 for _ in 0..ID_LENGTH {
62 result.push(SAFE[rng.random_range(0..SAFE.len())]);
63 }
64 Ok(result)
65}
66
67pub fn decode_jwt_no_verify<T: DeserializeOwned>(jwt: &str) -> ClResult<T> {
72 let mut parts = jwt.splitn(3, '.');
73 let _header = parts.next().ok_or(Error::Parse)?;
74 let payload = parts.next().ok_or(Error::Parse)?;
75 let _sig = parts.next().ok_or(Error::Parse)?;
76 let payload = URL_SAFE_NO_PAD.decode(payload.as_bytes()).map_err(|_| Error::Parse)?;
77 let payload: T = serde_json::from_slice(&payload).map_err(|_| Error::Parse)?;
78 Ok(payload)
79}
80
81pub fn parse_and_validate_identity_id_tag(
86 id_tag: &str,
87 registrar_domain: &str,
88) -> ClResult<(String, String)> {
89 if registrar_domain.is_empty() {
91 return Err(Error::ValidationError("Registrar domain cannot be empty".to_string()));
92 }
93 if id_tag.is_empty() {
94 return Err(Error::ValidationError("Identity id_tag cannot be empty".to_string()));
95 }
96
97 let domain_with_dot = format!(".{}", registrar_domain);
99 if let Some(pos) = id_tag.rfind(&domain_with_dot) {
100 let prefix = id_tag[..pos].to_string();
101 if prefix.is_empty() {
102 return Err(Error::ValidationError(
103 "Invalid id_tag: prefix cannot be empty (id_tag must be in format 'prefix.domain')"
104 .to_string(),
105 ));
106 }
107 Ok((prefix, registrar_domain.to_string()))
108 } else if id_tag == registrar_domain {
109 Err(Error::ValidationError(
111 "Invalid id_tag: prefix cannot be empty (id_tag must be in format 'prefix.domain')"
112 .to_string(),
113 ))
114 } else {
115 Err(Error::ValidationError(format!(
116 "Identity id_tag '{}' does not match registrar domain '{}'",
117 id_tag, registrar_domain
118 )))
119 }
120}
121
122pub fn parse_roles(roles: &str) -> Box<[Box<str>]> {
129 roles.split(',').filter(|s| !s.is_empty()).map(Into::into).collect()
130}
131
132pub fn mask_email(email: &str) -> Option<String> {
134 let (local, domain) = email.split_once('@')?;
135 let (domain_name, tld) = domain.rsplit_once('.')?;
136
137 if local.is_empty() || domain_name.is_empty() {
138 return None;
139 }
140
141 let local_visible = &local[..local.len().min(1)];
142 let domain_visible = &domain_name[..domain_name.len().min(1)];
143
144 Some(format!("{}***@{}***.{}", local_visible, domain_visible, tld))
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_derive_name_from_id_tag() {
153 assert_eq!(derive_name_from_id_tag("home.w9.hu"), "Home");
154 assert_eq!(derive_name_from_id_tag("john.example.com"), "John");
155 assert_eq!(derive_name_from_id_tag("alice"), "Alice");
156 assert_eq!(derive_name_from_id_tag("UPPER.test"), "UPPER");
157 assert_eq!(derive_name_from_id_tag(""), "");
158 }
159
160 #[test]
161 fn test_normalize_id_tag() {
162 assert!(matches!(
164 normalize_id_tag("alice.example.com"),
165 Cow::Borrowed("alice.example.com")
166 ));
167 assert_eq!(normalize_id_tag("Alice.Example.COM"), "alice.example.com");
168 assert!(matches!(normalize_id_tag(" alice.example.com \t"), Cow::Borrowed(_)));
170 assert_eq!(normalize_id_tag(" alice.example.com \t"), "alice.example.com");
171 assert_eq!(normalize_id_tag(" Alice.Example.com "), "alice.example.com");
172 assert_eq!(normalize_id_tag(""), "");
174 assert_eq!(normalize_id_tag(" "), "");
175 assert_eq!(normalize_id_tag("alice_123"), "alice_123");
178 assert_eq!(normalize_id_tag("MÜNCHEN.example.com"), "münchen.example.com");
180 assert_eq!(normalize_id_tag("xn--mnchen-3ya.example.com"), "münchen.example.com");
181 }
182
183 #[test]
184 fn test_simple_valid_identity() {
185 let result = parse_and_validate_identity_id_tag("alice.example.com", "example.com");
186 assert!(result.is_ok());
187 let (prefix, domain) = result.unwrap();
188 assert_eq!(prefix, "alice");
189 assert_eq!(domain, "example.com");
190 }
191
192 #[test]
193 fn test_multi_part_prefix_valid() {
194 let result = parse_and_validate_identity_id_tag("alice.bob.example.com", "example.com");
195 assert!(result.is_ok());
196 let (prefix, domain) = result.unwrap();
197 assert_eq!(prefix, "alice.bob");
198 assert_eq!(domain, "example.com");
199 }
200
201 #[test]
202 fn test_empty_prefix_fails() {
203 let result = parse_and_validate_identity_id_tag("example.com", "example.com");
204 assert!(result.is_err());
205 }
206
207 #[test]
208 fn test_domain_mismatch_fails() {
209 let result = parse_and_validate_identity_id_tag("alice.other.com", "example.com");
210 assert!(result.is_err());
211 }
212
213 #[test]
214 fn test_empty_id_tag_fails() {
215 let result = parse_and_validate_identity_id_tag("", "example.com");
216 assert!(result.is_err());
217 }
218
219 #[test]
220 fn test_empty_registrar_domain_fails() {
221 let result = parse_and_validate_identity_id_tag("alice.example.com", "");
222 assert!(result.is_err());
223 }
224 #[test]
225 fn test_parse_roles() {
226 let as_vec =
227 |s: &str| -> Vec<String> { parse_roles(s).iter().map(ToString::to_string).collect() };
228 assert!(parse_roles("").is_empty());
229 assert_eq!(as_vec("leader"), vec!["leader"]);
230 assert_eq!(as_vec("public,leader"), vec!["public", "leader"]);
231 assert_eq!(as_vec(",,leader,"), vec!["leader"]);
233 }
234
235 #[test]
236 fn test_mask_email() {
237 assert_eq!(super::mask_email("alice@example.com"), Some("a***@e***.com".to_string()));
238 assert_eq!(super::mask_email("a@x.co"), Some("a***@x***.co".to_string()));
239 assert_eq!(super::mask_email("bob@sub.domain.org"), Some("b***@s***.org".to_string()));
240 assert_eq!(super::mask_email("no-at-sign"), None);
241 assert_eq!(super::mask_email("user@nodot"), None);
242 assert_eq!(super::mask_email("@example.com"), None);
243 assert_eq!(super::mask_email("user@.com"), None);
244 assert_eq!(super::mask_email(""), None);
245 }
246}
247
248