Skip to content

Commit 8e42084

Browse files
sugmanueBrandon Dahler
andauthored
Fix timestamp unmarshalling off-by-one errors (#6071)
* Fix timestamp unmarshalling off-by-one errors * Fall back UNIX_TIMESTAMP behavior to previous behavior for now * Use the target member of the collection instead of the collection field --------- Co-authored-by: Brandon Dahler <[email protected]>
1 parent 6747fa2 commit 8e42084

File tree

5 files changed

+101
-13
lines changed

5 files changed

+101
-13
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "brandondahler",
5+
"description": "Fix timestamp unmarshalling off-by-one errors"
6+
}

core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/JsonUnmarshallingParser.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ private List<Object> parseList(JsonUnmarshallerContext c, SdkField<?> field, Jso
159159
if (isScalarType(marshallingType)) {
160160
MarshallingKnownType marshallingKnownType = marshallingType.getKnownType();
161161
while (currentToken != JsonToken.END_ARRAY) {
162-
result.add(simpleValueFor(field, marshallingKnownType, c, parser, currentToken));
162+
result.add(simpleValueFor(memberInfo, marshallingKnownType, c, parser, currentToken));
163163
currentToken = parser.nextToken();
164164
}
165165
return result;
@@ -190,7 +190,7 @@ private Map<String, Object> parseMap(JsonUnmarshallerContext c, SdkField<?> fiel
190190
while (currentToken != JsonToken.END_OBJECT) {
191191
String fieldName = parser.getText();
192192
currentToken = parser.nextToken();
193-
Object valueFor = simpleValueFor(field, valueMarshallingKnownType, c, parser, currentToken);
193+
Object valueFor = simpleValueFor(valueInfo, valueMarshallingKnownType, c, parser, currentToken);
194194
result.put(fieldName, valueFor);
195195
currentToken = parser.nextToken();
196196
}
@@ -383,17 +383,14 @@ private Instant instantValueFor(
383383
JsonToken lookAhead
384384
) throws IOException {
385385
TimestampFormatTrait.Format format = resolveTimestampFormat(field);
386-
switch (format) {
387-
case UNIX_TIMESTAMP:
388-
return Instant.ofEpochMilli((long) (parser.getDoubleValue() * 1_000d));
389-
case UNIX_TIMESTAMP_MILLIS:
390-
return Instant.ofEpochMilli(parser.getLongValue());
391-
default:
392-
JsonUnmarshaller<Object> unmarshaller = unmarshallerRegistry.getUnmarshaller(MarshallLocation.PAYLOAD,
393-
field.marshallingType());
394-
return (Instant) unmarshaller.unmarshall(context, jsonValueNodeFactory.node(parser, lookAhead),
395-
(SdkField<Object>) field);
386+
if (format == TimestampFormatTrait.Format.UNIX_TIMESTAMP_MILLIS) {
387+
return Instant.ofEpochMilli(parser.getLongValue());
396388
}
389+
390+
JsonUnmarshaller<Object> unmarshaller = unmarshallerRegistry.getUnmarshaller(MarshallLocation.PAYLOAD,
391+
field.marshallingType());
392+
return (Instant) unmarshaller.unmarshall(context, jsonValueNodeFactory.node(parser, lookAhead),
393+
(SdkField<Object>) field);
397394
}
398395

399396
/**

core/protocols/aws-json-protocol/src/test/java/software/amazon/awssdk/protocols/json/internal/unmarshall/JsonUnmarshallingParserTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
import java.io.InputStream;
2020
import java.io.UncheckedIOException;
2121
import java.nio.charset.StandardCharsets;
22+
import java.time.Instant;
2223
import org.junit.jupiter.api.Test;
2324
import software.amazon.awssdk.core.document.Document;
2425
import software.amazon.awssdk.core.protocol.MarshallLocation;
2526
import software.amazon.awssdk.thirdparty.jackson.core.JsonParseException;
2627

28+
import static org.junit.jupiter.api.Assertions.assertEquals;
2729
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
2830
import static org.junit.jupiter.api.Assertions.assertNotNull;
2931
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -99,6 +101,27 @@ public void parsingDocumentFieldWithBooleanValue() {
99101
assertTrue(doc.asBoolean());
100102
}
101103

104+
@Test
105+
public void parsingTimestampWithoutRoundingNeeded() {
106+
JsonUnmarshallingParser parser = parser();
107+
TestRequest req = (TestRequest) parser.parse(TestRequest.builder(), from("{\"timestampMember\": 1099510880.773}"));
108+
assertNotNull(req);
109+
Instant timestamp = req.timestampMember();
110+
assertNotNull(timestamp);
111+
assertEquals(Instant.ofEpochMilli(1099510880773L), timestamp);
112+
}
113+
114+
@Test
115+
public void parsingTimestampWithRoundingNeeded() {
116+
JsonUnmarshallingParser parser = parser();
117+
// NOTE: 1099510880.771d * 1_000d == 1.0995108807709999E12
118+
TestRequest req = (TestRequest) parser.parse(TestRequest.builder(), from("{\"timestampMember\": 1099510880.771}"));
119+
assertNotNull(req);
120+
Instant timestamp = req.timestampMember();
121+
assertNotNull(timestamp);
122+
assertEquals(Instant.ofEpochMilli(1099510880771L), timestamp);
123+
}
124+
102125
static JsonUnmarshallingParser parser() {
103126
ProtocolUnmarshallDependencies dependencies = JsonProtocolUnmarshaller.defaultProtocolUnmarshallDependencies();
104127
JsonUnmarshallingParser parser = JsonUnmarshallingParser

core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/NumberToInstant.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public Instant convert(Number value, SdkField<Instant> field) {
5151
TimestampFormatTrait.Format format = resolveTimestampFormat(field);
5252
switch (format) {
5353
case UNIX_TIMESTAMP:
54-
return Instant.ofEpochMilli((long) (value.doubleValue() * 1_000d));
54+
return Instant.ofEpochMilli(Math.round(value.doubleValue() * 1_000d));
5555
case UNIX_TIMESTAMP_MILLIS:
5656
return Instant.ofEpochMilli(value.longValue());
5757
default:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.protocols.core;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
21+
import java.time.Instant;
22+
import java.util.EnumMap;
23+
import java.util.Map;
24+
import org.junit.jupiter.api.Test;
25+
import software.amazon.awssdk.core.SdkField;
26+
import software.amazon.awssdk.core.protocol.MarshallLocation;
27+
import software.amazon.awssdk.core.protocol.MarshallingType;
28+
import software.amazon.awssdk.core.traits.LocationTrait;
29+
import software.amazon.awssdk.core.traits.TimestampFormatTrait;
30+
31+
class NumberToInstantTest {
32+
33+
@Test
34+
public void unixTimestampWithoutRoundingNeeded() {
35+
NumberToInstant instance = instance(TimestampFormatTrait.Format.UNIX_TIMESTAMP);
36+
Instant timestamp = instance.convert(1099510880.773d, instantField());
37+
assertNotNull(timestamp);
38+
assertEquals(Instant.ofEpochMilli(1099510880773L), timestamp);
39+
}
40+
41+
@Test
42+
public void parsingTimestampWithRoundingNeeded() {
43+
NumberToInstant instance = instance(TimestampFormatTrait.Format.UNIX_TIMESTAMP);
44+
// NOTE: 1099510880.771d * 1_000d == 1.0995108807709999E12
45+
Instant timestamp = instance.convert(1099510880.771d, instantField());
46+
assertNotNull(timestamp);
47+
assertEquals(Instant.ofEpochMilli(1099510880771L), timestamp);
48+
}
49+
50+
static NumberToInstant instance(TimestampFormatTrait.Format format) {
51+
Map<MarshallLocation, TimestampFormatTrait.Format> formats = new EnumMap<>(MarshallLocation.class);
52+
formats.put(MarshallLocation.PAYLOAD, format);
53+
54+
return NumberToInstant.create(formats);
55+
}
56+
57+
static SdkField<Instant> instantField() {
58+
return SdkField.builder(MarshallingType.INSTANT)
59+
.traits(LocationTrait.builder().location(MarshallLocation.PAYLOAD).build())
60+
.build();
61+
}
62+
}

0 commit comments

Comments
 (0)