This is an automated email from the ASF dual-hosted git repository.

rvesse pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git

commit df92b65808219ce0db9bf38a83d42be42009282a
Author: Rob Vesse <[email protected]>
AuthorDate: Wed Sep 30 11:40:30 2026 +0100

    afn:sprintf() bounds output
    
    The afn:sprintf() function provides direct access to the Java
    String.format() method from within SPARQL queries.  Badly formed format
    strings could lead to generating excessive output from small inputs.
    The implementation now configures a maximum output size (default 4096
    bytes) to prevent this.
---
 .../jena/sparql/expr/nodevalue/XSDFuncOp.java      | 56 +++++++++++++++++++++-
 .../apache/jena/sparql/function/MathLimits.java    | 23 ++++++++-
 .../org/apache/jena/sparql/expr/TestFunctions.java | 22 +++++++++
 .../function/library/TestFnFunctionsNumeric.java   |  1 -
 .../sparql/function/library/TestMathLimits.java    | 21 ++++++++
 5 files changed, 120 insertions(+), 3 deletions(-)

diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java
index 0e5410d4a5..df7de11a60 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java
@@ -27,10 +27,13 @@ import static 
org.apache.jena.sparql.expr.nodevalue.NumericType.OP_DOUBLE;
 import static org.apache.jena.sparql.expr.nodevalue.NumericType.OP_FLOAT;
 import static org.apache.jena.sparql.expr.nodevalue.NumericType.OP_INTEGER;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.math.MathContext;
 import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.text.Normalizer;
@@ -44,6 +47,7 @@ import javax.xml.datatype.DatatypeConstants;
 import javax.xml.datatype.Duration;
 import javax.xml.datatype.XMLGregorianCalendar;
 
+import org.apache.commons.io.output.ThresholdingOutputStream;
 import org.apache.jena.atlas.lib.IRILib;
 import org.apache.jena.atlas.lib.Lib;
 import org.apache.jena.atlas.lib.StrUtils;
@@ -72,6 +76,26 @@ public class XSDFuncOp
     //   F&O 3.1: section 4.2 (end intro)
     //   https://www.w3.org/TR/xpath-functions/#op.numeric section 4.2
     private static final int DIVIDE_PRECISION = 24;
+
+    /**
+     * The default maximum output length (in bytes) for {@link 
#MAX_SPRINTF_OUTPUT_LENGTH}
+     * <p>
+     * Currently this is 4096 bytes, i.e. 4 kilobytes, as this allows for 
formatting a reasonable amount of content, and
+     * in particular allows for string literals of a modest length to be 
formatted.
+     * </p>
+     */
+    protected static final int DEFAULT_MAX_SPRINTF_OUTPUT_LENGTH = 4096;
+
+    /**
+     * Maximum permitted length (in bytes) for output generated by {@link 
#javaSprintf(NodeValue, List)}.
+     * <p>
+     * This prevents format strings that use large width specifiers from 
generating very large outputs from small
+     * inputs.
+     * </p>
+     */
+    protected static int MAX_SPRINTF_OUTPUT_LENGTH = 
DEFAULT_MAX_SPRINTF_OUTPUT_LENGTH;
+
+
     // --------------------------------
     // Numeric operations
     // http://www.w3.org/TR/xpath-functions/#op.numeric
@@ -567,13 +591,43 @@ public class XSDFuncOp
                 }
             }
 
-            return 
NodeValue.makeString(String.format(formatForOutput,objVals.toArray()));
+            return 
NodeValue.makeString(boundedFormat(formatForOutput,objVals.toArray()));
 
         } catch (IndexOutOfBoundsException ex) {
             throw new ExprEvalException("IndexOutOfBounds", ex);
         }
     }
 
+    /**
+     * Generates formatted output bounding the maximum output length by {@link 
#MAX_SPRINTF_OUTPUT_LENGTH}, this
+     * prevents a badly formed format string generating excessive output.
+     * <p>
+     * This uses the {@link ThresholdingOutputStream} from Apache Commons IO 
to enforce that the maximum length is not
+     * exceeded and throwing an {@link ExprEvalException} as soon as it would 
be which aborts further formatting.
+     * </p>
+     * @param formatStr     Format string
+     * @param args          Format arguments
+     * @return Formatted string
+     * @throws ExprEvalException Thrown if the format string is invalid or too 
much output is generated
+     */
+    private static String boundedFormat(String formatStr, Object[] args) {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        try (ThresholdingOutputStream boundedOutput
+                     = new ThresholdingOutputStream(MAX_SPRINTF_OUTPUT_LENGTH,
+                             stream -> { throw new 
ExprEvalException("Formatted output too large");},
+                             stream -> output)) {
+            Formatter formatter =
+                    new Formatter(boundedOutput, StandardCharsets.UTF_8, 
Locale.getDefault(Locale.Category.FORMAT));
+            formatter.format(formatStr, args);
+            formatter.flush();
+            return output.toString(StandardCharsets.UTF_8);
+        } catch (IllegalFormatException e) {
+            throw new ExprEvalException("Invalid format string", e);
+        } catch (IOException e) {
+            throw new ExprEvalException("IO error applying format");
+        }
+    }
+
     public static NodeValue strlen(NodeValue nvString) {
         Node n = NodeValueOps.checkAndGetStringLiteral("strlen", nvString);
         String str = n.getLiteralLexicalForm();
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/function/MathLimits.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/function/MathLimits.java
index efe497d722..4f7608873a 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/function/MathLimits.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/MathLimits.java
@@ -1,3 +1,24 @@
+/*
+ * 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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+
 package org.apache.jena.sparql.function;
 
 import org.apache.jena.sparql.expr.ExprEvalException;
@@ -67,7 +88,7 @@ public class MathLimits {
      * Calculates how many digits raising {@code a} to the power of {@code b} 
would require.  If the number of digits is
      * very large then throw an expression evaluation error to prevent 
execution.
      * <p>
-     * This is based upon the following mathematical formula, for {@code a^b} 
then Number of Digits is
+     * This is based upon the following mathematical formula, for {@code a^b} 
then number of digits is
      * {@code floor(b * log10(a)) + 1}.
      * </p>
      *
diff --git 
a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestFunctions.java 
b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestFunctions.java
index 9fe5e99838..1ced6aceee 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestFunctions.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestFunctions.java
@@ -34,6 +34,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.Strings;
 import org.apache.jena.atlas.lib.DateTimeUtils;
 import org.apache.jena.datatypes.xsd.XSDDatatype;
@@ -85,6 +86,27 @@ public class TestFunctions
     @Test public void exprSprintf_10()      { test("afn:sprintf('this number 
is equal to %.5f', '1.23456789'^^xsd:double)",NodeValue.makeString("this number 
is equal to "+String.format("%.5f",1.23456789))); }
     @Test public void exprSprintf_11()      { test("afn:sprintf('%.0f != %s', 
'12.23456789'^^xsd:double,'15')",NodeValue.makeString("12 != 15")); }
     @Test public void exprSprintf_12()      { test("afn:sprintf('(%.0f,%s,%d) 
%4$tm %4$te,%4$tY', 
'12.23456789'^^xsd:double,'12',11,'2016-03-17'^^xsd:date)",NodeValue.makeString("(12,12,11)
 03 17,2016")); }
+    @Test public void exprSprintf_13()      { test("afn:sprintf('%20d', 
1)",NodeValue.makeString("                   1")); }
+    @Test public void exprSprintf_14()      { test("afn:sprintf('%020d', 
1)",NodeValue.makeString("00000000000000000001")); }
+    @Test public void exprSprintf_15()      { test("afn:sprintf('%200d', 
1)",NodeValue.makeString(StringUtils.repeat(' ', 199) + "1")); }
+
+    public static Stream<Arguments> badSprintf() {
+        return Stream.of(
+                Arguments.of("afn:sprintf('%2000000000d', 1)", "too large"),
+                Arguments.of("afn:sprintf('%1000d %1000d %1000d %1000d 
%1000d', 1, 2, 3, 4, 5)", "too large"),
+                Arguments.of("afn:sprintf('%f', 17)", "invalid format string")
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("badSprintf")
+    public void exprSprintf_bad(String expr, String expectedMessage)    {
+        // Given and When
+        ExprEvalException e = testEvalException(expr);
+
+        // Then
+        assertTrue(Strings.CI.contains(e.getMessage(), expectedMessage));
+    }
 
     // Timezone tests
 
diff --git 
a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java
 
b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java
index 821d512daf..51e453588d 100644
--- 
a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java
+++ 
b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java
@@ -62,7 +62,6 @@ public class TestFnFunctionsNumeric {
     // counter-intuitive -- would fail if float/double not translated to 
decimal
     @Test public void exprRoundHalfEven_08()    { 
test("fn:round-half-to-even('150.015'^^xsd:float, 2)",     
NodeValue.makeFloat((float)150.01)); }
 
-
     public static Stream<Arguments> outOfRangeInputs() {
         return Stream.of(Arguments.of("math:pow(2,2000000000)"),
                          Arguments.of("math:pow(2000000000, 16)"),
diff --git 
a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestMathLimits.java
 
b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestMathLimits.java
index 9b058aa6a9..c33f02eb1d 100644
--- 
a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestMathLimits.java
+++ 
b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestMathLimits.java
@@ -1,3 +1,24 @@
+/*
+ * 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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+
 package org.apache.jena.sparql.function.library;
 
 import org.apache.jena.sparql.expr.ExprEvalException;

Reply via email to