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.