hare

The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

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:
Mnet/uri/parse.ha | 15++++++++++-----
Anet/uri/query.ha | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/gen-stdlib | 1+
Mstdlib.mk | 2++
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