Skip to content

Commit 42d76ad

Browse files
minghuawxiaochengh
andauthored
script: Update name validation for attribute, element, and doctype (#37747)
A recent update in the spec (whatwg/dom#1079) introduced new rules for name validation of attribute, element, and doctype. This PR implements the new name validation rules in `components/script/dom/bindings/domname.rs`. The old XML name validation rules are not fully removed because there remains a few usage of it in `ProcessingInstructions` and `xpath`. Testing: Covered by WPT tests Fixes: #37746 --------- Signed-off-by: minghuaw <[email protected]> Signed-off-by: Minghua Wu <[email protected]> Co-authored-by: Xiaocheng Hu <[email protected]>
1 parent bd4e047 commit 42d76ad

15 files changed

+370
-587
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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+
7+
use html5ever::{LocalName, Namespace, Prefix, ns};
8+
use script_bindings::error::{Error, Fallible};
9+
use script_bindings::str::DOMString;
10+
11+
/// <https://infra.spec.whatwg.org/#xml-namespace>
12+
const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
13+
14+
/// <https://infra.spec.whatwg.org/#xmlns-namespace>
15+
const XMLNS_NAMESPACE: &str = "http://www.w3.org/2000/xmlns/";
16+
17+
/// <https://dom.spec.whatwg.org/#valid-namespace-prefix>
18+
fn 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>
32+
pub(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>
47+
pub(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>
94+
pub(crate) fn is_valid_doctype_name(name: &str) -> 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+
.chars()
99+
.any(|c| c.is_ascii_whitespace() || matches!(c, '\u{0000}' | '\u{003E}'))
100+
}
101+
102+
/// Convert a possibly-null URL to a namespace.
103+
///
104+
/// If the URL is None, returns the empty namespace.
105+
pub(crate) fn namespace_from_domstring(url: Option<DOMString>) -> Namespace {
106+
match url {
107+
None => ns!(),
108+
Some(s) => Namespace::from(s),
109+
}
110+
}
111+
112+
/// Context for [`validate_and_extract`] a namespace and qualified name
113+
///
114+
/// <https://dom.spec.whatwg.org/#validate-and-extract>
115+
#[derive(Clone, Copy, Debug)]
116+
pub(crate) enum Context {
117+
Attribute,
118+
Element,
119+
}
120+
121+
/// <https://dom.spec.whatwg.org/#validate-and-extract>
122+
pub(crate) fn validate_and_extract(
123+
namespace: Option<DOMString>,
124+
qualified_name: &str,
125+
context: Context,
126+
) -> Fallible<(Namespace, Option<Prefix>, LocalName)> {
127+
// Step 1. If namespace is the empty string, then set it to null.
128+
let namespace = namespace_from_domstring(namespace);
129+
130+
// Step 2. Let prefix be null.
131+
let mut prefix = None;
132+
// Step 3. Let localName be qualifiedName.
133+
let mut local_name = qualified_name;
134+
// Step 4. If qualifiedName contains a U+003A (:):
135+
if let Some(idx) = qualified_name.find(':') {
136+
// Step 4.1. Let splitResult be the result of running
137+
// strictly split given qualifiedName and U+003A (:).
138+
let p = &qualified_name[..idx];
139+
let local_name_start = (idx + 1).min(qualified_name.len());
140+
141+
// Step 4.2. Set prefix to splitResult[0].
142+
if !p.is_empty() {
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+
prefix = Some(p);
151+
}
152+
153+
// Step 4.3. Set localName to splitResult[1].
154+
local_name = &qualified_name[local_name_start..];
155+
}
156+
157+
match context {
158+
// Step 6. If context is "attribute" and localName
159+
// is not a valid attribute local name, then
160+
// throw an "InvalidCharacterError" DOMException.
161+
Context::Attribute => {
162+
if !is_valid_attribute_local_name(local_name) {
163+
debug!("Not a valid attribute name");
164+
return Err(Error::InvalidCharacter);
165+
}
166+
},
167+
// Step 7. If context is "element" and localName
168+
// is not a valid element local name, then
169+
// throw an "InvalidCharacterError" DOMException.
170+
Context::Element => {
171+
if !is_valid_element_local_name(local_name) {
172+
debug!("Not a valid element name");
173+
return Err(Error::InvalidCharacter);
174+
}
175+
},
176+
}
177+
178+
match prefix {
179+
// Step 8. If prefix is non-null and namespace is null,
180+
// then throw a "NamespaceError" DOMException.
181+
Some(_) if namespace.is_empty() => Err(Error::Namespace),
182+
// Step 9. If prefix is "xml" and namespace is not the XML namespace,
183+
// then throw a "NamespaceError" DOMException.
184+
Some("xml") if *namespace != *XML_NAMESPACE => Err(Error::Namespace),
185+
// Step 10. If either qualifiedName or prefix is "xmlns" and namespace
186+
// is not the XMLNS namespace, then throw a "NamespaceError" DOMException.
187+
p if (qualified_name == "xmlns" || p == Some("xmlns")) &&
188+
*namespace != *XMLNS_NAMESPACE =>
189+
{
190+
Err(Error::Namespace)
191+
},
192+
Some(_) if qualified_name == "xmlns" && *namespace != *XMLNS_NAMESPACE => {
193+
Err(Error::Namespace)
194+
},
195+
// Step 11. If namespace is the XMLNS namespace and neither qualifiedName
196+
// nor prefix is "xmlns", then throw a "NamespaceError" DOMException.
197+
p if *namespace == *XMLNS_NAMESPACE &&
198+
(qualified_name != "xmlns" && p != Some("xmlns")) =>
199+
{
200+
Err(Error::Namespace)
201+
},
202+
// Step 12. Return (namespace, prefix, localName).
203+
_ => Ok((
204+
namespace,
205+
prefix.map(Prefix::from),
206+
LocalName::from(local_name),
207+
)),
208+
}
209+
}

components/script/dom/bindings/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ pub(crate) mod buffer_source;
139139
pub(crate) mod cell;
140140
pub(crate) mod constructor;
141141
pub(crate) mod conversions;
142+
pub(crate) mod domname;
142143
pub(crate) mod error;
143144
pub(crate) mod frozenarray;
144145
pub(crate) mod function;

components/script/dom/bindings/xmlname.rs

Lines changed: 2 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,9 @@
44

55
//! Functions for validating and extracting qualified XML names.
66
7-
use html5ever::{LocalName, Namespace, Prefix, ns};
8-
9-
use crate::dom::bindings::error::{Error, Fallible};
10-
use crate::dom::bindings::str::DOMString;
11-
127
/// Check if an element name is valid. See <http://www.w3.org/TR/xml/#NT-Name>
138
/// for details.
14-
fn is_valid_start(c: char) -> bool {
9+
pub(crate) fn is_valid_start(c: char) -> bool {
1510
matches!(c, ':' |
1611
'A'..='Z' |
1712
'_' |
@@ -30,7 +25,7 @@ fn is_valid_start(c: char) -> bool {
3025
'\u{10000}'..='\u{EFFFF}')
3126
}
3227

33-
fn is_valid_continuation(c: char) -> bool {
28+
pub(crate) fn is_valid_continuation(c: char) -> bool {
3429
is_valid_start(c) ||
3530
matches!(c,
3631
'-' |
@@ -41,103 +36,6 @@ fn is_valid_continuation(c: char) -> bool {
4136
'\u{203F}'..='\u{2040}')
4237
}
4338

44-
/// Validate a qualified name. See <https://dom.spec.whatwg.org/#validate> for details.
45-
///
46-
/// On success, this returns a tuple `(prefix, local name)`.
47-
pub(crate) fn validate_and_extract_qualified_name(
48-
qualified_name: &str,
49-
) -> Fallible<(Option<&str>, &str)> {
50-
if qualified_name.is_empty() {
51-
// Qualified names must not be empty
52-
return Err(Error::InvalidCharacter);
53-
}
54-
let mut colon_offset = None;
55-
let mut at_start_of_name = true;
56-
57-
for (byte_position, c) in qualified_name.char_indices() {
58-
if c == ':' {
59-
if colon_offset.is_some() {
60-
// Qualified names must not contain more than one colon
61-
return Err(Error::InvalidCharacter);
62-
}
63-
colon_offset = Some(byte_position);
64-
at_start_of_name = true;
65-
continue;
66-
}
67-
68-
if at_start_of_name {
69-
if !is_valid_start(c) {
70-
// Name segments must begin with a valid start character
71-
return Err(Error::InvalidCharacter);
72-
}
73-
at_start_of_name = false;
74-
} else if !is_valid_continuation(c) {
75-
// Name segments must consist of valid characters
76-
return Err(Error::InvalidCharacter);
77-
}
78-
}
79-
80-
let Some(colon_offset) = colon_offset else {
81-
// Simple case: there is no prefix
82-
return Ok((None, qualified_name));
83-
};
84-
85-
let (prefix, local_name) = qualified_name.split_at(colon_offset);
86-
let local_name = &local_name[1..]; // Remove the colon
87-
88-
if prefix.is_empty() || local_name.is_empty() {
89-
// Neither prefix nor local name can be empty
90-
return Err(Error::InvalidCharacter);
91-
}
92-
93-
Ok((Some(prefix), local_name))
94-
}
95-
96-
/// Validate a namespace and qualified name and extract their parts.
97-
/// See <https://dom.spec.whatwg.org/#validate-and-extract> for details.
98-
pub(crate) fn validate_and_extract(
99-
namespace: Option<DOMString>,
100-
qualified_name: &str,
101-
) -> Fallible<(Namespace, Option<Prefix>, LocalName)> {
102-
// Step 1. If namespace is the empty string, then set it to null.
103-
let namespace = namespace_from_domstring(namespace);
104-
105-
// Step 2. Validate qualifiedName.
106-
// Step 3. Let prefix be null.
107-
// Step 4. Let localName be qualifiedName.
108-
// Step 5. If qualifiedName contains a U+003A (:):
109-
// NOTE: validate_and_extract_qualified_name does all of these things for us, because
110-
// it's easier to do them together
111-
let (prefix, local_name) = validate_and_extract_qualified_name(qualified_name)?;
112-
debug_assert!(!local_name.contains(':'));
113-
114-
match (namespace, prefix) {
115-
(ns!(), Some(_)) => {
116-
// Step 6. If prefix is non-null and namespace is null, then throw a "NamespaceError" DOMException.
117-
Err(Error::Namespace)
118-
},
119-
(ref ns, Some("xml")) if ns != &ns!(xml) => {
120-
// Step 7. If prefix is "xml" and namespace is not the XML namespace,
121-
// then throw a "NamespaceError" DOMException.
122-
Err(Error::Namespace)
123-
},
124-
(ref ns, p) if ns != &ns!(xmlns) && (qualified_name == "xmlns" || p == Some("xmlns")) => {
125-
// Step 8. If either qualifiedName or prefix is "xmlns" and namespace is not the XMLNS namespace,
126-
// then throw a "NamespaceError" DOMException.
127-
Err(Error::Namespace)
128-
},
129-
(ns!(xmlns), p) if qualified_name != "xmlns" && p != Some("xmlns") => {
130-
// Step 9. If namespace is the XMLNS namespace and neither qualifiedName nor prefix is "xmlns",
131-
// then throw a "NamespaceError" DOMException.
132-
Err(Error::Namespace)
133-
},
134-
(ns, p) => {
135-
// Step 10. Return namespace, prefix, and localName.
136-
Ok((ns, p.map(Prefix::from), LocalName::from(local_name)))
137-
},
138-
}
139-
}
140-
14139
pub(crate) fn matches_name_production(name: &str) -> bool {
14240
let mut iter = name.chars();
14341

@@ -146,13 +44,3 @@ pub(crate) fn matches_name_production(name: &str) -> bool {
14644
}
14745
iter.all(is_valid_continuation)
14846
}
149-
150-
/// Convert a possibly-null URL to a namespace.
151-
///
152-
/// If the URL is None, returns the empty namespace.
153-
pub(crate) fn namespace_from_domstring(url: Option<DOMString>) -> Namespace {
154-
match url {
155-
None => ns!(),
156-
Some(s) => Namespace::from(s),
157-
}
158-
}

0 commit comments

Comments
 (0)