This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/commons-secure-xml.git
The following commit(s) were added to refs/heads/main by this push:
new 632cb04 Delegate the Java 18 XPathFactory property methods (#76)
632cb04 is described below
commit 632cb04d3ce06303387f370338cb942a719cef7a
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Tue Sep 1 13:48:31 2026 +0200
Delegate the Java 18 XPathFactory property methods (#76)
Java 18 added setProperty and getProperty to XPathFactory, with
defaults that throw UnsupportedOperationException and an override in
the JDK implementation that accepts its jdk.xml.xpath* limits. The
wrapper is compiled against the Java 8 API, so it inherited those
defaults and answered for the delegate: on Java 18 or later a caller
could neither tighten nor read a limit through a secure factory, though
nothing could be loosened either.
Delegate both through method handles, the way newDefaultInstance
already reaches a later-release static method. Neither carries
@Override, which would not compile at release 8; each overrides at run
time where the platform declares the method, and reports the inherited
UnsupportedOperationException where it does not.
The tests reach the pair reflectively for the same reason, which is
also how a Java 18 caller resolves it, and skip on a platform without
the methods.
Assisted-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CLnTBsvmYtxzNTWVGNyz33
Co-authored-by: Gary Gregory <[email protected]>
---
src/changes/changes.xml | 1 +
.../commons/xml/secure/MethodHandleFactory.java | 22 +++++++++++
.../commons/xml/secure/SecureXPathFactory.java | 42 +++++++++++++++++++++
.../commons/xml/secure/SecureXPathFactoryTest.java | 43 ++++++++++++++++++++++
4 files changed, 108 insertions(+)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index c1a755b..5542ad0 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -48,6 +48,7 @@ The <action> type attribute can be add, update, fix, or
remove.
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary
Gregory">Parse a Source opted in by a caller-supplied URIResolver using a
secure parser.</action>
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary
Gregory">Secure the document parse behind the InputSource-taking XPath
evaluation entry points.</action>
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary
Gregory">Fall back to the standard factory lookup in the DOM, SAX and schema
newDefaultInstance methods on Android.</action>
+ <action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary
Gregory">Delegate the XPathFactory setProperty and getProperty methods
introduced in Java 18, so the implementation's properties stay reachable on a
secure factory.</action>
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary
Gregory">Bound the content model a schema expands into, so a compact schema
with a large maxOccurs cannot exhaust memory or CPU during validation.</action>
<!-- UPDATE -->
<action type="update" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary
Gregory"
issue="COMMONSXML-1,COMMONSXML-5,COMMONSXML-6,COMMONSXML-7,COMMONSXML-8">Recognize
XML implementations by the JAXP features and properties they support instead
of by their implementation class name, extending the securing to any compliant
implementation.</action>
diff --git
a/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java
b/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java
index 75b8c99..d2fd64e 100644
--- a/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java
+++ b/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java
@@ -49,6 +49,28 @@ static MethodHandle findStatic(final Class<?>
refcAndReturnType, final String na
}
}
+ /**
+ * Finds an instance method handle for the given class, method name and
signature.
+ *
+ * <p>Used to reach a method a later Java release added to a class this
library compiles against at an earlier one; the handle is {@code null} where the
+ * running platform does not have it.</p>
+ *
+ * @param refc the class to search for the method.
+ * @param name the name of the method.
+ * @param returnType the method's return type.
+ * @param parameterTypes the method's parameter types.
+ * @return the method handle, or {@code null} if not found.
+ * @throws SecurityException if a security manager is present and it <a
href="MethodHandles.Lookup.html#secmgr">refuses access</a>.
+ * @throws NullPointerException if any argument is null.
+ */
+ static MethodHandle findVirtual(final Class<?> refc, final String name,
final Class<?> returnType, final Class<?>... parameterTypes) {
+ try {
+ return MethodHandles.publicLookup().findVirtual(refc, name,
MethodType.methodType(returnType, parameterTypes));
+ } catch (final ReflectiveOperationException e) {
+ return null;
+ }
+ }
+
static <T, E extends Throwable> T invokeExact(final ThrowableCallable<T>
methodHandle, final Class<E> rethrow) throws E {
try {
return methodHandle.call();
diff --git
a/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java
b/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java
index 3c3aa9b..5724d1d 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java
@@ -74,6 +74,23 @@ public boolean getFeature(final String name) throws
XPathFactoryConfigurationExc
return delegate.getFeature(name);
}
+ /**
+ * Reports a property of the delegate, the Java 18 {@code
XPathFactory.getProperty(String)}.
+ *
+ * <p>Not marked {@code @Override}: this library compiles against the
Java 8 API, where {@link XPathFactory} declares no such method, so the
annotation
+ * would not compile. At run time on Java 18 or later it overrides the
inherited method, which would otherwise answer for the wrapper and hide the
+ * delegate's own limits ({@code jdk.xml.xpath*}) behind an {@code
UnsupportedOperationException}.</p>
+ *
+ * @param name the property name.
+ * @return the delegate's value for the property.
+ */
+ public String getProperty(final String name) {
+ if (MH_getProperty == null) {
+ throw new
UnsupportedOperationException("XPathFactory.getProperty(String) requires Java
18 or later");
+ }
+ return MethodHandleFactory.invokeExact(() -> (String)
MH_getProperty.invokeExact(delegate, name), RuntimeException.class);
+ }
+
@Override
public boolean isObjectModelSupported(final String objectModel) {
return delegate.isObjectModelSupported(objectModel);
@@ -107,6 +124,24 @@ public void setFeature(final String name, final boolean
value) throws XPathFacto
delegate.setFeature(name, value);
}
+ /**
+ * Sets a property on the delegate, the Java 18 {@code
XPathFactory.setProperty(String, String)}; see {@link #getProperty(String)} for
why it carries no
+ * {@code @Override}. The {@code jdk.xml.xpath*} limits reached this
way are processing limits like any other: an operator may tighten them, and
+ * loosening one is reconfiguration.
+ *
+ * @param name the property name.
+ * @param value the value to set.
+ */
+ public void setProperty(final String name, final String value) {
+ if (MH_setProperty == null) {
+ throw new
UnsupportedOperationException("XPathFactory.setProperty(String, String)
requires Java 18 or later");
+ }
+ MethodHandleFactory.invokeExact(() -> {
+ MH_setProperty.invokeExact(delegate, name, value);
+ return null;
+ }, RuntimeException.class);
+ }
+
@Override
public void setXPathFunctionResolver(final XPathFunctionResolver
resolver) {
delegate.setXPathFunctionResolver(resolver);
@@ -123,6 +158,13 @@ public void setXPathVariableResolver(final
XPathVariableResolver resolver) {
private static final MethodHandle MH_newDefaultInstance =
MethodHandleFactory.findStatic(XPathFactory.class, "newDefaultInstance");
+ /** {@code XPathFactory.getProperty(String)}, added in Java 18; {@code
null} on earlier releases, where the method does not exist to be called. */
+ private static final MethodHandle MH_getProperty =
MethodHandleFactory.findVirtual(XPathFactory.class, "getProperty",
String.class, String.class);
+
+ /** {@code XPathFactory.setProperty(String, String)}, added in Java 18;
{@code null} on earlier releases, where the method does not exist to be called.
*/
+ private static final MethodHandle MH_setProperty =
+ MethodHandleFactory.findVirtual(XPathFactory.class, "setProperty",
void.class, String.class, String.class);
+
/**
* Returns a new, secure {@link XPathFactory} of the system-default
implementation, supporting the default XPath object model.
* <p>
diff --git
a/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java
b/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java
index d6cbdc5..caaa5bd 100644
--- a/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java
+++ b/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java
@@ -17,18 +17,23 @@
package org.apache.commons.xml.secure;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFactoryConfigurationException;
import javax.xml.xpath.XPathFunctionResolver;
import javax.xml.xpath.XPathVariableResolver;
+import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -122,4 +127,42 @@ public void setXPathVariableResolver(final
XPathVariableResolver resolver) {
};
assertThrows(SecureException.class, () ->
SecureXPathFactory.secure(rejectingFactory));
}
+
+ /** A processing limit the JDK's XPath implementation recognizes through
the Java 18 property API. */
+ private static final String XPATH_GROUP_LIMIT =
"jdk.xml.xpathExprGrpLimit";
+
+ /**
+ * The Java 18 {@code XPathFactory} property method of the given name, or
an aborted test where the platform predates it.
+ *
+ * <p>Reached reflectively because this suite compiles against the Java 8
API, the same reason the wrapper delegates the pair through method handles: the
+ * call has to resolve at run time, which is also exactly how a Java 18
caller reaches it.</p>
+ */
+ private static Method propertyMethod(final String name, final Class<?>...
parameterTypes) {
+ try {
+ return XPathFactory.class.getMethod(name, parameterTypes);
+ } catch (final NoSuchMethodException e) {
+ Assumptions.abort("XPathFactory." + name + " requires Java 18 or
later");
+ throw new AssertionError("unreachable");
+ }
+ }
+
+ @Test
+ void delegatesTheJava18PropertyApi() throws Exception {
+ // The wrapper is compiled against the Java 8 API, so without an
explicit delegation the inherited default answers for it and every property the
+ // implementation supports, including its own limits, becomes
unreachable through a secured factory.
+ final Method setProperty = propertyMethod("setProperty", String.class,
String.class);
+ final Method getProperty = propertyMethod("getProperty", String.class);
+ final XPathFactory factory = SecureXPathFactory.newDefaultInstance();
+ setProperty.invoke(factory, XPATH_GROUP_LIMIT, "5");
+ assertEquals("5", getProperty.invoke(factory, XPATH_GROUP_LIMIT), "a
property set on the secured factory must be read back from the delegate");
+ }
+
+ @Test
+ void reportsAnUnknownPropertyLikeTheDelegate() {
+ final Method getProperty = propertyMethod("getProperty", String.class);
+ final XPathFactory factory = SecureXPathFactory.newDefaultInstance();
+ final InvocationTargetException thrown =
assertThrows(InvocationTargetException.class,
+ () -> getProperty.invoke(factory, "jdk.xml.noSuchProperty"));
+ assertInstanceOf(IllegalArgumentException.class, thrown.getCause(),
"an unrecognized property must surface the delegate's own rejection");
+ }
}