Skip to content

Commit a368b41

Browse files
authored
[8.18] Handle long overflow in dates (elastic#124048) (elastic#125697)
* 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 ca2fa59 commit a368b41

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
@@ -191,7 +191,7 @@ public static ZoneId of(String zoneId) {
191191

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

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

7677
import static org.elasticsearch.common.time.DateUtils.toLong;
78+
import static org.elasticsearch.common.time.DateUtils.toLongMillis;
7779

7880
/** A {@link FieldMapper} for dates. */
7981
public final class DateFieldMapper extends FieldMapper {
@@ -93,12 +95,13 @@ public final class DateFieldMapper extends FieldMapper {
9395
private static final DateMathParser EPOCH_MILLIS_PARSER = DateFormatter.forPattern("epoch_millis")
9496
.withLocale(DEFAULT_LOCALE)
9597
.toDateMathParser();
98+
public static final NodeFeature INVALID_DATE_FIX = new NodeFeature("mapper.range.invalid_date_fix");
9699

97100
public enum Resolution {
98101
MILLISECONDS(CONTENT_TYPE, NumericType.DATE, DateMillisDocValuesField::new) {
99102
@Override
100103
public long convert(Instant instant) {
101-
return instant.toEpochMilli();
104+
return toLongMillis(instant);
102105
}
103106

104107
@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
@@ -79,7 +79,8 @@ public Set<NodeFeature> getTestFeatures() {
7979
SORT_FIELDS_CHECK_FOR_NESTED_OBJECT_FIX,
8080
COUNTED_KEYWORD_SYNTHETIC_SOURCE_NATIVE_SUPPORT,
8181
SourceFieldMapper.SYNTHETIC_RECOVERY_SOURCE,
82-
ObjectMapper.SUBOBJECTS_FALSE_MAPPING_UPDATE_FIX
82+
ObjectMapper.SUBOBJECTS_FALSE_MAPPING_UPDATE_FIX,
83+
DateFieldMapper.INVALID_DATE_FIX
8384
);
8485
}
8586
}

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)