Repository navigation
XML connector: update, delete and authenticate break on an entry below another element #182
Description
Found while reviewing #181. Line references are to master at ccb33e9. Reproduced with XMLHandlerImpl directly and the test schema ef2bc95b-76e0-48e2-86d6-4d4f44d4e4a4.xsd, on master and on #181's head (6beb334): the results are the same on both.
getEntry finds an entry with the XQuery /icf:OpenICFContainer//ri:CLASS[...] (QueryBuilder.java#L97-L100), so it also finds an entry below another element, for example inside <ri:wrapper> or inside the value element of another entry. #181 keeps that: its EntryIndex answers with the same entries as the XQuery, nested ones included. The code that then works on the entry assumes a direct child of the container with no entries inside it:
updateremoves descendants as if they were children.removeChildrenFromElementcollects the elements to replace withgetElementsByTagName(L724-L735), which returns every descendant with that name, and callselement.removeChildon each. When the entry holds another entry, the firstremoveChildon the inner entry's element throwsDOMException NOT_FOUND_ERR. The entry's own element has already been removed by then, and the document is already marked dirty (L333-L334), sodispose()saves the entry without it.deleteremoves the entry from the container only. It callsgetDocument().getDocumentElement().removeChild(entry)(L372), which throwsDOMException NOT_FOUND_ERRfor an entry whose parent is another element. The caller gets an uncheckedDOMException, and the entry stays.authenticatereads the password from any descendant. It takesentry.getElementsByTagName("icf:__PASSWORD__").item(0)(L463-L465). For an entry without its own__PASSWORD__that holds an entry with one, the inner entry's password is accepted and the outer entry'sUidis returned. For an entry without any__PASSWORD__element,item(0)isnulland the call fails with aNullPointerExceptioninstead ofInvalidPasswordException.
Observed, on a store with these entries:
<ri:__ACCOUNT__><icf:__UID__>u1</icf:__UID__><icf:__NAME__>nopass</icf:__NAME__><ri:lastname>L1</ri:lastname></ri:__ACCOUNT__>
<ri:wrapper><ri:__ACCOUNT__><icf:__UID__>u8</icf:__UID__><icf:__NAME__>nested</icf:__NAME__><icf:__PASSWORD__>p8</icf:__PASSWORD__><ri:lastname>L8</ri:lastname></ri:__ACCOUNT__></ri:wrapper>
<ri:__ACCOUNT__><icf:__UID__>u11</icf:__UID__><icf:__NAME__>holder</icf:__NAME__><ri:lastname><ri:__ACCOUNT__><icf:__UID__>u12</icf:__UID__><icf:__NAME__>inner</icf:__NAME__><icf:__PASSWORD__>p12</icf:__PASSWORD__></ri:__ACCOUNT__></ri:lastname></ri:__ACCOUNT__>| Call | Result | File after dispose() |
|---|---|---|
update(ACCOUNT, Uid("u11"), {__NAME__ = "new"}) |
DOMException: NOT_FOUND_ERR |
u11 saved without __NAME__; u12 unchanged |
delete(ACCOUNT, Uid("u8")) |
DOMException: NOT_FOUND_ERR |
unchanged, u8 still there |
authenticate("holder", "p12") |
returns Uid u11 |
unchanged |
authenticate("nopass", "x") |
NullPointerException (NodeList.item(int) is null) |
unchanged |
Such a store does not validate against the schema (lastname is xsd:string, ri:wrapper is not declared, __PASSWORD__ is required), and the connector does not write this shape itself. But it does not validate the file on load either, and it treats the nested entries as entries everywhere else: search returns them, and #181 indexes them. So a hand-edited or generated file gets a half-applied update saved to disk, a delete that fails with an unchecked exception, and a login checked against another entry's password.
Possible fixes, one of them or both:
- make the three operations work on the entry's own children: remove only the entry's child elements in
update, remove the entry throughentry.getParentNode()indelete, read__PASSWORD__(and the__UID__/__NAME__thatauthenticatereturns) from the entry's child elements only, and reject a missing password withInvalidPasswordException; - or stop treating nested elements as entries (
/icf:OpenICFContainer/ri:CLASSinstead of//, inQueryBuilderand in [#157] Find XML entries by __UID__ and __NAME__ through an index #181'sEntryIndex), which changes what a search returns for such files.
Tests that depend on the current behaviour:
XMLHandlerReloadTests#failedDeleteOfANestedEntryLeavesTheFileAlone(L582, from [#157] Load and save the XML file only when it has changed #177) expects theDOMExceptionon the nested delete as its failing delete.- XML connector: a rejected update() has already removed the attribute's old values #178 names
removeChildrenFromElementthrowing midway on such a store as the failure thatfailedUpdateLeavesTheFileInAgreementWithMemorycould use once XML connector: a rejected update() has already removed the attribute's old values #178 is fixed.
Activity
maximthomas commentedon Oct 12, 2026
Two more cases in the same code, on a store that validates against the test schema.
updateandauthenticatefind the entry's elements by qualified name, so they depend on the prefixes the file uses and on whether an element is qualified at all. Neither case is about nesting, so they may fit #182's fix or an issue of their own.removeChildrenFromElementcallsgetElementsByTagName(childName)(L725) with the name thatprefixAttributeNamebuilds,icf:orri:plus the attribute name (L334, L737-L747).authenticatereadsicf:__PASSWORD__,icf:__UID__andicf:__NAME__the same way (L463, L477, L482).getElementsByTagNamecompares the tag name as written in the file, prefix included.getEntryfinds the entry by namespace URI: on master its XQuery declaresicfandriby URI (QueryBuilder.java#L85-L95) over a namespace-aware DOM, and on #181EntryIndexcomparesgetNamespaceURI()andgetLocalName().ConnectorObjectCreator, which builds the search results, reads the entry's children by local name only (ConnectorObjectCreator.java#L79-L85). SogetEntryand search see the entry's elements, andupdate's removal andauthenticate's reads do not.Reproduced with
XMLHandlerImplthroughConcurrentXMLHandler, and the searches also throughXMLConnector, with the test schemaef2bc95b-76e0-48e2-86d6-4d4f44d4e4a4.xsd, on master (ccb33e9) and on #181's head (6beb334): the results are the same on both. The store, whichxmllint --schema ef2bc95b-76e0-48e2-86d6-4d4f44d4e4a4.xsdvalidates:ICF elements under another prefix.
dispose()authenticate("pfx", "p20")NullPointerException(NodeList.item(int)is null)authenticate("pfx", "wrong")NullPointerExceptionupdate(ACCOUNT, Uid("u20"), {__NAME__ = "pfx2"})Uid u20<x:__NAME__>pfx</x:__NAME__>stays and<icf:__NAME__ xmlns:icf="…/resource-schema-1.xsd">pfx2</icf:__NAME__>is appendedAfter that update, a search for the old name
pfxstill returnsu20, reported with the namepfx2, before and after a reload of the file. With the same store written with theicfprefix, the firstauthenticatereturnsu20, the second returnsnull(whichXMLConnector.authenticateturns intoInvalidPasswordException), and the update replaces the name.RI attributes. The test schema has no
elementFormDefault, so its local elements, the RI attributes such aslastname, are in no namespace in a valid store, as in the one above.update(ACCOUNT, Uid("u20"), {lastname = "NEW"})on that store, with thexprefix and withicfalike, keeps<lastname>L20</lastname>and appends<ri:lastname>NEW</ri:lastname>, and a search then reportslastname=NEW. With the RI namespace bound to another prefix (<r:lastname>, which the schema does not accept), the update also keeps the old element and appends<ri:lastname xmlns:ri="…">NEW</ri:lastname>.For the first of #182's possible fixes (working on the entry's own children), this means that finding a child by namespace URI and local name covers the ICF elements but not the unqualified RI attributes. Matching the way
ConnectorObjectCreatorreads, by local name among the entry's own children, would cover both, andupdateand search would agree on which elements an attribute has.getElementsByTagNameNSon the entry would fix the prefix but still descend into nested entries.A note on validity: the connector writes RI attributes qualified, as
ri:lastname(L630-L631), which is also whatwriteAccountswrites for the tests, andxmllintrejects that (Element '{…/ef2bc95b-76e0-48e2-86d6-4d4f44d4e4a4}lastname': This element is not expected. Expected is one of ( firstname, lastname ).). The schema's container also takes a single element (<xsd:sequence><xsd:any/></xsd:sequence>, resource-schema-1.xsd#L45-L54), which is why the store above has one entry: a store with more entries fails for that reason too. So the stores the connector writes do not validate against this schema either.