Skip to content

Commit

Permalink
Merge pull request #67 from sile/encode-ip-address
Browse files Browse the repository at this point in the history
Add `jsone:ip_address_to_json_string/1` function
  • Loading branch information
sile authored Nov 1, 2021
2 parents 6ba5456 + 8176612 commit ed90938
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 1 deletion.
30 changes: 29 additions & 1 deletion src/jsone.erl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
try_decode/1, try_decode/2,
encode/1, encode/2,
try_encode/1, try_encode/2,
term_to_json_string/1
term_to_json_string/1,
ip_address_to_json_string/1
]).

-export_type([
Expand Down Expand Up @@ -413,3 +414,30 @@ try_encode(JsonValue, Options) ->
-spec term_to_json_string(term()) -> {ok, json_string()} | error.
term_to_json_string(X) ->
{ok, list_to_binary(io_lib:format("~p", [X]))}.

%% @doc Convert an IP address into a text representation.
%%
%% This function can be specified as the value of the `map_unknown_value' encoding option.
%%
%% This function formats IPv6 addresses by following the recommendation defined in RFC 5952.
%% Note that the trailing 32 bytes of special IPv6 addresses such as IPv4-Compatible (::X.X.X.X),
%% IPv4-Mapped (::ffff:X.X.X.X), IPv4-Translated (::ffff:0:X.X.X.X) and IPv4/IPv6 translation
%% (64:ff9b::X.X.X.X and 64:ff9b:1::X.X.X.X ~ 64:ff9b:1:ffff:ffff:ffff:X.X.X.X) are formatted
%% using the IPv4 format.
%%
%% ```
%% > EncodeOpt = [{map_unknown_value, fun jsone:ip_address_to_json_string/1}].
%%
%% > jsone:encode(#{ip => {127, 0, 0, 1}}, EncodeOpt).
%% <<"{\"ip\":\"127.0.0.1\"}">>
%%
%% > {ok, Addr} = inet:parse_address("2001:DB8:0000:0000:0001:0000:0000:0001").
%% > jsone:encode(Addr, EncodeOpt).
%% <<"\"2001:db8::1:0:0:1\"">>
%%
%% > jsone:encode([foo, {0, 0, 0, 0, 0, 16#FFFF, 16#7F00, 16#0001}], EncodeOpt).
%% <<"[\"foo\",\"::ffff:127.0.0.1\"]">>
%% '''
-spec ip_address_to_json_string(inet:ip_address()|any()) -> {ok, json_string()} | error.
ip_address_to_json_string(X) ->
jsone_inet:ip_address_to_json_string(X).
123 changes: 123 additions & 0 deletions src/jsone_inet.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
%%% @doc Utility functions for `inet' module
%%% @private
%%% @end
%%%
%%% Copyright (c) 2013-2021, Takeru Ohta <phjgt308@gmail.com>
%%%
%%% The MIT License
%%%
%%% Permission is hereby granted, free of charge, to any person obtaining a copy
%%% of this software and associated documentation files (the "Software"), to deal
%%% in the Software without restriction, including without limitation the rights
%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
%%% copies of the Software, and to permit persons to whom the Software is
%%% furnished to do so, subject to the following conditions:
%%%
%%% The above copyright notice and this permission notice shall be included in
%%% all copies or substantial portions of the Software.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
%%% THE SOFTWARE.
%%%
%%%---------------------------------------------------------------------------------------
-module(jsone_inet).

%%--------------------------------------------------------------------------------
%% Exported API
%%--------------------------------------------------------------------------------
-export([ip_address_to_json_string/1]).

%%--------------------------------------------------------------------------------
%% Macros
%%--------------------------------------------------------------------------------
-define(IS_IPV4_RANGE(X), is_integer(X) andalso 0 =< X andalso X =< 255).
-define(IS_IPV4(A, B, C, D),
?IS_IPV4_RANGE(A) andalso ?IS_IPV4_RANGE(B) andalso ?IS_IPV4_RANGE(C) andalso ?IS_IPV4_RANGE(D)).

-define(IS_IPV6_RANGE(X), is_integer(X) andalso 0 =< X andalso X =< 65535).
-define(IS_IPV6(A, B, C, D, E, F, G, H),
?IS_IPV6_RANGE(A) andalso ?IS_IPV6_RANGE(B) andalso ?IS_IPV6_RANGE(C) andalso ?IS_IPV6_RANGE(D) andalso
?IS_IPV6_RANGE(E) andalso ?IS_IPV6_RANGE(F) andalso ?IS_IPV6_RANGE(G) andalso ?IS_IPV6_RANGE(H)).

%%--------------------------------------------------------------------------------
%% Exported Functions
%%--------------------------------------------------------------------------------

%% @doc Convert an IP address into a text representation.
%%
%% Please refer to the doc of `jsone:ip_address_to_json_string/1' for the detail.
-spec ip_address_to_json_string(inet:ip_address()|any()) -> {ok, jsone:json_string()} | error.
ip_address_to_json_string({A, B, C, D}) when ?IS_IPV4(A, B, C, D) ->
{ok, iolist_to_binary(io_lib:format("~p.~p.~p.~p", [A, B, C, D]))};
ip_address_to_json_string({A, B, C, D, E, F, G, H}) when ?IS_IPV6(A, B, C, D, E, F, G, H) ->
Text =
case {A, B, C, D, E, F} of
{0, 0, 0, 0, 0, 0} ->
%% IPv4-compatible address
io_lib:format("::~p.~p.~p.~p", [G bsr 8, G band 16#ff, H bsr 8, H band 16#ff]);
{0, 0, 0, 0, 0, 16#ffff} ->
%% IPv4-mapped address
io_lib:format("::ffff:~p.~p.~p.~p", [G bsr 8, G band 16#ff, H bsr 8, H band 16#ff]);
{0, 0, 0, 0, 16#ffff, 0} ->
%% IPv4-translated address
io_lib:format("::ffff:0:~p.~p.~p.~p", [G bsr 8, G band 16#ff, H bsr 8, H band 16#ff]);
{16#64, 16#ff9b, 0, 0, 0, 0} ->
%% IPv4/IPv6 translation
io_lib:format("64:ff9b::~p.~p.~p.~p", [G bsr 8, G band 16#ff, H bsr 8, H band 16#ff]);
{16#64, 16#ff9b, 1, _, _, _} ->
%% IPv4/IPv6 translation
{Prefix, _} = format_ipv6([A, B, C, D, E, F], 0, 0),
Last = lists:flatten(io_lib:format("~p.~p.~p.~p", [G bsr 8, G band 16#ff, H bsr 8, H band 16#ff])),
string:join(Prefix ++ [Last], ":");
_ ->
format_ipv6([A, B, C, D, E, F, G, H])
end,
{ok, iolist_to_binary(Text)};
ip_address_to_json_string(_) ->
error.

%%--------------------------------------------------------------------------------
%% Internal Functions
%%--------------------------------------------------------------------------------
-spec format_ipv6([0..65535]) -> string().
format_ipv6(Xs) ->
case format_ipv6(Xs, 0, 0) of
{Ys, shortening} -> [$: | string:join(Ys, ":")];
{Ys, _} ->
Text = string:join(Ys, ":"),
case lists:last(Text) of
$: -> [Text | ":"];
_ -> Text
end
end.

-spec format_ipv6([0..65535], non_neg_integer(), non_neg_integer()) -> {[string()], not_shortened | shortening | shortened}.
format_ipv6([], _Zeros, _MaxZeros) ->
{[], not_shortened};
format_ipv6([X | Xs], Zeros0, MaxZeros) ->
Zeros1 =
case X of
0 -> Zeros0 + 1;
_ -> 0
end,
Shorten = Zeros1 > MaxZeros andalso Zeros1 > 1,
case format_ipv6(Xs, Zeros1, max(Zeros1, MaxZeros)) of
{Ys, not_shortened} ->
case Shorten of
true -> {["" | Ys], shortening};
false -> {[to_hex(X) | Ys], not_shortened}
end;
{Ys, shortening} when X =:= 0 ->
{Ys, shortening};
{Ys, _} ->
{[to_hex(X) | Ys], shortened}
end.

-spec to_hex(0..65535) -> string().
to_hex(N) ->
string:lowercase(integer_to_list(N, 16)).
9 changes: 9 additions & 0 deletions test/jsone_encode_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,15 @@ encode_test_() ->
Expected = <<"[\"{foo}\\n\"]">>,
?assertEqual(Expected, jsone:encode(Input, [{map_unknown_value, MapFun}]))
end},
{"IP address",
fun () ->
Input = #{ip => {127, 0, 0, 1}},
Expected = <<"{\"ip\":\"127.0.0.1\"}">>,
?assertEqual(Expected, jsone:encode(Input, [{map_unknown_value, fun jsone:ip_address_to_json_string/1}])),

%% Without `map_unknown_value' option.
?assertMatch({error, _}, jsone:try_encode(Input, [{map_unknown_value, undefined}]))
end},

%% Others
{"compound data",
Expand Down
41 changes: 41 additions & 0 deletions test/jsone_inet_tests.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
%% Copyright (c) 2013-2021, Takeru Ohta <phjgt308@gmail.com>
-module(jsone_inet_tests).

-include_lib("eunit/include/eunit.hrl").

format_ipv4_test() ->
Expected = <<"127.0.0.1">>,
{ok, Addr} = inet:parse_ipv4_address(binary_to_list(Expected)),
{ok, Actual} = jsone_inet:ip_address_to_json_string(Addr),
?assertEqual(Actual, Expected).

format_ipv6_test() ->
Addresses =
[
<<"::127.0.0.1">>,
<<"::ffff:192.0.2.1">>,
<<"::ffff:0:255.255.255.255">>,
<<"64:ff9b::0.0.0.0">>,
<<"64:ff9b:1::192.168.1.1">>,
<<"64:ff9b:1::1:192.168.1.1">>,
<<"::1:2:3:2001:db8">>,
<<"2001:db8::">>,
<<"2001:db8::1">>,
<<"2001:db8::1:0:0:1">>,
<<"2001:db8:0:1:1:1:1:1">>,
<<"2001:0:0:1::1">>,
<<"2001:db8:85a3::8a2e:370:7334">>
],
lists:foreach(
fun (Expected) ->
{ok, Addr} = inet:parse_ipv6_address(binary_to_list(Expected)),
{ok, Bin} = jsone_inet:ip_address_to_json_string(Addr),
?assertEqual(Expected, Bin)
end,
Addresses).

invalid_ip_addr_test() ->
?assertEqual(jsone_inet:ip_address_to_json_string(foo), error),
?assertEqual(jsone_inet:ip_address_to_json_string({1, 2, 3}), error),
?assertEqual(jsone_inet:ip_address_to_json_string({0, 10000, 0, 0}), error),
?assertEqual(jsone_inet:ip_address_to_json_string({-1, 0, 0, 0, 0, 0, 0, 0}), error).

0 comments on commit ed90938

Please sign in to comment.