From 0696c27429fe719e497128d54e671bff1824135d Mon Sep 17 00:00:00 2001 From: Naveed Khan Date: Sat, 8 Aug 2026 14:56:23 +0530 Subject: [PATCH 1/2] floor pre-epoch Timestamp seconds in DateTimeConverter getTime() / 1000 truncates toward zero, so a pre-epoch java.sql.Timestamp with a sub-second part gained a whole second; use Math.floorDiv so the whole-second term agrees with the non-negative getNanos() term. --- .../converters/DateTimeConverter.java | 2 +- .../converters/DateConverterTest.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java b/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java index 7515b3461..3e7952df8 100644 --- a/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java @@ -200,7 +200,7 @@ protected T convertToType(final Class targetType, final Object value) thr // didn't include the milliseconds. The following code // ensures it works consistently across JDK versions final java.sql.Timestamp timestamp = (java.sql.Timestamp) value; - long timeInMillis = timestamp.getTime() / 1000 * 1000; + long timeInMillis = Math.floorDiv(timestamp.getTime(), 1000) * 1000; timeInMillis += timestamp.getNanos() / 1000000; return toDate(targetType, timeInMillis); } diff --git a/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java index a3cd6e40c..282f67192 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java @@ -17,9 +17,14 @@ package org.apache.commons.beanutils2.converters; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; +import org.junit.jupiter.api.Test; + /** * Test Case for the DateConverter class. */ @@ -66,4 +71,16 @@ protected DateConverter makeConverter(final Date defaultValue) { protected Date toType(final Calendar value) { return value.getTime(); } + + /** + * A pre-epoch {@link Timestamp} carries a non-negative sub-second part in {@code getNanos()}, so decomposing + * {@code getTime()} into whole seconds must floor: integer division truncates toward zero for negative values and + * gains a whole second. + */ + @Test + void testConvertPreEpochSqlTimestamp() { + // 1969-12-31T23:59:59.500Z: getTime() == -500, getNanos() == 500_000_000 + final Timestamp timestamp = new Timestamp(-500L); + assertEquals(-500L, makeConverter().convert(getExpectedType(), timestamp).getTime()); + } } From 365c2e31e800ecfca899fc50201e228f3150e14d Mon Sep 17 00:00:00 2001 From: Naveed Khan Date: Sun, 9 Aug 2026 20:49:55 +0530 Subject: [PATCH 2/2] test the wrapping whole-second term near Long.MIN_VALUE For getTime() in [Long.MIN_VALUE, Long.MIN_VALUE + 807] the floorDiv term wraps, but adding the non-negative getNanos() / 1000000 wraps it back; the two terms reconstruct getTime() exactly, so the conversion stays correct without an overflow guard. --- .../beanutils2/converters/DateConverterTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java index 282f67192..7bb5380aa 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/DateConverterTest.java @@ -83,4 +83,17 @@ void testConvertPreEpochSqlTimestamp() { final Timestamp timestamp = new Timestamp(-500L); assertEquals(-500L, makeConverter().convert(getExpectedType(), timestamp).getTime()); } + + /** + * For {@code getTime()} in {@code [Long.MIN_VALUE, Long.MIN_VALUE + 807]} the whole-second term + * {@code Math.floorDiv(getTime(), 1000) * 1000} wraps around {@link Long#MIN_VALUE}, but adding the non-negative + * {@code getNanos() / 1_000_000} wraps it back: the two terms reconstruct {@code getTime()} exactly in + * two's-complement arithmetic, so no overflow guard is needed. + */ + @Test + void testConvertExtremePreEpochSqlTimestamp() { + assertEquals(Long.MIN_VALUE, makeConverter().convert(getExpectedType(), new Timestamp(Long.MIN_VALUE)).getTime()); + // last value whose whole-second term still wraps + assertEquals(Long.MIN_VALUE + 807, makeConverter().convert(getExpectedType(), new Timestamp(Long.MIN_VALUE + 807)).getTime()); + } }