Current section

Files

Jump to
locus src locus_util.erl
Raw

src/locus_util.erl

%% Copyright (c) 2018 Guilherme Andrade
%%
%% 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.
%%
%% locus is an independent project and has not been authorized, sponsored,
%% or otherwise approved by MaxMind.
%% @private
-module(locus_util).
-include_lib("kernel/include/file.hrl").
%% ------------------------------------------------------------------
%% API Function Exports
%% ------------------------------------------------------------------
-export(
[read_file_and_its_modification_date/1,
maybe_read_file_and_its_modification_date/2,
load_database_from_tarball/3,
parse_ip_address/1
]).
%% ------------------------------------------------------------------
%% Macro Definitions
%% ------------------------------------------------------------------
-define(is_ip_address(V), (?is_ip4_address((V)) orelse ?is_ip6_address((V)))).
-define(is_ip4_address(V), (is_tuple((V)) andalso tuple_size((V)) =:= 4 andalso
?is_uint8(element(1, ((V)))) andalso ?is_uint8(element(2, (V))) andalso
?is_uint8(element(3, ((V)))) andalso ?is_uint8(element(4, (V))))).
-define(is_ip6_address(V), (is_tuple((V)) andalso tuple_size((V)) =:= 8 andalso
?is_uint16(element(1, ((V)))) andalso ?is_uint16(element(2, (V))) andalso
?is_uint16(element(3, ((V)))) andalso ?is_uint16(element(4, (V))) andalso
?is_uint16(element(5, ((V)))) andalso ?is_uint16(element(7, (V))) andalso
?is_uint16(element(6, ((V)))) andalso ?is_uint16(element(8, (V))))).
-define(is_uint8(V), (is_integer((V)) andalso ((V) band 16#FF =:= (V)))).
-define(is_uint16(V), (is_integer((V)) andalso ((V) band 16#FFFF =:= (V)))).
%% ------------------------------------------------------------------
%% API Function Definitions
%% ------------------------------------------------------------------
-spec read_file_and_its_modification_date(nonempty_string())
-> {ok, binary(), calendar:datetime()} |
{error, {read_file | read_file_info, term()}}.
read_file_and_its_modification_date(Filename) ->
maybe_read_file_and_its_modification_date(Filename, undefined).
-spec maybe_read_file_and_its_modification_date(nonempty_string(),
calendar:datetime() | undefined)
-> {ok, unchanged} |
{ok, binary(), calendar:datetime()} |
{error, {read_file | read_file_info, term()}}.
maybe_read_file_and_its_modification_date(Filename, PrevModificationDate) ->
case file:read_file_info(Filename, [{time,universal}]) of
{ok, #file_info{ mtime = ModificationDate }}
when ModificationDate =:= PrevModificationDate ->
{ok, unchanged};
{ok, #file_info{ mtime = ModificationDate }} ->
case file:read_file(Filename) of
{ok, Content} ->
{ok, Content, ModificationDate};
{error, Error} ->
{error, {read_file, Error}}
end;
{error, Error} ->
{error, {read_file_info, Error}}
end.
-spec load_database_from_tarball(atom(), binary(), locus_mmdb:source())
-> {ok, calendar:datetime()} |
{error, {exception, atom(), term()}}.
load_database_from_tarball(Id, Tarball, Source) ->
try
BinDatabase = extract_database_from_tarball(Tarball),
Version = locus_mmdb:decode_and_update(Id, BinDatabase, Source),
{ok, Version}
catch
Class:Reason ->
{error, {exception, Class, Reason}}
end.
-spec parse_ip_address(binary() | string() | inet:ip_address())
-> {ok, inet:ip_address()} | {error, einval}.
parse_ip_address(Address) when ?is_ip_address(Address) ->
{ok, Address};
parse_ip_address(Binary) when is_binary(Binary) ->
String = binary_to_list(Binary),
parse_ip_address(String);
parse_ip_address(String) when length(String) >= 0 ->
case string:tokens(String, "/") of
[StrAddress] ->
inet:parse_address(StrAddress);
[StrAddress, _PrefixLenStr] ->
inet:parse_address(StrAddress);
_ ->
{error, einval}
end;
parse_ip_address(_Invalid) ->
{error, einval}.
%% ------------------------------------------------------------------
%% Internal Function Definitions
%% ------------------------------------------------------------------
-spec extract_database_from_tarball(binary()) -> binary().
extract_database_from_tarball(Tarball) ->
{ok, ContainedPaths} = erl_tar:table({binary, Tarball}, [compressed]),
{true, DatabasePath} = lists_anymap(fun has_mmdb_extension/1, ContainedPaths),
{ok, [{DatabasePath, BinDatabase}]} =
erl_tar:extract({binary, Tarball}, [{files, [DatabasePath]}, memory, compressed]),
BinDatabase.
lists_anymap(Fun, [H|T]) ->
case Fun(H) of
{true, Mapped} -> {true, Mapped};
true -> {true, H};
false -> lists_anymap(Fun, T)
end;
lists_anymap(_Fun, []) ->
false.
%-spec has_mmdb_extension(nonempty_string()) -> boolean().
has_mmdb_extension({Filename, _Type, _Size, _MTime, _Mode, _Uid, _Gid}) ->
% FIXME: this a placeholder for OTP 20; due to the incomplete spec
% of erl_tar:table/2, Dialyzer comes to believe that no strings
% can be returned, only the above tuple, which in fact is only returned
% if the 'verbose' option is picked, something that we are definitely
% not doing.
filename:extension(Filename) =:= ".mmdb" andalso {true, Filename};
has_mmdb_extension(Filename) ->
filename:extension(Filename) =:= ".mmdb".