diff options
| -rw-r--r-- | core/java/android/bluetooth/le/ScanRecord.java | 22 | ||||
| -rw-r--r-- | core/java/android/os/BytesMatcher.java | 473 | ||||
| -rw-r--r-- | core/tests/bluetoothtests/AndroidTest.xml | 32 | ||||
| -rw-r--r-- | core/tests/bluetoothtests/src/android/bluetooth/le/ScanRecordTest.java | 77 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/os/BytesMatcherTest.java | 53 |
5 files changed, 449 insertions, 208 deletions
diff --git a/core/java/android/bluetooth/le/ScanRecord.java b/core/java/android/bluetooth/le/ScanRecord.java index c0c1aa1634ff..794b512772fe 100644 --- a/core/java/android/bluetooth/le/ScanRecord.java +++ b/core/java/android/bluetooth/le/ScanRecord.java @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.Predicate; /** * Represents a scan record from Bluetooth LE scan. @@ -168,6 +169,27 @@ public final class ScanRecord { return mBytes; } + /** + * Test if any fields contained inside this scan record are matched by the + * given matcher. + * + * @hide + */ + public boolean matchesAnyField(@NonNull Predicate<byte[]> matcher) { + int pos = 0; + while (pos < mBytes.length) { + final int length = mBytes[pos] & 0xFF; + if (length == 0) { + break; + } + if (matcher.test(Arrays.copyOfRange(mBytes, pos, pos + length + 1))) { + return true; + } + pos += length + 1; + } + return false; + } + private ScanRecord(List<ParcelUuid> serviceUuids, List<ParcelUuid> serviceSolicitationUuids, SparseArray<byte[]> manufacturerData, diff --git a/core/java/android/os/BytesMatcher.java b/core/java/android/os/BytesMatcher.java index 8537f47eb575..8974c5ee4e32 100644 --- a/core/java/android/os/BytesMatcher.java +++ b/core/java/android/os/BytesMatcher.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.bluetooth.BluetoothUuid; import android.net.MacAddress; +import android.text.TextUtils; import android.util.Log; import com.android.internal.util.HexDump; @@ -42,207 +43,273 @@ import java.util.function.Predicate; * @hide */ public class BytesMatcher implements Predicate<byte[]> { - private static final String TAG = "BytesMatcher"; - - private static final char TYPE_ACCEPT = '+'; - private static final char TYPE_REJECT = '-'; - - private final ArrayList<Rule> mRules = new ArrayList<>(); - - private static class Rule { - public final char type; - public final @NonNull byte[] value; - public final @Nullable byte[] mask; - - public Rule(char type, @NonNull byte[] value, @Nullable byte[] mask) { - if (mask != null && value.length != mask.length) { - throw new IllegalArgumentException( - "Expected length " + value.length + " but found " + mask.length); - } - this.type = type; - this.value = value; - this.mask = mask; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - encode(builder); - return builder.toString(); - } - - public void encode(@NonNull StringBuilder builder) { - builder.append(type); - builder.append(HexDump.toHexString(value)); - if (mask != null) { - builder.append('/'); - builder.append(HexDump.toHexString(mask)); - } - } - - public boolean test(@NonNull byte[] value) { - if (value.length != this.value.length) { - return false; - } - for (int i = 0; i < this.value.length; i++) { - byte local = this.value[i]; - byte remote = value[i]; - if (this.mask != null) { - local &= this.mask[i]; - remote &= this.mask[i]; - } - if (local != remote) { - return false; - } - } - return true; - } - } - - /** - * Add a rule that will result in {@link #test(byte[])} returning - * {@code true} when a value being tested matches it. - * <p> - * Rules are tested in the order in which they were originally added, which - * means a narrow rule can reject a specific value before a later broader - * rule might accept that same value, or vice versa. - * - * @param value to be matched - * @param mask to be applied to both values before testing for equality; if - * {@code null} then both values must match exactly - */ - public void addAcceptRule(@NonNull byte[] value, @Nullable byte[] mask) { - mRules.add(new Rule(TYPE_ACCEPT, value, mask)); - } - - /** - * Add a rule that will result in {@link #test(byte[])} returning - * {@code false} when a value being tested matches it. - * <p> - * Rules are tested in the order in which they were originally added, which - * means a narrow rule can reject a specific value before a later broader - * rule might accept that same value, or vice versa. - * - * @param value to be matched - * @param mask to be applied to both values before testing for equality; if - * {@code null} then both values must match exactly - */ - public void addRejectRule(@NonNull byte[] value, @Nullable byte[] mask) { - mRules.add(new Rule(TYPE_REJECT, value, mask)); - } - - /** - * Test if the given {@code ParcelUuid} value matches the set of rules - * configured in this matcher. - */ - public boolean testBluetoothUuid(@NonNull ParcelUuid value) { - return test(BluetoothUuid.uuidToBytes(value)); - } - - /** - * Test if the given {@code MacAddress} value matches the set of rules - * configured in this matcher. - */ - public boolean testMacAddress(@NonNull MacAddress value) { - return test(value.toByteArray()); - } - - /** - * Test if the given {@code byte[]} value matches the set of rules - * configured in this matcher. - */ - @Override - public boolean test(@NonNull byte[] value) { - return test(value, false); - } - - /** - * Test if the given {@code byte[]} value matches the set of rules - * configured in this matcher. - */ - public boolean test(@NonNull byte[] value, boolean defaultValue) { - final int size = mRules.size(); - for (int i = 0; i < size; i++) { - final Rule rule = mRules.get(i); - if (rule.test(value)) { - return (rule.type == TYPE_ACCEPT); - } - } - return defaultValue; - } - - /** - * Encode the given matcher into a human-readable {@link String} which can - * be used to transport matchers across device boundaries. - * <p> - * The human-readable format is an ordered list separated by commas, where - * each rule is a {@code +} or {@code -} symbol indicating if the match - * should be accepted or rejected, then followed by a hex value and an - * optional hex mask. For example, {@code -caff,+cafe/ff00} is a valid - * encoded matcher. - * - * @see #decode(String) - */ - public static @NonNull String encode(@NonNull BytesMatcher matcher) { - final StringBuilder builder = new StringBuilder(); - final int size = matcher.mRules.size(); - for (int i = 0; i < size; i++) { - final Rule rule = matcher.mRules.get(i); - rule.encode(builder); - builder.append(','); - } - builder.deleteCharAt(builder.length() - 1); - return builder.toString(); - } - - /** - * Decode the given human-readable {@link String} used to transport matchers - * across device boundaries. - * <p> - * The human-readable format is an ordered list separated by commas, where - * each rule is a {@code +} or {@code -} symbol indicating if the match - * should be accepted or rejected, then followed by a hex value and an - * optional hex mask. For example, {@code -caff,+cafe/ff00} is a valid - * encoded matcher. - * - * @see #encode(BytesMatcher) - */ - public static @NonNull BytesMatcher decode(@NonNull String value) { - final BytesMatcher matcher = new BytesMatcher(); - final int length = value.length(); - for (int i = 0; i < length;) { - final char type = value.charAt(i); - - int nextRule = value.indexOf(',', i); - int nextMask = value.indexOf('/', i); - - if (nextRule == -1) nextRule = length; - if (nextMask > nextRule) nextMask = -1; - - final byte[] ruleValue; - final byte[] ruleMask; - if (nextMask >= 0) { - ruleValue = HexDump.hexStringToByteArray(value.substring(i + 1, nextMask)); - ruleMask = HexDump.hexStringToByteArray(value.substring(nextMask + 1, nextRule)); - } else { - ruleValue = HexDump.hexStringToByteArray(value.substring(i + 1, nextRule)); - ruleMask = null; - } - - switch (type) { - case TYPE_ACCEPT: - matcher.addAcceptRule(ruleValue, ruleMask); - break; - case TYPE_REJECT: - matcher.addRejectRule(ruleValue, ruleMask); - break; - default: - Log.w(TAG, "Ignoring unknown type " + type); - break; - } - - i = nextRule + 1; - } - return matcher; - } + private static final String TAG = "BytesMatcher"; + + private static final char TYPE_EXACT_ACCEPT = '+'; + private static final char TYPE_EXACT_REJECT = '-'; + private static final char TYPE_PREFIX_ACCEPT = '⊆'; + private static final char TYPE_PREFIX_REJECT = '⊈'; + + private final ArrayList<Rule> mRules = new ArrayList<>(); + + private static class Rule { + public final char type; + public final @NonNull byte[] value; + public final @Nullable byte[] mask; + + public Rule(char type, @NonNull byte[] value, @Nullable byte[] mask) { + if (mask != null && value.length != mask.length) { + throw new IllegalArgumentException( + "Expected length " + value.length + " but found " + mask.length); + } + this.type = type; + this.value = value; + this.mask = mask; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + encode(builder); + return builder.toString(); + } + + public void encode(@NonNull StringBuilder builder) { + builder.append(type); + builder.append(HexDump.toHexString(value)); + if (mask != null) { + builder.append('/'); + builder.append(HexDump.toHexString(mask)); + } + } + + public boolean test(@NonNull byte[] value) { + switch (type) { + case TYPE_EXACT_ACCEPT: + case TYPE_EXACT_REJECT: + if (value.length != this.value.length) { + return false; + } + break; + case TYPE_PREFIX_ACCEPT: + case TYPE_PREFIX_REJECT: + if (value.length < this.value.length) { + return false; + } + break; + } + for (int i = 0; i < this.value.length; i++) { + byte local = this.value[i]; + byte remote = value[i]; + if (this.mask != null) { + local &= this.mask[i]; + remote &= this.mask[i]; + } + if (local != remote) { + return false; + } + } + return true; + } + } + + /** + * Add a rule that will result in {@link #test(byte[])} returning + * {@code true} when a value being tested matches it. This rule will only + * match values of the exact same length. + * <p> + * Rules are tested in the order in which they were originally added, which + * means a narrow rule can reject a specific value before a later broader + * rule might accept that same value, or vice versa. + * + * @param value to be matched + * @param mask to be applied to both values before testing for equality; if + * {@code null} then both values must match exactly + */ + public void addExactAcceptRule(@NonNull byte[] value, @Nullable byte[] mask) { + mRules.add(new Rule(TYPE_EXACT_ACCEPT, value, mask)); + } + + /** + * Add a rule that will result in {@link #test(byte[])} returning + * {@code false} when a value being tested matches it. This rule will only + * match values of the exact same length. + * <p> + * Rules are tested in the order in which they were originally added, which + * means a narrow rule can reject a specific value before a later broader + * rule might accept that same value, or vice versa. + * + * @param value to be matched + * @param mask to be applied to both values before testing for equality; if + * {@code null} then both values must match exactly + */ + public void addExactRejectRule(@NonNull byte[] value, @Nullable byte[] mask) { + mRules.add(new Rule(TYPE_EXACT_REJECT, value, mask)); + } + + /** + * Add a rule that will result in {@link #test(byte[])} returning + * {@code true} when a value being tested matches it. This rule will match + * values of the exact same length or longer. + * <p> + * Rules are tested in the order in which they were originally added, which + * means a narrow rule can reject a specific value before a later broader + * rule might accept that same value, or vice versa. + * + * @param value to be matched + * @param mask to be applied to both values before testing for equality; if + * {@code null} then both values must match exactly + */ + public void addPrefixAcceptRule(@NonNull byte[] value, @Nullable byte[] mask) { + mRules.add(new Rule(TYPE_PREFIX_ACCEPT, value, mask)); + } + + /** + * Add a rule that will result in {@link #test(byte[])} returning + * {@code false} when a value being tested matches it. This rule will match + * values of the exact same length or longer. + * <p> + * Rules are tested in the order in which they were originally added, which + * means a narrow rule can reject a specific value before a later broader + * rule might accept that same value, or vice versa. + * + * @param value to be matched + * @param mask to be applied to both values before testing for equality; if + * {@code null} then both values must match exactly + */ + public void addPrefixRejectRule(@NonNull byte[] value, @Nullable byte[] mask) { + mRules.add(new Rule(TYPE_PREFIX_REJECT, value, mask)); + } + + /** + * Test if the given {@code ParcelUuid} value matches the set of rules + * configured in this matcher. + */ + public boolean testBluetoothUuid(@NonNull ParcelUuid value) { + return test(BluetoothUuid.uuidToBytes(value)); + } + + /** + * Test if the given {@code MacAddress} value matches the set of rules + * configured in this matcher. + */ + public boolean testMacAddress(@NonNull MacAddress value) { + return test(value.toByteArray()); + } + + /** + * Test if the given {@code byte[]} value matches the set of rules + * configured in this matcher. + */ + @Override + public boolean test(@NonNull byte[] value) { + return test(value, false); + } + + /** + * Test if the given {@code byte[]} value matches the set of rules + * configured in this matcher. + */ + public boolean test(@NonNull byte[] value, boolean defaultValue) { + final int size = mRules.size(); + for (int i = 0; i < size; i++) { + final Rule rule = mRules.get(i); + if (rule.test(value)) { + switch (rule.type) { + case TYPE_EXACT_ACCEPT: + case TYPE_PREFIX_ACCEPT: + return true; + case TYPE_EXACT_REJECT: + case TYPE_PREFIX_REJECT: + return false; + } + } + } + return defaultValue; + } + + /** + * Encode the given matcher into a human-readable {@link String} which can + * be used to transport matchers across device boundaries. + * <p> + * The human-readable format is an ordered list separated by commas, where + * each rule is a {@code +} or {@code -} symbol indicating if the match + * should be accepted or rejected, then followed by a hex value and an + * optional hex mask. For example, {@code -caff,+cafe/ff00} is a valid + * encoded matcher. + * + * @see #decode(String) + */ + public static @NonNull String encode(@NonNull BytesMatcher matcher) { + final StringBuilder builder = new StringBuilder(); + final int size = matcher.mRules.size(); + for (int i = 0; i < size; i++) { + final Rule rule = matcher.mRules.get(i); + rule.encode(builder); + builder.append(','); + } + if (builder.length() > 0) { + builder.deleteCharAt(builder.length() - 1); + } + return builder.toString(); + } + + /** + * Decode the given human-readable {@link String} used to transport matchers + * across device boundaries. + * <p> + * The human-readable format is an ordered list separated by commas, where + * each rule is a {@code +} or {@code -} symbol indicating if the match + * should be accepted or rejected, then followed by a hex value and an + * optional hex mask. For example, {@code -caff,+cafe/ff00} is a valid + * encoded matcher. + * + * @see #encode(BytesMatcher) + */ + public static @NonNull BytesMatcher decode(@Nullable String value) { + final BytesMatcher matcher = new BytesMatcher(); + if (TextUtils.isEmpty(value)) return matcher; + + final int length = value.length(); + for (int i = 0; i < length;) { + final char type = value.charAt(i); + + int nextRule = value.indexOf(',', i); + int nextMask = value.indexOf('/', i); + + if (nextRule == -1) nextRule = length; + if (nextMask > nextRule) nextMask = -1; + + final byte[] ruleValue; + final byte[] ruleMask; + if (nextMask >= 0) { + ruleValue = HexDump.hexStringToByteArray(value.substring(i + 1, nextMask)); + ruleMask = HexDump.hexStringToByteArray(value.substring(nextMask + 1, nextRule)); + } else { + ruleValue = HexDump.hexStringToByteArray(value.substring(i + 1, nextRule)); + ruleMask = null; + } + + switch (type) { + case TYPE_EXACT_ACCEPT: + matcher.addExactAcceptRule(ruleValue, ruleMask); + break; + case TYPE_EXACT_REJECT: + matcher.addExactRejectRule(ruleValue, ruleMask); + break; + case TYPE_PREFIX_ACCEPT: + matcher.addPrefixAcceptRule(ruleValue, ruleMask); + break; + case TYPE_PREFIX_REJECT: + matcher.addPrefixRejectRule(ruleValue, ruleMask); + break; + default: + Log.w(TAG, "Ignoring unknown type " + type); + break; + } + + i = nextRule + 1; + } + return matcher; + } } diff --git a/core/tests/bluetoothtests/AndroidTest.xml b/core/tests/bluetoothtests/AndroidTest.xml new file mode 100644 index 000000000000..f93c4ebf5bf6 --- /dev/null +++ b/core/tests/bluetoothtests/AndroidTest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 The Android Open Source Project + + Licensed 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. +--> +<configuration description="Config for Bluetooth test cases"> + <option name="test-suite-tag" value="apct"/> + <option name="test-suite-tag" value="apct-instrumentation"/> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="BluetoothTests.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct"/> + <option name="test-tag" value="BluetoothTests"/> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.bluetooth.tests" /> + <option name="hidden-api-checks" value="false"/> + <option name="runner" value="android.bluetooth.BluetoothTestRunner"/> + </test> +</configuration> diff --git a/core/tests/bluetoothtests/src/android/bluetooth/le/ScanRecordTest.java b/core/tests/bluetoothtests/src/android/bluetooth/le/ScanRecordTest.java index 8b3db7e4d934..c287ea9d8567 100644 --- a/core/tests/bluetoothtests/src/android/bluetooth/le/ScanRecordTest.java +++ b/core/tests/bluetoothtests/src/android/bluetooth/le/ScanRecordTest.java @@ -16,13 +16,18 @@ package android.bluetooth.le; -import android.bluetooth.le.ScanRecord; +import android.os.BytesMatcher; import android.os.ParcelUuid; import android.test.suitebuilder.annotation.SmallTest; +import com.android.internal.util.HexDump; + import junit.framework.TestCase; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; /** * Unit test cases for {@link ScanRecord}. @@ -31,6 +36,66 @@ import java.util.Arrays; * 'com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner' */ public class ScanRecordTest extends TestCase { + /** + * Example raw beacons captured from a Blue Charm BC011 + */ + private static final String RECORD_URL = "0201060303AAFE1716AAFE10EE01626C7565636861726D626561636F6E730009168020691E0EFE13551109426C7565436861726D5F313639363835000000"; + private static final String RECORD_UUID = "0201060303AAFE1716AAFE00EE626C7565636861726D31000000000001000009168020691E0EFE13551109426C7565436861726D5F313639363835000000"; + private static final String RECORD_TLM = "0201060303AAFE1116AAFE20000BF017000008874803FB93540916802069080EFE13551109426C7565436861726D5F313639363835000000000000000000"; + private static final String RECORD_IBEACON = "0201061AFF4C000215426C7565436861726D426561636F6E730EFE1355C509168020691E0EFE13551109426C7565436861726D5F31363936383500000000"; + + @SmallTest + public void testMatchesAnyField_Eddystone_Parser() { + final List<String> found = new ArrayList<>(); + final Predicate<byte[]> matcher = (v) -> { + found.add(HexDump.toHexString(v)); + return false; + }; + ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(RECORD_URL)) + .matchesAnyField(matcher); + + assertEquals(Arrays.asList( + "020106", + "0303AAFE", + "1716AAFE10EE01626C7565636861726D626561636F6E7300", + "09168020691E0EFE1355", + "1109426C7565436861726D5F313639363835"), found); + } + + @SmallTest + public void testMatchesAnyField_Eddystone() { + final BytesMatcher matcher = BytesMatcher.decode("⊆0016AAFE/00FFFFFF"); + assertMatchesAnyField(RECORD_URL, matcher); + assertMatchesAnyField(RECORD_UUID, matcher); + assertMatchesAnyField(RECORD_TLM, matcher); + assertNotMatchesAnyField(RECORD_IBEACON, matcher); + } + + @SmallTest + public void testMatchesAnyField_iBeacon_Parser() { + final List<String> found = new ArrayList<>(); + final Predicate<byte[]> matcher = (v) -> { + found.add(HexDump.toHexString(v)); + return false; + }; + ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(RECORD_IBEACON)) + .matchesAnyField(matcher); + + assertEquals(Arrays.asList( + "020106", + "1AFF4C000215426C7565436861726D426561636F6E730EFE1355C5", + "09168020691E0EFE1355", + "1109426C7565436861726D5F313639363835"), found); + } + + @SmallTest + public void testMatchesAnyField_iBeacon() { + final BytesMatcher matcher = BytesMatcher.decode("⊆00FF4C0002/00FFFFFFFF"); + assertNotMatchesAnyField(RECORD_URL, matcher); + assertNotMatchesAnyField(RECORD_UUID, matcher); + assertNotMatchesAnyField(RECORD_TLM, matcher); + assertMatchesAnyField(RECORD_IBEACON, matcher); + } @SmallTest public void testParser() { @@ -70,4 +135,14 @@ public class ScanRecordTest extends TestCase { } } + + private static void assertMatchesAnyField(String record, BytesMatcher matcher) { + assertTrue(ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(record)) + .matchesAnyField(matcher)); + } + + private static void assertNotMatchesAnyField(String record, BytesMatcher matcher) { + assertFalse(ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(record)) + .matchesAnyField(matcher)); + } } diff --git a/core/tests/coretests/src/android/os/BytesMatcherTest.java b/core/tests/coretests/src/android/os/BytesMatcherTest.java index 67c1b3c947fb..b28e3090edd0 100644 --- a/core/tests/coretests/src/android/os/BytesMatcherTest.java +++ b/core/tests/coretests/src/android/os/BytesMatcherTest.java @@ -59,6 +59,19 @@ public class BytesMatcherTest extends TestCase { } @Test + public void testPrefix() throws Exception { + BytesMatcher matcher = BytesMatcher.decode("⊆cafe,⊆beef/ff00"); + assertTrue(matcher.test(hexStringToByteArray("cafe"))); + assertFalse(matcher.test(hexStringToByteArray("caff"))); + assertTrue(matcher.test(hexStringToByteArray("cafecafe"))); + assertFalse(matcher.test(hexStringToByteArray("ca"))); + assertTrue(matcher.test(hexStringToByteArray("beef"))); + assertTrue(matcher.test(hexStringToByteArray("beff"))); + assertTrue(matcher.test(hexStringToByteArray("beffbeff"))); + assertFalse(matcher.test(hexStringToByteArray("be"))); + } + + @Test public void testMacAddress() throws Exception { BytesMatcher matcher = BytesMatcher.decode("+cafe00112233/ffffff000000"); assertTrue(matcher.testMacAddress( @@ -94,13 +107,23 @@ public class BytesMatcherTest extends TestCase { } @Test - public void testSerialize() throws Exception { + public void testSerialize_Empty() throws Exception { + BytesMatcher matcher = new BytesMatcher(); + matcher = BytesMatcher.decode(BytesMatcher.encode(matcher)); + + // Also very empty and null values + BytesMatcher.decode(""); + BytesMatcher.decode(null); + } + + @Test + public void testSerialize_Exact() throws Exception { BytesMatcher matcher = new BytesMatcher(); - matcher.addRejectRule(hexStringToByteArray("cafe00112233"), + matcher.addExactRejectRule(hexStringToByteArray("cafe00112233"), hexStringToByteArray("ffffff000000")); - matcher.addRejectRule(hexStringToByteArray("beef00112233"), + matcher.addExactRejectRule(hexStringToByteArray("beef00112233"), null); - matcher.addAcceptRule(hexStringToByteArray("000000000000"), + matcher.addExactAcceptRule(hexStringToByteArray("000000000000"), hexStringToByteArray("000000000000")); assertFalse(matcher.test(hexStringToByteArray("cafe00ffffff"))); @@ -116,6 +139,28 @@ public class BytesMatcherTest extends TestCase { } @Test + public void testSerialize_Prefix() throws Exception { + BytesMatcher matcher = new BytesMatcher(); + matcher.addExactRejectRule(hexStringToByteArray("aa"), null); + matcher.addExactAcceptRule(hexStringToByteArray("bb"), null); + matcher.addPrefixAcceptRule(hexStringToByteArray("aa"), null); + matcher.addPrefixRejectRule(hexStringToByteArray("bb"), null); + + assertFalse(matcher.test(hexStringToByteArray("aa"))); + assertTrue(matcher.test(hexStringToByteArray("bb"))); + assertTrue(matcher.test(hexStringToByteArray("aaaa"))); + assertFalse(matcher.test(hexStringToByteArray("bbbb"))); + + // Bounce through serialization pass and confirm it still works + matcher = BytesMatcher.decode(BytesMatcher.encode(matcher)); + + assertFalse(matcher.test(hexStringToByteArray("aa"))); + assertTrue(matcher.test(hexStringToByteArray("bb"))); + assertTrue(matcher.test(hexStringToByteArray("aaaa"))); + assertFalse(matcher.test(hexStringToByteArray("bbbb"))); + } + + @Test public void testOrdering_RejectFirst() throws Exception { BytesMatcher matcher = BytesMatcher.decode("-ff/0f,+ff/f0"); assertFalse(matcher.test(hexStringToByteArray("ff"))); |