Add BytesMatcher to modules-utils

Import BytesMatcher from internal and made this following changes:
* change packages name
* Replace HexDump by HexEncoding
* remove testMacAddress
     user will have to directly call `test(macAddress.toByteArray()`
* remove testBluetoothUuid
     user will have to directly call `test(BluetoothUuid.uuidToBytes(parcelUuid))`

Import BytesMatcherTest from internal and made this following changes:
* change packages name
* remove tests associated to MacAddress & BluetoothUuid
     testMacAddress
     testBluetoothUuid
     testBluetoothUuid_Mixed

Bug: 198418216
Bug: 190440540
Tag: #refactor
Test: Build
Change-Id: I4d2473111270bd75d83084ee8c680dcbd89862b7
diff --git a/java/com/android/modules/utils/Android.bp b/java/com/android/modules/utils/Android.bp
index cc4aed0..4b677ea 100644
--- a/java/com/android/modules/utils/Android.bp
+++ b/java/com/android/modules/utils/Android.bp
@@ -73,3 +73,20 @@
         "//apex_available:platform",
     ],
 }
+
+java_library {
+    name: "modules-utils-bytesmatcher",
+
+    sdk_version: "module_current",
+    min_sdk_version: "29",
+
+    srcs: ["BytesMatcher.java"],
+
+    libs: ["framework-annotations-lib"],
+
+    visibility: ["//visibility:public"],
+    apex_available: [
+        "//apex_available:anyapex",
+        "//apex_available:platform",
+    ],
+}
diff --git a/java/com/android/modules/utils/BytesMatcher.java b/java/com/android/modules/utils/BytesMatcher.java
new file mode 100644
index 0000000..ce597b6
--- /dev/null
+++ b/java/com/android/modules/utils/BytesMatcher.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+package com.android.modules.utils;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothUuid;
+import android.text.TextUtils;
+import android.util.Log;
+
+import libcore.util.HexEncoding;
+
+import java.util.ArrayList;
+import java.util.function.Predicate;
+
+/**
+ * Predicate that tests if a given {@code byte[]} value matches a set of
+ * configured rules.
+ * <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.
+ * <p>
+ * Matchers can contain rules of varying lengths, and tested values will only be
+ * matched against rules of the exact same length. This is designed to support
+ * {@link BluetoothUuid} style values which can be variable length.
+ *
+ * @hide
+ */
+public class BytesMatcher implements Predicate<byte[]> {
+    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(this.type);
+            builder.append(HexEncoding.encodeToString(this.value));
+            if (this.mask != null) {
+                builder.append('/');
+                builder.append(HexEncoding.encodeToString(this.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 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 = HexEncoding.decode(value.substring(i + 1, nextMask));
+                ruleMask = HexEncoding.decode(value.substring(nextMask + 1, nextRule));
+            } else {
+                ruleValue = HexEncoding.decode(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/javatests/Android.bp b/javatests/Android.bp
index 3980ebe..060be00 100644
--- a/javatests/Android.bp
+++ b/javatests/Android.bp
@@ -13,6 +13,7 @@
     static_libs: [
         "modules-utils-os",
         "modules-utils-backgroundthread",
+        "modules-utils-bytesmatcher",
         "androidx.test.runner",
         "androidx.test.rules",
     ],
diff --git a/javatests/com/android/modules/utils/BytesMatcherTest.java b/javatests/com/android/modules/utils/BytesMatcherTest.java
new file mode 100644
index 0000000..d37ac05
--- /dev/null
+++ b/javatests/com/android/modules/utils/BytesMatcherTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+package com.android.modules.utils;
+
+import androidx.test.filters.SmallTest;
+
+import libcore.util.HexEncoding;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+@SmallTest
+public class BytesMatcherTest extends TestCase {
+    @Test
+    public void testEmpty() throws Exception {
+        BytesMatcher matcher = BytesMatcher.decode("");
+        assertFalse(matcher.test(HexEncoding.decode("cafe")));
+        assertFalse(matcher.test(HexEncoding.decode("")));
+    }
+
+    @Test
+    public void testExact() throws Exception {
+        BytesMatcher matcher = BytesMatcher.decode("+cafe");
+        assertTrue(matcher.test(HexEncoding.decode("cafe")));
+        assertFalse(matcher.test(HexEncoding.decode("beef")));
+        assertFalse(matcher.test(HexEncoding.decode("ca")));
+        assertFalse(matcher.test(HexEncoding.decode("cafe00")));
+    }
+
+    @Test
+    public void testMask() throws Exception {
+        BytesMatcher matcher = BytesMatcher.decode("+cafe/ff00");
+        assertTrue(matcher.test(HexEncoding.decode("cafe")));
+        assertTrue(matcher.test(HexEncoding.decode("ca88")));
+        assertFalse(matcher.test(HexEncoding.decode("beef")));
+        assertFalse(matcher.test(HexEncoding.decode("ca")));
+        assertFalse(matcher.test(HexEncoding.decode("cafe00")));
+    }
+
+    @Test
+    public void testPrefix() throws Exception {
+        BytesMatcher matcher = BytesMatcher.decode("⊆cafe,⊆beef/ff00");
+        assertTrue(matcher.test(HexEncoding.decode("cafe")));
+        assertFalse(matcher.test(HexEncoding.decode("caff")));
+        assertTrue(matcher.test(HexEncoding.decode("cafecafe")));
+        assertFalse(matcher.test(HexEncoding.decode("ca")));
+        assertTrue(matcher.test(HexEncoding.decode("beef")));
+        assertTrue(matcher.test(HexEncoding.decode("beff")));
+        assertTrue(matcher.test(HexEncoding.decode("beffbeff")));
+        assertFalse(matcher.test(HexEncoding.decode("be")));
+    }
+
+    @Test
+    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.addExactRejectRule(HexEncoding.decode("cafe00112233"),
+                HexEncoding.decode("ffffff000000"));
+        matcher.addExactRejectRule(HexEncoding.decode("beef00112233"),
+                null);
+        matcher.addExactAcceptRule(HexEncoding.decode("000000000000"),
+                HexEncoding.decode("000000000000"));
+
+        assertFalse(matcher.test(HexEncoding.decode("cafe00ffffff")));
+        assertFalse(matcher.test(HexEncoding.decode("beef00112233")));
+        assertTrue(matcher.test(HexEncoding.decode("beef00ffffff")));
+
+        // Bounce through serialization pass and confirm it still works
+        matcher = BytesMatcher.decode(BytesMatcher.encode(matcher));
+
+        assertFalse(matcher.test(HexEncoding.decode("cafe00ffffff")));
+        assertFalse(matcher.test(HexEncoding.decode("beef00112233")));
+        assertTrue(matcher.test(HexEncoding.decode("beef00ffffff")));
+    }
+
+    @Test
+    public void testSerialize_Prefix() throws Exception {
+        BytesMatcher matcher = new BytesMatcher();
+        matcher.addExactRejectRule(HexEncoding.decode("aa"), null);
+        matcher.addExactAcceptRule(HexEncoding.decode("bb"), null);
+        matcher.addPrefixAcceptRule(HexEncoding.decode("aa"), null);
+        matcher.addPrefixRejectRule(HexEncoding.decode("bb"), null);
+
+        assertFalse(matcher.test(HexEncoding.decode("aa")));
+        assertTrue(matcher.test(HexEncoding.decode("bb")));
+        assertTrue(matcher.test(HexEncoding.decode("aaaa")));
+        assertFalse(matcher.test(HexEncoding.decode("bbbb")));
+
+        // Bounce through serialization pass and confirm it still works
+        matcher = BytesMatcher.decode(BytesMatcher.encode(matcher));
+
+        assertFalse(matcher.test(HexEncoding.decode("aa")));
+        assertTrue(matcher.test(HexEncoding.decode("bb")));
+        assertTrue(matcher.test(HexEncoding.decode("aaaa")));
+        assertFalse(matcher.test(HexEncoding.decode("bbbb")));
+    }
+
+    @Test
+    public void testOrdering_RejectFirst() throws Exception {
+        BytesMatcher matcher = BytesMatcher.decode("-ff/0f,+ff/f0");
+        assertFalse(matcher.test(HexEncoding.decode("ff")));
+        assertTrue(matcher.test(HexEncoding.decode("f0")));
+        assertFalse(matcher.test(HexEncoding.decode("0f")));
+    }
+
+    @Test
+    public void testOrdering_AcceptFirst() throws Exception {
+        BytesMatcher matcher = BytesMatcher.decode("+ff/f0,-ff/0f");
+        assertTrue(matcher.test(HexEncoding.decode("ff")));
+        assertTrue(matcher.test(HexEncoding.decode("f0")));
+        assertFalse(matcher.test(HexEncoding.decode("0f")));
+    }
+}