Skip to main content

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/// <https://html.spec.whatwg.org/multipage/#custom-data-attribute>
104pub(crate) fn is_custom_data_attribute(name: &str, namespace: Option<&str>) -> bool {
105    // A custom data attribute is an attribute in no namespace whose name starts with the string
106    // "data-", has at least one character after the hyphen, is a valid attribute local name, and
107    // contains no ASCII upper alphas.
108    namespace.is_none() &&
109        name.strip_prefix("data-")
110            .is_some_and(|substring| !substring.is_empty()) &&
111        is_valid_attribute_local_name(name) &&
112        name.chars()
113            .all(|code_point| !code_point.is_ascii_uppercase())
114}
115
116/// Convert a possibly-null URL to a namespace.
117///
118/// If the URL is None, returns the empty namespace.
119pub(crate) fn namespace_from_domstring(url: Option<DOMString>) -> Namespace {
120    match url {
121        None => ns!(),
122        Some(s) => Namespace::from(s),
123    }
124}
125
126/// Context for [`validate_and_extract`] a namespace and qualified name
127///
128/// <https://dom.spec.whatwg.org/#validate-and-extract>
129#[derive(Clone, Copy, Debug)]
130pub(crate) enum Context {
131    Attribute,
132    Element,
133}
134
135/// <https://dom.spec.whatwg.org/#validate-and-extract>
136pub(crate) fn validate_and_extract(
137    namespace: Option<DOMString>,
138    qualified_name: &DOMString,
139    context: Context,
140) -> Fallible<(Namespace, Option<Prefix>, LocalName)> {
141    let qualified_name = String::from(&*qualified_name.str());
142
143    // Step 1. If namespace is the empty string, then set it to null.
144    let namespace = namespace_from_domstring(namespace);
145
146    // Step 2. Let prefix be null.
147    let mut prefix = None;
148    // Step 3. Let localName be qualifiedName.
149    let mut local_name = qualified_name.as_str();
150    // Step 4. If qualifiedName contains a U+003A (:):
151    if let Some(idx) = qualified_name.find(':') {
152        //     Step 4.1. Let splitResult be the result of running
153        //          strictly split given qualifiedName and U+003A (:).
154        let p = &qualified_name[..idx];
155
156        // Step 5. If prefix is not a valid namespace prefix,
157        // then throw an "InvalidCharacterError" DOMException.
158        if !is_valid_namespace_prefix(p) {
159            debug!("Not a valid namespace prefix");
160            return Err(Error::InvalidCharacter(None));
161        }
162
163        //     Step 4.2. Set prefix to splitResult[0].
164        prefix = Some(p);
165
166        //     Step 4.3. Set localName to splitResult[1].
167        let remaining = &qualified_name.as_str()[(idx + 1).min(qualified_name.len())..];
168        match remaining.find(':') {
169            Some(end) => local_name = &remaining[..end],
170            None => local_name = remaining,
171        };
172    }
173
174    if let Some(p) = prefix {
175        // Step 5. If prefix is not a valid namespace prefix,
176        // then throw an "InvalidCharacterError" DOMException.
177        if !is_valid_namespace_prefix(p) {
178            debug!("Not a valid namespace prefix");
179            return Err(Error::InvalidCharacter(None));
180        }
181    }
182
183    match context {
184        // Step 6. If context is "attribute" and localName
185        //      is not a valid attribute local name, then
186        //      throw an "InvalidCharacterError" DOMException.
187        Context::Attribute => {
188            if !is_valid_attribute_local_name(local_name) {
189                debug!("Not a valid attribute name");
190                return Err(Error::InvalidCharacter(None));
191            }
192        },
193        // Step 7. If context is "element" and localName
194        //      is not a valid element local name, then
195        //      throw an "InvalidCharacterError" DOMException.
196        Context::Element => {
197            if !is_valid_element_local_name(local_name) {
198                debug!("Not a valid element name");
199                return Err(Error::InvalidCharacter(None));
200            }
201        },
202    }
203
204    match prefix {
205        // Step 8. If prefix is non-null and namespace is null,
206        //      then throw a "NamespaceError" DOMException.
207        Some(_) if namespace.is_empty() => Err(Error::Namespace(None)),
208        // Step 9. If prefix is "xml" and namespace is not the XML namespace,
209        //      then throw a "NamespaceError" DOMException.
210        Some("xml") if *namespace != *XML_NAMESPACE => Err(Error::Namespace(None)),
211        // Step 10. If either qualifiedName or prefix is "xmlns" and namespace
212        //      is not the XMLNS namespace, then throw a "NamespaceError" DOMException.
213        p if (qualified_name == "xmlns" || p == Some("xmlns")) &&
214            *namespace != *XMLNS_NAMESPACE =>
215        {
216            Err(Error::Namespace(None))
217        },
218        Some(_) if qualified_name == "xmlns" && *namespace != *XMLNS_NAMESPACE => {
219            Err(Error::Namespace(None))
220        },
221        // Step 11. If namespace is the XMLNS namespace and neither qualifiedName
222        //      nor prefix is "xmlns", then throw a "NamespaceError" DOMException.
223        p if *namespace == *XMLNS_NAMESPACE &&
224            (qualified_name != "xmlns" && p != Some("xmlns")) =>
225        {
226            Err(Error::Namespace(None))
227        },
228        // Step 12. Return (namespace, prefix, localName).
229        _ => Ok((
230            namespace,
231            prefix.map(Prefix::from),
232            LocalName::from(local_name),
233        )),
234    }
235}