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?
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
.