diff options
Diffstat (limited to 'components/script/dom/bindings/xmlname.rs')
-rw-r--r-- | components/script/dom/bindings/xmlname.rs | 219 |
1 files changed, 103 insertions, 116 deletions
diff --git a/components/script/dom/bindings/xmlname.rs b/components/script/dom/bindings/xmlname.rs index 6a859f2e5eb..fa72e7c4c92 100644 --- a/components/script/dom/bindings/xmlname.rs +++ b/components/script/dom/bindings/xmlname.rs @@ -6,17 +6,91 @@ use html5ever::{namespace_url, ns, LocalName, Namespace, Prefix}; -use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; +use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::str::DOMString; +/// Check if an element name is valid. See <http://www.w3.org/TR/xml/#NT-Name> +/// for details. +fn is_valid_start(c: char) -> bool { + matches!(c, ':' | + 'A'..='Z' | + '_' | + 'a'..='z' | + '\u{C0}'..='\u{D6}' | + '\u{D8}'..='\u{F6}' | + '\u{F8}'..='\u{2FF}' | + '\u{370}'..='\u{37D}' | + '\u{37F}'..='\u{1FFF}' | + '\u{200C}'..='\u{200D}' | + '\u{2070}'..='\u{218F}' | + '\u{2C00}'..='\u{2FEF}' | + '\u{3001}'..='\u{D7FF}' | + '\u{F900}'..='\u{FDCF}' | + '\u{FDF0}'..='\u{FFFD}' | + '\u{10000}'..='\u{EFFFF}') +} + +fn is_valid_continuation(c: char) -> bool { + is_valid_start(c) || + matches!(c, + '-' | + '.' | + '0'..='9' | + '\u{B7}' | + '\u{300}'..='\u{36F}' | + '\u{203F}'..='\u{2040}') +} + /// Validate a qualified name. See <https://dom.spec.whatwg.org/#validate> for details. -pub(crate) fn validate_qualified_name(qualified_name: &str) -> ErrorResult { - // Step 2. - match xml_name_type(qualified_name) { - XMLName::Invalid => Err(Error::InvalidCharacter), - XMLName::Name => Err(Error::InvalidCharacter), // see whatwg/dom#671 - XMLName::QName => Ok(()), +/// +/// 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. @@ -25,139 +99,52 @@ pub(crate) fn validate_and_extract( namespace: Option<DOMString>, qualified_name: &str, ) -> Fallible<(Namespace, Option<Prefix>, LocalName)> { - // Step 1. + // Step 1. If namespace is the empty string, then set it to null. let namespace = namespace_from_domstring(namespace); - // Step 2. - validate_qualified_name(qualified_name)?; - - let colon = ':'; - - // Step 5. - let mut parts = qualified_name.splitn(2, colon); - - let (maybe_prefix, local_name) = { - let maybe_prefix = parts.next(); - let maybe_local_name = parts.next(); - - debug_assert!(parts.next().is_none()); + // 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(':')); - if let Some(local_name) = maybe_local_name { - debug_assert!(!maybe_prefix.unwrap().is_empty()); - - (maybe_prefix, local_name) - } else { - (None, maybe_prefix.unwrap()) - } - }; - - debug_assert!(!local_name.contains(colon)); - - match (namespace, maybe_prefix) { + match (namespace, prefix) { (ns!(), Some(_)) => { - // Step 6. + // 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. + // 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. + // 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. + // 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. + // Step 10. Return namespace, prefix, and localName. Ok((ns, p.map(Prefix::from), LocalName::from(local_name))) }, } } -/// Results of `xml_name_type`. -#[derive(PartialEq)] -#[allow(missing_docs)] -pub(crate) enum XMLName { - QName, - Name, - Invalid, -} - -/// Check if an element name is valid. See <http://www.w3.org/TR/xml/#NT-Name> -/// for details. -pub(crate) fn xml_name_type(name: &str) -> XMLName { - fn is_valid_start(c: char) -> bool { - matches!(c, ':' | - 'A'..='Z' | - '_' | - 'a'..='z' | - '\u{C0}'..='\u{D6}' | - '\u{D8}'..='\u{F6}' | - '\u{F8}'..='\u{2FF}' | - '\u{370}'..='\u{37D}' | - '\u{37F}'..='\u{1FFF}' | - '\u{200C}'..='\u{200D}' | - '\u{2070}'..='\u{218F}' | - '\u{2C00}'..='\u{2FEF}' | - '\u{3001}'..='\u{D7FF}' | - '\u{F900}'..='\u{FDCF}' | - '\u{FDF0}'..='\u{FFFD}' | - '\u{10000}'..='\u{EFFFF}') - } - - fn is_valid_continuation(c: char) -> bool { - is_valid_start(c) || - matches!(c, - '-' | - '.' | - '0'..='9' | - '\u{B7}' | - '\u{300}'..='\u{36F}' | - '\u{203F}'..='\u{2040}') - } - +pub(crate) fn matches_name_production(name: &str) -> bool { let mut iter = name.chars(); - let mut non_qname_colons = false; - let mut seen_colon = false; - let mut last = match iter.next() { - None => return XMLName::Invalid, - Some(c) => { - if !is_valid_start(c) { - return XMLName::Invalid; - } - if c == ':' { - non_qname_colons = true; - } - c - }, - }; - - for c in iter { - if !is_valid_continuation(c) { - return XMLName::Invalid; - } - if c == ':' { - if seen_colon { - non_qname_colons = true; - } else { - seen_colon = true; - } - } - last = c - } - - if last == ':' { - non_qname_colons = true - } - if non_qname_colons { - XMLName::Name - } else { - XMLName::QName + if iter.next().is_none_or(|c| !is_valid_start(c)) { + return false; } + iter.all(is_valid_continuation) } /// Convert a possibly-null URL to a namespace. |