Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

code to handle FasterXML/jackson-databind/issues/2141 #84

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.fasterxml.jackson.datatype.jsr310.deser;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.JsonTokenId;
Expand All @@ -40,6 +41,11 @@ public class DurationDeserializer extends JSR310DeserializerBase<Duration>

public static final DurationDeserializer INSTANCE = new DurationDeserializer();

private static final BigDecimal INSTANT_MAX = new BigDecimal(
java.time.Instant.MAX.getEpochSecond() + "." + java.time.Instant.MAX.getNano());
private static final BigDecimal INSTANT_MIN = new BigDecimal(
java.time.Instant.MIN.getEpochSecond() + "." + java.time.Instant.MIN.getNano());

private DurationDeserializer()
{
super(Duration.class);
Expand All @@ -52,6 +58,13 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
{
case JsonTokenId.ID_NUMBER_FLOAT:
BigDecimal value = parser.getDecimalValue();
// If the decimal isnt within the bounds of a float, bail out
Copy link
Contributor

@toddjonker toddjonker Oct 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't "float" be "Instant"? Also, misspelling of "isn't".

Also might be worth pointing out in comment that Duration and Instant share the same range. UPDATE: they do not.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to // If the decimal isn't within the bounds of a duration, bail out since a duration has different bounds than an instant.

if(value.compareTo(INSTANT_MAX) > 0 ||
value.compareTo(INSTANT_MIN) < 0) {
throw new JsonParseException(context.getParser(),
"Value of Float too large to be converted to Duration");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out this isn't the correct boundary: Duration can hold a number-of-seconds up to Long.MAX_VALUE, which is larger than Instant.MAX.getEpochSecond().

For larger values, Jackson currently has the troubling behavior of BigDecimal.longValue(). This test passes:

    @Test
    public void testDeserializationAsFloat05() throws Exception
    {
        String customInstant = new BigInteger(Long.toString(Long.MAX_VALUE)).add(BigInteger.ONE) + ".0";
        Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
                                 .readValue(customInstant);
        assertEquals(Long.MIN_VALUE, value.getSeconds());   // The Duration is negative!
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference on behavior of similar overflow, Duration.of(Long.MAX_VALUE, ChronoUnit.MINUTES) throws:

java.lang.ArithmeticException: long overflow

while Duration.parse("PT" + Long.MAX_VALUE + "0S") -- note the added 0 to force overflow -- throws:

java.time.format.DateTimeParseException: Text cannot be parsed to a Duration: seconds

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swapped for the following check, although I am not 100% sure about the correct min value:

    private static final BigDecimal DURATION_MAX = new BigDecimal(
            Long.MAX_VALUE + "." + java.time.Instant.MAX.getNano());
    private static final BigDecimal DURATION_MIN = new BigDecimal(
            Long.MIN_VALUE + "." + java.time.Instant.MIN.getNano());

All the test cases, including yours above now throw new DateTimeException( "Instant exceeds minimum or maximum Duration")

}

long seconds = value.longValue();
int nanoseconds = DecimalUtils.extractNanosecondDecimal(value, seconds);
return Duration.ofSeconds(seconds, nanoseconds);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.fasterxml.jackson.datatype.jsr310.deser;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonParseException;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still needed? I think all three added imports are leftover from a prior revision.

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.JsonTokenId;
Expand All @@ -29,6 +30,8 @@

import java.io.IOException;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.OffsetDateTime;
Expand All @@ -52,6 +55,11 @@ public class InstantDeserializer<T extends Temporal>
{
private static final long serialVersionUID = 1L;

private static final BigDecimal INSTANT_MAX = new BigDecimal(
java.time.Instant.MAX.getEpochSecond() + "." + java.time.Instant.MAX.getNano());
private static final BigDecimal INSTANT_MIN = new BigDecimal(
java.time.Instant.MIN.getEpochSecond() + "." + java.time.Instant.MIN.getNano());

/**
* Constants used to check if the time offset is zero. See [jackson-modules-java8#18]
*
Expand Down Expand Up @@ -277,8 +285,15 @@ protected T _fromLong(DeserializationContext context, long timestamp)
timestamp, this.getZone(context)));
}

protected T _fromDecimal(DeserializationContext context, BigDecimal value)
protected T _fromDecimal(DeserializationContext context, BigDecimal value) throws JsonParseException
{
// If the decimal isnt within the bounds of an Instant, bail out
if(value.compareTo(INSTANT_MAX) > 0 ||
value.compareTo(INSTANT_MIN) < 0) {
throw new JsonParseException(context.getParser(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today, this method throws DateTimeException when the given value is out of bounds, so we should throw that here, else this is a backwards-incompatible change.

This test case:

    @Test // (expected = DateTimeException.class)                 // Disabled to provoke failure
    public void testDeserializationAsFloat04() throws Exception
    {
        Instant date = Instant.MAX;
        String customInstant = (date.getEpochSecond() + 1) + ".0";
        MAPPER.readValue(customInstant, Instant.class);
    }

results in:

java.time.DateTimeException: Instant exceeds minimum or maximum instant

	at java.time.Instant.create(Instant.java:411)
	at java.time.Instant.ofEpochSecond(Instant.java:330)
	at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.lambda$static$1(InstantDeserializer.java:66)
	at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer._fromDecimal(InstantDeserializer.java:284)
	at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:171)
	at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:50)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3004)
	at com.fasterxml.jackson.datatype.jsr310.TestInstantSerialization.testDeserializationAsFloat04(TestInstantSerialization.java:244)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I swapped the checks to throw throw new DateTimeException("Instant exceeds minimum or maximum instant");
and throw new DateTimeException("Instant exceeds minimum or maximum Duration"); respectively.

I updated the bounds for duration to be the following, although I am not 100% certain it is correct with regards to minimum:

    private static final BigDecimal DURATION_MAX = new BigDecimal(
            Long.MAX_VALUE + "." + java.time.Instant.MAX.getNano());
    private static final BigDecimal DURATION_MIN = new BigDecimal(
            Long.MIN_VALUE + "." + java.time.Instant.MIN.getNano());

"Value of String too large to be converted to Instant");
}

long seconds = value.longValue();
int nanoseconds = DecimalUtils.extractNanosecondDecimal(value, seconds);
return fromNanoseconds.apply(new FromDecimalArguments(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.time.Duration;
import java.time.temporal.TemporalAmount;

import com.fasterxml.jackson.core.JsonParseException;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
Expand Down Expand Up @@ -52,6 +53,19 @@ public void testDeserializationAsFloat03() throws Exception
assertEquals("The value is not correct.", Duration.ofSeconds(13498L, 8374), value);
}

/**
* This test can potentially hang the VM, so exit if it doesn't finish
* within a few seconds.
* @throws Exception
*/
@Test(timeout=3000, expected = JsonParseException.class)
public void testDeserializationAsFloatWhereStringTooLarge() throws Exception
{
String customDuration = "1000000000e1000000000";
READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
.readValue(customDuration);
}

@Test
public void testDeserializationAsFloat04() throws Exception
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.fasterxml.jackson.datatype.jsr310;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
Expand Down Expand Up @@ -376,6 +377,78 @@ public void testDeserializationWithTypeInfo04() throws Exception
assertEquals("The value is not correct.", date, value);
}

/**
* This should be within the range of a max Instant and should pass
* @throws Exception
*/
@Test(timeout=3000)
public void testDeserializationWithTypeInfo05() throws Exception
{
Instant date = Instant.MAX;
String customInstant = date.getEpochSecond() +"."+ date.getNano();
ObjectMapper m = newMapper()
.addMixIn(Temporal.class, MockObjectConfiguration.class);
Temporal value = m.readValue(
"[\"" + Instant.class.getName() + "\","+customInstant+"]", Temporal.class
);
assertTrue("The value should be an Instant.", value instanceof Instant);
assertEquals("The value is not correct.", date, value);
}

/**
* This test can potentially hang the VM, so exit if it doesn't finish
* within a few seconds.
*
* @throws Exception
*/
@Test(timeout=3000, expected = JsonParseException.class)
public void testDeserializationWithTypeInfoAndStringTooLarge01() throws Exception
{
String customInstant = "1000000000000e1000000000000";
ObjectMapper m = newMapper()
.addMixIn(Temporal.class, MockObjectConfiguration.class);
m.readValue(
"[\"" + Instant.class.getName() + "\","+customInstant+"]", Temporal.class
);
}

/**
* This test can potentially hang the VM, so exit if it doesn't finish
* within a few seconds.
*
* @throws Exception
*/
@Test(timeout=3000, expected = JsonParseException.class)
public void testDeserializationWithTypeInfoAndStringTooLarge02() throws Exception
{
Instant date = Instant.MAX;
// Add in an few extra zeros to be longer than what an epoch should be
String customInstant = date.getEpochSecond() +"0000000000000000."+ date.getNano();
ObjectMapper m = newMapper()
.addMixIn(Temporal.class, MockObjectConfiguration.class);
m.readValue(
"[\"" + Instant.class.getName() + "\","+customInstant+"]", Temporal.class
);
System.out.println("test");
}

/**
* This test can potentially hang the VM, so exit if it doesn't finish
* within a few seconds.
*
* @throws Exception
*/
@Test(timeout=13000, expected = JsonParseException.class)
public void testDeserializationWithTypeInfoAndStringTooFractional01() throws Exception
{
String customInstant = "1e-100000000000";
ObjectMapper m = newMapper()
.addMixIn(Temporal.class, MockObjectConfiguration.class);
m.readValue(
"[\"" + Instant.class.getName() + "\","+customInstant+"]", Temporal.class
);
}

@Test
public void testCustomPatternWithAnnotations01() throws Exception
{
Expand Down