milestone 21: abp list exceptions and a regex rule kind
This commit is contained in:
+111
-4
@@ -44,6 +44,7 @@ const Writer = std.Io.Writer;
|
||||
const model = @import("model.zig");
|
||||
const address = @import("../platform/address.zig");
|
||||
const dns_name = @import("../dns/name.zig");
|
||||
const regex = @import("../filter/regex.zig");
|
||||
const safe_url = @import("../safe_url.zig");
|
||||
const transport = @import("../upstream/transport.zig");
|
||||
|
||||
@@ -890,13 +891,31 @@ fn checkCollections(cfg: Config, diags: *Diagnostics, scratch: Allocator) error{
|
||||
|
||||
for (cfg.rules, 0..) |rule, i| {
|
||||
try checkGroupRef(diags, &group_names, rule.group, "rules[{d}].group", .{i});
|
||||
if (!try patternIsValid(scratch, rule.pattern, rule.kind)) {
|
||||
// `null` is a good pattern; anything else is the sentence fragment that
|
||||
// says which of the regex engine's limits refused it. The empty string
|
||||
// is a plain syntax refusal, which is the only verdict the exact and
|
||||
// wildcard kinds can reach.
|
||||
const detail: ?[]const u8 = if (patternIsValid(scratch, rule.pattern, rule.kind)) |valid|
|
||||
(if (valid) null else "")
|
||||
else |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.BadPattern => "",
|
||||
error.PatternTooLong => std.fmt.comptimePrint(
|
||||
" (over {d} bytes)",
|
||||
.{regex.max_pattern_len},
|
||||
),
|
||||
error.PatternTooComplex => std.fmt.comptimePrint(
|
||||
" (over {d} compiled instructions)",
|
||||
.{regex.max_program_len},
|
||||
),
|
||||
};
|
||||
if (detail) |suffix| {
|
||||
try diags.add(
|
||||
error.BadRulePattern,
|
||||
"rules[{d}].pattern",
|
||||
.{i},
|
||||
"{f} is not a valid {s} pattern",
|
||||
.{ safe_url.quoteText(rule.pattern), rule.kind.toDb() },
|
||||
"{f} is not a valid {s} pattern{s}",
|
||||
.{ safe_url.quoteText(rule.pattern), rule.kind.toDb(), suffix },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1096,11 +1115,18 @@ fn sourceUrlIsValid(url: []const u8) bool {
|
||||
/// Syntax only. Matching semantics are Phase 5's: an exact pattern carries no
|
||||
/// `*` at all, a wildcard pattern carries at least one label that is exactly
|
||||
/// `*`, and every remaining label must survive `dns.name.fromText`.
|
||||
///
|
||||
/// A regex pattern is validated by compiling it, and its three refusals arrive
|
||||
/// as errors rather than as `false` so the caller can name the one that fired.
|
||||
/// The distinction is the operator's, not the compiler's: "not a valid regex
|
||||
/// pattern" sends someone hunting for a typo in a pattern whose only fault is
|
||||
/// that it is longer than `regex.max_pattern_len` or wider than
|
||||
/// `regex.max_program_len`, and neither limit is visible in the pattern text.
|
||||
fn patternIsValid(
|
||||
scratch: Allocator,
|
||||
pattern: []const u8,
|
||||
kind: model.RuleKind,
|
||||
) error{OutOfMemory}!bool {
|
||||
) regex.Error!bool {
|
||||
switch (kind) {
|
||||
.exact => {
|
||||
if (std.mem.findScalar(u8, pattern, '*') != null) return false;
|
||||
@@ -1126,6 +1152,11 @@ fn patternIsValid(
|
||||
_ = dns_name.fromText(substituted.items) catch return false;
|
||||
return true;
|
||||
},
|
||||
.regex => {
|
||||
var program = try regex.compile(scratch, pattern);
|
||||
program.deinit(scratch);
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2105,6 +2136,82 @@ test "rule patterns accept wildcards only when the kind says so" {
|
||||
try expectProblem(bad_label, error.BadRulePattern, "rules[0].pattern");
|
||||
}
|
||||
|
||||
/// The message of the first failure, so a test can assert the sentence an
|
||||
/// operator reads and not only the error tag.
|
||||
fn expectMessage(cfg: Config, expected: ValidateError, expected_message: []const u8) !void {
|
||||
var diags: Diagnostics = .init(testing.allocator);
|
||||
defer diags.deinit();
|
||||
try testing.expectError(expected, validate(cfg, &diags));
|
||||
const failure = diags.firstFailure() orelse return error.TestExpectedFailure;
|
||||
try testing.expectEqualStrings(expected_message, failure.message);
|
||||
}
|
||||
|
||||
/// The tail of the first failure's message. `quoteText` truncates the value it
|
||||
/// quotes at `safe_url.max_len`, so a diagnostic about an over-long pattern
|
||||
/// cannot be matched whole.
|
||||
fn expectMessageSuffix(cfg: Config, expected: ValidateError, expected_suffix: []const u8) !void {
|
||||
var diags: Diagnostics = .init(testing.allocator);
|
||||
defer diags.deinit();
|
||||
try testing.expectError(expected, validate(cfg, &diags));
|
||||
const failure = diags.firstFailure() orelse return error.TestExpectedFailure;
|
||||
if (!std.mem.endsWith(u8, failure.message, expected_suffix)) {
|
||||
std.debug.print("message {s} does not end with {s}\n", .{ failure.message, expected_suffix });
|
||||
return error.TestExpectedEqual;
|
||||
}
|
||||
}
|
||||
|
||||
fn regexRule(pattern: []const u8) [1]model.Rule {
|
||||
return .{.{ .group = "default", .pattern = pattern, .kind = .regex, .action = .block }};
|
||||
}
|
||||
|
||||
test "a regex rule is validated by compiling it" {
|
||||
var cfg = baseConfig();
|
||||
const good = regexRule("^ad[0-9]+-\\.(example|test)\\.com$");
|
||||
cfg.rules = &good;
|
||||
try expectClean(cfg);
|
||||
|
||||
// A regex is not a name: the wildcard and exact kinds reject `*`, and this
|
||||
// one has to accept the characters that make a pattern a pattern.
|
||||
const starred = regexRule("ads.*\\.example");
|
||||
cfg.rules = &starred;
|
||||
try expectClean(cfg);
|
||||
}
|
||||
|
||||
test "each of the regex engine's three refusals names itself in the diagnostic" {
|
||||
var cfg = baseConfig();
|
||||
|
||||
const unclosed = regexRule("(");
|
||||
cfg.rules = &unclosed;
|
||||
try expectProblem(cfg, error.BadRulePattern, "rules[0].pattern");
|
||||
try expectMessage(cfg, error.BadRulePattern, "'(' is not a valid regex pattern");
|
||||
|
||||
// Too long and too complex are the two an operator cannot see by reading
|
||||
// the pattern, so the message has to carry the limit that fired.
|
||||
const too_long = regexRule("a" ** (regex.max_pattern_len + 1));
|
||||
cfg.rules = &too_long;
|
||||
try expectMessageSuffix(
|
||||
cfg,
|
||||
error.BadRulePattern,
|
||||
"is not a valid regex pattern (over 256 bytes)",
|
||||
);
|
||||
|
||||
// Well inside 256 bytes of pattern, well past 1024 instructions of program.
|
||||
const too_complex = regexRule("(abcdefghij){200}");
|
||||
cfg.rules = &too_complex;
|
||||
try expectMessage(
|
||||
cfg,
|
||||
error.BadRulePattern,
|
||||
"'(abcdefghij){200}' is not a valid regex pattern (over 1024 compiled instructions)",
|
||||
);
|
||||
}
|
||||
|
||||
test "an empty regex pattern is refused rather than matching every name" {
|
||||
var cfg = baseConfig();
|
||||
const empty = regexRule("");
|
||||
cfg.rules = ∅
|
||||
try expectProblem(cfg, error.BadRulePattern, "rules[0].pattern");
|
||||
}
|
||||
|
||||
test "parseResolver accepts udp and tcp with an IP literal and a port" {
|
||||
const udp4 = try parseResolver("udp://192.168.1.1:53");
|
||||
try testing.expectEqual(ResolverScheme.udp, udp4.scheme);
|
||||
|
||||
Reference in New Issue
Block a user