Skip to main content

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}