/* 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 and extracting qualified XML names. use html5ever::{LocalName, Namespace, Prefix, namespace_url, 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 { 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 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(); 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. /// /// 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), } }