Skip to content

Commit d35c301

Browse files
paulirwinclaude
andcommitted
Guard against negative-length dictionary lookup in HyphenationCompoundWordTokenFilter, #1072
HyphenationCompoundWordTokenFilter.Decompose() could pass a negative length (partLength - 1 == -1) to CharArraySet.Contains when partLength == 0. This happens when a term has leading non-letter characters (counted via iIgnoreAtBeginning), which pushes real hyphenation points past the synthetic end marker and yields two equal hyphenation points (e.g. "...rindfleisch" -> [0,7,11,11]). With minSubwordSize == 0 the zero-length part is not filtered out, and the genitive-'s fallback then probes the dictionary with length -1. Upstream Java tolerates the negative length (its CharArrayMap silently treats it as "not found"), but Lucene.NET's CharArrayMap validates its arguments and throws ArgumentOutOfRangeException. Guard the fallback so a negative length is never passed downstream, matching Java's effective behavior while leaving CharArrayMap's validation intact for other callers. This was the residual TestRandomChains failure under seed 0xc0ab8518c470366f:0x6ed44cef961049a2 reported on PR #1348. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dac4a2b commit d35c301

2 files changed

Lines changed: 41 additions & 1 deletion

File tree

src/Lucene.Net.Analysis.Common/Analysis/Compound/HyphenationCompoundWordTokenFilter.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,11 @@ protected override void Decompose()
244244
m_tokens.Enqueue(new CompoundToken(this, start, partLength));
245245
}
246246
}
247-
else if (m_dictionary.Contains(m_termAtt.Buffer, start, partLength - 1))
247+
// LUCENENET specific - guard against a negative length (partLength == 0).
248+
// Upstream Java relies on CharArrayMap silently treating a negative length as
249+
// "not found", but Lucene.NET's CharArrayMap validates its arguments and throws
250+
// ArgumentOutOfRangeException. See the BOGUS/BROKEN/FUNKY/WACKO note above.
251+
else if (partLength - 1 >= 0 && m_dictionary.Contains(m_termAtt.Buffer, start, partLength - 1))
248252
{
249253
// check the dictionary again with a word that is one character
250254
// shorter

src/Lucene.Net.Tests.Analysis.Common/Analysis/Compound/TestCompoundWordTokenFilter.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Lucene.Net.Analysis.Core;
55
using Lucene.Net.Analysis.TokenAttributes;
66
using Lucene.Net.Analysis.Util;
7+
using Lucene.Net.Attributes;
78
using Lucene.Net.Util;
89
using NUnit.Framework;
910
using System.IO;
@@ -266,6 +267,41 @@ public virtual void TestRandomStrings()
266267
CheckRandomData(Random, b, 1000 * RandomMultiplier);
267268
}
268269

270+
/// <summary>
271+
/// LUCENENET-specific regression test for <a href="https://github.com/apache/lucenenet/issues/1072">#1072</a>.
272+
/// <para/>
273+
/// When a term has leading non-letter characters (which the hyphenator counts via
274+
/// <c>iIgnoreAtBeginning</c>) the resulting hyphenation points can contain two equal values,
275+
/// so <see cref="HyphenationCompoundWordTokenFilter"/>'s <c>Decompose()</c> computes a
276+
/// <c>partLength</c> of <c>0</c>. With <c>minSubwordSize == 0</c> that zero-length part is not
277+
/// filtered out, and the genitive-'s fallback then probes the dictionary with a length of
278+
/// <c>partLength - 1 == -1</c>. Upstream Java tolerates the negative length (its CharArrayMap
279+
/// silently treats it as "not found"), but Lucene.NET's CharArrayMap validates its arguments
280+
/// and throws <see cref="System.ArgumentOutOfRangeException"/>. The filter must not pass a
281+
/// negative length downstream. The input <c>"...rindfleisch"</c> yields hyphenation points
282+
/// <c>[0, 7, 11, 11]</c>, deterministically reproducing the crash that
283+
/// <c>TestRandomChains</c> hit under seed <c>0xc0ab8518c470366f:0x6ed44cef961049a2</c>.
284+
/// </summary>
285+
[Test]
286+
[LuceneNetSpecific] // Issue #1072
287+
public virtual void TestZeroLengthSubwordDoesNotThrow()
288+
{
289+
CharArraySet dict = makeDictionary("rind", "fleisch");
290+
291+
using var @is = this.GetType().getResourceAsStream("da_UTF8.xml");
292+
HyphenationTree hyphenator = HyphenationCompoundWordTokenFilter.GetHyphenationTree(@is);
293+
294+
// minSubwordSize == 0 is the configuration that lets a zero-length part through.
295+
HyphenationCompoundWordTokenFilter tf = new HyphenationCompoundWordTokenFilter(TEST_VERSION_CURRENT,
296+
new MockTokenizer(new StringReader("...rindfleisch"), MockTokenizer.WHITESPACE, false),
297+
hyphenator, dict, CompoundWordTokenFilterBase.DEFAULT_MIN_WORD_SIZE, 0, 100, false);
298+
299+
// Before the fix this threw ArgumentOutOfRangeException while decomposing. The leading
300+
// "..." shifts the matched subword offsets, so no subword survives the dictionary check;
301+
// the only required behavior is that decomposition completes without throwing.
302+
AssertTokenStreamContents(tf, new string[] { "...rindfleisch" });
303+
}
304+
269305
[Test]
270306
public virtual void TestEmptyTerm()
271307
{

0 commit comments

Comments
 (0)