1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
%%
%% @doc URL safe base64-compatible codec.
%%
%% Based heavily on the code extracted from:
%% https://github.com/basho/riak_control/blob/master/src/base64url.erl and
%% https://github.com/mochi/mochiweb/blob/master/src/mochiweb_base64url.erl.
%%
-module(base64url).
-author('Vladimir Dronnikov <dronnikov@gmail.com>').
-export([
decode/1,
encode/1,
encode_mime/1
]).
-spec encode(
binary() | iolist()
) -> binary().
encode(Bin) when is_binary(Bin) ->
<< << (urlencode_digit(D)) >> || <<D>> <= base64:encode(Bin), D =/= $= >>;
encode(L) when is_list(L) ->
encode(iolist_to_binary(L)).
-spec encode_mime(
binary() | iolist()
) -> binary().
encode_mime(Bin) when is_binary(Bin) ->
<< << (urlencode_digit(D)) >> || <<D>> <= base64:encode(Bin) >>;
encode_mime(L) when is_list(L) ->
encode_mime(iolist_to_binary(L)).
-spec decode(
binary() | iolist()
) -> binary().
decode(Bin) when is_binary(Bin) ->
Bin2 = case byte_size(Bin) rem 4 of
% 1 -> << Bin/binary, "===" >>;
2 -> << Bin/binary, "==" >>;
3 -> << Bin/binary, "=" >>;
_ -> Bin
end,
base64:decode(<< << (urldecode_digit(D)) >> || <<D>> <= Bin2 >>);
decode(L) when is_list(L) ->
decode(iolist_to_binary(L)).
urlencode_digit($/) -> $_;
urlencode_digit($+) -> $-;
urlencode_digit(D) -> D.
urldecode_digit($_) -> $/;
urldecode_digit($-) -> $+;
urldecode_digit(D) -> D.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
aim_test() ->
% vanilla base64 produce URL unsafe output
?assertNotEqual(
binary:match(base64:encode([255,127,254,252]), [<<"=">>, <<"/">>, <<"+">>]),
nomatch),
% this codec produce URL safe output
?assertEqual(
binary:match(encode([255,127,254,252]), [<<"=">>, <<"/">>, <<"+">>]),
nomatch),
% the mime codec produces URL unsafe output, but only because of padding
?assertEqual(
binary:match(encode_mime([255,127,254,252]), [<<"/">>, <<"+">>]),
nomatch),
?assertNotEqual(
binary:match(encode_mime([255,127,254,252]), [<<"=">>]),
nomatch).
codec_test() ->
% codec is lossless with or without padding
?assertEqual(decode(encode(<<"foo">>)), <<"foo">>),
?assertEqual(decode(encode(<<"foo1">>)), <<"foo1">>),
?assertEqual(decode(encode(<<"foo12">>)), <<"foo12">>),
?assertEqual(decode(encode(<<"foo123">>)), <<"foo123">>),
?assertEqual(decode(encode_mime(<<"foo">>)), <<"foo">>),
?assertEqual(decode(encode_mime(<<"foo1">>)), <<"foo1">>),
?assertEqual(decode(encode_mime(<<"foo12">>)), <<"foo12">>),
?assertEqual(decode(encode_mime(<<"foo123">>)), <<"foo123">>).
iolist_test() ->
% codec supports iolists
?assertEqual(decode(encode("foo")), <<"foo">>),
?assertEqual(decode(encode(["fo", "o1"])), <<"foo1">>),
?assertEqual(decode(encode([255,127,254,252])), <<255,127,254,252>>),
?assertEqual(decode(encode_mime("foo")), <<"foo">>),
?assertEqual(decode(encode_mime(["fo", "o1"])), <<"foo1">>),
?assertEqual(decode(encode_mime([255,127,254,252])), <<255,127,254,252>>).
-endif.
|