Throw an exception if size-in-bytes values are out of Long range

Fixes 
This commit is contained in:
Havoc Pennington 2014-06-23 12:13:40 -04:00
parent 95b31ccd57
commit 6311ef8d4d
3 changed files with 69 additions and 15 deletions
HOCON.md
config/src
main/java/com/typesafe/config/impl
test/scala/com/typesafe/config/impl

View File

@ -1265,6 +1265,13 @@ spec copies that. You can certainly find examples of mapping these
to powers of ten, though. If you don't like ambiguity, don't use
the single-letter abbreviations.
Note: any value in zetta/zebi or yotta/yobi will overflow a 64-bit
integer, and of course large-enough values in any of the units may
overflow. Most real-world APIs and apps will not support byte
counts that overflow a 64-bit integer. The huge units are provided
just to be complete but probably aren't useful in practice. At
least not in 2014.
### Config object merging and file merging
It may be useful to offer a method to merge two objects. If such a

View File

@ -5,6 +5,8 @@ package com.typesafe.config.impl;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
@ -575,19 +577,13 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
final String prefix;
final int powerOf;
final int power;
final long bytes;
final BigInteger bytes;
MemoryUnit(String prefix, int powerOf, int power) {
this.prefix = prefix;
this.powerOf = powerOf;
this.power = power;
int i = power;
long bytes = 1;
while (i > 0) {
bytes *= powerOf;
--i;
}
this.bytes = bytes;
this.bytes = BigInteger.valueOf(powerOf).pow(power);
}
private static Map<String, MemoryUnit> makeUnitsMap() {
@ -628,6 +624,12 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
}
}
private static boolean isValidLong(BigInteger v) {
BigInteger max = BigInteger.valueOf(Long.MAX_VALUE);
BigInteger min = BigInteger.valueOf(Long.MIN_VALUE);
return max.compareTo(v) >= 0 && min.compareTo(v) <= 0;
}
/**
* Parses a size-in-bytes string. If no units are specified in the string,
* it is assumed to be in bytes. The returned value is in bytes. The purpose
@ -667,13 +669,22 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
}
try {
BigInteger result;
// if the string is purely digits, parse as an integer to avoid
// possible precision loss; otherwise as a double.
if (numberString.matches("[0-9]+")) {
return Long.parseLong(numberString) * units.bytes;
Long v = Long.parseLong(numberString);
result = units.bytes.multiply(BigInteger.valueOf(v));
} else {
return (long) (Double.parseDouble(numberString) * units.bytes);
Double v = Double.parseDouble(numberString);
BigDecimal resultDecimal = (new BigDecimal(units.bytes)).multiply(BigDecimal.valueOf(v));
result = resultDecimal.toBigInteger();
}
if (isValidLong(result))
return result.longValue();
else
throw new ConfigException.BadValue(originForException, pathForException,
"size-in-bytes value is out of range for a 64-bit long: '" + input + "'");
} catch (NumberFormatException e) {
throw new ConfigException.BadValue(originForException, pathForException,
"Could not parse size-in-bytes number '" + numberString + "'");

View File

@ -50,11 +50,11 @@ class UnitParserTest extends TestUtils {
val conf = parseConfig("foo = 1d")
assertEquals("could get 1d from conf as days",
1L, conf.getDuration("foo", TimeUnit.DAYS))
1L, conf.getDuration("foo", TimeUnit.DAYS))
assertEquals("could get 1d from conf as nanos",
dayInNanos, conf.getNanoseconds("foo"))
dayInNanos, conf.getNanoseconds("foo"))
assertEquals("could get 1d from conf as millis",
TimeUnit.DAYS.toMillis(1), conf.getMilliseconds("foo"))
TimeUnit.DAYS.toMillis(1), conf.getMilliseconds("foo"))
}
@Test
@ -88,7 +88,7 @@ class UnitParserTest extends TestUtils {
}
var result = 1024L * 1024 * 1024
for (unit <- Seq("tebi", "pebi", "exbi", "zebi", "yobi")) {
for (unit <- Seq("tebi", "pebi", "exbi")) {
val first = unit.substring(0, 1).toUpperCase()
result = result * 1024;
assertEquals(result, parseMem("1" + first))
@ -99,7 +99,7 @@ class UnitParserTest extends TestUtils {
}
result = 1000L * 1000 * 1000
for (unit <- Seq("tera", "peta", "exa", "zetta", "yotta")) {
for (unit <- Seq("tera", "peta", "exa")) {
val first = unit.substring(0, 1).toUpperCase()
result = result * 1000;
assertEquals(result, parseMem("1" + first + "B"))
@ -119,4 +119,40 @@ class UnitParserTest extends TestUtils {
}
assertTrue(e2.getMessage().contains("size-in-bytes number"))
}
// later on we'll want to check this with BigInteger version of getBytes
@Test
def parseHugeMemorySizes(): Unit = {
def parseMem(s: String) = SimpleConfig.parseBytes(s, fakeOrigin(), "test")
def assertOutOfRange(s: String) = {
val fail = intercept[ConfigException.BadValue] {
parseMem(s)
}
assertTrue("number was too big", fail.getMessage.contains("out of range"))
}
var result = 1024L * 1024 * 1024
for (unit <- Seq("zebi", "yobi")) {
val first = unit.substring(0, 1).toUpperCase()
assertOutOfRange("1" + first)
assertOutOfRange("1" + first + "i")
assertOutOfRange("1" + first + "iB")
assertOutOfRange("1" + unit + "byte")
assertOutOfRange("1" + unit + "bytes")
assertOutOfRange("1.1" + first)
assertOutOfRange("-1" + first)
}
result = 1000L * 1000 * 1000
for (unit <- Seq("zetta", "yotta")) {
val first = unit.substring(0, 1).toUpperCase()
assertOutOfRange("1" + first + "B")
assertOutOfRange("1" + unit + "byte")
assertOutOfRange("1" + unit + "bytes")
assertOutOfRange("1.1" + first + "B")
assertOutOfRange("-1" + first + "B")
}
assertOutOfRange("1000 exabytes")
assertOutOfRange("10000000 petabytes")
}
}