milestone 21: abp list exceptions and a regex rule kind
This commit is contained in:
@@ -59,6 +59,7 @@ pub const StatusView = struct {
|
||||
last_error: []const u8,
|
||||
domains: u32,
|
||||
wildcards: u32,
|
||||
exceptions: u32,
|
||||
skipped_regex: u32,
|
||||
|
||||
pub fn from(status: *const manager_mod.SourceStatus) StatusView {
|
||||
@@ -72,6 +73,7 @@ pub const StatusView = struct {
|
||||
.last_error = status.errorText(),
|
||||
.domains = status.counts.domains,
|
||||
.wildcards = status.counts.wildcards,
|
||||
.exceptions = status.counts.exceptions,
|
||||
.skipped_regex = status.counts.skipped_regex,
|
||||
};
|
||||
}
|
||||
@@ -317,6 +319,7 @@ test "editing a blocklist keeps the counters the refresh wrote" {
|
||||
.last_updated = 1700,
|
||||
.domain_count = 42,
|
||||
.wildcard_count = 3,
|
||||
.exception_count = 2,
|
||||
.skipped_regex_count = 1,
|
||||
.checksum = "abc",
|
||||
});
|
||||
@@ -380,7 +383,7 @@ test "a status becomes the flat shape the API answers with" {
|
||||
const message = "connection refused";
|
||||
@memcpy(status.last_error[0..message.len], message);
|
||||
status.last_error_len = message.len;
|
||||
status.counts = .{ .domains = 10, .wildcards = 2, .skipped_regex = 1 };
|
||||
status.counts = .{ .domains = 10, .wildcards = 2, .exceptions = 4, .skipped_regex = 1 };
|
||||
|
||||
const view: StatusView = .from(&status);
|
||||
try testing.expectEqual(@as(i64, 7), view.id);
|
||||
@@ -389,4 +392,5 @@ test "a status becomes the flat shape the API answers with" {
|
||||
try testing.expectEqualStrings(url, view.url);
|
||||
try testing.expectEqualStrings(message, view.last_error);
|
||||
try testing.expectEqual(@as(u32, 10), view.domains);
|
||||
try testing.expectEqual(@as(u32, 4), view.exceptions);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ pub const Body = struct {
|
||||
reason: []const u8,
|
||||
/// The rule or list entry that decided it; "" when nothing matched.
|
||||
matched: []const u8,
|
||||
/// The list that decided it, which for `blocklist_exception` is the list
|
||||
/// whose `@@` rule lifted the block rather than one that made it.
|
||||
source_url: ?[]const u8,
|
||||
safe_search_rewrite: ?[]const u8,
|
||||
};
|
||||
@@ -50,7 +52,8 @@ pub const Result = struct {
|
||||
blocked: bool,
|
||||
reason: matcher.Reason,
|
||||
matched: []const u8,
|
||||
/// `blocklist_sources` row id of the list that matched.
|
||||
/// `blocklist_sources` row id of the list that matched, whether it blocked
|
||||
/// the name or lifted it through an `@@` exception.
|
||||
source_id: ?i64,
|
||||
safe_search_rewrite: ?[]const u8,
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ const Created = union(enum) { id: i64, fail: Failure };
|
||||
/// not understood.
|
||||
fn toInput(body: Body) union(enum) { input: rules_repo.RuleInput, fail: Failure } {
|
||||
const kind = model.RuleKind.fromDb(body.kind) orelse
|
||||
return .{ .fail = .{ .invalid = "kind must be 'exact' or 'wildcard'" } };
|
||||
return .{ .fail = .{ .invalid = "kind must be 'exact', 'wildcard' or 'regex'" } };
|
||||
const action = model.RuleAction.fromDb(body.action) orelse
|
||||
return .{ .fail = .{ .invalid = "action must be 'allow' or 'block'" } };
|
||||
return .{ .input = .{
|
||||
@@ -308,7 +308,7 @@ test "an unknown kind or action is a 400 before anything is written" {
|
||||
try testing.expect(toInput(.{
|
||||
.group_id = 1,
|
||||
.pattern = "ads.example",
|
||||
.kind = "regex",
|
||||
.kind = "glob",
|
||||
.action = "block",
|
||||
}).fail == .invalid);
|
||||
|
||||
@@ -327,4 +327,46 @@ test "an unknown kind or action is a 400 before anything is written" {
|
||||
});
|
||||
try testing.expectEqual(model.RuleKind.wildcard, good.input.kind);
|
||||
try testing.expectEqual(model.RuleAction.allow, good.input.action);
|
||||
|
||||
const third = toInput(.{
|
||||
.group_id = 1,
|
||||
.pattern = "^ad[0-9]+-",
|
||||
.kind = "regex",
|
||||
.action = "block",
|
||||
});
|
||||
try testing.expectEqual(model.RuleKind.regex, third.input.kind);
|
||||
}
|
||||
|
||||
test "a regex rule is stored, and a pattern the engine refuses is a 400 that names it" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
const created = try applyCreate(&bench.state, bench.io(), bench.arena(), .{
|
||||
.group_id = 1,
|
||||
.pattern = "^ad[0-9]+-",
|
||||
.kind = .regex,
|
||||
.action = .block,
|
||||
});
|
||||
const row = (try rules_repo.getRule(&bench.database, bench.arena(), created.id)).?;
|
||||
try testing.expectEqual(model.RuleKind.regex, row.kind);
|
||||
// Stored verbatim: a regex is not a name, so nothing lowercases or
|
||||
// dot-strips it on the way to the table.
|
||||
try testing.expectEqualStrings("^ad[0-9]+-", row.pattern);
|
||||
|
||||
const unclosed = try applyCreate(&bench.state, bench.io(), bench.arena(), .{
|
||||
.group_id = 1,
|
||||
.pattern = "(",
|
||||
.kind = .regex,
|
||||
.action = .block,
|
||||
});
|
||||
try testing.expectEqualStrings(
|
||||
"rules[0].pattern: '(' is not a valid regex pattern",
|
||||
unclosed.fail.invalid,
|
||||
);
|
||||
|
||||
// The refused pattern reached no table, and the good one is still the only
|
||||
// row: a 400 costs no write and no reload.
|
||||
try testing.expectEqual(@as(i64, 1), try bench.queryInt("SELECT count(*) FROM rules"));
|
||||
try testing.expectEqual(@as(usize, 1), bench.reloads);
|
||||
}
|
||||
|
||||
@@ -1876,7 +1876,7 @@ components:
|
||||
|
||||
Blocklist:
|
||||
type: object
|
||||
required: [id, url, name, enabled, is_suggested, last_updated, domain_count, wildcard_count, skipped_regex_count, checksum]
|
||||
required: [id, url, name, enabled, is_suggested, last_updated, domain_count, wildcard_count, exception_count, skipped_regex_count, checksum]
|
||||
properties:
|
||||
id: { type: integer }
|
||||
url: { type: string }
|
||||
@@ -1888,6 +1888,7 @@ components:
|
||||
nullable: true
|
||||
domain_count: { type: integer }
|
||||
wildcard_count: { type: integer }
|
||||
exception_count: { type: integer }
|
||||
skipped_regex_count: { type: integer }
|
||||
checksum:
|
||||
type: string
|
||||
@@ -1918,7 +1919,7 @@ components:
|
||||
|
||||
SourceStatus:
|
||||
type: object
|
||||
required: [id, state, loaded, last_attempt, last_success, url, last_error, domains, wildcards, skipped_regex]
|
||||
required: [id, state, loaded, last_attempt, last_success, url, last_error, domains, wildcards, exceptions, skipped_regex]
|
||||
properties:
|
||||
id: { type: integer }
|
||||
state:
|
||||
@@ -1933,6 +1934,7 @@ components:
|
||||
description: Empty when the last attempt succeeded.
|
||||
domains: { type: integer }
|
||||
wildcards: { type: integer }
|
||||
exceptions: { type: integer }
|
||||
skipped_regex: { type: integer }
|
||||
|
||||
Rule:
|
||||
@@ -1945,7 +1947,7 @@ components:
|
||||
pattern: { type: string }
|
||||
kind:
|
||||
type: string
|
||||
enum: [exact, wildcard]
|
||||
enum: [exact, wildcard, regex]
|
||||
action:
|
||||
type: string
|
||||
enum: [allow, block]
|
||||
@@ -1959,7 +1961,7 @@ components:
|
||||
pattern: { type: string }
|
||||
kind:
|
||||
type: string
|
||||
enum: [exact, wildcard]
|
||||
enum: [exact, wildcard, regex]
|
||||
action:
|
||||
type: string
|
||||
enum: [allow, block]
|
||||
@@ -1973,7 +1975,7 @@ components:
|
||||
pattern: { type: string }
|
||||
kind:
|
||||
type: string
|
||||
enum: [exact, wildcard]
|
||||
enum: [exact, wildcard, regex]
|
||||
action:
|
||||
type: string
|
||||
enum: [allow, block]
|
||||
|
||||
@@ -1665,6 +1665,9 @@ fn createdId(body: []const u8) !i64 {
|
||||
return std.fmt.parseInt(i64, rest[0..end], 10);
|
||||
}
|
||||
|
||||
/// The three files a refresh publishes for one source. The `.allow` file is
|
||||
/// written here too: the delete path has to take every compiled body, and a
|
||||
/// sweep that missed one would leave an orphan this test could not see.
|
||||
fn writeCompiled(io: std.Io, dir: std.Io.Dir, id: i64, body: []const u8) !void {
|
||||
var buf: [64]u8 = undefined;
|
||||
try dir.writeFile(io, .{
|
||||
@@ -1675,6 +1678,10 @@ fn writeCompiled(io: std.Io, dir: std.Io.Dir, id: i64, body: []const u8) !void {
|
||||
.sub_path = try std.fmt.bufPrint(&buf, "{d}.wild", .{id}),
|
||||
.data = "",
|
||||
});
|
||||
try dir.writeFile(io, .{
|
||||
.sub_path = try std.fmt.bufPrint(&buf, "{d}.allow", .{id}),
|
||||
.data = "",
|
||||
});
|
||||
}
|
||||
|
||||
fn accessCompiled(io: std.Io, dir: std.Io.Dir, id: i64) !void {
|
||||
@@ -1726,6 +1733,11 @@ fn deleteSweepsCompiledFiles(io: std.Io, env: *Env) anyerror!void {
|
||||
try std.fmt.bufPrint(&name_buf, "{d}.wild", .{doomed}),
|
||||
.{},
|
||||
));
|
||||
try testing.expectError(error.FileNotFound, dir.access(
|
||||
io,
|
||||
try std.fmt.bufPrint(&name_buf, "{d}.allow", .{doomed}),
|
||||
.{},
|
||||
));
|
||||
try accessCompiled(io, dir, kept);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user