relay_core/identity/
mod.rs1mod address;
2mod agent;
3mod inbox;
4mod user;
5
6pub use address::Address;
7pub use agent::AgentId;
8pub use inbox::InboxId;
9pub use user::UserId;
10
11#[derive(Debug, PartialEq, Eq, thiserror::Error)]
12pub enum IdentityError {
13 #[error("invalid agent id")]
14 InvalidAgent,
15 #[error("invalid user id")]
16 InvalidUser,
17 #[error("invalid inbox")]
18 InvalidInbox,
19 #[error("invalid address")]
20 InvalidAddress,
21 #[error("invalid identity string")]
22 InvalidIdentityString,
23 #[error("invalid fqdn string")]
24 InvalidFqdnString,
25}
26
27pub const ALLOWED_SPECIAL_CHARS: &str = ".!$%&'*+-/=?^_`{}~";
30
31pub fn is_valid_identity_string(input: &str) -> bool {
35 input
36 .chars()
37 .all(|c| c.is_ascii_alphanumeric() || ALLOWED_SPECIAL_CHARS.contains(c))
38}
39
40pub fn canonical_identity_string(input: &str) -> String {
43 input.to_lowercase()
44}
45
46pub fn is_valid_dns_label(label: &str) -> bool {
48 let len = label.len();
49 if len == 0 || len > 63 {
50 return false;
51 }
52
53 let bytes = label.as_bytes();
54
55 if !bytes[0].is_ascii_alphanumeric() || !bytes[len - 1].is_ascii_alphanumeric() {
56 return false;
57 }
58
59 bytes
60 .iter()
61 .all(|b| b.is_ascii_alphanumeric() || *b == b'-')
62}
63
64pub fn is_valid_fqdn(input: &str) -> bool {
66 if input.len() > 253 {
67 return false;
68 }
69
70 if input.ends_with('.') {
71 return false;
72 }
73
74 let labels: Vec<&str> = input.split('.').collect();
75 if labels.len() < 2 {
76 return false;
77 }
78
79 labels.iter().all(|label| is_valid_dns_label(label))
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 mod identity_string {
87 use super::*;
88
89 #[test]
92 fn correct_allowed_chars() {
93 let test_str = "abcXYZ0123456789.!$%&'*+-/=?^_`{}~";
94 assert!(is_valid_identity_string(test_str));
95 }
96
97 #[test]
98 fn rejects_common_invalid_chars() {
99 assert!(!is_valid_identity_string("#"));
100 assert!(!is_valid_identity_string("@"));
101 assert!(!is_valid_identity_string("|"));
102 assert!(!is_valid_identity_string(" "));
103 }
104
105 #[test]
106 fn canonicalizes_properly() {
107 let input = "AbC.!$%&'*+-/=?^_`{}~XyZ0123456789";
108 let expected = "abc.!$%&'*+-/=?^_`{}~xyz0123456789";
109 assert_eq!(canonical_identity_string(input), expected);
110 }
111 }
112
113 mod dns_label {
114 use super::*;
115
116 #[test]
117 fn valid_labels() {
118 assert!(is_valid_dns_label("example"));
119 assert!(is_valid_dns_label("ex-ample"));
120 assert!(is_valid_dns_label("e"));
121 assert!(is_valid_dns_label("a".repeat(63).as_str()));
122 }
123
124 #[test]
125 fn invalid_labels() {
126 assert!(!is_valid_dns_label(""));
127 assert!(!is_valid_dns_label("a".repeat(64).as_str()));
128 assert!(!is_valid_dns_label("-example"));
129 assert!(!is_valid_dns_label("example-"));
130 assert!(!is_valid_dns_label("ex_ample"));
131 assert!(!is_valid_dns_label("ex ample"));
132 }
133 }
134
135 mod fqdn {
136 use super::*;
137
138 #[test]
139 fn valid_fqdns() {
140 assert!(is_valid_fqdn("example.org"));
141 assert!(is_valid_fqdn("sub.example.org"));
142 assert!(is_valid_fqdn(
143 "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z"
144 ));
145
146 let labels = [
147 "a".repeat(63),
148 "b".repeat(63),
149 "c".repeat(63),
150 "d".repeat(61),
151 ];
152 let fqdn = labels.join(".");
153 assert_eq!(fqdn.len(), 253);
154 assert!(is_valid_fqdn(&fqdn));
155 }
156
157 #[test]
158 fn invalid_fqdns() {
159 assert!(!is_valid_fqdn(""));
160 assert!(!is_valid_fqdn("example."));
161 assert!(!is_valid_fqdn("ex ample.org"));
162 assert!(!is_valid_fqdn("example_org"));
163 assert!(!is_valid_fqdn(&format!("{}.org", "a".repeat(247))));
164 assert!(!is_valid_fqdn("a..b.org"));
165 assert!(!is_valid_fqdn("a.-b.org"));
166 }
167 }
168}