2024-01-01 16:33:20 +01:00
|
|
|
package app.revanced.integrations.youtube.patches.components;
|
2023-05-09 04:16:10 +02:00
|
|
|
|
|
|
|
import android.os.Build;
|
|
|
|
import androidx.annotation.NonNull;
|
2023-08-01 19:05:31 +02:00
|
|
|
import androidx.annotation.Nullable;
|
2023-05-09 04:16:10 +02:00
|
|
|
import androidx.annotation.RequiresApi;
|
2023-10-07 11:31:25 +02:00
|
|
|
|
2023-05-09 04:16:10 +02:00
|
|
|
import java.nio.ByteBuffer;
|
2024-01-01 16:33:20 +01:00
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Arrays;
|
|
|
|
import java.util.Iterator;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.Spliterator;
|
2023-05-09 04:16:10 +02:00
|
|
|
import java.util.function.Consumer;
|
|
|
|
|
2024-01-01 16:33:20 +01:00
|
|
|
import app.revanced.integrations.shared.Logger;
|
|
|
|
import app.revanced.integrations.shared.settings.BooleanSetting;
|
|
|
|
import app.revanced.integrations.shared.settings.BaseSettings;
|
|
|
|
import app.revanced.integrations.youtube.ByteTrieSearch;
|
|
|
|
import app.revanced.integrations.youtube.StringTrieSearch;
|
|
|
|
import app.revanced.integrations.youtube.TrieSearch;
|
|
|
|
import app.revanced.integrations.youtube.settings.Settings;
|
|
|
|
|
2023-05-09 04:16:10 +02:00
|
|
|
abstract class FilterGroup<T> {
|
|
|
|
final static class FilterGroupResult {
|
2024-01-01 16:33:20 +01:00
|
|
|
private BooleanSetting setting;
|
2023-10-07 11:31:25 +02:00
|
|
|
private int matchedIndex;
|
|
|
|
private int matchedLength;
|
|
|
|
// In the future it might be useful to include which pattern matched,
|
|
|
|
// but for now that is not needed.
|
|
|
|
|
|
|
|
FilterGroupResult() {
|
|
|
|
this(null, -1, 0);
|
|
|
|
}
|
2023-05-09 04:16:10 +02:00
|
|
|
|
2024-01-01 16:33:20 +01:00
|
|
|
FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
|
2023-10-07 11:31:25 +02:00
|
|
|
setValues(setting, matchedIndex, matchedLength);
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:33:20 +01:00
|
|
|
public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
|
2023-05-09 04:16:10 +02:00
|
|
|
this.setting = setting;
|
2023-10-07 11:31:25 +02:00
|
|
|
this.matchedIndex = matchedIndex;
|
|
|
|
this.matchedLength = matchedLength;
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
/**
|
|
|
|
* A null value if the group has no setting,
|
|
|
|
* or if no match is returned from {@link FilterGroupList#check(Object)}.
|
|
|
|
*/
|
2024-01-01 16:33:20 +01:00
|
|
|
public BooleanSetting getSetting() {
|
2023-05-09 04:16:10 +02:00
|
|
|
return setting;
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isFiltered() {
|
2023-10-07 11:31:25 +02:00
|
|
|
return matchedIndex >= 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Matched index of first pattern that matched, or -1 if nothing matched.
|
|
|
|
*/
|
|
|
|
public int getMatchedIndex() {
|
|
|
|
return matchedIndex;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Length of the matched filter pattern.
|
|
|
|
*/
|
|
|
|
public int getMatchedLength() {
|
|
|
|
return matchedLength;
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:33:20 +01:00
|
|
|
protected final BooleanSetting setting;
|
2023-05-09 04:16:10 +02:00
|
|
|
protected final T[] filters;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize a new filter group.
|
|
|
|
*
|
|
|
|
* @param setting The associated setting.
|
2023-05-10 01:04:38 +02:00
|
|
|
* @param filters The filters.
|
2023-05-09 04:16:10 +02:00
|
|
|
*/
|
|
|
|
@SafeVarargs
|
2024-01-01 16:33:20 +01:00
|
|
|
public FilterGroup(final BooleanSetting setting, final T... filters) {
|
2023-05-09 04:16:10 +02:00
|
|
|
this.setting = setting;
|
|
|
|
this.filters = filters;
|
2023-08-01 19:05:31 +02:00
|
|
|
if (filters.length == 0) {
|
|
|
|
throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
|
|
|
|
}
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isEnabled() {
|
2024-01-01 16:33:20 +01:00
|
|
|
return setting == null || setting.get();
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
/**
|
|
|
|
* @return If {@link FilterGroupList} should include this group when searching.
|
|
|
|
* By default, all filters are included except non enabled settings that require reboot.
|
|
|
|
*/
|
|
|
|
public boolean includeInSearch() {
|
|
|
|
return isEnabled() || !setting.rebootApp;
|
|
|
|
}
|
|
|
|
|
|
|
|
@NonNull
|
|
|
|
@Override
|
|
|
|
public String toString() {
|
|
|
|
return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
|
|
|
|
}
|
|
|
|
|
2023-05-10 01:04:38 +02:00
|
|
|
public abstract FilterGroupResult check(final T stack);
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
class StringFilterGroup extends FilterGroup<String> {
|
|
|
|
|
2024-01-01 16:33:20 +01:00
|
|
|
public StringFilterGroup(final BooleanSetting setting, final String... filters) {
|
2023-05-09 04:16:10 +02:00
|
|
|
super(setting, filters);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2023-05-10 01:04:38 +02:00
|
|
|
public FilterGroupResult check(final String string) {
|
2023-10-07 11:31:25 +02:00
|
|
|
int matchedIndex = -1;
|
|
|
|
int matchedLength = 0;
|
|
|
|
if (isEnabled()) {
|
|
|
|
for (String pattern : filters) {
|
|
|
|
if (!string.isEmpty()) {
|
2024-03-26 04:47:21 +01:00
|
|
|
final int indexOf = string.indexOf(pattern);
|
2023-10-07 11:31:25 +02:00
|
|
|
if (indexOf >= 0) {
|
|
|
|
matchedIndex = indexOf;
|
|
|
|
matchedLength = pattern.length();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return new FilterGroupResult(setting, matchedIndex, matchedLength);
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
/**
|
|
|
|
* If you have more than 1 filter patterns, then all instances of
|
|
|
|
* this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])},
|
|
|
|
* which uses a prefix tree to give better performance.
|
|
|
|
*/
|
2023-05-10 01:04:38 +02:00
|
|
|
class ByteArrayFilterGroup extends FilterGroup<byte[]> {
|
2023-05-09 04:16:10 +02:00
|
|
|
|
2023-08-02 13:04:29 +02:00
|
|
|
private volatile int[][] failurePatterns;
|
2023-05-09 04:16:10 +02:00
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
// Modified implementation from https://stackoverflow.com/a/1507813
|
|
|
|
private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) {
|
2023-05-09 04:16:10 +02:00
|
|
|
// Finds the first occurrence of the pattern in the byte array using
|
2023-05-10 01:04:38 +02:00
|
|
|
// KMP matching algorithm.
|
2023-08-01 19:05:31 +02:00
|
|
|
int patternLength = pattern.length;
|
|
|
|
for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) {
|
2023-05-09 04:16:10 +02:00
|
|
|
while (j > 0 && pattern[j] != data[i]) {
|
|
|
|
j = failure[j - 1];
|
|
|
|
}
|
|
|
|
if (pattern[j] == data[i]) {
|
|
|
|
j++;
|
|
|
|
}
|
2023-08-01 19:05:31 +02:00
|
|
|
if (j == patternLength) {
|
|
|
|
return i - patternLength + 1;
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
private static int[] createFailurePattern(byte[] pattern) {
|
|
|
|
// Computes the failure function using a boot-strapping process,
|
|
|
|
// where the pattern is matched against itself.
|
|
|
|
final int patternLength = pattern.length;
|
|
|
|
final int[] failure = new int[patternLength];
|
|
|
|
|
|
|
|
for (int i = 1, j = 0; i < patternLength; i++) {
|
|
|
|
while (j > 0 && pattern[j] != pattern[i]) {
|
|
|
|
j = failure[j - 1];
|
|
|
|
}
|
|
|
|
if (pattern[j] == pattern[i]) {
|
|
|
|
j++;
|
|
|
|
}
|
|
|
|
failure[i] = j;
|
|
|
|
}
|
|
|
|
return failure;
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:33:20 +01:00
|
|
|
public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) {
|
2023-05-09 04:16:10 +02:00
|
|
|
super(setting, filters);
|
|
|
|
}
|
|
|
|
|
2023-12-12 23:40:28 +01:00
|
|
|
/**
|
|
|
|
* Converts the Strings into byte arrays. Used to search for text in binary data.
|
|
|
|
*/
|
|
|
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
2024-01-01 16:33:20 +01:00
|
|
|
public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
|
2023-12-12 23:40:28 +01:00
|
|
|
super(setting, Arrays.stream(filters).map(String::getBytes).toArray(byte[][]::new));
|
|
|
|
}
|
|
|
|
|
2023-08-02 13:04:29 +02:00
|
|
|
private synchronized void buildFailurePatterns() {
|
|
|
|
if (failurePatterns != null) return; // Thread race and another thread already initialized the search.
|
2024-01-01 16:33:20 +01:00
|
|
|
Logger.printDebug(() -> "Building failure array for: " + this);
|
2023-08-02 13:04:29 +02:00
|
|
|
int[][] failurePatterns = new int[filters.length][];
|
2023-08-01 19:05:31 +02:00
|
|
|
int i = 0;
|
|
|
|
for (byte[] pattern : filters) {
|
|
|
|
failurePatterns[i++] = createFailurePattern(pattern);
|
|
|
|
}
|
2023-08-02 13:04:29 +02:00
|
|
|
this.failurePatterns = failurePatterns; // Must set after initialization finishes.
|
2023-08-01 19:05:31 +02:00
|
|
|
}
|
|
|
|
|
2023-05-09 04:16:10 +02:00
|
|
|
@Override
|
2023-05-10 01:04:38 +02:00
|
|
|
public FilterGroupResult check(final byte[] bytes) {
|
2023-10-07 11:31:25 +02:00
|
|
|
int matchedLength = 0;
|
|
|
|
int matchedIndex = -1;
|
2023-08-01 19:05:31 +02:00
|
|
|
if (isEnabled()) {
|
2023-12-12 23:40:28 +01:00
|
|
|
int[][] failures = failurePatterns;
|
|
|
|
if (failures == null) {
|
2023-08-01 19:05:31 +02:00
|
|
|
buildFailurePatterns(); // Lazy load.
|
2023-12-12 23:40:28 +01:00
|
|
|
failures = failurePatterns;
|
2023-08-01 19:05:31 +02:00
|
|
|
}
|
|
|
|
for (int i = 0, length = filters.length; i < length; i++) {
|
2023-10-07 11:31:25 +02:00
|
|
|
byte[] filter = filters[i];
|
2023-12-12 23:40:28 +01:00
|
|
|
matchedIndex = indexOf(bytes, filter, failures[i]);
|
2023-10-07 11:31:25 +02:00
|
|
|
if (matchedIndex >= 0) {
|
|
|
|
matchedLength = filter.length;
|
2023-08-01 19:05:31 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
2023-10-07 11:31:25 +02:00
|
|
|
return new FilterGroupResult(setting, matchedIndex, matchedLength);
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
|
2023-05-09 04:16:10 +02:00
|
|
|
abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
|
2023-08-01 19:05:31 +02:00
|
|
|
|
|
|
|
private final List<T> filterGroups = new ArrayList<>();
|
2023-12-12 23:40:28 +01:00
|
|
|
private final TrieSearch<V> search = createSearchGraph();
|
2023-05-09 04:16:10 +02:00
|
|
|
|
|
|
|
@SafeVarargs
|
2023-08-01 19:05:31 +02:00
|
|
|
protected final void addAll(final T... groups) {
|
|
|
|
filterGroups.addAll(Arrays.asList(groups));
|
|
|
|
|
2023-12-12 23:40:28 +01:00
|
|
|
for (T group : groups) {
|
2023-08-01 19:05:31 +02:00
|
|
|
if (!group.includeInSearch()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
for (V pattern : group.filters) {
|
2023-10-07 11:31:25 +02:00
|
|
|
search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
|
2023-08-01 19:05:31 +02:00
|
|
|
if (group.isEnabled()) {
|
|
|
|
FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
|
2023-10-07 11:31:25 +02:00
|
|
|
result.setValues(group.setting, matchedStartIndex, matchedLength);
|
2023-08-01 19:05:31 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@NonNull
|
|
|
|
@Override
|
|
|
|
public Iterator<T> iterator() {
|
|
|
|
return filterGroups.iterator();
|
|
|
|
}
|
|
|
|
|
|
|
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
|
|
@Override
|
|
|
|
public void forEach(@NonNull Consumer<? super T> action) {
|
|
|
|
filterGroups.forEach(action);
|
|
|
|
}
|
|
|
|
|
|
|
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
|
|
@NonNull
|
|
|
|
@Override
|
|
|
|
public Spliterator<T> spliterator() {
|
|
|
|
return filterGroups.spliterator();
|
|
|
|
}
|
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
protected FilterGroup.FilterGroupResult check(V stack) {
|
2023-10-07 11:31:25 +02:00
|
|
|
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
|
2023-08-01 19:05:31 +02:00
|
|
|
search.matches(stack, result);
|
|
|
|
return result;
|
2023-10-07 11:31:25 +02:00
|
|
|
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
2023-08-01 19:05:31 +02:00
|
|
|
|
|
|
|
protected abstract TrieSearch<V> createSearchGraph();
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
|
2023-08-01 19:05:31 +02:00
|
|
|
protected StringTrieSearch createSearchGraph() {
|
|
|
|
return new StringTrieSearch();
|
|
|
|
}
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
/**
|
|
|
|
* If searching for a single byte pattern, then it is slightly better to use
|
|
|
|
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
|
|
|
|
* than a prefix tree to search for only 1 pattern.
|
|
|
|
*/
|
2023-05-09 04:16:10 +02:00
|
|
|
final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
|
2023-08-01 19:05:31 +02:00
|
|
|
protected ByteTrieSearch createSearchGraph() {
|
|
|
|
return new ByteTrieSearch();
|
|
|
|
}
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
|
2023-12-12 23:40:28 +01:00
|
|
|
/**
|
|
|
|
* Filters litho based components.
|
|
|
|
*
|
|
|
|
* Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
|
|
|
|
* and {@link #addPathCallbacks(StringFilterGroup...)}.
|
|
|
|
*
|
|
|
|
* To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to
|
|
|
|
* either an identifier or a path.
|
|
|
|
* Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
|
|
|
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
|
|
|
|
* or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern).
|
|
|
|
*
|
|
|
|
* All callbacks must be registered before the constructor completes.
|
|
|
|
*/
|
2023-05-09 04:16:10 +02:00
|
|
|
abstract class Filter {
|
2023-12-12 23:40:28 +01:00
|
|
|
|
|
|
|
public enum FilterContentType {
|
|
|
|
IDENTIFIER,
|
|
|
|
PATH,
|
|
|
|
PROTOBUFFER
|
|
|
|
}
|
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
/**
|
2023-12-12 23:40:28 +01:00
|
|
|
* Identifier callbacks. Do not add to this instance,
|
|
|
|
* and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}.
|
2023-08-01 19:05:31 +02:00
|
|
|
*/
|
2023-12-12 23:40:28 +01:00
|
|
|
protected final List<StringFilterGroup> identifierCallbacks = new ArrayList<>();
|
|
|
|
/**
|
|
|
|
* Path callbacks. Do not add to this instance,
|
|
|
|
* and instead use {@link #addPathCallbacks(StringFilterGroup...)}.
|
|
|
|
*/
|
|
|
|
protected final List<StringFilterGroup> pathCallbacks = new ArrayList<>();
|
2023-05-09 04:16:10 +02:00
|
|
|
|
2023-12-12 23:40:28 +01:00
|
|
|
/**
|
|
|
|
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
|
|
|
* if any of the groups are found.
|
|
|
|
*/
|
|
|
|
protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
|
|
|
|
identifierCallbacks.addAll(Arrays.asList(groups));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
|
|
|
* if any of the groups are found.
|
|
|
|
*/
|
|
|
|
protected final void addPathCallbacks(StringFilterGroup... groups) {
|
|
|
|
pathCallbacks.addAll(Arrays.asList(groups));
|
|
|
|
}
|
2023-05-09 04:16:10 +02:00
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
/**
|
|
|
|
* Called after an enabled filter has been matched.
|
2023-12-12 23:40:28 +01:00
|
|
|
* Default implementation is to always filter the matched component and log the action.
|
2023-08-01 19:05:31 +02:00
|
|
|
* Subclasses can perform additional or different checks if needed.
|
2023-08-27 21:40:07 +02:00
|
|
|
* <p>
|
2023-12-12 23:40:28 +01:00
|
|
|
* If the content is to be filtered, subclasses should always
|
|
|
|
* call this method (and never return a plain 'true').
|
|
|
|
* That way the logs will always show when a component was filtered and which filter hide it.
|
|
|
|
* <p>
|
2023-08-01 19:05:31 +02:00
|
|
|
* Method is called off the main thread.
|
|
|
|
*
|
|
|
|
* @param matchedGroup The actual filter that matched.
|
2023-12-12 23:40:28 +01:00
|
|
|
* @param contentType The type of content matched.
|
|
|
|
* @param contentIndex Matched index of the identifier or path.
|
|
|
|
* @return True if the litho component should be filtered out.
|
2023-08-01 19:05:31 +02:00
|
|
|
*/
|
2023-08-27 21:40:07 +02:00
|
|
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
2023-12-12 23:40:28 +01:00
|
|
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
2024-01-01 16:33:20 +01:00
|
|
|
if (BaseSettings.DEBUG.get()) {
|
2023-12-12 23:40:28 +01:00
|
|
|
String filterSimpleName = getClass().getSimpleName();
|
|
|
|
if (contentType == FilterContentType.IDENTIFIER) {
|
2024-01-01 16:33:20 +01:00
|
|
|
Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier);
|
2023-09-28 01:28:34 +02:00
|
|
|
} else {
|
2024-01-01 16:33:20 +01:00
|
|
|
Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path);
|
2023-08-01 19:05:31 +02:00
|
|
|
}
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
2023-08-01 19:05:31 +02:00
|
|
|
return true;
|
2023-05-09 04:16:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-12 23:40:28 +01:00
|
|
|
/**
|
|
|
|
* Placeholder for actual filters.
|
|
|
|
*/
|
|
|
|
final class DummyFilter extends Filter { }
|
|
|
|
|
2023-05-10 01:04:38 +02:00
|
|
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
2023-05-09 04:16:10 +02:00
|
|
|
@SuppressWarnings("unused")
|
|
|
|
public final class LithoFilterPatch {
|
2023-07-08 01:02:42 +02:00
|
|
|
/**
|
2023-08-01 19:05:31 +02:00
|
|
|
* Simple wrapper to pass the litho parameters through the prefix search.
|
2023-07-08 01:02:42 +02:00
|
|
|
*/
|
2023-08-01 19:05:31 +02:00
|
|
|
private static final class LithoFilterParameters {
|
2023-08-27 21:40:07 +02:00
|
|
|
@Nullable
|
2023-08-27 04:43:10 +02:00
|
|
|
final String identifier;
|
2023-08-27 21:40:07 +02:00
|
|
|
final String path;
|
2023-08-01 19:05:31 +02:00
|
|
|
final byte[] protoBuffer;
|
|
|
|
|
2024-02-07 12:23:40 +01:00
|
|
|
LithoFilterParameters(@Nullable String lithoIdentifier, String lithoPath, byte[] protoBuffer) {
|
2023-08-27 04:43:10 +02:00
|
|
|
this.identifier = lithoIdentifier;
|
2024-02-07 12:23:40 +01:00
|
|
|
this.path = lithoPath;
|
|
|
|
this.protoBuffer = protoBuffer;
|
2023-08-01 19:05:31 +02:00
|
|
|
}
|
2023-06-14 03:30:14 +02:00
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
@NonNull
|
|
|
|
@Override
|
|
|
|
public String toString() {
|
|
|
|
// Estimate the percentage of the buffer that are Strings.
|
2024-02-07 12:23:40 +01:00
|
|
|
StringBuilder builder = new StringBuilder(Math.max(100, protoBuffer.length / 2));
|
2023-08-01 19:05:31 +02:00
|
|
|
builder.append( "ID: ");
|
|
|
|
builder.append(identifier);
|
|
|
|
builder.append(" Path: ");
|
|
|
|
builder.append(path);
|
2024-01-01 16:33:20 +01:00
|
|
|
if (Settings.DEBUG_PROTOBUFFER.get()) {
|
2023-08-27 21:40:07 +02:00
|
|
|
builder.append(" BufferStrings: ");
|
|
|
|
findAsciiStrings(builder, protoBuffer);
|
|
|
|
}
|
2023-08-01 19:05:31 +02:00
|
|
|
|
|
|
|
return builder.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search through a byte array for all ASCII strings.
|
|
|
|
*/
|
|
|
|
private static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
|
|
|
|
// Valid ASCII values (ignore control characters).
|
|
|
|
final int minimumAscii = 32; // 32 = space character
|
|
|
|
final int maximumAscii = 126; // 127 = delete character
|
|
|
|
final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
|
|
|
|
String delimitingCharacter = "❙"; // Non ascii character, to allow easier log filtering.
|
|
|
|
|
|
|
|
final int length = buffer.length;
|
|
|
|
int start = 0;
|
|
|
|
int end = 0;
|
|
|
|
while (end < length) {
|
|
|
|
int value = buffer[end];
|
|
|
|
if (value < minimumAscii || value > maximumAscii || end == length - 1) {
|
|
|
|
if (end - start >= minimumAsciiStringLength) {
|
2023-08-27 21:40:07 +02:00
|
|
|
for (int i = start; i < end; i++) {
|
|
|
|
builder.append((char) buffer[i]);
|
|
|
|
}
|
2023-08-01 19:05:31 +02:00
|
|
|
builder.append(delimitingCharacter);
|
|
|
|
}
|
|
|
|
start = end + 1;
|
|
|
|
}
|
|
|
|
end++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static final Filter[] filters = new Filter[] {
|
|
|
|
new DummyFilter() // Replaced by patch.
|
|
|
|
};
|
2023-05-09 04:16:10 +02:00
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
|
|
|
|
private static final StringTrieSearch identifierSearchTree = new StringTrieSearch();
|
2023-08-27 21:40:07 +02:00
|
|
|
|
2024-02-07 12:23:40 +01:00
|
|
|
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
|
|
|
|
2023-08-27 21:40:07 +02:00
|
|
|
/**
|
|
|
|
* Because litho filtering is multi-threaded and the buffer is passed in from a different injection point,
|
|
|
|
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
|
|
|
|
*/
|
|
|
|
private static final ThreadLocal<ByteBuffer> bufferThreadLocal = new ThreadLocal<>();
|
2023-05-09 04:16:10 +02:00
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
static {
|
|
|
|
for (Filter filter : filters) {
|
2023-12-12 23:40:28 +01:00
|
|
|
filterUsingCallbacks(identifierSearchTree, filter,
|
|
|
|
filter.identifierCallbacks, Filter.FilterContentType.IDENTIFIER);
|
|
|
|
filterUsingCallbacks(pathSearchTree, filter,
|
|
|
|
filter.pathCallbacks, Filter.FilterContentType.PATH);
|
2023-08-01 19:05:31 +02:00
|
|
|
}
|
2023-05-09 04:16:10 +02:00
|
|
|
|
2024-01-01 16:33:20 +01:00
|
|
|
Logger.printDebug(() -> "Using: "
|
2023-08-01 19:05:31 +02:00
|
|
|
+ identifierSearchTree.numberOfPatterns() + " identifier filters"
|
2023-10-17 12:08:35 +02:00
|
|
|
+ " (" + identifierSearchTree.getEstimatedMemorySize() + " KB), "
|
|
|
|
+ pathSearchTree.numberOfPatterns() + " path filters"
|
|
|
|
+ " (" + pathSearchTree.getEstimatedMemorySize() + " KB)");
|
2023-08-01 19:05:31 +02:00
|
|
|
}
|
2023-06-14 03:30:14 +02:00
|
|
|
|
2023-12-12 23:40:28 +01:00
|
|
|
private static void filterUsingCallbacks(StringTrieSearch pathSearchTree,
|
|
|
|
Filter filter, List<StringFilterGroup> groups,
|
|
|
|
Filter.FilterContentType type) {
|
|
|
|
for (StringFilterGroup group : groups) {
|
2023-08-01 19:05:31 +02:00
|
|
|
if (!group.includeInSearch()) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-12-12 23:40:28 +01:00
|
|
|
for (String pattern : group.filters) {
|
2023-10-07 11:31:25 +02:00
|
|
|
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
|
2023-08-01 19:05:31 +02:00
|
|
|
if (!group.isEnabled()) return false;
|
|
|
|
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
|
2023-08-27 21:40:07 +02:00
|
|
|
return filter.isFiltered(parameters.identifier, parameters.path, parameters.protoBuffer,
|
2023-12-12 23:40:28 +01:00
|
|
|
group, type, matchedStartIndex);
|
2023-08-01 19:05:31 +02:00
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-06-14 03:30:14 +02:00
|
|
|
|
2023-08-01 19:05:31 +02:00
|
|
|
/**
|
|
|
|
* Injection point. Called off the main thread.
|
|
|
|
*/
|
|
|
|
@SuppressWarnings("unused")
|
2024-02-07 16:25:46 +01:00
|
|
|
public static void setProtoBuffer(@Nullable ByteBuffer protobufBuffer) {
|
2023-08-27 21:40:07 +02:00
|
|
|
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
|
|
|
|
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
|
|
|
|
// The buffer will be cleared from memory after a new buffer is set by the same thread,
|
|
|
|
// or when the calling thread eventually dies.
|
2024-02-07 16:25:46 +01:00
|
|
|
if (protobufBuffer == null) {
|
|
|
|
// It appears the buffer can be cleared out just before the call to #filter()
|
|
|
|
// Ignore this null value and retain the last buffer that was set.
|
|
|
|
Logger.printDebug(() -> "Ignoring null protobuffer");
|
|
|
|
} else {
|
|
|
|
bufferThreadLocal.set(protobufBuffer);
|
|
|
|
}
|
2023-08-27 21:40:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Injection point. Called off the main thread, and commonly called by multiple threads at the same time.
|
|
|
|
*/
|
|
|
|
@SuppressWarnings("unused")
|
|
|
|
public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) {
|
2023-08-01 19:05:31 +02:00
|
|
|
try {
|
|
|
|
// It is assumed that protobufBuffer is empty as well in this case.
|
|
|
|
if (pathBuilder.length() == 0)
|
|
|
|
return false;
|
|
|
|
|
2023-08-27 21:40:07 +02:00
|
|
|
ByteBuffer protobufBuffer = bufferThreadLocal.get();
|
2024-02-07 12:23:40 +01:00
|
|
|
final byte[] bufferArray;
|
2024-02-07 16:25:46 +01:00
|
|
|
// Potentially the buffer may have been null or never set up until now.
|
|
|
|
// Use an empty buffer so the litho id/path filters still work correctly.
|
2023-11-18 22:14:08 +01:00
|
|
|
if (protobufBuffer == null) {
|
2024-02-07 16:25:46 +01:00
|
|
|
Logger.printDebug(() -> "Proto buffer is null, using an empty buffer array");
|
2024-02-07 12:23:40 +01:00
|
|
|
bufferArray = EMPTY_BYTE_ARRAY;
|
|
|
|
} else if (!protobufBuffer.hasArray()) {
|
2024-02-07 16:25:46 +01:00
|
|
|
Logger.printDebug(() -> "Proto buffer does not have an array, using an empty buffer array");
|
2024-02-07 12:23:40 +01:00
|
|
|
bufferArray = EMPTY_BYTE_ARRAY;
|
|
|
|
} else {
|
|
|
|
bufferArray = protobufBuffer.array();
|
2023-08-27 21:40:07 +02:00
|
|
|
}
|
2023-11-18 19:18:13 +01:00
|
|
|
|
2024-02-07 12:23:40 +01:00
|
|
|
LithoFilterParameters parameter = new LithoFilterParameters(lithoIdentifier,
|
|
|
|
pathBuilder.toString(), bufferArray);
|
2024-01-01 16:33:20 +01:00
|
|
|
Logger.printDebug(() -> "Searching " + parameter);
|
2023-08-01 19:05:31 +02:00
|
|
|
|
|
|
|
if (parameter.identifier != null) {
|
|
|
|
if (identifierSearchTree.matches(parameter.identifier, parameter)) return true;
|
|
|
|
}
|
2023-08-27 21:40:07 +02:00
|
|
|
if (pathSearchTree.matches(parameter.path, parameter)) return true;
|
2023-08-01 19:05:31 +02:00
|
|
|
} catch (Exception ex) {
|
2024-01-01 16:33:20 +01:00
|
|
|
Logger.printException(() -> "Litho filter failure", ex);
|
2023-06-14 03:30:14 +02:00
|
|
|
}
|
2023-05-09 04:16:10 +02:00
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
2023-06-14 03:30:14 +02:00
|
|
|
}
|