From 7d103082866e460b72e1e4fe5788e27dfb40d226 Mon Sep 17 00:00:00 2001 From: SEPURI-SAI-KRISHNA Date: Wed, 12 Aug 2026 14:14:30 +0530 Subject: [PATCH] Make profile activation conditions locale-independent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit upper() and lower() called String.toUpperCase()/toLowerCase() without a Locale, and ConditionParser.toString() formatted whole doubles with String.format() without a Locale. All three therefore depended on the default locale of the machine running the build, so the same POM could activate different profiles on different machines. Turkish maps i to the dotted capital İ, so upper('windows') returns WİNDOWS and a condition such as upper(${os.name}) == 'WINDOWS' silently fails to activate. Under a locale whose default numbering system is not latin, such as hi-IN-u-nu-deva, toString(42.0) returns ४२ rather than 42. Use Locale.ROOT in all three places, matching ExecutableFinder in the same package and the other explicit-Locale call sites in the codebase. The existing tests missed this because none of the strings they convert contain an i or I. Add tests pinning the behaviour under tr and hi-IN-u-nu-deva; both fail without the production change. --- .../model/profile/ConditionFunctions.java | 11 ++++- .../impl/model/profile/ConditionParser.java | 8 +++- .../model/profile/ConditionParserTest.java | 44 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionFunctions.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionFunctions.java index 6dce84ee8853..1ee2f04231fc 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionFunctions.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionFunctions.java @@ -19,6 +19,7 @@ package org.apache.maven.impl.model.profile; import java.util.List; +import java.util.Locale; import org.apache.maven.api.services.InterpolatorException; import org.apache.maven.api.services.ModelBuilderException; @@ -65,6 +66,9 @@ public Object length(List args) { /** * Converts the given string to uppercase. + *

+ * The conversion uses {@link Locale#ROOT} so that profile activation does not depend on the + * default locale of the machine running the build. * * @param args A list containing a single string argument * @return The uppercase version of the input string @@ -75,11 +79,14 @@ public Object upper(List args) { throw new IllegalArgumentException("upper function requires exactly one argument"); } String s = ConditionParser.toString(args.get(0)); - return s.toUpperCase(); + return s.toUpperCase(Locale.ROOT); } /** * Converts the given string to lowercase. + *

+ * The conversion uses {@link Locale#ROOT} so that profile activation does not depend on the + * default locale of the machine running the build. * * @param args A list containing a single string argument * @return The lowercase version of the input string @@ -90,7 +97,7 @@ public Object lower(List args) { throw new IllegalArgumentException("lower function requires exactly one argument"); } String s = ConditionParser.toString(args.get(0)); - return s.toLowerCase(); + return s.toLowerCase(Locale.ROOT); } /** diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionParser.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionParser.java index 4b73b5217ecc..8faf5b5be012 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionParser.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionParser.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.UnaryOperator; @@ -554,6 +555,11 @@ private static Object compare(Object left, String operator, Object right) { * Converts an object to a string representation. * If the object is a {@code Double}, it formats it without any decimal places. * Otherwise, it uses the {@code String.valueOf} method. + *

+ * Formatting uses {@link Locale#ROOT} so that profile activation does not depend on the + * default locale of the machine running the build. Without it, a locale whose default + * numbering system is not latin (for example {@code hi-IN-u-nu-deva}) would render + * {@code 42} as {@code ४२}. * * @param value the object to convert to a string * @return the string representation of the object @@ -562,7 +568,7 @@ public static String toString(Object value) { if (value instanceof Double || value instanceof Float) { double doubleValue = ((Number) value).doubleValue(); if (doubleValue == Math.floor(doubleValue) && !Double.isInfinite(doubleValue)) { - return String.format("%.0f", doubleValue); + return String.format(Locale.ROOT, "%.0f", doubleValue); } } return String.valueOf(value); diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/profile/ConditionParserTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/profile/ConditionParserTest.java index be1da09919c1..474c586d9ecb 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/profile/ConditionParserTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/profile/ConditionParserTest.java @@ -18,6 +18,7 @@ */ package org.apache.maven.impl.model.profile; +import java.util.Locale; import java.util.Map; import java.util.function.UnaryOperator; @@ -94,6 +95,49 @@ void testCaseConversionFunctions() { assertEquals("world", parser.parse("lower('WORLD')")); } + /** + * The result of {@code upper()} and {@code lower()} must not depend on the default locale of + * the machine running the build. In the Turkish locale the default case mapping turns + * {@code i} into the dotted {@code İ} and {@code I} into the dotless {@code ı}, which would + * silently change whether a profile is activated. + */ + @Test + void testCaseConversionFunctionsAreLocaleIndependent() { + Locale orig = Locale.getDefault(); + try { + Locale[] locales = {Locale.ENGLISH, new Locale("tr")}; + for (Locale locale : locales) { + Locale.setDefault(locale); + assertEquals("WINDOWS", parser.parse("upper('windows')"), "upper() in " + locale); + assertEquals("LINUX", parser.parse("upper('linux')"), "upper() in " + locale); + assertEquals("linux", parser.parse("lower('LINUX')"), "lower() in " + locale); + assertEquals("ci", parser.parse("lower('CI')"), "lower() in " + locale); + } + } finally { + Locale.setDefault(orig); + } + } + + /** + * Numbers must render with latin digits regardless of the default locale, otherwise a + * condition comparing against a string literal would evaluate differently on a machine + * whose locale uses a non-latin numbering system. + */ + @Test + void testNumberFormattingIsLocaleIndependent() { + Locale orig = Locale.getDefault(); + try { + // this locale's default numbering system is Devanagari rather than latin + Locale.setDefault(Locale.forLanguageTag("hi-IN-u-nu-deva")); + assertEquals("42", ConditionParser.toString(42.0)); + assertEquals("42", ConditionParser.toString(42.0f)); + assertEquals("The answer is 42", parser.parse("'The answer is ' + 42.0")); + assertEquals(2, parser.parse("length(42.0)")); + } finally { + Locale.setDefault(orig); + } + } + @Test void testConcatFunction() { assertEquals("HelloWorld", parser.parse("'Hello' + 'World'"));