Decode query string chars

This commit is contained in:
jaina heartles 2022-11-15 01:03:25 -08:00
parent db1bd0f7c7
commit a15baaf2e7
2 changed files with 91 additions and 26 deletions

View file

@ -70,7 +70,7 @@ const QueryIter = @import("util").QueryIter;
///
/// TODO: values are currently case-sensitive, and are not url-decoded properly.
/// This should be fixed.
pub fn parseQuery(comptime T: type, query: []const u8) !T {
pub fn parseQuery(alloc: std.mem.Allocator, comptime T: type, query: []const u8) !T {
if (comptime !std.meta.trait.isContainer(T)) @compileError("T must be a struct");
var iter = QueryIter.from(query);
@ -85,27 +85,54 @@ pub fn parseQuery(comptime T: type, query: []const u8) !T {
} else std.log.debug("unknown param {s}", .{pair.key});
}
return (try parse(T, "", "", fields)).?;
return (try parse(alloc, T, "", "", fields)).?;
}
fn parseScalar(comptime T: type, comptime name: []const u8, fields: anytype) !?T {
fn decodeString(alloc: std.mem.Allocator, val: []const u8) ![]const 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 parseScalar(alloc: std.mem.Allocator, comptime T: type, comptime name: []const u8, fields: anytype) !?T {
const param = @field(fields, name);
return switch (param) {
.not_specified => null,
.no_value => try parseQueryValue(T, null),
.value => |v| try parseQueryValue(T, v),
.no_value => try parseQueryValue(alloc, T, null),
.value => |v| try parseQueryValue(alloc, T, v),
};
}
fn parse(comptime T: type, comptime prefix: []const u8, comptime name: []const u8, fields: anytype) !?T {
if (comptime isScalar(T)) return parseScalar(T, prefix ++ "." ++ name, fields);
fn parse(
alloc: std.mem.Allocator,
comptime T: type,
comptime prefix: []const u8,
comptime name: []const u8,
fields: anytype,
) !?T {
if (comptime isScalar(T)) return parseScalar(alloc, T, prefix ++ "." ++ name, fields);
switch (@typeInfo(T)) {
.Union => |info| {
var result: ?T = null;
inline for (info.fields) |field| {
const F = field.field_type;
const maybe_value = try parse(F, prefix, field.name, fields);
const maybe_value = try parse(alloc, F, prefix, field.name, fields);
if (maybe_value) |value| {
if (result != null) return error.DuplicateUnionField;
@ -124,7 +151,7 @@ fn parse(comptime T: type, comptime prefix: []const u8, comptime name: []const u
const F = field.field_type;
var maybe_value: ?F = null;
if (try parse(F, prefix ++ "." ++ name, field.name, fields)) |v| {
if (try parse(alloc, F, prefix ++ "." ++ name, field.name, fields)) |v| {
maybe_value = v;
} else if (field.default_value) |default| {
if (comptime @sizeOf(F) != 0) {
@ -151,7 +178,7 @@ fn parse(comptime T: type, comptime prefix: []const u8, comptime name: []const u
},
// Only applies to non-scalar optionals
.Optional => |info| return try parse(info.child, prefix, name, fields),
.Optional => |info| return try parse(alloc, info.child, prefix, name, fields),
else => @compileError("tmp"),
}
@ -204,7 +231,7 @@ fn Intermediary(comptime T: type) type {
} });
}
fn parseQueryValue(comptime T: type, value: ?[]const u8) !T {
fn parseQueryValue(alloc: std.mem.Allocator, comptime T: type, value: ?[]const u8) !T {
const is_optional = comptime std.meta.trait.is(.Optional)(T);
// If param is present, but without an associated value
if (value == null) {
@ -216,7 +243,7 @@ fn parseQueryValue(comptime T: type, value: ?[]const u8) !T {
error.InvalidValue;
}
return try parseQueryValueNotNull(if (is_optional) std.meta.Child(T) else T, value.?);
return try parseQueryValueNotNull(alloc, if (is_optional) std.meta.Child(T) else T, value.?);
}
const bool_map = std.ComptimeStringMap(bool, .{
@ -233,15 +260,27 @@ const bool_map = std.ComptimeStringMap(bool, .{
.{ "0", false },
});
fn parseQueryValueNotNull(comptime T: type, value: []const u8) !T {
if (comptime std.meta.trait.isZigString(T)) return value;
if (comptime std.meta.trait.isIntegral(T)) return try std.fmt.parseInt(T, value, 0);
if (comptime std.meta.trait.isFloat(T)) return try std.fmt.parseFloat(T, value);
if (comptime std.meta.trait.is(.Enum)(T)) return std.meta.stringToEnum(T, value) orelse error.InvalidEnumValue;
if (T == bool) return bool_map.get(value) orelse error.InvalidBool;
if (comptime std.meta.trait.hasFn("parse")(T)) return try T.parse(value);
fn parseQueryValueNotNull(alloc: std.mem.Allocator, comptime T: type, value: []const u8) !T {
const decoded = try decodeString(alloc, value);
errdefer alloc.free(decoded);
if (comptime std.meta.trait.isZigString(T)) return decoded;
const result = if (comptime std.meta.trait.isIntegral(T))
try std.fmt.parseInt(T, decoded, 0)
else if (comptime std.meta.trait.isFloat(T))
try std.fmt.parseFloat(T, decoded)
else if (comptime std.meta.trait.is(.Enum)(T))
std.meta.stringToEnum(T, decoded) orelse return error.InvalidEnumValue
else if (T == bool)
bool_map.get(value) orelse return error.InvalidBool
else if (comptime std.meta.trait.hasFn("parse")(T))
try T.parse(value)
else
@compileError("Invalid type " ++ @typeName(T));
alloc.free(decoded);
return result;
}
fn isScalar(comptime T: type) bool {
@ -261,14 +300,34 @@ pub fn formatQuery(params: anytype, writer: anytype) !void {
try format("", "", params, writer);
}
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.isZigString(T)) return std.fmt.format(writer, "{s}={s}&", .{ name, val });
_ = try switch (@typeInfo(T)) {
.Enum => std.fmt.format(writer, "{s}={s}&", .{ name, @tagName(val) }),
.Optional => if (val) |v| formatScalar(name, v, writer),
else => std.fmt.format(writer, "{s}={}&", .{ name, 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)) {
.Enum => urlFormatString(writer, @tagName(val)),
else => std.fmt.format(writer, "{}", .{val}),
};
try writer.writeByte('&');
}
fn format(comptime prefix: []const u8, comptime name: []const u8, params: anytype, writer: anytype) !void {

View file

@ -99,7 +99,13 @@ pub fn deepFree(alloc: ?std.mem.Allocator, val: anytype) void {
},
.Optional => if (val) |v| deepFree(alloc, v) else {},
.Struct => |struct_info| inline for (struct_info.fields) |field| deepFree(alloc, @field(val, field.name)),
.Union, .ErrorUnion => @compileError("TODO: Unions not yet supported by deepFree"),
.Union => |union_info| inline for (union_info.fields) |field| {
const tag = @field(std.meta.Tag(T), field.name);
if (@as(std.meta.Tag(T), val) == tag) {
deepFree(alloc, @field(val, field.name));
}
},
.ErrorUnion => if (val) |v| deepFree(alloc, v) else {},
.Array => for (val) |v| deepFree(alloc, v),
.Enum, .Int, .Float, .Bool, .Void, .Type => {},