jarld downloads blocklists (Adblock, hosts or plain domains), checks them daily and reloads named only when a list changed; your own block and allow rules take precedence, and the most specific rule wins. Lists and rules apply per view (default: every view that offers recursion). Web UI Blocking page with name checks, jarl blocklist and jarl block, the /blocking API, blocked counts in the statistics. Database schema 6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
5.8 KiB
Go
181 lines
5.8 KiB
Go
package blocking
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParse(t *testing.T) {
|
|
src := "\uFEFF[Adblock Plus]\r\n" + `! Title: test list
|
|
# a comment
|
|
|
|
||ads.example^
|
|
||Tracker.Example.^$important
|
|
||cosmetic.example^$third-party
|
|
@@||allowed.example^
|
|
example.org##.banner
|
|
|https://url.example/path
|
|
0.0.0.0 hosts.example other.example # two names
|
|
127.0.0.1 localhost
|
|
127.0.0.1 localhost.localdomain
|
|
::1 ip6-localhost
|
|
0.0.0.0 0.0.0.0
|
|
plain.example
|
|
*.wild.example
|
|
single
|
|
1.2.3.4
|
|
bad_label!.example
|
|
münchen.example
|
|
www.ads.example
|
|
` + strings.Repeat("a", 64) + ".example\n"
|
|
l, err := Parse(strings.NewReader(src), Limits{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]uint8{
|
|
"ads.example": Exact | Below, "tracker.example": Exact | Below,
|
|
"hosts.example": Exact, "other.example": Exact, "plain.example": Exact,
|
|
"wild.example": Exact | Below, "xn--mnchen-3ya.example": Exact, "www.ads.example": Exact,
|
|
}
|
|
if len(l.Domains) != len(want) {
|
|
t.Errorf("domains = %v", l.Domains)
|
|
}
|
|
for n, f := range want {
|
|
if l.Domains[n] != f {
|
|
t.Errorf("%s = %d, want %d", n, l.Domains[n], f)
|
|
}
|
|
}
|
|
// Skipped: $third-party, @@, ##, |url, localhost (x3), 0.0.0.0, single, 1.2.3.4, bad_label!, 64-char label.
|
|
if l.Skipped != 12 || l.Lines != 19 {
|
|
t.Errorf("lines %d, skipped %d", l.Lines, l.Skipped)
|
|
}
|
|
|
|
Compact(l)
|
|
if _, ok := l.Domains["www.ads.example"]; ok {
|
|
t.Error("www.ads.example is covered by ||ads.example^ and should be dropped")
|
|
}
|
|
var sb strings.Builder
|
|
n, hash, err := FeedZone(&sb, "test", 7, l)
|
|
if err != nil || n != 10 {
|
|
t.Fatalf("FeedZone: %d records, %v", n, err)
|
|
}
|
|
for _, line := range []string{"ads.example\tCNAME\t.\n", "*.ads.example\tCNAME\t.\n", "plain.example\tCNAME\t.\n", "@\tSOA\tlocalhost. hostmaster.localhost. 7 "} {
|
|
if !strings.Contains(sb.String(), line) {
|
|
t.Errorf("zone lacks %q:\n%s", line, sb.String())
|
|
}
|
|
}
|
|
if strings.Contains(sb.String(), "*.plain.example") || strings.Contains(sb.String(), "$ORIGIN") {
|
|
t.Errorf("zone:\n%s", sb.String())
|
|
}
|
|
// The hash ignores the serial.
|
|
var other strings.Builder
|
|
if _, h2, _ := FeedZone(&other, "test", 8, l); h2 != hash {
|
|
t.Error("hash depends on the serial")
|
|
}
|
|
}
|
|
|
|
func TestParseLimits(t *testing.T) {
|
|
_, err := Parse(strings.NewReader("a.example\nb.example\na.example\nc.example\n"), Limits{MaxEntries: 2})
|
|
if !errors.Is(err, ErrTooManyEntries) {
|
|
t.Errorf("err = %v", err)
|
|
}
|
|
if _, err := Parse(strings.NewReader("a.example\nb.example\na.example\n"), Limits{MaxEntries: 2}); err != nil {
|
|
t.Errorf("a repeated name counted against the limit: %v", err)
|
|
}
|
|
l, _ := Parse(strings.NewReader("long-name.example\nshort.example\n"), Limits{MaxNameLen: 15})
|
|
if len(l.Domains) != 1 || l.Skipped != 1 {
|
|
t.Errorf("max name length: %v", l.Domains)
|
|
}
|
|
}
|
|
|
|
func TestRuleEntries(t *testing.T) {
|
|
rules := []Rule{
|
|
{Name: "ads.example", Action: ActionBlock}, // and everything below
|
|
{Name: "cdn.ads.example", Action: ActionAllow, Exact: true}, // just this name
|
|
{Name: "wide.example", Action: ActionBlock}, // and below
|
|
{Name: "ok.wide.example", Action: ActionAllow}, // and below
|
|
{Name: "deep.x.y.wide.example", Action: ActionAllow, Exact: true}, // two empty names in between
|
|
{Name: "solo.example", Action: ActionBlock, Exact: true},
|
|
}
|
|
e := RuleEntries(rules)
|
|
for name, want := range map[string]string{
|
|
"ads.example": ActionBlock,
|
|
"x.ads.example": ActionBlock,
|
|
"cdn.ads.example": ActionAllow,
|
|
"y.cdn.ads.example": ActionBlock, // the wildcard above still applies
|
|
"ok.wide.example": ActionAllow,
|
|
"a.ok.wide.example": ActionAllow,
|
|
"y.wide.example": ActionBlock, // exists only because of a deeper rule
|
|
"x.y.wide.example": ActionBlock,
|
|
"z.x.y.wide.example": ActionBlock,
|
|
"deep.x.y.wide.example": ActionAllow,
|
|
"below.deep.x.y.wide.example": ActionBlock,
|
|
"solo.example": ActionBlock,
|
|
"other.example": "",
|
|
"sub.solo.example": "",
|
|
"example": "",
|
|
"unrelated.org": "",
|
|
} {
|
|
_, act, ok := MatchRules(e, name)
|
|
if !ok {
|
|
act = ""
|
|
}
|
|
if act != want {
|
|
t.Errorf("%s: %q, want %q", name, act, want)
|
|
}
|
|
}
|
|
z := RulesZoneFile("rules", rules)
|
|
if !strings.Contains(z, "cdn.ads.example\tCNAME\trpz-passthru.\n*.cdn.ads.example\tCNAME\t.\n") {
|
|
t.Errorf("rules zone:\n%s", z)
|
|
}
|
|
if strings.Index(z, "ads.example\t") > strings.Index(z, "*.ads.example\t") {
|
|
t.Errorf("a name should come before its wildcard:\n%s", z)
|
|
}
|
|
}
|
|
|
|
func TestMatchFeed(t *testing.T) {
|
|
l, _ := Parse(strings.NewReader("||tracker.example^\nexact.example\n||deep.a.example^\n"), Limits{})
|
|
Compact(l)
|
|
path := filepath.Join(t.TempDir(), "list.rpz")
|
|
f, _ := os.Create(path)
|
|
FeedZone(f, "t", 1, l)
|
|
f.Close()
|
|
for name, want := range map[string]string{
|
|
"tracker.example": "tracker.example",
|
|
"a.b.tracker.example": "*.tracker.example",
|
|
"exact.example": "exact.example",
|
|
"sub.exact.example": "",
|
|
"x.deep.a.example": "*.deep.a.example",
|
|
"a.example": "",
|
|
"example": "",
|
|
} {
|
|
owner, ok, err := MatchFeed(path, name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !ok {
|
|
owner = ""
|
|
}
|
|
if owner != want {
|
|
t.Errorf("%s: %q, want %q", name, owner, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNames(t *testing.T) {
|
|
if ListZone("hagezi") != "hagezi.blocklist.jarl.invalid" || ListName("hagezi.blocklist.jarl.invalid.") != "hagezi" || ListName(RulesZone) != "" || ListName("x.y.blocklist.jarl.invalid") != "" {
|
|
t.Error("zone names")
|
|
}
|
|
if n, err := NormalizeName("Ads.Example.COM."); err != nil || n != "ads.example.com" {
|
|
t.Errorf("NormalizeName = %q, %v", n, err)
|
|
}
|
|
for _, bad := range []string{"com", "1.2.3.4", "a b.example", "*.example.com", ""} {
|
|
if _, err := NormalizeName(bad); err == nil {
|
|
t.Errorf("%q accepted", bad)
|
|
}
|
|
}
|
|
}
|