diff --git a/src/main/java/org/apache/commons/validator/GenericTypeValidator.java b/src/main/java/org/apache/commons/validator/GenericTypeValidator.java index 62fe3f44b..5555e7346 100644 --- a/src/main/java/org/apache/commons/validator/GenericTypeValidator.java +++ b/src/main/java/org/apache/commons/validator/GenericTypeValidator.java @@ -66,9 +66,15 @@ public static Byte formatByte(final String value, final Locale locale) { if (value != null) { final ParsePosition pos = new ParsePosition(0); final Number num = getIntegerOnlyFormat(locale).parse(value, pos); - // If there was no error and we used the whole string - if (pos.getErrorIndex() == -1 && pos.getIndex() == value.length() && num.doubleValue() >= Byte.MIN_VALUE && num.doubleValue() <= Byte.MAX_VALUE) { - result = Byte.valueOf(num.byteValue()); + // parseIntegerOnly only stops at the decimal separator, so a fractional value written with a negative + // exponent and no decimal point (for example "15E-1" for 1.5) is consumed in full and returned as a + // Double; byteValue() would floor it. NumberFormat returns a Long only for a non-fractional value in + // long range, so guard on that before the byte range check, matching formatLong. + if (pos.getIndex() == value.length() && num instanceof Long) { + final long longValue = (Long) num; + if (longValue >= Byte.MIN_VALUE && longValue <= Byte.MAX_VALUE) { + result = Byte.valueOf((byte) longValue); + } } } return result; @@ -269,10 +275,15 @@ public static Integer formatInt(final String value, final Locale locale) { final NumberFormat formatter = getIntegerOnlyFormat(locale); final ParsePosition pos = new ParsePosition(0); final Number num = formatter.parse(value, pos); - // If there was no error and we used the whole string - if (pos.getErrorIndex() == -1 && pos.getIndex() == value.length() && num.doubleValue() >= Integer.MIN_VALUE - && num.doubleValue() <= Integer.MAX_VALUE) { - result = Integer.valueOf(num.intValue()); + // parseIntegerOnly only stops at the decimal separator, so a fractional value written with a negative + // exponent and no decimal point (for example "15E-1" for 1.5) is consumed in full and returned as a + // Double; intValue() would floor it. NumberFormat returns a Long only for a non-fractional value in + // long range, so guard on that before the int range check, matching formatLong. + if (pos.getIndex() == value.length() && num instanceof Long) { + final long longValue = (Long) num; + if (longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE) { + result = Integer.valueOf((int) longValue); + } } } return result; @@ -352,9 +363,15 @@ public static Short formatShort(final String value, final Locale locale) { final NumberFormat formatter = getIntegerOnlyFormat(locale); final ParsePosition pos = new ParsePosition(0); final Number num = formatter.parse(value, pos); - // If there was no error and we used the whole string - if (pos.getErrorIndex() == -1 && pos.getIndex() == value.length() && num.doubleValue() >= Short.MIN_VALUE && num.doubleValue() <= Short.MAX_VALUE) { - result = Short.valueOf(num.shortValue()); + // parseIntegerOnly only stops at the decimal separator, so a fractional value written with a negative + // exponent and no decimal point (for example "15E-1" for 1.5) is consumed in full and returned as a + // Double; shortValue() would floor it. NumberFormat returns a Long only for a non-fractional value in + // long range, so guard on that before the short range check, matching formatLong. + if (pos.getIndex() == value.length() && num instanceof Long) { + final long longValue = (Long) num; + if (longValue >= Short.MIN_VALUE && longValue <= Short.MAX_VALUE) { + result = Short.valueOf((short) longValue); + } } } return result; diff --git a/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java b/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java index 1a2be5245..dff887636 100644 --- a/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java @@ -133,6 +133,42 @@ void testLongLocaleOverflow() { assertNull(GenericTypeValidator.formatLong("123x", Locale.US)); } + /** + * Tests that {@link GenericTypeValidator#formatByte(String, Locale)} rejects a fractional value instead of truncating it. A value written with a negative + * exponent and no decimal point (for example "15E-1" for 1.5) is consumed in full by the integer-only format and used to be floored to a non-null result, + * unlike {@link GenericTypeValidator#formatLong(String, Locale)}. + */ + @Test + void testByteLocaleFractional() { + assertNull(GenericTypeValidator.formatByte("15E-1", Locale.US)); + assertNull(GenericTypeValidator.formatByte("5E-1", Locale.US)); + assertEquals(Byte.valueOf((byte) 100), GenericTypeValidator.formatByte("1E2", Locale.US)); + } + + /** + * Tests that {@link GenericTypeValidator#formatShort(String, Locale)} rejects a fractional value instead of truncating it. A value written with a negative + * exponent and no decimal point (for example "15E-1" for 1.5) is consumed in full by the integer-only format and used to be floored to a non-null result, + * unlike {@link GenericTypeValidator#formatLong(String, Locale)}. + */ + @Test + void testShortLocaleFractional() { + assertNull(GenericTypeValidator.formatShort("15E-1", Locale.US)); + assertNull(GenericTypeValidator.formatShort("5E-1", Locale.US)); + assertEquals(Short.valueOf((short) 100), GenericTypeValidator.formatShort("1E2", Locale.US)); + } + + /** + * Tests that {@link GenericTypeValidator#formatInt(String, Locale)} rejects a fractional value instead of truncating it. A value written with a negative + * exponent and no decimal point (for example "15E-1" for 1.5) is consumed in full by the integer-only format and used to be floored to a non-null result, + * unlike {@link GenericTypeValidator#formatLong(String, Locale)}. + */ + @Test + void testIntLocaleFractional() { + assertNull(GenericTypeValidator.formatInt("15E-1", Locale.US)); + assertNull(GenericTypeValidator.formatInt("5E-1", Locale.US)); + assertEquals(Integer.valueOf(100), GenericTypeValidator.formatInt("1E2", Locale.US)); + } + /** * Tests the byte validation. */