Browse Source

initial commit

tags/2.0.1
Jonathan Cobb 4 years ago
commit
662d8da818
24 changed files with 1292 additions and 0 deletions
  1. +9
    -0
      .gitignore
  2. +53
    -0
      pom.xml
  3. +29
    -0
      src/main/java/bubble/abp/spec/BlockDecision.java
  4. +11
    -0
      src/main/java/bubble/abp/spec/BlockDecisionType.java
  5. +57
    -0
      src/main/java/bubble/abp/spec/BlockList.java
  6. +55
    -0
      src/main/java/bubble/abp/spec/BlockListSource.java
  7. +179
    -0
      src/main/java/bubble/abp/spec/BlockSpec.java
  8. +102
    -0
      src/main/java/bubble/abp/spec/BlockSpecTarget.java
  9. +48
    -0
      src/main/java/bubble/abp/spec/selector/AbpClause.java
  10. +7
    -0
      src/main/java/bubble/abp/spec/selector/AbpClauseParseState.java
  11. +19
    -0
      src/main/java/bubble/abp/spec/selector/AbpClauseType.java
  12. +27
    -0
      src/main/java/bubble/abp/spec/selector/AbpContains.java
  13. +11
    -0
      src/main/java/bubble/abp/spec/selector/AbpContainsType.java
  14. +48
    -0
      src/main/java/bubble/abp/spec/selector/AbpProperty.java
  15. +11
    -0
      src/main/java/bubble/abp/spec/selector/AbpPropertyType.java
  16. +165
    -0
      src/main/java/bubble/abp/spec/selector/BlockSelector.java
  17. +71
    -0
      src/main/java/bubble/abp/spec/selector/SelectorAttribute.java
  18. +11
    -0
      src/main/java/bubble/abp/spec/selector/SelectorAttributeComparison.java
  19. +7
    -0
      src/main/java/bubble/abp/spec/selector/SelectorAttributeParseState.java
  20. +35
    -0
      src/main/java/bubble/abp/spec/selector/SelectorOperator.java
  21. +9
    -0
      src/main/java/bubble/abp/spec/selector/SelectorParseError.java
  22. +36
    -0
      src/main/java/bubble/abp/spec/selector/SelectorType.java
  23. +157
    -0
      src/test/java/bubble/abp/spec/BlockListTest.java
  24. +135
    -0
      src/test/java/bubble/abp/spec/SelectorTest.java

+ 9
- 0
.gitignore View File

@@ -0,0 +1,9 @@
*.iml
.idea
target
tmp
logs
dependency-reduced-pom.xml
velocity.log
*~
build.log

+ 53
- 0
pom.xml View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>

<!--
(c) Copyright 2019 Jonathan Cobb
This code is available under the Apache License, version 2: http://www.apache.org/licenses/LICENSE-2.0.html
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>bubble</groupId>
<artifactId>abp-parser</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>

<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>

<dependencies>
<dependency>
<groupId>org.cobbzilla</groupId>
<artifactId>cobbzilla-utils</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>11</source>
<target>11</target>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
</plugins>
</build>

</project>

+ 29
- 0
src/main/java/bubble/abp/spec/BlockDecision.java View File

@@ -0,0 +1,29 @@
package bubble.abp.spec;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;

import java.util.ArrayList;
import java.util.List;

@NoArgsConstructor @Accessors(chain=true)
public class BlockDecision {

public static final BlockDecision BLOCK = new BlockDecision().setDecisionType(BlockDecisionType.block);
public static final BlockDecision ALLOW = new BlockDecision().setDecisionType(BlockDecisionType.allow);

@Getter @Setter BlockDecisionType decisionType = BlockDecisionType.allow;
@Getter @Setter List<BlockSpec> specs;

public BlockDecision add(BlockSpec spec) {
if (specs == null) specs = new ArrayList<>();
specs.add(spec);
if (decisionType != BlockDecisionType.block && (spec.hasTypeMatches() || spec.hasSelector())) {
decisionType = BlockDecisionType.filter;
}
return this;
}

}

+ 11
- 0
src/main/java/bubble/abp/spec/BlockDecisionType.java View File

@@ -0,0 +1,11 @@
package bubble.abp.spec;

import com.fasterxml.jackson.annotation.JsonCreator;

public enum BlockDecisionType {

block, allow, filter;

@JsonCreator public static BlockDecisionType fromString (String v) { return valueOf(v.toLowerCase()); }

}

+ 57
- 0
src/main/java/bubble/abp/spec/BlockList.java View File

@@ -0,0 +1,57 @@
package bubble.abp.spec;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;

import java.util.*;

@NoArgsConstructor @Accessors(chain=true) @Slf4j
public class BlockList {

@Getter private Set<BlockSpec> blacklist = new HashSet<>();
@Getter private Set<BlockSpec> whitelist = new HashSet<>();

public void addToWhitelist(BlockSpec spec) {
whitelist.add(spec);
}

public void addToWhitelist(List<BlockSpec> specs) {
for (BlockSpec spec : specs) addToWhitelist(spec);
}

public void addToBlacklist(BlockSpec spec) {
blacklist.add(spec);
}

public void addToBlacklist(List<BlockSpec> specs) {
for (BlockSpec spec : specs) addToBlacklist(spec);
}

public void merge(BlockList other) {
for (BlockSpec allow : other.getWhitelist()) {
addToWhitelist(allow);
}
for (BlockSpec block : other.getBlacklist()) {
addToBlacklist(block);
}
}

public BlockDecision getDecision(String fqdn, String path) {
for (BlockSpec allow : whitelist) {
if (allow.matches(fqdn, path)) return BlockDecision.ALLOW;
}
final BlockDecision decision = new BlockDecision();
for (BlockSpec block : blacklist) {
if (block.matches(fqdn, path)) {
if (!block.hasSelector()) return BlockDecision.BLOCK;
decision.add(block);
} else if (block.hasSelector()) {
decision.add(block);
}
}
return decision;
}

}

+ 55
- 0
src/main/java/bubble/abp/spec/BlockListSource.java View File

@@ -0,0 +1,55 @@
package bubble.abp.spec;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.http.HttpUtil;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.daemon.ZillaRuntime.now;

@NoArgsConstructor @Accessors(chain=true) @Slf4j
public class BlockListSource {

@Getter @Setter private String url;
@Getter @Setter private String format;

@Getter @Setter private Long lastDownloaded;
public long age () { return lastDownloaded == null ? Long.MAX_VALUE : now() - lastDownloaded; }

@Getter @Setter private BlockList blockList = new BlockList();

public BlockListSource download() throws IOException {
try (BufferedReader r = new BufferedReader(new InputStreamReader(HttpUtil.get(url)))) {
String line;
boolean firstLine = true;
while ( (line = r.readLine()) != null ) {
if (empty(line)) continue;
line = line.trim();
if (firstLine && line.startsWith("[") && line.endsWith("]")) {
format = line.substring(1, line.length()-1);
}
firstLine = false;
if (line.startsWith("!")) continue;
try {
if (line.startsWith("@@")) {
blockList.addToWhitelist(BlockSpec.parse(line));
} else {
blockList.addToBlacklist(BlockSpec.parse(line));
}
} catch (Exception e) {
log.warn("download("+url+"): error parsing line (skipping due to "+shortError(e)+"): " + line);
}
}
}
lastDownloaded = now();
return this;
}

}

+ 179
- 0
src/main/java/bubble/abp/spec/BlockSpec.java View File

@@ -0,0 +1,179 @@
package bubble.abp.spec;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.http.HttpContentTypes;
import org.cobbzilla.util.string.StringUtil;

import java.util.ArrayList;
import java.util.List;

import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.http.HttpContentTypes.contentType;

@Slf4j @EqualsAndHashCode(of={"target", "domainExclusions", "typeMatches", "typeExclusions", "selector"})
public class BlockSpec {

public static final String OPT_DOMAIN_PREFIX = "domain=";
public static final String OPT_SCRIPT = "script";
public static final String OPT_IMAGE = "image";
public static final String OPT_STYLESHEET = "stylesheet";

@Getter private String line;
@Getter private BlockSpecTarget target;

@Getter private List<String> domainExclusions;
@Getter private List<String> typeMatches;
public boolean hasTypeMatches () { return !empty(typeMatches); }

@Getter private List<String> typeExclusions;

@Getter private String selector;
public boolean hasSelector() { return !empty(selector); }

public BlockSpec(String line, BlockSpecTarget target, List<String> options, String selector) {
this.line = line;
this.target = target;
this.selector = selector;
if (options != null) {
for (String opt : options) {
if (opt.startsWith(OPT_DOMAIN_PREFIX)) {
processDomainOptions(opt.substring(OPT_DOMAIN_PREFIX.length()));

} else if (opt.startsWith("~")) {
final String type = opt.substring(1);
if (isTypeOption(type)) {
if (typeExclusions == null) typeExclusions = new ArrayList<>();
typeExclusions.add(type);
} else {
log.warn("unsupported option (ignoring): " + opt);
}

} else {
if (isTypeOption(opt)) {
if (typeMatches == null) typeMatches = new ArrayList<>();
typeMatches.add(opt);
} else {
log.warn("unsupported option (ignoring): "+opt);
}
}
}
}
}

private void processDomainOptions(String option) {
final String[] parts = option.split("\\|");
for (String domainOption : parts) {
if (domainOption.startsWith("~")) {
if (domainExclusions == null) domainExclusions = new ArrayList<>();
domainExclusions.add(domainOption.substring(1));
} else {
log.warn("ignoring included domain: "+domainOption);
}
}
}

public boolean isTypeOption(String type) {
return type.equals(OPT_SCRIPT) || type.equals(OPT_IMAGE) || type.equals(OPT_STYLESHEET);
}

public static List<BlockSpec> parse(String line) {

line = line.trim();
int optionStartPos = line.indexOf('$');
int selectorStartPos = line.indexOf("#");

// sanity check that selectorStartPos > optionStartPos -- $ may occur AFTER ## if the selector contains a regex
if (selectorStartPos != -1 && optionStartPos > selectorStartPos) optionStartPos = -1;

final List<BlockSpecTarget> targets;
final List<String> options;
final String selector;
if (optionStartPos == -1) {
if (selectorStartPos == -1) {
// no options, no selector, entire line is the target
targets = BlockSpecTarget.parseBareLine(line);
options = null;
selector = null;
} else {
// no options, but selector present. split into target + selector
targets = BlockSpecTarget.parse(line.substring(0, selectorStartPos));
options = null;
selector = line.substring(selectorStartPos+1);
}
} else {
if (selectorStartPos == -1) {
// no selector, split into target + options
targets = BlockSpecTarget.parse(line.substring(0, optionStartPos));
options = StringUtil.splitAndTrim(line.substring(optionStartPos+1), ",");
selector = null;
} else {
// all 3 elements present
targets = BlockSpecTarget.parse(line.substring(0, optionStartPos));
options = StringUtil.splitAndTrim(line.substring(optionStartPos + 1, selectorStartPos), ",");
selector = line.substring(selectorStartPos+1);
}
}
final List<BlockSpec> specs = new ArrayList<>();
for (BlockSpecTarget target : targets) specs.add(new BlockSpec(line, target, options, selector));
return specs;
}

public boolean matches(String fqdn, String path) {
if (target.hasDomainRegex() && target.getDomainPattern().matcher(fqdn).find()) {
return checkDomainExclusionsAndType(fqdn, contentType(path));

} else if (target.hasRegex()) {
if (target.getRegexPattern().matcher(path).find()) {
return checkDomainExclusionsAndType(fqdn, contentType(path));
}
final String full = fqdn + path;
if (target.getRegexPattern().matcher(full).find()) {
return checkDomainExclusionsAndType(fqdn, contentType(path));
};
}
return false;
}

public boolean checkDomainExclusionsAndType(String fqdn, String contentType) {
if (domainExclusions != null) {
for (String domain : domainExclusions) {
if (domain.equals(fqdn)) return false;
}
}
if (typeExclusions != null) {
for (String type : typeExclusions) {
switch (type) {
case OPT_SCRIPT:
if (contentType.equals(HttpContentTypes.APPLICATION_JAVASCRIPT)) return false;
break;
case OPT_IMAGE:
if (contentType.startsWith(HttpContentTypes.IMAGE_PREFIX)) return false;
break;
case OPT_STYLESHEET:
if (contentType.equals(HttpContentTypes.TEXT_CSS)) return false;
break;
}
}
}
if (typeMatches != null) {
for (String type : typeMatches) {
switch (type) {
case OPT_SCRIPT:
if (contentType.equals(HttpContentTypes.APPLICATION_JAVASCRIPT)) return true;
break;
case OPT_IMAGE:
if (contentType.startsWith(HttpContentTypes.IMAGE_PREFIX)) return true;
break;
case OPT_STYLESHEET:
if (contentType.equals(HttpContentTypes.TEXT_CSS)) return true;
break;
}
}
return false;
}
return true;
}

}

+ 102
- 0
src/main/java/bubble/abp/spec/BlockSpecTarget.java View File

@@ -0,0 +1,102 @@
package bubble.abp.spec;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;

import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Pattern;

import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.http.HttpSchemes.stripScheme;

@NoArgsConstructor @Accessors(chain=true) @EqualsAndHashCode(of={"domainRegex", "regex"})
public class BlockSpecTarget {

@Getter @Setter private String domainRegex;
public boolean hasDomainRegex() { return !empty(domainRegex); }
@Getter(lazy=true) private final Pattern domainPattern = hasDomainRegex() ? Pattern.compile(getDomainRegex()) : null;

@Getter @Setter private String regex;
public boolean hasRegex() { return !empty(regex); }
@Getter(lazy=true) private final Pattern regexPattern = hasRegex() ? Pattern.compile(getRegex()) : null;

public static List<BlockSpecTarget> parse(String data) {
final List<BlockSpecTarget> targets = new ArrayList<>();
for (String part : data.split(",")) {
targets.add(parseTarget(part));
}
return targets;
}

public static List<BlockSpecTarget> parseBareLine(String data) {
if (data.contains("|") || data.contains("/") || data.contains("^")) return parse(data);

final List<BlockSpecTarget> targets = new ArrayList<>();
for (String part : data.split(",")) {
targets.add(new BlockSpecTarget().setDomainRegex(matchDomainOrAnySubdomains(part)));
}
return targets;
}

private static BlockSpecTarget parseTarget(String data) {
String domainRegex = null;
String regex = null;
if (data.startsWith("||")) {
final int caretPos = data.indexOf("^");
if (caretPos != -1) {
// domain match
final String domain = data.substring(2, caretPos);
domainRegex = matchDomainOrAnySubdomains(domain);
} else {
final String domain = data.substring(2);
domainRegex = matchDomainOrAnySubdomains(domain);
}
} else if (data.startsWith("|") && data.endsWith("|")) {
// exact match
final String verbatimMatch = stripScheme(data.substring(1, data.length() - 1));
regex = "^" + Pattern.quote(verbatimMatch) + "$";

} else if (data.startsWith("/")) {
// path match, possibly regex
if (data.endsWith("/") && (
data.contains("|") || data.contains("?")
|| (data.contains("(") && data.contains(")"))
|| (data.contains("{") && data.contains("}")))) {
regex = data.substring(1, data.length()-1);

} else if (data.contains("*")) {
regex = parseWildcardMatch(data);
} else {
regex = "^" + Pattern.quote(data) + ".*";
}

} else {
if (data.contains("*")) {
regex = parseWildcardMatch(data);
} else {
regex = "^" + Pattern.quote(data) + ".*";
}
}
return new BlockSpecTarget().setDomainRegex(domainRegex).setRegex(regex);
}

private static String parseWildcardMatch(String data) {
final StringBuilder b = new StringBuilder("^");
final StringTokenizer st = new StringTokenizer(data, "*", true);
while (st.hasMoreTokens()) {
final String token = st.nextToken();
b.append(token.equals("*") ? ".*?" : token);
}
return b.append("$").toString();
}

private static String matchDomainOrAnySubdomains(String domain) {
return ".*?"+Pattern.quote(domain)+"$";
}

}

+ 48
- 0
src/main/java/bubble/abp/spec/selector/AbpClause.java View File

@@ -0,0 +1,48 @@
package bubble.abp.spec.selector;

import lombok.*;
import lombok.experimental.Accessors;
import org.cobbzilla.util.collection.ArrayUtil;

import static bubble.abp.spec.selector.SelectorParseError.parseError;

@NoArgsConstructor @Accessors(chain=true)
@EqualsAndHashCode @ToString
public class AbpClause {

@Getter @Setter private AbpClauseType type;
@Getter @Setter private AbpContains contains;
@Getter @Setter private AbpProperty[] properties;
@Getter @Setter private BlockSelector selector;

public static AbpClause buildAbpClause(AbpClauseType type, String spec) throws SelectorParseError {
final AbpClause abp = new AbpClause().setType(type);
if (!spec.startsWith("(") || !spec.endsWith(")")) throw parseError("expected abp clause to begin with open paren and end with close paren: "+spec);
spec = spec.substring(1, spec.length()-1);
switch (type) {
case contains:
return abp.setContains(AbpContains.build(spec));

case properties:
if (spec.startsWith("/") && spec.endsWith("/")) {
return abp.setProperties(new AbpProperty[]{
new AbpProperty()
.setType(AbpPropertyType.regex)
.setValue(spec.substring(1, spec.length()-1))
});
}
AbpProperty[] props = new AbpProperty[0];
for (String prop : spec.split(";")) {
props = ArrayUtil.append(props, AbpProperty.buildProperty(prop));
}
return abp.setProperties(props);

case has:
if (!spec.startsWith(">")) throw parseError("expected abp-has argument to begin with > :"+spec);
spec = spec.substring(1);
return abp.setSelector(BlockSelector.buildNextSelector(spec.trim()));
}
throw parseError("invalid abp clause type: "+spec);
}

}

+ 7
- 0
src/main/java/bubble/abp/spec/selector/AbpClauseParseState.java View File

@@ -0,0 +1,7 @@
package bubble.abp.spec.selector;

public enum AbpClauseParseState {

seeking_open_paren, seeking_close_paren;

}

+ 19
- 0
src/main/java/bubble/abp/spec/selector/AbpClauseType.java View File

@@ -0,0 +1,19 @@
package bubble.abp.spec.selector;

import com.fasterxml.jackson.annotation.JsonCreator;

import static bubble.abp.spec.selector.SelectorParseError.parseError;

public enum AbpClauseType {

properties, has, contains;

public static final String ABP_CLAUSE_PREFIX = ":-abp-";

@JsonCreator public static AbpClauseType fromString (String v) { return valueOf(v.toLowerCase()); }

public static AbpClauseType fromSpec(String spec) throws SelectorParseError {
if (!spec.startsWith(ABP_CLAUSE_PREFIX)) throw parseError("invalid abp clause: "+spec);
return valueOf(spec.substring(ABP_CLAUSE_PREFIX.length()));
}
}

+ 27
- 0
src/main/java/bubble/abp/spec/selector/AbpContains.java View File

@@ -0,0 +1,27 @@
package bubble.abp.spec.selector;

import lombok.*;
import lombok.experimental.Accessors;

@NoArgsConstructor @Accessors(chain=true)
@EqualsAndHashCode @ToString
public class AbpContains {

@Getter @Setter private String value;
@Getter @Setter private AbpContainsType type;
@Getter @Setter private BlockSelector selector;

public static AbpContains build(String spec) throws SelectorParseError {
if (spec.startsWith("/") && (spec.endsWith("/") || spec.endsWith("/i"))) {
return new AbpContains()
.setValue(spec.substring(1, spec.length()-1))
.setType(spec.endsWith("/i") ? AbpContainsType.case_insensitive_regex : AbpContainsType.regex);
}
if (spec.contains("[") && spec.contains("]")) {
return new AbpContains()
.setSelector(BlockSelector.buildNextSelector(spec))
.setType(AbpContainsType.selector);
}
return new AbpContains().setType(AbpContainsType.literal).setValue(spec);
}
}

+ 11
- 0
src/main/java/bubble/abp/spec/selector/AbpContainsType.java View File

@@ -0,0 +1,11 @@
package bubble.abp.spec.selector;

import com.fasterxml.jackson.annotation.JsonCreator;

public enum AbpContainsType {

literal, regex, case_insensitive_regex, selector;

@JsonCreator public static AbpContainsType fromString (String v) { return valueOf(v.toLowerCase()); }

}

+ 48
- 0
src/main/java/bubble/abp/spec/selector/AbpProperty.java View File

@@ -0,0 +1,48 @@
package bubble.abp.spec.selector;

import lombok.*;
import lombok.experimental.Accessors;

import java.util.StringTokenizer;
import java.util.regex.Pattern;

import static bubble.abp.spec.selector.SelectorParseError.parseError;

@NoArgsConstructor @Accessors(chain=true)
@EqualsAndHashCode @ToString
public class AbpProperty {

@Getter @Setter private String name;
@Getter @Setter private AbpPropertyType type;
@Getter @Setter private String value;

public static AbpProperty buildProperty(String spec) throws SelectorParseError {
final int colonPos = spec.indexOf(':');
if (colonPos == -1) throw parseError("invalid abp property (expecting colon): "+spec);
if (colonPos == 0) throw parseError("invalid abp property (expecting name): "+spec);
if (colonPos == spec.length()) throw parseError("invalid abp property (expecting value): "+spec);
String value = spec.substring(colonPos + 1);
if (value.contains("*")) {
final StringBuilder regex = new StringBuilder();
final StringTokenizer st = new StringTokenizer(value, "*", true);
while (st.hasMoreTokens()) {
final String tok = st.nextToken();
if (tok.equals("*")) {
regex.append(".+?");
} else {
regex.append(Pattern.quote(tok));
}
}
return new AbpProperty()
.setName(spec.substring(0, colonPos))
.setType(AbpPropertyType.wildcard)
.setValue(regex.toString());

} else {
return new AbpProperty()
.setName(spec.substring(0, colonPos))
.setType(AbpPropertyType.exact)
.setValue(value);
}
}
}

+ 11
- 0
src/main/java/bubble/abp/spec/selector/AbpPropertyType.java View File

@@ -0,0 +1,11 @@
package bubble.abp.spec.selector;

import com.fasterxml.jackson.annotation.JsonCreator;

public enum AbpPropertyType {

exact, wildcard, regex;

@JsonCreator public static AbpPropertyType fromString (String v) { return valueOf(v.toLowerCase()); }

}

+ 165
- 0
src/main/java/bubble/abp/spec/selector/BlockSelector.java View File

@@ -0,0 +1,165 @@
package bubble.abp.spec.selector;

import lombok.*;
import lombok.experimental.Accessors;
import org.cobbzilla.util.collection.ArrayUtil;

import java.util.StringTokenizer;

import static bubble.abp.spec.selector.SelectorAttributeParseState.*;
import static bubble.abp.spec.selector.SelectorParseError.parseError;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;

@NoArgsConstructor @Accessors(chain=true)
@ToString(of={"type", "abpEnabled", "name", "cls", "attributes", "abp", "operator", "next"})
@EqualsAndHashCode(of={"type", "abpEnabled", "name", "cls", "attributes", "abp", "operator", "next"})
public class BlockSelector {

@Getter @Setter private String error;

@Getter @Setter private SelectorType type;
@Getter @Setter private Boolean abpEnabled;
public boolean abpEnabled() { return abpEnabled != null && abpEnabled; }

@Getter private String name;
public BlockSelector setName(String n) throws SelectorParseError {
final int dotPos = n.indexOf('.');
if (dotPos == -1) {
name = n;
} else {
if (dotPos == 0 || dotPos == n.length()-1) throw parseError("invalid name: "+n);
if (type == null) throw parseError("type not set, cannot set name: "+n);
name = n.substring(0, dotPos);
cls = n.substring(dotPos + 1);
switch (type) {
case id: type = SelectorType.id_and_cls; break;
case tag: type = SelectorType.tag_and_cls; break;
default: throw parseError("cannot add class to type "+type);
}
}
return this;
}

@Getter @Setter private String cls;
@Getter @Setter private SelectorAttribute[] attributes;
@Getter @Setter private AbpClause abp;
@Getter @Setter private SelectorOperator operator;
@Getter @Setter private BlockSelector next;

public static BlockSelector buildSelector(String spec) throws SelectorParseError {
if (empty(spec)) return null;

if (spec.charAt(0) != '#') throw parseError("expected spec to begin with '#'");
spec = spec.substring(1);

final BlockSelector sel = new BlockSelector();
spec = SelectorType.setType(spec, sel);

return _buildSelector(spec, sel);
}

public static BlockSelector buildNextSelector(String spec) throws SelectorParseError {
if (empty(spec)) return null;

final BlockSelector sel = new BlockSelector();
spec = SelectorType.setNextType(spec, sel);
return _buildSelector(spec, sel);
}

private static BlockSelector _buildSelector(String spec, BlockSelector sel) throws SelectorParseError {
boolean nameSet = false;
int bracketPos = spec.indexOf('[');
int spacePos = spec.indexOf(' ');
int abpPos = spec.indexOf(":-");
if ((spacePos != -1 && bracketPos > spacePos) || (abpPos != -1 && bracketPos > abpPos)) {
bracketPos = -1;
}
if (bracketPos != -1) {
sel.setName(spec.substring(0, bracketPos));
spec = spec.substring(bracketPos);
nameSet = true;
final StringTokenizer st = new StringTokenizer(spec, "[] ", true);
SelectorAttributeParseState state = seeking_open_bracket;
String attrSpec = null;
int charsConsumed = 0;
while (st.hasMoreTokens()) {
final String tok = st.nextToken();
charsConsumed += tok.length();
switch (tok) {
case "[":
if (state != seeking_open_bracket) throw parseError("invalid attribute (expecting open bracket): "+spec);
state = seeking_close_bracket;
break;

case "]":
if (state != seeking_close_bracket) throw parseError("invalid attribute (expecting close bracket): "+spec);
state = seeking_open_bracket;
sel.attributes = ArrayUtil.append(sel.attributes, SelectorAttribute.buildAttribute(attrSpec));
break;

case " ":
if (state != seeking_open_bracket) throw parseError("invalid attribute (unexpected space, expecting close bracket): "+spec);
state = finished;
break;

default:
attrSpec = tok;
break;
}
if (state == finished) break;
}
spec = spec.substring(charsConsumed);
}

if (abpPos != -1 && spacePos != -1 && abpPos > spacePos) abpPos = -1;
if (abpPos != -1) {
sel.setName(spec.substring(0, abpPos));
nameSet = true;

int openParen = spec.indexOf("(");
if (openParen == -1) throw parseError("found abp clause but no open paren: "+spec);
if (openParen == spec.length()-1) throw parseError("found abp clause but open paren has no closing paren: "+spec);
final AbpClauseType abpClauseType = AbpClauseType.fromSpec(spec.substring(abpPos, openParen));
spec = spec.substring(openParen);
int nestCount = 0;
final StringTokenizer st = new StringTokenizer(spec, "()", true);
boolean done = false;
final StringBuilder abpSpec = new StringBuilder();
while (st.hasMoreTokens()) {
final String tok = st.nextToken();
abpSpec.append(tok);
switch (tok) {
case "(":
nestCount++;
break;
case ")":
nestCount--;
if (nestCount == 0) done = true;
break;
}
if (done) break;
}
sel.setAbp(AbpClause.buildAbpClause(abpClauseType, abpSpec.toString()));
spec = spec.substring(abpSpec.length());
}

if (!nameSet) {
spacePos = spec.indexOf(' ');
if (spacePos == -1) {
sel.setName(spec);
return sel;

} else {
sel.setName(spec.substring(0, spacePos));
spec = spec.substring(spacePos).trim();
}
}

if (!empty(spec)) {
spec = SelectorOperator.setOperator(spec, sel);
sel.setNext(buildNextSelector(spec));
}
return sel;
}

}

+ 71
- 0
src/main/java/bubble/abp/spec/selector/SelectorAttribute.java View File

@@ -0,0 +1,71 @@
package bubble.abp.spec.selector;

import lombok.*;
import lombok.experimental.Accessors;
import org.cobbzilla.util.collection.ArrayUtil;
import org.cobbzilla.util.collection.NameAndValue;

import static bubble.abp.spec.selector.SelectorParseError.parseError;

@NoArgsConstructor @Accessors(chain=true)
@ToString(of={"name", "comparison", "value", "style"})
@EqualsAndHashCode(of={"name", "comparison", "value", "style"})
public class SelectorAttribute {

@Getter @Setter private String name;
@Getter @Setter private SelectorAttributeComparison comparison;
@Getter @Setter private String value;
@Getter @Setter private NameAndValue[] style;

public static SelectorAttribute buildAttribute(String spec) throws SelectorParseError {
final int eqPos = spec.indexOf('=');
if (eqPos == -1) throw parseError("invalid attribute ('=' not found): "+spec);
if (eqPos == 0) throw parseError("invalid attribute (nothing precedes '='): "+spec);

final SelectorAttribute attr = new SelectorAttribute();
switch (spec.charAt(eqPos-1)) {
case '^':
attr.setComparison(SelectorAttributeComparison.startsWith);
attr.setName(spec.substring(0, eqPos-1));
break;
case '$':
attr.setComparison(SelectorAttributeComparison.endsWith);
attr.setName(spec.substring(0, eqPos-1));
break;
case '*':
attr.setComparison(SelectorAttributeComparison.contains);
attr.setName(spec.substring(0, eqPos-1));
break;
default:
attr.setComparison(SelectorAttributeComparison.equals);
attr.setName(spec.substring(0, eqPos));
break;
}
final int openQuote = spec.indexOf("\"");
if (openQuote == -1) throw parseError("invalid attribute (expected opening double-quote char following '='): "+spec);

final int closeQuote = spec.indexOf("\"", openQuote+1);
if (closeQuote == -1) throw parseError("invalid attribute (expected closing double-quote char after opening double-quote char): "+spec);

attr.setValue(spec.substring(openQuote + 1, closeQuote));
if (attr.getName().equalsIgnoreCase("style")) {
attr.setStyle(buildStyles(spec.substring(openQuote + 1, closeQuote)));
}
return attr;
}

private static NameAndValue[] buildStyles(String spec) throws SelectorParseError {
NameAndValue[] styles = new NameAndValue[0];
for (String part : spec.split(";")) {
styles = ArrayUtil.append(styles, buildStyle(part));
}
return styles;
}

private static NameAndValue buildStyle(String style) throws SelectorParseError {
final int colonPos = style.indexOf(':');
if (colonPos == -1) throw parseError("invalid style (expected colon char): "+style);
if (colonPos == 0 || colonPos == style.length()-1) throw parseError("invalid style (no name or value): "+style);
return new NameAndValue(style.substring(0, colonPos), style.substring(colonPos+1));
}
}

+ 11
- 0
src/main/java/bubble/abp/spec/selector/SelectorAttributeComparison.java View File

@@ -0,0 +1,11 @@
package bubble.abp.spec.selector;

import com.fasterxml.jackson.annotation.JsonCreator;

public enum SelectorAttributeComparison {

equals, startsWith, endsWith, contains;

@JsonCreator public static SelectorAttributeComparison fromString (String v) { return valueOf(v.toLowerCase()); }

}

+ 7
- 0
src/main/java/bubble/abp/spec/selector/SelectorAttributeParseState.java View File

@@ -0,0 +1,7 @@
package bubble.abp.spec.selector;

public enum SelectorAttributeParseState {

seeking_open_bracket, seeking_close_bracket, finished;

}

+ 35
- 0
src/main/java/bubble/abp/spec/selector/SelectorOperator.java View File

@@ -0,0 +1,35 @@
package bubble.abp.spec.selector;

import com.fasterxml.jackson.annotation.JsonCreator;

import static bubble.abp.spec.selector.SelectorParseError.parseError;

public enum SelectorOperator {

next, encloses;

@JsonCreator public static SelectorOperator fromString (String v) { return valueOf(v.toLowerCase()); }

public static String setOperator(String spec, BlockSelector sel) throws SelectorParseError {
spec = spec.trim();
if (spec.length() == 0) throw parseError("unexpected end of spec");
switch (spec.charAt(0)) {
case '+':
sel.setOperator(next);
if (spec.charAt(1) != ' ') throw parseError("expected space after + operator: "+spec);
spec = spec.substring(2);
break;
case '>':
sel.setOperator(encloses);
if (spec.charAt(1) != ' ') throw parseError("expected space after > operator: "+spec);
spec = spec.substring(2);
break;
default:
// non-standard syntax, roll with it, assume they mean to enclose
sel.setOperator(encloses);
if (!Character.isAlphabetic(spec.charAt(0))) throw parseError("expected alphabetic char: "+spec);
break;
}
return spec;
}
}

+ 9
- 0
src/main/java/bubble/abp/spec/selector/SelectorParseError.java View File

@@ -0,0 +1,9 @@
package bubble.abp.spec.selector;

public class SelectorParseError extends RuntimeException {

public SelectorParseError(String s) { super(s); }

public static SelectorParseError parseError(String s) { return new SelectorParseError(s); }

}

+ 36
- 0
src/main/java/bubble/abp/spec/selector/SelectorType.java View File

@@ -0,0 +1,36 @@
package bubble.abp.spec.selector;

import com.fasterxml.jackson.annotation.JsonCreator;

import static bubble.abp.spec.selector.SelectorParseError.parseError;

public enum SelectorType {

id, tag, cls, id_and_cls, tag_and_cls;

@JsonCreator public static SelectorType fromString (String v) { return valueOf(v.toLowerCase()); }

public static String setType(String spec, BlockSelector sel) throws SelectorParseError {
if (spec.startsWith("?")) {
sel.setAbpEnabled(true);
spec = spec.substring(1);
}
if (!spec.startsWith("#")) throw parseError("invalid prefix: "+spec);
return setNextType(spec.substring(1), sel);
}

public static String setNextType(String spec, BlockSelector sel) throws SelectorParseError {
if (spec.startsWith("#")) {
sel.setType(id);
spec = spec.substring(1);

} else if (spec.startsWith(".")) {
sel.setType(cls);
spec = spec.substring(1);

} else {
sel.setType(tag);
}
return spec;
}
}

+ 157
- 0
src/test/java/bubble/abp/spec/BlockListTest.java View File

@@ -0,0 +1,157 @@
package bubble.abp.spec;

import org.junit.Test;

import java.util.Arrays;

import static org.junit.Assert.assertEquals;

public class BlockListTest {

public static final String BLOCK = BlockDecisionType.block.name();
public static final String ALLOW = BlockDecisionType.allow.name();
public static final String FILTER = BlockDecisionType.filter.name();

public static final String[][] BLOCK_TESTS = {
// rule // fqdn // path // expected decision

// bare hosts example (ala EasyList)
{"example.com", "example.com", "/some_path", BLOCK},
{"example.com", "foo.example.com", "/some_path", BLOCK},
{"example.com", "example.org", "/some_path", ALLOW},

// block example.com and all subdomains
{"||example.com^", "example.com", "/some_path", BLOCK},
{"||example.com^", "foo.example.com", "/some_path", BLOCK},
{"||example.com^", "example.org", "/some_path", ALLOW},

// block exact string
{"|example.com/|", "example.com", "/", BLOCK},
{"|example.com/|", "example.com", "/some_path", ALLOW},
{"|example.com/|", "foo.example.com", "/some_path", ALLOW},

// block example.com, but not foo.example.com or bar.example.com
{"||example.com^$domain=~foo.example.com|~bar.example.com",
"example.com", "/some_path", BLOCK},
{"||example.com^$domain=~foo.example.com|~bar.example.com",
"foo.example.com", "/some_path", ALLOW},
{"||example.com^$domain=~foo.example.com|~bar.example.com",
"bar.example.com", "/some_path", ALLOW},
{"||example.com^$domain=~foo.example.com|~bar.example.com",
"baz.example.com", "/some_path", BLOCK},

// block images and scripts on example.com, but not foo.example.com or bar.example.com
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com",
"example.com", "/some_path", ALLOW},

// test image blocking
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com",
"example.com", "/some_path.png", BLOCK},
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com",
"foo.example.com", "/some_path.png", ALLOW},
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com",
"bar.example.com", "/some_path.png", ALLOW},
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com",
"baz.example.com", "/some_path.png", BLOCK},

// test script blocking
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com",
"example.com", "/some_path.js", BLOCK},
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com",
"foo.example.com", "/some_path.js", ALLOW},
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com",
"bar.example.com", "/some_path.js", ALLOW},
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com",
"baz.example.com", "/some_path.js", BLOCK},

// test stylesheet blocking
{"||example.com^stylesheet,domain=~foo.example.com|~bar.example.com",
"example.com", "/some_path.css", BLOCK},
{"||example.com^$stylesheet,domain=~foo.example.com|~bar.example.com",
"foo.example.com", "/some_path.css", ALLOW},
{"||example.com^$stylesheet,domain=~foo.example.com|~bar.example.com",
"bar.example.com", "/some_path.css", ALLOW},
{"||example.com^$stylesheet,domain=~foo.example.com|~bar.example.com",
"baz.example.com", "/some_path.css", BLOCK},


// path matching
{"/foo", "example.com", "/some_path", ALLOW},
{"/foo", "example.com", "/foo", BLOCK},
{"/foo", "example.com", "/foo/bar", BLOCK},

// path matching with wildcard
{"/foo/*/img", "example.com", "/some_path", ALLOW},
{"/foo/*/img", "example.com", "/foo", ALLOW},
{"/foo/*/img", "example.com", "/foo/img", ALLOW},
{"/foo/*/img", "example.com", "/foo/x/img", BLOCK},
{"/foo/*/img", "example.com", "/foo/x/img.png", ALLOW},
{"/foo/*/img", "example.com", "/foo/x/y/z//img", BLOCK},

// path matching with regex
{"/foo/(apps|ads)/img.+/", "example.com", "/foo/x/y/z//img", ALLOW},
{"/foo/(apps|ads)/img.+/", "example.com", "/foo/apps/img.png", BLOCK},
{"/foo/(apps|ads)/img.+/", "example.com", "/foo/ads/img.png", BLOCK},
{"/foo/(apps|ads)/img.+/", "example.com", "/foo/bar/ads/img.png", ALLOW},

{"/(apps|ads)\\.example\\.(com|org)/",
"example.com", "/ad.png", ALLOW},
{"/(apps|ads)\\.example\\.(com|org)/",
"ads.example.com", "/ad.png", BLOCK},
{"/(apps|ads)\\.example\\.(com|org)/",
"apps.example.com", "/ad.png", BLOCK},
{"/(apps|ads)\\.example\\.(com|org)/",
"ads.example.org", "/ad.png", BLOCK},
{"/(apps|ads)\\.example\\.(com|org)/",
"apps.example.org", "/ad.png", BLOCK},
{"/(apps|ads)\\.example\\.(com|org)/",
"ads.example.net", "/ad.png", ALLOW},
{"/(apps|ads)\\.example\\.(com|org)/",
"apps.example.net", "/ad.png", ALLOW},

// selectors
{"example.com##.banner-ad", "example.com", "/ad.png", FILTER},

// putting it all together
{"||example.com^$domain=~foo.example.com|~bar.example.com##.banner-ad",
"example.com", "/some_path", FILTER},
{"||example.com^$domain=~foo.example.com|~bar.example.com##.banner-ad",
"baz.example.com", "/some_path", FILTER},
{"||example.com^$domain=~foo.example.com|~bar.example.com##.banner-ad",
"foo.example.com", "/some_path", FILTER},
{"||example.com^$domain=~foo.example.com|~bar.example.com##.banner-ad",
"bar.example.com", "/some_path", FILTER},
};

@Test public void testRules () throws Exception {
for (String[] test : BLOCK_TESTS) {
final BlockDecisionType expectedDecision = BlockDecisionType.fromString(test[3]);
final BlockList blockList = new BlockList();
blockList.addToBlacklist(BlockSpec.parse(test[0]));
assertEquals("testBlanketBlock: expected "+expectedDecision+" decision, test=" + Arrays.toString(test),
expectedDecision,
blockList.getDecision(test[1], test[2]).getDecisionType());
}
}

public String[] SELECTOR_SPECS = {
"||example.com##.banner-ad",
"||foo.example.com##.more-ads",
};

@Test public void testMultipleSelectorMatches () throws Exception {
final BlockList blockList = new BlockList();
for (String line : SELECTOR_SPECS) {
blockList.addToBlacklist(BlockSpec.parse(line));
}
BlockDecision decision;

decision = blockList.getDecision("example.com", "/some_path");
assertEquals("expected filter decision", BlockDecisionType.filter, decision.getDecisionType());
assertEquals("expected 1 filter specs", 2, decision.getSpecs().size());

decision = blockList.getDecision("foo.example.com", "/some_path");
assertEquals("expected filter decision", BlockDecisionType.filter, decision.getDecisionType());
assertEquals("expected 2 filter specs", 2, decision.getSpecs().size());
}
}

+ 135
- 0
src/test/java/bubble/abp/spec/SelectorTest.java View File

@@ -0,0 +1,135 @@
package bubble.abp.spec;

import bubble.abp.spec.selector.*;
import org.cobbzilla.util.collection.NameAndValue;
import org.junit.Test;

import static bubble.abp.spec.selector.SelectorAttributeComparison.*;
import static bubble.abp.spec.selector.SelectorOperator.encloses;
import static bubble.abp.spec.selector.SelectorOperator.next;
import static bubble.abp.spec.selector.SelectorType.*;
import static org.junit.Assert.assertEquals;

public class SelectorTest {

public static final Object[][] SELECTOR_TESTS = new Object[][] {
{"###foo", new BlockSelector().setType(id).setName("foo")},
{"##.foo", new BlockSelector().setType(cls).setName("foo")},
{"##foo", new BlockSelector().setType(tag).setName("foo")},

{"##foo > .bar", new BlockSelector().setType(tag).setName("foo").setOperator(encloses)
.setNext(new BlockSelector().setType(cls).setName("bar"))},
{"##foo + .bar", new BlockSelector().setType(tag).setName("foo").setOperator(next)
.setNext(new BlockSelector().setType(cls).setName("bar"))},

{"##foo > bar", new BlockSelector().setType(tag).setName("foo").setOperator(encloses)
.setNext(new BlockSelector().setType(tag).setName("bar"))},
{"##foo + bar", new BlockSelector().setType(tag).setName("foo").setOperator(next)
.setNext(new BlockSelector().setType(tag).setName("bar"))},

{"##.foo[href=\"bar\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] {
new SelectorAttribute().setName("href").setComparison(equals).setValue("bar")
})},
{"##.foo[href^=\"bar\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] {
new SelectorAttribute().setName("href").setComparison(startsWith).setValue("bar")
})},
{"##.foo[href$=\"bar\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] {
new SelectorAttribute().setName("href").setComparison(endsWith).setValue("bar")
})},
{"##.foo[href*=\"bar\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] {
new SelectorAttribute().setName("href").setComparison(contains).setValue("bar")
})},

{"##.foo[href=\"bar\"][target^=\"_blank\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] {
new SelectorAttribute().setName("href").setComparison(equals).setValue("bar"),
new SelectorAttribute().setName("target").setComparison(startsWith).setValue("_blank")
})},

{"##.foo[href=\"bar\"][target^=\"_blank\"][style*=\"width:300px;height:100px\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] {
new SelectorAttribute().setName("href").setComparison(equals).setValue("bar"),
new SelectorAttribute().setName("target").setComparison(startsWith).setValue("_blank"),
new SelectorAttribute().setName("style").setComparison(contains).setValue("width:300px;height:100px")
.setStyle(new NameAndValue[] {
new NameAndValue("width", "300px"),
new NameAndValue("height", "100px")
})
})},

{"##foo > .bar[src*=\"advert\"][height=\"30\"] + img > #ad_foo[src$=\".png\"]", new BlockSelector().setType(tag).setName("foo")
.setOperator(encloses)
.setNext(new BlockSelector().setType(cls).setName("bar").setAttributes(
new SelectorAttribute[] {
new SelectorAttribute().setName("src").setComparison(contains).setValue("advert"),
new SelectorAttribute().setName("height").setComparison(equals).setValue("30")
})
.setOperator(next)
.setNext(new BlockSelector().setType(tag).setName("img")
.setOperator(encloses)
.setNext(new BlockSelector().setType(id).setName("ad_foo").setAttributes(
new SelectorAttribute[] {
new SelectorAttribute().setName("src").setComparison(endsWith).setValue(".png")
}))
))
},
};

public static final Object[][] ABP_TESTS = new Object[][] {
{"#?#div:-abp-properties(width:300px;height:250px;)", new BlockSelector()
.setAbpEnabled(true).setType(tag).setName("div").setAbp(
new AbpClause()
.setType(AbpClauseType.properties)
.setProperties(new AbpProperty[] {
new AbpProperty().setName("width").setType(AbpPropertyType.exact).setValue("300px"),
new AbpProperty().setName("height").setType(AbpPropertyType.exact).setValue("250px")
}))
},

{"#?#div:-abp-has(> div > img.advert)", new BlockSelector()
.setAbpEnabled(true).setType(tag).setName("div").setAbp(
new AbpClause()
.setType(AbpClauseType.has)
.setSelector(new BlockSelector().setType(tag).setName("div").setOperator(encloses)
.setNext(new BlockSelector().setType(tag_and_cls).setName("img").setCls("advert"))))},

{"#?#div:-abp-has(> span:-abp-contains(Advertisement))", new BlockSelector()
.setAbpEnabled(true).setType(tag).setName("div").setAbp(
new AbpClause().setType(AbpClauseType.has)
.setSelector(new BlockSelector().setType(tag).setName("span").setAbp(
new AbpClause().setType(AbpClauseType.contains).setContains(
new AbpContains().setType(AbpContainsType.literal).setValue("Advertisement")
))))},

{"#?#div > img:-abp-properties(/width: 3[2-8]px;/)", new BlockSelector()
.setAbpEnabled(true).setType(tag).setName("div").setOperator(encloses).setNext(
new BlockSelector().setType(tag).setName("img").setAbp(
new AbpClause().setType(AbpClauseType.properties).setProperties(new AbpProperty[]{
new AbpProperty().setType(AbpPropertyType.regex).setValue("width: 3[2-8]px;")
})))},

{"#?#.panel:-abp-contains(a[href*=\"example.com\"])", new BlockSelector()
.setAbpEnabled(true).setType(cls).setName("panel").setAbp(
new AbpClause().setType(AbpClauseType.contains).setContains(
new AbpContains().setType(AbpContainsType.selector).setSelector(
new BlockSelector().setType(tag).setName("a").setAttributes(new SelectorAttribute[]{
new SelectorAttribute().setName("href").setComparison(contains).setValue("example.com")
}))))},

{"#?#.cls-content ol:-abp-contains(Download Foo)", new BlockSelector()
.setAbpEnabled(true).setType(cls).setName("cls-content").setOperator(encloses).setNext(
new BlockSelector().setType(tag).setName("ol").setAbp(
new AbpClause().setType(AbpClauseType.contains).setContains(
new AbpContains().setType(AbpContainsType.literal).setValue("Download Foo")
)))}};

@Test public void testSelectorParsing () throws Exception { runTests(SELECTOR_TESTS); }
@Test public void testAbpParsing () throws Exception { runTests(ABP_TESTS); }

private void runTests(Object[][] tests) throws SelectorParseError {
for (Object[] test : tests) {
final String spec = test[0].toString();
final BlockSelector sel = BlockSelector.buildSelector(spec);
assertEquals("BlockSelector object is incorrect for spec: "+spec, test[1], sel);
}
}

}

Loading…
Cancel
Save