1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.os;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.bluetooth.BluetoothUuid;
22 import android.net.MacAddress;
23 import android.text.TextUtils;
24 import android.util.Log;
25 
26 import com.android.internal.util.HexDump;
27 
28 import java.util.ArrayList;
29 import java.util.function.Predicate;
30 
31 /**
32  * Predicate that tests if a given {@code byte[]} value matches a set of
33  * configured rules.
34  * <p>
35  * Rules are tested in the order in which they were originally added, which
36  * means a narrow rule can reject a specific value before a later broader rule
37  * might accept that same value, or vice versa.
38  * <p>
39  * Matchers can contain rules of varying lengths, and tested values will only be
40  * matched against rules of the exact same length. This is designed to support
41  * {@link BluetoothUuid} style values which can be variable length.
42  *
43  * @hide
44  */
45 public class BytesMatcher implements Predicate<byte[]> {
46     private static final String TAG = "BytesMatcher";
47 
48     private static final char TYPE_EXACT_ACCEPT = '+';
49     private static final char TYPE_EXACT_REJECT = '-';
50     private static final char TYPE_PREFIX_ACCEPT = '⊆';
51     private static final char TYPE_PREFIX_REJECT = '⊈';
52 
53     private final ArrayList<Rule> mRules = new ArrayList<>();
54 
55     private static class Rule {
56         public final char type;
57         public final @NonNull byte[] value;
58         public final @Nullable byte[] mask;
59 
Rule(char type, @NonNull byte[] value, @Nullable byte[] mask)60         public Rule(char type, @NonNull byte[] value, @Nullable byte[] mask) {
61             if (mask != null && value.length != mask.length) {
62                 throw new IllegalArgumentException(
63                         "Expected length " + value.length + " but found " + mask.length);
64             }
65             this.type = type;
66             this.value = value;
67             this.mask = mask;
68         }
69 
70         @Override
toString()71         public String toString() {
72             StringBuilder builder = new StringBuilder();
73             encode(builder);
74             return builder.toString();
75         }
76 
encode(@onNull StringBuilder builder)77         public void encode(@NonNull StringBuilder builder) {
78             builder.append(type);
79             builder.append(HexDump.toHexString(value));
80             if (mask != null) {
81                 builder.append('/');
82                 builder.append(HexDump.toHexString(mask));
83             }
84         }
85 
test(@onNull byte[] value)86         public boolean test(@NonNull byte[] value) {
87             switch (type) {
88                 case TYPE_EXACT_ACCEPT:
89                 case TYPE_EXACT_REJECT:
90                     if (value.length != this.value.length) {
91                         return false;
92                     }
93                     break;
94                 case TYPE_PREFIX_ACCEPT:
95                 case TYPE_PREFIX_REJECT:
96                     if (value.length < this.value.length) {
97                         return false;
98                     }
99                     break;
100             }
101             for (int i = 0; i < this.value.length; i++) {
102                 byte local = this.value[i];
103                 byte remote = value[i];
104                 if (this.mask != null) {
105                     local &= this.mask[i];
106                     remote &= this.mask[i];
107                 }
108                 if (local != remote) {
109                     return false;
110                 }
111             }
112             return true;
113         }
114     }
115 
116     /**
117      * Add a rule that will result in {@link #test(byte[])} returning
118      * {@code true} when a value being tested matches it. This rule will only
119      * match values of the exact same length.
120      * <p>
121      * Rules are tested in the order in which they were originally added, which
122      * means a narrow rule can reject a specific value before a later broader
123      * rule might accept that same value, or vice versa.
124      *
125      * @param value to be matched
126      * @param mask to be applied to both values before testing for equality; if
127      *            {@code null} then both values must match exactly
128      */
addExactAcceptRule(@onNull byte[] value, @Nullable byte[] mask)129     public void addExactAcceptRule(@NonNull byte[] value, @Nullable byte[] mask) {
130         mRules.add(new Rule(TYPE_EXACT_ACCEPT, value, mask));
131     }
132 
133     /**
134      * Add a rule that will result in {@link #test(byte[])} returning
135      * {@code false} when a value being tested matches it. This rule will only
136      * match values of the exact same length.
137      * <p>
138      * Rules are tested in the order in which they were originally added, which
139      * means a narrow rule can reject a specific value before a later broader
140      * rule might accept that same value, or vice versa.
141      *
142      * @param value to be matched
143      * @param mask to be applied to both values before testing for equality; if
144      *            {@code null} then both values must match exactly
145      */
addExactRejectRule(@onNull byte[] value, @Nullable byte[] mask)146     public void addExactRejectRule(@NonNull byte[] value, @Nullable byte[] mask) {
147         mRules.add(new Rule(TYPE_EXACT_REJECT, value, mask));
148     }
149 
150     /**
151      * Add a rule that will result in {@link #test(byte[])} returning
152      * {@code true} when a value being tested matches it. This rule will match
153      * values of the exact same length or longer.
154      * <p>
155      * Rules are tested in the order in which they were originally added, which
156      * means a narrow rule can reject a specific value before a later broader
157      * rule might accept that same value, or vice versa.
158      *
159      * @param value to be matched
160      * @param mask to be applied to both values before testing for equality; if
161      *            {@code null} then both values must match exactly
162      */
addPrefixAcceptRule(@onNull byte[] value, @Nullable byte[] mask)163     public void addPrefixAcceptRule(@NonNull byte[] value, @Nullable byte[] mask) {
164         mRules.add(new Rule(TYPE_PREFIX_ACCEPT, value, mask));
165     }
166 
167     /**
168      * Add a rule that will result in {@link #test(byte[])} returning
169      * {@code false} when a value being tested matches it. This rule will match
170      * values of the exact same length or longer.
171      * <p>
172      * Rules are tested in the order in which they were originally added, which
173      * means a narrow rule can reject a specific value before a later broader
174      * rule might accept that same value, or vice versa.
175      *
176      * @param value to be matched
177      * @param mask to be applied to both values before testing for equality; if
178      *            {@code null} then both values must match exactly
179      */
addPrefixRejectRule(@onNull byte[] value, @Nullable byte[] mask)180     public void addPrefixRejectRule(@NonNull byte[] value, @Nullable byte[] mask) {
181         mRules.add(new Rule(TYPE_PREFIX_REJECT, value, mask));
182     }
183 
184     /**
185      * Test if the given {@code ParcelUuid} value matches the set of rules
186      * configured in this matcher.
187      */
testBluetoothUuid(@onNull ParcelUuid value)188     public boolean testBluetoothUuid(@NonNull ParcelUuid value) {
189         return test(BluetoothUuid.uuidToBytes(value));
190     }
191 
192     /**
193      * Test if the given {@code MacAddress} value matches the set of rules
194      * configured in this matcher.
195      */
testMacAddress(@onNull MacAddress value)196     public boolean testMacAddress(@NonNull MacAddress value) {
197         return test(value.toByteArray());
198     }
199 
200     /**
201      * Test if the given {@code byte[]} value matches the set of rules
202      * configured in this matcher.
203      */
204     @Override
test(@onNull byte[] value)205     public boolean test(@NonNull byte[] value) {
206         return test(value, false);
207     }
208 
209     /**
210      * Test if the given {@code byte[]} value matches the set of rules
211      * configured in this matcher.
212      */
test(@onNull byte[] value, boolean defaultValue)213     public boolean test(@NonNull byte[] value, boolean defaultValue) {
214         final int size = mRules.size();
215         for (int i = 0; i < size; i++) {
216             final Rule rule = mRules.get(i);
217             if (rule.test(value)) {
218                 switch (rule.type) {
219                     case TYPE_EXACT_ACCEPT:
220                     case TYPE_PREFIX_ACCEPT:
221                         return true;
222                     case TYPE_EXACT_REJECT:
223                     case TYPE_PREFIX_REJECT:
224                         return false;
225                 }
226             }
227         }
228         return defaultValue;
229     }
230 
231     /**
232      * Encode the given matcher into a human-readable {@link String} which can
233      * be used to transport matchers across device boundaries.
234      * <p>
235      * The human-readable format is an ordered list separated by commas, where
236      * each rule is a {@code +} or {@code -} symbol indicating if the match
237      * should be accepted or rejected, then followed by a hex value and an
238      * optional hex mask. For example, {@code -caff,+cafe/ff00} is a valid
239      * encoded matcher.
240      *
241      * @see #decode(String)
242      */
encode(@onNull BytesMatcher matcher)243     public static @NonNull String encode(@NonNull BytesMatcher matcher) {
244         final StringBuilder builder = new StringBuilder();
245         final int size = matcher.mRules.size();
246         for (int i = 0; i < size; i++) {
247             final Rule rule = matcher.mRules.get(i);
248             rule.encode(builder);
249             builder.append(',');
250         }
251         if (builder.length() > 0) {
252             builder.deleteCharAt(builder.length() - 1);
253         }
254         return builder.toString();
255     }
256 
257     /**
258      * Decode the given human-readable {@link String} used to transport matchers
259      * across device boundaries.
260      * <p>
261      * The human-readable format is an ordered list separated by commas, where
262      * each rule is a {@code +} or {@code -} symbol indicating if the match
263      * should be accepted or rejected, then followed by a hex value and an
264      * optional hex mask. For example, {@code -caff,+cafe/ff00} is a valid
265      * encoded matcher.
266      *
267      * @see #encode(BytesMatcher)
268      */
decode(@ullable String value)269     public static @NonNull BytesMatcher decode(@Nullable String value) {
270         final BytesMatcher matcher = new BytesMatcher();
271         if (TextUtils.isEmpty(value)) return matcher;
272 
273         final int length = value.length();
274         for (int i = 0; i < length;) {
275             final char type = value.charAt(i);
276 
277             int nextRule = value.indexOf(',', i);
278             int nextMask = value.indexOf('/', i);
279 
280             if (nextRule == -1) nextRule = length;
281             if (nextMask > nextRule) nextMask = -1;
282 
283             final byte[] ruleValue;
284             final byte[] ruleMask;
285             if (nextMask >= 0) {
286                 ruleValue = HexDump.hexStringToByteArray(value.substring(i + 1, nextMask));
287                 ruleMask = HexDump.hexStringToByteArray(value.substring(nextMask + 1, nextRule));
288             } else {
289                 ruleValue = HexDump.hexStringToByteArray(value.substring(i + 1, nextRule));
290                 ruleMask = null;
291             }
292 
293             switch (type) {
294                 case TYPE_EXACT_ACCEPT:
295                     matcher.addExactAcceptRule(ruleValue, ruleMask);
296                     break;
297                 case TYPE_EXACT_REJECT:
298                     matcher.addExactRejectRule(ruleValue, ruleMask);
299                     break;
300                 case TYPE_PREFIX_ACCEPT:
301                     matcher.addPrefixAcceptRule(ruleValue, ruleMask);
302                     break;
303                 case TYPE_PREFIX_REJECT:
304                     matcher.addPrefixRejectRule(ruleValue, ruleMask);
305                     break;
306                 default:
307                     Log.w(TAG, "Ignoring unknown type " + type);
308                     break;
309             }
310 
311             i = nextRule + 1;
312         }
313         return matcher;
314     }
315 }
316