From 0b4d1d2ee1fee4f415f17cd83edd584fbd75d478 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Wed, 1 Aug 2018 18:05:14 +0300 Subject: [PATCH] url-encode & url-decode --- modules/reitit-core/src/reitit/impl.cljc | 39 ++++++-- perf-test/clj/reitit/impl_perf_test.clj | 116 +++++++++++++++++++++++ test/cljc/reitit/impl_test.cljc | 93 ++++++++++++++++++ 3 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 perf-test/clj/reitit/impl_perf_test.clj diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index d5a41969..2d74dce2 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -166,15 +166,40 @@ ;; Path-parameters, see https://github.com/metosin/reitit/issues/75 ;; +#?(:clj + (def hex-digit + {0 "0" 1 "1" 2 "2" 3 "3" + 4 "4" 5 "5" 6 "6" 7 "7" + 8 "8" 9 "9" 10 "A" 11 "B" + 12 "C" 13 "D" 14 "E" 15 "F"})) + +#?(:clj + (defn byte->percent [byte] + (let [byte (bit-and 0xFF byte) + low-nibble (bit-and 0xF byte) + high-nibble (bit-shift-right byte 4)] + (str "%" (hex-digit high-nibble) (hex-digit low-nibble))))) + +#?(:clj + (defn percent-encode [^String unencoded] + (->> (.getBytes unencoded "UTF-8") (map byte->percent) (str/join)))) + +;; + is safe, but removed so it would work the same as with js (defn url-encode [s] - (some-> s - #?(:clj (URLEncoder/encode "UTF-8") - :cljs (js/encodeURIComponent)) - #?(:clj (.replace "+" "%20")))) + (if s + #?(:clj (str/replace s #"[^A-Za-z0-9\!'\(\)\*_~.-]+" percent-encode) + :cljs (js/encodeURIComponent s)))) (defn url-decode [s] - (some-> s #?(:clj (URLDecoder/decode "UTF-8") - :cljs (js/decodeURIComponent)))) + (if s + #?(:clj (if (.contains ^String s "%") + (URLDecoder/decode + (if (.contains ^String s "+") + (.replace ^String s "+" "%2B") + s) + "UTF-8") + s) + :cljs (js/decodeURIComponent s)))) (defprotocol IntoString (into-string [_])) @@ -203,7 +228,7 @@ (into-string [this] (str this)) nil - (into-string [this])) + (into-string [_])) (defn path-params "shallow transform of the path parameters values into strings" diff --git a/perf-test/clj/reitit/impl_perf_test.clj b/perf-test/clj/reitit/impl_perf_test.clj new file mode 100644 index 00000000..20b28d6c --- /dev/null +++ b/perf-test/clj/reitit/impl_perf_test.clj @@ -0,0 +1,116 @@ +(ns reitit.impl-perf-test + (:require [criterium.core :as cc] + [reitit.perf-utils :refer :all] + [ring.util.codec] + [reitit.impl]) + (:import (java.net URLDecoder URLEncoder))) + +;; +;; start repl with `lein perf repl` +;; perf measured with the following setup: +;; +;; Model Name: MacBook Pro +;; Model Identifier: MacBookPro11,3 +;; Processor Name: Intel Core i7 +;; Processor Speed: 2,5 GHz +;; Number of Processors: 1 +;; Total Number of Cores: 4 +;; L2 Cache (per Core): 256 KB +;; L3 Cache: 6 MB +;; Memory: 16 GB +;; + + +(defn test! [f input] + (do + (println "\u001B[33m") + (println (pr-str input) "=>" (pr-str (f input))) + (println "\u001B[0m") + (cc/quick-bench (f input)))) + +(defn url-decode-naive [s] + (URLDecoder/decode + (.replace ^String s "+" "%2B") + "UTF-8")) + +(defn decode! [] + + ;; ring + + ;; 890ns + ;; 190ns + ;; 90ns + ;; 80ns + + ;; naive + + ;; 750ns + ;; 340ns + ;; 420ns + ;; 200ns + + ;; reitit + + ;; 630ns (-29%) + ;; 12ns (-94%) + ;; 8ns (-91%) + ;; 8ns (-90%) + + (doseq [fs ['ring.util.codec/url-decode + 'url-decode-naive + 'reitit.impl/url-decode] + :let [f (deref (resolve fs))]] + (suite (str fs)) + (doseq [s ["aja%20hiljaa+sillalla" + "aja_hiljaa_sillalla" + "1+1" + "1"]] + (test! f s)))) + +(defn url-encode-naive [^String s] + (cond-> (.replace (URLEncoder/encode s "UTF-8") "+" "%20") + (.contains s "+") (.replace "%2B" "+") + (.contains s "~") (.replace "%7E" "~") + (.contains s "=") (.replace "%3D" "=") + (.contains s "!") (.replace "%21" "!") + (.contains s "'") (.replace "%27" "'") + (.contains s "(") (.replace "%28" "(") + (.contains s ")") (.replace "%29" ")"))) + +(defn encode! [] + + ;; ring + + ;; 2500ns + ;; 610ns + ;; 160ns + ;; 120ns + + ;; naive + + ;; 1000ns + ;; 440ns + ;; 570ns + ;; 200ns + + ;; reitit + + ;; 1400ns + ;; 740ns + ;; 180ns + ;; 130ns + + (doseq [fs ['ring.util.codec/url-encode + 'url-encode-naive + 'reitit.impl/url-encode] + :let [f (deref (resolve fs))]] + (suite (str fs)) + (doseq [s ["aja hiljaa+sillalla" + "aja_hiljaa_sillalla" + "1+1" + "1"]] + (test! f s)))) + +(comment + (decode!) + (encode!)) diff --git a/test/cljc/reitit/impl_test.cljc b/test/cljc/reitit/impl_test.cljc index 1d437ed8..782a3435 100644 --- a/test/cljc/reitit/impl_test.cljc +++ b/test/cljc/reitit/impl_test.cljc @@ -64,3 +64,96 @@ ;{:a ["c" "b"]} "a=c&a=b" ;{:a (seq [1 2])} "a=1&a=2" ;{:a #{"c" "b"}} "a=b&a=c" + +(deftest url-encode-test + (are [in out] + (= out (impl/url-encode in)) + + "/" "%2F" + "?" "%3F" + "#" "%23" + "[" "%5B" + "]" "%5D" + "!" "!" + #_#_"$" "$" + #_#_"&" "&" + "'" "'" + "(" "(" + ")" ")" + "*" "*" + #_#_"+" "+" + #_#_"," "," + #_#_";" ";" + #_#_"=" "=" + #_#_":" ":" + #_#_"@" "@" + "a" "a" + "z" "z" + "A" "A" + "Z" "Z" + "0" "0" + "9" "9" + "-" "-" + "." "." + "_" "_" + "~" "~" + "\000" "%00" + "\037" "%1F" + " " "%20" + "\"" "%22" + "%" "%25" + "<" "%3C" + ">" "%3E" + "\\" "%5C" + "^" "%5E" + "`" "%60" + "{" "%7B" + "|" "%7C" + "}" "%7D" + "\177" "%7F" + #_#_"\377" "%FF" + + "£0.25" "%C2%A30.25" + "€100" "%E2%82%AC100" + "«küßî»" "%C2%ABk%C3%BC%C3%9F%C3%AE%C2%BB" + "“ЌύБЇ”" "%E2%80%9C%D0%8C%CF%8D%D0%91%D0%87%E2%80%9D" + + "\000" "%00" + #_#_"\231" "%99" + #_#_"\252" "%AA" + #_#_"\377" "%FF" + + "" "" + "1" "1" + "12" "12" + "123" "123" + "1234567890" "1234567890" + + "Hello world" "Hello%20world" + "/home/foo" "%2Fhome%2Ffoo" + + " " "%20" + "+" "%2B" #_"+" + " +" "%20%2B" #_"%20+" + #_#_"1+2=3" "1+2=3" + #_#_"1 + 2 = 3" "1%20+%202%20=%203")) + +(deftest url-decode-test + (are [in out] + (= out (impl/url-decode in)) + + "1+1" "1+1" + "%21" "!" + "%61" "a" + "%31%32%33" "123" + "%2b" "+" + "%7e" "~" + "hello%20world" "hello world" + "a%2fb" "a/b" + "a/.." "a/.." + "a/." "a/." + "//a" "//a" + "a//b" "a//b" + "a//" "a//" + "/path/%C2%ABk%C3%BC%C3%9F%C3%AE%C2%BB" "/path/«küßî»" + "/path/%E2%80%9C%D0%8C%CF%8D%D0%91%D0%87%E2%80%9D" "/path/“ЌύБЇ”"))