Loading core/java/android/os/BytesMatcher.java 0 → 100644 +248 −0 Original line number Diff line number Diff line /* * 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. * <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_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; } } core/tests/coretests/src/android/os/BytesMatcherTest.java 0 → 100644 +133 −0 Original line number Diff line number Diff line /* * 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"))); } } Loading
core/java/android/os/BytesMatcher.java 0 → 100644 +248 −0 Original line number Diff line number Diff line /* * 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. * <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_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; } }
core/tests/coretests/src/android/os/BytesMatcherTest.java 0 → 100644 +133 −0 Original line number Diff line number Diff line /* * 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"))); } }