From c08eafae17a90e1b72044b22b3d802f89a574abb Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Mon, 28 Sep 2026 22:23:45 -0700 Subject: [PATCH] XML: preserve node methods with matching children QuickJS exposes child elements as own properties, so a child named like an XMLNode method hides the method on its prototype. Let methods take precedence over bare child names, as on the njs engine. Keep the child accessible through $tag$name and avoid deleting it when deleting the method name. Cover collisions for all XMLNode methods and prototype changes. --- external/qjs_xml_module.c | 55 +++++++++++++++++++++++++++++++++++++++ test/xml/xml.t.mjs | 48 ++++++++++++++++++++++++++++++++++ ts/njs_modules/xml.d.ts | 1 + 3 files changed, 104 insertions(+) diff --git a/external/qjs_xml_module.c b/external/qjs_xml_module.c index 9afb44de..29dd1491 100644 --- a/external/qjs_xml_module.c +++ b/external/qjs_xml_module.c @@ -72,6 +72,8 @@ static void qjs_xml_doc_free(JSRuntime *rt, qjs_xml_doc_t *current); static JSValue qjs_xml_node_make(JSContext *cx, qjs_xml_doc_t *doc, xmlNode *node); +static int qjs_xml_node_has_method(JSContext *cx, JSValueConst obj, + JSAtom prop, njs_str_t *name); static int qjs_xml_node_get_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop); static int qjs_xml_node_get_own_property_names(JSContext *cx, @@ -1006,10 +1008,49 @@ qjs_xml_node_text_handler(JSContext *cx, JSValue current, JSValue setval) } +static int +qjs_xml_node_has_method(JSContext *cx, JSValueConst obj, JSAtom prop, + njs_str_t *name) +{ + int rc; + size_t i, length; + JSValue proto; + const JSCFunctionListEntry *entry; + + for (i = 0; i < njs_nitems(qjs_xml_node_proto); i++) { + entry = &qjs_xml_node_proto[i]; + + if (entry->def_type != JS_DEF_CFUNC) { + continue; + } + + length = njs_strlen(entry->name); + + if (name->length == length + && njs_strncmp(name->start, entry->name, length) == 0) + { + proto = JS_GetPrototype(cx, obj); + if (JS_IsException(proto)) { + return -1; + } + + rc = JS_IsObject(proto) ? JS_GetOwnProperty(cx, NULL, proto, prop) + : 0; + JS_FreeValue(cx, proto); + + return rc; + } + } + + return 0; +} + + static int qjs_xml_node_get_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop) { + int rc; u_char *text; JSValue value; xmlNode *node; @@ -1241,6 +1282,12 @@ qjs_xml_node_get_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, nm = name; + rc = qjs_xml_node_has_method(cx, obj, prop, &name); + if (rc != 0) { + JS_FreeCString(cx, (char *) name.start); + return rc < 0 ? -1 : 0; + } + tag: value = qjs_xml_node_tag_handler(cx, current, &nm); @@ -1436,6 +1483,14 @@ qjs_xml_node_property_modify(JSContext *cx, JSValueConst obj, JSAtom atom, } } + if (delete) { + rc = qjs_xml_node_has_method(cx, obj, atom, &name); + if (rc != 0) { + JS_FreeCString(cx, (char *) name.start); + return rc < 0 ? -1 : 1; + } + } + rc = qjs_xml_node_tag_modify(cx, obj, &name, delete); JS_FreeCString(cx, (char *) name.start); diff --git a/test/xml/xml.t.mjs b/test/xml/xml.t.mjs index 6c129b88..ed6078c8 100644 --- a/test/xml/xml.t.mjs +++ b/test/xml/xml.t.mjs @@ -81,6 +81,54 @@ let parse_tsuite = { { doc: `text`, get: (doc) => doc.r.getChildNS(null, 'c').$name, expected: 'c' }, + { doc: `` + + `` + + `` + + `trusted`, + get: (doc) => { + const node = doc.r; + const methods = ['addChild', 'getAttributeNS', 'getChildNS', + 'removeChildren', 'setAttribute', + 'removeAttribute', 'removeAllAttributes', + 'setText', 'removeText']; + + return methods.every(name => typeof node[name] == 'function' + && node[`$tag$${name}`].$name == name) + && node.getAttributeNS(null, 'ID') == 'ok' + && node.getChildNS(null, 'Issuer').$text == 'trusted'; + }, + expected: true }, + { doc: ``, + get: (doc) => { + delete doc.r.$tag$getChildNS; + return typeof doc.r.getChildNS == 'function' + && doc.r.$tag$getChildNS === undefined; + }, + expected: true }, + { doc: ``, + skip: () => !has_quickjs(), + get: (doc) => { + delete doc.r.setAttribute; + return typeof doc.r.setAttribute == 'function' + && doc.r.$tag$setAttribute.$name == 'setAttribute'; + }, + expected: true }, + { doc: ``, + skip: () => !has_quickjs(), + get: (doc) => { + const proto = Object.getPrototypeOf(doc.r); + const method = proto.getChildNS; + + delete proto.getChildNS; + + try { + return doc.r.getChildNS.$name == 'getChildNS'; + + } finally { + proto.getChildNS = method; + } + }, + expected: true }, { doc: `]>` + ``, get: (doc) => doc.r.getAttributeNS('urn:other', 'ID'), diff --git a/ts/njs_modules/xml.d.ts b/ts/njs_modules/xml.d.ts index 4cd7431e..558df08f 100644 --- a/ts/njs_modules/xml.d.ts +++ b/ts/njs_modules/xml.d.ts @@ -103,6 +103,7 @@ declare module "xml" { /** * node.$tag$xxx - the node's first child tag named "xxx". + * Use this form when the tag name matches an XMLNode method. * Assigning a value throws TypeError at runtime. Deleting the property * removes matching child tags. */ -- 2.47.3