Browse Source

HPCC-24134 DESDL Transforms namespace handling for xpaths

Support namespaces in xpaths on the left side of transforms. Unittests
include cases for the new support. Xpath Processor evaluateAsString
checks for empty nodesets to avoid a segfault.

Signed-off-by: Terrence Asselin <terrence.asselin@lexisnexisrisk.com>
Terrence Asselin 5 years ago
parent
commit
2ab17bcd99

+ 22 - 5
esp/esdllib/esdl_script.cpp

@@ -655,7 +655,7 @@ static IPropertyTree *getOperationTargetPTree(MapStringToMyClass<IPropertyTree>
         return opTree;
     opTree = getTargetPTree(tree, xpathContext, mergedTarget);
     if (opTree)
-        treeMap.setValue(mergedTarget, LINK(opTree));
+        treeMap.setValue(mergedTarget, opTree);
     return opTree;
 }
 
@@ -664,6 +664,7 @@ class CEsdlCustomTransform : public CInterfaceOf<IEsdlCustomTransform>
 private:
     IArrayOf<IEsdlTransformOperation> m_variables; //keep separate and only at top level for now
     IArrayOf<IEsdlTransformOperation> m_operations;
+    Owned<IProperties> namespaces = createProperties(false);
     StringAttr m_name;
     StringAttr m_target;
     StringBuffer m_prefix;
@@ -711,6 +712,14 @@ public:
 
         DBGLOG("Compiling custom ESDL Transform: '%s'", m_name.str());
 
+        Owned<IAttributeIterator> attributes = tree.getAttributes();
+        ForEach(*attributes)
+        {
+            const char *name = attributes->queryName();
+            if (strncmp(name, "@xmlns:", 7)==0)
+                namespaces->setProp(name+7, attributes->queryValue());
+        }
+
         StringBuffer xpath;
         Owned<IPropertyTreeIterator> parameters = tree.getElements(makeOperationTagName(xpath, m_prefix, "param"));
         ForEach(*parameters)
@@ -753,13 +762,21 @@ public:
 
     void processTransformImpl(IEspContext * context, IPropertyTree *theroot, IXpathContext *xpathContext, const char *target)
     {
-        CXpathContextScope scope(xpathContext, "transform");
-
+        Owned<IProperties> savedNamespaces = createProperties(false);
+        Owned<IPropertyIterator> ns = namespaces->getIterator();
+        ForEach(*ns)
+        {
+            const char *prefix = ns->getPropKey();
+            const char *existing = xpathContext->queryNamespace(prefix);
+            savedNamespaces->setProp(prefix, isEmptyString(existing) ? "" : existing);
+            xpathContext->registerNamespace(prefix, namespaces->queryProp(prefix));
+        }
+        CXpathContextScope scope(xpathContext, "transform", savedNamespaces);
         if (m_target.length())
             target = m_target.str();
         MapStringToMyClass<IPropertyTree> treeMap; //cache trees because when there are merged targets they are likely to repeat
         IPropertyTree *txTree = getTargetPTree(theroot, xpathContext, target);
-        treeMap.setValue(target, LINK(txTree));
+        treeMap.setValue(target, txTree);
         ForEachItemIn(v, m_variables)
             m_variables.item(v).process(context, txTree, xpathContext);
         ForEachItemIn(i, m_operations)
@@ -802,7 +819,7 @@ void processServiceAndMethodTransforms(std::initializer_list<IEsdlCustomTransfor
         }
 
         bool strictParams = bindingCfg ? bindingCfg->getPropBool("@strictParams", false) : false;
-        Owned<IXpathContext> xpathContext = getXpathContext(content.str(), strictParams);
+        Owned<IXpathContext> xpathContext = getXpathContext(content.str(), strictParams, false);
 
         StringArray prefixes;
         for ( IEsdlCustomTransform * const & item : transforms)

+ 62 - 4
system/xmllib/libxml_xpathprocessor.cpp

@@ -200,12 +200,13 @@ public:
     ReadWriteLock m_rwlock;
     XPathScopeVector scopes;
     bool strictParameterDeclaration = true;
+    bool removeDocNamespaces = false;
 
     //saved state
     XPathContextStateVector saved;
 
 public:
-    CLibXpathContext(const char * xmldoc, bool _strictParameterDeclaration) : strictParameterDeclaration(_strictParameterDeclaration)
+    CLibXpathContext(const char * xmldoc, bool _strictParameterDeclaration, bool removeDocNs) : strictParameterDeclaration(_strictParameterDeclaration), removeDocNamespaces(removeDocNs)
     {
         beginScope("/");
         setXmlDoc(xmldoc);
@@ -281,6 +282,14 @@ public:
         }
     }
 
+    virtual const char *queryNamespace(const char *prefix) override
+    {
+        if (!m_xpathContext)
+            return nullptr;
+        WriteLockBlock wblock(m_rwlock);
+        return (const char *) xmlXPathNsLookup(m_xpathContext, (const xmlChar *)prefix);
+    }
+
     virtual void registerFunction(const char *xmlns, const char * name, void *f) override
     {
         if (m_xpathContext)
@@ -557,6 +566,54 @@ public:
     }
 
 private:
+    xmlNodePtr checkGetSoapNamespace(xmlNodePtr cur, const char *expected, const xmlChar *&soap)
+    {
+        if (!cur)
+            return nullptr;
+        if (!streq((const char *)cur->name, expected))
+            return cur;
+        if (!soap && cur->ns && cur->ns->href)
+            soap = cur->ns->href;
+        return xmlFirstElementChild(cur);
+    }
+    void removeNamespace(xmlNodePtr cur, const xmlChar *remove)
+    {
+        //we need to recognize different namespace behavior at each entry point
+        //
+        //for backward compatibility we need to be flexible with what we receive, from the user
+        //but other entry points are dealing with standardized content
+        //hopefully even calls out to a given 3rd party services will be self consistent
+        //
+        if (!remove)
+            cur->ns=nullptr; //just a "reference", no free
+        else if (cur->ns && cur->ns->href && streq((const char *)cur->ns->href, (const char *)remove))
+            cur->ns=nullptr;
+        for (xmlNodePtr child = xmlFirstElementChild(cur); child!=nullptr; child=xmlNextElementSibling(child))
+            removeNamespace(child, remove);
+        for (xmlAttrPtr att = cur->properties; att!=nullptr; att=att->next)
+        {
+            if (!remove)
+                att->ns=nullptr;
+            else if (att->ns && att->ns->href && streq((const char *)att->ns->href, (const char *)remove))
+                att->ns=nullptr;
+        }
+    }
+    void processNamespaces()
+    {
+        const xmlChar *soap = nullptr;
+        const xmlChar *n = nullptr;
+
+        xmlNodePtr cur = checkGetSoapNamespace(xmlDocGetRootElement(m_xmlDoc), "Envelope", soap);
+        cur = checkGetSoapNamespace(cur, "Body", soap);
+        if (soap)
+            xmlXPathRegisterNs(m_xpathContext, (const xmlChar *)"soap", soap);
+        if (cur->ns && cur->ns->href)
+            n = cur->ns->href;
+        if (n)
+            xmlXPathRegisterNs(m_xpathContext, (const xmlChar *)"n", n);
+        if (removeDocNamespaces)
+            removeNamespace(xmlDocGetRootElement(m_xmlDoc), nullptr);
+    }
     bool setContextDocument(xmlDocPtr doc, xmlNodePtr node)
     {
         WriteLockBlock rblock(m_rwlock);
@@ -573,6 +630,7 @@ private:
         if (node)
             m_xpathContext->node = node;
         xmlXPathRegisterVariableLookup(m_xpathContext, variableLookupFunc, this);
+        processNamespaces();
         return true;
     }
     bool evaluateAsBoolean(xmlXPathObjectPtr evaluatedXpathObj, const char* xpath)
@@ -604,7 +662,7 @@ private:
             case XPATH_NODESET:
             {
                 xmlNodeSetPtr nodes = evaluatedXpathObj->nodesetval;
-                for (int i = 0; i < nodes->nodeNr; i++)
+                for (int i = 0; nodes!=nullptr && i < nodes->nodeNr; i++)
                 {
                     xmlNodePtr nodeTab = nodes->nodeTab[i];
                     auto nodeContent = xmlNodeGetContent(nodeTab);
@@ -775,7 +833,7 @@ extern ICompiledXpath* compileXpath(const char * xpath)
     return new CLibCompiledXpath(xpath);
 }
 
-extern IXpathContext* getXpathContext(const char * xmldoc, bool strictParameterDeclaration)
+extern IXpathContext* getXpathContext(const char * xmldoc, bool strictParameterDeclaration, bool removeDocNamespaces)
 {
-    return new CLibXpathContext(xmldoc, strictParameterDeclaration);
+    return new CLibXpathContext(xmldoc, strictParameterDeclaration, removeDocNamespaces);
 }

+ 14 - 2
system/xmllib/xpathprocessor.hpp

@@ -50,6 +50,7 @@ interface XMLLIB_API IXpathContext : public IInterface
 
     virtual void registerFunction(const char *xmlns, const char * name, void *f) = 0;
     virtual void registerNamespace(const char *prefix, const char *uri) = 0;
+    virtual const char *queryNamespace(const char *prefix) = 0;
     virtual void beginScope(const char *name) = 0;
     virtual void endScope() = 0;
 
@@ -66,19 +67,30 @@ class CXpathContextScope : CInterface
 {
 private:
     Linked<IXpathContext> context;
+    Linked<IProperties> namespaces;
 public:
     IMPLEMENT_IINTERFACE;
-    CXpathContextScope(IXpathContext *ctx, const char *name) : context(ctx)
+    CXpathContextScope(IXpathContext *ctx, const char *name, IProperties *ns=nullptr) : context(ctx), namespaces(ns)
     {
         context->beginScope(name);
     }
     virtual ~CXpathContextScope()
     {
+        if (namespaces)
+        {
+            Owned<IPropertyIterator> ns = namespaces->getIterator();
+            ForEach(*ns)
+            {
+                const char *prefix = ns->getPropKey();
+                const char *uri = namespaces->queryProp(prefix);
+                context->registerNamespace(prefix, isEmptyString(uri) ? nullptr : uri);
+            }
+        }
         context->endScope();
     }
 };
 
 extern "C" XMLLIB_API ICompiledXpath* compileXpath(const char * xpath);
-extern "C" XMLLIB_API IXpathContext*  getXpathContext(const char * xmldoc, bool strictParameterDeclaration);
+extern "C" XMLLIB_API IXpathContext*  getXpathContext(const char * xmldoc, bool strictParameterDeclaration, bool removeDocNamespaces);
 
 #endif /* XPATH_MANAGER_HPP_ */

+ 465 - 58
testing/unittests/esdltests.cpp

@@ -202,6 +202,51 @@ static constexpr const char * esdlScriptNoPrefix = R"!!(<CustomRequestTransform
 </CustomRequestTransform>
 )!!";
 
+static constexpr const char * esdlScriptSelectPath = R"!!(
+<es:CustomRequestTransform xmlns:es="urn:hpcc:esdl:script" target="soap:Body/extra/{$query}/{$request}">
+  <es:param name="selectPath" select="''"/>
+
+  <es:set-value target="_OUTPUT_" select="$selectPath"/>
+</es:CustomRequestTransform>
+)!!";
+
+static constexpr const char* selectPathResult = R"!!(<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+ <soap:Body>
+  <extra>
+   <EchoPersonInfo>
+    <Context>
+     <Row>
+      <Common>
+       <TransactionId>1736623372_3126765312_1333296170</TransactionId>
+      </Common>
+     </Row>
+    </Context>
+    <_TransactionId>1736623372_3126765312_1333296170</_TransactionId>
+    <EchoPersonInfoRequest>
+     <Row>
+      <Addresses>
+       <Address>
+        <type>Home</type>
+        <Line2>Apt 202</Line2>
+        <Line1>101 Main street</Line1>
+        <City>Hometown</City>
+        <Zip>96703</Zip>
+        <State>HI</State>
+       </Address>
+      </Addresses>
+      <Name>
+       <Last>Doe</Last>
+       <First>Joe</First>
+      </Name>
+     </Row>
+     <_OUTPUT_>Joe</_OUTPUT_>
+    </EchoPersonInfoRequest>
+   </EchoPersonInfo>
+  </extra>
+ </soap:Body>
+</soap:Envelope>
+)!!";
+
 
 static const char *target_config = "<method queryname='EchoPersonInfo'/>";
 
@@ -223,6 +268,12 @@ class ESDLTests : public CppUnit::TestFixture
         CPPUNIT_TEST(testEsdlTransformFailLevel2A);
         CPPUNIT_TEST(testEsdlTransformFailLevel2B);
         CPPUNIT_TEST(testEsdlTransformFailLevel2C);
+        CPPUNIT_TEST(testEsdlTransformAnyDescendentPath);
+        CPPUNIT_TEST(testEsdlTransformAbsoluteSoapPath);
+        CPPUNIT_TEST(testEsdlTransformRelativePath);
+        CPPUNIT_TEST(testEsdlTransformSelectPath);
+        CPPUNIT_TEST(testEsdlTransformImplicitPrefix);
+        CPPUNIT_TEST(testEsdlTransformRequestNamespaces);
     CPPUNIT_TEST_SUITE_END();
 
 public:
@@ -270,6 +321,362 @@ public:
         }
     }
 
+    void testEsdlTransformSelectPath()
+    {
+      constexpr const char* config = R"!!(
+        <config strictParams='true'>
+          <Transform>
+            <Param name='testcase' value="select-path"/>
+            <Param name='selectPath' select="//First"/>
+          </Transform>
+        </config>
+      )!!";
+
+      runTest(esdlScriptSelectPath, soapRequest, config, selectPathResult, 0);
+    }
+
+    void testEsdlTransformAbsoluteSoapPath()
+    {
+      constexpr const char* config = R"!!(
+        <config strictParams='true'>
+          <Transform>
+            <Param name='testcase' value="absolute-soap-path"/>
+            <Param name='selectPath' select="/soap:Envelope/soap:Body/extra/EchoPersonInfo/EchoPersonInfoRequest/Row/Name/First"/>
+          </Transform>
+        </config>
+      )!!";
+
+      runTest(esdlScriptSelectPath, soapRequest, config, selectPathResult, 0);
+    }
+
+    void testEsdlTransformRelativePath()
+    {
+      constexpr const char* config = R"!!(
+        <config strictParams='true'>
+          <Transform>
+            <Param name='testcase' value="relative-path"/>
+            <Param name='selectPath' select="soap:Body/extra/EchoPersonInfo/EchoPersonInfoRequest/Row/Name/First"/>
+          </Transform>
+        </config>
+      )!!";
+
+      runTest(esdlScriptSelectPath, soapRequest, config, selectPathResult, 0);
+    }
+
+    void testEsdlTransformAnyDescendentPath()
+    {
+      constexpr const char* config = R"!!(
+        <config strictParams='true'>
+          <Transform>
+            <Param name='testcase' value="any-descendent-path"/>
+            <Param name='selectPath' select="//First"/>
+          </Transform>
+        </config>
+      )!!";
+
+      runTest(esdlScriptSelectPath, soapRequest, config, selectPathResult, 0);
+    }
+
+    void testEsdlTransformImplicitPrefix()
+    {
+      static constexpr const char * soapRequestImplicitPrefix = R"!!(<?xml version="1.0" encoding="UTF-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns="http://webservices.example.com/WsFooBar">
+<soap:Body>
+<extra>
+<EchoPersonInfo>
+  <_TransactionId>1736623372_3126765312_1333296170</_TransactionId>
+  <Context>
+   <Row>
+    <Common>
+     <TransactionId>1736623372_3126765312_1333296170</TransactionId>
+    </Common>
+   </Row>
+  </Context>
+  <EchoPersonInfoRequest>
+   <Row>
+    <Name>
+     <First>Joe</First>
+     <Last>Doe</Last>
+    </Name>
+    <Addresses>
+     <Address>
+      <type>Home</type>
+      <Line1>101 Main street</Line1>
+      <Line2>Apt 202</Line2>
+      <City>Hometown</City>
+      <State>HI</State>
+      <Zip>96703</Zip>
+     </Address>
+    </Addresses>
+   </Row>
+  </EchoPersonInfoRequest>
+</EchoPersonInfo>
+</extra>
+</soap:Body>
+</soap:Envelope>
+)!!";
+      constexpr const char* implicitPrefixResult = R"!!(<soap:Envelope xmlns="http://webservices.example.com/WsFooBar" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+ <soap:Body>
+  <extra>
+   <EchoPersonInfo>
+    <Context>
+     <Row>
+      <Common>
+       <TransactionId>1736623372_3126765312_1333296170</TransactionId>
+      </Common>
+     </Row>
+    </Context>
+    <_TransactionId>1736623372_3126765312_1333296170</_TransactionId>
+    <EchoPersonInfoRequest>
+     <Row>
+      <Addresses>
+       <Address>
+        <type>Home</type>
+        <Line2>Apt 202</Line2>
+        <Line1>101 Main street</Line1>
+        <City>Hometown</City>
+        <Zip>96703</Zip>
+        <State>HI</State>
+       </Address>
+      </Addresses>
+      <Name>
+       <Last>Doe</Last>
+       <First>Joe</First>
+      </Name>
+     </Row>
+     <_OUTPUT_>Joe</_OUTPUT_>
+    </EchoPersonInfoRequest>
+   </EchoPersonInfo>
+  </extra>
+ </soap:Body>
+</soap:Envelope>
+)!!";
+
+      constexpr const char* config = R"!!(
+        <config strictParams='true'>
+          <Transform>
+            <Param name='testcase' value="implicit-prefix"/>
+            <Param name='selectPath' select="soap:Body/n:extra/n:EchoPersonInfo/n:EchoPersonInfoRequest/n:Row/n:Name/n:First"/>
+          </Transform>
+        </config>
+      )!!";
+
+      constexpr const char* configNoPrefix = R"!!(
+        <config strictParams='true'>
+          <Transform>
+            <Param name='testcase' value="implicit-prefix-not-used"/>
+            <Param name='selectPath' select="soap:Body/extra/EchoPersonInfo/EchoPersonInfoRequest/Row/Name/First"/>
+          </Transform>
+        </config>
+      )!!";
+
+      runTest(esdlScriptSelectPath, soapRequestImplicitPrefix, config, implicitPrefixResult, 0);
+
+      // The implicit 'n' prefix is required if the content has a namespace defined
+      // with no prefix. This test is expected to throw an exception.
+      runTest(esdlScriptSelectPath, soapRequestImplicitPrefix, configNoPrefix, implicitPrefixResult, 99);
+    }
+
+    void testEsdlTransformRequestNamespaces()
+    {
+      static constexpr const char * soapRequestNsInvalid = R"!!(<?xml version="1.0" encoding="UTF-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns="invalid.uri.string">
+<soap:Body>
+<extra>
+<EchoPersonInfo>
+  <_TransactionId>1736623372_3126765312_1333296170</_TransactionId>
+  <Context>
+   <Row>
+    <Common>
+     <TransactionId>1736623372_3126765312_1333296170</TransactionId>
+    </Common>
+   </Row>
+  </Context>
+  <EchoPersonInfoRequest>
+   <Row>
+    <Name>
+     <First>Joe</First>
+     <Last>Doe</Last>
+    </Name>
+   </Row>
+  </EchoPersonInfoRequest>
+</EchoPersonInfo>
+</extra>
+</soap:Body>
+</soap:Envelope>
+)!!";
+
+      static constexpr const char * soapRequestNsArbitrary1 = R"!!(<?xml version="1.0" encoding="UTF-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns="arbitary:urn:text.here?really">
+<soap:Body>
+<extra>
+<EchoPersonInfo>
+  <_TransactionId>1736623372_3126765312_1333296170</_TransactionId>
+  <Context>
+   <Row>
+    <Common>
+     <TransactionId>1736623372_3126765312_1333296170</TransactionId>
+    </Common>
+   </Row>
+  </Context>
+  <EchoPersonInfoRequest>
+   <Row>
+    <Name>
+     <First>Joe</First>
+     <Last>Doe</Last>
+    </Name>
+   </Row>
+  </EchoPersonInfoRequest>
+</EchoPersonInfo>
+</extra>
+</soap:Body>
+</soap:Envelope>
+)!!";
+
+      static constexpr const char * soapRequestNsArbitrary2 = R"!!(<?xml version="1.0" encoding="UTF-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns="http:hullabaloo//nonsense:foo:bar#fragment">
+<soap:Body>
+<extra>
+<EchoPersonInfo>
+  <_TransactionId>1736623372_3126765312_1333296170</_TransactionId>
+  <Context>
+   <Row>
+    <Common>
+     <TransactionId>1736623372_3126765312_1333296170</TransactionId>
+    </Common>
+   </Row>
+  </Context>
+  <EchoPersonInfoRequest>
+   <Row>
+    <Name>
+     <First>Joe</First>
+     <Last>Doe</Last>
+    </Name>
+   </Row>
+  </EchoPersonInfoRequest>
+</EchoPersonInfo>
+</extra>
+</soap:Body>
+</soap:Envelope>
+)!!";
+
+      constexpr const char* namespaceResult = R"!!(<soap:Envelope xmlns="http://webservices.example.com/WsFooBar" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+ <soap:Body>
+  <extra>
+   <EchoPersonInfo>
+    <Context>
+     <Row>
+      <Common>
+       <TransactionId>1736623372_3126765312_1333296170</TransactionId>
+      </Common>
+     </Row>
+    </Context>
+    <_TransactionId>1736623372_3126765312_1333296170</_TransactionId>
+    <EchoPersonInfoRequest>
+     <Row>
+      <Name>
+       <Last>Doe</Last>
+       <First>Joe</First>
+      </Name>
+     </Row>
+     <_OUTPUT_>Joe</_OUTPUT_>
+    </EchoPersonInfoRequest>
+   </EchoPersonInfo>
+  </extra>
+ </soap:Body>
+</soap:Envelope>
+)!!";
+
+      constexpr const char* namespaceResultArbitrary1 = R"!!(<soap:Envelope xmlns="arbitary:urn:text.here?really" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+ <soap:Body>
+  <extra>
+   <EchoPersonInfo>
+    <Context>
+     <Row>
+      <Common>
+       <TransactionId>1736623372_3126765312_1333296170</TransactionId>
+      </Common>
+     </Row>
+    </Context>
+    <_TransactionId>1736623372_3126765312_1333296170</_TransactionId>
+    <EchoPersonInfoRequest>
+     <Row>
+      <Name>
+       <Last>Doe</Last>
+       <First>Joe</First>
+      </Name>
+     </Row>
+     <_OUTPUT_>Joe</_OUTPUT_>
+    </EchoPersonInfoRequest>
+   </EchoPersonInfo>
+  </extra>
+ </soap:Body>
+</soap:Envelope>
+)!!";
+
+      constexpr const char* namespaceResultArbitrary2 = R"!!(<soap:Envelope xmlns="http:hullabaloo//nonsense:foo:bar#fragment" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+ <soap:Body>
+  <extra>
+   <EchoPersonInfo>
+    <Context>
+     <Row>
+      <Common>
+       <TransactionId>1736623372_3126765312_1333296170</TransactionId>
+      </Common>
+     </Row>
+    </Context>
+    <_TransactionId>1736623372_3126765312_1333296170</_TransactionId>
+    <EchoPersonInfoRequest>
+     <Row>
+      <Name>
+       <Last>Doe</Last>
+       <First>Joe</First>
+      </Name>
+     </Row>
+     <_OUTPUT_>Joe</_OUTPUT_>
+    </EchoPersonInfoRequest>
+   </EchoPersonInfo>
+  </extra>
+ </soap:Body>
+</soap:Envelope>
+)!!";
+
+      constexpr const char* configInvalidURI = R"!!(
+        <config strictParams='true'>
+          <Transform>
+            <Param name='testcase' value="invalid-uri"/>
+            <Param name='selectPath' select="soap:Body/n:extra/n:EchoPersonInfo/n:EchoPersonInfoRequest/n:Row/n:Name/n:First"/>
+          </Transform>
+        </config>
+      )!!";
+
+      constexpr const char* configArbitraryURI1 = R"!!(
+        <config strictParams='true'>
+          <Transform>
+            <Param name='testcase' value="arbitrary-uri-1"/>
+            <Param name='selectPath' select="soap:Body/n:extra/n:EchoPersonInfo/n:EchoPersonInfoRequest/n:Row/n:Name/n:First"/>
+          </Transform>
+        </config>
+      )!!";
+
+      constexpr const char* configArbitraryURI2 = R"!!(
+        <config strictParams='true'>
+          <Transform>
+            <Param name='testcase' value="arbitrary-uri-2"/>
+            <Param name='selectPath' select="soap:Body/n:extra/n:EchoPersonInfo/n:EchoPersonInfoRequest/n:Row/n:Name/n:First"/>
+          </Transform>
+        </config>
+      )!!";
+
+      // An invalid namespace URI that is expected to throw an exception
+      runTest(esdlScriptSelectPath, soapRequestNsInvalid, configInvalidURI, namespaceResult, 99);
+
+      // Weird but valid URIs for namespaces
+      runTest(esdlScriptSelectPath, soapRequestNsArbitrary1, configArbitraryURI1, namespaceResultArbitrary1, 0);
+      runTest(esdlScriptSelectPath, soapRequestNsArbitrary2, configArbitraryURI2, namespaceResultArbitrary2, 0);
+
+    }
+
     void testEsdlTransformScript()
     {
         constexpr const char *config = R"!!(<config strictParams='true'>
@@ -480,42 +887,42 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
     void testEsdlTransformForEach()
     {
         static constexpr const char * input = R"!!(<?xml version="1.0" encoding="UTF-8"?>
-        <root>
+        <root xmlns:xx1="urn:x1" xmlns:xx2="urn:x2">
         <extra>
           <Friends>
             <Name>
              <First>Joe</First>
-             <Alias>Moe</Alias>
-             <Alias>Poe</Alias>
-             <Alias>Doe</Alias>
+             <xx1:Alias>Moe</xx1:Alias>
+             <xx1:Alias>Poe</xx1:Alias>
+             <xx1:Alias>Doe</xx1:Alias>
             </Name>
             <Name>
              <First>Jane</First>
-             <Alias>Jan</Alias>
-             <Alias>Janie</Alias>
-             <Alias>Janet</Alias>
+             <xx1:Alias>Jan</xx1:Alias>
+             <xx1:Alias>Janie</xx1:Alias>
+             <xx1:Alias>Janet</xx1:Alias>
             </Name>
           </Friends>
           <Relatives>
             <Name>
              <First>Jonathon</First>
-             <Alias>John</Alias>
-             <Alias>Jon</Alias>
-             <Alias>Johnny</Alias>
-             <Alias>Johnnie</Alias>
+             <xx1:Alias>John</xx1:Alias>
+             <xx1:Alias>Jon</xx1:Alias>
+             <xx1:Alias>Johnny</xx1:Alias>
+             <xx1:Alias>Johnnie</xx1:Alias>
             </Name>
             <Name>
              <First>Jennifer</First>
-             <Alias>Jen</Alias>
-             <Alias>Jenny</Alias>
-             <Alias>Jenna</Alias>
+             <xx1:Alias>Jen</xx1:Alias>
+             <xx1:Alias>Jenny</xx1:Alias>
+             <xx1:Alias>Jenna</xx1:Alias>
             </Name>
           </Relatives>
         </extra>
         </root>
         )!!";
 
-        static constexpr const char * forEachScript = R"!!(<es:CustomRequestTransform xmlns:es="urn:hpcc:esdl:script" target="extra">
+        static constexpr const char * forEachScript = R"!!(<es:CustomRequestTransform xmlns:es="urn:hpcc:esdl:script" xmlns:x1="urn:x1"  xmlns:x2="urn:x2" target="extra">
            <es:param name="ForBuildListPath"/>
            <es:param name="ForIdPath"/>
            <es:param name="section"/>
@@ -523,7 +930,7 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
            <es:for-each select="$ForBuildListPath">
                <es:variable name="q" select="str:decode-uri('%27')"/>
                <es:variable name="path" select="concat($section, '/Name[First=', $q, First, $q, ']/Aliases', $garbage)"/>
-               <es:for-each select="Alias">
+               <es:for-each select="x1:Alias">
                  <es:choose>
                    <es:when test="position()=1">
                      <es:append-to-value xpath_target="$path" select="."/>
@@ -541,21 +948,21 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
         </es:CustomRequestTransform>
         )!!";
 
-        constexpr const char * resultFriends = R"!!(<root>
+        constexpr const char * resultFriends = R"!!(<root xmlns:xx1="urn:x1" xmlns:xx2="urn:x2">
  <extra>
   <Relatives>
    <Name>
-    <Alias>John</Alias>
-    <Alias>Jon</Alias>
-    <Alias>Johnny</Alias>
-    <Alias>Johnnie</Alias>
     <First>Jonathon</First>
+    <xx1:Alias>John</xx1:Alias>
+    <xx1:Alias>Jon</xx1:Alias>
+    <xx1:Alias>Johnny</xx1:Alias>
+    <xx1:Alias>Johnnie</xx1:Alias>
    </Name>
    <Name>
-    <Alias>Jen</Alias>
-    <Alias>Jenny</Alias>
-    <Alias>Jenna</Alias>
     <First>Jennifer</First>
+    <xx1:Alias>Jen</xx1:Alias>
+    <xx1:Alias>Jenny</xx1:Alias>
+    <xx1:Alias>Jenna</xx1:Alias>
    </Name>
   </Relatives>
   <People>
@@ -566,18 +973,18 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
   </People>
   <Friends>
    <Name>
-    <Alias>Moe</Alias>
-    <Alias>Poe</Alias>
-    <Alias>Doe</Alias>
     <Aliases>Moe,Poe,Doe</Aliases>
     <First>Joe</First>
+    <xx1:Alias>Moe</xx1:Alias>
+    <xx1:Alias>Poe</xx1:Alias>
+    <xx1:Alias>Doe</xx1:Alias>
    </Name>
    <Name>
-    <Alias>Jan</Alias>
-    <Alias>Janie</Alias>
-    <Alias>Janet</Alias>
     <Aliases>Jan,Janie,Janet</Aliases>
     <First>Jane</First>
+    <xx1:Alias>Jan</xx1:Alias>
+    <xx1:Alias>Janie</xx1:Alias>
+    <xx1:Alias>Janet</xx1:Alias>
    </Name>
   </Friends>
  </extra>
@@ -596,23 +1003,23 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
 
         runTest(forEachScript, input, configFriends, resultFriends, 0);
 
-        constexpr const char * resultRelatives = R"!!(<root>
+        constexpr const char * resultRelatives = R"!!(<root xmlns:xx1="urn:x1" xmlns:xx2="urn:x2">
  <extra>
   <Relatives>
    <Name>
-    <Alias>John</Alias>
-    <Alias>Jon</Alias>
-    <Alias>Johnny</Alias>
-    <Alias>Johnnie</Alias>
     <Aliases>John,Jon,Johnny,Johnnie</Aliases>
     <First>Jonathon</First>
+    <xx1:Alias>John</xx1:Alias>
+    <xx1:Alias>Jon</xx1:Alias>
+    <xx1:Alias>Johnny</xx1:Alias>
+    <xx1:Alias>Johnnie</xx1:Alias>
    </Name>
    <Name>
-    <Alias>Jen</Alias>
-    <Alias>Jenny</Alias>
-    <Alias>Jenna</Alias>
     <Aliases>Jen,Jenny,Jenna</Aliases>
     <First>Jennifer</First>
+    <xx1:Alias>Jen</xx1:Alias>
+    <xx1:Alias>Jenny</xx1:Alias>
+    <xx1:Alias>Jenna</xx1:Alias>
    </Name>
   </Relatives>
   <People>
@@ -623,16 +1030,16 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
   </People>
   <Friends>
    <Name>
-    <Alias>Moe</Alias>
-    <Alias>Poe</Alias>
-    <Alias>Doe</Alias>
     <First>Joe</First>
+    <xx1:Alias>Moe</xx1:Alias>
+    <xx1:Alias>Poe</xx1:Alias>
+    <xx1:Alias>Doe</xx1:Alias>
    </Name>
    <Name>
-    <Alias>Jan</Alias>
-    <Alias>Janie</Alias>
-    <Alias>Janet</Alias>
     <First>Jane</First>
+    <xx1:Alias>Jan</xx1:Alias>
+    <xx1:Alias>Janie</xx1:Alias>
+    <xx1:Alias>Janet</xx1:Alias>
    </Name>
   </Friends>
  </extra>
@@ -663,35 +1070,35 @@ constexpr const char * result = R"!!(<soap:Envelope xmlns:soap="http://schemas.x
 
         runTest(forEachScript, input, configGarbagePathError, nullptr, -1);
 
-        constexpr const char * resultNada = R"!!(<root>
+        constexpr const char * resultNada = R"!!(<root xmlns:xx1="urn:x1" xmlns:xx2="urn:x2">
  <extra>
   <Relatives>
    <Name>
-    <Alias>John</Alias>
-    <Alias>Jon</Alias>
-    <Alias>Johnny</Alias>
-    <Alias>Johnnie</Alias>
     <First>Jonathon</First>
+    <xx1:Alias>John</xx1:Alias>
+    <xx1:Alias>Jon</xx1:Alias>
+    <xx1:Alias>Johnny</xx1:Alias>
+    <xx1:Alias>Johnnie</xx1:Alias>
    </Name>
    <Name>
-    <Alias>Jen</Alias>
-    <Alias>Jenny</Alias>
-    <Alias>Jenna</Alias>
     <First>Jennifer</First>
+    <xx1:Alias>Jen</xx1:Alias>
+    <xx1:Alias>Jenny</xx1:Alias>
+    <xx1:Alias>Jenna</xx1:Alias>
    </Name>
   </Relatives>
   <Friends>
    <Name>
-    <Alias>Moe</Alias>
-    <Alias>Poe</Alias>
-    <Alias>Doe</Alias>
     <First>Joe</First>
+    <xx1:Alias>Moe</xx1:Alias>
+    <xx1:Alias>Poe</xx1:Alias>
+    <xx1:Alias>Doe</xx1:Alias>
    </Name>
    <Name>
-    <Alias>Jan</Alias>
-    <Alias>Janie</Alias>
-    <Alias>Janet</Alias>
     <First>Jane</First>
+    <xx1:Alias>Jan</xx1:Alias>
+    <xx1:Alias>Janie</xx1:Alias>
+    <xx1:Alias>Janet</xx1:Alias>
    </Name>
   </Friends>
  </extra>