This is an automated email from the ASF dual-hosted git repository.
Apache9 pushed a commit to branch branch-2
in repository https://gitbox.apache.org/repos/asf/hbase.git
The following commit(s) were added to refs/heads/branch-2 by this push:
new c714400850d HBASE-30256 FuzzyRowFilter mishandles the fuzzy-mask
encoding on the no-unsafe path (#8424)
c714400850d is described below
commit c714400850d78a4ab145a9daa011fd390917ea1f
Author: Xiao Liu <[email protected]>
AuthorDate: Wed Jul 1 22:32:22 2026 +0800
HBASE-30256 FuzzyRowFilter mishandles the fuzzy-mask encoding on the
no-unsafe path (#8424)
(cherry picked from commit 69d510277bd124ca50c9c4d4cdbe51c99b5bdaaa)
Signed-off-by: Dávid Paksy <[email protected]>
Co-authored-by: Dávid Paksy <[email protected]>
---
.../apache/hadoop/hbase/filter/FuzzyRowFilter.java | 152 ++++++---
.../hadoop/hbase/filter/TestFuzzyRowFilter.java | 147 +++++++++
.../hbase/filter/TestFuzzyRowFilterWoUnsafe.java | 339 +++++++++++++++++++++
3 files changed, 603 insertions(+), 35 deletions(-)
diff --git
a/hbase-client/src/main/java/org/apache/hadoop/hbase/filter/FuzzyRowFilter.java
b/hbase-client/src/main/java/org/apache/hadoop/hbase/filter/FuzzyRowFilter.java
index e32cbbc1dd1..32348331e08 100644
---
a/hbase-client/src/main/java/org/apache/hadoop/hbase/filter/FuzzyRowFilter.java
+++
b/hbase-client/src/main/java/org/apache/hadoop/hbase/filter/FuzzyRowFilter.java
@@ -22,7 +22,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
-import java.util.Objects;
import java.util.PriorityQueue;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellComparator;
@@ -127,9 +126,11 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
p.setFirst(Arrays.copyOf(aFuzzyKeysData.getFirst(),
aFuzzyKeysData.getFirst().length));
p.setSecond(Arrays.copyOf(aFuzzyKeysData.getSecond(),
aFuzzyKeysData.getSecond().length));
- // update mask ( 0 -> -1 (0xff), 1 -> [0 or 2 depending on
processedWildcardMask value])
+ // Normalize the mask, zero the non-fixed key bytes, then fix the unsafe
mask to its final
+ // {-1, 0} form once here so it is never mutated during scanning.
p.setSecond(preprocessMask(p.getSecond()));
- preprocessSearchKey(p);
+ preprocessSearchKey(p, UNSAFE_UNALIGNED);
+ preprocessMaskForSatisfies(p.getSecond(), UNSAFE_UNALIGNED);
fuzzyKeyDataCopy.add(p);
}
@@ -137,29 +138,41 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
this.tracker = new RowTracker();
}
- private void preprocessSearchKey(Pair<byte[], byte[]> p) {
- if (!UNSAFE_UNALIGNED) {
- // do nothing
- return;
- }
+ /**
+ * Zeroes the non-fixed ("don't care") positions of the search key (on both
paths) so the
+ * next-cell hint from {@link #getNextForFuzzyRule} is the smallest matching
row. The byte at a
+ * non-fixed position is never compared, so this affects neither matching
nor deserialization.
+ */
+ static void preprocessSearchKey(Pair<byte[], byte[]> p, boolean
unsafeUnaligned) {
byte[] key = p.getFirst();
byte[] mask = p.getSecond();
for (int i = 0; i < mask.length; i++) {
- // set non-fixed part of a search key to 0.
- if (mask[i] == processedWildcardMask) {
+ // non-fixed = anything but -1 on unsafe ({-1, 0/2}); 1 on no-unsafe
({0, 1})
+ if ((unsafeUnaligned && mask[i] != -1) || (!unsafeUnaligned && mask[i]
== 1)) {
key[i] = 0;
}
}
}
/**
- * We need to preprocess mask array, as since we treat 2's as unfixed
positions and -1 (0xff) as
- * fixed positions
+ * Normalizes the incoming mask to the active path's encoding. Input is the
public {0, 1} form, or
+ * the already-preprocessed unsafe form ({-1, 0} v1 / {-1, 2} v2) when
{@link #parseFrom}
+ * deserializes a filter from a legacy (pre-HBASE-30256) unsafe peer --
current peers emit the
+ * public {0, 1} form via {@link #toByteArray}; accepting both keeps such
legacy filters working
+ * across platforms. Unsafe keeps/produces {-1, 0/2}; no-unsafe
keeps/produces {0, 1}.
* @return mask array
*/
private byte[] preprocessMask(byte[] mask) {
if (!UNSAFE_UNALIGNED) {
- // do nothing
+ // A mask from an unsafe peer is in internal form (-1 fixed; non-fixed 0
v1 / 2 v2). A byte
+ // outside the public {0, 1} marks internal form -> normalize to {0, 1}.
An all-zeros mask is
+ // kept as public all-fixed: ambiguous with a legacy unsafe all-wildcard
mask (HBASE-15676),
+ // resolved as all-fixed to avoid over-matching.
+ if (!isPublicMask(mask)) {
+ for (int i = 0; i < mask.length; i++) {
+ mask[i] = (byte) (mask[i] == -1 ? 0 : 1);
+ }
+ }
return mask;
}
if (isPreprocessedMask(mask)) return mask;
@@ -173,6 +186,19 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
return mask;
}
+ /**
+ * Returns true if {@code mask} is already in the public {0, 1} form, i.e.
it has no internal-form
+ * byte (a -1 or a 2). An all-zeros mask counts as public (all-fixed).
+ */
+ private static boolean isPublicMask(byte[] mask) {
+ for (int i = 0; i < mask.length; i++) {
+ if (mask[i] != 0 && mask[i] != 1) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private boolean isPreprocessedMask(byte[] mask) {
for (int i = 0; i < mask.length; i++) {
if (mask[i] != -1 && mask[i] != processedWildcardMask) {
@@ -182,6 +208,23 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
return true;
}
+ /**
+ * Converts a stored mask back to the public {0 (fixed), 1 (non-fixed)} form
as a new array, so
+ * serialization, {@link #getFuzzyKeys}, equals and hashCode never expose
the internal encoding.
+ * No-unsafe already stores {0, 1}; unsafe stores {-1, 0}, where -1 is fixed
and anything else is
+ * non-fixed.
+ * @return a new array in {0, 1} form
+ */
+ private static byte[] toConstructorMask(byte[] mask, boolean
unsafeUnaligned) {
+ byte[] out = Arrays.copyOf(mask, mask.length);
+ if (unsafeUnaligned) {
+ for (int i = 0; i < out.length; i++) {
+ out[i] = (byte) (out[i] == -1 ? 0 : 1);
+ }
+ }
+ return out;
+ }
+
/**
* Returns the Fuzzy keys in the format expected by the constructor.
* @return the Fuzzy keys in the format expected by the constructor
@@ -192,18 +235,7 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
Pair<byte[], byte[]> returnKey = new Pair<>();
// This won't revert the original key's don't care values, but we don't
care.
returnKey.setFirst(Arrays.copyOf(fuzzyKey.getFirst(),
fuzzyKey.getFirst().length));
- byte[] returnMask = Arrays.copyOf(fuzzyKey.getSecond(),
fuzzyKey.getSecond().length);
- if (UNSAFE_UNALIGNED && isPreprocessedMask(returnMask)) {
- // Revert the preprocessing.
- for (int i = 0; i < returnMask.length; i++) {
- if (returnMask[i] == -1) {
- returnMask[i] = 0; // -1 >> 0
- } else if (returnMask[i] == processedWildcardMask) {
- returnMask[i] = 1; // 0 or 2 >> 1 depending on mask version
- }
- }
- }
- returnKey.setSecond(returnMask);
+ returnKey.setSecond(toConstructorMask(fuzzyKey.getSecond(),
UNSAFE_UNALIGNED));
returnList.add(returnKey);
}
return returnList;
@@ -232,7 +264,6 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
for (int i = startIndex; i < size + startIndex; i++) {
final int index = i % size;
Pair<byte[], byte[]> fuzzyData = fuzzyKeysData.get(index);
- idempotentMaskShift(fuzzyData.getSecond());
SatisfiesCode satisfiesCode = satisfies(isReversed(), c.getRowArray(),
c.getRowOffset(),
c.getRowLength(), fuzzyData.getFirst(), fuzzyData.getSecond());
if (satisfiesCode == SatisfiesCode.YES) {
@@ -282,8 +313,7 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
* all next rows (one per every fuzzy key) and put them (the fuzzy key is
bundled) into a priority
* queue so that the smallest row key always appears at queue head, which
helps to decide the
* "Next Cell Hint". As scanning going on, the number of candidate rows in
the RowTracker will
- * remain the size of fuzzy keys until some of the fuzzy keys won't possibly
have matches any
- * more.
+ * remain the size of fuzzy keys until some of the fuzzy keys won't possibly
have matches anymore.
*/
private class RowTracker {
private final PriorityQueue<Pair<byte[], Pair<byte[], byte[]>>> nextRows;
@@ -348,9 +378,11 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
}
byte[] updateWith(Cell currentCell, Pair<byte[], byte[]> fuzzyData) {
- byte[] nextRowKeyCandidate =
- getNextForFuzzyRule(isReversed(), currentCell.getRowArray(),
currentCell.getRowOffset(),
- currentCell.getRowLength(), fuzzyData.getFirst(),
fuzzyData.getSecond());
+ // getNextForFuzzyRule needs {-1, 0}: a converted copy on no-unsafe, the
stored mask on
+ // unsafe.
+ byte[] fuzzyKeyMeta = preprocessMaskForHinting(fuzzyData.getSecond(),
UNSAFE_UNALIGNED);
+ byte[] nextRowKeyCandidate = getNextForFuzzyRule(isReversed(),
currentCell.getRowArray(),
+ currentCell.getRowOffset(), currentCell.getRowLength(),
fuzzyData.getFirst(), fuzzyKeyMeta);
if (nextRowKeyCandidate != null) {
nextRows.add(new Pair<>(nextRowKeyCandidate, fuzzyData));
}
@@ -372,7 +404,9 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
for (Pair<byte[], byte[]> fuzzyData : fuzzyKeysData) {
BytesBytesPair.Builder bbpBuilder = BytesBytesPair.newBuilder();
bbpBuilder.setFirst(UnsafeByteOperations.unsafeWrap(fuzzyData.getFirst()));
-
bbpBuilder.setSecond(UnsafeByteOperations.unsafeWrap(fuzzyData.getSecond()));
+ // Emit the public {0, 1} mask, not the internal form, so the wire is
platform-independent.
+ bbpBuilder.setSecond(UnsafeByteOperations
+ .unsafeWrap(toConstructorMask(fuzzyData.getSecond(),
UNSAFE_UNALIGNED)));
builder.addFuzzyKeysData(bbpBuilder);
}
return builder.build().toByteArray();
@@ -505,6 +539,40 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
return SatisfiesCode.YES;
}
+ /**
+ * Mutates {@code mask} in place into the form {@link #satisfies} expects.
Called once from the
+ * constructor so the stored mask is fixed up front and never mutated during
scanning. Unsafe:
+ * shift {-1, 0/2} -> {-1, 0} (the word-based satisfies wants non-fixed =
0). No-unsafe: no-op,
+ * {@link #satisfiesNoUnsafe} already wants {0, 1}.
+ */
+ static void preprocessMaskForSatisfies(byte[] mask, boolean unsafeUnaligned)
{
+ if (!unsafeUnaligned) {
+ return;
+ }
+ // unsafe stores {-1, <wildcard>}; shift to {-1, 0} for satisfies (works
for v1 and v2)
+ idempotentMaskShift(mask);
+ }
+
+ /**
+ * Returns the mask in the {-1 (fixed), 0 (non-fixed)} form {@link
#getNextForFuzzyRule} expects.
+ * No-unsafe converts {0, 1} into a NEW array (the stored {0, 1} is still
needed by
+ * {@link #satisfiesNoUnsafe}); unsafe is already {-1, 0}, returned as is.
+ */
+ static byte[] preprocessMaskForHinting(byte[] mask, boolean unsafeUnaligned)
{
+ if (unsafeUnaligned) {
+ return mask;
+ }
+ byte[] converted = Arrays.copyOf(mask, mask.length);
+ for (int i = 0; i < converted.length; i++) {
+ if (converted[i] == 0) {
+ converted[i] = -1;
+ } else if (converted[i] == 1) {
+ converted[i] = 0;
+ }
+ }
+ return converted;
+ }
+
static SatisfiesCode satisfiesNoUnsafe(boolean reverse, byte[] row, int
offset, int length,
byte[] fuzzyKeyBytes, byte[] fuzzyKeyMeta) {
if (row == null) {
@@ -739,7 +807,7 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
/**
* Returns true if and only if the fields of the filter that are serialized
are equal to the
- * corresponding fields in other. Used for testing.
+ * corresponding fields in others. Used for testing.
*/
@Override
boolean areSerializedFieldsEqual(Filter o) {
@@ -750,13 +818,20 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
return false;
}
FuzzyRowFilter other = (FuzzyRowFilter) o;
+ // The mask-encoding version (serialized as is_mask_v2 by toByteArray) is
a serialized field
+ // too, so two filters that would serialize to different bytes must not be
considered equal.
+ if (this.processedWildcardMask != other.processedWildcardMask) {
+ return false;
+ }
if (this.fuzzyKeysData.size() != other.fuzzyKeysData.size()) return false;
for (int i = 0; i < fuzzyKeysData.size(); ++i) {
Pair<byte[], byte[]> thisData = this.fuzzyKeysData.get(i);
Pair<byte[], byte[]> otherData = other.fuzzyKeysData.get(i);
+ // Compare masks in the normalized {0, 1} form, so equality matches the
serialized bytes.
if (
!(Bytes.equals(thisData.getFirst(), otherData.getFirst())
- && Bytes.equals(thisData.getSecond(), otherData.getSecond()))
+ && Bytes.equals(toConstructorMask(thisData.getSecond(),
UNSAFE_UNALIGNED),
+ toConstructorMask(otherData.getSecond(), UNSAFE_UNALIGNED)))
) {
return false;
}
@@ -771,6 +846,13 @@ public class FuzzyRowFilter extends FilterBase implements
HintingFilter {
@Override
public int hashCode() {
- return Objects.hash(this.fuzzyKeysData);
+ // Include the mask-encoding version (is_mask_v2) so hashCode stays
consistent with equals.
+ int result = 31 + processedWildcardMask;
+ for (Pair<byte[], byte[]> fuzzyData : fuzzyKeysData) {
+ result = 31 * result + Bytes.hashCode(fuzzyData.getFirst());
+ result =
+ 31 * result + Bytes.hashCode(toConstructorMask(fuzzyData.getSecond(),
UNSAFE_UNALIGNED));
+ }
+ return result;
}
}
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/filter/TestFuzzyRowFilter.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/filter/TestFuzzyRowFilter.java
index 7501acd1e84..7f2869cf0c2 100644
---
a/hbase-server/src/test/java/org/apache/hadoop/hbase/filter/TestFuzzyRowFilter.java
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/filter/TestFuzzyRowFilter.java
@@ -19,22 +19,33 @@ package org.apache.hadoop.hbase.filter;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.io.IOException;
import java.util.Arrays;
+import java.util.List;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.KeyValueUtil;
import org.apache.hadoop.hbase.testclassification.FilterTests;
import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.unsafe.HBasePlatformDependent;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.Pair;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
+import org.apache.hbase.thirdparty.com.google.protobuf.UnsafeByteOperations;
+
+import org.apache.hadoop.hbase.shaded.protobuf.generated.FilterProtos;
+import
org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BytesBytesPair;
+
@Tag(FilterTests.TAG)
@Tag(SmallTests.TAG)
public class TestFuzzyRowFilter {
@@ -420,6 +431,142 @@ public class TestFuzzyRowFilter {
}
}
+ /**
+ * Serializing a filter must not leak the internal mask encoding. On the
unsafe path the stored
+ * mask is {-1 (fixed), 0 (non-fixed)}; {@code toByteArray} must emit the
public constructor {0,
+ * 1} form so the round-tripped filter behaves identically (otherwise the
leaked {-1, 0} is
+ * reparsed as an all-fixed mask). Processing a cell first must not change
this.
+ */
+ @Test
+ public void testSerializationAfterFilterCellPreservesBehavior() throws
Exception {
+ FuzzyRowFilter original =
+ new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new
byte[] { 0, 1, 0 })));
+ // Process a cell first to prove serialization is unaffected by scanning.
+ original.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 50, 3
}));
+
+ FuzzyRowFilter parsed = FuzzyRowFilter.parseFrom(original.toByteArray());
+ // A row matching only via the wildcard position must still be INCLUDED
after the round-trip.
+ assertEquals(Filter.ReturnCode.INCLUDE,
+ parsed.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3
})));
+ }
+
+ /**
+ * Two filters built from the same rule (distinct array instances) must be
equal and hash equally,
+ * i.e. {@code equals}/{@code hashCode} must be content-based, not
identity-based, and consistent
+ * with each other and with the serialized ({0, 1}) form. This must also
hold after one of them
+ * has processed a cell.
+ */
+ @Test
+ public void testEqualsConsistentAfterFilterCell() {
+ FuzzyRowFilter fresh =
+ new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new
byte[] { 0, 1, 0 })));
+ FuzzyRowFilter scanned =
+ new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new
byte[] { 0, 1, 0 })));
+ scanned.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 50, 3 }));
+
+ assertEquals(fresh, scanned);
+ assertEquals(fresh.hashCode(), scanned.hashCode());
+ }
+
+ /**
+ * Unsafe-path coverage for branch-2's is_mask_v2 plumbing: the flag
round-trips (v1 stays v1,
+ * v2/client stays v2) and toByteArray always emits the public {0, 1} mask.
+ */
+ @Test
+ public void testIsMaskV2WireRoundTrip() throws Exception {
+ // v1 in (no flag) -> stays v1 on the wire, mask normalized to public {0,
1, 0}.
+ byte[] v1Wire = FilterProtos.FuzzyRowFilter.newBuilder()
+ .addFuzzyKeysData(BytesBytesPair.newBuilder()
+ .setFirst(UnsafeByteOperations.unsafeWrap(new byte[] { 1, 0, 3 }))
+ .setSecond(UnsafeByteOperations.unsafeWrap(new byte[] { -1, 0, -1 })))
+ .build().toByteArray();
+ FilterProtos.FuzzyRowFilter v1Out =
+
FilterProtos.FuzzyRowFilter.parseFrom(FuzzyRowFilter.parseFrom(v1Wire).toByteArray());
+ assertFalse(v1Out.getIsMaskV2());
+ assertArrayEquals(new byte[] { 0, 1, 0 },
v1Out.getFuzzyKeysData(0).getSecond().toByteArray());
+
+ // v2 in -> stays v2, mask normalized to public {0, 1, 0}.
+ byte[] v2Wire = FilterProtos.FuzzyRowFilter.newBuilder().setIsMaskV2(true)
+ .addFuzzyKeysData(BytesBytesPair.newBuilder()
+ .setFirst(UnsafeByteOperations.unsafeWrap(new byte[] { 1, 0, 3 }))
+ .setSecond(UnsafeByteOperations.unsafeWrap(new byte[] { -1, 2, -1 })))
+ .build().toByteArray();
+ FilterProtos.FuzzyRowFilter v2Out =
+
FilterProtos.FuzzyRowFilter.parseFrom(FuzzyRowFilter.parseFrom(v2Wire).toByteArray());
+ assertTrue(v2Out.getIsMaskV2());
+ assertArrayEquals(new byte[] { 0, 1, 0 },
v2Out.getFuzzyKeysData(0).getSecond().toByteArray());
+
+ // A client-built filter is always v2 on the wire.
+ FuzzyRowFilter client =
+ new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new
byte[] { 0, 1, 0 })));
+
assertTrue(FilterProtos.FuzzyRowFilter.parseFrom(client.toByteArray()).getIsMaskV2());
+ }
+
+ /**
+ * getFuzzyKeys must expose the public {0, 1} form, not the internal stored
mask (unsafe stores
+ * {-1, 0}) -- REST ScannerModel feeds it straight back into the constructor.
+ */
+ @Test
+ public void testGetFuzzyKeysReturnsPublicForm() {
+ FuzzyRowFilter filter =
+ new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new
byte[] { 0, 1, 0 })));
+ assertArrayEquals(new byte[] { 0, 1, 0 },
filter.getFuzzyKeys().get(0).getSecond());
+ }
+
+ /**
+ * Unsafe-path companion to the no-unsafe legacy-v1 test: a no-flag,
internal {-1, 0} v1 wire must
+ * still enforce its fixed positions after parseFrom.
+ */
+ @Test
+ public void testParseFromLegacyV1EncodedFilterEnforcesFixedPositions()
throws Exception {
+ byte[] wire = FilterProtos.FuzzyRowFilter.newBuilder()
+ .addFuzzyKeysData(BytesBytesPair.newBuilder()
+ .setFirst(UnsafeByteOperations.unsafeWrap(new byte[] { 1, 0, 3 }))
+ .setSecond(UnsafeByteOperations.unsafeWrap(new byte[] { -1, 0, -1 })))
+ .build().toByteArray();
+ assertEquals(Filter.ReturnCode.INCLUDE, FuzzyRowFilter.parseFrom(wire)
+ .filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3 })));
+ assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT,
FuzzyRowFilter.parseFrom(wire)
+ .filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 9 })));
+ }
+
+ /**
+ * Twin of the no-unsafe all-fixed test: a no-flag all-zeros wire {0, 0, 0}
is the legacy unsafe
+ * v1 all-wildcard encoding (HBASE-15676), so on the unsafe path it matches
every row -- the
+ * deliberate opposite of the no-unsafe all-fixed reading. Guarded to the
unsafe path.
+ */
+ @Test
+ public void testParseFromLegacyV1AllZerosMaskIsAllWildcardOnUnsafe() throws
Exception {
+ assumeTrue(HBasePlatformDependent.unaligned());
+ byte[] wire = FilterProtos.FuzzyRowFilter.newBuilder()
+ .addFuzzyKeysData(BytesBytesPair.newBuilder()
+ .setFirst(UnsafeByteOperations.unsafeWrap(new byte[] { 1, 2, 3 }))
+ .setSecond(UnsafeByteOperations.unsafeWrap(new byte[] { 0, 0, 0 })))
+ .build().toByteArray();
+ assertEquals(Filter.ReturnCode.INCLUDE, FuzzyRowFilter.parseFrom(wire)
+ .filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 9, 9, 9 })));
+ }
+
+ /**
+ * Two filters with identical fuzzy keys but different mask-encoding
versions (v1 vs v2) serialize
+ * to different bytes (is_mask_v2 differs), so they must not be equal. A v2
filter must still
+ * equal another v2 filter built from the same keys and hash alike. See
HBASE-30256.
+ */
+ @Test
+ public void testEqualsAndHashCodeAccountForMaskVersion() {
+ List<Pair<byte[], byte[]>> keys =
+ Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[] { 0, 1, 0
}));
+ FuzzyRowFilter v1 = new FuzzyRowFilter(keys,
FuzzyRowFilter.V1_PROCESSED_WILDCARD_MASK);
+ FuzzyRowFilter v2 = new FuzzyRowFilter(keys,
FuzzyRowFilter.V2_PROCESSED_WILDCARD_MASK);
+ FuzzyRowFilter v2Copy = new FuzzyRowFilter(keys,
FuzzyRowFilter.V2_PROCESSED_WILDCARD_MASK);
+
+ // Same logical keys, different serialized form -> not equal.
+ assertNotEquals(v1, v2);
+ // Same keys and same version -> equal, and equal objects must share a
hashCode.
+ assertEquals(v2, v2Copy);
+ assertEquals(v2.hashCode(), v2Copy.hashCode());
+ }
+
private static FuzzyRowFilter newReverseFuzzyRowFilter() {
FuzzyRowFilter filter =
new FuzzyRowFilter(Arrays.asList(new Pair<>(Bytes.toBytes("aaa"), new
byte[] { 0, 1, 0 })));
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/filter/TestFuzzyRowFilterWoUnsafe.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/filter/TestFuzzyRowFilterWoUnsafe.java
new file mode 100644
index 00000000000..906e5703dd1
--- /dev/null
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/filter/TestFuzzyRowFilterWoUnsafe.java
@@ -0,0 +1,339 @@
+/*
+ * 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.hadoop.hbase.filter;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mockStatic;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+import org.apache.hadoop.hbase.Cell;
+import org.apache.hadoop.hbase.KeyValue;
+import org.apache.hadoop.hbase.KeyValueUtil;
+import org.apache.hadoop.hbase.testclassification.FilterTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.unsafe.HBasePlatformDependent;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.hbase.util.Pair;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import org.apache.hbase.thirdparty.com.google.protobuf.UnsafeByteOperations;
+
+import org.apache.hadoop.hbase.shaded.protobuf.generated.FilterProtos;
+import
org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BytesBytesPair;
+
+/**
+ * Exercises {@link FuzzyRowFilter} on the "no-unsafe" code path
+ * ({@code HBasePlatformDependent.unaligned() == false}) end-to-end, i.e.
through the constructor,
+ * {@link FuzzyRowFilter#filterCell} and {@link
FuzzyRowFilter#getNextCellHint}. On this path the
+ * mask uses the {0 (fixed), 1 (non-fixed)} encoding, which differs from the
unsafe {-1, 2} encoding
+ * that the rest of the scan machinery assumes.
+ */
+@Tag(FilterTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestFuzzyRowFilterWoUnsafe {
+
+ @BeforeAll
+ public static void disableUnsafe() throws Exception {
+ // Force the no-unsafe path: make HBasePlatformDependent.unaligned()
return false and trigger
+ // FuzzyRowFilter's static initializer while the mock is active, so its
private static final
+ // UNSAFE_UNALIGNED is captured as false for this JVM fork.
+ try (MockedStatic<HBasePlatformDependent> mocked =
mockStatic(HBasePlatformDependent.class)) {
+ mocked.when(HBasePlatformDependent::isUnsafeAvailable).thenReturn(false);
+ mocked.when(HBasePlatformDependent::unaligned).thenReturn(false);
+ Field field = FuzzyRowFilter.class.getDeclaredField("UNSAFE_UNALIGNED");
+ field.setAccessible(true);
+ assertFalse(field.getBoolean(null), "expected FuzzyRowFilter to use the
no-unsafe path");
+ }
+ }
+
+ /**
+ * A row that matches the fuzzy rule only thanks to a wildcard position must
be INCLUDED. On the
+ * broken no-unsafe path the mask was shifted to all-zeroes (all positions
treated as fixed), so
+ * the wildcard row was wrongly rejected.
+ */
+ @Test
+ public void testForwardMatchesWildcardRow() {
+ FuzzyRowFilter filter = new FuzzyRowFilter(
+ Collections.singletonList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[]
{ 0, 1, 0 })));
+
+ KeyValue match = KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3 });
+ assertEquals(Filter.ReturnCode.INCLUDE, filter.filterCell(match));
+ }
+
+ /**
+ * For a non-matching row the next-cell hint must be the smallest row that
can satisfy the rule.
+ * With fixed positions 5 and 5 and a wildcard in the middle, the smallest
row at or after {3,0,0}
+ * is {5,0,5}. On the broken no-unsafe path the wildcard key byte was never
cleared and the mask
+ * was corrupted, producing a wrong hint.
+ */
+ @Test
+ public void testForwardHintSkipsToSmallestMatchingRow() {
+ FuzzyRowFilter filter = new FuzzyRowFilter(
+ Collections.singletonList(new Pair<>(new byte[] { 5, 100, 5 }, new
byte[] { 0, 1, 0 })));
+
+ KeyValue current = KeyValueUtil.createFirstOnRow(new byte[] { 3, 0, 0 });
+ assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT,
filter.filterCell(current));
+
+ Cell hint = filter.getNextCellHint(current);
+ assertRow(new byte[] { 5, 0, 5 }, hint);
+ }
+
+ /**
+ * No-unsafe analogue of {@code
TestFuzzyRowFilter#testReverseFilterCellSkipsSameRowHint}. This is
+ * the most subtle composition point this change touches: the reverse
next-cell hint is computed
+ * from the no-unsafe mask (updateWith ->
preprocessMaskForHinting({0,1}->{-1,0}) ->
+ * getNextForFuzzyRule(reverse)) and must still play with the HBASE-30226
same-row short-circuit.
+ * A non-matching row seeks back to "abb"; revisiting "abb" must be skipped
with NEXT_ROW instead
+ * of recreating the same hint.
+ */
+ @Test
+ public void testReverseHintSkipsSameRow() {
+ FuzzyRowFilter filter = new FuzzyRowFilter(
+ Collections.singletonList(new Pair<>(Bytes.toBytes("aaa"), new byte[] {
0, 1, 0 })));
+ filter.setReversed(true);
+
+ KeyValue abc = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abc"));
+ assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT,
filter.filterCell(abc));
+ assertRow(Bytes.toBytes("abb"), filter.getNextCellHint(abc));
+
+ KeyValue abb = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abb"));
+ assertEquals(Filter.ReturnCode.NEXT_ROW, filter.filterCell(abb));
+ }
+
+ /**
+ * With multiple fuzzy keys the RowTracker priority queue holds one
(separately converted) hint
+ * mask per key and must return the smallest matching row across all of
them. Here key1 {5,*,5}
+ * hints {5,0,5} and key2 {4,9,9} hints {4,9,9}; the smaller {4,9,9} must
win.
+ */
+ @Test
+ public void testForwardHintWithMultipleKeysReturnsSmallest() {
+ FuzzyRowFilter filter =
+ new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 5, 100, 5 },
new byte[] { 0, 1, 0 }),
+ new Pair<>(new byte[] { 4, 9, 9 }, new byte[] { 0, 0, 0 })));
+
+ KeyValue current = KeyValueUtil.createFirstOnRow(new byte[] { 3, 0, 0 });
+ assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT,
filter.filterCell(current));
+ assertRow(new byte[] { 4, 9, 9 }, filter.getNextCellHint(current));
+ }
+
+ /**
+ * A FuzzyRowFilter serialized by an unsafe peer puts the mask on the wire
in its preprocessed {-1
+ * (fixed), 2 (non-fixed)} form. A no-unsafe server must still interpret it
correctly: this is
+ * exactly the {key, mask} pair {@link FuzzyRowFilter#parseFrom} hands to
the constructor for such
+ * a filter. Before the fix the no-unsafe constructor left {-1, 2}
untouched, so
+ * {@link FuzzyRowFilter#satisfiesNoUnsafe} (which keys off {0, 1}) treated
every position as a
+ * wildcard and stopped enforcing the fixed bytes, wrongly INCLUDING
non-matching rows.
+ */
+ @Test
+ public void testUnsafeEncodedMaskFromPeerEnforcesFixedPositions() {
+ // {-1, 2, -1} == fixed, non-fixed, fixed -> equivalent no-unsafe mask {0,
1, 0}.
+ FuzzyRowFilter match = new FuzzyRowFilter(
+ Collections.singletonList(new Pair<>(new byte[] { 1, 0, 3 }, new byte[]
{ -1, 2, -1 })));
+ assertEquals(Filter.ReturnCode.INCLUDE,
+ match.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3
})));
+
+ // A row that differs at a FIXED position (pos 2: 9 != 3) must not be
INCLUDED.
+ FuzzyRowFilter reject = new FuzzyRowFilter(
+ Collections.singletonList(new Pair<>(new byte[] { 1, 0, 3 }, new byte[]
{ -1, 2, -1 })));
+ assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT,
+ reject.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 9
})));
+ }
+
+ /**
+ * Faithful wire-level companion to {@link
#testUnsafeEncodedMaskFromPeerEnforcesFixedPositions}:
+ * build the exact protobuf bytes an unsafe peer emits (mask in the
preprocessed {-1, 2} form,
+ * with the {@code is_mask_v2} flag set, as HBASE-26537 requires) and run
them through
+ * {@link FuzzyRowFilter#parseFrom}. A no-unsafe server must normalize the
mask back to {0, 1} on
+ * deserialization and still enforce the fixed positions.
+ */
+ @Test
+ public void testParseFromUnsafeEncodedFilterEnforcesFixedPositions() throws
Exception {
+ byte[] wire = FilterProtos.FuzzyRowFilter.newBuilder().setIsMaskV2(true)
+ .addFuzzyKeysData(BytesBytesPair.newBuilder()
+ .setFirst(UnsafeByteOperations.unsafeWrap(new byte[] { 1, 0, 3 }))
+ .setSecond(UnsafeByteOperations.unsafeWrap(new byte[] { -1, 2, -1 })))
+ .build().toByteArray();
+
+ assertEquals(Filter.ReturnCode.INCLUDE, FuzzyRowFilter.parseFrom(wire)
+ .filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3 })));
+ // A row that differs at a FIXED position (pos 2: 9 != 3) must not be
INCLUDED.
+ assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT,
FuzzyRowFilter.parseFrom(wire)
+ .filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 9 })));
+ }
+
+ /**
+ * A legacy pre-HBASE-26537 peer wires the v1 internal mask {-1 (fixed), 0
(non-fixed)} with no
+ * is_mask_v2 flag. A no-unsafe server must normalize it back to {0, 1} and
enforce the fixed
+ * positions, like the v2 case above.
+ */
+ @Test
+ public void testParseFromLegacyV1EncodedFilterEnforcesFixedPositions()
throws Exception {
+ // No is_mask_v2 flag -> v1; mask {-1, 0, -1} == fixed, non-fixed, fixed.
+ byte[] wire = FilterProtos.FuzzyRowFilter.newBuilder()
+ .addFuzzyKeysData(BytesBytesPair.newBuilder()
+ .setFirst(UnsafeByteOperations.unsafeWrap(new byte[] { 1, 0, 3 }))
+ .setSecond(UnsafeByteOperations.unsafeWrap(new byte[] { -1, 0, -1 })))
+ .build().toByteArray();
+
+ assertEquals(Filter.ReturnCode.INCLUDE, FuzzyRowFilter.parseFrom(wire)
+ .filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3 })));
+ // A row that differs at a FIXED position (pos 2: 9 != 3) must not be
INCLUDED.
+ assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT,
FuzzyRowFilter.parseFrom(wire)
+ .filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 9 })));
+ }
+
+ /**
+ * The other half of the {@code 0} ambiguity: a legacy no-unsafe peer wires
an all-fixed rule as
+ * all-zeros {0, 0, 0} (public form, no flag). With no -1 marker it must
read as public all-fixed,
+ * not as a legacy unsafe all-wildcard mask -- so a row differing at a fixed
position is rejected.
+ */
+ @Test
+ public void testParseFromLegacyNoUnsafeAllFixedMaskEnforcesFixedPositions()
throws Exception {
+ // No is_mask_v2 flag, public all-fixed mask {0, 0, 0} from a legacy
no-unsafe peer.
+ byte[] wire = FilterProtos.FuzzyRowFilter.newBuilder()
+ .addFuzzyKeysData(BytesBytesPair.newBuilder()
+ .setFirst(UnsafeByteOperations.unsafeWrap(new byte[] { 1, 2, 3 }))
+ .setSecond(UnsafeByteOperations.unsafeWrap(new byte[] { 0, 0, 0 })))
+ .build().toByteArray();
+
+ // Exact-match row is INCLUDED ...
+ assertEquals(Filter.ReturnCode.INCLUDE, FuzzyRowFilter.parseFrom(wire)
+ .filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 2, 3 })));
+ // ... but a row differing at a fixed position must NOT be (all-zeros is
all-fixed, not
+ // all-wildcard).
+ assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT,
FuzzyRowFilter.parseFrom(wire)
+ .filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3 })));
+ }
+
+ /**
+ * A v2 unsafe peer wires an all-wildcard rule as the internal {2, 2, 2}
(with is_mask_v2 set) --
+ * no -1 marker, but still internal since the public form never uses 2. A
no-unsafe server must
+ * normalize it to public {1, 1, 1} so getFuzzyKeys / toByteArray / equals
never leak {2}.
+ */
+ @Test
+ public void testParseFromUnsafeAllWildcardMaskNormalizesToPublicForm()
throws Exception {
+ byte[] wire = FilterProtos.FuzzyRowFilter.newBuilder().setIsMaskV2(true)
+ .addFuzzyKeysData(BytesBytesPair.newBuilder()
+ .setFirst(UnsafeByteOperations.unsafeWrap(new byte[] { 1, 2, 3 }))
+ .setSecond(UnsafeByteOperations.unsafeWrap(new byte[] { 2, 2, 2 })))
+ .build().toByteArray();
+
+ FuzzyRowFilter parsed = FuzzyRowFilter.parseFrom(wire);
+ // getFuzzyKeys must expose the public {0, 1} form, not the internal {2,
2, 2}.
+ assertArrayEquals(new byte[] { 1, 1, 1 },
parsed.getFuzzyKeys().get(0).getSecond());
+
+ // It must equal (and hash like) a filter built from the public
all-wildcard mask, and survive a
+ // serialization round-trip without leaking the internal form.
+ FuzzyRowFilter publicAllWildcard = new FuzzyRowFilter(
+ Collections.singletonList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[]
{ 1, 1, 1 })));
+ assertEquals(publicAllWildcard, parsed);
+ assertEquals(publicAllWildcard.hashCode(), parsed.hashCode());
+ assertArrayEquals(new byte[] { 1, 1, 1 },
+
FuzzyRowFilter.parseFrom(parsed.toByteArray()).getFuzzyKeys().get(0).getSecond());
+ }
+
+ /**
+ * A filter built on a no-unsafe server must survive its own {@link
FuzzyRowFilter#toByteArray} /
+ * {@link FuzzyRowFilter#parseFrom} round-trip with identical behavior and
identical
+ * {@code equals}/{@code hashCode} (the wire form is the canonical {0, 1}).
+ */
+ @Test
+ public void testSerializationRoundTripPreservesFilter() throws Exception {
+ FuzzyRowFilter original = new FuzzyRowFilter(
+ Collections.singletonList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[]
{ 0, 1, 0 })));
+ FuzzyRowFilter parsed = FuzzyRowFilter.parseFrom(original.toByteArray());
+
+ assertEquals(original, parsed);
+ assertEquals(original.hashCode(), parsed.hashCode());
+ assertEquals(Filter.ReturnCode.INCLUDE,
+ parsed.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3
})));
+ }
+
+ private static void assertRow(byte[] expected, Cell cell) {
+ byte[] actual = Bytes.copy(cell.getRowArray(), cell.getRowOffset(),
cell.getRowLength());
+ assertEquals(Bytes.toStringBinary(expected), Bytes.toStringBinary(actual));
+ }
+
+ //
---------------------------------------------------------------------------
+ // Fine-grained unit coverage of the mask/key preprocessing helpers used
above.
+ //
---------------------------------------------------------------------------
+
+ @Test
+ public void testPreprocessMaskForSatisfiesNoUnsafeKeepsMaskSemantics() {
+ byte[] row = new byte[] { 1, 2, 1, 3, 3 };
+ byte[] fuzzyKey = new byte[] { 1, 2, 0, 3 };
+ byte[] fuzzyKeyFiltered = new byte[] { 0, 2, 0, 3 };
+ byte[] mask = new byte[] { 0, 0, 1, 0 };
+
+ assertEquals(FuzzyRowFilter.SatisfiesCode.YES,
+ FuzzyRowFilter.satisfiesNoUnsafe(false, row, 0, row.length, fuzzyKey,
mask));
+ assertEquals(FuzzyRowFilter.SatisfiesCode.NO_NEXT,
+ FuzzyRowFilter.satisfiesNoUnsafe(false, row, 0, row.length,
fuzzyKeyFiltered, mask));
+
+ // On the no-unsafe path the mask must be left untouched (the broken code
shifted it to 0s).
+ FuzzyRowFilter.preprocessMaskForSatisfies(mask, false);
+ assertArrayEquals(new byte[] { 0, 0, 1, 0 }, mask);
+
+ assertEquals(FuzzyRowFilter.SatisfiesCode.YES,
+ FuzzyRowFilter.satisfiesNoUnsafe(false, row, 0, row.length, fuzzyKey,
mask));
+ assertEquals(FuzzyRowFilter.SatisfiesCode.NO_NEXT,
+ FuzzyRowFilter.satisfiesNoUnsafe(false, row, 0, row.length,
fuzzyKeyFiltered, mask));
+ }
+
+ @Test
+ public void testPreprocessMaskForHintingNoUnsafeConvertsToGetNextSemantics()
{
+ byte[] row = new byte[] { 1, 2, 1, 3, 3 };
+ byte[] fuzzyKey = new byte[] { 1, 2, 0, 3 };
+ byte[] noUnsafeMask = new byte[] { 0, 0, 1, 0 };
+
+ byte[] convertedMask =
FuzzyRowFilter.preprocessMaskForHinting(noUnsafeMask, false);
+ // The original mask must not be mutated (satisfiesNoUnsafe still needs
the {0, 1} form).
+ assertArrayEquals(new byte[] { 0, 0, 1, 0 }, noUnsafeMask);
+ assertArrayEquals(new byte[] { -1, -1, 0, -1 }, convertedMask);
+
+ byte[] nextWithConverted =
+ FuzzyRowFilter.getNextForFuzzyRule(false, row, 0, row.length, fuzzyKey,
convertedMask);
+ byte[] nextWithExpectedMask = FuzzyRowFilter.getNextForFuzzyRule(false,
row, 0, row.length,
+ fuzzyKey, new byte[] { -1, -1, 0, -1 });
+
+ assertNotNull(nextWithConverted);
+ assertArrayEquals(nextWithExpectedMask, nextWithConverted);
+ }
+
+ @Test
+ public void testNoUnsafePreprocessSearchKeyClearsWildcardBytes() {
+ Pair<byte[], byte[]> fuzzyData = new Pair<>(new byte[] { 1, 100, 3 }, new
byte[] { 0, 1, 0 });
+
+ FuzzyRowFilter.preprocessSearchKey(fuzzyData, false);
+ byte[] convertedMask =
FuzzyRowFilter.preprocessMaskForHinting(fuzzyData.getSecond(), false);
+ byte[] nextForFuzzyRule = FuzzyRowFilter.getNextForFuzzyRule(false, new
byte[] { 0, 0, 0 }, 0,
+ 3, fuzzyData.getFirst(), convertedMask);
+
+ // The wildcard byte (100) must be cleared so the next hint is the
smallest matching row.
+ assertArrayEquals(new byte[] { 1, 0, 3 }, fuzzyData.getFirst());
+ assertArrayEquals(new byte[] { 1, 0, 3 }, nextForFuzzyRule);
+ }
+}