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