script/dom/bindings/domname.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Functions for validating names as defined in the DOM Standard: <https://dom.spec.whatwg.org/#namespaces>
6
7use html5ever::{LocalName, Namespace, Prefix, ns};
8use script_bindings::error::{Error, Fallible};
9use script_bindings::str::DOMString;
10
11/// <https://infra.spec.whatwg.org/#xml-namespace>
12const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
13
14/// <https://infra.spec.whatwg.org/#xmlns-namespace>
15const XMLNS_NAMESPACE: &str = "http://www.w3.org/2000/xmlns/";
16
17/// <https://dom.spec.whatwg.org/#valid-namespace-prefix>
18fn is_valid_namespace_prefix(p: &str) -> bool {
19 // A string is a valid namespace prefix if its length
20 // is at least 1 and it does not contain ASCII whitespace,
21 // U+0000 NULL, U+002F (/), or U+003E (>).
22
23 if p.is_empty() {
24 return false;
25 }
26
27 !p.chars()
28 .any(|c| c.is_ascii_whitespace() || matches!(c, '\u{0000}' | '\u{002F}' | '\u{003E}'))
29}
30
31/// <https://dom.spec.whatwg.org/#valid-attribute-local-name>
32pub(crate) fn is_valid_attribute_local_name(name: &str) -> bool {
33 // A string is a valid attribute local name if its length
34 // is at least 1 and it does not contain ASCII whitespace,
35 // U+0000 NULL, U+002F (/), U+003D (=), or U+003E (>).
36
37 if name.is_empty() {
38 return false;
39 }
40
41 !name.chars().any(|c| {
42 c.is_ascii_whitespace() || matches!(c, '\u{0000}' | '\u{002F}' | '\u{003D}' | '\u{003E}')
43 })
44}
45
46/// <https://dom.spec.whatwg.org/#valid-element-local-name>
47pub(crate) fn is_valid_element_local_name(name: &str) -> bool {
48 // Step 1. If name’s length is 0, then return false.
49 if name.is_empty() {
50 return false;
51 }
52
53 let mut iter = name.chars();
54
55 // SAFETY: we have already checked that the &str is not empty
56 let c0 = iter.next().unwrap();
57
58 // Step 2. If name’s 0th code point is an ASCII alpha, then:
59 if c0.is_ascii_alphabetic() {
60 for c in iter {
61 // Step 2.1 If name contains ASCII whitespace,
62 // U+0000 NULL, U+002F (/), or U+003E (>), then return false.
63 if c.is_ascii_whitespace() || matches!(c, '\u{0000}' | '\u{002F}' | '\u{003E}') {
64 return false;
65 }
66 }
67 true
68 }
69 // Step 3. If name’s 0th code point is not U+003A (:), U+005F (_),
70 // or in the range U+0080 to U+10FFFF, inclusive, then return false.
71 else if matches!(c0, '\u{003A}' | '\u{005F}' | '\u{0080}'..='\u{10FFF}') {
72 for c in iter {
73 // Step 4. If name’s subsequent code points,
74 // if any, are not ASCII alphas, ASCII digits,
75 // U+002D (-), U+002E (.), U+003A (:), U+005F (_),
76 // or in the range U+0080 to U+10FFFF, inclusive,
77 // then return false.
78 if !c.is_ascii_alphanumeric() &&
79 !matches!(
80 c,
81 '\u{002D}' | '\u{002E}' | '\u{003A}' | '\u{005F}' | '\u{0080}'..='\u{10FFF}'
82 )
83 {
84 return false;
85 }
86 }
87 true
88 } else {
89 false
90 }
91}
92
93/// <https://dom.spec.whatwg.org/#valid-doctype-name>
94pub(crate) fn is_valid_doctype_name(name: &DOMString) -> bool {
95 // A string is a valid doctype name if it does not contain
96 // ASCII whitespace, U+0000 NULL, or U+003E (>).
97 !name
98 .str()
99 .chars()
100 .any(|c| c.is_ascii_whitespace() || matches!(c, '\u{0000}' | '\u{003E}'))
101}
102
103/// Convert a possibly-null URL to a namespace.
104///
105/// If the URL is None, returns the empty namespace.
106pub(crate) fn namespace_from_domstring(url: Option<DOMString>) -> Namespace {
107 match url {
108 None => ns!(),
109 Some(s) => Namespace::from(s),
110 }
111}
112
113/// Context for [`validate_and_extract`] a namespace and qualified name
114///
115/// <https://dom.spec.whatwg.org/#validate-and-extract>
116#[derive(Clone, Copy, Debug)]
117pub(crate) enum Context {
118 Attribute,
119 Element,
120}
121
122/// <https://dom.spec.whatwg.org/#validate-and-extract>
123pub(crate) fn validate_and_extract(
124 namespace: Option<DOMString>,
125 qualified_name: &DOMString,
126 context: Context,
127) -> Fallible<(Namespace, Option<Prefix>, LocalName)> {
128 let qualified_name = String::from(&*qualified_name.str());
129
130 // Step 1. If namespace is the empty string, then set it to null.
131 let namespace = namespace_from_domstring(namespace);
132
133 // Step 2. Let prefix be null.
134 let mut prefix = None;
135 // Step 3. Let localName be qualifiedName.
136 let mut local_name = qualified_name.as_str();
137 // Step 4. If qualifiedName contains a U+003A (:):
138 if let Some(idx) = qualified_name.find(':') {
139 // Step 4.1. Let splitResult be the result of running
140 // strictly split given qualifiedName and U+003A (:).
141 let p = &qualified_name[..idx];
142
143 // Step 5. If prefix is not a valid namespace prefix,
144 // then throw an "InvalidCharacterError" DOMException.
145 if !is_valid_namespace_prefix(p) {
146 debug!("Not a valid namespace prefix");
147 return Err(Error::InvalidCharacter);
148 }
149
150 // Step 4.2. Set prefix to splitResult[0].
151 prefix = Some(p);
152
153 // Step 4.3. Set localName to splitResult[1].
154 let remaining = &qualified_name.as_str()[(idx + 1).min(qualified_name.len())..];
155 match remaining.find(':') {
156 Some(end) => local_name = &remaining[..end],
157 None => local_name = remaining,
158 };
159 }
160
161 if let Some(p) = prefix {
162 // Step 5. If prefix is not a valid namespace prefix,
163 // then throw an "InvalidCharacterError" DOMException.
164 if !is_valid_namespace_prefix(p) {
165 debug!("Not a valid namespace prefix");
166 return Err(Error::InvalidCharacter);
167 }
168 }
169
170 match context {
171 // Step 6. If context is "attribute" and localName
172 // is not a valid attribute local name, then
173 // throw an "InvalidCharacterError" DOMException.
174 Context::Attribute => {
175 if !is_valid_attribute_local_name(local_name) {
176 debug!("Not a valid attribute name");
177 return Err(Error::InvalidCharacter);
178 }
179 },
180 // Step 7. If context is "element" and localName
181 // is not a valid element local name, then
182 // throw an "InvalidCharacterError" DOMException.
183 Context::Element => {
184 if !is_valid_element_local_name(local_name) {
185 debug!("Not a valid element name");
186 return Err(Error::InvalidCharacter);
187 }
188 },
189 }
190
191 match prefix {
192 // Step 8. If prefix is non-null and namespace is null,
193 // then throw a "NamespaceError" DOMException.
194 Some(_) if namespace.is_empty() => Err(Error::Namespace),
195 // Step 9. If prefix is "xml" and namespace is not the XML namespace,
196 // then throw a "NamespaceError" DOMException.
197 Some("xml") if *namespace != *XML_NAMESPACE => Err(Error::Namespace),
198 // Step 10. If either qualifiedName or prefix is "xmlns" and namespace
199 // is not the XMLNS namespace, then throw a "NamespaceError" DOMException.
200 p if (qualified_name == "xmlns" || p == Some("xmlns")) &&
201 *namespace != *XMLNS_NAMESPACE =>
202 {
203 Err(Error::Namespace)
204 },
205 Some(_) if qualified_name == "xmlns" && *namespace != *XMLNS_NAMESPACE => {
206 Err(Error::Namespace)
207 },
208 // Step 11. If namespace is the XMLNS namespace and neither qualifiedName
209 // nor prefix is "xmlns", then throw a "NamespaceError" DOMException.
210 p if *namespace == *XMLNS_NAMESPACE &&
211 (qualified_name != "xmlns" && p != Some("xmlns")) =>
212 {
213 Err(Error::Namespace)
214 },
215 // Step 12. Return (namespace, prefix, localName).
216 _ => Ok((
217 namespace,
218 prefix.map(Prefix::from),
219 LocalName::from(local_name),
220 )),
221 }
222}