diff --git a/circlet.c b/circlet.c index b94d731..75f0f11 100644 --- a/circlet.c +++ b/circlet.c @@ -1,6 +1,9 @@ #include #include "mongoose.h" #include +#if !defined(__FreeBSD__) +#include +#endif typedef struct { struct mg_connection *conn; @@ -295,10 +298,78 @@ static Janet cfun_bind_http(int32_t argc, Janet *argv) { return argv[0]; } +static int decode_nibble(uint8_t b) { + if (b >= '0' && b <= '9') + return b - '0'; + if (b >= 'a' && b <= 'f') + return 10 + b - 'a'; + if (b >= 'A' && b <= 'F') + return 10 + b - 'A'; + return 0; +} + +static Janet unescape_x_www_form_urlencoded(const uint8_t *str, size_t len) { + size_t nwritten = 0; + uint8_t *tmp = NULL; +#define NALLOCA 128 + if (len >= NALLOCA) + tmp = janet_smalloc(len); + else + tmp = alloca(len); + + int st = 0; + uint8_t nb1, nb2; + for (size_t i = 0; i < len; i++) { + uint8_t c = str[i]; + switch (st) { + case 0: + switch (c) { + case '+': + tmp[nwritten++] = ' '; + break; + case '%': + st = 1; + break; + default: + tmp[nwritten++] = c; + break; + } + break; + case 1: + st = 2; + nb1 = decode_nibble(c); + break; + case 2: + st = 0; + nb2 = decode_nibble(c); + tmp[nwritten++] = (nb1 << 4) | nb2; + break; + default: + abort(); + } + } + + Janet unescaped = janet_stringv(tmp, nwritten); + + if (len >= NALLOCA) + janet_sfree(tmp); + + return unescaped; +#undef NALLOCA +} + +static Janet cfun_escape_x_www_form_urlencoded(int argc, Janet *argv) { + janet_fixarity(argc, 1); + JanetByteView bv = janet_getbytes(argv, 0); + return unescape_x_www_form_urlencoded(bv.bytes, bv.len); +} + + static const JanetReg cfuns[] = { {"manager", cfun_manager, NULL}, {"poll", cfun_poll, NULL}, {"bind-http", cfun_bind_http, NULL}, + {"escape-x-www-form-urlencoded", cfun_escape_x_www_form_urlencoded, NULL}, {NULL, NULL, NULL} }; diff --git a/circlet_lib.janet b/circlet_lib.janet index 2e31cb1..8cfbe13 100644 --- a/circlet_lib.janet +++ b/circlet_lib.janet @@ -40,11 +40,12 @@ the handler function of the next middleware" [nextmw] (def grammar - (peg/compile - {:content '(some (if-not (set "=;") 1)) - :eql "=" - :sep '(between 1 2 (set "; ")) - :main '(some (* (<- :content) :eql (<- :content) (? :sep)))})) + (comptime + (peg/compile + {:content '(some (if-not (set "=;") 1)) + :eql "=" + :sep '(between 1 2 (set "; ")) + :main '(some (* (<- :content) :eql (<- :content) (? :sep)))}))) (fn [req] (-> req (put :cookies @@ -55,6 +56,20 @@ {})) nextmw))) +(defn parse-x-www-form-urlencoded + [q] + "Parse x-www-form-urlencoded bytes returning a table or nil." + (def grammar + (comptime + (peg/compile ~{ + :main (sequence (opt :query) (not 1)) + :query (sequence :pair (any (sequence "&" :pair))) + :pair (sequence (cmt (capture :key) ,escape-x-www-form-urlencoded) "=" (cmt (capture :value) ,escape-x-www-form-urlencoded)) + :key (any (sequence (not "=") 1)) + :value (any (sequence (not "&") 1))}))) + (when-let [matches (peg/match grammar q)] + (table ;matches))) + (defn server "Creates a simple http server. handler parameter is the function handling the requests. It could be middleware. port is the number of the port the server diff --git a/test/parse-x-www-form-urlencoded.janet b/test/parse-x-www-form-urlencoded.janet new file mode 100644 index 0000000..6df51d8 --- /dev/null +++ b/test/parse-x-www-form-urlencoded.janet @@ -0,0 +1,12 @@ +(import build/circlet :as circlet) + +(def tests [ + ["" @{}] + ["abc=5&%20+=" @{"abc" "5" " " ""}] + ["a=b" @{"a" "b"}] +]) + +(each tc tests + (def r (circlet/parse-x-www-form-urlencoded (tc 0))) + (unless (deep= r (tc 1)) + (errorf "fail: %j != %j" r (tc 1))))