revanced-integrations/app/src/main/java/app/revanced/integrations/youtube/patches/components/CustomFilter.java

162 lines
6.3 KiB
Java

package app.revanced.integrations.youtube.patches.components;
import static app.revanced.integrations.shared.StringRef.str;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.ByteTrieSearch;
import app.revanced.integrations.youtube.settings.Settings;
/**
* Allows custom filtering using a path and optionally a proto buffer string.
*/
@SuppressWarnings("unused")
final class CustomFilter extends Filter {
private static void showInvalidSyntaxToast(@NonNull String expression) {
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
}
private static class CustomFilterGroup extends StringFilterGroup {
/**
* Optional character for the path that indicates the custom filter path must match the start.
* Must be the first character of the expression.
*/
public static final String SYNTAX_STARTS_WITH = "^";
/**
* Optional character that separates the path from a proto buffer string pattern.
*/
public static final String SYNTAX_BUFFER_SYMBOL = "$";
/**
* @return the parsed objects, or NULL if there was a parse error.
*/
@Nullable
@SuppressWarnings("ConstantConditions")
static Collection<CustomFilterGroup> parseCustomFilterGroups() {
String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get();
if (rawCustomFilterText.isBlank()) {
return Collections.emptyList();
}
// Map key is the path including optional special characters (^ and/or $)
Map<String, CustomFilterGroup> result = new HashMap<>();
Pattern pattern = Pattern.compile(
"(" // map key group
+ "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" // optional starts with
+ "([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*)" // path
+ "(\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E?)" // optional buffer symbol
+ ")" // end map key group
+ "(.*)"); // optional buffer string
for (String expression : rawCustomFilterText.split("\n")) {
if (expression.isBlank()) continue;
Matcher matcher = pattern.matcher(expression);
if (!matcher.find()) {
showInvalidSyntaxToast(expression);
continue;
}
final String mapKey = matcher.group(1);
final boolean pathStartsWith = !matcher.group(2).isEmpty();
final String path = matcher.group(3);
final boolean hasBufferSymbol = !matcher.group(4).isEmpty();
final String bufferString = matcher.group(5);
if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) {
showInvalidSyntaxToast(expression);
continue;
}
// Use one group object for all expressions with the same path.
// This ensures the buffer is searched exactly once
// when multiple paths are used with different buffer strings.
CustomFilterGroup group = result.get(mapKey);
if (group == null) {
group = new CustomFilterGroup(pathStartsWith, path);
result.put(mapKey, group);
}
if (hasBufferSymbol) {
group.addBufferString(bufferString);
}
}
return result.values();
}
final boolean startsWith;
ByteTrieSearch bufferSearch;
CustomFilterGroup(boolean startsWith, @NonNull String path) {
super(Settings.CUSTOM_FILTER, path);
this.startsWith = startsWith;
}
void addBufferString(@NonNull String bufferString) {
if (bufferSearch == null) {
bufferSearch = new ByteTrieSearch();
}
bufferSearch.addPattern(bufferString.getBytes());
}
@NonNull
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("CustomFilterGroup{");
builder.append("path=");
if (startsWith) builder.append(SYNTAX_STARTS_WITH);
builder.append(filters[0]);
if (bufferSearch != null) {
String delimitingCharacter = "";
builder.append(", bufferStrings=");
builder.append(delimitingCharacter);
for (byte[] bufferString : bufferSearch.getPatterns()) {
builder.append(new String(bufferString));
builder.append(delimitingCharacter);
}
}
builder.append("}");
return builder.toString();
}
}
public CustomFilter() {
Collection<CustomFilterGroup> groups = CustomFilterGroup.parseCustomFilterGroups();
if (!groups.isEmpty()) {
CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]);
Logger.printDebug(()-> "Using Custom filters: " + Arrays.toString(groupsArray));
addPathCallbacks(groupsArray);
}
}
@Override
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// All callbacks are custom filter groups.
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
if (custom.startsWith && contentIndex != 0) {
return false;
}
if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) {
return false;
}
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}