commit 09f6c32bd19fb7436e441fc8cc17383290b01cc3
parent fe28773bba664a9ab82aa778e9d21b00336c2bfc
Author: Alexey Yerin <yyp@disroot.org>
Date: Sat, 23 Apr 2022 23:57:26 +0300
net/uri: add query string encoding and decoding
Implements: https://todo.sr.ht/~sircmpwn/hare/630
Signed-off-by: Alexey Yerin <yyp@disroot.org>
Diffstat:
4 files changed, 100 insertions(+), 5 deletions(-)
diff --git a/net/uri/parse.ha b/net/uri/parse.ha
@@ -321,13 +321,19 @@ fn parse_port(in: *strings::iterator) (u16 | invalid) = {
fn percent_decode(s: str) (str | invalid) = {
let buf = strio::dynamic();
+ percent_decode_static(&buf, s)?;
+ return strio::string(&buf);
+};
+
+fn percent_decode_static(out: io::handle, s: str) (void | invalid) = {
let iter = strings::iter(s);
+ let tmp = strio::dynamic();
+ defer io::close(&tmp);
for (true) {
match (strings::next(&iter)) {
case let r: rune =>
if (r == '%') {
- let tmp = strio::dynamic();
- defer io::close(&tmp);
+ strio::reset(&tmp);
for (let i = 0z; i < 2; i += 1) {
const r = wantrune(&iter)?;
strio::appendrune(&tmp, r)!;
@@ -336,18 +342,17 @@ fn percent_decode(s: str) (str | invalid) = {
match (strconv::stou8b(strio::string(&tmp),
strconv::base::HEX)) {
case let ord: u8 =>
- strio::appendrune(&buf, ord: u32: rune)!;
+ strio::appendrune(out, ord: u32: rune)!;
case =>
return invalid;
};
} else {
- strio::appendrune(&buf, r)!;
+ strio::appendrune(out, r)!;
};
case void =>
break;
};
};
- return strio::string(&buf);
};
fn wantrune(iter: *strings::iterator) (rune | invalid) = {
diff --git a/net/uri/query.ha b/net/uri/query.ha
@@ -0,0 +1,87 @@
+use io;
+use strings;
+use strio;
+
+export type query_decoder = struct {
+ tokenizer: strings::tokenizer,
+ bufs: (strio::stream, strio::stream),
+};
+
+// Initializes a decoder for a query string. Use [[query_next]] to walk it. The
+// caller must call [[query_finish]] once they're done using it.
+export fn decodequery(q: const str) query_decoder = query_decoder {
+ tokenizer = strings::tokenize(q, "&"),
+ bufs = (strio::dynamic(), strio::dynamic()),
+};
+
+// Frees resources associated with the [[query_decoder]].
+export fn query_finish(dec: *query_decoder) void = {
+ io::close(&dec.bufs.0);
+ io::close(&dec.bufs.1);
+};
+
+// Retrieves the next (key, value) pair from the query. The return value is
+// borrowed from the decoder and will be replaced on the next call, use
+// [[strings::dup]] to extend its lifetime.
+export fn query_next(dec: *query_decoder) ((str, str) | invalid | void) = {
+ const tok = match (strings::next_token(&dec.tokenizer)) {
+ case let s: str =>
+ yield s;
+ case => return;
+ };
+
+ const raw = strings::cut(tok, "=");
+ strio::reset(&dec.bufs.0);
+ percent_decode_static(&dec.bufs.0, raw.0)?;
+ strio::reset(&dec.bufs.1);
+ percent_decode_static(&dec.bufs.1, raw.1)?;
+ return (
+ strio::string(&dec.bufs.0),
+ strio::string(&dec.bufs.1),
+ );
+};
+
+// Encodes (key, value) pairs into a URI query string. The result must be
+// freed by the caller.
+export fn encodequery(pairs: [](str, str)) str = {
+ const buf = strio::dynamic();
+ for (let i = 0z; i < len(pairs); i += 1) {
+ const pair = pairs[i];
+ if (i > 0) strio::appendrune(&buf, '&')!;
+
+ assert(len(pair.0) > 0);
+ percent_encode(&buf, pair.0)!;
+ if (len(pair.1) > 0) {
+ strio::appendrune(&buf, '=')!;
+ percent_encode(&buf, pair.1)!;
+ };
+ };
+
+ return strio::string(&buf);
+};
+
+@test fn decodequery() void = {
+ const u = parse("https://sr.ht/projects?search=%23risc-v&sort=longest-active")!;
+ defer finish(&u);
+
+ const query = decodequery(u.query);
+ defer query_finish(&query);
+ const pair = query_next(&query)! as (str, str);
+ assert(pair.0 == "search");
+ assert(pair.1 == "#risc-v");
+
+ const pair = query_next(&query)! as (str, str);
+ assert(pair.0 == "sort");
+ assert(pair.1 == "longest-active");
+};
+
+@test fn encodequery() void = {
+ const pairs = [
+ ("search", "#risc-v"),
+ ("sort", "longest-active"),
+ ];
+ const encoded = encodequery(pairs);
+ defer free(encoded);
+
+ assert(encoded == "search=%23risc-v&sort=longest-active");
+};
diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib
@@ -963,6 +963,7 @@ gensrcs_net_uri() {
gen_srcs net::uri \
fmt.ha \
parse.ha \
+ query.ha \
uri.ha \
$*
}
diff --git a/stdlib.mk b/stdlib.mk
@@ -1582,6 +1582,7 @@ $(HARECACHE)/net/unix/net_unix-freebsd.ssa: $(stdlib_net_unix_freebsd_srcs) $(st
stdlib_net_uri_any_srcs = \
$(STDLIB)/net/uri/fmt.ha \
$(STDLIB)/net/uri/parse.ha \
+ $(STDLIB)/net/uri/query.ha \
$(STDLIB)/net/uri/uri.ha
$(HARECACHE)/net/uri/net_uri-any.ssa: $(stdlib_net_uri_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_ip_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM))
@@ -3629,6 +3630,7 @@ $(TESTCACHE)/net/unix/net_unix-freebsd.ssa: $(testlib_net_unix_freebsd_srcs) $(t
testlib_net_uri_any_srcs = \
$(STDLIB)/net/uri/fmt.ha \
$(STDLIB)/net/uri/parse.ha \
+ $(STDLIB)/net/uri/query.ha \
$(STDLIB)/net/uri/uri.ha \
$(STDLIB)/net/uri/+test.ha