From 464c6af39e82b2de187ecb1bcddeadb5efd20e24 Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Wed, 15 Jul 2026 17:05:21 +0530 Subject: [PATCH 1/2] reject fractional input in integer format helpers --- .../validator/GenericTypeValidator.java | 37 ++++++++++++++----- .../validator/GenericTypeValidatorTest.java | 20 ++++++++++ 2 files changed, 47 insertions(+), 10 deletions(-) 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..3aa4dbc9d 100644 --- a/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java @@ -133,6 +133,26 @@ void testLongLocaleOverflow() { assertNull(GenericTypeValidator.formatLong("123x", Locale.US)); } + /** + * Tests that the integer-typed locale format methods reject 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 testIntegerLocaleFractional() { + assertNull(GenericTypeValidator.formatByte("15E-1", Locale.US)); + assertNull(GenericTypeValidator.formatByte("5E-1", Locale.US)); + assertEquals(Byte.valueOf((byte) 100), GenericTypeValidator.formatByte("1E2", Locale.US)); + + assertNull(GenericTypeValidator.formatShort("15E-1", Locale.US)); + assertNull(GenericTypeValidator.formatShort("5E-1", Locale.US)); + assertEquals(Short.valueOf((short) 100), GenericTypeValidator.formatShort("1E2", Locale.US)); + + 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. */ From d8fe141d63f4d72d77d5e4e46a38eea484504377 Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Sat, 18 Jul 2026 14:17:34 +0530 Subject: [PATCH 2/2] Split fractional-input test into one method per API --- .../validator/GenericTypeValidatorTest.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java b/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java index 3aa4dbc9d..dff887636 100644 --- a/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java @@ -134,20 +134,36 @@ void testLongLocaleOverflow() { } /** - * Tests that the integer-typed locale format methods reject 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)}. + * 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 testIntegerLocaleFractional() { + 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));