Skip to content

Commit 1a56396

Browse files
committed
Optimise rejection of out-of-range long values (#40325)
Today if you try and insert a very large number like `1e9999999` into a long field we first construct this number as a `BigDecimal`, convert this to a `BigInteger` and then reject it because it is out of range. Unfortunately making such a large `BigInteger` is rather expensive. We can avoid this expense by performing a (weaker) range check on the `BigDecimal` representation of incoming `long`s too. Relates #26137 Closes #40323
1 parent ca99c27 commit 1a56396

File tree

4 files changed

+43
-9
lines changed

4 files changed

+43
-9
lines changed

libs/x-content/src/main/java/org/elasticsearch/common/xcontent/support/AbstractXContentParser.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,12 @@ public int intValue(boolean coerce) throws IOException {
179179

180180
protected abstract int doIntValue() throws IOException;
181181

182+
private static BigInteger LONG_MAX_VALUE_AS_BIGINTEGER = BigInteger.valueOf(Long.MAX_VALUE);
183+
private static BigInteger LONG_MIN_VALUE_AS_BIGINTEGER = BigInteger.valueOf(Long.MIN_VALUE);
184+
// weak bounds on the BigDecimal representation to allow for coercion
185+
private static BigDecimal BIGDECIMAL_GREATER_THAN_LONG_MAX_VALUE = BigDecimal.valueOf(Long.MAX_VALUE).add(BigDecimal.ONE);
186+
private static BigDecimal BIGDECIMAL_LESS_THAN_LONG_MIN_VALUE = BigDecimal.valueOf(Long.MIN_VALUE).subtract(BigDecimal.ONE);
187+
182188
/** Return the long that {@code stringValue} stores or throws an exception if the
183189
* stored value cannot be converted to a long that stores the exact same
184190
* value and {@code coerce} is false. */
@@ -191,19 +197,23 @@ private static long toLong(String stringValue, boolean coerce) {
191197

192198
final BigInteger bigIntegerValue;
193199
try {
194-
BigDecimal bigDecimalValue = new BigDecimal(stringValue);
200+
final BigDecimal bigDecimalValue = new BigDecimal(stringValue);
201+
if (bigDecimalValue.compareTo(BIGDECIMAL_GREATER_THAN_LONG_MAX_VALUE) >= 0 ||
202+
bigDecimalValue.compareTo(BIGDECIMAL_LESS_THAN_LONG_MIN_VALUE) <= 0) {
203+
throw new IllegalArgumentException("Value [" + stringValue + "] is out of range for a long");
204+
}
195205
bigIntegerValue = coerce ? bigDecimalValue.toBigInteger() : bigDecimalValue.toBigIntegerExact();
196206
} catch (ArithmeticException e) {
197207
throw new IllegalArgumentException("Value [" + stringValue + "] has a decimal part");
198208
} catch (NumberFormatException e) {
199209
throw new IllegalArgumentException("For input string: \"" + stringValue + "\"");
200210
}
201211

202-
if (bigIntegerValue.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 ||
203-
bigIntegerValue.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) {
212+
if (bigIntegerValue.compareTo(LONG_MAX_VALUE_AS_BIGINTEGER) > 0 || bigIntegerValue.compareTo(LONG_MIN_VALUE_AS_BIGINTEGER) < 0) {
204213
throw new IllegalArgumentException("Value [" + stringValue + "] is out of range for a long");
205214
}
206215

216+
assert bigIntegerValue.longValueExact() <= Long.MAX_VALUE; // asserting that no ArithmeticException is thrown
207217
return bigIntegerValue.longValue();
208218
}
209219

server/src/main/java/org/elasticsearch/common/Numbers.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ public static long toLongExact(Number n) {
211211
}
212212
}
213213

214+
// weak bounds on the BigDecimal representation to allow for coercion
215+
private static BigDecimal BIGDECIMAL_GREATER_THAN_LONG_MAX_VALUE = BigDecimal.valueOf(Long.MAX_VALUE).add(BigDecimal.ONE);
216+
private static BigDecimal BIGDECIMAL_LESS_THAN_LONG_MIN_VALUE = BigDecimal.valueOf(Long.MIN_VALUE).subtract(BigDecimal.ONE);
217+
214218
/** Return the long that {@code stringValue} stores or throws an exception if the
215219
* stored value cannot be converted to a long that stores the exact same
216220
* value and {@code coerce} is false. */
@@ -224,6 +228,10 @@ public static long toLong(String stringValue, boolean coerce) {
224228
final BigInteger bigIntegerValue;
225229
try {
226230
BigDecimal bigDecimalValue = new BigDecimal(stringValue);
231+
if (bigDecimalValue.compareTo(BIGDECIMAL_GREATER_THAN_LONG_MAX_VALUE) >= 0 ||
232+
bigDecimalValue.compareTo(BIGDECIMAL_LESS_THAN_LONG_MIN_VALUE) <= 0) {
233+
throw new IllegalArgumentException("Value [" + stringValue + "] is out of range for a long");
234+
}
227235
bigIntegerValue = coerce ? bigDecimalValue.toBigInteger() : bigDecimalValue.toBigIntegerExact();
228236
} catch (ArithmeticException e) {
229237
throw new IllegalArgumentException("Value [" + stringValue + "] has a decimal part");

server/src/test/java/org/elasticsearch/common/NumbersTests.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.common;
2121

22+
import com.carrotsearch.randomizedtesting.annotations.Timeout;
2223
import org.elasticsearch.test.ESTestCase;
2324

2425
import java.math.BigDecimal;
@@ -27,19 +28,26 @@
2728

2829
public class NumbersTests extends ESTestCase {
2930

31+
@Timeout(millis = 10000)
3032
public void testToLong() {
3133
assertEquals(3L, Numbers.toLong("3", false));
3234
assertEquals(3L, Numbers.toLong("3.1", true));
3335
assertEquals(9223372036854775807L, Numbers.toLong("9223372036854775807.00", false));
3436
assertEquals(-9223372036854775808L, Numbers.toLong("-9223372036854775808.00", false));
37+
assertEquals(9223372036854775807L, Numbers.toLong("9223372036854775807.00", true));
38+
assertEquals(-9223372036854775808L, Numbers.toLong("-9223372036854775808.00", true));
39+
assertEquals(9223372036854775807L, Numbers.toLong("9223372036854775807.99", true));
40+
assertEquals(-9223372036854775808L, Numbers.toLong("-9223372036854775808.99", true));
3541

36-
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
37-
() -> Numbers.toLong("9223372036854775808", false));
38-
assertEquals("Value [9223372036854775808] is out of range for a long", e.getMessage());
42+
assertEquals("Value [9223372036854775808] is out of range for a long", expectThrows(IllegalArgumentException.class,
43+
() -> Numbers.toLong("9223372036854775808", false)).getMessage());
44+
assertEquals("Value [-9223372036854775809] is out of range for a long", expectThrows(IllegalArgumentException.class,
45+
() -> Numbers.toLong("-9223372036854775809", false)).getMessage());
3946

40-
e = expectThrows(IllegalArgumentException.class,
41-
() -> Numbers.toLong("-9223372036854775809", false));
42-
assertEquals("Value [-9223372036854775809] is out of range for a long", e.getMessage());
47+
assertEquals("Value [1e99999999] is out of range for a long", expectThrows(IllegalArgumentException.class,
48+
() -> Numbers.toLong("1e99999999", false)).getMessage());
49+
assertEquals("Value [-1e99999999] is out of range for a long", expectThrows(IllegalArgumentException.class,
50+
() -> Numbers.toLong("-1e99999999", false)).getMessage());
4351
}
4452

4553
public void testToLongExact() {

server/src/test/java/org/elasticsearch/index/mapper/NumberFieldMapperTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.index.mapper;
2121

22+
import com.carrotsearch.randomizedtesting.annotations.Timeout;
2223
import org.apache.lucene.index.DocValuesType;
2324
import org.apache.lucene.index.IndexableField;
2425
import org.elasticsearch.common.Strings;
@@ -368,17 +369,20 @@ public void testEmptyName() throws IOException {
368369
}
369370
}
370371

372+
@Timeout(millis = 30000)
371373
public void testOutOfRangeValues() throws IOException {
372374
final List<OutOfRangeSpec<Object>> inputs = Arrays.asList(
373375
OutOfRangeSpec.of(NumberType.BYTE, "128", "is out of range for a byte"),
374376
OutOfRangeSpec.of(NumberType.SHORT, "32768", "is out of range for a short"),
375377
OutOfRangeSpec.of(NumberType.INTEGER, "2147483648", "is out of range for an integer"),
376378
OutOfRangeSpec.of(NumberType.LONG, "9223372036854775808", "out of range for a long"),
379+
OutOfRangeSpec.of(NumberType.LONG, "1e999999999", "out of range for a long"),
377380

378381
OutOfRangeSpec.of(NumberType.BYTE, "-129", "is out of range for a byte"),
379382
OutOfRangeSpec.of(NumberType.SHORT, "-32769", "is out of range for a short"),
380383
OutOfRangeSpec.of(NumberType.INTEGER, "-2147483649", "is out of range for an integer"),
381384
OutOfRangeSpec.of(NumberType.LONG, "-9223372036854775809", "out of range for a long"),
385+
OutOfRangeSpec.of(NumberType.LONG, "-1e999999999", "out of range for a long"),
382386

383387
OutOfRangeSpec.of(NumberType.BYTE, 128, "is out of range for a byte"),
384388
OutOfRangeSpec.of(NumberType.SHORT, 32768, "out of range of Java short"),
@@ -420,6 +424,10 @@ public void testOutOfRangeValues() throws IOException {
420424
e.getCause().getMessage(), containsString(item.message));
421425
}
422426
}
427+
428+
// the following two strings are in-range for a long after coercion
429+
parseRequest(NumberType.LONG, createIndexRequest("9223372036854775807.9"));
430+
parseRequest(NumberType.LONG, createIndexRequest("-9223372036854775808.9"));
423431
}
424432

425433
private void parseRequest(NumberType type, BytesReference content) throws IOException {

0 commit comments

Comments
 (0)