activity: select-only multi-client filter
the freetype client field is gone. the picker is a select-only menu of known clients with multi-select, chips for the active set, and a 32-client cap shared with the server. the api accepts a comma-separated client list and filters any-of with bound parameters. the trigger carets come from phosphor icons, newly adopted. bundle budget rises to 900000 bytes for the picker and the icon dependency.
This commit is contained in:
@@ -32,12 +32,34 @@ pub const max_limit: u32 = queries_repo.max_limit;
|
||||
pub const max_domain_len = logger.max_domain_len;
|
||||
pub const max_client_len = logger.max_client_len;
|
||||
|
||||
/// How many addresses one `client` parameter may name, the repository's cap.
|
||||
pub const max_clients = queries_repo.max_clients;
|
||||
|
||||
/// The longest `client` value that can decode to a full list: every address at
|
||||
/// its width, separated by commas.
|
||||
pub const max_client_list_len = max_clients * max_client_len + (max_clients - 1);
|
||||
|
||||
/// The buffer that value needs.
|
||||
///
|
||||
/// `queryValue` measures the value as it arrives and only then decodes it in
|
||||
/// place, so the buffer is sized for the percent-encoded form rather than the
|
||||
/// decoded one. A browser encodes the separating commas, and every colon of an
|
||||
/// IPv6 address with them, so a legal near-cap selection of IPv6 clients would
|
||||
/// be refused by a buffer sized for what it decodes to. Three bytes per byte is
|
||||
/// the worst `%XX` can do.
|
||||
const max_client_value_len = 3 * max_client_list_len;
|
||||
|
||||
/// Where the two string filters are copied to. The parsed filter borrows them,
|
||||
/// so it must not outlive the buffers — in the handler both live in the same
|
||||
/// stack frame.
|
||||
///
|
||||
/// `client` holds the raw comma-separated value and `clients` indexes into it,
|
||||
/// so the addresses are never copied a second time: the filter's slices point
|
||||
/// into the same bytes the query string was decoded into.
|
||||
pub const Buffers = struct {
|
||||
domain: [max_domain_len]u8 = undefined,
|
||||
client: [max_client_len]u8 = undefined,
|
||||
client: [max_client_value_len]u8 = undefined,
|
||||
clients: [max_clients][]const u8 = undefined,
|
||||
};
|
||||
|
||||
pub const Page = struct {
|
||||
@@ -82,8 +104,22 @@ pub fn parseFilter(query: []const u8, buffers: *Buffers) FilterError!queries_rep
|
||||
if (domain.len != 0) filter.domain_substring = domain;
|
||||
}
|
||||
|
||||
if (http_util.queryValue(query, "client", &buffers.client) catch return error.BadClient) |client| {
|
||||
if (client.len != 0) filter.client = client;
|
||||
// A comma-separated list of exact addresses, matched any-of. An empty entry
|
||||
// is a malformed list rather than a filter to drop: `?client=a,,b` is a
|
||||
// client bug, and answering it as `a,b` would hide the bug behind an answer
|
||||
// to a question nobody asked.
|
||||
if (http_util.queryValue(query, "client", &buffers.client) catch return error.BadClient) |raw| {
|
||||
if (raw.len != 0) {
|
||||
var count: usize = 0;
|
||||
var it = std.mem.splitScalar(u8, raw, ',');
|
||||
while (it.next()) |entry| {
|
||||
if (entry.len == 0 or entry.len > max_client_len) return error.BadClient;
|
||||
if (count == max_clients) return error.BadClient;
|
||||
buffers.clients[count] = entry;
|
||||
count += 1;
|
||||
}
|
||||
filter.clients = buffers.clients[0..count];
|
||||
}
|
||||
}
|
||||
|
||||
filter.blocked = http_util.queryBool(query, "blocked") catch return error.BadBlocked;
|
||||
@@ -98,7 +134,7 @@ pub fn message(err: FilterError) []const u8 {
|
||||
error.BadLimit => "limit must be between 1 and 1000",
|
||||
error.BadBefore => "before must be a positive row id",
|
||||
error.BadDomain => "domain is not a valid filter",
|
||||
error.BadClient => "client is not a valid filter",
|
||||
error.BadClient => "client must be up to 32 comma-separated client addresses",
|
||||
error.BadBlocked => "blocked must be true or false",
|
||||
error.BadSince => "since must be a unix timestamp in seconds",
|
||||
error.BadUntil => "until must be a unix timestamp in seconds",
|
||||
@@ -228,7 +264,8 @@ test "every filter reaches the repository untouched" {
|
||||
try testing.expectEqual(@as(u32, 250), filter.limit);
|
||||
try testing.expectEqual(@as(?i64, 900), filter.before);
|
||||
try testing.expectEqualStrings("ads.example", filter.domain_substring.?);
|
||||
try testing.expectEqualStrings("192.0.2.10", filter.client.?);
|
||||
try testing.expectEqual(@as(usize, 1), filter.clients.len);
|
||||
try testing.expectEqualStrings("192.0.2.10", filter.clients[0]);
|
||||
try testing.expectEqual(@as(?bool, true), filter.blocked);
|
||||
try testing.expectEqual(@as(?i64, 100), filter.since);
|
||||
try testing.expectEqual(@as(?i64, 200), filter.until);
|
||||
@@ -238,7 +275,48 @@ test "an empty string filter is no filter at all" {
|
||||
var buffers: Buffers = .{};
|
||||
const filter = try parseFilter("domain=&client=", &buffers);
|
||||
try testing.expectEqual(@as(?[]const u8, null), filter.domain_substring);
|
||||
try testing.expectEqual(@as(?[]const u8, null), filter.client);
|
||||
try testing.expectEqual(@as(usize, 0), filter.clients.len);
|
||||
}
|
||||
|
||||
test "a client list is several exact addresses and each entry must be well formed" {
|
||||
var buffers: Buffers = .{};
|
||||
const several = try parseFilter("client=192.0.2.10,192.0.2.11,192.0.2.12", &buffers);
|
||||
try testing.expectEqual(@as(usize, 3), several.clients.len);
|
||||
try testing.expectEqualStrings("192.0.2.10", several.clients[0]);
|
||||
try testing.expectEqualStrings("192.0.2.11", several.clients[1]);
|
||||
try testing.expectEqualStrings("192.0.2.12", several.clients[2]);
|
||||
|
||||
// An empty entry would silently widen the filter, so it is malformed input.
|
||||
try testing.expectError(error.BadClient, parseFilter("client=192.0.2.10,", &buffers));
|
||||
try testing.expectError(error.BadClient, parseFilter("client=,192.0.2.10", &buffers));
|
||||
try testing.expectError(error.BadClient, parseFilter("client=192.0.2.10,,192.0.2.11", &buffers));
|
||||
|
||||
var many: std.ArrayList(u8) = .empty;
|
||||
defer many.deinit(testing.allocator);
|
||||
try many.appendSlice(testing.allocator, "client=192.0.2.1");
|
||||
for (0..max_clients) |_| try many.appendSlice(testing.allocator, ",192.0.2.1");
|
||||
try testing.expectError(error.BadClient, parseFilter(many.items, &buffers));
|
||||
}
|
||||
|
||||
test "a full list of percent-encoded IPv6 clients fits" {
|
||||
// A browser encodes the separators and every colon it puts between them, so
|
||||
// the value on the wire is several times the length of what it decodes to.
|
||||
// Sizing the buffer for the decoded form refuses a selection that is legal.
|
||||
const address = "2001:0db8:0000:0000:0000:0000:0000:0001";
|
||||
const encoded = "2001%3A0db8%3A0000%3A0000%3A0000%3A0000%3A0000%3A0001";
|
||||
|
||||
var query: std.ArrayList(u8) = .empty;
|
||||
defer query.deinit(testing.allocator);
|
||||
try query.appendSlice(testing.allocator, "client=");
|
||||
for (0..max_clients) |index| {
|
||||
if (index != 0) try query.appendSlice(testing.allocator, "%2C");
|
||||
try query.appendSlice(testing.allocator, encoded);
|
||||
}
|
||||
|
||||
var buffers: Buffers = .{};
|
||||
const filter = try parseFilter(query.items, &buffers);
|
||||
try testing.expectEqual(@as(usize, max_clients), filter.clients.len);
|
||||
for (filter.clients) |entry| try testing.expectEqualStrings(address, entry);
|
||||
}
|
||||
|
||||
test "each malformed parameter names itself in a 400" {
|
||||
|
||||
Reference in New Issue
Block a user