From e4721f5b530397511e4479f8f2d07b95142cb8a2 Mon Sep 17 00:00:00 2001 From: Sabith Saheb Date: Fri, 9 Oct 2026 19:00:15 +0530 Subject: [PATCH] escape ampersand entity prefixes in pointer paths A dynamic property name containing the literal text ' produced the same asPath() as a name containing a real quote, so re-evaluating the stored path resolved to the wrong entry. escape() now emits & for an ampersand that starts an entity sequence and the parser's unescape() decodes it in a single pass. Lone ampersands are unchanged. --- .../commons/jxpath/ri/model/NodePointer.java | 49 ++++++++++--------- .../apache/commons/jxpath/ri/parser/XPath.jj | 33 ++++++++++--- .../commons/jxpath/ri/parser/XPathParser.java | 30 +++++++++--- .../dynamic/DynamicPropertiesModelTest.java | 18 +++++++ 4 files changed, 92 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/NodePointer.java b/src/main/java/org/apache/commons/jxpath/ri/model/NodePointer.java index 216de5db8..65c1983f3 100644 --- a/src/main/java/org/apache/commons/jxpath/ri/model/NodePointer.java +++ b/src/main/java/org/apache/commons/jxpath/ri/model/NodePointer.java @@ -398,38 +398,43 @@ public NodePointer createPath(final JXPathContext context, final Object value) { } /** - * Return a string escaping single and double quotes. + * Return a string escaping single and double quotes, as well as any ampersand that would otherwise be read back as the start of an {@code '}, + * {@code "} or {@code &} sequence. Without the latter, two distinct names, one containing a quote and one containing the corresponding entity + * sequence, would produce the same path. * * @param string string to treat * @return string with any necessary changes made. */ protected String escape(final String string) { - final char[] c = { '\'', '"' }; - final String[] esc = { "'", """ }; StringBuilder sb = null; - for (int i = 0; sb == null && i < c.length; i++) { - if (string.indexOf(c[i]) >= 0) { - sb = new StringBuilder(string); - } - } - if (sb == null) { - return string; - } - for (int i = 0; i < c.length; i++) { - if (string.indexOf(c[i]) < 0) { - continue; + for (int i = 0; i < string.length(); i++) { + final char c = string.charAt(i); + String entity = null; + switch (c) { + case '\'': + entity = "'"; + break; + case '"': + entity = """; + break; + case '&': + if (string.startsWith("apos;", i + 1) || string.startsWith("quot;", i + 1) || string.startsWith("amp;", i + 1)) { + entity = "&"; + } + break; + default: + break; } - int pos = 0; - while (pos < sb.length()) { - if (sb.charAt(pos) == c[i]) { - sb.replace(pos, pos + 1, esc[i]); - pos += esc[i].length(); - } else { - pos++; + if (entity != null) { + if (sb == null) { + sb = new StringBuilder(string.length() * 2).append(string, 0, i); } + sb.append(entity); + } else if (sb != null) { + sb.append(c); } } - return sb.toString(); + return sb == null ? string : sb.toString(); } /** diff --git a/src/main/java/org/apache/commons/jxpath/ri/parser/XPath.jj b/src/main/java/org/apache/commons/jxpath/ri/parser/XPath.jj index 6146aff8d..bb7dd8509 100644 --- a/src/main/java/org/apache/commons/jxpath/ri/parser/XPath.jj +++ b/src/main/java/org/apache/commons/jxpath/ri/parser/XPath.jj @@ -61,17 +61,34 @@ PARSER_BEGIN(XPathParser) } private String unescape(String string){ - int index = string.indexOf("'"); - while (index != -1){ - string = string.substring(0, index) + "\'" + string.substring(index + 6); - index = string.indexOf("'"); + int index = string.indexOf('&'); + if (index == -1){ + return string; } - index = string.indexOf("""); + StringBuilder sb = new StringBuilder(string.length()); + int pos = 0; while (index != -1){ - string = string.substring(0, index) + "\"" + string.substring(index + 6); - index = string.indexOf("""); + sb.append(string, pos, index); + if (string.startsWith("'", index)){ + sb.append('\''); + pos = index + 6; + } + else if (string.startsWith(""", index)){ + sb.append('"'); + pos = index + 6; + } + else if (string.startsWith("&", index)){ + sb.append('&'); + pos = index + 5; + } + else { + sb.append('&'); + pos = index + 1; + } + index = string.indexOf('&', pos); } - return string; + sb.append(string, pos, string.length()); + return sb.toString(); } } diff --git a/src/main/java/org/apache/commons/jxpath/ri/parser/XPathParser.java b/src/main/java/org/apache/commons/jxpath/ri/parser/XPathParser.java index 16cdb6120..643daa478 100644 --- a/src/main/java/org/apache/commons/jxpath/ri/parser/XPathParser.java +++ b/src/main/java/org/apache/commons/jxpath/ri/parser/XPathParser.java @@ -3813,17 +3813,31 @@ final public Object UnaryExpr() throws ParseException { } private String unescape(String string) { - int index = string.indexOf("'"); - while (index != -1) { - string = string.substring(0, index) + "\'" + string.substring(index + 6); - index = string.indexOf("'"); + int index = string.indexOf('&'); + if (index == -1) { + return string; } - index = string.indexOf("""); + final StringBuilder sb = new StringBuilder(string.length()); + int pos = 0; while (index != -1) { - string = string.substring(0, index) + "\"" + string.substring(index + 6); - index = string.indexOf("""); + sb.append(string, pos, index); + if (string.startsWith("'", index)) { + sb.append('\''); + pos = index + 6; + } else if (string.startsWith(""", index)) { + sb.append('"'); + pos = index + 6; + } else if (string.startsWith("&", index)) { + sb.append('&'); + pos = index + 5; + } else { + sb.append('&'); + pos = index + 1; + } + index = string.indexOf('&', pos); } - return string; + sb.append(string, pos, string.length()); + return sb.toString(); } /* [18] UnionExpr ::= PathExpr | UnionExpr '|' PathExpr */ diff --git a/src/test/java/org/apache/commons/jxpath/ri/model/dynamic/DynamicPropertiesModelTest.java b/src/test/java/org/apache/commons/jxpath/ri/model/dynamic/DynamicPropertiesModelTest.java index 8be0fc2c6..53e90bbec 100644 --- a/src/test/java/org/apache/commons/jxpath/ri/model/dynamic/DynamicPropertiesModelTest.java +++ b/src/test/java/org/apache/commons/jxpath/ri/model/dynamic/DynamicPropertiesModelTest.java @@ -68,6 +68,24 @@ void testAttributeName() { assertXPathValueAndPointer(context, "/map[@name='Key:4:5']", null, "/map[@name='Key:4:5']"); } + /** + * A property name containing a literal "&apos;" sequence must produce a pointer path distinct from the path of a property name containing an actual + * quote, and each path must evaluate back to its own entry. + */ + @Test + void testAttributeNameEntityRoundTrip() { + final Map map = (Map) context.getValue("map"); + map.put("Key'7", "quoted"); + map.put("Key'7", "entity"); + map.put("Tom & Jerry", "cartoon"); + assertXPathValueAndPointer(context, "map[@name = \"Key'7\"]", "quoted", "/map[@name='Key'7']"); + assertXPathValueAndPointer(context, "map[@name = 'Key&apos;7']", "entity", "/map[@name='Key&apos;7']"); + // a lone ampersand needs no escaping + assertXPathValueAndPointer(context, "map[@name = 'Tom & Jerry']", "cartoon", "/map[@name='Tom & Jerry']"); + assertEquals("quoted", context.getValue(context.getPointer("map[@name = \"Key'7\"]").asPath())); + assertEquals("entity", context.getValue(context.getPointer("map[@name = 'Key&apos;7']").asPath())); + } + @Test void testAxisChild() { assertXPathValue(context, "map/Key1", "Value 1");