This is an automated email from the ASF dual-hosted git repository.

Apache9 pushed a commit to branch branch-2.5
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/branch-2.5 by this push:
     new 1dcb5bbcf6f HBASE-30256 FuzzyRowFilter mishandles the fuzzy-mask 
encoding on the no-unsafe path (#8424)
1dcb5bbcf6f is described below

commit 1dcb5bbcf6f0efbc032505ef08db1bb19c607ea2
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]>
    (cherry picked from commit c714400850d78a4ab145a9daa011fd390917ea1f)
---
 .../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} -&gt; {-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 -&gt; 
preprocessMaskForHinting({0,1}-&gt;{-1,0}) -&gt;
+   * 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);
+  }
+}


Reply via email to