From 077f8ccf708249d0b1fe28d64c9b29d10770f5bf Mon Sep 17 00:00:00 2001 From: Dawid Weiss Date: Mon, 8 Feb 2021 21:49:00 +0100 Subject: [PATCH] LUCENE-9744: NPE on a degenerate query in MinimumShouldMatchIntervalsSource$MinimumMatchesIterator.getSubMatches() (#2323) --- lucene/CHANGES.txt | 4 +- .../lucene/queries/intervals/Intervals.java | 10 +++ .../MinimumShouldMatchIntervalsSource.java | 1 + .../intervals/NoMatchIntervalsSource.java | 75 +++++++++++++++++++ .../queries/intervals/TestIntervals.java | 21 ++++++ .../intervals/TestSimplifications.java | 9 +++ 6 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 lucene/queries/src/java/org/apache/lucene/queries/intervals/NoMatchIntervalsSource.java diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 104d9b911ec..f833be3206b 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -288,7 +288,9 @@ Optimizations Bug Fixes --------------------- -(No changes) + +* LUCENE-9744: NPE on a degenerate query in MinimumShouldMatchIntervalsSource + $MinimumMatchesIterator.getSubMatches(). (Alan Woodward) Other --------------------- diff --git a/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java b/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java index e2c72251be8..1094dbeee19 100644 --- a/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java +++ b/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java @@ -404,6 +404,16 @@ public final class Intervals { * Return intervals that span combinations of intervals from {@code minShouldMatch} of the sources */ public static IntervalsSource atLeast(int minShouldMatch, IntervalsSource... sources) { + if (minShouldMatch == sources.length) { + return unordered(sources); + } + if (minShouldMatch > sources.length) { + return new NoMatchIntervalsSource( + "Too few sources to match minimum of [" + + minShouldMatch + + "]: " + + Arrays.toString(sources)); + } return new MinimumShouldMatchIntervalsSource(sources, minShouldMatch); } diff --git a/lucene/queries/src/java/org/apache/lucene/queries/intervals/MinimumShouldMatchIntervalsSource.java b/lucene/queries/src/java/org/apache/lucene/queries/intervals/MinimumShouldMatchIntervalsSource.java index 87cd427a5e8..f9f2677350f 100644 --- a/lucene/queries/src/java/org/apache/lucene/queries/intervals/MinimumShouldMatchIntervalsSource.java +++ b/lucene/queries/src/java/org/apache/lucene/queries/intervals/MinimumShouldMatchIntervalsSource.java @@ -42,6 +42,7 @@ class MinimumShouldMatchIntervalsSource extends IntervalsSource { private final int minShouldMatch; MinimumShouldMatchIntervalsSource(IntervalsSource[] sources, int minShouldMatch) { + assert minShouldMatch < sources.length; this.sources = sources; this.minShouldMatch = minShouldMatch; } diff --git a/lucene/queries/src/java/org/apache/lucene/queries/intervals/NoMatchIntervalsSource.java b/lucene/queries/src/java/org/apache/lucene/queries/intervals/NoMatchIntervalsSource.java new file mode 100644 index 00000000000..cfa7364c7be --- /dev/null +++ b/lucene/queries/src/java/org/apache/lucene/queries/intervals/NoMatchIntervalsSource.java @@ -0,0 +1,75 @@ +/* + * 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.queries.intervals; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.QueryVisitor; + +/** A source returning no matches */ +class NoMatchIntervalsSource extends IntervalsSource { + final String reason; + + NoMatchIntervalsSource(String reason) { + this.reason = reason; + } + + @Override + public IntervalIterator intervals(String field, LeafReaderContext ctx) throws IOException { + return null; + } + + @Override + public IntervalMatchesIterator matches(String field, LeafReaderContext ctx, int doc) + throws IOException { + return null; + } + + @Override + public void visit(String field, QueryVisitor visitor) {} + + @Override + public int minExtent() { + return 0; + } + + @Override + public Collection pullUpDisjunctions() { + return Collections.singleton(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NoMatchIntervalsSource that = (NoMatchIntervalsSource) o; + return Objects.equals(reason, that.reason); + } + + @Override + public int hashCode() { + return Objects.hash(reason); + } + + @Override + public String toString() { + return "NOMATCH(" + reason + ")"; + } +} diff --git a/lucene/queries/src/test/org/apache/lucene/queries/intervals/TestIntervals.java b/lucene/queries/src/test/org/apache/lucene/queries/intervals/TestIntervals.java index f478c004724..57e86f831b9 100644 --- a/lucene/queries/src/test/org/apache/lucene/queries/intervals/TestIntervals.java +++ b/lucene/queries/src/test/org/apache/lucene/queries/intervals/TestIntervals.java @@ -756,6 +756,27 @@ public class TestIntervals extends LuceneTestCase { assertEquals(3, source.minExtent()); } + public void testDegenerateMinShouldMatch() throws IOException { + IntervalsSource source = + Intervals.ordered( + Intervals.atLeast(1, Intervals.term("interest")), + Intervals.atLeast(1, Intervals.term("anyone"))); + + MatchesIterator mi = getMatches(source, 0, "field1"); + assertMatch(mi, 2, 4, 11, 29); + MatchesIterator subs = mi.getSubMatches(); + assertNotNull(subs); + assertMatch(subs, 2, 2, 11, 19); + assertMatch(subs, 4, 4, 23, 29); + assertFalse(subs.next()); + assertFalse(mi.next()); + } + + public void testNoMatchMinShouldMatch() throws IOException { + IntervalsSource source = Intervals.atLeast(4, Intervals.term("a"), Intervals.term("b")); + checkIntervals(source, "field", 0, new int[][] {}); + } + public void testDefinedGaps() throws IOException { IntervalsSource source = Intervals.phrase( diff --git a/lucene/queries/src/test/org/apache/lucene/queries/intervals/TestSimplifications.java b/lucene/queries/src/test/org/apache/lucene/queries/intervals/TestSimplifications.java index da9531b89b3..6bdc2d57418 100644 --- a/lucene/queries/src/test/org/apache/lucene/queries/intervals/TestSimplifications.java +++ b/lucene/queries/src/test/org/apache/lucene/queries/intervals/TestSimplifications.java @@ -111,4 +111,13 @@ public class TestSimplifications extends LuceneTestCase { Intervals.term("a"), Intervals.term("b"), Intervals.term("c"), Intervals.term("d")), actual); } + + public void testMinShouldMatchSimplifications() { + IntervalsSource expected = Intervals.unordered(Intervals.term("a"), Intervals.term("b")); + assertEquals(expected, Intervals.atLeast(2, Intervals.term("a"), Intervals.term("b"))); + + assertEquals( + "NOMATCH(Too few sources to match minimum of [3]: [a, b])", + Intervals.atLeast(3, Intervals.term("a"), Intervals.term("b")).toString()); + } }