diff --git a/core/java/android/os/BytesMatcher.java b/core/java/android/os/BytesMatcher.java new file mode 100644 index 0000000000000..8537f47eb575a --- /dev/null +++ b/core/java/android/os/BytesMatcher.java @@ -0,0 +1,248 @@ +/* + * 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 android.os; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.bluetooth.BluetoothUuid; +import android.net.MacAddress; +import android.util.Log; + +import com.android.internal.util.HexDump; + +import java.util.ArrayList; +import java.util.function.Predicate; + +/** + * Predicate that tests if a given {@code byte[]} value matches a set of + * configured rules. + *
+ * 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. + *
+ * 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
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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;
+ }
+}
diff --git a/core/tests/coretests/src/android/os/BytesMatcherTest.java b/core/tests/coretests/src/android/os/BytesMatcherTest.java
new file mode 100644
index 0000000000000..67c1b3c947fb8
--- /dev/null
+++ b/core/tests/coretests/src/android/os/BytesMatcherTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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 android.os;
+
+import static com.android.internal.util.HexDump.hexStringToByteArray;
+
+import android.bluetooth.BluetoothUuid;
+import android.net.MacAddress;
+
+import androidx.test.filters.SmallTest;
+
+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(hexStringToByteArray("cafe")));
+ assertFalse(matcher.test(hexStringToByteArray("")));
+ }
+
+ @Test
+ public void testExact() throws Exception {
+ BytesMatcher matcher = BytesMatcher.decode("+cafe");
+ assertTrue(matcher.test(hexStringToByteArray("cafe")));
+ assertFalse(matcher.test(hexStringToByteArray("beef")));
+ assertFalse(matcher.test(hexStringToByteArray("ca")));
+ assertFalse(matcher.test(hexStringToByteArray("cafe00")));
+ }
+
+ @Test
+ public void testMask() throws Exception {
+ BytesMatcher matcher = BytesMatcher.decode("+cafe/ff00");
+ assertTrue(matcher.test(hexStringToByteArray("cafe")));
+ assertTrue(matcher.test(hexStringToByteArray("ca88")));
+ assertFalse(matcher.test(hexStringToByteArray("beef")));
+ assertFalse(matcher.test(hexStringToByteArray("ca")));
+ assertFalse(matcher.test(hexStringToByteArray("cafe00")));
+ }
+
+ @Test
+ public void testMacAddress() throws Exception {
+ BytesMatcher matcher = BytesMatcher.decode("+cafe00112233/ffffff000000");
+ assertTrue(matcher.testMacAddress(
+ MacAddress.fromString("ca:fe:00:00:00:00")));
+ assertFalse(matcher.testMacAddress(
+ MacAddress.fromString("f0:0d:00:00:00:00")));
+ }
+
+ @Test
+ public void testBluetoothUuid() throws Exception {
+ BytesMatcher matcher = BytesMatcher.decode("+cafe/ff00");
+ assertTrue(matcher.testBluetoothUuid(
+ BluetoothUuid.parseUuidFrom(hexStringToByteArray("cafe"))));
+ assertFalse(matcher.testBluetoothUuid(
+ BluetoothUuid.parseUuidFrom(hexStringToByteArray("beef"))));
+ }
+
+ /**
+ * Verify that single matcher can be configured to match Bluetooth UUIDs of
+ * varying lengths.
+ */
+ @Test
+ public void testBluetoothUuid_Mixed() throws Exception {
+ BytesMatcher matcher = BytesMatcher.decode("+aaaa/ff00,+bbbbbbbb/ffff0000");
+ assertTrue(matcher.testBluetoothUuid(
+ BluetoothUuid.parseUuidFrom(hexStringToByteArray("aaaa"))));
+ assertFalse(matcher.testBluetoothUuid(
+ BluetoothUuid.parseUuidFrom(hexStringToByteArray("bbbb"))));
+ assertTrue(matcher.testBluetoothUuid(
+ BluetoothUuid.parseUuidFrom(hexStringToByteArray("bbbbbbbb"))));
+ assertFalse(matcher.testBluetoothUuid(
+ BluetoothUuid.parseUuidFrom(hexStringToByteArray("aaaaaaaa"))));
+ }
+
+ @Test
+ public void testSerialize() throws Exception {
+ BytesMatcher matcher = new BytesMatcher();
+ matcher.addRejectRule(hexStringToByteArray("cafe00112233"),
+ hexStringToByteArray("ffffff000000"));
+ matcher.addRejectRule(hexStringToByteArray("beef00112233"),
+ null);
+ matcher.addAcceptRule(hexStringToByteArray("000000000000"),
+ hexStringToByteArray("000000000000"));
+
+ assertFalse(matcher.test(hexStringToByteArray("cafe00ffffff")));
+ assertFalse(matcher.test(hexStringToByteArray("beef00112233")));
+ assertTrue(matcher.test(hexStringToByteArray("beef00ffffff")));
+
+ // Bounce through serialization pass and confirm it still works
+ matcher = BytesMatcher.decode(BytesMatcher.encode(matcher));
+
+ assertFalse(matcher.test(hexStringToByteArray("cafe00ffffff")));
+ assertFalse(matcher.test(hexStringToByteArray("beef00112233")));
+ assertTrue(matcher.test(hexStringToByteArray("beef00ffffff")));
+ }
+
+ @Test
+ public void testOrdering_RejectFirst() throws Exception {
+ BytesMatcher matcher = BytesMatcher.decode("-ff/0f,+ff/f0");
+ assertFalse(matcher.test(hexStringToByteArray("ff")));
+ assertTrue(matcher.test(hexStringToByteArray("f0")));
+ assertFalse(matcher.test(hexStringToByteArray("0f")));
+ }
+
+ @Test
+ public void testOrdering_AcceptFirst() throws Exception {
+ BytesMatcher matcher = BytesMatcher.decode("+ff/f0,-ff/0f");
+ assertTrue(matcher.test(hexStringToByteArray("ff")));
+ assertTrue(matcher.test(hexStringToByteArray("f0")));
+ assertFalse(matcher.test(hexStringToByteArray("0f")));
+ }
+}