diff --git a/src/maxminddb.erl b/src/maxminddb.erl index 1c79cfc..35e0899 100644 --- a/src/maxminddb.erl +++ b/src/maxminddb.erl @@ -1,53 +1,82 @@ -module(maxminddb). +%% @doc Library to search MaxMind format binary databases +%% @reference See the data spec + -export([open/0, open/1, load/1, find/2, find/3]). --record(db, {root, - meta, - ip_version, - node_count, - record_size, - record_half, - record_left, - tree_size, - data_offs}). +-record(db, {root :: binary(), % file data + meta, % file level metadata, proplist + ip_version :: integer(), % 4 or 6 + node_count :: integer(), % number of index nodes + record_size :: integer(), % record size in bits + node_size :: integer(), % node size in bytes (2 x record size) + tree_size :: integer(), % tree size in bytes + data_offs :: integer() % offset of data section in file + }). +% Pattern which indicates start of metadata -define(MetaMagic, <<16#abcdef:24, "MaxMind.com">>). --define(MetaMax, 20000). +% 128kb is the max size of the metadata section +-define(MetaMax, 131072). +%% @doc Open and load default file (geoip) from priv dir +-spec open() -> #db{}. open() -> open(ego:priv(geoip)). +%% @doc Open and load specific file by path +-spec open(Path) -> #db{} when + Path :: list(). open(Path) -> load(ego:read(Path)). +%% @doc Parse data file +-spec load(Root) -> #db{} when + Root :: binary(). load(Root) -> Meta = meta(Root), IPVersion = proplists:get_value(<<"ip_version">>, Meta), NodeCount = proplists:get_value(<<"node_count">>, Meta), RecordSize = proplists:get_value(<<"record_size">>, Meta), - RecordHalf = RecordSize rem 8, - TreeSize = NodeCount * RecordSize div 4, + + % NodeSize in bytes = (RecordSize in bits * 2) / 8 bits per byte + NodeSize = RecordSize div 4, + + % TreeSize in bytes = NodeCount * NodeSize + TreeSize = NodeCount * NodeSize, + DataOffset = TreeSize + 16, + #db{root=Root, meta=Meta, ip_version=IPVersion, node_count=NodeCount, record_size=RecordSize, - record_half=RecordHalf, - record_left=RecordSize - RecordHalf, + node_size=NodeSize, tree_size=TreeSize, - data_offs=TreeSize + 16}. + data_offs=DataOffset}. +%% @doc Decode metadata +-spec meta(Root) -> list(Metadata) when + Root :: binary(), + Metadata :: {binary(), any()}. meta(Root) -> - {Pos, Len} = binary:match(Root, ?MetaMagic, [{scope, {size(Root), -?MetaMax}}]), + Matches = binary:matches(Root, ?MetaMagic, [{scope, {size(Root), -?MetaMax}}]), + {Pos, Len} = lists:last(Matches), value(#db{root=Root}, Pos + Len). +%% @doc Find data for IP +-spec find(G, Addr) -> Value when + G :: #db{}, + Addr :: inet:ip_address() | binary() | list(), + Value :: any(). % TODO: tighten this up find(#db{ip_version=6} = G, {A, B, C, D}) -> find(G, {0, 0, 0, 0, 0, 16#ffff, (A bsl 8) bor B, (C bsl 8) bor D}); +% TODO: support 6to4 addresses from 2002::/16 find(G, Addr) when is_binary(Addr) -> find(G, binary_to_list(Addr)); find(G, Addr) when is_list(Addr) -> @@ -56,6 +85,11 @@ find(G, Addr) when is_list(Addr) -> find(G, IP) -> find(G, path(IP), node(G, 0)). +-spec find(G, PathList, NodeValue) -> Result when + G :: #db{}, + PathList :: list(integer()), + NodeValue :: {stop, undefined} | {stop, any()} | {integer(), integer()}, % TODO: tighten up any() + Result :: {ok, any()} | {error, any()}. % TODO: tighten up any() find(_, _, {stop, Value}) -> {ok, Value}; find(_, [], Node) -> @@ -65,45 +99,87 @@ find(G, [0|Path], {L, _}) -> find(G, [1|Path], {_, R}) -> find(G, Path, node(G, R)). +%% @doc Handle node value +-spec node(G, N) -> Value when + G :: #db{}, + N :: integer(), + Value :: {stop, undefined} | {stop, any()} | {integer(), integer()}. % TODO: tighten up any() +% No data for this IP node(#db{node_count=N}, N) -> {stop, undefined}; +% Pointer into data section node(#db{node_count=N, tree_size=T} = G, K) when K > N -> {stop, value(G, K - N + T)}; -node(#db{root=Root, record_size=S, record_half=H, record_left=L}, K) -> - <> = binary:part(Root, K * S div 4, S div 4), - {Low + (High bsl L), Right}. +% Node +node(#db{root=Root, record_size=S, node_size=N}, K) -> + Offs = K * N, + {Left, Right} = case S of + 24 -> + <> = binary:part(Root, Offs, N), + {L, R}; + 28 -> + % The layout of 28 bit databases is weird + <> = binary:part(Root, Offs, N), + {(LH bsl 4) + LL, (RH bsl 4) + RL}; + 32 -> + <> = binary:part(Root, Offs, N), + {L, R} + end, + {Left, Right}. +%% @doc Decode data field +-spec decode(G, R) -> {Value, R} when + G :: #db{}, + R :: binary(), + Value :: number() | binary() | list({binary(), any()}) | list(any()). +% Type 1, pointer decode(G, <<1:3, S:2, R/bitstring>>) -> decode(G, {pointer, S}, R); +% Type 2, UTF-8 string TODO: use different 'string' type? decode(G, <<2:3, S:5, R/binary>>) -> decode(G, {binary, S}, R); +% Type 3, IEEE-754 double decode(_, <<3:3, 8:5, D:64/float, R/binary>>) -> {D, R}; +% Type 4, bytes decode(G, <<4:3, S:5, R/binary>>) -> decode(G, {binary, S}, R); +% Type 5, unsigned 16-bit integer decode(G, <<5:3, S:5, R/binary>>) -> decode(G, {uint, S}, R); +% Type 6, unsigned 32-bit integer decode(G, <<6:3, S:5, R/binary>>) -> decode(G, {uint, S}, R); +% Type 7, map decode(G, <<7:3, S:5, R/binary>>) -> decode(G, {map, S}, R); +% Extended types +% Type 8, signed 32-bit integer decode(G, <<0:3, S:5, 1, R/binary>>) -> decode(G, {int, S}, R); +% Type 9, unsigned 64-bit integer decode(G, <<0:3, S:5, 2, R/binary>>) -> decode(G, {uint, S}, R); +% Type 10, unsigned 128-bit integer decode(G, <<0:3, S:5, 3, R/binary>>) -> decode(G, {uint, S}, R); +% Type 11, array decode(G, <<0:3, S:5, 4, R/binary>>) -> decode(G, {array, S}, R); +% Type 12, data cache container decode(_, <<0:3, _:5, 5, R/binary>>) -> {cache, R}; +% Type 13, end marker decode(_, <<0:3, 0:5, 6, R/binary>>) -> {eod, R}; +% Type 14, boolean true decode(_, <<0:3, 1:5, 7, R/binary>>) -> {true, R}; +% Type 14, boolean false decode(_, <<0:3, 0:5, 7, R/binary>>) -> {false, R}; +% Type 15, IEEE-754 32-bit float decode(_, <<0:3, 4:5, 8, F:32/float, R/binary>>) -> {F, R}. @@ -165,14 +241,28 @@ decode(G, {array, N, Acc}, Data) -> {Val, Rest} = decode(G, Data), decode(G, {array, N - 1, [Val|Acc]}, Rest). +%% @doc Get value at offset into file +-spec value(G, Offs) -> Value when + G :: #db{}, + Offs :: integer(), + Value :: number() | binary() | list({binary(), any()}) | list(any()). value(#db{root=Root} = G, Offs) -> <<_:Offs/binary, Rest/binary>> = Root, {Val, _} = decode(G, Rest), Val. +%% @doc Get integer as a list of bits +-spec bits(I, Size) -> Value when + I :: integer(), + Size :: integer(), + Value :: list(integer()). bits(I, Size) -> [B || <> <= <>]. +%% @doc Convert inet:inet:ip_address() to list of ints +-spec path(Addr) -> Value when + Addr :: inet:ip_address(), + Value :: list(integer()). path({A, B, C, D}) -> bits((A bsl 24) bor (B bsl 16) bor (C bsl 8) bor D, 32); path({A, B, C, D, E, F, G, H}) ->