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