%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-
% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod:
%%%
%%%------------------------------------------------------------------------
%%% @doc
%%% ==CloudI IP Address Parsing==
%%% @end
%%%
%%% MIT License
%%%
%%% Copyright (c) 2013-2021 Michael Truog <mjtruog at protonmail dot com>
%%%
%%% 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.
%%%
%%% @author Michael Truog <mjtruog at protonmail dot com>
%%% @copyright 2013-2021 Michael Truog
%%% @version 2.0.3 {@date} {@time}
%%%------------------------------------------------------------------------

-module(cloudi_ip_address).
-author('mjtruog at protonmail dot com').

%% external interface
-export([from_binary/1,
         from_string/1,
         patterns/1,
         to_binary/1,
         to_string/1]).

-type format_binary() ::
    <<_:8, _:_*8>>.
% IPv4 dotted decimal address (no octal or hex)
% IPv6 lowercase hex with colons
-type format_string() ::
    nonempty_list($0..$9 | $. | $a..$f | $: | $%).
-type cidr_string() ::
    nonempty_list($0..$9 | $. | $a..$f | $: | $/).
-export_type([format_binary/0,
              format_string/0,
              cidr_string/0]).

%%%------------------------------------------------------------------------
%%% External interface functions
%%%------------------------------------------------------------------------

%%-------------------------------------------------------------------------
%% @doc
%% ===Parse a binary string representation.===
%% @end
%%-------------------------------------------------------------------------

-spec from_binary(format_binary()) ->
    inet:ip_address().

from_binary(BinaryIP) ->
    from_string(erlang:binary_to_list(BinaryIP)).

%%-------------------------------------------------------------------------
%% @doc
%% ===Parse a binary string representation.===
%% @end
%%-------------------------------------------------------------------------

-spec from_string(format_string()) ->
    inet:ip_address().

from_string(StringIP) ->
    {ok, IP} = inet:parse_strict_address(StringIP),
    IP.

%%-------------------------------------------------------------------------
%% @doc
%% ===Create service name pattern strings that represent a CIDR (Classless Inter-Domain Routing) notation string.===
%% Usage of the resulting patterns requires at least 1 suffix letter
%% in the service name pattern string created with each pattern
%% (i.e., each pattern wouldn't be used as a suffix).
%% @end
%%-------------------------------------------------------------------------

-spec patterns(cidr_string()) ->
    nonempty_list(cloudi:service_name_pattern()).

patterns([_ | _] = CIDR) ->
    {StringIP, StringBits} = cloudi_string:splitr($/, CIDR),
    patterns_expand(from_string(StringIP), erlang:list_to_integer(StringBits)).

%%-------------------------------------------------------------------------
%% @doc
%% ===Create a binary string representation.===
%% IPv6 doesn't shorten a group of zeroes so more exact pattern matches
%% are possible in service names.
%% @end
%%-------------------------------------------------------------------------

-spec to_binary(inet:ip_address()) ->
    format_binary().

to_binary(IP) ->
    erlang:list_to_binary(to_string(IP)).

%%-------------------------------------------------------------------------
%% @doc
%% ===Create a list string representation.===
%% IPv6 doesn't shorten a group of zeroes so more exact pattern matches
%% are possible in service names.
%% @end
%%-------------------------------------------------------------------------

-spec to_string(inet:ip_address()) ->
    format_string().

to_string({I0, I1, I2, I3})
    when ((I0 bor I1 bor I2 bor I3) band (bnot 16#ff)) =:= 0 ->
    Delimiter = $.,
    S0 = int_to_dec_list([], I3),
    S1 = int_to_dec_list([Delimiter | S0], I2),
    SN = int_to_dec_list([Delimiter | S1], I1),
    int_to_dec_list([Delimiter | SN], I0);
to_string({I0, I1, I2, I3, I4, I5, I6, I7})
    when ((I0 bor I1 bor I2 bor I3 bor
           I4 bor I5 bor I6 bor I7) band (bnot 16#ffff)) =:= 0 ->
    Delimiter = $:,
    S0 = int_to_hex_list([], I7),
    S1 = int_to_hex_list([Delimiter | S0], I6),
    S2 = int_to_hex_list([Delimiter | S1], I5),
    S3 = int_to_hex_list([Delimiter | S2], I4),
    S4 = int_to_hex_list([Delimiter | S3], I3),
    S5 = int_to_hex_list([Delimiter | S4], I2),
    SN = int_to_hex_list([Delimiter | S5], I1),
    int_to_hex_list([Delimiter | SN], I0).

%%%------------------------------------------------------------------------
%%% Private functions
%%%------------------------------------------------------------------------

patterns_expand({_, _, _, _} = IPv4, Bits)
    when Bits >= 0 andalso Bits =< 32 ->
    patterns_expand_segment(erlang:tuple_to_list(IPv4), [], 0, Bits, 8, ipv4);
patterns_expand({_, _, _, _, _, _, _, _} = IPv6, Bits)
    when Bits >= 0 andalso Bits =< 128 ->
    patterns_expand_segment(erlang:tuple_to_list(IPv6), [], 0, Bits, 16, ipv6).

patterns_expand_segment(SegmentL, Prefix, PrefixSize,
                        0, _, Version) ->
    true = lists:all(fun(Zero) -> Zero =:= 0 end, SegmentL),
    [patterns_expand_string(Prefix, PrefixSize, Version)];
patterns_expand_segment([Segment | SegmentL], Prefix, PrefixSize,
                        Bits, BitsMax, Version)
    when Bits >= BitsMax ->
    patterns_expand_segment(SegmentL, [Segment | Prefix], PrefixSize + 1,
                            Bits - BitsMax, BitsMax, Version);
patterns_expand_segment([Segment | SegmentL], Prefix, PrefixSize,
                        Bits, BitsMax, Version) ->
    true = lists:all(fun(Zero) -> Zero =:= 0 end, SegmentL),
    SegmentMask = (1 bsl (BitsMax - Bits)) - 1,
    0 = Segment band SegmentMask,
    [patterns_expand_string([SegmentValue | Prefix], PrefixSize + 1, Version)
     || SegmentValue <- lists:seq(Segment, Segment + SegmentMask)].

patterns_expand_string(Prefix, PrefixSize, ipv4) ->
    Delimiter = $.,
    S = patterns_expand_string_wildcard(4 - PrefixSize, [], Delimiter),
    patterns_expand_string_exact_dec(Prefix, S, Delimiter);
patterns_expand_string(Prefix, PrefixSize, ipv6) ->
    Delimiter = $:,
    S = patterns_expand_string_wildcard(8 - PrefixSize, [], Delimiter),
    patterns_expand_string_exact_hex(Prefix, S, Delimiter).

patterns_expand_string_exact_dec([], S, _) ->
    S;
patterns_expand_string_exact_dec([Segment | Prefix], S, Delimiter) ->
    patterns_expand_string_exact_dec(Prefix,
                                     int_to_dec_list(S, Segment, Delimiter),
                                     Delimiter).

patterns_expand_string_exact_hex([], S, _) ->
    S;
patterns_expand_string_exact_hex([Segment | Prefix], S, Delimiter) ->
    patterns_expand_string_exact_hex(Prefix,
                                     int_to_hex_list(S, Segment, Delimiter),
                                     Delimiter).

patterns_expand_string_wildcard(0, S, _) ->
    S;
patterns_expand_string_wildcard(1, S, _) ->
    [$? | S];
patterns_expand_string_wildcard(Count, S, Delimiter) ->
    patterns_expand_string_wildcard(Count - 1, [Delimiter, $? | S], Delimiter).

int_to_dec_list([] = L, I, _) ->
    int_to_dec_list(L, I);
int_to_dec_list(L, I, Delimiter) ->
    int_to_dec_list([Delimiter | L], I).

int_to_dec_list(L, I)
    when I < 10 ->
    [int_to_dec(I) | L];
int_to_dec_list(L, I) ->
    int_to_dec_list([int_to_dec(I rem 10) | L], I div 10).

int_to_hex_list([] = L, I, _) ->
    int_to_hex_list(L, I);
int_to_hex_list(L, I, Delimiter) ->
    int_to_hex_list([Delimiter | L], I).

int_to_hex_list(L, I)
    when I < 16 ->
    [int_to_hex(I) | L];
int_to_hex_list(L, I) ->
    int_to_hex_list([int_to_hex(I rem 16) | L], I div 16).

-compile({inline,
          [{int_to_dec,1},
           {int_to_hex,1}]}).

int_to_dec(I) when 0 =< I, I =< 9 ->
    I + $0.

int_to_hex(I) when 0 =< I, I =< 9 ->
    I + $0;
int_to_hex(I) when 10 =< I, I =< 15 ->
    (I - 10) + $a.

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

-include("cloudi_core_i_test.hrl").

module_test_() ->
    {timeout, ?TEST_TIMEOUT, [
        {"from_binary tests", ?_assertOk(t_from_binary())},
        {"from_string tests", ?_assertOk(t_from_string())},
        {"patterns tests", ?_assertOk(t_patterns())},
        {"to_binary tests", ?_assertOk(t_to_binary())},
        {"to_string tests", ?_assertOk(t_to_string())}
    ]}.

t_from_binary() ->
    {0,0,0,0,0,0,0,0} = from_binary(<<"::">>), % any
    {0,0,0,0,0,0,0,1} = from_binary(<<"::1">>), % localhost
    {8194,0,0,4660,43981,
     65535,49320,257} = from_binary(<<"2002::1234:abcd:ffff:c0a8:101">>),
    {8194,0,0,4660,43981,
     65535,49320,257} = from_binary(<<"2002:0:0:1234:abcd:ffff:c0a8:101">>),
    ok.

t_from_string() ->
    {0,0,0,0,0,0,0,0} = from_string("::"), % any
    {0,0,0,0,0,0,0,1} = from_string("::1"), % localhost
    {8194,0,0,4660,43981,
     65535,49320,257} = from_string("2002::1234:abcd:ffff:c0a8:101"),
    {8194,0,0,4660,43981,
     65535,49320,257} = from_string("2002:0:0:1234:abcd:ffff:c0a8:101"),
    ok.

t_patterns() ->
    ["?.?.?.?"] = patterns("0.0.0.0/0"),
    {'EXIT', {{badmatch,false}, _}} = (catch patterns("10.0.0.0/0")),
    ["10.?.?.?"] = patterns("10.0.0.0/8"),
    {'EXIT', {{badmatch,false}, _}} = (catch patterns("10.1.0.0/8")),
    ["192.168.?.?"] = patterns("192.168.0.0/16"),
    ["fd00:?:?:?:?:?:?:?"] = patterns("fd00::/16"),
    ["172.16.?.?", "172.17.?.?", "172.18.?.?", "172.19.?.?", "172.20.?.?",
     "172.21.?.?", "172.22.?.?", "172.23.?.?", "172.24.?.?", "172.25.?.?",
     "172.26.?.?", "172.27.?.?", "172.28.?.?", "172.29.?.?", "172.30.?.?",
     "172.31.?.?"] = patterns("172.16.0.0/12"),
    ["192.168.0.?", "192.168.1.?"] = patterns("192.168.0.0/23"),
    ["192.168.2.?", "192.168.3.?"] = patterns("192.168.2.0/23"),
    ["192.168.0.1"] = patterns("192.168.0.1/32"),
    ok.

t_to_binary() ->
    <<"0:0:0:0:0:0:0:0">> = to_binary({0,0,0,0,0,0,0,0}),
    <<"0:0:0:0:0:0:0:1">> = to_binary({0,0,0,0,0,0,0,1}),
    <<"2002:0:0:1234:abcd:ffff:c0a8:101">> = to_binary({8194,0,0,4660,43981,
                                                        65535,49320,257}),
    ok.

t_to_string() ->
    "0:0:0:0:0:0:0:0" = to_string({0,0,0,0,0,0,0,0}),
    "0:0:0:0:0:0:0:1" = to_string({0,0,0,0,0,0,0,1}),
    "2002:0:0:1234:abcd:ffff:c0a8:101" = to_string({8194,0,0,4660,43981,
                                                    65535,49320,257}),
    ok.

-endif.
