diff options
author | Louis Pilfold <louis@lpil.uk> | 2021-09-07 19:36:31 +0100 |
---|---|---|
committer | Louis Pilfold <louis@lpil.uk> | 2021-09-07 19:36:31 +0100 |
commit | 945152e8075e04d91aac193608ecc84b769c7ba4 (patch) | |
tree | 3a60c06153419138ab58d97997709eb0d7fee5ee | |
parent | b611364f02679c6c44592082db6012f2c27a0750 (diff) | |
download | gleam_stdlib-945152e8075e04d91aac193608ecc84b769c7ba4.tar.gz gleam_stdlib-945152e8075e04d91aac193608ecc84b769c7ba4.zip |
URI percent encode and decode for JS
-rw-r--r-- | src/gleam/uri.gleam | 135 | ||||
-rw-r--r-- | src/gleam_stdlib.erl | 71 | ||||
-rw-r--r-- | src/gleam_stdlib.js | 16 | ||||
-rw-r--r-- | test/gleam/should.gleam | 2 | ||||
-rw-r--r-- | test/gleam/uri_test.gleam | 143 |
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"]) |