kotlinxpathlibxml2kotlin-nativecinterop

Kotlin/Native Cinterop With libxml2 - Corrupted XPath Expression


Versions

The Problem

In a small Kotlin/Native application I use cinterop with libxml2 to evaluate a set of XPath expressions to extract element text and attribute values. I thought I had it figured out but after several weeks one of my tests failed "out of the blue": libxml had complained about an invalid XPath. I added a loop around my test code and indeed, given enough iterations, the test always fails. And it always fails on the same XPath expression (out of a set of 4). This is the minimal code I can reproduce the error with:

@OptIn(ExperimentalForeignApi::class)
fun getElementTextFake(xml: String, xPathToNode: String): Either<XmlError, String> {
    val xPathCValues = xPathToNode.encodeToByteArray().toUByteArray().toCValues()
    val doc: xmlDocPtr? = xmlReadDoc(cur = xml.trim().encodeToByteArray().toUByteArray().toCValues(), URL = null, encoding = "UTF-8", options = 0)
    val xPathCtx: xmlXPathContextPtr? = doc?.let { xmlXPathNewContext(doc = it) }
    val xPathObj: xmlXPathObjectPtr? = xPathCtx?.let { xmlXPathEvalExpression(str = xPathCValues, ctxt = it) }
    val nodeSet: xmlNodeSetPtr? = xPathObj?.pointed?.nodesetval

    val result =
        if (nodeSet == null || nodeSet.pointed.nodeNr == 0) ElementNotFound(xPathToNode, xml).left() else "fakeElementText".right()

    xmlXPathFreeObject(xPathObj)
    xmlXPathFreeContext(xPathCtx)
    xmlFreeDoc(doc)

    return result
}

The error printed by libxml2 is

XPath error : Invalid expression<br>
//*[local-name()='request']/*[local-name()='processing']P

The original XPath expression passed in is

//*[local-name()='request']/*[local-name()='processing']

Note the P at the end of the expression string, which makes it invalid. None of my other XPath expressions contain a capital P. The error almost always occurs within a couple hundred iterations.

The test runs single-threaded.

What could be causing the data corruption of the XPath expression string?


Solution

  • The problem is be the byte array:

    val xPathCValues = xPathToNode.encodeToByteArray().toUByteArray().toCValues()
    

    The xmlChar array has to be 0-terminated so, in the absence of a length parameter in the libxml API, the length of the array can be determined. Adding a a zero element at the end of the array like so fixes the issue:

    val xPathCValues = (xPathToNode.encodeToByteArray().toUByteArray() + 0u).toCValues()
    

    The cstr extension property on String returns a CString instance, which extends CValues<ByteVar>. CString adds the trailing 0-byte to the array copied to native memory when asked for a native pointer. Unfortunately there does not seem to exist a CValues<UByteVar> equivalent which is required for libxml's xmlChar.