jmap_metadata_types/namespace.rs
1//! draft-ietf-jmap-metadata-04 §2.1 — namespace identifier validation.
2//!
3//! Namespace identifiers are the keys of the `metadata` and
4//! `privateMetadata` objects. Each identifier is either a *registered name*
5//! (no dot) or a *domain name* (contains at least one dot).
6
7/// Returns `true` if `name` is a valid registered namespace identifier
8/// (draft-ietf-jmap-metadata-04 §2.1).
9///
10/// A registered name is a non-empty sequence of lowercase US-ASCII letters,
11/// digits, hyphens (`-`), and underscores (`_`), with **no dot** (`.`).
12/// Uppercase letters are rejected.
13///
14/// ```
15/// use jmap_metadata_types::is_registered_namespace;
16///
17/// assert!(is_registered_namespace("photography"));
18/// assert!(is_registered_namespace("my-namespace_v2"));
19/// assert!(!is_registered_namespace("acme.example.com")); // domain name
20/// assert!(!is_registered_namespace("")); // empty
21/// assert!(!is_registered_namespace("has space")); // space
22/// assert!(!is_registered_namespace("Photography")); // uppercase
23/// ```
24pub fn is_registered_namespace(name: &str) -> bool {
25 !name.is_empty()
26 && !name.contains('.')
27 && name
28 .bytes()
29 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'_')
30}
31
32/// Returns `true` if `name` is a valid vendor (domain-name) namespace
33/// identifier (draft-ietf-jmap-metadata-04 §2.1).
34///
35/// A domain-name namespace contains at least one dot (`.`) and is in DNS
36/// form (e.g. `example.com`, `acme.example.org`). Every label must be a
37/// lowercase LDH label: non-empty, only lowercase US-ASCII letters, digits
38/// and hyphens, and not starting or ending with a hyphen. This rejects
39/// uppercase ("Domain-name identifiers MUST be in lowercase"), U-labels
40/// (internationalized labels must be "in A-label form") and characters
41/// such as `/` that would collide with `/set` patch paths and `/get`
42/// subselectors (§3.1). No DNS resolution is performed.
43///
44/// ```
45/// use jmap_metadata_types::is_vendor_namespace;
46///
47/// assert!(is_vendor_namespace("acme.example.com"));
48/// assert!(is_vendor_namespace("a.b"));
49/// assert!(!is_vendor_namespace("photography")); // no dot → registered
50/// assert!(!is_vendor_namespace(".leading.dot")); // empty label
51/// assert!(!is_vendor_namespace("trailing.dot.")); // empty label
52/// assert!(!is_vendor_namespace("")); // empty
53/// assert!(!is_vendor_namespace("Acme.example.com")); // uppercase
54/// assert!(!is_vendor_namespace("acme.example.com/k")); // not LDH
55/// ```
56pub fn is_vendor_namespace(name: &str) -> bool {
57 if name.is_empty() || !name.contains('.') {
58 return false;
59 }
60 name.split('.').all(|label| {
61 !label.is_empty()
62 && !label.starts_with('-')
63 && !label.ends_with('-')
64 && label
65 .bytes()
66 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
67 })
68}
69
70/// Returns `true` if `name` is a valid namespace identifier of either kind
71/// (registered name or vendor domain name).
72///
73/// Equivalent to `is_registered_namespace(name) || is_vendor_namespace(name)`.
74///
75/// ```
76/// use jmap_metadata_types::is_valid_namespace;
77///
78/// assert!(is_valid_namespace("photography"));
79/// assert!(is_valid_namespace("acme.example.com"));
80/// assert!(!is_valid_namespace(""));
81/// assert!(!is_valid_namespace("has space"));
82/// ```
83pub fn is_valid_namespace(name: &str) -> bool {
84 is_registered_namespace(name) || is_vendor_namespace(name)
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 // -- registered namespace ------------------------------------------------
92
93 #[test]
94 fn registered_simple_name() {
95 assert!(is_registered_namespace("photography"));
96 }
97
98 #[test]
99 fn registered_with_hyphens_underscores_digits() {
100 assert!(is_registered_namespace("my-namespace_v2"));
101 assert!(is_registered_namespace("a"));
102 assert!(is_registered_namespace("a1-b2_c3"));
103 }
104
105 // -04 §2.1: "a sequence of lowercase US-ASCII letters, digits, hyphens,
106 // and underscores, with no dot".
107 #[test]
108 fn registered_rejects_uppercase() {
109 assert!(!is_registered_namespace("A1-b2_c3"));
110 assert!(!is_registered_namespace("Photography"));
111 assert!(!is_registered_namespace("photographY"));
112 }
113
114 #[test]
115 fn registered_rejects_empty() {
116 assert!(!is_registered_namespace(""));
117 }
118
119 #[test]
120 fn registered_rejects_dot() {
121 assert!(!is_registered_namespace("has.dot"));
122 }
123
124 #[test]
125 fn registered_rejects_space() {
126 assert!(!is_registered_namespace("has space"));
127 }
128
129 #[test]
130 fn registered_rejects_non_ascii() {
131 assert!(!is_registered_namespace("café"));
132 }
133
134 #[test]
135 fn registered_rejects_special_chars() {
136 assert!(!is_registered_namespace("foo@bar"));
137 assert!(!is_registered_namespace("foo/bar"));
138 assert!(!is_registered_namespace("foo:bar"));
139 }
140
141 // -- vendor namespace ----------------------------------------------------
142
143 #[test]
144 fn vendor_simple_domain() {
145 assert!(is_vendor_namespace("acme.example.com"));
146 assert!(is_vendor_namespace("a.b"));
147 }
148
149 #[test]
150 fn vendor_rejects_no_dot() {
151 assert!(!is_vendor_namespace("nodot"));
152 }
153
154 #[test]
155 fn vendor_rejects_empty() {
156 assert!(!is_vendor_namespace(""));
157 }
158
159 #[test]
160 fn vendor_rejects_leading_dot() {
161 assert!(!is_vendor_namespace(".leading"));
162 }
163
164 #[test]
165 fn vendor_rejects_trailing_dot() {
166 assert!(!is_vendor_namespace("trailing."));
167 }
168
169 #[test]
170 fn vendor_rejects_consecutive_dots() {
171 assert!(!is_vendor_namespace("a..b"));
172 }
173
174 #[test]
175 fn vendor_rejects_space_in_label() {
176 assert!(!is_vendor_namespace("a. b"));
177 }
178
179 // -04 §2.1: "Servers MUST reject ... a domain name containing uppercase
180 // ASCII letters."
181 #[test]
182 fn vendor_rejects_uppercase() {
183 assert!(!is_vendor_namespace("Acme.example.com"));
184 assert!(!is_vendor_namespace("acme.EXAMPLE.com"));
185 assert!(!is_vendor_namespace("acme.example.coM"));
186 }
187
188 // -04 §2.1: internationalized labels "in A-label form" (ASCII).
189 #[test]
190 fn vendor_accepts_a_label_rejects_u_label() {
191 assert!(is_vendor_namespace("xn--mnchen-3ya.example"));
192 assert!(!is_vendor_namespace("münchen.example"));
193 }
194
195 // -04 §2.1 (L342): a domain name "in DNS form"; §3.1 (L487-491): '/'
196 // separates the namespace from the key in patch paths and
197 // subselectors, so it cannot appear inside one.
198 #[test]
199 fn vendor_rejects_non_ldh_chars() {
200 for bad in [
201 "acme.example.com/k",
202 "a.b~c",
203 "a.b\"x",
204 "a_b.example",
205 "a.b:c",
206 "a@b.c",
207 ] {
208 assert!(!is_vendor_namespace(bad), "{bad}");
209 assert!(!is_valid_namespace(bad), "{bad}");
210 }
211 }
212
213 #[test]
214 fn vendor_rejects_edge_hyphens() {
215 assert!(!is_vendor_namespace("-acme.example"));
216 assert!(!is_vendor_namespace("acme-.example"));
217 assert!(!is_vendor_namespace("acme.example-"));
218 assert!(is_vendor_namespace("my-acme.example"));
219 assert!(is_vendor_namespace("0day.example"));
220 }
221
222 // -- is_valid_namespace --------------------------------------------------
223
224 #[test]
225 fn valid_accepts_both_kinds() {
226 assert!(is_valid_namespace("photography"));
227 assert!(is_valid_namespace("acme.example.com"));
228 }
229
230 #[test]
231 fn valid_rejects_invalid() {
232 assert!(!is_valid_namespace(""));
233 assert!(!is_valid_namespace("has space"));
234 assert!(!is_valid_namespace(".leading"));
235 assert!(!is_valid_namespace("Photography"));
236 assert!(!is_valid_namespace("Acme.example.com"));
237 }
238}