aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLouis Pilfold <louis@lpil.uk>2021-09-07 19:36:31 +0100
committerLouis Pilfold <louis@lpil.uk>2021-09-07 19:36:31 +0100
commit945152e8075e04d91aac193608ecc84b769c7ba4 (patch)
tree3a60c06153419138ab58d97997709eb0d7fee5ee
parentb611364f02679c6c44592082db6012f2c27a0750 (diff)
downloadgleam_stdlib-945152e8075e04d91aac193608ecc84b769c7ba4.tar.gz
gleam_stdlib-945152e8075e04d91aac193608ecc84b769c7ba4.zip
URI percent encode and decode for JS
-rw-r--r--src/gleam/uri.gleam135
-rw-r--r--src/gleam_stdlib.erl71
-rw-r--r--src/gleam_stdlib.js16
-rw-r--r--test/gleam/should.gleam2
-rw-r--r--test/gleam/uri_test.gleam143
5 files changed, 223 insertions, 144 deletions
diff --git a/src/gleam/uri.gleam b/src/gleam/uri.gleam
index 5482478..6a931e4 100644
--- a/src/gleam/uri.gleam
+++ b/src/gleam/uri.gleam
@@ -7,11 +7,8 @@
//// Query encoding (Form encoding) is defined in the w3c specification.
//// https://www.w3.org/TR/html52/sec-forms.html#urlencoded-form-data
-if erlang {
- import gleam/dynamic.{Dynamic}
-}
-
import gleam/function
+import gleam/string_builder.{StringBuilder}
import gleam/int
import gleam/list
import gleam/map
@@ -216,72 +213,80 @@ if javascript {
"../gleam_stdlib.js" "parse_query"
}
-if erlang {
- type Encoding {
- Utf8
- }
+/// Encodes a list of key value pairs as a URI query string.
+///
+/// The opposite operation is `uri.parse_query`.
+///
+/// ## Examples
+///
+/// ```
+/// > query_to_string([#("a", "1"), #("b", "2")])
+///
+/// "a=1&b=2"
+/// ```
+///
+pub fn query_to_string(query: List(#(String, String))) -> String {
+ query
+ |> list.map(query_pair)
+ |> list.intersperse(string_builder.from_string("&"))
+ |> string_builder.concat
+ |> string_builder.to_string
+}
- type ErlQueryToStringOption {
- Encoding(Encoding)
- }
+fn query_pair(pair: #(String, String)) -> StringBuilder {
+ string_builder.from_strings([
+ percent_encode(pair.0),
+ "=",
+ percent_encode(pair.1),
+ ])
+}
- external fn erl_query_to_string(
- List(#(String, String)),
- List(ErlQueryToStringOption),
- ) -> Dynamic =
- "uri_string" "compose_query"
+/// Encodes a string into a percent encoded representation.
+///
+/// ## Examples
+///
+/// ```
+/// > percent_encode("100% great")
+///
+/// "100%25%20great"
+/// ```
+///
+pub fn percent_encode(value: String) -> String {
+ do_percent_encode(value)
+}
- /// Encodes a list of key value pairs as a URI query string.
- ///
- /// The opposite operation is `uri.parse_query`.
- ///
- /// ## Examples
- ///
- /// ```
- /// > query_to_string([#("a", "1"), #("b", "2")])
- ///
- /// "a=1&b=2"
- /// ```
- ///
- pub fn query_to_string(query: List(#(String, String))) -> String {
- query
- |> erl_query_to_string([Encoding(Utf8)])
- |> dynamic.string
- |> result.unwrap("")
- }
+if erlang {
+ external fn do_percent_encode(String) -> String =
+ "gleam_stdlib" "percent_encode"
+}
- /// Encodes a string into a percent encoded representation.
- /// Note that this encodes space as +.
- ///
- /// ## Examples
- ///
- /// ```
- /// > percent_encode("100% great")
- ///
- /// "100%25+great"
- /// ```
- ///
- pub fn percent_encode(value: String) -> String {
- query_to_string([#("k", value)])
- |> string.replace(each: "k=", with: "")
- }
+if javascript {
+ external fn do_percent_encode(String) -> String =
+ "../gleam_stdlib.js" "percent_encode"
+}
- /// Decodes a percent encoded string.
- ///
- /// ## Examples
- ///
- /// ```
- /// > percent_decode("100%25+great")
- ///
- /// Ok("100% great")
- /// ```
- ///
- pub fn percent_decode(value: String) -> Result(String, Nil) {
- string.concat(["k=", value])
- |> parse_query
- |> result.then(list.head)
- |> result.map(pair.second)
- }
+/// Decodes a percent encoded string.
+///
+/// ## Examples
+///
+/// ```
+/// > percent_decode("100%25+great")
+///
+/// Ok("100% great")
+/// ```
+///
+pub fn percent_decode(value: String) -> Result(String, Nil) {
+ do_percent_decode(value)
+}
+
+if erlang {
+ external fn do_percent_decode(String) -> Result(String, Nil) =
+ "gleam_stdlib" "percent_decode"
+}
+
+if javascript {
+ external fn do_percent_decode(String) -> Result(String, Nil) =
+ "../gleam_stdlib.js" "percent_decode"
}
fn do_remove_dot_segments(
diff --git a/src/gleam_stdlib.erl b/src/gleam_stdlib.erl
index f8f6851..9429f71 100644
--- a/src/gleam_stdlib.erl
+++ b/src/gleam_stdlib.erl
@@ -9,9 +9,22 @@
string_ends_with/2, string_pad/4, decode_tuple2/1, decode_tuple3/1,
decode_tuple4/1, decode_tuple5/1, decode_tuple6/1, decode_map/1,
bit_string_int_to_u32/1, bit_string_int_from_u32/1, decode_result/1,
- bit_string_slice/3, decode_bit_string/1, compile_regex/2,
- regex_check/2, regex_split/2, regex_scan/2, base_decode64/1,
- parse_query/1, bit_string_concat/1]).
+ bit_string_slice/3, decode_bit_string/1, compile_regex/2, regex_scan/2,
+ percent_encode/1, percent_decode/1, regex_check/2, regex_split/2,
+ base_decode64/1, parse_query/1, bit_string_concat/1]).
+
+%% Taken from OTP's uri_string module
+-define(DEC2HEX(X),
+ if ((X) >= 0) andalso ((X) =< 9) -> (X) + $0;
+ ((X) >= 10) andalso ((X) =< 15) -> (X) + $A - 10
+ end).
+
+%% Taken from OTP's uri_string module
+-define(HEX2DEC(X),
+ if ((X) >= $0) andalso ((X) =< $9) -> (X) - $0;
+ ((X) >= $A) andalso ((X) =< $F) -> (X) - $A + 10;
+ ((X) >= $a) andalso ((X) =< $f) -> (X) - $a + 10
+ end).
should_equal(Actual, Expected) ->
?assertEqual(Expected, Actual),
@@ -245,3 +258,55 @@ parse_query(Query) ->
end, Pairs),
{ok, Pairs1}
end.
+
+percent_encode(B) -> percent_encode(B, <<>>).
+percent_encode(<<>>, Acc) ->
+ Acc;
+percent_encode(<<H,T/binary>>, Acc) ->
+ case percent_ok(H) of
+ true ->
+ percent_encode(T, <<Acc/binary,H>>);
+ false ->
+ <<A:4,B:4>> = <<H>>,
+ percent_encode(T, <<Acc/binary,$%,(?DEC2HEX(A)),(?DEC2HEX(B))>>)
+ end.
+
+percent_decode(Cs) -> percent_decode(Cs, <<>>).
+percent_decode(<<$%, C0, C1, Cs/binary>>, Acc) ->
+ case is_hex_digit(C0) andalso is_hex_digit(C1) of
+ true ->
+ B = ?HEX2DEC(C0)*16+?HEX2DEC(C1),
+ percent_decode(Cs, <<Acc/binary, B>>);
+ false ->
+ {error, nil}
+ end;
+percent_decode(<<C,Cs/binary>>, Acc) ->
+ percent_decode(Cs, <<Acc/binary, C>>);
+percent_decode(<<>>, Acc) ->
+ check_utf8(Acc).
+
+percent_ok($!) -> true;
+percent_ok($$) -> true;
+percent_ok($') -> true;
+percent_ok($() -> true;
+percent_ok($)) -> true;
+percent_ok($*) -> true;
+percent_ok($+) -> true;
+percent_ok($-) -> true;
+percent_ok($.) -> true;
+percent_ok($_) -> true;
+percent_ok($~) -> true;
+percent_ok(C) when $0 =< C, C =< $9 -> true;
+percent_ok(C) when $A =< C, C =< $Z -> true;
+percent_ok(C) when $a =< C, C =< $z -> true;
+percent_ok(_) -> false.
+
+is_hex_digit(C) ->
+ ($0 =< C andalso C =< $9) orelse ($a =< C andalso C =< $f) orelse ($A =< C andalso C =< $F).
+
+check_utf8(Cs) ->
+ case unicode:characters_to_list(Cs) of
+ {incomplete, _, _} -> {error, nil};
+ {error, _, _} -> {error, nil};
+ _ -> {ok, Cs}
+ end.
diff --git a/src/gleam_stdlib.js b/src/gleam_stdlib.js
index 082e661..3658142 100644
--- a/src/gleam_stdlib.js
+++ b/src/gleam_stdlib.js
@@ -353,17 +353,29 @@ export function map_insert(key, value, map) {
return map.insert(key, value);
}
-function decode_query_component(string) {
+function unsafe_percent_decode(string) {
return decodeURIComponent((string || "").replace("+", " "));
}
+export function percent_decode(string) {
+ try {
+ return new Ok(unsafe_percent_decode(string));
+ } catch (error) {
+ return new Error(Nil);
+ }
+}
+
+export function percent_encode(string) {
+ return encodeURIComponent(string);
+}
+
export function parse_query(query) {
try {
let pairs = [];
for (let section of query.split("&")) {
let [key, value] = section.split("=");
if (!key) continue;
- pairs.push([decode_query_component(key), decode_query_component(value)]);
+ pairs.push([unsafe_percent_decode(key), unsafe_percent_decode(value)]);
}
return new Ok(List.fromArray(pairs));
} catch (error) {
diff --git a/test/gleam/should.gleam b/test/gleam/should.gleam
index 044190f..483b1a3 100644
--- a/test/gleam/should.gleam
+++ b/test/gleam/should.gleam
@@ -5,8 +5,6 @@
//// documentation](https://rebar3.org/docs/testing/eunit/).
if erlang {
- // TODO: Move this module into another package so it can be used as a
- // dep only in test.
pub external fn equal(a, a) -> Nil =
"gleam_stdlib" "should_equal"
diff --git a/test/gleam/uri_test.gleam b/test/gleam/uri_test.gleam
index ca00200..1c8e0c1 100644
--- a/test/gleam/uri_test.gleam
+++ b/test/gleam/uri_test.gleam
@@ -5,6 +5,7 @@ import gleam/should
import gleam/option.{None, Some}
import gleam/string
import gleam/list
+import gleam/io
pub fn full_parse_test() {
let parsed =
@@ -319,90 +320,88 @@ pub fn error_parsing_query_test() {
should.equal(uri.parse_query("%C2"), Error(Nil))
}
-if erlang {
- pub fn query_to_string_test() {
- let query_string =
- uri.query_to_string([#("weebl bob", "1"), #("city", "örebro")])
- should.equal(query_string, "weebl+bob=1&city=%C3%B6rebro")
- }
+pub fn query_to_string_test() {
+ let query_string =
+ uri.query_to_string([#("weebl bob", "1"), #("city", "örebro")])
+ should.equal(query_string, "weebl%20bob=1&city=%C3%B6rebro")
+}
- pub fn empty_query_to_string_test() {
- let query_string = uri.query_to_string([])
- should.equal(query_string, "")
- }
+pub fn empty_query_to_string_test() {
+ let query_string = uri.query_to_string([])
+ should.equal(query_string, "")
+}
- fn percent_codec_fixtures() {
- [
- #(" ", "+"),
- #(",", "%2C"),
- #(";", "%3B"),
- #(":", "%3A"),
- #("!", "%21"),
- #("?", "%3F"),
- #("'", "%27"),
- #("(", "%28"),
- #(")", "%29"),
- #("[", "%5B"),
- #("@", "%40"),
- #("/", "%2F"),
- #("\\", "%5C"),
- #("&", "%26"),
- #("#", "%23"),
- #("=", "%3D"),
- #("~", "%7E"),
- #("ñ", "%C3%B1"),
- // Allowed chars
- #("-", "-"),
- #("_", "_"),
- #(".", "."),
- #("*", "*"),
- #("100% great", "100%25+great"),
- ]
- }
+const percent_codec_fixtures = [
+ #(" ", "%20"),
+ #(",", "%2C"),
+ #(";", "%3B"),
+ #(":", "%3A"),
+ #("!", "!"),
+ #("?", "%3F"),
+ #("'", "'"),
+ #("(", "("),
+ #(")", ")"),
+ #("[", "%5B"),
+ #("@", "%40"),
+ #("/", "%2F"),
+ #("\\", "%5C"),
+ #("&", "%26"),
+ #("#", "%23"),
+ #("=", "%3D"),
+ #("~", "~"),
+ #("ñ", "%C3%B1"),
+ #("-", "-"),
+ #("_", "_"),
+ #(".", "."),
+ #("*", "*"),
+ #("100% great", "100%25%20great"),
+]
- pub fn percent_encode_test() {
- percent_codec_fixtures()
- |> list.map(fn(t) {
- let #(a, b) = t
- uri.percent_encode(a)
- |> should.equal(b)
- })
- }
+// Allowed chars
+pub fn percent_encode_test() {
+ percent_codec_fixtures
+ |> list.map(fn(t) {
+ let #(a, b) = t
+ uri.percent_encode(a)
+ |> should.equal(b)
+ })
+}
- pub fn percent_encode_consistency_test() {
- let k = "weebl bob[]"
- let v = "ñaña (,:*~)"
+pub fn percent_encode_consistency_test() {
+ let k = "weebl bob[]"
+ let v = "ñaña (,:*~)"
- let query_string = uri.query_to_string([#(k, v)])
+ let query_string = uri.query_to_string([#(k, v)])
- let encoded_key = uri.percent_encode(k)
- let encoded_value = uri.percent_encode(v)
- let manual_query_string = string.concat([encoded_key, "=", encoded_value])
+ let encoded_key = uri.percent_encode(k)
+ let encoded_value = uri.percent_encode(v)
+ let manual_query_string = string.concat([encoded_key, "=", encoded_value])
- should.equal(query_string, manual_query_string)
- }
+ should.equal(query_string, manual_query_string)
+}
- pub fn percent_decode_test() {
- percent_codec_fixtures()
- |> list.map(fn(t) {
- let #(a, b) = t
- uri.percent_decode(b)
- |> should.equal(Ok(a))
- })
- }
+pub fn percent_decode_test() {
+ percent_codec_fixtures
+ |> list.map(fn(t) {
+ let #(a, b) = t
+ uri.percent_decode(b)
+ |> should.equal(Ok(a))
+ })
+}
- pub fn percent_decode_consistency_test() {
- let k = "weebl+bob[]"
- let v = "%C3%B6rebro"
- let query = string.concat([k, "=", v])
- assert Ok(parsed) = uri.parse_query(query)
+pub fn percent_decode_consistency_test() {
+ let k = "weebl%20bob[]"
+ let v = "%C3%B6rebro"
+ let query = string.concat([k, "=", v])
+ assert Ok(parsed) = uri.parse_query(query)
- assert Ok(decoded_key) = uri.percent_decode(k)
- assert Ok(decoded_value) = uri.percent_decode(v)
+ assert Ok(decoded_key) = uri.percent_decode(k)
+ assert Ok(decoded_value) = uri.percent_decode(v)
- should.equal(parsed, [#(decoded_key, decoded_value)])
- }
+ should.equal(parsed, [#(decoded_key, decoded_value)])
+}
+if erlang {
pub fn parse_segments_test() {
should.equal(uri.path_segments("/"), [])
should.equal(uri.path_segments("/weebl/bob"), ["weebl", "bob"])