Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 34229aaf authored by Jeff Sharkey's avatar Jeff Sharkey Committed by Android (Google) Code Review
Browse files

Merge "General BytesMatcher utility class." into sc-dev

parents faa55308 6644436a
Loading
Loading
Loading
Loading
+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;
   }
}
+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")));
    }
}