fediglam/src/http/urlencode.zig

385 lines
15 KiB
Zig

const std = @import("std");
const util = @import("util");
pub const Iter = struct {
const Pair = struct {
key: []const u8,
value: ?[]const u8,
};
iter: std.mem.SplitIterator(u8),
pub fn from(q: []const u8) Iter {
return Iter{
.iter = std.mem.split(u8, std.mem.trimLeft(u8, q, "?"), "&"),
};
}
pub fn next(self: *Iter) ?Pair {
while (true) {
const part = self.iter.next() orelse return null;
if (part.len == 0) continue;
const key = std.mem.sliceTo(part, '=');
if (key.len == part.len) return Pair{
.key = key,
.value = null,
};
return Pair{
.key = key,
.value = part[key.len + 1 ..],
};
}
}
};
/// Parses a set of query parameters described by the struct `T`.
///
/// To specify query parameters, provide a struct similar to the following:
/// ```
/// struct {
/// foo: bool = false,
/// bar: ?[]const u8 = null,
/// baz: usize = 10,
/// qux: enum { quux, snap } = .quux,
/// }
/// ```
///
/// This will allow it to parse a query string like the following:
/// `?foo&bar=abc&qux=snap`
///
/// Every parameter must have a default value that will be used when the
/// parameter is not provided, and parameter keys.
/// Numbers are parsed from their string representations, and a parameter
/// provided in the query string without a value is parsed either as a bool
/// `true` flag or as `null` depending on the type of its param.
///
/// Parameter types supported:
/// - []const u8
/// - numbers (both integer and float)
/// + Numbers are parsed in base 10
/// - bool
/// + See below for detals
/// - exhaustive enums
/// + Enums are treated as strings with values equal to the enum fields
/// - ?F (where isScalar(F) and F != bool)
/// - Any type that implements:
/// + pub fn parse([]const u8) !F
///
/// Boolean Parameters:
/// The following query strings will all parse a `true` value for the
/// parameter `foo: bool = false`:
/// - `?foo`
/// - `?foo=true`
/// - `?foo=t`
/// - `?foo=yes`
/// - `?foo=y`
/// - `?foo=1`
/// And the following query strings all parse a `false` value:
/// - `?`
/// - `?foo=false`
/// - `?foo=f`
/// - `?foo=no`
/// - `?foo=n`
/// - `?foo=0`
///
/// Compound Types:
/// Compound (struct) types are also supported, with the parameter key
/// for its parameters consisting of the struct's field + '.' + parameter
/// field. For example:
/// ```
/// struct {
/// foo: struct {
/// baz: usize = 0,
/// } = .{},
/// }
/// ```
/// Would be used to parse a query string like
/// `?foo.baz=12345`
///
pub fn parse(alloc: std.mem.Allocator, allow_unknown_fields: bool, comptime T: type, query: []const u8) !T {
var iter = Iter.from(query);
var deserializer = Deserializer(T).init(alloc);
defer deserializer.deinit();
while (iter.next()) |pair| {
try deserializer.setSerializedField(pair.key, pair.value);
deserializer.setSerializedField(pair.key, pair.value) catch |err| switch (err) {
error.UnknownField => if (allow_unknown_fields) continue else return err,
else => |e| return e,
};
}
return try deserializer.finish(alloc);
}
fn Deserializer(comptime Result: type) type {
return util.DeserializerContext(Result, ?[]const u8, struct {
pub const options = util.serialize.default_options;
pub fn deserializeScalar(_: @This(), alloc: std.mem.Allocator, comptime T: type, maybe_val: ?[]const u8) !T {
const is_optional = comptime std.meta.trait.is(.Optional)(T);
if (maybe_val) |val| {
if (val.len == 0 and is_optional) return null;
const decoded = try decodeString(alloc, val);
defer alloc.free(decoded);
return try util.serialize.deserializeString(alloc, T, decoded);
} else {
// If param is present, but without an associated value
return if (is_optional)
null
else if (T == bool)
true
else
error.InvalidValue;
}
}
});
}
pub fn parseFree(alloc: std.mem.Allocator, val: anytype) void {
util.deepFree(alloc, val);
}
fn decodeString(alloc: std.mem.Allocator, val: []const u8) ![]u8 {
var list = try std.ArrayList(u8).initCapacity(alloc, val.len);
errdefer list.deinit();
var idx: usize = 0;
while (idx < val.len) : (idx += 1) {
if (val[idx] != '%') {
try list.append(val[idx]);
} else {
if (val.len < idx + 2) return error.InvalidEscape;
const buf = [2]u8{ val[idx + 1], val[idx + 2] };
idx += 2;
const ch = try std.fmt.parseInt(u8, &buf, 16);
try list.append(ch);
}
}
return list.toOwnedSlice();
}
fn isScalar(comptime T: type) bool {
if (comptime std.meta.trait.isZigString(T)) return true;
if (comptime std.meta.trait.isIntegral(T)) return true;
if (comptime std.meta.trait.isFloat(T)) return true;
if (comptime std.meta.trait.is(.Enum)(T)) return true;
if (comptime std.meta.trait.is(.EnumLiteral)(T)) return true;
if (T == bool) return true;
if (comptime std.meta.trait.hasFn("parse")(T)) return true;
if (comptime std.meta.trait.is(.Optional)(T) and isScalar(std.meta.Child(T))) return true;
return false;
}
pub fn EncodeStruct(comptime Params: type) type {
return struct {
params: Params,
pub fn format(v: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
try formatQuery("", "", v.params, writer);
}
};
}
pub fn encodeStruct(val: anytype) EncodeStruct(@TypeOf(val)) {
return EncodeStruct(@TypeOf(val)){ .params = val };
}
fn urlFormatString(writer: anytype, val: []const u8) !void {
for (val) |ch| {
const printable = switch (ch) {
'0'...'9', 'a'...'z', 'A'...'Z' => true,
'-', '.', '_', '~', ':', '@', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
else => false,
};
try if (printable) writer.writeByte(ch) else std.fmt.format(writer, "%{x:0>2}", .{ch});
}
}
fn formatScalar(comptime name: []const u8, val: anytype, writer: anytype) !void {
const T = @TypeOf(val);
if (comptime std.meta.trait.is(.Optional)(T)) {
return if (val) |v| formatScalar(name, v, writer) else {};
}
try urlFormatString(writer, name);
try writer.writeByte('=');
if (comptime std.meta.trait.isZigString(T)) {
try urlFormatString(writer, val);
} else try switch (@typeInfo(T)) {
.EnumLiteral, .Enum => urlFormatString(writer, @tagName(val)),
else => std.fmt.format(writer, "{}", .{val}),
};
try writer.writeByte('&');
}
fn formatQuery(comptime prefix: []const u8, comptime name: []const u8, params: anytype, writer: anytype) !void {
const T = @TypeOf(params);
const eff_prefix = if (prefix.len == 0) "" else prefix ++ ".";
if (comptime isScalar(T)) return formatScalar(eff_prefix ++ name, params, writer);
switch (@typeInfo(T)) {
.Struct => {
inline for (std.meta.fields(T)) |field| {
const val = @field(params, field.name);
try formatQuery(eff_prefix ++ name, field.name, val, writer);
}
},
.Union => {
inline for (std.meta.fields(T)) |field| {
const tag = @field(std.meta.Tag(T), field.name);
const tag_name = field.name;
if (@as(std.meta.Tag(T), params) == tag) {
const val = @field(params, tag_name);
try formatQuery(prefix, tag_name, val, writer);
}
}
},
.Optional => {
if (params) |p| try formatQuery(prefix, name, p, writer);
},
else => @compileError("Unsupported query type"),
}
}
test "parse" {
const testCase = struct {
fn case(comptime T: type, expected: T, query_string: []const u8) !void {
const result = try parse(std.testing.allocator, T, query_string);
defer parseFree(std.testing.allocator, result);
try util.testing.expectDeepEqual(expected, result);
}
}.case;
try testCase(struct { int: usize = 3 }, .{ .int = 3 }, "");
try testCase(struct { int: usize = 3 }, .{ .int = 2 }, "int=2");
try testCase(struct { int: usize = 3 }, .{ .int = 2 }, "int=2&");
try testCase(struct { boolean: bool = false }, .{ .boolean = false }, "");
try testCase(struct { boolean: bool = false }, .{ .boolean = true }, "boolean");
try testCase(struct { boolean: bool = false }, .{ .boolean = true }, "boolean=true");
try testCase(struct { boolean: bool = false }, .{ .boolean = true }, "boolean=y");
try testCase(struct { boolean: bool = false }, .{ .boolean = false }, "boolean=f");
try testCase(struct { boolean: bool = false }, .{ .boolean = false }, "boolean=no");
try testCase(struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = null }, "");
try testCase(struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = .foo }, "str_enum=foo");
try testCase(struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = .bar }, "str_enum=bar");
try testCase(struct { str_enum: ?enum { foo, bar } = .foo }, .{ .str_enum = .foo }, "");
try testCase(struct { str_enum: ?enum { foo, bar } = .foo }, .{ .str_enum = null }, "str_enum");
try testCase(struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&n2=2");
try testCase(struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&n2=2&");
try testCase(struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&&n2=2&");
try testCase(struct { str: ?[]const u8 = null }, .{ .str = null }, "");
try testCase(struct { str: ?[]const u8 = null }, .{ .str = null }, "str");
try testCase(struct { str: ?[]const u8 = null }, .{ .str = null }, "str=");
try testCase(struct { str: ?[]const u8 = null }, .{ .str = "foo" }, "str=foo");
try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = "foo" }, "str=foo");
try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = "foo" }, "");
try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = null }, "str");
try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = null }, "str=");
const rand_uuid = comptime util.Uuid.parse("c1fb6578-4d0c-4eb9-9f67-d56da3ae6f5d") catch unreachable;
try testCase(struct { id: ?util.Uuid = null }, .{ .id = null }, "");
try testCase(struct { id: ?util.Uuid = null }, .{ .id = null }, "id=");
try testCase(struct { id: ?util.Uuid = null }, .{ .id = null }, "id");
try testCase(struct { id: ?util.Uuid = null }, .{ .id = rand_uuid }, "id=" ++ rand_uuid.toCharArray());
try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = rand_uuid }, "");
try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = null }, "id=");
try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = null }, "id");
try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = rand_uuid }, "id=" ++ rand_uuid.toCharArray());
const SubStruct = struct {
sub: struct {
foo: usize = 1,
bar: usize = 2,
} = .{},
};
try testCase(SubStruct, .{ .sub = .{ .foo = 1, .bar = 2 } }, "");
try testCase(SubStruct, .{ .sub = .{ .foo = 3, .bar = 3 } }, "sub.foo=3&sub.bar=3");
try testCase(SubStruct, .{ .sub = .{ .foo = 3, .bar = 2 } }, "sub.foo=3");
// TODO: Semantics are ill-defined here. What happens if the substruct doesn't have
// default values?
// const SubStruct2 = struct {
// sub: ?struct {
// foo: usize = 1,
// } = null,
// };
// try testCase(SubStruct2, .{ .sub = null }, "");
// try testCase(SubStruct2, .{ .sub = null }, "sub=");
// TODO: also here (semantics are well defined it just breaks tests)
// const SubUnion = struct {
// sub: ?union(enum) {
// foo: usize,
// bar: usize,
// } = null,
// };
// try testCase(SubUnion, .{ .sub = null }, "");
// try testCase(SubUnion, .{ .sub = null }, "sub=");
const SubUnion2 = struct {
sub: ?struct {
foo: usize,
val: union(enum) {
bar: []const u8,
baz: []const u8,
},
} = null,
};
try testCase(SubUnion2, .{ .sub = null }, "");
try testCase(SubUnion2, .{ .sub = .{ .foo = 1, .val = .{ .bar = "abc" } } }, "sub.foo=1&sub.bar=abc");
try testCase(SubUnion2, .{ .sub = .{ .foo = 1, .val = .{ .baz = "abc" } } }, "sub.foo=1&sub.baz=abc");
}
test "encodeStruct" {
try std.testing.expectFmt("", "{}", .{encodeStruct(.{})});
try std.testing.expectFmt("id=3&", "{}", .{encodeStruct(.{ .id = 3 })});
try std.testing.expectFmt("id=3&id2=4&", "{}", .{encodeStruct(.{ .id = 3, .id2 = 4 })});
try std.testing.expectFmt("str=foo&", "{}", .{encodeStruct(.{ .str = "foo" })});
try std.testing.expectFmt("enum_str=foo&", "{}", .{encodeStruct(.{ .enum_str = .foo })});
try std.testing.expectFmt("boolean=false&", "{}", .{encodeStruct(.{ .boolean = false })});
try std.testing.expectFmt("boolean=true&", "{}", .{encodeStruct(.{ .boolean = true })});
}
test "Iter" {
const testCase = struct {
fn case(str: []const u8, pairs: []const Iter.Pair) !void {
var iter = Iter.from(str);
for (pairs) |pair| {
try util.testing.expectDeepEqual(@as(?Iter.Pair, pair), iter.next());
}
try std.testing.expect(iter.next() == null);
}
}.case;
try testCase("", &.{});
try testCase("abc", &.{.{ .key = "abc", .value = null }});
try testCase("abc=", &.{.{ .key = "abc", .value = "" }});
try testCase("abc=def", &.{.{ .key = "abc", .value = "def" }});
try testCase("abc=def&", &.{.{ .key = "abc", .value = "def" }});
try testCase("?abc=def&", &.{.{ .key = "abc", .value = "def" }});
try testCase("?abc=def&foo&bar=baz&qux=", &.{
.{ .key = "abc", .value = "def" },
.{ .key = "foo", .value = null },
.{ .key = "bar", .value = "baz" },
.{ .key = "qux", .value = "" },
});
try testCase("?abc=def&&foo&bar=baz&&qux=&", &.{
.{ .key = "abc", .value = "def" },
.{ .key = "foo", .value = null },
.{ .key = "bar", .value = "baz" },
.{ .key = "qux", .value = "" },
});
try testCase("&=def&", &.{.{ .key = "", .value = "def" }});
}