stringToDate("2024123X"): the length 8 picks the yyyyMMdd format, the lenient parse reads 2024, 12 and 3 and stops at the X, so 2024-12-03 comes back instead of a ParseException. Select the still to open it at full size. Generated with AI, checked against the code.- You decode a date string with
DateTools.stringToDate(orstringToTime) and rely on the documentedParseExceptionto reject strings that are not in theyyyyMMdd...format. It does throw for a wrong length ("2019123") or an unreadable start ("X0191231"). - You pass
"2024123X": 8 characters, the length of a DAY string, but the last one is a letter. You expect aParseException. - The length picks the
yyyyMMddformat.DateFormat.parse(String)reads 2024, 12 and 3, stops at theX, and does not mind that 1 of the 8 characters is left over. - You get 2024-12-03T00:00:00Z, with no error, assertions on or off.
- The same happens to an ISO date (
"2019-12-31"gives 2018-10-31T17:00:00Z, the dashes read as minus signs) and to impossible dates ("20041301", month 13, gives 2005-01-01).DateTools' own strings still parse correctly.
Applications that call the public static DateTools.stringToDate or stringToTime on strings that did not come from timeToString (user input, an external feed, an ISO date passed by mistake) and count on the ParseException to reject bad ones. No setting is involved and every release since 1.9 has it; no Lucene code calls these methods outside tests.
Mechanism, terms and numbers390 words
DateTools.stringToDate(String) and stringToTime(String) document @throws ParseException if dateString is not in the expected format, the yyyyMMddHHmmssSSS-or-shorter format that timeToString writes. They throw for a wrong length or an unreadable start, but a string of one of the seven supported lengths (4, 6, 8, 10, 12, 14, 17) goes to a lenient SimpleDateFormat through DateFormat.parse(String) (DateTools.java:117 and :198-199 at 5adff78bb168), which ignores trailing text and rolls impossible fields over. So "2024123X" returns 2024-12-03T00:00:00Z, the ISO date "2019-12-31" returns 2018-10-31T17:00:00Z and "20041301" (month 13) returns 2005-01-01T00:00:00Z, with no exception and the same result with assertions on and off. No Lucene code calls these methods outside tests, and DateTools' own strings parse correctly. Every release since 1.9 (2006) has it.
- Lenient formats.
Resolutionbuilds each format asnew SimpleDateFormat("yyyyMMddHHmmssSSS".substring(0, formatLen), Locale.ROOT)with the GMT zone (:198-199, a fix site) and never callssetLenient(false).TL_FORMATSkeeps per-thread clones indexed by length (:54-65). - Chosen by length.
stringToDatecallsTL_FORMATS.get()[dateString.length()].parse(dateString)(:117, the other fix site). A length with no format hits an empty slot or an out-of-range index, and the catch-all (:118-121) turns that intoParseException("Input is not a valid date string: " + dateString); that is why wrong lengths are rejected. - Prefix parse. The JDK documents
DateFormat.parse(String)as parsing "from the beginning of the given string"; it "may not use the entire text" and throws only if the beginning cannot be parsed. For"2024123X"theyyyyMMddparse reads 2024, 12, 3 and stops: 7 of 8 characters used. - Roll-over. The lenient calendar turns month 13 of 2004 into January 2005 and Feb 30 into March 1. Minus signs are read as signs:
"2019-12-31"is consumed in full and its result equals a lenient calendar set to year 2019, month -1, day 2, hour -31.
| input (length) | today | with the fix |
|---|---|---|
"20191231" (8), a real DateTools string | 2019-12-31T00:00:00Z | same |
"2024123X" (8) | 2024-12-03T00:00:00Z | ParseException |
"201X" (4) | 0201-01-01T00:00:00Z | ParseException |
"2019-12-31" (10), an ISO date | 2018-10-31T17:00:00Z | ParseException |
"20041301" (8), month 13 | 2005-01-01T00:00:00Z | ParseException |
"20240230" (8), Feb 30 | 2024-03-01T00:00:00Z | ParseException |
"2019123" (7), "X0191231", "aaaa" | ParseException | ParseException |
The one-test repro checks only "2024123X"; the other rows come from a separate probe (sources).
Exposure and likelihood132 words
- Who. Code that decodes or validates date strings it did not write with
DateTools, for example a form field or a feed value checked withstringToDatebefore it becomes a range bound or a stored value. - Effect. A silently wrong
Date, sometimes far from the input:"2019-12-31"lands about 14 months earlier,"201X"in the year 201. - Default vs opt-in. A plain public static utility; no setting.
- Not hit. Lucene itself: outside tests no Lucene code calls either method (the only callers are
TestDateToolsand the benchmark'sTestTrecContentSource, which decodesDateTools' own output).DateTools' own strings for years AD 1..9999 parse correctly, today and with the fix; outside that range they do not round-trip even today (LFV-645, a different bug). - Scale. One call; no data, no concurrency.
One JUnit test method on the public API: expectThrows(ParseException.class, () -> DateTools.stringToDate("2024123X")). Deterministic (DateTools uses GMT and Locale.ROOT), single thread; it fails with assertions on and off.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.document;
import java.text.ParseException;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestDateToolsTrailingCharacter extends LuceneTestCase {
// Expected: ParseException ("2024123X" is not yyyyMMdd); actual: returns 2024-12-03T00:00:00Z.
public void testTrailingLetterIsRejected() {
expectThrows(ParseException.class, () -> DateTools.stringToDate("2024123X"));
}
}
cp $REPRO/TestDateToolsTrailingCharacter.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestDateToolsTrailingCharacter -Ptests.seed=DEADBEEF
# same failure with -Ptests.asserts=false
- Expected
stringToDate("2024123X")throwsParseException.- Observed
AssertionFailedError: Expected exception ParseException but no exception was thrown(the call returns 2024-12-03T00:00:00Z), with assertions on and off.
More files, environment, full output172 words
TestDateToolsTrailingCharacter (one method, extends LuceneTestCase, package org.apache.lucene.document of the lucene/core tests). It carries the Apache license header and passes checkGoogleJavaFormat. It uses the same expectThrows idiom as upstream TestDateTools.testStringToDate; as the regression test, the same line fits there next to the "aaaa" case. The test uses no randomness; the gradle run used a random test locale and time zone and still fails, so the seed does not matter. With the strict-parse fix it passes unchanged.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT): fails; the same call (in equivalent tests) also fails on 1.9.1, 9.12.3, 10.5.2 and 10.6.0 |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (pure Java; only this platform was run) |
cp $REPRO/TestDateToolsTrailingCharacter.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestDateToolsTrailingCharacter -Ptests.seed=DEADBEEF
# gradle, scratch worktree at 5adff78bb168, -Ptests.seed=DEADBEEF
TestDateToolsTrailingCharacter > testTrailingLetterIsRejected FAILED
junit.framework.AssertionFailedError: Expected exception ParseException but no exception was thrown
at org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1432)
at org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1418)
at org.apache.lucene.document.TestDateToolsTrailingCharacter.testTrailingLetterIsRejected(TestDateToolsTrailingCharacter.java:26)
# gradle, same, -Ptests.asserts=false: the same failure
# with the fix: PASS with assertions on and off- Both methods document
@throws ParseException if dateString is not in the expected format(yyyyMMdd...); a letter where a digit belongs is not in it. - Upstream
TestDateToolsalready expectsParseExceptionfor strings DateTools never writes ("97","aaaa"). - The JDK's own javadoc calls leniently parsed input "not in the form" of the format and offers
setLenient(false); DateTools never calls it.
The full argument231 words
- Contract.
@throws ParseException if <code>dateString</code> is not in the expected format(DateTools.java:101,:113); the format isyyyyMMddHHmmssSSSor shorter (:75,:87). - Tests.
TestDateTools.testStringToDate(TestDateTools.java:41-57) expectsParseExceptionfor"97","200401011235009999"and"aaaa", each commented "no date". No test covers a trailing character or an out-of-range field. - History. The first
DateTools(c7fbb991c3e, 2004) threw its ownParseException("Input is not valid date string: " + dateString)for every unsupported length, then parsed with the default leniency.git log -S'setLenient'and-S'ParsePosition'over the file's full history find no commit, and the prior-report searches found no Lucene issue or thread about it: the leniency is the JDK default, and no recorded decision either way was found. - The JDK's words.
DateFormat.parse(String, ParsePosition): "By default, parsing is lenient: If the input is not in the form used by this object's format method but can still be parsed as a date, then the parse succeeds. Clients may insist on strict adherence to the format by calling setLenient(false)." - Precedent. #9686 (LUCENE-8640,
d482312b854, 2019) added delimiter checks and per-field range checks to a sibling date parser, spatial-extras'DateRangePrefixTree.parseCalendar, so that it rejects strings such as2000-11T13instead of reinterpreting them. A precedent for validating the syntax, not a fully strict parser: it checks the day only as 1..31 and still rolls Feb 30 over (LFV-619).
The devil's-advocate check204 words
The strongest case for "not a bug" (the independent cross-model reviewer's verdict, high confidence): the javadoc's first sentence is "Converts a string produced by timeToString or dateToString back to a time", so other strings are outside the contract; leniency is the documented JDK default; the design is unchanged since 2004 and no report of it was found; a javadoc sentence would do.
Why it fails: that sentence states the purpose, not a precondition (no "must" or "only"), and the @throws clause in the same javadoc says what happens to other strings. Read strictly, the purpose sentence would leave the clause only DateTools' own output after year 9999 (timeToString writes "10000" and stringToTime throws on it: LFV-645, itself a bug). The reviewer did not read it that strictly: they took the clause to cover "strings that cannot be parsed in a supported resolution format", which already puts strings DateTools never writes in scope, as the upstream test does with "aaaa". What remains is how strict "the expected format" is, and a letter where the format has a digit is outside it under any reading. The argument does bear on likelihood, hence Low; a maintainer may still prefer a javadoc fix (option C).
Recommended Strict parse: setLenient(false) plus a whole-string ParsePosition check (fix A)
Make the parse strict: setLenient(false) on each Resolution format, and parse(String, ParsePosition) with a check that the whole string was used (+9/-1 in DateTools.java). Both parts are needed. The repro passes with assertions on and off; the closest core, query-parser and benchmark suites: 188 tests, 0 failures, 1 skipped by its suite.
SimpleDateFormat format = TL_FORMATS.get()[dateString.length()];
ParsePosition pos = new ParsePosition(0);
Date date = format.parse(dateString, pos);
if (date == null || pos.getIndex() != dateString.length()) {
throw new ParseException("Unparseable date: " + dateString, pos.getErrorIndex());
}
return date;
// and in Resolution(int formatLen): this.format.setLenient(false);All options and trade-offs3 options · 486 words
Option A was built and checked through gradle on 5adff78bb168; B and C were not run. Both parts of A are needed: non-leniency alone still accepts "2024123X" (the prefix parse), and the whole-string check alone still accepts Feb 30 (all 8 characters are used), as a JDK-only replica of the format shows. With A, every malformed string in the table above throws, real DateTools strings parse to the same value, and 2,100,000 timeToString outputs (7 resolutions x 300,000 random times in AD 1..9999) round-trip with 0 rejected and 0 mismatched, as on unpatched main. Separately, a strict, whole-string SimpleDateFormat (not the patched class) read 14,000,000 real timeToString outputs, with the days around the 1582 calendar switch, back identically: compatibility evidence for strict parsing. Outside AD 1..9999 DateTools' output already fails to round-trip today (LFV-645); this fix does not address that. Formatting does not validate fields, so timeToString is unaffected.
Strict parse: setLenient(false) plus a whole-string ParsePosition check (fix A) (validated by the checks) (recommended)
In Resolution, call this.format.setLenient(false); in stringToDate, parse with a ParsePosition and throw unless the whole string was used. The inner ParseException goes through the existing catch-all, so callers see the same exception type and message as for a wrong length.
SimpleDateFormat format = TL_FORMATS.get()[dateString.length()];
ParsePosition pos = new ParsePosition(0);
Date date = format.parse(dateString, pos);
if (date == null || pos.getIndex() != dateString.length()) {
throw new ParseException("Unparseable date: " + dateString, pos.getErrorIndex());
}
return date;
// and in Resolution(int formatLen): this.format.setLenient(false);Trade-offs
A caller that relies on today's leniency (Feb 30 on purpose) now gets ParseException; no Lucene code does. It is not a full syntax check: SimpleDateFormat's number parsing still accepts non-ASCII digits (full-width "20241203" gives 2024-12-03) and a space before a field ("2004 101" gives 2004-01-01), with and without the fix; an ASCII-digit check before parsing would close those too (not built). One ParsePosition per call.
What the checks validated
- Gradle in a scratch worktree at
5adff78bb168, seedDEADBEEF: the repro fails without the fix (assertions on and off) and passes with it (on and off). - With the fix: the repro,
TestDateTools(5),TestDateSort(1),TestCustomSearcherSort(2),TestQueryParser(73),TestQPHelper(38, 1 skipped by the suite),TestStandardQP(46),TestPrecedenceQueryParser(16) andTestTrecContentSource(6): 188 tests, 0 failures.:lucene:core:checkGoogleJavaFormatpasses.
Parse with a DateTimeFormatter per resolution in strict mode instead of SimpleDateFormat.
Trade-offs
Also strict, but java.time uses the proleptic ISO calendar while DateTools formats with GregorianCalendar (Julian before 1582) and the year-of-era pattern, so dates before 1582 would no longer round-trip unless timeToString changes too: a larger change that overlaps LFV-645.
Say in both javadocs that strings of a supported length are parsed leniently and may return a rolled-over date.
Trade-offs
Smallest change, but callers who rely on the @throws clause still get a wrong date silently.
Downstream, an application that validates user-supplied date strings with stringToDate accepts a malformed one as a different date instead of rejecting it; Lucene's own code never calls it.
A proof-obligation audit of DateTools' contract: the @throws ParseException clause obliges both methods to reject every string that is not in the yyyyMMdd... format, and a lenient prefix parse cannot meet that for strings of a supported length. A one-call public-API JUnit test witnessed it.
The method in full115 words
Method: semi-formal (a proof-obligation audit of a class's contract, witnessed by an executable repro).
- The finder read
stringToTimeandstringToDateagainst their@throwsobligation and traced which strings reach the parse: only the length is checked, thenDateFormat.parse(String)on a lenient format, which by its own javadoc needs only a parseable beginning. - The finder wrote a public-API test (month 13) that fails on main with assertions on and off, on 10.6.0 and 9.12.3; the tie-breaker replaced it with the trailing-letter test above, which no reading of the format allows.
- An independent reproducer, an executed cross-model review, a devil's-advocate verifier, an adjudication and the tie-breaker reran it with their own tests ("Prior reports, history and checks").
No prior report found in apache/lucene issues, PRs and commits (with the JIRA archive), the dev@, java-user@, issues@ and commits@ lists, downstream trackers or the web (searched 2026-11-03). The nearest item, #9686 (LUCENE-8640), added delimiter and per-field range checks to a sibling date parser in spatial-extras.
Searches3 searches
No matching public report was found in the documented searches. Nearest items, none a report: #9686 (LUCENE-8640, delimiter and per-field range checks added to DateRangePrefixTree.parseCalendar, a precedent), #2546 (LUCENE-1472, lock contention in the same method), #2810 (LUCENE-1736, the restructuring behind today's formats) and #2842 (LUCENE-1768), where Uwe Schindler wrote in 2011 that DateTools "is somehow deprecated (not officially)". Limits: keyword search; GitHub search does not index review comments; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive) | 2026-11-03, re-checked the same day | two scripted prior-report searches; 52 manual searches (setLenient, ParsePosition, "not a valid date string", "not in the expected format", "month 13"); bodies and comments of 55 items grepped for lenien/setLenient/ParsePosition; everything updated since 2026-11-01 | no report; #9686, #2546, #2810, #2842 are the nearest items |
| Open PRs | 2026-11-03 | the changed files of all 409 open apache/lucene PRs | none touches DateTools.java or TestDateTools.java |
| Mailing lists, JIRA, downstream, web | 2026-11-03 | dev@, java-user@, issues@ and commits@ (7 threads read in full), 9 JIRA queries, apache/solr, Elasticsearch, OpenSearch and Lucene.NET trackers, 10 web queries | nothing about this bug; only generic SimpleDateFormat leniency Q&A |
Present since DateTools was added (c7fbb991c3e, Daniel Naber, 2004; first release 1.9, 2006); every rewrite kept the lenient parse. All 157 final releases from 1.9.1 to 10.6.0 fail the month-13 form; the trailing-letter call (the repro or an equivalent test) fails on 1.9.1, 9.12.3, 10.5.2, 10.6.0 and main. Not fixed upstream.
Versions tested5 rows
Introduced by c7fbb991c3e "Add a new class that supports converting dates to readable strings, with limited resolution..." (Daniel Naber, 2004-09-05, SVN r150483; no PR or JIRA id): the length check, a lenient parse(dateString) and today's @throws text from the start. First release 1.9 (2006-02-27); 1.4.3 has no DateTools.
Later rewrites kept the lenient SimpleDateFormat and parse(String): LUCENE-491 (GMT), the static formats of 2.1, LUCENE-1653, LUCENE-1472 (ThreadLocal formats), LUCENE-1736 (today's TL_FORMATS), the Locale.ROOT change and LUCENE-9911 (initCause). Not fixed upstream: DateTools.java has the same blob on upstream main 1c2072f0284 (2026-11-02) and on 10.6.0; its last commit is dbaf97f5dcc (#14965, 2025-07-18, a lint cleanup). All runs used JDK 25.
| Version or commit | Verdict | Result |
|---|---|---|
main 5adff78bb168 | affected | gradle: fails with assertions on and off; passes with fix A |
| 1.9.1, 9.12.3, 10.5.2, 10.6.0 | trailing letter | "2024123X" returns 2024-12-03T00:00:00Z (JUnit, or a main() form on 1.9.1) |
| 1.9.1 to 10.6.0, all 157 final releases | month 13 | stringToTime("20041301") returns 2005-01-01T00:00:00Z |
c7fbb991c3e (2004) | introducing commit | its own DateTools.java fails the month-13 form |
| 1.4.3 | no DateTools | the class does not exist |
A finder, an independent reproducer, a devil's-advocate verifier and a tie-breaker say real (Low). An independent cross-model reviewer (OpenAI GPT-5.6) reproduced it and agreed on cause and fix but judged it intended; the adjudication called it disputed. This doc's gradle runs reproduced it and validated the fix.
Checks and verdicts282 words
The cross-model review ran on main ea57360912ed, where DateTools.java is unchanged, and its test was re-run on 5adff78bb168 with the same result. The other checks on main ran on 5adff78bb168; the release runs used each release's own jars.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Month-13 test fails on main (assertions on and off), 10.6.0 and 9.12.3; the probe shows trailing text, minus signs, inner spaces and out-of-range fields accepted; the fix passes the repro and the 5 upstream TestDateTools tests. |
| independent reproducer | real, Low | Own test (Feb 30, "2024123X") fails on main, 10.6.0, 10.5.2 and 9.12.3; its JDK-only check reports the two causes, so both fix parts are needed. |
| cross-model review, refute lens (OpenAI GPT-5.6, executed) | not a bug (high) | Reproduced the behaviour (own test fails 2 of 3, controls pass) and agreed on cause and fix; argued the inputs are outside the contract (see the devil's advocate). |
| devil's-advocate verifier | real, Low | Rebutted each intent argument; its own strict parser read 14,000,000 timeToString outputs back identically; "2004-01-15" gives 2003-11-30T09:00:00Z. |
| adjudication | disputed (medium) | Facts and cause agreed by all, only intent disputed; re-ran two tests on main: fail. |
| tie-breaker | confirmed, Low | Own test fails on main and 10.6.0 ("201X", "2019-12-31"); "2024123X" also accepted on 1.9.1; chose the one-test repro. |
| separate re-run, audit and final repro check | fails, fails; confirmed, Low | The one-test repro fails on main with assertions on and off; the final check set the impact class to minor. |
| this doc | reproduced; fix A validated | Gradle: fails without the fix, passes with it; 188 tests of the closest suites, 0 failures. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 482 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,540 words
- Registry title
- DateTools.stringToDate and stringToTime parse with a lenient SimpleDateFormat and ignore trailing text, so a string of a supported length that is not in the yyyyMMdd... format ("2024123X", the ISO date "2019-12-31", month 13) silently returns another date instead of the documented ParseException (since 1.9)
- Area
- core / document (DateTools string-to-date decoding: stringToDate and stringToTime)
- Priority
- Low, rank 537: DateTools.stringToDate and stringToTime silently return a different date, sometimes more than a year off, instead of the documented ParseException for a malformed string of a supported length such as an ISO date passed by mistake, in every release since 1.9. The wrong value is silent and the API is public, but no Lucene code calls these methods, so only applications that decode strings DateTools did not produce are hit.
- Impact class
- minor
- Impact note
- Hits applications that call the public static DateTools.stringToDate or stringToTime on date strings that did not come from timeToString/dateToString (a user-typed or external value, an ISO date such as "2019-12-31" passed by mistake, a value with a stray character, an impossible date such as month 13 or February 30) and rely on the documented ParseException to reject bad ones: a string whose length is one of the seven DateTools lengths (4, 6, 8, 10, 12, 14, 17) and whose beginning reads as digits silently comes back as another Date, sometimes more than a year away ("2019-12-31" gives 2018-10-31T17:00:00Z), with no setting and no scale needed, identically with -ea and -da, in every release since 1.9 (2006); strings of other lengths and unreadable beginnings still throw, DateTools' own output in years AD 1..9999 parses correctly, and no Lucene code calls these methods outside tests.
- Summary
- You call DateTools.stringToDate (or stringToTime) on a string that is not in DateTools' yyyyMMdd... format, for example "2024123X" or the ISO date "2019-12-31", and expect the ParseException that both methods document ('if dateString is not in the expected format') and already give for a wrong length or an unreadable start such as "aaaa". Instead, because the string has one of the seven supported lengths, it goes to a lenient SimpleDateFormat through DateFormat.parse(String), which ignores trailing text and rolls impossible fields over, so a different date comes back with no error: "2024123X" gives 2024-12-03T00:00:00Z, "2019-12-31" gives 2018-10-31T17:00:00Z and "20041301" (month 13) gives 2005-01-01T00:00:00Z.
- Introduced
- c7fbb991c3e55fb6c3c3f3d2f0757176a7eccb06 (trunk, svn r150483, 'Add a new class that supports converting dates to readable strings, with limited resolution...', Daniel Naber; svn era, no PR and no JIRA or Bugzilla id; CHANGES.txt 1.9 'New features' item 4): the first DateTools already chose the pattern by dateString.length(), threw its own ParseException for any other length and returned new SimpleDateFormat(pattern).parse(dateString), a lenient (JDK default) prefix parse, under today's '@throws ParseException if dateString is not in the expected format' [read: git show in the full-history clone; exec: that commit's own DateTools.java, shadowed over the 1.9.1 core, fails the month-13 test]. Its companion test commit bdcaaa7e623 asserted ParseException only for '97', '200401011235009999' and 'aaaa'. Not a regression: every later rewrite kept a lenient SimpleDateFormat and parse(String) (646d9646cbb LUCENE-491 GMT zone, 2006; f72c1e579fe static formats, 2006; 3c3b283e3c0 LUCENE-1653, 2009; fcf6e305b48 LUCENE-1472 ThreadLocal formats, 2011; 31479ab48ce LUCENE-1736 TL_FORMATS indexed by length and built from Resolution.format, 2011, today's :117 and :194-200; b6a4748f21b Locale.ROOT, 2012; 2971f311a2b LUCENE-9911 initCause, 2021), and git log -S'setLenient' and -S'ParsePosition' over the full history of DateTools.java find no commit [read]. (2004-09-05 (author and commit date of c7fbb991c3e, 2004-09-05T21:27:29Z))
- First affected version
- 1.9 [read] (1.9-rc1 2006-02-21, 1.9 final 2006-02-27; the first release with DateTools, so no release has the API without the defect; DateTools.java at the 1.9-rc1 and 1.9 tags is identical to 1.9.1's, and 1.9 is not on Maven Central); 1.9.1 [exec] (2006-03-02) is the first executed release and fails; 1.4.3 and earlier have no DateTools [exec: compile error]
- Affected versions
- Every release from 1.9 (2006) through 10.6.0 (4ae946e5a9b, released 2026-10-29) and main 5adff78bb168 (12.0.0-SNAPSHOT); never fixed. [exec] month-13 form (stringToTime("20041301") returns 2005-01-01T00:00:00Z): all 157 final lucene-core releases on Maven Central from 1.9.1 to 10.6.0 (a main() form of the same call for releases without today's test framework; only the 4.0.0-ALPHA and -BETA pre-releases were not run), the introducing commit's own DateTools.java, 10.6.0 also with -da, and main with -ea and -da. Trailing-letter form (stringToDate("2024123X") returns 2024-12-03T00:00:00Z): main with -ea and -da (tie-breaker; re-run), 10.6.0 (-ea and -da), 10.5.2 and 9.12.3, and 1.9.1 (tie-breaker main() probe on the SHA-1-checked jar). [read] every release tag from 1.9-rc1 to 10.6.0 parses with SimpleDateFormat parse(dateString) and has no setLenient or ParsePosition; DateTools.java has the same blob dda55bf2fb13 on 5adff78bb168, on releases/lucene/10.6.0 and on upstream main 1c2072f0284 (2026-11-02), and its newest upstream commit is dbaf97f5dcc (#14965, author and committer date 2025-07-18, an ecj cleanup that does not touch parsing). Not affected: 1.4.3 and earlier (no DateTools). Not new in 10.6 (10.5.2 and every earlier executed release fail). All runs used JDK 25.0.3 on Linux aarch64; older JDKs were not executed (documented java.text behaviour).
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that disputed it, an adjudication that rated it disputed and a tie-breaker that confirmed it (medium confidence), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: proof-obligation audit
- Upstream and prior reports
- none found (2026-11-03, on top of the finder's, the independent reproduction's, the intended-behaviour check's and the tie-breaker's searches; rechecked the same day): two scripted prior-report searches (run 2: 30 GitHub issue/PR, 3 commit, 2 downstream, 32 mailing-list queries over dev@, java-user@, issues@ and commits@, 5 JIRA-archive and 9 web queries; the finder's run 1: 51 queries), 52 manual GitHub searches (apache/lucene including setLenient, ParsePosition, 'not a valid date string', 'not in the expected format' and 'month 13'; apache/solr, apache/lucene-solr, Elasticsearch, OpenSearch and apache/lucenenet; all of GitHub; items updated since 2026-11-01), the bodies and comments of 55 apache/lucene items grepped for lenien/setLenient/ParsePosition, the changed files of all 409 open apache/lucene PRs (none touches DateTools.java or TestDateTools.java), 9 JIRA queries, 7 list threads read in full, git log -S'setLenient' and -S'ParsePosition' over the full history (no commit) and 10 web queries: no issue, PR, commit, JIRA entry, list thread, downstream issue or web page reports DateTools.stringToDate/stringToTime accepting a malformed string of a supported length. Nearest items, not this bug: #2546 (LUCENE-1472, lock contention in the same method), #2810 (LUCENE-1736, the restructuring that produced today's Resolution formats), #9686 (LUCENE-8640, the same kind of over-acceptance fixed in spatial-extras DateRangePrefixTree.parseCalendar, another class and fix site), #2842 (LUCENE-1768, where Uwe Schindler called DateTools 'somehow deprecated (not officially)' in 2011) and #7975 (LUCENE-6917, which only quotes a javadoc lint error on the @throws lines). Same-day recheck: GitHub search on apache/lucene for 'DateTools lenient', 'DateTools ParseException' and 'DateTools malformed' (0 each), 'stringToDate' (#2147, #2727, #2546), 'stringToTime' (#1146, the older DateField class, and #2546), open PRs 'DateTools' (0), DateTools items updated since 2026-11-01 (0); the changed files of the 28 open and 13 closed PRs updated since 2026-11-01 include neither DateTools.java nor TestDateTools.java; the newest upstream main commit on DateTools.java is still dbaf97f5dcc (author and committer date 2025-07-18, #14965).
- Lucene commit tested
- main
5adff78bb168(2026-11-02) through gradle in a scratch worktree with assertions on and off, without and with the strict-parse fix (the repro,TestDateTools,TestDateSort,TestCustomSearcherSort, four query-parser suites,TestTrecContentSource,checkGoogleJavaFormat), a supporting probe on the unpatched jar and the patched classes, and a standalone JUnit runner on the main jars. The hunt ran the month-13 form on all 157 final releases from 1.9.1 to 10.6.0 and on the 2004 commit's own code, and the trailing-letter form on 1.9.1, 9.12.3, 10.5.2, 10.6.0 and main. Only Linux aarch64 with OpenJDK 25.0.3 was run.
- Finding record
lucene/core/src/java/org/apache/lucene/document/DateTools.java:70-93 (at 5adff78bb168) (dateToString/timeToString: '@return a string in format yyyyMMddHHmmssSSS or shorter, depending on resolution' at :75 and :87), :95-105 (stringToTime: 'Converts a string produced by timeToString or dateToString back to a time' at :96-97, '@throws ParseException if dateString is not in the expected format' at :101, delegates to stringToDate at :104), :107-123 (stringToDate: the same @throws at :113; :117 TL_FORMATS.get()[dateString.length()].parse(dateString), fix site; :118-121 catch-all that turns any exception into ParseException 'Input is not a valid date string'), :194-200 (Resolution: new SimpleDateFormat("yyyyMMddHHmmssSSS".substring(0, formatLen), Locale.ROOT) with the GMT zone and no setLenient(false), fix site)lucene/core/src/test/org/apache/lucene/document/TestDateTools.java:41-57 (at 5adff78bb168) (testStringToDate asserts ParseException for '97', '200401011235009999' and 'aaaa', each commented 'no date'; no out-of-range or trailing-character case)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/document/DateTools.java:54-65, (at 5adff78bb168) :107-123, :194-200JDK 25 java.base java/text/DateFormat.java:410-421 (parse(String)) and :438-441 (lenient by default)
- Who is affected
Lucene main 5adff78bb168 grep of lucene/ for stringToDate/stringToTime (TestDateTools, TestTrecContentSource only)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/document/DateTools.java:70-123 (at 5adff78bb168) (javadoc and stringToDate)lucene/core/src/test/org/apache/lucene/document/TestDateTools.java:41-57 (at 5adff78bb168)JDK 25 java.base java/text/DateFormat.java:438-441https://github.com/apache/lucene/issues/9686 (LUCENE-8640)
- Diagrams
lucene/core/src/java/org/apache/lucene/document/DateTools.java:107-123, (at 5adff78bb168) :194-200
- TestDateToolsTrailingCharacter.java (lucene/core, package org.apache.lucene.document, 1 test):
repro/LFV-1206/TestDateToolsTrailingCharacter.java(28 lines)
The diagram illustrates the repro's call (main 5adff78bb168, DateTools.java:107-123, :194-200).
assets/LFV-1206/bug.png(482 KB)
