Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ target/
nb-configuration.xml
nbactions.xml
/CLAUDE.md

9 changes: 5 additions & 4 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,17 @@
<body>
<release version="3.7.1" date="YYYY-MM-DD" description="This is a feature and maintenance release. Java 8 or later is required.">
<!-- ADD -->
<action dev="henrib" type="add" issue="JEXL-467">IntelliJ and VSCode editors (TextMate bundle) support for JEXL.</action>
<action dev="henrib" due-to="Aurelien Mino" type="add" issue="JEXL-472">Add property access support for Java record component accessors.</action>
<action dev="henrib" due-to="Claude" type="add" issue="JEXL-469">Add JexlFeatures.namespaceInstantiation(boolean) to control whether a namespace bound to a Class or class-name string is reflectively auto-instantiated into a functor; string namespaces now resolve through the permission-aware uberspect.</action>
<action dev="henrib" type="add" issue="JEXL-467">IntelliJ and VSCode editors (TextMate bundle) support for JEXL.</action>
<!-- FIX -->
<action dev="henrib" due-to="Claude" type="fix" issue="JEXL-471">Runtime hardening: make regex matching (=~ operator) interruptible, enforce MathContext precision on BigInteger arithmetic results, and prevent O(n²) DoS from huge BigInteger literals at parse time.</action>
<action dev="henrib" due-to="Claude" type="fix" issue="JEXL-470">Fix several parser and interpreter correctness issues: Unicode escape hex-digit validation, safe-navigation array-access child indexing, regex escape preservation, empty()/size() error and cancellation propagation, and switch+continue semantics.</action>
<action dev="henrib" due-to="Claude" type="fix" issue="JEXL-468">Improve robustness of introspection permissions and sandbox delegation: close nested-class and interface-method denial gaps, fix class-initialization ordering, gate sandbox iteration, fix permission-parser polarity, deny second-stage compiler surface under RESTRICTED, and enforce parser feature restrictions in sub-parsers.</action>
<action dev="henrib" type="fix" issue="JEXL-466">IllegalStateException parsing a template with string interpolation.</action>
<action dev="NikRom5531" due-to="Felix Rudolphi" type="fix" issue="JEXL-411">Leading zeroes in floating point numbers should be optional.</action>
<action dev="ggregory" type="fix" due-to="Gary Gregory">Pick up commons.jacoco.version from the parent POM.</action>
<action dev="ggregory" type="fix" due-to="Gary Gregory">Add messages when throwing NullPointerException.</action>
<action dev="henrib" due-to="Claude" type="fix" issue="JEXL-468">Improve robustness of introspection permissions and sandbox delegation: close nested-class and interface-method denial gaps, fix class-initialization ordering, gate sandbox iteration, fix permission-parser polarity, deny second-stage compiler surface under RESTRICTED, and enforce parser feature restrictions in sub-parsers.</action>
<action dev="henrib" due-to="Claude" type="fix" issue="JEXL-470">Fix several parser and interpreter correctness issues: Unicode escape hex-digit validation, safe-navigation array-access child indexing, regex escape preservation, empty()/size() error and cancellation propagation, and switch+continue semantics.</action>
<action dev="henrib" due-to="Claude" type="fix" issue="JEXL-471">Runtime hardening: make regex matching (=~ operator) interruptible, enforce MathContext precision on BigInteger arithmetic results, and prevent O(n²) DoS from huge BigInteger literals at parse time.</action>
<!-- UPDATE -->
<action type="update" dev="ggregory" due-to="Gary Gregory">Bump org.apache.commons:commons-parent from 102 to 105.</action>
</release>
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/apache/commons/jexl3/JexlFeatures.java
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ public JexlFeatures referenceCapture(final boolean flag) {
*
* @param flag true to enable, false to disable
* @return this features instance
* @since 3.6
* @since 3.7.1
*/
public JexlFeatures namespaceInstantiation(final boolean flag) {
setFeature(NAMESPACE_INSTANTIATE, flag);
Expand Down Expand Up @@ -966,7 +966,7 @@ public boolean supportsReferenceCapture() {
* from a class or class-name binding?
*
* @return true if namespace auto-instantiation is allowed, false otherwise
* @since 3.6
* @since 3.7.1
*/
public boolean supportsNamespaceInstantiation() {
return getFeature(NAMESPACE_INSTANTIATE);
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/apache/commons/jexl3/JexlOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ public boolean isConstCapture() {
* binding allowed?
*
* @return true if namespace auto-instantiation is allowed, false otherwise
* @since 3.6
* @since 3.7.1
*/
public boolean isNamespaceInstantiation() {
return isSet(NAMESPACE_INSTANTIATE, flags);
Expand Down Expand Up @@ -453,7 +453,7 @@ public void setConstCapture(final boolean flag) {
* auto-instantiated into a functor.
*
* @param flag true to enable, false to disable
* @since 3.6
* @since 3.7.1
*/
public void setNamespaceInstantiation(final boolean flag) {
flags = set(NAMESPACE_INSTANTIATE, flags, flag);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,60 @@
import java.lang.invoke.MethodType;

/**
* Utility for Java9+ backport in Java8 of class and module related methods.
* Utility for Java9+ backport in Java8 of class and module related methods, and Java16+ backport of
* record introspection ({@code Class#isRecord()}, {@code Class#getRecordComponents()}).
*/
final class ClassTool {

/** The Class.getModule() method. */
/** {@code Class#isRecord()}; null on a pre Java-16 runtime. */
private static final MethodHandle IS_RECORD;

/** {@code Class#getRecordComponents()}; null on a pre Java-16 runtime. */
private static final MethodHandle GET_RECORD_COMPONENTS;

/** {@code java.lang.reflect.RecordComponent#getName()}; null on a pre Java-16 runtime. */
private static final MethodHandle RECORD_COMPONENT_GET_NAME;

/** {@code Class#getModule()}; null on a pre Java-9 runtime. */
private static final MethodHandle GET_MODULE;

/** The Class.getPackageName() method. */
/** {@code Class#getPackageName()}; null on a pre Java-9 runtime. */
private static final MethodHandle GET_PKGNAME;

/** The Module.isExported(String packageName) method. */
/** {@code Module#isExported(String, Module)}; null on a pre Java-9 runtime. */
private static final MethodHandle IS_EXPORTED;

/** The Module of JEXL itself. */
/** The {@code java.lang.Module} that declares this class; null on a pre Java-9 runtime. */
private static final Object JEXL_MODULE;

static {
final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
final ClassLoader loader = ClassTool.class.getClassLoader();

// Java 16+ record introspection backport
MethodHandle isRecord = null;
MethodHandle getRecordComponents = null;
MethodHandle recordComponentGetName = null;
try {
final Class<?> componentc = loader.loadClass("java.lang.reflect.RecordComponent");
final Class<?> componentArrayc = java.lang.reflect.Array.newInstance(componentc, 0).getClass();
isRecord = LOOKUP.findVirtual(Class.class, "isRecord", MethodType.methodType(boolean.class));
getRecordComponents = LOOKUP.findVirtual(Class.class, "getRecordComponents", MethodType.methodType(componentArrayc));
recordComponentGetName = LOOKUP.findVirtual(componentc, "getName", MethodType.methodType(String.class));
} catch (final Throwable xnotfound) {
// ignore all; records unsupported on this runtime
}
IS_RECORD = isRecord;
GET_RECORD_COMPONENTS = getRecordComponents;
RECORD_COMPONENT_GET_NAME = recordComponentGetName;

// Java 9+ module reflection backport
MethodHandle getModule = null;
MethodHandle getPackageName = null;
MethodHandle isExported = null;
Object myModule = null;
try {
final Class<?> modulec = ClassTool.class.getClassLoader().loadClass("java.lang.Module");
final Class<?> modulec = loader.loadClass("java.lang.Module");
if (modulec != null) {
getModule = LOOKUP.findVirtual(Class.class, "getModule", MethodType.methodType(modulec));
if (getModule != null) {
Expand All @@ -64,6 +94,46 @@ final class ClassTool {
IS_EXPORTED = isExported;
}

/**
* Whether the given class is a record on this runtime.
*
* @param clazz the class to check
* @return true if clazz is a record, false if it is not or if records are unsupported here
*/
static boolean isRecord(final Class<?> clazz) {
try {
return IS_RECORD != null && (boolean) IS_RECORD.invoke(clazz);
} catch (final Throwable xfail) {
return false;
}
}

/**
* Whether the given class declares a record component named {@code property}.
* <p>Used only to confirm {@code property} is a genuine record component before the accessor is
* resolved through the (permission-checked) {@link Introspector}; the accessor method instances
* gathered here are discarded.</p>
*
* @param clazz the record class
* @param property the property name to match against the record's components
* @return true if clazz declares a record component named property
*/
static boolean hasRecordComponent(final Class<?> clazz, final String property) {
if (GET_RECORD_COMPONENTS != null && RECORD_COMPONENT_GET_NAME != null) {
try {
final Object[] components = (Object[]) GET_RECORD_COMPONENTS.invoke(clazz);
for (final Object component : components) {
if (property.equals(RECORD_COMPONENT_GET_NAME.invoke(component))) {
return true;
}
}
} catch (final Throwable xfail) {
// ignore and fall through to return false
}
}
return false;
}

/**
* Gets the package name of a class (class.getPackage() may return null).
*
Expand Down Expand Up @@ -116,7 +186,7 @@ static String getPackageName(final Class<?> clz) {
* The code performs the following sequence through reflection (since the same jar can run
* on a Java8 or Java9+ runtime and the module features does not exist on 8).
* {@code
* Module jexlModule ClassTool.getClass().getModule();
* Module jexlModule = ClassTool.class.getModule();
* Module module = declarator.getModule();
* return module.isExported(declarator.getPackageName(), jexlModule);
* }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.jexl3.internal.introspection;

import java.lang.reflect.InvocationTargetException;

import org.apache.commons.jexl3.JexlException;

/**
* Specialized executor to get a property from a Java record component.
* <p>A record (JEP 395, Java 16+) exposes one accessor per component, named exactly like the
* component - {@code x()}, not {@code getX()}. {@link PropertyGetExecutor} only looks for the bean
* convention, so a record component was otherwise never resolved as a property.</p>
* <p>Record detection and lookup are delegated to {@link ClassTool}, which resolves the relevant
* methods through reflection so this module remains usable on the Java 8 baseline this project still
* targets; on such a runtime, {@code Class#isRecord()} and {@code Class#getRecordComponents()} simply
* do not exist and discovery quietly reports no match instead of failing to link.</p>
*
* @since 3.7.1
*/
public final class RecordGetExecutor extends AbstractExecutor.Get {

/** A static signature for method(). */
private static final Object[] EMPTY_PARAMS = {};

/**
* Discovers a RecordGetExecutor.
* <p>The class must be a record and declare a component named {@code property}; the accessor
* method itself is resolved through {@code is} so it goes through the same permission checks as
* every other property accessor.</p>
*
* @param is the introspector
* @param clazz the class to find the accessor method from
* @param property the property (record component) name to find
* @return the executor if found, null otherwise
*/
public static RecordGetExecutor discover(final Introspector is, final Class<?> clazz, final String property) {
if (property == null || property.isEmpty() || !ClassTool.isRecord(clazz)
|| !ClassTool.hasRecordComponent(clazz, property)) {
return null;
}
final java.lang.reflect.Method method = is.getMethod(clazz, property, EMPTY_PARAMS);
return method == null ? null : new RecordGetExecutor(clazz, method, property);
}

/** The property (record component name). */
private final String property;

/**
* Creates an instance.
*
* @param clazz the class the accessor applies to
* @param method the accessor method held by this executor
* @param identifier the property to get
*/
private RecordGetExecutor(final Class<?> clazz, final java.lang.reflect.Method method, final String identifier) {
super(clazz, method);
property = identifier;
}

@Override
public Object getTargetProperty() {
return property;
}

@Override
public Object invoke(final Object o) throws IllegalAccessException, InvocationTargetException {
return method == null ? null : method.invoke(o, (Object[]) null);
}

@Override
public Object tryInvoke(final Object o, final Object identifier) {
if (o != null && method != null
&& property.equals(castString(identifier))
&& objectClass.equals(o.getClass())) {
try {
return method.invoke(o, (Object[]) null);
} catch (IllegalAccessException | IllegalArgumentException xill) {
return TRY_FAILED; // fail
} catch (final InvocationTargetException xinvoke) {
throw JexlException.tryFailed(xinvoke); // throw
}
}
return TRY_FAILED;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ public JexlPropertyGet getPropertyGet(
if (executor == null) {
executor = BooleanGetExecutor.discover(is, clazz, property);
}
if (executor == null) {
// or a record component accessor, foo() rather than getFoo()
executor = RecordGetExecutor.discover(is, clazz, property);
}
break;
case MAP:
// let's see if we are a map...
Expand Down
3 changes: 3 additions & 0 deletions src/test/java/org/apache/commons/jexl3/ClassCreatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -315,6 +316,8 @@ void testFunctorThree() throws Exception {

@Test
void testMany() throws Exception {
// FIXME: java 28 has a new GC that seems to prevent classes from being GCed, so this test fails
Assumptions.assumeTrue(featureVersion() < 28, "testMany is known to fail on Java 28+");
// abort test if class creator cannot run
if (!ClassCreator.canRun) {
return;
Expand Down
5 changes: 5 additions & 0 deletions src/test/java/org/apache/commons/jexl3/JexlTestCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,9 @@ public void processPragma(final String key, final Object value) {
processPragma(null, key, value);
}
}

public static int featureVersion() {
final String spec = System.getProperty("java.specification.version");
return spec.startsWith("1.") ? Integer.parseInt(spec.substring(2)) : Integer.parseInt(spec);
}
}
Loading
Loading