From 96501ae3a6acb17e97f2223302e6f864d841e0bd Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 24 Sep 2026 18:34:22 -0700 Subject: [PATCH] XML: add namespace-aware node accessors The existing $attr$name and $tag$name accessors compare local names only, so x:ID and ID, or x:Issuer and Issuer, are indistinguishable. The new XMLNode methods getAttributeNS(namespaceURI, localName) and getChildNS(namespaceURI, localName) match both the namespace URI and the local name. --- external/njs_xml_module.c | 162 ++++++++++++++++++++++++++++++++++++ external/qjs_xml_module.c | 167 ++++++++++++++++++++++++++++++++++++++ test/ts/test.ts | 5 ++ test/xml/xml.t.mjs | 43 ++++++++++ ts/njs_modules/xml.d.ts | 23 ++++++ 5 files changed, 400 insertions(+) diff --git a/external/njs_xml_module.c b/external/njs_xml_module.c index e745a5d6..fce06d90 100644 --- a/external/njs_xml_module.c +++ b/external/njs_xml_module.c @@ -57,6 +57,12 @@ static njs_int_t njs_xml_attr_ext_prop_handler(njs_vm_t *vm, njs_value_t *unused, njs_value_t *retval); static njs_int_t njs_xml_node_ext_add_child(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); +static njs_int_t njs_xml_node_ext_get_attribute_ns(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused, + njs_value_t *retval); +static njs_int_t njs_xml_node_ext_get_child_ns(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused, + njs_value_t *retval); static njs_int_t njs_xml_node_ext_attrs(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); @@ -106,6 +112,10 @@ static njs_int_t njs_xml_node_tags_handler(njs_vm_t *vm, njs_value_t *value, static xmlNode *njs_xml_external_node(njs_vm_t *vm, njs_value_t *value); static const u_char *njs_xml_string_to_c_string(njs_vm_t *vm, njs_str_t *str, u_char *dst, size_t size); +static xmlNode *njs_xml_ns_args(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_str_t *uri, njs_str_t *local); +static njs_bool_t njs_xml_ns_match(const xmlChar *name, xmlNs *ns, + njs_str_t *uri, njs_str_t *local); static njs_bool_t njs_xml_node_is_live(xmlNode *node); static xmlNode *njs_xml_list_detach(xmlNode *parent, njs_str_t *name); static njs_int_t njs_xml_list_retire(njs_vm_t *vm, xmlNode *parent, @@ -259,6 +269,28 @@ static njs_external_t njs_ext_xml_node[] = { } }, + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("getAttributeNS"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_xml_node_ext_get_attribute_ns, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("getChildNS"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = njs_xml_node_ext_get_child_ns, + } + }, + { .flags = NJS_EXTERN_PROPERTY, .name.string = njs_str("$attrs"), @@ -760,6 +792,71 @@ njs_xml_node_ext_add_child(njs_vm_t *vm, njs_value_t *args, } +static njs_int_t +njs_xml_node_ext_get_attribute_ns(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) +{ + xmlAttr *attr; + xmlChar *text; + xmlNode *current; + njs_int_t ret; + njs_str_t uri, local; + + current = njs_xml_ns_args(vm, args, nargs, &uri, &local); + if (njs_slow_path(current == NULL)) { + return NJS_ERROR; + } + + for (attr = current->properties; attr != NULL; attr = attr->next) { + if (!njs_xml_ns_match(attr->name, attr->ns, &uri, &local)) { + continue; + } + + text = xmlNodeGetContent((xmlNode *) attr); + if (njs_slow_path(text == NULL)) { + njs_vm_internal_error(vm, "xmlNodeGetContent() failed"); + return NJS_ERROR; + } + + ret = njs_vm_value_string_create(vm, retval, text, njs_strlen(text)); + xmlFree(text); + + return ret; + } + + njs_value_undefined_set(retval); + + return NJS_OK; +} + + +static njs_int_t +njs_xml_node_ext_get_child_ns(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) +{ + xmlNode *current, *node; + njs_str_t uri, local; + + current = njs_xml_ns_args(vm, args, nargs, &uri, &local); + if (njs_slow_path(current == NULL)) { + return NJS_ERROR; + } + + for (node = current->children; node != NULL; node = node->next) { + if (node->type == XML_ELEMENT_NODE + && njs_xml_ns_match(node->name, node->ns, &uri, &local)) + { + return njs_vm_external_create(vm, retval, njs_xml_node_proto_id, + node, 0); + } + } + + njs_value_undefined_set(retval); + + return NJS_OK; +} + + static njs_int_t njs_xml_node_ext_attrs(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused, njs_value_t *value, njs_value_t *setval, njs_value_t *retval) @@ -1435,6 +1532,71 @@ njs_xml_string_to_c_string(njs_vm_t *vm, njs_str_t *str, u_char *dst, } +static xmlNode * +njs_xml_ns_args(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_str_t *uri, njs_str_t *local) +{ + xmlNode *current; + njs_value_t *value; + + current = njs_vm_external(vm, njs_xml_node_proto_id, + njs_argument(args, 0)); + if (njs_slow_path(current == NULL)) { + njs_vm_type_error(vm, "\"this\" is not a XMLNode object"); + return NULL; + } + + value = njs_arg(args, nargs, 1); + + if (njs_value_is_null(value)) { + uri->length = 0; + uri->start = NULL; + + } else if (njs_value_is_string(value)) { + njs_value_string_get(vm, value, uri); + + } else { + njs_vm_type_error(vm, "namespace URI is not a string or null"); + return NULL; + } + + value = njs_arg(args, nargs, 2); + + if (njs_slow_path(!njs_value_is_string(value))) { + njs_vm_type_error(vm, "local name is not a string"); + return NULL; + } + + njs_value_string_get(vm, value, local); + + return current; +} + + +/* + * An empty or null URI matches only names without a namespace, as in + * DOM getAttributeNS(). + */ + +static njs_bool_t +njs_xml_ns_match(const xmlChar *name, xmlNs *ns, njs_str_t *uri, + njs_str_t *local) +{ + if (local->length != njs_strlen(name) + || njs_strncmp(local->start, name, local->length) != 0) + { + return 0; + } + + if (ns == NULL || ns->href == NULL) { + return uri->length == 0; + } + + return uri->length == njs_strlen(ns->href) + && njs_strncmp(uri->start, ns->href, uri->length) == 0; +} + + static njs_bool_t njs_xml_node_is_live(xmlNode *node) { diff --git a/external/qjs_xml_module.c b/external/qjs_xml_module.c index c0f795c7..9afb44de 100644 --- a/external/qjs_xml_module.c +++ b/external/qjs_xml_module.c @@ -87,6 +87,10 @@ static int qjs_xml_node_property_modify(JSContext *cx, JSValueConst obj, JSAtom atom, JSValueConst value, int delete); static JSValue qjs_xml_node_add_child(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue qjs_xml_node_get_attribute_ns(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue qjs_xml_node_get_child_ns(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); static JSValue qjs_xml_node_remove_children(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv); static JSValue qjs_xml_node_set_attribute(JSContext *cx, JSValueConst this_val, @@ -119,6 +123,12 @@ static qjs_xml_nset_t *qjs_xml_nset_create(JSContext *cx, xmlDoc *doc, static qjs_xml_nset_t *qjs_xml_nset_add(qjs_xml_nset_t *nset, qjs_xml_nset_t *add); static void qjs_xml_nset_free(JSContext *cx, qjs_xml_nset_t *nset); +static qjs_xml_node_t *qjs_xml_ns_args(JSContext *cx, JSValueConst this_val, + JSValueConst *argv, njs_str_t *uri, njs_str_t *local); +static void qjs_xml_ns_args_free(JSContext *cx, njs_str_t *uri, + njs_str_t *local); +static int qjs_xml_ns_match(const xmlChar *name, xmlNs *ns, njs_str_t *uri, + njs_str_t *local); static int qjs_xml_node_is_live(xmlNode *node); static xmlNode *qjs_xml_list_detach(xmlNode *parent, njs_str_t *name); static int qjs_xml_list_retire(JSContext *cx, qjs_xml_doc_t *doc, @@ -148,6 +158,8 @@ static const JSCFunctionListEntry qjs_xml_doc_proto[] = { static const JSCFunctionListEntry qjs_xml_node_proto[] = { JS_PROP_STRING_DEF("[Symbol.toStringTag]", "XMLNode", JS_PROP_CONFIGURABLE), JS_CFUNC_DEF("addChild", 1, qjs_xml_node_add_child), + JS_CFUNC_DEF("getAttributeNS", 2, qjs_xml_node_get_attribute_ns), + JS_CFUNC_DEF("getChildNS", 2, qjs_xml_node_get_child_ns), JS_CFUNC_DEF("removeChildren", 1, qjs_xml_node_remove_children), JS_CFUNC_DEF("setAttribute", 2, qjs_xml_node_set_attribute), JS_CFUNC_DEF("removeAttribute", 1, qjs_xml_node_remove_attribute), @@ -1431,6 +1443,76 @@ qjs_xml_node_property_modify(JSContext *cx, JSValueConst obj, JSAtom atom, } +static JSValue +qjs_xml_node_get_attribute_ns(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + xmlAttr *attr; + xmlChar *text; + JSValue value; + njs_str_t uri, local; + qjs_xml_node_t *current; + + current = qjs_xml_ns_args(cx, this_val, argv, &uri, &local); + if (current == NULL) { + return JS_EXCEPTION; + } + + value = JS_UNDEFINED; + + for (attr = current->node->properties; attr != NULL; attr = attr->next) { + if (!qjs_xml_ns_match(attr->name, attr->ns, &uri, &local)) { + continue; + } + + text = xmlNodeGetContent((xmlNode *) attr); + if (text == NULL) { + value = JS_ThrowInternalError(cx, "xmlNodeGetContent() failed"); + break; + } + + value = JS_NewString(cx, (char *) text); + xmlFree(text); + break; + } + + qjs_xml_ns_args_free(cx, &uri, &local); + + return value; +} + + +static JSValue +qjs_xml_node_get_child_ns(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue value; + xmlNode *node; + njs_str_t uri, local; + qjs_xml_node_t *current; + + current = qjs_xml_ns_args(cx, this_val, argv, &uri, &local); + if (current == NULL) { + return JS_EXCEPTION; + } + + value = JS_UNDEFINED; + + for (node = current->node->children; node != NULL; node = node->next) { + if (node->type == XML_ELEMENT_NODE + && qjs_xml_ns_match(node->name, node->ns, &uri, &local)) + { + value = qjs_xml_node_make(cx, current->doc, node); + break; + } + } + + qjs_xml_ns_args_free(cx, &uri, &local); + + return value; +} + + static JSValue qjs_xml_node_add_child(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv) @@ -2042,6 +2124,91 @@ qjs_xml_nset_free(JSContext *cx, qjs_xml_nset_t *nset) } +static qjs_xml_node_t * +qjs_xml_ns_args(JSContext *cx, JSValueConst this_val, JSValueConst *argv, + njs_str_t *uri, njs_str_t *local) +{ + size_t length; + qjs_xml_node_t *current; + + current = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_XML_NODE); + if (current == NULL) { + JS_ThrowTypeError(cx, "\"this\" is not a XMLNode object"); + return NULL; + } + + if (!JS_IsNull(argv[0]) && !JS_IsString(argv[0])) { + JS_ThrowTypeError(cx, "namespace URI is not a string or null"); + return NULL; + } + + if (!JS_IsString(argv[1])) { + JS_ThrowTypeError(cx, "local name is not a string"); + return NULL; + } + + uri->start = NULL; + uri->length = 0; + + if (JS_IsString(argv[0])) { + uri->start = (u_char *) JS_ToCStringLen(cx, &length, argv[0]); + if (uri->start == NULL) { + return NULL; + } + + uri->length = length; + } + + local->start = (u_char *) JS_ToCStringLen(cx, &length, argv[1]); + if (local->start == NULL) { + if (uri->start != NULL) { + JS_FreeCString(cx, (char *) uri->start); + } + + return NULL; + } + + local->length = length; + + return current; +} + + +static void +qjs_xml_ns_args_free(JSContext *cx, njs_str_t *uri, njs_str_t *local) +{ + if (uri->start != NULL) { + JS_FreeCString(cx, (char *) uri->start); + } + + JS_FreeCString(cx, (char *) local->start); +} + + +/* + * An empty or null URI matches only names without a namespace, as in + * DOM getAttributeNS(). + */ + +static int +qjs_xml_ns_match(const xmlChar *name, xmlNs *ns, njs_str_t *uri, + njs_str_t *local) +{ + if (local->length != njs_strlen(name) + || njs_strncmp(local->start, name, local->length) != 0) + { + return 0; + } + + if (ns == NULL || ns->href == NULL) { + return uri->length == 0; + } + + return uri->length == njs_strlen(ns->href) + && njs_strncmp(uri->start, ns->href, uri->length) == 0; +} + + static int qjs_xml_node_is_live(xmlNode *node) { diff --git a/test/ts/test.ts b/test/ts/test.ts index 1b52a904..5a0e3c5b 100644 --- a/test/ts/test.ts +++ b/test/ts/test.ts @@ -179,6 +179,11 @@ function xml_module(str: string) { node?.$tag$xxx?.$tag$yyy?.$attr$zzz; + let child: XMLNode | undefined = node.getChildNS('urn:x', 'xxx'); + child = node.getChildNS(null, 'xxx'); + let attr: string | undefined = node.getAttributeNS('urn:x', 'yyy'); + attr = node.getAttributeNS(null, 'yyy'); + let buf:Buffer = xml.exclusiveC14n(node); buf = xml.exclusiveC14n(doc, node.$tag$xxx, false); buf = xml.exclusiveC14n(node, null, true, "aa bb"); diff --git a/test/xml/xml.t.mjs b/test/xml/xml.t.mjs index aff28956..6c129b88 100644 --- a/test/xml/xml.t.mjs +++ b/test/xml/xml.t.mjs @@ -52,6 +52,49 @@ let parse_tsuite = { { doc: ``, get: (doc) => doc.pdu.$ns, expected: 'http://a' }, + { doc: `` + + `foreigntrusted`, + get: (doc) => [ + doc.r.getAttributeNS('', 'ID'), + doc.r.getAttributeNS(null, 'ID'), + doc.r.getAttributeNS('urn:other', 'ID'), + doc.r.getAttributeNS('urn:missing', 'ID'), + doc.r.getChildNS('', 'Issuer').$text, + doc.r.getChildNS(null, 'Issuer').$text, + doc.r.getChildNS('urn:other', 'Issuer').$text, + doc.r.getChildNS('urn:missing', 'Issuer')].join(','), + expected: 'correct,correct,wrong,,trusted,trusted,foreign,' }, + { doc: ``, + get: (doc) => [ + doc.r.getAttributeNS(null, 'ID'), + doc.r.getAttributeNS('urn:d', 'ID'), + doc.r.getChildNS(null, 'c'), + doc.r.getChildNS('urn:d', 'c').$name].join(','), + expected: 'plain,,,c' }, + { doc: ``, + get: (doc) => doc.r.getAttributeNS( + 'http://www.w3.org/XML/1998/namespace', 'lang'), + expected: 'en' }, + { doc: ``, + get: (doc) => doc.r.getChildNS(null, 'b'), + expected: undefined }, + { doc: `text`, + get: (doc) => doc.r.getChildNS(null, 'c').$name, + expected: 'c' }, + { doc: `]>` + + ``, + get: (doc) => doc.r.getAttributeNS('urn:other', 'ID'), + expected: 'precompletepost' }, + { get: (doc) => doc.note.getChildNS(undefined, 'to'), + exception: 'TypeError: namespace URI is not a string or null' }, + { get: (doc) => doc.note.getAttributeNS(1, 'a'), + exception: 'TypeError: namespace URI is not a string or null' }, + { get: (doc) => doc.note.getChildNS(null), + exception: 'TypeError: local name is not a string' }, + { get: (doc) => doc.note.to.getAttributeNS(null, null), + exception: 'TypeError: local name is not a string' }, + { get: (doc) => doc.note.getChildNS.call({}, null, 'to'), + exception: 'TypeError: "this" is not a XMLNode object' }, { doc: `FOOBAR`, get: (doc) => doc.root.$tags$foo[0].$text, expected: 'FOO' }, diff --git a/ts/njs_modules/xml.d.ts b/ts/njs_modules/xml.d.ts index fa97af47..4cd7431e 100644 --- a/ts/njs_modules/xml.d.ts +++ b/ts/njs_modules/xml.d.ts @@ -15,6 +15,29 @@ declare module "xml" { } export interface XMLNode { + /** + * Returns the first direct child element with this namespace URI + * and local name, or undefined if there is none. + * @param namespaceURI - namespace URI; null or an empty string + * matches an element without a namespace. + * @param localName - local name of the element. + * @since 1.0.2. + */ + getChildNS(namespaceURI: string | null, localName: string): + XMLNode | undefined; + + /** + * Returns the value of an attribute present in the document with + * this namespace URI and local name, or undefined if there is none. + * @param namespaceURI - namespace URI; null or an empty string + * matches an attribute without a namespace. An unprefixed + * attribute never belongs to the default namespace. + * @param localName - local name of the attribute. + * @since 1.0.2. + */ + getAttributeNS(namespaceURI: string | null, localName: string): + string | undefined; + /** * Adds a recursive copy of a namespace-free child node. * @param node - XMLNode to be added. -- 2.47.3