commit 662d8da818cf543bfd781f20798cf030efcc4e79 Author: Jonathan Cobb Date: Fri Jan 31 11:37:38 2020 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c1ceb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.idea +target +tmp +logs +dependency-reduced-pom.xml +velocity.log +*~ +build.log diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9201fe9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,53 @@ + + + + + 4.0.0 + + bubble + abp-parser + 1.0.0-SNAPSHOT + jar + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + org.cobbzilla + cobbzilla-utils + 1.0.0-SNAPSHOT + + + + junit + junit + 4.12 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 11 + 11 + true + + + + + + diff --git a/src/main/java/bubble/abp/spec/BlockDecision.java b/src/main/java/bubble/abp/spec/BlockDecision.java new file mode 100644 index 0000000..7226fa0 --- /dev/null +++ b/src/main/java/bubble/abp/spec/BlockDecision.java @@ -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 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; + } + +} diff --git a/src/main/java/bubble/abp/spec/BlockDecisionType.java b/src/main/java/bubble/abp/spec/BlockDecisionType.java new file mode 100644 index 0000000..3d71390 --- /dev/null +++ b/src/main/java/bubble/abp/spec/BlockDecisionType.java @@ -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()); } + +} diff --git a/src/main/java/bubble/abp/spec/BlockList.java b/src/main/java/bubble/abp/spec/BlockList.java new file mode 100644 index 0000000..4fbda95 --- /dev/null +++ b/src/main/java/bubble/abp/spec/BlockList.java @@ -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 blacklist = new HashSet<>(); + @Getter private Set whitelist = new HashSet<>(); + + public void addToWhitelist(BlockSpec spec) { + whitelist.add(spec); + } + + public void addToWhitelist(List specs) { + for (BlockSpec spec : specs) addToWhitelist(spec); + } + + public void addToBlacklist(BlockSpec spec) { + blacklist.add(spec); + } + + public void addToBlacklist(List 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; + } + +} diff --git a/src/main/java/bubble/abp/spec/BlockListSource.java b/src/main/java/bubble/abp/spec/BlockListSource.java new file mode 100644 index 0000000..c27c3b3 --- /dev/null +++ b/src/main/java/bubble/abp/spec/BlockListSource.java @@ -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; + } + +} diff --git a/src/main/java/bubble/abp/spec/BlockSpec.java b/src/main/java/bubble/abp/spec/BlockSpec.java new file mode 100644 index 0000000..6f3c7c3 --- /dev/null +++ b/src/main/java/bubble/abp/spec/BlockSpec.java @@ -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 domainExclusions; + @Getter private List typeMatches; + public boolean hasTypeMatches () { return !empty(typeMatches); } + + @Getter private List typeExclusions; + + @Getter private String selector; + public boolean hasSelector() { return !empty(selector); } + + public BlockSpec(String line, BlockSpecTarget target, List 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 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 targets; + final List 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 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; + } + +} diff --git a/src/main/java/bubble/abp/spec/BlockSpecTarget.java b/src/main/java/bubble/abp/spec/BlockSpecTarget.java new file mode 100644 index 0000000..cf19d7d --- /dev/null +++ b/src/main/java/bubble/abp/spec/BlockSpecTarget.java @@ -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 parse(String data) { + final List targets = new ArrayList<>(); + for (String part : data.split(",")) { + targets.add(parseTarget(part)); + } + return targets; + } + + public static List parseBareLine(String data) { + if (data.contains("|") || data.contains("/") || data.contains("^")) return parse(data); + + final List 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)+"$"; + } + +} diff --git a/src/main/java/bubble/abp/spec/selector/AbpClause.java b/src/main/java/bubble/abp/spec/selector/AbpClause.java new file mode 100644 index 0000000..7e687b9 --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/AbpClause.java @@ -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); + } + +} diff --git a/src/main/java/bubble/abp/spec/selector/AbpClauseParseState.java b/src/main/java/bubble/abp/spec/selector/AbpClauseParseState.java new file mode 100644 index 0000000..147768e --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/AbpClauseParseState.java @@ -0,0 +1,7 @@ +package bubble.abp.spec.selector; + +public enum AbpClauseParseState { + + seeking_open_paren, seeking_close_paren; + +} diff --git a/src/main/java/bubble/abp/spec/selector/AbpClauseType.java b/src/main/java/bubble/abp/spec/selector/AbpClauseType.java new file mode 100644 index 0000000..1abd56a --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/AbpClauseType.java @@ -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())); + } +} diff --git a/src/main/java/bubble/abp/spec/selector/AbpContains.java b/src/main/java/bubble/abp/spec/selector/AbpContains.java new file mode 100644 index 0000000..a1be89b --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/AbpContains.java @@ -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); + } +} diff --git a/src/main/java/bubble/abp/spec/selector/AbpContainsType.java b/src/main/java/bubble/abp/spec/selector/AbpContainsType.java new file mode 100644 index 0000000..10b075b --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/AbpContainsType.java @@ -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()); } + +} diff --git a/src/main/java/bubble/abp/spec/selector/AbpProperty.java b/src/main/java/bubble/abp/spec/selector/AbpProperty.java new file mode 100644 index 0000000..76d1bd9 --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/AbpProperty.java @@ -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); + } + } +} diff --git a/src/main/java/bubble/abp/spec/selector/AbpPropertyType.java b/src/main/java/bubble/abp/spec/selector/AbpPropertyType.java new file mode 100644 index 0000000..17e4052 --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/AbpPropertyType.java @@ -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()); } + +} diff --git a/src/main/java/bubble/abp/spec/selector/BlockSelector.java b/src/main/java/bubble/abp/spec/selector/BlockSelector.java new file mode 100644 index 0000000..62f0cd4 --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/BlockSelector.java @@ -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; + } + +} diff --git a/src/main/java/bubble/abp/spec/selector/SelectorAttribute.java b/src/main/java/bubble/abp/spec/selector/SelectorAttribute.java new file mode 100644 index 0000000..9f2deb7 --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/SelectorAttribute.java @@ -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)); + } +} diff --git a/src/main/java/bubble/abp/spec/selector/SelectorAttributeComparison.java b/src/main/java/bubble/abp/spec/selector/SelectorAttributeComparison.java new file mode 100644 index 0000000..ba1f920 --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/SelectorAttributeComparison.java @@ -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()); } + +} diff --git a/src/main/java/bubble/abp/spec/selector/SelectorAttributeParseState.java b/src/main/java/bubble/abp/spec/selector/SelectorAttributeParseState.java new file mode 100644 index 0000000..35ab623 --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/SelectorAttributeParseState.java @@ -0,0 +1,7 @@ +package bubble.abp.spec.selector; + +public enum SelectorAttributeParseState { + + seeking_open_bracket, seeking_close_bracket, finished; + +} diff --git a/src/main/java/bubble/abp/spec/selector/SelectorOperator.java b/src/main/java/bubble/abp/spec/selector/SelectorOperator.java new file mode 100644 index 0000000..4dbf91f --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/SelectorOperator.java @@ -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; + } +} diff --git a/src/main/java/bubble/abp/spec/selector/SelectorParseError.java b/src/main/java/bubble/abp/spec/selector/SelectorParseError.java new file mode 100644 index 0000000..9d827d7 --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/SelectorParseError.java @@ -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); } + +} diff --git a/src/main/java/bubble/abp/spec/selector/SelectorType.java b/src/main/java/bubble/abp/spec/selector/SelectorType.java new file mode 100644 index 0000000..bfa816c --- /dev/null +++ b/src/main/java/bubble/abp/spec/selector/SelectorType.java @@ -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; + } +} diff --git a/src/test/java/bubble/abp/spec/BlockListTest.java b/src/test/java/bubble/abp/spec/BlockListTest.java new file mode 100644 index 0000000..8336c59 --- /dev/null +++ b/src/test/java/bubble/abp/spec/BlockListTest.java @@ -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()); + } +} diff --git a/src/test/java/bubble/abp/spec/SelectorTest.java b/src/test/java/bubble/abp/spec/SelectorTest.java new file mode 100644 index 0000000..37ebd92 --- /dev/null +++ b/src/test/java/bubble/abp/spec/SelectorTest.java @@ -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); + } + } + +}