mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 23:32:14 +00:00
JavaScript: XML rejects reserved namespace bindings and names
The XML parser now throws SyntaxError when markup binds the xml or xmlns prefix or either fixed URI against the Namespaces in XML rules. addNamespace, setName, setLocalName and setNamespace throw TypeError for the same bindings and leave the node unchanged. A new element or attribute cannot be in the xmlns namespace, and an attribute in no namespace cannot be named xmlns. The xmlns URI cannot be the default XML namespace. These assignments throw TypeError.
This commit is contained in:
@@ -27,6 +27,8 @@ fn parser_units(s: &str) -> Vec<u16> {
|
||||
|
||||
pub(crate) const STAR: &[u16] = &[0x2A];
|
||||
const XML_URI: &str = "http://www.w3.org/XML/1998/namespace";
|
||||
const XMLNS_URI: &str = "http://www.w3.org/2000/xmlns/";
|
||||
pub(crate) const RESERVED_NS_BINDING: &str = "a namespace declaration uses a reserved prefix or URI";
|
||||
/// The hidden binding that holds a function's default XML namespace URI.
|
||||
const DEFAULT_NS_BINDING: &[u16] = &[0, 0x64, 0x78, 0x6E, 0x73];
|
||||
|
||||
@@ -221,6 +223,30 @@ fn prefix_in_scope<'a>(stack: &'a [XmlNs], prefix: &[u16]) -> Option<&'a XmlNs>
|
||||
stack.iter().rev().find(|ns| ns.prefix.as_deref() == Some(prefix))
|
||||
}
|
||||
|
||||
/// Whether Namespaces in XML allows declaring `prefix` for `uri`: `xml` only with its fixed URI,
|
||||
/// `xmlns` never, and neither fixed URI with another prefix or as the default namespace.
|
||||
pub(crate) fn ns_binding_allowed(prefix: &[u16], uri: &[u16]) -> bool {
|
||||
if units_are(prefix, "xmlns") {
|
||||
return false;
|
||||
}
|
||||
let is_xml_uri = units_are(uri, XML_URI);
|
||||
if units_are(prefix, "xml") {
|
||||
return is_xml_uri;
|
||||
}
|
||||
!is_xml_uri && !units_are(uri, XMLNS_URI)
|
||||
}
|
||||
|
||||
/// Whether a node named `name` can be written as XML: no name is in the `xmlns` namespace, and
|
||||
/// no attribute in no namespace is named `xmlns`.
|
||||
pub(crate) fn xml_name_writable(name: &XmlName, attribute: bool) -> bool {
|
||||
let uri = name.uri.as_deref().unwrap_or_default();
|
||||
!units_are(uri, XMLNS_URI) && !(attribute && uri.is_empty() && units_are(&name.local, "xmlns"))
|
||||
}
|
||||
|
||||
fn units_are(units: &[u16], text: &str) -> bool {
|
||||
units.iter().copied().eq(text.encode_utf16())
|
||||
}
|
||||
|
||||
/// A prefix no entry of `stack` uses.
|
||||
fn fresh_prefix(stack: &[XmlNs]) -> Key {
|
||||
let mut n = 0u32;
|
||||
@@ -361,6 +387,12 @@ impl Interp {
|
||||
|
||||
/// A new node. What `node` refers to must be reachable from the roots.
|
||||
pub(crate) fn new_xml_node(&mut self, node: XmlNode) -> JsResult<ObjId> {
|
||||
if let Some(name) = &node.name
|
||||
&& matches!(node.kind, XmlKind::Element | XmlKind::Attribute)
|
||||
&& !xml_name_writable(name, node.kind == XmlKind::Attribute)
|
||||
{
|
||||
return Err(self.type_error(RESERVED_NS_BINDING));
|
||||
}
|
||||
let extra = XML_NODE_BYTES + node.name.as_ref().map_or(0, name_bytes) + string_bytes(node.value.len());
|
||||
self.reserve_heap(OBJECT_BYTES + extra)?;
|
||||
self.heap.bytes = self.heap.bytes.saturating_add(extra);
|
||||
@@ -432,6 +464,9 @@ impl Interp {
|
||||
|
||||
/// Sets the default XML namespace of the running function, or of the global code.
|
||||
pub(crate) fn xml_set_default_uri(&mut self, uri: Key) -> JsResult<()> {
|
||||
if units_are(&uri, XMLNS_URI) {
|
||||
return Err(self.type_error(RESERVED_NS_BINDING));
|
||||
}
|
||||
let env = self.frame()?.var_env;
|
||||
if let Some(Env::Decl { bindings, .. }) = self.heap.env_mut(env) {
|
||||
let value = Value::String(Str::from_rc(uri));
|
||||
@@ -2151,6 +2186,9 @@ impl Interp {
|
||||
for (start, end, value) in &raw_attrs {
|
||||
let raw = src.get(*start..*end).unwrap_or_default();
|
||||
if raw == key("xmlns").as_ref() {
|
||||
if !ns_binding_allowed(&[], value) {
|
||||
return Err(self.syntax(RESERVED_NS_BINDING));
|
||||
}
|
||||
if !seen_decl.insert(Vec::new()) {
|
||||
return Err(self.syntax("a namespace is declared twice"));
|
||||
}
|
||||
@@ -2161,6 +2199,9 @@ impl Interp {
|
||||
if value.is_empty() {
|
||||
return Err(self.syntax("a namespace prefix cannot be declared with an empty URI"));
|
||||
}
|
||||
if !ns_binding_allowed(local, value) {
|
||||
return Err(self.syntax(RESERVED_NS_BINDING));
|
||||
}
|
||||
if !seen_decl.insert(local.to_vec()) {
|
||||
return Err(self.syntax("a namespace is declared twice"));
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ use crate::heap::{
|
||||
use crate::interp::{Abrupt, Interp, JsResult};
|
||||
use crate::string_builtins::{new_array, set_element};
|
||||
use crate::value::{Str, Value};
|
||||
use crate::xml::{Access, Content, NameRef, STAR, Xv, canonical_index, is_ncname, is_script_xml_name};
|
||||
use crate::xml::{
|
||||
Access, Content, NameRef, RESERVED_NS_BINDING, STAR, Xv, canonical_index, is_ncname, is_script_xml_name, ns_binding_allowed, xml_name_writable,
|
||||
};
|
||||
|
||||
const SETTINGS: [(&str, Value); 5] = [
|
||||
("ignoreComments", Value::Bool(false)),
|
||||
@@ -807,6 +809,9 @@ fn add_in_scope(interp: &mut Interp, x: ObjId, ns: XmlNs) -> JsResult<()> {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(prefix) = ns.prefix.clone() else { return Ok(()) };
|
||||
if !ns_binding_allowed(&prefix, &ns.uri) {
|
||||
return Err(interp.type_error(RESERVED_NS_BINDING));
|
||||
}
|
||||
let node = interp.xml_node(x)?;
|
||||
if prefix.is_empty() && node.name.as_ref().and_then(|n| n.uri.as_ref()).is_some_and(|u| u.is_empty()) {
|
||||
return Ok(());
|
||||
@@ -998,7 +1003,8 @@ fn checked_local(interp: &mut Interp, local: &Key) -> JsResult<()> {
|
||||
|
||||
fn set_local_name(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let x = this_node(interp, &this)?;
|
||||
if matches!(interp.xml_kind(x)?, XmlKind::Text | XmlKind::Comment) {
|
||||
let kind = interp.xml_kind(x)?;
|
||||
if matches!(kind, XmlKind::Text | XmlKind::Comment) {
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
let v = arg(args, 0);
|
||||
@@ -1007,12 +1013,26 @@ fn set_local_name(interp: &mut Interp, this: Value, args: &[Value], _: bool) ->
|
||||
None => interp.to_str(&v)?.flat(),
|
||||
};
|
||||
checked_local(interp, &local)?;
|
||||
if kind == XmlKind::Attribute
|
||||
&& let Some(old) = interp.xml_node(x)?.name.clone()
|
||||
{
|
||||
check_name_binding(interp, &XmlName { local: local.clone(), ..old }, true)?;
|
||||
}
|
||||
if let Some(n) = &mut interp.xml_node_mut(x)?.name {
|
||||
n.local = local;
|
||||
}
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
/// Throws when an element or attribute named `name` could not be written as XML.
|
||||
fn check_name_binding(interp: &mut Interp, name: &XmlName, attribute: bool) -> JsResult<()> {
|
||||
let uri = name.uri.as_deref().unwrap_or_default();
|
||||
if !name.prefix.as_ref().is_none_or(|prefix| ns_binding_allowed(prefix, uri)) || !xml_name_writable(name, attribute) {
|
||||
return Err(interp.type_error(RESERVED_NS_BINDING));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name_set_in_scope(interp: &mut Interp, x: ObjId, name: &XmlName) -> JsResult<()> {
|
||||
let ns = XmlNs { prefix: name.prefix.clone(), uri: name.uri.clone().unwrap_or_else(empty) };
|
||||
match interp.xml_kind(x)? {
|
||||
@@ -1040,6 +1060,8 @@ fn set_name(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResu
|
||||
checked_local(interp, &name.local)?;
|
||||
if kind == XmlKind::Pi {
|
||||
name.uri = Some(empty());
|
||||
} else {
|
||||
check_name_binding(interp, &name, kind == XmlKind::Attribute)?;
|
||||
}
|
||||
interp.xml_node_mut(x)?.name = Some(name.clone());
|
||||
name_set_in_scope(interp, x, &name)?;
|
||||
@@ -1048,12 +1070,14 @@ fn set_name(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResu
|
||||
|
||||
fn set_namespace(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let x = this_node(interp, &this)?;
|
||||
if matches!(interp.xml_kind(x)?, XmlKind::Text | XmlKind::Comment | XmlKind::Pi) {
|
||||
let kind = interp.xml_kind(x)?;
|
||||
if matches!(kind, XmlKind::Text | XmlKind::Comment | XmlKind::Pi) {
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
let ns = build_namespace(interp, std::slice::from_ref(&arg(args, 0)))?;
|
||||
let Some(old) = interp.xml_node(x)?.name.clone() else { return Ok(Value::Undefined) };
|
||||
let name = XmlName { uri: Some(ns.uri), local: old.local, prefix: ns.prefix };
|
||||
check_name_binding(interp, &name, kind == XmlKind::Attribute)?;
|
||||
interp.xml_node_mut(x)?.name = Some(name.clone());
|
||||
name_set_in_scope(interp, x, &name)?;
|
||||
Ok(Value::Undefined)
|
||||
|
||||
@@ -360,6 +360,84 @@ fn namespaces_survive_serialization() {
|
||||
);
|
||||
}
|
||||
|
||||
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
|
||||
const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
|
||||
|
||||
#[test]
|
||||
fn markup_cannot_declare_a_reserved_namespace_binding() {
|
||||
let declarations = [
|
||||
"xmlns:xml='wrong'".to_string(),
|
||||
"xmlns:xmlns='u'".to_string(),
|
||||
format!("xmlns:xmlns='{XMLNS_NS}'"),
|
||||
format!("xmlns:p='{XML_NS}'"),
|
||||
format!("xmlns:p='{XMLNS_NS}'"),
|
||||
format!("xmlns='{XML_NS}'"),
|
||||
format!("xmlns='{XMLNS_NS}'"),
|
||||
];
|
||||
for declaration in declarations {
|
||||
assert_eq!(thrown_name(&format!("<a {declaration}/>")), "SyntaxError", "{declaration}");
|
||||
assert_eq!(thrown_name(&format!("new XML(\"<a {declaration}/>\")")), "SyntaxError", "{declaration}");
|
||||
}
|
||||
assert_eq!(string(&format!("(<a xmlns:xml='{XML_NS}'/>).toXMLString()")), format!("<a xmlns:xml=\"{XML_NS}\"/>"));
|
||||
assert_eq!(string("(<a xmlns=''/>).toXMLString()"), "<a xmlns=\"\"/>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_namespace_rejects_a_reserved_binding_and_keeps_the_element() {
|
||||
for (prefix, uri) in [("xmlns", "u"), ("xmlns", XMLNS_NS), ("xml", "u"), ("p", XML_NS), ("p", XMLNS_NS)] {
|
||||
let src =
|
||||
format!("var x = <a/>; var name = 'none'; try {{ x.addNamespace(new Namespace('{prefix}', '{uri}')); }} catch (e) {{ name = e.name; }}");
|
||||
assert_eq!(string(&format!("{src} name")), "TypeError", "{prefix} {uri}");
|
||||
assert_eq!(string(&format!("{src} x.toXMLString()")), "<a/>", "{prefix} {uri}");
|
||||
}
|
||||
assert_eq!(
|
||||
string(&format!("var x = <a/>; x.addNamespace(new Namespace('xml', '{XML_NS}')); x.toXMLString()")),
|
||||
format!("<a xmlns:xml=\"{XML_NS}\"/>")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_cannot_write_a_reserved_namespace_binding() {
|
||||
for setter in [
|
||||
"setNamespace(new Namespace('xmlns', 'u'))",
|
||||
"setName(new QName(new Namespace('xml', 'u'), 'b'))",
|
||||
"setNamespace(new Namespace('p', 'http://www.w3.org/2000/xmlns/'))",
|
||||
"setNamespace('http://www.w3.org/2000/xmlns/')",
|
||||
] {
|
||||
let src = format!("var x = <a/>; var name = 'none'; try {{ x.{setter}; }} catch (e) {{ name = e.name; }}");
|
||||
assert_eq!(string(&format!("{src} name")), "TypeError", "{setter}");
|
||||
assert_eq!(string(&format!("{src} x.toXMLString()")), "<a/>", "{setter}");
|
||||
}
|
||||
let q = |prefix: &str| format!("new QName(new Namespace('{prefix}', 'u'), 'b')");
|
||||
assert_eq!(string(&format!("var x = <a/>; x[{}] = '1'; x.toXMLString()", q("xmlns"))), "<a>\n <b xmlns=\"u\">1</b>\n</a>");
|
||||
assert_eq!(string(&format!("var x = <a/>; x.@[{}] = '1'; x.toXMLString()", q("xml"))), "<a ns0:b=\"1\" xmlns:ns0=\"u\"/>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_attribute_cannot_be_renamed_xmlns() {
|
||||
for setter in ["setName('xmlns')", "setLocalName('xmlns')"] {
|
||||
let src = format!("var x = <a b='1'/>; var name = 'none'; try {{ x.@b[0].{setter}; }} catch (e) {{ name = e.name; }}");
|
||||
assert_eq!(string(&format!("{src} name")), "TypeError", "{setter}");
|
||||
assert_eq!(string(&format!("{src} x.toXMLString()")), "<a b=\"1\"/>", "{setter}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_nodes_cannot_take_a_reserved_name() {
|
||||
let q = format!("new QName('{XMLNS_NS}', 'b')");
|
||||
for update in ["x.@xmlns = 'u'".to_string(), format!("x[{q}] = '1'"), format!("x.@[{q}] = '1'"), format!("x[{q}][0] = '1'")] {
|
||||
let src = format!("var x = <a/>; var name = 'none'; try {{ {update}; }} catch (e) {{ name = e.name; }}");
|
||||
assert_eq!(string(&format!("{src} name")), "TypeError", "{update}");
|
||||
assert_eq!(string(&format!("{src} x.toXMLString()")), "<a/>", "{update}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_xmlns_uri_cannot_be_the_default_namespace() {
|
||||
assert_eq!(thrown_name(&format!("default xml namespace = '{XMLNS_NS}'")), "TypeError");
|
||||
assert_eq!(thrown_name(&format!("setDefaultXMLNamespace('{XMLNS_NS}')")), "TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_garbage_collector_keeps_trees_and_lists_alive() {
|
||||
let mut r = realm();
|
||||
|
||||
Reference in New Issue
Block a user