milestone 16: behavioral fixes for silent failures, locks, counters and the query log
This commit is contained in:
+104
-1
@@ -85,13 +85,43 @@ pub fn isComment(line: []const u8) bool {
|
||||
|
||||
/// The element-hiding separators, which may also follow a domain list
|
||||
/// (`example.com##.ad-banner`).
|
||||
///
|
||||
/// Where the separator sits decides, because `#` is also the hosts comment
|
||||
/// marker and a hosts file banner is drawn out of the same two characters. A
|
||||
/// `##` counts only where an element-hiding rule can put one: at the start of
|
||||
/// the line with a selector behind it, or straight after the domain list it
|
||||
/// applies to. `## Title`, `####` and `see ## below` are therefore text, and a
|
||||
/// hosts file that opens with a banner keeps sniffing as hosts.
|
||||
///
|
||||
/// Guarding this with `isComment` instead would decide nothing: `isComment`
|
||||
/// asks this function.
|
||||
pub fn isElementHiding(line: []const u8) bool {
|
||||
for ([_][]const u8{ "##", "#@#", "#?#", "#$#", "#%#" }) |marker| {
|
||||
if (std.mem.find(u8, line, marker) != null) return true;
|
||||
var from: usize = 0;
|
||||
while (std.mem.find(u8, line[from..], marker)) |offset| {
|
||||
const at = from + offset;
|
||||
if (separatorStartsRule(line, at, marker.len)) return true;
|
||||
from = at + 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Whether the separator of `marker_len` bytes at `at` is a rule's separator
|
||||
/// rather than two characters of prose.
|
||||
fn separatorStartsRule(line: []const u8, at: usize, marker_len: usize) bool {
|
||||
if (at == 0) {
|
||||
// A generic rule carries its selector here. A banner carries a space,
|
||||
// another `#`, or nothing at all.
|
||||
if (line.len == marker_len) return false;
|
||||
const after = line[marker_len];
|
||||
return after != '#' and !std.ascii.isWhitespace(after);
|
||||
}
|
||||
// A domain list ends where the separator begins, with no space between.
|
||||
const before = line[at - 1];
|
||||
return before != '#' and !std.ascii.isWhitespace(before);
|
||||
}
|
||||
|
||||
fn hasAbpMarker(line: []const u8) bool {
|
||||
if (std.mem.startsWith(u8, line, "||")) return true;
|
||||
if (std.mem.startsWith(u8, line, "@@")) return true;
|
||||
@@ -188,6 +218,79 @@ test "detectFormat is not fooled by a dollar sign in a comment" {
|
||||
try testing.expectEqual(Format.hosts, detectFormat(sample));
|
||||
}
|
||||
|
||||
/// The banner style a hosts list published for malware URLs opens with: a rule
|
||||
/// of `#` characters around a titled header block. Every line of it contains
|
||||
/// `##`, and reading those as element hiding used to sniff the whole file as
|
||||
/// ABP — which put `0.0.0.0` in the domain set and dropped every hosts line
|
||||
/// that carried an inline comment.
|
||||
const urlhaus_banner_sample =
|
||||
\\################################################################
|
||||
\\# URLhaus Malicious Hosts File #
|
||||
\\# Last updated: 2026-08-05 06:05:04 (UTC) #
|
||||
\\# #
|
||||
\\# Terms Of Use: https://urlhaus.abuse.ch/api/ #
|
||||
\\################################################################
|
||||
\\0.0.0.0 bad1.example.com # https://urlhaus.abuse.ch/url/1/
|
||||
\\0.0.0.0 bad2.example.net # https://urlhaus.abuse.ch/url/2/
|
||||
\\0.0.0.0 bad3.example.org # https://urlhaus.abuse.ch/url/3/
|
||||
\\
|
||||
;
|
||||
|
||||
test "detectFormat reads a hosts file behind a hash banner as hosts" {
|
||||
try testing.expectEqual(Format.hosts, detectFormat(urlhaus_banner_sample));
|
||||
|
||||
// The lines the banner is made of are comments, so the sample the format
|
||||
// is decided from is the three hosts lines alone.
|
||||
var it = std.mem.splitScalar(u8, urlhaus_banner_sample, '\n');
|
||||
while (it.next()) |line| {
|
||||
if (line.len == 0) continue;
|
||||
if (line[0] != '#') continue;
|
||||
try testing.expect(isComment(line));
|
||||
try testing.expect(!isElementHiding(line));
|
||||
}
|
||||
}
|
||||
|
||||
test "detectFormat still recognizes a generic element-hiding rule" {
|
||||
const sample =
|
||||
\\##.ad-banner
|
||||
\\example.com
|
||||
\\
|
||||
;
|
||||
try testing.expectEqual(Format.abp, detectFormat(sample));
|
||||
try testing.expect(isElementHiding("##.ad-banner"));
|
||||
}
|
||||
|
||||
test "detectFormat recognizes element hiding after a domain list" {
|
||||
const sample =
|
||||
\\example.com##.ad
|
||||
\\other.example.net
|
||||
\\
|
||||
;
|
||||
try testing.expectEqual(Format.abp, detectFormat(sample));
|
||||
try testing.expect(isElementHiding("example.com##.ad"));
|
||||
}
|
||||
|
||||
test "detectFormat recognizes an exception separator after a domain" {
|
||||
const sample =
|
||||
\\example.com#@#.sponsored
|
||||
\\other.example.net
|
||||
\\
|
||||
;
|
||||
try testing.expectEqual(Format.abp, detectFormat(sample));
|
||||
try testing.expect(isElementHiding("example.com#@#.sponsored"));
|
||||
}
|
||||
|
||||
test "a comment line that mentions a separator stays a comment" {
|
||||
const line = "# the ##.ad rules live in the other list";
|
||||
try testing.expect(isComment(line));
|
||||
try testing.expect(!isElementHiding(line));
|
||||
|
||||
// A bare separator and a rule of hashes are text as well.
|
||||
try testing.expect(!isElementHiding("##"));
|
||||
try testing.expect(!isElementHiding("####"));
|
||||
try testing.expect(!isElementHiding("## Title"));
|
||||
}
|
||||
|
||||
test "parseLine dispatches to the hosts parser" {
|
||||
const line = parseLine(.hosts, "0.0.0.0 ads.example.com");
|
||||
try testing.expectEqual(Kind.domain, line.kind);
|
||||
|
||||
Reference in New Issue
Block a user