From 414e9b64fc05a06440ffbdf0f91769f46a21dce6 Mon Sep 17 00:00:00 2001 From: FadeHack Date: Sun, 23 Aug 2026 05:33:55 +0530 Subject: [PATCH] Compare prefix and local name when looking for duplicate attributes The duplicate check compared the raw attribute name we had just read against the local names of the attributes already on the tag. The stored ones have been through process_qname by then, so xml:lang is sitting there as prefix "xml" with local name "lang", and reading a plain lang right after it matched and got rejected as a duplicate. That is why it was order dependent. Writing lang first and xml:lang second was fine, because the raw name "xml:lang" never matches the local name "lang". Namespaces in XML section 6.3 says attributes are the same only when their expanded names are the same, and xml:lang and lang have different expanded names, so both orderings should parse cleanly. This also shows up on the WPT test table-align-float.xhtml, whose root element carries both. So process the name first and compare the prefix along with the local name. Real duplicates, including two attributes with the same prefix, are still reported. The tokenizer has no namespace bindings yet, they are resolved later in XmlTreeBuilder::bind_qname, so two different prefixes bound to the same namespace still slip through here. That is a separate thing and it needs to be handled in the tree builder. Fixes #775 --- xml5ever/src/tokenizer/mod.rs | 86 ++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/xml5ever/src/tokenizer/mod.rs b/xml5ever/src/tokenizer/mod.rs index 0d87e637..1ff184ee 100644 --- a/xml5ever/src/tokenizer/mod.rs +++ b/xml5ever/src/tokenizer/mod.rs @@ -1273,27 +1273,26 @@ impl XmlTokenizer { return; } - // Check for a duplicate attribute. + let qname = process_qname(replace( + &mut self.current_attr_name.borrow_mut(), + StrTendril::new(), + )); + + // Check for a duplicate attribute. Two attributes are the same only if + // both their prefix and their local name match, so xml:lang and lang + // are distinct names and may sit on the same element. // FIXME: the spec says we should error as soon as the name is finished. // FIXME: linear time search, do we care? - let dup = { - let current_attr_name = self.current_attr_name.borrow(); - let name = ¤t_attr_name[..]; - self.current_tag_attrs - .borrow() - .iter() - .any(|a| &*a.name.local == name) - }; + let dup = self + .current_tag_attrs + .borrow() + .iter() + .any(|a| a.name.prefix == qname.prefix && a.name.local == qname.local); if dup { self.emit_error(Borrowed("Duplicate attribute")); - self.current_attr_name.borrow_mut().clear(); self.current_attr_value.borrow_mut().clear(); } else { - let qname = process_qname(replace( - &mut self.current_attr_name.borrow_mut(), - StrTendril::new(), - )); let attr = Attribute { name: qname.clone(), value: replace(&mut self.current_attr_value.borrow_mut(), StrTendril::new()), @@ -1319,9 +1318,64 @@ impl XmlTokenizer { #[cfg(test)] mod test { - use super::process_qname; - use crate::tendril::SliceExt; + use super::{process_qname, ProcessResult, Token, TokenSink, XmlTokenizer}; + use crate::tendril::{SliceExt, StrTendril}; use crate::{LocalName, Prefix}; + use markup5ever::buffer_queue::BufferQueue; + use std::cell::RefCell; + + struct ErrorCollector { + errors: RefCell>, + } + + impl TokenSink for ErrorCollector { + type Handle = (); + + fn process_token(&self, token: Token) -> ProcessResult<()> { + if let Token::ParseError(error) = token { + self.errors.borrow_mut().push(error.to_string()); + } + ProcessResult::Continue + } + } + + fn tokenize_errors(input: &str) -> Vec { + let sink = ErrorCollector { + errors: RefCell::new(Vec::new()), + }; + let queue = BufferQueue::default(); + queue.push_back(StrTendril::from(input)); + let tokenizer = XmlTokenizer::new(sink, Default::default()); + let _ = tokenizer.feed(&queue); + tokenizer.end(); + tokenizer.sink.errors.into_inner() + } + + #[test] + fn qualified_and_unqualified_names_are_distinct() { + // xml:lang and lang have different expanded names, so both orderings + // are fine. See https://www.w3.org/TR/REC-xml-names/#uniqAttrs + assert!(tokenize_errors(r#""#).is_empty()); + assert!(tokenize_errors(r#""#).is_empty()); + } + + #[test] + fn different_prefixes_are_distinct() { + assert!(tokenize_errors(r#""#).is_empty()); + } + + #[test] + fn real_duplicates_are_still_reported() { + assert_eq!( + tokenize_errors(r#""#), + vec!["Duplicate attribute".to_owned()] + ); + + assert_eq!( + tokenize_errors(r#""#), + vec!["Duplicate attribute".to_owned()] + ); + } #[test] fn simple_namespace() {