Skip to content

Commit 6328078

Browse files
authored
[8.x] Handle long overflow in dates (elastic#124048) (elastic#125695)
* Handle long overflow in dates (elastic#124048) * Handle long overflow in dates (cherry picked from commit 07921a7) # Conflicts: # rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml # server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java # server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java * Add import
1 parent 2f0b5a3 commit 6328078

File tree

10 files changed

+126
-8
lines changed

10 files changed

+126
-8
lines changed

docs/changelog/124048.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 124048
2+
summary: Handle long overflow in dates
3+
area: Search
4+
type: bug
5+
issues:
6+
- 112483

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,39 @@ setup:
149149
to: 2023
150150
include_lower: false
151151
include_upper: false
152+
153+
---
154+
"test bad dates in range - past":
155+
- requires:
156+
cluster_features: [ "mapper.range.invalid_date_fix" ]
157+
reason: "Fix for invalid date required"
158+
- do:
159+
catch: /illegal_argument_exception/
160+
search:
161+
index: dates
162+
body:
163+
sort: field
164+
query:
165+
range:
166+
date:
167+
gte: -522000000
168+
lte: 2023
169+
format: date_optional_time
170+
171+
---
172+
"test bad dates in range - future":
173+
- requires:
174+
cluster_features: [ "mapper.range.invalid_date_fix" ]
175+
reason: "Fix for invalid date required"
176+
- do:
177+
catch: /illegal_argument_exception/
178+
search:
179+
index: dates
180+
body:
181+
sort: field
182+
query:
183+
range:
184+
date:
185+
gte: 2020
186+
lte: 522000000
187+
format: date_optional_time

server/src/main/java/org/elasticsearch/common/time/DateUtils.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public static ZoneId of(String zoneId) {
190190

191191
/**
192192
* convert a java time instant to a long value which is stored in lucene
193-
* the long value resembles the nanoseconds since the epoch
193+
* the long value represents the nanoseconds since the epoch
194194
*
195195
* @param instant the instant to convert
196196
* @return the nano seconds and seconds as a single long
@@ -209,10 +209,35 @@ public static long toLong(Instant instant) {
209209
return instant.getEpochSecond() * 1_000_000_000 + instant.getNano();
210210
}
211211

212+
/**
213+
* Convert a java time instant to a long value which is stored in lucene,
214+
* the long value represents the milliseconds since epoch
215+
*
216+
* @param instant the instant to convert
217+
* @return the total milliseconds as a single long
218+
*/
219+
public static long toLongMillis(Instant instant) {
220+
try {
221+
return instant.toEpochMilli();
222+
} catch (ArithmeticException e) {
223+
if (instant.isAfter(Instant.now())) {
224+
throw new IllegalArgumentException(
225+
"date[" + instant + "] is too far in the future to be represented in a long milliseconds variable",
226+
e
227+
);
228+
} else {
229+
throw new IllegalArgumentException(
230+
"date[" + instant + "] is too far in the past to be represented in a long milliseconds variable",
231+
e
232+
);
233+
}
234+
}
235+
}
236+
212237
/**
213238
* Returns an instant that is with valid nanosecond resolution. If
214239
* the parameter is before the valid nanosecond range then this returns
215-
* the minimum {@linkplain Instant} valid for nanosecond resultion. If
240+
* the minimum {@linkplain Instant} valid for nanosecond resolution. If
216241
* the parameter is after the valid nanosecond range then this returns
217242
* the maximum {@linkplain Instant} valid for nanosecond resolution.
218243
* <p>

server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.elasticsearch.common.util.LocaleUtils;
3535
import org.elasticsearch.core.Nullable;
3636
import org.elasticsearch.core.TimeValue;
37+
import org.elasticsearch.features.NodeFeature;
3738
import org.elasticsearch.index.IndexVersion;
3839
import org.elasticsearch.index.IndexVersions;
3940
import org.elasticsearch.index.fielddata.FieldDataContext;
@@ -76,6 +77,7 @@
7677
import java.util.function.LongSupplier;
7778

7879
import static org.elasticsearch.common.time.DateUtils.toLong;
80+
import static org.elasticsearch.common.time.DateUtils.toLongMillis;
7981

8082
/** A {@link FieldMapper} for dates. */
8183
public final class DateFieldMapper extends FieldMapper {
@@ -95,12 +97,13 @@ public final class DateFieldMapper extends FieldMapper {
9597
private static final DateMathParser EPOCH_MILLIS_PARSER = DateFormatter.forPattern("epoch_millis")
9698
.withLocale(DEFAULT_LOCALE)
9799
.toDateMathParser();
100+
public static final NodeFeature INVALID_DATE_FIX = new NodeFeature("mapper.range.invalid_date_fix");
98101

99102
public enum Resolution {
100103
MILLISECONDS(CONTENT_TYPE, NumericType.DATE, DateMillisDocValuesField::new) {
101104
@Override
102105
public long convert(Instant instant) {
103-
return instant.toEpochMilli();
106+
return toLongMillis(instant);
104107
}
105108

106109
@Override

server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ public Set<NodeFeature> getTestFeatures() {
8787
TSDB_NESTED_FIELD_SUPPORT,
8888
SourceFieldMapper.SYNTHETIC_RECOVERY_SOURCE,
8989
ObjectMapper.SUBOBJECTS_FALSE_MAPPING_UPDATE_FIX,
90-
UKNOWN_FIELD_MAPPING_UPDATE_ERROR_MESSAGE
90+
UKNOWN_FIELD_MAPPING_UPDATE_ERROR_MESSAGE,
91+
DateFieldMapper.INVALID_DATE_FIX
9192
);
9293
}
9394
}

server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static org.elasticsearch.common.time.DateUtils.compareNanosToMillis;
2828
import static org.elasticsearch.common.time.DateUtils.toInstant;
2929
import static org.elasticsearch.common.time.DateUtils.toLong;
30+
import static org.elasticsearch.common.time.DateUtils.toLongMillis;
3031
import static org.elasticsearch.common.time.DateUtils.toMilliSeconds;
3132
import static org.elasticsearch.common.time.DateUtils.toNanoSeconds;
3233
import static org.hamcrest.Matchers.containsString;
@@ -93,6 +94,44 @@ public void testInstantToLongMax() {
9394
assertThat(e.getMessage(), containsString("is after"));
9495
}
9596

97+
public void testInstantToLongMillis() {
98+
assertThat(toLongMillis(Instant.EPOCH), is(0L));
99+
100+
Instant instant = createRandomInstant();
101+
long timeSinceEpochInMillis = instant.toEpochMilli();
102+
assertThat(toLongMillis(instant), is(timeSinceEpochInMillis));
103+
104+
Instant maxInstant = Instant.ofEpochSecond(Long.MAX_VALUE / 1000);
105+
long maxInstantMillis = maxInstant.toEpochMilli();
106+
assertThat(toLongMillis(maxInstant), is(maxInstantMillis));
107+
108+
Instant minInstant = Instant.ofEpochSecond(Long.MIN_VALUE / 1000);
109+
long minInstantMillis = minInstant.toEpochMilli();
110+
assertThat(toLongMillis(minInstant), is(minInstantMillis));
111+
}
112+
113+
public void testInstantToLongMillisMin() {
114+
/* negative millisecond value of this instant exceeds the maximum value a java long variable can store */
115+
Instant tooEarlyInstant = Instant.MIN;
116+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooEarlyInstant));
117+
assertThat(e.getMessage(), containsString("too far in the past"));
118+
119+
Instant tooEarlyInstant2 = Instant.ofEpochSecond(Long.MIN_VALUE / 1000 - 1);
120+
e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooEarlyInstant2));
121+
assertThat(e.getMessage(), containsString("too far in the past"));
122+
}
123+
124+
public void testInstantToLongMillisMax() {
125+
/* millisecond value of this instant exceeds the maximum value a java long variable can store */
126+
Instant tooLateInstant = Instant.MAX;
127+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooLateInstant));
128+
assertThat(e.getMessage(), containsString("too far in the future"));
129+
130+
Instant tooLateInstant2 = Instant.ofEpochSecond(Long.MAX_VALUE / 1000 + 1);
131+
e = expectThrows(IllegalArgumentException.class, () -> toLongMillis(tooLateInstant2));
132+
assertThat(e.getMessage(), containsString("too far in the future"));
133+
}
134+
96135
public void testLongToInstant() {
97136
assertThat(toInstant(0), is(Instant.EPOCH));
98137
assertThat(toInstant(1), is(Instant.EPOCH.plusNanos(1)));

server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ protected List<ExampleMalformedValue> exampleMalformedValues() {
152152
return List.of(
153153
exampleMalformedValue("2016-03-99").mapping(mappingWithFormat("strict_date_optional_time||epoch_millis"))
154154
.errorMatches("failed to parse date field [2016-03-99] with format [strict_date_optional_time||epoch_millis]"),
155-
exampleMalformedValue("-522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("long overflow"),
155+
exampleMalformedValue("-522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("too far in the past"),
156+
exampleMalformedValue("522000000").mapping(mappingWithFormat("date_optional_time")).errorMatches("too far in the future"),
156157
exampleMalformedValue("2020").mapping(mappingWithFormat("strict_date"))
157158
.errorMatches("failed to parse date field [2020] with format [strict_date]"),
158159
exampleMalformedValue("hello world").mapping(mappingWithFormat("strict_date_optional_time"))

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDatetimeTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public static Iterable<Object[]> parameters() {
4242
read,
4343
TestCaseSupplier.dateCases(),
4444
DataType.DATETIME,
45-
v -> ((Instant) v).toEpochMilli(),
45+
v -> DateUtils.toLongMillis((Instant) v),
4646
emptyList()
4747
);
4848
TestCaseSupplier.unary(

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToLongTests.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ public static Iterable<Object[]> parameters() {
4343
TestCaseSupplier.forUnaryBoolean(suppliers, evaluatorName.apply("Boolean"), DataType.LONG, b -> b ? 1L : 0L, List.of());
4444

4545
// datetimes
46-
TestCaseSupplier.unary(suppliers, read, TestCaseSupplier.dateCases(), DataType.LONG, v -> ((Instant) v).toEpochMilli(), List.of());
46+
TestCaseSupplier.unary(
47+
suppliers,
48+
read,
49+
TestCaseSupplier.dateCases(),
50+
DataType.LONG,
51+
v -> DateUtils.toLongMillis((Instant) v),
52+
List.of()
53+
);
4754
TestCaseSupplier.unary(
4855
suppliers,
4956
read,

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToStringTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public static Iterable<Object[]> parameters() {
8787
"ToStringFromDatetimeEvaluator[field=" + read + "]",
8888
TestCaseSupplier.dateCases(),
8989
DataType.KEYWORD,
90-
i -> new BytesRef(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(((Instant) i).toEpochMilli())),
90+
i -> new BytesRef(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(DateUtils.toLongMillis((Instant) i))),
9191
List.of()
9292
);
9393
TestCaseSupplier.unary(

0 commit comments

Comments
 (0)