297 lines
11 KiB
Java
297 lines
11 KiB
Java
|
/*
|
||
|
* 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 com.android.modules.utils;
|
||
|
|
||
|
import android.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.text.TextUtils;
|
||
|
import android.util.Log;
|
||
|
|
||
|
import libcore.util.HexEncoding;
|
||
|
|
||
|
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.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public class BytesMatcher implements Predicate<byte[]> {
|
||
|
private static final String TAG = "BytesMatcher";
|
||
|
|
||
|
private static final char TYPE_EXACT_ACCEPT = '+';
|
||
|
private static final char TYPE_EXACT_REJECT = '-';
|
||
|
private static final char TYPE_PREFIX_ACCEPT = '⊆';
|
||
|
private static final char TYPE_PREFIX_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(this.type);
|
||
|
builder.append(HexEncoding.encodeToString(this.value));
|
||
|
if (this.mask != null) {
|
||
|
builder.append('/');
|
||
|
builder.append(HexEncoding.encodeToString(this.mask));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public boolean test(@NonNull byte[] value) {
|
||
|
switch (type) {
|
||
|
case TYPE_EXACT_ACCEPT:
|
||
|
case TYPE_EXACT_REJECT:
|
||
|
if (value.length != this.value.length) {
|
||
|
return false;
|
||
|
}
|
||
|
break;
|
||
|
case TYPE_PREFIX_ACCEPT:
|
||
|
case TYPE_PREFIX_REJECT:
|
||
|
if (value.length < this.value.length) {
|
||
|
return false;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
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. This rule will only
|
||
|
* match values of the exact same length.
|
||
|
* <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 addExactAcceptRule(@NonNull byte[] value, @Nullable byte[] mask) {
|
||
|
mRules.add(new Rule(TYPE_EXACT_ACCEPT, value, mask));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add a rule that will result in {@link #test(byte[])} returning
|
||
|
* {@code false} when a value being tested matches it. This rule will only
|
||
|
* match values of the exact same length.
|
||
|
* <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 addExactRejectRule(@NonNull byte[] value, @Nullable byte[] mask) {
|
||
|
mRules.add(new Rule(TYPE_EXACT_REJECT, value, mask));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add a rule that will result in {@link #test(byte[])} returning
|
||
|
* {@code true} when a value being tested matches it. This rule will match
|
||
|
* values of the exact same length or longer.
|
||
|
* <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 addPrefixAcceptRule(@NonNull byte[] value, @Nullable byte[] mask) {
|
||
|
mRules.add(new Rule(TYPE_PREFIX_ACCEPT, value, mask));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add a rule that will result in {@link #test(byte[])} returning
|
||
|
* {@code false} when a value being tested matches it. This rule will match
|
||
|
* values of the exact same length or longer.
|
||
|
* <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 addPrefixRejectRule(@NonNull byte[] value, @Nullable byte[] mask) {
|
||
|
mRules.add(new Rule(TYPE_PREFIX_REJECT, value, mask));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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)) {
|
||
|
switch (rule.type) {
|
||
|
case TYPE_EXACT_ACCEPT:
|
||
|
case TYPE_PREFIX_ACCEPT:
|
||
|
return true;
|
||
|
case TYPE_EXACT_REJECT:
|
||
|
case TYPE_PREFIX_REJECT:
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
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(',');
|
||
|
}
|
||
|
if (builder.length() > 0) {
|
||
|
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(@Nullable String value) {
|
||
|
final BytesMatcher matcher = new BytesMatcher();
|
||
|
if (TextUtils.isEmpty(value)) return matcher;
|
||
|
|
||
|
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 = HexEncoding.decode(value.substring(i + 1, nextMask));
|
||
|
ruleMask = HexEncoding.decode(value.substring(nextMask + 1, nextRule));
|
||
|
} else {
|
||
|
ruleValue = HexEncoding.decode(value.substring(i + 1, nextRule));
|
||
|
ruleMask = null;
|
||
|
}
|
||
|
|
||
|
switch (type) {
|
||
|
case TYPE_EXACT_ACCEPT:
|
||
|
matcher.addExactAcceptRule(ruleValue, ruleMask);
|
||
|
break;
|
||
|
case TYPE_EXACT_REJECT:
|
||
|
matcher.addExactRejectRule(ruleValue, ruleMask);
|
||
|
break;
|
||
|
case TYPE_PREFIX_ACCEPT:
|
||
|
matcher.addPrefixAcceptRule(ruleValue, ruleMask);
|
||
|
break;
|
||
|
case TYPE_PREFIX_REJECT:
|
||
|
matcher.addPrefixRejectRule(ruleValue, ruleMask);
|
||
|
break;
|
||
|
default:
|
||
|
Log.w(TAG, "Ignoring unknown type " + type);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
i = nextRule + 1;
|
||
|
}
|
||
|
return matcher;
|
||
|
}
|
||
|
}
|