From ad8317256efce77c3c1c52d27025fdcf74ec263c Mon Sep 17 00:00:00 2001 From: Giovanni Di Sirio Date: Wed, 27 May 2026 10:34:31 +0200 Subject: [PATCH] Add hexadecimal integer literals (e.g. 0xFF) Whole numbers can now be written in hexadecimal notation, using the 0x (or 0X) prefix followed by hex digits, e.g. 0xFF for 255. This has been requested on the mailing list several times over the years (the previous workaround being to expose Integer.decode as a shared variable). The change is purely additive and backward compatible: in an expression, a sequence like "0xFF" was previously a parse error (a number immediately followed by an identifier), so no currently-valid template can be affected. The new HEX_INTEGER token is only active in the expression lexer states, never in plain template text. Hex literals specify whole numbers only; the sign can be applied as a separate operator (e.g. -0x10). There's no limit on the number of digits: the value is narrowed to an Integer if it fits into a 32-bit signed integer, to a Long if it fits into a 64-bit signed integer, and is kept as a BigInteger otherwise. Added JUnit coverage and a note in the Manual's "Direct specification of values" section (with @since 2.3.35). --- .../src/main/javacc/freemarker/core/FTL.jj | 26 ++++- .../java/freemarker/core/HexLiteralTest.java | 94 +++++++++++++++++++ .../src/main/docgen/en_US/book.xml | 16 ++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 freemarker-core/src/test/java/freemarker/core/HexLiteralTest.java diff --git a/freemarker-core/src/main/javacc/freemarker/core/FTL.jj b/freemarker-core/src/main/javacc/freemarker/core/FTL.jj index 27d62b7b6..df267b2d2 100644 --- a/freemarker-core/src/main/javacc/freemarker/core/FTL.jj +++ b/freemarker-core/src/main/javacc/freemarker/core/FTL.jj @@ -33,6 +33,7 @@ import freemarker.core.LocalLambdaExpression.LambdaParameterList; import freemarker.template.*; import freemarker.template.utility.*; import java.io.*; +import java.math.BigInteger; import java.util.*; import static freemarker.template.Configuration.*; @@ -1253,6 +1254,8 @@ TOKEN: | "." > | + + | | @@ -2078,10 +2081,31 @@ Expression NumberLiteral() : t = | t = + | + t = ) { String s = t.image; - Expression result = new NumberLiteral(pCfg.getArithmeticEngine().toNumber(s)); + Number n; + if (t.kind == HEX_INTEGER) { + // Parse the digits after the "0x"/"0X" prefix. The value is narrowed to the + // smallest of Integer/Long that can represent it; beyond the range of a + // signed long it stays a BigInteger, so the number of digits isn't limited. + // (The token never carries a sign, so the value is always non-negative, and + // hence bitLength() is the number of significant bits.) + BigInteger hexValue = new BigInteger(s.substring(2), 16); + int bitLength = hexValue.bitLength(); + if (bitLength <= 31) { + n = Integer.valueOf(hexValue.intValue()); + } else if (bitLength <= 63) { + n = Long.valueOf(hexValue.longValue()); + } else { + n = hexValue; + } + } else { + n = pCfg.getArithmeticEngine().toNumber(s); + } + Expression result = new NumberLiteral(n); Token startToken = (op != null) ? op : t; result.setLocation(template, startToken, t); return result; diff --git a/freemarker-core/src/test/java/freemarker/core/HexLiteralTest.java b/freemarker-core/src/test/java/freemarker/core/HexLiteralTest.java new file mode 100644 index 000000000..8dcae7bb7 --- /dev/null +++ b/freemarker-core/src/test/java/freemarker/core/HexLiteralTest.java @@ -0,0 +1,94 @@ +/* + * 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 + * + * http://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 freemarker.core; + +import java.io.IOException; + +import org.junit.Test; + +import freemarker.template.TemplateException; +import freemarker.test.TemplateTest; + +public class HexLiteralTest extends TemplateTest { + + @Test + public void testBasicHex() throws IOException, TemplateException { + assertOutput("${0xFF?c}", "255"); + assertOutput("${0xff?c}", "255"); + assertOutput("${0x0?c}", "0"); + assertOutput("${0x1?c}", "1"); + assertOutput("${0x10?c}", "16"); + assertOutput("${0xA?c}", "10"); + assertOutput("${0xDEAD?c}", "57005"); + } + + @Test + public void testHexInExpressions() throws IOException, TemplateException { + assertOutput("${(0xFF + 1)?c}", "256"); + assertOutput("${(0x10 * 2)?c}", "32"); + assertOutput("${(0xFF - 0x0F)?c}", "240"); + } + + @Test + public void testHexAssignment() throws IOException, TemplateException { + assertOutput("<#assign x = 0xFF>${x?c}", "255"); + assertOutput("<#assign x = 0x00FF00>${x?c}", "65280"); + } + + @Test + public void testHexComparison() throws IOException, TemplateException { + assertOutput("<#if 0xFF == 255>yes", "yes"); + assertOutput("<#if (0xFF > 0xFE)>yes", "yes"); + } + + @Test + public void testHexLargeValues() throws IOException, TemplateException { + // Values that fit in int + assertOutput("${0x7FFFFFFF?c}", "2147483647"); + // Values that require long + assertOutput("${0x80000000?c}", "2147483648"); + assertOutput("${0xFFFFFFFF?c}", "4294967295"); + assertOutput("${0x100000000?c}", "4294967296"); + // Highest value that still fits in a signed long + assertOutput("${0x7FFFFFFFFFFFFFFF?c}", "9223372036854775807"); + } + + @Test + public void testHexBeyondLongRange() throws IOException, TemplateException { + // Values too large for a signed long stay exact (BigInteger), rather than + // overflowing or failing to parse. + assertOutput("${0x8000000000000000?c}", "9223372036854775808"); + assertOutput("${0xFFFFFFFFFFFFFFFF?c}", "18446744073709551615"); + // Arbitrarily many digits: 0x1 followed by 32 zeros is 16^32. + assertOutput("${0x100000000000000000000000000000000?c}", + "340282366920938463463374607431768211456"); + } + + @Test + public void testHexLeadingZerosDoNotChangeValue() throws IOException, TemplateException { + // Leading zeros must not push a small value into a wider type. + assertOutput("${0x00000000000000000000FF?c}", "255"); + } + + @Test + public void testHexUppercaseX() throws IOException, TemplateException { + assertOutput("${0XFF?c}", "255"); + assertOutput("${0Xff?c}", "255"); + } +} diff --git a/freemarker-manual/src/main/docgen/en_US/book.xml b/freemarker-manual/src/main/docgen/en_US/book.xml index c4318bea8..20fe93380 100644 --- a/freemarker-manual/src/main/docgen/en_US/book.xml +++ b/freemarker-manual/src/main/docgen/en_US/book.xml @@ -2436,6 +2436,22 @@ C:\foo\bar the number eight. Thus, ${08}, ${+8}, ${8.00} and ${8} will all print exactly same. + + Since FreeMarker 2.3.35, whole numbers + can also be written in hexadecimal notation, using the + 0x (or 0X) prefix followed by + hexadecimal digits (0-9, + a-f, + A-F). For example + 0xFF is the number 255, and + 0x100 is 256. Hexadecimal literals can only + specify whole numbers (no fractional part, no sign, no scientific + notation). The sign can still be applied as a separate operator, as + in -0x10. There's no limit on the number of + digits: the value is created as an Integer if it + fits into a 32-bit signed integer, as a Long if it + fits into a 64-bit signed integer, and as a + BigInteger otherwise.