From 5b507dc871cd89fb16aac2ed78398e40e07b5a0c Mon Sep 17 00:00:00 2001 From: minghuaw Date: Fri, 11 Jul 2025 10:45:52 +0800 Subject: [PATCH] script: Update name validation for attribute, element, and doctype (#37747) A recent update in the spec (https://github.com/whatwg/dom/pull/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 Signed-off-by: Minghua Wu Co-authored-by: Xiaocheng Hu --- components/script/dom/bindings/domname.rs | 219 ++++++++++++++++ components/script/dom/bindings/mod.rs | 1 + components/script/dom/bindings/xmlname.rs | 116 +-------- components/script/dom/document.rs | 34 ++- components/script/dom/domimplementation.rs | 14 +- components/script/dom/element.rs | 25 +- components/script/dom/htmlcollection.rs | 2 +- components/script/dom/namednodemap.rs | 2 +- components/script/dom/node.rs | 2 +- components/script/xpath/eval.rs | 111 ++++++++- .../DOMImplementation-createDocument.html.ini | 216 ---------------- ...Implementation-createDocumentType.html.ini | 141 ----------- .../nodes/Document-createAttribute.html.ini | 42 ---- .../dom/nodes/Document-createElement.html.ini | 72 ------ .../nodes/Document-createElementNS.html.ini | 234 ------------------ tests/wpt/meta/dom/nodes/attributes.html.ini | 6 - .../meta/dom/nodes/name-validation.html.ini | 15 -- 17 files changed, 380 insertions(+), 872 deletions(-) create mode 100644 components/script/dom/bindings/domname.rs delete mode 100644 tests/wpt/meta/dom/nodes/DOMImplementation-createDocument.html.ini delete mode 100644 tests/wpt/meta/dom/nodes/DOMImplementation-createDocumentType.html.ini delete mode 100644 tests/wpt/meta/dom/nodes/Document-createAttribute.html.ini delete mode 100644 tests/wpt/meta/dom/nodes/Document-createElement.html.ini delete mode 100644 tests/wpt/meta/dom/nodes/Document-createElementNS.html.ini delete mode 100644 tests/wpt/meta/dom/nodes/attributes.html.ini delete mode 100644 tests/wpt/meta/dom/nodes/name-validation.html.ini diff --git a/components/script/dom/bindings/domname.rs b/components/script/dom/bindings/domname.rs new file mode 100644 index 00000000000..ba1553e6193 --- /dev/null +++ b/components/script/dom/bindings/domname.rs @@ -0,0 +1,219 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Functions for validating names as defined in the DOM Standard: + +use html5ever::{LocalName, Namespace, Prefix, ns}; +use script_bindings::error::{Error, Fallible}; +use script_bindings::str::DOMString; + +/// +const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace"; + +/// +const XMLNS_NAMESPACE: &str = "http://www.w3.org/2000/xmlns/"; + +/// +fn is_valid_namespace_prefix(p: &str) -> bool { + // A string is a valid namespace prefix if its length + // is at least 1 and it does not contain ASCII whitespace, + // U+0000 NULL, U+002F (/), or U+003E (>). + + if p.is_empty() { + return false; + } + + !p.chars() + .any(|c| c.is_ascii_whitespace() || matches!(c, '\u{0000}' | '\u{002F}' | '\u{003E}')) +} + +/// +pub(crate) fn is_valid_attribute_local_name(name: &str) -> bool { + // A string is a valid attribute local name if its length + // is at least 1 and it does not contain ASCII whitespace, + // U+0000 NULL, U+002F (/), U+003D (=), or U+003E (>). + + if name.is_empty() { + return false; + } + + !name.chars().any(|c| { + c.is_ascii_whitespace() || matches!(c, '\u{0000}' | '\u{002F}' | '\u{003D}' | '\u{003E}') + }) +} + +/// +pub(crate) fn is_valid_element_local_name(name: &str) -> bool { + // Step 1. If name’s length is 0, then return false. + if name.is_empty() { + return false; + } + + let mut iter = name.chars(); + + // SAFETY: we have already checked that the &str is not empty + let c0 = iter.next().unwrap(); + + // Step 2. If name’s 0th code point is an ASCII alpha, then: + if c0.is_ascii_alphabetic() { + for c in iter { + // Step 2.1 If name contains ASCII whitespace, + // U+0000 NULL, U+002F (/), or U+003E (>), then return false. + if c.is_ascii_whitespace() || matches!(c, '\u{0000}' | '\u{002F}' | '\u{003E}') { + return false; + } + } + true + } + // Step 3. If name’s 0th code point is not U+003A (:), U+005F (_), + // or in the range U+0080 to U+10FFFF, inclusive, then return false. + else if matches!(c0, '\u{003A}' | '\u{005F}' | '\u{0080}'..='\u{10FFF}') { + for c in iter { + // Step 4. If name’s subsequent code points, + // if any, are not ASCII alphas, ASCII digits, + // U+002D (-), U+002E (.), U+003A (:), U+005F (_), + // or in the range U+0080 to U+10FFFF, inclusive, + // then return false. + if !c.is_ascii_alphanumeric() && + !matches!( + c, + '\u{002D}' | '\u{002E}' | '\u{003A}' | '\u{005F}' | '\u{0080}'..='\u{10FFF}' + ) + { + return false; + } + } + true + } else { + false + } +} + +/// +pub(crate) fn is_valid_doctype_name(name: &str) -> bool { + // A string is a valid doctype name if it does not contain + // ASCII whitespace, U+0000 NULL, or U+003E (>). + !name + .chars() + .any(|c| c.is_ascii_whitespace() || matches!(c, '\u{0000}' | '\u{003E}')) +} + +/// Convert a possibly-null URL to a namespace. +/// +/// If the URL is None, returns the empty namespace. +pub(crate) fn namespace_from_domstring(url: Option) -> Namespace { + match url { + None => ns!(), + Some(s) => Namespace::from(s), + } +} + +/// Context for [`validate_and_extract`] a namespace and qualified name +/// +/// +#[derive(Clone, Copy, Debug)] +pub(crate) enum Context { + Attribute, + Element, +} + +/// +pub(crate) fn validate_and_extract( + namespace: Option, + qualified_name: &str, + context: Context, +) -> Fallible<(Namespace, Option, LocalName)> { + // Step 1. If namespace is the empty string, then set it to null. + let namespace = namespace_from_domstring(namespace); + + // Step 2. Let prefix be null. + let mut prefix = None; + // Step 3. Let localName be qualifiedName. + let mut local_name = qualified_name; + // Step 4. If qualifiedName contains a U+003A (:): + if let Some(idx) = qualified_name.find(':') { + // Step 4.1. Let splitResult be the result of running + // strictly split given qualifiedName and U+003A (:). + let p = &qualified_name[..idx]; + + // Step 5. If prefix is not a valid namespace prefix, + // then throw an "InvalidCharacterError" DOMException. + if !is_valid_namespace_prefix(p) { + debug!("Not a valid namespace prefix"); + return Err(Error::InvalidCharacter); + } + + // Step 4.2. Set prefix to splitResult[0]. + prefix = Some(p); + + // Step 4.3. Set localName to splitResult[1]. + let remaining = &qualified_name[(idx + 1).min(qualified_name.len())..]; + match remaining.find(':') { + Some(end) => local_name = &remaining[..end], + None => local_name = remaining, + }; + } + + if let Some(p) = prefix { + // Step 5. If prefix is not a valid namespace prefix, + // then throw an "InvalidCharacterError" DOMException. + if !is_valid_namespace_prefix(p) { + debug!("Not a valid namespace prefix"); + return Err(Error::InvalidCharacter); + } + } + + match context { + // Step 6. If context is "attribute" and localName + // is not a valid attribute local name, then + // throw an "InvalidCharacterError" DOMException. + Context::Attribute => { + if !is_valid_attribute_local_name(local_name) { + debug!("Not a valid attribute name"); + return Err(Error::InvalidCharacter); + } + }, + // Step 7. If context is "element" and localName + // is not a valid element local name, then + // throw an "InvalidCharacterError" DOMException. + Context::Element => { + if !is_valid_element_local_name(local_name) { + debug!("Not a valid element name"); + return Err(Error::InvalidCharacter); + } + }, + } + + match prefix { + // Step 8. If prefix is non-null and namespace is null, + // then throw a "NamespaceError" DOMException. + Some(_) if namespace.is_empty() => Err(Error::Namespace), + // Step 9. If prefix is "xml" and namespace is not the XML namespace, + // then throw a "NamespaceError" DOMException. + Some("xml") if *namespace != *XML_NAMESPACE => Err(Error::Namespace), + // Step 10. If either qualifiedName or prefix is "xmlns" and namespace + // is not the XMLNS namespace, then throw a "NamespaceError" DOMException. + p if (qualified_name == "xmlns" || p == Some("xmlns")) && + *namespace != *XMLNS_NAMESPACE => + { + Err(Error::Namespace) + }, + Some(_) if qualified_name == "xmlns" && *namespace != *XMLNS_NAMESPACE => { + Err(Error::Namespace) + }, + // Step 11. If namespace is the XMLNS namespace and neither qualifiedName + // nor prefix is "xmlns", then throw a "NamespaceError" DOMException. + p if *namespace == *XMLNS_NAMESPACE && + (qualified_name != "xmlns" && p != Some("xmlns")) => + { + Err(Error::Namespace) + }, + // Step 12. Return (namespace, prefix, localName). + _ => Ok(( + namespace, + prefix.map(Prefix::from), + LocalName::from(local_name), + )), + } +} diff --git a/components/script/dom/bindings/mod.rs b/components/script/dom/bindings/mod.rs index d09d6363bd9..a7ee037794e 100644 --- a/components/script/dom/bindings/mod.rs +++ b/components/script/dom/bindings/mod.rs @@ -139,6 +139,7 @@ pub(crate) mod buffer_source; pub(crate) mod cell; pub(crate) mod constructor; pub(crate) mod conversions; +pub(crate) mod domname; pub(crate) mod error; pub(crate) mod frozenarray; pub(crate) mod function; diff --git a/components/script/dom/bindings/xmlname.rs b/components/script/dom/bindings/xmlname.rs index af02569d9c4..b4637b7889a 100644 --- a/components/script/dom/bindings/xmlname.rs +++ b/components/script/dom/bindings/xmlname.rs @@ -4,14 +4,9 @@ //! Functions for validating and extracting qualified XML names. -use html5ever::{LocalName, Namespace, Prefix, ns}; - -use crate::dom::bindings::error::{Error, Fallible}; -use crate::dom::bindings::str::DOMString; - /// Check if an element name is valid. See /// for details. -fn is_valid_start(c: char) -> bool { +pub(crate) fn is_valid_start(c: char) -> bool { matches!(c, ':' | 'A'..='Z' | '_' | @@ -30,7 +25,7 @@ fn is_valid_start(c: char) -> bool { '\u{10000}'..='\u{EFFFF}') } -fn is_valid_continuation(c: char) -> bool { +pub(crate) fn is_valid_continuation(c: char) -> bool { is_valid_start(c) || matches!(c, '-' | @@ -41,103 +36,6 @@ fn is_valid_continuation(c: char) -> bool { '\u{203F}'..='\u{2040}') } -/// Validate a qualified name. See for details. -/// -/// On success, this returns a tuple `(prefix, local name)`. -pub(crate) fn validate_and_extract_qualified_name( - qualified_name: &str, -) -> Fallible<(Option<&str>, &str)> { - if qualified_name.is_empty() { - // Qualified names must not be empty - return Err(Error::InvalidCharacter); - } - let mut colon_offset = None; - let mut at_start_of_name = true; - - for (byte_position, c) in qualified_name.char_indices() { - if c == ':' { - if colon_offset.is_some() { - // Qualified names must not contain more than one colon - return Err(Error::InvalidCharacter); - } - colon_offset = Some(byte_position); - at_start_of_name = true; - continue; - } - - if at_start_of_name { - if !is_valid_start(c) { - // Name segments must begin with a valid start character - return Err(Error::InvalidCharacter); - } - at_start_of_name = false; - } else if !is_valid_continuation(c) { - // Name segments must consist of valid characters - return Err(Error::InvalidCharacter); - } - } - - let Some(colon_offset) = colon_offset else { - // Simple case: there is no prefix - return Ok((None, qualified_name)); - }; - - let (prefix, local_name) = qualified_name.split_at(colon_offset); - let local_name = &local_name[1..]; // Remove the colon - - if prefix.is_empty() || local_name.is_empty() { - // Neither prefix nor local name can be empty - return Err(Error::InvalidCharacter); - } - - Ok((Some(prefix), local_name)) -} - -/// Validate a namespace and qualified name and extract their parts. -/// See for details. -pub(crate) fn validate_and_extract( - namespace: Option, - qualified_name: &str, -) -> Fallible<(Namespace, Option, LocalName)> { - // Step 1. If namespace is the empty string, then set it to null. - let namespace = namespace_from_domstring(namespace); - - // Step 2. Validate qualifiedName. - // Step 3. Let prefix be null. - // Step 4. Let localName be qualifiedName. - // Step 5. If qualifiedName contains a U+003A (:): - // NOTE: validate_and_extract_qualified_name does all of these things for us, because - // it's easier to do them together - let (prefix, local_name) = validate_and_extract_qualified_name(qualified_name)?; - debug_assert!(!local_name.contains(':')); - - match (namespace, prefix) { - (ns!(), Some(_)) => { - // Step 6. If prefix is non-null and namespace is null, then throw a "NamespaceError" DOMException. - Err(Error::Namespace) - }, - (ref ns, Some("xml")) if ns != &ns!(xml) => { - // Step 7. If prefix is "xml" and namespace is not the XML namespace, - // then throw a "NamespaceError" DOMException. - Err(Error::Namespace) - }, - (ref ns, p) if ns != &ns!(xmlns) && (qualified_name == "xmlns" || p == Some("xmlns")) => { - // Step 8. If either qualifiedName or prefix is "xmlns" and namespace is not the XMLNS namespace, - // then throw a "NamespaceError" DOMException. - Err(Error::Namespace) - }, - (ns!(xmlns), p) if qualified_name != "xmlns" && p != Some("xmlns") => { - // Step 9. If namespace is the XMLNS namespace and neither qualifiedName nor prefix is "xmlns", - // then throw a "NamespaceError" DOMException. - Err(Error::Namespace) - }, - (ns, p) => { - // Step 10. Return namespace, prefix, and localName. - Ok((ns, p.map(Prefix::from), LocalName::from(local_name))) - }, - } -} - pub(crate) fn matches_name_production(name: &str) -> bool { let mut iter = name.chars(); @@ -146,13 +44,3 @@ pub(crate) fn matches_name_production(name: &str) -> bool { } iter.all(is_valid_continuation) } - -/// Convert a possibly-null URL to a namespace. -/// -/// If the URL is None, returns the empty namespace. -pub(crate) fn namespace_from_domstring(url: Option) -> Namespace { - match url { - None => ns!(), - Some(s) => Namespace::from(s), - } -} diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index ba9a4fdd89e..13438cae3e0 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -110,6 +110,9 @@ use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSReso use crate::dom::bindings::codegen::UnionTypes::{ NodeOrString, StringOrElementCreationOptions, TrustedHTMLOrString, }; +use crate::dom::bindings::domname::{ + self, is_valid_attribute_local_name, is_valid_element_local_name, namespace_from_domstring, +}; use crate::dom::bindings::error::{Error, ErrorInfo, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::num::Finite; @@ -120,9 +123,7 @@ use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::bindings::trace::{HashMapTracedValues, NoTrace}; #[cfg(feature = "webgpu")] use crate::dom::bindings::weakref::WeakRef; -use crate::dom::bindings::xmlname::{ - matches_name_production, namespace_from_domstring, validate_and_extract, -}; +use crate::dom::bindings::xmlname::matches_name_production; use crate::dom::canvasrenderingcontext2d::CanvasRenderingContext2D; use crate::dom::cdatasection::CDATASection; use crate::dom::clipboardevent::{ClipboardEvent, ClipboardEventType}; @@ -5507,8 +5508,9 @@ impl DocumentMethods for Document { options: StringOrElementCreationOptions, can_gc: CanGc, ) -> Fallible> { - // Step 1. If localName does not match the Name production, then throw an "InvalidCharacterError" DOMException. - if !matches_name_production(&local_name) { + // Step 1. If localName is not a valid element local name, + // then throw an "InvalidCharacterError" DOMException. + if !is_valid_element_local_name(&local_name) { debug!("Not a valid element name"); return Err(Error::InvalidCharacter); } @@ -5549,9 +5551,11 @@ impl DocumentMethods for Document { options: StringOrElementCreationOptions, can_gc: CanGc, ) -> Fallible> { - // Step 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName - // to validate and extract. - let (namespace, prefix, local_name) = validate_and_extract(namespace, &qualified_name)?; + // Step 1. Let (namespace, prefix, localName) be the result of + // validating and extracting namespace and qualifiedName given "element". + let context = domname::Context::Element; + let (namespace, prefix, local_name) = + domname::validate_and_extract(namespace, &qualified_name, context)?; // Step 2. Let is be null. // Step 3. If options is a dictionary and options["is"] exists, then set is to it. @@ -5577,10 +5581,10 @@ impl DocumentMethods for Document { /// fn CreateAttribute(&self, mut local_name: DOMString, can_gc: CanGc) -> Fallible> { - // Step 1. If localName does not match the Name production in XML, - // then throw an "InvalidCharacterError" DOMException. - if !matches_name_production(&local_name) { - debug!("Not a valid element name"); + // Step 1. If localName is not a valid attribute local name, + // then throw an "InvalidCharacterError" DOMException + if !is_valid_attribute_local_name(&local_name) { + debug!("Not a valid attribute name"); return Err(Error::InvalidCharacter); } if self.is_html_document { @@ -5608,7 +5612,11 @@ impl DocumentMethods for Document { qualified_name: DOMString, can_gc: CanGc, ) -> Fallible> { - let (namespace, prefix, local_name) = validate_and_extract(namespace, &qualified_name)?; + // Step 1. Let (namespace, prefix, localName) be the result of validating and + // extracting namespace and qualifiedName given "attribute". + let context = domname::Context::Attribute; + let (namespace, prefix, local_name) = + domname::validate_and_extract(namespace, &qualified_name, context)?; let value = AttrValue::String("".to_owned()); let qualified_name = LocalName::from(qualified_name); Ok(Attr::new( diff --git a/components/script/dom/domimplementation.rs b/components/script/dom/domimplementation.rs index a6c95fad820..bfd9c9967aa 100644 --- a/components/script/dom/domimplementation.rs +++ b/components/script/dom/domimplementation.rs @@ -4,6 +4,7 @@ use dom_struct::dom_struct; use html5ever::{local_name, ns}; +use script_bindings::error::Error; use script_traits::DocumentActivity; use crate::document_loader::DocumentLoader; @@ -13,14 +14,12 @@ use crate::dom::bindings::codegen::Bindings::DocumentBinding::{ }; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::codegen::UnionTypes::StringOrElementCreationOptions; +use crate::dom::bindings::domname::{is_valid_doctype_name, namespace_from_domstring}; use crate::dom::bindings::error::Fallible; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object}; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::bindings::str::DOMString; -use crate::dom::bindings::xmlname::{ - namespace_from_domstring, validate_and_extract_qualified_name, -}; use crate::dom::document::{Document, DocumentSource, HasBrowsingContext, IsHTMLDocument}; use crate::dom::documenttype::DocumentType; use crate::dom::htmlbodyelement::HTMLBodyElement; @@ -58,6 +57,7 @@ impl DOMImplementation { } // https://dom.spec.whatwg.org/#domimplementation +#[allow(non_snake_case)] impl DOMImplementationMethods for DOMImplementation { /// fn CreateDocumentType( @@ -67,8 +67,12 @@ impl DOMImplementationMethods for DOMImplementation { sysid: DOMString, can_gc: CanGc, ) -> Fallible> { - // Step 1. Validate qualifiedName. - validate_and_extract_qualified_name(&qualified_name)?; + // Step 1. If name is not a valid doctype name, then throw an + // "InvalidCharacterError" DOMException. + if !is_valid_doctype_name(&qualified_name) { + debug!("Not a valid doctype name"); + return Err(Error::InvalidCharacter); + } Ok(DocumentType::new( qualified_name, diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 6a457f58784..9f001003ed9 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -88,15 +88,16 @@ use crate::dom::bindings::codegen::UnionTypes::{ NodeOrString, TrustedHTMLOrNullIsEmptyString, TrustedHTMLOrString, TrustedScriptURLOrUSVString, }; use crate::dom::bindings::conversions::DerivedFrom; +use crate::dom::bindings::domname::{ + self, is_valid_attribute_local_name, namespace_from_domstring, +}; use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::refcounted::{Trusted, TrustedPromise}; use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom, ToLayout}; use crate::dom::bindings::str::{DOMString, USVString}; -use crate::dom::bindings::xmlname::{ - matches_name_production, namespace_from_domstring, validate_and_extract, -}; +use crate::dom::bindings::xmlname::matches_name_production; use crate::dom::characterdata::CharacterData; use crate::dom::create::create_element; use crate::dom::csp::{CspReporting, InlineCheckType}; @@ -2664,6 +2665,7 @@ impl Element { } } +#[allow(non_snake_case)] impl ElementMethods for Element { // https://dom.spec.whatwg.org/#dom-element-namespaceuri fn GetNamespaceURI(&self) -> Option { @@ -2784,8 +2786,9 @@ impl ElementMethods for Element { force: Option, can_gc: CanGc, ) -> Fallible { - // Step 1. - if !matches_name_production(&name) { + // Step 1. If qualifiedName is not a valid attribute local name, + // then throw an "InvalidCharacterError" DOMException. + if !is_valid_attribute_local_name(&name) { return Err(Error::InvalidCharacter); } @@ -2827,9 +2830,9 @@ impl ElementMethods for Element { /// fn SetAttribute(&self, name: DOMString, value: DOMString, can_gc: CanGc) -> ErrorResult { - // Step 1. If qualifiedName does not match the Name production in XML, - // then throw an "InvalidCharacterError" DOMException. - if !matches_name_production(&name) { + // Step 1. If qualifiedName is not a valid attribute local name, + // then throw an "InvalidCharacterError" DOMException. + if !is_valid_attribute_local_name(&name) { return Err(Error::InvalidCharacter); } @@ -2858,7 +2861,11 @@ impl ElementMethods for Element { value: DOMString, can_gc: CanGc, ) -> ErrorResult { - let (namespace, prefix, local_name) = validate_and_extract(namespace, &qualified_name)?; + // Step 1. Let (namespace, prefix, localName) be the result of validating and + // extracting namespace and qualifiedName given "element". + let context = domname::Context::Element; + let (namespace, prefix, local_name) = + domname::validate_and_extract(namespace, &qualified_name, context)?; let qualified_name = LocalName::from(qualified_name); let value = self.parse_attribute(&namespace, &local_name, value); self.set_first_matching_attribute( diff --git a/components/script/dom/htmlcollection.rs b/components/script/dom/htmlcollection.rs index 4db3dc514ba..ef42f255aab 100644 --- a/components/script/dom/htmlcollection.rs +++ b/components/script/dom/htmlcollection.rs @@ -11,12 +11,12 @@ use style::str::split_html_space_chars; use stylo_atoms::Atom; use crate::dom::bindings::codegen::Bindings::HTMLCollectionBinding::HTMLCollectionMethods; +use crate::dom::bindings::domname::namespace_from_domstring; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object}; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; use crate::dom::bindings::str::DOMString; use crate::dom::bindings::trace::JSTraceable; -use crate::dom::bindings::xmlname::namespace_from_domstring; use crate::dom::element::Element; use crate::dom::node::{Node, NodeTraits}; use crate::dom::window::Window; diff --git a/components/script/dom/namednodemap.rs b/components/script/dom/namednodemap.rs index 45b7708bd4c..6159bb3a797 100644 --- a/components/script/dom/namednodemap.rs +++ b/components/script/dom/namednodemap.rs @@ -8,11 +8,11 @@ use html5ever::LocalName; use crate::dom::attr::Attr; use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; use crate::dom::bindings::codegen::Bindings::NamedNodeMapBinding::NamedNodeMapMethods; +use crate::dom::bindings::domname::namespace_from_domstring; use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object}; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::bindings::str::DOMString; -use crate::dom::bindings::xmlname::namespace_from_domstring; use crate::dom::element::Element; use crate::dom::window::Window; use crate::script_runtime::CanGc; diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index fc36965c17d..db946f6a8e6 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -76,6 +76,7 @@ use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::codegen::UnionTypes::NodeOrString; use crate::dom::bindings::conversions::{self, DerivedFrom}; +use crate::dom::bindings::domname::namespace_from_domstring; use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::{ Castable, CharacterDataTypeId, ElementTypeId, EventTargetTypeId, HTMLElementTypeId, NodeTypeId, @@ -85,7 +86,6 @@ use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::{DomObject, DomObjectWrap, reflect_dom_object_with_proto}; use crate::dom::bindings::root::{Dom, DomRoot, DomSlice, LayoutDom, MutNullableDom, ToLayout}; use crate::dom::bindings::str::{DOMString, USVString}; -use crate::dom::bindings::xmlname::namespace_from_domstring; use crate::dom::characterdata::{CharacterData, LayoutCharacterDataHelpers}; use crate::dom::cssstylesheet::CSSStyleSheet; use crate::dom::customelementregistry::{CallbackReaction, try_upgrade_element}; diff --git a/components/script/xpath/eval.rs b/components/script/xpath/eval.rs index 304af01aa09..5d4407ab019 100644 --- a/components/script/xpath/eval.rs +++ b/components/script/xpath/eval.rs @@ -4,7 +4,7 @@ use std::fmt; -use html5ever::{QualName, local_name, namespace_prefix, ns}; +use html5ever::{LocalName, Namespace, Prefix, QualName, local_name, namespace_prefix, ns}; use super::parser::{ AdditiveOp, Axis, EqualityOp, Expr, FilterExpr, KindTest, Literal, MultiplicativeOp, NodeTest, @@ -14,9 +14,11 @@ use super::parser::{ use super::{EvaluationCtx, Value}; use crate::dom::attr::Attr; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; +use crate::dom::bindings::domname::namespace_from_domstring; use crate::dom::bindings::inheritance::{Castable, CharacterDataTypeId, NodeTypeId}; use crate::dom::bindings::root::DomRoot; -use crate::dom::bindings::xmlname::validate_and_extract; +use crate::dom::bindings::str::DOMString; +use crate::dom::bindings::xmlname; use crate::dom::element::Element; use crate::dom::node::{Node, ShadowIncluding}; use crate::dom::processinginstruction::ProcessingInstruction; @@ -253,6 +255,111 @@ impl Evaluatable for PathExpr { } } +/// Error types for validate and extract a qualified name following +/// the XML naming rules. +#[derive(Debug)] +enum ValidationError { + InvalidCharacter, + Namespace, +} + +/// Validate a qualified name following the XML naming rules. +/// +/// On success, this returns a tuple `(prefix, local name)`. +fn validate_and_extract_qualified_name( + qualified_name: &str, +) -> Result<(Option<&str>, &str), ValidationError> { + if qualified_name.is_empty() { + // Qualified names must not be empty + return Err(ValidationError::InvalidCharacter); + } + let mut colon_offset = None; + let mut at_start_of_name = true; + + for (byte_position, c) in qualified_name.char_indices() { + if c == ':' { + if colon_offset.is_some() { + // Qualified names must not contain more than one colon + return Err(ValidationError::InvalidCharacter); + } + colon_offset = Some(byte_position); + at_start_of_name = true; + continue; + } + + if at_start_of_name { + if !xmlname::is_valid_start(c) { + // Name segments must begin with a valid start character + return Err(ValidationError::InvalidCharacter); + } + at_start_of_name = false; + } else if !xmlname::is_valid_continuation(c) { + // Name segments must consist of valid characters + return Err(ValidationError::InvalidCharacter); + } + } + + let Some(colon_offset) = colon_offset else { + // Simple case: there is no prefix + return Ok((None, qualified_name)); + }; + + let (prefix, local_name) = qualified_name.split_at(colon_offset); + let local_name = &local_name[1..]; // Remove the colon + + if prefix.is_empty() || local_name.is_empty() { + // Neither prefix nor local name can be empty + return Err(ValidationError::InvalidCharacter); + } + + Ok((Some(prefix), local_name)) +} + +/// Validate a namespace and qualified name following the XML naming rules +/// and extract their parts. +fn validate_and_extract( + namespace: Option, + qualified_name: &str, +) -> Result<(Namespace, Option, LocalName), ValidationError> { + // Step 1. If namespace is the empty string, then set it to null. + let namespace = namespace_from_domstring(namespace); + + // Step 2. Validate qualifiedName. + // Step 3. Let prefix be null. + // Step 4. Let localName be qualifiedName. + // Step 5. If qualifiedName contains a U+003A (:): + // NOTE: validate_and_extract_qualified_name does all of these things for us, because + // it's easier to do them together + let (prefix, local_name) = validate_and_extract_qualified_name(qualified_name)?; + debug_assert!(!local_name.contains(':')); + + match (namespace, prefix) { + (ns!(), Some(_)) => { + // Step 6. If prefix is non-null and namespace is null, then throw a "NamespaceError" DOMException. + Err(ValidationError::Namespace) + }, + (ref ns, Some("xml")) if ns != &ns!(xml) => { + // Step 7. If prefix is "xml" and namespace is not the XML namespace, + // then throw a "NamespaceError" DOMException. + Err(ValidationError::Namespace) + }, + (ref ns, p) if ns != &ns!(xmlns) && (qualified_name == "xmlns" || p == Some("xmlns")) => { + // Step 8. If either qualifiedName or prefix is "xmlns" and namespace is not the XMLNS namespace, + // then throw a "NamespaceError" DOMException. + Err(ValidationError::Namespace) + }, + (ns!(xmlns), p) if qualified_name != "xmlns" && p != Some("xmlns") => { + // Step 9. If namespace is the XMLNS namespace and neither qualifiedName nor prefix is "xmlns", + // then throw a "NamespaceError" DOMException. + Err(ValidationError::Namespace) + }, + (ns, p) => { + // Step 10. Return namespace, prefix, and localName. + Ok((ns, p.map(Prefix::from), LocalName::from(local_name))) + }, + } +} + pub(crate) struct QualNameConverter<'a> { qname: &'a ParserQualName, context: &'a EvaluationCtx, diff --git a/tests/wpt/meta/dom/nodes/DOMImplementation-createDocument.html.ini b/tests/wpt/meta/dom/nodes/DOMImplementation-createDocument.html.ini deleted file mode 100644 index a09911a0602..00000000000 --- a/tests/wpt/meta/dom/nodes/DOMImplementation-createDocument.html.ini +++ /dev/null @@ -1,216 +0,0 @@ -[DOMImplementation-createDocument.html] - [createDocument test: null,";foo",null,null] - expected: FAIL - - [createDocument test: metadata for null,";foo",null] - expected: FAIL - - [createDocument test: characterSet aliases for null,";foo",null] - expected: FAIL - - [createDocument test: null,"f}oo",null,null] - expected: FAIL - - [createDocument test: metadata for null,"f}oo",null] - expected: FAIL - - [createDocument test: characterSet aliases for null,"f}oo",null] - expected: FAIL - - [createDocument test: null,"foo}",null,null] - expected: FAIL - - [createDocument test: metadata for null,"foo}",null] - expected: FAIL - - [createDocument test: characterSet aliases for null,"foo}",null] - expected: FAIL - - [createDocument test: null,"\\ufffffoo",null,null] - expected: FAIL - - [createDocument test: metadata for null,"\\ufffffoo",null] - expected: FAIL - - [createDocument test: characterSet aliases for null,"\\ufffffoo",null] - expected: FAIL - - [createDocument test: null,"f\\uffffoo",null,null] - expected: FAIL - - [createDocument test: metadata for null,"f\\uffffoo",null] - expected: FAIL - - [createDocument test: characterSet aliases for null,"f\\uffffoo",null] - expected: FAIL - - [createDocument test: null,"foo\\uffff",null,null] - expected: FAIL - - [createDocument test: metadata for null,"foo\\uffff",null] - expected: FAIL - - [createDocument test: characterSet aliases for null,"foo\\uffff",null] - expected: FAIL - - [createDocument test: null,"f